diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 4f429e431..407957ffa 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -34,6 +34,10 @@ class SeActionBase(StateEngineTools.SeItemChild): def name(self): return self._name + @property + def action_status(self): + return self.__action_status + # Cast function for delay # value: value to cast @staticmethod @@ -73,7 +77,7 @@ def __init__(self, abitem, name: str): self._scheduler_name = None self.__function = None self.__template = None - self._state = None + self.__action_status = {} self.__queue = abitem.queue def update_delay(self, value): @@ -108,6 +112,43 @@ def update_modes(self, value): def get_order(self): return self.__order.get(1) + def update_webif_actionstatus(self, state, name, success, issue=None): + try: + if self._abitem.webif_infos[state.id].get('actions_stay'): + _key = ['{}'.format(state.id), 'actions_stay', '{}'.format(name), 'actionstatus', 'success'] + self._abitem.update_webif(_key, success) + _key = ['{}'.format(state.id), 'actions_stay', '{}'.format(name), 'actionstatus', 'issue'] + self._abitem.update_webif(_key, issue) + except Exception: + pass + try: + if self._abitem.webif_infos[state.id].get('actions_enter'): + _key = ['{}'.format(state.id), 'actions_enter', '{}'.format(name), 'actionstatus', 'success'] + self._abitem.update_webif(_key, success) + _key = ['{}'.format(state.id), 'actions_enter', '{}'.format(name), 'actionstatus', 'issue'] + self._abitem.update_webif(_key, issue) + except Exception: + pass + try: + if self._abitem.webif_infos[state.id].get('actions_enter_or_stay'): + _key = ['{}'.format(state.id), 'actions_enter_or_stay', '{}'.format(name), 'actionstatus', 'success'] + self._abitem.update_webif(_key, success) + _key = ['{}'.format(state.id), 'actions_enter_or_stay', '{}'.format(name), 'actionstatus', 'issue'] + self._abitem.update_webif(_key, issue) + except Exception: + pass + try: + state.update_name(state.state_item) + _key_name = ['{}'.format(state.id), 'name'] + self._abitem.update_webif(_key_name, state.name) + if self._abitem.webif_infos[state.id].get('actions_leave'): + _key = ['{}'.format(state.id), 'actions_leave', '{}'.format(name), 'actionstatus', 'success'] + self._abitem.update_webif(_key, success) + _key = ['{}'.format(state.id), 'actions_leave', '{}'.format(name), 'actionstatus', 'issue'] + self._abitem.update_webif(_key, issue) + except Exception: + pass + # Write action to logger def write_to_logger(self): self._log_debug("name: {}", self._name) @@ -145,118 +186,116 @@ def set_source(self, current_condition, previous_condition, previousstate_condit # item_allow_repeat: Is repeating actions generally allowed for the item? # state: state item that triggered the action def execute(self, is_repeat: bool, allow_item_repeat: bool, state): - self._state = state - if not self._can_execute(): + # check if any conditiontype is met or not + # condition: type of condition 'conditionset'/'previousconditionset'/'previousstate_conditionset' + def _check_condition(condition: str): + _conditions_met_count = 0 + _conditions_necessary_count = 0 + _condition_to_meet = None + _updated__current_condition = None + if condition == 'conditionset': + _condition_to_meet = None if self.conditionset.is_empty() else self.conditionset.get() + _current_condition = self._abitem.get_lastconditionset_id() + _updated__current_condition = self._abitem.get_variable("current.state_id") if _current_condition == '' else _current_condition + elif condition == 'previousconditionset': + _condition_to_meet = None if self.previousconditionset.is_empty() else self.previousconditionset.get() + _current_condition = self._abitem.get_previousconditionset_id() + _updated__current_condition = self._abitem.get_previousstate_id() if _current_condition == '' else _current_condition + elif condition == 'previousstate_conditionset': + _condition_to_meet = None if self.previousstate_conditionset.is_empty() else self.previousstate_conditionset.get() + _current_condition = self._abitem.get_previousstate_conditionset_id() + _updated__current_condition = self._abitem.get_previousstate_id() if _current_condition == '' else _current_condition + _condition_to_meet = _condition_to_meet if isinstance(_condition_to_meet, list) else [_condition_to_meet] + _condition_met = [] + for cond in _condition_to_meet: + if cond is not None: + _conditions_necessary_count += 1 + _orig_cond = cond + try: + cond = re.compile(cond) + _matching = cond.fullmatch(_updated__current_condition) + 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 + else: + self._log_debug("Given {} {} not matching current one: {}", condition, _orig_cond, _updated__current_condition) + except Exception as ex: + if cond is not None: + self._log_warning("Given {} {} is not a valid regex: {}", condition, _orig_cond, ex) + return _condition_met, _conditions_met_count, _conditions_necessary_count + + # update web interface with delay info + # action_type: 'actions_enter', etc. + # delay_info: delay information + def _update_delay_webif(action_type: str, delay_info: str): + try: + _key = ['{}'.format(state.id), '{}'.format(action_type), '{}'.format(self._name), 'delay'] + self._abitem.update_webif(_key, delay_info) + except Exception: + pass + + # update web interface with repeat info + # value: bool type True or False for repeat value + def _update_repeat_webif(value: bool): + _key1 = ['{}'.format(state.id), 'actions_stay', '{}'.format(self._name), 'repeat'] + _key2 = ['{}'.format(state.id), 'actions_enter_or_stay', '{}'.format(self._name), 'repeat'] + result = self._abitem.update_webif(_key1, value) + if result is False: + self._abitem.update_webif(_key2, value) + + self._log_decrease_indent(50) + self._log_increase_indent() + self._log_info("Action '{0}': Preparing", self._name) + if not self._can_execute(state): return conditions_met = 0 condition_necessary = 0 - condition_to_meet = None if self.conditionset.is_empty() else self.conditionset.get() - condition_to_meet = condition_to_meet if isinstance(condition_to_meet, list) else [condition_to_meet] - current_condition = self._abitem.get_lastconditionset_id() - updated_current_condition = self._abitem.get_laststate_id() if current_condition == '' else current_condition - current_condition_met = [] - for cond in condition_to_meet: - if cond is not None: - condition_necessary += 1 - try: - orig_cond = cond - cond = re.compile(cond) - matching = cond.fullmatch(updated_current_condition) - if matching: - self._log_debug("Given conditionset {} matches current one: {}", orig_cond, updated_current_condition) - current_condition_met.append(updated_current_condition) - conditions_met +=1 - else: - self._log_debug("Given conditionset {} not matching current one: {}", orig_cond, updated_current_condition) - except Exception as ex: - if cond is not None: - self._log_warning("Given conditionset {} is not a valid regex: {}", orig_cond, ex) - previouscondition_to_meet = None if self.previousconditionset.is_empty() else self.previousconditionset.get() - previouscondition_to_meet = previouscondition_to_meet if isinstance(previouscondition_to_meet, list) else [previouscondition_to_meet] - previous_condition = self._abitem.get_previousconditionset_id() - updated_previous_condition = self._abitem.get_previousstate_id() if previous_condition == '' else previous_condition - previous_condition_met = [] - for cond in previouscondition_to_meet: - if cond is not None: - condition_necessary += 1 - try: - orig_cond = cond - cond = re.compile(cond) - matching = cond.fullmatch(updated_previous_condition) - if matching: - self._log_debug("Given previousconditionset {} matches previous one: {}", orig_cond, updated_previous_condition) - previous_condition_met.append(updated_previous_condition) - conditions_met +=1 - else: - self._log_debug("Given previousconditionset {} not matching previous one: {}", orig_cond, updated_previous_condition) - except Exception as ex: - if cond is not None: - self._log_warning("Given previousconditionset {} is not a valid regex: {}", orig_cond, ex) - previousstate_condition_to_meet = None if self.previousstate_conditionset.is_empty() else self.previousstate_conditionset.get() - previousstate_condition_to_meet = previousstate_condition_to_meet if isinstance(previousstate_condition_to_meet, list) else [previousstate_condition_to_meet] - previousstate_condition = self._abitem.get_previousstate_conditionset_id() - updated_previousstate_condition = self._abitem.get_previousstate_id() if previousstate_condition == '' else previousstate_condition - previousstate_condition_met = [] - for cond in previousstate_condition_to_meet: - if cond is not None: - condition_necessary += 1 - try: - orig_cond = cond - cond = re.compile(cond) - matching = cond.fullmatch(updated_previousstate_condition) - if matching: - self._log_debug("Given previousstate_conditionset {} matches previous state's conditionset: {}", orig_cond, updated_previousstate_condition) - previousstate_condition_met.append(updated_previousstate_condition) - conditions_met +=1 - else: - self._log_debug("Given previousstate_conditionset {} not matching previous state's conditionset: {}", orig_cond, updated_previousstate_condition) - except Exception as ex: - if cond is not None: - self._log_warning("Given previousstate_conditionset {} is not a valid regex: {}", orig_cond, ex) + current_condition_met, cur_conditions_met, cur_condition_necessary = _check_condition('conditionset') + conditions_met += cur_conditions_met + condition_necessary += 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 + previousstate_condition_met, prevst_conditions_met, prevst_condition_necessary = _check_condition('previousstate_conditionset') + conditions_met += prevst_conditions_met + condition_necessary += 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) return - else: - self._log_info("Action '{0}': Running.", self._name) if is_repeat: - _key1 = ['{}'.format(state.id), 'actions_stay', '{}'.format(self._name), 'repeat'] - _key2 = ['{}'.format(state.id), 'actions_enter_or_stay', '{}'.format(self._name), 'repeat'] if self.__repeat is None: if allow_item_repeat: repeat_text = " Repeat allowed by item configuration." - result = self._abitem.update_webif(_key1, True) - if result is False: - self._abitem.update_webif(_key2, True) + _update_repeat_webif(True) else: self._log_info("Action '{0}': Repeat denied by item configuration.", self._name) - result = self._abitem.update_webif(_key1, False) - if result is False: - self._abitem.update_webif(_key2, False) + _update_repeat_webif(False) return elif self.__repeat.get(): repeat_text = " Repeat allowed by action configuration." - result = self._abitem.update_webif(_key1, True) - if result is False: - self._abitem.update_webif(_key2, True) + _update_repeat_webif(True) else: self._log_info("Action '{0}': Repeat denied by action configuration.", self._name) - result = self._abitem.update_webif(_key1, False) - if result is False: - self._abitem.update_webif(_key2, False) + _update_repeat_webif(False) return else: repeat_text = "" - + self._log_increase_indent() try: self._getitem_fromeval() + self._log_decrease_indent() _validitem = True except Exception as ex: _validitem = False self._log_error("Action '{0}': Ignored because {1}", self._name, ex) + self._log_decrease_indent() if _validitem: + delay = 0 if self.__delay.is_empty() else self.__delay.get() plan_next = self._se_plugin.scheduler_return_next(self._scheduler_name) - if plan_next is not None and plan_next > self.shtime.now(): + if plan_next is not None and plan_next > self.shtime.now() or delay == -1: self._log_info("Action '{0}: Removing previous delay timer '{1}'.", self._name, self._scheduler_name) self._se_plugin.scheduler_remove(self._scheduler_name) try: @@ -264,40 +303,35 @@ def execute(self, is_repeat: bool, allow_item_repeat: bool, state): except Exception: pass - delay = 0 if self.__delay.is_empty() else self.__delay.get() actionname = "Action '{0}'".format(self._name) if delay == 0 else "Delayed Action ({0} seconds) '{1}'".format( delay, self._scheduler_name) _delay_info = 0 if delay is None: + self._log_increase_indent() self._log_warning("Action '{0}': Ignored because of errors while determining the delay!", self._name) + self._log_decrease_indent() _delay_info = -1 - elif delay < 0: + elif delay == -1: + self._log_increase_indent() + self._log_info("Action '{0}': Ignored because delay is set to -1.", self._name) + self._log_decrease_indent() + _delay_info = -1 + elif delay < -1: + self._log_increase_indent() self._log_warning("Action '{0}': Ignored because delay is negative!", self._name) + self._log_decrease_indent() _delay_info = -1 else: - self._waitforexecute(actionname, self._name, repeat_text, delay, current_condition_met, previous_condition_met, previousstate_condition_met) + self._waitforexecute(state, actionname, self._name, repeat_text, delay, current_condition_met, previous_condition_met, previousstate_condition_met) - try: - _key = ['{}'.format(state.id), 'actions_stay', '{}'.format(self._name), 'delay'] - self._abitem.update_webif(_key, _delay_info) - except Exception: - pass - try: - _key = ['{}'.format(state.id), 'actions_enter', '{}'.format(self._name), 'delay'] - self._abitem.update_webif(_key, _delay_info) - except Exception: - pass - try: - _key = ['{}'.format(state.id), 'actions_enter_or_stay', '{}'.format(self._name), 'delay'] - self._abitem.update_webif(_key, _delay_info) - except Exception: - pass + _update_delay_webif('actions_stay', _delay_info) + _update_delay_webif('actions_enter', _delay_info) + _update_delay_webif('actions_enter_or_stay', _delay_info) try: state.update_name(state.state_item) _key_name = ['{}'.format(state.id), 'name'] - self.update_webif(_key_name, state.name) - _key = ['{}'.format(state.id), 'actions_leave', '{}'.format(self._name), 'delay'] - self._abitem.update_webif(_key, _delay_info) + self._abitem.update_webif(_key_name, state.name) + _update_delay_webif('actions_leave', _delay_info) except Exception: pass @@ -312,18 +346,18 @@ def complete(self, item_state, evals_items=None): raise NotImplementedError("Class {} doesn't implement complete()".format(self.__class__.__name__)) # Check if execution is possible - def _can_execute(self): + def _can_execute(self, state): return True def get(self): return True - def _waitforexecute(self, 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: str = "", previous_condition: str = "", previousstate_condition: str = ""): self._log_decrease_indent(50) self._log_increase_indent() if delay == 0: - self._log_info("Running action '{}'.", namevar) - self.real_execute(actionname, namevar, repeat_text, None, False, current_condition, previous_condition, previousstate_condition) + self._log_info("Action '{}': Running.", namevar) + self.real_execute(state, actionname, namevar, repeat_text, None, False, current_condition, previous_condition, previousstate_condition) else: instanteval = None if self.__instanteval is None else self.__instanteval.get() self._log_info("Action '{0}': Add {1} second timer '{2}' " @@ -333,7 +367,7 @@ def _waitforexecute(self, actionname: str, namevar: str = "", repeat_text: str = if instanteval is True: self._log_increase_indent() self._log_debug("Evaluating value for delayed action '{}'.", namevar) - value = self.real_execute(actionname, namevar, repeat_text, None, True, current_condition, previous_condition, previousstate_condition) + value = self.real_execute(state, actionname, namevar, repeat_text, None, True, current_condition, previous_condition, previousstate_condition) self._log_debug("Value for delayed action is going to be '{}'.", value) self._log_decrease_indent() else: @@ -344,17 +378,22 @@ def _waitforexecute(self, actionname: str, namevar: str = "", repeat_text: str = 'repeat_text': repeat_text, 'value': value, 'current_condition': current_condition, 'previous_condition': previous_condition, - 'previousstate_condition': previousstate_condition}, next=next_run) + 'previousstate_condition': previousstate_condition, + 'state': state}, next=next_run) - def _delayed_execute(self, actionname: str, namevar: str = "", repeat_text: str = "", value=None, current_condition=None, previous_condition=None, previousstate_condition=None): - self._log_debug("Putting delayed action '{}' into queue.", namevar) - self.__queue.put(["delayedaction", self, actionname, namevar, repeat_text, value, current_condition, previous_condition, previousstate_condition]) + def _delayed_execute(self, actionname: str, namevar: str = "", repeat_text: str = "", value=None, current_condition=None, previous_condition=None, previousstate_condition=None, state=None): + if state: + self._log_debug("Putting delayed action '{}' from state '{}' into queue.", namevar, state) + self.__queue.put(["delayedaction", self, actionname, namevar, repeat_text, value, current_condition, previous_condition, previousstate_condition, state]) + else: + self._log_debug("Putting delayed action '{}' into queue.", namevar) + self.__queue.put(["delayedaction", self, actionname, namevar, repeat_text, value, current_condition, previous_condition, previousstate_condition]) if not self._abitem.update_lock.locked(): self._log_debug("Running queue") self._abitem.run_queue() # Really execute the action (needs to be implemented in derived classes) - def real_execute(self, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): + def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): raise NotImplementedError("Class {} doesn't implement real_execute()".format(self.__class__.__name__)) def _getitem_fromeval(self): @@ -369,6 +408,8 @@ class SeActionSetItem(SeActionBase): def __init__(self, abitem, name: str): super().__init__(abitem, name) self.__item = None + self.__status = None + self.__delta = 0 self.__value = StateEngineValue.SeValue(self._abitem, "value") self.__mindelta = StateEngineValue.SeValue(self._abitem, "mindelta") self.__function = "set" @@ -387,25 +428,34 @@ def _getitem_fromeval(self): item = item.replace('shtime', 'self._shtime') item = eval(item) if item is not None: - self.__item = self._abitem.return_item(item) + self.__item, _issue = self._abitem.return_item(item) + _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} self.__value.set_cast(self.__item.cast) self.__mindelta.set_cast(self.__item.cast) self._scheduler_name = "{}-SeItemDelayTimer".format(self.__item.property.path) if self._abitem.id == self.__item.property.path: self._caller += '_self' except Exception as ex: + _issue = {self._name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} raise Exception("Problem evaluating item '{}' from eval: {}".format(self.__item, ex)) + finally: + self.__action_status = _issue if item is None: + _issue = {self._name: {'issue': 'Item {} from eval not existing'.format(self.__item), 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + self.__action_status = _issue raise Exception("Problem evaluating item '{}' from eval. It does not exist.".format(self.__item)) # set the action based on a set_(action_name) attribute # value: Value of the set_(action_name) attribute def update(self, value): - self.__value.set(value) + _, _, _issue = self.__value.set(value) + _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': 'set initital'}]}} + return _issue # Complete action # item_state: state item to read from def complete(self, item_state, evals_items=None): + _issue = {self._name: {'issue': None, 'issueorigin': [{'state': item_state.property.path, 'action': 'set initital'}]}} try: _name = evals_items.get(self.name) if _name is not None: @@ -414,7 +464,8 @@ def complete(self, item_state, evals_items=None): _selfitem = self.__item if self.__item is not None and self.__item != "None" else None _item = _item if _item is not None and _item != "None" else None _eval = _eval if _eval is not None and _eval != "None" else None - self.__item = _selfitem or self._abitem.return_item(_item) or _eval + self.__item = _selfitem or self._abitem.return_item(_item)[0] or _eval + _issue = {self._name: {'issue': self._abitem.return_item(_item)[1], 'issueorigin': [{'state': item_state.property.path, 'action': 'set (first try to get item)'}]}} except Exception as ex: self._log_error("No valid item info for action {}, trying to get differently. Problem: {}", self.name, ex) # missing item in action: Try to find it. @@ -422,24 +473,47 @@ def complete(self, item_state, evals_items=None): item = StateEngineTools.find_attribute(self._sh, item_state, "se_item_" + self._name) if item is not None: - self.__item = self._abitem.return_item(item) + self.__item, _issue = self._abitem.return_item(item) + _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': item_state.property.path, 'action': 'set'}]}} else: item = StateEngineTools.find_attribute(self._sh, item_state, "se_eval_" + self._name) - self.__item = str(item) + if item is not None: + self.__item = str(item) + + if self.__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': 'set'}]}} + # missing status in action: Try to find it. + if self.__status is None: + status = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self._name) + + if status is not None: + self.__status, _issue = self._abitem.return_item(status) + _issue = {self._name: {'issue': 'Item not defined in rules section', 'issueorigin': [{'state': item_state.property.path, 'action': 'set'}]}} if self.__mindelta.is_empty(): mindelta = StateEngineTools.find_attribute(self._sh, item_state, "se_mindelta_" + self._name) if mindelta is not None: self.__mindelta.set(mindelta) - if isinstance(self.__item, str): - pass - elif self.__item is not None: - self.__value.set_cast(self.__item.cast) - self.__mindelta.set_cast(self.__item.cast) - self._scheduler_name = "{}-SeItemDelayTimer".format(self.__item.property.path) - if self._abitem.id == self.__item.property.path: + if self.__status is not None: + self.__value.set_cast(self.__status.cast) + self.__mindelta.set_cast(self.__status.cast) + self._scheduler_name = "{}-SeItemDelayTimer".format(self.__status.property.path) + if self._abitem.id == self.__status.property.path: self._caller += '_self' + elif self.__status is None: + if isinstance(self.__item, str): + pass + elif self.__item is not None: + self.__value.set_cast(self.__item.cast) + self.__mindelta.set_cast(self.__item.cast) + self._scheduler_name = "{}-SeItemDelayTimer".format(self.__item.property.path) + if self._abitem.id == self.__item.property.path: + self._caller += '_self' + if _issue[self._name].get('issue') is not None: + self.__action_status = _issue + self._log_develop("Issue with set action {}", _issue) + return _issue # Write action to logger def write_to_logger(self): @@ -448,23 +522,31 @@ def write_to_logger(self): self._log_debug("item from eval: {0}", self.__item) elif self.__item is not None: self._log_debug("item: {0}", self.__item.property.path) + if self.__status is not None: + self._log_debug("status: {0}", self.__status.property.path) self.__mindelta.write_to_logger() self.__value.write_to_logger() # Check if execution is possible - def _can_execute(self): + def _can_execute(self, state): if self.__item is None: - self._log_info("Action '{0}': No item defined. Ignoring.", self._name) + self._log_increase_indent() + self._log_warning("Action '{0}': No item defined. Ignoring.", self._name) + self._log_decrease_indent() + self.update_webif_actionstatus(state, self._name, 'False', 'No item defined') return False if self.__value.is_empty(): - self._log_info("Action '{0}': No value defined. Ignoring.", self._name) + self._log_increase_indent() + self._log_warning("Action '{0}': No value for item {1} defined. Ignoring.", self._name, self.__item) + self._log_decrease_indent() + self.update_webif_actionstatus(state, self._name, 'False', 'No value for item {}'.format(self.__item)) return False - + self.update_webif_actionstatus(state, self._name, 'True') return True # Really execute the action (needs to be implemented in derived classes) - def real_execute(self, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): + def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): self._abitem.set_variable('current.action_name', namevar) self._log_increase_indent() if value is None: @@ -472,6 +554,8 @@ def real_execute(self, actionname: str, namevar: str = "", repeat_text: str = "" if value is None: self._log_debug("{0}: Value is None", actionname) + pat = "(?:[^,\(]*)\'(.*?)\'" + self.update_webif_actionstatus(state, re.findall(pat, actionname)[0], 'False', 'Value is None') return if returnvalue: @@ -480,31 +564,50 @@ def real_execute(self, actionname: str, namevar: str = "", repeat_text: str = "" if not self.__mindelta.is_empty(): mindelta = self.__mindelta.get() - # noinspection PyCallingNonCallable - delta = float(abs(self.__item() - value)) + if self.__status is not None: + # noinspection PyCallingNonCallable + delta = float(abs(self.__status() - value)) + additionaltext = "of statusitem " + else: + delta = float(abs(self.__item() - value)) + additionaltext = "" + + self.__delta = delta if delta < mindelta: - text = "{0}: Not setting '{1}' to '{2}' because delta '{3:.2}' is lower than mindelta '{4}'" - self._log_debug(text, actionname, self.__item.property.path, value, delta, mindelta) + text = "{0}: Not setting '{1}' to '{2}' because delta {3}'{4:.2}' is lower than mindelta '{5}'" + self._log_debug(text, actionname, self.__item.property.path, value, additionaltext, delta, mindelta) + self.update_webif_actionstatus(state, self._name, 'False') return - self._execute_set_add_remove(actionname, namevar, repeat_text, self.__item, value, current_condition, previous_condition, previousstate_condition) + self._execute_set_add_remove(state, actionname, namevar, repeat_text, self.__item, value, current_condition, previous_condition, previousstate_condition) - def _execute_set_add_remove(self, actionname, namevar, repeat_text, item, value, current_condition, previous_condition, previousstate_condition): + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition, previous_condition, previousstate_condition): 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 = "(?:[^,\(]*)\'(.*?)\'" + self.update_webif_actionstatus(state, re.findall(pat, actionname)[0], 'True') # noinspection PyCallingNonCallable item(value, caller=self._caller, source=source) def get(self): try: - item = str(self.__item.property.path) + _item = str(self.__item.property.path) except Exception: - item = str(self.__item) - return {'function': str(self.__function), 'item': item, - 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), - 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get())} + _item = str(self.__item) + _mindelta = self.__mindelta.get() + if _mindelta is None: + result = {'function': str(self.__function), 'item': _item, + 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + 'previousconditionset': str(self.previousconditionset.get()), + 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} + else: + result = {'function': str(self.__function), 'item': _item, + 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + 'previousconditionset': str(self.previousconditionset.get()), + 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}, + 'delta': str(self.__delta), 'mindelta': str(_mindelta)} + return result # Class representing a single "se_setbyattr" action @@ -524,11 +627,17 @@ def __repr__(self): # value: Value of the set_(action_name) attribute def update(self, value): self.__byattr = value + _issue = {self._name: {'issue': None, 'attribute': self.__byattr, + 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + return _issue # Complete action # item_state: state item to read from def complete(self, item_state, evals_items=None): self._scheduler_name = "{}-SeByAttrDelayTimer".format(self.__byattr) + _issue = {self._name: {'issue': None, 'attribute': self.__byattr, + 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + return _issue # Write action to logger def write_to_logger(self): @@ -538,7 +647,7 @@ def write_to_logger(self): self._log_debug("set by attribute: {0}", self.__byattr) # Really execute the action - def real_execute(self, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): + def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): self._abitem.set_variable('current.action_name', namevar) if returnvalue: return value @@ -549,9 +658,10 @@ def real_execute(self, actionname: str, namevar: str = "", repeat_text: str = "" item(item.conf[self.__byattr], caller=self._caller, source=source) def get(self): - return {'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())} + 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': {}} + return result # Class representing a single "se_trigger" action @@ -574,12 +684,18 @@ def update(self, value): logic, value = StateEngineTools.partition_strip(value, ":") self.__logic = logic value = None if value == "" else value - self.__value.set(value) + _, _, _issue = self.__value.set(value) + _issue = {self._name: {'issue': _issue, 'logic': self.__logic, + 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + return _issue # Complete action # item_state: state item to read from def complete(self, item_state, evals_items=None): self._scheduler_name = "{}-SeLogicDelayTimer".format(self.__logic) + _issue = {self._name: {'issue': None, 'logic': self.__logic, + 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + return _issue # Write action to logger def write_to_logger(self): @@ -591,7 +707,7 @@ def write_to_logger(self): self._log_debug("value: {0}", self.__value) # Really execute the action - def real_execute(self, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): + def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): self._abitem.set_variable('current.action_name', namevar) if value is None: try: @@ -606,11 +722,11 @@ def real_execute(self, actionname: str, namevar: str = "", repeat_text: str = "" self._sh.trigger(add_logics, by=self._caller, source=self._name, value=value) def get(self): - return {'function': str(self.__function), 'logic': str(self.__logic), - 'value': str(self.__value.get()), - 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get())} - + result = {'function': str(self.__function), 'logic': str(self.__logic), + 'value': str(self.__value.get()), + 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), + 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} + return result # Class representing a single "se_run" action class SeActionRun(SeActionBase): @@ -635,11 +751,17 @@ def update(self, value): if func == "eval": self.__eval = value + _issue = {self._name: {'issue': None, 'eval': StateEngineTools.get_eval_name(self.__eval), + 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + return _issue # Complete action # item_state: state item to read from def complete(self, item_state, evals_items=None): self._scheduler_name = "{}-SeRunDelayTimer".format(StateEngineTools.get_eval_name(self.__eval)) + _issue = {self._name: {'issue': None, 'eval': StateEngineTools.get_eval_name(self.__eval), + 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + return _issue # Write action to logger def write_to_logger(self): @@ -649,7 +771,7 @@ def write_to_logger(self): self._log_debug("eval: {0}", StateEngineTools.get_eval_name(self.__eval)) # Really execute the action - def real_execute(self, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): + def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): self._abitem.set_variable('current.action_name', namevar) self._log_increase_indent() if isinstance(self.__eval, str): @@ -694,9 +816,10 @@ def real_execute(self, actionname: str, namevar: str = "", repeat_text: str = "" self._log_error(text.format(actionname, StateEngineTools.get_eval_name(self.__eval), ex)) def get(self): - return {'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())} + 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': {}} + return result # Class representing a single "se_force" action @@ -717,31 +840,58 @@ 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): - self.__value.set(value) + _, _, _issue = self.__value.set(value) + _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': 'force initital'}]}} + return _issue # Complete action # item_state: state item to read from def complete(self, item_state, evals_items=None): + _issue = {self._name: {'issue': None, 'issueorigin': [{'state': item_state.property.path, 'action': 'force initital'}]}} # missing item in action: Try to find it. if self.__item is None: item = StateEngineTools.find_attribute(self._sh, item_state, "se_item_" + self._name) if item is not None: - self.__item = self._abitem.return_item(item) + self.__item, _issue = self._abitem.return_item(item) + _issue = {self._name: {'issue': 'Item not defined in rules section', 'issueorigin': [{'state': item_state.property.path, 'action': 'force'}]}} else: item = StateEngineTools.find_attribute(self._sh, item_state, "se_eval_" + self._name) self.__item = str(item) + # missing status in action: Try to find it. + if self.__status is None: + status = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self._name) + + if status is not None: + self.__status, _issue = self._abitem.return_item(status) + _issue = {self._name: {'issue': 'Item not defined in rules section', 'issueorigin': [{'state': item_state.property.path, 'action': 'force'}]}} + if self.__mindelta.is_empty(): mindelta = StateEngineTools.find_attribute(self._sh, item_state, "se_mindelta_" + self._name) if mindelta is not None: self.__mindelta.set(mindelta) - if isinstance(self.__item, str): - pass - elif self.__item is not None: - self.__value.set_cast(self.__item.cast) - self.__mindelta.set_cast(self.__item.cast) - self._scheduler_name = "{}-SeItemDelayTimer".format(self.__item.property.path) + if self.__item is None and _issue[self._name].get('issue') is None: + _issue = {self._name: {'issue': 'Item not found', 'issueorigin': [{'state': item_state.property.path, 'action': 'force'}]}} + if self.__status is not None: + self.__value.set_cast(self.__status.cast) + self.__mindelta.set_cast(self.__status.cast) + self._scheduler_name = "{}-SeItemDelayTimer".format(self.__status.property.path) + if self._abitem.id == self.__status.property.path: + self._caller += '_self' + elif self.__status is None: + if isinstance(self.__item, str): + pass + elif self.__item is not None: + self.__value.set_cast(self.__item.cast) + self.__mindelta.set_cast(self.__item.cast) + self._scheduler_name = "{}-SeItemDelayTimer".format(self.__item.property.path) + if self._abitem.id == self.__item.property.path: + self._caller += '_self' + if _issue[self._name].get('issue') is not None: + self.__action_status = _issue + self._log_develop("Issue with force action {}", _issue) + return _issue # Write action to logger def write_to_logger(self): @@ -752,24 +902,33 @@ def write_to_logger(self): self._log_debug("item from eval: {0}", self.__item) elif self.__item is not None: self._log_debug("item: {0}", self.__item.property.path) + if self.__status is not None: + self._log_debug("status: {0}", self.__status.property.path) self.__mindelta.write_to_logger() self.__value.write_to_logger() self._log_debug("force update: yes") # Check if execution is possible - def _can_execute(self): + def _can_execute(self, state): if self.__item is None: - self._log_info("Action '{0}': No item defined. Ignoring.", self._name) + self._log_increase_indent() + self._log_warning("Action '{0}': No item defined. Ignoring.", self._name) + self._log_decrease_indent() + self.update_webif_actionstatus(state, self._name, 'False', 'No item defined') return False if self.__value.is_empty(): - self._log_info("Action '{0}': No value defined. Ignoring.", self._name) + self._log_increase_indent() + self._log_warning("Action '{0}': No value defined for item {1}. Ignoring.", self._name, self.__item) + self._log_decrease_indent() + self.update_webif_actionstatus(state, self._name, 'False', 'No value defined for item {}'.format(self.__item)) return False - + self.update_webif_actionstatus(state, self._name, 'True') return True def _getitem_fromeval(self): if isinstance(self.__item, str): + _issue = {self._name: {'issue': None, 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} if "stateengine_eval" in self.__item or "se_eval" in self.__item: # noinspection PyUnusedLocal stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem) @@ -778,7 +937,8 @@ def _getitem_fromeval(self): item = item.replace('shtime', 'self._shtime') item = eval(item) if item is not None: - self.__item = self._abitem.return_item(item) + self.__item, _issue = self._abitem.return_item(item) + _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} self.__value.set_cast(self.__item.cast) self.__mindelta.set_cast(self.__item.cast) self._scheduler_name = "{}-SeItemDelayTimer".format(self.__item.property.path) @@ -786,14 +946,17 @@ def _getitem_fromeval(self): self._caller += '_self' else: self._log_error("Problem evaluating item '{}' from eval. It is None.", item) - return + except Exception as ex: + _issue = {self._name: {'issue': 'Problem evaluating item {} from eval'.format(self.__item), 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} self._log_error("Problem evaluating item '{}' from eval: {}.", self.__item, ex) - return + finally: + self.__action_status = _issue + return _issue # Really execute the action (needs to be implemented in derived classes) # noinspection PyProtectedMember - def real_execute(self, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): + def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): self._abitem.set_variable('current.action_name', namevar) self._log_increase_indent() if value is None: @@ -849,9 +1012,10 @@ def get(self): item = str(self.__item.property.path) except Exception: item = str(self.__item) - return {'function': str(self.__function), 'item': item, 'value': str(self.__value.get()), - 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get())} + result = {'function': str(self.__function), 'item': item, 'value': str(self.__value.get()), + 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), + 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} + return result # Class representing a single "se_special" action @@ -879,6 +1043,8 @@ def update(self, value): else: raise ValueError("Action {0}: Unknown special value '{1}'!".format(self._name, special)) self.__special = special + _issue = {self._name: {'issue': None, 'special': self.__value, 'issueorigin': [{'state': 'unknown', 'action': 'special'}]}} + return _issue # Complete action # item_state: state item to read from @@ -888,6 +1054,8 @@ def complete(self, item_state, evals_items=None): else: item = self.__value.property.path self._scheduler_name = "{}_{}-SeSpecialDelayTimer".format(self.__special, item) + _issue = {self._name: {'issue': None, 'special': item, 'issueorigin': [{'state': 'unknown', 'action': 'special'}]}} + return _issue # Write action to logger def write_to_logger(self): @@ -900,7 +1068,7 @@ def write_to_logger(self): self._log_debug("Retrigger item: {0}", self.__value.property.path) # Really execute the action - def real_execute(self, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): + def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): self._abitem.set_variable('current.action_name', namevar) if returnvalue: return None @@ -912,7 +1080,7 @@ def real_execute(self, actionname: str, namevar: str = "", repeat_text: str = "" actionname, self.__special, _log_value, current_condition, previous_condition, previousstate_condition, repeat_text) self._log_increase_indent() if self.__special == "suspend": - self.suspend_execute(current_condition, previous_condition, previousstate_condition) + self.suspend_execute(state, current_condition, previous_condition, previousstate_condition) self._log_decrease_indent() elif self.__special == "retrigger": # noinspection PyCallingNonCallable @@ -925,36 +1093,54 @@ def real_execute(self, actionname: str, namevar: str = "", repeat_text: str = "" self._log_debug("Special action {0}: done", self.__special) def suspend_get_value(self, value): + _issue = {self._name: {'issue': None, 'issueorigin': [{'state': 'suspend', 'action': 'suspend initital'}]}} if value is None: - raise ValueError("Action {0}: Special action 'suspend' requires arguments!", self._name) + _issue = {self._name: {'issue': 'Special action suspend requires arguments', 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} + self.__action_status = _issue + raise ValueError("Action {0}: Special action 'suspend' requires arguments!".format(self._name)) suspend, manual = StateEngineTools.partition_strip(value, ",") if suspend is None or manual is None: - raise ValueError("Action {0}: Special action 'suspend' requires two arguments (separated by a comma)!", self._name) + _issue = {self._name: {'issue': 'Special action suspend requires two arguments', 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} + self.__action_status = _issue + raise ValueError("Action {0}: Special action 'suspend' requires two arguments (separated by a comma)!".format(self._name)) - suspend_item = self._abitem.return_item(suspend) + suspend_item, _issue = self._abitem.return_item(suspend) + _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} if suspend_item is None: - raise ValueError("Action {0}: Suspend item '{1}' not found!", self._name, suspend) + _issue = {self._name: {'issue': 'Suspend item not found', 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} + self.__action_status = _issue + raise ValueError("Action {0}: Suspend item '{1}' not found!".format(self._name, suspend)) - manual_item = self._abitem.return_item(manual) + manual_item, _issue = self._abitem.return_item(manual) + _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} if manual_item is None: - raise ValueError("Action {0}: Manual item '{1}' not found!", self._name, manual) - + _issue = {self._name: {'issue': 'Manual item {} not found'.format(manual), 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} + self.__action_status = _issue + raise ValueError("Action {0}: Manual item '{1}' not found!".format(self._name, manual)) + self.__action_status = _issue return [suspend_item, manual_item.property.path] def retrigger_get_value(self, value): if value is None: - raise ValueError("Action {0}: Special action 'retrigger' requires item", self._name) + _issue = {self._name: {'issue': 'Special action retrigger requires item', 'issueorigin': [{'state': 'retrigger', 'action': 'retrigger'}]}} + self.__action_status = _issue + raise ValueError("Action {0}: Special action 'retrigger' requires item".format(self._name)) se_item, __ = StateEngineTools.partition_strip(value, ",") - se_item = self._abitem.return_item(se_item) + se_item, _issue = self._abitem.return_item(se_item) + _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'retrigger', 'action': 'retrigger'}]}} + self.__action_status = _issue if se_item is None: - raise ValueError("Action {0}: Retrigger item '{1}' not found!", self._name, se_item) + _issue = {self._name: {'issue': 'Retrigger item {} not found'.format(se_item), 'issueorigin': [{'state': 'retrigger', 'action': 'retrigger'}]}} + self.__action_status = _issue + raise ValueError("Action {0}: Retrigger item '{1}' not found!".format(self._name, se_item)) return se_item - def suspend_execute(self, current_condition=None, previous_condition=None, previousstate_condition=None): - suspend_item = self._abitem.return_item(self.__value[0]) + def suspend_execute(self, state=None, current_condition=None, previous_condition=None, previousstate_condition=None): + suspend_item, _issue = self._abitem.return_item(self.__value[0]) + _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': state.id, 'action': 'suspend'}]}} source = "SuspendAction, {}".format(self.set_source(current_condition, previous_condition, previousstate_condition)) if self._abitem.get_update_trigger_source() == self.__value[1]: # triggered by manual-item: Update suspend item @@ -972,6 +1158,7 @@ def suspend_execute(self, current_condition=None, previous_condition=None, previ suspend_remaining = int(suspend_time - suspend_over + 0.5) # adding 0.5 causes round up ... self._abitem.set_variable("item.suspend_remaining", suspend_remaining) self._log_debug("Updated variable 'item.suspend_remaining' to {0}", suspend_remaining) + self.__action_status = _issue def get(self): try: @@ -984,10 +1171,11 @@ def get(self): value_result[i] = val.property.path except Exception: pass - return {'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())} + 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': {}} + return result # Class representing a single "se_add" action @@ -1007,11 +1195,12 @@ def write_to_logger(self): SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) - def _execute_set_add_remove(self, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None): + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None): value = value if isinstance(value, list) else [value] self._log_debug("{0}: Add '{1}' to '{2}'.{3}", actionname, value, item.property.path, repeat_text) value = item.property.value + value source = self.set_source(current_condition, previous_condition, previousstate_condition) + self.update_webif_actionstatus(state, self._name, 'True') # noinspection PyCallingNonCallable item(value, caller=self._caller, source=source) @@ -1020,10 +1209,11 @@ def get(self): item = str(self.__item.property.path) except Exception: item = str(self.__item) - return {'function': str(self.__function), 'item': item, - 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), - 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get())} + result = {'function': str(self.__function), 'item': item, + 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + 'previousconditionset': str(self.previousconditionset.get()), + 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} + return result # Class representing a single "se_remove" action @@ -1043,7 +1233,7 @@ def write_to_logger(self): SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) - def _execute_set_add_remove(self, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None): + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None): currentvalue = item.property.value value = value if isinstance(value, list) else [value] for v in value: @@ -1055,6 +1245,7 @@ def _execute_set_add_remove(self, actionname, namevar, repeat_text, item, value, self._log_warning("{0}: Remove first entry '{1}' from '{2}' failed: {3}", actionname, value, item.property.path, ex) source = self.set_source(current_condition, previous_condition, previousstate_condition) + self.update_webif_actionstatus(state, self._name, 'True') item(currentvalue, caller=self._caller, source=source) def get(self): @@ -1062,10 +1253,11 @@ def get(self): item = str(self.__item.property.path) except Exception: item = str(self.__item) - return {'function': str(self.__function), 'item': item, - 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), - 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get())} + result = {'function': str(self.__function), 'item': item, + 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + 'previousconditionset': str(self.previousconditionset.get()), + 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} + return result # Class representing a single "se_remove" action @@ -1085,7 +1277,7 @@ def write_to_logger(self): SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) - def _execute_set_add_remove(self, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None): + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None): currentvalue = item.property.value value = value if isinstance(value, list) else [value] for v in value: @@ -1099,6 +1291,7 @@ def _execute_set_add_remove(self, actionname, namevar, repeat_text, item, value, self._log_warning("{0}: Remove last entry '{1}' from '{2}' failed: {3}", actionname, value, item.property.path, ex) source = self.set_source(current_condition, previous_condition, previousstate_condition) + self.update_webif_actionstatus(state, self._name, 'True') item(currentvalue, caller=self._caller, source=source) def get(self): @@ -1106,10 +1299,11 @@ def get(self): item = str(self.__item.property.path) except Exception: item = str(self.__item) - return {'function': str(self.__function), 'item': item, - 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), - 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get())} + result = {'function': str(self.__function), 'item': item, + 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + 'previousconditionset': str(self.previousconditionset.get()), + 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} + return result # Class representing a single "se_removeall" action @@ -1129,7 +1323,7 @@ def write_to_logger(self): SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) - def _execute_set_add_remove(self, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None): + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None): currentvalue = item.property.value value = value if isinstance(value, list) else [value] for v in value: @@ -1141,6 +1335,7 @@ def _execute_set_add_remove(self, actionname, namevar, repeat_text, item, value, self._log_warning("{0}: Remove all '{1}' from '{2}' failed: {3}", actionname, value, item.property.path, ex) source = self.set_source(current_condition, previous_condition, previousstate_condition) + self.update_webif_actionstatus(state, self._name, 'True') item(currentvalue, caller=self._caller, source=source) def get(self): @@ -1148,7 +1343,8 @@ def get(self): item = str(self.__item.property.path) except Exception: item = str(self.__item) - return {'function': str(self.__function), 'item': item, - 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), - 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get())} + result = {'function': str(self.__function), 'item': item, + 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + 'previousconditionset': str(self.previousconditionset.get()), + 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} + return result diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 229c1464f..5e134b467 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -20,6 +20,7 @@ ######################################################################### from . import StateEngineAction from . import StateEngineTools + import ast import threading import queue @@ -27,14 +28,6 @@ # Class representing a list of actions class SeActions(StateEngineTools.SeItemChild): - @property - def dict_actions(self): - result = {} - for name in self.__actions: - self._abitem._initactionname = name - result.update({name: self.__actions[name].get()}) - self._abitem._initactionname = None - return result # Initialize the set of actions # abitem: parent SeItem instance @@ -56,6 +49,21 @@ def __init__(self, abitem): def __repr__(self): return "SeActions, count {}".format(self.count()) + def dict_actions(self, type, state): + result = {} + for name in self.__actions: + 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')}) + except Exception: + pass + self._abitem._initactionname = None + return result + + def reset(self): + self.__actions = {} + # Return number of actions in list def count(self): return len(self.__actions) @@ -67,6 +75,7 @@ def update(self, attribute, value): # Split attribute in function and action name func, name = StateEngineTools.partition_strip(attribute, "_") _count = 0 + _issue = None try: if func == "se_delay": # set delay @@ -133,17 +142,18 @@ def update(self, attribute, value): self.__actions[name].update_order(value) return elif func == "se_action": # and name not in self.__actions: - self.__handle_combined_action_attribute(name, value) + _issue = self.__handle_combined_action_attribute(name, value) _count += 1 elif self.__ensure_action_exists(func, name): # update action - self.__actions[name].update(value) + _issue = self.__actions[name].update(value) _count += 1 except ValueError as ex: if name in self.__actions: del self.__actions[name] + _issue = {name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} self._log_warning("Ignoring action {0} because: {1}", attribute, ex) - return _count + return _count, _issue # ensure that action exists and create if missing # func: action function @@ -279,6 +289,7 @@ def __handle_combined_action_attribute(self, name, value_list): # create action based on function exists = False + _issue = None try: if parameter['function'] == "set": if self.__ensure_action_exists("se_set", name): @@ -342,6 +353,7 @@ def __handle_combined_action_attribute(self, name, value_list): exists = False if name in self.__actions: del self.__actions[name] + _issue = {name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} self._log_warning("Ignoring action {0} because: {1}", name, ex) # add additional parameters @@ -363,6 +375,8 @@ def __handle_combined_action_attribute(self, name, value_list): if parameter['mode'] is not None: self.__actions[name].update_modes(parameter['mode']) + return _issue + # noinspection PyMethodMayBeStatic def __raise_missing_parameter_error(self, parameter, param_name): if param_name not in parameter or parameter[param_name] is None: @@ -372,11 +386,14 @@ def __raise_missing_parameter_error(self, parameter, param_name): # Check the actions optimize and complete them # item_state: item to read from def complete(self, item_state, evals_items=None): + _status = {} for name in self.__actions: try: - self.__actions[name].complete(item_state, evals_items) + _status.update(self.__actions[name].complete(item_state, evals_items)) except ValueError as ex: + _status.update({name: {'issue': ex, 'issueorigin': {'state': item_state.property.path, 'action': 'unknown'}}}) raise ValueError("State '{0}', Action '{1}': {2}".format(item_state.property.path, name, ex)) + return _status def set(self, value): for name in self.__actions: @@ -390,7 +407,7 @@ def set(self, value): # item_allow_repeat: Is repeating actions generally allowed for the item? # state: state item triggering the action # additional_actions: SeActions-Instance containing actions which should be executed, too - def execute(self, is_repeat: bool, allow_item_repeat: bool, state: str, additional_actions=None): + def execute(self, is_repeat: bool, allow_item_repeat: bool, state, additional_actions=None): actions = [] for name in self.__actions: actions.append((self.__actions[name].get_order(), self.__actions[name])) diff --git a/stateengine/StateEngineCondition.py b/stateengine/StateEngineCondition.py index 30b25b7df..e15d2c935 100755 --- a/stateengine/StateEngineCondition.py +++ b/stateengine/StateEngineCondition.py @@ -22,8 +22,10 @@ from . import StateEngineCurrent from . import StateEngineValue from . import StateEngineEval + +from lib.item.item import Item +import datetime import re -from collections import OrderedDict # Class representing a single condition @@ -40,6 +42,7 @@ def __init__(self, abitem, name: str): super().__init__(abitem) self.__name = name self.__item = None + self.__status = None self.__eval = None self.__value = StateEngineValue.SeValue(self._abitem, "value", True) self.__min = StateEngineValue.SeValue(self._abitem, "min") @@ -49,30 +52,44 @@ def __init__(self, abitem, name: str): self.__agemax = StateEngineValue.SeValue(self._abitem, "agemax") self.__changedby = StateEngineValue.SeValue(self._abitem, "changedby", True) self.__updatedby = StateEngineValue.SeValue(self._abitem, "updatedby", True) + self.__triggeredby = StateEngineValue.SeValue(self._abitem, "triggeredby", True) self.__changedbynegate = None self.__updatedbynegate = None + self.__triggeredbynegate = None self.__agenegate = None self.__error = None + self.__itemClass = Item def __repr__(self): - return "SeCondition 'item': {}, 'eval': {}, 'value': {}".format(self.__item, self.__eval, self.__value) + return "SeCondition 'item': {}, 'status': {}, 'eval': {}, 'value': {}".format(self.__item, self.__status, self.__eval, self.__value) # set a certain function to a given value # func: Function to set ('item', 'eval', 'value', 'min', 'max', 'negate', 'changedby', 'updatedby', - # 'changedbynegate', 'updatedbynegate', 'agemin', 'agemax' or 'agenegate') + # 'triggeredby','changedbynegate', 'updatedbynegate', 'triggeredbynegate','agemin', 'agemax' or 'agenegate') # value: Value for function def set(self, func, value): + issue = None if func == "se_item": if ":" in value: self._log_warning("Your item configuration '{0}' is wrong! Define a plain (relative) " "item without item: at the beginning!", value) _, _, value = value.partition(":") - self.__item = self._abitem.return_item(value) - elif func == "se_eval": + self.__item, issue = self._abitem.return_item(value) + elif func == "se_status": if ":" in value: - self._log_warning("Your eval configuration '{0}' is wrong! Define a plain eval " - "term without eval: at the beginning!", value) + self._log_warning("Your status configuration '{0}' is wrong! Define a plain (relative) " + "item without item: at the beginning!", value) _, _, value = value.partition(":") + self.__status, issue = self._abitem.return_item(value) + elif func == "se_eval": + if value.startswith("eval:"): + _, _, value = value.partition("eval:") + wrong_start = ["item:", "regex:", "value:", "var:"] + if any(value.startswith(wrong_start) for wrong_start in wrong_start): + self._log_warning("Your eval configuration '{0}' is wrong! You have to define " + "a plain eval expression", value) + issue = "Your eval configuration '{0}' is wrong!".format(value) + value = None self.__eval = value if func == "se_value": self.__value.set(value, self.__name) @@ -88,35 +105,47 @@ def set(self, func, value): self.__changedby.set(value, self.__name) elif func == "se_updatedby": self.__updatedby.set(value, self.__name) + elif func == "se_triggeredby": + self.__triggeredby.set(value, self.__name) elif func == "se_changedbynegate": self.__changedbynegate = value elif func == "se_updatedbynegate": self.__updatedbynegate = value + elif func == "se_triggeredbynegate": + self.__triggeredbynegate = value elif func == "se_negate": self.__negate = value elif func == "se_agenegate": self.__agenegate = value - elif func != "se_item" and func != "se_eval": + elif func != "se_item" and func != "se_eval" and func != "se_status": self._log_warning("Function '{0}' is no valid function! Please check item attribute.", func) + issue = "Function '{0}' is no valid function!".format(func) + return issue def get(self): - eval_result = str(self.__eval) - if 'SeItem' in eval_result: - eval_result = eval_result.split('SeItem.')[1].split(' ')[0] - if 'SeCurrent' in eval_result: - eval_result = eval_result.split('SeCurrent.')[1].split(' ')[0] + _eval_result = str(self.__eval) + if 'SeItem' in _eval_result: + _eval_result = _eval_result.split('SeItem.')[1].split(' ')[0] + if 'SeCurrent' in _eval_result: + _eval_result = _eval_result.split('SeCurrent.')[1].split(' ')[0] _value_result = str(self.__value.get_for_webif()) try: _item = self.__item.property.path except Exception: _item = self.__item - result = {'item': _item, 'eval': eval_result, 'value': _value_result, + try: + _status = self.__status.property.path + except Exception: + _status = self.__status + result = {'item': _item, 'status': _status, 'eval': _eval_result, 'value': _value_result, 'min': str(self.__min), 'max': str(self.__max), 'agemin': str(self.__agemin), 'agemax': str(self.__agemax), 'negate': str(self.__negate), 'agenegate': str(self.__agenegate), 'changedby': str(self.__changedby), 'updatedby': str(self.__updatedby), + 'triggeredby': str(self.__triggeredby), 'triggeredbynegate': str(self.__triggeredbynegate), 'changedbynegate': str(self.__changedbynegate), - 'updatedbynegate': str(self.__updatedbynegate)} + 'updatedbynegate': str(self.__updatedbynegate), + 'current': {}, 'match': {}} return result # Complete condition (do some checks, cast value, min and max based on item or eval data types) @@ -126,11 +155,11 @@ def complete(self, item_state): # check if it is possible to complete this condition if self.__min.is_empty() and self.__max.is_empty() and self.__value.is_empty() \ and self.__agemin.is_empty() and self.__agemax.is_empty() \ - and self.__changedby.is_empty() and self.__updatedby.is_empty(): + and self.__changedby.is_empty() and self.__updatedby.is_empty() and self.__triggeredby.is_empty(): return False # set 'eval' for some known conditions if item and eval are not set, yet - if self.__item is None and self.__eval is None: + if self.__item is None and self.__status is None and self.__eval is None: if self.__name == "weekday": self.__eval = StateEngineCurrent.values.get_weekday elif self.__name == "sun_azimut": @@ -186,7 +215,13 @@ def complete(self, item_state): if self.__item is None: result = StateEngineTools.find_attribute(self._sh, item_state, "se_item_" + self.__name) if result is not None: - self.__item = self._abitem.return_item(result) + self.__item, issue = self._abitem.return_item(result) + + # missing status in condition: Try to find it + if self.__status is None: + result = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self.__name) + if result is not None: + self.__status, issue = self._abitem.return_item(result) # missing eval in condition: Try to find it if self.__eval is None: @@ -195,20 +230,25 @@ def complete(self, item_state): self.__eval = result # now we should have either 'item' or 'eval' set. If not, raise ValueError - if self.__item is None and self.__eval is None: - raise ValueError("Condition {}: Neither 'item' nor 'eval' given!".format(self.__name)) + if self.__item is None and self.__status is None and self.__eval is None: + raise ValueError("Neither 'item' nor 'status' nor 'eval' given!") - if (self.__item is not None or self.__eval is not None)\ + if (self.__item is not None or self.__status is not None or self.__eval is not None)\ and not self.__changedby.is_empty() and self.__changedbynegate is None: self.__changedbynegate = False - if (self.__item is not None or self.__eval is not None)\ + if (self.__item is not None or self.__status is not None or self.__eval is not None)\ and not self.__updatedby.is_empty() and self.__updatedbynegate is None: self.__updatedbynegate = False + if (self.__item is not None or self.__status is not None or self.__eval is not None)\ + and not self.__triggeredby.is_empty() and self.__triggeredbynegate is None: + self.__triggeredbynegate = False # cast stuff try: if self.__item is not None: self.__cast_all(self.__item.cast) + elif self.__status is not None: + self.__cast_all(self.__status.cast) elif self.__name in ("weekday", "sun_azimut", "sun_altitude", "age", "delay", "random", "month"): self.__cast_all(StateEngineTools.cast_num) elif self.__name in ( @@ -229,28 +269,31 @@ def complete(self, item_state): cond_evalitem = self.__eval and ("get_relative_item(" in self.__eval or "return_item(" in self.__eval) except Exception: cond_evalitem = False - if self.__item is None and not cond_min_max and not cond_evalitem: + if self.__item is None and self.__status is None and not cond_min_max and not cond_evalitem: raise ValueError("Condition {}: 'agemin'/'agemax' can not be used for eval!".format(self.__name)) return True # Check if condition is matching - def check(self): + def check(self, state): # Ignore if no current value can be determined (should not happen as we check this earlier, but to be sure ...) - if self.__item is None and self.__eval is None: - self._log_info("Condition '{0}': No item or eval found! Considering condition as matching!", self.__name) + if self.__item is None and self.__status is None and self.__eval is None: + self._log_info("Condition '{0}': No item, status or eval found! Considering condition as matching!", self.__name) return True self._log_debug("Condition '{0}': Checking all relevant stuff", self.__name) self._log_increase_indent() - if not self.__check_value(): + if not self.__check_value(state): + self._log_decrease_indent() + return False + if not self.__check_updatedby(state): self._log_decrease_indent() return False - if not self.__check_updatedby(): + if not self.__check_triggeredby(state): self._log_decrease_indent() return False - if not self.__check_changedby(): + if not self.__check_changedby(state): self._log_decrease_indent() return False - if not self.__check_age(): + if not self.__check_age(state): self._log_decrease_indent() return False self._log_decrease_indent() @@ -266,6 +309,12 @@ def write_to_logger(self): self._log_info("item: {0} ({1})", self.__name, i.property.path) else: self._log_info("item: {0} ({1})", self.__name, self.__item.property.path) + if self.__status is not None: + if isinstance(self.__status, list): + for i in self.__status: + self._log_info("status item: {0} ({1})", self.__name, i.property.path) + else: + self._log_info("status item: {0} ({1})", self.__name, self.__status.property.path) if self.__eval is not None: if isinstance(self.__item, list): for e in self.__item: @@ -276,17 +325,20 @@ def write_to_logger(self): self.__min.write_to_logger() self.__max.write_to_logger() if self.__negate is not None: - self._log_debug("negate: {0}", self.__negate) + self._log_info("negate: {0}", self.__negate) self.__agemin.write_to_logger() self.__agemax.write_to_logger() if self.__agenegate is not None: - self._log_debug("age negate: {0}", self.__agenegate) + self._log_info("age negate: {0}", self.__agenegate) self.__changedby.write_to_logger() if self.__changedbynegate is not None and not self.__changedby.is_empty(): - self._log_debug("changedby negate: {0}", self.__changedbynegate) + self._log_info("changedby negate: {0}", self.__changedbynegate) self.__updatedby.write_to_logger() if self.__updatedbynegate is not None and not self.__updatedby.is_empty(): - self._log_debug("updatedby negate: {0}", self.__updatedbynegate) + self._log_info("updatedby negate: {0}", self.__updatedbynegate) + self.__triggeredby.write_to_logger() + if self.__updatedbynegate is not None and not self.__triggeredby.is_empty(): + self._log_info("triggeredby negate: {0}", self.__triggeredbynegate) # Cast 'value', 'min' and 'max' using given cast function # cast_func: cast function to use @@ -304,11 +356,17 @@ def __cast_all(self, cast_func): self.__updatedby.set_cast(StateEngineTools.cast_str) if self.__updatedbynegate is not None: self.__updatedbynegate = StateEngineTools.cast_bool(self.__updatedbynegate) + self.__triggeredby.set_cast(StateEngineTools.cast_str) + if self.__triggeredbynegate is not None: + self.__triggeredbynegate = StateEngineTools.cast_bool(self.__triggeredbynegate) if self.__agenegate is not None: self.__agenegate = StateEngineTools.cast_bool(self.__agenegate) - def __change_update_value(self, value, valuetype): + def __change_update_value(self, value, valuetype, state): def __convert(convert_value, convert_current): + if convert_value is None: + self._log_develop("Ignoring value None for conversion") + return convert_value, convert_current _oldvalue = convert_value try: if isinstance(convert_value, re._pattern_type): @@ -317,16 +375,24 @@ def __convert(convert_value, convert_current): if isinstance(convert_value, re.Pattern): return convert_value, convert_current if isinstance(convert_current, bool): + self.__value.set_cast(StateEngineTools.cast_bool) convert_value = StateEngineTools.cast_bool(convert_value) elif isinstance(convert_current, int): + self.__value.set_cast(StateEngineTools.cast_num) convert_value = int(StateEngineTools.cast_num(convert_value)) elif isinstance(convert_current, float): + self.__value.set_cast(StateEngineTools.cast_num) convert_value = StateEngineTools.cast_num(convert_value) * 1.0 elif isinstance(convert_current, list): + self.__value.set_cast(StateEngineTools.cast_list) convert_value = StateEngineTools.cast_list(convert_value) + elif isinstance(convert_current, datetime.time): + self.__value.set_cast(StateEngineTools.cast_time) + convert_value = StateEngineTools.cast_time(convert_value) else: - convert_value = str(convert_value) - convert_current = str(convert_current) + self.__value.set_cast(StateEngineTools.cast_str) + convert_value = StateEngineTools.cast_str(convert_value) + convert_current = StateEngineTools.cast_str(convert_value) if not type(_oldvalue) == type(convert_value): self._log_debug("Value {} was type {} and therefore not the same" " type as item value {}. It got converted to {}.", @@ -335,13 +401,17 @@ def __convert(convert_value, convert_current): current = self.__get_current(eval_type='changedby') if valuetype == "changedby" else\ self.__get_current(eval_type='updatedby') if valuetype == "updatedby" else\ + self.__get_current(eval_type='triggeredby') if valuetype == "triggeredby" else\ self.__get_current(eval_type='value') negate = self.__changedbynegate if valuetype == "changedby" else\ self.__updatedbynegate if valuetype == "updatedby" else\ + self.__triggeredbynegate if valuetype == "triggeredby" else\ self.__negate 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._log_info(text, self.__name, valuetype, value, negate, current) self._log_increase_indent() for i, element in enumerate(value): @@ -361,20 +431,28 @@ def __convert(convert_value, convert_current): 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') 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') 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') 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') return False else: regex_result = None @@ -383,6 +461,8 @@ 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._log_info(text, self.__name, valuetype, value, negate, current) self._log_increase_indent() try: @@ -397,24 +477,30 @@ def __convert(convert_value, convert_current): 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') 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') 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') return False # Check if value conditions match - def __check_value(self): + def __check_value(self, state): try: cond_min_max = self.__min.is_empty() and self.__max.is_empty() if not self.__value.is_empty(): # 'value' is given. We ignore 'min' and 'max' and check only for the given value value = self.__value.get() value = StateEngineTools.flatten_list(value) - return self.__change_update_value(value, "value") + return self.__change_update_value(value, "value", state) elif not cond_min_max: min_get_value = self.__min.get() @@ -423,10 +509,14 @@ def __check_value(self): 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 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 @@ -436,6 +526,8 @@ def __check_value(self): 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._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. " @@ -445,13 +537,15 @@ def __check_value(self): for i, _ in enumerate(min_value): min = None if min_value[i] == 'novalue' else min_value[i] max = None if max_value[i] == 'novalue' else max_value[i] - self._log_debug("Checking minvalue {} and maxvalue {}", min, max) + self._log_debug("Checking minvalue {} ({}) and maxvalue {}({}) against current {}({})", min, type(min), max, type(max), current, type(current)) if min is not None and max is not None and min > max: min, max = max, min self._log_warning("Condition {}: min must not be greater than max! " "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') return True if not self.__negate: @@ -465,6 +559,8 @@ def __check_value(self): 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') return True else: if min is not None and current > min and (max is None or current < max): @@ -477,12 +573,18 @@ def __check_value(self): 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') 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') 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') return True elif self.__value.is_empty() and cond_min_max: @@ -491,21 +593,25 @@ def __check_value(self): " 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.') return True except Exception as ex: - self._log_warning("Problem checking value {}", 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)) finally: self._log_decrease_indent() # Check if changedby conditions match - def __check_changedby(self): + def __check_changedby(self, state): try: if not self.__changedby.is_empty(): # 'changedby' is given. changedby = self.__changedby.get() changedby = StateEngineTools.flatten_list(changedby) - return self.__change_update_value(changedby, "changedby") + return self.__change_update_value(changedby, "changedby", state) else: self._log_increase_indent() @@ -516,14 +622,31 @@ def __check_changedby(self): finally: self._log_decrease_indent() + # Check if triggeredby conditions match + def __check_triggeredby(self, state): + try: + if not self.__triggeredby.is_empty(): + # 'updatedby' is given. + triggeredby = self.__triggeredby.get() + triggeredby = StateEngineTools.flatten_list(triggeredby) + return self.__change_update_value(triggeredby, "triggeredby", state) + else: + self._log_increase_indent() + return True + + except Exception as ex: + self._log_warning("Problem checking triggeredby {}", ex) + finally: + self._log_decrease_indent() + # Check if updatedby conditions match - def __check_updatedby(self): + def __check_updatedby(self, state): try: if not self.__updatedby.is_empty(): # 'updatedby' is given. updatedby = self.__updatedby.get() updatedby = StateEngineTools.flatten_list(updatedby) - return self.__change_update_value(updatedby, "updatedby") + return self.__change_update_value(updatedby, "updatedby", state) else: self._log_increase_indent() return True @@ -534,14 +657,14 @@ def __check_updatedby(self): self._log_decrease_indent() # Check if age conditions match - def __check_age(self): + def __check_age(self, state): # No limits given -> OK if self.__agemin.is_empty() and self.__agemax.is_empty(): self._log_debug("Age of '{0}': No limits given", self.__name) return True # Ignore if no current value can be determined - if self.__item is None and self.__eval is None: + if self.__item is None and self.__status is None and self.__eval is None: self._log_warning("Age of '{0}': No item/eval found! Considering condition as matching!", self.__name) return True @@ -556,8 +679,11 @@ def __check_age(self): 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'] + self._abitem.update_webif(_key, 'Not possible to get age from eval {}'.format(self.__eval)) self._log_warning("Age of '{0}': Not possible to get age from eval {1}! " "Considering condition as matching: {2}", self.__name, self.__eval, ex) + return True agemin = None if self.__agemin.is_empty() else self.__agemin.get() agemax = None if self.__agemax.is_empty() else self.__agemax.get() @@ -571,6 +697,8 @@ def __check_age(self): 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'] + self._abitem.update_webif(_key, str(current)) self._log_info(text, self.__name, agemin, agemax, self.__agenegate, current) if diff_len != 0: self._log_warning("Min and max age are always evaluated as valuepairs." @@ -592,6 +720,8 @@ def __check_age(self): 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): @@ -604,22 +734,39 @@ def __check_age(self): 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: self._log_decrease_indent() # Current value of condition (based on item or eval) def __get_current(self, eval_type='value'): - if self.__item is not None: + if self.__status is not None: + # noinspection PyUnusedLocal + self._log_debug("Trying to get {} of status item {}", eval_type, self.__status) + return self.__status.property.last_change_age if eval_type == 'age' else\ + self.__status.property.last_change_by if eval_type == 'changedby' else\ + self.__status.property.last_update_by if eval_type == 'updatedby' else\ + self.__status.property.last_trigger_by if eval_type == 'triggeredby' else\ + self.__status.property.value + elif self.__item is not None: + # noinspection PyUnusedLocal + self._log_debug("Trying to get {} of item {}", eval_type, self.__item) return self.__item.property.last_change_age if eval_type == 'age' else\ self.__item.property.last_change_by if eval_type == 'changedby' else\ self.__item.property.last_update_by if eval_type == 'updatedby' else\ + self.__item.property.last_trigger_by if eval_type == 'triggeredby' else\ self.__item.property.value if self.__eval is not None: # noinspection PyUnusedLocal @@ -632,7 +779,14 @@ def __get_current(self, eval_type='value'): # noinspection PyUnusedLocal stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem) try: - value = eval(self.__eval).property.value if eval_type == 'value' else eval(self.__eval).property.last_change_age + if isinstance(eval(self.__eval), self.__itemClass): + value = eval(self.__eval).property.last_change_age if eval_type == 'age' else \ + eval(self.__eval).property.last_change_by if eval_type == 'changedby' else \ + eval(self.__eval).property.last_update_by if eval_type == 'updatedby' else \ + eval(self.__eval).property.last_trigger_by if eval_type == 'triggeredby' else \ + eval(self.__eval).property.value + else: + value = eval(self.__eval) except Exception as ex: text = "Condition {}: problem evaluating {}: {}" raise ValueError(text.format(self.__name, self.__eval, ex)) @@ -640,4 +794,4 @@ def __get_current(self, eval_type='value'): return value else: return self.__eval() - raise ValueError("Condition {}: Neither 'item' nor eval given!".format(self.__name)) + raise ValueError("Condition {}: Neither 'item' nor 'status' nor 'eval' given!".format(self.__name)) diff --git a/stateengine/StateEngineConditionSet.py b/stateengine/StateEngineConditionSet.py index a3a29aaca..256c2379b 100755 --- a/stateengine/StateEngineConditionSet.py +++ b/stateengine/StateEngineConditionSet.py @@ -20,6 +20,7 @@ ######################################################################### from . import StateEngineCondition from . import StateEngineTools + import collections.abc from collections import OrderedDict @@ -44,6 +45,16 @@ def conditions(self): def evals_items(self): return self.__evals_items + # Return orphaned definitions + @property + def unused_attributes(self): + return self.__unused_attributes + + # Return used definitions + @property + def used_attributes(self): + return self.__used_attributes + @property def dict_conditions(self): result = OrderedDict() @@ -61,6 +72,8 @@ def __init__(self, abitem, name, conditionid): self.__id = conditionid self.__conditions = OrderedDict() self.__evals_items = {} + self.__unused_attributes = {} + self.__used_attributes = {} def __repr__(self): return "SeConditionSet {}".format(self.__conditions) @@ -73,7 +86,6 @@ def update(self, item, grandparent_item): if isinstance(item, collections.abc.Mapping) or isinstance(grandparent_item, collections.abc.Mapping): self._log_error("Condition '{0}' item or parent is a dictionary. Something went wrong!", item) return - if item is not None: for attribute in item.conf: func, name = StateEngineTools.partition_strip(attribute, "_") @@ -83,28 +95,44 @@ def update(self, item, grandparent_item): # update this condition if name not in self.__conditions: self.__conditions[name] = StateEngineCondition.SeCondition(self._abitem, name) - self.__conditions[name].set(func, item.conf[attribute]) + issue = self.__conditions[name].set(func, item.conf[attribute]) self.__conditions.move_to_end(name, last=True) + if issue: + self.__unused_attributes.update({name: {'attribute': attribute, 'issue': issue}}) + elif name not in self.__used_attributes.keys(): + self.__used_attributes.update({name: {'attribute': attribute}}) except ValueError as ex: + self.__unused_attributes.update({name: {'attribute': attribute, 'issue': ex}}) raise ValueError("Condition {0} error: {1}".format(name, ex)) # Update item from grandparent_item for attribute in grandparent_item.conf: func, name = StateEngineTools.partition_strip(attribute, "_") + if name == "": continue + cond1 = name not in self.__used_attributes.keys() + cond2 = func == "se_item" or func == "se_eval" or func == "se_status" + cond3 = name not in self.__unused_attributes.keys() + if cond1: + if cond2 and cond3: + self.__unused_attributes.update({name: {'attribute': attribute}}) + continue # update item/eval in this condition - - if func == "se_item" or func == "se_eval": + if cond2: if name not in self.__conditions: self.__conditions[name] = StateEngineCondition.SeCondition(self._abitem, name) try: - self.__conditions[name].set(func, grandparent_item.conf[attribute]) + issue = self.__conditions[name].set(func, grandparent_item.conf[attribute]) + if issue: + self.__unused_attributes.update({name: {'attribute': attribute, 'issue': issue}}) except ValueError as ex: + self.__unused_attributes.update({name: {'attribute': attribute, 'issue': ex}}) text = "Item '{0}', Attribute '{1}' Error: {2}" raise ValueError(text.format(grandparent_item.property.path, attribute, ex)) self.__evals_items.update({name: self.__conditions[name].get()}) + return self.__unused_attributes, self.__used_attributes # Check the condition set, optimize and complete it # item_state: item to read from @@ -117,6 +145,9 @@ def complete(self, item_state): conditions_to_remove.append(name) continue except ValueError as ex: + self._abitem.update_attributes(self.__unused_attributes, self.__used_attributes) + self._abitem.update_issues('state', {item_state.property.path: {'issue': ex, 'issueorigin': + [{'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)) @@ -142,7 +173,7 @@ def __previousconditionset_set(self, conditionsetid, name): # Check all conditions in the condition set. Return # returns: True = all conditions in set are matching, False = at least one condition is not matching - def all_conditions_matching(self): + def all_conditions_matching(self, state): try: self._log_info("Check condition set '{0}'", self.__name) self._log_increase_indent() @@ -150,7 +181,7 @@ def all_conditions_matching(self): self.__currentconditionset_set(self.__id.property.path, self.__name) for name in self.__conditions: - if not self.__conditions[name].check(): + 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')) diff --git a/stateengine/StateEngineConditionSets.py b/stateengine/StateEngineConditionSets.py index 33c603e55..e7dc7b7e7 100755 --- a/stateengine/StateEngineConditionSets.py +++ b/stateengine/StateEngineConditionSets.py @@ -20,6 +20,7 @@ ######################################################################### from . import StateEngineConditionSet from . import StateEngineTools + from collections import OrderedDict @@ -42,6 +43,9 @@ def __init__(self, abitem): def __repr__(self): return "{}".format(self.get()) + def reset(self): + self.__condition_sets = OrderedDict() + # Return number of condition sets in list def count(self): return len(self.__condition_sets) @@ -61,7 +65,10 @@ def update(self, name, item, grandparent_item): if name not in self.__condition_sets: self.__condition_sets[name] = StateEngineConditionSet.SeConditionSet(self._abitem, name, item) # Update this condition set + self._log_develop("Starting update of condition '{0}'.", name) self.__condition_sets[name].update(item, grandparent_item) + self._log_develop("Finished update of condition '{0}'.", name) + 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 @@ -79,12 +86,12 @@ def write_to_logger(self): # check if one of the conditions sets in the list is matching. # returns: True = one condition set is matching or no condition sets are defined, False: no condition set matching - def one_conditionset_matching(self): + def one_conditionset_matching(self, state): if self.count() == 0: self._log_debug("No condition sets defined -> matching") return True for name in self.__condition_sets: - if self.__condition_sets[name].all_conditions_matching(): + if self.__condition_sets[name].all_conditions_matching(state): return True return False diff --git a/stateengine/StateEngineDefaults.py b/stateengine/StateEngineDefaults.py index 5dc52c8f0..5486edeca 100755 --- a/stateengine/StateEngineDefaults.py +++ b/stateengine/StateEngineDefaults.py @@ -26,28 +26,19 @@ suspend_time = 3600 -log_level = 0 - suntracking_offset = 0 lamella_open_value = 0 -instant_leaveaction = False - plugin_identification = "StateEngine Plugin" VERBOSE = logging.DEBUG - 1 logger = None -se_logger = logging.getLogger('stateengine') - -log_maxage = 0 - def write_to_log(logger): logger.info("StateEngine default suntracking offset = {0}".format(suntracking_offset)) logger.info("StateEngine default suntracking lamella open value = {0}".format(lamella_open_value)) logger.info("StateEngine default startup delay = {0}".format(startup_delay)) logger.info("StateEngine default suspension time = {0}".format(suspend_time)) - logger.info("StateEngine default instant_leaveaction = {0}".format(instant_leaveaction)) diff --git a/stateengine/StateEngineEval.py b/stateengine/StateEngineEval.py index a3a699b10..ffbc18f70 100755 --- a/stateengine/StateEngineEval.py +++ b/stateengine/StateEngineEval.py @@ -42,12 +42,12 @@ def __repr__(self): # Get lamella angle based on sun_altitude for sun tracking def sun_tracking(self, offset=None): - def reMap(_value, _minoutput): + def remap(_value, _minoutput): _value = 100 if _value > 100 else _value _value = 0 if _value < 0 else _value - outputSpan = 100 - _minoutput - scaledThrust = float(_value) / float(100) - return _minoutput + (scaledThrust * outputSpan) + output_span = 100 - _minoutput + scaled_thrust = float(_value) / float(100) + return _minoutput + (scaled_thrust * output_span) if offset is None: offset = StateEngineDefaults.suntracking_offset @@ -65,7 +65,7 @@ def reMap(_value, _minoutput): self._log_debug("Current sun altitude is {0:.2f}°", altitude) _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 + 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_decrease_indent() @@ -116,10 +116,10 @@ def get_relative_itemid(self, subitem_id): 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).property.path + 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).property.path + returnvalue = self._abitem.return_item(subitem_id)[0].property.path self._log_debug("Return item path '{0}'", returnvalue) except Exception as ex: returnvalue = None @@ -137,10 +137,10 @@ def get_relative_item(self, subitem_id): self._log_debug("Executing method 'get_relative_item({0})'", subitem_id) try: if self._abitem._initstate and subitem_id == '..state_name': - returnvalue = self._abitem.return_item(self._abitem._initstate.id) + returnvalue, issue = self._abitem.return_item(self._abitem._initstate.id) self._log_debug("Return item '{0}' during init", returnvalue) else: - returnvalue = self._abitem.return_item(subitem_id) + returnvalue, issue = self._abitem.return_item(subitem_id) self._log_debug("Return item '{0}'", returnvalue) except Exception as ex: returnvalue = None @@ -161,7 +161,7 @@ def get_relative_itemvalue(self, subitem_id): returnvalue = self._abitem._initstate.text self._log_debug("Return item value '{0}' during init", returnvalue) else: - item = self._abitem.return_item(subitem_id) + item, issue = self._abitem.return_item(subitem_id) returnvalue = item.property.value self._log_debug("Return item value '{0}' for item {1}", returnvalue, item.property.path) except Exception as ex: @@ -180,16 +180,16 @@ 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 = self._abitem.return_item(subitem_id) + item, issue = self._abitem.return_item(subitem_id) except Exception as ex: self._log_warning("Problem evaluating property of {0} - relative item might not exist. Error: {1}", subitem_id, ex) self._eval_lock.release() return try: if self._abitem._initstate and subitem_id == '..state_name': - returnvalue = getattr(self._abitem.return_item(self._abitem._initstate.id).property, prop) + 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).property.path, returnvalue) + self._abitem.return_item(self._abitem._initstate.id)[0].property.path, returnvalue) else: returnvalue = getattr(item.property, prop) self._log_debug("Return item property {0} from {1}: {2}", prop, item.property.path, returnvalue) @@ -201,6 +201,12 @@ def get_relative_itemproperty(self, subitem_id, prop): self._eval_lock.release() return returnvalue + # Alias for get_attributevalue + # item: can be a (relative) item or a stateengine variable + # attrib: name of attribute, can actually be any attribute name you can think of ;) + def get_attribute_value(self, item, attrib): + self.get_attributevalue(item, attrib) + # Return an attribute of the current state declaration # item: can be a (relative) item or a stateengine variable # attrib: name of attribute, can actually be any attribute name you can think of ;) @@ -211,14 +217,15 @@ def get_attributevalue(self, item, attrib): self._log_debug("Executing method 'get_attributevalue({0}, {1})'", item, attrib) if ":" in item: var_type, item = StateEngineTools.partition_strip(item, ":") - item = self._abitem.return_item(self._abitem.get_variable(item)) if var_type == "var" else item + if var_type == "var": + item, issue = self._abitem.return_item(self._abitem.get_variable(item)) else: - item = self._abitem.return_item(item) + 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).property.path, returnvalue) + attrib, self._abitem.return_item(self._abitem._initstate.id)[0].property.path, returnvalue) else: returnvalue = item.conf[attrib] self._log_debug("Return item attribute {0} from {1}: {2}", attrib, item.property.path, returnvalue) @@ -240,9 +247,9 @@ def insert_suspend_time(self, suspend_item_id, suspend_text="Ausgesetzt bis %X") self._log_debug("Executing method 'insert_suspend_time({0}, {1})'", suspend_item_id, suspend_text) self._log_increase_indent() try: - suspend_time = self._abitem.get_variable("item.suspend_time") + suspend_time = self._abitem.get_variable("item.suspend_time") or 0 self._log_debug("Suspend time is {0}", suspend_time) - suspend_item = self._abitem.return_item(suspend_item_id) + suspend_item, issue = self._abitem.return_item(suspend_item_id) if suspend_item is None: text = "Eval-Method 'insert_suspend_time': Suspend Item {0} not found!" self._eval_lock.release() @@ -253,7 +260,7 @@ def insert_suspend_time(self, suspend_item_id, suspend_text="Ausgesetzt bis %X") suspend_remaining = suspend_time - suspend_over self._log_debug("Remaining suspend time: {0}", suspend_remaining) if suspend_remaining < 0: - self._log_debug("Eval-Method 'insert_suspend_time': Suspend should already be finished!") + self._log_debug("Eval-Method 'insert_suspend_time': Suspend time already over.") self._eval_lock.release() return "Suspend already over." suspend_until = self._abitem.shtime.now() + datetime.timedelta(seconds=suspend_remaining) diff --git a/stateengine/StateEngineFunctions.py b/stateengine/StateEngineFunctions.py index 8d015288c..6b90c0822 100755 --- a/stateengine/StateEngineFunctions.py +++ b/stateengine/StateEngineFunctions.py @@ -37,7 +37,7 @@ def ab_alive(self): def ab_alive(self, value): self.__ab_alive = value - def __init__(self, smarthome, logger): + def __init__(self, smarthome=None, logger=None): self.logger = logger self.__sh = smarthome self.__locks = {} @@ -66,7 +66,7 @@ def __get_lock(self, lock_id): def manual_item_update_eval(self, item_id, caller=None, source=None): item = self.itemsApi.return_item(item_id) if item is None: - self.logger.error("manual_item_update_eval: item {0} not found!", item_id) + self.logger.error("manual_item_update_eval: item {0} not found!".format(item_id)) # Leave immediately in case StateEngine Plugin is not yet fully running if not self.__ab_alive: @@ -80,7 +80,7 @@ def manual_item_update_eval(self, item_id, caller=None, source=None): elog_item_id = item.conf["se_manual_logitem"] elog_item = self.itemsApi.return_item(elog_item_id) if elog_item is None: - self.logger.error("manual_item_update_item: se_manual_logitem {0} not found!", elog_item_id) + self.logger.error("manual_item_update_item: se_manual_logitem {0} not found!".format(elog_item_id)) elog = StateEngineLogger.SeLoggerDummy() else: elog = StateEngineLogger.SeLogger.create(elog_item) diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 65adfccec..3cd77f8f7 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -20,7 +20,9 @@ # along with this plugin. If not, see . ######################################################################### import datetime -from collections import OrderedDict +from collections import OrderedDict, defaultdict + +import lib.item.item from . import StateEngineTools from .StateEngineLogger import SeLogger from . import StateEngineState @@ -29,11 +31,14 @@ from . import StateEngineValue from . import StateEngineStruct from . import StateEngineStructs + from lib.item import Items from lib.shtime import Shtime from lib.item.item import Item +import copy import threading import queue +import re # Class representing a blind item @@ -44,10 +49,6 @@ class SeItem: def id(self): return self.__id - @property - def log_level(self): - return self.__log_level - @property def variables(self): return self.__variables @@ -94,45 +95,63 @@ def logger(self): def instant_leaveaction(self): return self.__instant_leaveaction + @property + def default_instant_leaveaction(self): + return self.__default_instant_leaveaction.get() + + @default_instant_leaveaction.setter + def default_instant_leaveaction(self, value): + self.__default_instant_leaveaction = value + @property def laststate(self): - return self.__laststate_item_id.property.value + _returnvalue = None if self.__laststate_item_id is None else self.__laststate_item_id.property.value + return _returnvalue @property def previousstate(self): - return self.__previousstate_item_id.property.value + _returnvalue = None if self.__previousstate_item_id is None else self.__previousstate_item_id.property.value + return _returnvalue @property def lastconditionset(self): - return self.__lastconditionset_item_id.property.value + _returnvalue = None if self.__lastconditionset_item_id is None else self.__lastconditionset_item_id.property.value + return _returnvalue @property def previousconditionset(self): - return self.__previousconditionset_item_id.property.value + _returnvalue = None if self.__previousconditionset_item_id is None else self.__previousconditionset_item_id.property.value + return _returnvalue @property def previousstate_conditionset(self): - return self.__previousstate_conditionset_item_id.property.value + _returnvalue = None if self.__previousstate_conditionset_item_id is None else self.__previousstate_conditionset_item_id.property.value + return _returnvalue @property def laststate_name(self): - return self.__laststate_item_name.property.value + _returnvalue = None if self.__laststate_item_name is None else self.__laststate_item_name.property.value + return _returnvalue @property def previousstate_name(self): - return self.__previousstate_item_name.property.value + _returnvalue = None if self.__previousstate_item_name is None else self.__previousstate_item_name.property.value + return _returnvalue @property def lastconditionset_name(self): - return self.__lastconditionset_item_name.property.value + _returnvalue = None if self.__lastconditionset_item_name is None else self.__lastconditionset_item_name.property.value + return _returnvalue @property def previousconditionset_name(self): - return self.__previousconditionset_item_name.property.value + _returnvalue = None if self.__previousconditionset_item_name is None else self.__previousconditionset_item_name.property.value + return _returnvalue @property def previousstate_conditionset_name(self): - return self.__previousstate_conditionset_item_name.property.value + _returnvalue = None if self.__previousstate_conditionset_item_name is None else self.__previousstate_conditionset_item_name.property.value + return _returnvalue @property def ab_alive(self): @@ -157,7 +176,8 @@ def __init__(self, smarthome, item, se_plugin): self.__shtime = Shtime.get_instance() self.__se_plugin = se_plugin self.__active_schedulers = [] - self.__all_releasedby = {} + self.__default_instant_leaveaction = StateEngineValue.SeValue(self, "Default Instant Leave Action", False, + "bool") #self.__all_torelease = {} try: self.__id = self.__item.property.path @@ -166,13 +186,40 @@ def __init__(self, smarthome, item, se_plugin): self.__name = str(self.__item) self.__itemClass = Item # initialize logging - - self.__log_level = StateEngineValue.SeValue(self, "Log Level", False, "num") - self.__log_level.set_from_attr(self.__item, "se_log_level", StateEngineDefaults.log_level) self.__logger.header("") - self.__logger.header("Initialize Item {0} (Log Level set" - " to {1})".format(self.id, self.__log_level)) - self.__logger.override_loglevel(self.__log_level, self.__item) + _log_level = StateEngineValue.SeValue(self, "Log Level", False, "num") + _default_log_level = SeLogger.default_log_level.get() + _returnvalue, _returntype, _using_default, _issue = _log_level.set_from_attr(self.__item, "se_log_level", + _default_log_level) + + if len(_returnvalue) > 1: + self.__logger.warning("se_log_level for item {} can not be defined as a list" + " ({}). Using default value {}.", self.id, _returnvalue, _default_log_level) + _log_level.set(_default_log_level) + elif len(_returnvalue) == 1 and _returnvalue[0] is None: + _log_level.set(_default_log_level) + self.__logger.header("Initialize Item {0} (Log {1}, Level set" + " to {2} based on default log level {3}" + " because se_log_level has issues)".format(self.id, self.__logger.name, _log_level, + _default_log_level)) + elif _using_default: + self.__logger.header("Initialize Item {0} (Log {1}, Level set" + " to {2} based on default log level {3})".format(self.id, self.__logger.name, + _log_level, + _default_log_level)) + else: + self.__logger.header("Initialize Item {0} (Log {1}, Level set" + " to {2}, default log level is {3})".format(self.id, self.__logger.name, _log_level, + _default_log_level)) + _startup_log_level = SeLogger.startup_log_level.get() + self.__logger.log_level.set(_startup_log_level) + self.__logger.info("Set log level to startup log level {}", _startup_log_level) + if _startup_log_level > 0: + base = self.__sh.get_basedir() + SeLogger.manage_logdirectory(base, SeLogger.log_directory, True) + self.__log_level = _log_level + self.__instant_leaveaction = StateEngineValue.SeValue(self, "Instant Leave Action", False, "num") + # get startup delay self.__startup_delay = StateEngineValue.SeValue(self, "Startup Delay", False, "num") self.__startup_delay.set_from_attr(self.__item, "se_startup_delay", StateEngineDefaults.startup_delay) @@ -180,55 +227,67 @@ def __init__(self, smarthome, item, se_plugin): # Init suspend settings self.__suspend_time = StateEngineValue.SeValue(self, "Suspension time on manual changes", False, "num") - self.__suspend_time.set_from_attr(self.__item, "se_suspend_time", StateEngineDefaults.suspend_time) + self.__suspend_time.set_from_attr(self.__item, "se_suspend_time", StateEngineDefaults.suspend_time.get()) # Init laststate and previousstate items/values - self.__laststate_item_id = self.return_item_by_attribute("se_laststate_item_id") + self.__config_issues = {} + self.__laststate_item_id, _issue = self.return_item_by_attribute("se_laststate_item_id") self.__laststate_internal_id = "" if self.__laststate_item_id is None else self.__laststate_item_id.property.value - self.__laststate_item_name = self.return_item_by_attribute("se_laststate_item_name") + self.__config_issues.update(_issue) + self.__laststate_item_name, _issue = self.return_item_by_attribute("se_laststate_item_name") self.__laststate_internal_name = "" if self.__laststate_item_name is None else self.__laststate_item_name.property.value - self.__previousstate_item_id = self.return_item_by_attribute("se_previousstate_item_id") + self.__config_issues.update(_issue) + self.__previousstate_item_id, _issue = self.return_item_by_attribute("se_previousstate_item_id") self.__previousstate_internal_id = "" if self.__previousstate_item_id is None else self.__previousstate_item_id.property.value - self.__previousstate_item_name = self.return_item_by_attribute("se_previousstate_item_name") + self.__config_issues.update(_issue) + self.__previousstate_item_name, _issue = self.return_item_by_attribute("se_previousstate_item_name") self.__previousstate_internal_name = "" if self.__previousstate_item_name is None else self.__previousstate_item_name.property.value - - # Init releasedby items/values - self.___shouldnotrelease_item = self.return_item_by_attribute("se_shouldnotrelease_item") - self.__hasreleased_item = self.return_item_by_attribute("se_hasreleased_item") - self.__has_released = {} if self.__hasreleased_item is None else self.__hasreleased_item.property.value - self.__logger.develop("has released = {}", self.__has_released) - self.__should_not_release = {} if self.___shouldnotrelease_item is None else self.___shouldnotrelease_item.property.value + self.__config_issues.update(_issue) # Init lastconditionset items/values - self.__lastconditionset_item_id = self.return_item_by_attribute("se_lastconditionset_item_id") + self.__lastconditionset_item_id, _issue = self.return_item_by_attribute("se_lastconditionset_item_id") self.__lastconditionset_internal_id = "" if self.__lastconditionset_item_id is None else \ self.__lastconditionset_item_id.property.value - self.__lastconditionset_item_name = self.return_item_by_attribute("se_lastconditionset_item_name") + self.__config_issues.update(_issue) + self.__lastconditionset_item_name, _issue = self.return_item_by_attribute("se_lastconditionset_item_name") self.__lastconditionset_internal_name = "" if self.__lastconditionset_item_name is None else \ self.__lastconditionset_item_name.property.value + self.__config_issues.update(_issue) # Init previousconditionset items/values - self.__previousconditionset_item_id = self.return_item_by_attribute("se_previousconditionset_item_id") + self.__previousconditionset_item_id, _issue = self.return_item_by_attribute("se_previousconditionset_item_id") self.__previousconditionset_internal_id = "" if self.__previousconditionset_item_id is None else \ self.__previousconditionset_item_id.property.value - self.__previousconditionset_item_name = self.return_item_by_attribute("se_previousconditionset_item_name") + self.__config_issues.update(_issue) + self.__previousconditionset_item_name, _issue = self.return_item_by_attribute("se_previousconditionset_item_name") self.__previousconditionset_internal_name = "" if self.__previousconditionset_item_name is None else \ self.__previousconditionset_item_name.property.value + self.__config_issues.update(_issue) - self.__previousstate_conditionset_item_id = self.return_item_by_attribute("se_previousstate_conditionset_item_id") + self.__previousstate_conditionset_item_id, _issue = self.return_item_by_attribute( + "se_previousstate_conditionset_item_id") self.__previousstate_conditionset_internal_id = "" if self.__previousstate_conditionset_item_id is None else \ self.__previousstate_conditionset_item_id.property.value - self.__previousstate_conditionset_item_name = self.return_item_by_attribute("se_previousstate_conditionset_item_name") + self.__config_issues.update(_issue) + self.__previousstate_conditionset_item_name, _issue = self.return_item_by_attribute( + "se_previousstate_conditionset_item_name") 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') is not None} + self.__config_issues = filtered_dict self.__states = [] self.__state_ids = {} self.__conditionsets = {} self.__templates = {} + self.__unused_attributes = {} + self.__used_attributes = {} + self.__action_status = {} + self.__state_issues = {} + self.__struct_issues = {} self.__webif_infos = OrderedDict() - self.__instant_leaveaction = StateEngineValue.SeValue(self, "Instant Leave Action", False, "bool") - self.__instant_leaveaction.set_from_attr(self.__item, "se_instant_leaveaction", StateEngineDefaults.instant_leaveaction) + 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) @@ -241,6 +300,8 @@ def __init__(self, smarthome, item, se_plugin): self.__update_original_item = None self.__update_original_caller = None self.__update_original_source = None + self.__using_default_instant_leaveaction = False + self.__using_default_suspendtime = False # Check item configuration self.__check_item_config() @@ -249,7 +310,12 @@ def __init__(self, smarthome, item, se_plugin): self.__variables = { "item.suspend_time": self.__suspend_time.get(), "item.suspend_remaining": 0, - "item.instant_leaveaction": self.__instant_leaveaction.get(), + "item.instant_leaveaction": 0, + "release.can_release": "", + "release.can_be_released_by": "", + "release.has_released": "", + "release.was_released_by": "", + "release.will_release": "", "current.state_id": "", "current.state_name": "", "current.conditionset_id": "", @@ -262,28 +328,25 @@ def __init__(self, smarthome, item, se_plugin): "previous.state_conditionset_id": "", "previous.state_conditionset_name": "" } + try: + _statecount = 0 + for item_state in self.__item.return_children(): + self.__initialize_state(item_state, _statecount) + except Exception as ex: + self.__logger.error("Ignoring stateevaluation for {} because {}", self.__id, ex) - # initialize states - for item_state in self.__item.return_children(): - try: - _state = StateEngineState.SeState(self, item_state) - self.__states.append(_state) - self.__state_ids.update({item_state.property.path: _state}) - except ValueError as ex: - self.__logger.error("Ignoring state {0} because: {1}".format(item_state.property.path, ex)) + try: + self.__finish_states() + except Exception as ex: + self.__logger.error("Issue finishing states because {}", ex) + return - if len(self.__states) == 0: - raise ValueError("{0}: No states defined!".format(self.id)) - # Write settings to log - self.__write_to_log() - try: - self.__has_released.pop('initial') - except Exception: - pass - self.__logger.develop("ALL RELEASEDBY: {}", self.__all_releasedby) - self.__logger.develop("HAS RELEASED: {}", self.__has_released) + def __repr__(self): + return self.__id + def startup(self): + self.__logger.info("".ljust(80, "_")) # start timer with startup-delay _startup_delay_param = self.__startup_delay.get() startup_delay = 1 if self.__startup_delay.is_empty() or _startup_delay_param == 0 else _startup_delay_param @@ -298,9 +361,54 @@ def __init__(self, smarthome, item, se_plugin): self.__add_triggers() else: self.__startup_delay_callback(self.__item, "Init", None, None) + self.__logger.info("Reset log level to {}", self.__log_level) + self.__logger.log_level = self.__log_level - def __repr__(self): - return self.__id + def show_issues_summary(self): + # show issues summary + filtered_dict = {key: value for key, value in self.__unused_attributes.items() if + key not in self.__used_attributes or 'issue' in value.keys()} + self.__unused_attributes = filtered_dict + + self.__logger.info("".ljust(80, "_")) + #filtered_dict = {key: value for key, value in self.__action_status.items() if value.get('issue') is not None} + #self.__action_status = filtered_dict + if self.__config_issues: + self.__log_issues('config entries') + if self.__unused_attributes: + self.__log_issues('attributes') + if self.__action_status: + self.__log_issues('actions') + if self.__state_issues: + self.__log_issues('states') + if self.__struct_issues: + self.__log_issues('structs') + + def update_leave_action(self, default_instant_leaveaction): + self.__default_instant_leaveaction = default_instant_leaveaction + + _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: + self.__logger.warning("se_instant_leaveaction for item {} can not be defined as a list" + " ({}). Using default value {}.", self.id, _returnvalue_leave, default_instant_leaveaction) + self.__instant_leaveaction.set(default_instant_leaveaction) + self.__variables.update({"item.instant_leaveaction": default_instant_leaveaction}) + elif len(_returnvalue_leave) == 1 and _returnvalue_leave[0] is None: + self.__instant_leaveaction.set(default_instant_leaveaction) + self.__variables.update({"item.instant_leaveaction": default_instant_leaveaction}) + self.__logger.info("Using default instant_leaveaction {0} " + "as no se_instant_leaveaction is set.".format(default_instant_leaveaction)) + elif _using_default_leave: + self.__variables.update({"item.instant_leaveaction": default_instant_leaveaction}) + self.__logger.info("Using default instant_leaveaction {0} " + "as no se_instant_leaveaction is set.".format(default_instant_leaveaction)) + else: + self.__variables.update({"item.instant_leaveaction": _returnvalue_leave}) + self.__logger.info("Using instant_leaveaction {0} " + "from attribute se_instant_leaveaction. " + "Default value is {1}".format(_returnvalue_leave, default_instant_leaveaction)) def updatetemplates(self, template, value): if value is None: @@ -323,30 +431,59 @@ def remove_all_schedulers(self): # run queue def run_queue(self): if not self.__ab_alive: - self.__logger.debug("{} not running (anymore). Queue not activated.", StateEngineDefaults.plugin_identification) + self.__logger.debug("{} not running (anymore). Queue not activated.", + StateEngineDefaults.plugin_identification) return + _current_log_level = self.__logger.get_loglevel() + _default_log_level = SeLogger.default_log_level.get() + if _current_log_level > 0: + base = self.__sh.get_basedir() + SeLogger.manage_logdirectory(base, SeLogger.log_directory, True) + self.__logger.debug("Current log level {}, default {}, currently using default {}", + self.__logger.log_level, _default_log_level, self.__logger.using_default_log_level) + if self.__instant_leaveaction.get() <= -1: + self.__using_default_instant_leaveaction = True + else: + self.__using_default_instant_leaveaction = False + self.__logger.debug("Current instant leave action {}, default {}, currently using default {}", + self.__instant_leaveaction, self.__default_instant_leaveaction, + self.__using_default_instant_leaveaction) + if self.__suspend_time.get() < 0: + self.__using_default_suspendtime = True + else: + self.__using_default_suspendtime = False + self.__logger.debug("Current suspend time {}, default {}, currently using default {}", + self.__suspend_time, StateEngineDefaults.suspend_time, + self.__using_default_suspendtime) self.update_lock.acquire(True, 10) + all_released_by = {} + new_state = None while not self.__queue.empty() and self.__ab_alive: job = self.__queue.get() + new_state = None if job is None or self.__ab_alive is False: self.__logger.debug("No jobs in queue left or plugin not active anymore") break elif job[0] == "delayedaction": self.__logger.debug("Job {}", job) - (_, action, actionname, namevar, repeat_text, value, current_condition, previous_condition, previousstate_condition) = job - self.__logger.info("Running delayed action: {0} based on current condition {1} or previous condition {2}", - actionname, current_condition, previous_condition) - action.real_execute(actionname, namevar, repeat_text, value, False, current_condition) + (_, action, actionname, namevar, repeat_text, value, current_condition, previous_condition, + previousstate_condition, state) = job + self.__logger.info( + "Running delayed action: {0} based on current condition {1} or previous condition {2}", + actionname, current_condition, previous_condition) + action.real_execute(state, actionname, namevar, repeat_text, value, False, current_condition) else: (_, item, caller, source, dest) = job item_id = item.property.path if item is not None else "(no item)" self.__logger.update_logfile() self.__logger.header("Update state of item {0}".format(self.__name)) if caller: - self.__logger.debug("Update triggered by {0} (item={1} source={2} dest={3})", caller, item_id, source, dest) + self.__logger.debug("Update triggered by {0} (item={1} source={2} dest={3})", caller, item_id, + 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.__logger, caller, + source, item) if orig_caller != caller: text = "{0} initially triggered by {1} (item={2} source={3} value={4})." self.__logger.debug(text, caller, orig_caller, orig_item.property.path, @@ -369,10 +506,11 @@ def run_queue(self): # Update current values StateEngineCurrent.update() - self.__variables["item.suspend_time"] = self.__suspend_time.get() + self.__variables["item.suspend_time"] = StateEngineDefaults.suspend_time.get() \ + if self.__using_default_suspendtime is True else self.__suspend_time.get() self.__variables["item.suspend_remaining"] = -1 - self.__variables["item.instant_leaveaction"] = self.__instant_leaveaction.get() - + self.__variables["item.instant_leaveaction"] = self.__default_instant_leaveaction.get() \ + if self.__using_default_instant_leaveaction is True else self.__instant_leaveaction.get() # get last state last_state = self.__laststate_get() if last_state is not None: @@ -387,34 +525,27 @@ def run_queue(self): _original_conditionset_id = _last_conditionset_id _original_conditionset_name = _last_conditionset_name if self.__previousconditionset_internal_id not in ['', None]: - self.__logger.info("Previous Conditionset: {0} ('{1}')", self.__previousconditionset_internal_id, self.__previousconditionset_internal_name) + self.__logger.info("Previous Conditionset: {0} ('{1}')", self.__previousconditionset_internal_id, + self.__previousconditionset_internal_name) else: self.__logger.info("Previous Conditionset is empty") _previous_conditionset_id = _last_conditionset_id _previous_conditionset_name = _last_conditionset_name # get previous state if self.__previousstate_internal_id not in ['', None]: - self.__logger.info("Previous state: {0} ('{1}')", self.__previousstate_internal_id, self.__previousstate_internal_name) + self.__logger.info("Previous state: {0} ('{1}')", self.__previousstate_internal_id, + self.__previousstate_internal_name) if self.__previousstate_conditionset_internal_id not in ['', None]: - self.__logger.info("Previous state's Conditionset: {0} ('{1}')", self.__previousstate_conditionset_internal_id, self.__previousstate_conditionset_internal_name) + self.__logger.info("Previous state's Conditionset: {0} ('{1}')", + self.__previousstate_conditionset_internal_id, + self.__previousstate_conditionset_internal_name) else: self.__logger.info("Previous state's Conditionset is empty") # find new state - new_state = None _leaveactions_run = False - # for releasedby functionality - _releasedby_active = True if self.__all_releasedby else False - if _releasedby_active: - _wouldenter = None - _wouldnotenter = [] - _flagged = [] - _checked_states = [] - _possible_states = [] - - _releasedby = [] for state in self.__states: if not self.__ab_alive: self.__logger.debug("StateEngine Plugin not running (anymore). Stop state evaluation.") @@ -423,64 +554,36 @@ def run_queue(self): _key_name = ['{}'.format(state.id), 'name'] self.update_webif(_key_name, state.name) - if _releasedby_active: - _checked_states.append(state) - if _wouldenter and not _releasedby: - new_state = self.__state_ids[_wouldenter] - _last_conditionset_id = self.__conditionsets[_wouldenter][0] - _last_conditionset_name = self.__conditionsets[_wouldenter][1] - if new_state.conditions.count() == 0: - self.lastconditionset_set('', '') - _last_conditionset_id = '' - _last_conditionset_name = '' - else: - self.lastconditionset_set(_last_conditionset_id, _last_conditionset_name) - self.__logger.debug("No release states True - Going back to {}. Condition set: {} ('{}')", new_state, _last_conditionset_id, _last_conditionset_name) - break result = self.__update_check_can_enter(state) _previousstate_conditionset_id = _last_conditionset_id _previousstate_conditionset_name = _last_conditionset_name _last_conditionset_id = self.__lastconditionset_internal_id _last_conditionset_name = self.__lastconditionset_internal_name if state is not None and result is True: - self.__conditionsets.update({state.state_item.property.path: [_last_conditionset_id, _last_conditionset_name]}) - if _releasedby_active: - _todo, _releasedby, _wouldenter, _wouldnotenter, new_state, _possible_state, _flagged = self.__check_releasedby( - state, _checked_states, _releasedby, _wouldenter, _wouldnotenter, _flagged, last_state, _possible_states, result) - if self.__hasreleased_item is not None: - self.__hasreleased_item(self.__has_released, StateEngineDefaults.plugin_identification, "StateEvaluation") - - if self.___shouldnotrelease_item is not None: - self.___shouldnotrelease_item(self.__should_not_release, StateEngineDefaults.plugin_identification, - "StateEvaluation") - if _possible_state: - _possible_states.append(_possible_state) - self.__logger.info("Possible states: {}", _possible_states) - if _todo == 'continue': - continue - if _todo == 'break': - break - - if not _releasedby: - # New state is different from last state - if result is False and last_state == state and self.__instant_leaveaction.get() is True: - self.__logger.info("Leaving {0} ('{1}'). Running actions immediately.", last_state.id, last_state.name) - last_state.run_leave(self.__repeat_actions.get()) - _leaveactions_run = True - if result is True: - new_state = state - break + self.__conditionsets.update( + {state.state_item.property.path: [_last_conditionset_id, _last_conditionset_name]}) + # New state is different from last state + _instant_leaveaction = self.__instant_leaveaction.get() + if self.__using_default_instant_leaveaction: + _instant_leaveaction = self.__default_instant_leaveaction.get() + if _instant_leaveaction >= 1 and caller != "Released_by Retrigger": + _instant_leaveaction = True + else: + _instant_leaveaction = False + if result is False and last_state == state and _instant_leaveaction is True: + self.__logger.info("Leaving {0} ('{1}'). Running actions immediately.", last_state.id, + last_state.name) + last_state.run_leave(self.__repeat_actions.get()) + _leaveactions_run = True + if result is True: + new_state = state + break # no new state -> stay if new_state is None: if last_state is None: self.__logger.info("No matching state found, no previous state available. Doing nothing.") else: - try: - _last_conditionset_id = self.__conditionsets[_wouldenter][0] - _last_conditionset_name = self.__conditionsets[_wouldenter][1] - except: - pass if last_state.conditions.count() == 0: self.lastconditionset_set('', '') _last_conditionset_id = '' @@ -492,12 +595,39 @@ def run_queue(self): self.__logger.info(text, last_state.id, last_state.name) else: text = "No matching state found, staying at {0} ('{1}') based on conditionset {2} ('{3}')" - self.__logger.info(text, last_state.id, last_state.name, _last_conditionset_id, _last_conditionset_name) + self.__logger.info(text, last_state.id, last_state.name, _last_conditionset_id, + _last_conditionset_name) last_state.run_stay(self.__repeat_actions.get()) if self.update_lock.locked(): self.update_lock.release() self.__logger.debug("State evaluation finished") self.__logger.info("State evaluation queue empty.") + self.__handle_releasedby(new_state, last_state) + return + + if new_state.is_copy_for: + new_state.has_released = new_state.is_copy_for + last_state.was_releasedby = new_state + self.__logger.info( + "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, False) + last_state.run_leave(self.__repeat_actions.get()) + _key_leave = ['{}'.format(last_state.id), 'leave'] + _key_stay = ['{}'.format(last_state.id), 'stay'] + _key_enter = ['{}'.format(last_state.id), 'enter'] + + self.update_webif(_key_leave, True) + self.update_webif(_key_stay, False) + self.update_webif(_key_enter, False) + self.__handle_releasedby(new_state, last_state) + if self.update_lock.locked(): + self.update_lock.release() + self.update_state(self.__item, "Released_by Retrigger", state.id) return _last_conditionset_id = self.__lastconditionset_internal_id @@ -508,7 +638,7 @@ def run_queue(self): _last_conditionset_id = '' _last_conditionset_name = '' self.previousconditionset_set(_previous_conditionset_id, _previous_conditionset_name) - #endblock + # endblock # get data for new state if last_state is not None and new_state.id == last_state.id: if _last_conditionset_id in ['', None]: @@ -523,10 +653,13 @@ def run_queue(self): self.__previousstate_set(last_state) else: - if last_state is not None and _leaveactions_run is True: + if caller == "Released_by Retrigger": + self.__logger.info("Leave actions already run during state release.") + elif last_state is not None and _leaveactions_run is True: self.__logger.info("Left {0} ('{1}')", last_state.id, last_state.name) if last_state.leaveactions.count() > 0: - self.__logger.info("Maybe some actions were performed directly after leave - see log above.") + self.__logger.info( + "Maybe some actions were performed directly after leave - see log above.") elif last_state is not None: self.lastconditionset_set(_original_conditionset_id, _original_conditionset_name) self.__logger.info("Leaving {0} ('{1}'). Condition set was: {2}.", @@ -539,7 +672,8 @@ def run_queue(self): _last_conditionset_name = '' else: self.lastconditionset_set(_last_conditionset_id, _last_conditionset_name) - self.previousstate_conditionset_set(_previousstate_conditionset_id, _previousstate_conditionset_name) + self.previousstate_conditionset_set(_previousstate_conditionset_id, + _previousstate_conditionset_name) if _last_conditionset_id in ['', None]: self.__logger.info("Entering {0} ('{1}')", new_state.id, new_state.name) else: @@ -559,10 +693,167 @@ def run_queue(self): self.update_webif(_key_enter, False) self.__logger.debug("State evaluation finished") + all_released_by = self.__handle_releasedby(new_state, last_state) + 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)) + if self.update_lock.locked(): self.update_lock.release() + def __update_release_item_value(self, value, state): + if state is None: + return value + if isinstance(value, Item): + value = value.property.value + if isinstance(value, str) and value.startswith(".."): + _returnvalue_issue = "Relative state {} defined by value in se_released_by attribute of " \ + "state {} has to be defined with one '.' only.".format(value, state.id) + self.__logger.warning("{} Changing it accordingly.", _returnvalue_issue) + value = re.sub(r'\.+', '.', value) + if isinstance(value, str) and value.startswith("."): + value = "{}{}".format(state.id.rsplit(".", 1)[0], value) + + return value + + def __update_can_release(self, can_release, new_state=None): + state_dict = {state.id: state for state in self.__states} + for entry, release_list in can_release.items(): # Iterate through the dictionary items + entry = self.__update_release_item_value(entry, new_state) + if entry in state_dict: + state = state_dict.get(entry) + if state.is_copy_for: + self.__logger.develop("State {} is a copy.", state.id) + #continue + can_release_list = [] + _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 {}, "\ + "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 "\ + "than state. Ignoring {}", state.id, e) + else: + can_release_list.append(e) + self.__logger.develop("Value added to possible can release states {}", e) + + state.update_can_release_internal(can_release_list) + self.__logger.develop("Updated 'can_release' property of state {} to {}", state.id, state.can_release) + + else: + self.__logger.info("Entry {} in se_released_by of state(s) is not a valid state.", entry) + + def __handle_releasedby(self, new_state, last_state): + def update_can_release_list(): + for e in _returnvalue: + e = self.__update_release_item_value(e, new_state) + if e and state.id not in can_release.setdefault(e, [state.id]): + can_release[e].append(state.id) + + self.__logger.info("".ljust(80, "_")) + self.__logger.info("Handling released_by attributes") + can_release = {} + all_released_by = {} + skip_copy = True + for state in self.__states: + if state.is_copy_for and skip_copy: + self.__logger.develop("Skipping {} because it is a copy", state.id) + skip_copy = False + continue + _returnvalue = state.releasedby + all_released_by.update({state: _returnvalue}) + + if _returnvalue: + _returnvalue = _returnvalue if isinstance(_returnvalue, list) else [_returnvalue] + update_can_release_list() + + self.__update_can_release(can_release, new_state) + + if last_state and new_state and last_state != new_state and last_state.is_copy_for: + self.__states.remove(last_state) + last_state.is_copy_for = None + self.__logger.debug("Removed state copy {} because it was just left.", last_state.id) + elif last_state and new_state and last_state != new_state and new_state.is_copy_for: + self.__states.remove(new_state) + new_state.is_copy_for = None + new_state.has_released = last_state + self.__logger.debug("Removed state copy {} because it has just released {}.", new_state.id, last_state.id) + if last_state and new_state and last_state != new_state: + new_states = self.__states.copy() + for entry in new_states: + if entry.is_copy_for and last_state == entry.is_copy_for: + self.__states.remove(entry) + entry.is_copy_for = None + if entry != new_state: + self.__logger.debug("Removed state copy {} (is copy for {}) because " + "state was released by other possible state.", entry.id, last_state.id) + else: + new_state.has_released = last_state + self.__logger.debug("Removed state copy {} because " + "it has already released state {}.", entry.id, last_state.id) + + if new_state: + new_state.was_releasedby = None + _can_release_list = [] + releasedby = all_released_by.get(new_state) + self.__logger.develop("releasedby {}", releasedby) + if releasedby: + state_dict = {item.id: item for item in self.__states} + _stateindex = list(state_dict.keys()).index(new_state.id) + releasedby = releasedby if isinstance(releasedby, list) else [releasedby] + _checkedentries = [] + for i, entry in enumerate(releasedby): + entry = self.__update_release_item_value(entry, new_state) + if entry in _checkedentries: + self.__logger.develop("Entry {} defined by {} already checked, skipping", entry, releasedby[i]) + continue + cond_copy_for = entry in state_dict.keys() + if cond_copy_for and new_state == state_dict.get(entry).is_copy_for: + _can_release_list.append(entry) + self.__logger.develop("Entry {} defined by {} is a copy, skipping", entry, releasedby[i]) + continue + _entryindex = list(state_dict.keys()).index(entry) if entry in state_dict else -1 + self.__logger.develop("Testing if entry {} should become a state copy. "\ + "stateindex {}, entryindex {}", entry, _stateindex, _entryindex) + if entry == new_state.id: + self.__logger.warning("Value in se_released_by must no be identical to state. Ignoring {}", + entry) + elif _entryindex == -1: + self.__logger.warning("State in se_released_by does not exist. Ignoring {}", entry) + elif _stateindex > _entryindex: + self.__logger.warning("Value in se_released_by must have lower priority than state. Ignoring {}", + entry) + elif entry in state_dict.keys(): + relevant_state = state_dict.get(entry) + index = self.__states.index(new_state) + cond_index = relevant_state in self.__states and self.__states.index(relevant_state) != index - 1 + if cond_index or relevant_state not in self.__states: + current_log_level = self.__log_level.get() + if current_log_level < 3: + self.__logger.log_level.set(0) + can_enter = self.__update_check_can_enter(relevant_state) + #can_enter = relevant_state.can_enter() + self.__logger.log_level.set(current_log_level) + if relevant_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) + elif not can_enter: + relevant_state.is_copy_for = new_state + self.__states.insert(index, relevant_state) + _can_release_list.append(relevant_state.id) + self.__logger.debug("Inserted copy of state {}", relevant_state.id) + _checkedentries.append(entry) + self.__logger.info("State {} can currently get released by: {}", new_state.id, _can_release_list) + + self.__logger.info("".ljust(80, "_")) + return all_released_by + def update_webif(self, key, value): def _nested_set(dic, keys, val): for nestedkey in keys[:-1]: @@ -585,167 +876,179 @@ def _nested_test(dic, keys): self.__webif_infos[key] = value return True - def update_releasedby(self, state): - # create dependencies - _id = state.id - _returnvalue, _returntype, _releasedby = state.update_releasedby_internal() - _releasedby = _releasedby if isinstance(_releasedby, list) else \ - [_releasedby] if _releasedby is not None else [] - _convertedlist = [] - for entry in _releasedby: - try: - if entry is not None: - _convertedlist.append(entry.property.path) - else: - self.__logger.warning("Found invalid state in se_released_by attribute. Ignoring {}", entry) - except Exception as ex: - self.__logger.error("Issue with {} for released_by check: {}", entry, ex) - if _releasedby: - self.__all_releasedby.update({_id: _convertedlist}) - self.__logger.debug("Updated releasedby for state {}: {}. All releasedby: {}", state, _releasedby, self.__all_releasedby) - if self.__hasreleased_item is None or self.__has_released.get('initial'): - self.__has_released.update({_id: _convertedlist}) - self.__logger.develop("Added to hasreleased: {} for state {}", self.__has_released, state) - - ''' - for i in _releasedby: - if i.property.path not in self.__all_torelease.keys(): - self.__all_torelease.update({i: [_id]}) - elif state.id not in self.__all_torelease.get(i): - self.__all_torelease[i].append(_id) - ''' - - def __check_releasedby(self, state, _checked_states, _releasedby, _wouldenter, _wouldnotenter, - _flagged, _laststate, _possible_states, result): - self.__logger.develop("Self ID {}, flagged: {}, wouldnotenter {}", state.id, _flagged, _wouldnotenter) - cond1 = state.id in _releasedby - cond2 = self.__has_released.get(_wouldenter) and state.id in self.__has_released.get(_wouldenter) - cond3 = self.__should_not_release.get(_wouldenter) and state.id in self.__should_not_release.get(_wouldenter) - if cond1 and cond2: - _releasedby.remove(state.id) - self.__logger.develop("State {} has already released, removed from _releasedby {}. has_released = {}", - state.id, _releasedby, self.__has_released) - if cond3: - self.__should_not_release.get(_wouldenter).remove(state.id) - self.__logger.develop("State {} removed from shouldnotrelease {}", state.id, self.__should_not_release) - if result is False: - self.__has_released[_wouldenter].remove(state.id) - self.__logger.develop("State {} removed from hasreleased because it got FALSE. {}", state.id, - self.__has_released) - _possible_state = None - else: - _possible_state = state - self.__logger.debug("State {} added to possible_states as it is true", state.id) - self.__logger.develop("Skipping rest of evaluation") - return 'continue', _releasedby, _wouldenter, _wouldnotenter, None, _possible_state, _flagged - if result is False and state.id in self.__all_releasedby.keys(): - _flagged = list(self.__all_releasedby.get(state.id)) - if state.id not in _wouldnotenter: - _wouldnotenter.append(state.id) - self.__logger.develop("FLAGGED {}, wouldnot {}", _flagged, _wouldnotenter) - cond4 = result is True and _laststate == state and state.id in self.__all_releasedby.keys() - if cond4: - self.__logger.develop("State {} cond4. wouldenter: {}, releasedby {}.", state.id, _wouldenter, _releasedby) - if _wouldenter: - self.logger.debug("State {} could be released, too. Prevent layered releaseby", state.id) - else: - _releasedby = list(self.__all_releasedby.get(state.id)) - _wouldenter = state.id - self.__logger.debug("State {} could be entered but can be released by {}.", state.id, _releasedby) - elif result is True and not cond1 and not state.id == _wouldenter: - _possible_state = state - self.__logger.develop("State {} has nothing to do with release, writing down " - "as possible candidate to enter", state.id) - return 'nothing', _releasedby, _wouldenter, _wouldnotenter, None, _possible_state, _flagged - if result is True and state.id in _flagged: - self.__logger.develop("State {} should get added to shouldnotrelease {}. wouldnotenter = {}.", - state.id, self.__should_not_release, _wouldnotenter) - for entry in _wouldnotenter: - if self.__should_not_release.get(entry): - if state.id not in self.__should_not_release[entry]: - self.__should_not_release[entry].append(state.id) + 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: + combined_dict[key]['issueorigin'].extend(value['issueorigin']) else: - self.__should_not_release.update({entry: [state.id]}) - self.__logger.develop("State {} added to should not release of {}.", state.id, entry) - if result is True and state.id in _releasedby: - if self.__has_released.get(_wouldenter) and state.id in self.__has_released.get(_wouldenter): - _releasedby.remove(state.id) - self.__logger.develop("State {} has already released, removed from _releasedby {}. has_released = {}", - state.id, _releasedby, self.__has_released) - else: - self.__logger.develop("shouldnotrelease: {}, wouldenter: {}", self.__should_not_release, - _wouldenter) - if self.__should_not_release.get(_wouldenter) and state.id in self.__should_not_release.get( - _wouldenter): - _releasedby.remove(state.id) - self.__logger.develop("State {} has not released yet, but it is in shouldnotrelease. " - "Removed from releasedby {}.", state.id, _releasedby) - return 'continue', _releasedby, _wouldenter, _wouldnotenter, None, None, _flagged - if self.__has_released.get(_wouldenter): - if state.id not in self.__has_released.get(_wouldenter): - self.__has_released[_wouldenter].append(state.id) - self.__logger.develop("State {} in releasedby, not released yet, appended to hasreleased {}", - state.id, self.__has_released) + combined_dict[key] = value + + return combined_dict + + combined_dict = combine_dicts(action_status, self.__action_status) + self.__action_status = combined_dict + + def update_issues(self, issue_type, issues): + def combine_dicts(dict1, dict2): + 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: - self.__has_released.update({_wouldenter: [state.id]}) - self.__logger.develop("State {} in releasedby, created hasreleased {}", - state.id, self.__has_released) - self.__logger.develop("State {} has not released yet, could set as new state.", state.id) - if _possible_states: - self.logger.develop("However, higher ranked state could be entered - entering that: {}", - _possible_states) - new_state = _possible_states[0] + combined_dict[key] = value + + return combined_dict + + if issue_type == "state": + combined_dict = combine_dicts(issues, self.__state_issues) + self.__state_issues = combined_dict + elif issue_type == "config": + combined_dict = combine_dicts(issues, self.__config_issues) + self.__config_issues = combined_dict + elif issue_type == "struct": + combined_dict = combine_dicts(issues, self.__struct_issues) + self.__struct_issues = combined_dict + + def update_attributes(self, unused_attributes, used_attributes): + combined_unused_dict = unused_attributes.copy() # Create a copy of dict1 + for key, value in self.__unused_attributes.items(): + if key in combined_unused_dict: + if unused_attributes.get(key): + existing_issue = unused_attributes[key].get('issueorigin') else: - new_state = state - return 'break', _releasedby, _wouldenter, _wouldnotenter, new_state, None, _flagged - - elif result is False: - self.__logger.develop("State {} FALSE, has_released {}, _releasedby list {}, _wouldenter {}", - state.id, self.__has_released, _releasedby, _wouldenter) - if _wouldenter is None: - for entry in self.__has_released.keys(): - if state.id in self.__has_released.get(entry): - self.__has_released[entry].remove(state.id) - self.__logger.develop("State {} removed from hasreleased because wouldenter is None. {}", - state.id, self.__has_released) - for entry in self.__should_not_release.keys(): - if state.id in self.__should_not_release.get(entry): - self.__should_not_release[entry].remove(state.id) - self.__logger.develop("State {} removed from shouldnotrelease because wouldenter is None. {}", - state.id, self.__has_released) - if self.__has_released.get(_wouldenter) and state.id in self.__has_released.get(_wouldenter): - self.__has_released[_wouldenter].remove(state.id) - self.__should_not_release[_wouldenter].remove(state.id) - self.__logger.develop("State {} in releasedby but FALSE, removed from hasreleased {} and shouldnot {}", - state.id, self.__has_released, self.__should_not_release) - if state.id in _releasedby: - _releasedby.remove(state.id) - self.__logger.develop("State {} removed from _releasedby {} because FALSE and hasreleased", - state.id, _releasedby) - new_state = self.__state_ids[_wouldenter] - self.__logger.develop("State {} - Going back to {}", state.id, _wouldenter) - return 'break', _releasedby, _wouldenter, _wouldnotenter, new_state, None, _flagged - elif state.id in _releasedby: - _releasedby.remove(state.id) - if self.__should_not_release.get(_wouldenter) and state.id in self.__should_not_release.get( - _wouldenter): - self.__should_not_release[_wouldenter].remove(state.id) - self.__logger.develop("State {} removed from shouldnotrelease FALSE", state.id, - self.__should_not_release) - self.__logger.develop("State {} removed from _releasedby {} because FALSE", state.id, _releasedby) - if cond4 and _releasedby == self.__has_released.get(_wouldenter): - _checked = True - for entry in _checked_states: - if entry not in _releasedby: - _checked = False - if _checked is True: - self.__logger.info("State {} could be releaed by {}, but those states already have released it", - state.id, _releasedby) - new_state = state - return 'break', _releasedby, _wouldenter, _wouldnotenter, new_state, None, _flagged + existing_issue = None + combined_unused_dict[key].update(value) # Update nested dictionaries + if existing_issue: + try: + combined_dict = defaultdict(set) + for entry in existing_issue + combined_unused_dict[key].get('issueorigin'): + combined_dict[entry['state']].add(entry['conditionset']) + + 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: + pass + + self.__unused_attributes = combined_unused_dict + + combined_dict = self.__used_attributes.copy() # Create a copy of dict1 + for key, value in used_attributes.items(): + if key in combined_dict: + combined_dict[key].update(value) # Update nested dictionaries + else: + combined_dict[key] = value # Add new key-value pairs + self.__used_attributes = combined_dict + + def __log_issues(self, issue_type): + 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) + self.__logger.decrease_indent() + elif isinstance(_issuelist, list) and len(_issuelist) == 1: + self.__logger.info("has the following issue: {}", _issuelist[0]) else: - self.__logger.debug( "State {} still could be releaed by {}, have to check that value", state.id, _releasedby) - return 'nothing', _releasedby, _wouldenter, _wouldnotenter, None, None, _flagged + self.__logger.info("has the following issue: {}", _issuelist) + + if issue_type == 'actions': + to_check = self.__action_status.items() + warn = ', '.join(key for key in self.__action_status.keys()) + elif issue_type == 'structs': + to_check = self.__struct_issues.items() + warn = ', '.join(key for key in self.__struct_issues.keys()) + elif issue_type == 'states': + to_check = self.__state_issues.items() + warn = ', '.join(key for key in self.__state_issues.keys()) + elif issue_type == 'config entries': + to_check = self.__config_issues.items() + warn = ', '.join(key for key in self.__config_issues.keys()) + else: + to_check = self.__unused_attributes.items() + warn = ', '.join(key for key in self.__unused_attributes.keys()) + self.__logger.info("") + if issue_type == 'attributes': + self.__logger.info("These attributes are not used: {} Please check extended " + "log file for details.", warn) + else: + self.__logger.warning("There are {} issues: {} Please check extended " + "log file for details.", issue_type, warn) + self.__logger.info("") + self.__logger.info("The following {} have issues:", issue_type) + 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) + elif issue_type == 'config entries': + if value.get('attribute'): + self.__logger.info("Attribute {}", value.get('attribute')) + self.__logger.increase_indent() + self.__logger.info("defined in state {}", entry) + self.__logger.decrease_indent() + list_issues(value) + else: + self.__logger.info("Attribute {} has an issue: {}", entry, value.get('issue')) + self.__logger.info("") + continue + elif issue_type == 'structs': + self.__logger.info("Struct {} has an issue: {}", entry, value.get('issue')) + self.__logger.info("") + continue + else: + additional = " used in" if origin_list else "" + self.__logger.info("Definition {}{}", entry, additional) + self.__logger.increase_indent() + for origin in origin_list: + if issue_type == 'actions': + origin_text = 'state {}, action {}, on_{}'.format(origin.get('state'), origin.get('action'), + origin.get('type')) + elif issue_type == 'states': + origin_text = 'condition {} defined in conditionset {}'.format(origin.get('condition'), + origin.get('conditionset')) + else: + origin_text = 'state {}, conditionset {}'.format(origin.get('state'), + origin.get('conditionset')) + self.__logger.info("{}", origin_text) + self.__logger.decrease_indent() + list_issues(value) + self.__logger.info("") + for entry, value in to_check: + if 'issue' not in value: + text = "Definition {} not used in any action or condition.".format(entry) + self.__logger.info("{}", text) + self.__logger.decrease_indent() + + def __initialize_state(self, item_state, _statecount): + # initialize states + try: + _state = StateEngineState.SeState(self, item_state) + _statecount += 1 + self.__states.append(_state) + self.__state_ids.update({item_state.property.path: _state}) + if _statecount == 1: + self.__unused_attributes = _state.unused_attributes.copy() + filtered_dict = {key: value for key, value in self.__unused_attributes.items() if + key not in _state.used_attributes} + self.__unused_attributes = filtered_dict + except ValueError as ex: + self.__logger.error("Ignoring state {0} because ValueError: {1}", item_state.property.path, ex) + except Exception as ex: + self.__logger.error("Ignoring state {0} because: {1}", item_state.property.path, ex) + + def __finish_states(self): + # initialize states + if len(self.__states) == 0: + raise ValueError("{0}: No states defined!".format(self.id)) # Find the state, matching the current conditions and perform the actions of this state # caller: Caller that triggered the update @@ -756,15 +1059,37 @@ 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: {}".format(item, 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 # state: state to check - def __update_check_can_enter(self, state): + def __update_check_can_enter(self, state, refill=True): try: + wasreleasedby = state.was_releasedby.id + except Exception as ex: + wasreleasedby = state.was_releasedby + try: + iscopyfor = state.is_copy_for.id + except: + iscopyfor = state.is_copy_for + try: + hasreleased = state.has_released.id + except: + hasreleased = state.has_released + try: + canrelease = state.can_release.id + except: + canrelease = state.can_release + try: + self.__variables["release.can_release"] = canrelease + self.__variables["release.can_be_released_by"] = state.releasedby + self.__variables["release.has_released"] = hasreleased + self.__variables["release.was_released_by"] = wasreleasedby + self.__variables["release.will_release"] = iscopyfor self.__variables["previous.state_id"] = self.__previousstate_internal_id self.__variables["previous.state_name"] = self.__previousstate_internal_name + self.__variables["item.instant_leaveaction"] = self.__instant_leaveaction.get() self.__variables["current.state_id"] = state.id self.__variables["current.state_name"] = state.name self.__variables["current.conditionset_id"] = self.__lastconditionset_internal_id @@ -773,11 +1098,19 @@ def __update_check_can_enter(self, state): self.__variables["previous.conditionset_name"] = self.__previousconditionset_internal_name self.__variables["previous.state_conditionset_id"] = self.__previousstate_conditionset_internal_id self.__variables["previous.state_conditionset_name"] = self.__previousstate_conditionset_internal_name - state.refill() - return state.can_enter() + self.__logger.develop("Current variables: {}", self.__variables) + if refill: + state.refill() + return state.can_enter() except Exception as ex: self.__logger.warning("Problem with currentstate {0}. Error: {1}", state.id, ex) # The variables where originally reset in a finally: statement. No idea why... ;) + self.__variables["release.can_release"] = "" + self.__variables["release.can_be_released_by"] = "" + self.__variables["release.has_released"] = "" + self.__variables["release.was_released_by"] = "" + self.__variables["release.will_release"] = "" + self.__variables["item.instant_leaveaction"] = "" self.__variables["current.state_id"] = "" self.__variables["current.state_name"] = "" self.__variables["current.conditionset_id"] = "" @@ -798,13 +1131,16 @@ def __laststate_set(self, new_state): self.__laststate_internal_id = '' if new_state is None else new_state.id if self.__laststate_item_id is not None: # noinspection PyCallingNonCallable - self.__laststate_item_id(self.__laststate_internal_id, StateEngineDefaults.plugin_identification, "StateEvaluation") + self.__laststate_item_id(self.__laststate_internal_id, StateEngineDefaults.plugin_identification, + "StateEvaluation") self.__laststate_internal_name = '' if new_state is None else new_state.text if self.__laststate_item_name is not None: # noinspection PyCallingNonCallable - self.__laststate_item_name(self.__laststate_internal_name, StateEngineDefaults.plugin_identification, "StateEvaluation") - self.__logger.develop("Setting last state to {0} ('{1}')", self.__laststate_internal_id, self.__laststate_internal_name) + self.__laststate_item_name(self.__laststate_internal_name, StateEngineDefaults.plugin_identification, + "StateEvaluation") + self.__logger.develop("Setting last state to {0} ('{1}')", self.__laststate_internal_id, + self.__laststate_internal_name) # get last state object based on laststate_id # returns: SeState instance of last state or "None" if no last state could be found @@ -816,13 +1152,13 @@ def __laststate_get(self): # return id of last conditionset def __lastconditionset_get_id(self): - _lastconditionset_item_id = self.return_item_by_attribute("se_lastconditionset_item_id") + _lastconditionset_item_id, _ = self.return_item_by_attribute("se_lastconditionset_item_id") _lastconditionset_item_id = "" if _lastconditionset_item_id is None else _lastconditionset_item_id.property.value return _lastconditionset_item_id # return name of last conditionset def __lastconditionset_get_name(self): - _lastconditionset_item_name = self.return_item_by_attribute("se_lastconditionset_item_name") + _lastconditionset_item_name, _ = self.return_item_by_attribute("se_lastconditionset_item_name") _lastconditionset_item_name = "" if _lastconditionset_item_name is None else _lastconditionset_item_name.property.value return _lastconditionset_item_name @@ -830,13 +1166,16 @@ def lastconditionset_set(self, new_id, new_name): self.__lastconditionset_internal_id = new_id if self.__lastconditionset_item_id is not None: # noinspection PyCallingNonCallable - self.__lastconditionset_item_id(self.__lastconditionset_internal_id, StateEngineDefaults.plugin_identification, "StateEvaluation") + self.__lastconditionset_item_id(self.__lastconditionset_internal_id, + StateEngineDefaults.plugin_identification, "StateEvaluation") self.__lastconditionset_internal_name = new_name if self.__lastconditionset_item_name is not None: # noinspection PyCallingNonCallable - self.__lastconditionset_item_name(self.__lastconditionset_internal_name, StateEngineDefaults.plugin_identification, "StateEvaluation") - self.__logger.develop("Setting current Conditionset to {0} ('{1}')", self.__lastconditionset_internal_id, self.__lastconditionset_internal_name) + self.__lastconditionset_item_name(self.__lastconditionset_internal_name, + StateEngineDefaults.plugin_identification, "StateEvaluation") + self.__logger.develop("Setting current Conditionset to {0} ('{1}')", self.__lastconditionset_internal_id, + self.__lastconditionset_internal_name) # endregion @@ -847,12 +1186,14 @@ def __previousstate_set(self, last_state): self.__previousstate_internal_id = 'None' if last_state is None else last_state.id if self.__previousstate_item_id is not None: # noinspection PyCallingNonCallable - self.__previousstate_item_id(self.__previousstate_internal_id, StateEngineDefaults.plugin_identification, "StateEvaluation") + self.__previousstate_item_id(self.__previousstate_internal_id, StateEngineDefaults.plugin_identification, + "StateEvaluation") self.__previousstate_internal_name = 'None' if last_state is None else last_state.text if self.__previousstate_item_name is not None: # noinspection PyCallingNonCallable - self.__previousstate_item_name(self.__previousstate_internal_name, StateEngineDefaults.plugin_identification, "StateEvaluation") + self.__previousstate_item_name(self.__previousstate_internal_name, + StateEngineDefaults.plugin_identification, "StateEvaluation") # get previous state object based on previousstate_id # returns: SeState instance of last state or "None" if no last state could be found @@ -864,51 +1205,58 @@ def __previousstate_get(self): # return id of last conditionset def __previousconditionset_get_id(self): - _previousconditionset_item_id = self.return_item_by_attribute("se_previousconditionset_item_id") + _previousconditionset_item_id, _ = self.return_item_by_attribute("se_previousconditionset_item_id") _previousconditionset_item_id = "" if _previousconditionset_item_id is None else _previousconditionset_item_id.property.value return _previousconditionset_item_id # return name of last conditionset def __previousconditionset_get_name(self): - _previousconditionset_item_name = self.return_item_by_attribute("se_previousconditionset_item_name") + _previousconditionset_item_name, _ = self.return_item_by_attribute("se_previousconditionset_item_name") _previousconditionset_item_name = "" if _previousconditionset_item_name is None else _previousconditionset_item_name.property.value return _previousconditionset_item_name # return id of conditionset of last state def __previousstate_conditionset_get_id(self): - _previousconditionset_item_id = self.return_item_by_attribute("se_previousstate_conditionset_item_id") - _previousconditionset_item_id = "" if _previousstate_conditionset_item_id is None else _previousstate_conditionset_item_id.property.value - return _previousconditionset_item_id + _previousstate_conditionset_item_id, _ = self.return_item_by_attribute("se_previousstate_conditionset_item_id") + _previousstate_conditionset_item_id = "" if _previousstate_conditionset_item_id is None else _previousstate_conditionset_item_id.property.value + return _previousstate_conditionset_item_id # return name of conditionset of last state def __previousstate_conditionset_get_name(self): - _previousconditionset_item_name = self.return_item_by_attribute("se_previousstate_conditionset_item_name") - _previousconditionset_item_name = "" if _previousstate_conditionset_item_name is None else _previousstate_conditionset_item_name.property.value - return _previousconditionset_item_name + _previousstate_conditionset_item_name, _ = self.return_item_by_attribute("se_previousstate_conditionset_item_name") + _previousstate_conditionset_item_name = "" if _previousstate_conditionset_item_name is None else _previousstate_conditionset_item_name.property.value + return _previousstate_conditionset_item_name def previousconditionset_set(self, last_id, last_name): self.__previousconditionset_internal_id = last_id if self.__previousconditionset_item_id is not None: # noinspection PyCallingNonCallable - self.__previousconditionset_item_id(self.__previousconditionset_internal_id, StateEngineDefaults.plugin_identification, "StateEvaluation") + self.__previousconditionset_item_id(self.__previousconditionset_internal_id, + StateEngineDefaults.plugin_identification, "StateEvaluation") self.__previousconditionset_internal_name = last_name if self.__previousconditionset_item_name is not None: # noinspection PyCallingNonCallable - self.__previousconditionset_item_name(self.__previousconditionset_internal_name, StateEngineDefaults.plugin_identification, "StateEvaluation") - self.__logger.develop("Setting previous Conditionset to {0} ('{1}')", self.__previousconditionset_internal_id, self.__previousconditionset_internal_name) + self.__previousconditionset_item_name(self.__previousconditionset_internal_name, + StateEngineDefaults.plugin_identification, "StateEvaluation") + self.__logger.develop("Setting previous Conditionset to {0} ('{1}')", self.__previousconditionset_internal_id, + self.__previousconditionset_internal_name) def previousstate_conditionset_set(self, last_id, last_name): self.__previousstate_conditionset_internal_id = last_id if self.__previousstate_conditionset_item_id is not None: # noinspection PyCallingNonCallable - self.__previousstate_conditionset_item_id(self.__previousstate_conditionset_internal_id, StateEngineDefaults.plugin_identification, "StateEvaluation") + self.__previousstate_conditionset_item_id(self.__previousstate_conditionset_internal_id, + StateEngineDefaults.plugin_identification, "StateEvaluation") self.__previousstate_conditionset_internal_name = last_name if self.__previousstate_conditionset_item_name is not None: # noinspection PyCallingNonCallable - self.__previousstate_conditionset_item_name(self.__previousstate_conditionset_internal_name, StateEngineDefaults.plugin_identification, "StateEvaluation") - self.__logger.develop("Setting Conditionset of previous state to {0} ('{1}')", self.__previousstate_conditionset_internal_id, self.__previousstate_conditionset_internal_name) + self.__previousstate_conditionset_item_name(self.__previousstate_conditionset_internal_name, + StateEngineDefaults.plugin_identification, "StateEvaluation") + self.__logger.develop("Setting Conditionset of previous state to {0} ('{1}')", + self.__previousstate_conditionset_internal_id, + self.__previousstate_conditionset_internal_name) # endregion @@ -1020,8 +1368,156 @@ def __verbose_crons_and_cycles(self): crons = "Inactive" return crons, cycles + def __init_releasedby(self): + def process_returnvalue(value): + self.__logger.info("Testing value {}", value) + _returnvalue_issue = None + if value is None: + return _returnvalue_issue + try: + original_value = value + value = self.__update_release_item_value(_evaluated_returnvalue[i], state) + _stateindex = list(state_dict.keys()).index(state.id) + _valueindex = list(state_dict.keys()).index(value) if value in state_dict else -1 + if _returntype[i] == 'value' and _valueindex == - 1: #not any(value == test.id for test in self.__states): + _returnvalue_issue = "State {} defined by value in se_released_by attribute of state {} " \ + "does not exist.".format(value, state.id) + self.__logger.warning("{} Removing it.", _returnvalue_issue) + elif _returntype[i] == 'value' and _valueindex < _stateindex: + _returnvalue_issue = "State {} defined by value in se_released_by attribute of state {} " \ + "must be lower priority than actual state.".format(value, state.id) + self.__logger.warning("{} Removing it.", _returnvalue_issue) + elif _returntype[i] == 'value' and value == state.id: + _returnvalue_issue = "State {} defined by value in se_released_by attribute of state {} " \ + "must not be identical.".format(value, state.id) + self.__logger.warning("{} Removing it.", _returnvalue_issue) + elif _returntype[i] == 'item': + if value == state.id: + _returnvalue_issue = "State {} defined by {} in se_released_by attribute of state {} " \ + "must not be identical.".format(value, _returnvalue[i], state.id) + 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(value, _returnvalue[i], state.id) + elif _valueindex < _stateindex: + _returnvalue_issue = "State {} defined by value in se_released_by attribute of state {} " \ + "must be lower priority than actual state.".format(value, state.id) + if _returnvalue_issue: + self.__logger.warning("{} Make sure to change item value.", _returnvalue_issue) + _convertedlist.append(original_value) + _converted_evaluatedlist.append(value) + _converted_typelist.append(_returntype[i]) + self.__logger.develop("Adding {} from item as releasedby for state {}", original_value, state.id) + elif _returntype[i] == 'regex': + matches = [test.id for test in self.__states if _evaluated_returnvalue[i].match(test.id)] + self.__logger.develop("matches {}", matches) + _returnvalue_issue_list = [] + for match in matches: + _valueindex = list(state_dict.keys()).index(match) if match in state_dict else -1 + if _valueindex == _stateindex: + _returnvalue_issue = "State {} defined by {} in se_released_by attribute of state {} " \ + "must not be identical.".format(match, _returnvalue[i], state.id) + self.__logger.warning("{} Removing it.", _returnvalue_issue) + if _returnvalue_issue not in _returnvalue_issue_list: + _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 "\ + "than actual state.".format(match, _returnvalue[i], state.id) + self.__logger.warning("{} Removing it.", _returnvalue_issue) + if _returnvalue_issue not in _returnvalue_issue_list: + _returnvalue_issue_list.append(_returnvalue_issue) + else: + _convertedlist.append(match) + _converted_evaluatedlist.append(value) + _converted_typelist.append(_returntype[i]) + self.__logger.develop("Adding {} from regex as releasedby for state {}", match, state.id) + _returnvalue_issue = _returnvalue_issue_list + if not matches: + _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': + if value == state.id: + _returnvalue_issue = "State {} defined by {} in se_released_by attribute of state {} " \ + "must not be identical.".format(value, _returnvalue[i], state.id) + self.__logger.warning("{} Make sure eval will result in a useful value later on.", + _returnvalue_issue) + elif _valueindex < _stateindex: + _returnvalue_issue = "State {} defined by value in se_released_by attribute of state {} " \ + "must be lower priority than actual state.".format(value, state.id) + self.__logger.warning("{} Make sure eval will result in a useful value later on.", + _returnvalue_issue) + elif value is None: + _returnvalue_issue = "Eval defined by {} in se_released_by attribute of state {} " \ + "does currently return None.".format(_returnvalue[i], state.id) + self.__logger.warning("{} Make sure eval will result in a useful value later on.", + _returnvalue_issue) + _convertedlist.append(_returnvalue[i]) + _converted_evaluatedlist.append(value) + _converted_typelist.append(_returntype[i]) + self.__logger.develop("Adding {} from eval as releasedby for state {}", _returnvalue[i], state.id) + elif value and value == state.id: + _returnvalue_issue = "State {} defined by {} in se_released_by attribute of state {} " \ + "must not be identical.".format(value, _returnvalue[i], state.id) + self.__logger.warning("{} Removing it.", _returnvalue_issue) + elif value and value not in _convertedlist: + _convertedlist.append(value) + _converted_evaluatedlist.append(value) + _converted_typelist.append(_returntype[i]) + self.__logger.develop("Adding {} as releasedby for state {}", value, state.id) + else: + _returnvalue_issue = "Found invalid definition in se_released_by attribute "\ + "of state {}, original {}.".format(state.id, value, original_value) + self.__logger.warning("{} Removing it.", _returnvalue_issue) + except Exception as ex: + _returnvalue_issue = "Issue with {} for released_by of state {} check: {}".format(value, state.id, ex) + self.__logger.error(_returnvalue_issue) + return _returnvalue_issue + + def update_can_release_list(): + for i, value in enumerate(_convertedlist): + if _converted_typelist[i] == 'item': + value = self.__update_release_item_value(_converted_evaluatedlist[i], state) + elif _converted_typelist[i] == 'eval': + value = _converted_evaluatedlist[i] + if value and can_release.get(value) and state.id not in can_release.get(value): + can_release[value].append(state.id) + elif value: + can_release.update({value: [state.id]}) + + self.__logger.info("".ljust(80, "_")) + self.__logger.info("Checking released_by attributes") + can_release = {} + state_dict = {state.id: state for state in self.__states} + for state in self.__states: + _issuelist = [] + _returnvalue, _returntype, _issue = state.update_releasedby_internal() + _returnvalue = copy.copy(_returnvalue) + + _issuelist.append(_issue) + if _returnvalue: + _convertedlist = [] + _converted_evaluatedlist = [] + _converted_typelist = [] + _returnvalue = _returnvalue if isinstance(_returnvalue, list) else [_returnvalue] + _evaluated_returnvalue = state.releasedby + _evaluated_returnvalue = _evaluated_returnvalue if isinstance(_evaluated_returnvalue, list) \ + else [_evaluated_returnvalue] + for i, entry in enumerate(_returnvalue): + _issue = process_returnvalue(entry) + _issuelist.append(_issue) + update_can_release_list() + _issuelist = StateEngineTools.flatten_list(_issuelist) + _issuelist = [issue for issue in _issuelist if issue is not None and issue != []] + _issuelist = None if len(_issuelist) == 0 else _issuelist[0] if len(_issuelist) == 1 else _issuelist + self.__config_issues.update({state.id: {'issue': _issuelist, 'attribute': 'se_released_by'}}) + state.update_releasedby_internal(_convertedlist) + self.__update_can_release(can_release, state) + + self.__logger.info("".ljust(80, "_")) + # log item data - def __write_to_log(self): + def write_to_log(self): # get crons and cycles crons, cycles = self.__verbose_crons_and_cycles() triggers = self.__verbose_triggers() @@ -1036,7 +1532,7 @@ def __write_to_log(self): self.__logger.info("Template {0}: {1}", t, self.__templates.get(t)) self.__logger.info("Cycle: {0}", cycles) self.__logger.info("Cron: {0}", crons) - self.__logger.info("Trigger: {0}".format(triggers)) + self.__logger.info("Trigger: {0}", triggers) self.__repeat_actions.write_to_logger() # log laststate settings @@ -1051,42 +1547,34 @@ def __write_to_log(self): if self.__previousstate_item_name is not None: self.__logger.info("Item 'Previousstate Name': {0}", self.__previousstate_item_name.property.path) - # log releasedby settings - if self.___shouldnotrelease_item is not None: - self.__logger.info("Item 'Should not release': {0}", self.___shouldnotrelease_item.property.path) - if self.__hasreleased_item is not None: - self.__logger.info("Item 'Has released': {0}", self.__hasreleased_item.property.path) - # log lastcondition settings - _conditionset_id = self.return_item_by_attribute("se_lastconditionset_item_id") - _conditionset_name = self.return_item_by_attribute("se_lastconditionset_item_name") - if _conditionset_id is not None: - self.__logger.info("Item 'Lastcondition Id': {0}", _conditionset_id.property.path) - if _conditionset_name is not None: - self.__logger.info("Item 'Lastcondition Name': {0}", _conditionset_name.property.path) + if self.__lastconditionset_item_id is not None: + self.__logger.info("Item 'Lastcondition Id': {0}", self.__lastconditionset_item_id.property.path) + if self.__lastconditionset_item_name is not None: + self.__logger.info("Item 'Lastcondition Name': {0}", self.__lastconditionset_item_name.property.path) # log previouscondition settings - _previousconditionset_id = self.return_item_by_attribute("se_previousconditionset_item_id") - _previousconditionset_name = self.return_item_by_attribute("se_previousconditionset_item_name") - if _previousconditionset_id is not None: - self.__logger.info("Item 'Previouscondition Id': {0}", _previousconditionset_id.property.path) - if _previousconditionset_name is not None: - self.__logger.info("Item 'Previouscondition Name': {0}", _previousconditionset_name.property.path) - - _previousstate_conditionset_id = self.return_item_by_attribute("se_previousstate_conditionset_item_id") - _previousstate_conditionset_name = self.return_item_by_attribute("se_previousstate_conditionset_item_name") - if _previousstate_conditionset_id is not None: - self.__logger.info("Item 'Previousstate condition Id': {0}", _previousstate_conditionset_id.property.path) - if _previousstate_conditionset_name is not None: - self.__logger.info("Item 'Previousstate condition Name': {0}", _previousstate_conditionset_name.property.path) - - # log states + if self.__previousconditionset_item_id is not None: + self.__logger.info("Item 'Previouscondition Id': {0}", self.__previousconditionset_item_id.property.path) + if self.__previousconditionset_item_name is not None: + self.__logger.info("Item 'Previouscondition Name': {0}", self.__previousconditionset_item_name.property.path) + + if self.__previousstate_conditionset_item_id is not None: + self.__logger.info("Item 'Previousstate condition Id': {0}", self.__previousstate_conditionset_item_id.property.path) + if self.__previousstate_conditionset_item_name is not None: + self.__logger.info("Item 'Previousstate condition Name': {0}", + self.__previousstate_conditionset_item_name.property.path) + + self.__init_releasedby() + for state in self.__states: - # Update Releasedby Dict - self.update_releasedby(state) + # log states state.write_to_log() self._initstate = None + filtered_dict = {key: value for key, value in self.__config_issues.items() if value.get('issue') not in [[], None]} + self.__config_issues = filtered_dict + # endregion # region Methods for CLI commands ********************************************************************************** @@ -1099,10 +1587,14 @@ def cli_detail(self, handler): triggers = self.__verbose_triggers() handler.push("AutoState Item {0}:\n".format(self.id)) handler.push("\tCurrent state: {0} ('{1}')\n".format(self.get_laststate_id(), self.get_laststate_name())) - handler.push("\tCurrent conditionset: {0} ('{1}')\n".format(self.get_lastconditionset_id(), self.get_lastconditionset_name())) - handler.push("\tPrevious state: {0} ('{1}')\n".format(self.get_previousstate_id(), self.get_previousstate_name())) - handler.push("\tPrevious state conditionset: {0} ('{1}')\n".format(self.get_previousstate_conditionset_id(), self.get_previousstate_conditionset_name())) - handler.push("\tPrevious conditionset: {0} ('{1}')\n".format(self.get_previousconditionset_id(), self.get_previousconditionset_name())) + handler.push("\tCurrent conditionset: {0} ('{1}')\n".format(self.get_lastconditionset_id(), + self.get_lastconditionset_name())) + handler.push( + "\tPrevious state: {0} ('{1}')\n".format(self.get_previousstate_id(), self.get_previousstate_name())) + handler.push("\tPrevious state conditionset: {0} ('{1}')\n".format(self.get_previousstate_conditionset_id(), + self.get_previousstate_conditionset_name())) + handler.push("\tPrevious conditionset: {0} ('{1}')\n".format(self.get_previousconditionset_id(), + self.get_previousconditionset_name())) handler.push(self.__startup_delay.get_text("\t", "\n")) handler.push("\tCycle: {0}\n".format(cycles)) handler.push("\tCron: {0}\n".format(crons)) @@ -1214,7 +1706,7 @@ def __startup_delay_callback(self, item, caller=None, source=None, dest=None): if not self.__ab_alive and self.__se_plugin.scheduler_get(scheduler_name): next_run = self.__shtime.now() + datetime.timedelta(seconds=3) self.__logger.debug( - "Startup Delay over but StateEngine Plugin not running yet. Will try again at {}".format(next_run)) + "Startup Delay over but StateEngine Plugin not running yet. Will try again at {}", next_run) self.__se_plugin.scheduler_change(scheduler_name, next=next_run) self.__se_plugin.scheduler_trigger(scheduler_name) else: @@ -1239,34 +1731,39 @@ def __startup_delay_callback(self, item, caller=None, source=None, dest=None): # - item_id = "..threedots" will return item "my.threedots" # - item_id = "..threedots.further.down" will return item "my.threedots.further.down" def return_item(self, item_id): + _issue = None if isinstance(item_id, (StateEngineStruct.SeStruct, self.__itemClass)): - return item_id + return item_id, None if isinstance(item_id, StateEngineState.SeState): - return self.itemsApi.return_item(item_id.id) + return self.itemsApi.return_item(item_id.id), None if item_id is None: - return None + _issue = "item_id is None" + return None, _issue if not isinstance(item_id, str): - self.__logger.info("'{0}' should be defined as string. Check your item config! " - "Everything might run smoothly, nevertheless.".format(item_id)) - return item_id + _issue = "'{0}' is not defined as string.".format(item_id) + self.__logger.info("{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)) + # self.__logger.debug("Creating struct for id {}".format(item_id)) item = StateEngineStructs.create(self, item_id) except Exception as e: - self.__logger.error("struct {} creation failed. Error: {}".format(item_id, e)) + _issue = "Struct {} creation failed. Error: {}".format(item_id, e) + self.__logger.error(_issue) if item is None: - self.__logger.warning("Item '{0}' not found!".format(item_id)) - return item + _issue = "Item '{0}' in struct not found.".format(item_id) + self.__logger.warning(_issue) + return item, _issue if not item_id.startswith("."): item = self.itemsApi.return_item(item_id) if item is None: - self.__logger.warning("Item '{0}' not found!".format(item_id)) - return item - self.__logger.debug("Testing for relative item declaration {}".format(item_id)) + _issue = "Item '{0}' not found.".format(item_id) + self.__logger.warning(_issue) + return item, _issue + self.__logger.debug("Testing for relative item declaration {}", item_id) parent_level = 0 for c in item_id: if c != '.': @@ -1286,15 +1783,19 @@ def return_item(self, item_id): result += "." + rel_item_id item = self.itemsApi.return_item(result) if item is None: - self.__logger.warning("Determined item '{0}' does not exist.".format(result)) + _issue = "Determined item '{0}' does not exist.".format(item_id) + self.__logger.warning(_issue) else: - self.__logger.develop("Determined item '{0}' for id {1}.".format(item.id, item_id)) - return item + self.__logger.develop("Determined item '{0}' for id {1}.", item.id, item_id) + return item, _issue # Return an item related to the StateEngine object item # attribute: Name of the attribute of the StateEngine object item, which contains the item_id to read def return_item_by_attribute(self, attribute): if attribute not in self.__item.conf: - self.__logger.warning("Problem with attribute '{0}'.".format(attribute)) - return None - return self.return_item(self.__item.conf[attribute]) + _issue = {attribute: {'issue': 'Attribute missing in stateeninge configuration.'}} + self.__logger.warning("Attribute '{0}' missing in stateeninge configuration.", attribute) + return None, _issue + _returnvalue, _issue = self.return_item(self.__item.conf[attribute]) + _issue = {attribute: {'issue': _issue}} + return _returnvalue, _issue diff --git a/stateengine/StateEngineLogger.py b/stateengine/StateEngineLogger.py index 8ce03712d..6e56af86f 100755 --- a/stateengine/StateEngineLogger.py +++ b/stateengine/StateEngineLogger.py @@ -27,64 +27,105 @@ class SeLogger: + @property + def default_log_level(self): + return SeLogger.__default_log_level.get() + + @default_log_level.setter + def default_log_level(self, value): + SeLogger.__default_log_level = value + + @property + def startup_log_level(self): + return SeLogger.__startup_log_level.get() + + @startup_log_level.setter + def startup_log_level(self, value): + SeLogger.__startup_log_level = value + + @property + def log_maxage(self): + return SeLogger.__log_maxage.get() + + @log_maxage.setter + def log_maxage(self, value): + try: + SeLogger.__log_maxage = int(value) + except ValueError: + SeLogger.__log_maxage = 0 + logger = StateEngineDefaults.logger + logger.error("The maximum age of the log files has to be an int number.") + + @property + def using_default_log_level(self): + return self.__using_default_log_level + + @using_default_log_level.setter + def using_default_log_level(self, value): + self.__using_default_log_level = value + + @property + def name(self): + return self.__name + # Set global log level # loglevel: current loglevel - @staticmethod - def set_loglevel(loglevel): + @property + def log_level(self): + return self.__log_level.get() + + @log_level.setter + def log_level(self, value): try: - SeLogger.__loglevel = int(loglevel) + self.__log_level = int(value) except ValueError: - SeLogger.__loglevel = 0 + self.__log_level = 0 logger = StateEngineDefaults.logger logger.error("Loglevel has to be an int number!") - # Set log directory - # logdirectory: Target directory for StateEngine log files + @property + def log_directory(self): + return SeLogger.__log_directory + + @log_directory.setter + def log_directory(self, value): + SeLogger.__log_directory = value + @staticmethod - def set_logdirectory(logdirectory): - SeLogger.__logdirectory = logdirectory + def init(sh): + SeLogger.__sh = sh # Create log directory # logdirectory: Target directory for StateEngine log files @staticmethod - def create_logdirectory(base, log_directory): + def manage_logdirectory(base, log_directory, create=True): if log_directory[0] != "/": if base[-1] != "/": base += "/" log_directory = base + log_directory - if not os.path.exists(log_directory): + if create is True and not os.path.isdir(log_directory): os.makedirs(log_directory) return log_directory - # Set max age for log files - # logmaxage: Maximum age for log files (days) - @staticmethod - def set_logmaxage(logmaxage): - try: - SeLogger.__logmaxage = int(logmaxage) - except ValueError: - SeLogger.__logmaxage = 0 - logger = StateEngineDefaults.logger - logger.error("The maximum age of the log files has to be an int number.") # Remove old log files (by scheduler) @staticmethod def remove_old_logfiles(): - if SeLogger.__logmaxage == 0: + if SeLogger.log_maxage.get() == 0 or not os.path.isdir(str(SeLogger.log_directory)): return logger = StateEngineDefaults.logger - logger.info("Removing logfiles older than {0} days".format(SeLogger.__logmaxage)) + logger.info("Removing logfiles older than {0} days".format(SeLogger.log_maxage)) count_success = 0 count_error = 0 now = datetime.datetime.now() - for file in os.listdir(SeLogger.__logdirectory): + for file in os.listdir(str(SeLogger.log_directory)): if file.endswith(".log"): try: - abs_file = os.path.join(SeLogger.__logdirectory, file) + abs_file = os.path.join(str(SeLogger.log_directory), file) stat = os.stat(abs_file) mtime = datetime.datetime.fromtimestamp(stat.st_mtime) age_in_days = (now - mtime).total_seconds() / 86400.0 - if age_in_days > SeLogger.__logmaxage: + if age_in_days > SeLogger.log_maxage.get(): os.unlink(abs_file) count_success += 1 except Exception as ex: @@ -102,33 +143,30 @@ def create(item): # Constructor # item: item for which the detailed log is (used as part of file name) def __init__(self, item): - #self.logger = StateEngineDefaults.se_logger self.logger = logging.getLogger('stateengine.{}'.format(item.property.path)) + self.__name = 'stateengine.{}'.format(item.property.path) self.__section = item.property.path.replace(".", "_").replace("/", "") self.__indentlevel = 0 - self.__loglevel = StateEngineDefaults.log_level - self.__logmaxage = StateEngineDefaults.log_maxage + self.__default_log_level = None + self.__startup_log_level = None + self.__log_level = None + self.__using_default_log_level = False + self.__logmaxage = None self.__date = None self.__logerror = False self.__filename = "" self.update_logfile() - # override log level for specific items by using se_log_level attribute - def override_loglevel(self, loglevel, item=None): - self.__loglevel = loglevel.get() - if self.__loglevel != StateEngineDefaults.log_level: - self.logger.info("Loglevel for item {0} got individually set to {1}.".format(item.property.path, self.__loglevel)) - # get current log level of abitem def get_loglevel(self): - return self.__loglevel + return self.log_level.get() # Update name logfile if required def update_logfile(self): if self.__date == datetime.datetime.today() and self.__filename is not None: return self.__date = str(datetime.date.today()) - self.__filename = str(SeLogger.__logdirectory + self.__date + '-' + self.__section + ".log") + self.__filename = f"{SeLogger.log_directory}{self.__date}-{self.__section}.log" # Increase indentation level # by: number of levels to increase @@ -148,9 +186,16 @@ def decrease_indent(self, by=1): # text: text to log def log(self, level, text, *args): # Section given: Check level - if level <= self.__loglevel: + _log_level = self.get_loglevel() + if _log_level <= -1: + self.using_default_log_level = True + _log_level = SeLogger.default_log_level.get() + else: + self.using_default_log_level = False + if level <= _log_level: indent = "\t" * self.__indentlevel - text = text.format(*args) + if args: + text = text.format(*args) logtext = "{0}{1} {2}\r\n".format(datetime.datetime.now(), indent, text) try: with open(self.__filename, mode="a", encoding="utf-8") as f: @@ -176,7 +221,9 @@ def info(self, text, *args): self.log(1, text, *args) indent = "\t" * self.__indentlevel text = '{}{}'.format(indent, text) - self.logger.info(text.format(*args)) + if args: + text = text.format(*args) + self.logger.info(text) # log with level=debug # text: text to log @@ -185,7 +232,9 @@ def debug(self, text, *args): self.log(2, text, *args) indent = "\t" * self.__indentlevel text = '{}{}'.format(indent, text) - self.logger.debug(text.format(*args)) + if args: + text = text.format(*args) + self.logger.debug(text) # log with level=develop # text: text to log @@ -194,7 +243,9 @@ def develop(self, text, *args): self.log(3, "DEV: " + text, *args) indent = "\t" * self.__indentlevel text = '{}{}'.format(indent, text) - self.logger.log(StateEngineDefaults.VERBOSE, text.format(*args)) + if args: + text = text.format(*args) + self.logger.log(StateEngineDefaults.VERBOSE, text) # log warning (always to main smarthome.py log) # text: text to log @@ -204,7 +255,9 @@ def warning(self, text, *args): self.log(1, "WARNING: " + text, *args) indent = "\t" * self.__indentlevel text = '{}{}'.format(indent, text) - self.logger.warning(text.format(*args)) + if args: + text = text.format(*args) + self.logger.warning(text) # log error (always to main smarthome.py log) # text: text to log @@ -214,7 +267,9 @@ def error(self, text, *args): self.log(1, "ERROR: " + text, *args) indent = "\t" * self.__indentlevel text = '{}{}'.format(indent, text) - self.logger.error(text.format(*args)) + if args: + text = text.format(*args) + self.logger.error(text) # log exception (always to main smarthome.py log' # msg: message to log @@ -268,27 +323,35 @@ def header(self, text): # @param text text to log # @param *args parameters for text def info(self, text, *args): - self.logger.info(text.format(*args)) + if args: + text = text.format(*args) + self.logger.info(text) # log with level=debug (always to main smarthomeNG log) # text: text to log # *args: parameters for text def debug(self, text, *args): - self.logger.debug(text.format(*args)) + if args: + text = text.format(*args) + self.logger.debug(text) # log warning (always to main smarthomeNG log) # text: text to log # *args: parameters for text # noinspection PyMethodMayBeStatic def warning(self, text, *args): - self.logger.warning(text.format(*args)) + if args: + text = text.format(*args) + self.logger.warning(text) # log error (always to main smarthomeNG log) # text: text to log # *args: parameters for text # noinspection PyMethodMayBeStatic def error(self, text, *args): - self.logger.error(text.format(*args)) + if args: + text = text.format(*args) + self.logger.error(text) # log exception (always to main smarthomeNG log) # msg: message to log diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index de0e7ee09..e665a0788 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -19,13 +19,16 @@ # You should have received a copy of the GNU General Public License # along with this plugin. If not, see . ######################################################################### -from lib.item.item import Item + from . import StateEngineTools from . import StateEngineConditionSets from . import StateEngineActions from . import StateEngineValue from . import StateEngineStruct + from lib.item import Items +from lib.item.item import Item +from copy import copy # Class representing an object state, consisting of name, conditions to be met and configured actions for state @@ -59,9 +62,61 @@ def text(self): def conditions(self): return self.__conditions + # Return orphaned definitions + @property + def unused_attributes(self): + return self.__unused_attributes + + # Return used definitions + @property + def used_attributes(self): + return self.__used_attributes + + # Return used definitions + @property + def action_status(self): + return self.__action_status + + # Return releasedby information @property def releasedby(self): - return self.__release.get() + return self.__releasedby.get() + + @releasedby.setter + def releasedby(self, value): + self.__releasedby.set(value) + + @property + def can_release(self): + return self.__can_release.get() + + @can_release.setter + def can_release(self, value): + self.__can_release.set(value) + + @property + def has_released(self): + return self.__has_released.get() + + @has_released.setter + def has_released(self, value): + self.__has_released.set(value) + + @property + def was_releasedby(self): + return self.__was_releasedby.get() + + @was_releasedby.setter + def was_releasedby(self, value): + self.__was_releasedby.set(value) + + @property + def is_copy_for(self): + return self.__is_copy_for.get() + + @is_copy_for.setter + def is_copy_for(self, value): + self.__is_copy_for.set(value) # Constructor # abitem: parent SeItem instance @@ -71,16 +126,23 @@ def __init__(self, abitem, item_state): self.itemsApi = Items.get_instance() self.__item = item_state self.__itemClass = Item + self.__is_copy_for = StateEngineValue.SeValue(self._abitem, "State is a copy to release") try: self.__id = self.__item.property.path self._log_info("Init state {}", self.__id) - except Exception as err: + except Exception as ex: self.__id = None - self._log_info("Problem init state ID of Item {}. {}", self.__item, err) + self._log_info("Problem init state ID of Item {}. {}", self.__item, ex) self.__text = StateEngineValue.SeValue(self._abitem, "State Name", False, "str") self.__use = StateEngineValue.SeValue(self._abitem, "State configuration extension", True, "item") - self.__release = StateEngineValue.SeValue(self._abitem, "State released by", True, "item") + self.__releasedby = StateEngineValue.SeValue(self._abitem, "State can be released by", True, "str") + self.__can_release = StateEngineValue.SeValue(self._abitem, "State can release") + self.__has_released = StateEngineValue.SeValue(self._abitem, "State has released") + self.__was_releasedby = StateEngineValue.SeValue(self._abitem, "State was released by") self.__name = '' + self.__unused_attributes = {} + self.__used_attributes = {} + self.__action_status = {} self.__use_done = [] self.__conditions = StateEngineConditionSets.SeConditionSets(self._abitem) self.__actions_enter_or_stay = StateEngineActions.SeActions(self._abitem) @@ -102,7 +164,10 @@ def can_enter(self): self._log_decrease_indent(10) self._log_info("Check if state '{0}' ('{1}') can be entered:", self.id, self.name) self._log_increase_indent() - result = self.__conditions.one_conditionset_matching() + self.__is_copy_for.write_to_logger() + self.__releasedby.write_to_logger() + self.__can_release.write_to_logger() + result = self.__conditions.one_conditionset_matching(self) self._log_decrease_indent() if result: self._log_info("State {} can be entered", self.id) @@ -119,6 +184,13 @@ def write_to_log(self): self._abitem.set_variable("current.state_name", self.name) self._abitem.set_variable("current.state_id", self.id) self.__text.write_to_logger() + self.__is_copy_for.write_to_logger() + self.__releasedby.write_to_logger() + self.__can_release.write_to_logger() + if self.__use_done: + _log_se_use = self.__use_done[0] if len(self.__use_done) == 1 else self.__use_done + self._log_info("State configuration extended by se_use: {}", _log_se_use) + self._log_info("Updating Web Interface...") self._log_increase_indent() self._abitem.update_webif(self.id, {'name': self.name, @@ -130,10 +202,7 @@ def write_to_log(self): 'leave': False, 'enter': False, 'stay': False}) self._log_decrease_indent() self._log_info("Finished Web Interface Update") - if self.__use_done: - _log_se_use = self.__use_done[0] if len(self.__use_done) == 1 else self.__use_done - self._log_info("State configuration extended by se_use: {}", _log_se_use) - self.__release.write_to_logger() + if self.__conditions.count() > 0: self._log_info("Condition sets to enter state:") self._log_increase_indent() @@ -145,28 +214,28 @@ def write_to_log(self): self._log_increase_indent() self.__actions_enter.write_to_logger() self._log_decrease_indent() - self._abitem.update_webif([self.id, 'actions_enter'], self.__actions_enter.dict_actions) + self._abitem.update_webif([self.id, 'actions_enter'], self.__actions_enter.dict_actions('actions_enter', self.id)) if self.__actions_stay.count() > 0: self._log_info("Actions to perform on stay:") self._log_increase_indent() self.__actions_stay.write_to_logger() self._log_decrease_indent() - self._abitem.update_webif([self.id, 'actions_stay'], self.__actions_stay.dict_actions) + self._abitem.update_webif([self.id, 'actions_stay'], self.__actions_stay.dict_actions('actions_stay', self.id)) if self.__actions_enter_or_stay.count() > 0: self._log_info("Actions to perform on enter or stay:") self._log_increase_indent() self.__actions_enter_or_stay.write_to_logger() self._log_decrease_indent() - self._abitem.update_webif([self.id, 'actions_enter_or_stay'], self.__actions_enter_or_stay.dict_actions) + self._abitem.update_webif([self.id, 'actions_enter_or_stay'], self.__actions_enter_or_stay.dict_actions('actions_enter_or_stay', self.id)) if self.__actions_leave.count() > 0: self._log_info("Actions to perform on leave (instant leave: {})", self._abitem.instant_leaveaction) self._log_increase_indent() self.__actions_leave.write_to_logger() self._log_decrease_indent() - self._abitem.update_webif([self.id, 'actions_leave'], self.__actions_leave.dict_actions) + self._abitem.update_webif([self.id, 'actions_leave'], self.__actions_leave.dict_actions('actions_leave', self.id)) self._abitem.set_variable("current.state_name", "") self._abitem.set_variable("current.state_id", "") self._log_decrease_indent() @@ -186,8 +255,10 @@ def run_enter(self, allow_item_repeat: bool): self._log_increase_indent() self._log_debug("Update web interface enter {}", self.id) self._log_increase_indent() - self._abitem.update_webif([self.id, 'actions_enter_or_stay'], self.__actions_enter_or_stay.dict_actions) - self._abitem.update_webif([self.id, 'actions_enter'], self.__actions_enter.dict_actions) + if self.__actions_enter_or_stay.count() > 0: + self._abitem.update_webif([self.id, 'actions_enter_or_stay'], self.__actions_enter_or_stay.dict_actions('actions_enter_or_stay', self.id)) + if self.__actions_enter.count() > 0: + self._abitem.update_webif([self.id, 'actions_enter'], self.__actions_enter.dict_actions('actions_enter', self.id)) self._log_decrease_indent() self._log_decrease_indent() @@ -206,8 +277,10 @@ def run_stay(self, allow_item_repeat: bool): self._log_increase_indent() self._log_debug("Update web interface stay {}", self.id) self._log_increase_indent() - self._abitem.update_webif([self.id, 'actions_enter_or_stay'], self.__actions_enter_or_stay.dict_actions) - self._abitem.update_webif([self.id, 'actions_stay'], self.__actions_stay.dict_actions) + if self.__actions_enter_or_stay.count() > 0: + self._abitem.update_webif([self.id, 'actions_enter_or_stay'], self.__actions_enter_or_stay.dict_actions('actions_enter_or_stay', self.id)) + if self.__actions_stay.count() > 0: + self._abitem.update_webif([self.id, 'actions_stay'], self.__actions_stay.dict_actions('actions_stay', self.id)) self._log_decrease_indent() self._log_decrease_indent() @@ -224,35 +297,39 @@ def run_leave(self, allow_item_repeat: bool): self._log_increase_indent() self._log_debug("Update web interface leave {}", self.id) self._log_increase_indent() - self._abitem.update_webif([self.id, 'actions_leave'], self.__actions_leave.dict_actions) + if self.__actions_leave.count() > 0: + self._abitem.update_webif([self.id, 'actions_leave'], self.__actions_leave.dict_actions('actions_leave', self.id)) self._log_decrease_indent() self._log_decrease_indent() def refill(self): - cond1 = not self.__use.is_empty() and "eval" in self.__use.get_type() - cond2 = not self.__release.is_empty() and ("eval" in self.__release.get_type() or "item" in self.__release.get_type()) - if cond1 and cond2: - self._log_debug("State {}: se_use attribute including eval and se_released_by " - "attribute including item or eval - updating state conditions and actions", self.__name) - self._log_increase_indent() - self.__fill(self.__item, 0, "refill") - self._log_decrease_indent() - elif cond1: - self._log_debug("State {}: se_use attribute including eval " + cond_refill = not self.__use.is_empty() and ("eval" in self.__use.get_type() or "item" in self.__use.get_type()) + if cond_refill: + 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, "refill") - self._log_decrease_indent() - elif cond2: - self._log_debug("State {}: se_released_by attribute including eval or item " - "- updating released by states", self.__name) - self._log_increase_indent() - self._abitem.update_releasedby(self) + self.__fill(self.__item, 0, "reinit") self._log_decrease_indent() - def update_releasedby_internal(self): - _returnvalue, _returntype = self.__release.set_from_attr(self.__item, "se_released_by") - return _returnvalue, _returntype, self.releasedby + def update_releasedby_internal(self, states=None): + if states == []: + _returnvalue, _returntype, _issue = self.__releasedby.set([None]) + elif states: + self._log_develop("Setting releasedby to {}", states) + _returnvalue, _returntype, _issue = self.__releasedby.set(states) + self._log_develop("returnvalue {}", _returnvalue) + else: + _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]) + elif states: + _returnvalue, _returntype, _issue = self.__can_release.set(states) + else: + _returnvalue, _returntype, _issue = [None], [None], None + return _returnvalue, _returntype, _issue def update_name(self, item_state, recursion_depth=0): # if an item name is given, or if we do not have a name after returning from all recursions, @@ -272,25 +349,88 @@ def update_name(self, item_state, recursion_depth=0): # 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): + #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) + self.__used_attributes.update(used_attributes) + + def update_action_status(action_status, actiontype): + if action_status is None: + return + + for itm, dct in action_status.items(): + if itm not in self.__action_status: + self.__action_status.update({itm: dct}) + + for (itm, dct) in action_status.items(): + issues = dct.get('issue') + if issues: + origin_list = self.__action_status[itm].get('issueorigin', []) + new_list = origin_list.copy() + for i, listitem in enumerate(origin_list): + entry_unknown = {'state': 'unknown', 'action': listitem.get('action')} + entry_unknown2 = {'state': 'unknown', 'action': 'unknown'} + entry_notype = {'state': self.id, 'action': listitem.get('action')} + entry_final = {'state': self.id, 'action': listitem.get('action'), 'type': actiontype} + + if listitem in (entry_unknown, entry_unknown2, entry_notype): + new_list[i] = entry_final + elif entry_final not in origin_list: + new_list.append(entry_final) + + self.__action_status[itm]['issueorigin'] = new_list + + filtered_dict = {} + for key, nested_dict in self.__action_status.items(): + filtered_dict.update({key: {}}) + filtered_dict[key].update({'used in': actiontype}) + filtered_dict[key].update(nested_dict) + #self._log_develop("Add {} to used {}", key, filtered_dict) + self.__used_attributes = copy(filtered_dict) + filtered_dict = {key: value for key, value in self.__action_status.items() if value.get('issue') not in [[], None]} + self.__action_status = filtered_dict + #self._log_develop("Updated action status: {}, updated used {}", self.__action_status, self.__used_attributes) + + if se_use == "reinit": + self._log_develop("Resetting conditions and actions at re-init") + 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: - self.__use.set_from_attr(item_state, "se_use") + _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: - self._log_warning("se_use is set up in a wrong way - ignoring {}", _use) + _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 StateEngineTools.flatten_list(_use) - - for loop, element in enumerate(_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}" @@ -298,24 +438,38 @@ def __fill(self, item_state, recursion_depth, se_use=None): 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 not in cleaned_use_list: + cleaned_use_list.append(_path) if _path is None: - if not isinstance(element, str): - self._log_warning("se_use {} is not valid.", element) + pass elif _fill and element not in self.__use_done: - #self._log_debug("Adding {} again to state fill function.", _name) + self._log_develop("Adding element {} to state fill function.", _name) self.__use_done.append(element) self.__fill(element, recursion_depth + 1, _name) - if "se_released_by" in item_state.conf: - #_release_by_value, _release_by_type = self.__release.set_from_attr(item_state, "se_released_by") - _release_result = self.releasedby - self._log_debug("(fill) State {} has released attribute result: {}", item_state.property.path, _release_result) + + self.__use.set(cleaned_use_list) # Get action sets and condition sets parent_item = item_state.return_parent() @@ -325,47 +479,106 @@ def __fill(self, item_state, recursion_depth, se_use=None): _enter_stay_actioncount = 0 _leave_actioncount = 0 _stay_actioncount = 0 + _actioncount = 0 + _unused_attributes = {} + _used_attributes = {} + # first check all conditions + for child_item in child_items: + child_name = StateEngineTools.get_last_part_of_item_id(child_item) + try: + if child_name == "enter" or child_name.startswith("enter_"): + _conditioncount += 1 + _unused_attributes, _used_attributes = self.__conditions.update(child_name, child_item, parent_item) + if _conditioncount == 1: + self.__unused_attributes = copy(_unused_attributes) + self.__used_attributes = copy(_used_attributes) + for item in self.__unused_attributes.keys(): + if 'issue' in self.__unused_attributes[item].keys(): + if not self.__unused_attributes[item].get('issueorigin'): + self.__unused_attributes[item].update({'issueorigin': []}) + entry = {'state': self.id, 'conditionset': child_name} + if entry not in self.__unused_attributes[item].get('issueorigin'): + self.__unused_attributes[item]['issueorigin'].append(entry) + except ValueError as ex: + raise ValueError("Condition {0} error: {1}".format(child_name, ex)) + + if _conditioncount == 0: + for attribute in parent_item.conf: + func, name = StateEngineTools.partition_strip(attribute, "_") + cond1 = name and name not in self.__used_attributes + cond2 = func == "se_item" or func == "se_eval" or func == "se_status" + cond3 = name not in self.__unused_attributes.keys() + + if cond1 and cond2 and cond3: + self.__unused_attributes.update({name: {}}) + + child_items = item_state.return_children() for child_item in child_items: child_name = StateEngineTools.get_last_part_of_item_id(child_item) try: if child_name == "on_enter": + _actioncount += 1 for attribute in child_item.conf: _enter_actioncount += 1 - self.__actions_enter.update(attribute, child_item.conf[attribute]) + _, _action_status = self.__actions_enter.update(attribute, child_item.conf[attribute]) + if _action_status: + update_action_status(_action_status, 'enter') + update_unused(_used_attributes, 'action', child_name) elif child_name == "on_stay": + _actioncount += 1 for attribute in child_item.conf: _stay_actioncount += 1 - self.__actions_stay.update(attribute, child_item.conf[attribute]) + _, _action_status = self.__actions_stay.update(attribute, child_item.conf[attribute]) + if _action_status: + update_action_status(_action_status, 'stay') + 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 - self.__actions_enter_or_stay.update(attribute, child_item.conf[attribute]) + _, _action_status = self.__actions_enter_or_stay.update(attribute, child_item.conf[attribute]) + if _action_status: + update_action_status(_action_status, 'enter_or_stay') + update_unused(_used_attributes, 'action', child_name) elif child_name == "on_leave": + _actioncount += 1 for attribute in child_item.conf: _leave_actioncount += 1 - self.__actions_leave.update(attribute, child_item.conf[attribute]) - elif child_name == "enter" or child_name.startswith("enter_"): - _conditioncount += 1 - self.__conditions.update(child_name, child_item, parent_item) + _, _action_status = self.__actions_leave.update(attribute, child_item.conf[attribute]) + if _action_status: + update_action_status(_action_status, 'leave') + update_unused(_used_attributes, 'action', child_name) except ValueError as ex: - raise ValueError("Condition {0} error: {1}".format(child_name, 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: - _enter_stay_actioncount += self.__actions_enter_or_stay.update(attribute, item_state.conf[attribute]) or 0 + _result = self.__actions_enter_or_stay.update(attribute, item_state.conf[attribute]) + _enter_stay_actioncount += _result[0] if _result else 0 + _action_status = _result[1] + if _action_status: + update_action_status(_action_status, 'enter_or_stay') _total_actioncount = _enter_actioncount + _stay_actioncount + _enter_stay_actioncount + _leave_actioncount self.update_name(item_state, recursion_depth) - # Complete condition sets and actions at the end if recursion_depth == 0: self.__conditions.complete(item_state) - - self.__actions_enter.complete(item_state, self.__conditions.evals_items) - self.__actions_stay.complete(item_state, self.__conditions.evals_items) - self.__actions_enter_or_stay.complete(item_state, self.__conditions.evals_items) - self.__actions_leave.complete(item_state, self.__conditions.evals_items) - + _action_status = self.__actions_enter.complete(item_state, self.__conditions.evals_items) + if _action_status: + update_action_status(_action_status, 'enter') + _action_status = self.__actions_stay.complete(item_state, self.__conditions.evals_items) + if _action_status: + update_action_status(_action_status, 'stay') + _action_status = self.__actions_enter_or_stay.complete(item_state, self.__conditions.evals_items) + if _action_status: + update_action_status(_action_status, 'enter_or_stay') + _action_status = self.__actions_leave.complete(item_state, self.__conditions.evals_items) + if _action_status: + update_action_status(_action_status, 'leave') + 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 se_use is not None: self._log_debug("Added {} action(s) based on se_use {}. " + _summary, _total_actioncount, se_use, diff --git a/stateengine/StateEngineStruct.py b/stateengine/StateEngineStruct.py index 0da5a4a89..4c6edb69e 100755 --- a/stateengine/StateEngineStruct.py +++ b/stateengine/StateEngineStruct.py @@ -19,9 +19,7 @@ # along with this plugin. If not, see . ######################################################################### from . import StateEngineTools -from . import StateEngineFunctions import collections.abc -#import copy from lib.item import Items @@ -97,7 +95,9 @@ def convert(self): self._struct_rest = struct_rest self.get() except Exception as ex: - raise Exception("Struct {0} conversion error: {1}".format(self.struct_path, ex)) + _issue = "Conversion error: {}".format(ex) + self._abitem.update_issues('struct', {self.struct_path: {'issue': _issue}}) + raise Exception("Struct {} {}".format(self.struct_path, _issue)) def get(self): raise NotImplementedError("Class {} doesn't implement get()".format(self.__class__.__name__)) @@ -120,7 +120,9 @@ def create_parent(self): parent = SeStructParent(self._abitem, self.struct_path, self._global_struct) self._parent_struct = parent except Exception as ex: - raise Exception("Struct {0} create parent error: {1}".format(self.struct_path, ex)) + _issue = "Create parent error: {}".format(ex) + self._abitem.update_issues('struct', {self.struct_path: {'issue': _issue}}) + raise Exception("Struct {} {}".format(self.struct_path, _issue)) def return_parent(self): return self._parent_struct @@ -136,14 +138,17 @@ def create_children(self): c = SeStructChild(self._abitem, '{}.{}'.format(self.struct_path, c), self._global_struct) self._children_structs.append(c) except Exception as ex: - raise Exception("Struct {0} create children error: {1}".format(self.struct_path, ex)) + _issue = "Create children error: {}".format(ex) + self._abitem.update_issues('struct', {self.struct_path: {'issue': _issue}}) + raise Exception("Struct {} {}".format(self.struct_path, _issue)) self.valid_se_use = _se_ok def return_children(self): return self._children_structs def get(self): - _temp_dict = self.dict_get(self._global_struct.get(self._struct) or {}, self._struct_rest, self._global_struct.get(self._struct) or {}) + _temp_dict = self.dict_get(self._global_struct.get(self._struct) or {}, self._struct_rest, + self._global_struct.get(self._struct) or {}) self._full_conf = _temp_dict try: _temp_dict = collections.OrderedDict( @@ -155,9 +160,13 @@ def get(self): self.create_children() self.valid_se_use = True if "se_use" in self._full_conf else self.valid_se_use else: - self._log_error("Item '{}' does not exist in struct {}", self._struct_rest, self._struct) - except Exception as e: - self._log_error("Problem getting struct {}: {}", self._conf, e) + _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: + _issue = "Problem getting struct {}".format(ex) + self._abitem.update_issues('struct', {self.struct_path: {'issue': _issue}}) + self._log_error("Problem getting struct {}: {}", self._conf, ex) self._conf = {} diff --git a/stateengine/StateEngineStructs.py b/stateengine/StateEngineStructs.py index 21add4262..de630d0ab 100755 --- a/stateengine/StateEngineStructs.py +++ b/stateengine/StateEngineStructs.py @@ -19,7 +19,6 @@ # along with this plugin. If not, see . ######################################################################### from . import StateEngineStruct -from lib.item import Items global_struct = {} __allstructs = [] diff --git a/stateengine/StateEngineTools.py b/stateengine/StateEngineTools.py index 86c893378..da3f9c4af 100755 --- a/stateengine/StateEngineTools.py +++ b/stateengine/StateEngineTools.py @@ -19,6 +19,7 @@ # You should have received a copy of the GNU General Public License # along with this plugin. If not, see . ######################################################################### +from . import StateEngineLogger import datetime from ast import literal_eval from lib.item import Items @@ -37,41 +38,45 @@ class SeItemChild: # abitem: parent SeItem instance def __init__(self, abitem): self._abitem = abitem + if self._abitem is None: + self.__logger = StateEngineLogger.SeLoggerDummy() + else: + self.__logger = self._abitem.logger self.se_plugin = abitem.se_plugin self._sh = abitem.sh self._shtime = abitem.shtime # wrapper method for logger.info def _log_info(self, text, *args): - self._abitem.logger.info(text, *args) + self.__logger.info(text, *args) # wrapper method for logger.debug def _log_develop(self, text, *args): - self._abitem.logger.develop(text, *args) + self.__logger.develop(text, *args) # wrapper method for logger.debug def _log_debug(self, text, *args): - self._abitem.logger.debug(text, *args) + self.__logger.debug(text, *args) # wrapper method for logger.warning def _log_warning(self, text, *args): - self._abitem.logger.warning(text, *args) + self.__logger.warning(text, *args) # wrapper method for logger.error def _log_error(self, text, *args): - self._abitem.logger.error(text, *args) + self.__logger.error(text, *args) # wrapper method for logger.exception def _log_exception(self, msg, *args, **kwargs): - self._abitem.logger.exception(msg, *args, **kwargs) + self.__logger.exception(msg, *args, **kwargs) # wrapper method for logger.increase_indent def _log_increase_indent(self, by=1): - self._abitem.logger.increase_indent(by) + self.__logger.increase_indent(by) # wrapper method for logger.decrease_indent def _log_decrease_indent(self, by=1): - self._abitem.logger.decrease_indent(by) + self.__logger.decrease_indent(by) # Find a certain item below a given item. @@ -98,6 +103,8 @@ def get_last_part_of_item_id(item): def parse_relative(evalstr, begintag, endtags): + if begintag == '' and endtags == '': + return evalstr if evalstr.find(begintag+'.') == -1: return evalstr pref = '' @@ -117,7 +124,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: @@ -198,7 +205,10 @@ def cast_str(value): if isinstance(value, str): return value else: - raise ValueError("Can't cast {0} to str!".format(value)) + try: + return str(value) + except Exception: + raise ValueError("Can't cast {0} to str!".format(value)) # cast a value as list. Throws ValueError if cast is not possible diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index 0dfe64255..4f26c950a 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -23,6 +23,7 @@ from . import StateEngineEval from . import StateEngineStruct from . import StateEngineStructs + from lib.item import Items from lib.item.item import Item import re @@ -39,7 +40,10 @@ class SeValue(StateEngineTools.SeItemChild): # allow_value_list: Flag: list of values allowed # value_type: Type of value to preset the cast function (allowed: str, num, bool, time) def __init__(self, abitem, name, allow_value_list=False, value_type=None): - super().__init__(abitem) + try: + super().__init__(abitem) + except Exception: + pass self.__name = name self.__allow_value_list = allow_value_list self.__value = None @@ -49,6 +53,7 @@ def __init__(self, abitem, name, allow_value_list=False, value_type=None): self.__struct = None self.__varname = None self.__template = None + self.__issues = [] self._additional_sources = [] self.itemsApi = Items.get_instance() self.__itemClass = Item @@ -83,9 +88,18 @@ def is_empty(self): # 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): - value = copy.deepcopy(item.conf.get(attribute_name)) or default_value + value = copy.deepcopy(item.conf.get(attribute_name)) if value is not None: - self._log_develop("Processing value {0} from attribute name {1}, reset {2}", value, attribute_name, reset) + _using_default = False + 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 + else: + value = default_value + _using_default = True + self._log_develop("Processing value from attribute name {0}, reset {1}, type {2}: using default value {3}", + attribute_name, reset, value, attr_type) value_list = [] if value is not None and isinstance(value, list) and attr_type is not None: for i, entry in enumerate(value): @@ -94,6 +108,9 @@ def set_from_attr(self, item, attribute_name, default_value=None, reset=True, at else: value_list.append("{}:{}".format(attr_type, entry)) value = value_list + elif value is not None and attr_type is not None: + # update value type correctly based on attr_type + value = "{}:{}".format(attr_type, value) # Convert weird string representation of OrderedDict correctly if isinstance(value, str) and value.startswith("["): value = re.split('(, (?![^(]*\)))', value.strip('][')) @@ -111,9 +128,11 @@ def set_from_attr(self, item, attribute_name, default_value=None, reset=True, at except Exception as ex: pass if value is not None: - self._log_develop("Setting value {0}, attribute name {1}, reset {2}", value, attribute_name, reset) - _returnvalue, _returntype = self.set(value, attribute_name, reset, item) - return _returnvalue, _returntype + 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 def _set_additional(self, _additional_sources): for _use in _additional_sources: @@ -137,7 +156,7 @@ def __resetvalue(self): # 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): - value = copy.deepcopy(value) + #value = copy.deepcopy(value) if reset: self.__resetvalue() if isinstance(value, list): @@ -163,9 +182,11 @@ def set(self, value, name="", reset=True, item=None): value[i] = field_value[i] field_value[i] = value[i] if source[i] not in self.__valid_valuetypes: - self._log_warning("{0} is not a valid value type. Use one of {1} instead. Value '{2}' " + _issue = "{0} is not a valid value type.".format(source[i]) + self.__issues.append(_issue) + self._log_warning("{0} Use one of {1} instead. Value '{2}' " "will be handled the same as the item type, e.g. string, bool, etc.", - source[i], self.__valid_valuetypes, field_value[i]) + _issue, self.__valid_valuetypes, field_value[i]) source[i] = "value" self.__type_listorder.append(source[i]) if source[i] == "value": @@ -186,7 +207,9 @@ def set(self, value, name="", reset=True, item=None): self._log_warning("Removing template {}: {}", field_value[i], ex) val, field_value[i], source[i] = None, None, None else: - self._log_warning("Template with name '{}' does not exist for this SE Item!", field_value[i]) + _issue = "Template with name '{}' does not exist for this SE Item!".format(field_value[i]) + self.__issues.append(_issue) + self._log_warning(_issue) self.__listorder = [i for i in self.__listorder if i != val] source[i], field_value[i], val = None, None, None try: @@ -212,12 +235,14 @@ def set(self, value, name="", reset=True, item=None): self._abitem.updatetemplates(self.__template, None) self._log_warning("Removing template {}: {}", self.__template, ex) else: - self._log_warning("Template with name '{}' does not exist for this SE Item!", self.__template) + _issue = "Template with name '{}' does not exist for this SE Item!".format(self.__template) + self.__issues.append(_issue) + self._log_warning(_issue) self.__listorder = [i for i in self.__listorder if i != value] source, field_value, value = None, None, None try: - cond1 = source.isdigit() - cond2 = field_value.isdigit() + cond1 = source.lstrip('-').replace('.','',1).isdigit() + cond2 = field_value.lstrip('-').replace('.','',1).isdigit() except Exception: cond1 = False cond2 = False @@ -228,9 +253,11 @@ def set(self, value, name="", reset=True, item=None): field_value = source source = "value" if source not in self.__valid_valuetypes: - self._log_warning("{0} is not a valid value type. Use one of {1} instead. Value '{2}' " + _issue = "{0} is not a valid value type.".format(source) + self.__issues.append(_issue) + self._log_warning("{0} Use one of {1} instead. Value '{2}' " "will be handled the same as the item type, e.g. string, bool, etc.", - source, self.__valid_valuetypes, field_value) + _issue, self.__valid_valuetypes, field_value) source = "value" if source == "value": self.__listorder = [field_value] @@ -255,12 +282,14 @@ def set(self, value, name="", reset=True, item=None): s, field_value[i] = StateEngineTools.partition_strip( self._abitem.templates[field_value[i]], ":") else: - self._log_warning("Template with name '{}' does not exist for this SE Item!", - self.__template) + _issue = "Template with name '{}' does not exist for this SE Item!".format( + self.__template) + self.__issues.append(_issue) + self._log_warning(_issue) s = None try: - cond1 = s.isdigit() - cond2 = field_value[i].isdigit() + cond1 = s.lstrip('-').replace('.','',1).isdigit() + cond2 = field_value[i].lstrip('-').replace('.','',1).isdigit() except Exception: cond1 = False cond2 = False @@ -270,10 +299,21 @@ def set(self, value, name="", reset=True, item=None): elif field_value[i] == "": field_value[i] = s s = "value" + 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']: + field_value[i] = True + elif isinstance(field_value[i], str) and field_value[i].lower() in ['false', 'no']: + field_value[i] = False self.__value = [] if self.__value is None else [self.__value] if not isinstance(self.__value, list) else self.__value self.__value.append(None if s != "value" else self.__do_cast(field_value[i])) self.__item = [] if self.__item is None else [self.__item] if not isinstance(self.__item, list) else self.__item - self.__item.append(None if s != "item" else self.__absolute_item(self._abitem.return_item(field_value[i]), field_value[i])) + if s == "item": + _item, _issue = self._abitem.return_item(field_value[i]) + if _issue: + 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 self.__eval.append(None if s != "eval" else field_value[i]) self.__regex = [] if self.__regex is None else [self.__regex] if not isinstance(self.__regex, list) else self.__regex @@ -296,7 +336,11 @@ def set(self, value, name="", reset=True, item=None): self.__varname = self.__varname[0] if len(self.__varname) == 1 else None if len(self.__varname) == 0 else self.__varname else: - self.__item = None if source != "item" else self.__absolute_item(self._abitem.return_item(field_value), field_value) + if source == "item": + _item, _issue = self._abitem.return_item(field_value) + if _issue: + 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 self.__regex = None if source != "regex" else field_value self.__struct = None if source != "struct" else StateEngineStructs.create(self._abitem, field_value) @@ -305,12 +349,20 @@ def set(self, value, name="", reset=True, item=None): 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() + if cond3: + field_value = ast.literal_eval(field_value) + elif isinstance(field_value, str) and field_value.lower() in ['true', 'yes']: + field_value = True + elif isinstance(field_value, str) and field_value.lower() in ['false', 'no']: + field_value = False self.__value = self.__do_cast(field_value) else: self.__value = None + self.__issues = StateEngineTools.flatten_list(self.__issues) self.__listorder = StateEngineTools.flatten_list(self.__listorder) self.__type_listorder = StateEngineTools.flatten_list(self.__type_listorder) - return self.__listorder, self.__type_listorder + return self.__listorder, self.__type_listorder, self.__issues # Set cast function # cast_func: cast function @@ -322,7 +374,7 @@ def set_cast(self, cast_func): def get(self, default=None, originalorder=True): returnvalues = [] try: - _original_listorder = copy.copy(self.__listorder) + _original_listorder = self.__listorder.copy() except Exception as ex: self._log_error("Can not read listorder. Error: {}", ex) originalorder = False @@ -376,14 +428,14 @@ def get_type(self): # Write condition to logger def write_to_logger(self): if self.__template is not None: - self._log_debug("{0}: Using template(s) {1}", self.__name, self.__template) + self._log_info("{0}: Using template(s) {1}", self.__name, self.__template) if self.__value is not None: if isinstance(self.__value, list): for i in self.__value: if i is not None: - self._log_debug("{0}: {1}", self.__name, i) + self._log_debug("{0}: {1} ({2})", self.__name, i, type(i)) else: - self._log_debug("{0}: {1}", self.__name, self.__value) + self._log_debug("{0}: {1} ({2})", self.__name, self.__value, type(self.__value)) if self.__regex is not None: if isinstance(self.__regex, list): for i in self.__regex: @@ -396,18 +448,23 @@ def write_to_logger(self): for i in self.__struct: if i is not None: self._log_debug("{0} from struct: {1}", self.__name, i.property.path) + else: self._log_debug("{0} from struct: {1}", self.__name, self.__struct.property.path) if self.__item is not None: + _original_listorder = self.__listorder.copy() if isinstance(self.__item, list): - for i in self.__item: - if i is not None: - self._log_debug("{0} from item: {1}", self.__name, i.property.path) + for i, item in enumerate(self.__item): + if item is not None: + self._log_debug("{0} from item: {1}", self.__name, item.property.path) + self._log_debug("Currently item results in {}", self.__get_from_item()[i]) else: self._log_debug("{0} from item: {1}", self.__name, self.__item.property.path) + self._log_debug("Currently item results in {}", self.__get_from_item()) + self.__listorder = _original_listorder if self.__eval is not None: self._log_debug("{0} from eval: {1}", self.__name, self.__eval) - _original_listorder = copy.copy(self.__listorder) + _original_listorder = self.__listorder.copy() self._log_debug("Currently eval results in {}", self.__get_eval()) self.__listorder = _original_listorder if self.__varname is not None: @@ -446,9 +503,12 @@ def get_text(self, prefix=None, suffix=None): # returns: value as item or struct def cast_item(self, value): try: - return self._abitem.return_item(value) - except Exception as e: - self._log_error("Can't cast {0} to item/struct! {1}".format(value, e)) + _returnvalue, _issue = self._abitem.return_item(value) + if _issue: + self.__issues.append(_issue) + return _returnvalue + except Exception as ex: + self._log_error("Can't cast {0} to item/struct! {1}".format(value, ex)) return value def __update_item_listorder(self, value, newvalue, id=None): @@ -469,9 +529,10 @@ def __update_item_listorder(self, value, newvalue, id=None): _item_value, newvalue.property.path, self.__listorder) def __absolute_item(self, value, id=None): - if isinstance(value, list): + if value is None: + self.__update_item_listorder(value, value, id) + elif isinstance(value, list): valuelist = [] - idlist = [] if id is None else [id] if not isinstance(id, list) else id for i, element in enumerate(value): element = self.cast_item(element) self.__update_item_listorder(value, element, id[i]) @@ -489,13 +550,14 @@ def __do_cast(self, value, id=None): try: if isinstance(value, list): valuelist = [] - idlist = [] if id is None else [id] if not isinstance(id, list) else id for i, element in enumerate(value): try: _newvalue = element if element == 'novalue' else self.__cast_func(element) except Exception as ex: _newvalue = None - self._log_warning("Problem casting element '{0}' to {1}: {2}.", element, self.__cast_func, ex) + _issue = "Problem casting element '{0}' to {1}: {2}.".format(element, self.__cast_func, ex) + self.__issues.append(_issue) + self._log_warning(_issue) valuelist.append(_newvalue) if element in self.__listorder: self.__listorder[self.__listorder.index(element)] = _newvalue @@ -521,14 +583,20 @@ def __do_cast(self, value, id=None): self.__listorder[self.__listorder.index(_item_value)] = _newvalue except Exception as ex: if any(x in value for x in ['sh.', '_eval', '(']): - raise ValueError("You most likely forgot to prefix your expression with 'eval:'") + _issue = "You most likely forgot to prefix your expression with 'eval:'" + self.__issues.append(_issue) + raise ValueError(_issue) else: - raise ValueError("Not possible to cast: {}".format(ex)) + _issue = "Not possible to cast because {}".format(ex) + self.__issues.append(_issue) + raise ValueError(_issue) if value in self.__listorder: self.__listorder[self.__listorder.index(value)] = _newvalue value = _newvalue except Exception as ex: - self._log_warning("Problem casting '{0}' to {1}: {2}.", value, self.__cast_func, ex) + _issue = "Problem casting '{0}' to {1}: {2}.".format(value, self.__cast_func, ex) + self.__issues.append(_issue) + self._log_warning(_issue) if '_cast_list' in self.__cast_func.__globals__ and self.__cast_func == self.__cast_func.__globals__['_cast_list']: try: _newvalue = StateEngineTools.cast_num(value) @@ -568,9 +636,11 @@ def __get_from_struct(self): if 'struct:{}'.format(self.__struct) in self.__listorder: self.__listorder[self.__listorder.index('struct:{}'.format(self.__struct))] = _newvalue values = _newvalue - except Exception as ex2: + except Exception as ex: values = self.__struct - self._log_info("Problem while getting from struct '{0}': {1}.", values, ex2) + _issue = "Problem while getting from struct '{0}': {1}.".format(values, ex) + self.__issues.append(_issue) + self._log_info(_issue) return values @@ -596,9 +666,11 @@ def __get_from_regex(self): if 'regex:{}'.format(self.__regex) in self.__listorder: self.__listorder[self.__listorder.index('regex:{}'.format(self.__regex))] = _newvalue values = _newvalue - except Exception as ex2: + except Exception as ex: values = self.__regex - self._log_info("Problem while creating regex '{0}': {1}.", values, ex2) + _issue = "Problem while creating regex '{0}': {1}.".format(values, ex) + self.__issues.append(_issue) + self._log_info(_issue) return values # Determine value by executing eval-function @@ -611,7 +683,7 @@ def __get_eval(self): if "stateengine_eval" in self.__eval or "se_eval" in self.__eval: # noinspection PyUnusedLocal stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem) - self._log_debug("Checking eval: {0} from list {1}", self.__eval, self.__listorder) + self._log_debug("Checking eval: {0}", self.__eval) self._log_increase_indent() try: _newvalue = self.__do_cast(eval(self.__eval)) @@ -619,11 +691,13 @@ def __get_eval(self): self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = _newvalue values = _newvalue self._log_decrease_indent() - self._log_debug("Eval result: {0}.", values) + self._log_debug("Eval result: {0} ({1}).", values, type(values)) self._log_increase_indent() except Exception as ex: self._log_decrease_indent() - self._log_warning("Problem evaluating '{0}': {1}.", StateEngineTools.get_eval_name(self.__eval), ex) + _issue = "Problem evaluating '{0}': {1}.".format(StateEngineTools.get_eval_name(self.__eval), ex) + self.__issues.append(_issue) + self._log_warning(_issue) self._log_increase_indent() values = None finally: @@ -652,8 +726,10 @@ def __get_eval(self): self._log_increase_indent() except Exception as ex: self._log_decrease_indent() - self._log_warning("Problem evaluating from list '{0}': {1}.", - StateEngineTools.get_eval_name(val), ex) + _issue = "Problem evaluating from list '{0}': {1}.".format( + StateEngineTools.get_eval_name(val), ex) + self.__issues.append(_issue) + self._log_warning(_issue) self._log_increase_indent() value = None else: @@ -664,7 +740,10 @@ def __get_eval(self): value = _newvalue except Exception as ex: self._log_decrease_indent() - self._log_info("Problem evaluating '{0}': {1}.", StateEngineTools.get_eval_name(val), ex) + _issue = "Problem evaluating '{0}': {1}.".format( + StateEngineTools.get_eval_name(val), ex) + self.__issues.append(_issue) + self._log_info(_issue) value = None if value is not None: values.append(self.__do_cast(value)) @@ -682,7 +761,9 @@ def __get_eval(self): self._log_increase_indent() except Exception as ex: self._log_decrease_indent() - self._log_warning("Problem evaluating '{0}': {1}.", StateEngineTools.get_eval_name(self.__eval), ex) + _issue = "Problem evaluating '{0}': {1}.".format(StateEngineTools.get_eval_name(self.__eval), ex) + self.__issues.append(_issue) + self._log_warning(_issue) self._log_increase_indent() return None @@ -715,38 +796,39 @@ def __get_from_item(self): if 'item:{}'.format(self.__item) in self.__listorder: self.__listorder[self.__listorder.index('item:{}'.format(self.__item))] = _newvalue values = _newvalue - except Exception as ex2: + except Exception as ex: values = self.__item - self._log_info("Problem while reading item '{0}' path: {1}.", values, ex2) + _issue = "Problem while reading item path '{0}': {1}.".format(values, ex) + self.__issues.append(_issue) + self._log_info(_issue) return self.__do_cast(values) # Determine value from variable def __get_from_variable(self): + def update_value(varname): + value = self._abitem.get_variable(varname) + new_value = 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) + new_value = '' + self._log_debug("Checking variable '{0}', value {1} from list {2}", + varname, new_value, self.__listorder) + if 'var:{}'.format(varname) in self.__listorder: + self.__listorder[self.__listorder.index('var:{}'.format(varname))] = new_value + return new_value + + values = [] + if isinstance(self.__varname, list): - values = [] for var in self.__varname: - value = self._abitem.get_variable(var) - _newvalue = self.__do_cast(value) - _newvalue = 'var:{}'.format(self.__varname) if _newvalue == '' else _newvalue - if isinstance(_newvalue, str) and 'Unknown variable' in _newvalue: - self._log_warning("There is a problem with your variable: {}", _newvalue) - _newvalue = '' - values.append(_newvalue) - if 'var:{}'.format(var) in self.__listorder: - self._log_debug("Checking variable in loop '{0}', value {1} from list {2}", - self.__varname, _newvalue, self.__listorder) - self.__listorder[self.__listorder.index('var:{}'.format(var))] = _newvalue + values.append(update_value(var)) + self._log_debug("Checking variable in loop '{0}', value {1} from list {2}", + var, values[-1], self.__listorder) else: - _newvalue = self._abitem.get_variable(self.__varname) - _newvalue = 'var:{}'.format(self.__varname) if _newvalue == '' else _newvalue - if isinstance(_newvalue, str) and 'Unknown variable' in _newvalue: - self._log_warning("There is a problem with your variable: {}", _newvalue) - _newvalue = '' - self._log_debug("Checking variable '{0}', value {1} from list {2}", - self.__varname, _newvalue, self.__listorder) - if 'var:{}'.format(self.__varname) in self.__listorder: - self.__listorder[self.__listorder.index('var:{}'.format(self.__varname))] = _newvalue - values = _newvalue + values = update_value(self.__varname) self._log_debug("Variable result: {0}", values) return values diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index 42b127ac1..84399112a 100755 --- a/stateengine/StateEngineWebif.py +++ b/stateengine/StateEngineWebif.py @@ -38,123 +38,158 @@ def __init__(self, smarthome, abitem): if not REQUIRED_PACKAGE_IMPORTED: self._log_warning("Unable to import Python package 'pydotplus'. Visualizing SE items will not work.") - + self.__img_path = abitem.se_plugin.path_join(abitem.se_plugin.get_plugin_dir(), 'webif/static/img/visualisations') self.__states = abitem.webif_infos + self._abitem = abitem self.__name = abitem.id self.__active_conditionset = abitem.lastconditionset_name self.__active_state = abitem.laststate self.__graph = pydotplus.Dot('StateEngine', graph_type='digraph', splines='false', - overlap='scale', compound='false') + overlap='scale', compound='false', imagepath='{}'.format(self.__img_path)) self.__graph.set_node_defaults(color='lightgray', style='filled', shape='box', fontname='Helvetica', fontsize='10') self.__graph.set_edge_defaults(color='darkgray', style='filled', shape='box', fontname='Helvetica', fontsize='10') self.__nodes = {} self.__scalefactor = 0.1 - self.__textlimit = 145 + self.__textlimit = 105 self.__conditionset_count = 0 def __repr__(self): 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, label_format='table'): - actionlabel = '<' if label_format == 'table' else '<' + 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 _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 + for cond in _condition_check: + try: + _cond = re.compile(cond) + _matching = _cond.fullmatch(conditionset) + except Exception: + _matching = True + _condition_count += 1 if _matching else 0 + _condition = True if _matching else False + return _condition_count, _condition, _condition_check, _condition_necessary + + actionlabel = actionstart = '<
' + action_tooltip = '' originaltype = label_type types = [label_type] if label_type == 'actions_leave' else ['actions_enter_or_stay', label_type] + tooltip_count = 0 for label_type in types: for action in self.__states[state].get(label_type): - _repeat = self.__states[state][label_type][action].get('repeat') - _delay = self.__states[state][label_type][action].get('delay') or 0 + action_dict = self.__states[state][label_type].get(action) + if action_dict.get('actionstatus'): + _success = action_dict['actionstatus'].get('success') + _issue = action_dict['actionstatus'].get('issue') + else: + _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 + condition_necessary = 0 condition_met = True condition_count = 0 - condition_to_meet = self.__states[state][label_type][action].get('conditionset') - condition_to_meet = StateEngineTools.flatten_list(condition_to_meet) - condition_necessary += 1 if condition_to_meet != 'None' else 0 - condition_to_meet = condition_to_meet if isinstance(condition_to_meet, list) else [condition_to_meet] - for cond in condition_to_meet: - try: - cond = re.compile(cond) - matching = cond.fullmatch(conditionset) - except Exception: - matching = True - condition_count += 1 if matching else 0 - condition1 = True if matching else False - - previouscondition_to_meet = self.__states[state][label_type][action].get('previousconditionset') - previouscondition_to_meet = StateEngineTools.flatten_list(previouscondition_to_meet) - condition_necessary += 1 if previouscondition_to_meet != 'None' else 0 - previouscondition_to_meet = previouscondition_to_meet if isinstance(previouscondition_to_meet, list) else [previouscondition_to_meet] - for cond in previouscondition_to_meet: - try: - cond = re.compile(cond) - matching = cond.fullmatch(previousconditionset) - except Exception: - matching = True - condition_count += 1 if matching else 0 - condition2 = True if matching else False - - previousstate_condition_to_meet = self.__states[state][label_type][action].get('previousstate_conditionset') - previousstate_condition_to_meet = StateEngineTools.flatten_list(previousstate_condition_to_meet) - condition_necessary += 1 if previousstate_condition_to_meet != 'None' else 0 - previousstate_condition_to_meet = previousstate_condition_to_meet if isinstance(previousstate_condition_to_meet, list) else [previousstate_condition_to_meet] - for cond in previousstate_condition_to_meet: - try: - cond = re.compile(cond) - matching = cond.fullmatch(previousstate_conditionset) - except Exception: - matching = True - condition_count += 1 if matching else 0 - condition3 = True if matching else False + count, condition1, condition_to_meet, necessary = _check_webif_conditions(action_dict, 'conditionset', conditionset) + condition_count += count + condition_necessary += necessary + count, condition2, previouscondition_to_meet, necessary = _check_webif_conditions(action_dict, 'previousconditionset', previousconditionset) + condition_count += count + condition_necessary += necessary + count, condition3, previousstate_condition_to_meet, necessary = _check_webif_conditions(action_dict, 'previousstate_conditionset', previousstate_conditionset) + condition_count += count + condition_necessary += 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 - fontcolor = "white" if cond1 and cond2 and (not condition_met or (_repeat is False and originaltype == 'actions_stay'))\ - else "#5c5646" if _delay > 0 else "darkred" if _delay < 0 else "black" + cond_delta = float(_delta) < float(_mindelta) + fontcolor = "white" if cond1 and cond2 and (cond_delta or\ + (not condition_met or (_repeat is False and originaltype == 'actions_stay')))\ + else "#5c5646" if _delay > 0 else "darkred" if _delay < 0 \ + else "#303030" if not condition_met 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 "" - additionaltext = " ({} not met)".format(condition_info) if not condition_met\ + if _issue: + if tooltip_count > 0: + action_tooltip += ' ' + tooltip_count += 1 + action_tooltip += '{}'.format(_issue) if _issue is not None else '' + + additionaltext = " (issue: see tooltip)" if _issue is not None\ + else " ({} not met)".format(condition_info) if not condition_met\ else " (no repeat)" if _repeat is False and originaltype == 'actions_stay'\ else " (delay: {})".format(_delay) if _delay > 0\ - else " (wrong delay!)" if _delay < 0 else "" - action1 = self.__states[state][label_type][action].get('function') + else " (wrong delay!)" if _delay < 0\ + else " (delta {} < {})".format(_delta, _mindelta) if cond_delta and cond1 and cond2\ + else "" + action1 = action_dict.get('function') if action1 == 'set': - action2 = self.__states[state][label_type][action].get('item') - value_check = self.__states[state][label_type][action].get('value') + action2 = action_dict.get('item') + value_check = action_dict.get('value') value_check = '""' if value_check == "" else value_check + is_number = value_check.lstrip('-').replace('.','',1).isdigit() + if is_number and "." in value_check: + value_check = round(float(value_check), 2) action3 = 'to {}'.format(value_check) elif action1 == 'special': - action2 = self.__states[state][label_type][action].get('special') - action3 = self.__states[state][label_type][action].get('value') + action2 = action_dict.get('special') + action3 = action_dict.get('value') else: action2 = 'None' - action3 = '' - if label_format == 'table' and not action2 == 'None': - actionlabel += '' \ - ''.format(fontcolor, action1, action2, action3) - elif not action2 == 'None': - actionlabel += '  {} {} {}{}  
'.format( - fontcolor, action1, action2, action3, additionaltext) - actionlabel += '
{}{}{}
>' if label_format == 'table' else '>' - actionlabel = '' if actionlabel == '<
>' or actionlabel == '<>' else actionlabel + action3 = "" + cond1 = conditionset in ['', self.__active_conditionset] and state == self.__active_state + cond_enter = originaltype == 'actions_enter' and self.__states[state].get('enter') is True + cond_stay = originaltype == 'actions_stay' and self.__states[state].get('stay') is True + active = True if (cond_enter or cond_stay) and cond1 else False + success_info = '' \ + if _issue is not None and active \ + else '' \ + if (_success == 'False' or not condition_met) and active \ + else '' \ + if _success == 'True' and active \ + else '' + if not action2 == 'None': + actionlabel += '{} {} {} {}'.format(fontcolor, action1, action2, action3, additionaltext) + actionlabel += '{}'.format(success_info) + + actionlabel += '>' + actionend = '>' + actionlabel = '' if actionlabel == '{}{}'.format(actionstart, actionend)\ + or actionlabel == '<>' else actionlabel #self._log_debug('actionlabel: {}', actionlabel) - return actionlabel + return actionlabel, action_tooltip, tooltip_count - def _conditionlabel(self, state, conditionset): + def _conditionlabel(self, state, conditionset, i): condition_tooltip = '' + conditions_done = [] _empty_set = self.__states[state]['conditionsets'].get(conditionset) == '' if _empty_set: - return '', '' - conditionlist = '<' - + return '', '', 0 + conditionlist = '<
' + tooltip_count = 0 for k, condition in enumerate(self.__states[state]['conditionsets'].get(conditionset)): condition_dict = self.__states[state]['conditionsets'][conditionset].get(condition) - item_none = condition_dict.get('item') == 'None' + current = condition_dict.get('current') + match = condition_dict.get('match') + + item_none = str(condition_dict.get('item')) == 'None' + status_none = str(condition_dict.get('status')) == 'None' eval_none = condition_dict.get('eval') == 'None' value_none = condition_dict.get('value') == 'None' min_none = condition_dict.get('min') == 'None' @@ -163,6 +198,7 @@ def _conditionlabel(self, state, conditionset): agemax_none = condition_dict.get('agemax') == 'None' changedby_none = condition_dict.get('changedby') == 'None' updatedby_none = condition_dict.get('updatedby') == 'None' + triggeredby_none = condition_dict.get('triggeredby') == 'None' for compare in condition_dict: cond1 = not condition_dict.get(compare) == 'None' @@ -172,23 +208,70 @@ def _conditionlabel(self, state, conditionset): cond5 = not compare == 'agenegate' cond6 = not compare == 'changedbynegate' cond7 = not compare == 'updatedbynegate' - if cond1 and cond2 and cond3 and cond4 and cond5 and cond6 and cond7: + cond8 = not compare == 'triggeredbynegate' + cond9 = not compare == 'status' + cond10 = not compare == 'current' + cond11 = not compare == 'match' + if cond1 and cond2 and cond3 and cond4 and cond5 and cond6 and cond7 and cond8 and cond9 and cond10 and cond11: + try: + list_index = list(self.__states.keys()).index(self.__active_state) + except Exception: + list_index = 0 + 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) + conditions_done.append(condition) conditionlist += '' + info_status = str(condition_dict.get('status') or '') + info_item = str(condition_dict.get('item') or '') + info_eval = str(condition_dict.get('eval') or '') + info_compare = str(condition_dict.get(compare) or '') + if not status_none: + textlength = len(info_status) + if textlength > self.__textlimit: + if tooltip_count > 0: + condition_tooltip += ' ' + tooltip_count += 1 + condition_tooltip += '{}'.format(condition_dict.get('status')) + elif not item_none: + textlength = len(info_item) + if textlength > self.__textlimit: + if tooltip_count > 0: + condition_tooltip += ' ' + tooltip_count += 1 + condition_tooltip += '{}'.format(condition_dict.get('item')) + elif not eval_none: + textlength = len(info_eval) + if textlength > self.__textlimit: + if tooltip_count > 0: + condition_tooltip += ' ' + tooltip_count += 1 + condition_tooltip += '{}'.format(condition_dict.get('eval')) + else: + textlength = 0 + + info_item = info_item[:self.__textlimit] + '..  ' * int(textlength > self.__textlimit) + info_status = info_status[:self.__textlimit] + '..  ' * int(textlength > self.__textlimit) + info_eval = info_eval[:self.__textlimit] + '..  ' * int(textlength > self.__textlimit) + info_value = info_compare[:self.__textlimit] + '..  ' * \ + int(len(info_compare) > self.__textlimit) + textlength = len(info_compare) + if textlength > self.__textlimit: + if tooltip_count > 0: + condition_tooltip += ' ' + tooltip_count += 1 + condition_tooltip += '{}'.format(condition_dict.get(compare)) + + if not status_none: + info = info_status + elif not item_none: + info = info_item + elif not eval_none: + info = info_eval + else: + info = "" + conditionlist += '{}'.format(info) comparison = ">=" if not min_none and compare == "min"\ else "<=" if not max_none and compare == "max"\ else "older" if not agemin_none and compare == "agemin"\ @@ -199,23 +282,36 @@ def _conditionlabel(self, state, conditionset): else "not updated by" if (not updatedby_none and compare == "updatedby" 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')\ + else "triggered by" if not triggeredby_none and compare == "triggeredby"\ else "!=" if (not value_none and compare == "value" and condition_dict.get('negate') == 'True')\ else "==" + + match_info = '' + if match and len(match) > 0: + match_info = match.get('value') if compare in ["min", "max", "value"]\ + else match.get('age') if compare in ["agemin", "agemax", "age"]\ + else match.get(compare) conditionlist += '' - conditionlist += '
{}:{}
' - textlength = len(str(condition_dict.get('item'))) - condition_tooltip += '{} '.format(condition_dict.get('item')) \ - if textlength > self.__textlimit else '' - info_item = str(condition_dict.get('item'))[:self.__textlimit] + '..  ' * (textlength > self.__textlimit) - info_eval = str(condition_dict.get('eval'))[:self.__textlimit] + '..  ' * (textlength > self.__textlimit) - info_value = str(condition_dict.get(compare))[:self.__textlimit] + '..  ' * \ - (len(str(condition_dict.get(compare))) > self.__textlimit) - info = info_eval if info_item == "None" and info_eval != "None" else info_item - conditionlist += '{}'.format(info) if not item_none else '' - textlength = len(str(condition_dict.get('eval'))) - condition_tooltip += '{} '.format(condition_dict.get('eval')) \ - if textlength > self.__textlimit else '' - info = info_value if info_item == "None" and info_eval != "None" else info_eval - conditionlist += '{}'.format(info) if not eval_none and item_none else '' - conditionlist += '{}'.format(comparison) - conditionlist += '"{}"'.format(info) if not item_none and not eval_none else '' - textlength = len(str(condition_dict.get(compare))) - condition_tooltip += '{} '.format(condition_dict.get(compare)) \ - if textlength > self.__textlimit else '' + conditionlist += '"{}"'.format(info) if not item_none and not status_none and not eval_none else '' + info = info_value conditionlist += '{}'.format(info) if not condition_dict.get(compare) == 'None' and ( - (eval_none and not item_none) or (not eval_none and item_none)) else '' + (eval_none and not item_none) or (eval_none and not status_none) or \ + (not eval_none and item_none) or (not eval_none and status_none)) else '' conditionlist += ' (negate)' if condition_dict.get('negate') == 'True' and "age" \ not in compare and not compare == "value" else '' conditionlist += ' (negate)' if condition_dict.get('agenegate') == 'True' and "age" in compare else '' - conditionlist += '
>' - return conditionlist, condition_tooltip + active = i < list_index or (i == list_index and conditionset in ['', self.__active_conditionset]) + match_info = '' if match_info == 'yes' and active\ + else '' if match_info == 'no' and active\ + else '' if match_info and len(match_info) > 0 and active\ + else '' + conditionlist += '{}'.format(match_info) + conditionlist += '>' + return conditionlist, condition_tooltip, tooltip_count def _add_actioncondition(self, state, conditionset, action_type, new_y, cond1, cond2): cond4 = conditionset in ['', self.__active_conditionset] and state == self.__active_state @@ -223,9 +319,9 @@ def _add_actioncondition(self, state, conditionset, action_type, new_y, cond1, c cond_enter = action_type == 'actions_enter' and self.__states[state].get('enter') is False cond_stay = action_type == 'actions_stay' and self.__states[state].get('stay') is False color_enter = "gray" if (cond1 and cond2 and cond5) or \ - (cond_enter and cond4 and cond5) else "chartreuse3" if cond4 else "indianred2" + (cond_enter and cond4 and cond5) else "olivedrab" if cond4 else "indianred2" color_stay = "gray" if (cond1 and cond2 and cond5) or \ - (cond_stay and cond4 and cond5) else "chartreuse3" if cond4 else "indianred2" + (cond_stay and cond4 and cond5) else "olivedrab" if cond4 else "indianred2" label = 'first enter' if action_type == 'actions_enter' else 'staying at state' @@ -272,8 +368,12 @@ def drawgraph(self, filename): self.__conditionset_count = len(self.__states[state].get('conditionsets')) if self.__conditionset_count == 0: self.__states[state]['conditionsets'][''] = '' - color = "chartreuse3" if state == self.__active_state \ - else "gray" if i > list(self.__states.keys()).index(self.__active_state) else "indianred2" + try: + list_index = list(self.__states.keys()).index(self.__active_state) + except Exception: + list_index = 0 + color = "olivedrab" if state == self.__active_state \ + else "gray" if i > list_index else "indianred2" new_y -= 1 * self.__scalefactor position = '{},{}!'.format(0, new_y) @@ -312,24 +412,30 @@ def drawgraph(self, filename): actionlist_stay = '' actionlist_leave = '' condition_tooltip = '' + action_tooltip = '' j = 0 new_x = 0.9 + actions_enter = self.__states[state].get('actions_enter') or [] + 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 [] for j, conditionset in enumerate(self.__states[state]['conditionsets']): - if len(self.__states[state].get('actions_enter')) > 0 or len(self.__states[state].get('actions_enter_or_stay')) > 0: - actionlist_enter = self._actionlabel(state, 'actions_enter', conditionset, previousconditionset, previousstate_conditionset, 'list') - if len(self.__states[state].get('actions_stay')) > 0 or len(self.__states[state].get('actions_enter_or_stay')) > 0: - actionlist_stay = self._actionlabel(state, 'actions_stay', conditionset, previousconditionset, previousstate_conditionset, 'list') + if len(actions_enter) > 0 or len(actions_enter_or_stay) > 0: + actionlist_enter, action_tooltip_enter, action_tooltip_count_enter = self._actionlabel(state, 'actions_enter', conditionset, previousconditionset, previousstate_conditionset) - if len(self.__states[state].get('actions_leave')) > 0: - actionlist_leave = self._actionlabel(state, 'actions_leave', conditionset, previousconditionset, previousstate_conditionset, 'list') + if len(actions_stay) > 0 or len(actions_enter_or_stay) > 0: + actionlist_stay, action_tooltip_stay, action_tooltip_count_stay = self._actionlabel(state, 'actions_stay', conditionset, previousconditionset, previousstate_conditionset) + + if len(actions_leave) > 0: + actionlist_leave, action_tooltip_leave, action_tooltip_count_leave = self._actionlabel(state, 'actions_leave', conditionset, previousconditionset, previousstate_conditionset) new_y -= 1 * self.__scalefactor if j == 0 else 2 * self.__scalefactor position = '{},{}!'.format(0.5, new_y) conditionset_positions.append(new_y) #self._log_debug('conditionset: {} {}, previous {}', conditionset, position, previous_conditionset) - conditionlist, condition_tooltip = self._conditionlabel(state, conditionset) + conditionlist, condition_tooltip, condition_tooltip_count = self._conditionlabel(state, conditionset, i) cond3 = conditionset == '' try: cond1 = i >= list(self.__states.keys()).index(self.__active_state) @@ -348,7 +454,7 @@ def drawgraph(self, filename): except Exception as ex: #self._log_debug('Condition 2 problem {}'.format(ex)) cond2 = False if cond3 and cond4 else True - color = "gray" if cond1 and cond2 else "chartreuse3" \ + color = "gray" if cond1 and cond2 else "olivedrab" \ if (conditionset == self.__active_conditionset or cond3) and state == self.__active_state else "indianred2" label = 'no condition' if conditionset == '' else conditionset self.__nodes['{}_{}'.format(state, conditionset)] = pydotplus.Node( @@ -356,31 +462,41 @@ def drawgraph(self, filename): label=label, pos=position) #self._log_debug('Node {} {} drawn', state, conditionset) position = '{},{}!'.format(0.2, new_y) + xlabel = '1 tooltip' if condition_tooltip_count == 1\ + else '{} tooltips'.format(condition_tooltip_count)\ + if condition_tooltip_count > 1 else '' if not conditionlist == '': self.__nodes['{}_{}_conditions'.format(state, conditionset)] = pydotplus.Node( '{}_{}_conditions'.format(state, conditionset), style="filled", fillcolor=color, - shape="rect", label=conditionlist, pos=position, tooltip=condition_tooltip) + shape="rect", label=conditionlist, pos=position, tooltip=condition_tooltip, xlabel=xlabel) self.__graph.add_node(self.__nodes['{}_{}_conditions'.format(state, conditionset)]) - self.__graph.add_node(self.__nodes['{}_{}'.format(state, conditionset)]) new_x = 0.9 if not actionlist_enter == '': position = '{},{}!'.format(new_x, new_y) + xlabel = '1 tooltip' if action_tooltip_count_enter == 1\ + else '{} tooltips'.format(action_tooltip_count_enter)\ + if action_tooltip_count_enter > 1 else '' #self._log_debug('action enter: {}', position) self.__nodes['{}_{}_actions_enter'.format(state, conditionset)] = pydotplus.Node( '{}_{}_actions_enter'.format(state, conditionset), style="filled", fillcolor=color, - shape="rectangle", label=actionlist_enter, pos=position) + shape="rectangle", label=actionlist_enter, pos=position, tooltip=action_tooltip_enter, + xlabel=xlabel) self.__graph.add_node(self.__nodes['{}_{}_actions_enter'.format(state, conditionset)]) self._add_actioncondition(state, conditionset, 'actions_enter', new_y, cond1, cond2) if not actionlist_stay == '': new_y -= 0.05 if not actionlist_enter == '' else 0 position = '{},{}!'.format(new_x, new_y) + xlabel = '1 tooltip' if action_tooltip_count_stay == 1\ + else '{} tooltips'.format(action_tooltip_count_stay)\ + if action_tooltip_count_stay > 1 else '' #self._log_debug('action stay: {}', position) self.__nodes['{}_{}_actions_stay'.format(state, conditionset)] = pydotplus.Node( '{}_{}_actions_stay'.format(state, conditionset), style="filled", fillcolor=color, - shape="rectangle", label=actionlist_stay, pos=position) + shape="rectangle", label=actionlist_stay, pos=position, tooltip=action_tooltip_stay, + xlabel=xlabel) self.__graph.add_node(self.__nodes['{}_{}_actions_stay'.format(state, conditionset)]) self._add_actioncondition(state, conditionset, 'actions_stay', new_y, cond1, cond2) @@ -411,7 +527,7 @@ def drawgraph(self, filename): style='bold', color='black', tooltip='check next conditionset')) previous_conditionset = self.__nodes['{}_{}'.format(state, conditionset)] - if len(self.__states[state].get('actions_leave')) > 0: + if len(actions_leave) > 0: new_y -= 1 * self.__scalefactor if j == 0 else 2 * self.__scalefactor position = '{},{}!'.format(0.5, new_y) #self._log_debug('leaveconditions {}', position) @@ -424,7 +540,7 @@ def drawgraph(self, filename): except Exception: cond2 = True cond3 = True if self.__states[state].get('leave') is True else False - color = "gray" if cond1 and cond2 and not cond3 else "chartreuse3" if cond3 else "indianred2" + color = "gray" if cond1 and cond2 and not cond3 else "olivedrab" if cond3 else "indianred2" self.__nodes['{}_leave'.format(state)] = pydotplus.Node('{}_leave'.format(state), style="filled", fillcolor=color, shape="diamond", label='leave', pos=position) @@ -433,11 +549,16 @@ def drawgraph(self, filename): style='bold', color='black', tooltip='check leave')) position = '{},{}!'.format(new_x, new_y) + xlabel = '1 tooltip' if action_tooltip_count_leave == 1\ + else '{} tooltips'.format(action_tooltip_count_leave)\ + if action_tooltip_count_leave > 1 else '' #self._log_debug('action leave: {}', position) self.__nodes['{}_actions_leave'.format(state)] = pydotplus.Node('{}_actions_leave'.format(state), style="filled", fillcolor=color, shape="rectangle", label=actionlist_leave, - pos=position, align="center") + pos=position, align="center", + tooltip=action_tooltip_leave, + xlabel=xlabel) self.__graph.add_node(self.__nodes['{}_actions_leave'.format(state)]) self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_leave'.format(state)], self.__nodes['{}_actions_leave'.format(state)], style='bold', diff --git a/stateengine/__init__.py b/stateengine/__init__.py index ce2382c9d..7b0598afb 100755 --- a/stateengine/__init__.py +++ b/stateengine/__init__.py @@ -26,6 +26,7 @@ from . import StateEngineTools from . import StateEngineCliCommands from . import StateEngineFunctions +from . import StateEngineValue from . import StateEngineWebif from . import StateEngineStructs import logging @@ -35,11 +36,18 @@ from lib.item import Items from .webif import WebInterface +try: + import pydotplus + VIS_ENABLED = True +except Exception: + VIS_ENABLED = False + + logging.addLevelName(StateEngineDefaults.VERBOSE, 'DEVELOP') class StateEngine(SmartPlugin): - PLUGIN_VERSION = '1.9.6' + PLUGIN_VERSION = '2.0.0' # Constructor # noinspection PyUnusedLocal,PyMissingConstructor @@ -52,36 +60,57 @@ def __init__(self, sh): self.__sh = sh self.alive = False 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.init_webinterface(WebInterface) - self.__log_directory = self.get_parameter_value("log_directory") + self.get_sh().stateengine_plugin_functions = StateEngineFunctions.SeFunctions(self.get_sh(), self.logger) try: log_level = self.get_parameter_value("log_level") - StateEngineDefaults.log_level = log_level - log_directory = self.__log_directory + startup_log_level = self.get_parameter_value("startup_log_level") + log_directory = self.get_parameter_value("log_directory") + log_maxage = self.get_parameter_value("log_maxage") + log_level_value = StateEngineValue.SeValue(self, "Log Level", False, "num") + log_level_value.set(log_level) + SeLogger.log_level = log_level_value + startup_log_level_value = StateEngineValue.SeValue(self, "Startup Log Level", False, "num") + startup_log_level_value.set(startup_log_level) + SeLogger.startup_log_level = startup_log_level_value + log_maxage_value = StateEngineValue.SeValue(self, "Log MaxAge", False, "num") + log_maxage_value.set(log_maxage) + SeLogger.log_maxage = log_maxage_value + default_log_level_value = StateEngineValue.SeValue(self, "Default Log Level", False, "num") + default_log_level_value.set(log_level) + SeLogger.default_log_level = default_log_level_value + self.logger.info("Set default log level to {}, Startup log level to {}.".format(SeLogger.log_level, SeLogger.startup_log_level)) self.logger.info("Init StateEngine (log_level={0}, log_directory={1})".format(log_level, log_directory)) StateEngineDefaults.startup_delay = self.get_parameter_value("startup_delay_default") - StateEngineDefaults.suspend_time = self.get_parameter_value("suspend_time_default") - StateEngineDefaults.instant_leaveaction = self.get_parameter_value("instant_leaveaction") + suspend_time = self.get_parameter_value("suspend_time_default") + suspend_time_value = StateEngineValue.SeValue(self, "Default Suspend Time", False, "num") + suspend_time_value.set(suspend_time) + StateEngineDefaults.suspend_time = suspend_time_value + default_instant_leaveaction = self.get_parameter_value("instant_leaveaction") + self.__default_instant_leaveaction = StateEngineValue.SeValue(self, "Default Instant Leave Action", False, "bool") + self.__default_instant_leaveaction.set(default_instant_leaveaction) + StateEngineDefaults.suntracking_offset = self.get_parameter_value("lamella_offset") StateEngineDefaults.lamella_open_value = self.get_parameter_value("lamella_open_value") StateEngineDefaults.write_to_log(self.logger) - self.get_sh().stateengine_plugin_functions = StateEngineFunctions.SeFunctions(self.get_sh(), self.logger) - StateEngineCurrent.init(self.get_sh()) + StateEngineCurrent.init(self.get_sh()) base = self.get_sh().get_basedir() - log_directory = SeLogger.create_logdirectory(base, log_directory) + log_directory = SeLogger.manage_logdirectory(base, log_directory, False) + SeLogger.log_directory = log_directory if log_level > 0: text = "StateEngine extended logging is active. Logging to '{0}' with log level {1}." self.logger.info(text.format(log_directory, log_level)) - log_maxage = self.get_parameter_value("log_maxage") + + if log_maxage > 0: self.logger.info("StateEngine extended log files will be deleted after {0} days.".format(log_maxage)) - SeLogger.set_logmaxage(log_maxage) cron = ['init', '30 0 * *'] self.scheduler_add('StateEngine: Remove old logfiles', SeLogger.remove_old_logfiles, cron=cron, offset=0) - SeLogger.set_loglevel(log_level) - SeLogger.set_logdirectory(log_directory) except Exception as ex: self._init_complete = False @@ -96,13 +125,14 @@ def parse_item(self, item): item.expand_relativepathes('se_item_*', '', '') except Exception: pass + try: + item.expand_relativepathes('se_status_*', '', '') + except Exception: + pass if self.has_iattr(item.conf, "se_manual_include") or self.has_iattr(item.conf, "se_manual_exclude"): item._eval = "sh.stateengine_plugin_functions.manual_item_update_eval('" + item.id() + "', caller, source)" elif self.has_iattr(item.conf, "se_manual_invert"): item._eval = "not sh." + item.id() + "()" - if self.has_iattr(item.conf, "se_log_level"): - base = self.get_sh().get_basedir() - SeLogger.create_logdirectory(base, self.__log_directory) return None # Initialization of plugin @@ -115,6 +145,10 @@ def run(self): try: abitem = StateEngineItem.SeItem(self.get_sh(), item, self) abitem.ab_alive = True + abitem.update_leave_action(self.__default_instant_leaveaction) + abitem.write_to_log() + abitem.show_issues_summary() + abitem.startup() self._items[abitem.id] = abitem except ValueError as ex: self.logger.error("Problem with Item: {0}: {1}".format(item.property.path, ex)) @@ -202,10 +236,34 @@ def get_graph(self, abitem, graphtype='link'): \ '.format(abitem) + except pydotplus.graphviz.InvocationException as ex: + self.logger.error("Problem getting graph for {}. Error: {}".format(abitem, ex)) + return '

Can not show visualization. Most likely GraphViz is not installed.

' \ + 'Current issue: ' + str(ex) + '
'\ + 'Please make sure ' \ + 'graphviz is installed.
' \ + 'On Windows add install path to your environment path AND run dot -c. ' \ + 'Additionally copy dot.exe to fdp.exe!' except Exception as ex: - self.logger.error("Problem getting graph for {}. Error: {}".format(abitem, ex)) - return '

Can not show visualization. Most likely GraphViz is missing.

' \ - 'Please download and install ' \ - 'https://graphviz.org/download/
' \ - 'on Windows add install path to your environment path AND run dot -c.' \ - 'Additionally copy dot.exe to fdp.exe!' + self.logger.error("Problem getting graph for {}. Unspecified Error: {}".format(abitem, ex)) + 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) + graph.set_node_defaults(color='lightgray', style='filled', shape='box', + fontname='Helvetica', fontsize='10') + graph.set_edge_defaults(color='darkgray', style='filled', shape='box', + fontname='Helvetica', fontsize='10') + + try: + result = graph.write_svg(img_path, prog='fdp') + except pydotplus.graphviz.InvocationException: + return False + return True diff --git a/stateengine/locale.yaml b/stateengine/locale.yaml index bf1520f27..229980725 100755 --- a/stateengine/locale.yaml +++ b/stateengine/locale.yaml @@ -6,10 +6,15 @@ plugin_translations: 'Zustände': {'de': '=', 'en': 'States'} 'aktueller Zustand': {'de': '=', 'en': 'current state'} 'aktuelles Bedingungsset': {'de': '=', 'en': 'current conditionset'} + 'Standard Log Level': {'de': '=', 'en': 'Default Log Level'} 'Log Level': {'de': '=', 'en': '='} 'Log Verzeichnis': {'de': '=', 'en': 'Log Folder'} + 'Logs löschen nach': {'de': '=', 'en': 'Delete logs after'} + 'Tagen': {'de': '=', 'en': 'days'} + 'Tag': {'de': '=', 'en': 'day'} 'Startverzögerung': {'de': '=', 'en': 'Startup Delay'} 'Suspend Dauer': {'de': '=', 'en': 'Suspend Duration'} 'Items': {'de': '=', 'en': '='} 'SE Item': {'de': '=', 'en': '='} 'Detailvisualisierung': {'de': '=', 'en': 'Detailed Visualization'} + 'KeineVisualisierung': {'de': 'Visualisierung nicht verfügbar', 'en': 'Visualization not available'} diff --git a/stateengine/plugin.yaml b/stateengine/plugin.yaml index 5c2cae2ba..377a8b2d2 100755 --- a/stateengine/plugin.yaml +++ b/stateengine/plugin.yaml @@ -35,11 +35,11 @@ plugin: - Python Modul pydotplus: ``pip3 install pydotplus``\n ' maintainer: onkelandy - tester: '?' + tester: 'Morg42' state: ready support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1303071-stateengine-plugin-support - version: 1.9.6 + version: 2.0.0 sh_minversion: 1.6 multi_instance: False classname: StateEngine @@ -117,29 +117,59 @@ parameters: type: int default: 0 valid_list: + - -1 - 0 - 1 - 2 - 3 valid_list_description: - de: ['deaktiviert', 'Info', 'Debug', 'Develop'] - en: ['deactivated', 'Info', 'Debug', 'Develop'] + de: ['Standardlevel aus plugin.yaml', 'deaktiviert', 'Info', 'Debug', 'Develop'] + en: ['default level from plugin.yaml', 'deactivated', 'Info', 'Debug', 'Develop'] description: - de: 'Erweiterte Protokollierung: Loglevel (0: aus, 1: Info, 2: Debug, 3: Develop/Verbose)' - en: 'Extended Logging: Loglevel (0: off, 1: Info, 2: Debug, 3: Develop/Verbose)' + de: 'Erweiterte Protokollierung: Loglevel (-1: Wert aus plugin.yaml, 0: aus, 1: Info, 2: Debug, 3: Develop/Verbose)' + en: 'Extended Logging: Loglevel (-1: value from plugin.yaml, 0: off, 1: Info, 2: Debug, 3: Develop/Verbose)' description_long: de: '**Erweiterte Protokollierung:**\n - Bei einem Wert von 1 oder 2 wird pro Item eine eigene Logdatei im unter log_directory + Bei einem Wert von 1, 2 oder 3 wird pro Item eine eigene Logdatei im unter log_directory angegebenen Verzeichnis angelegt. Wenn der Parameter nicht angegeben oder auf 0 gestellt ist, ist die erweiterte Protokollierung deaktiviert. ' en: '**Extended Logging**:\n - If 1 or 2 is given there will be a seperate log file for each item in the folder + If 1, 2 or 3 is given there will be a seperate log file for each item in the folder defined as log_directory. If the parameter is absent or set to 0 no extended logging is active ' + startup_log_level: + type: int + default: 2 + valid_list: + - -1 + - 0 + - 1 + - 2 + - 3 + valid_list_description: + de: ['Standardlevel aus plugin.yaml', 'deaktiviert', 'Info', 'Debug', 'Develop'] + en: ['default level from plugin.yaml', 'deactivated', 'Info', 'Debug', 'Develop'] + description: + de: 'Erweiterte Protokollierung beim Starten des Plugins: Loglevel (-1: Wert aus plugin.yaml, 0: aus, 1: Info, 2: Debug, 3: Develop/Verbose)' + en: 'Extended Logging at plugin startup: Loglevel (-1: value from plugin.yaml, 0: off, 1: Info, 2: Debug, 3: Develop/Verbose)' + description_long: + de: '**Erweiterte Protokollierung:**\n + Bei einem Wert von 1, 2 oder 3 wird pro Item eine eigene Logdatei im unter log_directory + angegebenen Verzeichnis angelegt. + Ist die Initialisierung eines Stateengine Items abgeschlossen, wird der über se_log_level oder + den Standard Loglevel im plugin.yaml eingestellt. + ' + en: '**Extended Logging**:\n + If 1, 2 or 3 is given there will be a seperate log file for each item in the folder + defined as log_directory. + After initialization of the stateengine items the log level will be set to the one + defined in se_log_level or as a standard log level in plugin.yaml. + ' + log_directory: type: str default: 'var/log/stateengine/' @@ -258,12 +288,23 @@ item_attributes: en: 'Data type of the object item. Has to be "bool"' se_instant_leaveaction: - type: bool + type: foo default: False description: - de: 'Ist dieser Parameter auf True gesetzt, werden "on leave" Aktionen sofort ausgeführt, wenn der aktuelle Zustand nicht mehr eingenommen wird. Standardmäßig werden die Aktionen erst direkt vor dem Eintreten in einen neuen Zustand getriggert.' - en: 'If this parameter is set to True the "on leave" actions are run immediately after not entering the current state - again. By default the actions are triggered directly before entering a new state.' + de: 'Ist dieser Parameter auf True bzw. 1 gesetzt, werden "on leave" Aktionen + sofort ausgeführt, wenn der aktuelle Zustand nicht mehr eingenommen wird. + Standardmäßig werden die Aktionen erst direkt vor dem Eintreten in + einen neuen Zustand getriggert. Kann auch über ein Item zur Laufzeit geändert werden, + beispielsweise durch item:..settings.instant_leaveaction. Der Wert -1 sorgt dafür, + dass der in der plugin.yaml angegebene Standardwert herangezogen wird. + ' + en: 'If this parameter is set to True or 1 the "on leave" actions are run + immediately after not entering the current state + again. By default the actions are triggered directly before + entering a new state. Can also be defined by a valid item, + e.g. item:..settings.instant_leaveaction. By setting the value to -1 + the default value from the plugin.yaml file is used. + ' se_plugin: type: str @@ -455,41 +496,6 @@ item_attributes: It can be used for visualization, actions and conditions. ' - se_shouldnotrelease_item: - type: str - description: - de: 'ID des Items, in dem interne Informationen für das released_by Feature gespeichert werden' - en: 'ID of the item that saves internal information for the released_by feature' - description_long: - de: '**ID des Items, in dem interne Informationen für das released_by Feature gespeichert werden:**\n - Ein Dictionary mit den Stati als Keys, die ein se_released_by Attribut haben und den Stati/Items - als Values, die zuvor schon aktiv waren und erst bei einem erneuten Wechsel von "can not be entered" - auf "can be entered" den entsprechenden Status released, also (vorübergehend) auflöst. - ' - en: '**ID of the item that saves internal information for the released_by feature:**\n - A dictionary with the states as keys that have an se_released_by attribute and the states/items - as values that have been active before already and only release/overrule the relevant state - if they change from "can not be entered" to "can be entered". - ' - - se_hasreleased_item: - type: str - description: - de: 'ID des Items, in dem interne Informationen für das released_by Feature gespeichert werden' - en: 'ID of the item that saves internal information for the released_by feature' - description_long: - de: '**ID des Items, in dem interne Informationen für das released_by Feature gespeichert werden:**\n - Ein Dictionary mit den Stati als Keys, die ein se_released_by Attribut haben und den Stati/Items - als Values, die jene Items bereits aufgelöst haben und somit erst bei erneutem Wechsel - von "can not be entered" auf "can be entered" den enstprechenden Status released, also (vorübergehend) - auflösen. - ' - en: '**ID of the item that saves internal information for the released_by feature:**\n - A dictionary with the states as keys that have an se_released_by attribute and the states/items - as values that have released them already and only release/overrule the relevant state - if they change from "can not be entered" to "can be entered" again. - ' - se_repeat_actions: type: bool description: @@ -514,7 +520,7 @@ item_attributes: se_suspend_time: type: foo - valid_min: 1 + valid_min: -60 valid_max: 86400 description: de: 'Dauer der Unterbrechung der automatischen Steuerung nach manuellen Aktionen in Sekunden' @@ -564,24 +570,20 @@ item_attributes: ' se_log_level: - type: int - valid_list: - - 0 - - 1 - - 2 - - 3 - valid_list_description: - de: ['deaktiviert', 'Info', 'Debug', 'Develop'] - en: ['deactivated', 'Info', 'Debug', 'Develop'] + type: foo description: de: 'Loglevel für ein konkretes Stateengine Item' en: 'Log level for a distinct stateengine item' description_long: de: 'Das global angegebene Loglevel kann für jedes SE item individuell überschrieben werden. Somit ist es einfacher möglich, bestimme State Engines zu debuggen. + Das Loglevel kann durch das Settings Item log_level im laufenden Betrieb angepasst werden. + Ist der Wert auf -1 gesetzt, wird der Standardwert aus dem plugin.yaml übernommen. ' en: 'The globally defined log level can be overwritten for each SE item individually. Therefore it is easier to debug specific state engines. + The log level can be adjusted by changing the setting item log_level while the plugin is running. + If the value is set to -1 the standard value from plugin.yaml is used. ' se_use: @@ -608,9 +610,14 @@ item_attributes: de: 'Definieren von untergeordneten Zuständen, die den aktuellen Zustand auflösen können' en: 'Definition of subordinate states that can release the current state' description_long: - de: ' + de: 'Durch Setzen dieses Attributs können Zustände durch untergeordnete Zustände aufgelöst + werden. Sobald ein durch das Attribut definierter untergeordneter Zustand neu + eingenommen werden könnte, wird der aktuelle Zustand verlassen und eine neue + Zustandsevaluierung ausgeführt. ' - en: ' + en: 'By setting this attribute, states can be resolved by subordinate states. + As soon as a subordinate state defined by the attribute could be newly + entered by the attribute, the current state is left and a new state evaluation is performed. ' item_structs: @@ -678,40 +685,32 @@ item_structs: visu_acl: ro cache: True - shouldnotrelease: - remark: Helper item for releasedby function - type: dict - visu_acl: ro - cache: True - initial_value: "{}" - - hasreleased: - remark: Helper item for releasedby function - type: dict - visu_acl: ro - cache: True - initial_value: "{'initial': 'start'}" - retrigger: remark: Item to retrigger the rule set evaluation type: bool visu_acl: rw enforce_updates: True - on_update: ..rules = True + on_update: ..rules = True if value is True else None + autotimer: 1 = False settings: - settings_edited: - type: bool - name: settings editiert - eval_trigger: ...settings.* - eval: not sh..self() - on_update: ...retrigger = True if sh..self.property.prev_update_age > 0.1 else None + log_level: + type: num + cache: True + initial_value: -1 + + instant_leaveaction: + type: num + cache: True + initial_value: -1 rules: name: Zustandsautomat remark: configure your se_item_* and eval_triggers here type: bool se_plugin: active + se_log_level: item:..settings.log_level + se_instant_leaveaction: item:..settings.instant_leaveaction eval: True se_laststate_item_id: ..state_id se_laststate_item_name: ..state_name @@ -723,8 +722,6 @@ item_structs: se_previousstate_item_name: ..previousstate_name se_previousstate_conditionset_item_id: ..previousstate_conditionset_id se_previousstate_conditionset_item_name: ..previousstate_conditionset_name - se_shouldnotrelease_item: ..shouldnotrelease - se_hasreleased_item: ..hasreleased state_lock: name: Zustandsvorlage zum Sperren der Evaluierung @@ -738,6 +735,7 @@ item_structs: rules: se_item_lock: ..lock eval_trigger: + - merge_unique* - ..lock lock: @@ -815,7 +813,8 @@ item_structs: type: bool visu_acl: rw enforce_updates: True - on_update: ..rules = True + on_update: ..rules = True if value is True else None + autotimer: 1 = False settings: remark: Use these settings for your condition values @@ -872,6 +871,7 @@ item_structs: se_suspend_time: item:..settings.suspendduration.seconds eval_trigger: + - merge_unique* - ..manuell suspend: @@ -925,6 +925,11 @@ item_structs: - 'to: ' - 'order: 5' - 'delay: 1' + se_action_retrigger: + - 'function: special' + - 'value: retrigger:..retrigger' + - 'delay: -1' + - 'order: 1' enter_manuell: se_value_trigger_source: eval:se_eval.get_relative_itemproperty('..manuell', 'path') @@ -1005,7 +1010,8 @@ item_structs: type: bool visu_acl: rw enforce_updates: True - on_update: ..rules = True + on_update: ..rules = True if value is True else None + autotimer: 1 = False settings: remark: Use these settings for your condition values @@ -1026,7 +1032,7 @@ item_structs: visu_acl: rw cache: True enforce_updates: True - initial_value: 60 + initial_value: -1 on_change: .seconds = value * 60 if not sh..self.property.last_change_by in ["On_Change:{}".format(sh..seconds.property.path), "On_Change:{}".format(sh..duration_format.property.path)] else None on_update: .seconds = value * 60 if "Init" in sh..self.property.last_update_by else None @@ -1052,7 +1058,7 @@ item_structs: visu_acl: rw cache: True enforce_updates: True - initial_value: 60 + initial_value: -1 on_change: .seconds = value * 60 if not sh..self.property.last_change_by in ["On_Change:{}".format(sh..seconds.property.path), "On_Change:{}".format(sh..duration_format.property.path)] else None on_update: .seconds = value * 60 if "Init" in sh..self.property.last_update_by else None @@ -1078,7 +1084,7 @@ item_structs: visu_acl: rw cache: True enforce_updates: True - initial_value: 60 + initial_value: -1 on_change: .seconds = value * 60 if not sh..self.property.last_change_by in ["On_Change:{}".format(sh..seconds.property.path), "On_Change:{}".format(sh..duration_format.property.path)] else None on_update: .seconds = value * 60 if "Init" in sh..self.property.last_update_by else None @@ -1125,7 +1131,7 @@ item_structs: visu_acl: rw cache: True enforce_updates: True - initial_value: 60 + initial_value: -1 on_change: .seconds = value * 60 if not sh..self.property.last_change_by in ["On_Change:{}".format(sh..seconds.property.path), "On_Change:{}".format(sh..duration_format.property.path)] else None on_update: .seconds = value * 60 if "Init" in sh..self.property.last_update_by else None @@ -1169,6 +1175,7 @@ item_structs: se_suspend_time: eval:se_eval.get_relative_itemvalue('..settings.suspendvariant.suspendduration{}.seconds'.format(max(0, min(se_eval.get_relative_itemvalue('..settings.suspendvariant'), 2)))) eval_trigger: + - merge_unique* - ..manuell suspend: @@ -1227,6 +1234,11 @@ item_structs: - 'to: ' - 'order: 5' - 'delay: 1' + se_action_retrigger: + - 'function: special' + - 'value: retrigger:..retrigger' + - 'delay: -1' + - 'order: 1' enter_manuell: se_value_trigger_source: eval:se_eval.get_relative_itemproperty('..manuell', 'path') @@ -1255,6 +1267,8 @@ item_structs: knx_dpt: 1 visu_acl: rw enforce_updates: True + on_update: ..rules = True if value is True else None + autotimer: 1 = False suspend: type: bool @@ -1314,8 +1328,6 @@ item_structs: se_item_release: ..release se_item_suspend_end: ..suspend_end.date_time se_item_suspend_start: ..suspend_start.date_time - eval_trigger: - - ..release release: name: release @@ -1359,6 +1371,12 @@ item_attribute_prefixes: de: 'Definiert das Item, das in einem konkreten Zustand evaluiert oder geändert werden soll' en: 'Definition of an item that should be evaluated or changed in a specific state' + se_status_: + type: foo + description: + de: 'Definiert das Item, das in einem konkreten Zustand evaluiert werden soll' + en: 'Definition of an item that should be evaluated in a specific state' + se_eval_: type: foo description: diff --git a/stateengine/user_doc/03_regelwerk.rst b/stateengine/user_doc/03_regelwerk.rst index 8718ac663..ab841d13c 100755 --- a/stateengine/user_doc/03_regelwerk.rst +++ b/stateengine/user_doc/03_regelwerk.rst @@ -43,29 +43,28 @@ das Attribute ``se_plugin`` auf inactive zu setzen: Item-Definitionen ----------------- -Bedingungen und Aktionen beziehen sich überlicherweise auf Items wie beispielsweise +Bedingungen und Aktionen beziehen sich üblicherweise auf Items wie beispielsweise die Höhe einer Jalousie oder die Außenhelligkeit. Diese Items müssen auf Ebene des Regelwerk-Items über das Attribut -``se_item_`` bekannt gemacht werden. +``se_item_`` bekannt gemacht werden. Um einfacher zwischen Items, +die für Bedingungen und solchen, die für Aktionen genutzt werden, unterscheiden zu können, +können Items, die nur für Bedingungen gebraucht werden, mittels ``se_status_`` +deklariert werden. Diese Variante ist auch besonders dann relevant, wenn es zwei separate Items +für "Senden" und "Empfangen" gibt, also z.B. Senden der Jalousiehöhe und Empfangen des aktuellen +Werts vom KNX-Aktor. Anstatt direkt das Item in Form des absoluten oder relativen Pfades mittels ``se_item_`` zu setzen, kann auch die Angabe ``se_eval_`` genutzt werden. In diesem Fall wird eine beliebige -Funktion anstelle des Itemnamen angegeben. Dies ist sowohl für Bedingungsabfragen, -als auch für das Setzen von "dynamischen" Items möglich. +Funktion anstelle des Itemnamen angegeben. Dies ist primär für das Setzen von "dynamischen" Items +gedacht, allerdings ist es auch möglich, hier einen beliebigen Eval-Ausdruck als Bedingung festzulegen. -An dieser Stelle ist es auch möglich, über ``se_mindelta_`` zu definieren, um welchen Wert -sich ein Item mindestens geändert haben muss, um neu gesetzt zu werden. Siehe auch :ref:`Aktionen`. - -Außerdem ist es möglich, über ``se_repeat_actions`` generell zu definieren, -ob Aktionen für die Stateengine wiederholt ausgeführt werden sollen oder nicht. Diese Konfiguration -kann für einzelne Aktionen individuell über die Angabe ``repeat`` überschrieben werden. Siehe auch :ref:`Aktionen`. Beispiel se_item ================ Im Beispiel wird durch ``se_item_height`` das Item ``beispiel.raffstore1.hoehe`` dem Plugin unter dem Namen "height" bekannt gemacht. Das Item ``beispiel.wetterstation.helligkeit`` -wird durch ``se_item_brightness`` als "brightness" referenziert. +wird durch ``se_item_brightness`` (alternativ via ``se_status_brightness``) als "brightness" referenziert. Auf diese Namen beziehen sich nun in weiterer Folge Bedingungen und Aktionen. Im Beispiel wird im Zustand Nacht das Item ``beispiel.raffstore1.hoehe`` auf den Wert 100 gesetzt, sobald @@ -95,7 +94,7 @@ und Aktionen folgen auf den nächsten Seiten. Beispiel se_eval ================ -se_eval ist für Sonderfälle und etwas komplexere Konfiurationen sinnvoll, kann aber +se_eval ist für Sonderfälle und etwas komplexere Konfigurationen sinnvoll, kann aber im ersten Durchlauf ignoriert werden. Es wird daher empfohlen, als Beginner dieses Beispiel einfach zu überspringen ;) diff --git a/stateengine/user_doc/04_zustand.rst b/stateengine/user_doc/04_zustand.rst index dc42deecc..6404227a9 100755 --- a/stateengine/user_doc/04_zustand.rst +++ b/stateengine/user_doc/04_zustand.rst @@ -61,9 +61,9 @@ und mittels ``se_use`` zu referenzieren, bietet sich ab **smarthomeNG 1.6** das Zum einen können in der Datei ``etc/struct.yaml`` eigene Vorlagen definiert werden, zum anderen stellt das Plugin folgende Vorlagen fix fertig bereit: +- stateengine.state_release: Sofortiges Entsperren und Beenden des Suspend-Modus und Neuevaluierung - stateengine.state_lock: Sperren der Zustandsevaluierung, sobald das Sperritem "lock" aktiv ist. - stateengine.state_suspend: Aussetzen der Evaluierung für eine bestimmte Zeit bei manueller Betätigung (z.B. Taster) -- stateengine.state_release: Sofortiges Entsperren und Beenden des Suspend-Modus und Neuevaluierung Gemeinsam mit der Vorlage stateengine.general, die allgemein relevante Items automatisch erstellt, könnte ein Item wie unten zu sehen bestückt werden. Das Einbinden der Zustandsvorlagen findet dabei auf gleicher @@ -79,9 +79,9 @@ Zusätzlich können eigene Zustände (beispielsweise day) definiert werden. automatik: struct: - stateengine.general - - stateengine.state_lock - - stateengine.state_suspend - stateengine.state_release + - stateengine.state_lock + - stateengine.state_suspend rules: day: diff --git a/stateengine/user_doc/05_bedingungen.rst b/stateengine/user_doc/05_bedingungen.rst index 031914e3a..c72b2e312 100755 --- a/stateengine/user_doc/05_bedingungen.rst +++ b/stateengine/user_doc/05_bedingungen.rst @@ -10,7 +10,7 @@ Beispiel -------- Im folgenden Beispiel wird der Zustand "Daemmerung" eingenommen, sobald -die Helligkeit (über se_item_brightness definiert) über 500 Lux liegt. +die Helligkeit (über se_item_brightness oder se_status_brightness definiert) über 500 Lux liegt. .. code-block:: yaml @@ -19,7 +19,7 @@ die Helligkeit (über se_item_brightness definiert) über 500 Lux liegt. automatik: struct: stateengine.general rules: - se_item_brightness: beispiel.wetterstation.helligkeit + se_status_brightness: beispiel.wetterstation.helligkeit Daemmerung: name: Dämmerung remark: @@ -62,7 +62,7 @@ Der zu vergleichende Wert einer Bedingung kann auf folgende Arten definiert werd - statischer Wert (also z.B. 500 Lux). Wird angegegeben mit ``value:500``, wobei das value: auch weggelassen werden kann. - Item (beispielsweise ein Item namens settings.helligkeitsschwellwert). Wird angegeben mit ``item:settings.helligkeitsschwellwert`` - Eval-Funktion (siehe auch `eval Ausdrücke `_). Wird angegeben mit ``eval:1*2*se_eval.get_relative_itemvalue('..bla')`` -- Regular Expression (siehe auch ` RegEx Howto `_) - Vergleich mittels re.fullmatch, wobei Groß/Kleinschreibung ignoriert wird. Wird angegeben mit ``regex:StateEngine Plugin:(.*)`` +- Regular Expression (siehe auch ` RegEx Howto `_) - Vergleich mittels re.fullmatch, wobei Groß/Kleinschreibung ignoriert wird. Wird angegeben mit ``regex:StateEngine Plugin:(.*)`` - Template: eine Vorlage, z.B. eine eval Funktion, die immer wieder innerhalb des StateEngine Items eingesetzt werden kann. Angegeben durch ``template:`` @@ -75,7 +75,8 @@ die jeweils mit einem Unterstrich "_" getrennt werden: - ``se_``: eindeutiger Prefix, um dem Plugin zugeordnet zu werden - ````: siehe unten. Beispiel: min = der Wert des muss mindestens dem beim Attribut angegebenen Wert entsprechen. -- ````: Hier wird entweder das im Regelwerk-Item mittels ``se_item_`` deklarierte Item oder eine besondere Bedingung (siehe unten) referenziert. +- ````: Hier wird entweder das im Regelwerk-Item mittels ``se_item_`` +oder ``se_status_`` deklarierte Item oder eine besondere Bedingung (siehe unten) referenziert. Templates für Bedingungsabfragen @@ -83,7 +84,7 @@ Templates für Bedingungsabfragen Setzt man für mehrere Bedingungsabfragen (z.B. Helligkeit, Temperatur, etc.) immer die gleichen Ausdrücke ein (z.B. eine eval-Funktion), so kann Letzteres als Template -definiert und referenziert werden. Dadurch wird die Handhabung +definiert und referenziert werden. Dadurch wird die Handhabung komplexerer Abfragen deutlich vereinfacht. Diese Templates müssen wie se_item/se_eval auf höchster Ebene des StateEngine Items (also z.B. rules) deklariert werden. @@ -103,6 +104,27 @@ Bei sämtlichen Bedingungen ist es möglich, Werte als Liste anzugeben. Es ist a nicht möglich, Templates als Listen zu definieren. +Bedingungen mittels eval-Ausdrücken +----------------------------------- + +``se_eval_`` kann neben der dynamischen Definition von Items auch für einen +herkömmlichen eval-Ausdruck herhalten, der dann in einem Bedingungsset geprüft wird. + +.. code-block:: yaml + + #items/item.yaml + raffstore1: + automatik: + struct: stateengine.general + rules: + se_eval_berechnung: sh.test.property.value + 1 + + Zustand_Eins: + name: sueden + enter: + se_value_berechnung: 3 + + Bedingungslisten ---------------- @@ -233,6 +255,28 @@ Die Werte(liste) kann auch durch ``se_changedbynegate_`` negiert se_changedbynegate_: True|False +**Triggerung des Items durch** + +.. code-block:: yaml + + se_triggeredby_: [Wert] + +Die Bedingung ist erfüllt, wenn das Item durch den angegebenen Wert bzw. +einen der angegebenen Werte getriggert wurde. Dies kann relevant werden, +um herauszufinden, wodurch ein Item mit einem eval-Attribut getriggert wurde, +unabhängig davon, ob sich daraus eine Wertänderung ergibt oder nicht. +Hier bietet es sich an, den Wert als Regular Expression mittels +``se_triggeredby_: regex:StateEngine Plugin`` zu definieren. +Die Werte(liste) kann auch durch ``se_triggeredbynegate_`` negiert werden. + +.. code-block:: yaml + + se_triggeredby_: + - [Wert1] + - [Wert2] + - regex:[WertN] + + se_triggeredbynegate_: True|False **Mindestalter** diff --git a/stateengine/user_doc/06_aktionen.rst b/stateengine/user_doc/06_aktionen.rst index a663f0889..0182d969b 100755 --- a/stateengine/user_doc/06_aktionen.rst +++ b/stateengine/user_doc/06_aktionen.rst @@ -24,6 +24,12 @@ stehenden Beispiel wird der Lamellenwert abhängig vom Sonnenstand berechnet. Oh würden sich die Lamellen ständig um wenige Grad(bruchteile) ändern. Wird jedoch mindelta beispielsweise auf den Wert 10 gesetzt, findet eine Änderung erst statt, wenn sich der errechnete Wert um mindestens 10 Grad vom aktuellen Lamellenwert unterscheidet. +Im Beispiel wird auch mittels ``se_status_`` ein gesondertes Item definiert, +das den Wert vom KNX-Aktor empfängt. + +Außerdem ist es möglich, über ``se_repeat_actions`` generell zu definieren, +ob Aktionen für die Stateengine wiederholt ausgeführt werden sollen oder nicht. Diese Konfiguration +kann für einzelne Aktionen individuell über die Angabe ``repeat`` überschrieben werden. Siehe auch :ref:`Aktionen`. Beispiel zu Aktionen -------------------- @@ -43,6 +49,7 @@ Das folgende Beispiel führt je nach Zustand folgende Aktionen aus: rules: se_item_height: raffstore1.hoehe # Definition des zu ändernden Höhe-Items se_item_lamella: raffstore1.lamelle # Definition des zu ändernden Lamellen-Items + se_status_lamella: raffstore1.lamelle.status # Definition des Lamellen Statusitems se_mindelta_lamella: 10 # Mindeständerung von 10 Grad, sonst werden die Lamellen nicht aktualisiert. Daemmerung: <...> @@ -259,10 +266,13 @@ kann auch durch ein eval oder Item zur Laufzeit berechnet werden. 'delay: /' --> Ergebnis eines Eval-Ausdrucks oder eines Items 'delay: 30' --> 30 Sekunden 'delay: 30m' --> 30 Minuten + 'delay: -1' --> Entfernen des Schedulers -Der Timer zur Ausführung der Aktion nach der angegebenen -Verzögerung wird entfernt, wenn eine gleichartige Aktion -ausgeführt werden soll (egal ob verzögert oder nicht). +Der Timer zur Ausführung der Aktion nach der angegebenen Verzögerung wird entfernt, +wenn eine gleichartige Aktion mit Delay-Angabe ausgeführt werden soll. Außerdem +ist es möglich, den Timer bewusst abzubrechen, ohne eine Aktion auszuführen, +indem der Delay auf -1 gesetzt wird. Dies macht insbesondere beim Verlassen von +Zuständen Sinn, um ungewünschte verzögerte Aktionen vom "alten" Zustand zu verhindern. **instanteval: ** diff --git a/stateengine/user_doc/07_zeitpunkt.rst b/stateengine/user_doc/07_zeitpunkt.rst index 9c852977e..dadb59360 100755 --- a/stateengine/user_doc/07_zeitpunkt.rst +++ b/stateengine/user_doc/07_zeitpunkt.rst @@ -47,3 +47,12 @@ als Attribute definiert werden. - **on_leave**: Aktionen, die ausgeführt werden, direkt bevor ein anderer Zustand aktiv wird. + +Die Konfiguration von instant_leaveaction bestimmt, ob on_leave Aktionen sofort nach dem Verlassen +eines Zustands ausgeführt werden oder erst am Ende der Statusevaluierung. +Die Option kann sowohl in der globalen Pluginkonfiguration +mittels ``instant_leaveaction`` (boolscher Wert True oder False), als auch pro Item +mittels ``se_instant_leaveaction``festgelegt werden. Letzteres Attribut kann auch +auf ein Item verweisen, dem der Wert -1 = Nutzen des Default Wertes, 0 = False, +1 = True zugewiesen werden kann. Im ``general struct`` sind bereits entsprechende +Einträge und Items angelegt (mit einem Wert von -1). diff --git a/stateengine/user_doc/10_funktionen_variablen.rst b/stateengine/user_doc/10_funktionen_variablen.rst index d22c93a2c..c1ee17a7b 100755 --- a/stateengine/user_doc/10_funktionen_variablen.rst +++ b/stateengine/user_doc/10_funktionen_variablen.rst @@ -15,9 +15,14 @@ mittels ``eval:`` verwendet werden können: **Sonnenstandsabhängige Lamellenausrichtung** *Die Neigung der Lamellen wird automatisch von der Höhe der Sonne bestimmt.* -Optional kann noch ein Offset in Klammer mitgegeben werden, um etwaige kleine Abweichungen auszugleichen. Diese Abweichung -kann auch global bei der Pluginkonfiguration mittels ``lamella_offset`` eingestellt werden, was sich dann auf -sämtliche Aufrufe der Funktion auswirkt. Der Offset wird in Grad angegeben, wobei ein negativer Offset dafür sorgt, dass sich die Lamellen weniger weit drehen. Bei einem positiven Offset hingegen werden die Lamellen mehr geschlossen. Die Angabe beim direkten Aufruf der Funktion hat dabei immer Vorrang. Da verschiedene Lamellenarten unterschiedliche Prozentwerte im offenen Zustand +Optional kann noch ein Offset in Klammer mitgegeben werden, um etwaige kleine +Abweichungen auszugleichen. Diese Abweichung kann auch global bei der +Pluginkonfiguration mittels ``lamella_offset`` eingestellt werden, was sich dann auf +sämtliche Aufrufe der Funktion auswirkt. Der Offset wird in Grad angegeben, +wobei ein negativer Offset dafür sorgt, dass sich die Lamellen weniger weit drehen. +Bei einem positiven Offset hingegen werden die Lamellen mehr geschlossen. +Die Angabe beim direkten Aufruf der Funktion hat dabei immer Vorrang. +Da verschiedene Lamellenarten unterschiedliche Prozentwerte im offenen Zustand haben können, kann die Berechnung auch mittels ``lamella_open_value`` manipuliert werden. .. code-block:: yaml @@ -149,10 +154,15 @@ Bedarf aber auch für andere Zwecke, welche auch immer, genutzt werden. **item.instant_leaveaction:** *Information, wie das leave_action Attribut für das Regelwerkitem gesetzt ist* -Die Option instant_leaveaction kann sowohl in der globalen Pluginkonfiguration -mittels ``instant_leaveaction``, als auch pro Item mittels ``se_instant_leaveaction`` -festgelegt werden. Sie bestimmt, ob on_leave Aktionen sofort nach dem Verlassen +Die Konfiguration von instant_leaveaction bestimmt, ob on_leave Aktionen sofort nach dem Verlassen eines Zustands ausgeführt werden oder erst am Ende der Statusevaluierung. +Die Option kann sowohl in der globalen Pluginkonfiguration +mittels ``instant_leaveaction`` (boolscher Wert True oder False), als auch pro Item +mittels ``se_instant_leaveaction`` festgelegt werden. Letzteres Attribut kann auch +auf ein Item verweisen, dem der Wert -1 = Nutzen des Default Wertes, 0 = False, +1 = True zugewiesen werden kann. Im ``general struct`` sind bereits entsprechende +Einträge und Items angelegt (mit einem Wert von -1). + **current.action_name:** *Der Name der Aktion, in der auf die Variable zugegriffen wird* @@ -213,15 +223,16 @@ auf das passende Unteritem in licht1.automatik.settings. **current.state_id:** *Die Id des Status, der gerade geprüft wird* -Diese Variable wird leer, sobald die Statusevaluierung beendet wurde, noch bevor die Aktionen des -zuletzt eingenommenen Zustands ausgeführt werden. Sie kann daher nur in der Evaluierung, nicht aber -in on_enter(_or_stay) genutzt werden. Hierfür wird stattdessen ``se_eval.get_relative_itemvalue('..state_id')`` genutzt. +Diese Variable wird leer, sobald die Statusevaluierung beendet wurde, +noch bevor die Aktionen des zuletzt eingenommenen Zustands ausgeführt werden. +Sie kann daher nur in der Evaluierung, nicht aber in on_enter(_or_stay) genutzt +werden. Hierfür wird stattdessen ``se_eval.get_relative_itemvalue('..state_id')`` genutzt. **current.state_name:** *Der Name des Status, der gerade geprüft wird* -Wie die state_id Variable wird diese nur während der Statusevaluierung entsprechend befüllt und sofort beim Eintritt -in einen neuen Zustand geleert (noch vor dem Durchführen der Aktionen). +Wie die state_id Variable wird diese nur während der Statusevaluierung entsprechend +befüllt und sofort beim Eintritt in einen neuen Zustand geleert (noch vor dem Durchführen der Aktionen). Das angeführte Beispiel zeigt, wie eine Bedingung mit einem Wert abgeglichen werden kann, der in einem passenden Settingitem hinterlegt ist. Konkret @@ -327,7 +338,8 @@ auf True gesetzt - aber nur, wenn zuvor der x-Zustand aktiv war. **previous.conditionset_name:** *Der Name der Bedingungsgruppe, die beim vorigen Durchlauf aktiv war* -Bei den previous.conditionset Variablen spielt es keine Rolle, ob ein neuer Zustand eingenommen wurde oder nicht. +Bei den previous.conditionset Variablen spielt es keine Rolle, ob ein neuer Zustand +eingenommen wurde oder nicht. Beispiel: Ein Item ist aktuell im Zustand "Suspend" auf Grund einer manuellen Triggerung, also der Bedingungsgruppe "enter_manuell". Die Variable ``previous.conditionset_name`` beinhaltet nun den Namen der Bedingungsgruppe vom vorherigen Zustand. Bei einer erneuten @@ -351,3 +363,42 @@ Zustand aktiv gewesen ist. Ansonsten gelten alle vorhin beschriebenen Regeln. **previous.state_conditionset_name:** *Der Name der Bedingungsgruppe, die beim vorigen Zustand zuletzt aktiv war* + +**release.can_be_released_by:** +*Die Definitionen, die den aktuellen Zustand generell auflösen könnten* + +Nach einer Bereinigung und Berechnung der ``se_released_by`` Angaben +(z.B. Löschen von ungültigen Zuständen, Auflösen relativer Angaben, etc.) +wird eine Liste mit den noch übrigen Einträgen in diese Variable gespeichert. +Dabei bleiben die originalen Datentypen (z.B. Item, Eval, etc.) erhalten. + +**release.was_released_by:** +*Die Id des Zustandes, der zuletzt den aktuellen Zustand aufgelöst hat* + +Nachdem der im ``se_released_by`` Attribut angegebene Zustand den aktuellen Zustand +aufgelöst hat, wird der auflösende Zustand in dieser Variable hinterlegt. + +**release.can_release:** +*Die Ids von Zuständen, die durch den aktuellen Zustand aufgelöst werden können* + +Wird das ``se_released_by`` Attribut genutzt, wird dessen Inhalt nach jeder +Zustandsevaluierung aktualisiert. Ist das Attribut beispielsweise im Zustand +"suspend" mit dem Wert ".schnee" definiert, wird die ``can_release`` Variable +des Zustands .schnee mit dem Wert "suspend" aktualisiert. + +**release.will_release:** +*Die Id des Zustandes, der aufgelöst wird, sobald die Bedingungen erfüllt werden* + +Ein im ``se_released_by`` Attribut angegebener Zustand wird "scharf gestellt" +(intern an eine übergeordnete Stelle in der Hierarchie kopiert), sobald seine +Bedingungen nicht mehr erfüllt sind, er also aktuell nicht eingenommen werden kann. +Sobald die Bedingungen erfüllt sind, wird er allerdings den in dieser Variable +hinterlegten Zustand auflösen. Sobald die Auflösung stattgefunden hat, wird die +Variable wieder auf "None" gestellt. + +**release.has_released:** +*Die Id des Zustandes, der zuletzt durch den aktuellen Zustand aufgelöst wurde* + +Nachdem der im ``se_released_by`` Attribut angegebene Zustand einen anderen +Zustand erfolgreich aufgelöst ("released") hat, wird die ID jenes aufgelösten +Zustandes in dieser Variablen gespeichert. diff --git a/stateengine/user_doc/13_sonstiges.rst b/stateengine/user_doc/13_sonstiges.rst index c970111f0..69e5768e5 100755 --- a/stateengine/user_doc/13_sonstiges.rst +++ b/stateengine/user_doc/13_sonstiges.rst @@ -28,8 +28,12 @@ sondern auch auf die verschiedenen Schlüsselwörter zurückzugreifen: ein einzelner Punkt vor dem Namen des Zustands. - struct: ermögicht den Zugriff auf Structs, die entweder selbst oder durch irgendein Plugin bereit gestellt werden -Beinhaltet ein verknüpfter State ebenfalls ein se_use Attribut, werden auch die weiteren Zustände mit eingebunden. Je "tiefer" eine -Deklaration steckt, desto geringer ist ihre Priorität. Heißt, etwaige Zustandseinstellungen im eigentlichen Item erweitern und +Wird se_use über ein Item oder einen Eval-Ausdruck definiert, wird bei jedem Durchlauf der Zustand neu eingelesen bzw. +neu aufgebaut. So können Zustände dynamisch zur Laufzeit erweitert und geändert werden. + +Beinhaltet ein verknüpfter State ebenfalls ein se_use Attribut, werden auch die weiteren Zustände mit eingebunden. +Je "tiefer" eine Deklaration steckt, desto geringer ist ihre Priorität. +Heißt, etwaige Zustandseinstellungen im eigentlichen Item erweitern und überschreiben Einstellungen, die mit se_use eingebunden wurden. Weitere Details sind unter :ref:`Zustand-Templates` zu finden. @@ -42,24 +46,25 @@ Auflösen von Zuständen Das Attribut ermöglicht es, andere untergeordnete Zustände zu definieren, die den Zustand auflösen, sobald jene -eingenommen werden könnten. Im Normalfall bleibt dennoch -der übergeordnete Zustand aktiv, bis die Bedingungen nicht -mehr wahr sind. Gewünscht wird dies normalerweise beim +eingenommen werden könnten. Gewünscht wird dies normalerweise beim Suspendzustand, allerdings kann das Attribut bei jedem -beliebigem Zustand genutzt werden. +beliebigem Zustand genutzt werden. Seit Version 2.0 wird se_released_by gleich +behandelt wie andere Plugin spezifische Attribute mit Wertzuweisung, es können +also alle gültigen Schlüsselwörter genutzt werden. Außerdem wurde in +der Pluginversion 2.0 das released_by Feature komplett überarbeitet, sodass +es nun zuverlässig funktionieren sollte ;) Ein Zustand mit diesem Attribut wird aufgelöst, also (vorerst) nicht mehr eingenommen, sobald ein mit dem Attribut angegebener Zustand eingenommen werden könnte. -Befindet sich in der Hierarchie noch ein weiterer Zustand, +Befindet sich in der Hierarchie noch ein weiterer übergeordneter Zustand, dessen Bedingungen erfüllt werden, wird eben dieser Zustand -eingenommen - auch wenn er den ursprünglichen Zustand -nicht aufgelöst hat. +eingenommen - auch wenn er den ursprünglichen Zustand nicht aufgelöst hat. Bei einer relativen Angabe eines Zustands/Items ist darauf zu achten, dass hier nicht relativ vom rules Item aus gesucht wird (wie sonst üblich), sondern relativ -zum Zustandsitem. Es reicht also ein Punkt vor dem Namen des Zustandes. +zum Zustandsitem. Es reicht also ein einzelner Punkt vor dem Namen des Zustandes. Ein Beispiel: Der in der Hierarchie ganz oben (unter lock und release) @@ -85,19 +90,17 @@ Und bleibt auch dort, selbst wenn es noch Nacht ist. Werden beim nächsten Check die Bedingungen für den Nachzustand nicht mehr erfüllt, passiert nach wie vor nichts, da ja der Suspendzustand ohnehin übergeordnet -ist. +ist. Intern wird der Zustand allerdings nach einer Aktualisierung +der released_by Einträge (z.B. wenn sie durch Items oder evals definiert +sind) eine Position über den "Suspend" Zustand kopiert. -Sind bei einem späteren Check der Zustände allerdings +Sind bei einem späteren Check der Zustände die Bedingungen aus einem Bedingungsset für "Nacht" alle wahr, -wird der Suspendmodus deaktiviert und der nächste mögliche -Zustand in der Hierarchie wird eingenommen. Im obigen -Beispiel wäre das der Nacht-Zustand. - -Um die Abfolge der Zustände bzw. interne Informationen -zur Funktionsweise des Release-Features auch nach einem -Neustart zur Verfügung zu haben, sind zwei zusätzliche -Items notwendig. Diese sind bereits in der struct Vorlage -``stateengine.general`` vorhanden. +wird der Suspendmodus aufgelöst. Die Leave-Aktionen werden ausgeführt, +der intern kopierte Zustand und alle anderen released_by Zustände werden +aus der Hierarchie gelöscht und der nächste mögliche Zustand in der Hierarchie +wird nach einer neuen Evaluierung eingenommen. +Im obigen Beispiel wäre das der Nacht-Zustand. Zustandsnamen ------------- diff --git a/stateengine/webif/__init__.py b/stateengine/webif/__init__.py index 4ce23edc6..de8ba7332 100755 --- a/stateengine/webif/__init__.py +++ b/stateengine/webif/__init__.py @@ -58,6 +58,7 @@ def __init__(self, webif_dir, plugin): self.webif_dir = webif_dir self.plugin = plugin self.tplenv = self.init_template_environment() + self.vis_enabled = plugin.vis_enabled @cherrypy.expose def index(self, action=None, item_id=None, item_path=None, reload=None, abitem=None, page='index'): @@ -80,12 +81,14 @@ def index(self, action=None, item_id=None, item_path=None, reload=None, abitem=N self.logger.warning("Item {} not initialized yet. " "Try again later. Error: {}".format(abitem, e)) return None - self.plugin.get_graph(abitem, 'graph') + if self.vis_enabled: + self.plugin.get_graph(abitem, 'graph') tmpl = self.tplenv.get_template('visu.html') return tmpl.render(p=self.plugin, item=abitem, language=self.plugin.get_sh().get_defaultlanguage(), now=self.plugin.shtime.now()) # add values to be passed to the Jinja2 template eg: tmpl.render(p=self.plugin, interface=interface, ...) return tmpl.render(p=self.plugin, + vis_enabled=self.vis_enabled, webif_pagelength=pagelength, item_count=len(self.plugin._items), language=self.plugin.get_sh().get_defaultlanguage(), now=self.plugin.shtime.now()) @@ -106,7 +109,9 @@ def get_data_html(self, dataSet=None): for item in self.plugin.get_items(): conditionset = item.lastconditionset_name conditionset = "-" if conditionset == "" else conditionset - data.update({item.id: {'laststate': item.laststate_name, 'lastconditionset': conditionset}}) + log_level = str(item.logger.log_level) + data.update({item.id: {'laststate': item.laststate_name, + 'lastconditionset': conditionset, 'log_level': log_level}}) try: return json.dumps(data) except Exception as e: diff --git a/stateengine/webif/static/img/visualisations/.gitignore b/stateengine/webif/static/img/visualisations/.gitignore index d6b7ef32c..ff837c366 100755 --- a/stateengine/webif/static/img/visualisations/.gitignore +++ b/stateengine/webif/static/img/visualisations/.gitignore @@ -1,2 +1,3 @@ -* +*.svg +se_test !.gitignore diff --git a/stateengine/webif/static/img/visualisations/sign_false.png b/stateengine/webif/static/img/visualisations/sign_false.png new file mode 100644 index 000000000..6d6433cc1 Binary files /dev/null and b/stateengine/webif/static/img/visualisations/sign_false.png differ diff --git a/stateengine/webif/static/img/visualisations/sign_true.png b/stateengine/webif/static/img/visualisations/sign_true.png new file mode 100644 index 000000000..c77f8557e Binary files /dev/null and b/stateengine/webif/static/img/visualisations/sign_true.png differ diff --git a/stateengine/webif/static/img/visualisations/sign_warn.png b/stateengine/webif/static/img/visualisations/sign_warn.png new file mode 100644 index 000000000..e58ab170e Binary files /dev/null and b/stateengine/webif/static/img/visualisations/sign_warn.png differ diff --git a/stateengine/webif/templates/index.html b/stateengine/webif/templates/index.html index b6b70a42f..183d5d6f1 100755 --- a/stateengine/webif/templates/index.html +++ b/stateengine/webif/templates/index.html @@ -35,19 +35,8 @@