Legrand Nuvo multi-room audio support

I’ve had my Nuvo running as a serial connection for a couple of years now but have always thought it would be great if it could run on IP instead. I did some searching and found that you can connect to Port 5004 and issue a few commands before getting dumped with the message:
Error: Your license does not permit this unauthorized client.

I opened wireshark and watched the traffic and between the web nuvo client and the server found that it after it connects to port 5004 and issues a bunch of commands it then opens a connection to port 5006 and sends:

*autonomic

which then gives you:

Autonomic Controls NuVo Bridge version 3.0.16725.0 Release.
More info found on the Web http://www.Autonomic-Controls.com

Type ‘?’ for help or 'help ’ for help on .

where you can run command and follow events. I have had this connection open for about 20 minutes without being dropped where the connection to port 5004 always drops within 30 seconds.

I’m really hoping someone can run with this info and get an integration made. I’ll probably eventually get around to it if no one else makes any progress but I don’t have a ton of free time for a project like this.

edit: If you don’t send *autonomic you can send and receive the same commands as the serial interface.

I have a Nuvo system as well, add my vote for some good HA integration. Right now I have a Pi connected to it via usb to serial adapter. The Pi runs Node Red, and there is a node for serial connections. There is a good reference for all the serial based commands. So I wrote a pretty simple flow(s) that lets me turn zones on/off, set the volume, and pick the source. You can build an interface right there Node Red. It’s pretty clunky… We really need a good media_player integration. I just don’t have the programming skills.

Joe.

I just started using Home Assistant today for the first time so my experience is pretty limited to the entire project and the community. I’ve been able to connect a surprising number of things already in just a few hours. One of the big ones would be my older Nuvo system. I happened to find this: https://github.com/ejonesnospam/hass.media_player.nuvo/ which is exactly what we need!

@ejonesnospam seems to be on here so maybe he could comment on the future of the project. I can understand why there was a number of problems, both technical and non technical, getting it merged in earlier in the year from looking at things, but it would be nice if it could still be used to the benefit of the entire community.

I have an older Essentia D that for whatever reason, refuses to respond to *ZxxSTATUS but does respond to *ZxxCONSR which is documented in an older NV-A4D protocol document I found. Maybe newer Nuvo products still will accept that command as well. It does the same thing as the STATUS command it looks like.

I was able to get the nuvo integration installed that I mentioned above into an older version of HA. I had to make a few changes to make the Simplese protocol work and for it to work with my system but it works great! I hope to see this project continue!! It is certainly a very much needed project and I am extremely appreciative of the work done so far! For the time being I’m going to work on it and attempt, as I understand HA more, to make it work with newer versions and address a few issues I found. That’s all I know to do at this point.

How does one add this integration? I re-did my RPI to run HA instead of node-red, so I am ready to test.

I had to downgrade my new install to 0.92.2 to get it to work. It might work with newer releases, but certainly not the latest. I think after that version some things changed in how the integrations were packaged. I haven’t had time yet to look into exactly what.

If you’re still running an older release though, it’s not too bad but you need to do a little work.
Go to your base homeassistant directory and install pynuvo and make a directory ./lib/python3.7/site-packages/homeassistant/components/nuvo for the other files. This will be wherever you have HA installed. I think for the Pi it’s at /srv/homeassistant but I did my install manually on a Debian server so I’m not sure.

You’ll also need to download nuvo.py at https://github.com/ejonesnospam/hass.media_player.nuvo and transfer it over to your Pi. There is a text file as well that shows you what changes to make to your configuration.yaml file to make it work.

It should go something like this I guess though:

sudo -u homeassistant -H -s
cd /srv/homeassistant
python3 -m venv .
source bin/activate

You should now see a prompt like:
(homeassistant) homeassistant@raspberrypi:/srv/homeassistant $

Then run:
python3 -m pip install pynuvo
mkdir ./lib/python3.7/site-packages/homeassistant/components/nuvo
cp <nuvo.py you downloaded> ./lib/python3.7/site-packages/homeassistant/components/nuvo/media_player.py

You also need an init.py but you can just copy one from something else for the time being, like:
cp ./lib/python3.7/site-packages/homeassistant/components/ziggo_mediabox_xl/__init__.py ./lib/python3.7/site-packages/homeassistant/components

After you do this and edit your configuraton.yaml file you should be good to go. Don’t put the “model:” line in though.

I hope this works and I apologize in advance, I’m not the best at writing out directions, but I’ll try to help if I can.

Well that was a longer journey than I expected. Not your instructions, just getting an older version of HA installed and running.

So it looks like I have the integration installed and I can see the media player entities in the frontend. Nothing really works though. I am getting this in the logs (form all zones)

[pynuvo] Sending "Z01STATUS" 
[pynuvo] Expected response from command but no response before timeout

From my previous work, I know I need an * before each command. Looks like that might be missing. I’ll continue to research.

Since I am brand new to HA, I didn’t have to worry about losing much, but yes, it was a pain for me to downgrade too. Do you know if your Nuvo responds with #Z0xPWRppp,SRCs,GRPt,VOL-yy? The original code will not work if yours responds with a GRP. I can send you one that I fixed that in, and a few other little things. It still needs a lot of work before I’ll maybe submit a pull request. My system must be too old to even respond to the STATUS command. I finally found another command CONSR which does the same thing. I think all Essentia and Simplese models have the GRP.

I have a NV-I8GM, says Concerto on the front. When I issue *Z1STATUS?, I get this

#Z1,ON,SRC1,VOL60,DND0,LOCK0

So sounds like we have different systems.

When I have HA running and a screen running, all I see is #? on the screen output. Like it’s not recognizing the command HA is sending. Any way to see the string HA is sending to the serial port?

Yes, yours is certainly different. I’ll send you a nuvo.py to try in a few minutes. I’ve enabled all the logging as well so we can see what is happening. One of the main issues that remain is the original script had no zone checking and I havent fixed that yet, so if you’re pushing buttons on a keypad it can misinturpret the zone. and say one is on that really isn’t. It’ll fix itself in a few seconds though.

I forgot to ask, would you care to do *Z1CONSR and see what happens?

That just gives me a ? at the console. So it doesn’t understand that.

Thanks! I found the documentation to the protocol yours uses. There’s several differences in the way the volume and so forth is handled as well, so it’s not going to be as quick as I first thought. Unfortunately I’m working on a big project right now but I’d be happy to see if I can put something together in the next couple of days.

Yeah no worries. I am just poking around since I really don’t know python.

All I have found so far is this

def _format_zone_status_request(zone: int) -> str:
     return 'Z{:0=2}STATUS'.format(zone)

should be

def _format_zone_status_request(zone: int) -> str:
    return 'Z{:0=2}STATUS?'.format(zone)

Now it actually returns the zone status but I think it’s timing out with the response.

edit: The baud rate in pynuvo was wrong too. It was set to 9600, while my amp is 57600.

edit 2: Turning my logging up to debug gives me this:

2019-11-02 16:41:39 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-02 16:41:39 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-02 16:41:39 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-02 16:41:39 DEBUG (SyncWorker_9) [pynuvo] Received: b'#Z5,OFF'
2019-11-02 16:41:39 DEBUG (SyncWorker_9) [pynuvo] Zone Status Request - Response Invalid - Retry Count: 2
2019-11-02 16:41:40 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-02 16:41:40 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - No Data received
2019-11-02 16:41:40 INFO (SyncWorker_9) [pynuvo] Sending "Z05STATUS?"
2019-11-02 16:41:40 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-02 16:41:40 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-02 16:41:40 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-02 16:41:40 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-02 16:41:40 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-02 16:41:40 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-02 16:41:40 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-02 16:41:40 DEBUG (SyncWorker_9) [pynuvo] Received: b'#Z5,OFF'
2019-11-02 16:41:40 DEBUG (SyncWorker_9) [pynuvo] Zone Status Request - Response Invalid - Retry Count: 3
2019-11-02 16:41:41 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-02 16:41:41 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - No Data received
2019-11-02 16:41:41 INFO (SyncWorker_9) [pynuvo] Sending "Z05STATUS?"
2019-11-02 16:41:41 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-02 16:41:41 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-02 16:41:41 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-02 16:41:41 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-02 16:41:41 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-02 16:41:41 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-02 16:41:41 DEBUG (SyncWorker_9) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-02 16:41:41 DEBUG (SyncWorker_9) [pynuvo] Received: b'#Z5,OFF'

So something is adding a “b” into the string…

I’m far from an expert in Python myself! I’ve learned the b is fine, that in Python just means a byte literal follows… You’re going to have to change the pattern to make it work. I had to do some changes to get it to work with mine as it would not work with the GRP mine was sending and would also freak out when I had a zone muted.

The two big changes for yours is that the volume it sends and expects is 0 to 79 or MUTE, instead of MT. The others send that out as a negative value, that is 0 is the loudest and -78 is as low as it’ll go. the code expects to see a negative value then uses a float to convert that from a 0 to 1 range for HA. so like -40, midway, would be like 0.5 to HA.

So you’re going to need something like this to start… NEW_CONCERTO_PWR_ON_PATTERN = re.compile('Z(?P<zone>\d)'
'ON,'
'SRC(?P<source>\d),'
'VOL(?P<volume>\d\d|MUTE),'
'DND(?P<dnd>\d),'
'LOCK(?P<lock>\d')

The other difference is that the other models send the same thing when they are off, but yours only sends #Z1, OFF so that will need to be matched up with a new pattern. The protocol yours uses seems to be the one at http://smarthomebus.com/dealers/Protocols/NUVO%20Protocol.pdf

I’ve got a car in my garage with manifolds and fuel rails, etc off that I need to get drivable this weekend or I’d have more time to spend on this right now haha. That’s at least a start of what needs to happen if you want to play around with it. If you can get the pattern to match, there’s still some more work to be done but it’s not all that bad.

Got some pattern matching figured out. Also I get a <CR><LF> at the end of ever response (original code only has <CR>. On to the next bit.

2019-11-03 00:44:37 INFO (SyncWorker_18) [pynuvo] Sending "Z01STATUS?"
2019-11-03 00:44:37 DEBUG (SyncWorker_18) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-03 00:44:37 DEBUG (SyncWorker_18) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-03 00:44:37 DEBUG (SyncWorker_18) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-03 00:44:37 DEBUG (SyncWorker_18) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-03 00:44:37 DEBUG (SyncWorker_18) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-03 00:44:37 DEBUG (SyncWorker_18) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-03 00:44:37 DEBUG (SyncWorker_18) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-03 00:44:37 DEBUG (SyncWorker_18) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-03 00:44:37 DEBUG (SyncWorker_18) [pynuvo] Received buffer: b'#Z1,OFF\r\n'
2019-11-03 00:44:37 DEBUG (SyncWorker_18) [pynuvo] Received: b'#Z1,OFF'
2019-11-03 00:44:37 DEBUG (SyncWorker_18) [pynuvo] CONCERTO_PWR_OFF_PATTERN - Match
2019-11-03 00:44:37 DEBUG (SyncWorker_18) [pynuvo] CONCERTO_PWR_OFF_PATTERN - Match
2019-11-03 00:44:37 DEBUG (SyncWorker_18) [pynuvo] Zone Status Request - Response Invalid - Retry Count: 2
2019-11-03 00:44:38 DEBUG (SyncWorker_18) [pynuvo] Expecting response from command sent - No Data received

Thanks for helping out, and I understand you probably have better things to do.

I’d love to be able to help do what I can so all Nuvo users could use this one day.

I’ve really done next to nothing in python though, but I’m trying…

Try out this one at https://drive.google.com/open?id=1HVUSMr84OBrD6Nk8N9pZ0EM-cCAMuSEJ

It is not going to work if a zone is powered off, I don’t know how to do that part yet, but I think it will detect if a zone is on, get the volume, and let you set the volume and source, and also mute it.

We are both on the same track for the regex. I had just accounted for the extra carriage return. Where my code keeps failing is with the ZoneStatus class. I don’t really understand what this part is suppose to do, and it always ends up returning “None”:

    def from_string(cls, string: bytes):
        if not string:
            return None

        match = _parse_response(string)
   
        if not match:
            return None

        try:
           rtn = ZoneStatus(*[str(m) for m in match.groups()])
        except:
           rtn = None
        return rtn

edit:

some logs:

`2019-11-06 20:57:06 INFO (SyncWorker_2) [pynuvo] Sending "Z02STATUS?"
2019-11-06 20:57:06 DEBUG (SyncWorker_2) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-06 20:57:06 DEBUG (SyncWorker_2) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-06 20:57:06 DEBUG (SyncWorker_2) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-06 20:57:06 DEBUG (SyncWorker_2) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-06 20:57:06 DEBUG (SyncWorker_2) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-06 20:57:06 DEBUG (SyncWorker_2) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-06 20:57:06 DEBUG (SyncWorker_2) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-06 20:57:06 DEBUG (SyncWorker_2) [pynuvo] Expecting response from command sent - Data received but no EOL yet :(
2019-11-06 20:57:06 DEBUG (SyncWorker_2) [pynuvo] Received buffer: b'#Z2,OFF\r\n'
2019-11-06 20:57:06 DEBUG (SyncWorker_2) [pynuvo] Received: b'#Z2,OFF'
2019-11-06 20:57:06 DEBUG (SyncWorker_2) [pynuvo] CONCERTO_PWR_OFF_PATTERN - Match - <re.Match object; span=(2, 9), match='#Z2,OFF'>
2019-11-06 20:57:06 DEBUG (SyncWorker_2) [pynuvo] Request return: b'#Z2,OFF'
2019-11-06 20:57:06 DEBUG (SyncWorker_2) [pynuvo] string passed to ZoneStatus.from_string - b'#Z2,OFF'
2019-11-06 20:57:06 DEBUG (SyncWorker_2) [pynuvo] CONCERTO_PWR_OFF_PATTERN - Match - <re.Match object; span=(2, 9), match='#Z2,OFF'>
2019-11-06 20:57:06 DEBUG (SyncWorker_2) [pynuvo] match.groups =- ('2', 'OFF')
2019-11-06 20:57:06 DEBUG (SyncWorker_2) [pynuvo] ZoneStatus rtn - None
2019-11-06 20:57:06 DEBUG (SyncWorker_2) [pynuvo] zone_status rtn: None
2019-11-06 20:57:06 DEBUG (SyncWorker_2) [pynuvo] Zone Status Request - Response Invalid - Retry Count: 1
2019-11-06 20:57:07 DEBUG (SyncWorker_2) [pynuvo] Expecting response from command sent - No Data received
`

I’ve learned in getting mine to work is that the ZoneClass must have the same variables that the regex pattern has assigned to it. That’s easy on the older Nuvos, since they report the same thing, except OFF vs ON, but on the newer ones they don’t return anything else when turned off.

I plan on looking at this some more this week and seeing if another ZoneClass or something can be defined depending on the pattern or something, but I’m not sure, I just don’t know enough about python at the moment.

Ideally it would be nice to send a VER command down and find out what the system actually is, and then define classes around that, as well as commands, as they are all a little different. Why in the world Nuvo couldn’t just stick to one format is beyond me. I can see them adding stuff to it of course, and new commands, but they just go and practically change the whole thing. Even the newer units don’t want a “0” in front of the zone like on the older ones, at least that’s what the protocol docs say anyway.

@ejonesnospam Any advice you would care to lend or let us know if you would like to see this project continue? I appreciate all of your work and would like to help by submitting pull requests or whatever else I might could do. You’ve started a nice addition that could be enjoyed by many!

So I think I have this working. I was able to init the variables in the class that were missing, this took care of it failing when a zone is off. Then I had to redo all the math in nuvo/media_player.py for the volume.

Now I need to get this instance of HA connected to my main instance of HA. That should be fun.