Skip to content

Commit

Permalink
Merge pull request #687 from onkelandy/fader_opt
Browse files Browse the repository at this point in the history
Fader method: introduce new features
  • Loading branch information
onkelandy authored Nov 17, 2024
2 parents 0948296 + 2ca501b commit c1fa99e
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 27 deletions.
40 changes: 36 additions & 4 deletions doc/user/source/referenz/items/funktionen.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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.
Expand Down
59 changes: 45 additions & 14 deletions lib/item/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
79 changes: 70 additions & 9 deletions lib/item/item.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import json
import threading
import ast
import re

import inspect

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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))
Expand All @@ -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:
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions lib/item/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit c1fa99e

Please sign in to comment.