Multiple Russound Cav6.6 Configuration

Hey so I have 3 Russound Cav6.6’s using RNET to link them all together. I have a USB -> R232 cable connected to my PI and im able to control one out of three of them using the Russound RNET using ser2net. This is what my config looks like right now.

#Russounds @ dev/TTYUSB0
media_player:
  - platform: russound_rnet
    host: 127.0.1.1
    port: 4001
    name: Russound
    zones:
      1:
        name: Front Entry
      2:
        name: Main Hall 1
      3:
        name: Main Hall 2
      4:
        name: Main Hall 3
      5:
        name: Mecha Hall 1
      6:
        name: Mecha Hall 2
    sources:
      - name: Church Sound

But i’m unable to get at the other 2 Cavs because I’m not sure how to configure them. Does anyone have any tips on how to access the other two. I tried to add a number 7 to the zones with no luck. Not sure what to do next.

Hi there,

Did you find a resolve? thanks in advance

No still in the same situation. I haven’t gotten to use the other 2 yet just because we haven’t reached that part of the project yet, but to my knowledge you can use multiple serial cables. I’ll see if i can find the post.

https://packet6.com/configuring-your-raspberry-pi-as-a-console-server/

I would start about where the installing of ser2net starts and then the next two paragraphs show you how to put multiple serial cables and set them up in the ser2net config.

I’m actually not connecting to the rasberry directly. I have a global cache device that communicates through my network and have 2 addressable serial connections on it. I know this device and connection is working correctly as it sees the 1-6 on the first controller, but I don’t know if this rnet plugin supports multiple controllers or what the syntax would be on the yaml.

What does your configuration for the global cache device look like for the first set? and how do you have your RNET connected?

Here is the entry I have in my configuration.yaml

#controller 1 connected to Global cache

  • platform: russound_rnet
    host: 192.168.1.39
    port: 4999
    name: Russound
    zones:
    1:
    name: Spare Bedroom Audio
    2:
    name: Ensuite Audio
    3:
    name: Logans Bedroom Audio
    4:
    name: Nolans Bedroom Audio
    5:
    name: Great Room Audio
    6:
    name: Basement Audio

    sources:

    • name: XM Radio
    • name: FM Tuner
    • name: Bell Satellite
    • name: Blu-Ray
    • name: Bose Soundlink
    • name: Apple TV

#this part does not work
#controller 2 connected to Global cache

- platform: russound_rnet

host: 192.168.1.39

port: 5000

name: Russound

zones:

7:

name: Test 1

8:

name: Test 2

9:

name: Test 3

10:

name: Test 4

11:

name: Test 5

12:

name: Test 6

sources:

- name: XM Radio

- name: FM Tuner

- name: Bell Satellite

- name: Blu-Ray

- name: Apple TV

Did you make any head way with integrating the global cache and the Russound? I have the same setup and would love to know how you got everything working through home assistant

I am trying to get my MCA to work with HA, but in my reading it stated that you need to have an independent serial connector for each unit, so that may be the source of your problem.

Happy hunting.

I am in a similar situation, ie trying to get multiple Russound (RNET) controllers to work. I think that the implementation of russound.py (/usr/local/lib/python3.9/site-packages/russound/russound.py) based on reviewing this file: https://www.dropbox.com/s/6p9cfydamth76z1/russound.py?dl=0 (pulled from this post: Success with integrating to russound caa66 #6569) seems to assume a single controller, although it does contain code to support multiple controllers. However, I do not know if that file is the same as the actual russound.py being used so am really not sure. Can anyone tell me how I can get access to/see the contents of /usr/local/lib/python3.9/site-packages/russound/russound.py? I am planning to copy it to a custom_components/russound_rnet folder to see if I can work out what changes are needed there and for the russound_rnet.py file, if any, to include controller id.
Ta much.

Its ok, I’ve figured it out. The russound.py file is stored onm PyPI and can be installed using pip with pip install russound

I think I’ve solved this. I have it working now with two controllers, specifically a CAM6.6 and a CAV6.6, The issue is that media_player.py in the core/homeassistant/components/russound_rnet folder assumes a single controller. My fix involved creating a custom_components/russound/rnet folder and modifying media_player to replace the controllerid (currently hard coded as “1”) and zoneid references in calls to russound.py with a derived value based on some math processing. So, a config zone_id value of 1 maps to an rnet_controller_id value of 1 and rnet_zone_id value of 1 and a config zone_id value of 7 maps to an rnet_controller_id value of 2 and rnet_zone_id value of 1, etc. Interestingly, I think I also had to have a new manifest.json file that included a version entry, eg “version”: “1.0.1” to get this to work.
However, all of this should really be an enhancement request which I shall endeavour to submit.

2 Likes

any shot u can show the code on this? im in the same boat - trying to add a second controller

Hi Richard. I wanted to submit an enhancement request to add additional RNET functions. Specifically bass/treble/balance would be awesome. I can’t se those exposed in the entities. I’m a noob. How/where do I submit the enhancement request?

Stevan, honestly I have no idea how to do that. In the end I went a different route completely. I now use mqtt to communicate to an entirely different version of what the ser2net component does. It means setting up the front end is more laborious and it’s probably not a preferred solution but it was relatively straightforward once I’d decoded the RNET traffic and offers the full range of RNET functionality. Performance is good, there’s no polling and no traffic when the Russound system is not being used. I’m sure proper developers/programmers would be tearing out their hair but it works for me :slight_smile:

2 Likes

Any chance you could post some details? Very interested in MQTT-enabling my 2x russound C5 receivers and being able to potentially use ESP32/ESP8266 to for audio control via MQTT.

Sure, what would you like to know? In the mqtt2serial component here’s the mapping I make for RNET commands (promise not to laugh):

    def get_action_message(self, action, ccid, zzid, pram = 0):
        if action == 'PowerOn':
            command = [0xF0, ccid, zzid, 0x7F, 0x00, 0x00, 0x70, 0x05, 0x02, 0x02, 0x00, 0x00, 0xF1, 0x23, 0x00, 0x01, 0x00, zzid, 0x00, 0x01, 0x00, 0x00]
        elif action == 'PowerOff':
            command = [0xF0, ccid, zzid, 0x7F, 0x00, 0x00, 0x70, 0x05, 0x02, 0x02, 0x00, 0x00, 0xF1, 0x23, 0x00, 0x00, 0x00, zzid, 0x00, 0x01, 0x00, 0x00]
        elif action == 'GetOnOff':
            command = [0xF0, ccid, zzid, 0x7F, 0x00, 0x00, 0x70, 0x01, 0x04, 0x02, 0x00, zzid, 0x06, 0x00, 0x00, 0x00, 0x00]
        elif action == 'SystemOn':
            command = [0xF0, 0x7E, 0x00, 0x7F, 0x00, 0x00, 0x70, 0x05, 0x02, 0x02, 0x00, 0x00, 0xF1, 0x22, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'SystemOff':
            command = [0xF0, 0x7E, 0x00, 0x7F, 0x00, 0x00, 0x70, 0x05, 0x02, 0x02, 0x00, 0x00, 0xF1, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'SetSource':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x00, 0x00, 0x00, 0xF1, 0x3E, 0x00, 0x00, 0x00, pram, 0x00, 0x01, 0x00, 0x00]
        elif action == 'SetSource0':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x00, 0x00, 0x00, 0xF1, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'SetSource1':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x00, 0x00, 0x00, 0xF1, 0x3E, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00]
        elif action == 'SetSource2':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x00, 0x00, 0x00, 0xF1, 0x3E, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00]
        elif action == 'SetSource3':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x00, 0x00, 0x00, 0xF1, 0x3E, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00]
        elif action == 'SetSource4':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x00, 0x00, 0x00, 0xF1, 0x3E, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x00]
        elif action == 'SetSource5':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x00, 0x00, 0x00, 0xF1, 0x3E, 0x00, 0x00, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00]
        elif action == 'SetSource6':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x00, 0x00, 0x00, 0xF1, 0x3E, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00]
        elif action == 'GetSource':
            command = [0xF0, ccid, 0x00, 0x7F, 0x00, 0x00, 0x70, 0x01, 0x04, 0x02, 0x00, zzid, 0x02, 0x00, 0x00, 0x00, 0x00]
        elif action == 'SetVolume':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x02, 0x00, 0x00, 0xF1, 0x21, 0x00, pram, 0x00, zzid, 0x00, 0x01, 0x00, 0x00]
        elif action == 'VolumeUp':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x02, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'VolumeDown':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x02, 0x00, 0x00, 0xF1, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'GetVolume':
            command = [0xF0, ccid, 0x00, 0x7F, 0x00, 0x00, 0x70, 0x01, 0x04, 0x02, 0x00, zzid, 0x01, 0x00, 0x00, 0x00, 0x00]
        elif action == 'BassUp':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x05, 0x02, 0x00, zzid, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'BassDown':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x05, 0x02, 0x00, zzid, 0x00, 0x00, 0x00, 0x6A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'SetBass':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x00, 0x05, 0x02, 0x00, zzid, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, pram, 0x00, 0x00]
        elif action == 'GetBass':
            command = [0xF0, ccid, 0x00, 0x7F, 0x00, 0x00, 0x70, 0x01, 0x05, 0x02, 0x00, zzid, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
        elif action == 'TrebleUp':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x05, 0x02, 0x00, zzid, 0x00, 0x01, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'TrebleDown':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x05, 0x02, 0x00, zzid, 0x00, 0x01, 0x00, 0x6A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'SetTreble':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x00, 0x05, 0x02, 0x00, zzid, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, pram, 0x00, 0x00]
        elif action == 'GetTreble':
            command = [0xF0, ccid, 0x00, 0x7F, 0x00, 0x00, 0x70, 0x01, 0x05, 0x02, 0x00, zzid, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]
        elif action == 'LoudnessOn':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x00, 0x05, 0x02, 0x00, zzid, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00]
        elif action == 'LoudnessOff':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x00, 0x05, 0x02, 0x00, zzid, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]
        elif action == 'LoudnessOnOff':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x00, 0x05, 0x02, 0x00, zzid, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]
        elif action == 'GetLoudness':
            command = [0xF0, ccid, 0x00, 0x7F, 0x00, 0x00, 0x70, 0x01, 0x05, 0x02, 0x00, zzid, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00]
        elif action == 'SetBalanceLeft':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x00, 0x05, 0x02, 0x00, zzid, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]
        elif action == 'SetBalanceCenter':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x00, 0x05, 0x02, 0x00, zzid, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x0A, 0x00, 0x00]
        elif action == 'SetBalanceRight':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x00, 0x05, 0x02, 0x00, zzid, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x14, 0x00, 0x00]
        elif action == 'BalanceLeft':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x05, 0x02, 0x00, zzid, 0x00, 0x03, 0x00, 0x6A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'BalanceRight':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x05, 0x02, 0x00, zzid, 0x00, 0x03, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'GetBalance':
            command = [0xF0, ccid, 0x00, 0x7F, 0x00, 0x00, 0x70, 0x01, 0x05, 0x02, 0x00, zzid, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00]
        elif action == 'IncreaseOnVolume':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x05, 0x02, 0x00, zzid, 0x00, 0x04, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'DecreaseOnVolume':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x05, 0x02, 0x00, zzid, 0x00, 0x04, 0x00, 0x6A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'SetOnVolume':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x00, 0x05, 0x02, 0x00, zzid, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, pram, 0x00, 0x00]
        elif action == 'GetOnVolume':
            command = [0xF0, ccid, 0x00, 0x7F, 0x00, 0x00, 0x70, 0x01, 0x05, 0x02, 0x00, zzid, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00]
        elif action == 'BackgroundOnOff':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x05, 0x02, 0x00, zzid, 0x00, 0x05, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'BackgroundGreen':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x00, 0x05, 0x02, 0x00, zzid, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00]
        elif action == 'BackgroundAmber':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x00, 0x05, 0x02, 0x00, zzid, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00]
        elif action == 'BackgroundOff':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x00, 0x05, 0x02, 0x00, zzid, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]
        elif action == 'GetBackgroundColor':
            command = [0xF0, ccid, 0x00, 0x7F, 0x00, 0x00, 0x70, 0x01, 0x05, 0x02, 0x00, zzid, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00]
        elif action == 'DNDOnOff':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x00, 0x05, 0x02, 0x00, zzid, 0x00, 0x06, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'DNDOn':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x00, 0x05, 0x02, 0x00, zzid, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00]
        elif action == 'DNDOff':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x00, 0x05, 0x02, 0x00, zzid, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]
        elif action == 'GetDND':
            command = [0xF0, ccid, 0x00, 0x7F, 0x00, 0x00, 0x70, 0x01, 0x05, 0x02, 0x00, zzid, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00]
        elif action == 'PartyModeOnOff':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x00, 0x05, 0x02, 0x00, zzid, 0x00, 0x07, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'PartyModeMaster':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x00, 0x05, 0x02, 0x00, zzid, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00]
        elif action == 'PartyModeOn':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x00, 0x05, 0x02, 0x00, zzid, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00]
        elif action == 'PartyModeOff':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x00, 0x05, 0x02, 0x00, zzid, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]
        elif action == 'GetPartyMode':
            command = [0xF0, ccid, 0x00, 0x7F, 0x00, 0x00, 0x70, 0x01, 0x05, 0x02, 0x00, zzid, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00]
        elif action == 'GetZoneInfo':
            command = [0xF0, ccid, 0x00, 0x7F, 0x00, 0x00, 0x70, 0x01, 0x04, 0x02, 0x00, zzid, 0x07, 0x00, 0x00, 0x00, 0x00]
        elif action == 'KeyPrevious':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x02, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'KeyNext':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x02, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'KeyPlus':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x02, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'KeyMinus':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x02, 0x00, 0x00, 0x6A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'KeyPlay':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x02, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'KeyStop':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x02, 0x00, 0x00, 0x6D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'KeyPause':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x02, 0x00, 0x00, 0x6E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'KeyF1':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x02, 0x00, 0x00, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'KeyF2':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x02, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]
        elif action == 'SearchForward':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x02, 0x00, 0x00, 0xF1, 0x40, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x01, 0x00, 0x00]
        elif action == 'SearchBack':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x02, 0x00, 0x00, 0xF1, 0x40, 0x00, 0x00, 0x00, 0x1D, 0x00, 0x01, 0x00, 0x00]
        elif action == 'DisplayMessage':
            command = [0xF0, ccid, zzid, 0x7F, ccid, zzid, 0x70, 0x05, 0x02, 0x02, 0x00, 0x00, 0xF1, 0x23, 0x00, 0x00, 0x00, zzid, 0x00, 0x01, 0x00, 0x00]
        else:
            logger.debug('No such action')
            return
        total = len(command) - 2
        for c in command:
            total += c
        command[-2] = total & 0x7F
        command[-1] = 0xF7
        message = bytes([command[0]])
        for b in command[1:]:
            message += bytes([b])
        return message

These are the messages I intercept. It’s much harder to decode the keypad and system messages so this is even more crazy:

            while (self.mqtt_q.empty() == False) and (self.connected_rc == 0):
                message = mqtt_q.get()
                # only worried about certain messages
                if len(message) == 38 and message[24:36] == bytearray(b'\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a'):
                    # treat this is a power off indicator
                    # update zone data
                    controller_id = message[1]
                    zone_id = message[2]
                    self.zone[controller_id][zone_id].state = 0
                    # publish zone
                    self.publish('/rnet/zone/{0}/{1}'.format(controller_id, zone_id), self.zone[controller_id][zone_id].zone_json, False)

                elif len(message) == 15 and message[7] == 6:
                    if message[12] == 5:
                        # this is a set source with power on message
                        # this is also generated when returning to the main screen from making
                        # various attribute changes
                        # Determine if this change is to do with Power/ Source or something else
                        controller_id = message[1]
                        zone_id = message[2]
                        if self.zone[controller_id][zone_id].state != 1 or self.zone[controller_id][zone_id].source != message[9]:
                            # update zone data
                            self.zone[controller_id][zone_id].state = 1
                            self.zone[controller_id][zone_id].source = message[9]
                            # publish zone
                            self.publish('/rnet/zone/{0}/{1}'.format(controller_id, zone_id), self.zone[controller_id][zone_id].zone_json, False)
                        else:
                            # something else has changed, possibly
                            # so make a GetZoneInfo call to find out
                            request = self.get_action_message("GetZoneInfo", z.controller_id, z.zone_id)
                            self.serial_q.put(request)

                        # when the control pad returns from making Bass, Treble, Loudness or Balance adjustments
                        # it sets the zone so no need to pick up specific changes.

                    elif message[12] == 16:
                        # this is a volume changed message
                        # it also occurs when the zone is powered on so will generate
                        # another mqqt send to Home Assistant
                        controller_id = message[1]
                        zone_id = message[2]
                        self.zone[controller_id][zone_id].volume = message[8]
                        # publish zone
                        self.publish('/rnet/zone/{0}/{1}'.format(controller_id, zone_id), self.zone[controller_id][zone_id].zone_json, False)

                #if len(message) == 11 and message[

                elif len(message) == 34 and message[13] == 7:
                    # get GetZoneInfo response
                    # update zone data
                    controller_id = message[4]
                    zone_id = message[12]
                    self.zone[controller_id][zone_id].state = message[20]
                    self.zone[controller_id][zone_id].source = message[21]
                    self.zone[controller_id][zone_id].volume = message[22]
                    self.zone[controller_id][zone_id].bass = message[23]
                    self.zone[controller_id][zone_id].treble = message[24]
                    self.zone[controller_id][zone_id].loudness = message[25]
                    self.zone[controller_id][zone_id].balance = message[26]
                    self.zone[controller_id][zone_id].partymode = message[29]
                    self.zone[controller_id][zone_id].donotdisturb = message[30]

                    # publish zone
                    self.publish('/rnet/zone/{0}/{1}'.format(controller_id, zone_id), self.zone[controller_id][zone_id].zone_json, False)

                    if self.mqqt_request_getzoneinfo:
                        # publish the full response
                        topic = '/rnet/response'
                        payload = '{'
                        payload += '"rnet_controller" : {0}'.format(message[4])
                        payload += ', "rnet_zone" : {0}'.format(message[12])
                        payload += ', "ha_zone" : {0}'.format((message[4] * 6) + message[12] + 1)
                        payload += ', "state" : {0}'.format(message[20])
                        payload += ', "source" : "{0}"'.format(SOURCE_LIST[message[21]])
                        payload += ', "volume" : {0}'.format(message[22] * 2)
                        payload += ', "bass" : {0}'.format(message[23] - 10)
                        payload += ', "treble" : {0}'.format(message[24] - 10)
                        payload += ', "loudness" : {0}'.format(message[25])
                        payload += ', "balance" : {0}'.format(message[26] - 10)
                        payload += ', "systemonstate" : {0}'.format(message[27])
                        payload += ', "sharedsource" : {0}'.format(message[28])
                        payload += ', "partymode" : {0}'.format(message[29])
                        payload += ', "donotdisturb" : {0}'.format(message[30])
                        payload += '}'
                        self.publish(topic, payload, False)

                elif message[0:6] == bytearray(b'\xf0\x7d\x00\x79\x00\x7d'):
                    # this is a source message
                    # determnine the source
                    source_id = message[6]
                    # determine the text
                    # text starts from character 23 and continues ignoring the last three characters
                    display_text = message[23:len(message)-3]
                    # update the source display text
                    self.source[source_id].display_text = display_text.decode("utf-8")
                    # publish a source message
                    self.publish('/rnet/source/{0}'.format(source_id), self.source[source_id].source_json, False)

                elif len(message) == 23 and message[5:12] ==  bytearray(b'\x00\x7f\x00\x00\x04\x02\x00'):
                    if message[13] == 6:
                        # this is the GetState respoonse
                        # update zone data
                        controller_id = message[1]
                        zone_id = message[2]
                        self.zone[controller_id][zone_id].state = message[20]
                        # publish zone
                        self.publish('/rnet/zone/{0}/{1}'.format(controller_id, zone_id), self.zone[controller_id][zone_id].zone_json, False)

                        # publish the raw result
                        topic = '/rnet/response'
                        payload = '{'
                        payload += '"rnet_controller" : {0}, '.format(message[4])
                        payload += '"rnet_zone" : {0}, '.format(message[12])
                        payload += '"ha_zone" : {0}, '.format((message[4] * 6) + message[12] + 1)
                        payload += '"state" : {0}'.format(message[20])
                        payload += '}'
                        self.publish(topic, payload, False)

                    if message[13] == 2:
                        # this is the GetSource response
                        # update zone data
                        controller_id = message[1]
                        zone_id = message[2]
                        self.zone[controller_id][zone_id].source = message[20]
                        # publish zone
                        self.publish('/rnet/zone/{0}/{1}'.format(controller_id, zone_id), self.zone[controller_id][zone_id].zone_json, False)

                        # publish the raw result
                        topic = '/rnet/response'
                        payload = '{'
                        payload += '"rnet_controller" : {0}'.format(message[4])
                        payload += ', "rnet_zone" : {0}'.format(message[12])
                        payload += ', "ha_zone" : {0}'.format((message[4] * 6) + message[12] + 1)
                        payload += ', "source" : "{0}"'.format(SOURCE_LIST[message[20]])
                        payload += '}'
                        self.publish(topic, payload, False)

                    if message[13] == 1:
                        # this is the GetVolume response
                        # update zone data
                        controller_id = message[1]
                        zone_id = message[2]
                        self.zone[controller_id][zone_id].volume = message[20]
                        # publish zone
                        self.publish('/rnet/zone/{0}/{1}'.format(controller_id, zone_id), self.zone[controller_id][zone_id].zone_json, False)

                        # publish the raw result
                        topic = '/rnet/response'
                        payload = '{'
                        payload += '"rnet_controller" : {0}'.format(message[4])
                        payload += ', "rnet_zone" : {0}'.format(message[12])
                        payload += ', "ha_zone" : {0}'.format((message[4] * 6) + message[12] + 1)
                        payload += ', "volume" : {0}'.format(message[20] * 2)
                        payload += '}'
                        self.publish(topic, payload, False)

                elif len(message) == 24 and message[5:12] ==  bytearray(b'\x00\x7f\x00\x00\x04\x02\x00'):
                    # publish the raw result
                    topic = '/rnet/response'
                    payload = '{'
                    payload += '"rnet_controller" : {0}'.format(message[4])
                    payload += ', "rnet_zone" : {0}'.format(message[12])
                    payload += ', "ha_zone" : {0}'.format((message[4] * 6) + message[12] + 1)
                    payload += ', "turnonvolume" : {0}'.format(message[21] * 2)
                    payload += '}'
                    self.publish(topic, payload, False)

                elif len(message) == 24 and message[5:12] ==  bytearray(b'\x00\x7f\x00\x00\x05\x02\x00'):
                    if message[14] == 0:
                        # this is the GetBass respoonse
                        # update zone data
                        controller_id = message[1]
                        zone_id = message[2]
                        self.zone[controller_id][zone_id].bass = message[21]
                        # publish zone
                        self.publish('/rnet/zone/{0}/{1}'.format(controller_id, zone_id), self.zone[controller_id][zone_id].zone_json, False)

                        # publish the raw result
                        topic = '/rnet/response'
                        payload = '{'
                        payload += '"rnet_controller" : {0}'.format(message[4])
                        payload += ', "rnet_zone" : {0}'.format(message[12])
                        payload += ', "ha_zone" : {0}'.format((message[4] * 6) + message[12] + 1)
                        payload += ', "bass" : {0}'.format(message[21] - 10)
                        payload += '}'
                        self.publish(topic, payload, False)

                    if message[14] == 1:
                        # this is the GetTreble respoonse
                        # update zone data
                        controller_id = message[1]
                        zone_id = message[2]
                        self.zone[controller_id][zone_id].treble = message[21]
                        # publish zone
                        self.publish('/rnet/zone/{0}/{1}'.format(controller_id, zone_id), self.zone[controller_id][zone_id].zone_json, False)

                        # publish the raw result
                        topic = '/rnet/response'
                        payload = '{'
                        payload += '"rnet_controller" : {0}'.format(message[4])
                        payload += ', "rnet_zone" : {0}'.format(message[12])
                        payload += ', "ha_zone" : {0}'.format((message[4] * 6) + message[12] + 1)
                        payload += ', "treble" : {0}'.format(message[21] - 10)
                        payload += '}'
                        self.publish(topic, payload, False)

                    if message[14] == 2:
                        # this is the GetLoudness respoonse
                        topic = '/rnet/response'
                        payload = '{'
                        payload += '"rnet_controller" : {0}'.format(message[4])
                        payload += ', "rnet_zone" : {0}'.format(message[12])
                        payload += ', "ha_zone" : {0}'.format((message[4] * 6) + message[12] + 1)
                        payload += ', "loudness" : {0}'.format(message[21])
                        payload += '}'
                        self.publish(topic, payload, False)

                    if message[14] == 3:
                        # this is the GetBalance respoonse
                        # update zone data
                        controller_id = message[1]
                        zone_id = message[2]
                        self.zone[controller_id][zone_id].balance = message[21]
                        # publish zone
                        self.publish('/rnet/zone/{0}/{1}'.format(controller_id, zone_id), self.zone[controller_id][zone_id].zone_json, False)

                        # publish the raw result
                        topic = '/rnet/response'
                        payload = '{'
                        payload += '"rnet_controller" : {0}'.format(message[4])
                        payload += ', "rnet_zone" : {0}'.format(message[12])
                        payload += ', "ha_zone" : {0}'.format((message[4] * 6) + message[12] + 1)
                        payload += ', "balance" : {0}'.format(message[21] - 10)
                        payload += '}'
                        self.publish(topic, payload, False)

                    if message[14] == 5:
                        # this is the GetBackgroundColour respoonse
                        topic = '/rnet/response'
                        payload = '{'
                        payload += '"rnet_controller" : {0}'.format(message[4])
                        payload += ', "rnet_zone" : {0}'.format(message[12])
                        payload += ', "ha_zone" : {0}'.format((message[4] * 6) + message[12] + 1)
                        resp = ["OFF", "AMBER", "GREEN"]
                        payload += ', "backgroundcolour" : "{0}"'.format(resp[message[21]])
                        payload += '}'
                        self.publish(topic, payload, False)

                    if message[14] == 6:
                        # this is the GetDoNotDisturbrespoonse
                        # update zone data
                        controller_id = message[1]
                        zone_id = message[2]
                        self.zone[controller_id][zone_id].donotdisturb = message[21]
                        # publish zone
                        self.publish('/rnet/zone/{0}/{1}'.format(controller_id, zone_id), self.zone[controller_id][zone_id].zone_json, False)

                        # publish the raw result
                        topic = '/rnet/response'
                        payload = '{'
                        payload += '"rnet_controller" : {0}'.format(message[4])
                        payload += ', "rnet_zone" : {0}'.format(message[12])
                        payload += ', "ha_zone" : {0}'.format((message[4] * 6) + message[12] + 1)
                        resp = ["OFF", "ON"]
                        payload += ', "donotdisturb" : "{0}"'.format(resp[message[21]])
                        payload += '}'
                        self.publish(topic, payload, False)

                    if message[14] == 7:
                        # this is the GetPartyMode respoonse
                        # update zone data
                        controller_id = message[1]
                        zone_id = message[2]
                        self.zone[controller_id][zone_id].partymode = message[21]
                        # publish zone
                        self.publish('/rnet/zone/{0}/{1}'.format(controller_id, zone_id), self.zone[controller_id][zone_id].zone_json, False)

                        # publish the raw result
                        topic = '/rnet/response'
                        payload = '{'
                        payload += '"rnet_controller" : {0}'.format(message[4])
                        payload += ', "rnet_zone" : {0}'.format(message[12])
                        payload += ', "ha_zone" : {0}'.format((message[4] * 6) + message[12] + 1)
                        resp = ["OFF", "ON", "MASTER"]
                        payload += ', "partymode" : {0}'.format(message[resp[21]])
                        payload += '}'
                        self.publish(topic, payload, False)

                else:
                    # dump message it's not needed
                    continue

And I have a loop that in essence goes:

            # Write to serial port
            # RNET IN
            # These are messages received from MQQT
            serialclient.serial_write()

            # Rceive from MQTT
            # MQTT IN
            # start the MQTT Loop this will check for messages and add them to the serial_q
            #logger.debug('MQTT loop start')
            mqttclient.client.loop(0.05)
            #logger.debug('MQTT loop end')

            # Send to MQTT
            # MQTT OUT
            # the filtered commands to go to Home Assistant stored in mqtt queue
            mqttclient.mqtt_send()

            # Read serial port
            # RNET OUT
            # these commands will be filtered, the ones to go to Home Assistant added to mqtt queue
            #logger.debug('RNET READ loop start')
            serialclient.serial_read()
            #logger.debug('RNET READ loop end')

From RNET, via this mqtt 2 serial component running on a dedicated Raspberry Pi 2B, I pass one of two json objects. One for the sources and one for the Russound zones.

In Home Assisstant I have a script aka Russound Button:

service: mqtt.publish
data:
  topic: /rnet/message
  payload_template: >-
    {"action": "{{ action }}", "controller_id": {{ controller_id }}, "zone_id":
    {{ zone_id }}, "parameter": {{ parameter }} }

So, for example, to turn on a zone, this would generate an mqtt message with: payload:

{  
  "action": "PowerOn",
  "controller_id": 1,
  "zone_id": 3,
  "parameter": 0
}

and returns a JSON object that looks like this:

{
  "name" : "Kitchen", 
  "controller_id" : 1, 
  "zone_id" : 3, 
  "ha_zone_id" : 10, 
  "availability" : "active", 
  "state" : "on", 
  "source_id" : 0, 
  "source" : "Tuner 1", 
  "ha_source_id" : "1", 
  "volume" : 60, 
  "bass" : 5, 
  "treble" : 0, 
  "loudness" : "on", 
  "balance" : 0, 
  "partymode" : "off", 
  "donotdisturb" : "off"
}

There’s a lot of duplication in the front end, so it’s not easy to maintain and it’s not real pretty but it’s functional. I think that about covers the key bits.

Finally here’s a screenshot from HA (I delibertately chose not to show values for Volume, Treble, etc bad decision?) , many apologies for the multiple posts,

this is all fantastic.

I’m very frustrated with the disconnects and restarts necessary to get the RIO components to work. Moving to an MQTT-based solution is extremely attractive. Thanks for sharing these bits, i think i have everything i need to get a migration done.