Last active
January 29, 2024 06:51
-
-
Save othmar52/eeea6f81526df07ab4cb576736d1c5c8 to your computer and use it in GitHub Desktop.
control symetrix audio device via mqtt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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