Comms with NuVo Essentia using IP2SL

I own an aging NuVo Essentia whole house audio system and a user of another forum indicated they were successful at using The Home Remote to control his system (power on/off of the individual zones and source control), but unfortunately provided no further details. I have a GC IP2SL connected to the controller’s RS232 port and I’ve been trying to gleen information from the YouTube videos on setting up the GC device and from other forum entries on how to program the plugin to work with no luck. I can’t even get the debug function to display an data (despite binding the log output to a label on the remote draft), so I’m guessing that the script I’ve entered isn’t even being executed. I’ve attached a link to the
Manual as a reference. I’m just trying to establish that I can communicate with the controller by turning one of the zones off and on, I feel like I can develop the rest of the code once I get a start. I’d attach the plugin I’m using, but I can’t as a new user.

I wrote a very simple plugin to help get you started. It only has the zone 1 power on/off commands implemented. Just import this into a new project & use the default templates to test. Don’t forget to add the NuvoEssentia device to the HomeGroup so you can test with the defaults.

Also, make sure you open the log so you can track what the plugin is doing. There’s a button to open the log on the bottom left corner of the main window.

NuvoEssentia.plugin (1.4 KB)

plugin.Name = "NuvoEssentia";
plugin.OnChangeRequest = onChangeRequest;
plugin.OnConnect = onConnect;
plugin.OnDisconnect = onDisconnect;
plugin.OnPoll = onPoll;
plugin.OnSynchronizeDevices = onSynchronizeDevices;
plugin.PollingInterval = -1;
plugin.DefaultSettings = { "Host": "192.168.1.100" };

var socket = new TCPClient();

var commands = {
    "Zone1PowerOn": "*Z1ON",
    "Zone1PowerOff": "*Z1OFF",
}

function onChangeRequest(device, attribute, value) {
    if (attribute != "MediaCommand")
        throw "Invalid attribute";

    var command = commands[value];
    if (!command)
        throw "Invalid command";

    socket.send(command);
    console.log("sent command : " + command);
}

function onConnect() {
    console.log("connecting");

    var nuvoDevice = plugin.Devices["1"];
    if (nuvoDevice != null) {
        nuvoDevice.SupportedMediaCommands = Object.keys(commands);
    }

    socket.connect(plugin.Settings["Host"], 4999);

    console.log("connected");
}

function onDisconnect() {
    console.log("disconnected");
}

function onPoll() {
    console.log("polling");
}

function onSynchronizeDevices() {
    var nuvoDevice = new Device();
    nuvoDevice.Id = "1";
    nuvoDevice.DisplayName = "Nuvo Essentia";
    nuvoDevice.Capabilities = ["MediaControl"];
    nuvoDevice.Attributes = [];
    plugin.Devices[nuvoDevice.Id] = nuvoDevice;
}

Thank you so much for the assistance, Bill. I followed your directions importing the plugin and ran it. No effect on the controller, and I checked the log to see that the commands were being sent. After realizing how to open the log, I tried my original code with the same results. So the validation that I wasn’t crazy was nice. :slight_smile:

Good news though - I ended up getting the code to work though. after a little trial and error. Section 4 of the manual states "Each Command string is STARTED with an ASCII “*” character and terminated by a < CR > and with this being my first foray into this kind of communication, so I’d been literally following my code with < CR>. In my research, I’d seen people use \n and \r and after trying them out - success.

Thanks so much for taking the time to get me started.

Can’t believe I forgot to include that. Yeah, pretty much all serial commands are usually terminated with a carriage return. The socket.send command should look like this:

socket.send(command + "\r");

Yes, my plugin captures that code, thank you.

One last question to permit me to forge ahead. I’ve tweeked the plugin to parse the controller response when a change occurs to identify the affected zone and the status of power. I’d like to update the remote display based on the response, but I don’t know how to create a binding from the plugin so I can set data triggers for it.

Nevermind. I created added a new Attribute to my NuVo device named Zone so I can now populate that from the script and use it for triggers.

1 Like

Another suggestion would be to consider creating a separate Device object for each zone. Kind of like I did in this Monoprice plugin.

MonopriceMultizoneController.hrp (81.4 KB)

I appreciate the suggestion. Always nice to see alternative ways of doing things. For now, I’ve got what I need keeping it as one device since the type of control I’m looking for across all 6 zones is actually pretty limited.

I have a question though. I have the On Connect function send a message to each zone to get a status to update the display, but anytime the system is changed from anywhere, a message is sent out with the status of that zone. So if someone turns a zone on using the physical control pad, currently the app doesn’t see the message because it isn’t look for it. Is there a way to look for these asynchronous messages from the controller? Or is there a way to run code on an interval to get zone status?

Enable polling. Set the PollingInterval to a value greater than zero. A lot of people get confused about how polling works. If the interval is greater than zero it calls onPoll, waits for number of milliseconds specified by PollingInterval, then repeats the loop. Think of that interval more as a “Delay”. So if your PollingInterval is 250 ms it isn’t guaranteed that onPoll will be called every 250 ms. All receive functions are blocking so your onPoll may only run when something is received. Review that plugin I shared with you earlier, it monitors for status changes so the device is always in sync, regardless of where the change is made.

Awesome. I tried that, but forgot about the PollingInternal setting. I figured it would be something easy.

Bill, I’ve got everything working but there is one lingering issue I cannot seem to resolve.

I have individual volume fields bound to Zone#Vol capabilities for the device. When the zone is off, there is no volume, so the fields are set to “” in the plugin. Once the zone is turned on, the volume level is received from the Essentia and set to the zone’s volume capability.These fields work as expected in the simulator, but when loaded to the app, they appear blank (see photo). If I switch to a different page in the app and then switch back, the field displays properly and will continue to display if I toggle the zone power. Any thoughts about why this is happening?

When it is off, set it to 0 instead of an empty string.

Can you please share your code for the nuvo? Would be great to save hours coding and jump right in.

Thanks!

{edited to strike post, see better formatting of message later in thread}

Thanks for sharing this! Would you mind also posting it in the Plugins section?

When embedding code in the post, you can use the 3 back-ticks “```” at the start & end of the code. That way it’s a little easier to read.

Much better:)

Looks like you might have 3 extra ones at the bottom.

Here’s my script, and a screen shot of the script’s flow. There are a number of DebugLog statements I commented out. You might find these useful when getting the code running on your end. The updateDisplay function is assigning variable values to attributes I added in the Nuvo device Attributes field. These attributes are bound to data triggers for the app controls to change colors based on power status, update labels for current volume level and update the zone’s currently selected source. I haven’t had to modify the script since June, so I may be a bit rusty, but I’m happy to answer questions to the best of my ability. Good luck!

plugin.OnChangeRequest = onChangeRequest;
plugin.OnConnect = onConnect;
plugin.OnDisconnect = onDisconnect;
plugin.OnPoll = onPoll;
plugin.OnSynchronizeDevices = onSynchronizeDevices;
plugin.PollingInterval = 500;
plugin.DefaultSettings = {
   "Host"  : "192.168.1.50",
   "Port"  : "4998",
};

var VERSION = "2020-06-22";
var debug = true;
var LOG_MAX = 5000;
var LOG_TRIM = 500;
var socket = new TCPClient();
var power = ["OFF", "OFF", "OFF", "OFF", "OFF", "OFF"]; //initial power values to detect when zone are
   										  		 //manually turned on at the control pads
var volume = [11, 16, 31, 16, 23, 40]; //preset volume values per zone

var mediaCommandMappings = {    
   "Zone1PowerOn"		: "*Z1ON",
   "Zone1PowerOff"		: "*Z1OFF",
   "Zone1Source+"		: "*Z1SRC+",
   "Zone1VolDown"		: "*Z1VOL-",
   "Zone1VolUp"			: "*Z1VOL+",
   "Zone2PowerOn"		: "*Z2ON",
   "Zone2PowerOff"		: "*Z2OFF",
   "Zone2Source+"		: "*Z2SRC+",
   "Zone2VolDown"		: "*Z2VOL-",
   "Zone2VolUp"			: "*Z2VOL+",
   "Zone3PowerOn"		: "*Z3ON",
   "Zone3PowerOff"		: "*Z3OFF",
   "Zone3Source+"		: "*Z3SRC+",
   "Zone3VolDown"		: "*Z3VOL-",
   "Zone3VolUp"			: "*Z3VOL+",
   "Zone4PowerOn"		: "*Z4ON",
   "Zone4PowerOff"		: "*Z4OFF",
   "Zone4Source+"		: "*Z4SRC+",
   "Zone4VolDown"		: "*Z4VOL-",
   "Zone4VolUp"			: "*Z4VOL+",
   "Zone5PowerOn"		: "*Z5ON",
   "Zone5PowerOff"		: "*Z5OFF",
   "Zone5Source+"		: "*Z5SRC+",
   "Zone5VolDown"		: "*Z5VOL-",
   "Zone5VolUp"			: "*Z5VOL+",
   "Zone6PowerOn"		: "*Z6ON",
   "Zone6PowerOff"		: "*Z6OFF",
   "Zone6Source+"		: "*Z6SRC+",
   "Zone6VolDown"		: "*Z6VOL-",
   "Zone6VolUp"			: "*Z6VOL+",
};

var inputSourceMappings = {
   "Input1"        : "ky src.1",
};

function onChangeRequest(device, attribute, value) {
   debugLog("onChangeRequest called...");
   switch(attribute) {
       case "MediaCommand":
       case "InputSource":

           var cmd = "";

           if (attribute == "MediaCommand") {
               cmd = mediaCommandMappings[value];
           } else if(attribute == "InputSource") {
               cmd = inputSourceMappings[value];
           }
           if (cmd) {
               debugLog("  sending command: " + cmd);
               socket.send(cmd + "\r");
               var response = onReceive (socket.receive());
               var last = cmd.slice(cmd.length-2, cmd.length);
               if(last == "ON") { //set initial source and volume when turning zone on
               	sleep(50);
               	response = setupInit(response[2]);
               }
               // debugLog("Stored setting = " + power[response[2] -1] + ", current = " + response.split(",")[1]);
   	  updateArray(response[2], response.split(",")[1]) //updates zone power in status array
               // debugLog("Stored setting = " + power[response[2] -1] + ", current = " + response.split(",")[1]);
               parseResponse(device, response);
               
           } else {
               debugLog("ERROR: unsupported command: " + value);
           }
           break;

       default:
           debugLog("ERROR: unsupported attribute type: " + attribute);
           break;
   }
}

function onConnect() {
   //debugLog("onConnect called...");
   var host = plugin.Settings["Host"];
   var port = plugin.Settings["Port"];

   var device = plugin.Devices[plugin.Name];
   if (device) {
      // debugLog("  setting supported media commands...");
       device.SupportedMediaCommands = Object.keys(mediaCommandMappings);
     // debugLog("  set...");

       //debugLog("  setting supported input sources...");
       device.SupportedInputSources = Object.keys(inputSourceMappings);
      // debugLog("  set...");
   }
   
   debugLog("  connecting on Host: " + host + ", Port: " + port);
   socket.connect(host, port);
   debugLog("  connected");
   socket.send("\r"); //wake up controller
   debugLog("Getting initial zone statuses...");
   var i;
   for (i=1; i < 7; i++) { //loop to get zone statuses
      sleep(100);
       socket.send("*Z" + i +"STATUS?\r");
       var response = onReceive(socket.receive());
       //debugLog("Updating array");
       updateArray(i, response.split(",")[1]); //updates zone power in status array
      // debugLog("calling Parse function");
       parseResponse(device, response);
   }
   debugLog(power[0] + " " + power[1] + " " + power[2] + " " + power[3] + " " + power[4] + " " + power[5]);
   debugLog("Status complete...");
}

function onReceive (response) {
   response = response.trim();
   return response;
}

function onDisconnect() {
   //debugLog("onDisconnect called...");
   socket.close();
   debugLog("  disconnected");
}

function onPoll() {
   //debugLog("onPoll called...");
  	var device = plugin.Devices[plugin.Name];
   var response = onReceive (socket.receive()); //detect asynchronous messages from control pad usage
      //debugLog("Stored setting is " + power[response[2] - 1] + ", Current is " + response.split(",")[1]);
      //debugLog(power[0] + " " + power[1] + " " + power[2] + " " + power[3] + " " + power[4] + " " + power[5]);
      if (updateArray(response[2], response.split(",")[1])) {
      	response = setupInit(response[2]);
      } 
      parseResponse(device, response);
}

function updateArray(zone, currentPwr) {
   if(power[zone - 1] == "ON" && currentPwr == "OFF") {
   	power[zone - 1] = "OFF";
   	//debugLog("Array OFF for Zone " + zone);
   	return false;
   } else if (power[zone - 1] == "OFF" && currentPwr == "ON") {
   	power[zone - 1] = "ON";
   	//debugLog("Array ON for Zone " + zone);
   	return true;
   } else {
   	//debugLog("No change to array" + (zone -1));
   	return false;
   }
}

function parseResponse(device, response) {
   //debugLog("Response loaded to parse is " + response);
   var lineParts = response.split(","); 
   if (lineParts[1] == "ON") { //Power is on for responding zone
   	var zvol; //determines volume values
   	if (lineParts[3].length < 5){
   		zvol = lineParts[3][3];
   	} else {
   		zvol = lineParts[3][3] + lineParts[3][4];
   	}
   	//debugLog("zone" + response[2] + ", pwr" +lineParts[1] + ", src" + lineParts[2][3] , 
   	//	zvol, lineParts[4][3], lineParts[5][4]);
   	updateDisplay(device, response[2], lineParts[1], lineParts[2][3] , 
   		zvol, lineParts[4][3], lineParts[5][4]);
   } else {
   	updateDisplay(device, response[2], lineParts[1], -1, -1, -1 ,-1);
   }
   //debugLog ("Done parsing response and updating display");
}

function updateDisplay(device, zone, pwr, src, vol, dnd, lock) {		
   //debugLog("Updating display...");
   if(pwr == "ON") { //Power is on for responding zone
   	var scaledVol = (vol-79) * -1.266;
   	//scaledVol = scaledVol.toPrecision(2);
   	scaledVol = Math.round(scaledVol);
   	switch(zone) {
   		case "1":
   			device.Zone1Power = pwr; 
   			device.Zone1Source = src;
   			device.Zone1Vol = scaledVol;
   			break;
   		case "2":
   			device.Zone2Power = pwr; 
   			device.Zone2Source = src;
   			device.Zone2Vol = scaledVol;
   			break;
   		case "3":
   			device.Zone3Power = pwr; 
   			device.Zone3Source = src;
   			device.Zone3Vol = scaledVol;
   			break;
   		case "4":
   			device.Zone4Power = pwr;
   			device.Zone4Source = src;
   			device.Zone4Vol = scaledVol;
   			break;
   		case "5":;
   			device.Zone5Power = pwr;
   			device.Zone5Source = src;
   			device.Zone5Vol = scaledVol;
   			break;
   		case "6":
   			device.Zone6Power = pwr;; 
   			device.Zone6Source = src;
   			device.Zone6Vol = scaledVol;
   			break;
   	}
   	
   } else {
   	//debugLog("Done updating display...");
   	switch(zone) {
   		case "1":
   			device.Zone1Power = pwr; 
   			device.Zone1Source = "";
   			device.Zone1Vol = 0;
   			break;
   		case "2":
   			device.Zone2Power = pwr; 
   			device.Zone2Source = "";
   			device.Zone2Vol = 0;
   			break;
   		case "3":
   			device.Zone3Power = pwr;  
   			device.Zone3Source = ""; 			
   			device.Zone3Vol = 0;
   			break;
   		case "4":
   			device.Zone4Power = pwr; 
   			device.Zone4Source = "";
   			device.Zone4Vol = 0;
   			break;
   		case "5":
   			device.Zone5Power = pwr; 
   			device.Zone5Source = "";
   			device.Zone5Vol = 0;
   			break;
   		case "6":
   			device.Zone6Power = pwr;  
   			device.Zone6Source = "";
   			device.Zone6Vol = 0;
   			break;
   	}
   }
}

function setupInit(zone) {
   socket.send("*Z" + zone +"SRC4\r"); //sets source 4 (Sonos)
   //debugLog("Source to 4");
   var response = onReceive(socket.receive());
   if (response[10] != 4) {
   	//debugLog("Response is " + response);
   	sleep(50);
   	setupInit(zone);
   }
   sleep(50);
   //debugLog("Volume to " + volume[zone - 1] + " for this zone");
   socket.send("*Z" + zone +"VOL" + volume[zone - 1] + "\r"); //sets volume at zone specific value
   response = onReceive(socket.receive());
   if (response[15] + response[16] != volume[zone - 1]) {
   	//debugLog("Response is " + response);
   	sleep(50);
   	setupInit(zone);
   }
   return response
}

function debugLog(s) {
   // Logs a string to both the console and the device Log attribute
   //  because the console is not available in the app.  It can also
   //  trim the device Log to keep it from growing too large if
   //  LOG_MAX is not 0.
   if(debug) {
       var device = plugin.Devices[plugin.Name];

       // is this the first time through?
       if(device && !device.Log) {
           // log is empty, so start it off with version info
           var verstr = plugin.Name + ": version " + VERSION;
           console.log(verstr);
           device.Log = verstr + "\n";
       }

       var logstr = plugin.Name + ": " + s;

       // send it to the console
       console.log(logstr);

       // send it to the device log
       if(device && device.Log) {
           device.Log += logstr + "\n";
          
           // trim the log if necessary
           if(LOG_MAX && (device.Log.length > LOG_MAX)) {
               device.Log = device.Log.slice(-(LOG_MAX + LOG_TRIM));
           }
       }
   }
}

function onSynchronizeDevices() {
   debugLog("onSynchronizeDevices called...");

   var device = new Device();

   device.Id = plugin.Name;
   device.DisplayName = "NuVo";
   device.Icon = "Speaker";
   device.Capabilities = [ "MediaControl", "MediaInputSource" ];
   device.Attributes = [ "Log", "Zone1Source", "Zone2Source", "Zone3Source", "Zone4Source",
   	"Zone5Source", "Zone6Source", "Zone1Power", "Zone2Power", "Zone3Power",
   	"Zone4Power", "Zone5Power", "Zone6Power", "Zone1Vol", "Zone2Vol", "Zone3Vol",
   	"Zone4Vol", "Zone5Vol", "Zone6Vol" ];

   plugin.Devices[plugin.Name] = device;

   debugLog("  done syncing");
   }
1 Like

Hi Sabrefox
Any chance you can share your hrp file to take a look.
I am trying to move my Nuvo over from iruleathome - server is finally coming down.
New to this app and would be helpful to look at detail configuration.
email is my username at yahoo.ca
thanks for the script and help

Of course! I’m happy to share it. Check your mail.