Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SONOS: introduce item handling from smartplugin #971

Merged
merged 2 commits into from
Nov 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mit updating=True werden Items immer auf updating gesetzt, bei sonos_recv und bei sonos_send.

Wenn das - siehe nächste Zeile - nur bei sonos_send passieren soll, hier updating=True weglassen, das wird für sonos_send durch die Item-Klasse automatisch gesetzt


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
Loading