Here's the first cut of Anthem IP control pluging

I’m using this to control my AVM60, but the same interface is used for other products in Anthem’s range like the MRX series. As such, a few things might be hardcoded specific to the AVM60 (like 2 zones), but that could easily be modified as required.

plugin.Name = "AnthemAVMController";
plugin.OnChangeRequest = onChangeRequest;
plugin.OnConnect = onConnect;
plugin.OnDisconnect = onDisconnect;
plugin.OnPoll = onPoll;
plugin.OnSynchronizeDevices = onSynchronizeDevices;
plugin.PollingInterval = 1000;
plugin.DefaultSettings = { "Host": "10.1.1.1", "Port": "14999" };

var socket = new TCPClient();
var zoneCount = 2;

var unprocessedData = "";
var rawVolume;
var data = null;
var options = { timeout: 2000 };
var _readBuffer = "";
var _eol = ";";

function readMessage() {
    while (true) {
        var eolIndex = _readBuffer.indexOf(_eol);
        if (eolIndex != -1) {
            var message = _readBuffer.substring(0, eolIndex + 1);
            _readBuffer = _readBuffer.substring(eolIndex + 1);
            return message;
        }
        else {
            var data = socket.receive();
            if (!data) {
                throw "End of the stream";
            }
            _readBuffer = _readBuffer + data;
        }
    }
}

function onChangeRequest(device, attribute, value) {
    console.log("AVM onChangeRequest-" + attribute + ":" + value);
    var cmd = null;
    switch (attribute) {
        case "MediaCommand":
            if (value == "VolumeUp") {
                cmd = "Z" + device.Id + "VUP01"
            }
            else if (value == "VolumeDown") {
                cmd = "Z" + device.Id + "VDN01"
            }
            else {
                throw "not implemented";
            }
            break;
        case "InputSource":
            cmd = "Z" + device.Id + "INP" + parseInt(value).toString().padStart(2, '0');
            break;
        case "Mute":
            cmd = "Z" + device.Id + "MUT" + ((value == "Muted") ? "1" : "0");
            break;
        case "Switch":
            cmd = "Z" + device.Id + "POW" + ((value == "On") ? "1" : "0");
            break;
        case "Volume":
            var newvol = Math.round(((value - 100) * 90 / 100));
            cmd = "Z" + device.Id + "VOL" + newvol.toString();
            break;
        default:
            throw "Not implemented!";
    }
    console.log("command: " + cmd);
    cmd = cmd + ";";
    socket.send(cmd);
}

function onConnect() {
    console.log("AVM onConnect");
    socket.connect(plugin.Settings["Host"], parseInt(plugin.Settings["Port"]));
    for (var i = 1; i <= zoneCount; i++) {
        var id = i.toString();
        socket.send("Z" + id + "POW?;");
        socket.send("Z" + id + "MUT?;");
        socket.send("Z" + id + "VOL?;");
        socket.send("Z" + id + "INP?;");
    }
}

function onDisconnect() {
    console.log("AVM onDisconnect");
    socket.close();
}

function onPoll() {
    var message = readMessage();
    console.log("Msg: " + message);
    if (message.length > 5 && message.charAt(0) == 'Z') {
        var deviceId = message.substring(1,2).toString();
        var messageId = message.substring(2, 5);
        var val1 = message.substring(5,6);
        var val2 = message.substring(5,7);
        var val3 = message.substring(5,8);
        var device = plugin.Devices[deviceId];
        switch (messageId) {
            case "VOL":
                var vol = parseInt(val3);
                var vol2 = 100 + (100 * vol / 90);
                device.Volume = vol2;
                console.log("Volume: " + device.Volume);
                break;
            case "POW":
                device.Switch = (val1 == "1" ? "On" : "Off");
                break;
            case "MUT":
                device.Mute = (val1 == "1" ? "Muted" : "Unmuted");
                break;
            case "INP":
                device.InputSource = parseInt(val2);
                break;
            default:
                console.log("unknown data read: " + message);
                break;
        }
    }
}

function onSynchronizeDevices() {
    console.log("AVM onSynchronizeDevices");
    //Generate zones 1 and 2
    //First digit in Id is the zone number.
    for (var i = 1; i <= zoneCount; i++) {
        var device = new Device();
        device.Id = i.toString();
        device.DisplayName = "AVM Zone " + i.toString();
        device.Icon = "Receiver";
        device.Capabilities = ["AudioMute", "AudioVolume", "MediaControl", "MediaInputSource", "Switch"];
        device.DeviceType = "AudioZone";
        device.TileTemplate = "AudioZoneTile.xaml";
        device.DetailsTemplate = "AudioZoneDetails.xaml";

        plugin.Devices[device.Id] = device;
    }
}
2 Likes

I’m about to pick up a pair of MRX-720’s. Happy to see this plugin!!

1 Like

Do you know, by any chance, how many tcp connections Anthem allows for IP control? Is it just one?

Honestly, I haven’t set it up yet and tried. I’m rebuilding my AV rack and I’m not sure if I should use my GC iTach RS232 (which allows 8 tcp connections) or just control it though IP.

It appears to be at least four concurrent sessions over TCP on my AVM60

I have a weird issue on my setup. I don’t know what is causing it. I’ve looked at the plugin and can’t see anything wrong with it, but often (usually when someone with THR installed returns from work), the whole system will shut off. I’ve disabled HDMI-CEC on my Apple TV unit, the TV itself. It’s super annoying and I can’t trace it down to a cause yet.

Thanks for the quick reply!

That’s interesting. Hopefully I will be setup by the weekend… I’ll let ya know if I see the same thing.

Did you figure out your power issue? I’ve had this plugin up and running for a week and no problems. I really appreciate the effort you put into this! Are you planning anymore updates? I may take a stab at adding more into the plugin but its going to be a learning process for me.
The only change I’ve made so far was change the PollingInterval to 250ms.

It’s been behaving itself recently. Nothing has changed. Weird.
I’m adding an AVM70 to my home system soon so I’m glad I’ve already got the plug-in done. I might add the capability to enable “night mode” for late night listening, but I’m not planning on adding anything else. I’d consider adding functionality if there’s anything you particularly want.

The one thing I miss that I don’t have with this is the audio input status… Dolby, DTS, etc… Not a big deal though.

Have you tried to bind the volume to a label? The value comes back with a lot a decimal places… ie 75.777777. Again not a big deal since I shortened the label to cut off what I don’t want. I hate to sound critical because I very much appreciate this.

Now that I got one of my MRXs up and running, time to get the second one installed which will be replacing an old Denon 3805 :sunglasses:

Hey guys,

Bought an AVM70 and started using your plugin (thanks…much appreciated!).
Control seems to work fine (ie Volume up/down), but feedback seems a bit odd.
I’ve setup the Volume slider and it does work, but it seems to grab the volume status (level), but after about 1 or 2 seconds the status disappears (ie I have a volume label displaying the volume that shows up as 61.1111, then disappears after a couple of seconds).

Any thoughts would be greatly appreciated…

Thanks

Figured it out…
Changed ‘var zoneCount = 2;’ to ‘var zoneCount = 1;’
I’m guessing it has something to do with Zone 2 being powered off and it timing out trying to get feedback from Zone 2.
Anyway, I’m in the same boat as you Jdamore…I used to have audio format feedback and would love to have it again, so I’m going to attempt to add it to the plugin.
Once I get it up and running I’ll post it here.

Thanks again Kryten67 for the first attempt…works great!

1 Like

I added a webbrowser to my page for the web UI… now I have quick access to all the tweaks I could imagine! Working well! To bring up the webbrowser, I put a button (above the Vol Up button) that makes the webbrowser IsVisible or not.

I’ve updated the plugin. It is more responsive, it reports volume as a whole number, and it now has “AudioMode” that can be set from 0 to 9 that represents the dolby volume leveler (from off to max) which is handy for late-night viewing.

plugin.Name = "AnthemAVMController";
plugin.OnChangeRequest = onChangeRequest;
plugin.OnConnect = onConnect;
plugin.OnDisconnect = onDisconnect;
plugin.OnPoll = onPoll;
plugin.OnSynchronizeDevices = onSynchronizeDevices;
// Note the polling interval can dynamically change 
plugin.PollingInterval = 1500;
plugin.DefaultSettings = { "Host": "10.1.1.10", "Port": "14999" };

var socket = new TCPClient();
var zoneCount = 1; // Change to 2 if you want to use both zones supported

var unprocessedData = "";
var rawVolume;
var data = null;
var _readBuffer = "";
var _eol = ";";

function readMessage() {
    
    for(var wait = 1; wait < 50; wait++) {
        if(socket.available > 0)
            break;
        sleep(1);
    }
    
    if(socket.available <= 0)
        return "";
    
    while (socket.available) {
        var eolIndex = _readBuffer.indexOf(_eol);
        if (eolIndex != -1) {
            var message = _readBuffer.substring(0, eolIndex + 1);
            if (eolIndex + 2 < _readBuffer.length)
                _readBuffer = _readBuffer.substring(eolIndex + 1);
            else
                _readBuffer = "";
            return message;
        }
        else {
            var data = socket.receive();
            if (!data) {
                console.log("AVM: end of stream reached");
                throw "End of the stream";
            }
            _readBuffer = _readBuffer + data;
        }
    }
}

function onChangeRequest(device, attribute, value) {
    console.log("AVM: onChangeRequest-" + attribute + " : " + value);
    var cmd = null;
    switch (attribute) {
        case "MediaCommand":
            if (value == "VolumeUp") {
                cmd = "Z" + device.Id + "VUP01;"
            }
            else if (value == "VolumeDown") {
                cmd = "Z" + device.Id + "VDN01;"
            }
            else {
                throw "MediaCommand value not implemented";
            }
            break;
        case "InputSource":
            cmd = "Z" + device.Id + "INP" + parseInt(value).toString().padStart(2, '0') + ";";
            break;
        case "Mute":
            cmd = "Z" + device.Id + "MUT" + ((value == "Muted") ? "1" : "0") + ";";
            break;
        case "Switch":
            var cmdPwr = "Z" + device.Id + "POW" + ((value == "On") ? "1" : "0") + ";";
            if (value == "On") 
                // send power-on twice, the first to wake up device from ECO mode (if enabled)
                cmd = cmdPwr + cmdPwr;
            else
                cmd = cmdPwr;
            break;
        case "Volume":
            var newvol = Math.round(((value - 100) * 90 / 100));
            cmd = "Z" + device.Id + "VOL" + newvol.toString() + ";";
            break;
        case "SoundMode":
            var val1 = String(value).substring(0,1);
            if(val1 < '0' || val1 > '9') {
                throw "AVM: AudioMode out of range (0-9)";
            }
            if (val1 == '0') {
                cmd = "SDVL000;SDVS000;"; // turn off dolby volume leveler
            }
            else {
                cmd = "SDVS001;SDVL00" + val1 + ";"; // turn on dolby volume leveler
            }
            
            break;
        default:
            throw "Not implemented!";
    }
    console.log("AVM: command: " + cmd);
    socket.send(cmd);
}

function onConnect() {
    console.log("AVM: onConnect");
    socket.connect(plugin.Settings["Host"], parseInt(plugin.Settings["Port"]));
    socket.send("SIP1;"); // enable standby IP control
}

function onDisconnect() {
    console.log("AVM: onDisconnect");
    socket.close();
}

function onPoll() {
    for (var i = 1; i <= zoneCount; i++) {
        var id = i.toString();
        socket.send("Z" + id + "POW?;");
    }

    var message = null;
    
    while(true) {        
        try {
            message = readMessage();
        }
        catch(err)
        {
            console.log("AVM: Exception during readMessage()");
        }
        
        if (!message || message == "") {
            break;
        }
        
        if(message.substring(0,2) == "!E") {
            console.log("AVM: Processor signalled error: " + message);
            continue; // get another message if available
        }
        
        console.log("Msg: " + message);
        if (message.length <= 5)
            continue;
        
        switch(message.charAt(0)) {
            case 'Z':
                var id = message.substring(1,2).toString();
                var messageId = message.substring(2, 5);
                var val1 = message.substring(5,6);
                var val2 = message.substring(5,7);
                var val3 = message.substring(5,8);
                var device = plugin.Devices[id];
                switch (messageId) {
                    case "VOL":
                        var vol = parseInt(val3);
                        var vol2 = Math.round(100 + (100 * vol / 90));
                        device.Volume = vol2;
                        console.log("AVM: Volume: " + device.Volume.toString());
                        break;
                    case "POW":
                        device.Switch = (val1 == "1" ? "On" : "Off");
                        if (device.Switch == "On") {
                            // If this device is on, let's query the mute status,
                            // volume and input selection
                            plugin.PollingInterval = 200;
                            socket.send("Z" + id + "MUT?;");
                            socket.send("Z" + id + "VOL?;");
                            socket.send("Z" + id + "INP?;");
                            socket.send("SDVL00?;");
                        }
                        else
                        {
                            plugin.PollingInterval = 1500;
                        }
                        break;
                    case "MUT":
                        device.Mute = (val1 == "1" ? "Muted" : "Unmuted");
                        break;
                    case "INP":
                        device.InputSource = parseInt(val2).toString();
                        break;
                    default:
                        console.log("AVM: unknown Z data read: " + message);
                        break;
                }
                break;
            case 'S':
                var messageId = message.substring(1, 4);
                // "S" commands operate on the primary zone (device)
                var device = plugin.Devices[1];
                switch (messageId) {
                    case "DVL":
                        device.SoundMode = message.substring(6, 7);
                        console.log("Sound mode: " + device.SoundMode.toString());
                        break;
                    case "DVS":
                        break;
                    default:
                        console.log("AVM: unknown S data read: " + message);
                        break;
                }
                break;
        }
    }
}

function onSynchronizeDevices() {
    console.log("AVM: onSynchronizeDevices");
    //Generate zones
    //First digit in Id is the zone number.
    for (var i = 1; i <= zoneCount; i++) {
        var device = new Device();
        device.Id = i.toString();
        device.DisplayName = "AVM Zone " + i.toString();
        device.Icon = "Receiver";
        device.Capabilities = ["AudioMute", "AudioVolume", "MediaControl", "MediaInputSource", "Switch", "AudioSoundMode"];
        device.SupportedSoundModes = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9" ];
        device.DeviceType = "AudioZone";
        device.TileTemplate = "AudioZoneTile.xaml";
        device.DetailsTemplate = "AudioZoneDetails.xaml";

        plugin.Devices[device.Id] = device;
    }
}
1 Like

You are awesome! I’ll load this up tomorrow.

Thanks for this. Here’s my tweaked version. I’m very new to Home Remote so there may -not- be some ‘best practice’ stuff here…!

The changes are:-

  • Volume in Decibels
  • Addition of a MuteToggle Command
  • Generation of status text which can be bound to a label. This gives audio format, bitrate and video mode. This is returned in the ‘SoundMode’
plugin.Name = "AnthemAVMController";
plugin.OnChangeRequest = onChangeRequest;
plugin.OnConnect = onConnect;
plugin.OnDisconnect = onDisconnect;
plugin.OnPoll = onPoll;
plugin.OnSynchronizeDevices = onSynchronizeDevices;
plugin.PollingInterval = 0;
plugin.DefaultSettings = { "Host": "10.1.1.1", "Port": "14999" };

var socket = new TCPClient();
var zoneCount = 2;

var unprocessedData = "";
var rawVolume;
var data = null;
var options = { timeout: 2000 };
var _readBuffer = "";
var _eol = ";";

var AIN= "";
var AIR = "";
var VIR = "";
var AIC = "";

function readMessage() {
    while (true) {
        var eolIndex = _readBuffer.indexOf(_eol);
        if (eolIndex != -1) {
            var message = _readBuffer.substring(0, eolIndex + 1);
            _readBuffer = _readBuffer.substring(eolIndex + 1);
            return message;
        }
        else {
            var data = socket.receive();
            if (!data) {
                throw "End of the stream";
            }
            _readBuffer = _readBuffer + data;
        }
    }
}

function onChangeRequest(device, attribute, value) {
    console.log("AVM onChangeRequest-" + attribute + ":" + value);
    var cmd = null;
    switch (attribute) {
        case "MediaCommand":
            if (value == "VolumeUp") {
                cmd = "Z" + device.Id + "VUP01"
            }
            else if (value == "VolumeDown") {
                cmd = "Z" + device.Id + "VDN01"
            }
            else if (value == "MuteToggle") {
                cmd = "Z" + device.Id + "SIM0027"
            }
            else {
                throw "not implemented";
            }
            break;
        case "InputSource":
            cmd = "Z" + device.Id + "INP" + parseInt(value).toString().padStart(2, '0');
            break;
        case "Mute":
            cmd = "Z" + device.Id + "MUT" + ((value == "Muted") ? "1" : "0");
            break;
        case "Switch":
            cmd = "Z" + device.Id + "POW" + ((value == "On") ? "1" : "0");
            break;
        case "VolumeDecibel":
            cmd = "Z" + device.Id + "VOL" + value.toString();
            break;
        default:
            throw "Not implemented!";
    }
    console.log("command: " + cmd);
    cmd = cmd + ";";
    socket.send(cmd);
}

function onConnect() {
    console.log("AVM onConnect");
    socket.connect(plugin.Settings["Host"], parseInt(plugin.Settings["Port"]));
    for (var i = 1; i <= zoneCount; i++) {
        var id = i.toString();
        socket.send("Z" + id + "POW?;");
        socket.send("Z" + id + "MUT?;");
        socket.send("Z" + id + "VOL?;");
        socket.send("Z" + id + "AIR?;");
        socket.send("Z" + id + "AIN?;");
        socket.send("Z" + id + "AIC?;");
        socket.send("Z" + id + "VIR?;");
        socket.send("Z" + id + "INP?;");
    }
}

function onDisconnect() {
    console.log("AVM onDisconnect");
    socket.close();
}

function onPoll() {
    var dirtystatus = 0;
    var message = readMessage();
    console.log("Msg: " + message);
    if (message.length > 5 && message.charAt(0) == 'Z') {
        var deviceId = message.substring(1,2).toString();
        var messageId = message.substring(2, 5);
        var val1 = message.substring(5,6);
        var val2 = message.substring(5,7);
        var val3 = message.substring(5,8);
        var valfull = message.substring(5,message.length-1)
        var device = plugin.Devices[deviceId];
        switch (messageId) {
            case "VOL":
                var vol = parseInt(val3);
                device.VolumeDecibel = vol;
                console.log("Volume: " + device.Volume);
                break;
            case "AIR":
                AIR = valfull;
                console.log ("AIR: " + AIR);
                dirtystatus=1;
                break;
            case "AIN":
                AIN = valfull;
                console.log ("AIN: " + AIN);
                dirtystatus=1;
                break;
            case "AIC":
                switch (val1) {
                    case "1":
                       AIC = "Other";
                       break;
                    case "2":
                       AIC = "1.0";
                       break;
                    case "3":
                       AIC = "2.0";
                       break;
                    case "4":
                       AIC = "5.1";
                       break;
                    case "5":
                       AIC = "6.1";
                       break;
                    case "6":
                       AIC = "7.1";
                       break;
                    default:
                       AIC="";
                       break
                 }
                console.log ("AIC: " + AIC);
                dirtystatus=1;
                break;    
            case "VIR":
                switch (val1) {
                    case "2":
                       VIR = "1080p60";
                       break;
                    case "3":
                       VIR = "1080p50";
                       break;
                    case "4":
                       VIR = "1080p24";
                       break;
                    case "5":
                       VIR = "1080i60";
                       break;
                    case "6":
                       VIR = "1080i50";
                       break;
                    case "7":
                       VIR = "720p60";
                       break;
                    case "8":
                       VIR = "720p50";
                       break;
                    case "9":
                       VIR = "576p50";
                       break;
                    case "10":
                       VIR = "576i50";
                       break;
                    case "11":
                       VIR = "480p60";
                       break;
                    case "12":
                       VIR = "480i60";
                       break;
                    case "13":
                       VIR = "3D";
                       break;
                    case "14":
                       VIR = "4K";
                       break;
                    default:
                       VIR="Unknown";
                       break
                 }
                console.log ("VIR: " + VIR);
                dirtystatus=1;
                break;
            case "POW":
                device.Switch = (val1 == "1" ? "On" : "Off");
                break;
            case "MUT":
                device.Mute = (val1 == "1" ? "Muted" : "Unmuted");
                break;
            case "INP":
                device.InputSource = parseInt(val2);
                break;
            default:
                console.log("unknown data read: " + message);
                break;
        }
    }
    
    if (dirtystatus == 1) {
    	if (VIR == "Unknown") 
    	{
    		device.SoundMode = "No Input";
    	} else
    	{
    	    device.SoundMode = AIN + " " + AIR + " " + AIC + " (" + VIR + ")"
      }
    }
}

function onSynchronizeDevices() {
    console.log("AVM onSynchronizeDevices");
    //Generate zones 1 and 2
    //First digit in Id is the zone number.
    for (var i = 1; i <= zoneCount; i++) {
        var device = new Device();
        device.Id = i.toString();
        device.DisplayName = "AVM Zone " + i.toString();
        device.Icon = "Receiver";
        device.Capabilities = ["AudioMute", "AudioSoundMode", "AudioVolumeDecibel", "MediaControl", "MediaInputSource", "Switch"];
        device.DeviceType = "AudioZone";
        device.TileTemplate = "AudioZoneTile_hw.xaml";
        device.DetailsTemplate = "AudioZoneDetails_hw.xaml";

        plugin.Devices[device.Id] = device;
    }
}
2 Likes

Thanks for this plugin, although I don’t (yet?) have an Anthem. I was going to pick up a MRX 1140 8k to replace my Denon AVR-X4500H, but many are telling me to stay away from “boutique” brands.

Are you folks happy with the Anthems and what are the biggest selling points for you?

I love mine. ARC is brilliant - I still think it’s the best room correction system without going mad on stuff like Lyngdorf

User interface is basic but flexible.

The amplification is good.

1 Like

I don’t consider Anthem to be “boutique” at all. Sure, they’re low-volume when compared to a Sony or Denon, but they have excellent QA, great design, and good support. I’m on my third product of theirs and have loved their stuff.

1 Like

I have two MRX-740s in my rack and LOVE them. This plug in works great and I haven’t had any control issues in the last year. Like I mentioned above, adding a web browser to your Home Remote gives you all the control.

My previous Marantz and Denon receivers had more bells and whistles but, doesn’t compare in sound quality after running the room correction (and some manual tweaks). My biggest reason for upgrading was sound and to get the latest audio formats. For me, 8k is beyond a resolution my eye balls can distinguish so personally I’ll never pay you update my units. If for whatever reason I outgrow the MRX, I’ll stick with Anthem and move to the AVM.

My only complaint is the spotify connect feature is absolutely dogsh!t. Countless times I’ve had volume sync issues between my phone (spotify app) and the MRX and the volume blasts out at damaging levels. All i need is the ability to disable volume from the spotify app. Imagine how terrible my friends feel when they connect (group session) and the MRX syncs to their phones volume level (full lol) I had multiple conversations with their support about this and they actually got their engineering involved once I showed them how to reproduce the issue. After 6 months there was still no fix so I just moved onto dedicated streamer for Spotify. I don’t hold this against them in anyway… they really put a lot of effort into my issue.

1 Like

Thank you @stevelup @Kryten67 & @Jdamore for the feedback and not giving me heck for hijacking the thread. :slight_smile: Reviews and specs look decent, but I’ll have to try and get a listen. Plus I would love to support a local (Canadian) company, too.

I was looking at the MRX-1140 but given that I’ve already got L/C/R Outlaw 2200 monoblocks and a MiniDSP HD 2x45 to tune the 3 subs, the MRX-740 might be fine after all (I probably only have a small room and space for one more pair of surrounds/heights anyhow). I would be opting for the 8k board, as my PJ can support 4k@120. And I’m not worried about streaming as I have an offboard Bluesound Node.

Now…back to our regularly scheduled programming, I guess (pun intended).