diff --git a/doc/user/source/referenz/items/funktionen.rst b/doc/user/source/referenz/items/funktionen.rst index c7fb2c1d1..4fa157171 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. diff --git a/lib/item/helpers.py b/lib/item/helpers.py index b0424fabf..b3166c1b7 100644 --- a/lib/item/helpers.py +++ b/lib/item/helpers.py @@ -248,23 +248,54 @@ 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: + 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: + break + 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(item._fadingdetails.get('dest'), 'Fader', item._fadingdetails.get('caller')) \ No newline at end of file diff --git a/lib/item/item.py b/lib/item/item.py old mode 100644 new mode 100755 index 831995151..29083ecec --- a/lib/item/item.py +++ b/lib/item/item.py @@ -30,6 +30,7 @@ import json import threading import ast +import re import inspect @@ -295,6 +296,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 @@ -2335,7 +2337,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: @@ -2367,7 +2369,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)) @@ -2378,6 +2380,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: @@ -2414,21 +2431,46 @@ 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"): + pass + 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, caller=caller, source=source, dest=dest) if _changed or self._enforce_updates or self._type == 'scene': @@ -2482,7 +2524,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 @@ -2596,9 +2637,29 @@ 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): + """ + 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 + """ + 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) - 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: diff --git a/lib/item/items.py b/lib/item/items.py index 83c5946a4..212bd3e8b 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):