Skip to content

Commit

Permalink
Merge pull request #971 from sisamiwe/dev-sonos1
Browse files Browse the repository at this point in the history
SONOS: introduce item handling from smartplugin
  • Loading branch information
Morg42 authored Nov 17, 2024
2 parents f1993ec + 2fa3b4f commit 5a00ce3
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 96 deletions.
162 changes: 85 additions & 77 deletions sonos/__init__.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def renew_error_callback(exception): # events_twisted: failure
# Redundant, as the exception will be logged by the events module
self.logger.error(msg)

# ToDo possible improvement: Do not do periodic renew but do propper disposal on renew failure here instead. sub.renew(requested_timeout=10)
# ToDo possible improvement: Do not do periodic renew but do proper disposal on renew failure here instead. sub.renew(requested_timeout=10)


class SubscriptionHandler(object):
Expand All @@ -201,7 +201,7 @@ def __init__(self, endpoint, service, logger, threadName):
def subscribe(self):
self.logger.dbglow(f"start subscribe for endpoint {self._endpoint}")
if 'eventAvTransport' in self._threadName:
self.logger.dbghigh(f"subscribe(): endpoint av envent detected. Enabling debugging logs")
self.logger.dbghigh(f"subscribe(): endpoint av event detected. Enabling debugging logs")
debug = 1
else:
debug = 0
Expand Down Expand Up @@ -254,7 +254,7 @@ def subscribe(self):
def unsubscribe(self):
self.logger.dbglow(f"unsubscribe(): start for endpoint {self._endpoint}")
if 'eventAvTransport' in self._threadName:
self.logger.dbghigh(f"unsubscribe: endpoint av envent detected. Enabling debugging logs")
self.logger.dbghigh(f"unsubscribe: endpoint av event detected. Enabling debugging logs")
debug = 1
else:
debug = 0
Expand Down Expand Up @@ -283,11 +283,11 @@ def unsubscribe(self):
self.logger.dbghigh(f"unsubscribe(): Thread joined for endpoint {self._endpoint}")

if not self._thread.is_alive():
self.logger.dbglow("Thread killed for enpoint {self._endpoint}")
self.logger.dbglow("Thread killed for endpoint {self._endpoint}")
if debug:
self.logger.dbghigh(f"Thread killed for endpoint {self._endpoint}")
else:
self.logger.warning("unsubscibe(): Error, thread is still alive after termination (join timed-out)")
self.logger.warning("unsubscribe(): Error, thread is still alive after termination (join timed-out)")
self._thread = None
self.logger.info(f"Event {self._endpoint} thread terminated")

Expand All @@ -297,7 +297,6 @@ def unsubscribe(self):
if debug:
self.logger.dbghigh(f"unsubscribe(): {self._endpoint}: lock released")


@property
def eventSignalIsSet(self):
if self._signal:
Expand Down Expand Up @@ -514,7 +513,6 @@ def subscribe_base_events(self):
# Important note:
# av event is not subscribed here because it has special handling in function zone group event.
pass


def refresh_static_properties(self) -> None:
"""
Expand Down Expand Up @@ -707,12 +705,12 @@ def _av_transport_event(self, sub_handler: SubscriptionHandler) -> None:

self.logger.dbghigh(f"_av_transport_event: {self.uid}: av transport event handler active.")
while not sub_handler.signal.wait(1):
# self.logger.dbglow(f"_av_transport_event: {self.uid}: start try")
# self.logger.dbglow(f"_av_transport_event: {self.uid}: start try")

try:
event = sub_handler.event.events.get(timeout=0.5)
except Empty:
#self.logger.dbglow(f"av_transport_event: got empty exception, which is normal")
# self.logger.dbglow(f"av_transport_event: got empty exception, which is normal")
pass
except Exception as e:
self.logger.error(f"_av_tranport_event: Exception during events.get(): {e}")
Expand Down Expand Up @@ -1108,7 +1106,7 @@ def loudness(self) -> bool:
@loudness.setter
def loudness(self, loudness: bool) -> None:
"""
Setter for loudnes (internal)
Setter for loudness (internal)
:param loudness: True or False
:rtype: None
:return: None
Expand Down Expand Up @@ -1259,7 +1257,7 @@ def volume(self, value: int) -> None:
def _check_max_volume_exceeded(self, volume: int, max_volume: int) -> bool:
"""
Checks if the volume exceeds a maximum volume value.
:param volume: volme
:param volume: volume
:param max_volume: maximum volume
:return: 'True' if volume exceeds maximum volume, 'False# otherwise.
"""
Expand Down Expand Up @@ -1442,21 +1440,20 @@ def zone_group_members(self, value: list) -> None:
pass
else:
# Register AV event for coordinator speakers:
#self.logger.dbglow(f"Un/Subscribe av event for uid '{self.uid}' in fct zone_group_members")
# self.logger.dbglow(f"Un/Subscribe av event for uid '{self.uid}' in fct zone_group_members")

active = member.av_subscription.subscriptionThreadIsActive
is_subscribed = member.av_subscription.is_subscribed
self.logger.dbghigh(f"zone_group_members(): Subscribe av event for uid '{self.uid}': Status before measure: AV Thread is {active}, subscription is {is_subscribed}, Eventflag: {member.av_subscription.eventSignalIsSet}")

if active == False:
if active is False:
self.logger.dbghigh(f"zone_group_members: Subscribe av event for uid '{self.uid}' because thread is not active")
#member.av_subscription.unsubscribe()
#
# Workaround:
# member.av_subscription.update_endpoint(endpoint=self._av_transport_event)
member.av_subscription.subscribe()
self.logger.dbghigh(f"zone_group_members: Subscribe av event for uid '{self.uid}': Status after measure: AV thread is {member.av_subscription.subscriptionThreadIsActive}, subscription {member.av_subscription.is_subscribed}, Eventflag: {member.av_subscription.eventSignalIsSet}")


@property
def streamtype(self) -> str:
Expand Down Expand Up @@ -1906,7 +1903,7 @@ def is_coordinator(self) -> bool:
def is_coordinator(self, value: bool) -> None:
"""
is_coordinator setter
:param value: 'True' to indicate that the speker is the coordiantor of the group, otherwise 'False'
:param value: 'True' to indicate that the speaker is the coordinator of the group, otherwise 'False'
"""
self._is_coordinator = value
for item in self.is_coordinator_items:
Expand Down Expand Up @@ -2511,7 +2508,6 @@ def _play_radio(self, station_name: str, music_service: str = 'TuneIn', start: b
self.soco.play_uri(uri=uri, meta=metadata, title=the_station.title, start=start, force_radio=True)
return True, ""


def play_sharelink(self, url: str, start: bool = True) -> None:
"""
Plays a sharelink from a given url
Expand Down Expand Up @@ -2998,7 +2994,7 @@ class Sonos(SmartPlugin):
"""
Main class of the Plugin. Does all plugin specific stuff
"""
PLUGIN_VERSION = "1.8.7"
PLUGIN_VERSION = "1.8.8"

def __init__(self, sh):
"""Initializes the plugin."""
Expand Down Expand Up @@ -3030,7 +3026,6 @@ def __init__(self, sh):
self._uid_lookup_levels = 4 # iterations of return_parent() on lookup for item uid
self._speaker_ips = [] # list of fixed speaker ips
self.zones = {} # dict to hold zone information via soco objects
self.item_list = [] # list of all items, used by / linked to that plugin
self.alive = False # plugin alive property
self.webservice = None # webservice thread

Expand Down Expand Up @@ -3089,43 +3084,56 @@ def parse_item(self, item: Items) -> object:
:param item: item to parse
:return: update function or None
"""
uid = None

item_config = dict()

# handling sonos_recv and sonos_send
if self.has_iattr(item.conf, 'sonos_recv') or self.has_iattr(item.conf, 'sonos_send'):
self.logger.debug(f"parse item: {item.property.path}")
# get uid from parent item
uid = self._resolve_uid(item)
if not uid:
self.logger.error(f"No uid found for {item.property.path}.")
return

item_config.update({'uid': uid})

if self.has_iattr(item.conf, 'sonos_recv'):
# create Speaker instance if not exists
_initialize_speaker(uid, self.logger, self.get_shortname())
if self.has_iattr(item.conf, 'sonos_recv'):
# create Speaker instance if not exists
_initialize_speaker(uid, self.logger, self.get_shortname())

# to make code smaller, map sonos_cmd value to the Speaker property by name
item_attribute = self.get_iattr_value(item.conf, 'sonos_recv')
list_name = f"{item_attribute}_items"
try:
attr = getattr(sonos_speaker[uid], list_name)
self.logger.debug(f"Adding item {item.property.path} to {uid}: list {list_name}")
attr.append(item)
if item not in self.item_list:
self.item_list.append(item)
except Exception:
self.logger.warning(f"No item list available for sonos_cmd '{item_attribute}'.")

if self.has_iattr(item.conf, 'sonos_send'):
self.logger.debug(f"Item {item.property.path} registered to 'sonos_send' commands.")
if item not in self.item_list:
self.item_list.append(item)
return self.update_item

# some special handling for dpt3 volume
if self.has_iattr(item.conf, 'sonos_attrib'):
if self.get_iattr_value(item.conf, 'sonos_attrib') != 'vol_dpt3':
if item not in self.item_list:
self.item_list.append(item)
# to make code smaller, map sonos_cmd value to the Speaker property by name
item_attribute = self.get_iattr_value(item.conf, 'sonos_recv')
list_name = f"{item_attribute}_items"
try:
attr = getattr(sonos_speaker[uid], list_name)
self.logger.debug(f"Adding item {item.property.path} to {uid}: list {list_name}")
attr.append(item)
item_config.update({'sonos_recv': item_attribute})
self.logger.debug(f"Item {item.property.path} registered to 'sonos_send' commands with '{item_attribute}'.")
except Exception:
self.logger.warning(f"No item list available for sonos_cmd '{item_attribute}'.")

if self.has_iattr(item.conf, 'sonos_send'):
item_attribute = self.get_iattr_value(item.conf, 'sonos_send')
item_config.update({'sonos_send': item_attribute})
self.logger.debug(f"Item {item.property.path} registered to 'sonos_send' commands with '{item_attribute}'.")

if 'sonos_recv' in item_config or 'sonos_send' in item_config:
self.add_item(item, config_data_dict=item_config, updating=True)

if 'sonos_send' in item_config:
return self.update_item

# handling sonos_attrib incl some special handling for dpt3 volume
elif self.has_iattr(item.conf, 'sonos_attrib'):
uid = self._resolve_uid(item)
item_config.update({'uid': uid})
item_attribute = self.get_iattr_value(item.conf, 'sonos_attrib')

if item_attribute != 'vol_dpt3':
item_config.update({'sonos_attrib': item_attribute})
self.add_item(item, config_data_dict=item_config, updating=True)
return

# check, if a volume parent item exists
Expand All @@ -3139,8 +3147,6 @@ def parse_item(self, item: Items) -> object:
self.logger.warning("volume_dpt3 item has no volume parent item. Ignoring!")
return

item.conf['volume_parent'] = parent_item

# make sure there is a child helper item
child_helper = None
for child in item.return_children():
Expand All @@ -3153,21 +3159,14 @@ def parse_item(self, item: Items) -> object:
self.logger.warning("volume_dpt3 item has no helper item. Ignoring!")
return

item.conf['helper'] = child_helper

if not self.has_iattr(item.conf, 'sonos_dpt3_step'):
item.conf['sonos_dpt3_step'] = self._sonos_dpt3_step
self.logger.debug(f"No sonos_dpt3_step defined, using default value {self._sonos_dpt3_step}.")

if not self.has_iattr(item.conf, 'sonos_dpt3_time'):
item.conf['sonos_dpt3_time'] = self._sonos_dpt3_time
self.logger.debug(f"No sonos_dpt3_time defined, using default value {self._sonos_dpt3_time}.")
dpt3_step = self.get_iattr_value(item.conf, 'sonos_dpt3_step')
dpt3_time = self.get_iattr_value(item.conf, 'sonos_dpt3_time')

if item not in self.item_list:
self.item_list.append(item)
item_config.update({'volume_item': parent_item, 'helper': child_helper, 'dpt3_step': dpt3_step, 'dpt3_time': dpt3_time})
self.add_item(item, config_data_dict=item_config, updating=True)
return self._handle_dpt3

def play_alert_all_speakers(self, alert_uri, speaker_list = [], alert_volume=20, alert_duration=0, fade_back=False):
def play_alert_all_speakers(self, alert_uri, speaker_list=[], alert_volume=20, alert_duration=0, fade_back=False):
"""
Demo function using soco.snapshot across multiple Sonos players.
Expand Down Expand Up @@ -3226,11 +3225,14 @@ def play_alert_all_speakers(self, alert_uri, speaker_list = [], alert_volume=20,
self.logger.warning(f"Debug: restoring {zone.player_name}")
zone.snap.restore(fade=fade_back)


def _handle_dpt3(self, item, caller=None, source=None, dest=None):
if caller != self.get_shortname():
volume_item = self.get_iattr_value(item.conf, 'volume_parent')
volume_helper = self.get_iattr_value(item.conf, 'helper')

item_config = self.get_item_config(item)
volume_item = item_config['volume_item']
volume_helper = item_config['helper']
vol_step = item_config['dpt3_step']
vol_time = item_config['dpt3_time']
vol_max = self._resolve_max_volume_command(item)

if vol_max < 0:
Expand All @@ -3243,8 +3245,6 @@ def _handle_dpt3(self, item, caller=None, source=None, dest=None):
current_volume = 100

volume_helper(current_volume)
vol_step = int(item.conf['sonos_dpt3_step'])
vol_time = int(item.conf['sonos_dpt3_time'])

if item()[1] == 1:
if item()[0] == 1:
Expand Down Expand Up @@ -3293,7 +3293,7 @@ def _check_local_webservice_path(self, local_webservice_path: str) -> bool:
self.logger.warning(f"Mandatory path for local webserver for TTS not given in Plugin parameters. TTS disabled!")
return False

# if path is given, check avilability, create and check access rights
# if path is given, check availability, create and check access rights
try:
os.makedirs(local_webservice_path, exist_ok=True)
except OSError:
Expand Down Expand Up @@ -3322,7 +3322,7 @@ def _check_local_webservice_path_snippet(self, local_webservice_path_snippet: st
self._local_webservice_path_snippet = self._local_webservice_path
return True

# if path is given, check avilability, create and check access rights
# if path is given, check availability, create and check access rights
try:
os.makedirs(local_webservice_path_snippet, exist_ok=True)
except OSError:
Expand Down Expand Up @@ -3407,16 +3407,14 @@ def _parse_speaker_ips(self, speaker_ips: list) -> list:
# return unique items in list
return utils.unique_list(self._speaker_ips)


def debug_speaker(self, uid):
self.logger.warning(f"debug_speaker: Starting function for uid {uid}")
#sonos_speaker[uid].set_stop()
# sonos_speaker[uid].set_stop()
self.logger.warning(f"debug_speaker: check sonos_speaker[uid].av.subscription: {sonos_speaker[uid].av_subscription}")
# Event objekt is not callable:
#sonos_speaker[uid]._av_transport_event(sonos_speaker[uid].av_subscription)
# sonos_speaker[uid]._av_transport_event(sonos_speaker[uid].av_subscription)
self.logger.warning(f"debug_speaker: av_subscription: thread active {sonos_speaker[uid].av_subscription.subscriptionThreadIsActive}, eventSignal: {sonos_speaker[uid].av_subscription.eventSignalIsSet}")


def get_soco_version(self) -> str:
"""
Get version of used Soco and return it
Expand Down Expand Up @@ -3461,9 +3459,15 @@ def update_item(self, item: Items, caller: object, source: object, dest: object)
"""

if self.alive and caller != self.get_fullname():
if self.has_iattr(item.conf, 'sonos_send'):
uid = self._resolve_uid(item)
command = self.get_iattr_value(item.conf, "sonos_send").lower()

self.logger.debug(f"update_item called for {item.path()} with value {item()}")
item_config = self.get_item_config(item)
command = item_config.get('sonos_send', '').lower()
uid = item_config.get('uid')

self.logger.debug(f"{uid=}, {command=}, ")

if command and uid:

if command == "play":
sonos_speaker[uid].set_play() if item() else sonos_speaker[uid].set_pause()
Expand Down Expand Up @@ -3634,9 +3638,11 @@ def _resolve_group_command(self, item: Items) -> bool:
:return: 'True' or 'False' (whether the command should execute as a group command or not)
"""

item_config = self.get_item_config(item)

# special handling for dpt_volume
if self.get_iattr_value(item.conf, 'sonos_attrib') == 'vol_dpt3':
group_item = self.get_iattr_value(item.conf, 'volume_parent')
if item_config.get('sonos_attrib', '') == 'vol_dpt3':
group_item = item_config['volume_item']
else:
group_item = item

Expand All @@ -3653,8 +3659,10 @@ def _resolve_max_volume_command(self, item: Items) -> int:
:return:
"""

if self.get_iattr_value(item.conf, 'sonos_attrib') == 'vol_dpt3':
volume_item = self.get_iattr_value(item.conf, 'volume_parent')
item_config = self.get_item_config(item)

if item_config.get('sonos_attrib', '') == 'vol_dpt3':
volume_item = item_config['volume_item']
else:
volume_item = item

Expand Down
Loading

0 comments on commit 5a00ce3

Please sign in to comment.