Skip to content

Instantly share code, notes, and snippets.

@othmar52
Last active January 29, 2024 06:51
Show Gist options
  • Save othmar52/eeea6f81526df07ab4cb576736d1c5c8 to your computer and use it in GitHub Desktop.
Save othmar52/eeea6f81526df07ab4cb576736d1c5c8 to your computer and use it in GitHub Desktop.
control symetrix audio device via mqtt
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''
wrapper script for integrating symetrix audio devices into MQTT infrastructure
@see http://www.symetrix.co/repository/SymNet_cp.pdf
tested with OUTPUT ports/parameters of symetrix Jupiter 8
in this example you can control volume and mute states by publishing to those mqtt topics
cmnd/home/jupiter/output/<name>/mute
cmnd/home/jupiter/output/<name>/unmute
cmnd/home/jupiter/output/<name>/vol/up
cmnd/home/jupiter/output/<name>/vol/down
cmnd/home/jupiter/output/<name>/vol/set # int between 0 an 100 as message
parameter changes (controlled by any controller) gets published to
stat/home/jupiter/output/<name>
with payload
{
'name': <name>, # the configured name
'level': <level %>, # float between 0 and 100
'muted': <mute state> # boolean
}
to find out the controller numbers refer to your jupiter device appendix
or @see https://gist.github.com/othmar52/53401e21b91051d194b7dc451779f5a4
TODO handle dis- / reconnect in case we loose connection to symetrix socket or mqtt broker
TODO documentation of requirements (like paho-mqtt,...)
'''
import random
import re
from paho.mqtt import client as mqtt_client
import json
import socket
import threading
SYMETRIX_IP = 'insert symetrix ip/domain here'
SYMETRIX_PORT = 48630
MQTT_BROKER_IP = 'insert broker ip/domain here'
MQTT_BROKER_PORT = 1883
MQTT_USERNAME = ''
MQTT_PASSWORD = ''
topicSubscribe = 'cmnd/home/jupiter/#'
topicPublish = 'stat/home/jupiter/'
# generate mqtt client ID with pub prefix randomly
client_id = f'python-symetrix2mqtt-{random.randint(0, 100)}'
# define name, subTopic and the symetrix controller numbers we are interested in
audioTargetsConf = [
# name subTopic levelChannels muteChannels Jupiter8 names
['Tannoy', 'output/tannoy', [7170], [313, 323] ], # OUTPUT 1+2 VOLUME, OUTPUT 1 MUTE, OUTPUT 2 MUTE
['Woofer', 'output/db1', [7134], [333] ], # OUTPUT 3 VOLUME, OUTPUT 3 MUTE
['Bassbins', 'output/bassbins', [7140], [343] ], # OUTPUT 4 VOLUME, OUTPUT 4 MUTE
['KRK', 'output/krk', [7178], [353, 363] ], # OUTPUT 5+6 VOLUME, OUTPUT 5 MUTE, OUTPUT 6 MUTE
['Halle V', 'output/indoor1', [7182], [373, 383] ] # OUTPUT 7+8 VOLUME, OUTPUT 7 MUTE, OUTPUT 8 MUTE
]
class audioTarget():
def __init__(self, name, subTopic, levelChannels, muteChannels):
self.name = name
self.subTopic = subTopic
self.pubTopic = topicPublish + subTopic
self.levelChannels = levelChannels
self.muteChannels = muteChannels
self.lastLevel = .0
self.isMuted = False
self.stepSize = 3 # percent for de- or increase via up|down
self.max = 65535
def incomingControllerValue(self, key, value):
if key in self.levelChannels:
percent = 100*(value/self.max)
self.lastLevel = percent
return self
if key in self.muteChannels:
self.isMuted = True if int(value) == self.max else False
return self
return False
def getStatJson(self):
return json.dumps({
'name': self.name,
'level': self.lastLevel,
'muted': self.isMuted
})
def publishStats(self, mqttClient):
mqttClient.publish(
self.pubTopic,
self.getStatJson()
)
def levelBoundries(self, val):
val = int(val)
if val < 0:
return 0
if val > self.max:
return self.max
return val
def percentBoundries(self, val):
if val < 0:
return 0
if val > 100:
return 100
return val
def applyMqttCommand(self, mqttMsg):
payload = mqttMsg.payload.decode()
socketMessages = []
newMuteChannelValue = None
if mqttMsg.topic.find(f'{self.subTopic}/mute') > 0:
newMuteChannelValue = self.max
if payload == '1' or payload.upper() == 'TRUE':
newMuteChannelValue = self.max
if payload == '0' or payload.upper() == 'FALSE':
newMuteChannelValue = 0
if mqttMsg.topic.find(f'{self.subTopic}/unmute') > 0:
newMuteChannelValue = 0
if newMuteChannelValue != None:
for muteChannel in self.muteChannels:
socketMessages.append('CS %d %d' % (muteChannel, newMuteChannelValue))
newLevelPercent = None
if mqttMsg.topic.find(f'{self.subTopic}/vol/up') > 0:
newLevelPercent = self.percentBoundries(self.lastLevel + self.stepSize)
if mqttMsg.topic.find(f'{self.subTopic}/vol/down') > 0:
newLevelPercent = self.percentBoundries(self.lastLevel - self.stepSize)
if mqttMsg.topic.find(f'{self.subTopic}/vol/set') > 0:
if payload.isnumeric():
newLevelPercent = self.percentBoundries(int(payload))
if newLevelPercent != None:
newLevelValue = self.levelBoundries(newLevelPercent / 100 * self.max)
for levelChannel in self.levelChannels:
socketMessages.append('CS %d %d' % (levelChannel, newLevelValue))
if len(socketMessages) == 0:
return
sock.sendto(
bytes('%s\r' % '\r\n'.join(socketMessages), 'utf-8'),
(SYMETRIX_IP, SYMETRIX_PORT)
)
def incomingControllerValue(key, value):
for audioTarget in audioTargets:
matchingAudioTarget = audioTarget.incomingControllerValue(key, value)
if matchingAudioTarget:
return audioTarget
return False
def findAudioTargetByTopic(topic):
for audioTarget in audioTargets:
if topic.find(audioTarget.subTopic) > 0:
return audioTarget
return False
audioTargets = []
for audioTargetConf in audioTargetsConf:
audioTargets.append(
audioTarget(
audioTargetConf[0],
audioTargetConf[1],
audioTargetConf[2],
audioTargetConf[3]
)
)
cNumbers = []
for audioTarget in audioTargets:
cNumbers.extend(audioTarget.levelChannels)
cNumbers.extend(audioTarget.muteChannels)
# strange series of commands to limit push (of symetrix device) only for controller numbers above
onConnectCommands = [
'PU 1', # enable global push
'PU 1 1 10000', # enable global push for controllers 1 - 10000
'PUD', # disable controller push
'PUD 1 10000' # disable controller push for controllers 1 - 10000
]
for cNumber in cNumbers:
onConnectCommands.append('PUE %d' % cNumber)
onConnectCommands.append('PUR') # to get all values of push enabled controllers after connect
def connect_mqtt() -> mqtt_client:
def on_connect(client, userdata, flags, rc):
if rc == 0:
print('Connected to MQTT Broker!')
else:
print('Failed to connect, return code %d\n', rc)
client = mqtt_client.Client(client_id)
client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
client.on_connect = on_connect
client.connect(MQTT_BROKER_IP, MQTT_BROKER_PORT)
return client
def subscribe(client: mqtt_client):
def on_message(client, userdata, msg):
audioTarget = findAudioTargetByTopic(msg.topic)
if not audioTarget:
#print(f'Ignoring received msg `{msg.payload.decode()}` from `{msg.topic}` topic')
return
audioTarget.applyMqttCommand(msg)
client.subscribe(topicSubscribe)
client.on_message = on_message
client.loop_forever()
def symetrixSocket(mqttClient):
global sock
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP
sock.sendto(bytes('%s\r' % '\r\n'.join(onConnectCommands), 'utf-8'), (SYMETRIX_IP, SYMETRIX_PORT))
while True:
keyvals = sock.recv(1024).decode('utf-8').split('\r')
for keyval in keyvals:
match = re.match(r'^#(\d*)=(\d*)$', keyval)
if not match:
continue
key = int(match.group(1))
if key not in cNumbers:
continue
val = int(match.group(2))
audioTarget = incomingControllerValue(key, val)
if audioTarget:
audioTarget.publishStats(mqttClient)
def main():
client = connect_mqtt()
sub=threading.Thread(target=subscribe, args=(client,))
pub=threading.Thread(target=symetrixSocket, args=(client,))
sub.start()
pub.start()
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment