#!/usr/bin/env python3 import http import logging import lib.connection import lib.tools import re import threading import json import requests import time from lib.model.smartplugin import SmartPlugin EXPECTED_BROKER_VERSION = "1.1" sonos_speaker = {} class UDPDispatcher(lib.connection.Server): def __init__(self, ip, port, sh): self._logger = logging.getLogger('sonos') lib.connection.Server.__init__(self, ip, port, proto='UDP') self.dest = 'udp:' + ip + ':{port}'.format(port=port) self._logger.debug('starting udp listener with {url}'.format(url=self.dest)) self._sh = sh self.connect() def handle_connection(self): try: data, address = self.socket.recvfrom(10000) address = "{}:{}".format(address[0], address[1]) self._logger.debug("{}: incoming connection from {}".format('sonos', address)) except Exception as err: self._logger.error("{}: {}".format(self._name, err)) return try: sonos = json.loads(data.decode('utf-8').strip()) uid = sonos['uid'] if not uid: self._logger.error("No uid found in sonos udp response!\nResponse: {}") if uid not in sonos_speaker: self._logger.warning("no sonos speaker configured with uid '{uid}".format(uid=uid)) return for key, value in sonos.items(): try: instance_var = getattr(sonos_speaker[uid], key) if isinstance(instance_var, list): for item in instance_var: try: item(value, 'Sonos', '') except Exception as ex: self._logger.error(ex) continue except Exception as ex: self._logger.error(ex) continue except Exception as err: self._logger.error("Error parsing sonos broker response!\nError: {}".format(err)) class Sonos(SmartPlugin): PLUGIN_VERSION = "1.3.0.2" ALLOW_MULTIINSTANCE = False def __init__(self, sh, listen_host='0.0.0.0', listen_port=9999, broker_url=None, refresh=120): self._sonoslock = threading.Lock() self._lan_ip = get_lan_ip() self._logger = logging.getLogger('sonos') self._dpt3_vol_step = 1 self._dpt3_vol_time = 1 self._dpt3_vol_max = 100 if not self._lan_ip: self._logger.critical("Could not fetch internal ip address. Set it manually!") self.alive = False return self._logger.info("using local ip address {ip}".format(ip=self._lan_ip)) # check broker variable if broker_url: self._broker_url = broker_url else: self._broker_url = "http://{ip}:12900".format(ip=self._lan_ip) if self._broker_url: self._logger.warning("No broker url given, assuming current ip and default broker port: {url}". format(url=self._broker_url)) else: self._logger.error("Could not detect broker url !!!") return # normalize broker url if not self._broker_url.startswith('http://'): self._broker_url = "http://{url}".format(url=self._broker_url) # version check against Sonos Broker broker_version = self._send_cmd(SonosCommand.sonos_broker_version()) self._logger.debug("Sonos broker version: {version}".format(version=broker_version)) try: if EXPECTED_BROKER_VERSION != broker_version: self._logger.warning("This plugin is desgined to work with Sonos Broker version {version}. " "Your plugin version is probably out-of-date or too new. " "Please update your plugin and/or the Sonos Broker Server".format( version=EXPECTED_BROKER_VERSION)) except Exception: self._logger.warning("Unknown Sonos broker version string '{version_string}.'". format(version_string=broker_version)) self._listen_host = listen_host self._listen_port = listen_port self._sh = sh self._command = SonosCommand() self._logger.debug('refresh sonos speakers every {refresh} seconds'.format(refresh=refresh)) # add current_state command to scheduler self._sh.scheduler.add('sonos-update', self._subscribe, cycle=refresh) # start UDP listener UDPDispatcher(self._listen_host, self._listen_port, self._sh) def run(self): self.alive = True self._subscribe() def _subscribe(self): """ Subscribe the plugin to the Sonos Broker """ self._logger.debug('(re)registering to sonos broker server ...') self._send_cmd(SonosCommand.subscribe(self._lan_ip, self._listen_port)) for uid, speaker in sonos_speaker.items(): self._send_cmd(SonosCommand.current_state(uid)) def _unsubscribe(self): """ Unsubscribe the plugin from the Sonos Broker """ self._logger.debug('unsubscribing from sonos broker server ...') self._send_cmd(SonosCommand.unsubscribe(self._lan_ip, self._listen_port)) def stop(self): """ Will be executed, if Smarthome.py receives a terminate signal """ # try to unsubscribe the plugin from the Sonos Broker self._unsubscribe() self.alive = False def _resolve_uid(self, item): uid = None if 'volume_dpt3.helper' in item._name: parent_item = item.return_parent().return_parent().return_parent() else: parent_item = item.return_parent() if (parent_item is not None) and ('sonos_uid' in parent_item.conf): uid = parent_item.conf['sonos_uid'].lower() else: self._logger.warning("sonos: could not resolve sonos_uid".format(item)) return uid def parse_item(self, item): if 'sonos_recv' in item.conf: uid = self._resolve_uid(item) if uid is None: return None attr = item.conf['sonos_recv'] self._logger.debug("sonos: {} receives updates by {}".format(item, attr)) if not uid in sonos_speaker: sonos_speaker[uid] = SonosSpeaker() attr_list = getattr(sonos_speaker[uid], attr) if not item in attr_list: attr_list.append(item) if 'sonos_send' in item.conf: uid = self._resolve_uid(item) if uid is None: return None attr = item.conf['sonos_send'] self._logger.debug("sonos: {} is send to {}".format(item, attr)) return self._update_item if self.has_iattr(item.conf, 'sonos_volume_dpt3'): if not self.has_iattr(item.conf, 'sonos_vol_step'): item.conf['sonos_vol_step'] = self._dpt3_vol_step self._logger.debug("Sonos: no sonos_vol_step defined, using default value {step}.". format(step=self._dpt3_vol_step)) if not self.has_iattr(item.conf, 'sonos_vol_time'): item.conf['sonos_vol_time'] = self._dpt3_vol_time self._logger.debug("Sonos: no sonos_vol_time defined, using default value {time}.". format(time=self._dpt3_vol_time)) if not self.has_iattr(item.conf, 'sonos_vol_max'): item.conf['sonos_vol_max'] = self._dpt3_vol_max self._logger.debug("Sonos: no sonos_vol_max defined, using default value {max}.". format(max=self._dpt3_vol_max)) return self._handle_volume_dpt3 return None def _handle_volume_dpt3(self, item, caller=None, source=None, dest=None): self._logger.debug(caller) if caller != 'Sonos': volume_helper = None volume_item = item.return_parent() if volume_item is None: self._logger.warning("Sonos: no parent volume item found for volume_dpt3 item!") return dpt3_helper_name = '{}.helper'.format(item._name) for child in item.return_children(): if child._name == dpt3_helper_name: volume_helper = child if volume_helper is None: self._logger.warning("Sonos: no child helper item found for volume_dpt3 item!") return volume_helper(volume_item()) vol_step = int(item.conf['sonos_vol_step']) vol_time = int(item.conf['sonos_vol_time']) vol_max = int(item.conf['sonos_vol_max']) if item()[1] == 1: if item()[0] == 1: # up volume_helper.fade(vol_max, vol_step, vol_time) else: # down volume_helper.fade(0-vol_step, vol_step, vol_time) else: volume_helper(int(volume_helper() + 1)) volume_helper(int(volume_helper() - 1)) def parse_logic(self, logic): pass def _update_item(self, item, caller=None, source=None, dest=None): if caller != 'Sonos': value = item() if 'sonos_send' in item.conf: uid = self._resolve_uid(item) if not uid: return None command = item.conf['sonos_send'] cmd = '' if command == 'mute': if isinstance(value, bool): group_item_name = '{}.group_command'.format(item._name) group_command = 0 for child in item.return_children(): if child._name.lower() == group_item_name.lower(): group_command = child() break cmd = self._command.mute(uid, value, group_command) if command == 'led': if isinstance(value, bool): group_item_name = '{}.group_command'.format(item._name) group_command = 0 for child in item.return_children(): if child._name.lower() == group_item_name.lower(): group_command = child() break cmd = self._command.led(uid, value, group_command) if command == 'play': if isinstance(value, bool): cmd = self._command.play(uid, value) if command == 'pause': if isinstance(value, bool): cmd = self._command.pause(uid, value) if command == 'stop': if isinstance(value, bool): cmd = self._command.stop(uid, value) if command == 'volume': if isinstance(value, int): group_item_name = '{}.group_command'.format(item._name) group_command = 0 for child in item.return_children(): if child._name.lower() == group_item_name.lower(): group_command = child() break value = 0 if value < 0 else value cmd = self._command.volume(uid, value, group_command) if command == 'max_volume': if isinstance(value, int): group_item_name = '{}.group_command'.format(item._name) group_command = 0 for child in item.return_children(): if child._name.lower() == group_item_name.lower(): group_command = child() break cmd = self._command.max_volume(uid, value, group_command) if command == 'bass': if isinstance(value, int): group_item_name = '{}.group_command'.format(item._name) group_command = 0 for child in item.return_children(): if child._name.lower() == group_item_name.lower(): group_command = child() break cmd = self._command.bass(uid, value, group_command) if command == 'balance': if isinstance(value, int): group_item_name = '{}.group_command'.format(item._name) group_command = 0 for child in item.return_children(): if child._name.lower() == group_item_name.lower(): group_command = child() break cmd = self._command.balance(uid, value, group_command) if command == 'treble': if isinstance(value, int): group_item_name = '{}.group_command'.format(item._name) group_command = 0 for child in item.return_children(): if child._name.lower() == group_item_name.lower(): group_command = child() break cmd = self._command.treble(uid, value, group_command) if command == 'nightmode': if isinstance(value, bool): cmd = self._command.nightmode(uid, value) if command == 'loudness': if isinstance(value, bool): group_item_name = '{}.group_command'.format(item._name) group_command = 0 for child in item.return_children(): if child._name.lower() == group_item_name.lower(): group_command = child() break cmd = self._command.loudness(uid, value, group_command) if command == 'playmode': value = value.lower().strip('\'').strip('\"') if value in ['normal', 'shuffle_norepeat', 'shuffle', 'repeat_all']: cmd = self._command.playmode(uid, value) else: self._logger.warning( "Ignoring PLAYMODE command. Value {value} not a valid paramter!".format(value=value)) if command == 'next': cmd = self._command.next(uid) if command == 'previous': cmd = self._command.previous(uid) if command == 'play_uri': cmd = self._command.play_uri(uid, value) if command == 'play_tunein': cmd = self._command.play_tunein(uid, value) if command == 'play_snippet': volume_item_name = '{}.volume'.format(item._name) group_item_name = '{}.group_command'.format(item._name) fade_item_name = '{}.fade_in'.format(item._name) volume = -1 group_command = 0 fade_in = 0 for child in item.return_children(): if child._name.lower() == volume_item_name.lower(): volume = child() if child._name.lower() == group_item_name.lower(): group_command = child() if child._name.lower() == fade_item_name.lower(): fade_in = child() cmd = self._command.play_snippet(uid, value, volume, group_command, fade_in) if command == 'play_tts': volume_item_name = '{}.volume'.format(item._name) language_item_name = '{}.language'.format(item._name) group_item_name = '{}.group_command'.format(item._name) force_item_name = '{}.force_stream_mode'.format(item._name) fade_item_name = '{}.fade_in'.format(item._name) volume = -1 language = 'de' group_command = 0 force_stream_mode = 0 fade_in = 0 for child in item.return_children(): if child._name.lower() == volume_item_name.lower(): volume = child() if child._name.lower() == language_item_name.lower(): language = child() if child._name.lower() == group_item_name.lower(): group_command = child() if child._name.lower() == force_item_name.lower(): force_stream_mode = child() if child._name.lower() == fade_item_name.lower(): fade_in = child() if child._name.lower() == fade_item_name.lower(): fade_in = child() cmd = self._command.play_tts(uid, value, language, volume, group_command, fade_in, force_stream_mode) if command == 'load_sonos_playlist': clear_item_name = '{}.clear_queue'.format(item._name) play_item_name = '{}.play_after_insert'.format(item._name) clear_queue = 0 play_after_insert = 0 for child in item.return_children(): if child._name.lower() == clear_item_name.lower(): clear_queue = child() if child._name.lower() == play_item_name.lower(): play_after_insert = child() cmd = self._command.load_sonos_playlist(uid, value, play_after_insert, clear_queue) if command == 'seek': if not re.match(r'^[0-9][0-9]?:[0-9][0-9]:[0-9][0-9]$', value): self._logger.warning('invalid timestamp for sonos seek command, use HH:MM:SS format') cmd = None else: cmd = self._command.seek(uid, value) if command == 'current_state': cmd = self._command.current_state(uid) if command == 'join': cmd = self._command.join(uid, value) if command == 'unjoin': play_item_name = '{}.play'.format(item._name) play = 0 for child in item.return_children(): if child._name.lower() == play_item_name.lower(): play = child() break cmd = self._command.unjoin(uid, play) if command == 'partymode': cmd = self._command.partymode(uid) if command == 'volume_up': group_item_name = '{}.group_command'.format(item._name) group_command = 0 for child in item.return_children(): if child._name.lower() == group_item_name.lower(): group_command = child() break cmd = self._command.volume_up(uid, group_command) if command == 'volume_down': group_item_name = '{}.group_command'.format(item._name) group_command = 0 for child in item.return_children(): if child._name.lower() == group_item_name.lower(): group_command = child() break cmd = self._command.volume_down(uid, group_command) if command == 'clear_queue': if value in ['y', '1', 'Y', 1, 'yes']: cmd = self._command.clear_queue(uid) if command == 'wifi_state': if isinstance(value, bool): persistent_item_name = '{}.persistent'.format(item._name) persistent = 0 for child in item.return_children(): if child._name.lower() == persistent_item_name.lower(): if value != 0 and persistent == 1: self._logger.warning("command wifi_state: persistent parameter with value '1' will" "only affect wifi_state with value '1' (the wifi interface will" "remain deactivated after reboot). Ignoring 'persistent' " "parameter.") else: persistent = child() break cmd = self._command.wifi_state(uid, value, persistent) if cmd: self._send_cmd(cmd) return return None def _send_cmd(self, payload): try: self._logger.debug("Sending request: {0}".format(payload)) headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} response = requests.post(self._broker_url, data=json.dumps(payload), headers=headers) html_start = "