diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index e8f2392da..04679281e 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -22,6 +22,7 @@ from . import StateEngineEval from . import StateEngineValue from . import StateEngineDefaults +from . import StateEngineCurrent import datetime from lib.shtime import Shtime import re @@ -44,11 +45,13 @@ def action_status(self): # Cast function for delay # value: value to cast @staticmethod - def __cast_delay(value): + def __cast_seconds(value): if isinstance(value, str): delay = value.strip() if delay.endswith('m'): return int(delay.strip('m')) * 60 + elif delay.endswith('h'): + return int(delay.strip('h')) * 3600 else: return int(delay) elif isinstance(value, int): @@ -71,11 +74,14 @@ def __init__(self, abitem, name: str): self.__delay = StateEngineValue.SeValue(self._abitem, "delay") self.__repeat = None self.__instanteval = None + self.nextconditionset = StateEngineValue.SeValue(self._abitem, "nextconditionset", True, "str") self.conditionset = StateEngineValue.SeValue(self._abitem, "conditionset", True, "str") self.previousconditionset = StateEngineValue.SeValue(self._abitem, "previousconditionset", True, "str") self.previousstate_conditionset = StateEngineValue.SeValue(self._abitem, "previousstate_conditionset", True, "str") self.__mode = StateEngineValue.SeValue(self._abitem, "mode", True, "str") self.__order = StateEngineValue.SeValue(self._abitem, "order", False, "num") + self._minagedelta = StateEngineValue.SeValue(self._abitem, "minagedelta") + self._agedelta = 0 self._scheduler_name = None self._function = None self.__template = None @@ -83,67 +89,84 @@ def __init__(self, abitem, name: str): self._retrigger_issue = None self._suspend_issue = None self.__queue = abitem.queue - - def update_delay(self, value): + self._action_type = None + self._state = None + self._info_dict = {} + + def update_action_details(self, state, action_type): + if self._action_type is None: + self._action_type = action_type + if self._state is None: + self._state = state + self._log_develop("Updating state for action {} to {}, action type {}", self._name, state.id, action_type) + + def _update_value(self, value_type, value, attribute, cast=None): _issue_list = [] - _, _, _issue, _ = self.__delay.set(value) + if value_type is None: + return _issue_list + _, _, _issue, _ = value_type.set(value) if _issue: - _issue = {self._name: {'issue': _issue, 'attribute': 'delay', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} - _issue_list.append(_issue) - _issue = self.__delay.set_cast(SeActionBase.__cast_delay) - if _issue: - _issue = {self._name: {'issue': _issue, 'attribute': 'delay', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = {self._name: {'issue': _issue, 'attribute': [attribute], + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} _issue_list.append(_issue) + if cast == 'seconds': + _issue = value_type.set_cast(SeActionBase.__cast_seconds) + if _issue: + _issue = {self._name: {'issue': _issue, 'attribute': [attribute], + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} + _issue_list.append(_issue) _issue_list = StateEngineTools.flatten_list(_issue_list) return _issue_list + def update_delay(self, value): + _issue = self._update_value(self.__delay, value, 'delay', 'seconds') + return _issue + def update_instanteval(self, value): - if self.__instanteval is None: - self.__instanteval = StateEngineValue.SeValue(self._abitem, "instanteval", False, "bool") - _, _, _issue, _ = self.__instanteval.set(value) - _issue = {self._name: {'issue': _issue, 'attribute': 'instanteval', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = self._update_value(self.__instanteval, value, 'instanteval') + return _issue + + def update_mindelta(self, value): + self._log_warning("Mindelta is only relevant for set (force) actions - ignoring {}", value) + _issue = {self._name: {'issue': 'Mindelta not relevant for this action type', 'attribute': ['mindelta'], + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} + return _issue + + def update_minagedelta(self, value): + if self._minagedelta is None: + self._minagedelta = StateEngineValue.SeValue(self._abitem, "minagedelta", False, "num") + _issue = self._update_value(self._minagedelta, value, 'minagedelta', 'seconds') return _issue def update_repeat(self, value): if self.__repeat is None: self.__repeat = StateEngineValue.SeValue(self._abitem, "repeat", False, "bool") - _, _, _issue, _ = self.__repeat.set(value) - _issue = {self._name: {'issue': _issue, 'attribute': 'repeat', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = self._update_value(self.__repeat, value, 'repeat') return _issue def update_order(self, value): - _, _, _issue, _ = self.__order.set(value) - _issue = {self._name: {'issue': _issue, 'attribute': 'order', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = self._update_value(self.__order, value, 'order') + return _issue + + def update_nextconditionset(self, value): + _issue = self._update_value(self.nextconditionset, value, 'nextconditionset') return _issue def update_conditionset(self, value): - _, _, _issue, _ = self.conditionset.set(value) - _issue = {self._name: {'issue': _issue, 'attribute': 'conditionset', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = self._update_value(self.conditionset, value, 'conditionset') return _issue def update_previousconditionset(self, value): - _, _, _issue, _ = self.previousconditionset.set(value) - _issue = {self._name: {'issue': _issue, 'attribute': 'previousconditionset', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = self._update_value(self.previousconditionset, value, 'previousconditionset') return _issue def update_previousstate_conditionset(self, value): - _, _, _issue, _ = self.previousstate_conditionset.set(value) - _issue = {self._name: {'issue': _issue, 'attribute': 'previousstate_conditionset', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = self._update_value(self.previousstate_conditionset, value, 'previousstate_conditionset') return _issue def update_mode(self, value): - _value, _, _issue, _ = self.__mode.set(value) - _issue = {self._name: {'issue': _issue, 'attribute': 'mode', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} - return _value[0], _issue + _issue = self._update_value(self.__mode, value, 'mode') + return _issue def get_order(self): order = self.__order.get(1) @@ -152,64 +175,66 @@ def get_order(self): order = 1 return order - 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 + def update_webif_actionstatus(self, state, name, success, issue=None, reason=None): 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'] + if self._action_type == "actions_leave": + state.update_name(state.state_item) + _key_name = ['{}'.format(state.id), 'name'] + self._abitem.update_webif(_key_name, state.name, True) + if self._abitem.webif_infos[state.id].get(self._action_type): + _key = ['{}'.format(state.id), self._action_type, '{}'.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 + if issue is not None: + _key = ['{}'.format(state.id), self._action_type, '{}'.format(name), 'actionstatus', 'issue'] + self._abitem.update_webif(_key, issue) + if reason is not None: + _key = ['{}'.format(state.id), self._action_type, '{}'.format(name), 'actionstatus', 'reason'] + self._abitem.update_webif(_key, reason) + except Exception as ex: + self._log_warning("Error setting action status {}: {}", name, ex) # Write action to logger def write_to_logger(self): self._log_info("function: {}", self._function) - self.__delay.write_to_logger() + delay = self.__delay.write_to_logger() or 0 if self.__repeat is not None: - self.__repeat.write_to_logger() + repeat = self.__repeat.write_to_logger() + else: + repeat = False if self.__instanteval is not None: - self.__instanteval.write_to_logger() + instanteval = self.__instanteval.write_to_logger() + else: + instanteval = False + if self.nextconditionset is not None: + nextconditionset = self.nextconditionset.write_to_logger() + else: + nextconditionset = None if self.conditionset is not None: - self.conditionset.write_to_logger() + conditionset = self.conditionset.write_to_logger() + else: + conditionset = None if self.previousconditionset is not None: - self.previousconditionset.write_to_logger() + previousconditionset = self.previousconditionset.write_to_logger() + else: + previousconditionset = None if self.previousstate_conditionset is not None: - self.previousstate_conditionset.write_to_logger() + previousstate_conditionset = self.previousstate_conditionset.write_to_logger() + else: + previousstate_conditionset = None if self.__mode is not None: - self.__mode.write_to_logger() - self.__order.write_to_logger() - - def set_source(self, current_condition, previous_condition, previousstate_condition): + mode = self.__mode.write_to_logger() + else: + mode = None + order = self.__order.write_to_logger() or 0 + self._info_dict.update({'function': str(self._function), 'nextconditionset': nextconditionset, + 'conditionset': conditionset, 'repeat': str(repeat), 'delay': str(delay), 'mode': mode, + 'order': str(order), 'previousconditionset': previousconditionset, + 'instanteval': str(instanteval), 'previousstate_conditionset': previousstate_conditionset, + 'actionstatus': {}}) + + def set_source(self, current_condition, previous_condition, previousstate_condition, next_condition): source = [] - if current_condition in [[], None] and previous_condition in [[], None] and previousstate_condition in [[], None]: + if current_condition in [[], None] and previous_condition in [[], None] and previousstate_condition in [[], None] and next_condition in [[], None]: source = self._parent else: if current_condition != []: @@ -218,6 +243,8 @@ def set_source(self, current_condition, previous_condition, previousstate_condit source.append("previouscondition={}".format(previous_condition)) if previousstate_condition != []: source.append("previousstate_condition={}".format(previousstate_condition)) + if next_condition != []: + source.append("nextcondition={}".format(next_condition)) source = ", ".join(source) return source @@ -230,7 +257,7 @@ def set_source(self, current_condition, previous_condition, previousstate_condit # newly evaluated mindelta # Any issue that might have occured as a dict def check_getitem_fromeval(self, check_item, check_value=None, check_mindelta=None): - _issue = {self._name: {'issue': None, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = {self._name: {'issue': None, 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} if isinstance(check_item, str): item = None #self._log_develop("Get item from eval on {} {}", self._function, check_item) @@ -245,21 +272,21 @@ def check_getitem_fromeval(self, check_item, check_value=None, check_mindelta=No "plain eval expression without a preceeding eval. "\ "Please update your config of {}" _issue = { - self._name: {'issue': _text.format(check_item), 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + self._name: {'issue': _text.format(check_item), 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} self._log_warning(_text, check_item) _, _, item = item.partition(":") elif re.match(r'^.*:', item): _text = "se_eval/item attributes have to be plain eval expression. Please update your config of {}" _issue = { self._name: {'issue': _text.format(check_item), - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} self._log_warning(_text, check_item) _, _, item = item.partition(":") item = eval(item) if item is not None: check_item, _issue = self._abitem.return_item(item) _issue = { - self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + self._name: {'issue': _issue, 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} if check_value: check_value.set_cast(check_item.cast) if check_mindelta: @@ -271,25 +298,54 @@ def check_getitem_fromeval(self, check_item, check_value=None, check_mindelta=No else: self._log_develop("Got no item from eval on {} with initial item {}", self._function, item) except Exception as ex: - _issue = {self._name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = {self._name: {'issue': ex, 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} # raise Exception("Problem evaluating item '{}' from eval: {}".format(check_item, ex)) self._log_error("Problem evaluating item '{}' from eval: {}", check_item, ex) check_item = None if item is None: _issue = {self._name: {'issue': ['Item {} from eval not existing'.format(check_item)], - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} # raise Exception("Problem evaluating item '{}' from eval. It does not exist.".format(check_item)) self._log_error("Problem evaluating item '{}' from eval. It does not exist", check_item) check_item = None elif check_item is None: _issue = {self._name: {'issue': ['Item is None'], - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} return check_item, check_value, check_mindelta, _issue - def check_complete(self, state, check_item, check_status, check_mindelta, check_value, action_type, evals_items=None, use=None): + def eval_minagedelta(self, actioninfo, state): + lastrun = self._abitem.last_run.get(self._name) + if not lastrun: + return False + if not self._minagedelta.is_empty(): + minagedelta = self._minagedelta.get() + try: + minagedelta = float(minagedelta) + except Exception: + self._log_warning("{0}: minagedelta {1} seems to be no number.", self._name, minagedelta) + minagedelta = 0.0 + self._agedelta = float((datetime.datetime.now() - lastrun).total_seconds()) + self._info_dict.update({'agedelta': self._agedelta, 'minagedelta': str(minagedelta)}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) + if self._agedelta < minagedelta: + text = "{0}: {1} because age delta '{2:.2f}' is lower than minagedelta '{3:.2f}'." + self._log_debug(text, self.name, actioninfo, self._agedelta, minagedelta) + self.update_webif_actionstatus(state, self._name, 'False', None, + f"(age delta '{self._agedelta:.2f}' < '{minagedelta:.2f})") + return True + else: + text = "{0}: Proceeding as age delta '{1:.2f}' is higher than minagedelta '{2:.2f}'." + self.update_webif_actionstatus(state, self._name, 'True', None, + f"(age delta '{self._agedelta:.2f}' > '{minagedelta:.2f})") + self._log_debug(text, self.name, self._agedelta, minagedelta) + return False + else: + return False + + def check_complete(self, state, check_item, check_status, check_mindelta, check_minagedelta, check_value, action_type, evals_items=None, use=None): _issue = {self._name: {'issue': None, 'issueorigin': [{'state': state.id, 'action': self._function}]}} - self._log_develop("Check item {} status {} value {} use {} evals_items {}", check_item, check_status, check_value, use, evals_items) try: _name = evals_items.get(self.name) if _name is not None: @@ -337,6 +393,11 @@ def check_complete(self, state, check_item, check_status, check_mindelta, check_ if mindelta is not None: check_mindelta.set(mindelta) + if check_minagedelta.is_empty(): + minagedelta = StateEngineTools.find_attribute(self._sh, state, "se_minagedelta_" + self._name, 0, use) + if minagedelta is not None: + check_minagedelta.set(minagedelta) + if check_status is not None: check_value.set_cast(check_status.cast) check_mindelta.set_cast(check_status.cast) @@ -358,7 +419,7 @@ def check_complete(self, state, check_item, check_status, check_mindelta, check_ _issue = {self._name: {'issue': None, 'issueorigin': [{'state': state.id, 'action': self._function}]}} - return check_item, check_status, check_mindelta, check_value, _issue + return check_item, check_status, check_mindelta, check_minagedelta, check_value, _issue # Execute action (considering delay, etc) # is_repeat: Indicate if this is a repeated action without changing the state @@ -366,24 +427,28 @@ def check_complete(self, state, check_item, check_status, check_mindelta, check_ # state: state item that triggered the action def execute(self, is_repeat: bool, allow_item_repeat: bool, state): # check if any conditiontype is met or not - # condition: type of condition 'conditionset'/'previousconditionset'/'previousstate_conditionset' + # condition: type of condition 'conditionset'/'previousconditionset'/'previousstate_conditionset'/'nextconditionset' def _check_condition(condition: str): _conditions_met_count = 0 _conditions_necessary_count = 0 _condition_to_meet = None - _updated__current_condition = 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 + _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 + _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 + _updated_current_condition = self._abitem.get_previousstate_id() if _current_condition == '' else _current_condition + elif condition == 'nextconditionset': + _condition_to_meet = None if self.nextconditionset.is_empty() else self.nextconditionset.get() + _current_condition = self._abitem.get_nextconditionset_id() + _updated_current_condition = self._abitem.get_variable("next.conditionset_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: @@ -392,13 +457,14 @@ def _check_condition(condition: str): _orig_cond = cond try: cond = re.compile(cond) - _matching = cond.fullmatch(_updated__current_condition) + _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) + 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) + self._log_debug("Given {} {} not matching current one: {}", condition, _orig_cond, _updated_current_condition) + self.update_webif_actionstatus(state, self._name, 'False', None, f"({condition} {_orig_cond} not met)") except Exception as ex: if cond is not None: self._log_warning("Given {} {} is not a valid regex: {}", condition, _orig_cond, ex) @@ -407,25 +473,29 @@ def _check_condition(condition: str): # 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): + def _update_delay_webif(delay_info: str): + if self._action_type == "actions_leave": + try: + state.update_name(state.state_item) + _key_name = ['{}'.format(state.id), 'name'] + self._abitem.update_webif(_key_name, state.name, True) + except Exception: + pass try: - _key = ['{}'.format(state.id), '{}'.format(action_type), '{}'.format(self._name), 'delay'] - self._abitem.update_webif(_key, delay_info) + _key = ['{}'.format(state.id), self._action_type, '{}'.format(self._name), 'delay'] + self._abitem.update_webif(_key, delay_info, True) 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) + _key1 = [state.id, self._action_type, self._name, 'repeat'] + self._abitem.update_webif(_key1, value, True) self._log_decrease_indent(50) self._log_increase_indent() - self._log_info("Action '{0}': Preparing", self._name) + self._log_info("Action '{0}' defined in '{1}': Preparing", self._name, self._action_type) self._log_increase_indent() try: self._getitem_fromeval() @@ -439,20 +509,32 @@ def _update_repeat_webif(value: bool): return conditions_met = 0 condition_necessary = 0 - current_condition_met, cur_conditions_met, cur_condition_necessary = _check_condition('conditionset') - conditions_met += cur_conditions_met - condition_necessary += min(1, cur_condition_necessary) - previous_condition_met, prev_conditions_met, prev_condition_necessary = _check_condition('previousconditionset') - conditions_met += prev_conditions_met - condition_necessary += min(1, prev_condition_necessary) - previousstate_condition_met, prevst_conditions_met, prevst_condition_necessary = _check_condition('previousstate_conditionset') - conditions_met += prevst_conditions_met - condition_necessary += min(1, prevst_condition_necessary) + current_condition_met = None + previous_condition_met = None + previousstate_condition_met = None + next_condition_met = None + if not self.conditionset.is_empty(): + current_condition_met, cur_conditions_met, cur_condition_necessary = _check_condition('conditionset') + conditions_met += cur_conditions_met + condition_necessary += min(1, cur_condition_necessary) + if not self.previousconditionset.is_empty(): + previous_condition_met, prev_conditions_met, prev_condition_necessary = _check_condition('previousconditionset') + conditions_met += prev_conditions_met + condition_necessary += min(1, prev_condition_necessary) + if not self.previousstate_conditionset.is_empty(): + previousstate_condition_met, prevst_conditions_met, prevst_condition_necessary = _check_condition('previousstate_conditionset') + conditions_met += prevst_conditions_met + condition_necessary += min(1, prevst_condition_necessary) + if not self.nextconditionset.is_empty(): + next_condition_met, next_conditions_met, next_conditionset_necessary = _check_condition('nextconditionset') + conditions_met += next_conditions_met + condition_necessary += min(1, next_conditionset_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 - + elif condition_necessary > 0 and conditions_met == condition_necessary: + self.update_webif_actionstatus(state, self._name, 'True', None, "(all conditions met)") if is_repeat: if self.__repeat is None: if allow_item_repeat: @@ -460,17 +542,26 @@ def _update_repeat_webif(value: bool): _update_repeat_webif(True) else: self._log_info("Action '{0}': Repeat denied by item configuration.", self._name) + self.update_webif_actionstatus(state, self._name, 'False', None, "(no repeat by item)") _update_repeat_webif(False) return elif self.__repeat.get(): repeat_text = " Repeat allowed by action configuration." + self.update_webif_actionstatus(state, self._name, 'True') _update_repeat_webif(True) else: self._log_info("Action '{0}': Repeat denied by action configuration.", self._name) + self.update_webif_actionstatus(state, self._name, 'False', None, "(no repeat by action)") _update_repeat_webif(False) return else: - repeat_text = "" + if self.__repeat is None: + repeat_text = "" + elif self.__repeat.get(): + repeat_text = " Repeat allowed by action configuration but not applicable." + self.update_webif_actionstatus(state, self._name, 'True') + else: + repeat_text = "" self._log_increase_indent() if _validitem: delay = 0 if self.__delay.is_empty() else self.__delay.get() @@ -483,7 +574,7 @@ def _update_repeat_webif(value: bool): except Exception: pass - actionname = "Action '{0}'".format(self._name) if delay == 0 else "Delayed Action ({0} seconds) '{1}'".format( + 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: @@ -502,18 +593,10 @@ def _update_repeat_webif(value: bool): self._log_decrease_indent() _delay_info = -1 else: - self._waitforexecute(state, actionname, self._name, repeat_text, delay, current_condition_met, previous_condition_met, previousstate_condition_met) + _delay_info = delay + self._waitforexecute(state, actionname, self._name, repeat_text, delay, current_condition_met, previous_condition_met, previousstate_condition_met, next_condition_met) - _update_delay_webif('actions_stay', str(_delay_info)) - _update_delay_webif('actions_enter', str(_delay_info)) - _update_delay_webif('actions_enter_or_stay', str(_delay_info)) - try: - state.update_name(state.state_item) - _key_name = ['{}'.format(state.id), 'name'] - self._abitem.update_webif(_key_name, state.name) - _update_delay_webif('actions_leave', str(_delay_info)) - except Exception: - pass + _update_delay_webif(str(_delay_info)) # set the action based on a set_(action_name) attribute # value: Value of the set_(action_name) attribute @@ -522,29 +605,28 @@ def update(self, value): # Complete action # state: state (item) to read from - def complete(self, state, evals_items=None, use=None): + def complete(self, evals_items=None, use=None): raise NotImplementedError("Class {} doesn't implement complete()".format(self.__class__.__name__)) # Check if execution is possible def _can_execute(self, state): return True - def get(self): - return True - - def _waitforexecute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", delay: int = 0, current_condition: list[str] = None, previous_condition: list[str] = None, previousstate_condition: list[str] = None): + def _waitforexecute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", delay: int = 0, current_condition: list[str] = None, previous_condition: list[str] = None, previousstate_condition: list[str] = None, next_condition: list[str] = None): if current_condition is None: current_condition = [] if previous_condition is None: previous_condition = [] if previousstate_condition is None: previousstate_condition = [] + if next_condition is None: + next_condition = [] self._log_decrease_indent(50) self._log_increase_indent() if delay == 0: self._log_info("Action '{}': Running.", namevar) - self.real_execute(state, actionname, namevar, repeat_text, None, False, current_condition, previous_condition, previousstate_condition) + self.real_execute(state, actionname, namevar, repeat_text, None, False, current_condition, previous_condition, previousstate_condition, next_condition) else: instanteval = None if self.__instanteval is None else self.__instanteval.get() self._log_info("Action '{0}': Add {1} second timer '{2}' " @@ -554,7 +636,7 @@ def _waitforexecute(self, state, actionname: str, namevar: str = "", repeat_text if instanteval is True: self._log_increase_indent() self._log_debug("Evaluating value for delayed action '{}'.", namevar) - value = self.real_execute(state, 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, next_condition) self._log_debug("Value for delayed action is going to be '{}'.", value) self._log_decrease_indent() else: @@ -567,113 +649,123 @@ def _waitforexecute(self, state, actionname: str, namevar: str = "", repeat_text 'current_condition': current_condition, 'previous_condition': previous_condition, 'previousstate_condition': previousstate_condition, - 'state': state}, next=next_run) + 'next_condition': next_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, state=None, caller=None): + def _delayed_execute(self, actionname: str, namevar: str = "", repeat_text: str = "", value=None, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None, state=None, caller=None): if state: self._log_debug("Putting delayed action '{}' from state '{}' into queue. Caller: {}", namevar, state, caller) - self.__queue.put(["delayedaction", self, actionname, namevar, repeat_text, value, current_condition, previous_condition, previousstate_condition, state]) + self.__queue.put(["delayedaction", self, actionname, namevar, repeat_text, value, current_condition, previous_condition, previousstate_condition, next_condition, state]) else: self._log_debug("Putting delayed action '{}' into queue. Caller: {}", namevar, caller) - self.__queue.put(["delayedaction", self, actionname, namevar, repeat_text, value, current_condition, previous_condition, previousstate_condition]) + self.__queue.put(["delayedaction", self, actionname, namevar, repeat_text, value, current_condition, previous_condition, previousstate_condition, next_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, state, 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, next_condition=None): raise NotImplementedError("Class {} doesn't implement real_execute()".format(self.__class__.__name__)) def _getitem_fromeval(self): return -# Class representing a single "se_set" action -class SeActionSetItem(SeActionBase): - # Initialize the action - # abitem: parent SeItem instance - # name: Name of action +# Class with methods that are almost the same for set and force +class SeActionMixSetForce: def __init__(self, abitem, name: str): super().__init__(abitem, name) - self.__item = None - self.__eval_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" - - def __repr__(self): - return "SeAction Set {}".format(self._name) + self._item = None + self._eval_item = None + self._status = None + self._delta = 0 + self._value = StateEngineValue.SeValue(self._abitem, "value") + self._mindelta = StateEngineValue.SeValue(self._abitem, "mindelta", False, "num") def _getitem_fromeval(self): - if self.__item is None: + if self._item is None: return - self.__eval_item = self.__item - self.__item, self.__value, self.__mindelta, _issue = self.check_getitem_fromeval(self.__item, self.__value, - self.__mindelta) - if self.__item is None: + self._eval_item = self._item + self._item, self._value, self._mindelta, _issue = self.check_getitem_fromeval(self._item, self._value, + self._mindelta) + if self._item is None: self._action_status = _issue - raise Exception("Problem evaluating item '{}' from eval.".format(self.__item)) + raise Exception("Problem evaluating item '{}' from eval.".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): - _, _, _issue, _ = self.__value.set(value) - _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _, _, _issue, _ = self._value.set(value) + _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} return _issue - # Complete action - # state: state (item) to read from - def complete(self, state, evals_items=None, use=None): - self.__item, self.__status, self.__mindelta, self.__value, _issue = self.check_complete( - state, self.__item, self.__status, self.__mindelta, self.__value, "set", evals_items, use) - self._action_status = _issue - return _issue - - # Write action to logger def write_to_logger(self): SeActionBase.write_to_logger(self) - if isinstance(self.__item, str): + if isinstance(self._item, str): try: - self._log_debug("item from eval: {0}", self.__item) + self._log_debug("item from eval: {0}", self._item) self._log_increase_indent() - current, _, _, _ = self.check_getitem_fromeval(self.__item) + current, _, _, _ = self.check_getitem_fromeval(self._item) self._log_debug("Currently eval results in {}", current) self._log_decrease_indent() except Exception as ex: + current = None self._log_warning("Issue while getting item from eval {}", ex) - elif self.__item is not None: - self._log_debug("item: {0}", self.__item.property.path) + item = current + elif self._item is not None: + self._log_debug("item: {0}", self._item.property.path) + item = self._item.property.path else: self._log_debug("item is not defined! Check log file.") - self.__mindelta.write_to_logger() - self.__value.write_to_logger() + item = None + mindelta = self._mindelta.write_to_logger() or 0 + minagedelta = self._minagedelta.write_to_logger() or 0 + value = self._value.write_to_logger() + self._info_dict.update({'item': item, 'mindelta': str(mindelta), 'minagedelta': str(minagedelta), 'agedelta': str(self._agedelta), 'delta': str(self._delta), 'value': str(value)}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) + return value + + # Complete action + # state: state (item) to read from + def complete(self, evals_items=None, use=None): + self._log_develop('Completing action {}, action type {}, state {}', self._name, self._action_type, self._state) + self._abitem.set_variable('current.action_name', self._name) + self._abitem.set_variable('current.state_name', self._state.name) + self._item, self._status, self._mindelta, self._minagedelta, self._value, _issue = self.check_complete( + self._state, self._item, self._status, self._mindelta, self._minagedelta, self._value, "set/force", evals_items, use) + self._action_status = _issue + + self._abitem.set_variable('current.action_name', '') + self._abitem.set_variable('current.state_name', '') + return _issue # Check if execution is possible def _can_execute(self, state): - if self.__item is None: + if self._item is None: 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') + self.update_webif_actionstatus(state, self._name, 'False', 'Action {}: No item defined'.format(self._name)) return False - if self.__value.is_empty(): + if self._value.is_empty(): self._log_increase_indent() - self._log_warning("Action '{0}': No value for item {1} defined. Ignoring.", self._name, self.__item) + 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)) + self.update_webif_actionstatus(state, self._name, 'False', 'Action {}: No value for item {}'.format(self._name, 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, state, 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, next_condition=None): self._abitem.set_variable('current.action_name', namevar) self._log_increase_indent() if value is None: - value = self.__value.get() + value = self._value.get() + self._info_dict.update({'value': str(value)}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) if value is None: self._log_debug("{0}: Value is None", actionname) @@ -684,72 +776,118 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if returnvalue: self._log_decrease_indent() return value - - if not self.__mindelta.is_empty(): - mindelta = self.__mindelta.get() - if self.__status is not None: - # noinspection PyCallingNonCallable - delta = float(abs(self.__status() - value)) - additionaltext = "of statusitem " - else: - delta = float(abs(self.__item() - value)) + minagedelta = self.eval_minagedelta(f"Not setting {self._item.property.path} to {value}", state) + if minagedelta: + return + if not self._mindelta.is_empty(): + mindelta = float(self._mindelta.get()) + try: + 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 = "" + except Exception: + delta = None additionaltext = "" + self._log_warning("{0}: Can not evaluate delta as value '{1}' is no number.", self._name, value) + if delta is not None: + self._delta = delta + self._info_dict.update({'delta': str(delta), 'mindelta': str(mindelta)}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) + if delta < mindelta: + text = "{0}: Not setting '{1}' to '{2}' because delta {3}'{4:.2f}' is lower than mindelta '{5:.2f}'." + self._log_debug(text, actionname, self._item.property.path, value, additionaltext, delta, mindelta) + self.update_webif_actionstatus(state, self._name, 'False', None, f"(delta '{delta:.2f}' < '{mindelta:.2f})") + return + else: + text = "{0}: Proceeding because delta {1}'{2:.2f}' is lower than mindelta '{3:.2f}'." + self.update_webif_actionstatus(state, self._name, 'True', None, + f"(delta '{delta:.2f}' > '{mindelta:.2f})") + self._log_debug(text, actionname, additionaltext, delta, mindelta) + source = self.set_source(current_condition, previous_condition, previousstate_condition, next_condition) + self._force_set(actionname, self._item, value, source) + self._execute_set_add_remove(state, actionname, namevar, repeat_text, self._item, value, source, current_condition, previous_condition, previousstate_condition, next_condition) + + def _force_set(self, actionname, item, value, source): + pass + + def update_mindelta(self, value): + if self._mindelta is None: + self._mindelta = StateEngineValue.SeValue(self._abitem, "mindelta", False, "num") + _issue = self._update_value(self._mindelta, value, 'mindelta') + return _issue - self.__delta = delta - if delta < mindelta: - text = "{0}: Not setting '{1}' to '{2}' because delta {3}'{4:.2}' is lower than mindelta '{5}'" - self._log_debug(text, actionname, self.__item.property.path, value, additionaltext, delta, mindelta) - self.update_webif_actionstatus(state, self._name, 'False') - return - - 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, state, 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, source, current_condition, previous_condition, previousstate_condition, next_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) + self._log_debug("{0}: Set '{1}' to '{2}'.{3}", actionname, item.property.path, value, repeat_text) pat = r"(?:[^,(]*)\'(.*?)\'" self.update_webif_actionstatus(state, re.findall(pat, actionname)[0], 'True') # noinspection PyCallingNonCallable item(value, caller=self._caller, source=source) - self.__item = self.__eval_item + self._abitem.last_run = {self._name: datetime.datetime.now()} + self._item = self._eval_item - def get(self): - orig_item = self.__item - try: - self._getitem_fromeval() - except Exception as ex: - self._log_warning("Issue while getting item from eval {}", ex) - item_from_eval = orig_item if orig_item != self.__item else False - try: - if self.__item is not None: - item = str(self.__item.property.path) - else: - item = None - except Exception: - item = None - try: - val = self.__value.get() - if val is not None: - value = str(val) + +# Class representing a single "se_set" action +class SeActionSetItem(SeActionMixSetForce, SeActionBase): + # Initialize the action + # abitem: parent SeItem instance + # name: Name of action + def __init__(self, abitem, name: str): + super().__init__(abitem, name) + self._function = "set" + + def __repr__(self): + return "SeAction Set {}".format(self._name) + + # Write action to logger + def write_to_logger(self): + super().write_to_logger() + self._log_debug("force update: no") + + +# Class representing a single "se_force" action +class SeActionForceItem(SeActionMixSetForce, SeActionBase): + # Initialize the action + # abitem: parent SeItem instance + # name: Name of action + def __init__(self, abitem, name: str): + super().__init__(abitem, name) + self._function = "force set" + + def __repr__(self): + return "SeAction Force {}".format(self._name) + + # Write action to logger + def write_to_logger(self): + super().write_to_logger() + self._log_debug("force update: yes") + + def _force_set(self, actionname, item, value, source): + # Set to different value first ("force") + current_value = item() + if current_value == value: + if self._item._type == 'bool': + self._log_debug("{0}: Set '{1}' to '{2}' (Force).", actionname, item.property.path, not value) + item(not value, caller=self._caller, source=source) + elif self._item._type == 'str': + if value != '': + self._log_debug("{0}: Set '{1}' to '{2}' (Force).", actionname, item.property.path, '') + item('', caller=self._caller, source=source) + else: + self._log_debug("{0}: Set '{1}' to '{2}' (Force).", actionname, item.property.path, '-') + item('-', caller=self._caller, source=source) + elif self._item._type == 'num': + self._log_debug("{0}: Set '{1}' to '{2}' (Force).", actionname, item.property.path, current_value+0.1) + item(current_value+0.1, caller=self._caller, source=source) else: - value = None - except Exception: - value = None - self.__item = orig_item - mindelta = self.__mindelta.get() - if mindelta is None: - result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, - 'value': value, 'conditionset': self.conditionset.get(), - 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} + self._log_warning("{0}: Force not implemented for item type '{1}'.", actionname, item._type) else: - result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, - 'value': value, 'conditionset': self.conditionset.get(), - 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}, - 'delta': str(self.__delta), 'mindelta': str(mindelta)} - return result + self._log_debug("{0}: New value differs from old value, no force required.", actionname) # Class representing a single "se_setbyattr" action @@ -770,15 +908,20 @@ def __repr__(self): def update(self, value): self.__byattr = value _issue = {self._name: {'issue': None, 'attribute': self.__byattr, - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} return _issue # Complete action # state: state (item) to read from - def complete(self, state, evals_items=None, use=None): + def complete(self, evals_items=None, use=None): + self._log_develop('Completing action {}, action type {}, state {}', self._name, self._action_type, self._state) + self._abitem.set_variable('current.action_name', self._name) + self._abitem.set_variable('current.state_name', self._state.name) self._scheduler_name = "{}-SeByAttrDelayTimer".format(self.__byattr) _issue = {self._name: {'issue': None, 'attribute': self.__byattr, - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} + self._abitem.set_variable('current.action_name', '') + self._abitem.set_variable('current.state_name', '') return _issue # Write action to logger @@ -786,24 +929,25 @@ def write_to_logger(self): SeActionBase.write_to_logger(self) if self.__byattr is not None: self._log_debug("set by attribute: {0}", self.__byattr) + self._info_dict.update({'byattr': self.__byattr}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) # Really execute the action - 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): + 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, next_condition=None): self._abitem.set_variable('current.action_name', namevar) if returnvalue: return value + minagedelta = self.eval_minagedelta(f"Not setting values by attribute {self.__byattr}", state) + if minagedelta: + return self._log_info("{0}: Setting values by attribute '{1}'.{2}", actionname, self.__byattr, repeat_text) self.update_webif_actionstatus(state, self._name, 'True') - source = self.set_source(current_condition, previous_condition, previousstate_condition) + source = self.set_source(current_condition, previous_condition, previousstate_condition, next_condition) for item in self._sh.find_items(self.__byattr): self._log_info("\t{0} = {1}", item.property.path, item.conf[self.__byattr]) item(item.conf[self.__byattr], caller=self._caller, source=source) - - def get(self): - result = {'function': str(self._function), 'byattr': str(self.__byattr), - 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} - return result + self._abitem.last_run = {self._name: datetime.datetime.now()} # Class representing a single "se_trigger" action @@ -828,15 +972,20 @@ def update(self, value): value = None if value == "" else value _, _, _issue, _ = self.__value.set(value) _issue = {self._name: {'issue': _issue, 'logic': self.__logic, - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} return _issue # Complete action # state: state (item) to read from - def complete(self, state, evals_items=None, use=None): + def complete(self, evals_items=None, use=None): + self._log_develop('Completing action {}, action type {}, state {}', self._name, self._action_type, self._state) + self._abitem.set_variable('current.action_name', self._name) + self._abitem.set_variable('current.state_name', self._state.name) self._scheduler_name = "{}-SeLogicDelayTimer".format(self.__logic) _issue = {self._name: {'issue': None, 'logic': self.__logic, - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} + self._abitem.set_variable('current.action_name', '') + self._abitem.set_variable('current.state_name', '') return _issue # Write action to logger @@ -845,10 +994,16 @@ def write_to_logger(self): if self.__logic is not None: self._log_debug("trigger logic: {0}", self.__logic) if self.__value is not None: - self._log_debug("value: {0}", self.__value) + value = self.__value.write_to_logger() + else: + value = None + self._info_dict.update( + {'logic': self.__logic, 'value': str(value)}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) # Really execute the action - 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): + 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, next_condition=None): self._abitem.set_variable('current.action_name', namevar) if value is None: try: @@ -858,25 +1013,17 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if returnvalue: return value + minagedelta = self.eval_minagedelta(f"Not triggering logic {self.__logic}", state) + if minagedelta: + return + self._info_dict.update({'value': str(value)}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) self.update_webif_actionstatus(state, self._name, 'True') self._log_info("{0}: Triggering logic '{1}' using value '{2}'.{3}", actionname, self.__logic, value, repeat_text) add_logics = 'logics.{}'.format(self.__logic) if not self.__logic.startswith('logics.') else self.__logic self._sh.trigger(add_logics, by=self._caller, source=self._name, value=value) - - def get(self): - try: - val = self.__value.get() - if val is not None: - value = str(val) - else: - value = None - except Exception: - value = None - result = {'function': str(self._function), 'logic': str(self.__logic), - 'value': value, - 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} - return result + self._abitem.last_run = {self._name: datetime.datetime.now()} # Class representing a single "se_run" action @@ -903,15 +1050,20 @@ 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': self._function}]}} + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} return _issue # Complete action # state: state (item) to read from - def complete(self, state, evals_items=None, use=None): + def complete(self, evals_items=None, use=None): + self._log_develop('Completing action {}, action type {}, state {}', self._name, self._action_type, self._state) + self._abitem.set_variable('current.action_name', self._name) + self._abitem.set_variable('current.state_name', self._state.name) self._scheduler_name = "{}-SeRunDelayTimer".format(StateEngineTools.get_eval_name(self.__eval)) _issue = {self._name: {'issue': None, 'eval': StateEngineTools.get_eval_name(self.__eval), - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} + self._abitem.set_variable('current.action_name', '') + self._abitem.set_variable('current.state_name', '') return _issue # Write action to logger @@ -920,10 +1072,29 @@ def write_to_logger(self): if self.__eval is not None: self._log_debug("eval: {0}", StateEngineTools.get_eval_name(self.__eval)) + self._info_dict.update({'eval': str(self.__eval)}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) + # Really execute the action - 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): + 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, next_condition=None): + def log_conditions(): + if current_condition: + self._log_debug("Running eval {0} based on conditionset {1}.", self.__eval, current_condition) + if previous_condition: + self._log_debug("Running eval {0} based on previous conditionset {1}.", self.__eval, previous_condition) + if previousstate_condition: + self._log_debug("Running eval {0} based on previous state's conditionset {1}.", self.__eval, + previousstate_condition) + if next_condition: + self._log_debug("Running eval {0} based on next conditionset {1}.", self.__eval, next_condition) + + minagedelta = self.eval_minagedelta(f"Not running eval {self.__eval}", state) + if minagedelta: + return self._abitem.set_variable('current.action_name', namevar) self._log_increase_indent() + eval_result = '' if isinstance(self.__eval, str): # noinspection PyUnusedLocal sh = self._sh @@ -935,13 +1106,8 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if returnvalue: self._log_decrease_indent() return eval(self.__eval) - if current_condition: - self._log_debug("Running eval {0} based on conditionset {1}", self.__eval, current_condition) - if previous_condition: - self._log_debug("Running eval {0} based on previous conditionset {1}", self.__eval, previous_condition) - if previousstate_condition: - self._log_debug("Running eval {0} based on previous state's conditionset {1}", self.__eval, previousstate_condition) - eval(self.__eval) + log_conditions() + eval_result = eval(self.__eval) self.update_webif_actionstatus(state, self._name, 'True') self._log_decrease_indent() except Exception as ex: @@ -954,13 +1120,8 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if returnvalue: self._log_decrease_indent() return self.__eval() - if current_condition: - self._log_debug("Running eval {0} based on conditionset {1}", self.__eval, current_condition) - if previous_condition: - self._log_debug("Running eval {0} based on previous conditionset {1}", self.__eval, previous_condition) - if previousstate_condition: - self._log_debug("Running eval {0} based on previous state's conditionset {1}", self.__eval, previousstate_condition) - self.__eval() + log_conditions() + eval_result = self.__eval() self.update_webif_actionstatus(state, self._name, 'True') self._log_decrease_indent() except Exception as ex: @@ -968,195 +1129,10 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self.update_webif_actionstatus(state, self._name, 'False', 'Problem calling: {}'.format(ex)) text = "{0}: Problem calling '{0}': {1}." self._log_error(text, actionname, StateEngineTools.get_eval_name(self.__eval), ex) - - def get(self): - result = {'function': str(self._function), 'eval': str(self.__eval), - 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} - return result - - -# Class representing a single "se_force" action -class SeActionForceItem(SeActionBase): - # Initialize the action - # abitem: parent SeItem instance - # name: Name of action - def __init__(self, abitem, name: str): - super().__init__(abitem, name) - self.__item = None - self.__eval_item = None - self.__status = None - self.__value = StateEngineValue.SeValue(self._abitem, "value") - self.__delta = 0 - self.__mindelta = StateEngineValue.SeValue(self._abitem, "mindelta") - self._function = "force set" - - def __repr__(self): - return "SeAction Force {}".format(self._name) - - # set the action based on a set_(action_name) attribute - # value: Value of the set_(action_name) attribute - def update(self, value): - _, _, _issue, _ = self.__value.set(value) - _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} - return _issue - - # Complete action - # state: state (item) to read from - def complete(self, state, evals_items=None, use=None): - self.__item, self.__status, self.__mindelta, self.__value, _issue = self.check_complete( - state, self.__item, self.__status, self.__mindelta, self.__value, "force", evals_items, use) - self._action_status = _issue - return _issue - - # Write action to logger - def write_to_logger(self): - SeActionBase.write_to_logger(self) - if isinstance(self.__item, str): - try: - self._log_debug("item from eval: {0}", self.__item) - self._log_increase_indent() - current, _, _, _ = self.check_getitem_fromeval(self.__item) - self._log_debug("Currently eval results in {}", current) - self._log_decrease_indent() - except Exception as ex: - self._log_warning("Issue while getting item from eval {}", ex) - elif self.__item is not None: - self._log_debug("item: {0}", self.__item.property.path) - else: - self._log_debug("item is not defined! Check log file.") - 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, state): - if self.__item is None: - 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_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 self.__item is None: - return - self.__eval_item = self.__item - self.__item, self.__value, self.__mindelta, _issue = self.check_getitem_fromeval(self.__item, self.__value, - self.__mindelta) - if self.__item is None: - self._action_status = _issue - raise Exception("Problem evaluating item '{}' from eval.".format(self.__item)) - - # Really execute the action (needs to be implemented in derived classes) - # noinspection PyProtectedMember - 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: - value = self.__value.get() - - if value is None: - self._log_debug("{0}: Value is None", actionname) - pat = r"(?:[^,(]*)\'(.*?)\'" - self.update_webif_actionstatus(state, re.findall(pat, actionname)[0], 'False', 'Value is None') - return - - if returnvalue: - self._log_decrease_indent() - return value - - if not self.__mindelta.is_empty(): - mindelta = self.__mindelta.get() - if self.__status is not None: - # noinspection PyCallingNonCallable - delta = float(abs(self.__status() - value)) - additionaltext = "of statusitem " - else: - delta = float(abs(self.__item() - value)) - additionaltext = "" - - self.__delta = delta - if delta < mindelta: - text = "{0}: Not setting '{1}' to '{2}' because delta {3}'{4:.2}' is lower than mindelta '{5}'" - self._log_debug(text, actionname, self.__item.property.path, value, additionaltext, delta, mindelta) - self.update_webif_actionstatus(state, self._name, 'False') - return - source = self.set_source(current_condition, previous_condition, previousstate_condition) - # Set to different value first ("force") - current_value = self.__item() - if current_value == value: - if self.__item._type == 'bool': - self._log_debug("{0}: Set '{1}' to '{2}' (Force)", actionname, self.__item.property.path, not value) - self.__item(not value, caller=self._caller, source=source) - elif self.__item._type == 'str': - if value != '': - self._log_debug("{0}: Set '{1}' to '{2}' (Force)", actionname, self.__item.property.path, '') - self.__item('', caller=self._caller, source=source) - else: - self._log_debug("{0}: Set '{1}' to '{2}' (Force)", actionname, self.__item.property.path, '-') - self.__item('-', caller=self._caller, source=source) - elif self.__item._type == 'num': - self._log_debug("{0}: Set '{1}' to '{2}' (Force)", actionname, self.__item.property.path, current_value+0.1) - self.__item(current_value+0.1, caller=self._caller, source=source) - else: - self._log_warning("{0}: Force not implemented for item type '{1}'", actionname, self.__item._type) - else: - self._log_debug("{0}: New value differs from old value, no force required.", actionname) - self._log_decrease_indent() - self._log_debug("{0}: Set '{1}' to '{2}'.{3}", actionname, self.__item.property.path, value, repeat_text) - self.update_webif_actionstatus(state, self._name, 'True') - # noinspection PyCallingNonCallable - self.__item(value, caller=self._caller, source=source) - self.__item = self.__eval_item - - def get(self): - orig_item = self.__item - try: - self._getitem_fromeval() - except Exception as ex: - self._log_warning("Issue while getting item from eval {}", ex) - item_from_eval = orig_item if orig_item != self.__item else False - try: - if self.__item is not None: - item = str(self.__item.property.path) - else: - item = None - except Exception: - item = None - try: - val = self.__value.get() - if val is not None: - value = str(val) - else: - value = None - except Exception: - value = None - self.__item = orig_item - mindelta = self.__mindelta.get() - if mindelta is None: - result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, - 'value': value, 'conditionset': self.conditionset.get(), - 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} - else: - result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, - 'value': value, 'conditionset': self.conditionset.get(), - 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}, - 'delta': str(self.__delta), 'mindelta': str(mindelta)} - return result + self._abitem.last_run = {self._name: datetime.datetime.now()} + self._info_dict.update({'value': str(eval_result)}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) # Class representing a single "se_special" action @@ -1184,18 +1160,23 @@ 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': self._function}]}} + _issue = {self._name: {'issue': None, 'special': self.__value, 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} return _issue # Complete action # state: state (item) to read from - def complete(self, state, evals_items=None, use=None): + def complete(self, evals_items=None, use=None): + self._log_develop('Completing action {}, action type {}, state {}', self._name, self._action_type, self._state) + self._abitem.set_variable('current.action_name', self._name) + self._abitem.set_variable('current.state_name', self._state.name) if isinstance(self.__value, list): item = self.__value[0].property.path 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': self._function}]}} + _issue = {self._name: {'issue': None, 'special': item, 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} + self._abitem.set_variable('current.action_name', '') + self._abitem.set_variable('current.state_name', '') return _issue # Write action to logger @@ -1206,21 +1187,28 @@ def write_to_logger(self): self._log_debug("value: {0}", self.__value) else: self._log_debug("Retrigger item: {0}", self.__value.property.path) + self._info_dict.update({'value': str(self.__value)}) + self._info_dict.update({'special': str(self.__special)}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) # Really execute the action - 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): + 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, next_condition=None): self._abitem.set_variable('current.action_name', namevar) if returnvalue: return None + minagedelta = self.eval_minagedelta(f"Not executing special action {self.__special}", state) + if minagedelta: + return try: _log_value = self.__value.property.path except Exception: _log_value = self.__value - self._log_info("{0}: Executing special action '{1}' using item '{2}' based on '{3}/{4}/{5}'.{6}", - actionname, self.__special, _log_value, current_condition, previous_condition, previousstate_condition, repeat_text) + self._log_info("{0}: Executing special action '{1}' using item '{2}' based on current condition {3} / previous condition {4} / previousstate condition {5} / next_condition {6}.{7}", + actionname, self.__special, _log_value, current_condition, previous_condition, previousstate_condition, next_condition, repeat_text) self._log_increase_indent() if self.__special == "suspend": - self.suspend_execute(state, current_condition, previous_condition, previousstate_condition) + self.suspend_execute(state, current_condition, previous_condition, previousstate_condition, next_condition) if self._suspend_issue in ["", [], None, [None]]: self.update_webif_actionstatus(state, self._name, 'True') else: @@ -1240,6 +1228,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self.update_webif_actionstatus(state, self._name, 'False', 'Unknown special value {}'.format(self.__special)) raise ValueError("{0}: Unknown special value '{1}'!".format(actionname, self.__special)) self._log_debug("Special action {0}: done", self.__special) + self._abitem.last_run = {self._name: datetime.datetime.now()} def suspend_get_value(self, value): _issue = {self._name: {'issue': None, 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} @@ -1301,10 +1290,10 @@ def retrigger_get_value(self, value): raise ValueError("Action {0}: {1}".format(self._name, text)) return se_item - def suspend_execute(self, state=None, current_condition=None, previous_condition=None, previousstate_condition=None): + def suspend_execute(self, state=None, current_condition=None, previous_condition=None, previousstate_condition=None, next_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)) + source = "SuspendAction, {}".format(self.set_source(current_condition, previous_condition, previousstate_condition, next_condition)) if self._abitem.get_update_trigger_source() == self.__value[1]: # triggered by manual-item: Update suspend item if suspend_item.property.value: @@ -1323,23 +1312,6 @@ def suspend_execute(self, state=None, current_condition=None, previous_condition self._log_debug("Updated variable 'item.suspend_remaining' to {0}", suspend_remaining) self._action_status = _issue - def get(self): - try: - value_result = self.__value.property.path - except Exception: - value_result = self.__value - if isinstance(value_result, list): - for i, val in enumerate(value_result): - try: - value_result[i] = val.property.path - except Exception: - pass - result = {'function': str(self._function), 'special': str(self.__special), - 'value': str(value_result), 'conditionset': self.conditionset.get(), - 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} - return result - # Class representing a single "se_add" action class SeActionAddItem(SeActionSetItem): @@ -1354,40 +1326,17 @@ def __repr__(self): return "SeAction Add {}".format(self._name) def write_to_logger(self): - SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) + SeActionSetItem.write_to_logger(self) - def _execute_set_add_remove(self, state, 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, source, current_condition=None, previous_condition=None, previousstate_condition=None, next_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) - def get(self): - try: - if self.__item is not None: - item = str(self.__item.property.path) - else: - item = None - except Exception: - item = None - try: - val = self.__value.get() - if val is not None: - value = str(val) - else: - value = None - except Exception: - value = None - result = {'function': str(self._function), 'item': item, - 'value': value, 'conditionset': self.conditionset.get(), - 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} - return result - # Class representing a single "se_remove" action class SeActionRemoveFirstItem(SeActionSetItem): @@ -1402,10 +1351,10 @@ def __repr__(self): return "SeAction RemoveFirst {}".format(self._name) def write_to_logger(self): - SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) + SeActionSetItem.write_to_logger(self) - def _execute_set_add_remove(self, state, 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, source, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): currentvalue = item.property.value value = value if isinstance(value, list) else [value] for v in value: @@ -1416,32 +1365,9 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, except Exception as ex: 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): - try: - if self.__item is not None: - item = str(self.__item.property.path) - else: - item = None - except Exception: - item = None - try: - val = self.__value.get() - if val is not None: - value = str(val) - else: - value = None - except Exception: - value = None - result = {'function': str(self._function), 'item': item, - 'value': value, 'conditionset': self.conditionset.get(), - 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} - return result - # Class representing a single "se_remove" action class SeActionRemoveLastItem(SeActionSetItem): @@ -1456,10 +1382,10 @@ def __repr__(self): return "SeAction RemoveLast {}".format(self._name) def write_to_logger(self): - SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) + SeActionSetItem.write_to_logger(self) - def _execute_set_add_remove(self, state, 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, source, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): currentvalue = item.property.value value = value if isinstance(value, list) else [value] for v in value: @@ -1472,32 +1398,9 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, except Exception as ex: 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): - try: - if self.__item is not None: - item = str(self.__item.property.path) - else: - item = None - except Exception: - item = None - try: - val = self.__value.get() - if val is not None: - value = str(val) - else: - value = None - except Exception: - value = None - result = {'function': str(self._function), 'item': item, - 'value': value, 'conditionset': self.conditionset.get(), - 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} - return result - # Class representing a single "se_removeall" action class SeActionRemoveAllItem(SeActionSetItem): @@ -1512,10 +1415,10 @@ def __repr__(self): return "SeAction RemoveAll {}".format(self._name) def write_to_logger(self): - SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) + SeActionSetItem.write_to_logger(self) - def _execute_set_add_remove(self, state, 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, source, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): currentvalue = item.property.value value = value if isinstance(value, list) else [value] for v in value: @@ -1526,28 +1429,5 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, except Exception as ex: 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): - try: - if self.__item is not None: - item = str(self.__item.property.path) - else: - item = None - except Exception: - item = None - try: - val = self.__value.get() - if val is not None: - value = str(val) - else: - value = None - except Exception: - value = None - result = {'function': str(self._function), 'item': item, - 'value': value, 'conditionset': self.conditionset.get(), - 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} - return result diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 89bef33bd..92cf17fcb 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -33,10 +33,15 @@ class SeActions(StateEngineTools.SeItemChild): def __init__(self, abitem): super().__init__(abitem) self.__actions = {} + self.__action_type = None + self.__state = None + self.__unassigned_mindeltas = {} + self.__unassigned_minagedeltas = {} self.__unassigned_delays = {} self.__unassigned_repeats = {} self.__unassigned_instantevals = {} self.__unassigned_orders = {} + self.__unassigned_nextconditionsets = {} self.__unassigned_conditionsets = {} self.__unassigned_previousconditionsets = {} self.__unassigned_previousstate_conditionsets = {} @@ -48,18 +53,6 @@ def __init__(self, abitem): def __repr__(self): return "SeActions, count {}".format(self.count()) - def dict_actions(self, action_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][action_type][name].get('actionstatus')}) - except Exception: - pass - self._abitem.initactionname = None - return result - def reset(self): self.__actions = {} @@ -67,6 +60,13 @@ def reset(self): def count(self): return len(self.__actions) + def update_action_details(self, state, action_type): + if self.__action_type is None: + self.__action_type = action_type + if self.__state is None: + self._log_develop("Updating state for actions: {}, action type: {}", state.id, action_type) + self.__state = state + # update action # attribute: name of attribute that defines action # value: value of the attribute @@ -84,6 +84,8 @@ def update(self, attribute, value): value = ":".join(map(str.strip, value.split(":"))) if value[:1] == '[' and value[-1:] == ']': value = StateEngineTools.convert_str_to_list(value, False) + if name in self.__actions: + self.__actions[name].update_action_details(self.__state, self.__action_type) if func == "se_delay": # set delay if name not in self.__actions: @@ -92,6 +94,22 @@ def update(self, attribute, value): else: _issue = self.__actions[name].update_delay(value) return _count, _issue + elif func == "se_mindelta": + # set mindelta + if name not in self.__actions: + # If we do not have the action yet (delay-attribute before action-attribute), ... + self.__unassigned_mindeltas[name] = value + else: + _issue = self.__actions[name].update_mindelta(value) + return _count, _issue + elif func == "se_minagedelta": + # set minagedelta + if name not in self.__actions: + # If we do not have the action yet (delay-attribute before action-attribute), ... + self.__unassigned_minagedeltas[name] = value + else: + _issue = self.__actions[name].update_minagedelta(value) + return _count, _issue elif func == "se_instanteval": # set instant calculation if name not in self.__actions: @@ -108,6 +126,14 @@ def update(self, attribute, value): else: _issue = self.__actions[name].update_repeat(value) return _count, _issue + elif func == "se_nextconditionset": + # set nextconditionset + if name not in self.__actions: + # If we do not have the action yet (conditionset-attribute before action-attribute), ... + self.__unassigned_nextconditionsets[name] = value + else: + _issue = self.__actions[name].update_nextconditionset(value) + return _count, _issue elif func == "se_conditionset": # set conditionset if name not in self.__actions: @@ -236,6 +262,7 @@ def __ensure_action_exists(self, func, name): # Check if action exists _issue = None if name in self.__actions: + self.__actions[name].update_action_details(self.__state, self.__action_type) return True, _issue # Create action depending on function @@ -262,6 +289,7 @@ def __ensure_action_exists(self, func, name): else: return False, _issue _issue_list = [] + action.update_action_details(self.__state, self.__action_type) if name in self.__unassigned_delays: _issue = action.update_delay(self.__unassigned_delays[name]) if _issue: @@ -274,6 +302,18 @@ def __ensure_action_exists(self, func, name): _issue_list.append(_issue) del self.__unassigned_instantevals[name] + if name in self.__unassigned_mindeltas: + _issue = action.update_mindelta(self.__unassigned_mindeltas[name]) + if _issue: + _issue_list.append(_issue) + del self.__unassigned_mindeltas[name] + + if name in self.__unassigned_minagedeltas: + _issue = action.update_minagedelta(self.__unassigned_minagedeltas[name]) + if _issue: + _issue_list.append(_issue) + del self.__unassigned_minagedeltas[name] + if name in self.__unassigned_repeats: _issue = action.update_repeat(self.__unassigned_repeats[name]) if _issue: @@ -295,6 +335,12 @@ def __ensure_action_exists(self, func, name): _issue_list.append(_issue) del self.__unassigned_orders[name] + if name in self.__unassigned_nextconditionsets: + _issue = action.update_nextconditionset(self.__unassigned_nextconditionsets[name]) + if _issue: + _issue_list.append(_issue) + del self.__unassigned_nextconditionsets[name] + if name in self.__unassigned_conditionsets: _issue = action.update_conditionset(self.__unassigned_conditionsets[name]) if _issue: @@ -308,7 +354,7 @@ def __ensure_action_exists(self, func, name): del self.__unassigned_previousconditionsets[name] if name in self.__unassigned_previousstate_conditionsets: - _issue = action.update_previousconditionset(self.__unassigned_previousstate_conditionsets[name]) + _issue = action.update_previousstate_conditionset(self.__unassigned_previousstate_conditionsets[name]) if _issue: _issue_list.append(_issue) del self.__unassigned_previousstate_conditionsets[name] @@ -317,15 +363,15 @@ def __ensure_action_exists(self, func, name): return True, _issue_list def __handle_combined_action_attribute(self, name, value_list): - def remove_action(ex): + def remove_action(e): if name in self.__actions: del self.__actions[name] - _issue = {name: {'issue': [ex], 'issueorigin': [{'state': 'unknown', 'action': parameter['function']}], 'ignore': True}} - _issue_list.append(_issue) - self._log_warning("Ignoring action {0} because: {1}", name, ex) + i = {name: {'issue': [e], 'issueorigin': [{'state': 'unknown', 'action': parameter['function']}], 'ignore': True}} + _issue_list.append(i) + self._log_warning("Ignoring action {0} because: {1}", name, e) - parameter = {'function': None, 'force': None, 'repeat': None, 'delay': 0, 'order': None, 'conditionset': None, - 'previousconditionset': None, 'previousstate_conditionset': None, 'mode': None, 'instanteval': None} + parameter = {'function': None, 'force': None, 'repeat': None, 'delay': 0, 'order': None, 'nextconditionset': None, 'conditionset': None, + 'previousconditionset': None, 'previousstate_conditionset': None, 'mode': None, 'instanteval': None, 'mindelta': None, 'minagedelta': None} _issue = None _issue_list = [] # value_list needs to be string or list @@ -381,6 +427,7 @@ def remove_action(ex): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'to') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['to']) elif parameter['function'] == "force": _action_exists, _issue = self.__ensure_action_exists("se_force", name) @@ -388,6 +435,7 @@ def remove_action(ex): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'to') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['to']) elif parameter['function'] == "run": _action_exists, _issue = self.__ensure_action_exists("se_run", name) @@ -395,6 +443,7 @@ def remove_action(ex): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'eval') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['eval']) elif parameter['function'] == "byattr": _action_exists, _issue = self.__ensure_action_exists("se_byattr", name) @@ -402,6 +451,7 @@ def remove_action(ex): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'attribute') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['attribute']) elif parameter['function'] == "trigger": _action_exists, _issue = self.__ensure_action_exists("se_trigger", name) @@ -409,6 +459,7 @@ def remove_action(ex): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'logic') + self.__actions[name].update_action_details(self.__state, self.__action_type) if 'value' in parameter and parameter['value'] is not None: self.__actions[name].update(parameter['logic'] + ':' + parameter['value']) else: @@ -419,6 +470,7 @@ def remove_action(ex): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['value']) elif parameter['function'] == "add": _action_exists, _issue = self.__ensure_action_exists("se_add", name) @@ -426,6 +478,7 @@ def remove_action(ex): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['value']) elif parameter['function'] == "remove": _action_exists, _issue = self.__ensure_action_exists("se_remove", name) @@ -433,6 +486,7 @@ def remove_action(ex): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['value']) elif parameter['function'] == "removeall": _action_exists, _issue = self.__ensure_action_exists("se_removeall", name) @@ -440,6 +494,7 @@ def remove_action(ex): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['value']) elif parameter['function'] == "removefirst": _action_exists, _issue = self.__ensure_action_exists("se_removefirst", name) @@ -447,6 +502,7 @@ def remove_action(ex): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['value']) elif parameter['function'] == "removelast": _action_exists, _issue = self.__ensure_action_exists("se_removelast", name) @@ -454,6 +510,7 @@ def remove_action(ex): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['value']) except ValueError as ex: @@ -469,6 +526,14 @@ def remove_action(ex): _issue = self.__actions[name].update_repeat(parameter['repeat']) if _issue: _issue_list.append(_issue) + if parameter['mindelta'] is not None: + _issue = self.__actions[name].update_mindelta(parameter['mindelta']) + if _issue: + _issue_list.append(_issue) + if parameter['minagedelta'] is not None: + _issue = self.__actions[name].update_minagedelta(parameter['minagedelta']) + if _issue: + _issue_list.append(_issue) if parameter['delay'] != 0: _issue = self.__actions[name].update_delay(parameter['delay']) if _issue: @@ -477,6 +542,10 @@ def remove_action(ex): _issue = self.__actions[name].update_order(parameter['order']) if _issue: _issue_list.append(_issue) + if parameter['nextconditionset'] is not None: + _issue = self.__actions[name].update_nextconditionset(parameter['nextconditionset']) + if _issue: + _issue_list.append(_issue) if parameter['conditionset'] is not None: _issue = self.__actions[name].update_conditionset(parameter['conditionset']) if _issue: @@ -498,6 +567,7 @@ def remove_action(ex): _issue_list.append(_issue) if _action: self.__actions[name] = _action + self._log_debug("Handle combined issuelist {}", _issue_list) return _issue_list # noinspection PyMethodMayBeStatic @@ -508,16 +578,16 @@ def __raise_missing_parameter_error(self, parameter, param_name): # Check the actions optimize and complete them # state: state (item) to read from - def complete(self, state, evals_items=None, use=None): + def complete(self, evals_items=None, use=None): _status = {} - if use is None: - use = state.use.get() + if not self.__actions: + return _status for name in self.__actions: try: - _status.update(self.__actions[name].complete(state, evals_items, use)) + _status.update(self.__actions[name].complete(evals_items, use)) except ValueError as ex: - _status.update({name: {'issue': ex, 'issueorigin': {'state': state.id, 'action': 'unknown'}}}) - raise ValueError("State '{0}', Action '{1}': {2}".format(state.id, name, ex)) + _status.update({name: {'issue': ex, 'issueorigin': {'state': self.__state.id, 'action': 'unknown'}}}) + raise ValueError("Completing State '{0}', Action '{1}': {2}".format(self.__state.id, name, ex)) return _status def set(self, value): @@ -525,7 +595,7 @@ def set(self, value): try: self.__actions[name].update(value) except ValueError as ex: - raise ValueError("State '{0}', Action '{1}': {2}".format(value.property.path, name, ex)) + raise ValueError("Setting State '{0}', Action '{1}': {2}".format(value.property.path, name, ex)) # Execute all actions # is_repeat: Indicate if this is a repeated action without changing the state diff --git a/stateengine/StateEngineCondition.py b/stateengine/StateEngineCondition.py index abfe71745..19e872d6f 100755 --- a/stateengine/StateEngineCondition.py +++ b/stateengine/StateEngineCondition.py @@ -59,6 +59,7 @@ def __init__(self, abitem, name: str): self.__triggeredbynegate = None self.__agenegate = None self.__error = None + self.__state = None self.__itemClass = Item def __repr__(self): @@ -228,6 +229,7 @@ def get(self): # state: state (item) to read from # abitem_object: Related SeItem instance for later determination of current age and current delay def complete(self, state, use): + self.__state = 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() \ @@ -292,14 +294,14 @@ def complete(self, state, use): if all(item is None for item in [self.__item, self.__status, self.__eval, self.__status_eval]): raise ValueError("Neither 'item' nor 'status' nor '(status)eval' given!") - if any(item is not None for item in [self.__item, self.__status, self.__eval, self.__status_eval])\ - and not self.__changedby.is_empty() and self.__changedbynegate is None: + if any(item is not None for item in [self.__item, self.__status, self.__eval, self.__status_eval]) \ + and not self.__changedby.is_empty() and self.__changedbynegate is None: self.__changedbynegate = False - if any(item is not None for item in [self.__item, self.__status, self.__eval, self.__status_eval])\ - and not self.__updatedby.is_empty() and self.__updatedbynegate is None: + if any(item is not None for item in [self.__item, self.__status, self.__eval, self.__status_eval]) \ + and not self.__updatedby.is_empty() and self.__updatedbynegate is None: self.__updatedbynegate = False - if any(item is not None for item in [self.__item, self.__status, self.__eval, self.__status_eval])\ - and not self.__triggeredby.is_empty() and self.__triggeredbynegate is None: + if any(item is not None for item in [self.__item, self.__status, self.__eval, self.__status_eval]) \ + and not self.__triggeredby.is_empty() and self.__triggeredbynegate is None: self.__triggeredbynegate = False # cast stuff @@ -368,50 +370,80 @@ def check(self, state): # Write condition to logger def write_to_logger(self): + def write_item(item_type, value): + item_list = [] + if value is not None: + if isinstance(value, list): + for i in value: + try: + itm = i.property.path + except Exception: + itm = i + item_list.append(itm) + self._log_info("{0}: {1} ({2})", item_type, self.__name, itm) + else: + try: + itm = value.property.path + except Exception: + itm = value + item_list.append(itm) + self._log_info("{0}: {1} ({2})", item_type, self.__name, itm) + item_list = StateEngineTools.flatten_list(item_list) + item_list = None if len(item_list) == 0 else str(item_list[0]) if len(item_list) == 1 else str(item_list) + return item_list + + def write_eval(eval_type, value): + eval_list = [] + if value is not None: + if isinstance(value, list): + for e in value: + name = StateEngineTools.get_eval_name(e) + eval_list.append(name) + self._log_info("{0}: {1}", eval_type, name) + else: + name = StateEngineTools.get_eval_name(value) + eval_list.append(name) + self._log_info("{0}: {1}", eval_type, name) + eval_list = StateEngineTools.flatten_list(eval_list) + eval_list = None if len(eval_list) == 0 else str(eval_list[0]) if len(eval_list) == 1 else str(eval_list) + return str(eval_list) + if self.__error is not None: self._log_warning("error: {0}", self.__error) - if self.__item is not None: - if isinstance(self.__item, list): - for i in self.__item: - 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.__eval, list): - for e in self.__eval: - self._log_info("eval: {0}", StateEngineTools.get_eval_name(e)) - else: - self._log_info("eval: {0}", StateEngineTools.get_eval_name(self.__eval)) - if self.__status_eval is not None: - if isinstance(self.__status_eval, list): - for e in self.__status_eval: - self._log_info("status eval: {0}", StateEngineTools.get_eval_name(e)) - else: - self._log_info("status eval: {0}", StateEngineTools.get_eval_name(self.__status_eval)) - self.__value.write_to_logger() - self.__min.write_to_logger() - self.__max.write_to_logger() + item_result = self.__item + status_result = self.__status + item = write_item("item", item_result) + status = write_item("status item", status_result) + eval_result = self.__eval + status_eval_result = self.__status_eval + eval_result = write_eval("eval", eval_result) + status_eval_result = write_eval("status", status_eval_result) + val = self.__value.write_to_logger() + value_result = self.__value.get_for_webif(val) + min_result = self.__min.write_to_logger() + max_result = self.__max.write_to_logger() if self.__negate is not None: self._log_info("negate: {0}", self.__negate) - self.__agemin.write_to_logger() - self.__agemax.write_to_logger() + agemin = self.__agemin.write_to_logger() + agemax = self.__agemax.write_to_logger() if self.__agenegate is not None: self._log_info("age negate: {0}", self.__agenegate) - self.__changedby.write_to_logger() + changedby = self.__changedby.write_to_logger() if self.__changedbynegate is not None and not self.__changedby.is_empty(): self._log_info("changedby negate: {0}", self.__changedbynegate) - self.__updatedby.write_to_logger() + updatedby = self.__updatedby.write_to_logger() if self.__updatedbynegate is not None and not self.__updatedby.is_empty(): self._log_info("updatedby negate: {0}", self.__updatedbynegate) - self.__triggeredby.write_to_logger() + triggeredby = 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) + return {self.name: {'item': item, 'status': status, 'eval': eval_result, 'status_eval': status_eval_result, + 'value': value_result, 'min': str(min_result), 'max': str(max_result), 'agemin': str(agemin), + 'agemax': str(agemax), 'negate': str(self.__negate), 'agenegate': str(self.__agenegate), + 'changedby': str(changedby), 'updatedby': str(updatedby), + 'triggeredby': str(triggeredby), 'triggeredbynegate': str(self.__triggeredbynegate), + 'changedbynegate': str(self.__changedbynegate), + 'updatedbynegate': str(self.__updatedbynegate), 'current': {}, 'match': {}}} # Cast 'value', 'min' and 'max' using given cast function # cast_func: cast function to use @@ -462,19 +494,19 @@ def __convert(convert_value, convert_current): self.__value.set_cast(StateEngineTools.cast_str) convert_value = StateEngineTools.cast_str(convert_value) convert_current = StateEngineTools.cast_str(convert_current) - if not type(_oldvalue) == type(convert_value): + if not type(_oldvalue) is type(convert_value): self._log_debug("Value {} was type {} and therefore not the same" " type as item value {}. It got converted to {}.", _oldvalue, type(_oldvalue), convert_current, type(convert_value)) return 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\ + 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\ + negate = self.__changedbynegate if valuetype == "changedby" else \ + self.__updatedbynegate if valuetype == "updatedby" else \ + self.__triggeredbynegate if valuetype == "triggeredby" else \ self.__negate _key_current = ['{}'.format(state.id), 'conditionsets', '{}'.format( self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), @@ -490,25 +522,25 @@ def __convert(convert_value, convert_current): for i, element in enumerate(value): regex_result = None regex_check = False - if valuetype == "value" and type(element) != type(current) and current is not None: + if valuetype == "value" and type(element) is not type(current) and current is not None: element, current = __convert(element, current) if isinstance(element, re.Pattern): regex_result = element.fullmatch(str(current)) regex_check = True if negate: - if (regex_result is not None and regex_check is True)\ - or (current == element and regex_check is False): + 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) self._abitem.update_webif(_key_match, 'no') return False else: - if (regex_result is not None and regex_check is True)\ - or (current == element and regex_check is False): + 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) self._abitem.update_webif(_key_match, 'yes') return True if regex_check is True: - self._log_debug("Regex '{}' result: {}, element {}", element, regex_result) + self._log_debug("Regex '{0}' result: {1}.", element, regex_result) if negate: self._log_debug("{0} not in list -> matching", current) @@ -522,7 +554,7 @@ def __convert(convert_value, convert_current): regex_result = None regex_check = False # If current and value have different types, convert both to string - if valuetype == "value" and type(value) != type(current) and current is not None: + if valuetype == "value" and type(value) is not type(current) and current is not None: value, current = __convert(value, current) text = "Condition '{0}': {1}={2} negate={3} current={4}" self._abitem.update_webif(_key_current, str(current)) @@ -532,14 +564,14 @@ def __convert(convert_value, convert_current): regex_result = value.fullmatch(str(current)) regex_check = True if negate: - if (regex_result is None and regex_check is True)\ - or (current != value and regex_check is False): + 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") self._abitem.update_webif(_key_match, 'yes') return True else: - if (regex_result is not None and regex_check is True)\ - or (current == value and regex_check is False): + if (regex_result is not None and regex_check is True) \ + or (current == value and regex_check is False): self._log_debug("OK -> matching") self._abitem.update_webif(_key_match, 'yes') return True @@ -823,10 +855,10 @@ def check_eval(eval_or_status_eval): eval_result = eval(eval_or_status_eval) if isinstance(eval_result, self.__itemClass): value = eval_result.property.last_change_age if eval_type == 'age' else \ - eval_result.property.last_change_by if eval_type == 'changedby' else \ - eval_result.property.last_update_by if eval_type == 'updatedby' else \ - eval_result.property.last_trigger_by if eval_type == 'triggeredby' else \ - eval_result.property.value + eval_result.property.last_change_by if eval_type == 'changedby' else \ + eval_result.property.last_update_by if eval_type == 'updatedby' else \ + eval_result.property.last_trigger_by if eval_type == 'triggeredby' else \ + eval_result.property.value else: value = eval_result except Exception as ex: @@ -840,10 +872,10 @@ def check_eval(eval_or_status_eval): if self.__status is not None: # noinspection PyUnusedLocal self._log_debug("Trying to get {} of status item {}", eval_type, self.__status.property.path) - return self.__status.property.last_change_age if eval_type == 'age' else\ - self.__status.property.last_change_by if eval_type == 'changedby' else\ - self.__status.property.last_update_by if eval_type == 'updatedby' else\ - self.__status.property.last_trigger_by if eval_type == 'triggeredby' else\ + 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.__status_eval is not None: self._log_debug("Trying to get {} of statuseval {}", eval_type, self.__status_eval) @@ -852,10 +884,10 @@ def check_eval(eval_or_status_eval): elif self.__item is not None: # noinspection PyUnusedLocal self._log_debug("Trying to get {} of item {}", eval_type, self.__item.property.path) - return self.__item.property.last_change_age if eval_type == 'age' else\ - self.__item.property.last_change_by if eval_type == 'changedby' else\ - self.__item.property.last_update_by if eval_type == 'updatedby' else\ - self.__item.property.last_trigger_by if eval_type == 'triggeredby' else\ + 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 elif self.__eval is not None: self._log_debug("Trying to get {} of eval {}", eval_type, self.__eval) diff --git a/stateengine/StateEngineConditionSet.py b/stateengine/StateEngineConditionSet.py index ed4e4b6d0..176b6a7c9 100755 --- a/stateengine/StateEngineConditionSet.py +++ b/stateengine/StateEngineConditionSet.py @@ -78,6 +78,7 @@ def __init__(self, abitem, name, conditionid): self.__evals_items = {} self.__unused_attributes = {} self.__used_attributes = {} + self.__state = None def __repr__(self): return "SeConditionSet {}".format(self.__conditions) @@ -150,6 +151,7 @@ def update(self, item, grandparent_item): # Check the condition set, optimize and complete it # state: state (item) to read from def complete(self, state, use): + self.__state = state conditions_to_remove = [] # try to complete conditions @@ -174,25 +176,21 @@ def write_to_logger(self): for name in self.__conditions: self._log_info("Condition '{0}':", name) self._log_increase_indent() - self.__conditions[name].write_to_logger() + _webif = self.__conditions[name].write_to_logger() + _key = [self.__state.id, 'conditionsets', self.name] + self._abitem.update_webif(_key, _webif, True) self._log_decrease_indent() def __currentconditionset_set(self, conditionsetid, name): self._abitem.set_variable('current.conditionset_id', conditionsetid) self._abitem.set_variable('current.conditionset_name', name) - def __previousconditionset_set(self, conditionsetid, name): - self._abitem.set_variable('previous.conditionset_id', conditionsetid) - self._abitem.set_variable('previous.conditionset_name', 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, state): try: self._log_info("Check condition set '{0}'", self.__name) self._log_increase_indent() - self.__previousconditionset_set(self._abitem.get_variable('current.conditionset_id'), - self._abitem.get_variable('current.conditionset_name')) self.__currentconditionset_set(self.__id.property.path, self.__name) for name in self.__conditions: diff --git a/stateengine/StateEngineConditionSets.py b/stateengine/StateEngineConditionSets.py index c8dbf3612..0cf4212ff 100755 --- a/stateengine/StateEngineConditionSets.py +++ b/stateengine/StateEngineConditionSets.py @@ -91,9 +91,9 @@ def write_to_logger(self): def one_conditionset_matching(self, state): if self.count() == 0: self._log_debug("No condition sets defined -> matching") - return True + return True, '' for name in self.__condition_sets: if self.__condition_sets[name].all_conditions_matching(state): - return True + return True, name - return False + return False, '' diff --git a/stateengine/StateEngineFunctions.py b/stateengine/StateEngineFunctions.py index ae5c2d7b1..1e869d033 100755 --- a/stateengine/StateEngineFunctions.py +++ b/stateengine/StateEngineFunctions.py @@ -85,14 +85,14 @@ def check_include_exclude(entry_type): # If current value is in list -> Return "Trigger" for e in conf_entry: e = re.compile(e, re.IGNORECASE) - result = e.match(original) - elog.info("Checking regex result {}", result) - if result is not None: + r = e.match(original) + elog.info("Checking regex result {}", r) + if r is not None: elog.info("{0}: matching.", e) elog.decrease_indent() - returnvalue = retval_trigger if entry_type == "include" else retval_no_trigger - elog.info("Writing value {0}", returnvalue) - return returnvalue + retval = retval_trigger if entry_type == "include" else retval_no_trigger + elog.info("Writing value {0}", retval) + return retval elog.info("{0}: not matching", e) elog.decrease_indent() return None diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 42202d905..40a9f32d2 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -103,6 +103,14 @@ def logger(self): def instant_leaveaction(self): return self.__instant_leaveaction.get() + @property + def last_run(self): + return self.__last_run + + @last_run.setter + def last_run(self, value_dict): + self.__last_run.update(value_dict) + @property def default_instant_leaveaction(self): return self.__default_instant_leaveaction.get() @@ -111,6 +119,10 @@ def default_instant_leaveaction(self): def default_instant_leaveaction(self, value): self.__default_instant_leaveaction.set(value) + @property + def nextconditionset(self): + return self.__nextconditionset_id + @property def laststate(self): _returnvalue = None if self.__laststate_item_id is None else self.__laststate_item_id.property.value @@ -167,6 +179,14 @@ def previousstate_conditionset_name(self): _returnvalue = None if self.__previousstate_conditionset_item_name is None else self.__previousstate_conditionset_item_name.property.value return _returnvalue + @property + def cache(self): + return self.__cache + + @cache.setter + def cache(self, value): + self.__cache.update(value) + @property def ab_alive(self): return self.__ab_alive @@ -191,6 +211,9 @@ def __init__(self, smarthome, item, se_plugin): self.__se_plugin = se_plugin self.__active_schedulers = [] self.__release_info = {} + self.__cache = {} + self.__last_run = {} + self.__pass_repeat = {} self.__default_instant_leaveaction = StateEngineValue.SeValue(self, "Default Instant Leave Action", False, "bool") self.__instant_leaveaction = StateEngineValue.SeValue(self, "Instant Leave Action", False, "num") try: @@ -204,8 +227,9 @@ def __init__(self, smarthome, item, se_plugin): self.__log_level = StateEngineValue.SeValue(self, "Log Level", False, "num") _default_log_level = self.__logger.default_log_level.get() - _returnvalue, _returntype, _using_default, _issue, _ = self.__log_level.set_from_attr(self.__item, "se_log_level", - _default_log_level) + _returnvalue, _returntype, _using_default, _issue, _ = self.__log_level.set_from_attr(self.__item, + "se_log_level", + _default_log_level) self.__using_default_log_level = _using_default _returnvalue = self.__log_level.get() if isinstance(_returnvalue, list) and len(_returnvalue) == 1: @@ -266,6 +290,8 @@ def __init__(self, smarthome, item, se_plugin): self.__config_issues.update(_issue) # Init lastconditionset items/values + self.__nextconditionset_id = "" + self.__nextconditionset_name = "" 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 @@ -305,7 +331,7 @@ def __init__(self, smarthome, item, se_plugin): self.__templates = {} self.__unused_attributes = {} self.__used_attributes = {} - self.__action_status = {} + self.__action_status = {"enter": {}, "enter_or_stay": {}, "stay": {}, "pass": {}, "leave": {}} self.__state_issues = {} self.__struct_issues = {} self.__webif_infos = OrderedDict() @@ -338,6 +364,8 @@ def __init__(self, smarthome, item, se_plugin): "release.has_released": "", "release.was_released_by": "", "release.will_release": "", + "next.conditionset_id": "", + "next.conditionset_name": "", "current.state_id": "", "current.state_name": "", "current.conditionset_id": "", @@ -402,9 +430,21 @@ def show_issues_summary(self): if self.__unused_attributes: issues += 1 self.__log_issues('attributes') - if self.__action_status: + if self.__action_status['enter']: issues += 1 - self.__log_issues('actions') + self.__log_issues('actions_enter') + if self.__action_status['enter_or_stay']: + issues += 1 + self.__log_issues('actions_enter_or_stay') + if self.__action_status['stay']: + issues += 1 + self.__log_issues('actions_stay') + if self.__action_status['leave']: + issues += 1 + self.__log_issues('actions_leave') + if self.__action_status['pass']: + issues += 1 + self.__log_issues('actions_pass') if self.__state_issues: issues += 1 self.__log_issues('states') @@ -462,6 +502,15 @@ def remove_all_schedulers(self): # region Updatestate *********************************************************************************************** # run queue def run_queue(self): + def update_current_to_empty(d): + if isinstance(d, dict): # Check if the current level is a dictionary + for key, val in d.items(): + if key in ['current', 'match', 'actionstatus'] and isinstance(val, dict): # If the key is 'current', and value is a dict + d[key] = {} # Set it to an empty dict + else: + # Recur for nested dictionaries + update_current_to_empty(val) + if not self.__ab_alive: self.__logger.debug("{} not running (anymore). Queue not activated.", StateEngineDefaults.plugin_identification) @@ -532,15 +581,16 @@ def run_queue(self): elif job[0] == "delayedaction": self.__logger.debug("Job {}", job) (_, action, actionname, namevar, repeat_text, value, current_condition, previous_condition, - previousstate_condition, state) = job + previousstate_condition, next_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) + "Running delayed action: {0} based on current_condition {1} / previous_condition {2} / previousstate_condition {3} or next condition {4}", + actionname, current_condition, previous_condition, previousstate_condition, next_condition) + action.real_execute(state, actionname, namevar, repeat_text, value, False, current_condition, previous_condition, previousstate_condition, next_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.__cache = {} 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, @@ -612,6 +662,7 @@ def run_queue(self): # find new state _leaveactions_run = False + _pass_state = None if _instant_leaveaction >= 1 and caller != "Released_by Retrigger": evaluated_instant_leaveaction = True @@ -619,6 +670,9 @@ def run_queue(self): evaluated_instant_leaveaction = False _previousstate_conditionset_id = '' _previousstate_conditionset_name = '' + + update_current_to_empty(self.__webif_infos) + self.__logger.develop("Reset current info for webif info. It is now: {}", self.__webif_infos) for state in self.__states: if not self.__ab_alive: self.__logger.debug("StateEngine Plugin not running (anymore). Stop state evaluation.") @@ -627,7 +681,13 @@ def run_queue(self): _key_name = ['{}'.format(state.id), 'name'] self.update_webif(_key_name, state.name) - result = self.__update_check_can_enter(state, _instant_leaveaction) + result, self.__nextconditionset_name = self.__update_check_can_enter(state, _instant_leaveaction) + if self.__nextconditionset_name: + self.__nextconditionset_id = f"{state.state_item.property.path}.{self.__nextconditionset_name}" + else: + self.__nextconditionset_id = "" + self.set_variable('next.conditionset_id', self.__nextconditionset_id) + self.set_variable('next.conditionset_name', self.__nextconditionset_name) _previousstate_conditionset_id = _last_conditionset_id _previousstate_conditionset_name = _last_conditionset_name _last_conditionset_id = self.__lastconditionset_internal_id @@ -636,27 +696,47 @@ def run_queue(self): self.__conditionsets.update( {state.state_item.property.path: [_last_conditionset_id, _last_conditionset_name]}) # New state is different from last state - - if result is False and last_state == state and evaluated_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 False and last_state == state: + if evaluated_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 + elif result is False and last_state != state and state.actions_pass.count() > 0: + _pass_state = state + state.run_pass(self.__pass_repeat.get(state, False), self.__repeat_actions.get()) + _key_pass = ['{}'.format(last_state.id), 'pass'] + self.update_webif(_key_pass, True) + self.__pass_repeat.update({state: True}) if result is True: new_state = state + for repeat_state in self.__pass_repeat: + if new_state.order < repeat_state.order: + self.__pass_repeat.update({repeat_state: False}) + _key_pass = ['{}'.format(repeat_state.id), 'pass'] + self.update_webif(_key_pass, False) break # no new state -> stay if new_state is None: if last_state is None: + self.__nextconditionset_id = '' + self.__nextconditionset_name = '' + self.set_variable('next.conditionset_id', self.__nextconditionset_id) + self.set_variable('next.conditionset_name', self.__nextconditionset_name) self.__logger.info("No matching state found, no previous state available. Doing nothing.") else: - if last_state.conditions.count() == 0: + if last_state.conditionsets.count() == 0: self.lastconditionset_set('', '') _last_conditionset_id = '' _last_conditionset_name = '' else: self.lastconditionset_set(_last_conditionset_id, _last_conditionset_name) + self.__nextconditionset_id = _last_conditionset_id + self.__nextconditionset_name = _last_conditionset_name + self.set_variable('next.conditionset_id', self.__nextconditionset_id) + self.set_variable('next.conditionset_name', self.__nextconditionset_name) + self.__logger.develop("Current variables: {}", self.__variables) if _last_conditionset_id in ['', None]: text = "No matching state found, staying at {0} ('{1}')" self.__logger.info(text, last_state.id, last_state.name) @@ -681,17 +761,28 @@ def run_queue(self): "State is a copy and therefore just releasing {}. Skipping state actions, running leave actions " "of last state, then retriggering.", new_state.is_copy_for.id) if last_state is not None and self.__ab_alive: - self.__logger.info("Leaving {0} ('{1}'). Condition set was: {2}.", - last_state.id, last_state.name, _original_conditionset_id) - self.__update_check_can_enter(last_state, _instant_leaveaction, False) + _, self.__nextconditionset_name = self.__update_check_can_enter(last_state, _instant_leaveaction, False) + if self.__nextconditionset_name: + self.__nextconditionset_id = f"{state.state_item.property.path}.{self.__nextconditionset_name}" + else: + self.__nextconditionset_id = "" + self.set_variable('next.conditionset_id', self.__nextconditionset_id) + self.set_variable('next.conditionset_name', self.__nextconditionset_name) + self.__logger.develop("Current variables: {}", self.__variables) + self.__logger.info("Leaving {0} ('{1}'). Condition set was: {2} ({3}), will be {4} ({5}).", + last_state.id, last_state.name, _original_conditionset_id, + _original_conditionset_name, self.__nextconditionset_id, + self.__nextconditionset_name) 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'] + _key_pass = ['{}'.format(last_state.id), 'pass'] self.update_webif(_key_leave, True) self.update_webif(_key_stay, False) self.update_webif(_key_enter, False) + self.update_webif(_key_pass, False) self.__handle_releasedby(new_state, last_state, _instant_leaveaction) if self.update_lock.locked(): @@ -702,7 +793,7 @@ def run_queue(self): _last_conditionset_id = self.__lastconditionset_internal_id _last_conditionset_name = self.__lastconditionset_internal_name - if new_state.conditions.count() == 0: + if new_state.conditionsets.count() == 0: self.lastconditionset_set('', '') _last_conditionset_id = '' _last_conditionset_name = '' @@ -710,6 +801,11 @@ def run_queue(self): # endblock # get data for new state if last_state is not None and new_state.id == last_state.id: + self.__nextconditionset_id = _last_conditionset_id + self.__nextconditionset_name = _last_conditionset_name + self.set_variable('next.conditionset_id', self.__nextconditionset_id) + self.set_variable('next.conditionset_name', self.__nextconditionset_name) + self.__logger.develop("Current variables: {}", self.__variables) if _last_conditionset_id in ['', None]: self.__logger.info("Staying at {0} ('{1}')", new_state.id, new_state.name) else: @@ -726,23 +822,30 @@ def run_queue(self): 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: + if last_state.actions_leave.count() > 0: 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}.", - last_state.id, last_state.name, _original_conditionset_id) + self.__logger.develop("Current variables: {}", self.__variables) + self.__logger.info("Leaving {0} ('{1}'). Condition set was: {2} ({3}), will be {4} ({5}).", + last_state.id, last_state.name, _original_conditionset_id, + _original_conditionset_name, self.__nextconditionset_id, self.__nextconditionset_name) last_state.run_leave(self.__repeat_actions.get()) _leaveactions_run = True - if new_state.conditions.count() == 0: + if new_state.conditionsets.count() == 0: self.lastconditionset_set('', '') _last_conditionset_id = '' _last_conditionset_name = '' + self.__nextconditionset_id = _last_conditionset_id + self.__nextconditionset_name = _last_conditionset_name else: self.lastconditionset_set(_last_conditionset_id, _last_conditionset_name) self.previousstate_conditionset_set(_previousstate_conditionset_id, _previousstate_conditionset_name) + self.set_variable('next.conditionset_id', self.__nextconditionset_id) + self.set_variable('next.conditionset_name', self.__nextconditionset_name) + self.__logger.develop("Current variables: {}", self.__variables) if _last_conditionset_id in ['', None]: self.__logger.info("Entering {0} ('{1}')", new_state.id, new_state.name) else: @@ -750,16 +853,19 @@ def run_queue(self): new_state.id, new_state.name, _last_conditionset_id, _last_conditionset_name) new_state.run_enter(self.__repeat_actions.get()) + self.__laststate_set(new_state) self.__previousstate_set(last_state) if _leaveactions_run is True and self.__ab_alive: _key_leave = ['{}'.format(last_state.id), 'leave'] _key_stay = ['{}'.format(last_state.id), 'stay'] _key_enter = ['{}'.format(last_state.id), 'enter'] + _key_pass = ['{}'.format(last_state.id), 'pass'] self.update_webif(_key_leave, True) self.update_webif(_key_stay, False) self.update_webif(_key_enter, False) + self.update_webif(_key_pass, False) self.__logger.debug("State evaluation finished") all_released_by = self.__handle_releasedby(new_state, last_state, _instant_leaveaction) @@ -826,10 +932,10 @@ def __update_can_release(self, can_release, new_state=None): def __handle_releasedby(self, new_state, last_state, instant_leaveaction): def update_can_release_list(): - for e in _returnvalue: - e = self.__update_release_item_value(e, new_state) - e = e if isinstance(e, list) else [e] - for entry in e: + for r in _returnvalue: + r = self.__update_release_item_value(r, new_state) + r = r if isinstance(r, list) else [r] + for entry in r: if entry and state.id not in can_release.setdefault(entry, [state.id]): can_release[entry].append(state.id) @@ -918,7 +1024,7 @@ def update_can_release_list(): current_log_level = self.__log_level.get() if current_log_level < 3: self.__logger.log_level_as_num = 0 - can_enter = self.__update_check_can_enter(relevant_state, instant_leaveaction) + can_enter, _ = self.__update_check_can_enter(relevant_state, instant_leaveaction) self.__logger.log_level_as_num = current_log_level if relevant_state == last_state: self.__logger.debug("Possible release state {} = last state {}, " @@ -940,16 +1046,24 @@ def update_can_release_list(): self.__logger.info("".ljust(80, "_")) return all_released_by - def update_webif(self, key, value): + def update_webif(self, key, value, update=False): def _nested_set(dic, keys, val): for nestedkey in keys[:-1]: dic = dic.setdefault(nestedkey, {}) - dic[keys[-1]] = val + # Check if both existing value and new value are dictionaries + if update is True and isinstance(dic.get(keys[-1]), dict) and isinstance(val, dict): + # Update the existing dictionary with the new dictionary + dic[keys[-1]].update(val) + #self.__logger.develop("Updating WEBIF with list {}, value: {}. infos is {}", key, value, self.__webif_infos) + else: + # Otherwise, set the value as is + dic[keys[-1]] = val + #self.__logger.develop("Setting WEBIF with list {}, value: {}. infos is {}", key, value, self.__webif_infos) def _nested_test(dic, keys): for nestedkey in keys[:-2]: dic = dic.setdefault(nestedkey, {}) - return dic[keys[-2]] + return dic.get(keys[-2], {}) if isinstance(key, list): try: @@ -959,24 +1073,41 @@ def _nested_test(dic, keys): except Exception: return False else: - self.__webif_infos[key] = value + if update is True: + self.__webif_infos.setdefault(key, {}).update(value) + self.__logger.develop("Updating WEBIF {}, value: {}. infos is {}", key, value, self.__webif_infos) + else: + self.__webif_infos[key] = value + self.__logger.develop("Setting WEBIF {}, value: {}. infos is {}", key, value, self.__webif_infos) return True def update_action_status(self, action_status): def combine_dicts(dict1, dict2): - combined_dict = dict1.copy() - for key, value in dict2.items(): - if key in combined_dict: - for k, v in combined_dict.items(): - v['issueorigin'].extend( - [item for item in v['issueorigin'] if item not in combined_dict[k]['issueorigin']]) - v['issue'].extend([item for item in v['issue'] if item not in combined_dict[k]['issue']]) + combined = copy.deepcopy(dict1) + for action_type, action_dict in dict2.items(): + if action_type in combined: + # Merge the inner dictionary for this action_type + for key, value in action_dict.items(): + if key in combined[action_type]: + combined[action_type][key]['issueorigin'].extend( + [item for item in value['issueorigin'] if + item not in combined[action_type][key]['issueorigin']] + ) + combined[action_type][key]['issue'].extend( + [item for item in value['issue'] if item not in combined[action_type][key]['issue']] + ) + else: + # Add new key at the inner level if it doesn't exist + combined[action_type][key] = value else: - combined_dict[key] = value - return combined_dict + # Add the entire action_type dictionary if it's not in combined + combined[action_type] = action_dict + + return combined - combined_dict = combine_dicts(action_status, self.__action_status) + combined_dict = combine_dicts(copy.deepcopy(action_status), copy.deepcopy(self.__action_status)) self.__action_status = combined_dict + del combined_dict def update_issues(self, issue_type, issues): def combine_dicts(dict1, dict2): @@ -992,19 +1123,19 @@ def update_list(existing, new_entries): existing.append(entry) return existing - combined_dict = dict1.copy() + comb_dict = dict1.copy() for key, value in dict2.items(): - if key not in combined_dict: - combined_dict[key] = value + if key not in comb_dict: + comb_dict[key] = value continue - combined_entry = combined_dict[key] + combined_entry = comb_dict[key] if 'issue' in value: combined_entry['issue'] = update_list(combined_entry.get('issue', []), value['issue']) if 'issueorigin' in value: combined_entry['issueorigin'] = update_list(combined_entry.get('issueorigin', []), value['issueorigin']) - return combined_dict + return comb_dict if issue_type == "state": combined_dict = combine_dicts(issues, self.__state_issues) @@ -1048,7 +1179,7 @@ def update_attributes(self, unused_attributes, used_attributes): self.__used_attributes = combined_dict def __log_issues(self, issue_type): - def print_readable_dict(data): + def print_readable_dict(attr, data): for key, value in data.items(): if isinstance(value, list): formatted_entries = [] @@ -1062,45 +1193,64 @@ def print_readable_dict(data): else: formatted_entries.append(item) if formatted_entries: - self.__logger.info("- {}: {}", key, ', '.join(formatted_entries)) + self.__logger.info("- {}{}: {}", attr, key, ', '.join(formatted_entries)) else: - self.__logger.info("- {}: {}", key, value) + self.__logger.info("- {}{}: {}", attr, key, value) + def list_issues(v): _issuelist = StateEngineTools.flatten_list(v.get('issue')) + _attrlist = StateEngineTools.flatten_list(v.get('attribute')) if isinstance(_issuelist, list) and len(_issuelist) > 1: self.__logger.info("has the following issues:") self.__logger.increase_indent() - for e in _issuelist: + for i, e in enumerate(_issuelist): + _attr = "" if _attrlist is None or not _attrlist[i] else "attribute {}: ".format(_attrlist[i]) if isinstance(e, dict): - print_readable_dict(e) + print_readable_dict(_attr, e) else: - self.__logger.info("- {}", e) + self.__logger.info("- {}{}", _attr, e) self.__logger.decrease_indent() elif isinstance(_issuelist, list) and len(_issuelist) == 1: if isinstance(_issuelist[0], dict): self.__logger.info("has the following issues:") self.__logger.increase_indent() - print_readable_dict(_issuelist[0]) + _attr = "" if _attrlist is None or not _attrlist[0] else "attribute {}: ".format(_attrlist[0]) + print_readable_dict(_attr, _issuelist[0]) self.__logger.decrease_indent() else: - self.__logger.info("has the following issue: {}", _issuelist[0]) + _attr = "" if _attrlist is None or not _attrlist[0] else " for attribute {}".format(_attrlist[0]) + self.__logger.info("has the following issue{}: {}", _attr, _issuelist[0]) else: if isinstance(_issuelist, dict): self.__logger.info("has the following issues:") self.__logger.increase_indent() - print_readable_dict(_issuelist) + _attr = "" if not _attrlist else "attribute {}: ".format(_attrlist) + print_readable_dict(_attr, _issuelist) self.__logger.decrease_indent() else: - self.__logger.info("has the following issue: {}", _issuelist) + _attr = "" if not _attrlist else " for attribute {}".format(_attrlist) + self.__logger.info("has the following issue{}: {}", _attr, _issuelist) if "ignore" in v: self.__logger.info("It will be ignored") warn_unused = "" warn_issues = "" warn = "" - if issue_type == 'actions': - to_check = self.__action_status.items() - warn = ', '.join(key for key in self.__action_status.keys()) + if issue_type == 'actions_enter': + to_check = self.__action_status['enter'].items() + warn = ', '.join(key for key in self.__action_status['enter'].keys()) + elif issue_type == 'actions_enter_or_stay': + to_check = self.__action_status['enter_or_stay'].items() + warn = ', '.join(key for key in self.__action_status['enter_or_stay'].keys()) + elif issue_type == 'actions_stay': + to_check = self.__action_status['stay'].items() + warn = ', '.join(key for key in self.__action_status['stay'].keys()) + elif issue_type == 'actions_pass': + to_check = self.__action_status['pass'].items() + warn = ', '.join(key for key in self.__action_status['pass'].keys()) + elif issue_type == 'actions_leave': + to_check = self.__action_status['leave'].items() + warn = ', '.join(key for key in self.__action_status['leave'].keys()) elif issue_type == 'structs': to_check = self.__struct_issues.items() warn = ', '.join(key for key in self.__struct_issues.keys()) @@ -1159,9 +1309,8 @@ def list_issues(v): 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')) + if issue_type.startswith('actions_'): + origin_text = 'state {}, action {}'.format(origin.get('state'), origin.get('action')) elif issue_type == 'states': if origin.get('condition') == 'GeneralError' and len(origin_list) == 1: origin_text = 'there was a general error. The state' @@ -1263,7 +1412,7 @@ def __initialize_state(self, item_state, _statecount): _issue = _state.update_order(_statecount) if _issue: self.__config_issues.update({item_state.property.path: - {'issue': _issue, 'attribute': 'se_stateorder'}}) + {'issue': _issue, 'attribute': 'se_stateorder'}}) self.__logger.error("Issue with state {0} while setting order: {1}", item_state.property.path, _issue) self.__states.append(_state) @@ -1276,13 +1425,13 @@ def __initialize_state(self, item_state, _statecount): return _statecount + 1 except ValueError as ex: self.update_issues('state', {item_state.property.path: {'issue': ex, 'issueorigin': - [{'conditionset': 'None', 'condition': 'ValueError'}]}}) + [{'conditionset': 'None', 'condition': 'ValueError'}]}}) self.__logger.error("Ignoring state {0} because ValueError: {1}", item_state.property.path, ex) return _statecount except Exception as ex: self.update_issues('state', {item_state.property.path: {'issue': ex, 'issueorigin': - [{'conditionset': 'None', 'condition': 'GeneralError'}]}}) + [{'conditionset': 'None', 'condition': 'GeneralError'}]}}) self.__logger.error("Ignoring state {0} because: {1}", item_state.property.path, ex) return _statecount @@ -1333,6 +1482,8 @@ def __update_check_can_enter(self, state, instant_leaveaction, refill=True): self.__variables["previous.state_id"] = self.__previousstate_internal_id self.__variables["previous.state_name"] = self.__previousstate_internal_name self.__variables["item.instant_leaveaction"] = instant_leaveaction + self.__variables["next.conditionset_id"] = self.__nextconditionset_id + self.__variables["next.conditionset_name"] = self.__nextconditionset_name self.__variables["current.state_id"] = state.id self.__variables["current.state_name"] = state.name self.__variables["current.conditionset_id"] = self.__lastconditionset_internal_id @@ -1344,7 +1495,8 @@ def __update_check_can_enter(self, state, instant_leaveaction, refill=True): self.__logger.develop("Current variables: {}", self.__variables) if refill: state.refill() - return state.can_enter() + can_enter = state.can_enter() + return 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... ;) @@ -1354,6 +1506,8 @@ def __update_check_can_enter(self, state, instant_leaveaction, refill=True): self.__variables["release.was_released_by"] = "" self.__variables["release.will_release"] = "" self.__variables["item.instant_leaveaction"] = "" + self.__variables["next.conditionset_id"] = "" + self.__variables["next.conditionset_name"] = "" self.__variables["current.state_id"] = "" self.__variables["current.state_name"] = "" self.__variables["current.conditionset_id"] = "" @@ -1732,11 +1886,11 @@ def process_returnvalue(value): 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] + for z, value in enumerate(_convertedlist): + if _converted_typelist[z] == 'item': + value = self.__update_release_item_value(_converted_evaluatedlist[z], state) + elif _converted_typelist[z] == 'eval': + value = _converted_evaluatedlist[z] value = value if isinstance(value, list) else [value] for v in value: if v and can_release.get(v) and state.id not in can_release.get(v): @@ -1854,6 +2008,8 @@ def cli_detail(self, handler): self.get_previousstate_conditionset_name())) handler.push("\tPrevious conditionset: {0} ('{1}')\n".format(self.get_previousconditionset_id(), self.get_previousconditionset_name())) + handler.push("\tNext conditionset: {0} ('{1}')\n".format(self.get_nextconditionset_id(), + self.get_nextconditionset_name())) handler.push(self.__startup_delay.get_text("\t", "\n")) handler.push("\tCycle: {0}\n".format(cycles)) handler.push("\tCron: {0}\n".format(crons)) @@ -1878,6 +2034,14 @@ def get_condition_age(self): self.__logger.warning('No item for last condition id given. Can not determine age!') return 0 + # return id of new (upcoming) conditionset + def get_nextconditionset_id(self): + return self.__nextconditionset_id + + # return name of new (upcoming) conditionset + def get_nextconditionset_name(self): + return self.__nextconditionset_name + # return id of last state def get_laststate_id(self): return self.__laststate_internal_id @@ -2031,7 +2195,7 @@ def return_item(self, item_id): _, _, item = item.partition(":") return item, None elif match: - _issue = ("Item '{0}' has to be defined as an item path " + _issue = ("Item '{}' has to be defined as an item path " "or eval expression without {}.").format(match.group(1), item_id) self.__logger.warning(_issue) return None, [_issue] diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index c8986e21b..905a969f8 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -28,7 +28,7 @@ from lib.item import Items from lib.item.item import Item -from copy import copy +from copy import copy, deepcopy # Class representing an object state, consisting of name, conditions to be met and configured actions for state @@ -57,9 +57,25 @@ def name(self): # Return leave actions @property - def leaveactions(self): + def actions_leave(self): return self.__actions_leave + @property + def actions_enter(self): + return self.__actions_enter + + @property + def actions_enter_or_stay(self): + return self.__actions_enter_or_stay + + @property + def actions_stay(self): + return self.__actions_stay + + @property + def actions_pass(self): + return self.__actions_pass + # Return text of state @property def text(self): @@ -67,8 +83,8 @@ def text(self): # Return conditions @property - def conditions(self): - return self.__conditions + def conditionsets(self): + return self.__conditionsets # Return orphaned definitions @property @@ -164,15 +180,16 @@ def __init__(self, abitem, item_state): self.__name = '' self.__unused_attributes = {} self.__used_attributes = {} - self.__action_status = {} + self.__action_status = {"enter": {}, "enter_or_stay": {}, "stay": {}, "pass": {}, "leave": {}} self.__use_done = [] self.__use_list = [] self.__use_ignore_list = [] - self.__conditions = StateEngineConditionSets.SeConditionSets(self._abitem) + self.__conditionsets = StateEngineConditionSets.SeConditionSets(self._abitem) self.__actions_enter_or_stay = StateEngineActions.SeActions(self._abitem) self.__actions_enter = StateEngineActions.SeActions(self._abitem) self.__actions_stay = StateEngineActions.SeActions(self._abitem) self.__actions_leave = StateEngineActions.SeActions(self._abitem) + self.__actions_pass = StateEngineActions.SeActions(self._abitem) self.__order = StateEngineValue.SeValue(self._abitem, "State Order", False, "num") self._log_increase_indent() try: @@ -193,13 +210,13 @@ def can_enter(self): 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) + result, conditionset = self.__conditionsets.one_conditionset_matching(self) self._log_decrease_indent() if result: - self._log_info("State {} can be entered", self.id) + self._log_info("State {} can be entered based on conditionset {}", self.id, conditionset) else: self._log_info("State {} can not be entered", self.id) - return result + return result, conditionset # log state data def write_to_log(self): @@ -219,20 +236,21 @@ def write_to_log(self): self._log_info("Updating Web Interface...") self._log_increase_indent() self._abitem.update_webif(self.id, {'name': self.name, - 'conditionsets': self.__conditions.get(), + 'conditionsets': {}, 'actions_enter': {}, 'actions_enter_or_stay': {}, 'actions_stay': {}, 'actions_leave': {}, + 'actions_pass': {}, 'leave': False, 'enter': False, 'stay': False, 'is_copy_for': None, 'releasedby': None}) self._log_decrease_indent() self._log_info("Finished Web Interface Update") - if self.__conditions.count() > 0: + if self.__conditionsets.count() > 0: self._log_info("Condition sets to enter state:") self._log_increase_indent() - self.__conditions.write_to_logger() + self.__conditionsets.write_to_logger() self._log_decrease_indent() if self.__actions_enter.count() > 0: @@ -240,28 +258,31 @@ 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('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('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('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('actions_leave', self.id)) + + if self.__actions_pass.count() > 0: + self._log_info("Actions to perform on pass / transit:") + self._log_increase_indent() + self.__actions_pass.write_to_logger() + self._log_decrease_indent() + self._abitem.set_variable("current.state_name", "") self._abitem.set_variable("current.state_id", "") self._log_decrease_indent() @@ -291,20 +312,13 @@ def run_enter(self, allow_item_repeat: bool): _key_leave = ['{}'.format(self.id), 'leave'] _key_stay = ['{}'.format(self.id), 'stay'] _key_enter = ['{}'.format(self.id), 'enter'] + _key_pass = ['{}'.format(self.id), 'pass'] self._abitem.update_webif(_key_leave, False) self._abitem.update_webif(_key_stay, False) self._abitem.update_webif(_key_enter, True) + self._abitem.update_webif(_key_pass, False) self.__actions_enter.execute(False, allow_item_repeat, self, self.__actions_enter_or_stay) self._log_decrease_indent(50) - self._log_increase_indent() - self._log_debug("Update web interface enter {}", self.id) - self._log_increase_indent() - 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() # run actions when staying at the state # item_allow_repeat: Is repeating actions generally allowed for the item? @@ -313,20 +327,29 @@ def run_stay(self, allow_item_repeat: bool): _key_leave = ['{}'.format(self.id), 'leave'] _key_stay = ['{}'.format(self.id), 'stay'] _key_enter = ['{}'.format(self.id), 'enter'] + _key_pass = ['{}'.format(self.id), 'pass'] self._abitem.update_webif(_key_leave, False) self._abitem.update_webif(_key_stay, True) self._abitem.update_webif(_key_enter, False) + self._abitem.update_webif(_key_pass, False) self.__actions_stay.execute(True, allow_item_repeat, self, self.__actions_enter_or_stay) self._log_decrease_indent(50) + + # run actions when passing the state + # item_allow_repeat: Is repeating actions generally allowed for the item? + def run_pass(self, is_repeat: bool, allow_item_repeat: bool): + self._log_info("Passing state {}, running pass actions.", self.id) self._log_increase_indent() - self._log_debug("Update web interface stay {}", self.id) - self._log_increase_indent() - 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() + _key_leave = ['{}'.format(self.id), 'leave'] + _key_stay = ['{}'.format(self.id), 'stay'] + _key_enter = ['{}'.format(self.id), 'enter'] + _key_pass = ['{}'.format(self.id), 'pass'] + self._abitem.update_webif(_key_leave, False) + self._abitem.update_webif(_key_stay, False) + self._abitem.update_webif(_key_enter, False) + self._abitem.update_webif(_key_pass, True) + self.__actions_pass.execute(is_repeat, allow_item_repeat, self) + self._log_decrease_indent(50) # run actions when leaving the state # item_allow_repeat: Is repeating actions generally allowed for the item? @@ -337,13 +360,6 @@ def run_leave(self, allow_item_repeat: bool): self._abitem.update_webif(_key_leave, False) self.__actions_leave.execute(False, allow_item_repeat, self) self._log_decrease_indent(50) - self._log_increase_indent() - self._log_debug("Update web interface leave {}", self.id) - self._log_increase_indent() - 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): cond_refill = not self.__use.is_empty() and ("eval" in self.__use.get_type() or "item" in self.__use.get_type()) @@ -379,13 +395,16 @@ def update_name(self, item_state, recursion_depth=0): # use item name as state name if "se_name" in item_state.conf: self.__text.set_from_attr(item_state, "se_name") + self._log_develop("Updated name of state {} to {} based on se_name.", item_state, self.__name) elif str(item_state) != item_state.property.path or (self.__name == "" and recursion_depth == 0): _name = str(item_state).split('.')[-1] self.__text.set(_name) + self._log_develop("Updated name of state {} to {} based on item name (recursion_depth = {}).", + item_state, self.__name, recursion_depth) elif self.__text.is_empty() and recursion_depth == 0: self.__text.set("value:" + self.__name) + self._log_develop("Set name of state {} to {} as it was empty.", item_state, self.__name) self.__name = self.text - self._log_develop("Updated name of state {} to {}.", item_state, self.__name) return self.__name def __fill_list(self, item_states, recursion_depth, se_use=None, use=None): @@ -402,7 +421,6 @@ def __fill_list(self, item_states, recursion_depth, se_use=None, use=None): self.__use_done.append(element) self.__fill(element, recursion_depth, se_use, use) - def __initialize_se_use(self, state, recursion_depth): # Import data from other item if attribute "use" is found if isinstance(state, SeState): @@ -486,9 +504,8 @@ def __initialize_se_use(self, state, recursion_depth): _configorigvalue[i] in item)): _issue_list = [item for key, value in _issues.items() if value for item in value] self._log_warning("se_use {} points to invalid item. Ignoring.", _configorigvalue[i]) - self._abitem.update_issues('config', {state.id: - {'issue': _issue_list, - 'attribute': 'se_use', 'origin': state_type}}) + self._abitem.update_issues('config', {state.id: {'issue': _issue_list, + 'attribute': 'se_use', 'origin': state_type}}) self.__use_ignore_list.append(_configorigvalue[i]) _path = None elif _returntype[i] in ['item', 'eval']: @@ -502,9 +519,8 @@ def __initialize_se_use(self, state, recursion_depth): _issue_list = [item for key, value in _issues.items() if value for item in value] self._log_warning("se_use {} defined by invalid item/eval. Ignoring.", _path) - self._abitem.update_issues('config', {state.id: - {'issue': _issue_list, - 'attribute': 'se_use', 'origin': state_type}}) + self._abitem.update_issues('config', {state.id: {'issue': _issue_list, + 'attribute': 'se_use', 'origin': state_type}}) self.__use_ignore_list.append(_path) _path = None if _path is None: @@ -558,50 +574,47 @@ def update_unused(used_attributes, attrib_type, attrib_name): used_attributes[nested_entry].update(nested_dict) self.__used_attributes.update(used_attributes) - def update_action_status(action_status, actiontype): + def update_action_status(actn_type, action_status): + def filter_issues(input_dict): + return { + key: {sub_key: sub_value for sub_key, sub_value in value.items() if + sub_value.get('issue') not in (None, [], [None])} + for key, value in input_dict.items() + } + if action_status is None: return action_status = StateEngineTools.flatten_list(action_status) if isinstance(action_status, list): for e in action_status: - update_action_status(e, actiontype) + update_action_status(actn_type, e) return for itm, dct in action_status.items(): - if itm not in self.__action_status: - self.__action_status.update({itm: dct}) + if itm not in self.__action_status[actn_type]: + self.__action_status[actn_type].update({itm: dct}) for (itm, dct) in action_status.items(): issues = dct.get('issue') + attributes = dct.get('attribute') if issues: if isinstance(issues, list): - self.__action_status[itm]['issue'].extend( - [issue for issue in issues if issue not in self.__action_status[itm]['issue']]) - 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], None]} - self.__action_status = filtered_dict - #self._log_develop("Updated action status: {}, updated used {}", self.__action_status, self.__used_attributes) + for i, issue in enumerate(issues): + if issue not in self.__action_status[actn_type][itm]['issue']: + self.__action_status[actn_type][itm]['issue'].append(issue) + self.__action_status[actn_type][itm]['attribute'].append(attributes[i]) + + flattened_dict = {} + for key, action_type_dict in self.__action_status.items(): + # Iterate through the inner dictionaries + for inner_key, nested_dict in action_type_dict.items(): + # Initialize the entry in the flattened dictionary + if inner_key not in flattened_dict: + flattened_dict[inner_key] = {} + # Add 'used in' and update with existing data + flattened_dict[inner_key]['used in'] = key + flattened_dict[inner_key].update(nested_dict) + self.__used_attributes = deepcopy(flattened_dict) + self.__action_status = filter_issues(self.__action_status) if isinstance(state, SeState): item_state = state.state_item @@ -610,11 +623,12 @@ def update_action_status(action_status, actiontype): self._log_develop("Fill state {} type {}, called by {}, recursion {}", item_state, type(item_state), se_use, recursion_depth) if se_use == "reinit": self._log_develop("Resetting conditions and actions at re-init use is {}", use) - self.__conditions.reset() + self.__conditionsets.reset() self.__actions_enter_or_stay.reset() self.__actions_enter.reset() self.__actions_stay.reset() self.__actions_leave.reset() + self.__actions_pass.reset() self.__use_done = [] use = self.__use.get() @@ -625,13 +639,12 @@ def update_action_status(action_status, actiontype): use = StateEngineTools.flatten_list(use) self.__fill_list(use, recursion_depth, se_use, use) # Get action sets and condition sets - self._log_develop("Use is {}", use) parent_item = item_state.return_parent() if parent_item == Items.get_instance(): parent_item = None child_items = item_state.return_children() _conditioncount = 0 - _action_counts = {"enter": 0, "stay": 0, "enter_or_stay": 0, "leave": 0} + _action_counts = {"enter": 0, "stay": 0, "enter_or_stay": 0, "leave": 0, "pass": 0} _unused_attributes = {} _used_attributes = {} _action_status = {} @@ -641,7 +654,7 @@ def update_action_status(action_status, actiontype): 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) + _unused_attributes, _used_attributes = self.__conditionsets.update(child_name, child_item, parent_item) self.__unused_attributes = copy(_unused_attributes) self.__used_attributes = copy(_used_attributes) for item in self.__unused_attributes.keys(): @@ -670,20 +683,22 @@ def update_action_status(action_status, actiontype): child_name = StateEngineTools.get_last_part_of_item_id(child_item) try: action_mapping = { - "on_enter": ("enter", self.__actions_enter), - "on_stay": ("stay", self.__actions_stay), - "on_enter_or_stay": ("enter_or_stay", self.__actions_enter_or_stay), - "on_leave": ("leave", self.__actions_leave) + "on_enter": ("enter", "actions_enter", self.__actions_enter), + "on_stay": ("stay", "actions_stay", self.__actions_stay), + "on_enter_or_stay": ("enter_or_stay", "actions_enter_or_stay", self.__actions_enter_or_stay), + "on_leave": ("leave", "actions_leave", self.__actions_leave), + "on_pass": ("pass", "actions_pass", self.__actions_pass) } if child_name in action_mapping: - action_name, action_method = action_mapping[child_name] + action_name, action_type, action_method = action_mapping[child_name] for attribute in child_item.conf: - self._log_develop("Filling state with {} action named {}", child_name, attribute) + self._log_develop("Filling state with {} action named {} for state {} with config {}", child_name, attribute, state.id, child_item.conf) + action_method.update_action_details(self, action_type) _action_counts[action_name] += 1 - _, _action_status = action_method.update(attribute, child_item.conf[attribute]) + _, _action_status = action_method.update(attribute, child_item.conf.get(attribute)) if _action_status: - update_action_status(_action_status, action_name) + update_action_status(action_name, _action_status) self._abitem.update_action_status(self.__action_status) update_unused(_used_attributes, 'action', child_name) @@ -693,11 +708,12 @@ def update_action_status(action_status, actiontype): self._abitem.update_attributes(self.__unused_attributes, self.__used_attributes) # Actions defined directly in the item go to "enter_or_stay" for attribute in item_state.conf: - _result = self.__actions_enter_or_stay.update(attribute, item_state.conf[attribute]) + self.__actions_enter_or_stay.update_action_details(self, "actions_enter_or_stay") + _result = self.__actions_enter_or_stay.update(attribute, item_state.conf.get(attribute)) _action_counts["enter_or_stay"] += _result[0] if _result else 0 _action_status = _result[1] if _action_status: - update_action_status(_action_status, 'enter_or_stay') + update_action_status("enter_or_stay", _action_status) self._abitem.update_action_status(self.__action_status) _total_actioncount = _action_counts["enter"] + _action_counts["stay"] + _action_counts["enter_or_stay"] + _action_counts["leave"] @@ -706,35 +722,39 @@ def update_action_status(action_status, actiontype): # Complete condition sets and actions at the end if recursion_depth == 0: - self.__conditions.complete(self, use) - _action_status = self.__actions_enter.complete(self, self.__conditions.evals_items, use) + self.__conditionsets.complete(self, use) + _action_status = self.__actions_enter.complete(self.__conditionsets.evals_items, use) + if _action_status: + update_action_status("enter", _action_status) + self._abitem.update_action_status(self.__action_status) + _action_status = self.__actions_stay.complete(self.__conditionsets.evals_items, use) if _action_status: - update_action_status(_action_status, 'enter') + update_action_status("stay", _action_status) self._abitem.update_action_status(self.__action_status) - _action_status = self.__actions_stay.complete(self, self.__conditions.evals_items, use) + _action_status = self.__actions_enter_or_stay.complete(self.__conditionsets.evals_items, use) if _action_status: - update_action_status(_action_status, 'stay') + update_action_status("enter_or_stay", _action_status) self._abitem.update_action_status(self.__action_status) - _action_status = self.__actions_enter_or_stay.complete(self, self.__conditions.evals_items, use) + _action_status = self.__actions_pass.complete(self.__conditionsets.evals_items, use) if _action_status: - update_action_status(_action_status, 'enter_or_stay') + update_action_status("pass", _action_status) self._abitem.update_action_status(self.__action_status) - _action_status = self.__actions_leave.complete(self, self.__conditions.evals_items, use) + _action_status = self.__actions_leave.complete(self.__conditionsets.evals_items, use) if _action_status: - update_action_status(_action_status, 'leave') + update_action_status("leave", _action_status) self._abitem.update_action_status(self.__action_status) self._abitem.update_action_status(self.__action_status) self._abitem.update_attributes(self.__unused_attributes, self.__used_attributes) - _summary = "{} on_enter, {} on_stay , {} on_enter_or_stay, {} on_leave" + _summary = "{} on_enter, {} on_stay , {} on_enter_or_stay, {} on_leave, {} on_pass" if self.__action_status: _ignore_list = [entry for entry in self.__action_status if self.__action_status[entry].get('ignore') is True] if _ignore_list: self._log_info("Ignored {} action(s) due to errors: {}", len(_ignore_list), _ignore_list) if se_use is not None: self._log_debug("Added {} action(s) based on se_use {}. " + _summary, _total_actioncount, se_use, - _action_counts["enter"], _action_counts["stay"], _action_counts["enter_or_stay"], _action_counts["leave"]) + _action_counts["enter"], _action_counts["stay"], _action_counts["enter_or_stay"], _action_counts["leave"], _action_counts["pass"]) self._log_debug("Added {} condition set(s) based on se_use: {}", _conditioncount, se_use) else: self._log_debug("Added {} action(s) based on item configuration: " + _summary, _total_actioncount, - _action_counts["enter"], _action_counts["stay"], _action_counts["enter_or_stay"], _action_counts["leave"]) + _action_counts["enter"], _action_counts["stay"], _action_counts["enter_or_stay"], _action_counts["leave"], _action_counts["pass"]) self._log_debug("Added {} condition set(s) based on item configuration", _conditioncount) diff --git a/stateengine/StateEngineStructs.py b/stateengine/StateEngineStructs.py index de630d0ab..98a6afeb2 100755 --- a/stateengine/StateEngineStructs.py +++ b/stateengine/StateEngineStructs.py @@ -25,7 +25,7 @@ def create(_abitem, struct): - _find_result = next((item for item in __allstructs if item["name"] == struct), False) + _find_result = next((item for item in __allstructs if item["name"] == struct), {}) if not _find_result: created_struct = StateEngineStruct.SeStructMain(_abitem, struct, global_struct) __allstructs.append({'name': struct, 'struct': created_struct}) diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index bfc2c87b8..0e14b1296 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -136,7 +136,8 @@ def set_from_attr(self, item, attribute_name, default_value=None, reset=True, at self._log_develop("Setting value {0}, attribute name {1}, reset {2}, type {3}", value, attribute_name, reset, attr_type) _returnvalue, _returntype, _issue, _origvalue = self.set(value, attribute_name, reset) - self._log_develop("Set from attribute returnvalue {}, returntype {}, issue {}, original {}", _returnvalue, _returntype, _issue, _origvalue) + self._log_develop("Set from attribute returnvalue {}, returntype {}, issue {}, original {}", + _returnvalue, _returntype, _issue, _origvalue) return _returnvalue, _returntype, _using_default, _issue, _origvalue def _set_additional(self, _additional_sources): @@ -446,10 +447,14 @@ def get(self, default=None, originalorder=True): else: return returnvalues - def get_for_webif(self): - returnvalues = self.get() + def get_for_webif(self, value=None): + if value is None: + returnvalues = self.get() + else: + returnvalues = value returnvalues = self.__varname if returnvalues == '' else returnvalues - return str(returnvalues) + returnvalues = str(returnvalues) + return returnvalues def get_type(self): if len(self.__listorder) <= 1: @@ -479,6 +484,7 @@ def write_to_logger(self): self._log_debug("{0}: {1} ({2})", self.__name, i, type(i)) else: self._log_debug("{0}: {1} ({2})", self.__name, self.__value, type(self.__value)) + return self.__value if self.__regex is not None: if isinstance(self.__regex, list): for i in self.__regex: @@ -486,6 +492,7 @@ def write_to_logger(self): self._log_debug("{0} from regex: {1}", self.__name, i) else: self._log_debug("{0} from regex: {1}", self.__name, self.__regex) + return f"regex:{self.__regex}" if self.__struct is not None: if isinstance(self.__struct, list): for i in self.__struct: @@ -494,22 +501,30 @@ def write_to_logger(self): else: self._log_debug("{0} from struct: {1}", self.__name, self.__struct.property.path) + return self.__struct if self.__item is not None: _original_listorder = self.__listorder.copy() + items = [] if isinstance(self.__item, list): 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]) + current = self.__get_from_item()[i] + items.append(current) + self._log_debug("Currently item results in {}", current) 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()) + items = self.__get_from_item() + self._log_debug("Currently item results in {}", items) self.__listorder = _original_listorder + return items if self.__eval is not None: self._log_debug("{0} from eval: {1}", self.__name, self.__eval) _original_listorder = self.__listorder.copy() - self._log_debug("Currently eval results in {}. ", self.__get_eval()) + eval_result = self.__get_eval() + self._log_debug("Currently eval results in {}. ", eval_result) self.__listorder = _original_listorder + return eval_result if self.__varname is not None: if isinstance(self.__varname, list): for i in self.__varname: @@ -517,6 +532,11 @@ def write_to_logger(self): self._log_debug("{0} from variable: {1}", self.__name, i) else: self._log_debug("{0} from variable: {1}", self.__name, self.__varname) + _original_listorder = self.__listorder.copy() + var_result = self.__get_from_variable() + self.__listorder = _original_listorder + return var_result + return None # Get Text (similar to logger text) # prefix: Prefix for text @@ -614,7 +634,7 @@ def __do_cast(self, value, item_id=None): _newvalue = element if element == 'novalue' else self.__cast_func(element) except Exception as ex: _newvalue = None - _issue = "Problem casting element '{0}' to {1}: {2}.".format(element, self.__cast_func, ex) + _issue = "Problem casting element '{0}': {1}.".format(element, ex) self._log_warning(_issue) valuelist.append(_newvalue) if element in self.__listorder: @@ -644,7 +664,7 @@ def __do_cast(self, value, item_id=None): _issue = "You most likely forgot to prefix your expression with 'eval:'" raise ValueError(_issue) else: - _issue = "Not possible to cast '{}' because {}".format(value, ex) + _issue = "{}".format(ex) raise ValueError(_issue) if value in self.__listorder: self.__listorder[self.__listorder.index(value)] = _newvalue @@ -744,13 +764,28 @@ def __get_from_regex(self): def __get_eval(self): # noinspection PyUnusedLocal sh = self._sh + # noinspection PyUnusedLocal shtime = self._shtime + patterns = [ + "get_variable('current.", + 'get_variable("current.', + "get_variable('next.", + 'get_variable("next.' + ] if isinstance(self.__eval, str): self.__eval = StateEngineTools.parse_relative(self.__eval, 'sh.', ['()', '.property.']) 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}", self.__eval) + if self.__eval in self._abitem.cache: + self._log_increase_indent() + result = self._abitem.cache.get(self.__eval) + self._log_debug("Loading eval from cache: {}", result) + self._log_decrease_indent() + if 'eval:{}'.format(self.__eval) in self.__listorder: + self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = [result] + return result self._log_increase_indent() try: _newvalue, _issue = self.__do_cast(eval(self.__eval)) @@ -762,6 +797,8 @@ def __get_eval(self): values = _newvalue self._log_decrease_indent() self._log_debug("Eval result: {0} ({1}).", values, type(values)) + if not any(pattern in self.__eval for pattern in patterns): + self._abitem.cache = {self.__eval: values} self._log_increase_indent() except Exception as ex: self._log_decrease_indent() @@ -785,6 +822,14 @@ def __get_eval(self): pass self._log_debug("Checking eval {0} from list {1}.", val, self.__eval) self._log_increase_indent() + if val in self._abitem.cache: + result = self._abitem.cache.get(val) + self._log_debug("Loading eval in list from cache: {} ({})", result, type(result)) + self._log_decrease_indent() + values.append(result) + if 'eval:{}'.format(val) in self.__listorder: + self.__listorder[self.__listorder.index('eval:{}'.format(val))] = [result] + continue if isinstance(val, str): if "stateengine_eval" in val or "se_eval" in val: # noinspection PyUnusedLocal @@ -830,9 +875,19 @@ def __get_eval(self): value = None if value is not None: values.append(value) + if not any(pattern in val for pattern in patterns): + self._abitem.cache = {val: value} self._log_decrease_indent() else: self._log_debug("Checking eval (no str, no list): {0}.", self.__eval) + if self.__eval in self._abitem.cache: + self._log_increase_indent() + result = self._abitem.cache.get(self.__eval) + self._log_debug("Loading eval (no str, no list) from cache: {}", result) + self._log_decrease_indent() + if 'eval:{}'.format(self.__eval) in self.__listorder: + self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = [result] + return result try: self._log_increase_indent() _newvalue, _issue = self.__do_cast(self.__eval()) @@ -844,6 +899,7 @@ def __get_eval(self): values = _newvalue self._log_decrease_indent() self._log_debug("Eval result (no str, no list): {0}.", values) + self._abitem.cache = {self.__eval: values} self._log_increase_indent() except Exception as ex: self._log_decrease_indent() @@ -963,11 +1019,10 @@ def update_value(varname): if isinstance(self.__varname, list): for var in self.__varname: - values.append(update_value(var)) self._log_debug("Checking variable in loop '{0}', value {1} from list {2}", var, values[-1], self.__listorder) + values.append(update_value(var)) else: values = update_value(self.__varname) - self._log_debug("Variable result: {0}", values) - + self._log_debug("Variable result: {0}", values) return values diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index 3181788b2..040ff6209 100755 --- a/stateengine/StateEngineWebif.py +++ b/stateengine/StateEngineWebif.py @@ -59,98 +59,66 @@ def __init__(self, abitem): 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): + def _strip_regex(self, regex_list): + pattern_strings = [] + if not isinstance(regex_list, list): + regex_list = [regex_list] + for item in regex_list: + if isinstance(item, re.Pattern): + pattern_strings.append(item.pattern) + else: + pattern_match = re.search(r"re\.compile\('([^']*)'", item) + if pattern_match: + item = f"regex:{pattern_match.group(1)}" + pattern_strings.append(str(item)) + if len(pattern_strings) <= 1: + pattern_strings = pattern_strings[0] + return str(pattern_strings) + + def _actionlabel(self, state, label_type, conditionset, active): # Check if conditions for action are met or not - # action_dict: abitem[state]['on_enter'/'on_stay'/'on_enter_or_stay'/'on_leave'].get(action) - # condition_to_meet: 'conditionset'/'previousconditionset''previousstate_conditionset' - # conditionset: name of conditionset that should get checked - def _strip_regex(regex_list): - pattern_strings = [] - for item in regex_list: - if isinstance(item, re.Pattern): - pattern_strings.append(item.pattern) - else: - pattern_strings.append(str(item)) - return str(pattern_strings) - - def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: str): - _condition_check = action_dict.get(condition_to_meet) - _condition_check = StateEngineTools.flatten_list(_condition_check) - _condition_necessary = 1 if _condition_check != 'None' else 0 - _condition_check = _condition_check if isinstance(_condition_check, list) else [_condition_check] - _condition_count = 0 - _condition = False - for cond in _condition_check: - try: - if isinstance(cond, str): - 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 + # state: state where action is defined in + # label_type: on_enter, on_stay, on_leave, on_pass + # active: if action is currently run actionlabel = actionstart = '<' action_tooltip = '' - originaltype = label_type - types = [label_type] if label_type == 'actions_leave' else ['actions_enter_or_stay', label_type] + types = [label_type] if label_type in ['actions_leave', 'actions_pass'] else ['actions_enter_or_stay', label_type] tooltip_count = 0 + for label_type in types: for action in self.__states[state].get(label_type): 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') + _reason = action_dict['actionstatus'].get('reason') else: _success = None _issue = None + _reason = None _repeat = action_dict.get('repeat') _delay = int(float(action_dict.get('delay') or 0)) - _delta = action_dict.get('delta') or '0' - _mindelta = action_dict.get('mindelta') or '0' - - condition_necessary = 0 - condition_met = True - condition_count = 0 - count, condition1, condition_to_meet, necessary = _check_webif_conditions(action_dict, 'conditionset', conditionset) - condition_count += count - condition_necessary += min(1, necessary) - count, condition2, previouscondition_to_meet, necessary = _check_webif_conditions(action_dict, 'previousconditionset', previousconditionset) - condition_count += count - condition_necessary += min(1, necessary) - count, condition3, previousstate_condition_to_meet, necessary = _check_webif_conditions(action_dict, 'previousstate_conditionset', previousstate_conditionset) - condition_count += count - condition_necessary += min(1, necessary) - - if condition_count < condition_necessary: - condition_met = False + cond1 = conditionset in ['', self.__active_conditionset] and state == self.__active_state cond2 = self.__states[state]['conditionsets'].get(conditionset) is not None - cond_delta = float(_delta) < float(_mindelta) - fontcolor = "white" if cond1 and cond2 and ( - cond_delta or - (not condition_met or (_repeat is False and originaltype == 'actions_stay'))) \ - else "#5c5646" if _delay > 0 else "darkred" if _delay < 0 \ - else "#303030" if not condition_met or _issue else "black" - condition_info = _strip_regex(condition_to_meet) if condition1 is False \ - else _strip_regex(previouscondition_to_meet) if condition2 is False \ - else _strip_regex(previousstate_condition_to_meet) if condition3 is False \ - else "" + + fontcolor = "white" if (_success == "False" and active) and ((cond1 and cond2 and _reason) or (_reason and label_type in ['actions_leave', 'actions_pass'])) \ + else "#f4c430" if _delay > 0 and active else "darkred" if _delay < 0 and active \ + else "#303030" if _issue else "black" + 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 " (cancel delay!)" if _delay == -1 \ - else " (wrong delay!)" if _delay < -1 \ - else " (delta {} < {})".format(_delta, _mindelta) if cond_delta and cond1 and cond2\ - else "" + additionaltext = " (issue: see tooltip)" if _issue is not None \ + else _reason if _reason is not None \ + else " (delay: {})".format(_delay) if _delay > 0\ + else " (cancel delay!)" if _delay == -1 \ + else " (wrong delay!)" if _delay < -1 \ + else "" action1 = action_dict.get('function') if action1 in ['set', 'force set']: action2 = str(action_dict.get('item')) @@ -166,14 +134,10 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s else: action2 = 'None' 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 \ + if _success == 'False' and active \ else '' \ if _success == 'Scheduled' and active \ else '' \ @@ -192,19 +156,19 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s #self._log_debug('actionlabel: {}', actionlabel) return actionlabel, action_tooltip, tooltip_count - def _conditionlabel(self, state, conditionset, i): + def _conditionlabel(self, state, conditionset): condition_tooltip = '' conditions_done = [] _empty_set = self.__states[state]['conditionsets'].get(conditionset) == '' if _empty_set: return '', '', 0 conditionlist = '<
' + conditionlist += ''.format(conditionset) tooltip_count = 0 for k, condition in enumerate(self.__states[state]['conditionsets'].get(conditionset)): condition_dict = self.__states[state]['conditionsets'][conditionset].get(condition) current = condition_dict.get('current') match = condition_dict.get('match') - status_none = str(condition_dict.get('status')) == 'None' item_none = str(condition_dict.get('item')) == 'None' or not status_none status_eval_none = condition_dict.get('status_eval') == 'None' @@ -224,24 +188,21 @@ def _conditionlabel(self, state, conditionset, i): 'updatedbynegate', 'triggeredbynegate', 'status', 'current', 'match', 'status_eval'] if cond1 and compare not in excluded_values: - 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." + text = " Current {}".format(current_clean) if current is not None and len(current) > 0 else " Not evaluated." conditionlist += ('').format(condition.upper(), text) conditions_done.append(condition) conditionlist += ''.format(info) comparison = ">=" if not min_none and compare == "min"\ else "<=" if not max_none and compare == "max"\ @@ -332,10 +294,9 @@ def _conditionlabel(self, state, conditionset, i): 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 '' - 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\ + match_info = '' if match_info == 'yes'\ + else '' if match_info == 'no'\ + else '' if match_info and len(match_info) > 0 \ else '' conditionlist += ''.format(match_info) 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_status_eval = str(condition_dict.get('status_eval') or '') + info_status = str(condition_dict.get('status') or 'n/a') + info_item = str(condition_dict.get('item') or 'n/a') + info_eval = str(condition_dict.get('eval') or 'n/a') + info_status_eval = str(condition_dict.get('status_eval') or 'n/a') info_compare = str(condition_dict.get(compare) or '') + info_compare = self._strip_regex(info_compare) if not status_none: textlength = len(info_status) if textlength > self.__textlimit: @@ -294,7 +255,8 @@ def _conditionlabel(self, state, conditionset, i): elif not item_none: info = info_item else: - info = "" + info = "n/a" + conditionlist += '{}{}
>' @@ -344,16 +305,16 @@ def _conditionlabel(self, state, conditionset, i): def _add_actioncondition(self, state, conditionset, action_type, new_y, cond1, cond2): cond4 = conditionset in ['', self.__active_conditionset] and state == self.__active_state cond5 = self.__states[state]['conditionsets'].get(conditionset) is not None - 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 + cond_enter = action_type in ['actions_enter', 'actions_enter_or_stay'] and self.__states[state].get('enter') is False + cond_stay = action_type in ['actions_stay', 'actions_enter_or_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 "olivedrab" if cond4 else "indianred2" color_stay = "gray" if (cond1 and cond2 and cond5) or \ (cond_stay and cond4 and cond5) else "olivedrab" if cond4 else "indianred2" - label = 'first enter' if action_type == 'actions_enter' else 'staying at state' + label = 'first enter' if action_type in ['actions_enter', 'actions_enter_or_stay'] else 'staying at state' - position = '{},{}!'.format(0.63, new_y) + position = '{},{}!'.format(0.38, new_y) color = color_enter if label == 'first enter' else color_stay self.__nodes['{}_{}_state_{}'.format(state, conditionset, action_type)] = \ pydotplus.Node('{}_{}_state_{}'.format(state, conditionset, action_type), style="filled", fillcolor=color, @@ -386,8 +347,6 @@ def drawgraph(self, filename): new_y = 2 previous_state = '' previous_conditionset = '' - previousconditionset = '' - previousstate_conditionset = '' for i, state in enumerate(self.__states): #self._log_debug('Adding state for webif {}', self.__states[state]) if isinstance(self.__states[state], (OrderedDict, dict)): @@ -404,13 +363,14 @@ def drawgraph(self, filename): new_y -= 1 * self.__scalefactor position = '{},{}!'.format(0, new_y) if not i == 0: - condition_node = 'leave' if self.__nodes.get('{}_leave'.format(previous_state)) \ - else list(self.__states[previous_state]['conditionsets'].keys())[-1] + condition_node = 'pass' if self.__nodes.get('{}_pass'.format(previous_state)) \ + else 'leave' if self.__nodes.get('{}_leave'.format(previous_state)) \ + else list(self.__states[previous_state]['conditionsets'].keys())[-1] lastnode = self.__nodes['{}_{}'.format(previous_state, condition_node)] self.__nodes['{}_above'.format(state)] = pydotplus.Node('{}_above'.format(state), pos=position, shape="square", width="0", label="") self.__graph.add_node(self.__nodes['{}_above'.format(state)]) - position = '{},{}!'.format(0.5, new_y) + position = '{},{}!'.format(0.3, new_y) self.__nodes['{}_above_right'.format(state)] = pydotplus.Node('{}_above_right'.format(state), pos=position, shape="square", width="0", label="") self.__graph.add_node(self.__nodes['{}_above_right'.format(state)]) @@ -430,7 +390,7 @@ def drawgraph(self, filename): label='<
' '
{}
{}
>'.format( state, self.__states[state]['name'])) - position = '{},{}!'.format(0.5, new_y) + position = '{},{}!'.format(0.3, new_y) self.__nodes['{}_right'.format(state)] = pydotplus.Node('{}_right'.format(state), pos=position, shape="square", width="0", label="") self.__graph.add_node(self.__nodes[state]) @@ -439,66 +399,82 @@ def drawgraph(self, filename): actionlist_enter = '' actionlist_stay = '' actionlist_leave = '' + actionlist_pass = '' condition_tooltip = '' action_tooltip = '' j = 0 - new_x = 0.9 + new_x = 0.55 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 [] + actions_pass = self.__states[state].get('actions_pass') or [] action_tooltip_count_enter = 0 action_tooltip_count_stay = 0 action_tooltip_count_leave = 0 + action_tooltip_count_pass = 0 action_tooltip_enter = "" action_tooltip_stay = "" action_tooltip_leave = "" + action_tooltip_pass = "" for j, conditionset in enumerate(self.__states[state]['conditionsets']): - - 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(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, condition_tooltip_count = self._conditionlabel(state, conditionset, i) cond3 = conditionset == '' try: cond1 = i >= list(self.__states.keys()).index(self.__active_state) - except Exception as ex: - #self._log_debug('Condition 1 problem {}'.format(ex)) + except Exception: cond1 = True try: cond4 = i == list(self.__states.keys()).index(self.__active_state) - except Exception as ex: - #self._log_debug('Condition 4 problem {}'.format(ex)) + except Exception: cond4 = True #self._log_debug('i {}, index of active state {}', i, list(self.__states.keys()).index(self.__active_state)) try: cond2 = (j > list(self.__states[state]['conditionsets'].keys()).index(self.__active_conditionset) or i > list(self.__states.keys()).index(self.__active_state)) - except Exception as ex: - #self._log_debug('Condition 2 problem {}'.format(ex)) + except Exception: cond2 = False if cond3 and cond4 else True color = "gray" if cond1 and cond2 else "olivedrab" \ if (conditionset == self.__active_conditionset or cond3) and state == self.__active_state else "indianred2" + try: + cond5 = i >= list(self.__states.keys()).index(self.__active_state) + except Exception: + cond5 = True + + cond6 = conditionset in ['', self.__active_conditionset] and state == self.__active_state + cond_enter = True if self.__states[state].get('enter') is True else False + cond_stay = True if self.__states[state].get('stay') is True else False + active = True if cond_enter and cond6 else False + + 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, active) + active = True if cond_stay and cond6 else False + 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, active) + cond_leave = True if self.__states[state].get('leave') is True else False + active = True if cond_leave else False + + if len(actions_leave) > 0: + actionlist_leave, action_tooltip_leave, action_tooltip_count_leave = \ + self._actionlabel(state, 'actions_leave', conditionset, active) + cond_pass = True if self.__states[state].get('pass') is True else False + active = False if (cond5 and not cond_pass) or cond_leave else True + if len(actions_pass) > 0: + actionlist_pass, action_tooltip_pass, action_tooltip_count_pass = \ + self._actionlabel(state, 'actions_pass', conditionset, active) + + new_y -= 1 * self.__scalefactor if j == 0 else 2 * self.__scalefactor + position = '{},{}!'.format(0.3, new_y) + conditionset_positions.append(new_y) + conditionlist, condition_tooltip, condition_tooltip_count = self._conditionlabel(state, conditionset) + label = 'no condition' if conditionset == '' else conditionset self.__nodes['{}_{}'.format(state, conditionset)] = pydotplus.Node( '{}_{}'.format(state, conditionset), style="filled", fillcolor=color, shape="diamond", label=label, pos=position) - #self._log_debug('Node {} {} drawn', state, conditionset) - position = '{},{}!'.format(0.2, new_y) + #self._log_debug('Node {} {} drawn. Conditionlist {}', state, conditionset, conditionlist) + position = '{},{}!'.format(0.1, new_y) xlabel = '1 tooltip' if condition_tooltip_count == 1\ else '{} tooltips'.format(condition_tooltip_count)\ if condition_tooltip_count > 1 else '' @@ -507,9 +483,12 @@ def drawgraph(self, filename): '{}_{}_conditions'.format(state, conditionset), style="filled", fillcolor=color, shape="rect", label=conditionlist, pos=position, tooltip=condition_tooltip, xlabel=xlabel) self.__graph.add_node(self.__nodes['{}_{}_conditions'.format(state, conditionset)]) + # Create a dotted line between conditionlist and conditionset name + parenthesis_edge = pydotplus.Edge(self.__nodes['{}_{}_conditions'.format(state, conditionset)], self.__nodes['{}_{}'.format(state, conditionset)], arrowhead="none", color="black", style="dotted", constraint="false") + self.__graph.add_edge(parenthesis_edge) self.__graph.add_node(self.__nodes['{}_{}'.format(state, conditionset)]) - new_x = 0.9 + new_x = 0.55 if not actionlist_enter == '': position = '{},{}!'.format(new_x, new_y) xlabel = '1 tooltip' if action_tooltip_count_enter == 1\ @@ -537,11 +516,12 @@ def drawgraph(self, filename): self.__graph.add_node(self.__nodes['{}_{}_actions_stay'.format(state, conditionset)]) self._add_actioncondition(state, conditionset, 'actions_stay', new_y, cond1, cond2) - position = '{},{}!'.format(0.9, new_y) + position = '{},{}!'.format(0.55, new_y) cond1 = self.__nodes.get('{}_{}_actions_enter'.format(state, conditionset)) is None cond2 = self.__nodes.get('{}_{}_actions_stay'.format(state, conditionset)) is None cond3 = self.__nodes.get('{}_{}_actions_leave'.format(state, conditionset)) is None - if cond1 and cond2 and cond3: + cond4 = self.__nodes.get('{}_{}_actions_pass'.format(state, conditionset)) is None + if cond1 and cond2 and cond3 and cond4: self.__nodes['{}_{}_right'.format(state, conditionset)] = pydotplus.Node('{}_{}_right'.format( state, conditionset), shape="circle", width="0.7", pos=position, label="", fillcolor="black", style="filled", tooltip="No Action") @@ -557,7 +537,7 @@ def drawgraph(self, filename): xlabel = "" if j == 0: self.__graph.add_edge(pydotplus.Edge(self.__nodes[state], self.__nodes['{}_right'.format(state)], - style='bold', color='black', dir='none', + style='bold', color='black', dir='none', xlabel=xlabel, edgetooltip='check first conditionset')) self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_right'.format(state)], self.__nodes['{}_{}'.format(state, conditionset)], @@ -571,18 +551,13 @@ def drawgraph(self, filename): 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) - try: - cond1 = j > list(self.__states[state]['conditionsets'].keys()).index(self.__active_conditionset) - except Exception: - cond1 = True + position = '{},{}!'.format(0.3, new_y) try: cond2 = i >= list(self.__states.keys()).index(self.__active_state) - except Exception: + except Exception as ex: cond2 = True cond3 = True if self.__states[state].get('leave') is True else False - color = "gray" if cond1 and cond2 and not cond3 else "olivedrab" if cond3 else "indianred2" + color = "gray" if 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) @@ -605,6 +580,39 @@ def drawgraph(self, filename): self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_leave'.format(state)], self.__nodes['{}_actions_leave'.format(state)], style='bold', taillabel=" True", tooltip='run leave actions')) + previous_conditionset = self.__nodes['{}_leave'.format(state)] + + if len(actions_pass) > 0: + new_y -= 1 * self.__scalefactor if j == 0 else 2 * self.__scalefactor + position = '{},{}!'.format(0.3, new_y) + try: + cond2 = i >= list(self.__states.keys()).index(self.__active_state) + except Exception: + cond2 = True + cond3 = True if self.__states[state].get('pass') is True else False + color = "gray" if cond2 and not cond3 else "olivedrab" if cond3 else "indianred2" + self.__nodes['{}_pass'.format(state)] = pydotplus.Node('{}_pass'.format(state), + style="filled", fillcolor=color, shape="diamond", + label='pass', pos=position) + self.__graph.add_node(self.__nodes['{}_pass'.format(state)]) + self.__graph.add_edge(pydotplus.Edge(previous_conditionset, self.__nodes['{}_pass'.format(state)], + style='bold', color='black', tooltip='check pass')) + + position = '{},{}!'.format(new_x, new_y) + xlabel = '1 tooltip' if action_tooltip_count_pass == 1\ + else '{} tooltips'.format(action_tooltip_count_pass)\ + if action_tooltip_count_pass > 1 else '' + #self._log_debug('action pass: {}', position) + self.__nodes['{}_actions_pass'.format(state)] = pydotplus.Node('{}_actions_pass'.format(state), + style="filled", fillcolor=color, + shape="rectangle", label=actionlist_pass, + pos=position, align="center", + tooltip=action_tooltip_pass, + xlabel=xlabel) + self.__graph.add_node(self.__nodes['{}_actions_pass'.format(state)]) + self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_pass'.format(state)], + self.__nodes['{}_actions_pass'.format(state)], style='bold', + taillabel=" True", tooltip='run pass actions')) previous_state = state diff --git a/stateengine/__init__.py b/stateengine/__init__.py index bfa0c5c71..5b5296938 100755 --- a/stateengine/__init__.py +++ b/stateengine/__init__.py @@ -47,7 +47,7 @@ class StateEngine(SmartPlugin): - PLUGIN_VERSION = '2.1.0' + PLUGIN_VERSION = '2.2.0' # Constructor # noinspection PyUnusedLocal,PyMissingConstructor diff --git a/stateengine/plugin.yaml b/stateengine/plugin.yaml index 5897f722a..be3495046 100755 --- a/stateengine/plugin.yaml +++ b/stateengine/plugin.yaml @@ -39,7 +39,7 @@ plugin: state: ready support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1303071-stateengine-plugin-support - version: '2.1.0' + version: '2.2.0' sh_minversion: '1.6' multi_instance: False classname: StateEngine @@ -853,6 +853,12 @@ item_structs: eval: sh..suspendduration(sh..suspendduration(), "Init", "Start") crontab: init = True + conditionset_leaveactions: + suspend: + type: str + initial_value: '.*' + cache: True + suspendduration: remark: duration of suspend mode in minutes (gets converted automatically) type: num @@ -937,29 +943,52 @@ item_structs: - 'repeat: True' - 'order: 5' + on_pass: + se_action_suspend: + - 'function: set' + - 'to: False' + - 'repeat: False' + se_action_suspend_visu: + - 'function: set' + - 'to: False' + - 'repeat: False' + se_action_suspend_end: + - 'function: set' + - 'to: ' + - 'repeat: False' + se_action_suspend_start: + - 'function: set' + - 'to: ' + - 'repeat: False' + on_leave: se_action_suspend: - 'function: set' - 'to: False' - 'order: 2' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' se_action_suspend_visu: - 'function: set' - 'to: False' - 'order: 3' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' se_action_suspend_end: - 'function: set' - 'to: ' - 'order: 4' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' se_action_suspend_start: - 'function: set' - 'to: ' - 'order: 5' - 'delay: 1' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' se_action_retrigger: - 'function: special' - 'value: retrigger:..retrigger' - 'delay: -1' - 'order: 1' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' enter_manuell: se_value_trigger_source: eval:se_eval.get_relative_itemproperty('..manuell', 'path') @@ -1049,6 +1078,12 @@ item_structs: eval: (sh..suspendduration(sh..suspendduration(), "Init", "Start"), sh..suspendvariant.suspendduration0(sh..suspendduration(), "Init", "Start"), sh..suspendvariant.suspendduration1(sh..suspendvariant.suspendduration1(), "Init", "Start"), sh..suspendvariant.suspendduration2(sh..suspendvariant.suspendduration2(), "Init", "Start")) crontab: init = True + conditionset_leaveactions: + suspend: + type: str + initial_value: '.*' + cache: True + suspendvariant: remark: number between 0 and 2 to define which suspendduration should be used type: num @@ -1241,33 +1276,57 @@ item_structs: - 'repeat: True' - 'order: 5' + on_pass: + se_action_suspend: + - 'function: set' + - 'to: False' + - 'repeat: False' + se_action_suspend_visu: + - 'function: set' + - 'to: False' + - 'repeat: False' + se_action_suspend_end: + - 'function: set' + - 'to: ' + - 'repeat: False' + se_action_suspend_start: + - 'function: set' + - 'to: ' + - 'repeat: False' + on_leave: se_action_suspendvariant: - 'function: set' - 'to: eval:se_eval.get_relative_itemproperty("..settings.suspendvariant", "last_value")' - 'order: 1' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' se_action_suspend: - 'function: set' - 'to: False' - 'order: 2' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' se_action_suspend_visu: - 'function: set' - 'to: False' - 'order: 3' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' se_action_suspend_end: - 'function: set' - 'to: ' - 'order: 4' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' se_action_suspend_start: - 'function: set' - 'to: ' - 'order: 5' - 'delay: 1' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' se_action_retrigger: - 'function: special' - 'value: retrigger:..retrigger' - 'delay: -1' - 'order: 1' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' enter_manuell: se_value_trigger_source: eval:se_eval.get_relative_itemproperty('..manuell', 'path') @@ -1418,6 +1477,12 @@ item_attribute_prefixes: de: 'Definiert um welchen Wert sich das Item mindestens geändert haben muss, um neu gesetzt zu werden' en: 'Definition of a threshold the item has to surpass to be newly set' + se_minagedelta_: + type: foo + description: + de: 'Definiert eine Pause zwischen mehreren Aktionsaufrufen.' + en: 'Definition of pause interval between multiple runs of actions' + se_min_: type: foo description: @@ -1431,6 +1496,7 @@ item_attribute_prefixes: en: 'Condition: The value of the item has to be lower than defined by se_max' se_value_: + type: foo description: de: 'Bedingung: Das Item muss exakt dem durch se_value angegebenem Wert entsprechen' en: 'Condition: The item has to have the exact same value as defined by se_value' @@ -1508,11 +1574,13 @@ item_attribute_prefixes: en: 'Special action like suspend or retrigger' se_set_: + type: foo description: de: 'Setzen eines Items auf einen bestimmten Wert (veraltet - Nutze stattdessen se_action)' en: 'Setting an item to a specific value (deprecated - use se_action instead)' se_force_: + type: foo description: de: 'Setzen eines Items auf einen bestimmten Wert, egal ob sich der Wert geändert hat oder nicht (veraltet - Nutze stattdessen se_action)' en: 'Setting an item to a specific value no matter whether the value has changed or not (deprecated - use se_action instead)' @@ -1542,17 +1610,19 @@ item_attribute_prefixes: en: 'Delaying an action (deprecated - use se_action instead)' se_repeat_: - type: bool + type: foo description: de: 'Definiert, ob eine Aktion beim erneuten Eintritt in den gleichen Status wiederholt werden soll oder nicht (veraltet - Nutze stattdessen se_action)' en: 'Definition wether an action should be repeated or not when reentering the same state (deprecated - use se_action instead)' se_order_: + type: foo description: de: 'Definiert die Reihenfolge einer Aktion als Integerzahl (veraltet - Nutze stattdessen se_action)' en: 'Definition of the running order of an action as integer (deprecated - use se_action instead)' se_manual_: + type: foo description: de: 'Diverse Funktion für den Suspendmodus wie include, exclude, invert, logitem.' en: 'Some functions relevant for the suspend mode like include, exclude, invert, logitem.' diff --git a/stateengine/user_doc/06_aktionen.rst b/stateengine/user_doc/06_aktionen.rst index 339427c70..c278d3752 100755 --- a/stateengine/user_doc/06_aktionen.rst +++ b/stateengine/user_doc/06_aktionen.rst @@ -18,8 +18,9 @@ definiert und benannt wurde. Die Herangehensweise ähnelt also stark der Deklara Zusätzlich zu ``se_item_`` lässt sich über den Eintrag ``se_mindelta_`` definieren, um welchen Wert -sich ein Item mindestens geändert haben muss, um neu gesetzt zu werden. Im unten -stehenden Beispiel wird der Lamellenwert abhängig vom Sonnenstand berechnet. Ohne mindelta +sich ein Item mindestens geändert haben muss, um neu gesetzt zu werden. Diese Konfiguration +kann für einzelne Aktionen individuell über die Angabe ``mindelta`` überschrieben werden. +Im unten stehenden Beispiel wird der Lamellenwert abhängig vom Sonnenstand berechnet. Ohne mindelta 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. @@ -300,6 +301,7 @@ ursprünglichen Zustands (regen) gesetzt werden soll, kann der Parameter ``insta .. code-block:: yaml + 'repeat: /' --> Ergebnis eines Eval-Ausdrucks oder eines Items 'repeat: [True|False]' Über das Attribut wird unabhängig vom globalen Setting für das @@ -393,7 +395,7 @@ regnet hingegen auf den Wert, der in den Settings hinterlegt ist. .. code-block:: yaml - previousconditionset: regex:enter_(.*)_test" + "previousconditionset: regex:enter_(.*)_test" Über das Attribut wird festgelegt, dass die Aktion nur dann ausgeführt werden soll, wenn die vorherige Bedingungsgruppe des aktuellen Zustands mit dem angegebenen Ausdruck übereinstimmt. @@ -403,12 +405,65 @@ Die Abfrage erfolgt dabei nach den gleichen Regeln wie bei ``conditionset`` oben .. code-block:: yaml - previousstate_conditionset: regex:enter_(.*)_test" + "previousstate_conditionset: regex:enter_(.*)_test" Über das Attribut wird festgelegt, dass die Aktion nur dann ausgeführt werden soll, wenn die Bedingungsgruppe, mit der der vorherige Zustand eingenommen wurde, mit dem angegebenen Ausdruck übereinstimmt. Die Abfrage erfolgt dabei ebenfalls nach den gleichen Regeln wie bei ``conditionset`` oben angegeben. +**next_conditionset: ** + +.. code-block:: yaml + + "next_conditionset: regex:enter_(.*)_test" + +Über das Attribut wird festgelegt, dass die Aktion nur dann ausgeführt werden +soll, wenn die Bedingungsgruppe, mit der der zukünftige Zustand eingenommen wird, mit dem angegebenen Ausdruck übereinstimmt. +Die Abfrage erfolgt dabei ebenfalls nach den gleichen Regeln wie bei ``conditionset`` oben angegeben. +Diese Angabe ist primär bei leave_actions sinnvoll. + +**mindelta: ** + +Im folgenden Beispiel wird mindelta für eine einzelne Aktion gesetzt. Anstatt also eine minimale Änderung +für alle Aktionen mit bestimmtem Namen festzulegen, wird eine einzelne Aktion nur dann ausgeführt, +wenn sich der Wert um mindestens den angegeben Wert geändert hat. +Wird 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. + +.. code-block:: yaml + + #items/item.yaml + raffstore1: + automatik: + struct: stateengine.general + 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 + Daemmerung: + <...> + se_action_height: + - 'function: set' + - 'to: value:100' + se_action_lamella: + - 'function: set' + - 'to: value:25' + - 'mindelta: 10' + <...> + +**minagedelta: ** + +.. code-block:: yaml + + minagedelta: 300 + +Über das Attribut wird festgelegt, dass eine Aktion nur in einem vorgegebenen Intervall ausgeführt wird. +Im angegebenen Beispiel wird die Aktion also nur ausgeführt, wenn der letzte Ausführungszeitpunkt mindestens +5 Minuten zurück liegt. So kann verhindert werden, dass die Aktion bei jeder Neuevaluierung des Status ausgeführt wird. +Ein Neustart des Plugins setzt den Counter wieder zurück, der letzte Ausführungszeitpunkt wird also nur bei laufendem +Betrieb gespeichert und überdauert einen Neustart nicht. + + Templates für Aktionen ---------------------- diff --git a/stateengine/user_doc/10_funktionen_variablen.rst b/stateengine/user_doc/10_funktionen_variablen.rst index c1ee17a7b..e84e1f0e7 100755 --- a/stateengine/user_doc/10_funktionen_variablen.rst +++ b/stateengine/user_doc/10_funktionen_variablen.rst @@ -282,13 +282,10 @@ die Situation deutlich vereinfachen würde. **current.conditionset_name:** *Der Name der Bedingungsgruppe, die gerade geprüft wird* -Beide current.conditionset Variablen können ebenso wie die oben erwähnten current.state_id/name -nur während der Prüfung der Bedingungen genutzt werden, nicht jedoch für Aktionen. - Das Beispiel zeigt einen Windzustand. Dieser übernimmt keine Funktionen, sondern dient lediglich der Visualisierung (Sicherheitsrelevante Features sollten unbedingt z.B. über den KNX Bus erfolgen!). Außerdem wird davon -ausgegangen, dass es einen untergeordneten Zustand names x gibt. +ausgegangen, dass es einen untergeordneten Zustand namens x gibt. - enter_normal wird angenommen, sobald das Wind-Item aktiv ist, zuvor aber nicht der x-Zustand aktiv war. @@ -364,6 +361,14 @@ 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* +**next.conditionset_id:** +*Die Id der Bedingungsgruppe, die für einen Zustandswechsel verantwortlich ist* + +**next.conditionset_name:** +*Der Name der Bedingungsgruppe, die für einen Zustandswechsel verantwortlich ist* + +Beide next.conditionset Variablen können sinnvoll nur für Aktionen genutzt werden. + **release.can_be_released_by:** *Die Definitionen, die den aktuellen Zustand generell auflösen könnten* diff --git a/stateengine/user_doc/12_aktioneneinzeln.rst b/stateengine/user_doc/12_aktioneneinzeln.rst index 9363459ae..cb00bf9dc 100755 --- a/stateengine/user_doc/12_aktioneneinzeln.rst +++ b/stateengine/user_doc/12_aktioneneinzeln.rst @@ -150,8 +150,50 @@ Es ist möglich, eine Minimumabweichung für aktuellen Wert des Items und dem ermittelten neuen Wert kleiner ist als die festgelegte Minimumabweichung wird keine Änderung vorgenommen. Die Minimumabweichung wird über das Attribut -``se_mindelta_`` auf der Ebene des Regelwerk-Items -festgelegt. +``se_mindelta_`` auf der Ebene des Regelwerk-Items für alle +Aktionen mit dem entsprechenden Namen festgelegt. Alternativ kann dieses +Attribut aber auch für einzelne Aktionen festgelegt und/oder überschrieben werden. + +Im angeführten Beispiel werden die Lamellen beim Eintritt in einen Zustand und +beim Verlassen des Zustands nur dann gesetzt, wenn sich der Wert der Lamellen inzwischen +um mindestens 10 verändert hat. Wird der Zustand erneut eingenommen (stay), wird der +Wert hingegen mit 5 überschrieben. + +.. code-block:: yaml + + #items/item.yaml + raffstore1: + automatik: + struct: stateengine.general + 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 # Alle Aktionen mit Namen lamella sind betroffen + Daemmerung: + on_enter: + se_set_height: 100 + se_set_lamella: 20 + on_leave: + se_set_height: 100 + se_set_lamella: 15 + on_stay: + se_set_height: 100 + se_set_lamella: 20 + se_set_lamella: 20 + se_mindelta_lamella: 5 + +**minagedelta: ** + +.. code-block:: yaml + + se_minagedelta_: 300 (Sekunden)|30m (Minuten) + +Über das Attribut wird festgelegt, dass eine Aktion nur in einem vorgegebenen Intervall ausgeführt wird. +Im angegebenen Beispiel wird die Aktion also nur ausgeführt, wenn der letzte Ausführungszeitpunkt mindestens +300 Sekunden zurück liegt. So kann verhindert werden, dass die Aktion bei jeder Neuevaluierung des Status ausgeführt wird. +Ein Neustart des Plugins setzt den Counter wieder zurück, der letzte Ausführungszeitpunkt wird also nur bei laufendem +Betrieb gespeichert und überdauert einen Neustart nicht. **delay: ** @@ -167,8 +209,10 @@ 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). Wenn also die Verzögerung größer als der ``cycle`` ist, wird die Aktion -nie durchgeführt werden, es sei denn die Aktion soll nur -einmalig ausgeführt werden. +nicht mehr durchgeführt. +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. **repeat: **