diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index d115c289c..e8f2392da 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 @@ -88,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}]}} @@ -104,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 @@ -112,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 @@ -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)) @@ -288,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, 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, use=None): _issue = {self._name: {'issue': None, - 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} - self._log_develop("Check item {} status {} value {} evals_items {}", check_item, check_status, check_value, evals_items) + 'issueorigin': [{'state': state.id, 'action': self._function}]}} + 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: @@ -302,43 +300,40 @@ 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) + '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, 0, use) 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, 0, use) 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, 0, use) 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}]}} - elif check_status is not None: - check_status = str(status) + '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, 0, use) if mindelta is not None: check_mindelta.set(mindelta) @@ -361,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 @@ -401,7 +396,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: @@ -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): @@ -446,13 +441,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) @@ -509,14 +504,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 @@ -526,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, use=None): raise NotImplementedError("Class {} doesn't implement complete()".format(self.__class__.__name__)) # Check if execution is possible @@ -537,7 +532,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: @@ -617,15 +619,15 @@ 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 # 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, use=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, use) self._action_status = _issue return _issue @@ -675,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 @@ -706,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) @@ -724,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() @@ -738,15 +740,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 @@ -772,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, use=None): self._scheduler_name = "{}-SeByAttrDelayTimer".format(self.__byattr) _issue = {self._name: {'issue': None, 'attribute': self.__byattr, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} @@ -793,14 +795,14 @@ 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) 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 @@ -824,14 +826,14 @@ 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 # 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, use=None): self._scheduler_name = "{}-SeLogicDelayTimer".format(self.__logic) _issue = {self._name: {'issue': None, 'logic': self.__logic, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} @@ -871,11 +873,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 @@ -904,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, 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}]}} @@ -945,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: @@ -964,12 +967,12 @@ 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), - '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 +987,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" @@ -993,15 +997,15 @@ 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 # 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, use=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, use) self._action_status = _issue return _issue @@ -1065,7 +1069,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: @@ -1074,12 +1079,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") @@ -1132,9 +1144,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 @@ -1167,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, use=None): if isinstance(self.__value, list): item = self.__value[0].property.path else: @@ -1314,9 +1335,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 +1383,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 +1437,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 +1493,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 +1547,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/StateEngineActions.py b/stateengine/StateEngineActions.py index 6aa9976f2..89bef33bd 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,14 @@ 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) if func == "se_delay": # set delay if name not in self.__actions: @@ -149,9 +156,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 +169,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 +198,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 +317,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 +374,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 +382,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 +389,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 +396,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 +403,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 +413,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 +420,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 +427,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 +434,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 +441,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 +448,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 +455,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 @@ -504,15 +507,17 @@ 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, use=None): _status = {} + if use is None: + use = state.use.get() 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, use)) 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): @@ -573,7 +578,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..abfe71745 100755 --- a/stateengine/StateEngineCondition.py +++ b/stateengine/StateEngineCondition.py @@ -63,18 +63,21 @@ 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): + 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, item_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:"): @@ -94,7 +97,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, 0, use) if isinstance(value, str): match = re.match(r'^(.*):', value) if isinstance(value, str) and value.startswith("eval:"): @@ -113,7 +116,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, 0, use) if isinstance(value, str): match = re.match(r'^(.*):', value) if value.startswith("eval:"): @@ -129,7 +132,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, 0, use) if isinstance(value, str): match = re.match(r'^(.*):', value) if value.startswith("eval:"): @@ -222,9 +225,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, 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() \ @@ -283,8 +286,7 @@ def complete(self, item_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, item_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]): @@ -309,8 +311,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 +441,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 +476,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 +492,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 +525,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 +567,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 +580,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 +588,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 +643,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 +734,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 +754,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 +764,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 +812,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 +841,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 +853,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..ed4e4b6d0 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): @@ -111,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, "_") @@ -142,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, use): 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, use): 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': - [{'conditionset': self.name, 'condition': name}]}}) + 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: @@ -184,14 +191,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/StateEngineConditionSets.py b/stateengine/StateEngineConditionSets.py index e7dc7b7e7..c8dbf3612 100755 --- a/stateengine/StateEngineConditionSets.py +++ b/stateengine/StateEngineConditionSets.py @@ -71,10 +71,12 @@ 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, use=None): + if use is None: + use = state.use.get() for name in self.__condition_sets: - self.__condition_sets[name].complete(item_state) + self.__condition_sets[name].complete(state, use) # Write all condition sets to logger def write_to_logger(self): diff --git a/stateengine/StateEngineEval.py b/stateengine/StateEngineEval.py index dea0a5bb3..55abe6db6 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,14 +159,16 @@ 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) 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: @@ -182,16 +185,17 @@ 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) + 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": @@ -221,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": @@ -228,13 +233,14 @@ 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, 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/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..42202d905 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,13 @@ from . import StateEngineStructs from . import StateEngineEval -from lib.item import Items from lib.shtime import Shtime from lib.item.item import Item +from lib.item.items import Items import copy import threading import queue import re -from ast import literal_eval # Class representing a blind item @@ -51,6 +49,10 @@ class SeItem: def id(self): return self.__id + @property + def path(self): + return self.__id + @property def variables(self): return self.__variables @@ -117,7 +119,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 @@ -180,7 +182,7 @@ 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.__logging_off = False self.update_lock = threading.Lock() self.__ab_alive = False self.__queue = queue.Queue() @@ -201,8 +203,8 @@ 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() - _returnvalue, _returntype, _using_default, _issue = self.__log_level.set_from_attr(self.__item, "se_log_level", + _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 _returnvalue = self.__log_level.get() @@ -210,11 +212,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) @@ -293,7 +295,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 = [] @@ -310,8 +313,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 @@ -360,7 +363,6 @@ def __init__(self, smarthome, item, se_plugin): self.__logger.error("Issue finishing states because {}", ex) return - def __repr__(self): return self.__id @@ -416,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: @@ -465,22 +467,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: @@ -538,8 +547,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 +583,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 +617,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 +667,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 +681,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 +696,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 +764,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 +807,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 +900,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 +921,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 +965,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) @@ -971,13 +980,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 @@ -1009,7 +1034,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 @@ -1023,19 +1048,56 @@ 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("- {}: {}", key, ', '.join(formatted_entries)) + else: + self.__logger.info("- {}: {}", 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]) + 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") + 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 +1130,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) @@ -1076,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: @@ -1084,7 +1149,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: @@ -1196,7 +1263,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) @@ -1234,7 +1301,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 @@ -1573,7 +1641,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: @@ -1602,7 +1670,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: @@ -1617,7 +1685,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': @@ -1654,7 +1722,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) @@ -1753,16 +1821,17 @@ 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]} + 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 @@ -1926,20 +1995,23 @@ 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] + 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.".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:"): 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) @@ -1959,11 +2031,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 +2059,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..c8986e21b 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 @@ -84,7 +92,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 +100,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 +108,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 +116,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 +124,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 +138,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 @@ -158,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) @@ -166,12 +176,13 @@ 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.__initialize_se_use(self, 0) + self.__fill(self, 0, "reinit") finally: 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 @@ -192,7 +203,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) @@ -265,9 +276,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, None, False) + _, _, _issue, _ = self.__order.set(value, "", True, False) else: _issue = [None] @@ -324,7 +335,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() @@ -341,25 +351,25 @@ 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): 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") + _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, 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 @@ -375,33 +385,177 @@ 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): + 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, item_state, recursion_depth, se_use=None): - def update_unused(used_attributes, type, name): + 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) - 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): @@ -449,97 +603,38 @@ 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 {}, 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.property.path) - 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'}}) - _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._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.property.path - 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 - 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 _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'}}) - 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] - 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: - 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) - self.__use.set(cleaned_use_list) + use = self.__use.get() + 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 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) @@ -560,7 +655,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 @@ -574,84 +669,72 @@ 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 + 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: - _enter_actioncount += 1 - _, _action_status = self.__actions_enter.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, '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 - for attribute in child_item.conf: - _leave_actioncount += 1 - _, _action_status = self.__actions_leave.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 + 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, 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(item_state, 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(item_state, 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(item_state, 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) 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..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 @@ -61,7 +65,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 @@ -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 @@ -160,7 +164,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 +188,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 = {} @@ -199,12 +204,14 @@ 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] - _temp_dict = self.dict_get(self._global_struct.get(self._struct) or {}, parent_name, self._global_struct.get(self._struct) or {}) + 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( {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..a29968b0a 100755 --- a/stateengine/StateEngineTools.py +++ b/stateengine/StateEngineTools.py @@ -22,12 +22,9 @@ from . import StateEngineLogger import datetime from ast import literal_eval -from lib.item import Items -from lib.item.item import Item import re +from lib.item.items import Items -itemsApi = Items.get_instance() -__itemClass = Item # General class for everything that is below the SeItem Class # This class provides some general stuff: @@ -124,7 +121,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: @@ -260,28 +257,42 @@ 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): - # 1: parent of given item could have attribute - try: - parent_item = base_item.return_parent() - except Exception: +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) + if result is not None: + return result return None + + # 1: parent of given item could have attribute try: - _parent_conf = parent_item.conf - if parent_item is not None and attribute in _parent_conf: - return parent_item.conf[attribute] + # if state is state object, get the item and se_use information + base_item = state.state_item + if use is None: + use = state.use.get() except Exception: - return None + # 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: + 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 item has attribute "se_use", get the item to use and search this item for required attribute - if "se_use" in base_item.conf: + # 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 - 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 + result = find_attribute(smarthome, use, attribute, recursion_depth + 1) + if result is not None: + return result # 3: nothing found return None @@ -292,8 +303,8 @@ def find_attribute(smarthome, base_item, attribute, recursion_depth=0): # 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() @@ -305,8 +316,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 +328,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 +356,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 +384,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 +394,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..bfc2c87b8 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -54,11 +54,13 @@ 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 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 @@ -83,18 +85,32 @@ 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 # 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: - return None, None, False, None + return None, None, False, None, None else: value = default_value _using_default = True @@ -119,9 +135,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, item) - 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: @@ -140,11 +156,12 @@ def __resetvalue(self): self._additional_sources = [] self.__listorder = [] self.__type_listorder = [] + self.__orig_listorder = [] # 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: @@ -180,6 +197,7 @@ def set(self, value, name="", reset=True, item=None, 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": @@ -256,6 +274,7 @@ def set(self, value, name="", reset=True, item=None, 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 @@ -283,8 +302,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 +316,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']: @@ -315,6 +334,9 @@ def set(self, value, name="", reset=True, item=None, 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 +371,9 @@ def set(self, value, name="", reset=True, item=None, 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 @@ -359,7 +384,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']: @@ -374,15 +399,19 @@ def set(self, value, name="", reset=True, item=None, 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 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): @@ -479,7 +508,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): @@ -518,14 +547,31 @@ 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 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 except Exception as ex: - self._log_error("Can't cast {0} to item/struct! {1}".format(value, 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(_issue) 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 +579,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 +620,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 +633,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) @@ -628,29 +674,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 @@ -669,7 +726,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: @@ -678,7 +734,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_dict not in self.__get_issues['regex']: + self.__get_issues['regex'].append(_issue_dict) self._log_info(_issue) return values @@ -696,6 +754,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 @@ -704,8 +765,11 @@ 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) + _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 @@ -719,7 +783,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: @@ -727,6 +791,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 @@ -737,13 +804,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: _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() 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 @@ -751,7 +823,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: @@ -762,6 +836,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 @@ -770,17 +847,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_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: @@ -797,8 +875,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) @@ -812,6 +891,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: @@ -826,8 +906,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] @@ -852,12 +933,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 @@ -867,10 +947,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: diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index a40a5a300..3181788b2 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: @@ -57,23 +57,34 @@ def __init__(self, smarthome, 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 # 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) _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) - _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 @@ -95,36 +106,37 @@ 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 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 = _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: action_tooltip += ' ' @@ -140,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 @@ -219,7 +231,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 +307,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')\ @@ -372,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]) @@ -433,6 +447,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..85b171e8d 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 @@ -48,14 +47,13 @@ class StateEngine(SmartPlugin): - PLUGIN_VERSION = '2.0.0' + PLUGIN_VERSION = '2.1' # Constructor # noinspection PyUnusedLocal,PyMissingConstructor 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 * *'] @@ -124,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"): @@ -141,12 +130,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 +150,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 +165,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 +173,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 +186,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 ( @@ -221,7 +210,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: @@ -261,21 +250,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/plugin.yaml b/stateengine/plugin.yaml index c08e5f59b..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.0 + version: 2.1 sh_minversion: '1.6' multi_instance: False classname: StateEngine @@ -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: 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: 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 %}