From 2052d55ff4ae95416bd41897cc330477d6e068f8 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 3 Aug 2023 14:05:26 +0200 Subject: [PATCH 01/60] stateengine plugin: introduce new status function (if e.g. lamella items for sending and receiving values are separate) --- stateengine/StateEngineAction.py | 40 ++++++++++---- stateengine/StateEngineCondition.py | 70 ++++++++++++++++++------- stateengine/StateEngineConditionSet.py | 2 +- stateengine/StateEngineWebif.py | 55 ++++++++++++------- stateengine/__init__.py | 4 ++ stateengine/plugin.yaml | 6 +++ stateengine/user_doc/03_regelwerk.rst | 23 ++++---- stateengine/user_doc/05_bedingungen.rst | 11 ++-- stateengine/user_doc/06_aktionen.rst | 7 +++ 9 files changed, 154 insertions(+), 64 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 4f429e431..2cc9e279f 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -369,6 +369,7 @@ class SeActionSetItem(SeActionBase): def __init__(self, abitem, name: str): super().__init__(abitem, name) self.__item = None + self.__status = None self.__value = StateEngineValue.SeValue(self._abitem, "value") self.__mindelta = StateEngineValue.SeValue(self._abitem, "mindelta") self.__function = "set" @@ -427,19 +428,33 @@ def complete(self, item_state, evals_items=None): item = StateEngineTools.find_attribute(self._sh, item_state, "se_eval_" + self._name) self.__item = str(item) + # missing status in action: Try to find it. + if self.__status is None: + status = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self._name) + + if status is not None: + self.__status = self._abitem.return_item(status) + if self.__mindelta.is_empty(): mindelta = StateEngineTools.find_attribute(self._sh, item_state, "se_mindelta_" + self._name) if mindelta is not None: self.__mindelta.set(mindelta) - if isinstance(self.__item, str): - pass - elif self.__item is not None: - self.__value.set_cast(self.__item.cast) - self.__mindelta.set_cast(self.__item.cast) - self._scheduler_name = "{}-SeItemDelayTimer".format(self.__item.property.path) - if self._abitem.id == self.__item.property.path: + if self.__status is not None: + self.__value.set_cast(self.__status.cast) + self.__mindelta.set_cast(self.__status.cast) + self._scheduler_name = "{}-SeItemDelayTimer".format(self.__status.property.path) + if self._abitem.id == self.__status.property.path: self._caller += '_self' + elif self.__status is None: + if isinstance(self.__item, str): + pass + elif self.__item is not None: + self.__value.set_cast(self.__item.cast) + self.__mindelta.set_cast(self.__item.cast) + self._scheduler_name = "{}-SeItemDelayTimer".format(self.__item.property.path) + if self._abitem.id == self.__item.property.path: + self._caller += '_self' # Write action to logger def write_to_logger(self): @@ -480,8 +495,13 @@ def real_execute(self, actionname: str, namevar: str = "", repeat_text: str = "" if not self.__mindelta.is_empty(): mindelta = self.__mindelta.get() - # noinspection PyCallingNonCallable - delta = float(abs(self.__item() - value)) + if self.__status is not None: + # noinspection PyCallingNonCallable + delta = float(abs(self.__status() - value)) + additionaltext = "of statusitem " + else: + delta = float(abs(self.__item() - value)) + additionaltext = "" if delta < mindelta: text = "{0}: Not setting '{1}' to '{2}' because delta '{3:.2}' is lower than mindelta '{4}'" self._log_debug(text, actionname, self.__item.property.path, value, delta, mindelta) @@ -752,6 +772,8 @@ def write_to_logger(self): self._log_debug("item from eval: {0}", self.__item) elif self.__item is not None: self._log_debug("item: {0}", self.__item.property.path) + if self.__status is not None: + self._log_debug("status: {0}", self.__status.property.path) self.__mindelta.write_to_logger() self.__value.write_to_logger() self._log_debug("force update: yes") diff --git a/stateengine/StateEngineCondition.py b/stateengine/StateEngineCondition.py index 30b25b7df..b4f1908ec 100755 --- a/stateengine/StateEngineCondition.py +++ b/stateengine/StateEngineCondition.py @@ -40,6 +40,7 @@ def __init__(self, abitem, name: str): super().__init__(abitem) self.__name = name self.__item = None + self.__status = None self.__eval = None self.__value = StateEngineValue.SeValue(self._abitem, "value", True) self.__min = StateEngineValue.SeValue(self._abitem, "min") @@ -55,7 +56,7 @@ def __init__(self, abitem, name: str): self.__error = None def __repr__(self): - return "SeCondition 'item': {}, 'eval': {}, 'value': {}".format(self.__item, self.__eval, self.__value) + return "SeCondition 'item': {}, 'status': {}, 'eval': {}, 'value': {}".format(self.__item, self.__status, self.__eval, self.__value) # set a certain function to a given value # func: Function to set ('item', 'eval', 'value', 'min', 'max', 'negate', 'changedby', 'updatedby', @@ -68,6 +69,12 @@ def set(self, func, value): "item without item: at the beginning!", value) _, _, value = value.partition(":") self.__item = self._abitem.return_item(value) + elif func == "se_status": + if ":" in value: + self._log_warning("Your status configuration '{0}' is wrong! Define a plain (relative) " + "item without item: at the beginning!", value) + _, _, value = value.partition(":") + self.__status = self._abitem.return_item(value) elif func == "se_eval": if ":" in value: self._log_warning("Your eval configuration '{0}' is wrong! Define a plain eval " @@ -96,21 +103,25 @@ def set(self, func, value): self.__negate = value elif func == "se_agenegate": self.__agenegate = value - elif func != "se_item" and func != "se_eval": + elif func != "se_item" and func != "se_eval" and func != "se_status": self._log_warning("Function '{0}' is no valid function! Please check item attribute.", func) def get(self): - eval_result = str(self.__eval) - if 'SeItem' in eval_result: - eval_result = eval_result.split('SeItem.')[1].split(' ')[0] - if 'SeCurrent' in eval_result: - eval_result = eval_result.split('SeCurrent.')[1].split(' ')[0] + _eval_result = str(self.__eval) + if 'SeItem' in _eval_result: + _eval_result = _eval_result.split('SeItem.')[1].split(' ')[0] + if 'SeCurrent' in _eval_result: + _eval_result = _eval_result.split('SeCurrent.')[1].split(' ')[0] _value_result = str(self.__value.get_for_webif()) try: _item = self.__item.property.path except Exception: _item = self.__item - result = {'item': _item, 'eval': eval_result, 'value': _value_result, + try: + _status = self.__status.property.path + except Exception: + _status = self.__status + result = {'item': _item, 'status': _status, 'eval': _eval_result, 'value': _value_result, 'min': str(self.__min), 'max': str(self.__max), 'agemin': str(self.__agemin), 'agemax': str(self.__agemax), 'negate': str(self.__negate), 'agenegate': str(self.__agenegate), @@ -130,7 +141,7 @@ def complete(self, item_state): return False # set 'eval' for some known conditions if item and eval are not set, yet - if self.__item is None and self.__eval is None: + if self.__item is None and self.__status is None and self.__eval is None: if self.__name == "weekday": self.__eval = StateEngineCurrent.values.get_weekday elif self.__name == "sun_azimut": @@ -188,6 +199,12 @@ def complete(self, item_state): if result is not None: self.__item = self._abitem.return_item(result) + # missing status in condition: Try to find it + if self.__status is None: + result = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self.__name) + if result is not None: + self.__status = self._abitem.return_item(result) + # missing eval in condition: Try to find it if self.__eval is None: result = StateEngineTools.find_attribute(self._sh, item_state, "se_eval_" + self.__name) @@ -195,13 +212,13 @@ def complete(self, item_state): self.__eval = result # now we should have either 'item' or 'eval' set. If not, raise ValueError - if self.__item is None and self.__eval is None: - raise ValueError("Condition {}: Neither 'item' nor 'eval' given!".format(self.__name)) + if self.__item is None and self.__status is None and self.__eval is None: + raise ValueError("Condition {}: Neither 'item' nor 'status' nor 'eval' given!".format(self.__name)) - if (self.__item is not None or self.__eval is not None)\ + if (self.__item is not None or self.__status is not None or self.__eval is not None)\ and not self.__changedby.is_empty() and self.__changedbynegate is None: self.__changedbynegate = False - if (self.__item is not None or self.__eval is not None)\ + if (self.__item is not None or self.__status is not None or self.__eval is not None)\ and not self.__updatedby.is_empty() and self.__updatedbynegate is None: self.__updatedbynegate = False @@ -209,6 +226,8 @@ def complete(self, item_state): try: if self.__item is not None: self.__cast_all(self.__item.cast) + elif self.__status is not None: + self.__cast_all(self.__status.cast) elif self.__name in ("weekday", "sun_azimut", "sun_altitude", "age", "delay", "random", "month"): self.__cast_all(StateEngineTools.cast_num) elif self.__name in ( @@ -229,15 +248,15 @@ def complete(self, item_state): cond_evalitem = self.__eval and ("get_relative_item(" in self.__eval or "return_item(" in self.__eval) except Exception: cond_evalitem = False - if self.__item is None and not cond_min_max and not cond_evalitem: + if self.__item is None and self.__status is None and not cond_min_max and not cond_evalitem: raise ValueError("Condition {}: 'agemin'/'agemax' can not be used for eval!".format(self.__name)) return True # Check if condition is matching def check(self): # Ignore if no current value can be determined (should not happen as we check this earlier, but to be sure ...) - if self.__item is None and self.__eval is None: - self._log_info("Condition '{0}': No item or eval found! Considering condition as matching!", self.__name) + if self.__item is None and self.__status is None and self.__eval is None: + self._log_info("Condition '{0}': No item, status or eval found! Considering condition as matching!", self.__name) return True self._log_debug("Condition '{0}': Checking all relevant stuff", self.__name) self._log_increase_indent() @@ -266,6 +285,12 @@ def write_to_logger(self): self._log_info("item: {0} ({1})", self.__name, i.property.path) else: self._log_info("item: {0} ({1})", self.__name, self.__item.property.path) + if self.__status is not None: + if isinstance(self.__status, list): + for i in self.__status: + self._log_info("status item: {0} ({1})", self.__name, i.property.path) + else: + self._log_info("status item: {0} ({1})", self.__name, self.__status.property.path) if self.__eval is not None: if isinstance(self.__item, list): for e in self.__item: @@ -541,7 +566,7 @@ def __check_age(self): return True # Ignore if no current value can be determined - if self.__item is None and self.__eval is None: + if self.__item is None and self.__status is None and self.__eval is None: self._log_warning("Age of '{0}': No item/eval found! Considering condition as matching!", self.__name) return True @@ -616,7 +641,16 @@ def __check_age(self): # Current value of condition (based on item or eval) def __get_current(self, eval_type='value'): - if self.__item is not None: + if self.__status is not None: + # noinspection PyUnusedLocal + self._log_debug("Trying to get {} of status item {}", eval_type, self.__status) + return self.__status.property.last_change_age if eval_type == 'age' else\ + self.__status.property.last_change_by if eval_type == 'changedby' else\ + self.__status.property.last_update_by if eval_type == 'updatedby' else\ + self.__status.property.value + elif self.__item is not None: + # noinspection PyUnusedLocal + self._log_debug("Trying to get {} of item {}", eval_type, self.__item) return self.__item.property.last_change_age if eval_type == 'age' else\ self.__item.property.last_change_by if eval_type == 'changedby' else\ self.__item.property.last_update_by if eval_type == 'updatedby' else\ diff --git a/stateengine/StateEngineConditionSet.py b/stateengine/StateEngineConditionSet.py index a3a29aaca..56d950512 100755 --- a/stateengine/StateEngineConditionSet.py +++ b/stateengine/StateEngineConditionSet.py @@ -96,7 +96,7 @@ def update(self, item, grandparent_item): continue # update item/eval in this condition - if func == "se_item" or func == "se_eval": + if func == "se_item" or func == "se_eval" or func == "se_status": if name not in self.__conditions: self.__conditions[name] = StateEngineCondition.SeCondition(self._abitem, name) try: diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index 42b127ac1..b88959e24 100755 --- a/stateengine/StateEngineWebif.py +++ b/stateengine/StateEngineWebif.py @@ -154,7 +154,9 @@ def _conditionlabel(self, state, conditionset): for k, condition in enumerate(self.__states[state]['conditionsets'].get(conditionset)): condition_dict = self.__states[state]['conditionsets'][conditionset].get(condition) - item_none = condition_dict.get('item') == 'None' + + item_none = str(condition_dict.get('item')) == 'None' + status_none = str(condition_dict.get('status')) == 'None' eval_none = condition_dict.get('eval') == 'None' value_none = condition_dict.get('value') == 'None' min_none = condition_dict.get('min') == 'None' @@ -172,23 +174,37 @@ def _conditionlabel(self, state, conditionset): cond5 = not compare == 'agenegate' cond6 = not compare == 'changedbynegate' cond7 = not compare == 'updatedbynegate' - if cond1 and cond2 and cond3 and cond4 and cond5 and cond6 and cond7: - conditionlist += '' - textlength = len(str(condition_dict.get('item'))) - condition_tooltip += '{} '.format(condition_dict.get('item')) \ - if textlength > self.__textlimit else '' - info_item = str(condition_dict.get('item'))[:self.__textlimit] + '..  ' * (textlength > self.__textlimit) - info_eval = str(condition_dict.get('eval'))[:self.__textlimit] + '..  ' * (textlength > self.__textlimit) + cond8 = not compare == 'status' + if cond1 and cond2 and cond3 and cond4 and cond5 and cond6 and cond7 and cond8: + conditionlist += ''.format(compare, condition_dict.get(compare)) + if not status_none: + textlength = len(str(condition_dict.get('status'))) + condition_tooltip += '{} '.format(condition_dict.get('status')) \ + if textlength > self.__textlimit else '' + elif not item_none: + textlength = len(str(condition_dict.get('item'))) + condition_tooltip += '{} '.format(condition_dict.get('item')) \ + if textlength > self.__textlimit else '' + elif not eval_none: + textlength = len(str(condition_dict.get('eval'))) + condition_tooltip += '{} '.format(condition_dict.get('eval')) \ + if textlength > self.__textlimit else '' + else: + textlength = 0 + info_item = str(condition_dict.get('item'))[:self.__textlimit] + '..  ' * int(textlength > self.__textlimit) + info_status = str(condition_dict.get('status'))[:self.__textlimit] + '..  ' * int(textlength > self.__textlimit) + info_eval = str(condition_dict.get('eval'))[:self.__textlimit] + '..  ' * int(textlength > self.__textlimit) info_value = str(condition_dict.get(compare))[:self.__textlimit] + '..  ' * \ - (len(str(condition_dict.get(compare))) > self.__textlimit) - info = info_eval if info_item == "None" and info_eval != "None" else info_item - conditionlist += '{}'.format(info) if not item_none else '' - textlength = len(str(condition_dict.get('eval'))) - condition_tooltip += '{} '.format(condition_dict.get('eval')) \ - if textlength > self.__textlimit else '' - info = info_value if info_item == "None" and info_eval != "None" else info_eval - conditionlist += '{}'.format(info) if not eval_none and item_none else '' - conditionlist += '' + int(len(str(condition_dict.get(compare))) > self.__textlimit) + if not status_none: + info = info_status + elif not item_none: + info = info_item + elif not eval_none: + info = info_eval + else: + info = "" + conditionlist += '{}'.format(info) comparison = ">=" if not min_none and compare == "min"\ else "<=" if not max_none and compare == "max"\ else "older" if not agemin_none and compare == "agemin"\ @@ -203,13 +219,14 @@ def _conditionlabel(self, state, conditionset): and condition_dict.get('negate') == 'True')\ else "==" conditionlist += '{}'.format(comparison) - conditionlist += '"{}"'.format(info) if not item_none and not eval_none else '' + conditionlist += '"{}"'.format(info) if not item_none and not status_none and not eval_none else '' textlength = len(str(condition_dict.get(compare))) condition_tooltip += '{} '.format(condition_dict.get(compare)) \ if textlength > self.__textlimit else '' info = info_value conditionlist += '{}'.format(info) if not condition_dict.get(compare) == 'None' and ( - (eval_none and not item_none) or (not eval_none and item_none)) else '' + (eval_none and not item_none) or (eval_none and not status_none) or \ + (not eval_none and item_none) or (not eval_none and status_none)) else '' conditionlist += ' (negate)' if condition_dict.get('negate') == 'True' and "age" \ not in compare and not compare == "value" else '' conditionlist += ' (negate)' if condition_dict.get('agenegate') == 'True' and "age" in compare else '' diff --git a/stateengine/__init__.py b/stateengine/__init__.py index ce2382c9d..0455cf18a 100755 --- a/stateengine/__init__.py +++ b/stateengine/__init__.py @@ -96,6 +96,10 @@ def parse_item(self, item): item.expand_relativepathes('se_item_*', '', '') except Exception: pass + try: + item.expand_relativepathes('se_status_*', '', '') + except Exception: + pass if self.has_iattr(item.conf, "se_manual_include") or self.has_iattr(item.conf, "se_manual_exclude"): item._eval = "sh.stateengine_plugin_functions.manual_item_update_eval('" + item.id() + "', caller, source)" elif self.has_iattr(item.conf, "se_manual_invert"): diff --git a/stateengine/plugin.yaml b/stateengine/plugin.yaml index 5c2cae2ba..ec6da280d 100755 --- a/stateengine/plugin.yaml +++ b/stateengine/plugin.yaml @@ -1359,6 +1359,12 @@ item_attribute_prefixes: de: 'Definiert das Item, das in einem konkreten Zustand evaluiert oder geändert werden soll' en: 'Definition of an item that should be evaluated or changed in a specific state' + se_status_: + type: foo + description: + de: 'Definiert das Item, das in einem konkreten Zustand evaluiert werden soll' + en: 'Definition of an item that should be evaluated in a specific state' + se_eval_: type: foo description: diff --git a/stateengine/user_doc/03_regelwerk.rst b/stateengine/user_doc/03_regelwerk.rst index 8718ac663..ab841d13c 100755 --- a/stateengine/user_doc/03_regelwerk.rst +++ b/stateengine/user_doc/03_regelwerk.rst @@ -43,29 +43,28 @@ das Attribute ``se_plugin`` auf inactive zu setzen: Item-Definitionen ----------------- -Bedingungen und Aktionen beziehen sich überlicherweise auf Items wie beispielsweise +Bedingungen und Aktionen beziehen sich üblicherweise auf Items wie beispielsweise die Höhe einer Jalousie oder die Außenhelligkeit. Diese Items müssen auf Ebene des Regelwerk-Items über das Attribut -``se_item_`` bekannt gemacht werden. +``se_item_`` bekannt gemacht werden. Um einfacher zwischen Items, +die für Bedingungen und solchen, die für Aktionen genutzt werden, unterscheiden zu können, +können Items, die nur für Bedingungen gebraucht werden, mittels ``se_status_`` +deklariert werden. Diese Variante ist auch besonders dann relevant, wenn es zwei separate Items +für "Senden" und "Empfangen" gibt, also z.B. Senden der Jalousiehöhe und Empfangen des aktuellen +Werts vom KNX-Aktor. Anstatt direkt das Item in Form des absoluten oder relativen Pfades mittels ``se_item_`` zu setzen, kann auch die Angabe ``se_eval_`` genutzt werden. In diesem Fall wird eine beliebige -Funktion anstelle des Itemnamen angegeben. Dies ist sowohl für Bedingungsabfragen, -als auch für das Setzen von "dynamischen" Items möglich. +Funktion anstelle des Itemnamen angegeben. Dies ist primär für das Setzen von "dynamischen" Items +gedacht, allerdings ist es auch möglich, hier einen beliebigen Eval-Ausdruck als Bedingung festzulegen. -An dieser Stelle ist es auch möglich, über ``se_mindelta_`` zu definieren, um welchen Wert -sich ein Item mindestens geändert haben muss, um neu gesetzt zu werden. Siehe auch :ref:`Aktionen`. - -Außerdem ist es möglich, über ``se_repeat_actions`` generell zu definieren, -ob Aktionen für die Stateengine wiederholt ausgeführt werden sollen oder nicht. Diese Konfiguration -kann für einzelne Aktionen individuell über die Angabe ``repeat`` überschrieben werden. Siehe auch :ref:`Aktionen`. Beispiel se_item ================ Im Beispiel wird durch ``se_item_height`` das Item ``beispiel.raffstore1.hoehe`` dem Plugin unter dem Namen "height" bekannt gemacht. Das Item ``beispiel.wetterstation.helligkeit`` -wird durch ``se_item_brightness`` als "brightness" referenziert. +wird durch ``se_item_brightness`` (alternativ via ``se_status_brightness``) als "brightness" referenziert. Auf diese Namen beziehen sich nun in weiterer Folge Bedingungen und Aktionen. Im Beispiel wird im Zustand Nacht das Item ``beispiel.raffstore1.hoehe`` auf den Wert 100 gesetzt, sobald @@ -95,7 +94,7 @@ und Aktionen folgen auf den nächsten Seiten. Beispiel se_eval ================ -se_eval ist für Sonderfälle und etwas komplexere Konfiurationen sinnvoll, kann aber +se_eval ist für Sonderfälle und etwas komplexere Konfigurationen sinnvoll, kann aber im ersten Durchlauf ignoriert werden. Es wird daher empfohlen, als Beginner dieses Beispiel einfach zu überspringen ;) diff --git a/stateengine/user_doc/05_bedingungen.rst b/stateengine/user_doc/05_bedingungen.rst index 031914e3a..bcc4bae13 100755 --- a/stateengine/user_doc/05_bedingungen.rst +++ b/stateengine/user_doc/05_bedingungen.rst @@ -10,7 +10,7 @@ Beispiel -------- Im folgenden Beispiel wird der Zustand "Daemmerung" eingenommen, sobald -die Helligkeit (über se_item_brightness definiert) über 500 Lux liegt. +die Helligkeit (über se_item_brightness oder se_status_brightness definiert) über 500 Lux liegt. .. code-block:: yaml @@ -19,7 +19,7 @@ die Helligkeit (über se_item_brightness definiert) über 500 Lux liegt. automatik: struct: stateengine.general rules: - se_item_brightness: beispiel.wetterstation.helligkeit + se_status_brightness: beispiel.wetterstation.helligkeit Daemmerung: name: Dämmerung remark: @@ -62,7 +62,7 @@ Der zu vergleichende Wert einer Bedingung kann auf folgende Arten definiert werd - statischer Wert (also z.B. 500 Lux). Wird angegegeben mit ``value:500``, wobei das value: auch weggelassen werden kann. - Item (beispielsweise ein Item namens settings.helligkeitsschwellwert). Wird angegeben mit ``item:settings.helligkeitsschwellwert`` - Eval-Funktion (siehe auch `eval Ausdrücke `_). Wird angegeben mit ``eval:1*2*se_eval.get_relative_itemvalue('..bla')`` -- Regular Expression (siehe auch ` RegEx Howto `_) - Vergleich mittels re.fullmatch, wobei Groß/Kleinschreibung ignoriert wird. Wird angegeben mit ``regex:StateEngine Plugin:(.*)`` +- Regular Expression (siehe auch ` RegEx Howto `_) - Vergleich mittels re.fullmatch, wobei Groß/Kleinschreibung ignoriert wird. Wird angegeben mit ``regex:StateEngine Plugin:(.*)`` - Template: eine Vorlage, z.B. eine eval Funktion, die immer wieder innerhalb des StateEngine Items eingesetzt werden kann. Angegeben durch ``template:`` @@ -75,7 +75,8 @@ die jeweils mit einem Unterstrich "_" getrennt werden: - ``se_``: eindeutiger Prefix, um dem Plugin zugeordnet zu werden - ````: siehe unten. Beispiel: min = der Wert des muss mindestens dem beim Attribut angegebenen Wert entsprechen. -- ````: Hier wird entweder das im Regelwerk-Item mittels ``se_item_`` deklarierte Item oder eine besondere Bedingung (siehe unten) referenziert. +- ````: Hier wird entweder das im Regelwerk-Item mittels ``se_item_`` +oder ``se_status_`` deklarierte Item oder eine besondere Bedingung (siehe unten) referenziert. Templates für Bedingungsabfragen @@ -83,7 +84,7 @@ Templates für Bedingungsabfragen Setzt man für mehrere Bedingungsabfragen (z.B. Helligkeit, Temperatur, etc.) immer die gleichen Ausdrücke ein (z.B. eine eval-Funktion), so kann Letzteres als Template -definiert und referenziert werden. Dadurch wird die Handhabung +definiert und referenziert werden. Dadurch wird die Handhabung komplexerer Abfragen deutlich vereinfacht. Diese Templates müssen wie se_item/se_eval auf höchster Ebene des StateEngine Items (also z.B. rules) deklariert werden. diff --git a/stateengine/user_doc/06_aktionen.rst b/stateengine/user_doc/06_aktionen.rst index a663f0889..95b75479e 100755 --- a/stateengine/user_doc/06_aktionen.rst +++ b/stateengine/user_doc/06_aktionen.rst @@ -24,6 +24,12 @@ stehenden Beispiel wird der Lamellenwert abhängig vom Sonnenstand berechnet. Oh würden sich die Lamellen ständig um wenige Grad(bruchteile) ändern. Wird jedoch mindelta beispielsweise auf den Wert 10 gesetzt, findet eine Änderung erst statt, wenn sich der errechnete Wert um mindestens 10 Grad vom aktuellen Lamellenwert unterscheidet. +Im Beispiel wird auch mittels ``se_status_`` ein gesondertes Item definiert, +das den Wert vom KNX-Aktor empfängt. + +Außerdem ist es möglich, über ``se_repeat_actions`` generell zu definieren, +ob Aktionen für die Stateengine wiederholt ausgeführt werden sollen oder nicht. Diese Konfiguration +kann für einzelne Aktionen individuell über die Angabe ``repeat`` überschrieben werden. Siehe auch :ref:`Aktionen`. Beispiel zu Aktionen -------------------- @@ -43,6 +49,7 @@ Das folgende Beispiel führt je nach Zustand folgende Aktionen aus: rules: se_item_height: raffstore1.hoehe # Definition des zu ändernden Höhe-Items se_item_lamella: raffstore1.lamelle # Definition des zu ändernden Lamellen-Items + se_status_lamella: raffstore1.lamelle.status # Definition des Lamellen Statusitems se_mindelta_lamella: 10 # Mindeständerung von 10 Grad, sonst werden die Lamellen nicht aktualisiert. Daemmerung: <...> From 58dd8e8f0fd3e3d4bccfd5dbf52685462392ceff Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 3 Aug 2023 14:06:50 +0200 Subject: [PATCH 02/60] stateengine plugin: add status log message --- stateengine/StateEngineAction.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 2cc9e279f..3962336d7 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -463,6 +463,8 @@ def write_to_logger(self): self._log_debug("item from eval: {0}", self.__item) elif self.__item is not None: self._log_debug("item: {0}", self.__item.property.path) + if self.__status is not None: + self._log_debug("status: {0}", self.__status.property.path) self.__mindelta.write_to_logger() self.__value.write_to_logger() From f297c2433e5238a569ba6f9e660113091bd054c9 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 3 Aug 2023 14:10:17 +0200 Subject: [PATCH 03/60] stateengine plugin: some bug fixes and minor adjustments --- stateengine/StateEngineAction.py | 2 +- stateengine/StateEngineActions.py | 2 +- stateengine/StateEngineCondition.py | 10 +++++++++- stateengine/StateEngineValue.py | 10 +++++----- stateengine/StateEngineWebif.py | 2 +- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 3962336d7..a5ba925dd 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -295,7 +295,7 @@ def execute(self, is_repeat: bool, allow_item_repeat: bool, state): try: state.update_name(state.state_item) _key_name = ['{}'.format(state.id), 'name'] - self.update_webif(_key_name, state.name) + self._abitem.update_webif(_key_name, state.name) _key = ['{}'.format(state.id), 'actions_leave', '{}'.format(self._name), 'delay'] self._abitem.update_webif(_key, _delay_info) except Exception: diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 229c1464f..e6da5db49 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -390,7 +390,7 @@ def set(self, value): # item_allow_repeat: Is repeating actions generally allowed for the item? # state: state item triggering the action # additional_actions: SeActions-Instance containing actions which should be executed, too - def execute(self, is_repeat: bool, allow_item_repeat: bool, state: str, additional_actions=None): + def execute(self, is_repeat: bool, allow_item_repeat: bool, state, additional_actions=None): actions = [] for name in self.__actions: actions.append((self.__actions[name].get_order(), self.__actions[name])) diff --git a/stateengine/StateEngineCondition.py b/stateengine/StateEngineCondition.py index b4f1908ec..dd280d120 100755 --- a/stateengine/StateEngineCondition.py +++ b/stateengine/StateEngineCondition.py @@ -334,6 +334,9 @@ def __cast_all(self, cast_func): def __change_update_value(self, value, valuetype): def __convert(convert_value, convert_current): + if convert_value is None: + self._log_develop("Ignoring value None for conversion") + return convert_value, convert_current _oldvalue = convert_value try: if isinstance(convert_value, re._pattern_type): @@ -342,14 +345,19 @@ def __convert(convert_value, convert_current): if isinstance(convert_value, re.Pattern): return convert_value, convert_current if isinstance(convert_current, bool): + self.__value.set_cast(StateEngineTools.cast_bool) convert_value = StateEngineTools.cast_bool(convert_value) elif isinstance(convert_current, int): + self.__value.set_cast(StateEngineTools.cast_num) convert_value = int(StateEngineTools.cast_num(convert_value)) elif isinstance(convert_current, float): + self.__value.set_cast(StateEngineTools.cast_num) convert_value = StateEngineTools.cast_num(convert_value) * 1.0 elif isinstance(convert_current, list): + self.__value.set_cast(StateEngineTools.cast_list) convert_value = StateEngineTools.cast_list(convert_value) else: + self.__value = str(convert_value) convert_value = str(convert_value) convert_current = str(convert_current) if not type(_oldvalue) == type(convert_value): @@ -470,7 +478,7 @@ def __check_value(self): for i, _ in enumerate(min_value): min = None if min_value[i] == 'novalue' else min_value[i] max = None if max_value[i] == 'novalue' else max_value[i] - self._log_debug("Checking minvalue {} and maxvalue {}", min, max) + self._log_debug("Checking minvalue {} ({}) and maxvalue {}({}) against current {}({})", min, type(min), max, type(max), current, type(current)) if min is not None and max is not None and min > max: min, max = max, min self._log_warning("Condition {}: min must not be greater than max! " diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index 0dfe64255..931037566 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -259,8 +259,8 @@ def set(self, value, name="", reset=True, item=None): self.__template) s = None try: - cond1 = s.isdigit() - cond2 = field_value[i].isdigit() + cond1 = s.lstrip('-').replace('.','',1).isdigit() + cond2 = field_value[i].lstrip('-').replace('.','',1).isdigit() except Exception: cond1 = False cond2 = False @@ -381,9 +381,9 @@ def write_to_logger(self): if isinstance(self.__value, list): for i in self.__value: if i is not None: - self._log_debug("{0}: {1}", self.__name, i) + self._log_debug("{0}: {1} ({2})", self.__name, i, type(i)) else: - self._log_debug("{0}: {1}", self.__name, self.__value) + self._log_debug("{0}: {1} ({2})", self.__name, self.__value, type(self.__value)) if self.__regex is not None: if isinstance(self.__regex, list): for i in self.__regex: @@ -619,7 +619,7 @@ def __get_eval(self): self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = _newvalue values = _newvalue self._log_decrease_indent() - self._log_debug("Eval result: {0}.", values) + self._log_debug("Eval result: {0} ({1}).", values, type(values)) self._log_increase_indent() except Exception as ex: self._log_decrease_indent() diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index b88959e24..3f5afcd6e 100755 --- a/stateengine/StateEngineWebif.py +++ b/stateengine/StateEngineWebif.py @@ -52,7 +52,7 @@ def __init__(self, smarthome, abitem): fontname='Helvetica', fontsize='10') self.__nodes = {} self.__scalefactor = 0.1 - self.__textlimit = 145 + self.__textlimit = 105 self.__conditionset_count = 0 def __repr__(self): From 3945a6b365fbc25512c350b2a1c46817e8ded85d Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 3 Aug 2023 14:11:32 +0200 Subject: [PATCH 04/60] stateengine plugin: improve handling of min_delta and it's integration in the webif visu --- stateengine/StateEngineAction.py | 27 +++++++++++++++++++-------- stateengine/StateEngineWebif.py | 14 +++++++++++--- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index a5ba925dd..6f95fe98c 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -370,6 +370,7 @@ def __init__(self, abitem, name: str): super().__init__(abitem, name) self.__item = None self.__status = None + self.__delta = 0 self.__value = StateEngineValue.SeValue(self._abitem, "value") self.__mindelta = StateEngineValue.SeValue(self._abitem, "mindelta") self.__function = "set" @@ -504,9 +505,11 @@ def real_execute(self, actionname: str, namevar: str = "", repeat_text: str = "" else: delta = float(abs(self.__item() - value)) additionaltext = "" + + self.__delta = delta if delta < mindelta: - text = "{0}: Not setting '{1}' to '{2}' because delta '{3:.2}' is lower than mindelta '{4}'" - self._log_debug(text, actionname, self.__item.property.path, value, delta, mindelta) + text = "{0}: Not setting '{1}' to '{2}' because delta {3}'{4:.2}' is lower than mindelta '{5}'" + self._log_debug(text, actionname, self.__item.property.path, value, additionaltext, delta, mindelta) return self._execute_set_add_remove(actionname, namevar, repeat_text, self.__item, value, current_condition, previous_condition, previousstate_condition) @@ -520,13 +523,21 @@ def _execute_set_add_remove(self, actionname, namevar, repeat_text, item, value, def get(self): try: - item = str(self.__item.property.path) + _item = str(self.__item.property.path) except Exception: - item = str(self.__item) - return {'function': str(self.__function), 'item': item, - 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), - 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get())} + _item = str(self.__item) + _mindelta = self.__mindelta.get() + if _mindelta is None: + return {'function': str(self.__function), 'item': _item, + 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + 'previousconditionset': str(self.previousconditionset.get()), + 'previousstate_conditionset': str(self.previousstate_conditionset.get())} + else: + return {'function': str(self.__function), 'item': _item, + 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + 'previousconditionset': str(self.previousconditionset.get()), + 'previousstate_conditionset': str(self.previousstate_conditionset.get()), + 'delta': str(self.__delta), 'mindelta': str(_mindelta)} # Class representing a single "se_setbyattr" action diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index 3f5afcd6e..75b6dcd06 100755 --- a/stateengine/StateEngineWebif.py +++ b/stateengine/StateEngineWebif.py @@ -66,6 +66,8 @@ def _actionlabel(self, state, label_type, conditionset, previousconditionset, pr for action in self.__states[state].get(label_type): _repeat = self.__states[state][label_type][action].get('repeat') _delay = self.__states[state][label_type][action].get('delay') or 0 + _delta = self.__states[state][label_type][action].get('delta') or 0 + _mindelta = self.__states[state][label_type][action].get('mindelta') or 0 condition_necessary = 0 condition_met = True condition_count = 0 @@ -112,8 +114,9 @@ def _actionlabel(self, state, label_type, conditionset, previousconditionset, pr condition_met = False cond1 = conditionset in ['', self.__active_conditionset] and state == self.__active_state cond2 = self.__states[state]['conditionsets'].get(conditionset) is not None - fontcolor = "white" if cond1 and cond2 and (not condition_met or (_repeat is False and originaltype == 'actions_stay'))\ - else "#5c5646" if _delay > 0 else "darkred" if _delay < 0 else "black" + cond_delta = float(_delta) < float(_mindelta) + fontcolor = "white" if cond1 and cond2 and (cond_delta or (not condition_met or (_repeat is False and originaltype == 'actions_stay')))\ + else "#5c5646" if _delay > 0 else "darkred" if _delay < 0 else "#303030" if not condition_met else "black" condition_info = condition_to_meet if condition1 is False\ else previouscondition_to_meet if condition2 is False\ else previousstate_condition_to_meet if condition3 is False\ @@ -121,12 +124,17 @@ def _actionlabel(self, state, label_type, conditionset, previousconditionset, pr additionaltext = " ({} not met)".format(condition_info) if not condition_met\ else " (no repeat)" if _repeat is False and originaltype == 'actions_stay'\ else " (delay: {})".format(_delay) if _delay > 0\ - else " (wrong delay!)" if _delay < 0 else "" + else " (wrong delay!)" if _delay < 0\ + else " (delta {} < {})".format(_delta, _mindelta) if cond_delta and cond1 and cond2\ + else "" action1 = self.__states[state][label_type][action].get('function') if action1 == 'set': action2 = self.__states[state][label_type][action].get('item') value_check = self.__states[state][label_type][action].get('value') value_check = '""' if value_check == "" else value_check + is_number = value_check.lstrip('-').replace('.','',1).isdigit() + if is_number and "." in value_check: + value_check = round(float(value_check), 2) action3 = 'to {}'.format(value_check) elif action1 == 'special': action2 = self.__states[state][label_type][action].get('special') From b1440090b0ec7043d43e98faab192a6f1fca7e9b Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 3 Aug 2023 14:12:10 +0200 Subject: [PATCH 05/60] stateengine plugin: remove unneccessary code in web interface index.html --- stateengine/webif/templates/index.html | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/stateengine/webif/templates/index.html b/stateengine/webif/templates/index.html index b6b70a42f..6c5fb3be1 100755 --- a/stateengine/webif/templates/index.html +++ b/stateengine/webif/templates/index.html @@ -35,19 +35,8 @@