From 16e217b84ee085af01c8fcbb4aaa5d60fda344d9 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 18 Jul 2024 12:11:32 +0200 Subject: [PATCH 01/19] stateengine plugin: add se_name to attribute parameters --- stateengine/plugin.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/stateengine/plugin.yaml b/stateengine/plugin.yaml index c08e5f59b..196c7bd21 100755 --- a/stateengine/plugin.yaml +++ b/stateengine/plugin.yaml @@ -344,6 +344,20 @@ item_attributes: parameter ``startup_delay_default``. ' + se_name: + type: str + description: + de: 'Zustandsname, überschreibt den im Attribut "name" angegebene Wert' + en: 'Name of state, overwriting the value defined by "name" attribute' + description_long: + de: '**Zustandsname, überschreibt den im Attribut "name" angegebene Wert**\n + Dies kann beispielsweise nützlich sein, um den Namen abhängig + von einer Bedingungsgruppe zu ändern. + ' + en: '**Name of state, overwriting the value defined by "name" attribute**\n + Could be useful to change the state name based on a condition group + ' + se_laststate_item_name: type: str description: From 50b56526c7a7111ae90a487d3d4d449ec54fb6f9 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 18 Jul 2024 12:12:01 +0200 Subject: [PATCH 02/19] stateengine plugin: fix docu --- stateengine/user_doc/06_aktionen.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stateengine/user_doc/06_aktionen.rst b/stateengine/user_doc/06_aktionen.rst index 55d6b187c..339427c70 100755 --- a/stateengine/user_doc/06_aktionen.rst +++ b/stateengine/user_doc/06_aktionen.rst @@ -348,11 +348,11 @@ Die einzelnen Angaben einer Liste werden als ``OR`` evaluiert. screens: conditionset_to_check: type: str - value: "screens.osten_s1.automatik.rules.abend.enter_abend" + initial_value: "screens.osten_s1.automatik.rules.abend.enter_abend" conditionset: - regex:enter_(.*)_test - - eval:sh.screens.conditionset_to_check.property.name + - eval:sh.screens.conditionset_to_check.property.value Der gesamte Pfad könnte wie folgt evaluiert werden: From f43c6f4431a2d04bd3740c2c357349cee8e59eec Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 18 Jul 2024 12:13:24 +0200 Subject: [PATCH 03/19] stateengine plugin: minor webif code fixes --- stateengine/StateEngineWebif.py | 33 ++++++++++++++++++++++----------- stateengine/__init__.py | 2 +- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index a40a5a300..c8cf7e7d6 100755 --- a/stateengine/StateEngineWebif.py +++ b/stateengine/StateEngineWebif.py @@ -33,7 +33,7 @@ class WebInterface(StateEngineTools.SeItemChild): # Constructor # abitem: parent SeItem instance - def __init__(self, smarthome, abitem): + def __init__(self, abitem): super().__init__(abitem) if not REQUIRED_PACKAGE_IMPORTED: @@ -70,6 +70,7 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s _condition_necessary = 1 if _condition_check != 'None' else 0 _condition_check = _condition_check if isinstance(_condition_check, list) else [_condition_check] _condition_count = 0 + _condition = False for cond in _condition_check: try: _cond = re.compile(cond) @@ -117,14 +118,15 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s cond1 = conditionset in ['', self.__active_conditionset] and state == self.__active_state cond2 = self.__states[state]['conditionsets'].get(conditionset) is not None 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 or _issue 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\ - else "" + 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 or _issue 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 \ + else "" if _issue: if tooltip_count > 0: action_tooltip += ' ' @@ -219,7 +221,10 @@ def _conditionlabel(self, state, conditionset, i): if condition not in conditions_done: current_clean = ", ".join(f"{k} = {v}" for k, v in current.items()) text = " Current {}".format(current_clean) if current and len(current) > 0 else " Not evaluated." - conditionlist += '
{}:{}
'.format(condition.upper(), text) + conditionlist += ('' + '' + '
{}:{}
').format(condition.upper(), text) conditions_done.append(condition) conditionlist += '' info_status = str(condition_dict.get('status') or '') @@ -292,7 +297,7 @@ def _conditionlabel(self, state, conditionset, i): and condition_dict.get('updatedbynegate') == 'True')\ else "updated by" if not updatedby_none and compare == "updatedby"\ else "not triggered by" if (not triggeredby_none and compare == "triggeredby" - and condition_dict.get('triggeredbynegate') == 'True')\ + and condition_dict.get('triggeredbynegate') == 'True')\ else "triggered by" if not triggeredby_none and compare == "triggeredby"\ else "!=" if (not value_none and compare == "value" and condition_dict.get('negate') == 'True')\ @@ -433,6 +438,12 @@ def drawgraph(self, filename): actions_enter_or_stay = self.__states[state].get('actions_enter_or_stay') or [] actions_stay = self.__states[state].get('actions_stay') or [] actions_leave = self.__states[state].get('actions_leave') or [] + action_tooltip_count_enter = 0 + action_tooltip_count_stay = 0 + action_tooltip_count_leave = 0 + action_tooltip_enter = "" + action_tooltip_stay = "" + action_tooltip_leave = "" for j, conditionset in enumerate(self.__states[state]['conditionsets']): if len(actions_enter) > 0 or len(actions_enter_or_stay) > 0: diff --git a/stateengine/__init__.py b/stateengine/__init__.py index 1b879799e..9b85e3836 100755 --- a/stateengine/__init__.py +++ b/stateengine/__init__.py @@ -221,7 +221,7 @@ def get_items(self): def get_graph(self, abitem, graphtype='link'): if isinstance(abitem, str): abitem = self._items[abitem] - webif = StateEngineWebif.WebInterface(self.__sh, abitem) + webif = StateEngineWebif.WebInterface(abitem) try: os.makedirs(self.path_join(self.get_plugin_dir(), 'webif/static/img/visualisations/')) except OSError: From 0f4dce3c69cb4ebc0571db812e4dc72ea1da938d Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 18 Jul 2024 16:07:09 +0200 Subject: [PATCH 04/19] stateengine plugin: fix web interface.. corrct handling of "force set", delay and conditions --- stateengine/StateEngineAction.py | 68 ++++++++++++++++---------------- stateengine/StateEngineWebif.py | 35 ++++++++++------ 2 files changed, 57 insertions(+), 46 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index d115c289c..4ae26e045 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -509,14 +509,14 @@ def _update_repeat_webif(value: bool): else: self._waitforexecute(state, actionname, self._name, repeat_text, delay, current_condition_met, previous_condition_met, previousstate_condition_met) - _update_delay_webif('actions_stay', _delay_info) - _update_delay_webif('actions_enter', _delay_info) - _update_delay_webif('actions_enter_or_stay', _delay_info) + _update_delay_webif('actions_stay', str(_delay_info)) + _update_delay_webif('actions_enter', str(_delay_info)) + _update_delay_webif('actions_enter_or_stay', str(_delay_info)) try: state.update_name(state.state_item) _key_name = ['{}'.format(state.id), 'name'] self._abitem.update_webif(_key_name, state.name) - _update_delay_webif('actions_leave', _delay_info) + _update_delay_webif('actions_leave', str(_delay_info)) except Exception: pass @@ -738,15 +738,15 @@ def get(self): mindelta = self.__mindelta.get() if mindelta is None: result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, - 'value': value, 'conditionset': str(self.conditionset.get()), - 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} + 'value': value, 'conditionset': self.conditionset.get(), + 'previousconditionset': self.previousconditionset.get(), + 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} else: result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, - 'value': value, 'conditionset': str(self.conditionset.get()), - 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}, - 'delta': str(self.__delta), 'mindelta': str(mindelta)} + 'value': value, 'conditionset': self.conditionset.get(), + 'previousconditionset': self.previousconditionset.get(), + 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}, + 'delta': str(self.__delta), 'mindelta': str(mindelta)} return result @@ -799,8 +799,8 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s def get(self): result = {'function': str(self._function), 'byattr': str(self.__byattr), - 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} + 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), + 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} return result @@ -871,11 +871,12 @@ def get(self): except Exception: value = None result = {'function': str(self._function), 'logic': str(self.__logic), - 'value': value, - 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} + 'value': value, + 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), + 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} return result + # Class representing a single "se_run" action class SeActionRun(SeActionBase): # Initialize the action @@ -968,8 +969,8 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s def get(self): result = {'function': str(self._function), 'eval': str(self.__eval), - 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} + 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), + 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} return result @@ -984,6 +985,7 @@ def __init__(self, abitem, name: str): self.__eval_item = None self.__status = None self.__value = StateEngineValue.SeValue(self._abitem, "value") + self.__delta = 0 self.__mindelta = StateEngineValue.SeValue(self._abitem, "mindelta") self._function = "force set" @@ -1314,9 +1316,9 @@ def get(self): except Exception: pass result = {'function': str(self._function), 'special': str(self.__special), - 'value': str(value_result), 'conditionset': str(self.conditionset.get()), - 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} + 'value': str(value_result), 'conditionset': self.conditionset.get(), + 'previousconditionset': self.previousconditionset.get(), + 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} return result @@ -1362,9 +1364,9 @@ def get(self): except Exception: value = None result = {'function': str(self._function), 'item': item, - 'value': value, 'conditionset': str(self.conditionset.get()), - 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} + 'value': value, 'conditionset': self.conditionset.get(), + 'previousconditionset': self.previousconditionset.get(), + 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} return result @@ -1416,9 +1418,9 @@ def get(self): except Exception: value = None result = {'function': str(self._function), 'item': item, - 'value': value, 'conditionset': str(self.conditionset.get()), - 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} + 'value': value, 'conditionset': self.conditionset.get(), + 'previousconditionset': self.previousconditionset.get(), + 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} return result @@ -1472,9 +1474,9 @@ def get(self): except Exception: value = None result = {'function': str(self._function), 'item': item, - 'value': value, 'conditionset': str(self.conditionset.get()), - 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} + 'value': value, 'conditionset': self.conditionset.get(), + 'previousconditionset': self.previousconditionset.get(), + 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} return result @@ -1526,7 +1528,7 @@ def get(self): except Exception: value = None result = {'function': str(self._function), 'item': item, - 'value': value, 'conditionset': str(self.conditionset.get()), - 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} + 'value': value, 'conditionset': self.conditionset.get(), + 'previousconditionset': self.previousconditionset.get(), + 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} return result diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index c8cf7e7d6..69f5b44e8 100755 --- a/stateengine/StateEngineWebif.py +++ b/stateengine/StateEngineWebif.py @@ -64,6 +64,15 @@ def _actionlabel(self, state, label_type, conditionset, previousconditionset, pr # action_dict: abitem[state]['on_enter'/'on_stay'/'on_enter_or_stay'/'on_leave'].get(action) # condition_to_meet: 'conditionset'/'previousconditionset''previousstate_conditionset' # conditionset: name of conditionset that should get checked + def _strip_regex(regex_list): + pattern_strings = [] + for item in regex_list: + if isinstance(item, re.Pattern): + pattern_strings.append(item.pattern) + else: + pattern_strings.append(str(item)) + return str(pattern_strings) + def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: str): _condition_check = action_dict.get(condition_to_meet) _condition_check = StateEngineTools.flatten_list(_condition_check) @@ -73,8 +82,9 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s _condition = False for cond in _condition_check: try: - _cond = re.compile(cond) - _matching = _cond.fullmatch(conditionset) + if isinstance(cond, str): + cond = re.compile(cond) + _matching = cond.fullmatch(conditionset) except Exception: _matching = True _condition_count += 1 if _matching else 0 @@ -96,22 +106,22 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s _success = None _issue = None _repeat = action_dict.get('repeat') - _delay = action_dict.get('delay') or 0 - _delta = action_dict.get('delta') or 0 - _mindelta = action_dict.get('mindelta') or 0 + _delay = int(float(action_dict.get('delay') or 0)) + _delta = action_dict.get('delta') or '0' + _mindelta = action_dict.get('mindelta') or '0' condition_necessary = 0 condition_met = True condition_count = 0 count, condition1, condition_to_meet, necessary = _check_webif_conditions(action_dict, 'conditionset', conditionset) condition_count += count - condition_necessary += necessary + condition_necessary += min(1, necessary) count, condition2, previouscondition_to_meet, necessary = _check_webif_conditions(action_dict, 'previousconditionset', previousconditionset) condition_count += count - condition_necessary += necessary + condition_necessary += min(1, necessary) count, condition3, previousstate_condition_to_meet, necessary = _check_webif_conditions(action_dict, 'previousstate_conditionset', previousstate_conditionset) condition_count += count - condition_necessary += necessary + condition_necessary += min(1, necessary) if condition_count < condition_necessary: condition_met = False @@ -123,9 +133,9 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s (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 or _issue 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 \ + condition_info = _strip_regex(condition_to_meet) if condition1 is False \ + else _strip_regex(previouscondition_to_meet) if condition2 is False \ + else _strip_regex(previousstate_condition_to_meet) if condition3 is False \ else "" if _issue: if tooltip_count > 0: @@ -142,7 +152,7 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s else " (delta {} < {})".format(_delta, _mindelta) if cond_delta and cond1 and cond2\ else "" action1 = action_dict.get('function') - if action1 == 'set': + if action1 in ['set', 'force set']: action2 = str(action_dict.get('item')) value_check = str(action_dict.get('value')) value_check = '""' if value_check == "" else value_check @@ -377,7 +387,6 @@ def drawgraph(self, filename): previous_state = '' previous_conditionset = '' previousconditionset = '' - previousstate = '' previousstate_conditionset = '' for i, state in enumerate(self.__states): #self._log_debug('Adding state for webif {}', self.__states[state]) From 10a9b0bf244e1bb0951ccea48075ae2648661f38 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 18 Jul 2024 16:08:36 +0200 Subject: [PATCH 05/19] stateengine plugin: fix condition handling, now it's working as OR, was AND first by accident --- stateengine/StateEngineAction.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 4ae26e045..3c9752f13 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -401,7 +401,7 @@ def _check_condition(condition: str): if _matching: self._log_debug("Given {} {} matches current one: {}", condition, _orig_cond, _updated__current_condition) _condition_met.append(_updated__current_condition) - _conditions_met_count +=1 + _conditions_met_count += 1 else: self._log_debug("Given {} {} not matching current one: {}", condition, _orig_cond, _updated__current_condition) except Exception as ex: @@ -446,13 +446,13 @@ def _update_repeat_webif(value: bool): condition_necessary = 0 current_condition_met, cur_conditions_met, cur_condition_necessary = _check_condition('conditionset') conditions_met += cur_conditions_met - condition_necessary += cur_condition_necessary + condition_necessary += min(1, cur_condition_necessary) previous_condition_met, prev_conditions_met, prev_condition_necessary = _check_condition('previousconditionset') conditions_met += prev_conditions_met - condition_necessary += prev_condition_necessary + condition_necessary += min(1, prev_condition_necessary) previousstate_condition_met, prevst_conditions_met, prevst_condition_necessary = _check_condition('previousstate_conditionset') conditions_met += prevst_conditions_met - condition_necessary += prevst_condition_necessary + condition_necessary += min(1, prevst_condition_necessary) self._log_develop("Action '{0}': conditions met: {1}, necessary {2}.", self._name, conditions_met, condition_necessary) if conditions_met < condition_necessary: self._log_info("Action '{0}': Skipping because not all conditions are met.", self._name) @@ -537,7 +537,14 @@ def _can_execute(self, state): def get(self): return True - def _waitforexecute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", delay: int = 0, current_condition: str = "", previous_condition: str = "", previousstate_condition: str = ""): + def _waitforexecute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", delay: int = 0, current_condition: list[str] = None, previous_condition: list[str] = None, previousstate_condition: list[str] = None): + if current_condition is None: + current_condition = [] + if previous_condition is None: + previous_condition = [] + if previousstate_condition is None: + previousstate_condition = [] + self._log_decrease_indent(50) self._log_increase_indent() if delay == 0: From 956ac7ad4fd887cded0df3d427b8bb9a7fc611b7 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 18 Jul 2024 16:10:02 +0200 Subject: [PATCH 06/19] stateengine plugin: update SetForce Action --- stateengine/StateEngineAction.py | 33 ++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 3c9752f13..9e18035a3 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -1074,7 +1074,8 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if value is None: self._log_debug("{0}: Value is None", actionname) - self.update_webif_actionstatus(state, self._name, 'False', 'Value is None') + pat = r"(?:[^,(]*)\'(.*?)\'" + self.update_webif_actionstatus(state, re.findall(pat, actionname)[0], 'False', 'Value is None') return if returnvalue: @@ -1083,12 +1084,19 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s 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 = "" + + self.__delta = delta if 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) self.update_webif_actionstatus(state, self._name, 'False') - 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) return source = self.set_source(current_condition, previous_condition, previousstate_condition) # Set to different value first ("force") @@ -1141,9 +1149,18 @@ def get(self): except Exception: value = None self.__item = orig_item - result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, 'value': value, - 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} + mindelta = self.__mindelta.get() + if mindelta is None: + result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, + 'value': value, 'conditionset': self.conditionset.get(), + 'previousconditionset': self.previousconditionset.get(), + 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} + else: + result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, + 'value': value, 'conditionset': self.conditionset.get(), + 'previousconditionset': self.previousconditionset.get(), + 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}, + 'delta': str(self.__delta), 'mindelta': str(mindelta)} return result From 35aca7dba6a81d1fff0d4ebfefc6f377a0022b49 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 18 Jul 2024 16:17:24 +0200 Subject: [PATCH 07/19] stateengine plugin: code cleanup, fixes, improvements --- stateengine/StateEngineAction.py | 23 ++- stateengine/StateEngineActions.py | 182 ++++++++++++------------ stateengine/StateEngineCliCommands.py | 4 +- stateengine/StateEngineCondition.py | 189 +++++++++++-------------- stateengine/StateEngineConditionSet.py | 6 +- stateengine/StateEngineEval.py | 36 ++--- stateengine/StateEngineFunctions.py | 8 +- stateengine/StateEngineItem.py | 61 ++++---- stateengine/StateEngineLogger.py | 2 - stateengine/StateEngineState.py | 102 ++++++------- stateengine/StateEngineStruct.py | 10 +- stateengine/StateEngineTools.py | 51 +++---- stateengine/StateEngineValue.py | 39 ++--- stateengine/__init__.py | 38 +++-- stateengine/webif/__init__.py | 10 +- stateengine/webif/templates/index.html | 2 +- 16 files changed, 353 insertions(+), 410 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 9e18035a3..421801c84 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -24,7 +24,6 @@ from . import StateEngineDefaults import datetime from lib.shtime import Shtime -from lib.item import Items import re @@ -68,7 +67,6 @@ def __init__(self, abitem, name: str): self._parent = self._abitem.id self._caller = StateEngineDefaults.plugin_identification self.shtime = Shtime.get_instance() - self.itemsApi = Items.get_instance() self._name = name self.__delay = StateEngineValue.SeValue(self._abitem, "delay") self.__repeat = None @@ -271,7 +269,7 @@ def check_getitem_fromeval(self, check_item, check_value=None, check_mindelta=No self._caller += '_self' #self._log_develop("Got item from eval on {} {}", self._function, check_item) else: - self._log_develop("Got no item from eval on {} with initial item {}", self._function, self.__item) + self._log_develop("Got no item from eval on {} with initial item {}", self._function, item) except Exception as ex: _issue = {self._name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} # raise Exception("Problem evaluating item '{}' from eval: {}".format(check_item, ex)) @@ -302,14 +300,13 @@ def check_complete(self, item_state, check_item, check_status, check_mindelta, c _eval = _eval if _eval not in (None, "None") else None check_item = _selfitem or _eval if check_item is None: - _returnitem, _returnissue = self._abitem.return_item(_item) - check_item = _returnitem + check_item, _returnissue = self._abitem.return_item(_item) else: _returnissue = None _issue = {self._name: {'issue': _returnissue, 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} - self._log_debug("Check item {} status {} value {} _returnissue {}", check_item, check_status, check_value, - _returnissue) + self._log_debug("Check item {} status {} value {} _returnissue {}", check_item, check_status, + check_value, _returnissue) except Exception as ex: self._log_info("No valid item info for action {}, trying to get differently. Problem: {}", self._name, ex) # missing item in action: Try to find it. @@ -334,8 +331,6 @@ def check_complete(self, item_state, check_item, check_status, check_mindelta, c check_status, _issue = self._abitem.return_item(status) _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} - elif check_status is not None: - check_status = str(status) if check_mindelta.is_empty(): mindelta = StateEngineTools.find_attribute(self._sh, item_state, "se_mindelta_" + self._name) @@ -436,7 +431,7 @@ def _update_repeat_webif(value: bool): self._getitem_fromeval() self._log_decrease_indent() _validitem = True - except Exception as ex: + except Exception: _validitem = False self._log_decrease_indent() if not self._can_execute(state): @@ -682,7 +677,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if value is None: self._log_debug("{0}: Value is None", actionname) - pat = "(?:[^,\(]*)\'(.*?)\'" + pat = r"(?:[^,(]*)\'(.*?)\'" self.update_webif_actionstatus(state, re.findall(pat, actionname)[0], 'False', 'Value is None') return @@ -713,7 +708,7 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, self._log_decrease_indent() self._log_debug("{0}: Set '{1}' to '{2}'{3}", actionname, item.property.path, value, repeat_text) source = self.set_source(current_condition, previous_condition, previousstate_condition) - pat = "(?:[^,\(]*)\'(.*?)\'" + pat = r"(?:[^,(]*)\'(.*?)\'" self.update_webif_actionstatus(state, re.findall(pat, actionname)[0], 'True') # noinspection PyCallingNonCallable item(value, caller=self._caller, source=source) @@ -731,7 +726,7 @@ def get(self): item = str(self.__item.property.path) else: item = None - except Exception as ex: + except Exception: item = None try: val = self.__value.get() @@ -800,7 +795,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._log_info("{0}: Setting values by attribute '{1}'.{2}", actionname, self.__byattr, repeat_text) self.update_webif_actionstatus(state, self._name, 'True') source = self.set_source(current_condition, previous_condition, previousstate_condition) - for item in self.itemsApi.find_items(self.__byattr): + for item in self._sh.find_items(self.__byattr): self._log_info("\t{0} = {1}", item.property.path, item.conf[self.__byattr]) item(item.conf[self.__byattr], caller=self._caller, source=source) diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 6aa9976f2..14b4a6149 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -21,7 +21,6 @@ from . import StateEngineAction from . import StateEngineTools -import ast import threading import queue @@ -49,16 +48,16 @@ def __init__(self, abitem): def __repr__(self): return "SeActions, count {}".format(self.count()) - def dict_actions(self, type, state): + def dict_actions(self, action_type, state): result = {} for name in self.__actions: - self._abitem._initactionname = name + self._abitem.initactionname = name result.update({name: self.__actions[name].get()}) try: - result[name].update({'actionstatus': self._abitem.webif_infos[state][type][name].get('actionstatus')}) + result[name].update({'actionstatus': self._abitem.webif_infos[state][action_type][name].get('actionstatus')}) except Exception: pass - self._abitem._initactionname = None + self._abitem.initactionname = None return result def reset(self): @@ -77,6 +76,15 @@ def update(self, attribute, value): _count = 0 _issue = None try: + if func == "se_action": # and name not in self.__actions: + _issue = self.__handle_combined_action_attribute(name, value) + _count += 1 + return _count, _issue + elif isinstance(value, str): + value = ":".join(map(str.strip, value.split(":"))) + if value[:1] == '[' and value[-1:] == ']': + value = StateEngineTools.convert_str_to_list(value, False) + self._log_debug("Value is now {}", value) if func == "se_delay": # set delay if name not in self.__actions: @@ -149,9 +157,6 @@ def update(self, attribute, value): else: _issue = self.__actions[name].update_order(value) return _count, _issue - elif func == "se_action": # and name not in self.__actions: - _issue = self.__handle_combined_action_attribute(name, value) - _count += 1 else: _issue_list = [] _ensure_action, _issue = self.__ensure_action_exists(func, name) @@ -165,9 +170,9 @@ def update(self, attribute, value): _count += 1 _issue = StateEngineTools.flatten_list(_issue_list) except ValueError as ex: + _issue = {name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': self.__actions[name].function}], 'ignore': True}} if name in self.__actions: del self.__actions[name] - _issue = {name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': self.__actions[name].function}]}} self._log_warning("Ignoring action {0} because: {1}", attribute, ex) return _count, _issue @@ -194,13 +199,14 @@ def __check_force_setting(self, name, value, function): "because parameter 'force' is 'False'!", name) _returnfunction = "set" return _issue, _returnfunction + def __check_mode_setting(self, name, value, function, action): if value is not None: possible_mode_list = ['first', 'last', 'all'] _issue = None # Parameter mode is supported only for type "remove" - if not "remove" in function: - _issue = {name: {'issue': ['Parameter mode not supported for this function'], 'attribute': 'mode', + if "remove" not in function: + _issue = {name: {'issue': ['Parameter mode only supported for remove function'], 'attribute': 'mode', 'issueorigin': [{'state': 'unknown', 'action': function}]}} self._log_warning("Attribute 'se_action_{0}': Parameter 'mode' not supported for function '{1}'", name, function) @@ -312,39 +318,54 @@ def __ensure_action_exists(self, func, name): return True, _issue_list def __handle_combined_action_attribute(self, name, value_list): + def remove_action(ex): + if name in self.__actions: + del self.__actions[name] + _issue = {name: {'issue': [ex], 'issueorigin': [{'state': 'unknown', 'action': parameter['function']}], 'ignore': True}} + _issue_list.append(_issue) + self._log_warning("Ignoring action {0} because: {1}", name, ex) + + parameter = {'function': None, 'force': None, 'repeat': None, 'delay': 0, 'order': None, 'conditionset': None, + 'previousconditionset': None, 'previousstate_conditionset': None, 'mode': None, 'instanteval': None} + _issue = None + _issue_list = [] # value_list needs to be string or list if isinstance(value_list, str): value_list = [value_list, ] elif not isinstance(value_list, list): - raise ValueError("Attribute 'se_action_{0}': Value must be a string or a list!".format(name)) + remove_action("Value must be a string or a list!") + return _issue_list # parse parameters - parameter = {'function': None, 'force': None, 'repeat': None, 'delay': 0, 'order': None, 'conditionset': None, - 'previousconditionset': None, 'previousstate_conditionset': None, 'mode': None, 'instanteval': None} for entry in value_list: - if isinstance(entry, dict): - entry = list("{!s}:{!s}".format(k, v) for (k, v) in entry.items())[0] - key, val = StateEngineTools.partition_strip(entry, ":") - val = ":".join(map(str.strip, val.split(":"))) - if val[:1] == '[' and val[-1:] == ']': - val = ast.literal_eval(val) - if key == "function": - parameter[key] = StateEngineTools.cast_str(val) - elif key == "force": - parameter[key] = StateEngineTools.cast_bool(val) - else: - parameter[key] = val + try: + if isinstance(entry, dict): + entry = list("{!s}:{!s}".format(k, v) for (k, v) in entry.items())[0] + key, val = StateEngineTools.partition_strip(entry, ":") + val = ":".join(map(str.strip, val.split(":"))) + if val[:1] == '[' and val[-1:] == ']': + val = StateEngineTools.convert_str_to_list(val, False) + if key == "function": + parameter[key] = StateEngineTools.cast_str(val) + elif key == "force": + parameter[key] = StateEngineTools.cast_bool(val) + else: + parameter[key] = val + except Exception as ex: + remove_action("Problem with entry {} for action {}: {}".format(entry, name, ex)) + if _issue_list: + return _issue_list parameter['action'] = name - _issue_list = [] + # function given and valid? if parameter['function'] is None: - raise ValueError("Attribute 'se_action_{0}: Parameter 'function' must be set!".format(name)) + remove_action("Attribute 'se_action_{0}: Parameter 'function' must be set!".format(name)) + return _issue_list if parameter['function'] not in ('set', 'force', 'run', 'byattr', 'trigger', 'special', 'add', 'remove', 'removeall', 'removefirst', 'removelast'): - raise ValueError("Attribute 'se_action_{0}: Invalid value '{1}' for parameter " - "'function'!".format(name, parameter['function'])) + remove_action("Attribute 'se_action_{0}: Invalid value '{1}' for parameter 'function'!".format(name, parameter['function'])) + return _issue_list - _issue = None _issue, parameter['function'] = self.__check_force_setting(name, parameter['force'], parameter['function']) if _issue: _issue_list.append(_issue) @@ -354,7 +375,6 @@ def __handle_combined_action_attribute(self, name, value_list): if _action: self.__actions[name] = _action # create action based on function - exists = False try: if parameter['function'] == "set": _action_exists, _issue = self.__ensure_action_exists("se_set", name) @@ -363,7 +383,6 @@ def __handle_combined_action_attribute(self, name, value_list): if _action_exists: self.__raise_missing_parameter_error(parameter, 'to') self.__actions[name].update(parameter['to']) - exists = True elif parameter['function'] == "force": _action_exists, _issue = self.__ensure_action_exists("se_force", name) if _issue: @@ -371,7 +390,6 @@ def __handle_combined_action_attribute(self, name, value_list): if _action_exists: self.__raise_missing_parameter_error(parameter, 'to') self.__actions[name].update(parameter['to']) - exists = True elif parameter['function'] == "run": _action_exists, _issue = self.__ensure_action_exists("se_run", name) if _issue: @@ -379,7 +397,6 @@ def __handle_combined_action_attribute(self, name, value_list): if _action_exists: self.__raise_missing_parameter_error(parameter, 'eval') self.__actions[name].update(parameter['eval']) - exists = True elif parameter['function'] == "byattr": _action_exists, _issue = self.__ensure_action_exists("se_byattr", name) if _issue: @@ -387,7 +404,6 @@ def __handle_combined_action_attribute(self, name, value_list): if _action_exists: self.__raise_missing_parameter_error(parameter, 'attribute') self.__actions[name].update(parameter['attribute']) - exists = True elif parameter['function'] == "trigger": _action_exists, _issue = self.__ensure_action_exists("se_trigger", name) if _issue: @@ -398,7 +414,6 @@ def __handle_combined_action_attribute(self, name, value_list): self.__actions[name].update(parameter['logic'] + ':' + parameter['value']) else: self.__actions[name].update(parameter['logic']) - exists = True elif parameter['function'] == "special": _action_exists, _issue = self.__ensure_action_exists("se_special", name) if _issue: @@ -406,7 +421,6 @@ def __handle_combined_action_attribute(self, name, value_list): if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') self.__actions[name].update(parameter['value']) - exists = True elif parameter['function'] == "add": _action_exists, _issue = self.__ensure_action_exists("se_add", name) if _issue: @@ -414,7 +428,6 @@ def __handle_combined_action_attribute(self, name, value_list): if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') self.__actions[name].update(parameter['value']) - exists = True elif parameter['function'] == "remove": _action_exists, _issue = self.__ensure_action_exists("se_remove", name) if _issue: @@ -422,7 +435,6 @@ def __handle_combined_action_attribute(self, name, value_list): if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') self.__actions[name].update(parameter['value']) - exists = True elif parameter['function'] == "removeall": _action_exists, _issue = self.__ensure_action_exists("se_removeall", name) if _issue: @@ -430,7 +442,6 @@ def __handle_combined_action_attribute(self, name, value_list): if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') self.__actions[name].update(parameter['value']) - exists = True elif parameter['function'] == "removefirst": _action_exists, _issue = self.__ensure_action_exists("se_removefirst", name) if _issue: @@ -438,7 +449,6 @@ def __handle_combined_action_attribute(self, name, value_list): if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') self.__actions[name].update(parameter['value']) - exists = True elif parameter['function'] == "removelast": _action_exists, _issue = self.__ensure_action_exists("se_removelast", name) if _issue: @@ -446,55 +456,49 @@ def __handle_combined_action_attribute(self, name, value_list): if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') self.__actions[name].update(parameter['value']) - exists = True except ValueError as ex: - exists = False - if name in self.__actions: - del self.__actions[name] - _issue = {name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': parameter['function']}]}} - _issue_list.append(_issue) - self._log_warning("Ignoring action {0} because: {1}", name, ex) + remove_action(ex) + return _issue_list # add additional parameters - if exists: - if parameter['instanteval'] is not None: - _issue = self.__actions[name].update_instanteval(parameter['instanteval']) - if _issue: - _issue_list.append(_issue) - if parameter['repeat'] is not None: - _issue = self.__actions[name].update_repeat(parameter['repeat']) - if _issue: - _issue_list.append(_issue) - if parameter['delay'] != 0: - _issue = self.__actions[name].update_delay(parameter['delay']) - if _issue: - _issue_list.append(_issue) - if parameter['order'] is not None: - _issue = self.__actions[name].update_order(parameter['order']) - if _issue: - _issue_list.append(_issue) - if parameter['conditionset'] is not None: - _issue = self.__actions[name].update_conditionset(parameter['conditionset']) - if _issue: - _issue_list.append(_issue) - if parameter['previousconditionset'] is not None: - _issue = self.__actions[name].update_previousconditionset(parameter['previousconditionset']) - if _issue: - _issue_list.append(_issue) - if parameter['previousstate_conditionset'] is not None: - _issue = self.__actions[name].update_previousstate_conditionset(parameter['previousstate_conditionset']) - if _issue: - _issue_list.append(_issue) - if parameter['mode'] is not None: - _val, _issue = self.__actions[name].update_mode(parameter['mode']) - if _issue: - _issue_list.append(_issue) - _issue, _action = self.__check_mode_setting(name, _val, parameter['function'], self.__actions[name]) - if _issue: - _issue_list.append(_issue) - if _action: - self.__actions[name] = _action + if parameter['instanteval'] is not None: + _issue = self.__actions[name].update_instanteval(parameter['instanteval']) + if _issue: + _issue_list.append(_issue) + if parameter['repeat'] is not None: + _issue = self.__actions[name].update_repeat(parameter['repeat']) + if _issue: + _issue_list.append(_issue) + if parameter['delay'] != 0: + _issue = self.__actions[name].update_delay(parameter['delay']) + if _issue: + _issue_list.append(_issue) + if parameter['order'] is not None: + _issue = self.__actions[name].update_order(parameter['order']) + if _issue: + _issue_list.append(_issue) + if parameter['conditionset'] is not None: + _issue = self.__actions[name].update_conditionset(parameter['conditionset']) + if _issue: + _issue_list.append(_issue) + if parameter['previousconditionset'] is not None: + _issue = self.__actions[name].update_previousconditionset(parameter['previousconditionset']) + if _issue: + _issue_list.append(_issue) + if parameter['previousstate_conditionset'] is not None: + _issue = self.__actions[name].update_previousstate_conditionset(parameter['previousstate_conditionset']) + if _issue: + _issue_list.append(_issue) + if parameter['mode'] is not None: + _val, _issue = self.__actions[name].update_mode(parameter['mode']) + if _issue: + _issue_list.append(_issue) + _issue, _action = self.__check_mode_setting(name, _val, parameter['function'], self.__actions[name]) + if _issue: + _issue_list.append(_issue) + if _action: + self.__actions[name] = _action return _issue_list # noinspection PyMethodMayBeStatic @@ -573,7 +577,7 @@ def write_to_logger(self): # noinspection PyProtectedMember self._log_info("Action '{0}':", action.name) self._log_increase_indent() - self._abitem._initactionname = action.name + self._abitem.initactionname = action.name action.write_to_logger() - self._abitem._initactionname = None + self._abitem.initactionname = None self._log_decrease_indent() diff --git a/stateengine/StateEngineCliCommands.py b/stateengine/StateEngineCliCommands.py index 0c82d6c78..e3eb363d5 100755 --- a/stateengine/StateEngineCliCommands.py +++ b/stateengine/StateEngineCliCommands.py @@ -23,7 +23,6 @@ # noinspection PyUnresolvedReferences from lib.model.smartplugin import SmartPlugin from lib.plugin import Plugins -from bin.smarthome import VERSION class SeCliCommands: @@ -42,7 +41,8 @@ def __init__(self, smarthome, items, logger): self.logger.info("StateEngine: Additional CLI commands not registered because CLI plugin is too old") else: cli.commands.add_command("se_list", self.cli_list, "StateEngine", "se_list: list StateEngine items") - cli.commands.add_command("se_detail", self.cli_detail, "StateEngine", "se_detail [seItem]: show details on StateEngine item [seItem]") + cli.commands.add_command("se_detail", self.cli_detail, "StateEngine", + "se_detail [seItem]: show details on StateEngine item [seItem]") self.logger.info("StateEngine: Two additional CLI commands registered") except AttributeError as err: self.logger.error("StateEngine: Additional CLI commands not registered because error occured.") diff --git a/stateengine/StateEngineCondition.py b/stateengine/StateEngineCondition.py index 2e1993b55..e896006ac 100755 --- a/stateengine/StateEngineCondition.py +++ b/stateengine/StateEngineCondition.py @@ -63,7 +63,8 @@ def __init__(self, abitem, name: str): def __repr__(self): return "SeCondition 'item': {}, 'status': {}, 'eval': {}, " \ - "'status_eval': {}, 'value': {}".format(self.__item, self.__status, self.__eval, self.__status_eval, self.__value) + "'status_eval': {}, 'value': {}".format(self.__item, self.__status, self.__eval, + self.__status_eval, self.__value) def check_items(self, check, value=None, item_state=None): item_issue, status_issue, eval_issue, status_eval_issue = None, None, None, None @@ -309,8 +310,9 @@ def complete(self, item_state): elif self.__name in ("weekday", "sun_azimut", "sun_altitude", "age", "delay", "random", "month"): self.__cast_all(StateEngineTools.cast_num) elif self.__name in ( - "laststate", "laststate_id", "laststate_name", "lastconditionset", "lastconditionset_id", "lastconditionset_name", - "previousstate", "previousstate_name", "previousstate_id", "previousconditionset", "previousconditionset_id", "previousconditionset_name", + "laststate", "laststate_id", "laststate_name", "lastconditionset", "lastconditionset_id", + "lastconditionset_name", "previousstate", "previousstate_name", "previousstate_id", + "previousconditionset", "previousconditionset_id", "previousconditionset_name", "previousstate_conditionset", "previousstate_conditionset_id", "previousstate_conditionset_name", "trigger_item", "trigger_caller", "trigger_source", "trigger_dest", "original_item", "original_caller", "original_source"): @@ -438,12 +440,8 @@ def __convert(convert_value, convert_current): self._log_develop("Ignoring value None for conversion") return convert_value, convert_current _oldvalue = convert_value - try: - if isinstance(convert_value, re._pattern_type): - return convert_value, convert_current - except Exception: - if isinstance(convert_value, re.Pattern): - return 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) @@ -477,10 +475,15 @@ def __convert(convert_value, convert_current): self.__updatedbynegate if valuetype == "updatedby" else\ self.__triggeredbynegate if valuetype == "triggeredby" else\ self.__negate + _key_current = ['{}'.format(state.id), 'conditionsets', '{}'.format( + self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), + 'current', '{}'.format(valuetype)] + _key_match = ['{}'.format(state.id), 'conditionsets', '{}'.format( + self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), + 'match', '{}'.format(valuetype)] if isinstance(value, list): text = "Condition '{0}': {1}={2} negate={3} current={4}" - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'current', '{}'.format(valuetype)] - self._abitem.update_webif(_key, str(current)) + self._abitem.update_webif(_key_current, str(current)) self._log_info(text, self.__name, valuetype, value, negate, current) self._log_increase_indent() for i, element in enumerate(value): @@ -488,40 +491,31 @@ def __convert(convert_value, convert_current): regex_check = False if valuetype == "value" and type(element) != type(current) and current is not None: element, current = __convert(element, current) - try: - if isinstance(element, re._pattern_type): - regex_result = element.fullmatch(str(current)) - regex_check = True - except Exception: - if isinstance(element, re.Pattern): - regex_result = element.fullmatch(str(current)) - regex_check = True + if isinstance(element, re.Pattern): + regex_result = element.fullmatch(str(current)) + regex_check = True if negate: if (regex_result is not None and regex_check is True)\ or (current == element and regex_check is False): self._log_debug("{0} found but negated -> not matching", element) - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', '{}'.format(valuetype)] - self._abitem.update_webif(_key, 'no') + self._abitem.update_webif(_key_match, 'no') return False else: if (regex_result is not None and regex_check is True)\ or (current == element and regex_check is False): self._log_debug("{0} found -> matching", element) - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', '{}'.format(valuetype)] - self._abitem.update_webif(_key, 'yes') + self._abitem.update_webif(_key_match, 'yes') return True if regex_check is True: self._log_debug("Regex '{}' result: {}, element {}", element, regex_result) if negate: self._log_debug("{0} not in list -> matching", current) - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', '{}'.format(valuetype)] - self._abitem.update_webif(_key, 'yes') + self._abitem.update_webif(_key_match, 'yes') return True else: self._log_debug("{0} not in list -> not matching", current) - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', '{}'.format(valuetype)] - self._abitem.update_webif(_key, 'no') + self._abitem.update_webif(_key_match, 'no') return False else: regex_result = None @@ -530,39 +524,36 @@ def __convert(convert_value, convert_current): if valuetype == "value" and type(value) != type(current) and current is not None: value, current = __convert(value, current) text = "Condition '{0}': {1}={2} negate={3} current={4}" - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'current', valuetype] - self._abitem.update_webif(_key, str(current)) + self._abitem.update_webif(_key_current, str(current)) self._log_info(text, self.__name, valuetype, value, negate, current) self._log_increase_indent() - try: - if isinstance(value, re._pattern_type): - regex_result = value.fullmatch(str(current)) - regex_check = True - except Exception: - if isinstance(value, re.Pattern): - regex_result = value.fullmatch(str(current)) - regex_check = True + if isinstance(value, re.Pattern): + regex_result = value.fullmatch(str(current)) + regex_check = True if negate: if (regex_result is None and regex_check is True)\ or (current != value and regex_check is False): self._log_debug("not OK but negated -> matching") - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', '{}'.format(valuetype)] - self._abitem.update_webif(_key, 'yes') + self._abitem.update_webif(_key_match, 'yes') return True else: if (regex_result is not None and regex_check is True)\ or (current == value and regex_check is False): self._log_debug("OK -> matching") - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', '{}'.format(valuetype)] - self._abitem.update_webif(_key, 'yes') + self._abitem.update_webif(_key_match, 'yes') return True self._log_debug("not OK -> not matching") - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', '{}'.format(valuetype)] - self._abitem.update_webif(_key, 'no') + self._abitem.update_webif(_key_match, 'no') return False # Check if value conditions match def __check_value(self, state): + _key_current = ['{}'.format(state.id), 'conditionsets', '{}'.format( + self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), + 'current', 'value'] + _key_match = ['{}'.format(state.id), 'conditionsets', '{}'.format( + self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), + 'match', 'value'] try: cond_min_max = self.__min.is_empty() and self.__max.is_empty() if not self.__value.is_empty(): @@ -575,18 +566,11 @@ def __check_value(self, state): min_get_value = self.__min.get() max_get_value = self.__max.get() current = self.__get_current() - try: - if isinstance(min_get_value, re._pattern_type) or isinstance(max_get_value, re._pattern_type): - self._log_warning("You can not use regular expression with min/max -> ignoring") - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', 'value'] - self._abitem.update_webif(_key, 'You can not use regular expression with min or max') - return True - except Exception: - if isinstance(min_get_value, re.Pattern) or isinstance(max_get_value, re.Pattern): - self._log_warning("You can not use regular expression with min/max -> ignoring") - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', 'value'] - self._abitem.update_webif(_key, 'You can not use regular expression with min or max') - return True + + if isinstance(min_get_value, re.Pattern) or isinstance(max_get_value, re.Pattern): + self._log_warning("You can not use regular expression with min/max -> ignoring") + self._abitem.update_webif(_key_match, 'You can not use regular expression with min or max') + return True min_value = [min_get_value] if not isinstance(min_get_value, list) else min_get_value max_value = [max_get_value] if not isinstance(max_get_value, list) else max_get_value min_value = StateEngineTools.flatten_list(min_value) @@ -595,8 +579,7 @@ def __check_value(self, state): min_value = min_value + [None] * abs(diff_len) if diff_len < 0 else min_value max_value = max_value + [None] * diff_len if diff_len > 0 else max_value text = "Condition '{0}': min={1} max={2} negate={3} current={4}" - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'current', 'value'] - self._abitem.update_webif(_key, str(current)) + self._abitem.update_webif(_key_current, str(current)) self._log_info(text, self.__name, min_value, max_value, self.__negate, current) if diff_len != 0: self._log_debug("Min and max are always evaluated as valuepairs. " @@ -604,56 +587,53 @@ def __check_value(self, state): self._log_increase_indent() _notmatching = 0 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 {} ({}) 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 + _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 {} ({}) 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! " - "Values got switched: min is now {}, max is now {}", self.__name, min, max) - if min is None and max is None: + "Values got switched: min is now {}, max is now {}", + self.__name, _min, _max) + if _min is None and _max is None: self._log_debug("no limit given -> matching") - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', 'value'] - self._abitem.update_webif(_key, 'yes') + self._abitem.update_webif(_key_match, 'yes') return True if not self.__negate: - if min is not None and current < min: + if _min is not None and current < _min: self._log_debug("too low -> not matching") _notmatching += 1 - elif max is not None and current > max: + elif _max is not None and current > _max: self._log_debug("too high -> not matching") _notmatching += 1 else: self._log_debug("given limits ok -> matching") - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', 'value'] - self._abitem.update_webif(_key, 'yes') + self._abitem.update_webif(_key_match, 'yes') return True else: - if min is not None and current > min and (max is None or current < max): + if _min is not None and current > _min and (_max is None or current < _max): self._log_debug("not lower than min -> not matching") _notmatching += 1 - elif max is not None and current < max and (min is None or current > min): + elif _max is not None and current < _max and (_min is None or current > _min): self._log_debug("not higher than max -> not matching") _notmatching += 1 else: self._log_debug("given limits ok -> matching") - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', 'value'] - self._abitem.update_webif(_key, 'yes') + self._abitem.update_webif(_key_match, 'yes') return True if _notmatching == len(min_value): - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', 'value'] - self._abitem.update_webif(_key, 'no') + self._abitem.update_webif(_key_match, 'no') return False else: self._log_debug("given limits ok -> matching") - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', 'value'] - self._abitem.update_webif(_key, 'yes') + self._abitem.update_webif(_key_match, 'yes') return True elif self.__value.is_empty() and cond_min_max: @@ -662,14 +642,12 @@ def __check_value(self, state): " evalutions. Min {}, max {}, value {}", self.__name, self.__min.get(), self.__max.get(), self.__value.get()) self._log_increase_indent() - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', 'value'] - self._abitem.update_webif(_key, 'Neither value nor min/max given.') + self._abitem.update_webif(_key_match, 'Neither value nor min/max given.') return True except Exception as ex: self._log_warning("Problem checking value: {}", ex) - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', 'value'] - self._abitem.update_webif(_key, 'Problem checking value: {}'.format(ex)) + self._abitem.update_webif(_key_match, 'Problem checking value: {}'.format(ex)) finally: self._log_decrease_indent() @@ -755,7 +733,8 @@ def __check_age(self, state): try: current = self.__get_current(eval_type='age') except Exception as ex: - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', 'age'] + _key = ['{}'.format(state.id), 'conditionsets', '{}'.format( + self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', 'age'] self._abitem.update_webif(_key, 'Not possible to get age from eval {} ' 'or status_eval {}'.format(self.__eval, self.__status_eval)) self._log_warning("Age of '{0}': Not possible to get age from eval {1} or status_eval {2}! " @@ -774,7 +753,9 @@ def __check_age(self, state): agemin = agemin + [None] * abs(diff_len) if diff_len < 0 else agemin agemax = agemax + [None] * diff_len if diff_len > 0 else agemax text = "Age of '{0}': min={1} max={2} negate={3} current={4}" - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'current', 'age'] + _key = ['{}'.format(state.id), 'conditionsets', '{}'.format( + self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), + 'current', 'age'] self._abitem.update_webif(_key, str(current)) self._log_info(text, self.__name, agemin, agemax, self.__agenegate, current) if diff_len != 0: @@ -782,46 +763,45 @@ def __check_age(self, state): " If needed you can also provide 'novalue' as a list value") self._log_increase_indent() _notmatching = 0 + _key = ['{}'.format(state.id), 'conditionsets', '{}'.format( + self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), + 'match', 'age'] for i, _ in enumerate(agemin): - min = None if agemin[i] == 'novalue' else agemin[i] - max = None if agemax[i] == 'novalue' else agemax[i] - self._log_debug("Testing valuepair min {} and max {}", min, max) + _min = None if agemin[i] == 'novalue' else agemin[i] + _max = None if agemax[i] == 'novalue' else agemax[i] + self._log_debug("Testing valuepair min {} and max {}", _min, _max) if not self.__agenegate: - if min is not None and current < min: + if _min is not None and current < _min: self._log_debug("too young -> not matching") _notmatching += 1 - elif max is not None and current > max: + elif _max is not None and current > _max: self._log_debug("too old -> not matching") _notmatching += 1 else: self._log_debug("given limits ok -> matching") - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', 'age'] self._abitem.update_webif(_key, 'yes') return True else: - if min is not None and current > min and (max is None or current < max): + if _min is not None and current > _min and (_max is None or current < _max): self._log_debug("not younger than min -> not matching") _notmatching += 1 - elif max is not None and current < max and (min is None or current > min): + elif _max is not None and current < _max and (_min is None or current > _min): self._log_debug("not older than max -> not matching") _notmatching += 1 else: self._log_debug("given limits ok -> matching") - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', 'age'] self._abitem.update_webif(_key, 'yes') return True if _notmatching == len(agemin): - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', 'age'] self._abitem.update_webif(_key, 'no') return False else: self._log_debug("given limits ok -> matching") - _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', 'age'] self._abitem.update_webif(_key, 'yes') return True finally: @@ -831,9 +811,10 @@ def __check_age(self, state): def __get_current(self, eval_type='value'): def check_eval(eval_or_status_eval): if isinstance(eval_or_status_eval, str): + # noinspection PyUnusedLocal sh = self._sh - shtime = self._shtime # noinspection PyUnusedLocal + shtime = self._shtime if "stateengine_eval" in eval_or_status_eval or "se_eval" in eval_or_status_eval: # noinspection PyUnusedLocal stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem) @@ -859,10 +840,10 @@ def check_eval(eval_or_status_eval): # noinspection PyUnusedLocal self._log_debug("Trying to get {} of status item {}", eval_type, self.__status.property.path) 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.last_trigger_by if eval_type == 'triggeredby' else\ - self.__status.property.value + 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.last_trigger_by if eval_type == 'triggeredby' else\ + self.__status.property.value elif self.__status_eval is not None: self._log_debug("Trying to get {} of statuseval {}", eval_type, self.__status_eval) return_value = check_eval(self.__status_eval) @@ -871,10 +852,10 @@ def check_eval(eval_or_status_eval): # noinspection PyUnusedLocal self._log_debug("Trying to get {} of item {}", eval_type, self.__item.property.path) 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\ - self.__item.property.last_trigger_by if eval_type == 'triggeredby' else\ - self.__item.property.value + self.__item.property.last_change_by if eval_type == 'changedby' else\ + self.__item.property.last_update_by if eval_type == 'updatedby' else\ + self.__item.property.last_trigger_by if eval_type == 'triggeredby' else\ + self.__item.property.value elif self.__eval is not None: self._log_debug("Trying to get {} of eval {}", eval_type, self.__eval) return_value = check_eval(self.__eval) diff --git a/stateengine/StateEngineConditionSet.py b/stateengine/StateEngineConditionSet.py index fa9f46837..3aae5bc86 100755 --- a/stateengine/StateEngineConditionSet.py +++ b/stateengine/StateEngineConditionSet.py @@ -154,7 +154,7 @@ def complete(self, item_state): except ValueError as ex: self._abitem.update_attributes(self.__unused_attributes, self.__used_attributes) self._abitem.update_issues('state', {item_state.property.path: {'issue': ex, 'issueorigin': - [{'conditionset': self.name, 'condition': name}]}}) + [{'conditionset': self.name, 'condition': name}]}}) text = "State '{0}', Condition Set '{1}', Condition '{2}' Error: {3}" raise ValueError(text.format(item_state.property.path, self.name, name, ex)) @@ -184,14 +184,14 @@ def all_conditions_matching(self, state): try: self._log_info("Check condition set '{0}'", self.__name) self._log_increase_indent() - self.__previousconditionset_set(self._abitem.get_variable('current.conditionset_id'), self._abitem.get_variable('current.conditionset_name')) + self.__previousconditionset_set(self._abitem.get_variable('current.conditionset_id'), + self._abitem.get_variable('current.conditionset_name')) self.__currentconditionset_set(self.__id.property.path, self.__name) for name in self.__conditions: if not self.__conditions[name].check(state): self.__currentconditionset_set('', '') return False - #self._abitem.previousconditionset_set(self._abitem.get_variable('previous.conditionset_id'), self._abitem.get_variable('previous.conditionset_name')) self._abitem.lastconditionset_set(self.__id.property.path, self.__name) return True finally: diff --git a/stateengine/StateEngineEval.py b/stateengine/StateEngineEval.py index dea0a5bb3..e47afe401 100755 --- a/stateengine/StateEngineEval.py +++ b/stateengine/StateEngineEval.py @@ -66,7 +66,8 @@ def remap(_value, _minoutput): _lamella_open_value = StateEngineDefaults.lamella_open_value _lamella_text = " (based on lamella open value of {0})".format(_lamella_open_value) value = remap(90 - altitude, _lamella_open_value) + offset - self._log_debug("Blinds at right angle to the sun at {0}° with an offset of {1}°{2}", value, offset, _lamella_text) + self._log_debug("Blinds at right angle to the sun at {0}° with an offset of {1}°{2}", + value, offset, _lamella_text) self._log_decrease_indent() self._eval_lock.release() @@ -94,8 +95,8 @@ def get_variable(self, varname): self._eval_lock.acquire() self._log_debug("Executing method 'get_variable({0})'", varname) try: - if self._abitem._initactionname and varname == 'current.action_name': - returnvalue = self._abitem._initactionname + if self._abitem.initactionname and varname == 'current.action_name': + returnvalue = self._abitem.initactionname self._log_debug("Return '{}' for variable {} during init", returnvalue, varname) else: returnvalue = self._abitem.get_variable(varname) @@ -115,8 +116,8 @@ def get_relative_itemid(self, subitem_id): self._eval_lock.acquire() self._log_debug("Executing method 'get_relative_itemid({0})'", subitem_id) try: - if self._abitem._initstate and subitem_id == '..state_name': - returnvalue = self._abitem.return_item(self._abitem._initstate.id)[0].property.path + if self._abitem.initstate and subitem_id == '..state_name': + returnvalue = self._abitem.return_item(self._abitem.initstate.id)[0].property.path self._log_debug("Return item path '{0}' during init", returnvalue) else: returnvalue = self._abitem.return_item(subitem_id)[0].property.path @@ -136,8 +137,8 @@ def get_relative_item(self, subitem_id): self._eval_lock.acquire() self._log_debug("Executing method 'get_relative_item({0})'", subitem_id) try: - if self._abitem._initstate and subitem_id == '..state_name': - returnvalue, issue = self._abitem.return_item(self._abitem._initstate.id) + if self._abitem.initstate and subitem_id == '..state_name': + returnvalue, issue = self._abitem.return_item(self._abitem.initstate.id) self._log_debug("Return item '{0}' during init", returnvalue) else: returnvalue, issue = self._abitem.return_item(subitem_id) @@ -158,8 +159,8 @@ def get_relative_itemvalue(self, subitem_id): returnvalue = [] self._log_debug("Executing method 'get_relative_itemvalue({0})'", subitem_id) try: - if self._abitem._initstate and subitem_id == '..state_name': - returnvalue = self._abitem._initstate.text + if self._abitem.initstate and subitem_id == '..state_name': + returnvalue = self._abitem.initstate.text self._log_debug("Return item value '{0}' during init", returnvalue) else: item, issue = self._abitem.return_item(subitem_id) @@ -184,14 +185,15 @@ def get_relative_itemproperty(self, subitem_id, prop): try: item, issue = self._abitem.return_item(subitem_id) except Exception as ex: - self._log_warning("Problem evaluating property of {0} - relative item might not exist. Error: {1}", subitem_id, ex) + self._log_warning("Problem evaluating property of {0} - relative item might not exist. Error: {1}", + subitem_id, ex) self._eval_lock.release() return try: - if self._abitem._initstate and subitem_id == '..state_name': - returnvalue = getattr(self._abitem.return_item(self._abitem._initstate.id)[0].property, prop) + if self._abitem.initstate and subitem_id == '..state_name': + returnvalue = getattr(self._abitem.return_item(self._abitem.initstate.id)[0].property, prop) self._log_debug("Return item property '{0}' from {1}: {2} during init", prop, - self._abitem.return_item(self._abitem._initstate.id)[0].property.path, returnvalue) + self._abitem.return_item(self._abitem.initstate.id)[0].property.path, returnvalue) else: returnvalue = getattr(item.property, prop) if prop == "value": @@ -228,10 +230,10 @@ def get_attributevalue(self, item, attrib): else: item, issue = self._abitem.return_item(item) try: - if self._abitem._initstate and item == '..state_name': - returnvalue = self._abitem.return_item(self._abitem._initstate.id).conf[attrib] - self._log_debug("Return item attribute '{0}' from {1}: {2} during init", - attrib, self._abitem.return_item(self._abitem._initstate.id)[0].property.path, returnvalue) + if self._abitem.initstate and item == '..state_name': + returnvalue = self._abitem.return_item(self._abitem.initstate.id).conf[attrib] + self._log_debug("Return item attribute '{0}' from {1}: {2} during init", attrib, + self._abitem.return_item(self._abitem.initstate.id)[0].property.path, returnvalue) else: returnvalue = item.conf[attrib] self._log_debug("Return item attribute {0} from {1}: {2}", attrib, item.property.path, returnvalue) diff --git a/stateengine/StateEngineFunctions.py b/stateengine/StateEngineFunctions.py index a8df0f13e..ae5c2d7b1 100755 --- a/stateengine/StateEngineFunctions.py +++ b/stateengine/StateEngineFunctions.py @@ -25,7 +25,6 @@ from . import StateEngineLogger from . import StateEngineTools from . import StateEngineDefaults -from lib.item import Items from ast import literal_eval @@ -45,7 +44,6 @@ def __init__(self, smarthome=None, logger=None): self.__locks = {} self.__global_struct = {} self.__ab_alive = False - self.itemsApi = Items.get_instance() def __repr__(self): return "SeFunctions" @@ -99,7 +97,7 @@ def check_include_exclude(entry_type): elog.decrease_indent() return None - item = self.itemsApi.return_item(item_id) + item = self.__sh.return_item(item_id) if item is None: self.logger.error("manual_item_update_eval: item {0} not found!".format(item_id)) @@ -113,7 +111,7 @@ def check_include_exclude(entry_type): if "se_manual_logitem" in item.conf: elog_item_id = item.conf["se_manual_logitem"] - elog_item = self.itemsApi.return_item(elog_item_id) + elog_item = self.__sh.return_item(elog_item_id) if elog_item is None: self.logger.error("manual_item_update_item: se_manual_logitem {0} not found!".format(elog_item_id)) elog = StateEngineLogger.SeLoggerDummy() @@ -128,7 +126,7 @@ def check_include_exclude(entry_type): retval_trigger = not item() elog.info("Current value of item {0} is {1}", item_id, retval_no_trigger) - original_caller, original_source = StateEngineTools.get_original_caller(elog, caller, source) + original_caller, original_source = StateEngineTools.get_original_caller(self.__sh, elog, caller, source) elog.info("get_caller({0}, {1}): original trigger by {2}:{3}", caller, source, original_caller, original_source) original = "{}:{}".format(original_caller, original_source) diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index bc7c33166..92cda698e 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -22,7 +22,6 @@ import datetime from collections import OrderedDict, defaultdict -import lib.item.item from . import StateEngineTools from .StateEngineLogger import SeLogger from . import StateEngineState @@ -33,14 +32,12 @@ from . import StateEngineStructs from . import StateEngineEval -from lib.item import Items from lib.shtime import Shtime from lib.item.item import Item import copy import threading import queue import re -from ast import literal_eval # Class representing a blind item @@ -180,7 +177,6 @@ def ab_alive(self, value): def __init__(self, smarthome, item, se_plugin): self.__item = item self.__logger = SeLogger.create(self.__item) - self.itemsApi = Items.get_instance() self.update_lock = threading.Lock() self.__ab_alive = False self.__queue = queue.Queue() @@ -310,8 +306,8 @@ def __init__(self, smarthome, item, se_plugin): self.__repeat_actions = StateEngineValue.SeValue(self, "Repeat actions if state is not changed", False, "bool") self.__repeat_actions.set_from_attr(self.__item, "se_repeat_actions", True) self.__first_run = None - self._initstate = None - self._initactionname = None + self.initstate = None + self.initactionname = None self.__update_trigger_item = None self.__update_trigger_caller = None self.__update_trigger_source = None @@ -538,8 +534,8 @@ def run_queue(self): source, dest) # Find out what initially caused the update to trigger if the caller is "Eval" - orig_caller, orig_source, orig_item = StateEngineTools.get_original_caller(self.__logger, caller, - source, item) + orig_caller, orig_source, orig_item = StateEngineTools.get_original_caller(self.__sh, self.__logger, + caller, source, item) if orig_item is None: orig_item = item if orig_caller != caller: @@ -574,8 +570,8 @@ def run_queue(self): if last_state is not None: self.__logger.info("Last state: {0} ('{1}')", last_state.id, last_state.name) - _last_conditionset_id = self.__lastconditionset_internal_id #self.__lastconditionset_get_id() - _last_conditionset_name = self.__lastconditionset_internal_name # self.__lastconditionset_get_name() + _last_conditionset_id = self.__lastconditionset_internal_id # self.__lastconditionset_get_id() + _last_conditionset_name = self.__lastconditionset_internal_name # self.__lastconditionset_get_name() if _last_conditionset_id not in ['', None]: self.__logger.info("Last Conditionset: {0} ('{1}')", _last_conditionset_id, _last_conditionset_name) else: @@ -608,6 +604,8 @@ def run_queue(self): evaluated_instant_leaveaction = True else: evaluated_instant_leaveaction = False + _previousstate_conditionset_id = '' + _previousstate_conditionset_name = '' for state in self.__states: if not self.__ab_alive: self.__logger.debug("StateEngine Plugin not running (anymore). Stop state evaluation.") @@ -656,6 +654,7 @@ def run_queue(self): last_state.run_stay(self.__repeat_actions.get()) if self.update_lock.locked(): self.update_lock.release() + self.__logger.decrease_indent(50) self.__logger.debug("State evaluation finished") self.__logger.info("State evaluation queue empty.") self.__handle_releasedby(new_state, last_state, _instant_leaveaction) @@ -669,7 +668,6 @@ def run_queue(self): "State is a copy and therefore just releasing {}. Skipping state actions, running leave actions " "of last state, then retriggering.", new_state.is_copy_for.id) if last_state is not None and self.__ab_alive: - #self.lastconditionset_set(_original_conditionset_id, _original_conditionset_name) self.__logger.info("Leaving {0} ('{1}'). Condition set was: {2}.", last_state.id, last_state.name, _original_conditionset_id) self.__update_check_can_enter(last_state, _instant_leaveaction, False) @@ -685,7 +683,7 @@ def run_queue(self): if self.update_lock.locked(): self.update_lock.release() - self.update_state(self.__item, "Released_by Retrigger", state.id) + self.update_state(self.__item, "Released_by Retrigger") return _last_conditionset_id = self.__lastconditionset_internal_id @@ -753,6 +751,7 @@ def run_queue(self): self.__logger.debug("State evaluation finished") all_released_by = self.__handle_releasedby(new_state, last_state, _instant_leaveaction) + self.__logger.decrease_indent(50) self.__logger.info("State evaluation queue empty.") if new_state: self.__logger.develop("States {}, Current state released by {}", self.__states, all_released_by.get(new_state)) @@ -795,12 +794,12 @@ def __update_can_release(self, can_release, new_state=None): _stateindex = list(state_dict.keys()).index(state.id) for e in release_list: _valueindex = list(state_dict.keys()).index(e) if e in state_dict else -1 - self.__logger.develop("Testing entry in canrelease {}, state {} stateindex {}, "\ + self.__logger.develop("Testing entry in canrelease {}, state {} stateindex {}, " "valueindex {}", e, state.id, _stateindex, _valueindex) if e == state.id: self.__logger.info("Value in se_released_by must not be identical to state. Ignoring {}", e) elif _stateindex < _valueindex and not state.is_copy_for: - self.__logger.info("Value {} in se_released_by must have lower priority "\ + self.__logger.info("Value {} in se_released_by must have lower priority " "than state. Ignoring {}", state.id, e) else: can_release_list.append(e) @@ -888,7 +887,7 @@ def update_can_release_list(): self.__logger.develop("Entry {} defined by {} is a copy, skipping", e, releasedby[i]) continue _entryindex = list(state_dict.keys()).index(e) if e in state_dict else -1 - self.__logger.develop("Testing if entry {} should become a state copy. "\ + self.__logger.develop("Testing if entry {} should become a state copy. " "stateindex {}, entryindex {}", e, _stateindex, _entryindex) if e == new_state.id: self.__logger.warning("Value in se_released_by must no be identical to state. Ignoring {}", @@ -909,7 +908,7 @@ def update_can_release_list(): can_enter = self.__update_check_can_enter(relevant_state, instant_leaveaction) self.__logger.log_level_as_num = current_log_level if relevant_state == last_state: - self.__logger.debug("Possible release state {} = last state {}, "\ + self.__logger.debug("Possible release state {} = last state {}, " "not copying", relevant_state.id, last_state.id) elif can_enter: self.__logger.debug("Relevant state {} could enter, not copying", relevant_state.id) @@ -953,17 +952,14 @@ def _nested_test(dic, keys): def update_action_status(self, action_status): def combine_dicts(dict1, dict2): combined_dict = dict1.copy() - for key, value in dict2.items(): if key in combined_dict: for k, v in combined_dict.items(): v['issueorigin'].extend( [item for item in v['issueorigin'] if item not in combined_dict[k]['issueorigin']]) v['issue'].extend([item for item in v['issue'] if item not in combined_dict[k]['issue']]) - else: combined_dict[key] = value - return combined_dict combined_dict = combine_dicts(action_status, self.__action_status) @@ -1009,7 +1005,7 @@ def update_attributes(self, unused_attributes, used_attributes): combined_entries = [{'state': state, 'conditionset': ', '.join(conditionsets)} for state, conditionsets in combined_dict.items()] combined_unused_dict[key]['issueorigin'] = combined_entries - except Exception as ex: + except Exception: pass self.__unused_attributes = combined_unused_dict @@ -1035,7 +1031,12 @@ def list_issues(v): self.__logger.info("has the following issue: {}", _issuelist[0]) else: self.__logger.info("has the following issue: {}", _issuelist) + if "ignore" in v: + self.__logger.info("It will be ignored") + warn_unused = "" + warn_issues = "" + warn = "" if issue_type == 'actions': to_check = self.__action_status.items() warn = ', '.join(key for key in self.__action_status.keys()) @@ -1068,7 +1069,6 @@ def list_issues(v): self.__logger.increase_indent() for entry, value in to_check: if 'issue' in value: - origin_text = '' origin_list = value.get('issueorigin') or [] if issue_type == 'states': self.__logger.info("State {} is ignored because", entry) @@ -1209,13 +1209,13 @@ def __initialize_state(self, item_state, _statecount): return _statecount + 1 except ValueError as ex: self.update_issues('state', {item_state.property.path: {'issue': ex, 'issueorigin': - [{'conditionset': 'None', 'condition': 'ValueError'}]}}) + [{'conditionset': 'None', 'condition': 'ValueError'}]}}) self.__logger.error("Ignoring state {0} because ValueError: {1}", item_state.property.path, ex) return _statecount except Exception as ex: self.update_issues('state', {item_state.property.path: {'issue': ex, 'issueorigin': - [{'conditionset': 'None', 'condition': 'GeneralError'}]}}) + [{'conditionset': 'None', 'condition': 'GeneralError'}]}}) self.__logger.error("Ignoring state {0} because: {1}", item_state.property.path, ex) return _statecount @@ -1573,7 +1573,7 @@ def process_returnvalue(value): if v == state.id: _returnvalue_issue = "State {} defined by {} in se_released_by attribute of state {} " \ "must not be identical.".format(v, _returnvalue[i], state.id) - elif _valueindex == - 1: #not any(value == test.id for test in self.__states): + elif _valueindex == - 1: # not any(value == test.id for test in self.__states): _returnvalue_issue = "State {} defined by {} in se_released_by attribute of state {} " \ "does currently not exist.".format(v, _returnvalue[i], state.id) elif _valueindex < _stateindex: @@ -1753,14 +1753,14 @@ def write_to_log(self): self.__logger.debug("Item 'Previousstate condition Id': {0}", self.__previousstate_conditionset_item_id.property.path) if self.__previousstate_conditionset_item_name is not None: self.__logger.debug("Item 'Previousstate condition Name': {0}", - self.__previousstate_conditionset_item_name.property.path) + self.__previousstate_conditionset_item_name.property.path) self.__init_releasedby() for state in self.__states: # log states state.write_to_log() - self._initstate = None + self.initstate = None filtered_dict = {key: value for key, value in self.__config_issues.items() if value.get('issue') not in [[], [None], None]} self.__config_issues = filtered_dict @@ -1926,7 +1926,7 @@ def return_item(self, item_id): if isinstance(item_id, (StateEngineStruct.SeStruct, self.__itemClass)): return item_id, None if isinstance(item_id, StateEngineState.SeState): - return self.itemsApi.return_item(item_id.id), None + return self.__sh.return_item(item_id.id), None if item_id is None: _issue = "item_id is None" return None, [_issue] @@ -1959,11 +1959,12 @@ def return_item(self, item_id): _, _, item = item.partition(":") return item, None elif match: - _issue = "Item '{0}' has to be defined as an item path or eval expression without {}.".format(match.group(1), item_id) + _issue = ("Item '{0}' has to be defined as an item path " + "or eval expression without {}.").format(match.group(1), item_id) self.__logger.warning(_issue) return None, [_issue] else: - item = self.itemsApi.return_item(item_id) + item = self.__sh.return_item(item_id) if item is None: _issue = "Item '{0}' not found.".format(item_id) self.__logger.warning(_issue) @@ -1986,7 +1987,7 @@ def return_item(self, item_id): rel_item_id = item_id[parent_level:] if rel_item_id != "": result += "." + rel_item_id - item = self.itemsApi.return_item(result) + item = self.__sh.return_item(result) if item is None: _issue = "Determined item '{0}' does not exist.".format(item_id) self.__logger.warning(_issue) diff --git a/stateengine/StateEngineLogger.py b/stateengine/StateEngineLogger.py index 07eabac00..a398a6b15 100755 --- a/stateengine/StateEngineLogger.py +++ b/stateengine/StateEngineLogger.py @@ -92,7 +92,6 @@ def manage_logdirectory(base, log_directory, create=True): os.makedirs(log_directory) return log_directory - # Remove old log files (by scheduler) @staticmethod def remove_old_logfiles(): @@ -142,7 +141,6 @@ def __init__(self, item, manual=False): self.__filename = "" self.update_logfile() - # Update name logfile if required def update_logfile(self): if self.__date == datetime.datetime.today() and self.__filename is not None: diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index 8efc61aff..7243f3c2e 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -84,7 +84,7 @@ def releasedby(self): @releasedby.setter def releasedby(self, value): - self.__releasedby.set(value, "", True, None, False) + self.__releasedby.set(value, "", True, False) @property def order(self): @@ -92,7 +92,7 @@ def order(self): @order.setter def order(self, value): - self.__order.set(value, "", True, None, False) + self.__order.set(value, "", True, False) @property def can_release(self): @@ -100,7 +100,7 @@ def can_release(self): @can_release.setter def can_release(self, value): - self.__can_release.set(value, "", True, None, False) + self.__can_release.set(value, "", True, False) @property def has_released(self): @@ -108,7 +108,7 @@ def has_released(self): @has_released.setter def has_released(self, value): - self.__has_released.set(value, "", True, None, False) + self.__has_released.set(value, "", True, False) @property def was_releasedby(self): @@ -116,7 +116,7 @@ def was_releasedby(self): @was_releasedby.setter def was_releasedby(self, value): - self.__was_releasedby.set(value, "", True, None, False) + self.__was_releasedby.set(value, "", True, False) @property def is_copy_for(self): @@ -130,7 +130,7 @@ def is_copy_for(self, value): webif_id = None _key_copy = ['{}'.format(self.id), 'is_copy_for'] self._abitem.update_webif(_key_copy, webif_id) - self.__is_copy_for.set(value, "", True, None, False) + self.__is_copy_for.set(value, "", True, False) # Constructor # abitem: parent SeItem instance @@ -192,7 +192,7 @@ def can_enter(self): # log state data def write_to_log(self): - self._abitem._initstate = self + self._abitem.initstate = self self._log_info("State {0}:", self.id) self._log_increase_indent() self.update_name(self.__item) @@ -267,7 +267,7 @@ def update_order(self, value=None): if value is None and "se_stateorder" in self.__item.conf: _, _, _, _issue = self.__order.set_from_attr(self.__item, "se_stateorder") elif value is not None: - _, _, _issue = self.__order.set(value, "", True, None, False) + _, _, _issue = self.__order.set(value, "", True, False) else: _issue = [None] @@ -324,7 +324,6 @@ def run_leave(self, allow_item_repeat: bool): for elem in self._abitem.webif_infos: _key_leave = ['{}'.format(elem), 'leave'] self._abitem.update_webif(_key_leave, False) - #self._log_debug('set leave for {} to false', elem) self.__actions_leave.execute(False, allow_item_repeat, self) self._log_decrease_indent(50) self._log_increase_indent() @@ -346,10 +345,10 @@ def refill(self): def update_releasedby_internal(self, states=None): if states == []: - _returnvalue, _returntype, _issue = self.__releasedby.set([None], "", True, None, False) + _returnvalue, _returntype, _issue = self.__releasedby.set([None], "", True, False) elif states: self._log_develop("Setting releasedby to {}", states) - _returnvalue, _returntype, _issue = self.__releasedby.set(states, "", True, None, False) + _returnvalue, _returntype, _issue = self.__releasedby.set(states, "", True, False) self._log_develop("returnvalue {}", _returnvalue) else: _returnvalue, _returntype, _, _issue = self.__releasedby.set_from_attr(self.__item, "se_released_by") @@ -357,9 +356,9 @@ def update_releasedby_internal(self, states=None): def update_can_release_internal(self, states): if states == []: - _returnvalue, _returntype, _issue = self.__can_release.set([None], "", True, None, False) + _returnvalue, _returntype, _issue = self.__can_release.set([None], "", True, False) elif states: - _returnvalue, _returntype, _issue = self.__can_release.set(states, "", True, None, False) + _returnvalue, _returntype, _issue = self.__can_release.set(states, "", True, False) else: _returnvalue, _returntype, _issue = [None], [None], None return _returnvalue, _returntype, _issue @@ -394,14 +393,14 @@ def __fill_list(self, item_states, recursion_depth, se_use=None): # recursion_depth: current recursion_depth (recursion is canceled after five levels) # se_use: If se_use Attribute is used or not def __fill(self, item_state, recursion_depth, se_use=None): - def update_unused(used_attributes, type, name): + def update_unused(used_attributes, attrib_type, attrib_name): #filtered_dict = {key: value for key, value in self.__unused_attributes.items() if key not in used_attributes} #self.__unused_attributes = copy(filtered_dict) - for item, nested_dict in self.__unused_attributes.items(): - if item in used_attributes.keys(): - used_attributes[item].update({type: name}) - used_attributes[item].update(nested_dict) + for nested_entry, nested_dict in self.__unused_attributes.items(): + if nested_entry in used_attributes.keys(): + used_attributes[nested_entry].update({attrib_type: attrib_name}) + used_attributes[nested_entry].update(nested_dict) self.__used_attributes.update(used_attributes) def update_action_status(action_status, actiontype): @@ -533,13 +532,10 @@ def update_action_status(action_status, actiontype): parent_item = item_state.return_parent() child_items = item_state.return_children() _conditioncount = 0 - _enter_actioncount = 0 - _enter_stay_actioncount = 0 - _leave_actioncount = 0 - _stay_actioncount = 0 - _actioncount = 0 + _action_counts = {"enter": 0, "stay": 0, "enter_or_stay": 0, "leave": 0} _unused_attributes = {} _used_attributes = {} + _action_status = {} # first check all conditions for child_item in child_items: child_name = StateEngineTools.get_last_part_of_item_id(child_item) @@ -574,55 +570,39 @@ def update_action_status(action_status, actiontype): for child_item in child_items: child_name = StateEngineTools.get_last_part_of_item_id(child_item) try: - if child_name == "on_enter": - _actioncount += 1 - for attribute in child_item.conf: - _enter_actioncount += 1 - _, _action_status = self.__actions_enter.update(attribute, child_item.conf[attribute]) - if _action_status: - update_action_status(_action_status, 'enter') - self._abitem.update_action_status(self.__action_status) - update_unused(_used_attributes, 'action', child_name) - elif child_name == "on_stay": - _actioncount += 1 - for attribute in child_item.conf: - _stay_actioncount += 1 - _, _action_status = self.__actions_stay.update(attribute, child_item.conf[attribute]) - if _action_status: - update_action_status(_action_status, 'stay') - self._abitem.update_action_status(self.__action_status) - update_unused(_used_attributes, 'action', child_name) - elif child_name == "on_enter_or_stay": - _actioncount += 1 - for attribute in child_item.conf: - _enter_stay_actioncount += 1 - _, _action_status = self.__actions_enter_or_stay.update(attribute, child_item.conf[attribute]) - if _action_status: - update_action_status(_action_status, 'enter_or_stay') - self._abitem.update_action_status(self.__action_status) - update_unused(_used_attributes, 'action', child_name) - elif child_name == "on_leave": - _actioncount += 1 + + action_mapping = { + "on_enter": ("enter", self.__actions_enter), + "on_stay": ("stay", self.__actions_stay), + "on_enter_or_stay": ("enter_or_stay", self.__actions_enter_or_stay), + "on_leave": ("leave", self.__actions_leave) + } + + if child_name in action_mapping: + action_name, action_method = action_mapping[child_name] for attribute in child_item.conf: - _leave_actioncount += 1 - _, _action_status = self.__actions_leave.update(attribute, child_item.conf[attribute]) + self._log_develop("Filling state with {} action named {}", child_name, attribute) + _action_counts[action_name] += 1 + _, _action_status = action_method.update(attribute, child_item.conf[attribute]) if _action_status: - update_action_status(_action_status, 'leave') + update_action_status(_action_status, action_name) self._abitem.update_action_status(self.__action_status) update_unused(_used_attributes, 'action', child_name) + except ValueError as ex: raise ValueError("Condition {0} check for actions error: {1}".format(child_name, ex)) + self._abitem.update_attributes(self.__unused_attributes, self.__used_attributes) # Actions defined directly in the item go to "enter_or_stay" for attribute in item_state.conf: _result = self.__actions_enter_or_stay.update(attribute, item_state.conf[attribute]) - _enter_stay_actioncount += _result[0] if _result else 0 + _action_counts["enter_or_stay"] += _result[0] if _result else 0 _action_status = _result[1] if _action_status: update_action_status(_action_status, 'enter_or_stay') self._abitem.update_action_status(self.__action_status) - _total_actioncount = _enter_actioncount + _stay_actioncount + _enter_stay_actioncount + _leave_actioncount + _total_actioncount = _action_counts["enter"] + _action_counts["stay"] + _action_counts["enter_or_stay"] + _action_counts["leave"] self.update_name(item_state, recursion_depth) # Complete condition sets and actions at the end @@ -647,11 +627,15 @@ def update_action_status(action_status, actiontype): self._abitem.update_action_status(self.__action_status) self._abitem.update_attributes(self.__unused_attributes, self.__used_attributes) _summary = "{} on_enter, {} on_stay , {} on_enter_or_stay, {} on_leave" + if self.__action_status: + _ignore_list = [entry for entry in self.__action_status if self.__action_status[entry].get('ignore') is True] + if _ignore_list: + self._log_info("Ignored {} action(s) due to errors: {}", len(_ignore_list), _ignore_list) if se_use is not None: self._log_debug("Added {} action(s) based on se_use {}. " + _summary, _total_actioncount, se_use, - _enter_actioncount, _stay_actioncount, _enter_stay_actioncount, _leave_actioncount) + _action_counts["enter"], _action_counts["stay"], _action_counts["enter_or_stay"], _action_counts["leave"]) self._log_debug("Added {} condition set(s) based on se_use: {}", _conditioncount, se_use) else: self._log_debug("Added {} action(s) based on item configuration: " + _summary, _total_actioncount, - _enter_actioncount, _stay_actioncount, _enter_stay_actioncount, _leave_actioncount) + _action_counts["enter"], _action_counts["stay"], _action_counts["enter_or_stay"], _action_counts["leave"]) self._log_debug("Added {} condition set(s) based on item configuration", _conditioncount) diff --git a/stateengine/StateEngineStruct.py b/stateengine/StateEngineStruct.py index 4c6edb69e..163ee042f 100755 --- a/stateengine/StateEngineStruct.py +++ b/stateengine/StateEngineStruct.py @@ -61,7 +61,7 @@ def __init__(self, abitem, struct_path, global_struct): self._conf = {} self._full_conf = {} self._struct = None - self._global_struct = global_struct # copy.deepcopy(self.itemsApi.return_struct_definitions()) + self._global_struct = global_struct # copy.deepcopy(self.itemsApi.return_struct_definitions()) self._struct_rest = None self._children_structs = [] self._parent_struct = None @@ -160,7 +160,7 @@ def get(self): self.create_children() self.valid_se_use = True if "se_use" in self._full_conf else self.valid_se_use else: - _issue = "Item '{}' does not exist".format( self._struct_rest) + _issue = "Item '{}' does not exist".format(self._struct_rest) self._abitem.update_issues('struct', {self.struct_path: {'issue': _issue}}) self._log_error("{} in struct {}", _issue, self._struct) except Exception as ex: @@ -184,7 +184,8 @@ def __repr__(self): def get(self): try: - self._conf = self.dict_get(self._global_struct.get(self._struct) or {}, self._struct_rest, self._global_struct.get(self._struct) or {}) + self._conf = self.dict_get(self._global_struct.get(self._struct) or {}, + self._struct_rest, self._global_struct.get(self._struct) or {}) except Exception: self._conf = {} @@ -204,7 +205,8 @@ def __repr__(self): def get(self): try: parent_name = self.struct_path.split(".")[-2] - _temp_dict = self.dict_get(self._global_struct.get(self._struct) or {}, parent_name, self._global_struct.get(self._struct) or {}) + _temp_dict = self.dict_get(self._global_struct.get(self._struct) or {}, parent_name, + self._global_struct.get(self._struct) or {}) _temp_dict = collections.OrderedDict( {key: value for (key, value) in _temp_dict.items() if not isinstance(value, collections.abc.Mapping)}) self._conf = _temp_dict diff --git a/stateengine/StateEngineTools.py b/stateengine/StateEngineTools.py index 65e5cc5c0..0da8d1107 100755 --- a/stateengine/StateEngineTools.py +++ b/stateengine/StateEngineTools.py @@ -22,12 +22,8 @@ from . import StateEngineLogger import datetime from ast import literal_eval -from lib.item import Items -from lib.item.item import Item import re -itemsApi = Items.get_instance() -__itemClass = Item # General class for everything that is below the SeItem Class # This class provides some general stuff: @@ -124,7 +120,7 @@ def parse_relative(evalstr, begintag, endtags): rel = rest[:rest.find(endtag)] rest = rest[rest.find(endtag)+len(endtag):] if 'property' in endtag: - rest1 = re.split('([ +\-*/])', rest, 1) + rest1 = re.split('([- +*/])', rest, 1) rest = ''.join(rest1[1:]) pref += "se_eval.get_relative_itemproperty('{}', '{}')".format(rel, rest1[0]) elif '()' in endtag: @@ -261,30 +257,15 @@ def cast_time(value): # base_item: base item to search in # attribute: name of attribute to find def find_attribute(smarthome, base_item, attribute, recursion_depth=0): - # 1: parent of given item could have attribute - try: - parent_item = base_item.return_parent() - except Exception: - return None - try: - _parent_conf = parent_item.conf - if parent_item is not None and attribute in _parent_conf: - return parent_item.conf[attribute] - except Exception: + # if item has attribute "se_use", get the item to use and search this item for required attribute + if recursion_depth > 5: return None - - # 2: if item has attribute "se_use", get the item to use and search this item for required attribute - if "se_use" in base_item.conf: - if recursion_depth > 5: - return None - use_item = itemsApi.return_item(base_item.conf.get("se_use")) - if use_item is not None: - result = find_attribute(smarthome, use_item, attribute, recursion_depth + 1) - if result is not None: - return result - - # 3: nothing found - return None + use_item = smarthome.return_item(base_item.find_attribute("se_use", None, 0)) + if use_item is None: + return base_item.find_attribute(attribute, None, 1) + else: + result = find_attribute(smarthome, use_item, attribute, recursion_depth + 1) + return result # partition value at splitchar and strip resulting parts @@ -305,8 +286,8 @@ def partition_strip(value, splitchar): # return list representation of string # value: list as string # returns: list or original value -def convert_str_to_list(value): - if isinstance(value, str) and ("," in value and value.startswith("[")): +def convert_str_to_list(value, force=True): + if isinstance(value, str) and (value[:1] == '[' and value[-1:] == ']'): value = value.strip("[]") if isinstance(value, str) and "," in value: try: @@ -317,11 +298,12 @@ def convert_str_to_list(value): return literal_eval(formatted_str) except Exception as ex: raise ValueError("Problem converting string to list: {}".format(ex)) - elif isinstance(value, list): + elif isinstance(value, list) or force is False: return value else: return [value] + # return dict representation of string # value: OrderedDict as string # returns: OrderedDict or original value @@ -344,6 +326,7 @@ def convert_str_to_dict(value): except Exception as ex: raise ValueError("Problem converting string to OrderedDict: {}".format(ex)) + # return string representation of eval function # eval_func: eval function # returns: string representation @@ -371,7 +354,9 @@ def get_eval_name(eval_func): # source: source # item: item being updated # eval_type: update or change -def get_original_caller(elog, caller, source, item=None, eval_keyword=['Eval'], eval_type='update'): +def get_original_caller(smarthome, elog, caller, source, item=None, eval_keyword=None, eval_type='update'): + if eval_keyword is None: + eval_keyword = ['Eval'] original_caller = caller original_item = item if isinstance(source, str): @@ -379,7 +364,7 @@ def get_original_caller(elog, caller, source, item=None, eval_keyword=['Eval'], else: original_source = "None" while partition_strip(original_caller, ":")[0] in eval_keyword: - original_item = itemsApi.return_item(original_source) + original_item = smarthome.return_item(original_source) if original_item is None: elog.info("get_caller({0}, {1}): original item not found", caller, source) break diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index 11774967c..2da9cde26 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -119,7 +119,7 @@ def set_from_attr(self, item, attribute_name, default_value=None, reset=True, at if value is not None: self._log_develop("Setting value {0}, attribute name {1}, reset {2}, type {3}", value, attribute_name, reset, attr_type) - _returnvalue, _returntype, _issue = self.set(value, attribute_name, reset, item) + _returnvalue, _returntype, _issue = self.set(value, attribute_name, reset) self._log_develop("Set from attribute returnvalue {}, returntype {}, issue {}", _returnvalue, _returntype, _issue) return _returnvalue, _returntype, _using_default, _issue @@ -144,7 +144,7 @@ def __resetvalue(self): # Set value # value: string indicating value or source of value # name: name of object ("time" is being handled differently) - def set(self, value, name="", reset=True, item=None, copyvalue=True): + def set(self, value, name="", reset=True, copyvalue=True): if copyvalue is True: value = copy.copy(value) if reset: @@ -283,8 +283,8 @@ def set(self, value, name="", reset=True, item=None, copyvalue=True): self._log_warning(_issue) s = None try: - cond1 = s.lstrip('-').replace('.','',1).isdigit() - cond2 = field_value[i].lstrip('-').replace('.','',1).isdigit() + cond1 = s.lstrip('-').replace('.', '', 1).isdigit() + cond2 = field_value[i].lstrip('-').replace('.', '', 1).isdigit() except Exception: cond1 = False cond2 = False @@ -297,7 +297,7 @@ def set(self, value, name="", reset=True, item=None, copyvalue=True): self.__value = [] if self.__value is None else [self.__value] if not isinstance(self.__value, list) else self.__value if s == "value": - cond3 = isinstance(field_value[i], str) and field_value[i].lstrip('-').replace('.','',1).isdigit() + cond3 = isinstance(field_value[i], str) and field_value[i].lstrip('-').replace('.', '', 1).isdigit() if cond3: field_value[i] = ast.literal_eval(field_value[i]) elif isinstance(field_value[i], str) and field_value[i].lower() in ['true', 'yes']: @@ -359,7 +359,7 @@ def set(self, value, name="", reset=True, item=None, copyvalue=True): if isinstance(field_value, list) and not self.__allow_value_list: raise ValueError("{0}: value_in is not allowed, problem with {1}. Allowed = {2}".format( self.__name, field_value, self.__allow_value_list)) - cond3 = isinstance(field_value, str) and field_value.lstrip('-').replace('.','',1).isdigit() + cond3 = isinstance(field_value, str) and field_value.lstrip('-').replace('.', '', 1).isdigit() if cond3: field_value = ast.literal_eval(field_value) elif isinstance(field_value, str) and field_value.lower() in ['true', 'yes']: @@ -382,7 +382,10 @@ def set(self, value, name="", reset=True, item=None, copyvalue=True): def set_cast(self, cast_func): self.__cast_func = cast_func self.__value, _issue = self.__do_cast(self.__value) - return [_issue] + if _issue: + return [_issue] + else: + return [] # determine and return value def get(self, default=None, originalorder=True): @@ -523,9 +526,9 @@ def cast_item(self, value): self._log_error("Can't cast {0} to item/struct! {1}".format(value, ex)) return value - def __update_item_listorder(self, value, newvalue, id=None): + def __update_item_listorder(self, value, newvalue, item_id=None): if value is None: - _id_value = "item:{}".format(id) + _id_value = "item:{}".format(item_id) self.__listorder[self.__listorder.index(_id_value)] = newvalue if value in self.__listorder: self.__listorder[self.__listorder.index(value)] = newvalue @@ -533,31 +536,31 @@ def __update_item_listorder(self, value, newvalue, id=None): _item_value = "item:{}".format(value.property.path) if _item_value in self.__listorder: self.__listorder[self.__listorder.index(_item_value)] = newvalue - if id: - _item_value = "item:{}".format(id) + if item_id: + _item_value = "item:{}".format(item_id) if _item_value in self.__listorder: self.__listorder[self.__listorder.index(_item_value)] = "item:{}".format(newvalue.property.path) self._log_develop("Updated relative declaration {} with absolute item path {}. Listorder is now: {}", _item_value, newvalue.property.path, self.__listorder) - def __absolute_item(self, value, id=None): + def __absolute_item(self, value, item_id=None): if value is None: - self.__update_item_listorder(value, value, id) + self.__update_item_listorder(value, value, item_id) elif isinstance(value, list): valuelist = [] for i, element in enumerate(value): element = self.cast_item(element) - self.__update_item_listorder(value, element, id[i]) + self.__update_item_listorder(value, element, item_id[i]) value = valuelist else: _newvalue = self.cast_item(value) - self.__update_item_listorder(value, _newvalue, id) + self.__update_item_listorder(value, _newvalue, item_id) value = _newvalue return value # Cast given value, if cast-function is set # value: value to cast - def __do_cast(self, value, id=None): + def __do_cast(self, value, item_id=None): _issue = None if value is not None and self.__cast_func is not None: try: @@ -574,7 +577,7 @@ def __do_cast(self, value, id=None): if element in self.__listorder: self.__listorder[self.__listorder.index(element)] = _newvalue if isinstance(element, self.__itemClass): - self.__update_item_listorder(value, _newvalue, id[i]) + self.__update_item_listorder(value, _newvalue, item_id[i]) if isinstance(element, StateEngineStruct.SeStruct): _item_value = "struct:{}".format(element.property.path) @@ -587,7 +590,7 @@ def __do_cast(self, value, id=None): if value in self.__listorder: self.__listorder[self.__listorder.index(value)] = _newvalue if isinstance(value, self.__itemClass): - self.__update_item_listorder(value, _newvalue, id) + self.__update_item_listorder(value, _newvalue, item_id) if isinstance(value, StateEngineStruct.SeStruct): _item_value = "struct:{}".format(value.property.path) diff --git a/stateengine/__init__.py b/stateengine/__init__.py index 9b85e3836..dca97285e 100755 --- a/stateengine/__init__.py +++ b/stateengine/__init__.py @@ -33,7 +33,6 @@ import os import copy from lib.model.smartplugin import * -from lib.item import Items from .webif import WebInterface from datetime import datetime @@ -55,7 +54,6 @@ class StateEngine(SmartPlugin): def __init__(self, sh): super().__init__() StateEngineDefaults.logger = self.logger - self.itemsApi = Items.get_instance() self._items = self.abitems = {} self.mod_http = None self.__sh = sh @@ -63,9 +61,9 @@ def __init__(self, sh): self.__cli = None self.vis_enabled = self._test_visualization() if not self.vis_enabled: - self.logger.warning(f'StateEngine is missing the PyDotPlus package, WebIf visualization is disabled') + self.logger.warning(f'StateEngine is missing the PyDotPlus package or GraphViz, WebIf visualization is disabled') self.init_webinterface(WebInterface) - self.get_sh().stateengine_plugin_functions = StateEngineFunctions.SeFunctions(self.get_sh(), self.logger) + self.__sh.stateengine_plugin_functions = StateEngineFunctions.SeFunctions(self.__sh, self.logger) try: log_level = self.get_parameter_value("log_level") startup_log_level = self.get_parameter_value("startup_log_level") @@ -100,8 +98,8 @@ def __init__(self, sh): StateEngineDefaults.plugin_version = self.PLUGIN_VERSION StateEngineDefaults.write_to_log(self.logger) - StateEngineCurrent.init(self.get_sh()) - base = self.get_sh().get_basedir() + StateEngineCurrent.init(self.__sh) + base = self.__sh.get_basedir() log_directory = SeLogger.manage_logdirectory(base, log_directory, False) SeLogger.log_directory = log_directory @@ -109,7 +107,6 @@ def __init__(self, sh): text = "StateEngine extended logging is active. Logging to '{0}' with log level {1}." self.logger.info(text.format(log_directory, log_level)) - if log_maxage > 0: self.logger.info("StateEngine extended log files will be deleted after {0} days.".format(log_maxage)) cron = ['init', '30 0 * *'] @@ -141,12 +138,12 @@ def parse_item(self, item): # Initialization of plugin def run(self): # Initialize - StateEngineStructs.global_struct = copy.deepcopy(self.itemsApi.return_struct_definitions()) + StateEngineStructs.global_struct = copy.deepcopy(self.__sh.items.return_struct_definitions()) self.logger.info("Init StateEngine items") - for item in self.itemsApi.find_items("se_plugin"): + for item in self.__sh.find_items("se_plugin"): if item.conf["se_plugin"] == "active": try: - abitem = StateEngineItem.SeItem(self.get_sh(), item, self) + abitem = StateEngineItem.SeItem(self.__sh, item, self) abitem.ab_alive = True abitem.update_leave_action(self.__default_instant_leaveaction) abitem.write_to_log() @@ -161,9 +158,9 @@ def run(self): else: self.logger.info("StateEngine deactivated because no items have been found.") - self.__cli = StateEngineCliCommands.SeCliCommands(self.get_sh(), self._items, self.logger) + self.__cli = StateEngineCliCommands.SeCliCommands(self.__sh, self._items, self.logger) self.alive = True - self.get_sh().stateengine_plugin_functions.ab_alive = True + self.__sh.stateengine_plugin_functions.ab_alive = True # Stopping of plugin def stop(self): @@ -176,7 +173,7 @@ def stop(self): self._items[item].remove_all_schedulers() self.alive = False - self.get_sh().stateengine_plugin_functions.ab_alive = False + self.__sh.stateengine_plugin_functions.ab_alive = False self.logger.debug("stop method finished") # Determine if caller/source are contained in changed_by list @@ -184,7 +181,7 @@ def stop(self): # source: Source to check # changed_by: List of callers/source (element format :) to check against def is_changed_by(self, caller, source, changed_by): - original_caller, original_source = StateEngineTools.get_original_caller(self.logger, caller, source) + original_caller, original_source = StateEngineTools.get_original_caller(self.__sh, self.logger, caller, source) for entry in changed_by: entry_caller, __, entry_source = entry.partition(":") if (entry_caller == original_caller or entry_caller == "*") and ( @@ -197,7 +194,7 @@ def is_changed_by(self, caller, source, changed_by): # source: Source to check # changed_by: List of callers/source (element format :) to check against def not_changed_by(self, caller, source, changed_by): - original_caller, original_source = StateEngineTools.get_original_caller(self.logger, caller, source) + original_caller, original_source = StateEngineTools.get_original_caller(self.__sh, self.logger, caller, source) for entry in changed_by: entry_caller, __, entry_source = entry.partition(":") if (entry_caller == original_caller or entry_caller == "*") and ( @@ -261,21 +258,20 @@ def get_graph(self, abitem, graphtype='link'): return '

Can not show visualization.

' \ 'Current issue: ' + str(ex) + '
' - def _test_visualization(self): if not VIS_ENABLED: return False img_path = self.path_join(self.get_plugin_dir(), 'webif/static/img/visualisations/se_test') graph = pydotplus.Dot('StateEngine', graph_type='digraph', splines='false', - overlap='scale', compound='false', imagepath=img_path) + overlap='scale', compound='false', imagepath=img_path) graph.set_node_defaults(color='lightgray', style='filled', shape='box', - fontname='Helvetica', fontsize='10') + fontname='Helvetica', fontsize='10') graph.set_edge_defaults(color='darkgray', style='filled', shape='box', - fontname='Helvetica', fontsize='10') + fontname='Helvetica', fontsize='10') try: result = graph.write_svg(img_path, prog='fdp') except pydotplus.graphviz.InvocationException: - return False - return True + result = False + return result diff --git a/stateengine/webif/__init__.py b/stateengine/webif/__init__.py index b53855a6e..809f8084b 100755 --- a/stateengine/webif/__init__.py +++ b/stateengine/webif/__init__.py @@ -25,13 +25,8 @@ # ######################################################################### -import datetime -import time -import os -import logging import json -from lib.item import Items from lib.model.smartplugin import SmartPluginWebIf @@ -40,8 +35,6 @@ # ------------------------------------------ import cherrypy -import csv -from jinja2 import Environment, FileSystemLoader class WebInterface(SmartPluginWebIf): @@ -54,6 +47,7 @@ def __init__(self, webif_dir, plugin): :type webif_dir: str :type plugin: object """ + super().__init__() self.logger = plugin.logger self.webif_dir = webif_dir self.plugin = plugin @@ -69,7 +63,7 @@ def index(self, action=None, item_id=None, item_path=None, reload=None, abitem=N :return: contents of the template after beeing rendered """ - item = self.plugin.itemsApi.return_item(item_path) + item = self.plugin.get_sh().return_item(item_path) tmpl = self.tplenv.get_template('{}.html'.format(page)) pagelength = self.plugin.get_parameter_value('webif_pagelength') diff --git a/stateengine/webif/templates/index.html b/stateengine/webif/templates/index.html index e52c915f4..16aabcd6d 100755 --- a/stateengine/webif/templates/index.html +++ b/stateengine/webif/templates/index.html @@ -142,7 +142,7 @@ {% endif %} {{ item.logger.log_level_as_num }} - {% for cond in item.webif_infos.keys() %}{% if not p.itemsApi.return_item(cond) == None %}{% if loop.index > 1 %},{% endif %}{{ p.itemsApi.return_item(cond)._name.split('.')[-1] }}{% endif %}{% endfor %} + {% for cond in item.webif_infos.keys() %}{% if not p.get_sh().return_item(cond) == None %}{% if loop.index > 1 %},{% endif %}{{ p.get_sh().return_item(cond)._name.split('.')[-1] }}{% endif %}{% endfor %} From 3612b4cf426a961dcc50cbc8ea0e24cdaa9b09c6 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 18 Jul 2024 16:17:55 +0200 Subject: [PATCH 08/19] stateengine plugin: write log message when log is turned off (set to 0) --- stateengine/StateEngineItem.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 92cda698e..a538bcf98 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -177,6 +177,7 @@ def ab_alive(self, value): def __init__(self, smarthome, item, se_plugin): self.__item = item self.__logger = SeLogger.create(self.__item) + self.__logging_off = False self.update_lock = threading.Lock() self.__ab_alive = False self.__queue = queue.Queue() @@ -197,7 +198,7 @@ def __init__(self, smarthome, item, se_plugin): self.__log_level = StateEngineValue.SeValue(self, "Log Level", False, "num") - _default_log_level = SeLogger.default_log_level.get() + _default_log_level = self.__logger.default_log_level.get() _returnvalue, _returntype, _using_default, _issue = self.__log_level.set_from_attr(self.__item, "se_log_level", _default_log_level) self.__using_default_log_level = _using_default @@ -206,11 +207,11 @@ def __init__(self, smarthome, item, se_plugin): _returnvalue = _returnvalue[0] self.__logger.log_level_as_num = 2 - _startup_log_level = SeLogger.startup_log_level.get() + _startup_log_level = self.__logger.startup_log_level.get() if _startup_log_level > 0: base = self.__sh.get_basedir() - SeLogger.manage_logdirectory(base, SeLogger.log_directory, True) + self.__logger.manage_logdirectory(base, self.__logger.log_directory, True) self.__logger.log_level_as_num = _startup_log_level self.__logger.header("") self.__logger.info("Set log level to startup log level {}", _startup_log_level) @@ -356,7 +357,6 @@ def __init__(self, smarthome, item, se_plugin): self.__logger.error("Issue finishing states because {}", ex) return - def __repr__(self): return self.__id @@ -461,22 +461,29 @@ def run_queue(self): StateEngineDefaults.plugin_identification) return _current_log_level = self.__log_level.get() - _default_log_level = SeLogger.default_log_level.get() + _default_log_level = self.__logger.default_log_level.get() if _current_log_level <= -1: self.__using_default_log_level = True - value = SeLogger.default_log_level.get() + value = self.__logger.default_log_level.get() else: value = _current_log_level self.__using_default_log_level = False self.__logger.log_level_as_num = value - + additional_text = ", currently using default" if self.__using_default_log_level is True else "" if _current_log_level > 0: base = self.__sh.get_basedir() - SeLogger.manage_logdirectory(base, SeLogger.log_directory, True) - additional_text = ", currently using default" if self.__using_default_log_level is True else "" + self.__logger.manage_logdirectory(base, self.__logger.log_directory, True) + self.__logging_off = False + elif self.__logging_off is False: + self.__logger.log_level_as_num = 1 + self.__logger.info("Logging turned off! Current log level {} ({}), default {}{}", + _current_log_level, type(self.__logger.log_level), _default_log_level, additional_text) + self.__logger.log_level_as_num = value + self.__logging_off = True + self.__logger.info("Current log level {} ({}), default {}{}", - _current_log_level, type(self.__logger.log_level), _default_log_level, additional_text) + _current_log_level, type(self.__logger.log_level), _default_log_level, additional_text) _instant_leaveaction = self.__instant_leaveaction.get() _default_instant_leaveaction_value = self.__default_instant_leaveaction.get() if _instant_leaveaction <= -1: From 168e63f92278aa3535fa41df94390d06d46df11e Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 18 Jul 2024 16:18:15 +0200 Subject: [PATCH 09/19] stateengine plugin: dump version to 2.0.1 --- stateengine/__init__.py | 2 +- stateengine/plugin.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stateengine/__init__.py b/stateengine/__init__.py index dca97285e..819c5079d 100755 --- a/stateengine/__init__.py +++ b/stateengine/__init__.py @@ -47,7 +47,7 @@ class StateEngine(SmartPlugin): - PLUGIN_VERSION = '2.0.0' + PLUGIN_VERSION = '2.0.1' # Constructor # noinspection PyUnusedLocal,PyMissingConstructor diff --git a/stateengine/plugin.yaml b/stateengine/plugin.yaml index 196c7bd21..fc58cdc29 100755 --- a/stateengine/plugin.yaml +++ b/stateengine/plugin.yaml @@ -39,7 +39,7 @@ plugin: state: ready support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1303071-stateengine-plugin-support - version: 2.0.0 + version: 2.0.1 sh_minversion: '1.6' multi_instance: False classname: StateEngine From 4a6309f5abb9d4687f7658420fe96df5f0ff7ba3 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 25 Jul 2024 17:46:19 +0200 Subject: [PATCH 10/19] stateengine plugin: minor updates --- stateengine/StateEngineActions.py | 1 - stateengine/StateEngineConditionSet.py | 4 ++++ stateengine/StateEngineEval.py | 7 ++++-- stateengine/StateEngineItem.py | 31 ++++++++++++++++---------- stateengine/StateEngineState.py | 11 ++++++++- stateengine/StateEngineStruct.py | 9 ++++++-- stateengine/StateEngineValue.py | 4 ++-- stateengine/StateEngineWebif.py | 2 +- 8 files changed, 48 insertions(+), 21 deletions(-) diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 14b4a6149..c91e18b0b 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -84,7 +84,6 @@ def update(self, attribute, value): value = ":".join(map(str.strip, value.split(":"))) if value[:1] == '[' and value[-1:] == ']': value = StateEngineTools.convert_str_to_list(value, False) - self._log_debug("Value is now {}", value) if func == "se_delay": # set delay if name not in self.__actions: diff --git a/stateengine/StateEngineConditionSet.py b/stateengine/StateEngineConditionSet.py index 3aae5bc86..ab26b1240 100755 --- a/stateengine/StateEngineConditionSet.py +++ b/stateengine/StateEngineConditionSet.py @@ -36,6 +36,10 @@ def name(self): def id(self): return self.__id + @property + def path(self): + return self.__id + # List of conditions that are part of this condition set @property def conditions(self): diff --git a/stateengine/StateEngineEval.py b/stateengine/StateEngineEval.py index e47afe401..719c73c67 100755 --- a/stateengine/StateEngineEval.py +++ b/stateengine/StateEngineEval.py @@ -166,7 +166,9 @@ def get_relative_itemvalue(self, subitem_id): item, issue = self._abitem.return_item(subitem_id) returnvalue = item.property.value returnvalue = StateEngineTools.convert_str_to_list(returnvalue) - self._log_debug("Return item value '{0}' for item {1}", returnvalue, subitem_id) + issue = f" Issue: {issue}" if issue not in [[], None, [None]] else "" + self._log_debug("Return item value '{0}' for item {1}.{2}", + returnvalue, subitem_id, issue) except Exception as ex: self._log_warning("Problem evaluating value of '{0}': {1}", subitem_id, ex) finally: @@ -183,7 +185,7 @@ def get_relative_itemproperty(self, subitem_id, prop): self._eval_lock.acquire() self._log_debug("Executing method 'get_relative_itemproperty({0}, {1})'", subitem_id, prop) try: - item, issue = self._abitem.return_item(subitem_id) + item, _ = self._abitem.return_item(subitem_id) except Exception as ex: self._log_warning("Problem evaluating property of {0} - relative item might not exist. Error: {1}", subitem_id, ex) @@ -223,6 +225,7 @@ def get_attribute_value(self, item, attrib): def get_attributevalue(self, item, attrib): self._eval_lock.acquire() self._log_debug("Executing method 'get_attributevalue({0}, {1})'", item, attrib) + issue = None if ":" in item: var_type, item = StateEngineTools.partition_strip(item, ":") if var_type == "var": diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index a538bcf98..2f84d31ee 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -48,6 +48,10 @@ class SeItem: def id(self): return self.__id + @property + def path(self): + return self.__id + @property def variables(self): return self.__variables @@ -114,7 +118,7 @@ def laststate(self): @property def laststate_releasedby(self): _returnvalue = None if self.__laststate_item_id is None \ - else self.__release_info.get(self.__laststate_item_id.property.value) + else self.__release_info.get(self.__laststate_item_id.property.value) return _returnvalue @property @@ -290,7 +294,8 @@ def __init__(self, smarthome, item, se_plugin): self.__previousstate_conditionset_internal_name = "" if self.__previousstate_conditionset_item_name is None else \ self.__previousstate_conditionset_item_name.property.value self.__config_issues.update(_issue) - filtered_dict = {key: value for key, value in self.__config_issues.items() if value.get('issue') not in [[], [None], None]} + filtered_dict = {key: value for key, value in self.__config_issues.items() if + value.get('issue') not in [[], [None], None]} self.__config_issues = filtered_dict self.__states = [] @@ -1203,7 +1208,7 @@ def __initialize_state(self, item_state, _statecount): _issue = _state.update_order(_statecount) if _issue: self.__config_issues.update({item_state.property.path: - {'issue': _issue, 'attribute': 'se_stateorder'}}) + {'issue': _issue, 'attribute': 'se_stateorder'}}) self.__logger.error("Issue with state {0} while setting order: {1}", item_state.property.path, _issue) self.__states.append(_state) @@ -1216,13 +1221,13 @@ def __initialize_state(self, item_state, _statecount): return _statecount + 1 except ValueError as ex: self.update_issues('state', {item_state.property.path: {'issue': ex, 'issueorigin': - [{'conditionset': 'None', 'condition': 'ValueError'}]}}) + [{'conditionset': 'None', 'condition': 'ValueError'}]}}) self.__logger.error("Ignoring state {0} because ValueError: {1}", item_state.property.path, ex) return _statecount except Exception as ex: self.update_issues('state', {item_state.property.path: {'issue': ex, 'issueorigin': - [{'conditionset': 'None', 'condition': 'GeneralError'}]}}) + [{'conditionset': 'None', 'condition': 'GeneralError'}]}}) self.__logger.error("Ignoring state {0} because: {1}", item_state.property.path, ex) return _statecount @@ -1241,7 +1246,8 @@ def update_state(self, item, caller=None, source=None, dest=None): return self.__queue.put(["stateevaluation", item, caller, source, dest]) if not self.update_lock.locked(): - self.__logger.debug("Run queue to update state. Item: {}, caller: {}, source: {}", item.property.path, caller, source) + self.__logger.debug("Run queue to update state. Item: {}, caller: {}, source: {}", item.property.path, + caller, source) self.run_queue() # check if state can be entered after setting state-specific variables @@ -1609,7 +1615,7 @@ def process_returnvalue(value): _returnvalue_issue_list.append(_returnvalue_issue) elif _valueindex < _stateindex: _returnvalue_issue = "State {} defined by {} in se_released_by " \ - "attribute of state {} must be lower priority "\ + "attribute of state {} must be lower priority " \ "than actual state.".format(match, _returnvalue[i], state.id) self.__logger.warning("{} Removing it.", _returnvalue_issue) if _returnvalue_issue not in _returnvalue_issue_list: @@ -1624,7 +1630,7 @@ def process_returnvalue(value): _returnvalue_issue = _returnvalue_issue_list if not matches: - _returnvalue_issue = "No states match regex {} defined in "\ + _returnvalue_issue = "No states match regex {} defined in " \ "se_released_by attribute of state {}.".format(value, state.id) self.__logger.warning("{} Removing it.", _returnvalue_issue) elif _returntype[i] == 'eval': @@ -1661,7 +1667,7 @@ def process_returnvalue(value): v_list.append(v) _converted_typelist.append(_returntype[i]) else: - _returnvalue_issue = "Found invalid definition in se_released_by attribute "\ + _returnvalue_issue = "Found invalid definition in se_released_by attribute " \ "of state {}, original {}.".format(state.id, v, original_value) self.__logger.warning("{} Removing it.", _returnvalue_issue) _converted_evaluatedlist.append(v_list) @@ -1769,7 +1775,8 @@ def write_to_log(self): state.write_to_log() self.initstate = None - filtered_dict = {key: value for key, value in self.__config_issues.items() if value.get('issue') not in [[], [None], None]} + filtered_dict = {key: value for key, value in self.__config_issues.items() if + value.get('issue') not in [[], [None], None]} self.__config_issues = filtered_dict # endregion @@ -1938,8 +1945,8 @@ def return_item(self, item_id): _issue = "item_id is None" return None, [_issue] if not isinstance(item_id, str): - _issue = "'{0}' is not defined as string.".format(item_id) - self.__logger.info("{0} Check your item config!", _issue, item_id) + _issue = "'{0}' is not defined as string, cannot find item.".format(item_id) + self.__logger.warning("{0} Check your item config!", _issue, item_id) return None, [_issue] item_id = item_id.strip() if item_id.startswith("struct:"): diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index 7243f3c2e..120e59126 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -38,6 +38,14 @@ class SeState(StateEngineTools.SeItemChild): def id(self): return self.__id + @property + def path(self): + return self.__id + + @property + def use(self): + return self.__use + @property def state_item(self): return self.__item @@ -171,7 +179,7 @@ def __init__(self, abitem, item_state): self._log_decrease_indent() def __repr__(self): - return "SeState item: {}, id {}.".format(self.__item, self.__id) + return "SeState item: {}, id {}".format(self.__item, self.__id) # Check conditions if state can be entered # returns: True = At least one enter condition set is fulfilled, False = No enter condition set is fulfilled @@ -374,6 +382,7 @@ def update_name(self, item_state, recursion_depth=0): elif self.__text.is_empty() and recursion_depth == 0: self.__text.set("value:" + self.__name) self.__name = self.text + self._log_develop("Updated name of state {} to {}.", item_state, self.__name) return self.__name def __fill_list(self, item_states, recursion_depth, se_use=None): diff --git a/stateengine/StateEngineStruct.py b/stateengine/StateEngineStruct.py index 163ee042f..51a958efa 100755 --- a/stateengine/StateEngineStruct.py +++ b/stateengine/StateEngineStruct.py @@ -47,6 +47,10 @@ def conf(self): def id(self): return self.struct_path + @property + def path(self): + return self.struct_path + def return_children(self): for child in self._conf.keys(): yield child @@ -103,7 +107,7 @@ def get(self): raise NotImplementedError("Class {} doesn't implement get()".format(self.__class__.__name__)) -# Class representing struct child +# Class representing struct class SeStructMain(SeStruct): # Initialize the action # abitem: parent SeItem instance @@ -200,11 +204,12 @@ def __init__(self, abitem, struct_path, global_struct): #self._log_debug("Struct path {} for {}", self.struct_path, __class__.__name__) def __repr__(self): - return "SeStructParent {}".format(self.struct_path, self._conf) + return "SeStructParent {}".format(self.struct_path) def get(self): try: parent_name = self.struct_path.split(".")[-2] + self.struct_path = self.struct_path.rsplit('.', 1)[0] _temp_dict = self.dict_get(self._global_struct.get(self._struct) or {}, parent_name, self._global_struct.get(self._struct) or {}) _temp_dict = collections.OrderedDict( diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index 2da9cde26..b60b57bbf 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -482,7 +482,7 @@ def write_to_logger(self): if self.__eval is not None: self._log_debug("{0} from eval: {1}", self.__name, self.__eval) _original_listorder = self.__listorder.copy() - self._log_debug("Currently eval results in {}", self.__get_eval()) + self._log_debug("Currently eval results in {}. ", self.__get_eval()) self.__listorder = _original_listorder if self.__varname is not None: if isinstance(self.__varname, list): @@ -722,7 +722,7 @@ def __get_eval(self): val = val.replace("\n", "") except Exception: pass - self._log_debug("Checking eval from list: {0}.", val) + self._log_debug("Checking eval {0} from list {1}.", val, self.__eval) self._log_increase_indent() if isinstance(val, str): if "stateengine_eval" in val or "se_eval" in val: diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index 69f5b44e8..3181788b2 100755 --- a/stateengine/StateEngineWebif.py +++ b/stateengine/StateEngineWebif.py @@ -57,7 +57,7 @@ def __init__(self, abitem): self.__conditionset_count = 0 def __repr__(self): - return "WebInterface item: {}, id {}.".format(self.__states, self.__name) if REQUIRED_PACKAGE_IMPORTED else "None" + return "WebInterface item: {}, id {}".format(self.__states, self.__name) if REQUIRED_PACKAGE_IMPORTED else "None" def _actionlabel(self, state, label_type, conditionset, previousconditionset, previousstate_conditionset): # Check if conditions for action are met or not From 6634e68cf16d03e1f209d158a4d035713bcbb43f Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 25 Jul 2024 17:49:46 +0200 Subject: [PATCH 11/19] stateengine plugin: improve and extend issue reporting when getting values --- stateengine/StateEngineEval.py | 9 +-- stateengine/StateEngineItem.py | 48 +++++++++++++-- stateengine/StateEngineState.py | 21 ++++++- stateengine/StateEngineValue.py | 100 ++++++++++++++++++++++++-------- 4 files changed, 143 insertions(+), 35 deletions(-) diff --git a/stateengine/StateEngineEval.py b/stateengine/StateEngineEval.py index 719c73c67..55abe6db6 100755 --- a/stateengine/StateEngineEval.py +++ b/stateengine/StateEngineEval.py @@ -234,12 +234,13 @@ def get_attributevalue(self, item, attrib): item, issue = self._abitem.return_item(item) try: if self._abitem.initstate and item == '..state_name': - returnvalue = self._abitem.return_item(self._abitem.initstate.id).conf[attrib] - self._log_debug("Return item attribute '{0}' from {1}: {2} during init", attrib, - self._abitem.return_item(self._abitem.initstate.id)[0].property.path, returnvalue) + returnvalue, issue = self._abitem.return_item(self._abitem.initstate.id).conf[attrib] + self._log_debug("Return item attribute '{0}' from {1}: {2} during init. Issue {3}", attrib, + self._abitem.return_item(self._abitem.initstate.id)[0].property.path, returnvalue, issue) else: returnvalue = item.conf[attrib] - self._log_debug("Return item attribute {0} from {1}: {2}", attrib, item.property.path, returnvalue) + self._log_debug("Return item attribute {0} from {1}: {2}. Issue {3}", + attrib, item.property.path, returnvalue, issue) except Exception as ex: returnvalue = None self._log_warning("Problem evaluating attribute {0} of {1} - attribute might not exist. " diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 2f84d31ee..c5d30a9e5 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -979,13 +979,29 @@ def combine_dicts(dict1, dict2): def update_issues(self, issue_type, issues): def combine_dicts(dict1, dict2): + def update_list(existing, new_entries): + # Ensure existing is a list + if not isinstance(existing, list): + existing = [existing] + if not isinstance(new_entries, list): + new_entries = [new_entries] + # Append new entries to the list if they don't exist + for entry in new_entries: + if entry not in existing: + existing.append(entry) + return existing + combined_dict = dict1.copy() for key, value in dict2.items(): - if key in combined_dict and combined_dict[key].get('issueorigin'): - combined_dict[key]['issueorigin'].extend(value['issueorigin']) - else: + if key not in combined_dict: combined_dict[key] = value + continue + combined_entry = combined_dict[key] + if 'issue' in value: + combined_entry['issue'] = update_list(combined_entry.get('issue', []), value['issue']) + if 'issueorigin' in value: + combined_entry['issueorigin'] = update_list(combined_entry.get('issueorigin', []), value['issueorigin']) return combined_dict @@ -1031,13 +1047,33 @@ def update_attributes(self, unused_attributes, used_attributes): self.__used_attributes = combined_dict def __log_issues(self, issue_type): + def print_readable_dict(data): + for key, value in data.items(): + if isinstance(value, list): + formatted_entries = [] + for item in value: + if isinstance(item, dict): + for sub_key, sub_value in item.items(): + if isinstance(sub_value, list): + formatted_entries.append(f"{sub_key}: {', '.join(sub_value)}") + else: + formatted_entries.append(f"{sub_key}: {sub_value}") + else: + formatted_entries.append(item) + if formatted_entries: + self.__logger.info(f"- {key}: {', '.join(formatted_entries)}") + else: + self.__logger.info(f"- {key}: {value}") def list_issues(v): _issuelist = StateEngineTools.flatten_list(v.get('issue')) if isinstance(_issuelist, list) and len(_issuelist) > 1: self.__logger.info("has the following issues:") self.__logger.increase_indent() for e in _issuelist: - self.__logger.info("- {}", e) + if isinstance(e, dict): + print_readable_dict(e) + else: + self.__logger.info("- {}", e) self.__logger.decrease_indent() elif isinstance(_issuelist, list) and len(_issuelist) == 1: self.__logger.info("has the following issue: {}", _issuelist[0]) @@ -1096,7 +1132,9 @@ def list_issues(v): self.__logger.info("") continue elif issue_type == 'structs': - self.__logger.info("Struct {} has an issue: {}", entry, value.get('issue')) + self.__logger.info("Struct {} ", entry) + #self.__logger.info("") + list_issues(value) self.__logger.info("") continue else: diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index 120e59126..a40d75815 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -519,9 +519,24 @@ def update_action_status(action_status, actiontype): cleaned_use_list.append(_configvalue[i]) if _returntype[i] in ['item', 'eval']: _path = _configvalue[i] - self._log_info("se_use {} defined by item/eval. Even if current result is not valid, " - "entry will be re-evaluated on next state evaluation.", _path) - if _path is not None and _path not in cleaned_use_list: + _issues = self.__use.get_issues() + for list_key in ['cast_item', 'eval', 'item']: + if list_key in _issues: + for item in _issues[list_key]: + if _path is not None and StateEngineTools.partition_strip(_path, ":")[1] in item: + self._log_warning("se_use {} defined by invalid item/eval. Ignoring", _path) + _issue_list = [item for key, value in _issues.items() if value for item in value] + + self._abitem.update_issues('config', {state.id: + {'issue': [_issues], + 'attribute': 'se_use'}}) + _path = None + if _path is None: + pass + + elif _path is not None and _path not in cleaned_use_list: + self._log_info("se_use {} defined by item/eval. Even if current result is not valid, " + "entry will be re-evaluated on next state evaluation.", _path) cleaned_use_list.append(_path) self.__use_done.append(_path) if _path is None: diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index b60b57bbf..3cd4009b8 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -54,6 +54,7 @@ def __init__(self, abitem, name, allow_value_list=False, value_type=None): self.__varname = None self.__template = None self.__issues = [] + self.__get_issues = {'cast_item': [], 'eval': [], 'regex': [], 'struct': [], 'var': [], 'item': []} self._additional_sources = [] self.itemsApi = Items.get_instance() self.__itemClass = Item @@ -83,6 +84,9 @@ def is_empty(self): return self.__value is None and self.__item is None and self.__eval is None and \ self.__varname is None and self.__regex is None and self.__struct is None + def get_issues(self): + return self.__get_issues + # Set value directly from attribute # item: item containing the attribute # attribute_name: name of attribute to use @@ -315,6 +319,9 @@ def set(self, value, name="", reset=True, copyvalue=True): if s == "item": _item, _issue = self._abitem.return_item(field_value[i]) if _issue not in [[], None, [None], self.__issues]: + _issue_dict = {field_value[i]: _issue} + if _issue_dict not in self.__get_issues['item']: + self.__get_issues['item'].append(_issue_dict) self.__issues.append(_issue) self.__item.append(None if s != "item" else self.__absolute_item(_item, field_value[i])) self.__eval = [] if self.__eval is None else [self.__eval] if not isinstance(self.__eval, list) else self.__eval @@ -349,6 +356,9 @@ def set(self, value, name="", reset=True, copyvalue=True): if source == "item": _item, _issue = self._abitem.return_item(field_value) if _issue not in [[], None, [None], self.__issues]: + _issue_dict = {field_value: _issue} + if _issue_dict not in self.__get_issues['item']: + self.__get_issues['item'].append(_issue_dict) self.__issues.append(_issue) self.__item = None if source != "item" else self.__absolute_item(_item, field_value) self.__eval = None if source != "eval" else field_value @@ -521,8 +531,18 @@ def get_text(self, prefix=None, suffix=None): def cast_item(self, value): try: _returnvalue, _issue = self._abitem.return_item(value) + if _issue not in [[], None, [None]]: + _issue_dict = {str(value): _issue[0]} + else: + _issue_dict = {} + if _issue_dict and _issue_dict not in self.__get_issues['cast_item']: + self.__get_issues['cast_item'].append(_issue_dict) return _returnvalue except Exception as ex: + _issue = "Can't cast {0} to item/struct! {1}".format(value, ex) + _issue_dict = {str(value): _issue} + if _issue_dict not in self.__get_issues['cast_item']: + self.__get_issues['cast_item'].append(_issue_dict) self._log_error("Can't cast {0} to item/struct! {1}".format(value, ex)) return value @@ -631,29 +651,40 @@ def __get_from_struct(self): for val in self.__struct: if val is not None: _newvalue, _issue = self.__do_cast(val) + _issue_dict = {val: _issue} + if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['struct']: + self.__get_issues['struct'].append(_issue_dict) values.append(_newvalue) if 'struct:{}'.format(val.property.path) in self.__listorder: self.__listorder[self.__listorder.index('struct:{}'.format(val.property.path))] = _newvalue else: if self.__struct is not None: _newvalue, _issue = self.__do_cast(self.__struct) + _issue_dict = {self.__struct: _issue} + if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['struct']: + self.__get_issues['struct'].append(_issue_dict) if 'struct:{}'.format(self.__regex) in self.__listorder: self.__listorder[self.__listorder.index('struct:{}'.format(self.__struct))] = _newvalue values = _newvalue + if values: return values try: _newvalue, _issue = self.__do_cast(self.__struct) + _issue_dict = {_newvalue: _issue} + if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['struct']: + self.__get_issues['struct'].append(_issue_dict) if 'struct:{}'.format(self.__struct) in self.__listorder: self.__listorder[self.__listorder.index('struct:{}'.format(self.__struct))] = _newvalue values = _newvalue except Exception as ex: values = self.__struct _issue = "Problem while getting from struct '{0}': {1}.".format(values, ex) - #self.__issues.append(_issue) + _issue_dict = {values: _issue} + if _issue_dict not in self.__get_issues['struct']: + self.__get_issues['struct'].append(_issue_dict) self._log_info(_issue) - return values # Determine value by regular expression @@ -672,7 +703,6 @@ def __get_from_regex(self): values = _newvalue if values is not None: return values - try: _newvalue = re.compile(self.__regex, re.IGNORECASE) if 'regex:{}'.format(self.__regex) in self.__listorder: @@ -681,7 +711,9 @@ def __get_from_regex(self): except Exception as ex: values = self.__regex _issue = "Problem while creating regex '{0}': {1}.".format(values, ex) - #self.__issues.append(_issue) + _issue_dict = {values: _issue} + if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['regex']: + self.__get_issues['regex'].append(_issue_dict) self._log_info(_issue) return values @@ -699,6 +731,9 @@ def __get_eval(self): self._log_increase_indent() try: _newvalue, _issue = self.__do_cast(eval(self.__eval)) + _issue_dict = {StateEngineTools.get_eval_name(self.__eval): _issue} + if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['eval']: + self.__get_issues['eval'].append(_issue_dict) if 'eval:{}'.format(self.__eval) in self.__listorder: self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = [_newvalue] values = _newvalue @@ -707,8 +742,9 @@ def __get_eval(self): self._log_increase_indent() except Exception as ex: self._log_decrease_indent() - _issue = "Problem evaluating '{0}': {1}.".format(StateEngineTools.get_eval_name(self.__eval), ex) - #self.__issues.append(_issue) + _name = StateEngineTools.get_eval_name(self.__eval) + _issue = "Problem evaluating '{0}': {1}.".format(_name, ex) + self.__get_issues['eval'].append({_name: ex}) self._log_warning(_issue) self._log_increase_indent() values = None @@ -730,6 +766,9 @@ def __get_eval(self): stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem) try: _newvalue, _issue = self.__do_cast(eval(val)) + _issue_dict = {val: _issue} + if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['eval']: + self.__get_issues['eval'].append(_issue_dict) if 'eval:{}'.format(val) in self.__listorder: self.__listorder[self.__listorder.index('eval:{}'.format(val))] = [_newvalue] value = _newvalue @@ -740,13 +779,18 @@ def __get_eval(self): self._log_decrease_indent() _issue = "Problem evaluating from list '{0}': {1}.".format( StateEngineTools.get_eval_name(val), ex) - #self.__issues.append(_issue) + _issue_dict = {val: ex} + if _issue_dict not in self.__get_issues['eval']: + self.__get_issues['eval'].append(_issue_dict) self._log_warning(_issue) self._log_increase_indent() value = None else: try: _newvalue, _issue = self.__do_cast(val()) + _issue_dict = {str(val): _issue} + if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['eval']: + self.__get_issues['eval'].append(_issue_dict) if 'eval:{}'.format(val) in self.__listorder: self.__listorder[self.__listorder.index('eval:{}'.format(val))] = [_newvalue] value = _newvalue @@ -754,7 +798,9 @@ def __get_eval(self): self._log_decrease_indent() _issue = "Problem evaluating '{0}': {1}.".format( StateEngineTools.get_eval_name(val), ex) - #self.__issues.append(_issue) + _issue_dict = {str(val): _issue} + if _issue_dict not in self.__get_issues['eval']: + self.__get_issues['eval'].append(_issue_dict) self._log_info(_issue) value = None if value is not None: @@ -765,6 +811,9 @@ def __get_eval(self): try: self._log_increase_indent() _newvalue, _issue = self.__do_cast(self.__eval()) + _issue_dict = {_newvalue: _issue} + if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['eval']: + self.__get_issues['eval'].append(_issue_dict) if 'eval:{}'.format(self.__eval) in self.__listorder: self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = [_newvalue] values = _newvalue @@ -773,17 +822,18 @@ def __get_eval(self): self._log_increase_indent() except Exception as ex: self._log_decrease_indent() - _issue = "Problem evaluating '{0}': {1}.".format(StateEngineTools.get_eval_name(self.__eval), ex) - #self.__issues.append(_issue) + _name = StateEngineTools.get_eval_name(self.__eval) + _issue = "Problem evaluating '{0}': {1}.".format(_name, ex) self._log_warning(_issue) self._log_increase_indent() + _issue_dict = {_name: _issue} + if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['eval']: + self.__get_issues['eval'].append(_issue_dict) return None - return values # Determine value from item def __get_from_item(self): - _issue_list = [] if isinstance(self.__item, list): values = [] for val in self.__item: @@ -800,8 +850,9 @@ def __get_from_item(self): for entry in checked_entry: _newvalue, _issue = self.__do_cast(entry) - if _issue not in [[], None, [None], _issue_list]: - _issue_list.append(_issue) + _issue_dict = {entry: _issue} + if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['item']: + self.__get_issues['item'].append(_issue_dict) if _newvalue is not None: _new_values.append(_newvalue) @@ -815,6 +866,7 @@ def __get_from_item(self): index = self.__listorder.index(search_item) self.__listorder[index] = _new_values values.append(_new_values) + if values is not None: return values else: @@ -829,8 +881,9 @@ def __get_from_item(self): _new_values = [] for entry in checked_entry: _newvalue, _issue = self.__do_cast(entry) - if _issue not in [[], None, [None], _issue_list]: - _issue_list.append(_issue) + _issue_dict = {entry: _issue} + if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['item']: + self.__get_issues['item'].append(_issue_dict) if _newvalue is not None: _new_values.append(_newvalue) _new_values = _new_values[0] if len(_new_values) == 1 else None if len(_new_values) == 0 else [_new_values] @@ -855,12 +908,11 @@ def __get_from_item(self): except Exception as ex: values = self.__item _issue = "Problem while reading item path '{0}': {1}.".format(values, ex) - if _issue not in _issue_list: - _issue_list.append(_issue) self._log_info(_issue) _newvalue, _issue = self.__do_cast(values) - if _issue not in [[], None, [None], _issue_list]: - _issue_list.append(_issue) + _issue_dict = {_newvalue: _issue} + if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['item']: + self.__get_issues['item'].append(_issue_dict) return _newvalue # Determine value from variable @@ -870,10 +922,12 @@ def update_value(varname): new_value, _issue = self.__do_cast(value) new_value = 'var:{}'.format(varname) if new_value == '' else new_value if isinstance(new_value, str) and 'Unknown variable' in new_value: - issue = "There is a problem with your variable {}".format(new_value) - #self.__issues.append(issue) - self._log_warning(issue) + _issue = "There is a problem with your variable {}".format(new_value) + self._log_warning(_issue) new_value = '' + _issue_dict = {varname: _issue} + if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['var']: + self.__get_issues['var'].append(_issue_dict) self._log_debug("Checking variable '{0}', value {1} from list {2}", varname, new_value, self.__listorder) if 'var:{}'.format(varname) in self.__listorder: From d6f96ff5e59d273de9ac2e0c268dcef655af2957 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 25 Jul 2024 17:51:43 +0200 Subject: [PATCH 12/19] stateengine plugin: attributes from items, structs, etc. referenced by se_use are now evaluated correctly, even if they are defined as lists. --- stateengine/StateEngineAction.py | 54 ++++++++++++------------- stateengine/StateEngineActions.py | 10 ++--- stateengine/StateEngineCondition.py | 16 ++++---- stateengine/StateEngineConditionSet.py | 13 +++--- stateengine/StateEngineConditionSets.py | 6 +-- stateengine/StateEngineItem.py | 5 +++ stateengine/StateEngineState.py | 53 ++++++++++++++---------- stateengine/StateEngineTools.py | 45 +++++++++++++++++---- 8 files changed, 124 insertions(+), 78 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 421801c84..fdc5460b1 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -286,9 +286,9 @@ def check_getitem_fromeval(self, check_item, check_value=None, check_mindelta=No 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return check_item, check_value, check_mindelta, _issue - def check_complete(self, item_state, check_item, check_status, check_mindelta, check_value, action_type, evals_items=None): + def check_complete(self, state, check_item, check_status, check_mindelta, check_value, action_type, evals_items=None): _issue = {self._name: {'issue': None, - 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} + 'issueorigin': [{'state': state.id, 'action': self._function}]}} self._log_develop("Check item {} status {} value {} evals_items {}", check_item, check_status, check_value, evals_items) try: _name = evals_items.get(self.name) @@ -304,36 +304,36 @@ def check_complete(self, item_state, check_item, check_status, check_mindelta, c else: _returnissue = None _issue = {self._name: {'issue': _returnissue, - 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} + 'issueorigin': [{'state': state.id, 'action': self._function}]}} self._log_debug("Check item {} status {} value {} _returnissue {}", check_item, check_status, check_value, _returnissue) except Exception as ex: self._log_info("No valid item info for action {}, trying to get differently. Problem: {}", self._name, ex) # missing item in action: Try to find it. if check_item is None: - item = StateEngineTools.find_attribute(self._sh, item_state, "se_item_" + self._name) + item = StateEngineTools.find_attribute(self._sh, state, "se_item_" + self._name) if item is not None: check_item, _issue = self._abitem.return_item(item) _issue = {self._name: {'issue': _issue, - 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} + 'issueorigin': [{'state': state.id, 'action': self._function}]}} else: - item = StateEngineTools.find_attribute(self._sh, item_state, "se_eval_" + self._name) + item = StateEngineTools.find_attribute(self._sh, state, "se_eval_" + self._name) if item is not None: check_item = str(item) if check_item is None and _issue[self._name].get('issue') is None: _issue = {self._name: {'issue': ['Item not defined in rules section'], - 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} + 'issueorigin': [{'state': state.id, 'action': self._function}]}} # missing status in action: Try to find it. if check_status is None: - status = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self._name) + status = StateEngineTools.find_attribute(self._sh, state, "se_status_" + self._name) if status is not None: check_status, _issue = self._abitem.return_item(status) _issue = {self._name: {'issue': _issue, - 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} + 'issueorigin': [{'state': state.id, 'action': self._function}]}} if check_mindelta.is_empty(): - mindelta = StateEngineTools.find_attribute(self._sh, item_state, "se_mindelta_" + self._name) + mindelta = StateEngineTools.find_attribute(self._sh, state, "se_mindelta_" + self._name) if mindelta is not None: check_mindelta.set(mindelta) @@ -356,7 +356,7 @@ def check_complete(self, item_state, check_item, check_status, check_mindelta, c self._log_develop("Issue with {} action {}", action_type, _issue) else: _issue = {self._name: {'issue': None, - 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} + 'issueorigin': [{'state': state.id, 'action': self._function}]}} return check_item, check_status, check_mindelta, check_value, _issue @@ -521,8 +521,8 @@ def update(self, value): raise NotImplementedError("Class {} doesn't implement update()".format(self.__class__.__name__)) # Complete action - # item_state: state item to read from - def complete(self, item_state, evals_items=None): + # state: state (item) to read from + def complete(self, state, evals_items=None): raise NotImplementedError("Class {} doesn't implement complete()".format(self.__class__.__name__)) # Check if execution is possible @@ -624,10 +624,10 @@ def update(self, value): return _issue # Complete action - # item_state: state item to read from - def complete(self, item_state, evals_items=None): + # state: state (item) to read from + def complete(self, state, evals_items=None): self.__item, self.__status, self.__mindelta, self.__value, _issue = self.check_complete( - item_state, self.__item, self.__status, self.__mindelta, self.__value, "set", evals_items) + state, self.__item, self.__status, self.__mindelta, self.__value, "set", evals_items) self._action_status = _issue return _issue @@ -774,8 +774,8 @@ def update(self, value): return _issue # Complete action - # item_state: state item to read from - def complete(self, item_state, evals_items=None): + # state: state (item) to read from + def complete(self, state, evals_items=None): self._scheduler_name = "{}-SeByAttrDelayTimer".format(self.__byattr) _issue = {self._name: {'issue': None, 'attribute': self.__byattr, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} @@ -832,8 +832,8 @@ def update(self, value): return _issue # Complete action - # item_state: state item to read from - def complete(self, item_state, evals_items=None): + # state: state (item) to read from + def complete(self, state, evals_items=None): self._scheduler_name = "{}-SeLogicDelayTimer".format(self.__logic) _issue = {self._name: {'issue': None, 'logic': self.__logic, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} @@ -907,8 +907,8 @@ def update(self, value): return _issue # Complete action - # item_state: state item to read from - def complete(self, item_state, evals_items=None): + # state: state (item) to read from + def complete(self, state, evals_items=None): self._scheduler_name = "{}-SeRunDelayTimer".format(StateEngineTools.get_eval_name(self.__eval)) _issue = {self._name: {'issue': None, 'eval': StateEngineTools.get_eval_name(self.__eval), 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} @@ -1002,10 +1002,10 @@ def update(self, value): return _issue # Complete action - # item_state: state item to read from - def complete(self, item_state, evals_items=None): + # state: state (item) to read from + def complete(self, state, evals_items=None): self.__item, self.__status, self.__mindelta, self.__value, _issue = self.check_complete( - item_state, self.__item, self.__status, self.__mindelta, self.__value, "force", evals_items) + state, self.__item, self.__status, self.__mindelta, self.__value, "force", evals_items) self._action_status = _issue return _issue @@ -1188,8 +1188,8 @@ def update(self, value): return _issue # Complete action - # item_state: state item to read from - def complete(self, item_state, evals_items=None): + # state: state (item) to read from + def complete(self, state, evals_items=None): if isinstance(self.__value, list): item = self.__value[0].property.path else: diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index c91e18b0b..8d3a6d554 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -507,15 +507,15 @@ def __raise_missing_parameter_error(self, parameter, param_name): "function '{2}'!".format(parameter['action'], param_name, parameter['function'])) # Check the actions optimize and complete them - # item_state: item to read from - def complete(self, item_state, evals_items=None): + # state: state (item) to read from + def complete(self, state, evals_items=None): _status = {} for name in self.__actions: try: - _status.update(self.__actions[name].complete(item_state, evals_items)) + _status.update(self.__actions[name].complete(state, evals_items)) except ValueError as ex: - _status.update({name: {'issue': ex, 'issueorigin': {'state': item_state.property.path, 'action': 'unknown'}}}) - raise ValueError("State '{0}', Action '{1}': {2}".format(item_state.property.path, name, ex)) + _status.update({name: {'issue': ex, 'issueorigin': {'state': state.id, 'action': 'unknown'}}}) + raise ValueError("State '{0}', Action '{1}': {2}".format(state.id, name, ex)) return _status def set(self, value): diff --git a/stateengine/StateEngineCondition.py b/stateengine/StateEngineCondition.py index e896006ac..ed06a44ce 100755 --- a/stateengine/StateEngineCondition.py +++ b/stateengine/StateEngineCondition.py @@ -66,7 +66,7 @@ def __repr__(self): "'status_eval': {}, 'value': {}".format(self.__item, self.__status, self.__eval, self.__status_eval, self.__value) - def check_items(self, check, value=None, item_state=None): + def check_items(self, check, value=None, state=None): item_issue, status_issue, eval_issue, status_eval_issue = None, None, None, None item_value, status_value, eval_value, status_eval_value = None, None, None, None if check == "attribute": @@ -75,7 +75,7 @@ def check_items(self, check, value=None, item_state=None): _orig_value = None if check == "se_item" or (check == "attribute" and self.__item is None and self.__eval is None): if value is None: - value = StateEngineTools.find_attribute(self._sh, item_state, "se_item_" + self.__name) + value = StateEngineTools.find_attribute(self._sh, state, "se_item_" + self.__name) if isinstance(value, str): match = re.match(r'^(.*):', value) if value.startswith("eval:"): @@ -95,7 +95,7 @@ def check_items(self, check, value=None, item_state=None): if check == "attribute": value = _orig_value if value is None: - value = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self.__name) + value = StateEngineTools.find_attribute(self._sh, state, "se_status_" + self.__name) if isinstance(value, str): match = re.match(r'^(.*):', value) if isinstance(value, str) and value.startswith("eval:"): @@ -114,7 +114,7 @@ def check_items(self, check, value=None, item_state=None): status_value = value if check == "se_eval" or (check == "attribute" and self.__eval is None): if value is None: - value = StateEngineTools.find_attribute(self._sh, item_state, "se_eval_" + self.__name) + value = StateEngineTools.find_attribute(self._sh, state, "se_eval_" + self.__name) if isinstance(value, str): match = re.match(r'^(.*):', value) if value.startswith("eval:"): @@ -130,7 +130,7 @@ def check_items(self, check, value=None, item_state=None): if check == "attribute": value = _orig_value if value is None: - value = StateEngineTools.find_attribute(self._sh, item_state, "se_status_eval_" + self.__name) + value = StateEngineTools.find_attribute(self._sh, state, "se_status_eval_" + self.__name) if isinstance(value, str): match = re.match(r'^(.*):', value) if value.startswith("eval:"): @@ -223,9 +223,9 @@ def get(self): return result # Complete condition (do some checks, cast value, min and max based on item or eval data types) - # item_state: item to read from + # state: state (item) to read from # abitem_object: Related SeItem instance for later determination of current age and current delay - def complete(self, item_state): + def complete(self, state): # check if it is possible to complete this condition if self.__min.is_empty() and self.__max.is_empty() and self.__value.is_empty() \ and self.__agemin.is_empty() and self.__agemax.is_empty() \ @@ -285,7 +285,7 @@ def complete(self, item_state): elif self.__name == "original_source": self.__eval = self._abitem.get_update_original_source - self.check_items("attribute", None, item_state) + self.check_items("attribute", None, state) # now we should have either 'item' or '(status)eval' set. If not, raise ValueError if all(item is None for item in [self.__item, self.__status, self.__eval, self.__status_eval]): diff --git a/stateengine/StateEngineConditionSet.py b/stateengine/StateEngineConditionSet.py index ab26b1240..37934f4d3 100755 --- a/stateengine/StateEngineConditionSet.py +++ b/stateengine/StateEngineConditionSet.py @@ -115,6 +115,8 @@ def update(self, item, grandparent_item): raise ValueError("Condition {0} error: {1}".format(name, ex)) # Update item from grandparent_item + if grandparent_item is None: + return self.__unused_attributes, self.__used_attributes for attribute in grandparent_item.conf: func, name = StateEngineTools.partition_strip(attribute, "_") @@ -146,21 +148,22 @@ def update(self, item, grandparent_item): return self.__unused_attributes, self.__used_attributes # Check the condition set, optimize and complete it - # item_state: item to read from - def complete(self, item_state): + # state: state (item) to read from + def complete(self, state): conditions_to_remove = [] # try to complete conditions + for name in self.conditions: try: - if not self.__conditions[name].complete(item_state): + if not self.__conditions[name].complete(state): conditions_to_remove.append(name) continue except ValueError as ex: self._abitem.update_attributes(self.__unused_attributes, self.__used_attributes) - self._abitem.update_issues('state', {item_state.property.path: {'issue': ex, 'issueorigin': + self._abitem.update_issues('state', {state.id: {'issue': ex, 'issueorigin': [{'conditionset': self.name, 'condition': name}]}}) text = "State '{0}', Condition Set '{1}', Condition '{2}' Error: {3}" - raise ValueError(text.format(item_state.property.path, self.name, name, ex)) + raise ValueError(text.format(state.id, self.name, name, ex)) # Remove incomplete conditions for name in conditions_to_remove: diff --git a/stateengine/StateEngineConditionSets.py b/stateengine/StateEngineConditionSets.py index e7dc7b7e7..7727f6878 100755 --- a/stateengine/StateEngineConditionSets.py +++ b/stateengine/StateEngineConditionSets.py @@ -71,10 +71,10 @@ def update(self, name, item, grandparent_item): return self.__condition_sets[name].unused_attributes, self.__condition_sets[name].used_attributes # Check the condition sets, optimize and complete them - # item_state: item to read from - def complete(self, item_state): + # state: item (item) to read from + def complete(self, state): for name in self.__condition_sets: - self.__condition_sets[name].complete(item_state) + self.__condition_sets[name].complete(state) # Write all condition sets to logger def write_to_logger(self): diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index c5d30a9e5..14582f30f 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -34,6 +34,7 @@ from lib.shtime import Shtime from lib.item.item import Item +from lib.item.items import Items import copy import threading import queue @@ -1982,6 +1983,10 @@ def return_item(self, item_id): if item_id is None: _issue = "item_id is None" return None, [_issue] + if item_id == Items.get_instance(): + _issue = "'{0}' is no valid item.".format(item_id) + self.__logger.warning("{0} Check your item config!", _issue, item_id) + return None, [_issue] if not isinstance(item_id, str): _issue = "'{0}' is not defined as string, cannot find item.".format(item_id) self.__logger.warning("{0} Check your item config!", _issue, item_id) diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index a40d75815..6da0af77f 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -174,7 +174,7 @@ def __init__(self, abitem, item_state): self.__order = StateEngineValue.SeValue(self._abitem, "State Order", False, "num") self._log_increase_indent() try: - self.__fill(self.__item, 0) + self.__fill(self, 0) finally: self._log_decrease_indent() @@ -348,7 +348,7 @@ def refill(self): self._log_debug("State {}: se_use attribute including item or eval " "- updating state conditions and actions", self.__name) self._log_increase_indent() - self.__fill(self.__item, 0, "reinit") + self.__fill(self, 0, "reinit") self._log_decrease_indent() def update_releasedby_internal(self, states=None): @@ -401,7 +401,7 @@ def __fill_list(self, item_states, recursion_depth, se_use=None): # item_state: item to read from # recursion_depth: current recursion_depth (recursion is canceled after five levels) # se_use: If se_use Attribute is used or not - def __fill(self, item_state, recursion_depth, se_use=None): + def __fill(self, state, recursion_depth, se_use=None): def update_unused(used_attributes, attrib_type, attrib_name): #filtered_dict = {key: value for key, value in self.__unused_attributes.items() if key not in used_attributes} #self.__unused_attributes = copy(filtered_dict) @@ -457,6 +457,11 @@ def update_action_status(action_status, actiontype): self.__action_status = filtered_dict #self._log_develop("Updated action status: {}, updated used {}", self.__action_status, self.__used_attributes) + if isinstance(state, SeState): + item_state = state.state_item + else: + item_state = state + self._log_develop("Fill state {} type {}", item_state, type(item_state)) if se_use == "reinit": self._log_develop("Resetting conditions and actions at re-init") self.__conditions.reset() @@ -466,19 +471,18 @@ def update_action_status(action_status, actiontype): self.__actions_leave.reset() self.__use_done = [] if recursion_depth > 5: - self._log_error("{0}/{1}: too many levels of 'use'", self.id, item_state.property.path) + self._log_error("{0}/{1}: too many levels of 'use'", self.id, item_state) return # Import data from other item if attribute "use" is found if "se_use" in item_state.conf: _returnvalue, _returntype, _, _issue = self.__use.set_from_attr(item_state, "se_use") _configvalue = copy(_returnvalue) _configvalue = [_configvalue] if not isinstance(_configvalue, list) else _configvalue - self._abitem.update_issues('config', {item_state.property.path: {'issue': _issue, 'attribute': 'se_use'}}) + self._abitem.update_issues('config', {state.id: {'issue': _issue, 'attribute': 'se_use'}}) _use = self.__use.get() if self.__use.is_empty() or _use is None: _issue = "se_use {} is set up in a wrong way".format(_use) - self._abitem.update_issues('config', - {item_state.property.path: {'issue': _issue, 'attribute': 'se_use'}}) + self._abitem.update_issues('config', {state.id: {'issue': _issue, 'attribute': 'se_use'}}) self._log_warning("{} - ignoring.", _issue) else: _use = [_use] if not isinstance(_use, list) else _use @@ -486,12 +490,11 @@ def update_action_status(action_status, actiontype): cleaned_use_list = [] for i, element in enumerate(_use): try: - _name = element.property.path + _name = element.id except Exception: _name = element _fill = True _path = None - if isinstance(element, StateEngineStruct.SeStruct): _path = element.property.path text1 = "Reading struct {0}. It is{1} a valid struct for the state configuration.{2}" @@ -506,15 +509,20 @@ def update_action_status(action_status, actiontype): cleaned_use_list.append(_configvalue[i]) elif isinstance(element, self.__itemClass): _path = element.property.path - text1 = "Reading Item {0}. It is{1} a valid item for the state configuration.{2}" - valid1 = " NOT" if _fill is False else " most likely" - valid2 = " Ignoring." if _fill is False else "" - self._log_info(text1, _path, valid1, valid2) + if element.return_parent() == Items.get_instance(): + valid1 = " most likely NOT" + valid3 = "" + valid2 = ", because it has no parent item!" + else: + valid2 = "" + valid1 = " NOT" if _fill is False else " most likely" + valid3 = " Ignoring." if _fill is False else "" + text1 = "Reading Item {0}. It is{1} a valid item for the state configuration{2}.{3}" + self._log_info(text1, _path, valid1, valid2, valid3) if _fill is False: _issue = "Item {} is not a valid item for the state configuration.".format(_path) self._abitem.update_issues('config', - {item_state.property.path: {'issue': _issue, - 'attribute': 'se_use'}}) + {state.id: {'issue': _issue, 'attribute': 'se_use'}}) elif _configvalue and _configvalue[i] not in cleaned_use_list: cleaned_use_list.append(_configvalue[i]) if _returntype[i] in ['item', 'eval']: @@ -551,9 +559,10 @@ def update_action_status(action_status, actiontype): self.__use_done.append(element) self.__fill(element, recursion_depth + 1, _name) self.__use.set(cleaned_use_list) - # Get action sets and condition sets parent_item = item_state.return_parent() + if parent_item == Items.get_instance(): + parent_item = None child_items = item_state.return_children() _conditioncount = 0 _action_counts = {"enter": 0, "stay": 0, "enter_or_stay": 0, "leave": 0} @@ -580,7 +589,7 @@ def update_action_status(action_status, actiontype): except ValueError as ex: raise ValueError("Condition {0} error: {1}".format(child_name, ex)) - if _conditioncount == 0: + if _conditioncount == 0 and parent_item: for attribute in parent_item.conf: func, name = StateEngineTools.partition_strip(attribute, "_") cond1 = name and name not in self.__used_attributes @@ -631,20 +640,20 @@ def update_action_status(action_status, actiontype): self.update_name(item_state, recursion_depth) # Complete condition sets and actions at the end if recursion_depth == 0: - self.__conditions.complete(item_state) - _action_status = self.__actions_enter.complete(item_state, self.__conditions.evals_items) + self.__conditions.complete(self) + _action_status = self.__actions_enter.complete(self, self.__conditions.evals_items) if _action_status: update_action_status(_action_status, 'enter') self._abitem.update_action_status(self.__action_status) - _action_status = self.__actions_stay.complete(item_state, self.__conditions.evals_items) + _action_status = self.__actions_stay.complete(self, self.__conditions.evals_items) if _action_status: update_action_status(_action_status, 'stay') self._abitem.update_action_status(self.__action_status) - _action_status = self.__actions_enter_or_stay.complete(item_state, self.__conditions.evals_items) + _action_status = self.__actions_enter_or_stay.complete(self, self.__conditions.evals_items) if _action_status: update_action_status(_action_status, 'enter_or_stay') self._abitem.update_action_status(self.__action_status) - _action_status = self.__actions_leave.complete(item_state, self.__conditions.evals_items) + _action_status = self.__actions_leave.complete(self, self.__conditions.evals_items) if _action_status: update_action_status(_action_status, 'leave') self._abitem.update_action_status(self.__action_status) diff --git a/stateengine/StateEngineTools.py b/stateengine/StateEngineTools.py index 0da8d1107..b513d84c5 100755 --- a/stateengine/StateEngineTools.py +++ b/stateengine/StateEngineTools.py @@ -23,6 +23,7 @@ import datetime from ast import literal_eval import re +from lib.item.items import Items # General class for everything that is below the SeItem Class @@ -256,16 +257,44 @@ def cast_time(value): # smarthome: instance of smarthome.py base class # base_item: base item to search in # attribute: name of attribute to find -def find_attribute(smarthome, base_item, attribute, recursion_depth=0): - # if item has attribute "se_use", get the item to use and search this item for required attribute - if recursion_depth > 5: +def find_attribute(smarthome, state, attribute, recursion_depth=0): + if isinstance(state, list): + for element in state: + result = find_attribute(smarthome, element, attribute, recursion_depth) + if result is not None: + return result return None - use_item = smarthome.return_item(base_item.find_attribute("se_use", None, 0)) - if use_item is None: - return base_item.find_attribute(attribute, None, 1) + + # 1: parent of given item could have attribute + try: + # if state is state object, get the item and se_use information + base_item = state.state_item + _use = state.use.get() + except Exception: + # if state is a standard item (e.g. evaluated by se_use, just take it as it is + base_item = state + _use = None + parent_item = base_item.return_parent() + if parent_item == Items.get_instance(): + pass else: - result = find_attribute(smarthome, use_item, attribute, recursion_depth + 1) - return result + try: + _parent_conf = parent_item.conf + if parent_item is not None and attribute in _parent_conf: + return parent_item.conf[attribute] + except Exception: + return None + + # 2: if state has attribute "se_use", get the item to use and search this item for required attribute + if _use is not None: + if recursion_depth > 5: + return None + result = find_attribute(smarthome, _use, attribute, recursion_depth + 1) + if result is not None: + return result + + # 3: nothing found + return None # partition value at splitchar and strip resulting parts From 7aeb57b6a487cedec9202714f256e1577fd6d845 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 25 Jul 2024 17:52:46 +0200 Subject: [PATCH 13/19] stateengine plugin: don't expand item pathes at beginning so relative item attributes are correctly assigned when using structs and se_use --- stateengine/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/stateengine/__init__.py b/stateengine/__init__.py index 819c5079d..59b23aa7c 100755 --- a/stateengine/__init__.py +++ b/stateengine/__init__.py @@ -121,14 +121,6 @@ def __init__(self, sh): # noinspection PyMethodMayBeStatic def parse_item(self, item): item.expand_relativepathes('se_manual_logitem', '', '') - try: - 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.property.path + "', caller, source)" elif self.has_iattr(item.conf, "se_manual_invert"): From 5b00ffe6f0e72fa82849c1fce2739190e943190c Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Fri, 26 Jul 2024 00:44:41 +0200 Subject: [PATCH 14/19] stateengine plugin: improve se_use handling when searching for attributes (gets evaluated only once in the beginning) --- stateengine/StateEngineAction.py | 30 ++++++++++++------------- stateengine/StateEngineActions.py | 3 ++- stateengine/StateEngineCondition.py | 17 +++++++------- stateengine/StateEngineConditionSet.py | 4 ++-- stateengine/StateEngineConditionSets.py | 3 ++- stateengine/StateEngineState.py | 2 ++ stateengine/StateEngineTools.py | 12 +++++----- 7 files changed, 39 insertions(+), 32 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index fdc5460b1..12106b907 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -286,10 +286,10 @@ def check_getitem_fromeval(self, check_item, check_value=None, check_mindelta=No 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return check_item, check_value, check_mindelta, _issue - def check_complete(self, state, check_item, check_status, check_mindelta, check_value, action_type, evals_items=None): + def check_complete(self, state, check_item, check_status, check_mindelta, check_value, action_type, evals_items=None, use=None): _issue = {self._name: {'issue': None, 'issueorigin': [{'state': state.id, 'action': self._function}]}} - self._log_develop("Check item {} status {} value {} evals_items {}", check_item, check_status, check_value, evals_items) + self._log_develop("Check item {} status {} value {} use {} evals_items {}", check_item, check_status, check_value, use, evals_items) try: _name = evals_items.get(self.name) if _name is not None: @@ -311,13 +311,13 @@ def check_complete(self, state, check_item, check_status, check_mindelta, check_ self._log_info("No valid item info for action {}, trying to get differently. Problem: {}", self._name, ex) # missing item in action: Try to find it. if check_item is None: - item = StateEngineTools.find_attribute(self._sh, state, "se_item_" + self._name) + item = StateEngineTools.find_attribute(self._sh, state, "se_item_" + self._name, 0, use) if item is not None: check_item, _issue = self._abitem.return_item(item) _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': state.id, 'action': self._function}]}} else: - item = StateEngineTools.find_attribute(self._sh, state, "se_eval_" + self._name) + item = StateEngineTools.find_attribute(self._sh, state, "se_eval_" + self._name, 0, use) if item is not None: check_item = str(item) @@ -326,14 +326,14 @@ def check_complete(self, state, check_item, check_status, check_mindelta, check_ 'issueorigin': [{'state': state.id, 'action': self._function}]}} # missing status in action: Try to find it. if check_status is None: - status = StateEngineTools.find_attribute(self._sh, state, "se_status_" + self._name) + status = StateEngineTools.find_attribute(self._sh, state, "se_status_" + self._name, 0, use) if status is not None: check_status, _issue = self._abitem.return_item(status) _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': state.id, 'action': self._function}]}} if check_mindelta.is_empty(): - mindelta = StateEngineTools.find_attribute(self._sh, state, "se_mindelta_" + self._name) + mindelta = StateEngineTools.find_attribute(self._sh, state, "se_mindelta_" + self._name, 0, use) if mindelta is not None: check_mindelta.set(mindelta) @@ -522,7 +522,7 @@ def update(self, value): # Complete action # state: state (item) to read from - def complete(self, state, evals_items=None): + def complete(self, state, evals_items=None, use=None): raise NotImplementedError("Class {} doesn't implement complete()".format(self.__class__.__name__)) # Check if execution is possible @@ -625,9 +625,9 @@ def update(self, value): # Complete action # state: state (item) to read from - def complete(self, state, evals_items=None): + def complete(self, state, evals_items=None, use=None): self.__item, self.__status, self.__mindelta, self.__value, _issue = self.check_complete( - state, self.__item, self.__status, self.__mindelta, self.__value, "set", evals_items) + state, self.__item, self.__status, self.__mindelta, self.__value, "set", evals_items, use) self._action_status = _issue return _issue @@ -775,7 +775,7 @@ def update(self, value): # Complete action # state: state (item) to read from - def complete(self, state, evals_items=None): + def complete(self, state, evals_items=None, use=None): self._scheduler_name = "{}-SeByAttrDelayTimer".format(self.__byattr) _issue = {self._name: {'issue': None, 'attribute': self.__byattr, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} @@ -833,7 +833,7 @@ def update(self, value): # Complete action # state: state (item) to read from - def complete(self, state, evals_items=None): + def complete(self, state, evals_items=None, use=None): self._scheduler_name = "{}-SeLogicDelayTimer".format(self.__logic) _issue = {self._name: {'issue': None, 'logic': self.__logic, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} @@ -908,7 +908,7 @@ def update(self, value): # Complete action # state: state (item) to read from - def complete(self, state, evals_items=None): + def complete(self, state, evals_items=None, use=None): self._scheduler_name = "{}-SeRunDelayTimer".format(StateEngineTools.get_eval_name(self.__eval)) _issue = {self._name: {'issue': None, 'eval': StateEngineTools.get_eval_name(self.__eval), 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} @@ -1003,9 +1003,9 @@ def update(self, value): # Complete action # state: state (item) to read from - def complete(self, state, evals_items=None): + def complete(self, state, evals_items=None, use=None): self.__item, self.__status, self.__mindelta, self.__value, _issue = self.check_complete( - state, self.__item, self.__status, self.__mindelta, self.__value, "force", evals_items) + state, self.__item, self.__status, self.__mindelta, self.__value, "force", evals_items, use) self._action_status = _issue return _issue @@ -1189,7 +1189,7 @@ def update(self, value): # Complete action # state: state (item) to read from - def complete(self, state, evals_items=None): + def complete(self, state, evals_items=None, use=None): if isinstance(self.__value, list): item = self.__value[0].property.path else: diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 8d3a6d554..6c1233ab4 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -510,9 +510,10 @@ def __raise_missing_parameter_error(self, parameter, param_name): # state: state (item) to read from def complete(self, state, evals_items=None): _status = {} + use = state.use.get() for name in self.__actions: try: - _status.update(self.__actions[name].complete(state, evals_items)) + _status.update(self.__actions[name].complete(state, evals_items, use)) except ValueError as ex: _status.update({name: {'issue': ex, 'issueorigin': {'state': state.id, 'action': 'unknown'}}}) raise ValueError("State '{0}', Action '{1}': {2}".format(state.id, name, ex)) diff --git a/stateengine/StateEngineCondition.py b/stateengine/StateEngineCondition.py index ed06a44ce..abfe71745 100755 --- a/stateengine/StateEngineCondition.py +++ b/stateengine/StateEngineCondition.py @@ -66,16 +66,18 @@ def __repr__(self): "'status_eval': {}, 'value': {}".format(self.__item, self.__status, self.__eval, self.__status_eval, self.__value) - def check_items(self, check, value=None, state=None): + def check_items(self, check, value=None, state=None, use=None): item_issue, status_issue, eval_issue, status_eval_issue = None, None, None, None item_value, status_value, eval_value, status_eval_value = None, None, None, None + if state and use is None: + use = state.use.get() if check == "attribute": _orig_value = value else: _orig_value = None if check == "se_item" or (check == "attribute" and self.__item is None and self.__eval is None): if value is None: - value = StateEngineTools.find_attribute(self._sh, state, "se_item_" + self.__name) + value = StateEngineTools.find_attribute(self._sh, state, "se_item_" + self.__name, 0, use) if isinstance(value, str): match = re.match(r'^(.*):', value) if value.startswith("eval:"): @@ -95,7 +97,7 @@ def check_items(self, check, value=None, state=None): if check == "attribute": value = _orig_value if value is None: - value = StateEngineTools.find_attribute(self._sh, state, "se_status_" + self.__name) + value = StateEngineTools.find_attribute(self._sh, state, "se_status_" + self.__name, 0, use) if isinstance(value, str): match = re.match(r'^(.*):', value) if isinstance(value, str) and value.startswith("eval:"): @@ -114,7 +116,7 @@ def check_items(self, check, value=None, state=None): status_value = value if check == "se_eval" or (check == "attribute" and self.__eval is None): if value is None: - value = StateEngineTools.find_attribute(self._sh, state, "se_eval_" + self.__name) + value = StateEngineTools.find_attribute(self._sh, state, "se_eval_" + self.__name, 0, use) if isinstance(value, str): match = re.match(r'^(.*):', value) if value.startswith("eval:"): @@ -130,7 +132,7 @@ def check_items(self, check, value=None, state=None): if check == "attribute": value = _orig_value if value is None: - value = StateEngineTools.find_attribute(self._sh, state, "se_status_eval_" + self.__name) + value = StateEngineTools.find_attribute(self._sh, state, "se_status_eval_" + self.__name, 0, use) if isinstance(value, str): match = re.match(r'^(.*):', value) if value.startswith("eval:"): @@ -225,7 +227,7 @@ def get(self): # Complete condition (do some checks, cast value, min and max based on item or eval data types) # state: state (item) to read from # abitem_object: Related SeItem instance for later determination of current age and current delay - def complete(self, state): + def complete(self, state, use): # check if it is possible to complete this condition if self.__min.is_empty() and self.__max.is_empty() and self.__value.is_empty() \ and self.__agemin.is_empty() and self.__agemax.is_empty() \ @@ -284,8 +286,7 @@ def complete(self, state): self.__eval = self._abitem.get_update_original_caller elif self.__name == "original_source": self.__eval = self._abitem.get_update_original_source - - self.check_items("attribute", None, state) + self.check_items("attribute", None, state, use) # now we should have either 'item' or '(status)eval' set. If not, raise ValueError if all(item is None for item in [self.__item, self.__status, self.__eval, self.__status_eval]): diff --git a/stateengine/StateEngineConditionSet.py b/stateengine/StateEngineConditionSet.py index 37934f4d3..ed4e4b6d0 100755 --- a/stateengine/StateEngineConditionSet.py +++ b/stateengine/StateEngineConditionSet.py @@ -149,13 +149,13 @@ def update(self, item, grandparent_item): # Check the condition set, optimize and complete it # state: state (item) to read from - def complete(self, state): + def complete(self, state, use): conditions_to_remove = [] # try to complete conditions for name in self.conditions: try: - if not self.__conditions[name].complete(state): + if not self.__conditions[name].complete(state, use): conditions_to_remove.append(name) continue except ValueError as ex: diff --git a/stateengine/StateEngineConditionSets.py b/stateengine/StateEngineConditionSets.py index 7727f6878..9fdf8929b 100755 --- a/stateengine/StateEngineConditionSets.py +++ b/stateengine/StateEngineConditionSets.py @@ -73,8 +73,9 @@ def update(self, name, item, grandparent_item): # Check the condition sets, optimize and complete them # state: item (item) to read from def complete(self, state): + use = state.use.get() for name in self.__condition_sets: - self.__condition_sets[name].complete(state) + self.__condition_sets[name].complete(state, use) # Write all condition sets to logger def write_to_logger(self): diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index 6da0af77f..d966a4035 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -558,6 +558,8 @@ def update_action_status(action_status, actiontype): else: self.__use_done.append(element) self.__fill(element, recursion_depth + 1, _name) + elif _fill and element is not None and element in self.__use_done: + self._log_debug("Ignoring element {} as it is already added.", element) self.__use.set(cleaned_use_list) # Get action sets and condition sets parent_item = item_state.return_parent() diff --git a/stateengine/StateEngineTools.py b/stateengine/StateEngineTools.py index b513d84c5..c6dfefd0b 100755 --- a/stateengine/StateEngineTools.py +++ b/stateengine/StateEngineTools.py @@ -257,7 +257,7 @@ def cast_time(value): # smarthome: instance of smarthome.py base class # base_item: base item to search in # attribute: name of attribute to find -def find_attribute(smarthome, state, attribute, recursion_depth=0): +def find_attribute(smarthome, state, attribute, recursion_depth=0, use=None): if isinstance(state, list): for element in state: result = find_attribute(smarthome, element, attribute, recursion_depth) @@ -269,11 +269,13 @@ def find_attribute(smarthome, state, attribute, recursion_depth=0): try: # if state is state object, get the item and se_use information base_item = state.state_item - _use = state.use.get() + if use is None: + use = state.use.get() + print(f"got use {use}") except Exception: # if state is a standard item (e.g. evaluated by se_use, just take it as it is base_item = state - _use = None + use = None parent_item = base_item.return_parent() if parent_item == Items.get_instance(): pass @@ -286,10 +288,10 @@ def find_attribute(smarthome, state, attribute, recursion_depth=0): return None # 2: if state has attribute "se_use", get the item to use and search this item for required attribute - if _use is not None: + if use is not None: if recursion_depth > 5: return None - result = find_attribute(smarthome, _use, attribute, recursion_depth + 1) + result = find_attribute(smarthome, use, attribute, recursion_depth + 1) if result is not None: return result From 9a358f839d8cb5894905bd0121dadac122f188a6 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Tue, 30 Jul 2024 17:49:12 +0200 Subject: [PATCH 15/19] stateengine plugin: minor code and logging improvements --- stateengine/StateEngineAction.py | 4 ++-- stateengine/StateEngineItem.py | 21 ++++++++++++++++----- stateengine/StateEngineTools.py | 5 ++--- stateengine/StateEngineValue.py | 12 +++++++----- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 12106b907..57a45b375 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -948,7 +948,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._log_decrease_indent() text = "{0}: Problem evaluating '{1}': {2}." self.update_webif_actionstatus(state, self._name, 'False', 'Problem evaluating: {}'.format(ex)) - self._log_error(text.format(actionname, StateEngineTools.get_eval_name(self.__eval), ex)) + self._log_error(text, actionname, StateEngineTools.get_eval_name(self.__eval), ex) else: try: if returnvalue: @@ -967,7 +967,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._log_decrease_indent() self.update_webif_actionstatus(state, self._name, 'False', 'Problem calling: {}'.format(ex)) text = "{0}: Problem calling '{0}': {1}." - self._log_error(text.format(actionname, StateEngineTools.get_eval_name(self.__eval), ex)) + self._log_error(text, actionname, StateEngineTools.get_eval_name(self.__eval), ex) def get(self): result = {'function': str(self._function), 'eval': str(self.__eval), diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 14582f30f..5f38c0989 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -1062,9 +1062,9 @@ def print_readable_dict(data): else: formatted_entries.append(item) if formatted_entries: - self.__logger.info(f"- {key}: {', '.join(formatted_entries)}") + self.__logger.info("- {}: {}", key, ', '.join(formatted_entries)) else: - self.__logger.info(f"- {key}: {value}") + self.__logger.info("- {}: {}", key, value) def list_issues(v): _issuelist = StateEngineTools.flatten_list(v.get('issue')) if isinstance(_issuelist, list) and len(_issuelist) > 1: @@ -1077,9 +1077,21 @@ def list_issues(v): self.__logger.info("- {}", e) self.__logger.decrease_indent() elif isinstance(_issuelist, list) and len(_issuelist) == 1: - self.__logger.info("has the following issue: {}", _issuelist[0]) + if isinstance(_issuelist[0], dict): + self.__logger.info("has the following issues:") + self.__logger.increase_indent() + print_readable_dict(_issuelist[0]) + self.__logger.decrease_indent() + else: + self.__logger.info("has the following issue: {}", _issuelist[0]) else: - self.__logger.info("has the following issue: {}", _issuelist) + if isinstance(_issuelist, dict): + self.__logger.info("has the following issues:") + self.__logger.increase_indent() + print_readable_dict(_issuelist) + self.__logger.decrease_indent() + else: + self.__logger.info("has the following issue: {}", _issuelist) if "ignore" in v: self.__logger.info("It will be ignored") @@ -1996,7 +2008,6 @@ def return_item(self, item_id): item = None _, item_id = StateEngineTools.partition_strip(item_id, ":") try: - # self.__logger.debug("Creating struct for id {}".format(item_id)) item = StateEngineStructs.create(self, item_id) except Exception as e: _issue = "Struct {} creation failed. Error: {}".format(item_id, e) diff --git a/stateengine/StateEngineTools.py b/stateengine/StateEngineTools.py index c6dfefd0b..a29968b0a 100755 --- a/stateengine/StateEngineTools.py +++ b/stateengine/StateEngineTools.py @@ -271,7 +271,6 @@ def find_attribute(smarthome, state, attribute, recursion_depth=0, use=None): base_item = state.state_item if use is None: use = state.use.get() - print(f"got use {use}") except Exception: # if state is a standard item (e.g. evaluated by se_use, just take it as it is base_item = state @@ -304,8 +303,8 @@ def find_attribute(smarthome, state, attribute, recursion_depth=0, use=None): # splitchar: where to split # returns: Parts before and after split, whitespaces stripped def partition_strip(value, splitchar): - if isinstance(value, list): - raise ValueError("You can not use list entries!") + if not isinstance(value, str): + raise ValueError("value has to be a string!") elif value.startswith("se_") and splitchar == "_": part1, __, part2 = value[3:].partition(splitchar) return "se_" + part1.strip(), part2.strip() diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index 3cd4009b8..f9da0beaf 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -543,7 +543,7 @@ def cast_item(self, value): _issue_dict = {str(value): _issue} if _issue_dict not in self.__get_issues['cast_item']: self.__get_issues['cast_item'].append(_issue_dict) - self._log_error("Can't cast {0} to item/struct! {1}".format(value, ex)) + self._log_error(_issue) return value def __update_item_listorder(self, value, newvalue, item_id=None): @@ -712,7 +712,7 @@ def __get_from_regex(self): values = self.__regex _issue = "Problem while creating regex '{0}': {1}.".format(values, ex) _issue_dict = {values: _issue} - if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['regex']: + if _issue_dict not in self.__get_issues['regex']: self.__get_issues['regex'].append(_issue_dict) self._log_info(_issue) return values @@ -744,7 +744,9 @@ def __get_eval(self): self._log_decrease_indent() _name = StateEngineTools.get_eval_name(self.__eval) _issue = "Problem evaluating '{0}': {1}.".format(_name, ex) - self.__get_issues['eval'].append({_name: ex}) + _issue_dict = {_name: _issue} + if _issue_dict not in self.__get_issues['eval']: + self.__get_issues['eval'].append(_issue_dict) self._log_warning(_issue) self._log_increase_indent() values = None @@ -779,7 +781,7 @@ def __get_eval(self): self._log_decrease_indent() _issue = "Problem evaluating from list '{0}': {1}.".format( StateEngineTools.get_eval_name(val), ex) - _issue_dict = {val: ex} + _issue_dict = {val: _issue} if _issue_dict not in self.__get_issues['eval']: self.__get_issues['eval'].append(_issue_dict) self._log_warning(_issue) @@ -827,7 +829,7 @@ def __get_eval(self): self._log_warning(_issue) self._log_increase_indent() _issue_dict = {_name: _issue} - if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['eval']: + if _issue_dict not in self.__get_issues['eval']: self.__get_issues['eval'].append(_issue_dict) return None return values From cde3630628a8230ade0430ed1e9bca027b156178 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Tue, 30 Jul 2024 17:53:14 +0200 Subject: [PATCH 16/19] stateengine plugin: "set" function now also returns original value, necessary for optimal "se_use" handling --- stateengine/StateEngineAction.py | 22 +++++++++++----------- stateengine/StateEngineItem.py | 4 ++-- stateengine/StateEngineState.py | 15 +++++++-------- stateengine/StateEngineValue.py | 15 ++++++++++----- 4 files changed, 30 insertions(+), 26 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 57a45b375..e8f2392da 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -86,7 +86,7 @@ def __init__(self, abitem, name: str): def update_delay(self, value): _issue_list = [] - _, _, _issue = self.__delay.set(value) + _, _, _issue, _ = self.__delay.set(value) if _issue: _issue = {self._name: {'issue': _issue, 'attribute': 'delay', 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} @@ -102,7 +102,7 @@ def update_delay(self, value): def update_instanteval(self, value): if self.__instanteval is None: self.__instanteval = StateEngineValue.SeValue(self._abitem, "instanteval", False, "bool") - _, _, _issue = self.__instanteval.set(value) + _, _, _issue, _ = self.__instanteval.set(value) _issue = {self._name: {'issue': _issue, 'attribute': 'instanteval', 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue @@ -110,37 +110,37 @@ def update_instanteval(self, value): def update_repeat(self, value): if self.__repeat is None: self.__repeat = StateEngineValue.SeValue(self._abitem, "repeat", False, "bool") - _, _, _issue = self.__repeat.set(value) + _, _, _issue, _ = self.__repeat.set(value) _issue = {self._name: {'issue': _issue, 'attribute': 'repeat', 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue def update_order(self, value): - _, _, _issue = self.__order.set(value) + _, _, _issue, _ = self.__order.set(value) _issue = {self._name: {'issue': _issue, 'attribute': 'order', 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue def update_conditionset(self, value): - _, _, _issue = self.conditionset.set(value) + _, _, _issue, _ = self.conditionset.set(value) _issue = {self._name: {'issue': _issue, 'attribute': 'conditionset', 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue def update_previousconditionset(self, value): - _, _, _issue = self.previousconditionset.set(value) + _, _, _issue, _ = self.previousconditionset.set(value) _issue = {self._name: {'issue': _issue, 'attribute': 'previousconditionset', 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue def update_previousstate_conditionset(self, value): - _, _, _issue = self.previousstate_conditionset.set(value) + _, _, _issue, _ = self.previousstate_conditionset.set(value) _issue = {self._name: {'issue': _issue, 'attribute': 'previousstate_conditionset', 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue def update_mode(self, value): - _value, _, _issue = self.__mode.set(value) + _value, _, _issue, _ = self.__mode.set(value) _issue = {self._name: {'issue': _issue, 'attribute': 'mode', 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _value[0], _issue @@ -619,7 +619,7 @@ def _getitem_fromeval(self): # set the action based on a set_(action_name) attribute # value: Value of the set_(action_name) attribute def update(self, value): - _, _, _issue = self.__value.set(value) + _, _, _issue, _ = self.__value.set(value) _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue @@ -826,7 +826,7 @@ def update(self, value): logic, value = StateEngineTools.partition_strip(value, ":") self.__logic = logic value = None if value == "" else value - _, _, _issue = self.__value.set(value) + _, _, _issue, _ = self.__value.set(value) _issue = {self._name: {'issue': _issue, 'logic': self.__logic, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue @@ -997,7 +997,7 @@ def __repr__(self): # set the action based on a set_(action_name) attribute # value: Value of the set_(action_name) attribute def update(self, value): - _, _, _issue = self.__value.set(value) + _, _, _issue, _ = self.__value.set(value) _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 5f38c0989..9d30aa753 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -204,7 +204,7 @@ def __init__(self, smarthome, item, se_plugin): self.__log_level = StateEngineValue.SeValue(self, "Log Level", False, "num") _default_log_level = self.__logger.default_log_level.get() - _returnvalue, _returntype, _using_default, _issue = self.__log_level.set_from_attr(self.__item, "se_log_level", + _returnvalue, _returntype, _using_default, _issue, _ = self.__log_level.set_from_attr(self.__item, "se_log_level", _default_log_level) self.__using_default_log_level = _using_default _returnvalue = self.__log_level.get() @@ -418,7 +418,7 @@ def update_leave_action(self, default_instant_leaveaction): default_instant_leaveaction_value = default_instant_leaveaction.get() self.__default_instant_leaveaction = default_instant_leaveaction - _returnvalue_leave, _returntype_leave, _using_default_leave, _issue = self.__instant_leaveaction.set_from_attr( + _returnvalue_leave, _returntype_leave, _using_default_leave, _issue, _ = self.__instant_leaveaction.set_from_attr( self.__item, "se_instant_leaveaction", default_instant_leaveaction) if len(_returnvalue_leave) > 1: diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index d966a4035..1138f87cf 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -273,9 +273,9 @@ def update_order(self, value=None): elif len(value) == 1: value = value[0] if value is None and "se_stateorder" in self.__item.conf: - _, _, _, _issue = self.__order.set_from_attr(self.__item, "se_stateorder") + _, _, _, _issue, _ = self.__order.set_from_attr(self.__item, "se_stateorder") elif value is not None: - _, _, _issue = self.__order.set(value, "", True, False) + _, _, _issue, _ = self.__order.set(value, "", True, False) else: _issue = [None] @@ -353,20 +353,20 @@ def refill(self): def update_releasedby_internal(self, states=None): if states == []: - _returnvalue, _returntype, _issue = self.__releasedby.set([None], "", True, False) + _returnvalue, _returntype, _issue, _ = self.__releasedby.set([None], "", True, False) elif states: self._log_develop("Setting releasedby to {}", states) - _returnvalue, _returntype, _issue = self.__releasedby.set(states, "", True, False) + _returnvalue, _returntype, _issue, _ = self.__releasedby.set(states, "", True, False) self._log_develop("returnvalue {}", _returnvalue) else: - _returnvalue, _returntype, _, _issue = self.__releasedby.set_from_attr(self.__item, "se_released_by") + _returnvalue, _returntype, _, _issue, _ = self.__releasedby.set_from_attr(self.__item, "se_released_by") return _returnvalue, _returntype, _issue def update_can_release_internal(self, states): if states == []: - _returnvalue, _returntype, _issue = self.__can_release.set([None], "", True, False) + _returnvalue, _returntype, _issue, _ = self.__can_release.set([None], "", True, False) elif states: - _returnvalue, _returntype, _issue = self.__can_release.set(states, "", True, False) + _returnvalue, _returntype, _issue, _ = self.__can_release.set(states, "", True, False) else: _returnvalue, _returntype, _issue = [None], [None], None return _returnvalue, _returntype, _issue @@ -605,7 +605,6 @@ def update_action_status(action_status, actiontype): for child_item in child_items: child_name = StateEngineTools.get_last_part_of_item_id(child_item) try: - action_mapping = { "on_enter": ("enter", self.__actions_enter), "on_stay": ("stay", self.__actions_stay), diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index f9da0beaf..daa070c0e 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -60,6 +60,7 @@ def __init__(self, abitem, name, allow_value_list=False, value_type=None): self.__itemClass = Item self.__listorder = [] self.__type_listorder = [] + self.__orig_listorder = [] self.__valid_valuetypes = ["value", "regex", "eval", "var", "item", "template", "struct"] if value_type == "str": self.__cast_func = StateEngineTools.cast_str @@ -98,7 +99,7 @@ def set_from_attr(self, item, attribute_name, default_value=None, reset=True, at self._log_develop("Processing value {0} from attribute name {1}, reset {2}, type {3}", value, attribute_name, reset, attr_type) elif default_value is None: - return None, None, False, None + return None, None, False, None, None else: value = default_value _using_default = True @@ -123,9 +124,9 @@ def set_from_attr(self, item, attribute_name, default_value=None, reset=True, at if value is not None: self._log_develop("Setting value {0}, attribute name {1}, reset {2}, type {3}", value, attribute_name, reset, attr_type) - _returnvalue, _returntype, _issue = self.set(value, attribute_name, reset) - self._log_develop("Set from attribute returnvalue {}, returntype {}, issue {}", _returnvalue, _returntype, _issue) - return _returnvalue, _returntype, _using_default, _issue + _returnvalue, _returntype, _issue, _origvalue = self.set(value, attribute_name, reset) + self._log_develop("Set from attribute returnvalue {}, returntype {}, issue {}, original {}", _returnvalue, _returntype, _issue, _origvalue) + return _returnvalue, _returntype, _using_default, _issue, _origvalue def _set_additional(self, _additional_sources): for _use in _additional_sources: @@ -144,6 +145,7 @@ def __resetvalue(self): self._additional_sources = [] self.__listorder = [] self.__type_listorder = [] + self.__orig_listorder = [] # Set value # value: string indicating value or source of value @@ -184,6 +186,7 @@ def set(self, value, name="", reset=True, copyvalue=True): _issue, self.__valid_valuetypes, field_value[i]) source[i] = "value" self.__type_listorder.append(source[i]) + self.__orig_listorder.append(val) if source[i] == "value": self.__listorder[i] = value[i] if source[i] == "template": @@ -260,6 +263,7 @@ def set(self, value, name="", reset=True, copyvalue=True): if source == "value": self.__listorder = [field_value] self.__type_listorder.append(source) + self.__orig_listorder.append(value) else: source = "value" field_value = value @@ -384,8 +388,9 @@ def set(self, value, name="", reset=True, copyvalue=True): self.__issues = StateEngineTools.flatten_list(self.__issues) self.__listorder = StateEngineTools.flatten_list(self.__listorder) self.__type_listorder = StateEngineTools.flatten_list(self.__type_listorder) + self.__orig_listorder = StateEngineTools.flatten_list(self.__orig_listorder) del value - return self.__listorder, self.__type_listorder, self.__issues + return self.__listorder, self.__type_listorder, self.__issues, self.__orig_listorder # Set cast function # cast_func: cast function From 88b91c6aad5f2c61b7afe4a2455677224dfccade Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Tue, 30 Jul 2024 17:54:57 +0200 Subject: [PATCH 17/19] stateengine plugin: set_from_attribute function now takes an ignore list of entries that should not be checked --- stateengine/StateEngineValue.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index daa070c0e..bfc2c87b8 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -92,10 +92,21 @@ def get_issues(self): # item: item containing the attribute # attribute_name: name of attribute to use # default_value: default value to be used if item contains no such attribute - def set_from_attr(self, item, attribute_name, default_value=None, reset=True, attr_type=None): + def set_from_attr(self, item, attribute_name, default_value=None, reset=True, attr_type=None, ignore=None): value = copy.deepcopy(item.conf.get(attribute_name)) if value is not None: _using_default = False + if isinstance(value, list): + if not ignore: + seen = set() + else: + ignore = ignore if isinstance(ignore, list) else [ignore] + seen = set(ignore) + self._log_develop("Ignoring values {}", ignore) + value = [x for x in value if not (x in seen or seen.add(x))] + elif value == ignore: + self._log_develop("Not setting value {} as it should be ignored", value) + return None, None, False, None, None self._log_develop("Processing value {0} from attribute name {1}, reset {2}, type {3}", value, attribute_name, reset, attr_type) elif default_value is None: @@ -540,6 +551,13 @@ def cast_item(self, value): _issue_dict = {str(value): _issue[0]} else: _issue_dict = {} + if isinstance(_returnvalue, str): + try: + _returnvalue = eval(_returnvalue) + except Exception: + _issue = "Got string {0} while casting item {1}".format(_returnvalue, value) + _issue_dict = {str(value): _issue} + self._log_error(_issue) if _issue_dict and _issue_dict not in self.__get_issues['cast_item']: self.__get_issues['cast_item'].append(_issue_dict) return _returnvalue From 8bc09e2f722007fb1a93d97cac40f5989c4ce1e4 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Tue, 30 Jul 2024 17:56:41 +0200 Subject: [PATCH 18/19] stateengine plugin: massive update of se_use handling. Now the item config gets correctly scanned and only at init. Multiple fixes, improvements and better issue handling --- stateengine/StateEngineActions.py | 5 +- stateengine/StateEngineConditionSets.py | 5 +- stateengine/StateEngineItem.py | 6 +- stateengine/StateEngineState.py | 269 +++++++++++++++--------- 4 files changed, 178 insertions(+), 107 deletions(-) diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 6c1233ab4..89bef33bd 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -508,9 +508,10 @@ def __raise_missing_parameter_error(self, parameter, param_name): # Check the actions optimize and complete them # state: state (item) to read from - def complete(self, state, evals_items=None): + def complete(self, state, evals_items=None, use=None): _status = {} - use = state.use.get() + if use is None: + use = state.use.get() for name in self.__actions: try: _status.update(self.__actions[name].complete(state, evals_items, use)) diff --git a/stateengine/StateEngineConditionSets.py b/stateengine/StateEngineConditionSets.py index 9fdf8929b..c8dbf3612 100755 --- a/stateengine/StateEngineConditionSets.py +++ b/stateengine/StateEngineConditionSets.py @@ -72,8 +72,9 @@ def update(self, name, item, grandparent_item): # Check the condition sets, optimize and complete them # state: item (item) to read from - def complete(self, state): - use = state.use.get() + def complete(self, state, use=None): + if use is None: + use = state.use.get() for name in self.__condition_sets: self.__condition_sets[name].complete(state, use) diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 9d30aa753..42202d905 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -1137,7 +1137,11 @@ def list_issues(v): if value.get('attribute'): self.__logger.info("Attribute {}", value.get('attribute')) self.__logger.increase_indent() - self.__logger.info("defined in state {}", entry) + if value.get('origin'): + origin = value.get('origin') + else: + origin = "state" + self.__logger.info("defined in {} {}", origin, entry) self.__logger.decrease_indent() list_issues(value) else: diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index 1138f87cf..c8986e21b 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -166,6 +166,8 @@ def __init__(self, abitem, item_state): self.__used_attributes = {} self.__action_status = {} self.__use_done = [] + self.__use_list = [] + self.__use_ignore_list = [] self.__conditions = StateEngineConditionSets.SeConditionSets(self._abitem) self.__actions_enter_or_stay = StateEngineActions.SeActions(self._abitem) self.__actions_enter = StateEngineActions.SeActions(self._abitem) @@ -174,7 +176,8 @@ def __init__(self, abitem, item_state): self.__order = StateEngineValue.SeValue(self._abitem, "State Order", False, "num") self._log_increase_indent() try: - self.__fill(self, 0) + self.__initialize_se_use(self, 0) + self.__fill(self, 0, "reinit") finally: self._log_decrease_indent() @@ -385,23 +388,166 @@ def update_name(self, item_state, recursion_depth=0): self._log_develop("Updated name of state {} to {}.", item_state, self.__name) return self.__name - def __fill_list(self, item_states, recursion_depth, se_use=None): + def __fill_list(self, item_states, recursion_depth, se_use=None, use=None): for i, element in enumerate(item_states): if element == self.state_item: self._log_info("Use element {} is same as current state - Ignoring.", element) elif element is not None and element not in self.__use_done: + if isinstance(se_use, list): + se_use = se_use[i] try: - _use = se_use[i] + se_use = element.property.path except Exception: - _use = element - self.__fill(element, recursion_depth, _use) + se_use = element self.__use_done.append(element) + self.__fill(element, recursion_depth, se_use, use) + + + def __initialize_se_use(self, state, recursion_depth): + # Import data from other item if attribute "use" is found + if isinstance(state, SeState): + item_state = state.state_item + state_type = "state" + elif isinstance(state, Item): + item_state = state + state_type = "item" + elif isinstance(state, list): + for item in state: + item_state = item + self.__initialize_se_use(item_state, recursion_depth + 1) + else: + item_state = state + state_type = "struct" + if recursion_depth > 5: + self._log_error("{0}/{1}: too many levels of 'use'", self.id, item_state) + return + if "se_use" in item_state.conf: + _returnvalue, _returntype, _, _issue, _origvalue = self.__use.set_from_attr( + item_state, "se_use", None, True, None, + self.__use_list + self.__use_ignore_list) + _configvalue = copy(_returnvalue) + _configvalue = [_configvalue] if not isinstance(_configvalue, list) else _configvalue + _configorigvalue = copy(_origvalue) + _configorigvalue = [_configorigvalue] if not isinstance(_configorigvalue, list) else _configorigvalue + self._abitem.update_issues('config', {state.id: {'issue': _issue, 'attribute': 'se_use'}}) + _use = self.__use.get() + if self.__use.is_empty() or _use is None: + _issue = "se_use {} is set up in a wrong way".format(_use) + self._abitem.update_issues('config', {state.id: {'issue': _issue, 'attribute': 'se_use', 'origin': state_type}}) + self._log_warning("{} - ignoring.", _issue) + else: + _use = [_use] if not isinstance(_use, list) else _use + _returntype = [_returntype] if not isinstance(_returntype, list) else _returntype + cleaned_use_list = [] + for i, element in enumerate(_use): + try: + _name = element.id + except Exception: + _name = element + _fill = True + _path = None + if isinstance(element, StateEngineStruct.SeStruct): + _path = element.property.path + text1 = "Reading struct {0}. It is{1} a valid struct for the state configuration.{2}" + _fill = element.property.valid_se_use + valid1 = " NOT" if _fill is False else "" + valid2 = " Ignoring." if _fill is False else "" + self._log_info(text1, _path, valid1, valid2) + if _fill is False: + _issue = "Not valid. Ensure it is addressed by .rules.." + self._abitem.update_issues('struct', {_path: {'issue': _issue}}) + self.__use_ignore_list.append(_path) + elif _configvalue and _configvalue[i] not in cleaned_use_list: + cleaned_use_list.append(_configvalue[i]) + elif isinstance(element, self.__itemClass): + _path = element.property.path + if element.return_parent() == Items.get_instance(): + valid1 = " most likely NOT" + valid3 = "" + valid2 = ", because it has no parent item!" + else: + valid2 = "" + valid1 = " NOT" if _fill is False else " most likely" + valid3 = " Ignoring." if _fill is False else "" + text1 = "Reading Item {0}. It is{1} a valid item for the state configuration{2}.{3}" + self._log_info(text1, _path, valid1, valid2, valid3) + if _fill is False: + _issue = "Item {} is not a valid item for the state configuration.".format(_path) + self._abitem.update_issues('config', + {state.id: {'issue': _issue, 'attribute': 'se_use', 'origin': state_type}}) + self.__use_ignore_list.append(_path) + elif _configorigvalue and _configorigvalue[i] not in cleaned_use_list: + cleaned_use_list.append(_configorigvalue[i]) + if _returntype[i] == 'value': + _issues = self.__use.get_issues() + for item in _issues.get('cast_item'): + if (_configorigvalue[i] is not None and isinstance(_configorigvalue[i], str) and + (StateEngineTools.partition_strip(_configorigvalue[i], ":")[1] in item or + _configorigvalue[i] in item)): + _issue_list = [item for key, value in _issues.items() if value for item in value] + self._log_warning("se_use {} points to invalid item. Ignoring.", _configorigvalue[i]) + self._abitem.update_issues('config', {state.id: + {'issue': _issue_list, + 'attribute': 'se_use', 'origin': state_type}}) + self.__use_ignore_list.append(_configorigvalue[i]) + _path = None + elif _returntype[i] in ['item', 'eval']: + _path = _configvalue[i] + _issues = self.__use.get_issues() + for list_key in ['cast_item', 'eval', 'item']: + if list_key in _issues: + for item in _issues[list_key]: + if (_path is not None and isinstance(_path, str) and + StateEngineTools.partition_strip(_path, ":")[1] in item): + + _issue_list = [item for key, value in _issues.items() if value for item in value] + self._log_warning("se_use {} defined by invalid item/eval. Ignoring.", _path) + self._abitem.update_issues('config', {state.id: + {'issue': _issue_list, + 'attribute': 'se_use', 'origin': state_type}}) + self.__use_ignore_list.append(_path) + _path = None + if _path is None: + pass + + elif _path is not None and _configorigvalue[i] not in cleaned_use_list: + self._log_info("se_use {} defined by item/eval {}. Even if current result is not valid, " + "entry will be re-evaluated on next state evaluation. element: {}", _path, _configorigvalue[i], element) + cleaned_use_list.append(_configorigvalue[i]) + #self.__use_done.append(_path) + if _path is None: + pass + elif element == self.state_item: + self._log_info("Use element {} is same as current state - Ignoring.", _name) + self.__use_ignore_list.append(element) + elif _fill and element is not None and _configorigvalue[i] not in self.__use_list: + + if isinstance(_name, list): + self._log_develop( + "Adding list element {} to state fill function. path is {}, name is {}. configvalue {}", + element, _path, _name, _configorigvalue[i]) + self.__use_list.append(_configorigvalue[i]) + for item in _name: + self.__initialize_se_use(item, recursion_depth + 1) + else: + self._log_develop( + "Adding element {} to state fill function. path is {}, name is {}.", + _configorigvalue[i], _path, _name) + self.__use_list.append(_configorigvalue[i]) + self.__initialize_se_use(element, recursion_depth + 1) + elif _fill and element is not None and _configorigvalue[i] in self.__use_list: + self._log_debug("Ignoring element {} as it is already added. cleaned use {}", element, cleaned_use_list) + self.__use_list.extend(cleaned_use_list) + seen = set() + self.__use_list = [x for x in self.__use_list if not (x in seen or seen.add(x))] + self.__use.set(self.__use_list) # Read configuration from item and populate data in class # item_state: item to read from # recursion_depth: current recursion_depth (recursion is canceled after five levels) # se_use: If se_use Attribute is used or not - def __fill(self, state, recursion_depth, se_use=None): + def __fill(self, state, recursion_depth, se_use=None, use=None): + def update_unused(used_attributes, attrib_type, attrib_name): #filtered_dict = {key: value for key, value in self.__unused_attributes.items() if key not in used_attributes} #self.__unused_attributes = copy(filtered_dict) @@ -461,107 +607,25 @@ def update_action_status(action_status, actiontype): item_state = state.state_item else: item_state = state - self._log_develop("Fill state {} type {}", item_state, type(item_state)) + self._log_develop("Fill state {} type {}, called by {}, recursion {}", item_state, type(item_state), se_use, recursion_depth) if se_use == "reinit": - self._log_develop("Resetting conditions and actions at re-init") + self._log_develop("Resetting conditions and actions at re-init use is {}", use) self.__conditions.reset() self.__actions_enter_or_stay.reset() self.__actions_enter.reset() self.__actions_stay.reset() self.__actions_leave.reset() self.__use_done = [] - if recursion_depth > 5: - self._log_error("{0}/{1}: too many levels of 'use'", self.id, item_state) - return - # Import data from other item if attribute "use" is found - if "se_use" in item_state.conf: - _returnvalue, _returntype, _, _issue = self.__use.set_from_attr(item_state, "se_use") - _configvalue = copy(_returnvalue) - _configvalue = [_configvalue] if not isinstance(_configvalue, list) else _configvalue - self._abitem.update_issues('config', {state.id: {'issue': _issue, 'attribute': 'se_use'}}) - _use = self.__use.get() - if self.__use.is_empty() or _use is None: - _issue = "se_use {} is set up in a wrong way".format(_use) - self._abitem.update_issues('config', {state.id: {'issue': _issue, 'attribute': 'se_use'}}) - self._log_warning("{} - ignoring.", _issue) - else: - _use = [_use] if not isinstance(_use, list) else _use - _returntype = [_returntype] if not isinstance(_returntype, list) else _returntype - cleaned_use_list = [] - for i, element in enumerate(_use): - try: - _name = element.id - except Exception: - _name = element - _fill = True - _path = None - if isinstance(element, StateEngineStruct.SeStruct): - _path = element.property.path - text1 = "Reading struct {0}. It is{1} a valid struct for the state configuration.{2}" - _fill = element.property.valid_se_use - valid1 = " NOT" if _fill is False else "" - valid2 = " Ignoring." if _fill is False else "" - self._log_info(text1, _path, valid1, valid2) - if _fill is False: - _issue = "Not valid. Ensure it is addressed by .rules.." - self._abitem.update_issues('struct', {_path: {'issue': _issue}}) - elif _configvalue and _configvalue[i] not in cleaned_use_list: - cleaned_use_list.append(_configvalue[i]) - elif isinstance(element, self.__itemClass): - _path = element.property.path - if element.return_parent() == Items.get_instance(): - valid1 = " most likely NOT" - valid3 = "" - valid2 = ", because it has no parent item!" - else: - valid2 = "" - valid1 = " NOT" if _fill is False else " most likely" - valid3 = " Ignoring." if _fill is False else "" - text1 = "Reading Item {0}. It is{1} a valid item for the state configuration{2}.{3}" - self._log_info(text1, _path, valid1, valid2, valid3) - if _fill is False: - _issue = "Item {} is not a valid item for the state configuration.".format(_path) - self._abitem.update_issues('config', - {state.id: {'issue': _issue, 'attribute': 'se_use'}}) - elif _configvalue and _configvalue[i] not in cleaned_use_list: - cleaned_use_list.append(_configvalue[i]) - if _returntype[i] in ['item', 'eval']: - _path = _configvalue[i] - _issues = self.__use.get_issues() - for list_key in ['cast_item', 'eval', 'item']: - if list_key in _issues: - for item in _issues[list_key]: - if _path is not None and StateEngineTools.partition_strip(_path, ":")[1] in item: - self._log_warning("se_use {} defined by invalid item/eval. Ignoring", _path) - _issue_list = [item for key, value in _issues.items() if value for item in value] - self._abitem.update_issues('config', {state.id: - {'issue': [_issues], - 'attribute': 'se_use'}}) - _path = None - if _path is None: - pass + use = self.__use.get() - elif _path is not None and _path not in cleaned_use_list: - self._log_info("se_use {} defined by item/eval. Even if current result is not valid, " - "entry will be re-evaluated on next state evaluation.", _path) - cleaned_use_list.append(_path) - self.__use_done.append(_path) - if _path is None: - pass - elif element == self.state_item: - self._log_info("Use element {} is same as current state - Ignoring.", _name) - elif _fill and element is not None and element not in self.__use_done: - self._log_develop("Adding element {} to state fill function.", _name) - if isinstance(_name, list): - self.__fill_list(element, recursion_depth + 1, _name) - else: - self.__use_done.append(element) - self.__fill(element, recursion_depth + 1, _name) - elif _fill and element is not None and element in self.__use_done: - self._log_debug("Ignoring element {} as it is already added.", element) - self.__use.set(cleaned_use_list) + if use is not None: + use = use if isinstance(use, list) else [use] + use = [u for u in use if u is not None] + use = StateEngineTools.flatten_list(use) + self.__fill_list(use, recursion_depth, se_use, use) # Get action sets and condition sets + self._log_develop("Use is {}", use) parent_item = item_state.return_parent() if parent_item == Items.get_instance(): parent_item = None @@ -640,21 +704,22 @@ def update_action_status(action_status, actiontype): self.update_name(item_state, recursion_depth) # Complete condition sets and actions at the end + if recursion_depth == 0: - self.__conditions.complete(self) - _action_status = self.__actions_enter.complete(self, self.__conditions.evals_items) + self.__conditions.complete(self, use) + _action_status = self.__actions_enter.complete(self, self.__conditions.evals_items, use) if _action_status: update_action_status(_action_status, 'enter') self._abitem.update_action_status(self.__action_status) - _action_status = self.__actions_stay.complete(self, self.__conditions.evals_items) + _action_status = self.__actions_stay.complete(self, self.__conditions.evals_items, use) if _action_status: update_action_status(_action_status, 'stay') self._abitem.update_action_status(self.__action_status) - _action_status = self.__actions_enter_or_stay.complete(self, self.__conditions.evals_items) + _action_status = self.__actions_enter_or_stay.complete(self, self.__conditions.evals_items, use) if _action_status: update_action_status(_action_status, 'enter_or_stay') self._abitem.update_action_status(self.__action_status) - _action_status = self.__actions_leave.complete(self, self.__conditions.evals_items) + _action_status = self.__actions_leave.complete(self, self.__conditions.evals_items, use) if _action_status: update_action_status(_action_status, 'leave') self._abitem.update_action_status(self.__action_status) From fb1ddc7a65ceb7380ff8e0318966ee4115e0c49c Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Tue, 30 Jul 2024 21:03:47 +0200 Subject: [PATCH 19/19] stateengine plugin: bump version to 2.1 as there were quite some edits --- stateengine/__init__.py | 2 +- stateengine/plugin.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/stateengine/__init__.py b/stateengine/__init__.py index 59b23aa7c..85b171e8d 100755 --- a/stateengine/__init__.py +++ b/stateengine/__init__.py @@ -47,7 +47,7 @@ class StateEngine(SmartPlugin): - PLUGIN_VERSION = '2.0.1' + PLUGIN_VERSION = '2.1' # Constructor # noinspection PyUnusedLocal,PyMissingConstructor diff --git a/stateengine/plugin.yaml b/stateengine/plugin.yaml index fc58cdc29..c1f40d7b4 100755 --- a/stateengine/plugin.yaml +++ b/stateengine/plugin.yaml @@ -39,7 +39,7 @@ plugin: state: ready support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1303071-stateengine-plugin-support - version: 2.0.1 + version: 2.1 sh_minversion: '1.6' multi_instance: False classname: StateEngine @@ -351,8 +351,8 @@ item_attributes: en: 'Name of state, overwriting the value defined by "name" attribute' description_long: de: '**Zustandsname, überschreibt den im Attribut "name" angegebene Wert**\n - Dies kann beispielsweise nützlich sein, um den Namen abhängig - von einer Bedingungsgruppe zu ändern. + Dies kann beispielsweise nützlich sein, um den Namen abhängig + von einer Bedingungsgruppe zu ändern. ' en: '**Name of state, overwriting the value defined by "name" attribute**\n Could be useful to change the state name based on a condition group