Marantz AV Receiver serial component

Hi all,

Ive been working on a Marantz component for older receivers that only support RS232 (serial). Most of the other components seem to either support Telnet or HTTP control. My receiver is an SR6004 from about 7 years ago. Its based on the code from the NAD receiver github project so thank you to that author.

It seems to be working well for me but if anyone else would find this useful and like to test please let me know. Its based on local polling rather than two-way but I think thats the simplest and most reliable approach right now.

Cheers

1 Like

Woow that’s great! I’ve got a SR5004 so I thought about the same but never did anything with the ideas.

Could you please share you’re code and uses materials?

Thanks in advance.

Hey Imperial-Guard

As its early days, its a little bit manual to install but here goes.

  1. Make a ‘marantz-receiver’ directory in your dependencies location. For me this is ‘/ha-config/deps/lib/python3.6/site-packages/’
  2. Put the contents of this github directory in the above location
  3. If you dont have one already create a ‘custom_components/media_player/’ directory in your ha-config folder. For me this is ‘/ha-config/custom_components/media_player’
  4. Put this file in that location

Then configure your specific serial/receiver details in configuration.yaml. Ive added it to my existing media player entries. Here is mine:

media_player:
  - platform: cast
  - platform: samsungtv
    host: 192.168.1.225
  - platform: marantz
    serial_port: /dev/ttyS0
    name: Marantz Receiver
    min_volume: -71
    max_volume: -1
    sources:
      1: 'TV'
      2: 'Chromecast'
      3: 'VCR'
      4: 'VCR2'
      5: 'DSS'
      #6: 'LD'
      7: 'USB'
      #8: 'Net/USB'
      9: 'AUX'
      #A: 'AUX2'
      #B: 'CD'
      C: 'CD/CD-R'
      #D: 'CD/CD-R'
      #E: 'Tape'
      F: 'Tuner'
      G: 'FM'
      H: 'AM'
      #I:
      #J: 'XM'
      #K: 'Sirius'
      #L: 'Phono'
      M: 'Foxtel'
      N: 'M-Xport - Bluetooth'

Your receiver might have slightly different mappings, which you can find in the xls here

Let me know how it goes for you. Depends on the user that you run HA as, you may need to give it access to the serial port. I forget the command, but it can be found on the forum.

If it works for you you should be able to add the marantz media player entity to your dashboard which will look like this:


Good luck

@andrewpc,

Took some time to reply. But finally working on it :slight_smile:
Instead of connecting it directly I would like to connect it over MQTT to for e.g. a Raspberry Pi Zero as MQTT gateway to serial.

First steps of the scripts its slowly getting there.
I’m not a pro developer in Python but with some help I hope to get there :slight_smile:

@andrewpc,

Any thoughts on this error?

I’m running on HASSIO

Error while setting up platform marantz
Traceback (most recent call last):
  File "/usr/lib/python3.6/site-packages/homeassistant/helpers/entity_platform.py", line 129, in _async_setup_platform
    SLOW_SETUP_MAX_WAIT, loop=hass.loop)
  File "/usr/lib/python3.6/asyncio/tasks.py", line 358, in wait_for
    return fut.result()
  File "/usr/lib/python3.6/concurrent/futures/thread.py", line 56, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/config/custom_components/media_player/marantz.py", line 49, in setup_platform
    from marantz_receiver import MarantzReceiver
ModuleNotFoundError: No module named 'marantz_receiver'

Solver foldername should be: marantz_receiver instead of marantz-receiver.
It’s working !

Indeed you are correct. Will update. Glad you got it working!

Till so far most of the time it’s working great, noticed some issues that somehow the connection was lost.

Restart of Hass help me out, did you noticed this also?

Short update for the error:

8:08 AM helpers/entity_platform.py (WARNING)
Updating marantz media_player took longer than the scheduled update interval 0:00:10

018-07-08 19:07:25 ERROR (MainThread) [homeassistant.helpers.entity] Update for media_player.marantz_receiver fails
Traceback (most recent call last):
  File "/usr/lib/python3.6/site-packages/homeassistant/helpers/entity.py", line 196, in async_update_ha_state
    yield from self.async_device_update()
  File "/usr/lib/python3.6/site-packages/homeassistant/helpers/entity.py", line 319, in async_device_update
    yield from self.hass.async_add_job(self.update)
  File "/usr/lib/python3.6/concurrent/futures/thread.py", line 56, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/config/custom_components/media_player/marantz.py", line 105, in update
    if self._marantz_receiver.main_power(':', '?') == '1':
  File "/config/deps/lib/python3.6/site-packages/marantz_receiver/__init__.py", line 83, in main_power
    return self.exec_command('main', 'power', operator, value)
  File "/config/deps/lib/python3.6/site-packages/marantz_receiver/__init__.py", line 58, in exec_command
    self.ser.write(final_command)
  File "/usr/lib/python3.6/site-packages/serial/serialposix.py", line 515, in write
    raise writeTimeoutError
serial.serialutil.SerialTimeoutException: Write timeout
2018-07-08 19:07:45 WARNING (MainThread) [homeassistant.helpers.entity] Update of media_player.marantz_receiver is taking over 10 seconds

Small update:

Some improvements to your code, with the original code the following things could happen:

1: self.lock.acquire()
1: self.ser.write(final_command)
2: self.reset_input_buffer()
2: self.reset_output_buffer()
2: self.lock.acquire() # deadlock until thread 1 self.lock.release called.
1: self.ser.read_until() # deadlock due too buffer is just emptied by 2, nothing to read.

Changed Code:

"""
Marantz has an RS232 interface to control the receiver.

Not all receivers have all functions.
Functions can be found on in the xls file within this repository
"""

import codecs
import socket
from time import sleep
from marantz_receiver.marantz_commands import CMDS
import serial  # pylint: disable=import-error
import threading
import telnetlib
import logging
import time

DEFAULT_TIMEOUT = 0.5
DEFAULT_WRITE_TIMEOUT = 0.5

_LOGGER = logging.getLogger(__name__)

class MarantzReceiver(object):
    """Marantz receiver."""

    def __init__(self, serial_port, timeout=DEFAULT_TIMEOUT,
                 write_timeout=DEFAULT_WRITE_TIMEOUT):
        """Create RS232 connection."""
        self.ser = serial.Serial(serial_port, baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=timeout,
                                 write_timeout=write_timeout)
        self.lock = threading.Lock()

    def exec_command(self, domain, function, operator, value=None):
        """
        Write a command to the receiver and read the value it returns.
        The receiver will always return a value, also when setting a value.
        """
        raw_command = CMDS[domain][function]['cmd']
        if operator in CMDS[domain][function]['supported_operators']:
            if value is None:
                raise ValueError('No value provided')
            else:
                cmd = ''.join([raw_command, operator, str(value)])

        else:
            raise ValueError('Invalid operator provided %s' % operator)
        with self.lock:
            if not self.ser.is_open:
                self.ser.open()

            self.ser.reset_input_buffer()
            self.ser.reset_output_buffer()
#            self.lock.acquire()
    
            # Marantz uses the prefix @ and the suffix \r, so add those to the above cmd.
            final_command = ''.join(['@', cmd, '\r']).encode('utf-8')
            _LOGGER.debug ('Send Command %s',final_command)
    
            self.ser.write(final_command)
    
            msg = self.ser.read_until(bytes('\r'.encode()))
#            self.lock.release()

        _LOGGER.debug ('Response msg %s', msg.decode())

        split_string = msg.decode().strip().split(':')

        _LOGGER.debug("Decoded split string %s", split_string)
        _LOGGER.debug ("Original command: %s", raw_command)
        # Check return value contains the same command value as requested. Sometimes the marantz gets out of sync. Ignore if this is the case
        if split_string[0] != ('@' + raw_command):
            _LOGGER.debug ("Send & Response command values dont match %s != %s - Ignoring returned value", split_string[0], '@' + raw_command )
            return None
        else:
             return split_string[1]
             # b'AMT:0\r will return 0

    def main_mute(self, operator, value=None):
        """Execute Main.Mute."""
        return self.exec_command('main', 'mute', operator, value)

    def main_power(self, operator, value=None):
        """Execute Main.Power."""
        return self.exec_command('main', 'power', operator, value)

    def main_volume(self, operator, value=None):
        """
        Execute Main.Volume.
        Returns int
        """
        vol_result = self.exec_command('main', 'volume', operator, value)
        if vol_result != None:
            return int(vol_result)

    def main_source(self, operator, value=None):
        """Execute Main.Source."""
        result = self.exec_command('main', 'source', operator, value)
        """
        The receiver often returns the source value twice. If so take the
        second value as the source, otherwise return original
        """
        if result != None and len(result) == 2:
            _LOGGER.debug("Source Result: %s", result[1])
            return result[1]
        else:
            return result

    def main_autostatus (self, operator, value=None):
        """
        Execute autostatus.
        Not currently used but will allow two-way communications in future

        Returns int
        """
        return int(self.exec_command('main', 'autostatus', operator, value))

Hello

I am trying to get remote control working via WiFi(Client)-serial adapter (Hi-Link HLK-WR02) my old Marantz SR6003 AVR. The Hi-Link adapter can forward TCP-packets to serial port of Marantz and it is working OK. I can manually control Marantz via this adapter.
I am running HA version 0.81.4 inside of Docker on PineA64. Unfortunately I cannot manage to get this part working. I am not sure where I put the marantz_receiver directory and it’s content?
What else I must do in order to get HA working?

Hello

I got help from retired and very excellent software designer to solve this problem. Now I can control wirelessly Marantz SR6003 via H-Link adapter.
Only three files must be changed a little.
init.py:
“”"
Marantz has an RS232 interface to control the receiver.

Not all receivers have all functions.
Functions can be found on in the xls file within this repository
"""

import codecs
import socket
from time import sleep
from marantz_receiver.marantz_commands import CMDS
import serial  # pylint: disable=import-error
import threading
import telnetlib
import logging
import time

DEFAULT_TIMEOUT = 0.5
DEFAULT_WRITE_TIMEOUT = 0.5

_LOGGER = logging.getLogger(__name__)

class MarantzReceiver(object):
    """Marantz receiver."""

    def __init__(self, host, port, timeout=DEFAULT_TIMEOUT, write_timeout=DEFAULT_WRITE_TIMEOUT):
         """Create TCP/IP connection."""
         self.socket = socket.create_connection((host, port))
         # socket.py line 691
         # def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=None):
         self.lock = threading.Lock()
        
    def exec_command(self, domain, function, operator, value=None):
        """
        Write a command to the receiver and read the value it returns.
        The receiver will always return a value, also when setting a value.
        """
        raw_command = CMDS[domain][function]['cmd']
        if operator in CMDS[domain][function]['supported_operators']:
            if value is None:
                raise ValueError('No value provided')
            else:
                cmd = ''.join([raw_command, operator, str(value)])

        else:
            raise ValueError('Invalid operator provided %s' % operator)
        with self.lock:
#            if not self.ser.is_open:
#                self.ser.open()
#            self.ser.reset_input_buffer()
#            self.ser.reset_output_buffer()
    
            # Marantz uses the prefix @ and the suffix \r, so add those to the above cmd.
            final_command = ''.join(['@', cmd, '\r']).encode('utf-8')
            _LOGGER.debug('Send Command %s', final_command)
            self.socket.send(final_command)
    
            msg = self.socket.recv(256)

        _LOGGER.debug('Response msg %s', msg.decode())

        split_string = msg.decode().strip().split(':')

        _LOGGER.debug("Decoded split string %s", split_string)
        _LOGGER.debug("Original command: %s", raw_command)
        # Check return value contains the same command value as requested. Sometimes the marantz gets out of sync. Ignore if this is the case
        if split_string[0] != ('@' + raw_command):
            _LOGGER.debug("Send & Response command values dont match %s != %s - Ignoring returned value", split_string[0], '@' + raw_command )
            return None
        else:
             return split_string[1]
             # b'AMT:0\r will return 0

    def main_mute(self, operator, value=None):
        """Execute Main.Mute."""
        return self.exec_command('main', 'mute', operator, value)

    def main_power(self, operator, value=None):
        """Execute Main.Power."""
        return self.exec_command('main', 'power', operator, value)

    def main_volume(self, operator, value=None):
        """
        Execute Main.Volume.
        Returns int
        """
        vol_result = self.exec_command('main', 'volume', operator, value)
        if vol_result != None:
            return int(vol_result)

    def main_source(self, operator, value=None):
        """Execute Main.Source."""
        result = self.exec_command('main', 'source', operator, value)
        """
        The receiver often returns the source value twice. If so take the
        second value as the source, otherwise return original
        """
        if result != None and len(result) == 2:
            _LOGGER.debug("Source Result: %s", result[1])
            return result[1]
        else:
            return result

    def main_autostatus (self, operator, value=None):
        """
        Execute autostatus.
        Not currently used but will allow two-way communications in future

        Returns int
        """
        return int(self.exec_command('main', 'autostatus', operator, value))

marantz.py:
“”"
Support for interfacing with Marantz receivers through RS-232.

For more details about this platform, please refer to the documentation at

"""
import logging

import voluptuous as vol

from homeassistant.components.media_player import (
    SUPPORT_VOLUME_SET,
    SUPPORT_VOLUME_MUTE, SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
    SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, MediaPlayerDevice,
    PLATFORM_SCHEMA)
from homeassistant.const import (
    CONF_NAME, STATE_OFF, STATE_ON)
import homeassistant.helpers.config_validation as cv

#REQUIREMENTS = ['marantz_receiver==0.0.1']
#REQUIREMENTS = ['https://github.com/andrewpc/marantz_receiver/archive/0.0.0.1.zip']

_LOGGER = logging.getLogger(__name__)

DEFAULT_NAME = 'Marantz Receiver'
DEFAULT_MIN_VOLUME = -71
DEFAULT_MAX_VOLUME = -1

SUPPORT_MARANTZ = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
    SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \
    SUPPORT_SELECT_SOURCE

#CONF_SERIAL_PORT = 'serial_port'
CONF_HOST = 'host'
CONF_PORT = 'port'
CONF_MIN_VOLUME = 'min_volume'
CONF_MAX_VOLUME = 'max_volume'
CONF_SOURCE_DICT = 'sources'

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    #vol.Required(CONF_SERIAL_PORT): cv.string,
    vol.Required(CONF_HOST): cv.string,
    vol.Required(CONF_PORT): cv.string,
    vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
    vol.Optional(CONF_MIN_VOLUME, default=DEFAULT_MIN_VOLUME): int,
    vol.Optional(CONF_MAX_VOLUME, default=DEFAULT_MAX_VOLUME): int,
    vol.Optional(CONF_SOURCE_DICT, default={}): {cv.string: cv.string},
})


def setup_platform(hass, config, add_devices, discovery_info=None):
    """Set up the Marantz platform."""
    from marantz_receiver import MarantzReceiver
    add_devices([Marantz(
        config.get(CONF_NAME),
        MarantzReceiver(config.get(CONF_HOST), config.get(CONF_PORT)),
        config.get(CONF_MIN_VOLUME),
        config.get(CONF_MAX_VOLUME),
        config.get(CONF_SOURCE_DICT)
    )], True)


class Marantz(MediaPlayerDevice):
    """Representation of a Marantz Receiver."""

    def __init__(self, name, marantz_receiver, min_volume, max_volume, source_dict):
        """Initialize the Marantz Receiver device."""
        self._name = name
        self._marantz_receiver = marantz_receiver
        self._min_volume = min_volume
        self._max_volume = max_volume
        self._source_dict = source_dict
        self._reverse_mapping = {value: key for key, value in
                                 self._source_dict.items()}

        self._volume = self._state = self._mute = self._source = None

    def calc_volume(self, decibel):
        """
        Calculate the volume given the decibel.

        Return the volume (0..1).
        """
        return abs(self._min_volume - decibel) / abs(
            self._min_volume - self._max_volume)

    def calc_db(self, volume):
        """
        Calculate the decibel given the volume.

        Return the dB.
        """
        return self._min_volume + round(
            abs(self._min_volume - self._max_volume) * volume)

    @property
    def name(self):
        """Return the name of the device."""
        return self._name

    @property
    def state(self):
        """Return the state of the device."""
        return self._state

    def update(self):
        """Retrieve latest state."""
        if self._marantz_receiver.main_power(':', '?') == '1':
            self._state = STATE_OFF
        else:
            self._state = STATE_ON

        if self._marantz_receiver.main_mute(':', '?') == '1':
            self._mute = False
        else:
            self._mute = True

        volume_result = self._marantz_receiver.main_volume(':', '?')
        if (volume_result != None):
            self._volume = self.calc_volume(volume_result)
        self._source = self._source_dict.get(
            self._marantz_receiver.main_source(':', '?'))

    @property
    def volume_level(self):
        """Volume level of the media player (0..1)."""
        return self._volume

    @property
    def is_volume_muted(self):
        """Boolean if volume is currently muted."""
        return self._mute

    @property
    def supported_features(self):
        """Flag media player features that are supported."""
        return SUPPORT_MARANTZ

    def turn_off(self):
        """Turn the media player off."""
        self._marantz_receiver.main_power(':', '3')

    def turn_on(self):
        """Turn the media player on."""
        self._marantz_receiver.main_power(':', '2')

    def volume_up(self):
        """Volume up the media player."""
        self._marantz_receiver.main_volume(':', '1')

    def volume_down(self):
        """Volume down the media player."""
        self._marantz_receiver.main_volume(':', '2')

    def set_volume_level(self, volume):
        """Set volume level, range 0..1."""
        vol_calc = '0' + str(self.calc_db(volume))
        self._marantz_receiver.main_volume(':', vol_calc)

    def select_source(self, source):
        """Select input source."""
        self._marantz_receiver.main_source(':', self._reverse_mapping.get(source))

    @property
    def source(self):
        """Name of the current input source."""
        return self._source

    @property
    def source_list(self):
        """List of available input sources."""
        return sorted(list(self._reverse_mapping.keys()))

    def mute_volume(self, mute):
        """Mute (true) or unmute (false) media player."""
        if mute:
            self._marantz_receiver.main_mute(':', '2')
        else:
            self._marantz_receiver.main_mute(':', '1')

configuration.yaml:

media_player:
#  - platform: cast
  - platform: samsungtv
    host: 192.168.1.177
  - platform: marantz
    host: 192.168.1.151
    port: 8080
#    serial_port: /dev/ttyS3
    name: Marantz Receiver
    min_volume: -71
    max_volume: -1
    sources:
      1: 'TV'
      3: 'VCR'
      5: 'DSS'
      9: 'AUX'
      F: 'Tuner'
      G: 'FM'
      H: 'AM'

:finland:

Glad you solved it!

Few months ago I added the sound mode to it:

configuration.yaml:

# Marantz Reciever    
  - platform: marantz
    serial_port: /dev/ttyUSB0
    name: Marantz Receiver
    min_volume: -71
    max_volume: -15
    sources:
      1: 'TV'
      2: 'MediaCenter'
      3: 'Chromecast'
      G: 'FM'
    soundmode:
      '0' : 'Auto'
      '1' : 'Stereo'
      'T' : 'Source Direct'
      'U' : 'Pure Direct'

marantz_commands.py

"""
Commands and operators used by Marantz.

CMDS[domain][function]

Majority of Marantz commands use ':' as operator although there are also some
multi-zone commands that use '='

"""
CMDS = {
    'main':
        {
            'mute':
                {'cmd': 'AMT',
                 'supported_operators': [':']
                 },
            'power':
                {'cmd': 'PWR',
                 'supported_operators': [':']
                 },
            'volume':
                {'cmd': 'VOL',
                 'supported_operators': [':']
                 },
            'source':
                {'cmd': 'SRC',
                 'supported_operators': [':']
                 },
            'sound_mode':
                {'cmd': 'SUR',
                 'supported_operators': [':']
                 },
            'autostatus':
                {'cmd': 'AST',
                 'supported_operators': [':']
                 }
        }
}

init.py

"""
Marantz has an RS232 interface to control the receiver.

Not all receivers have all functions.
Functions can be found on in the xls file within this repository
"""

import codecs
import socket
from time import sleep
from marantz_receiver.marantz_commands import CMDS
import serial  # pylint: disable=import-error
import threading
import telnetlib
import logging
import time

DEFAULT_TIMEOUT = 0.5
DEFAULT_WRITE_TIMEOUT = 0.5

_LOGGER = logging.getLogger(__name__)

class MarantzReceiver(object):
    """Marantz receiver."""

    def __init__(self, serial_port, timeout=DEFAULT_TIMEOUT,
                 write_timeout=DEFAULT_WRITE_TIMEOUT):
        """Create RS232 connection."""
        self.ser = serial.Serial(serial_port, baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=timeout,
                                 write_timeout=write_timeout)
        self.lock = threading.Lock()

    def exec_command(self, domain, function, operator, value=None):
        """
        Write a command to the receiver and read the value it returns.
        The receiver will always return a value, also when setting a value.
        """
        raw_command = CMDS[domain][function]['cmd']
        if operator in CMDS[domain][function]['supported_operators']:
            if value is None:
                raise ValueError('No value provided')
            else:
                cmd = ''.join([raw_command, operator, str(value)])

        else:
            raise ValueError('Invalid operator provided %s' % operator)
        with self.lock:
            if not self.ser.is_open:
                self.ser.open()

            self.ser.reset_input_buffer()
            self.ser.reset_output_buffer()
#            self.lock.acquire()
    
            # Marantz uses the prefix @ and the suffix \r, so add those to the above cmd.
            final_command = ''.join(['@', cmd, '\r']).encode('utf-8')
            _LOGGER.debug ('Send Command %s',final_command)
    
            self.ser.write(final_command)
    
            msg = self.ser.read_until(bytes('\r'.encode()))
#            self.lock.release()

        _LOGGER.debug ('Response msg %s', msg.decode())

        split_string = msg.decode().strip().split(':')

        _LOGGER.debug("Decoded split string %s", split_string)
        _LOGGER.debug ("Original command: %s", raw_command)
        # Check return value contains the same command value as requested. Sometimes the marantz gets out of sync. Ignore if this is the case
        if split_string[0] != ('@' + raw_command):
            _LOGGER.debug ("Send & Response command values dont match %s != %s - Ignoring returned value", split_string[0], '@' + raw_command )
            return None
        else:
             return split_string[1]
             # b'AMT:0\r will return 0

    def main_mute(self, operator, value=None):
        """Execute Main.Mute."""
        return self.exec_command('main', 'mute', operator, value)

    def main_power(self, operator, value=None):
        """Execute Main.Power."""
        return self.exec_command('main', 'power', operator, value)

    def main_volume(self, operator, value=None):
        """
        Execute Main.Volume.
        Returns int
        """
        vol_result = self.exec_command('main', 'volume', operator, value)
        if vol_result != None:
            return int(vol_result)

    def main_source(self, operator, value=None):
        """Execute Main.Source."""
        result = self.exec_command('main', 'source', operator, value)
        """
        The receiver often returns the source value twice. If so take the
        second value as the source, otherwise return original
        """
        if result != None and len(result) == 2:
            _LOGGER.debug("Source Result: %s", result[1])
            return result[1]
        else:
            return result

    def main_sound_mode(self, operator, value=None):
        """Execute Main.SoundMode."""
        result_sound_mode = self.exec_command('main', 'sound_mode', operator, value)

        if result_sound_mode != None :
            return result_sound_mode

    def main_autostatus (self, operator, value=None):
        """
        Execute autostatus.
        Not currently used but will allow two-way communications in future

        Returns int
        """
        return int(self.exec_command('main', 'autostatus', operator, value))

marantz.py

"""
Support for interfacing with Marantz receivers through RS-232.

For more details about this platform, please refer to the documentation at

"""
import logging

import voluptuous as vol

from homeassistant.components.media_player import (
    SUPPORT_VOLUME_SET,
    SUPPORT_VOLUME_MUTE, SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
    SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_SELECT_SOUND_MODE, MediaPlayerDevice,
    PLATFORM_SCHEMA)
from homeassistant.const import (
    CONF_NAME, STATE_OFF, STATE_ON)
import homeassistant.helpers.config_validation as cv

#REQUIREMENTS = ['marantz_receiver==0.0.1']
#REQUIREMENTS = ['https://github.com/andrewpc/marantz_receiver/archive/0.0.0.1.zip']

_LOGGER = logging.getLogger(__name__)

DEFAULT_NAME = 'Marantz Receiver'
DEFAULT_MIN_VOLUME = -71
DEFAULT_MAX_VOLUME = -1

SUPPORT_MARANTZ = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
    SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \
    SUPPORT_SELECT_SOURCE | SUPPORT_SELECT_SOUND_MODE 

CONF_SERIAL_PORT = 'serial_port'
CONF_MIN_VOLUME = 'min_volume'
CONF_MAX_VOLUME = 'max_volume'
CONF_SOURCE_DICT = 'sources'
CONF_SOUNDMODE_DICT = 'soundmode'

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Required(CONF_SERIAL_PORT): cv.string,
    vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
    vol.Optional(CONF_MIN_VOLUME, default=DEFAULT_MIN_VOLUME): int,
    vol.Optional(CONF_MAX_VOLUME, default=DEFAULT_MAX_VOLUME): int,
    vol.Optional(CONF_SOURCE_DICT, default={}): {cv.string: cv.string},
    vol.Optional(CONF_SOUNDMODE_DICT, default={}): {cv.string: cv.string},
})


def setup_platform(hass, config, add_devices, discovery_info=None):
    """Set up the Marantz platform."""
    from marantz_receiver import MarantzReceiver
    add_devices([Marantz(
        config.get(CONF_NAME),
        MarantzReceiver(config.get(CONF_SERIAL_PORT)),
        config.get(CONF_MIN_VOLUME),
        config.get(CONF_MAX_VOLUME),
        config.get(CONF_SOURCE_DICT),
        config.get(CONF_SOUNDMODE_DICT)
    )], True)


class Marantz(MediaPlayerDevice):
    """Representation of a Marantz Receiver."""

    def __init__(self, name, marantz_receiver, min_volume, max_volume,
                 source_dict, sound_mode_dict):
        """Initialize the Marantz Receiver device."""
        self._name = name
        self._marantz_receiver = marantz_receiver
        self._min_volume = min_volume
        self._max_volume = max_volume
        self._source_dict = source_dict
        self._sound_mode_dict = sound_mode_dict
        self._reverse_mapping = {value: key for key, value in
                                 self._source_dict.items()}
        self._reverse_mapping_sound_mode = {value: "0{}".format(key) for key, value in
                                 self._sound_mode_dict.items()}

        self._volume = self._state = self._mute = self._source = None

    def calc_volume(self, decibel):
        """
        Calculate the volume given the decibel.

        Return the volume (0..1).
        """
        return abs(self._min_volume - decibel) / abs(
            self._min_volume - self._max_volume)

    def calc_db(self, volume):
        """
        Calculate the decibel given the volume.

        Return the dB.
        """
        return self._min_volume + round(
            abs(self._min_volume - self._max_volume) * volume)

    @property
    def name(self):
        """Return the name of the device."""
        return self._name

    @property
    def state(self):
        """Return the state of the device."""
        return self._state

    def update(self):
        """Retrieve latest state."""
        if self._marantz_receiver.main_power(':', '?') == '1':
            self._state = STATE_OFF
        else:
            self._state = STATE_ON

        if self._marantz_receiver.main_mute(':', '?') == '1':
            self._mute = False
        else:
            self._mute = True

        volume_result = self._marantz_receiver.main_volume(':', '?')
        if (volume_result != None):
            self._volume = self.calc_volume(volume_result)
            
        self._source = self._source_dict.get(
            self._marantz_receiver.main_source(':', '?'))
        self._sound_mode = self._sound_mode_dict.get(
            self._marantz_receiver.main_sound_mode(':', '?'))

    @property
    def volume_level(self):
        """Volume level of the media player (0..1)."""
        return self._volume

    @property
    def is_volume_muted(self):
        """Boolean if volume is currently muted."""
        return self._mute

    @property
    def supported_features(self):
        """Flag media player features that are supported."""
        return SUPPORT_MARANTZ

    def turn_off(self):
        """Turn the media player off."""
        self._marantz_receiver.main_power(':', '3')

    def turn_on(self):
        """Turn the media player on."""
        self._marantz_receiver.main_power(':', '2')

    def volume_up(self):
        """Volume up the media player."""
        self._marantz_receiver.main_volume(':', '1')

    def volume_down(self):
        """Volume down the media player."""
        self._marantz_receiver.main_volume(':', '2')

    def set_volume_level(self, volume):
        """Set volume level, range 0..1."""
        vol_calc = '0' + str(self.calc_db(volume))
        self._marantz_receiver.main_volume(':', vol_calc)

    def select_source(self, source):
        """Select input source."""
        self._marantz_receiver.main_source(':', self._reverse_mapping.get(source))

    def select_sound_mode(self, sound_mode):
        """Select sound mode."""
        self._marantz_receiver.main_sound_mode(':', self._reverse_mapping_sound_mode.get(sound_mode))
        
    def mute_volume(self, mute):
        """Mute (true) or unmute (false) media player."""
        if mute:
            self._marantz_receiver.main_mute(':', '2')
        else:
            self._marantz_receiver.main_mute(':', '1')
            
    @property
    def source(self):
        """Name of the current input source."""
        return self._source

    @property
    def sound_mode(self):
        """Name of the current sound_mode."""
        return self._sound_mode

    @property
    def source_list(self):
        """List of available input sources."""
        return sorted(list(self._reverse_mapping.keys()))

    @property
    def sound_mode_list(self):
        """List of available sound_modes."""
        return sorted(list(self._reverse_mapping_sound_mode.keys()))

Is this still working for you? I’m not having any luck communicating with my receiver.

The component is loading properly and I can see it in the Overview of HA, but it’s not displaying the correct status or receiving commands I’m sending to it.

Could you provide a little bit more info, for me and I guess also for the others here. More info is needed to provide a clear answers where it goes wrong.

Turns out I had a bad Pi. Popped the SD card into another one and everything works as expected.

Noob here, how hard would it be to use this with other brands? I have a Cambridge Audio surround amp that has RS232. They even provide all the codes in the manual. I have zero experience with RS232. Can I just change the commands in the library to suit?

Great work! Got my old Marantz SR7500 up and running using this. Unfortunately, when upgrading to 0.88 it broke. Any chance of updating it or sharing some instructions how to do it?

Ok, so I’m still trying to get it to work. I’ve updated to Hass.io 0.89.2 and when I try to get the component going i get the following error message:

2019-03-13 18:04:38 ERROR (MainThread) [homeassistant.components.media_player] Error while setting up platform marantz
Traceback (most recent call last):
File “/usr/local/lib/python3.7/site-packages/homeassistant/helpers/entity_platform.py”, line 128, in _async_setup_platform
SLOW_SETUP_MAX_WAIT, loop=hass.loop)
File “/usr/local/lib/python3.7/asyncio/tasks.py”, line 416, in wait_for
return fut.result()
File “/usr/local/lib/python3.7/concurrent/futures/thread.py”, line 57, in run
result = self.fn(*self.args, **self.kwargs)
File “/config/custom_components/marantz/media_player.py”, line 52, in setup_platform
from marantz_receiver import MarantzReceiver
ModuleNotFoundError: No module named ‘marantz_receiver’

Any ideas how to get it working again will be greatly appreciated.

Correct since the latest version you have to do some modifications:

What I did, I created under custom_components a folder called “Marantz” under this folder are 2 files,

__init__.py (this file is empty)

media_player.py

"""
Support for interfacing with Marantz receivers through RS-232.

For more details about this platform, please refer to the documentation at

"""
import logging

import voluptuous as vol

from homeassistant.components.media_player import (
    MediaPlayerDevice,
    PLATFORM_SCHEMA)
from homeassistant.components.media_player.const import (
# from homeassistant.components.media_player import (
    SUPPORT_VOLUME_SET,
    SUPPORT_VOLUME_MUTE, SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
    SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_SELECT_SOUND_MODE
#, MediaPlayerDevice,
#    PLATFORM_SCHEMA)
)
from homeassistant.const import (
    CONF_NAME, STATE_OFF, STATE_ON)
import homeassistant.helpers.config_validation as cv

#REQUIREMENTS = ['marantz_receiver==0.0.1']
#REQUIREMENTS = ['https://github.com/andrewpc/marantz_receiver/archive/0.0.0.1.zip']

_LOGGER = logging.getLogger(__name__)

DEFAULT_NAME = 'Marantz Receiver'
DEFAULT_MIN_VOLUME = -71
DEFAULT_MAX_VOLUME = -1

SUPPORT_MARANTZ = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
    SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \
    SUPPORT_SELECT_SOURCE | SUPPORT_SELECT_SOUND_MODE 

CONF_SERIAL_PORT = 'serial_port'
CONF_MIN_VOLUME = 'min_volume'
CONF_MAX_VOLUME = 'max_volume'
CONF_SOURCE_DICT = 'sources'
CONF_SOUNDMODE_DICT = 'soundmode'

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Required(CONF_SERIAL_PORT): cv.string,
    vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
    vol.Optional(CONF_MIN_VOLUME, default=DEFAULT_MIN_VOLUME): int,
    vol.Optional(CONF_MAX_VOLUME, default=DEFAULT_MAX_VOLUME): int,
    vol.Optional(CONF_SOURCE_DICT, default={}): {cv.string: cv.string},
    vol.Optional(CONF_SOUNDMODE_DICT, default={}): {cv.string: cv.string},
})


def setup_platform(hass, config, add_devices, discovery_info=None):
    """Set up the Marantz platform."""
    from marantz_receiver import MarantzReceiver
    add_devices([Marantz(
        config.get(CONF_NAME),
        MarantzReceiver(config.get(CONF_SERIAL_PORT)),
        config.get(CONF_MIN_VOLUME),
        config.get(CONF_MAX_VOLUME),
        config.get(CONF_SOURCE_DICT),
        config.get(CONF_SOUNDMODE_DICT)
    )], True)


class Marantz(MediaPlayerDevice):
    """Representation of a Marantz Receiver."""

    def __init__(self, name, marantz_receiver, min_volume, max_volume,
                 source_dict, sound_mode_dict):
        """Initialize the Marantz Receiver device."""
        self._name = name
        self._marantz_receiver = marantz_receiver
        self._min_volume = min_volume
        self._max_volume = max_volume
        self._source_dict = source_dict
        self._sound_mode_dict = sound_mode_dict
        self._reverse_mapping = {value: key for key, value in
                                 self._source_dict.items()}
        self._reverse_mapping_sound_mode = {value: "0{}".format(key) for key, value in
                                 self._sound_mode_dict.items()}

        self._volume = self._state = self._mute = self._source = None

    def calc_volume(self, decibel):
        """
        Calculate the volume given the decibel.

        Return the volume (0..1).
        """
        return abs(self._min_volume - decibel) / abs(
            self._min_volume - self._max_volume)

    def calc_db(self, volume):
        """
        Calculate the decibel given the volume.

        Return the dB.
        """
        return self._min_volume + round(
            abs(self._min_volume - self._max_volume) * volume)

    @property
    def name(self):
        """Return the name of the device."""
        return self._name

    @property
    def state(self):
        """Return the state of the device."""
        return self._state

    def update(self):
        """Retrieve latest state."""
        if self._marantz_receiver.main_power(':', '?') == '1':
            self._state = STATE_OFF
        else:
            self._state = STATE_ON

        if self._marantz_receiver.main_mute(':', '?') == '1':
            self._mute = False
        else:
            self._mute = True

        volume_result = self._marantz_receiver.main_volume(':', '?')
        if (volume_result != None):
            self._volume = self.calc_volume(volume_result)
            
        self._source = self._source_dict.get(
            self._marantz_receiver.main_source(':', '?'))
        self._sound_mode = self._sound_mode_dict.get(
            self._marantz_receiver.main_sound_mode(':', '?'))

    @property
    def volume_level(self):
        """Volume level of the media player (0..1)."""
        return self._volume

    @property
    def is_volume_muted(self):
        """Boolean if volume is currently muted."""
        return self._mute

    @property
    def supported_features(self):
        """Flag media player features that are supported."""
        return SUPPORT_MARANTZ

    def turn_off(self):
        """Turn the media player off."""
        self._marantz_receiver.main_power(':', '3')

    def turn_on(self):
        """Turn the media player on."""
        self._marantz_receiver.main_power(':', '2')

    def volume_up(self):
        """Volume up the media player."""
        self._marantz_receiver.main_volume(':', '1')

    def volume_down(self):
        """Volume down the media player."""
        self._marantz_receiver.main_volume(':', '2')

    def set_volume_level(self, volume):
        """Set volume level, range 0..1."""
        vol_calc = '0' + str(self.calc_db(volume))
        self._marantz_receiver.main_volume(':', vol_calc)

    def select_source(self, source):
        """Select input source."""
        self._marantz_receiver.main_source(':', self._reverse_mapping.get(source))

    def select_sound_mode(self, sound_mode):
        """Select sound mode."""
        self._marantz_receiver.main_sound_mode(':', self._reverse_mapping_sound_mode.get(sound_mode))
        
    def mute_volume(self, mute):
        """Mute (true) or unmute (false) media player."""
        if mute:
            self._marantz_receiver.main_mute(':', '2')
        else:
            self._marantz_receiver.main_mute(':', '1')
            
    @property
    def source(self):
        """Name of the current input source."""
        return self._source

    @property
    def sound_mode(self):
        """Name of the current sound_mode."""
        return self._sound_mode

    @property
    def source_list(self):
        """List of available input sources."""
        return sorted(list(self._reverse_mapping.keys()))

    @property
    def sound_mode_list(self):
        """List of available sound_modes."""
        return sorted(list(self._reverse_mapping_sound_mode.keys()))

Under a different folder called deps -> lib -> site-packages -> marantz_receiver are 2 files:

__init__.py

"""
Marantz has an RS232 interface to control the receiver.

Not all receivers have all functions.
Functions can be found on in the xls file within this repository
"""

import codecs
import socket
from time import sleep
from marantz_receiver.marantz_commands import CMDS
import serial  # pylint: disable=import-error
import threading
import telnetlib
import logging
import time

DEFAULT_TIMEOUT = 0.5
DEFAULT_WRITE_TIMEOUT = 0.5

_LOGGER = logging.getLogger(__name__)

class MarantzReceiver(object):
    """Marantz receiver."""

    def __init__(self, serial_port, timeout=DEFAULT_TIMEOUT,
                 write_timeout=DEFAULT_WRITE_TIMEOUT):
        """Create RS232 connection."""
        self.ser = serial.Serial(serial_port, baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=timeout,
                                 write_timeout=write_timeout)
        self.lock = threading.Lock()

    def exec_command(self, domain, function, operator, value=None):
        """
        Write a command to the receiver and read the value it returns.
        The receiver will always return a value, also when setting a value.
        """
        raw_command = CMDS[domain][function]['cmd']
        if operator in CMDS[domain][function]['supported_operators']:
            if value is None:
                raise ValueError('No value provided')
            else:
                cmd = ''.join([raw_command, operator, str(value)])

        else:
            raise ValueError('Invalid operator provided %s' % operator)
        with self.lock:
            if not self.ser.is_open:
                self.ser.open()

            self.ser.reset_input_buffer()
            self.ser.reset_output_buffer()
#            self.lock.acquire()
    
            # Marantz uses the prefix @ and the suffix \r, so add those to the above cmd.
            final_command = ''.join(['@', cmd, '\r']).encode('utf-8')
            _LOGGER.debug ('Send Command %s',final_command)
    
            self.ser.write(final_command)
    
            msg = self.ser.read_until(bytes('\r'.encode()))
#            self.lock.release()

        _LOGGER.debug ('Response msg %s', msg.decode())

        split_string = msg.decode().strip().split(':')

        _LOGGER.debug("Decoded split string %s", split_string)
        _LOGGER.debug ("Original command: %s", raw_command)
        # Check return value contains the same command value as requested. Sometimes the marantz gets out of sync. Ignore if this is the case
        if split_string[0] != ('@' + raw_command):
            _LOGGER.debug ("Send & Response command values dont match %s != %s - Ignoring returned value", split_string[0], '@' + raw_command )
            return None
        else:
             return split_string[1]
             # b'AMT:0\r will return 0

    def main_mute(self, operator, value=None):
        """Execute Main.Mute."""
        return self.exec_command('main', 'mute', operator, value)

    def main_power(self, operator, value=None):
        """Execute Main.Power."""
        return self.exec_command('main', 'power', operator, value)

    def main_volume(self, operator, value=None):
        """
        Execute Main.Volume.
        Returns int
        """
        vol_result = self.exec_command('main', 'volume', operator, value)
        if vol_result != None:
            return int(vol_result)

    def main_source(self, operator, value=None):
        """Execute Main.Source."""
        result = self.exec_command('main', 'source', operator, value)
        """
        The receiver often returns the source value twice. If so take the
        second value as the source, otherwise return original
        """
        if result != None and len(result) == 2:
            _LOGGER.debug("Source Result: %s", result[1])
            return result[1]
        else:
            return result

    def main_sound_mode(self, operator, value=None):
        """Execute Main.SoundMode."""
        result_sound_mode = self.exec_command('main', 'sound_mode', operator, value)

#        if result_sound_mode != None and len(result_sound_mode) == 2:
#            _LOGGER.debug("Sound_Mode Result: %s", result_sound_mode[1])
#            return result_sound_mode[1]
#        else:
#            return result_sound_mode

        if result_sound_mode != None :
            return result_sound_mode

    def main_autostatus (self, operator, value=None):
        """
        Execute autostatus.
        Not currently used but will allow two-way communications in future

        Returns int
        """
        return int(self.exec_command('main', 'autostatus', operator, value))

marantz_commands.py

"""
Commands and operators used by Marantz.

CMDS[domain][function]

Majority of Marantz commands use ':' as operator although there are also some
multi-zone commands that use '='

"""
CMDS = {
    'main':
        {
            'mute':
                {'cmd': 'AMT',
                 'supported_operators': [':']
                 },
            'power':
                {'cmd': 'PWR',
                 'supported_operators': [':']
                 },
            'volume':
                {'cmd': 'VOL',
                 'supported_operators': [':']
                 },
            'source':
                {'cmd': 'SRC',
                 'supported_operators': [':']
                 },
            'sound_mode':
                {'cmd': 'SUR',
                 'supported_operators': [':']
                 },
            'autostatus':
                {'cmd': 'AST',
                 'supported_operators': [':']
                 }
        }
}
1 Like

That worked! Thank you so much for taking the time to update.