Attached is a Plugin for a Multizone Audio Controller. This was originally developed for the Monoprice 6 Zone controller but there several other controllers this plugin should be compatible with. The following products all appear to use the same control protocol.
-
Monoprice 6 Zone Home Audio Multizone Controller.
https://www.monoprice.com/product?p_id=10761 -
Factor X-66-AMP
https://www.mcssl.com/store/factorelectronics/001v-66 -
McLELLAND MAP-1200
https://www.mclellandmusic.com/productdetail/162 -
WaveSound WMR66
http://wavesound.com.br/produto/wmr66-multiroom-6-zonas-6-fontes/ -
WaveSound WMR46
http://wavesound.com.br/produto/wmr46-multiroom-4-zonas-6-fontes/
This setup also requires an IP to Serial converter so it can be controlled over a standard network connection. You can use a product like the Global Cache IP2SL.
https://www.globalcache.com/products/itach/ip2slspecs
To install the plugin, use the Plugin Import utility in the Designer. The import process will ask you for the IP Address & Port of the IP to Serial converter. The default port for Global Cache serial converters is port 4999. It will also ask you for the number of zones, default is 6.
Here are the custom XAML templates that were built for the plugin:
AudioZoneTile.xaml (3.1 KB)
AudioZoneDetails.xaml (10.8 KB)
Here is the plugin code:
MultizoneAudioController.plugin (5.1 KB)
plugin.Name = "MultizoneAudioController";
plugin.OnChangeRequest = onChangeRequest;
plugin.OnConnect = onConnect;
plugin.OnDisconnect = onDisconnect;
plugin.OnPoll = onPoll;
plugin.OnSynchronizeDevices = onSynchronizeDevices;
plugin.PollingInterval = 250;
plugin.DefaultSettings = { "Host": "192.168.1.100", "Port": "4999", "Zones": "6" };
var socket = new TCPClient();
var unprocessedData = "";
var rawVolume;
function onChangeRequest(device, attribute, value) {
var cmd = null;
switch (attribute) {
case "MediaCommand":
if (value == "VolumeUp") {
cmd = "VO" + (rawVolume + 1).toString().padStart(2, '0');
}
else if (value == "VolumeDown") {
cmd = "VO" + (rawVolume - 1).toString().padStart(2, '0');
}
else {
throw "not implemented";
}
break;
case "InputSource":
cmd = "CH" + parseInt(value).toString().padStart(2, '0');
break;
case "Mute":
cmd = "MU" + ((value == "Muted") ? "01" : "00");
break;
case "Switch":
cmd = "PR" + ((value == "On") ? "01" : "00");
break;
case "Volume":
cmd = "VO" + parseInt((value / 100) * 38).toString().padStart(2, '0');
break;
case "Treble":
cmd = "TR" + parseInt((value)).toString().padStart(2, '0');
break;
case "Bass":
cmd = "BS" + parseInt((value)).toString().padStart(2, '0');
break;
case "Balance":
cmd = "BL" + parseInt((value)).toString().padStart(2, '0');
break;
default:
throw "Not implemented!";
}
socket.send("<" + device.Id + cmd + "\r");
}
function onConnect() {
socket.connect(plugin.Settings["Host"], parseInt(plugin.Settings["Port"]));
socket.send("?10PR\r");
socket.send("?10MU\r");
socket.send("?10VO\r");
socket.send("?10CH\r");
socket.send("?10TR\r");
socket.send("?10BS\r");
socket.send("?10BL\r");
}
function onDisconnect() {
socket.close();
}
function onPoll() {
var options = { timeout: -1 };
var data = socket.receive(options);
data = unprocessedData + data;
unprocessedData = "";
if (data.length > 0) {
var lines = data.split("\r\n");
for (var l in lines) {
var line = lines[l];
//The last line should always be '#'. If it isn't we know it's a partial line that needs to be included in next polling loop.
if (l == (lines.length - 1) && line != "#") {
unprocessedData = line;
return;
}
console.log(line);
var item = line;
if (line.length >= 7) {
var item = (line[0] == '#') ? line.substring(1) : line;
var zone = item.substring(1, 3);
var cmd = item.substring(3, 5);
var value = item.substring(5, 7);
var device = plugin.Devices[zone];
if (device != null) {
switch (cmd) {
case "CH":
device.InputSource = parseInt(value);
break;
case "MU":
device.Mute = ((value == "01") ? "Muted" : "Unmuted");
break;
case "PR":
device.Switch = ((value == "01") ? "On" : "Off");
break;
case "VO":
rawVolume = parseInt(value);
device.Volume = parseInt((value / 38) * 100);
break;
case "TR":
device.Treble = parseInt((value));
break;
case "BS":
device.Bass = parseInt((value));
break;
case "BL":
device.Balance = parseInt((value));
break;
default:
return;
}
}
}
}
}
}
function onSynchronizeDevices() {
//Generate zones 1-6 of Main unit 1.
//First digit in Id is the unit number.
//Second digit in Id is the zone number.
var zoneCount = parseInt(plugin.Settings["Zones"]);
for (var i = 1; i <= zoneCount; i++) {
var device = new Device();
device.Id = "1" + i.toString();
device.DisplayName = "Zone " + i.toString();
device.Icon = "Speaker";
device.Capabilities = ["AudioMute", "AudioVolume", "MediaControl", "MediaInputSource", "Switch"];
device.Attributes = ["Treble", "Bass", "Balance"];
device.DeviceType = "AudioZone";
device.TileTemplate = "AudioZoneTile.xaml";
device.DetailsTemplate = "AudioZoneDetails.xaml";
plugin.Devices[device.Id] = device;
}
}