Monoprice Multizone Audio Controller

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.

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;
    }
}
1 Like

I have mine connected into my network using a serial to USB cable into a Raspberry Pi. I run ser2net. Since I already have several Raspberry Pis in my setup, the cable was a much cheaper integration than a Global Cache device. Glad to help with anyone trying to set it up this way.

1 Like

Thank you for a great plugin.

There is a typo in the plugin attribute.
It should be “Treble” instead of “Trebel” unless some country or someone use it that way.

also, in AudioZoneDetail. volume % displays double. like 50% %

No problem! Thanks for the tip. I updated the files in the post. The spelling error has been fixed & the double percent sign is now gone. I also noticed the mute button wasn’t configured correctly. That should work now too.

Great!
Thank You Bill

i think that volume up down button in details.xaml don’t work.
i looked up the xaml and no triggers were there.

i hope there will be mute/unmute indicator with mute icon in the tile, so user knows the status while a zone’s power on.

thanks in advance.

You are free to modify those XAML files. Those were put together rather quickly. They were just meant to be a some starter templates that you can build off of. You can definitely monitor the mute status.

Sure thing, I will post a modified file here once I learn how to do and get it done.

Thank you.

Edit : I figured out how to display “mute & unmuted” text in the tile. but I will make it displaying nothing or mute icon button displays when it’s muted, then trigger it to unmute in the tile. Wish me luck. :slight_smile:

Doesn’t that Mute ToggleSwitch I have in there change to the correct position when the state changes?

That’s why I didn’t have a Label in there. I though it’d be redundant. You can already see the state based on the position of the switch.

You can also see what’s be done in MediaPlayerDetails.xaml. The Mute button there toggles between 2 different icons with a standard Button control.

Mute toggle switch is working fine in the details.xaml, but I am referring to Tile.
User can see the power status in the tile, but mute status.
When a zone is powered on and muted, Tile displays power on, but there will be no audio.
even though you power off and on again, mute remains. this is how this amp acts.

thanks for the reference. i will look into it.

Hi, Community
Here are a modified version of the plugin, tile, and details.xaml.
This adds Mute status in the tile and a little design changes in details.xaml.

I am glad to have something to share with community.

Screenshots

MultizoneAudioController_hw.plugin (5.6 KB)
AudioZoneTile_hw.xaml (5.3 KB)
AudioZoneDetails_hw.xaml (14.8 KB)

1 Like

@hwj Hi, plugin works great. Trying to figure out how to customize the labels. I have the zones customized but don’t see a way to label the input sources to something more descriptive. Any ideas?

are you talking about input label in the tile?
I didn’t code it that further when I uploaded.
When I have a chance, I will reopen and check to see if I can do that.

@hwj yes, I was wanting to label the source from the system Streamer 1, Streamer 2, Alexa, HEOS, etc… There’s an option for selecting “input 1, input 2…” currently and I wanted to customize those labels.

ok, you are talking about input label in ZoneDetails, right?
then, you can just open AudioZoneDetails_hw.xaml file and change the label.

@hwj yes indeed, not sure why I didn’t see that before. Thank you!

Great!
It’s my pleasure.

Peace~