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 Plugin: improve dpt3 handling, make plugin restartable #975

Merged
merged 4 commits into from
Dec 8, 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
89 changes: 53 additions & 36 deletions sonos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2994,7 +2994,7 @@ class Sonos(SmartPlugin):
"""
Main class of the Plugin. Does all plugin specific stuff
"""
PLUGIN_VERSION = "1.8.8"
PLUGIN_VERSION = "1.8.9"

def __init__(self, sh):
"""Initializes the plugin."""
Expand All @@ -3007,14 +3007,14 @@ def __init__(self, sh):
self._tts = self.get_parameter_value("tts")
self._snippet_duration_offset = float(self.get_parameter_value("snippet_duration_offset"))
self._discover_cycle = self.get_parameter_value("discover_cycle")
self.webif_pagelength = self.get_parameter_value('webif_pagelength')
local_webservice_path = self.get_parameter_value("local_webservice_path")
local_webservice_path_snippet = self.get_parameter_value("local_webservice_path_snippet")
webservice_ip = self.get_parameter_value("webservice_ip")
webservice_port = self.get_parameter_value("webservice_port")
speaker_ips = self.get_parameter_value("speaker_ips")
self._pause_item_path = self.get_parameter_value('pause_item')
except KeyError as e:
Copy link
Member

Choose a reason for hiding this comment

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

Ist keine deiner Änderungen, aber wenn die Parameter in plugin.yaml definiert sind, sind sie immer vorhanden, wenn sie required oder defaults vorgegeben sind.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

so ist die aktuelle Definition:

  pause_item:
    type: str
    default: ''
    description:
      de: 'Item, um die Ausführung des Plugins zu steuern'
      en: 'item for controlling plugin execution'

Somit muss das Plugin auch laufen, wenn der parameter nicht gesetzt ist.

Copy link
Contributor Author

@sisamiwe sisamiwe Nov 25, 2024

Choose a reason for hiding this comment

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

@Morg42
Schau mal in Zeile 3059. Ich glaube da liegt der Fehler.

Es muss dort sicher heißen:
if self._pause_item_path:

oder man muss in der init noch den self._pause_item = None setzen.

Nach der Methode init wird doch parse_item abgearbeitet. Nur wenn self._pause_item_path gesetzt ist, wir auch self._pause_item gesetzt, wenn nicht gibt es diese Variable nicht. Somit kann die auch bei, ausführen von run nicht gefunden werden und das plugin hängt, oder?

Copy link
Member

Choose a reason for hiding this comment

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

Nee. Die Variable gibt es schon, die kommt direkt aus dem SmartPlugin.

Copy link
Member

Choose a reason for hiding this comment

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

Und zu "muss auch laufen" - die Korrekturen in SmartPlugin sind erst im 1.10er branch vorhanden, mit 1.9.x gibt es Fehler.

self.logger.critical(f"Plugin '{self.get_shortname()}': Inconsistent plugin (invalid metadata definition: {e} not defined)")
self.logger.critical(f"Plugin '{self.get_fullname()}': Inconsistent plugin (invalid metadata definition: {e} not defined)")
self._init_complete = False
return

Expand Down Expand Up @@ -3054,6 +3054,10 @@ def __init__(self, sh):

def run(self):
self.logger.debug("Run method called")

# let the plugin change the state of pause_item
if self._pause_item:
self._pause_item(False, self.get_fullname())

# do initial speaker discovery and set scheduler
self._discover()
Expand All @@ -3063,13 +3067,16 @@ def run(self):
self.alive = True

def stop(self):
self.logger.debug("Stop method called")
self.logger.dbghigh(self.translate("Methode '{method}' aufgerufen", {'method': 'stop()'}))

# let the plugin change the state of pause_item
if self._pause_item:
self._pause_item(True, self.get_fullname())

if self.webservice:
self.webservice.stop()

if self.scheduler_get('sonos_discover_scheduler'):
self.scheduler_remove('sonos_discover_scheduler')
self.scheduler_remove_all()

for uid, speaker in sonos_speaker.items():
speaker.dispose()
Expand All @@ -3084,6 +3091,12 @@ def parse_item(self, item: Items) -> object:
:param item: item to parse
:return: update function or None
"""

if item.property.path == self._pause_item_path:
self.logger.debug(f'pause item {item.property.path} registered')
self._pause_item = item
self.add_item(item, updating=True)
return self.update_item

item_config = dict()

Expand All @@ -3100,7 +3113,7 @@ def parse_item(self, item: Items) -> object:

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

# to make code smaller, map sonos_cmd value to the Speaker property by name
item_attribute = self.get_iattr_value(item.conf, 'sonos_recv')
Expand Down Expand Up @@ -3147,22 +3160,22 @@ def parse_item(self, item: Items) -> object:
self.logger.warning("volume_dpt3 item has no volume parent item. Ignoring!")
return

# make sure there is a child helper item
child_helper = None
# make sure there is a child volume helper item
volume_helper_item = None
for child in item.return_children():
if self.has_iattr(child.conf, 'sonos_attrib'):
if self.get_iattr_value(child.conf, 'sonos_attrib').lower() == 'dpt3_helper':
child_helper = child
volume_helper_item = child
break

if child_helper is None:
self.logger.warning("volume_dpt3 item has no helper item. Ignoring!")
if volume_helper_item is None:
self.logger.warning("volume_dpt3 item has no volume helper item. Ignoring!")
return

dpt3_step = self.get_iattr_value(item.conf, 'sonos_dpt3_step')
dpt3_time = self.get_iattr_value(item.conf, 'sonos_dpt3_time')

item_config.update({'volume_item': parent_item, 'helper': child_helper, 'dpt3_step': dpt3_step, 'dpt3_time': dpt3_time})
item_config.update({'volume_item': parent_item, 'helper_item': volume_helper_item, 'dpt3_step': dpt3_step, 'dpt3_time': dpt3_time})
self.add_item(item, config_data_dict=item_config, updating=True)
return self._handle_dpt3

Expand Down Expand Up @@ -3226,36 +3239,31 @@ def play_alert_all_speakers(self, alert_uri, speaker_list=[], alert_volume=20, a
zone.snap.restore(fade=fade_back)

def _handle_dpt3(self, item, caller=None, source=None, dest=None):
if caller != self.get_shortname():
"""Handle relative volumen change via a received relative dim command (dpt3) by making use of internal fadeing"""

if caller != self.get_fullname():
item_config = self.get_item_config(item)
volume_item = item_config['volume_item']
volume_helper = item_config['helper']
volume_helper_item = 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:
vol_max = 100

current_volume = int(volume_item())
if current_volume < 0:
current_volume = 0
if current_volume > 100:
current_volume = 100

volume_helper(current_volume)
vol_max = max(0, self._resolve_max_volume_command(item)) or 100
_current_volume = max(0, min(100, int(volume_item())))
volume_helper_item(_current_volume, self.get_fullname())

if item()[1] == 1:
self.logger.debug(f"Starte relative Lautstärkeänderung.")
if item()[0] == 1:
# up
volume_helper.fade(vol_max, vol_step, vol_time)
self.logger.debug(f"erhöhe Lautstärke mit {vol_step} Stufe(n) pro {vol_time}s")
volume_helper_item.fade(vol_max, vol_step, vol_time)
else:
# down
volume_helper.fade(0 - vol_step, vol_step, vol_time)
self.logger.debug(f"reduziere Lautstärke mit {vol_step} Stufe(n) pro {vol_time}s")
volume_helper_item.fade(0 - vol_step, vol_step, vol_time)
else:
volume_helper(int(volume_helper() + 1))
volume_helper(int(volume_helper() - 1))
self.logger.debug(f"Stoppe relative Lautstärkeänderung.")
volume_helper_item(int(volume_helper_item()), self.get_fullname())

def _check_webservice_ip(self, webservice_ip: str) -> bool:
if not webservice_ip == '' and not webservice_ip == '0.0.0.0':
Expand Down Expand Up @@ -3283,7 +3291,7 @@ def _check_webservice_port(self, webservice_port: int) -> bool:
else:
self.logger.error(f"Your webservice_port parameter is invalid. '{webservice_port}' is not within port range 1024-65535. TTS disabled!")
return False

return True

def _check_local_webservice_path(self, local_webservice_path: str) -> bool:
Expand Down Expand Up @@ -3457,10 +3465,19 @@ def update_item(self, item: Items, caller: object, source: object, dest: object)
:param source: if given it represents the source
:param dest: if given it represents the dest
"""

if self.alive and caller != self.get_fullname():

self.logger.debug(f"update_item called for {item.path()} with value {item()}")
# check for pause item
if item is self._pause_item and caller != self.get_fullname():
self.logger.debug(f'pause item changed to {item()}')
if item() and self.alive:
self.stop()
elif not item() and not self.alive:
self.run()
return

# check for sonos item
if self.alive and caller != self.get_fullname():
self.logger.debug(f"update_item called for {item.path()} with value {item()} {caller=}")
item_config = self.get_item_config(item)
command = item_config.get('sonos_send', '').lower()
uid = item_config.get('uid')
Expand Down Expand Up @@ -3780,7 +3797,7 @@ def _discover(self, force: bool = False) -> None:
# sonos_speaker[uid].check_subscriptions()
else:
self.logger.warning(f"Initializing new speaker with uid={uid} and ip={zone.ip_address}")
_initialize_speaker(uid, self.logger, self.get_shortname())
_initialize_speaker(uid, self.logger, self.get_fullname())
sonos_speaker[uid].soco = zone

sonos_speaker[uid].is_initialized = True
Expand Down
17 changes: 12 additions & 5 deletions sonos/plugin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ plugin:
documentation: https://github.com/smarthomeNG/plugins/blob/master/sonos/README.md
support: https://knx-user-forum.de/forum/supportforen/smarthome-py/25151-sonos-anbindung

version: 1.8.8 # Plugin version
sh_minversion: '1.5.1' # minimum shNG version to use this plugin
py_minversion: '3.8' # minimum Python version to use for this plugin
version: 1.8.9 # Plugin version
sh_minversion: '1.10.0.3' # minimum shNG version to use this plugin
py_minversion: '3.9' # minimum Python version to use for this plugin
multi_instance: False # plugin supports multi instance
restartable: unknown
restartable: yes
classname: Sonos # class containing the plugin

parameters:
Expand Down Expand Up @@ -61,7 +61,7 @@ parameters:
If "tts" is enabled and this option is not set, the value "local_webservice_path" is used for the audio snippet path.'

webservice_ip:
type: ip
type: ipv4
description:
de: '(optional) Für TTS und die Audio-Snippet-Funktionalität wird ein simpler Webservice gestartet. \n
Die IP-Adresse wird per default automatisch ermittelt, kann hier aber manuell gesetzt werden.'
Expand All @@ -85,6 +85,13 @@ parameters:
de: "(optional) Verlängert die Dauer von Snippet Audio Dateien um einen festen Offset in Sekunden."
en: "(optional) Extend snippet duration by a fixed offset specified in seconds"

pause_item:
type: str
default: ''
description:
de: 'Item, um die Ausführung des Plugins zu steuern'
en: 'item for controlling plugin execution'

item_attributes:
sonos_uid:
type: str
Expand Down