Nuvo Essentia Whole House Audio

The plugin communicates with the system’s controller using its serial connection. There are a number of DebugLog statements I retained for troubleshooting but commented out. 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. Very simple interface, but very effective at remotely controlling power, source and volume.

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