You’re welcome. For what it’s worth, here are links to the three things I needed to get ip–>RS232 -->DB15 working.
amazon: 5ft serial cable with bare ends
Seems I can only post two links…so see next message too…
…
You’re welcome. For what it’s worth, here are links to the three things I needed to get ip–>RS232 -->DB15 working.
amazon: 5ft serial cable with bare ends
Seems I can only post two links…so see next message too…
…
amazon: trendnet Serial to IP Converter
In addition, I downloaded the dragon and mrip software from Xantech legacy products and used this (and front rs232 port on MX88ai) to do fun stuff including macros that can be called from the back port DB15 RS232 protocol. Why they don;t let you control the thing over the TCP IP port, I don’t know.
Keep me posted on your progress!
Did you ever post the code for this? I looked on your git but did not see it. I’ve followed your last few examples and would love to give this one a try. Thanks!
Hi,
I spent yesterday getting a version of this working with Hubitat – but got so frustrated by the UI, I gave up.
I spent today spinning up HA on an rpi5. Love it. I have my MX88ai running perfectly.
I have a few questions:
I haven’t been looking at the Xantech HA integration for a bit, but I’m sure keeping the attributes when zone is turned off is possible. I only have my MX88ai right now to use for testing, and hadn’t built a new cable based on @skavan’s pinouts until tonight! (I also have a MRC88 and MRAUDIO8x8, but they are in storage right now and not accessible. I’ve never had the MX88ai working)
I’m in the midst of rewriting the control layer (pyxantech) so that it is much easier to add more devices that can be controlled.
What did you do with the MRIP and Dragon software? How do you call the macros and why create macros vs just calling various commands directly via RS232?
@skavan do you have other equipment you are controlling over RS232/IP/etc? In a few weeks I should have something that makes interfacing with these much easier (if they do not already have libraries/integrations for control).
Also, you don’t need an Serial to IP Ethernet Converter if you have spare RaspberryPi or host that can be connected to the device. There are tools like Virtual IP2SL and others that can be run in a Docker container.
I have tons of stuff that I am (or in some cases, want to) control by ip. I once built a definition language (in json) for ip control via node-red and got it working with about 10 devices including the mx-88. I just started rewriting/improving it, hence the cable. But have been distracted by seeing whether it’s worth the work, in the presence of hubitat, homey, homeassistant. So, far, my view is a “qualified”, yes - its worth it.
I think a node-red “back-end”, feeding an mqtt state tree is incredibly powerful.
Where I don’t have a good solution/direction is the front end. I can write my own in node.js/material ui, which is beautiful, but a little bespoke. The HomeRemote project is kind of interesting as a gui…that can talk to mqtt for device state and commands.
On your other question about MRIP and Dragon…not a lot. I played around to see whether their web interface could be hijacked, for control purposes, and it can…but (a) it’s quite “heavy” and (b) only the “ai” models support it. I built the macros (for all zones off, all zones on, all zones query) just to see if they would work, and they do.
Here, for example, is the json for the Xantech in my v1. Works perfectly in my system. But I have been working on a rewrite that is more refined and powerful.
Note, it’s a commented jsonc file.
{
"matrix":{
"xantech_mx": {
"general":{
"description": "A generic template for a Xantech MX-88 Switcher",
"supported_models": ["MX-88ai"]
},
/*
Setup the basic configuration of the device
*/
"config":{
"interface": "tcp",
"port": 4999,
"prefix": "",
"suffix": "",
"delimiterOut": "", // Our commands include the + suffix, so no need to have one here
"delimiterIn": "+\r", // Received Responses are delimited by a + followed by \r
"substitutionCharacter": "$",
"statusResponseType": "regex", // This means our default processing engine for analysing responses is regex
"messageDelay": 250, // The device should have a rate throttle of 4 messages a second
"contentType": "text", // The payload of the messafe is handled as text
"messageFormat": "delimited", // Messages are delimited
"buffered": true, // We want to use buffer management to handle messages that don't come in, in one burst.
"contentTypeChoices": ["json", "html", "xml", "text", "byte"],
"messageFormatChoices": ["delimited", "stxetx", "byte", "fixed", "complete"],
"messageXform" : [{"from":"OK\r","to":""}, {"from":"ERROR\r","to":""}], // This device sends non-standard messages that we want to trap and throw away (hence the "")
"messageTimeout": 1000, // If we are awaiting a message, when should we timeout and move on (buffer management)
"messageLength": 0, // not used
"messageLengthByte": 0, // not used
"messageLengthIncrement": 0 // not used
},
/* commands: Define commands that can be sent to the device.
command: the command string sent to the device to do something
if its an http command, this is the GET/PUT/POST command
allows use of formula engine ${..}
statusRequest: is a corresponding Command that asks the device to report the status of the property.
allows use of formula engine ${..}
valueMap: is where we lookup the inbound human readable instruction (from a GUI or elsewhere) and translate it to the device code.
example: {"z1 power", "OFF"} -- we use the valueMap to translate OFF to 0. The ${%1} is the placeholder to insert that value.
if there is more than one value, we can use ${%1}, ${%2} etc..
if valueMap is missing, the system will look for a valueMap of the same name as the command.
actionSet: is for postprocessing.
"." is a special actionSet that says, after executing the command, execute a statusRequest. Typically used when the device doesn't automatically
update its status.
template: directs the system to use a template, for more complex command payloads or http body data
category: TODO
advanced: instructs the engine to execute advanced processing instructions
*/
"commands": {
"z1 power": {"command": "!1PR${%1}+", "statusRequest": "?1PR+", "valueMap":"power", "actionSet": "."},
"z2 power": {"command": "!2PR${%1}+", "statusRequest": "?2PR+", "valueMap":"power", "actionSet": "."},
"z3 power": {"command": "!3PR${%1}+", "statusRequest": "?3PR+", "valueMap":"power", "actionSet": "."},
"z4 power": {"command": "!4PR${%1}+", "statusRequest": "?4PR+", "valueMap":"power", "actionSet": "."},
"z5 power": {"command": "!5PR${%1}+", "statusRequest": "?5PR+", "valueMap":"power", "actionSet": "."},
"z6 power": {"command": "!6PR${%1}+", "statusRequest": "?6PR+", "valueMap":"power", "actionSet": "."},
"z7 power": {"command": "!7PR${%1}+", "statusRequest": "?7PR+", "valueMap":"power", "actionSet": "."},
"z8 power": {"command": "!8PR${%1}+", "statusRequest": "?8PR+", "valueMap":"power", "actionSet": "."},
"z1 input": {"command": "!1SS${%1}+", "statusRequest": "?1SS+", "valueMap":"input", "actionSet": "."},
"z2 input": {"command": "!2SS${%1}+", "statusRequest": "?2SS+", "valueMap":"input", "actionSet": "."},
"z3 input": {"command": "!3SS${%1}+", "statusRequest": "?3SS+", "valueMap":"input", "actionSet": "."},
"z4 input": {"command": "!4SS${%1}+", "statusRequest": "?4SS+", "valueMap":"input", "actionSet": "."},
"z5 input": {"command": "!5SS${%1}+", "statusRequest": "?5SS+", "valueMap":"input", "actionSet": "."},
"z6 input": {"command": "!6SS${%1}+", "statusRequest": "?6SS+", "valueMap":"input", "actionSet": "."},
"z7 input": {"command": "!7SS${%1}+", "statusRequest": "?7SS+", "valueMap":"input", "actionSet": "."},
"z8 input": {"command": "!8SS${%1}+", "statusRequest": "?8SS+", "valueMap":"input", "actionSet": "."},
"z1 mute": {"command": "!1MU${%1}+", "statusRequest": "?1MU+", "valueMap":"mute", "actionSet": "."},
"z2 mute": {"command": "!2MU${%1}+", "statusRequest": "?2MU+", "valueMap":"mute", "actionSet": "."},
"z3 mute": {"command": "!3MU${%1}+", "statusRequest": "?3MU+", "valueMap":"mute", "actionSet": "."},
"z4 mute": {"command": "!4MU${%1}+", "statusRequest": "?4MU+", "valueMap":"mute", "actionSet": "."},
"z5 mute": {"command": "!5MU${%1}+", "statusRequest": "?5MU+", "valueMap":"mute", "actionSet": "."},
"z6 mute": {"command": "!6MU${%1}+", "statusRequest": "?6MU+", "valueMap":"mute", "actionSet": "."},
"z7 mute": {"command": "!7MU${%1}+", "statusRequest": "?7MU+", "valueMap":"mute", "actionSet": "."},
"z8 mute": {"command": "!8MU${%1}+", "statusRequest": "?8MU+", "valueMap":"mute", "actionSet": "."},
"z1 volume": {"command": "!1VO${%1}+", "statusRequest": "?1VO+", "valueMap":"volume", "actionSet": "."},
"z2 volume": {"command": "!2VO${%1}+", "statusRequest": "?2VO+", "valueMap":"volume", "actionSet": "."},
"z3 volume": {"command": "!3VO${%1}+", "statusRequest": "?3VO+", "valueMap":"volume", "actionSet": "."},
"z4 volume": {"command": "!4VO${%1}+", "statusRequest": "?4VO+", "valueMap":"volume", "actionSet": "."},
"z5 volume": {"command": "!5VO${%1}+", "statusRequest": "?5VO+", "valueMap":"volume", "actionSet": "."},
"z6 volume": {"command": "!6VO${%1}+", "statusRequest": "?6VO+", "valueMap":"volume", "actionSet": "."},
"z7 volume": {"command": "!7VO${%1}+", "statusRequest": "?7VO+", "valueMap":"volume", "actionSet": "."},
"z8 volume": {"command": "!8VO${%1}+", "statusRequest": "?8VO+", "valueMap":"volume", "actionSet": "."},
"z1 volumedB": {"command": "!1VO${%1}+", "statusRequest": "?1VO+", "valueMap":"volumedB", "actionSet": "."},
"z2 volumedB": {"command": "!2VO${%1}+", "statusRequest": "?2VO+", "valueMap":"volumedB", "actionSet": "."},
"z3 volumedB": {"command": "!3VO${%1}+", "statusRequest": "?3VO+", "valueMap":"volumedB", "actionSet": "."},
"z4 volumedB": {"command": "!4VO${%1}+", "statusRequest": "?4VO+", "valueMap":"volumedB", "actionSet": "."},
"z5 volumedB": {"command": "!5VO${%1}+", "statusRequest": "?5VO+", "valueMap":"volumedB", "actionSet": "."},
"z6 volumedB": {"command": "!6VO${%1}+", "statusRequest": "?6VO+", "valueMap":"volumedB", "actionSet": "."},
"z7 volumedB": {"command": "!7VO${%1}+", "statusRequest": "?7VO+", "valueMap":"volumedB", "actionSet": "."},
"z8 volumedB": {"command": "!8VO${%1}+", "statusRequest": "?8VO+", "valueMap":"volumedB", "actionSet": "."},
"power": {"command": "!AO+", "statusRequest": "?1PR+"}
},
/* feedbacks: Define responses received from a device.
Order is important as the feedback array is processed from top to bottom.
Multiple matches are permitted
id: a unique id for each feedback.
statusReponse: the Regex or Object to match against the inbound payload. when it's an object, we need
a key (the target object). a value (the required matching value). valueKey = where to find the
value that we use in the valueMap lookup.
allows use of formula engine ${..}
{"key": "URI", "value":"/state/device/power_mode", "valueKey":"ITEMS.0.VALUE"}
category: the MQTT category or group that this feedback will show up in. default is "main"
topic: the actual descriptor of this feedback (defaults to id)
sph: instructs the engine to stop processing any more feedbacks.
valueMap: the valueMap to use to look up the human friendly display of the device property/feedback status.
requestReply: instructs the engine to require that the last "command" issued to the device, is a match, in
order for this feedback to be processed.
xform: tells the engine to convert the extracted value to a different dataType (i.e. a charcode becomes integer)
actionSet: tells the engine to send commands in a postProcessing actionSet.
advanced: instructs the engine to execute advanced processing instructions. overrides valueMap
*/
"feedbacks": [
{"id": "anyPower","statusResponse": "(?:^\\?)(\\d)PR(.{1})$", "category":"main", "advanced": true},
{"id": "power","statusResponse": "(?:^\\?\\dPR)(.{1})$", "category":"main", "valueMap": "systemPower"},
{"id": "z1 power","statusResponse": "(?:^\\?1PR)(.{1})$", "category":"zone 1", "sph":"end", "valueMap":"power"},
{"id": "z2 power","statusResponse": "(?:^\\?2PR)(.{1})$", "category":"zone 2", "sph":"end", "valueMap":"power"},
{"id": "z3 power","statusResponse": "(?:^\\?3PR)(.{1})$", "category":"zone 3", "sph":"end", "valueMap":"power"},
{"id": "z4 power","statusResponse": "(?:^\\?4PR)(.{1})$", "category":"zone 4", "sph":"end", "valueMap":"power"},
{"id": "z5 power","statusResponse": "(?:^\\?5PR)(.{1})$", "category":"zone 5", "sph":"end", "valueMap":"power"},
{"id": "z6 power","statusResponse": "(?:^\\?6PR)(.{1})$", "category":"zone 6", "sph":"end", "valueMap":"power"},
{"id": "z7 power","statusResponse": "(?:^\\?7PR)(.{1})$", "category":"zone 7", "sph":"end", "valueMap":"power"},
{"id": "z8 power","statusResponse": "(?:^\\?8PR)(.{1})$", "category":"zone 8", "sph":"end", "valueMap":"power"},
{"id": "z1 input","statusResponse": "(?:^\\?1SS)(.{1})$", "category":"zone 1", "sph":"end", "valueMap":"input"},
{"id": "z2 input","statusResponse": "(?:^\\?2SS)(.{1})$", "category":"zone 2", "sph":"end", "valueMap":"input"},
{"id": "z3 input","statusResponse": "(?:^\\?3SS)(.{1})$", "category":"zone 3", "sph":"end", "valueMap":"input"},
{"id": "z4 input","statusResponse": "(?:^\\?4SS)(.{1})$", "category":"zone 4", "sph":"end", "valueMap":"input"},
{"id": "z5 input","statusResponse": "(?:^\\?5SS)(.{1})$", "category":"zone 5", "sph":"end", "valueMap":"input"},
{"id": "z6 input","statusResponse": "(?:^\\?6SS)(.{1})$", "category":"zone 6", "sph":"end", "valueMap":"input"},
{"id": "z7 input","statusResponse": "(?:^\\?7SS)(.{1})$", "category":"zone 7", "sph":"end", "valueMap":"input"},
{"id": "z8 input","statusResponse": "(?:^\\?8SS)(.{1})$", "category":"zone 8", "sph":"end", "valueMap":"input"},
{"id": "z1 mute","statusResponse": "(?:^\\?1MU)(.{1})$", "category":"zone 1", "sph":"end", "valueMap":"mute"},
{"id": "z2 mute","statusResponse": "(?:^\\?2MU)(.{1})$", "category":"zone 2", "sph":"end", "valueMap":"mute"},
{"id": "z3 mute","statusResponse": "(?:^\\?3MU)(.{1})$", "category":"zone 3", "sph":"end", "valueMap":"mute"},
{"id": "z4 mute","statusResponse": "(?:^\\?4MU)(.{1})$", "category":"zone 4", "sph":"end", "valueMap":"mute"},
{"id": "z5 mute","statusResponse": "(?:^\\?5MU)(.{1})$", "category":"zone 5", "sph":"end", "valueMap":"mute"},
{"id": "z6 mute","statusResponse": "(?:^\\?6MU)(.{1})$", "category":"zone 6", "sph":"end", "valueMap":"mute"},
{"id": "z7 mute","statusResponse": "(?:^\\?7MU)(.{1})$", "category":"zone 7", "sph":"end", "valueMap":"mute"},
{"id": "z8 mute","statusResponse": "(?:^\\?8MU)(.{1})$", "category":"zone 8", "sph":"end", "valueMap":"mute"},
{"id": "z1 volume","statusResponse": "(?:^\\?1VO)(.*)$", "category":"zone 1", "valueMap":"volume", "sph":"end"},
{"id": "z2 volume","statusResponse": "(?:^\\?2VO)(.*)$", "category":"zone 2", "valueMap":"volume", "sph":"end"},
{"id": "z3 volume","statusResponse": "(?:^\\?3VO)(.*)$", "category":"zone 3", "valueMap":"volume", "sph":"end"},
{"id": "z4 volume","statusResponse": "(?:^\\?4VO)(.*)$", "category":"zone 4", "valueMap":"volume", "sph":"end"},
{"id": "z5 volume","statusResponse": "(?:^\\?5VO)(.*)$", "category":"zone 5", "valueMap":"volume", "sph":"end"},
{"id": "z6 volume","statusResponse": "(?:^\\?6VO)(.*)$", "category":"zone 6", "valueMap":"volume", "sph":"end"},
{"id": "z7 volume","statusResponse": "(?:^\\?7VO)(.*)$", "category":"zone 7", "valueMap":"volume", "sph":"end"},
{"id": "z8 volume","statusResponse": "(?:^\\?8VO)(.*)$", "category":"zone 8", "valueMap":"volume", "sph":"end"},
{"id": "z1 volumedB","statusResponse": "(?:^\\?1VO)(.*)$", "category":"zone 1", "valueMap":"volumedB", "sph":"end"},
{"id": "z2 volumedB","statusResponse": "(?:^\\?2VO)(.*)$", "category":"zone 2", "valueMap":"volumedB", "sph":"end"},
{"id": "z3 volumedB","statusResponse": "(?:^\\?3VO)(.*)$", "category":"zone 3", "valueMap":"volumedB", "sph":"end"},
{"id": "z4 volumedB","statusResponse": "(?:^\\?4VO)(.*)$", "category":"zone 4", "valueMap":"volumedB", "sph":"end"},
{"id": "z5 volumedB","statusResponse": "(?:^\\?5VO)(.*)$", "category":"zone 5", "valueMap":"volumedB", "sph":"end"},
{"id": "z6 volumedB","statusResponse": "(?:^\\?6VO)(.*)$", "category":"zone 6", "valueMap":"volumedB", "sph":"end"},
{"id": "z7 volumedB","statusResponse": "(?:^\\?7VO)(.*)$", "category":"zone 7", "valueMap":"volumedB", "sph":"end"},
{"id": "z8 volumedB","statusResponse": "(?:^\\?8VO)(.*)$", "category":"zone 8", "valueMap":"volumedB", "sph":"end"}
],
/* valueMaps: provide cross translation of device codes to human codes and vice versa.
deviceValue: the value required by (when inserted into a command), or reported by the device (when extracted from a feedback)
deviceValues can also be formulaic using the ${} syntax. (see volume valueMap below)
standardValue: one or more standardized values that will trigger a command attached to this map. i.e. "power", "OFF"
displayValue: the value to display to downstream GUI's or clients
displayValues can also be formulaic using the ${} syntax (see volume valueMap below)
feedbackActionSet: an actionSet to be be run, if this valueMap is a result of a feedback. This allows actionSets to be linked to specific values.
commandActionSet: an actionSet to be be run, if this valueMap is a result of a command. This allows actionSets to be linked to specific values.
clear: reset an entire mqtt node and its children. possibly useful on Power Off for some/many devices.
*/
"valuemaps":{
"power": [
{"deviceValue": "0","standardValue": [0,"OFF"], "displayValue": "OFF"},
{"deviceValue": "1","standardValue": [1,"ON"], "displayValue": "ON"}
],
"systemPower": [
{"deviceValue": "","standardValue":"", "displayValue": "${eval(['%z1p','%z2p','%z3p','%z4p','%z5p','%z6p','%z7p','%z8p'].indexOf('ON')>-1 ? 'ON':'OFF')}"}
],
"mute": [
{"deviceValue": "0","standardValue": [0,"OFF"], "displayValue": "OFF"},
{"deviceValue": "1","standardValue": [1,"ON"], "displayValue": "ON"}
],
"input": [
{"deviceValue": "1", "standardValue": [1, "INPUT1"], "displayValue": "INPUT 1"},
{"deviceValue": "2", "standardValue": [2, "INPUT2"], "displayValue": "INPUT 2"},
{"deviceValue": "3", "standardValue": [3, "INPUT3"], "displayValue": "INPUT 3"},
{"deviceValue": "4", "standardValue": [4, "INPUT4"], "displayValue": "INPUT 4"},
{"deviceValue": "5", "standardValue": [5, "INPUT5"], "displayValue": "INPUT 5"},
{"deviceValue": "6", "standardValue": [5, "INPUT6"], "displayValue": "INPUT 6"},
{"deviceValue": "7", "standardValue": [5, "INPUT7"], "displayValue": "INPUT 7"},
{"deviceValue": "8", "standardValue": [5, "INPUT8"], "displayValue": "INPUT 8"}
],
"volume": [
{"deviceValue": "${range(0,38,1,0,100)}", "standardValue": "",
"displayValue": "${range(0,100,1,0,38)}"}
],
"volumedB": [
{"deviceValue": "${eval((parseInt(%1)+38))}", "standardValue": "",
"displayValue": "${eval((parseInt((%1>18 ? (%1-38)*1.25 : %1>7 ? ((%1-25)*2.5)-7.5 : ((%1-52.5)*3.75)+118.125)*2)/2) + 'dB')}"}
]
},
"advanced":{
"updatePowerVariable":{
"feedback": "anyPower",
"type": "variable",
"inArray": false,
"variableMap":{
// all variables extracted in the variableMap are temporary.
"value" : "${%2}", // the feedback extracts two matches. one for matrix channel {%1} and one for power on or off {%2)
"name" : "z${%1}p",
"z${%1}p" : "${%2}" // we can do clver things like make the variable name a variable
},
"valueMap": "power",
"valueMapVariable": "z${%1}p", // when we call the valuemap, we need to specify which of the message value variables we
"outArray": false, // want to use. mostly it will be {%1}...but in this case its {%2}
"outArrayVariable": "",
"insertTemplate": { // the insertTemplate can use any variable from the variable map OR the deviceMap.variables
"parent" : "variables", // but it can't use the %1 etc.. as they don't exist in this scope
"node": "${%name}",
"template" : "${var(%name)}" // var is a special command. instead of "${var(%name)}", we could also have written ${%value}
}
}
},
"templates":{
// not used
},
"actionSets":{
"initialize":[
{"command": "z1 power", "value": "STATUS", "delay": "100"},
{"command": "z2 power", "value": "STATUS", "delay": "100"},
{"command": "z3 power", "value": "STATUS", "delay": "100"},
{"command": "z4 power", "value": "STATUS", "delay": "100"},
{"command": "z5 power", "value": "STATUS", "delay": "100"},
{"command": "z6 power", "value": "STATUS", "delay": "100"},
{"command": "z7 power", "value": "STATUS", "delay": "100"},
{"command": "z8 power", "value": "STATUS", "delay": "100"},
{"command": "z1 input", "value": "STATUS", "delay": "100"},
{"command": "z2 input", "value": "STATUS", "delay": "100"},
{"command": "z3 input", "value": "STATUS", "delay": "100"},
{"command": "z4 input", "value": "STATUS", "delay": "100"},
{"command": "z5 input", "value": "STATUS", "delay": "100"},
{"command": "z6 input", "value": "STATUS", "delay": "100"},
{"command": "z7 input", "value": "STATUS", "delay": "100"},
{"command": "z8 input", "value": "STATUS", "delay": "100"},
{"command": "z1 volume", "value": "STATUS", "delay": "100"},
{"command": "z2 volume", "value": "STATUS", "delay": "100"},
{"command": "z3 volume", "value": "STATUS", "delay": "100"},
{"command": "z4 volume", "value": "STATUS", "delay": "100"},
{"command": "z5 volume", "value": "STATUS", "delay": "100"},
{"command": "z6 volume", "value": "STATUS", "delay": "100"},
{"command": "z7 volume", "value": "STATUS", "delay": "100"},
{"command": "z8 volume", "value": "STATUS", "delay": "100"},
{"command": "z1 mute", "value": "STATUS", "delay": "100"},
{"command": "z2 mute", "value": "STATUS", "delay": "100"},
{"command": "z3 mute", "value": "STATUS", "delay": "100"},
{"command": "z4 mute", "value": "STATUS", "delay": "100"},
{"command": "z5 mute", "value": "STATUS", "delay": "100"},
{"command": "z6 mute", "value": "STATUS", "delay": "100"},
{"command": "z7 mute", "value": "STATUS", "delay": "100"},
{"command": "z8 mute", "value": "STATUS", "delay": "100"}
]
},
"variables":{
// not prefilled.
}
}
}
}
@skavan Thanks for posting your config, will be useful for me to read through to understand your mental model for interacting with a device. This is similar to what I’ve been working though (modeling the interfaces, what models it applied to, etc). What I’ve run across is that so much is shared, but slightly different across models. I’m looking at Apple PKL (relatively new and may not be a good option since limited support today), as well as Nickel, Nix, RCL, etc.
The biggest need for me is the ability to import portions of the configuration, ability to delete/override some configuration, easy to read, and as cross-language as possible (so other people can create similar tools in other languages using the existing configuration). Also would be cool to be able to generate YAML/JSON from one of the configurations, just for wider compatibility with other tools.
I should have something very interesting for you in a few weeks for IP control if the API over TCP is text based.
Cool. We should coordinate!
Here’s the beginnings of my version 2, supporting variables, regex and math expressions.
It also supports overrides. We should probably put into a repository instead of clogging up this channel.
[
{
"capability": "switch",
"name": "power",
"description": "Power",
"actions": {
"onoff": {
"command": "",
"arguments": [
{
"name": "setPower",
"type": "enum",
"enum": ["on", "off"]
}
],
"valueMap": [
{ "value": "on", "deviceValue": 1 },
{ "value": "off", "deviceValue": 0 }
]
},
"status": { "command": "", "arguments": [] },
"toggle": { "command": "", "arguments": [] }
},
"allowedValues": ["on", "off"],
"displayFormat": ""
},
{
"capability": "audioVolume",
"name": "volume",
"description": "Volume Control",
"actions": {
"volume": {
"command": "",
"arguments": [
{
"name": "setVolume",
"type": "integer",
"minimum": 0,
"maximum": 100
}
]
},
"volumeUp": { "command": "", "arguments": [] },
"volumeDown": { "command": "", "arguments": [] },
"status": { "command": "", "arguments": [] }
}
},
{
"capability": "mediaInput",
"name": "input",
"description": "Media Source Control",
"actions": {
"input": {
"command": "",
"arguments": [
{
"name": "setInput",
"type": "enum",
"enum": ["Input 1", "Input 2", "Input 3", "Input 4", "Input 5", "Input 6", "Input 7", "Input 8"]
}
]
}
}
},
{
"capability": "audioMute",
"name": "mute",
"description": "Audio Mute",
"actions": {
"onoff": {
"command": "",
"arguments": [
{
"name": "setMute",
"type": "enum",
"enum": ["on", "off"]
}
]
},
"status": { "command": "", "arguments": [] },
"toggle": { "command": "", "arguments": [] }
},
"allowedValues": ["on", "off"],
"displayFormat": ""
}
]```
2. Beginnings of a revised Matrix controller.
```{
"id": "i5TTEWeL",
"name": "MultiZoneAmp",
"description": "Xantech Matrix Switcher",
"version": "1.0.0",
"type": "Amplifier",
"category": "AV",
"need a name": "matrix switch",
"license": "MIT",
"author": "Suresh Kavan",
"supportedModels": ["a", "b"],
"protocol": {
"interface": "tcp",
"port": 4999,
"delimiterOut": "",
"delimiterIn": "+\r",
"substitutionCharacter": "$",
"responseParser": "regex",
"messageDelay": 250,
"messageTimeout": 1000
},
"preProcessorCommand": "{%pp}",
"variablePlaceholder": "%",
// first we have a device. (matrix001)
// then we have a component (main)
// then we have a capability (switch, audioVolume, mediaInput, audioMute)
// capabilities have one or more properties,
// that are defined by the capability statement
// i.e. power, volume, volumePct, volumedB, mute, input
// matrix001/main/switch/power
// matrix001/main/audioVolume/volume
// matrix001/main/audioVolume/volumedB
// matrix001/main/audioVolume/volumePct
// matrix001/main/audioMute/mute
// matrix001/main/mediaInput/input
// commands, components, feedbacks, variables can by of type "array"
"components": [
{
"component": "main",
"name": "main",
"description": "Master Controls",
// pull in the capabilty and override here
"capabilities": [
{
"capability": "switch",
"name": "power",
"description": "All Zones Power"
},
{
"capability": "audioVolume",
"description": "All Zones Volume"
},
{
"capability": "mediaInput",
"description": "All Zones Source"
},
{
"capability": "audioMute",
"description": "All Zones Mute"
}
],
"actions": {
"switch": {
"onoff": {
"command": ["!1PR{%1}+", "!2PR{%1}+"]
},
"status": {
"command": ["?1PR+", "?2PR+"]
},
"toggle": {
"command": ["!1PT+", "!2PT+"]
}
},
"audioVolume": {
"volume": {
"command": ["!1VO${%1}+", "!2VO${%1}+"],
"arguments": [
{
"name": "setVolume",
"type": "integer",
"minimum": 0,
"maximum": 38
}
]
},
"volumeUp": {
"command": ["!1VI$1+", "!2VI$1+"],
"arguments": []
},
"volumeDown": {
"command": ["!1VD$1-", "!2VD$1-"],
"arguments": []
}
}
}
},
{
"component": "zone{%pp}",
// doesn't have to be integers, could be text too.
"type": {"array": [1, 2, 3]} ,
"name": "Zone{%pp}",
"description": "Zone {%pp} Controls",
"capabilities": [
{
"capability": "switch",
"name": "power",
"description": "Zone {%pp} Power"
},
{
"capability": "audioVolume",
"description": "Zone {%pp} Volume"
},
{
"capability": "mediaInput",
"description": "Zones {%pp} Source"
},
{
"capability": "audioMute",
"description": "Zone {%1} Mute"
}
],
"actions": {
"switch": {
"onoff": {
"command": "!{%pp}PR{%1}+"
},
"status": {
"command": "?{%pp}PR+"
},
"toggle": {
"command": "!{%pp}PT+"
}
},
"audioVolume": {
"volume": {
"command": "!{%pp}VO{%1}+",
"arguments": [
{
"name": "setVolume",
"type": "integer",
"minimum": 0,
"maximum": 38
}
]
},
"volumeUp": {
"command": "!{%pp}VI+",
"arguments": []
},
"volumeDown": {
"command": "!{%pp}VD+",
"arguments": []
}
}
}
}
],
"feedbacks": [
{"name": "Power", "match": "\\?(\\d)PO(\\d)\\+", "component": "zone{%1}", "capability": "switch", "value": "{%2}", "variables": ["zonePower"]},
{"name": "Volume", "match": "\\?(\\d)VO(\\d{1,3})\\+", "component": "zone{%1}", "capability": "audioVolume", "value": "{%2}", "variables": ["zoneVolume"]}
],
"variables":[
{"name":"masterPower", "formula": "${zone1.switch}} + ${zone2.switch}} + ${zone3.switch} > 0", "type":"boolean"},
],
// these override predefined capabilities
"capabilities": [
{
"capability": "mediaInput",
"name": "input",
"description": "Media Source Control",
"actions": {
"input": {
"command": "",
"arguments": [
{
"name": "setInput",
"type": "enum",
"enum": ["Sonos", "Input 2", "Input 3", "Input 4", "Input 5", "Input 6", "Input 7", "Input 8"]
}
]
}
}
}
]
}
@ryans … please rememebr I also posted enhanced (hacked as best I could) code for bass/treble/balance. While I know that many set it and forget it, sometimes this can be valuable. My GUI now has this for every zone in my DAX:
Is there any chance you would consider cooperation to also bring the bass, treble and balance settings for the Soundavo ws66i integration? (now part of home assistant)
Browsing through the code, I see lines describing the setting of these, but I don’t see them in the actual entities/devices in home assistant
I am only a “hack” when it comes to python coding. I am hoping that @ryans will have time to review and eventually put this into the code for all the players (and done the way it should be).
As such, I chose not to create things beyond attributes off the media_player
entity.
Then I implemented services (xantech.set_bass
, xantech.set_treble
, xantech.set_balance
so that they can be changed in say an automation.
I just tied a input_number to these through an automation. The only thing I would guess is missing would be to have created those input_number entities under each zone, but I though that overkill (3 x 8 zones = 24 input_number entities … versus 3 attributes on each zone).
alias: Change DAX Bass_Treble_Balance
description: ""
trigger:
- platform: state
entity_id:
- input_number.dax_bass_level
- input_number.dax_treble_level
- input_number.dax_balance_level
- platform: state
entity_id:
- input_select.dax_zones
condition: []
action:
- choose:
- conditions:
- condition: template
value_template: "{{ trigger.entity_id == 'input_number.dax_treble_level' }}"
sequence:
- service: xantech.set_treble
target:
entity_id: >
{% set selzone = states('input_select.dax_zones') %} {% for
entity_id in state_attr('group.dax_media_players','entity_id')
%} {% if state_attr(entity_id,'friendly_name') == selzone %} {{
entity_id }} {% endif %} {% endfor %}
data_template:
treble: "{{ states(trigger.entity_id) | int }}"
- conditions:
- condition: template
value_template: "{{ trigger.entity_id == 'input_number.dax_bass_level' }}"
sequence:
- service: xantech.set_bass
target:
entity_id: >
{% set selzone = states('input_select.dax_zones') %} {% for
entity_id in state_attr('group.dax_media_players','entity_id')
%} {% if state_attr(entity_id,'friendly_name') == selzone %} {{
entity_id }} {% endif %} {% endfor %}
data_template:
bass: "{{ states(trigger.entity_id) | int }}"
- conditions:
- condition: template
value_template: "{{ trigger.entity_id == 'input_number.dax_balance_level' }}"
sequence:
- service: xantech.set_balance
target:
entity_id: >
{% set selzone = states('input_select.dax_zones') %} {% for
entity_id in state_attr('group.dax_media_players','entity_id')
%} {% if state_attr(entity_id,'friendly_name') == selzone %} {{
entity_id }} {% endif %} {% endfor %}
data_template:
balance: "{{ states(trigger.entity_id) | int }}"
- conditions:
- condition: template
value_template: "{{ trigger.entity_id == 'input_select.dax_zones' }}"
sequence:
- service: input_number.set_value
data_template:
value: >
{% set selzone = states('input_select.dax_zones') %} {% for
entity_id in state_attr('group.dax_media_players','entity_id')
%} {% if state_attr(entity_id,'friendly_name') == selzone %}
{{state_attr(entity_id,'balance') }} {% endif %} {% endfor %}
target:
entity_id: input_number.dax_balance_level
- service: input_number.set_value
data_template:
value: >
{% set selzone = states('input_select.dax_zones') %} {% for
entity_id in state_attr('group.dax_media_players','entity_id')
%} {% if state_attr(entity_id,'friendly_name') == selzone %}
{{state_attr(entity_id,'bass') }} {% endif %} {% endfor %}
target:
entity_id: input_number.dax_bass_level
- service: input_number.set_value
data_template:
value: >
{% set selzone = states('input_select.dax_zones') %} {% for
entity_id in state_attr('group.dax_media_players','entity_id')
%} {% if state_attr(entity_id,'friendly_name') == selzone %}
{{state_attr(entity_id,'treble') }} {% endif %} {% endfor %}
target:
entity_id: input_number.dax_treble_level
mode: single
I suppose it could have been one service instead of three and set them all, but the service takes in the target entity and the value to set it to with value checking. This is for bass. Now that I write this it could have been just set level and likely took in bass, treble and balance.
Certainly this has the service references:
And the attributes as extras:
@property
def extra_state_attributes(self):
"""Return the extra state attributes for bass, treble, balance."""
bass = self._status.get('bass')
treble = self._status.get('treble')
balance = self._status.get('balance')
return {
"bass": bass,
"treble": treble,
"balance": balance
}
Carried in constants:
Of course, I made mods in the series and protocols of pyxantech:
The key to another integration would be to modify this I believe for your particular receiver. :
set_volume: '<{zone}VO{volume:02}' # volume: 0-38
set_treble: '<{zone}TR{treble}:02}' # treble: 0-14
set_bass: '<{zone}BS{bass:02}' # bass: 0-14
set_balance: '<{zone}BL{balance:02}' # balance: 0-20
set_source: '<{zone}CH{source:02}' # source: 0-6
And also the overall get current state might need to be modified to be sure it returns bass, treble and balance and restore sets them.
responses:
zone_status: '>(?P<zone>\d\d)(?P<unknown>\d\d)(?P<power>[01]{2})(?P<mute>[01]{2})(?P<do_not_disturb>[01]{2})(?P<volume>\d\d)(?P<treble>\d\d)(?P<bass>\d\d)(?P<balance>\d\d)(?P<source>\d\d)(?P<keypad>\d\d)'
extra:
restore_zone: [ 'set_power', 'set_source', 'set_volume', 'set_mute', 'set_bass', 'set_balance', 'set_treble' ]
restore_success: "OK\r"
Tying it together, it rocks:
In regards to balance/bass/treble control questions/suggestions from@AlexMPH and @kbrown01, I created a new thread on adding this support to Home Assistant to allow ALL media player entities to provide support for these interfaces.
This is the best layer to add this so that it becomes codified as a standard that any media player integration can support.
Here is the thread, please feel free to start contributing there. I don’t have the time to setup an environment and test the changes to Home Assistant core right now or I would.
Hopefully some enthuasiastic developer takes this up as a fun project to add to core…it even would make a great first project.
See thread here:
YOU CAN ALSO VOTE FOR THIS FEATURE on the above link.
Voted and thanks!
I added a comment that specifically this should be targeted at stereo controls and not surround sound systems.
I’m new to HA and was hoping for some help getting this set up to control my Sonance C4630 SE in HA.
If I used this in the config
media_player:
- platform: xantech
type: sonance
I get an error saying
Invalid config for ‘xantech’ from integration ‘media_player’ at configuration.yaml, line 14: value must be one of [‘dax88’, ‘monoprice6’, ‘sonance6’, ‘xantech8’, ‘zpr68-10’] for dictionary value ‘type’, got ‘sonance’
If I change it to sonance6 I get this error.
Traceback (most recent call last):
File “/usr/src/homeassistant/homeassistant/helpers/entity_platform.py”, line 350, in _async_setup_platform
await asyncio.shield(awaitable)
File “/config/custom_components/xantech/media_player.py”, line 137, in async_setup_platform
amp = await async_get_amp_controller(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File “/usr/local/lib/python3.12/site-packages/pyxantech/init.py”, line 568, in async_get_amp_controller
protocol_config = PROTOCOL_CONFIG[protocol]
~~~~~~~~~~~~~~~^^^^^^^^^^
KeyError: ‘sonance6’
Any help would be appreciated… Joe
Did you make any headway with this ? I seem to be in the same boat.
I can now control the Xantech MRC88 with an Itach IP2SL from my Mac and via USB to Serial cable from a windows VM but I cannot seem to control via home assistant either via IP or USB. The media player entities show up but I’m unable turn them on and just get the error -
‘Failed to call service media_player/turn_on. unknown error’
Anyone have any ideas ? I’m running HA as a VM on an Unraid server if that makes any difference.
Edit: I can control the zones (on and off) via telnet switch through HA.
Also to add, this is the error showing in HA logs for all zones -
WARNING (MainThread) [custom_components.xantech.media_player] Failed updating Xantech House Audio zone 13 (Living Room):
Ok, I’ve managed to get it working and my issue appears to have been using 11,12,13 etc for my zones rather than 1, 2, 3…
Not sure why as I originally copied the example config from github and changed it but it seems to be working as expected now. Hopefully this helps anyone else who might come across the same issue in future.
Also, this is working over an iTach IP2SL.
hi all
so for the DAX88 - if you have say 6 pairs of speakers hard wired to it - you can cast youtube music (like you cast from the app on your phone) to any / all zones?
This seems like exactly what i’m looking for.
thank you