From d6f96ff5e59d273de9ac2e0c268dcef655af2957 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 25 Jul 2024 17:51:43 +0200 Subject: [PATCH] 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