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