From b97e9f07b924fea2cb7004483059d6bcb018d420 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 17 Oct 2024 00:38:29 +0200 Subject: [PATCH 1/7] lib.item: fader introduce stop_fade, continue_fade, instant_set and update functionality --- lib/item/helpers.py | 60 ++++++++++++++++++++++++++++++----------- lib/item/item.py | 65 ++++++++++++++++++++++++++++++++++++++------- lib/item/items.py | 2 ++ 3 files changed, 102 insertions(+), 25 deletions(-) diff --git a/lib/item/helpers.py b/lib/item/helpers.py index b0424fabf0..d0c989840b 100644 --- a/lib/item/helpers.py +++ b/lib/item/helpers.py @@ -248,23 +248,53 @@ def cache_write(filename, value, cformat=CACHE_FORMAT): ##################################################################### # Fade Method ##################################################################### -def fadejob(item, dest, step, delta, caller=None): +def fadejob(item): if item._fading: return else: item._fading = True - if item._value < dest: - while (item._value + step) < dest and item._fading: - item(item._value + step, 'fader') - item._lock.acquire() - item._lock.wait(delta) - item._lock.release() - else: - while (item._value - step) > dest and item._fading: - item(item._value - step, 'fader') - item._lock.acquire() - item._lock.wait(delta) - item._lock.release() + + # Determine if instant_set is needed + instant_set = item._fadingdetails.get('instant_set', False) + while item._fading: + current_value = item._value + target_dest = item._fadingdetails.get('dest') + fade_step = item._fadingdetails.get('step') + delta_time = item._fadingdetails.get('delta') + caller = item._fadingdetails.get('caller') + + # Determine the direction of the fade (increase or decrease) + if current_value < target_dest: + # If fading upwards, but next step overshoots, set value to target_dest + if (current_value + fade_step) >= target_dest: + fade_value = target_dest + else: + fade_value = current_value + fade_step + elif current_value > target_dest: + # If fading downwards, but next step overshoots, set value to target_dest + if (current_value - fade_step) <= target_dest: + fade_value = target_dest + else: + fade_value = current_value - fade_step + else: + # If the current value has reached the destination, stop fading + break + + # Set the new value at the beginning + if instant_set and item._fading: + item._fadingdetails['value'] = fade_value + item(fade_value, 'Fader', caller) + else: + instant_set = True # Enable instant_set for the next loop iteration + + # Wait for the delta time before continuing to the next step + item._lock.acquire() + item._lock.wait(delta_time) + item._lock.release() + + if fade_value == target_dest: + break + + # Stop fading if item._fading: - item._fading = False - item(dest, 'Fader') + item._fading = False \ No newline at end of file diff --git a/lib/item/item.py b/lib/item/item.py index 8425a1aa69..7452b31e78 100644 --- a/lib/item/item.py +++ b/lib/item/item.py @@ -30,6 +30,7 @@ import json import threading import ast +import re import inspect @@ -294,6 +295,7 @@ def __init__(self, smarthome, parent, path, config, items_instance=None): self._log_rules = {} self._log_text = None self._fading = False + self._fadingdetails = {} self._items_to_trigger = [] self.__last_change = self.shtime.now() self.__last_update = self.__last_change @@ -2326,7 +2328,7 @@ def __trigger_logics(self, source_details=None): def _set_value(self, value, caller, source=None, dest=None, prev_change=None, last_change=None): """ - Set item value, update last aund prev information and perform log_change for item + Set item value, update last and prev information and perform log_change for item :param value: :param caller: @@ -2358,7 +2360,7 @@ def _set_value(self, value, caller, source=None, dest=None, prev_change=None, la self.__updated_by = "{0}:{1}".format(caller, source) self.__triggered_by = "{0}:{1}".format(caller, source) - if caller != "fader": + if caller != "Fader": # log every item change to standard logger, if level is DEBUG # log with level INFO, if 'item_change_log' is set in etc/smarthome.yaml self._change_logger("Item {} = {} via {} {} {}".format(self._path, value, caller, source, dest)) @@ -2369,6 +2371,21 @@ def _set_value(self, value, caller, source=None, dest=None, prev_change=None, la def __update(self, value, caller='Logic', source=None, dest=None, key=None, index=None): + def check_external_change(entry_type, entry_value): + matches = [] + for pattern in entry_value: + regex = re.compile(pattern, re.IGNORECASE) + if regex.match(f'{caller}:{source}'): + if entry_type == "stop_fade": + matches.append(True) # Match in stop_fade, should stop + else: + matches.append(False) # Match in continue_fade, should continue fading + else: + if entry_type == "continue_fade": + matches.append(True) # No match in continue_fade -> we can stop + else: + matches.append(False) # No match in stop_fade -> keep fading + return matches # special handling, if item is a hysteresys item (has a hysteresis_input attribute) if self._hysteresis_input is not None: @@ -2405,21 +2422,48 @@ def __update(self, value, caller='Logic', source=None, dest=None, key=None, inde elif index is not None and self._type == 'list': # Update a list item element (selected by index) value = self.__set_listentry(value, index) + if self._fading: + stop_fade = self._fadingdetails.get("stop_fade") + continue_fade = self._fadingdetails.get("continue_fade") + stopping = check_external_change("stop_fade", stop_fade) if stop_fade else [False] + continuing = check_external_change("continue_fade", continue_fade) if continue_fade else [True] + # If stop_fade is set and there's a match, stop fading immediately + if stop_fade and True in stopping: + logger.dbghigh(f"Item {self._path}: Stopping fade loop, {caller} matches stop list {stop_fade}") + self._fading = False + self._lock.notify_all() + + # If continue_fade is set and there is no match, stop fading immediately + elif continue_fade and False not in continuing and caller != "Fader": + logger.dbghigh(f"Item {self._path}: Stopping fade loop, {caller} matches no value in continue list {continue_fade}") + self._fading = False + self._lock.notify_all() + + # If nothing is set, stop (original behaviour) + elif not continue_fade and not stop_fade and caller != "Fader": + logger.dbghigh(f"Item {self._path}: Stopping fade loop by {caller}, current value {value}") + self._fading = False + self._lock.notify_all() + + elif value == self._fadingdetails.get("value"): + self._set_value(value, caller, source, dest, prev_change=None, last_change=None) + self._lock.release() + return + else: + logger.dbghigh(f"Item {self._path}: Ignoring update by {caller} as item is fading") + self._lock.release() + return if value != self._value or self._enforce_change: _changed = True self._set_value(value, caller, source, dest, prev_change=None, last_change=None) trigger_source_details = self.__changed_by - if caller != "fader": - self._fading = False - self._lock.notify_all() else: self.__prev_update = self.__last_update self.__last_update = self.shtime.now() self.__prev_update_by = self.__updated_by self.__updated_by = "{0}:{1}".format(caller, source) self._lock.release() - # ms: call run_on_update() from here self.__run_on_update(value) if _changed or self._enforce_updates or self._type == 'scene': @@ -2473,7 +2517,6 @@ def __update(self, value, caller='Logic', source=None, dest=None, key=None, inde next = self.shtime.now() + datetime.timedelta(seconds=_time) self._sh.scheduler.add(self._itemname_prefix+self.id() + '-Timer', self.__call__, value={'value': _value, 'caller': 'Autotimer'}, next=next) - def add_logic_trigger(self, logic): """ Add a logic trigger to the item @@ -2587,9 +2630,11 @@ def autotimer(self, time=None, value=None, compat=ATTRIB_COMPAT_LATEST): self._autotimer_value = None - def fade(self, dest, step=1, delta=1): + def fade(self, dest, step=1, delta=1, caller=None, stop_fade=None, continue_fade=None, instant_set=True, update=False): dest = float(dest) - self._sh.trigger(self._path, fadejob, value={'item': self, 'dest': dest, 'step': step, 'delta': delta}) + if not self._fading or (self._fading and update): + self._fadingdetails = {'value': self._value, 'dest': dest, 'step': step, 'delta': delta, 'caller': caller, 'stop_fade': stop_fade, 'continue_fade': continue_fade, 'instant_set': instant_set} + self._sh.trigger(self._path, fadejob, value={'item': self}) def return_children(self): for child in self.__children: @@ -2620,7 +2665,7 @@ def return_parent(self, level: int = 1, strict: bool = False): item = self while level >= 1: - print(f'level is {level}, item is {item}') + # print(f'level is {level}, item is {item}') if item._is_top_of_item_tree(): if strict: return diff --git a/lib/item/items.py b/lib/item/items.py index 83c5946a4c..212bd3e8b1 100755 --- a/lib/item/items.py +++ b/lib/item/items.py @@ -473,6 +473,8 @@ def stop(self, signum=None, frame=None): """ for item in self.__items: self.__item_dict[item]._fading = False + with self.__item_dict[item]._lock: + self.__item_dict[item]._lock.notify_all() def add_plugin_attribute(self, plugin_name, attribute_name, attribute): From 5120614bd70ee6a8b1bf5594aa29828b64ecf2b9 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 17 Oct 2024 00:47:17 +0200 Subject: [PATCH 2/7] lib.item: inline docu for fader --- lib/item/item.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/item/item.py b/lib/item/item.py index 7452b31e78..3774db3bfa 100644 --- a/lib/item/item.py +++ b/lib/item/item.py @@ -2631,6 +2631,18 @@ def autotimer(self, time=None, value=None, compat=ATTRIB_COMPAT_LATEST): def fade(self, dest, step=1, delta=1, caller=None, stop_fade=None, continue_fade=None, instant_set=True, update=False): + """ + fades an item value to a given destination value + + :param dest: destination value of fade job + :param step: step size for fading + :param delta: time interval between value changes + :param caller: Used as a source for upcoming item changes. Caller will always be "Fader" + :param stop_fade: list of callers that can stop the fading (all others won't stop it!) + :param continue_fade: list of callers that can continue fading exclusively (all others will stop it) + :param instant_set: If set to True, first fade value is set immediately after fade method is called, otherwise only after delta time + :param update: If set to True, an ongoing fade will be updated by the new parameters on the fly + """ dest = float(dest) if not self._fading or (self._fading and update): self._fadingdetails = {'value': self._value, 'dest': dest, 'step': step, 'delta': delta, 'caller': caller, 'stop_fade': stop_fade, 'continue_fade': continue_fade, 'instant_set': instant_set} From 2ab4db2ecf8de2faa90e64951c0f864250d99210 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 17 Oct 2024 00:49:49 +0200 Subject: [PATCH 3/7] lib.item: fader update docu for new functionality --- doc/user/source/referenz/items/funktionen.rst | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/doc/user/source/referenz/items/funktionen.rst b/doc/user/source/referenz/items/funktionen.rst index c7fb2c1d11..4fa157171f 100644 --- a/doc/user/source/referenz/items/funktionen.rst +++ b/doc/user/source/referenz/items/funktionen.rst @@ -23,11 +23,13 @@ genutzt werden können. | | :doc:`autotimer <./standard_attribute/autotimer>` | | | nachlesen. | +--------------------------------+--------------------------------------------------------------------------------+ -| fade(end, step, delta) | Blendet das Item mit der definierten Schrittweite (int oder float) und | -| | timedelta (int oder float in Sekunden) auf einen angegebenen Wert auf oder | -| | ab. So wird z.B.: **sh.living.light.fade(100, 1, 2.5)** das Licht im | +| fade(end, step, delta, caller, | Blendet das Item mit der definierten Schrittweite (int oder float) und | +| stop_fade, continue_fade, | timedelta (int oder float in Sekunden) auf einen angegebenen Wert auf oder | +| instant_set, update) | ab. So wird z.B.: **sh.living.light.fade(100, 1, 2.5)** das Licht im | | | Wohnzimmer mit einer Schrittweite von **1** und einem Zeitdelta von **2,5** | -| | Sekunden auf **100** herunterregeln. | +| | Sekunden auf **100** herunter regeln. Bei manueller Änderung wird der Prozess | +| | gestoppt. Dieses Verhalten kann jedoch durch stop_fade oder continue_fade | +| | geändert werden. Genaueres dazu ist in den Beispielen unten zu finden. | +--------------------------------+--------------------------------------------------------------------------------+ | remove_timer() | Entfernen eines vorher mit der Funktion timer() gestarteten Timers ohne dessen | | | Ablauf abzuwarten und die mit dem Ablauf verbundene Aktion auszuführen. | @@ -135,8 +137,38 @@ Die folgende Beispiel Logik nutzt einige der oben beschriebenen Funktionen: sh.item.autotimer() # will in- or decrement the living room light to 100 by a stepping of ``1`` and a timedelta of ``2.5`` seconds. + # As soon as the item living.light gets changed manually, the fader stops. sh.living.light.fade(100, 1, 2.5) +Die folgenden Beispiele erläutern die fade-Funktion im Detail. stop_fade und continue_fade werden als +reguläre Ausdrücke angegeben/verglichen (case insensitive). +Beispiel 1: Der Fade-Prozess wird nur gestoppt, wenn ein manueller Item-Wert über das Admin-Interface +eingegeben wurde. Wird das Item von einem anderen Caller aktualisiert, wird normal weiter gefadet. +Beispiel 2: Der Fade-Prozess wird durch sämtliche manuelle Item-Änderungen gestoppt, außer die Änderung +kommt von einem Caller, der "KNX" beinhaltet. +Beispiel 3: Der Fade-Prozess wird bei jeder manuellen Item-Änderung gestoppt. Die erste Wertänderung +findet erst nach Ablauf der delta Zeit statt, in dem Fall wird der Wert also (erst) nach 2,5 Sekunden um 1 erhöht/verringert. +Beispiel 4: Wird die Fade-Funktion für das gleiche Item erneut mit anderen Werten aufgerufen und +der update Parameter ist auf True gesetzt, dann wird das Fading "on the fly" den neuen Werten angepasst. +So könnte während eines Hochfadens durch Setzen eines niedrigeren Wertes der Itemwert direkt abwärts gefadet werden. +Auch die anderen Parameter werden für den aktuellen Fade-Vorgang überschrieben/aktualisiert. + + .. code-block:: python + :caption: logics/fading.py + + # erstes Beispiel + sh.living.light.fade(100, 1, 2.5, stop_fade=["admin:*"]) + + # zweites Beispiel + sh.living.light.fade(100, 1, 2.5, continue_fade=["KNX"]) + + # drittes Beispiel + sh.living.light.fade(100, 1, 2.5, instant_set=False) + + # viertes Beispiel + sh.living.light.fade(100, 1, 2.5, update=True) + sh.living.light.fade(5, 2, 5.5, update=True) + Der folgende Beispiel eval Ausdruck sorgt dafür, dass ein Item den zugewiesenen Wert nur dann übernimmt, wenn die Wertänderung bzw. das Anstoßen der eval Funktion über das Admin Interface erfolgt ist und das letzte Update vor der aktuellen Triggerung über 10 Sekunden zurück liegt. From c73f81cb54a7b19ffecc95068fe9c8d917e0746d Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 17 Oct 2024 07:46:27 +0200 Subject: [PATCH 4/7] lib.item: fader check if stop/continue_fade is list and handle issues --- lib/item/item.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/item/item.py b/lib/item/item.py index 3774db3bfa..2e7f3c9c93 100644 --- a/lib/item/item.py +++ b/lib/item/item.py @@ -2643,6 +2643,12 @@ def fade(self, dest, step=1, delta=1, caller=None, stop_fade=None, continue_fade :param instant_set: If set to True, first fade value is set immediately after fade method is called, otherwise only after delta time :param update: If set to True, an ongoing fade will be updated by the new parameters on the fly """ + if stop_fade and not isinstance(stop_fade, list): + logger.warning(f"stop_fade parameter {stop_fade} for fader {self} has to be a list. Ignoring") + stop_fade = None + if continue_fade and not isinstance(continue_fade, list): + logger.warning(f"continue_fade parameter {continue_fade} for fader {self} has to be a list. Ignoring") + continue_fade = None dest = float(dest) if not self._fading or (self._fading and update): self._fadingdetails = {'value': self._value, 'dest': dest, 'step': step, 'delta': delta, 'caller': caller, 'stop_fade': stop_fade, 'continue_fade': continue_fade, 'instant_set': instant_set} From 2ed035c9fcd013eb202b25e6cd7dee34b4ab5808 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 19 Oct 2024 21:09:07 +0200 Subject: [PATCH 5/7] lib.item: fix fading function, previously introduced issue --- lib/item/item.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) mode change 100644 => 100755 lib/item/item.py diff --git a/lib/item/item.py b/lib/item/item.py old mode 100644 new mode 100755 index 2e7f3c9c93..a5d51abf32 --- a/lib/item/item.py +++ b/lib/item/item.py @@ -2446,9 +2446,7 @@ def check_external_change(entry_type, entry_value): self._lock.notify_all() elif value == self._fadingdetails.get("value"): - self._set_value(value, caller, source, dest, prev_change=None, last_change=None) - self._lock.release() - return + pass else: logger.dbghigh(f"Item {self._path}: Ignoring update by {caller} as item is fading") self._lock.release() From 2a875fceed2f87bc83a58d557c648210e4edd0ef Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 20 Oct 2024 20:20:35 +0200 Subject: [PATCH 6/7] lib.item fader method: fix caching after successful fade --- lib/item/helpers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/item/helpers.py b/lib/item/helpers.py index d0c989840b..b3166c1b7a 100644 --- a/lib/item/helpers.py +++ b/lib/item/helpers.py @@ -267,13 +267,13 @@ def fadejob(item): if current_value < target_dest: # If fading upwards, but next step overshoots, set value to target_dest if (current_value + fade_step) >= target_dest: - fade_value = target_dest + break else: fade_value = current_value + fade_step elif current_value > target_dest: # If fading downwards, but next step overshoots, set value to target_dest if (current_value - fade_step) <= target_dest: - fade_value = target_dest + break else: fade_value = current_value - fade_step else: @@ -297,4 +297,5 @@ def fadejob(item): # Stop fading if item._fading: - item._fading = False \ No newline at end of file + item._fading = False + item(item._fadingdetails.get('dest'), 'Fader', item._fadingdetails.get('caller')) \ No newline at end of file From 2ca501bdd218c8c7af61cbea17ae781e998a894a Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 17 Nov 2024 20:25:35 +0100 Subject: [PATCH 7/7] lib.item remove debug print line --- lib/item/item.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/item/item.py b/lib/item/item.py index a5d51abf32..e13a4d966c 100755 --- a/lib/item/item.py +++ b/lib/item/item.py @@ -2681,7 +2681,6 @@ def return_parent(self, level: int = 1, strict: bool = False): item = self while level >= 1: - # print(f'level is {level}, item is {item}') if item._is_top_of_item_tree(): if strict: return