diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 407957ffa..6848f21a5 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -34,9 +34,13 @@ class SeActionBase(StateEngineTools.SeItemChild): def name(self): return self._name + @property + def function(self): + return self._function + @property def action_status(self): - return self.__action_status + return self._action_status # Cast function for delay # value: value to cast @@ -75,42 +79,78 @@ def __init__(self, abitem, name: str): self.__mode = StateEngineValue.SeValue(self._abitem, "mode", True, "str") self.__order = StateEngineValue.SeValue(self._abitem, "order", False, "num") self._scheduler_name = None - self.__function = None + self._function = None self.__template = None - self.__action_status = {} + self._action_status = {} self.__queue = abitem.queue def update_delay(self, value): - self.__delay.set(value) - self.__delay.set_cast(SeActionBase.__cast_delay) + _issue_list = [] + _, _, _issue = self.__delay.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_list.append(_issue) + _issue_list = StateEngineTools.flatten_list(_issue_list) + return _issue_list def update_instanteval(self, value): if self.__instanteval is None: self.__instanteval = StateEngineValue.SeValue(self._abitem, "instanteval", False, "bool") - self.__instanteval.set(value) + _, _, _issue = self.__instanteval.set(value) + _issue = {self._name: {'issue': _issue, 'attribute': 'instanteval', + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + return _issue def update_repeat(self, value): if self.__repeat is None: self.__repeat = StateEngineValue.SeValue(self._abitem, "repeat", False, "bool") - self.__repeat.set(value) + _, _, _issue = self.__repeat.set(value) + _issue = {self._name: {'issue': _issue, 'attribute': 'repeat', + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + return _issue def update_order(self, value): - self.__order.set(value) + _, _, _issue = self.__order.set(value) + _issue = {self._name: {'issue': _issue, 'attribute': 'order', + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + return _issue - def update_conditionsets(self, value): - self.conditionset.set(value) + def update_conditionset(self, value): + _, _, _issue = self.conditionset.set(value) + _issue = {self._name: {'issue': _issue, 'attribute': 'conditionset', + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + return _issue - def update_previousconditionsets(self, value): - self.previousconditionset.set(value) + def update_previousconditionset(self, value): + _, _, _issue = self.previousconditionset.set(value) + _issue = {self._name: {'issue': _issue, 'attribute': 'previousconditionset', + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + return _issue - def update_previousstate_conditionsets(self, value): - self.previousstate_conditionset.set(value) + 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}]}} + return _issue - def update_modes(self, value): - self.__mode.set(value) + 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 def get_order(self): - return self.__order.get(1) + order = self.__order.get(1) + if not isinstance(order, int): + self._log_warning("Order is currently {} but must be an integer. Setting it to 1.", order) + order = 1 + return order def update_webif_actionstatus(self, state, name, success, issue=None): try: @@ -151,7 +191,7 @@ def update_webif_actionstatus(self, state, name, success, issue=None): # Write action to logger def write_to_logger(self): - self._log_debug("name: {}", self._name) + self._log_info("function: {}", self._function) self.__delay.write_to_logger() if self.__repeat is not None: self.__repeat.write_to_logger() @@ -181,6 +221,152 @@ def set_source(self, current_condition, previous_condition, previousstate_condit source = ", ".join(source) return source + # If se_item_ starts with eval: the eval expression is getting evaluated + # check_item: the eval entry as a string + # check_value: current value of an action, will get newly cast based on eval (optional) + # check_mindelta: current mindelta of an action, will get newly cast based on eval (optional) + # returns: evaluated expression + # newly evaluated value + # 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}]}} + if isinstance(check_item, str): + item = None + #self._log_develop("Get item from eval on {} {}", self._function, check_item) + if "stateengine_eval" in check_item or "se_eval" in check_item: + # noinspection PyUnusedLocal + stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem) + try: + item = check_item.replace('sh', 'self._sh') + item = item.replace('shtime', 'self._shtime') + if item.startswith("eval:"): + _text = "If you define an item by se_eval_ you should use a "\ + "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._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}]}} + 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._action_status = _issue + if check_value: + check_value.set_cast(check_item.cast) + if check_mindelta: + check_mindelta.set_cast(check_item.cast) + self._scheduler_name = "{}-SeItemDelayTimer".format(check_item.property.path) + if self._abitem.id == check_item.property.path: + self._caller += '_self' + #self._log_develop("Got item from eval on {} {}", self._function, check_item) + else: + self._log_develop("Got no item from eval on {} with initial item {}", self._function, self.__item) + except Exception as ex: + _issue = {self._name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + # self._action_status = _issue + # 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}]}} + # self._action_status = _issue + # 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}]}} + return check_item, check_value, check_mindelta, _issue + + def check_complete(self, item_state, check_item, check_status, check_mindelta, check_value, action_type, evals_items=None): + _issue = {self._name: {'issue': None, + 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} + self._log_develop("Check item {} status {} value {} evals_items {}", check_item, check_status, check_value, evals_items) + try: + _name = evals_items.get(self.name) + if _name is not None: + _item = _name.get('item') + _eval = str(_name.get('eval')) + _selfitem = check_item if check_item not in (None, "None") else None + _item = _item if _item not in (None, "None") else None + _eval = _eval if _eval not in (None, "None") else None + check_item = _selfitem or _eval + if check_item is None: + _returnitem, _returnissue = self._abitem.return_item(_item) + check_item = _returnitem + else: + _returnissue = None + _issue = {self._name: {'issue': _returnissue, + 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} + self._log_debug("Check item {} status {} value {} _returnissue {}", check_item, check_status, check_value, + _returnissue) + except Exception as ex: + self._log_info("No valid item info for action {}, trying to get differently. Problem: {}", self._name, ex) + # missing item in action: Try to find it. + if check_item is None: + item = StateEngineTools.find_attribute(self._sh, item_state, "se_item_" + self._name) + if item is not None: + check_item, _issue = self._abitem.return_item(item) + _issue = {self._name: {'issue': _issue, + 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} + else: + item = StateEngineTools.find_attribute(self._sh, item_state, "se_eval_" + self._name) + if item is not None: + check_item = str(item) + + if check_item is None and _issue[self._name].get('issue') is None: + _issue = {self._name: {'issue': ['Item not defined in rules section'], + 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} + # missing status in action: Try to find it. + if check_status is None: + status = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self._name) + if status is not None: + check_status, _issue = self._abitem.return_item(status) + _issue = {self._name: {'issue': _issue, + 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} + elif check_status is not None: + check_status = str(status) + + if check_mindelta.is_empty(): + mindelta = StateEngineTools.find_attribute(self._sh, item_state, "se_mindelta_" + self._name) + if mindelta is not None: + check_mindelta.set(mindelta) + + if check_status is not None: + check_value.set_cast(check_status.cast) + check_mindelta.set_cast(check_status.cast) + self._scheduler_name = "{}-SeItemDelayTimer".format(check_status.property.path) + if self._abitem.id == check_status.property.path: + self._caller += '_self' + elif check_status is None: + if isinstance(check_item, str): + pass + elif check_item is not None: + check_value.set_cast(check_item.cast) + check_mindelta.set_cast(check_item.cast) + self._scheduler_name = "{}-SeItemDelayTimer".format(check_item.property.path) + if self._abitem.id == check_item.property.path: + self._caller += '_self' + if _issue[self._name].get('issue') not in [[], [None], None]: + # self._action_status = _issue + self._log_develop("Issue with {} action {}", action_type, _issue) + else: + _issue = {self._name: {'issue': None, + 'issueorigin': [{'state': item_state.property.path, 'action': self._function}]}} + + return check_item, check_status, check_mindelta, check_value, _issue + # Execute action (considering delay, etc) # is_repeat: Indicate if this is a repeated action without changing the state # item_allow_repeat: Is repeating actions generally allowed for the item? @@ -247,7 +433,17 @@ def _update_repeat_webif(value: bool): self._log_decrease_indent(50) self._log_increase_indent() self._log_info("Action '{0}': Preparing", self._name) + self._log_increase_indent() + try: + self._getitem_fromeval() + self._log_decrease_indent() + _validitem = True + except Exception as ex: + _validitem = False + #self._log_develop("action issues {}", self._action_status) + self._log_decrease_indent() if not self._can_execute(state): + self._log_decrease_indent() return conditions_met = 0 condition_necessary = 0 @@ -284,14 +480,6 @@ def _update_repeat_webif(value: bool): else: repeat_text = "" self._log_increase_indent() - try: - self._getitem_fromeval() - self._log_decrease_indent() - _validitem = True - except Exception as ex: - _validitem = False - self._log_error("Action '{0}': Ignored because {1}", self._name, ex) - self._log_decrease_indent() if _validitem: delay = 0 if self.__delay.is_empty() else self.__delay.get() plan_next = self._se_plugin.scheduler_return_next(self._scheduler_name) @@ -412,116 +600,51 @@ def __init__(self, abitem, name: str): self.__delta = 0 self.__value = StateEngineValue.SeValue(self._abitem, "value") self.__mindelta = StateEngineValue.SeValue(self._abitem, "mindelta") - self.__function = "set" + self._function = "set" def __repr__(self): return "SeAction Set {}".format(self._name) def _getitem_fromeval(self): - if isinstance(self.__item, str): - item = None - if "stateengine_eval" in self.__item or "se_eval" in self.__item: - # noinspection PyUnusedLocal - stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem) - try: - item = self.__item.replace('sh', 'self._sh') - item = item.replace('shtime', 'self._shtime') - item = eval(item) - if item is not None: - self.__item, _issue = self._abitem.return_item(item) - _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} - self.__value.set_cast(self.__item.cast) - self.__mindelta.set_cast(self.__item.cast) - self._scheduler_name = "{}-SeItemDelayTimer".format(self.__item.property.path) - if self._abitem.id == self.__item.property.path: - self._caller += '_self' - except Exception as ex: - _issue = {self._name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} - raise Exception("Problem evaluating item '{}' from eval: {}".format(self.__item, ex)) - finally: - self.__action_status = _issue - if item is None: - _issue = {self._name: {'issue': 'Item {} from eval not existing'.format(self.__item), 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} - self.__action_status = _issue - raise Exception("Problem evaluating item '{}' from eval. It does not exist.".format(self.__item)) + if self.__item is None: + return + 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)) # 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': 'set initital'}]}} + _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Complete action # item_state: state item to read from def complete(self, item_state, evals_items=None): - _issue = {self._name: {'issue': None, 'issueorigin': [{'state': item_state.property.path, 'action': 'set initital'}]}} - try: - _name = evals_items.get(self.name) - if _name is not None: - _item = _name.get('item') - _eval = str(_name.get('eval')) - _selfitem = self.__item if self.__item is not None and self.__item != "None" else None - _item = _item if _item is not None and _item != "None" else None - _eval = _eval if _eval is not None and _eval != "None" else None - self.__item = _selfitem or self._abitem.return_item(_item)[0] or _eval - _issue = {self._name: {'issue': self._abitem.return_item(_item)[1], 'issueorigin': [{'state': item_state.property.path, 'action': 'set (first try to get item)'}]}} - except Exception as ex: - self._log_error("No valid item info for action {}, trying to get differently. Problem: {}", self.name, ex) - # missing item in action: Try to find it. - if self.__item is None: - item = StateEngineTools.find_attribute(self._sh, item_state, "se_item_" + self._name) - - if item is not None: - self.__item, _issue = self._abitem.return_item(item) - _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': item_state.property.path, 'action': 'set'}]}} - else: - item = StateEngineTools.find_attribute(self._sh, item_state, "se_eval_" + self._name) - if item is not None: - self.__item = str(item) - - if self.__item is None and _issue[self._name].get('issue') is None: - _issue = {self._name: {'issue': 'Item not defined in rules section', 'issueorigin': [{'state': item_state.property.path, 'action': 'set'}]}} - # missing status in action: Try to find it. - if self.__status is None: - status = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self._name) - - if status is not None: - self.__status, _issue = self._abitem.return_item(status) - _issue = {self._name: {'issue': 'Item not defined in rules section', 'issueorigin': [{'state': item_state.property.path, 'action': 'set'}]}} - - if self.__mindelta.is_empty(): - mindelta = StateEngineTools.find_attribute(self._sh, item_state, "se_mindelta_" + self._name) - if mindelta is not None: - self.__mindelta.set(mindelta) - - if self.__status is not None: - self.__value.set_cast(self.__status.cast) - self.__mindelta.set_cast(self.__status.cast) - self._scheduler_name = "{}-SeItemDelayTimer".format(self.__status.property.path) - if self._abitem.id == self.__status.property.path: - self._caller += '_self' - elif self.__status is None: - if isinstance(self.__item, str): - pass - elif self.__item is not None: - self.__value.set_cast(self.__item.cast) - self.__mindelta.set_cast(self.__item.cast) - self._scheduler_name = "{}-SeItemDelayTimer".format(self.__item.property.path) - if self._abitem.id == self.__item.property.path: - self._caller += '_self' - if _issue[self._name].get('issue') is not None: - self.__action_status = _issue - self._log_develop("Issue with set action {}", _issue) + self.__item, self.__status, self.__mindelta, self.__value, _issue = self.check_complete( + item_state, self.__item, self.__status, self.__mindelta, self.__value, "set", evals_items) + 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): - self._log_debug("item from eval: {0}", self.__item) + 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() @@ -591,22 +714,39 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, item(value, caller=self._caller, source=source) 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 as ex: + item = None try: - _item = str(self.__item.property.path) + val = self.__value.get() + if val is not None: + value = str(val) + else: + value = None except Exception: - _item = str(self.__item) - _mindelta = self.__mindelta.get() - if _mindelta is None: - result = {'function': str(self.__function), 'item': _item, - 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + value = None + mindelta = self.__mindelta.get() + if mindelta is None: + result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, + 'value': value, 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} else: - result = {'function': str(self.__function), 'item': _item, - 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, + 'value': value, 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}, - 'delta': str(self.__delta), 'mindelta': str(_mindelta)} + 'delta': str(self.__delta), 'mindelta': str(mindelta)} return result @@ -618,7 +758,7 @@ class SeActionSetByattr(SeActionBase): def __init__(self, abitem, name: str): super().__init__(abitem, name) self.__byattr = None - self.__function = "set by attribute" + self._function = "set by attribute" def __repr__(self): return "SeAction SetByAttr {}".format(self._name) @@ -628,7 +768,7 @@ def __repr__(self): def update(self, value): self.__byattr = value _issue = {self._name: {'issue': None, 'attribute': self.__byattr, - 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Complete action @@ -636,12 +776,11 @@ def update(self, value): def complete(self, item_state, evals_items=None): self._scheduler_name = "{}-SeByAttrDelayTimer".format(self.__byattr) _issue = {self._name: {'issue': None, 'attribute': self.__byattr, - 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Write action to logger def write_to_logger(self): - self._log_debug("function: {}", self.__function) SeActionBase.write_to_logger(self) if self.__byattr is not None: self._log_debug("set by attribute: {0}", self.__byattr) @@ -658,7 +797,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s item(item.conf[self.__byattr], caller=self._caller, source=source) def get(self): - result = {'function': str(self.__function), 'byattr': str(self.__byattr), + result = {'function': str(self._function), 'byattr': str(self.__byattr), 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} return result @@ -673,7 +812,7 @@ def __init__(self, abitem, name: str): super().__init__(abitem, name) self.__logic = None self.__value = StateEngineValue.SeValue(self._abitem, "value") - self.__function = "trigger" + self._function = "trigger" def __repr__(self): return "SeAction Trigger {}".format(self._name) @@ -686,7 +825,7 @@ 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': 'unknown'}]}} + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Complete action @@ -694,12 +833,11 @@ def update(self, value): def complete(self, item_state, evals_items=None): self._scheduler_name = "{}-SeLogicDelayTimer".format(self.__logic) _issue = {self._name: {'issue': None, 'logic': self.__logic, - 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Write action to logger def write_to_logger(self): - self._log_debug("function: {}", self.__function) SeActionBase.write_to_logger(self) if self.__logic is not None: self._log_debug("trigger logic: {0}", self.__logic) @@ -722,8 +860,16 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._sh.trigger(add_logics, by=self._caller, source=self._name, value=value) def get(self): - result = {'function': str(self.__function), 'logic': str(self.__logic), - 'value': str(self.__value.get()), + 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': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} return result @@ -736,7 +882,7 @@ class SeActionRun(SeActionBase): def __init__(self, abitem, name: str): super().__init__(abitem, name) self.__eval = None - self.__function = "run" + self._function = "run" def __repr__(self): return "SeAction Run {}".format(self._name) @@ -752,7 +898,7 @@ def update(self, value): if func == "eval": self.__eval = value _issue = {self._name: {'issue': None, 'eval': StateEngineTools.get_eval_name(self.__eval), - 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Complete action @@ -760,12 +906,11 @@ def update(self, value): def complete(self, item_state, evals_items=None): self._scheduler_name = "{}-SeRunDelayTimer".format(StateEngineTools.get_eval_name(self.__eval)) _issue = {self._name: {'issue': None, 'eval': StateEngineTools.get_eval_name(self.__eval), - 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Write action to logger def write_to_logger(self): - self._log_debug("function: {}", self.__function) SeActionBase.write_to_logger(self) if self.__eval is not None: self._log_debug("eval: {0}", StateEngineTools.get_eval_name(self.__eval)) @@ -816,7 +961,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._log_error(text.format(actionname, StateEngineTools.get_eval_name(self.__eval), ex)) def get(self): - result = {'function': str(self.__function), 'eval': str(self.__eval), + result = {'function': str(self._function), 'eval': str(self.__eval), 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} return result @@ -830,9 +975,10 @@ class SeActionForceItem(SeActionBase): def __init__(self, abitem, name: str): super().__init__(abitem, name) self.__item = None + self.__status = None self.__value = StateEngineValue.SeValue(self._abitem, "value") self.__mindelta = StateEngineValue.SeValue(self._abitem, "mindelta") - self.__function = "force set" + self._function = "force set" def __repr__(self): return "SeAction Force {}".format(self._name) @@ -841,67 +987,33 @@ def __repr__(self): # 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': 'force initital'}]}} + _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Complete action # item_state: state item to read from def complete(self, item_state, evals_items=None): - _issue = {self._name: {'issue': None, 'issueorigin': [{'state': item_state.property.path, 'action': 'force initital'}]}} - # missing item in action: Try to find it. - if self.__item is None: - item = StateEngineTools.find_attribute(self._sh, item_state, "se_item_" + self._name) - if item is not None: - self.__item, _issue = self._abitem.return_item(item) - _issue = {self._name: {'issue': 'Item not defined in rules section', 'issueorigin': [{'state': item_state.property.path, 'action': 'force'}]}} - else: - item = StateEngineTools.find_attribute(self._sh, item_state, "se_eval_" + self._name) - self.__item = str(item) - - # missing status in action: Try to find it. - if self.__status is None: - status = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self._name) - - if status is not None: - self.__status, _issue = self._abitem.return_item(status) - _issue = {self._name: {'issue': 'Item not defined in rules section', 'issueorigin': [{'state': item_state.property.path, 'action': 'force'}]}} - - if self.__mindelta.is_empty(): - mindelta = StateEngineTools.find_attribute(self._sh, item_state, "se_mindelta_" + self._name) - if mindelta is not None: - self.__mindelta.set(mindelta) - - if self.__item is None and _issue[self._name].get('issue') is None: - _issue = {self._name: {'issue': 'Item not found', 'issueorigin': [{'state': item_state.property.path, 'action': 'force'}]}} - if self.__status is not None: - self.__value.set_cast(self.__status.cast) - self.__mindelta.set_cast(self.__status.cast) - self._scheduler_name = "{}-SeItemDelayTimer".format(self.__status.property.path) - if self._abitem.id == self.__status.property.path: - self._caller += '_self' - elif self.__status is None: - if isinstance(self.__item, str): - pass - elif self.__item is not None: - self.__value.set_cast(self.__item.cast) - self.__mindelta.set_cast(self.__item.cast) - self._scheduler_name = "{}-SeItemDelayTimer".format(self.__item.property.path) - if self._abitem.id == self.__item.property.path: - self._caller += '_self' - if _issue[self._name].get('issue') is not None: - self.__action_status = _issue - self._log_develop("Issue with force action {}", _issue) + self.__item, self.__status, self.__mindelta, self.__value, _issue = self.check_complete( + item_state, self.__item, self.__status, self.__mindelta, self.__value, "force", evals_items) + self._action_status = _issue return _issue # Write action to logger def write_to_logger(self): - self._log_debug("function: {}", self.__function) - self._log_debug("value: {}", self.__value) SeActionBase.write_to_logger(self) if isinstance(self.__item, str): - self._log_debug("item from eval: {0}", self.__item) + 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() @@ -927,32 +1039,13 @@ def _can_execute(self, state): return True def _getitem_fromeval(self): - if isinstance(self.__item, str): - _issue = {self._name: {'issue': None, 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} - if "stateengine_eval" in self.__item or "se_eval" in self.__item: - # noinspection PyUnusedLocal - stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem) - try: - item = self.__item.replace('sh', 'self._sh') - item = item.replace('shtime', 'self._shtime') - item = eval(item) - if item is not None: - self.__item, _issue = self._abitem.return_item(item) - _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} - self.__value.set_cast(self.__item.cast) - self.__mindelta.set_cast(self.__item.cast) - self._scheduler_name = "{}-SeItemDelayTimer".format(self.__item.property.path) - if self._abitem.id == self.__item.property.path: - self._caller += '_self' - else: - self._log_error("Problem evaluating item '{}' from eval. It is None.", item) - - except Exception as ex: - _issue = {self._name: {'issue': 'Problem evaluating item {} from eval'.format(self.__item), 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} - self._log_error("Problem evaluating item '{}' from eval: {}.", self.__item, ex) - finally: - self.__action_status = _issue - return _issue + if self.__item is None: + return + 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 @@ -1008,11 +1101,28 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self.__item(value, caller=self._caller, source=source) 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: - item = str(self.__item.property.path) + 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: - item = str(self.__item) - result = {'function': str(self.__function), 'item': item, 'value': str(self.__value.get()), + value = None + result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, 'value': value, 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} return result @@ -1027,7 +1137,7 @@ def __init__(self, abitem, name: str): super().__init__(abitem, name) self.__special = None self.__value = None - self.__function = "special" + self._function = "special" def __repr__(self): return "SeAction Special {}".format(self._name) @@ -1043,7 +1153,7 @@ def update(self, value): else: raise ValueError("Action {0}: Unknown special value '{1}'!".format(self._name, special)) self.__special = special - _issue = {self._name: {'issue': None, 'special': self.__value, 'issueorigin': [{'state': 'unknown', 'action': 'special'}]}} + _issue = {self._name: {'issue': None, 'special': self.__value, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Complete action @@ -1054,12 +1164,11 @@ def complete(self, item_state, evals_items=None): else: item = self.__value.property.path self._scheduler_name = "{}_{}-SeSpecialDelayTimer".format(self.__special, item) - _issue = {self._name: {'issue': None, 'special': item, 'issueorigin': [{'state': 'unknown', 'action': 'special'}]}} + _issue = {self._name: {'issue': None, 'special': item, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue # Write action to logger def write_to_logger(self): - self._log_debug("function: {}", self.__function) SeActionBase.write_to_logger(self) self._log_debug("Special Action: {0}", self.__special) if isinstance(self.__value, list): @@ -1093,48 +1202,48 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._log_debug("Special action {0}: done", self.__special) def suspend_get_value(self, value): - _issue = {self._name: {'issue': None, 'issueorigin': [{'state': 'suspend', 'action': 'suspend initital'}]}} + _issue = {self._name: {'issue': None, 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} if value is None: _issue = {self._name: {'issue': 'Special action suspend requires arguments', 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} - self.__action_status = _issue + self._action_status = _issue raise ValueError("Action {0}: Special action 'suspend' requires arguments!".format(self._name)) suspend, manual = StateEngineTools.partition_strip(value, ",") if suspend is None or manual is None: _issue = {self._name: {'issue': 'Special action suspend requires two arguments', 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} - self.__action_status = _issue + self._action_status = _issue raise ValueError("Action {0}: Special action 'suspend' requires two arguments (separated by a comma)!".format(self._name)) suspend_item, _issue = self._abitem.return_item(suspend) _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} if suspend_item is None: _issue = {self._name: {'issue': 'Suspend item not found', 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} - self.__action_status = _issue + self._action_status = _issue raise ValueError("Action {0}: Suspend item '{1}' not found!".format(self._name, suspend)) manual_item, _issue = self._abitem.return_item(manual) _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} if manual_item is None: _issue = {self._name: {'issue': 'Manual item {} not found'.format(manual), 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} - self.__action_status = _issue + self._action_status = _issue raise ValueError("Action {0}: Manual item '{1}' not found!".format(self._name, manual)) - self.__action_status = _issue + self._action_status = _issue return [suspend_item, manual_item.property.path] def retrigger_get_value(self, value): if value is None: _issue = {self._name: {'issue': 'Special action retrigger requires item', 'issueorigin': [{'state': 'retrigger', 'action': 'retrigger'}]}} - self.__action_status = _issue + self._action_status = _issue raise ValueError("Action {0}: Special action 'retrigger' requires item".format(self._name)) se_item, __ = StateEngineTools.partition_strip(value, ",") se_item, _issue = self._abitem.return_item(se_item) _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'retrigger', 'action': 'retrigger'}]}} - self.__action_status = _issue + self._action_status = _issue if se_item is None: _issue = {self._name: {'issue': 'Retrigger item {} not found'.format(se_item), 'issueorigin': [{'state': 'retrigger', 'action': 'retrigger'}]}} - self.__action_status = _issue + self._action_status = _issue raise ValueError("Action {0}: Retrigger item '{1}' not found!".format(self._name, se_item)) return se_item @@ -1158,7 +1267,7 @@ def suspend_execute(self, state=None, current_condition=None, previous_condition suspend_remaining = int(suspend_time - suspend_over + 0.5) # adding 0.5 causes round up ... self._abitem.set_variable("item.suspend_remaining", suspend_remaining) self._log_debug("Updated variable 'item.suspend_remaining' to {0}", suspend_remaining) - self.__action_status = _issue + self._action_status = _issue def get(self): try: @@ -1171,7 +1280,7 @@ def get(self): value_result[i] = val.property.path except Exception: pass - result = {'function': str(self.__function), 'special': str(self.__special), + result = {'function': str(self._function), 'special': str(self.__special), 'value': str(value_result), 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} @@ -1185,13 +1294,12 @@ class SeActionAddItem(SeActionSetItem): # name: Name of action def __init__(self, abitem, name: str): super().__init__(abitem, name) - self.__function = "add to list" + self._function = "add to list" def __repr__(self): return "SeAction Add {}".format(self._name) def write_to_logger(self): - self._log_debug("function: {}", self.__function) SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) @@ -1206,11 +1314,22 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, def get(self): try: - item = str(self.__item.property.path) + 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: - item = str(self.__item) - result = {'function': str(self.__function), 'item': item, - 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + value = None + result = {'function': str(self._function), 'item': item, + 'value': value, 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} return result @@ -1223,13 +1342,12 @@ class SeActionRemoveFirstItem(SeActionSetItem): # name: Name of action def __init__(self, abitem, name: str): super().__init__(abitem, name) - self.__function = "remove first from list" + self._function = "remove first from list" def __repr__(self): return "SeAction RemoveFirst {}".format(self._name) def write_to_logger(self): - self._log_debug("function: {}", self.__function) SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) @@ -1250,11 +1368,22 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, def get(self): try: - item = str(self.__item.property.path) + 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: - item = str(self.__item) - result = {'function': str(self.__function), 'item': item, - 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + value = None + result = {'function': str(self._function), 'item': item, + 'value': value, 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} return result @@ -1267,13 +1396,12 @@ class SeActionRemoveLastItem(SeActionSetItem): # name: Name of action def __init__(self, abitem, name: str): super().__init__(abitem, name) - self.__function = "remove last from list" + self._function = "remove last from list" def __repr__(self): return "SeAction RemoveLast {}".format(self._name) def write_to_logger(self): - self._log_debug("function: {}", self.__function) SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) @@ -1296,11 +1424,22 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, def get(self): try: - item = str(self.__item.property.path) + 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: - item = str(self.__item) - result = {'function': str(self.__function), 'item': item, - 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + value = None + result = {'function': str(self._function), 'item': item, + 'value': value, 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} return result @@ -1313,13 +1452,12 @@ class SeActionRemoveAllItem(SeActionSetItem): # name: Name of action def __init__(self, abitem, name: str): super().__init__(abitem, name) - self.__function = "remove all from list" + self._function = "remove all from list" def __repr__(self): return "SeAction RemoveAll {}".format(self._name) def write_to_logger(self): - self._log_debug("function: {}", self.__function) SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) @@ -1340,11 +1478,22 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, def get(self): try: - item = str(self.__item.property.path) + 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: - item = str(self.__item) - result = {'function': str(self.__function), 'item': item, - 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + value = None + result = {'function': str(self._function), 'item': item, + 'value': value, 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} return result diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 5e134b467..6aa9976f2 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -83,85 +83,155 @@ def update(self, attribute, value): # If we do not have the action yet (delay-attribute before action-attribute), ... self.__unassigned_delays[name] = value else: - self.__actions[name].update_delay(value) - return + _issue = self.__actions[name].update_delay(value) + return _count, _issue elif func == "se_instanteval": # set instant calculation if name not in self.__actions: # If we do not have the action yet (repeat-attribute before action-attribute), ... self.__unassigned_instantevals[name] = value else: - self.__actions[name].update_instanteval(value) - return + _issue = self.__actions[name].update_instanteval(value) + return _count, _issue elif func == "se_repeat": # set repeat if name not in self.__actions: # If we do not have the action yet (repeat-attribute before action-attribute), ... self.__unassigned_repeats[name] = value else: - self.__actions[name].update_repeat(value) - return + _issue = self.__actions[name].update_repeat(value) + return _count, _issue elif func == "se_conditionset": # set conditionset if name not in self.__actions: # If we do not have the action yet (conditionset-attribute before action-attribute), ... self.__unassigned_conditionsets[name] = value else: - self.__actions[name].update_conditionsets(value) - return + _issue = self.__actions[name].update_conditionset(value) + return _count, _issue elif func == "se_previousconditionset": # set conditionset if name not in self.__actions: # If we do not have the action yet (conditionset-attribute before action-attribute), ... self.__unassigned_previousconditionsets[name] = value else: - self.__actions[name].update_previousconditionsets(value) - return + _issue = self.__actions[name].update_previousconditionset(value) + return _count, _issue elif func == "se_previousstate_conditionset": # set conditionset if name not in self.__actions: # If we do not have the action yet (conditionset-attribute before action-attribute), ... self.__unassigned_previousstate_conditionsets[name] = value else: - self.__actions[name].update_previousstate_conditionsets(value) - return + _issue = self.__actions[name].update_previousstate_conditionset(value) + return _count, _issue elif func == "se_mode": # set remove mode + _issue_list = [] if name not in self.__actions: # If we do not have the action yet (mode-attribute before action-attribute), ... self.__unassigned_modes[name] = value else: - self.__actions[name].update_modes(value) - return + _val, _issue = self.__actions[name].update_mode(value) + if _issue: + _issue_list.append(_issue) + _issue, _action = self.__check_mode_setting(name, _val, self.__actions[name].function, self.__actions[name]) + if _issue: + _issue_list.append(_issue) + if _action: + self.__actions[name] = _action + return _count, _issue_list elif func == "se_order": # set order if name not in self.__actions: # If we do not have the action yet (order-attribute before action-attribute), ... self.__unassigned_orders[name] = value else: - self.__actions[name].update_order(value) - return + _issue = self.__actions[name].update_order(value) + return _count, _issue elif func == "se_action": # and name not in self.__actions: _issue = self.__handle_combined_action_attribute(name, value) _count += 1 - elif self.__ensure_action_exists(func, name): - # update action - _issue = self.__actions[name].update(value) - _count += 1 + else: + _issue_list = [] + _ensure_action, _issue = self.__ensure_action_exists(func, name) + if _issue: + _issue_list.append(_issue) + if _ensure_action: + # update action + _issue = self.__actions[name].update(value) + if _issue: + _issue_list.append(_issue) + _count += 1 + _issue = StateEngineTools.flatten_list(_issue_list) except ValueError as ex: if name in self.__actions: del self.__actions[name] - _issue = {name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + _issue = {name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': self.__actions[name].function}]}} self._log_warning("Ignoring action {0} because: {1}", attribute, ex) return _count, _issue + def __check_force_setting(self, name, value, function): + _issue = None + _returnfunction = function + if value is not None: + # Parameter force is supported only for type "set" and type "force" + if function not in ["set", "force"]: + _issue = { + name: {'issue': ['Parameter force not supported for this function'], + 'attribute': 'force', 'issueorigin': [{'state': 'unknown', 'action': function}]}} + _issue = "Parameter 'force' not supported for this function" + self._log_warning("Attribute 'se_action_{0}': Parameter 'force' not supported " + "for function '{1}'", name, function) + elif value and function == "set": + # Convert type "set" with force=True to type "force" + self._log_info("Attribute 'se_action_{0}': Parameter 'function' changed from 'set' to 'force', " + "because parameter 'force' is 'True'!", name) + _returnfunction = "force" + elif not value and function == "force": + # Convert type "force" with force=False to type "set" + self._log_info("Attribute 'se_action_{0}': Parameter 'function' changed from 'force' to 'set', " + "because parameter 'force' is 'False'!", name) + _returnfunction = "set" + return _issue, _returnfunction + def __check_mode_setting(self, name, value, function, action): + if value is not None: + possible_mode_list = ['first', 'last', 'all'] + _issue = None + # Parameter mode is supported only for type "remove" + if not "remove" in function: + _issue = {name: {'issue': ['Parameter mode not supported for this function'], 'attribute': 'mode', + 'issueorigin': [{'state': 'unknown', 'action': function}]}} + self._log_warning("Attribute 'se_action_{0}': Parameter 'mode' not supported for function '{1}'", + name, function) + elif function in ["remove", "remove all from list"]: + # Convert type "remove" with mode to specific remove type + if value in possible_mode_list: + if value == "all": + action = StateEngineAction.SeActionRemoveAllItem(self._abitem, name) + elif value == "first": + action = StateEngineAction.SeActionRemoveFirstItem(self._abitem, name) + elif value == "last": + action = StateEngineAction.SeActionRemoveLastItem(self._abitem, name) + self._log_info("Attribute 'se_action_{0}': Function 'remove' changed to '{1}'", name, value) + else: + _issue = { + name: {'issue': ['Parameter {} not allowed for mode!'.format(value)], 'attribute': 'mode', + 'issueorigin': [{'state': 'unknown', 'action': function}]}} + self._log_warning( + "Attribute 'se_action_{0}': Parameter '{1}' for 'mode' is wrong - can only be {2}", + name, value, possible_mode_list) + return _issue, action + return None, None + # ensure that action exists and create if missing # func: action function # name: action name def __ensure_action_exists(self, func, name): # Check if action exists + _issue = None if name in self.__actions: - return True + return True, _issue # Create action depending on function if func == "se_set": @@ -185,41 +255,61 @@ def __ensure_action_exists(self, func, name): elif func == "se_removelast": action = StateEngineAction.SeActionRemoveLastItem(self._abitem, name) else: - return False + return False, _issue + _issue_list = [] if name in self.__unassigned_delays: - action.update_delay(self.__unassigned_delays[name]) + _issue = action.update_delay(self.__unassigned_delays[name]) + if _issue: + _issue_list.append(_issue) del self.__unassigned_delays[name] if name in self.__unassigned_instantevals: - action.update_instanteval(self.__unassigned_instantevals[name]) + _issue = action.update_instanteval(self.__unassigned_instantevals[name]) + if _issue: + _issue_list.append(_issue) del self.__unassigned_instantevals[name] if name in self.__unassigned_repeats: - action.update_repeat(self.__unassigned_repeats[name]) + _issue = action.update_repeat(self.__unassigned_repeats[name]) + if _issue: + _issue_list.append(_issue) del self.__unassigned_repeats[name] if name in self.__unassigned_modes: - action.update_modes(self.__unassigned_modes[name]) + _val, _issue = action.update_mode(self.__unassigned_modes[name]) + if _issue: + _issue_list.append(_issue) + _issue, action = self.__check_mode_setting(name, _val, func.replace("se_", ""), action) + if _issue: + _issue_list.append(_issue) del self.__unassigned_modes[name] if name in self.__unassigned_orders: - action.update_order(self.__unassigned_orders[name]) + _issue = action.update_order(self.__unassigned_orders[name]) + if _issue: + _issue_list.append(_issue) del self.__unassigned_orders[name] if name in self.__unassigned_conditionsets: - action.update_conditionsets(self.__unassigned_conditionsets[name]) + _issue = action.update_conditionset(self.__unassigned_conditionsets[name]) + if _issue: + _issue_list.append(_issue) del self.__unassigned_conditionsets[name] if name in self.__unassigned_previousconditionsets: - action.update_previousconditionsets(self.__unassigned_previousconditionsets[name]) + _issue = action.update_previousconditionset(self.__unassigned_previousconditionsets[name]) + if _issue: + _issue_list.append(_issue) del self.__unassigned_previousconditionsets[name] if name in self.__unassigned_previousstate_conditionsets: - action.update_previousconditionsets(self.__unassigned_previousstate_conditionsets[name]) + _issue = action.update_previousconditionset(self.__unassigned_previousstate_conditionsets[name]) + if _issue: + _issue_list.append(_issue) del self.__unassigned_previousstate_conditionsets[name] self.__actions[name] = action - return True + return True, _issue_list def __handle_combined_action_attribute(self, name, value_list): # value_list needs to be string or list @@ -245,7 +335,7 @@ def __handle_combined_action_attribute(self, name, value_list): else: parameter[key] = val parameter['action'] = name - + _issue_list = [] # function given and valid? if parameter['function'] is None: raise ValueError("Attribute 'se_action_{0}: Parameter 'function' must be set!".format(name)) @@ -254,65 +344,55 @@ def __handle_combined_action_attribute(self, name, value_list): raise ValueError("Attribute 'se_action_{0}: Invalid value '{1}' for parameter " "'function'!".format(name, parameter['function'])) - # handle force - if parameter['force'] is not None: - # Parameter force is supported only for type "set" and type "force" - if parameter['function'] != "set" and parameter['function'] != "force": - self._log_warning("Attribute 'se_action_{0}': Parameter 'force' not supported " - "for function '{1}'", name, parameter['function']) - elif parameter['force'] and parameter['function'] == "set": - # Convert type "set" with force=True to type "force" - self._log_info("Attribute 'se_action_{0}': Parameter 'function' changed from 'set' to 'force', " - "because parameter 'force' is 'True'!", name) - parameter['function'] = "force" - elif not parameter['force'] and parameter['function'] == "force": - # Convert type "force" with force=False to type "set" - self._log_info("Attribute 'se_action_{0}': Parameter 'function' changed from 'force' to 'set', " - "because parameter 'force' is 'False'!", name) - parameter['function'] = "set" - - possible_mode_list = ['first', 'last', 'all'] - if parameter['mode'] is not None: - # Parameter mode is supported only for type "remove" - if parameter['function'] != "remove": - self._log_warning("Attribute 'se_action_{0}': Parameter 'mode' not supported for function '{1}'", - name, parameter['function']) - elif parameter['mode'] and parameter['function'] == "remove": - # Convert type "remove" with mode to specific remove type - if parameter['mode'] in possible_mode_list: - parameter['function'] = "remove{}".format(parameter['mode']) - self._log_info("Attribute 'se_action_{0}': Function 'remove' changed to '{1}'", name, parameter['function']) - else: - parameter['function'] = "remove" - self._log_info("Attribute 'se_action_{0}': Parameter '{1}' for 'mode' is wrong - can only be {2}", - name, parameter['mode'], possible_mode_list) - + _issue = None + _issue, parameter['function'] = self.__check_force_setting(name, parameter['force'], parameter['function']) + if _issue: + _issue_list.append(_issue) + _issue, _action = self.__check_mode_setting(name, parameter['mode'], parameter['function'], parameter['action']) + if _issue: + _issue_list.append(_issue) + if _action: + self.__actions[name] = _action # create action based on function exists = False - _issue = None try: if parameter['function'] == "set": - if self.__ensure_action_exists("se_set", name): + _action_exists, _issue = self.__ensure_action_exists("se_set", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'to') self.__actions[name].update(parameter['to']) exists = True elif parameter['function'] == "force": - if self.__ensure_action_exists("se_force", name): + _action_exists, _issue = self.__ensure_action_exists("se_force", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'to') self.__actions[name].update(parameter['to']) exists = True elif parameter['function'] == "run": - if self.__ensure_action_exists("se_run", name): + _action_exists, _issue = self.__ensure_action_exists("se_run", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'eval') self.__actions[name].update(parameter['eval']) exists = True elif parameter['function'] == "byattr": - if self.__ensure_action_exists("se_byattr", name): + _action_exists, _issue = self.__ensure_action_exists("se_byattr", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'attribute') self.__actions[name].update(parameter['attribute']) exists = True elif parameter['function'] == "trigger": - if self.__ensure_action_exists("se_trigger", name): + _action_exists, _issue = self.__ensure_action_exists("se_trigger", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'logic') if 'value' in parameter and parameter['value'] is not None: self.__actions[name].update(parameter['logic'] + ':' + parameter['value']) @@ -320,62 +400,102 @@ def __handle_combined_action_attribute(self, name, value_list): self.__actions[name].update(parameter['logic']) exists = True elif parameter['function'] == "special": - if self.__ensure_action_exists("se_special", name): + _action_exists, _issue = self.__ensure_action_exists("se_special", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') self.__actions[name].update(parameter['value']) exists = True elif parameter['function'] == "add": - if self.__ensure_action_exists("se_add", name): + _action_exists, _issue = self.__ensure_action_exists("se_add", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') self.__actions[name].update(parameter['value']) exists = True elif parameter['function'] == "remove": - if self.__ensure_action_exists("se_remove", name): + _action_exists, _issue = self.__ensure_action_exists("se_remove", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') self.__actions[name].update(parameter['value']) exists = True elif parameter['function'] == "removeall": - if self.__ensure_action_exists("se_removeall", name): + _action_exists, _issue = self.__ensure_action_exists("se_removeall", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') self.__actions[name].update(parameter['value']) exists = True elif parameter['function'] == "removefirst": - if self.__ensure_action_exists("se_removefirst", name): + _action_exists, _issue = self.__ensure_action_exists("se_removefirst", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') self.__actions[name].update(parameter['value']) exists = True elif parameter['function'] == "removelast": - if self.__ensure_action_exists("se_removelast", name): + _action_exists, _issue = self.__ensure_action_exists("se_removelast", name) + if _issue: + _issue_list.append(_issue) + if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') self.__actions[name].update(parameter['value']) exists = True + except ValueError as ex: exists = False if name in self.__actions: del self.__actions[name] - _issue = {name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': 'unknown'}]}} + _issue = {name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': parameter['function']}]}} + _issue_list.append(_issue) self._log_warning("Ignoring action {0} because: {1}", name, ex) # add additional parameters if exists: if parameter['instanteval'] is not None: - self.__actions[name].update_instanteval(parameter['instanteval']) + _issue = self.__actions[name].update_instanteval(parameter['instanteval']) + if _issue: + _issue_list.append(_issue) if parameter['repeat'] is not None: - self.__actions[name].update_repeat(parameter['repeat']) + _issue = self.__actions[name].update_repeat(parameter['repeat']) + if _issue: + _issue_list.append(_issue) if parameter['delay'] != 0: - self.__actions[name].update_delay(parameter['delay']) + _issue = self.__actions[name].update_delay(parameter['delay']) + if _issue: + _issue_list.append(_issue) if parameter['order'] is not None: - self.__actions[name].update_order(parameter['order']) + _issue = self.__actions[name].update_order(parameter['order']) + if _issue: + _issue_list.append(_issue) if parameter['conditionset'] is not None: - self.__actions[name].update_conditionsets(parameter['conditionset']) + _issue = self.__actions[name].update_conditionset(parameter['conditionset']) + if _issue: + _issue_list.append(_issue) if parameter['previousconditionset'] is not None: - self.__actions[name].update_previousconditionsets(parameter['previousconditionset']) + _issue = self.__actions[name].update_previousconditionset(parameter['previousconditionset']) + if _issue: + _issue_list.append(_issue) if parameter['previousstate_conditionset'] is not None: - self.__actions[name].update_previousstate_conditionsets(parameter['previousstate_conditionset']) + _issue = self.__actions[name].update_previousstate_conditionset(parameter['previousstate_conditionset']) + if _issue: + _issue_list.append(_issue) if parameter['mode'] is not None: - self.__actions[name].update_modes(parameter['mode']) - - return _issue + _val, _issue = self.__actions[name].update_mode(parameter['mode']) + if _issue: + _issue_list.append(_issue) + _issue, _action = self.__check_mode_setting(name, _val, parameter['function'], self.__actions[name]) + if _issue: + _issue_list.append(_issue) + if _action: + self.__actions[name] = _action + return _issue_list # noinspection PyMethodMayBeStatic def __raise_missing_parameter_error(self, parameter, param_name): @@ -410,9 +530,11 @@ def set(self, value): def execute(self, is_repeat: bool, allow_item_repeat: bool, state, additional_actions=None): actions = [] for name in self.__actions: + self._log_develop("Append action {}", self.__actions[name]) actions.append((self.__actions[name].get_order(), self.__actions[name])) if additional_actions is not None: for name in additional_actions.__actions: + self._log_develop("Append additional action {}", additional_actions.__actions[name]) actions.append((additional_actions.__actions[name].get_order(), additional_actions.__actions[name])) for order, action in sorted(actions, key=lambda x: x[0]): self.__queue.put([action, is_repeat, allow_item_repeat, state]) diff --git a/stateengine/StateEngineCondition.py b/stateengine/StateEngineCondition.py index e15d2c935..ce3dd0e75 100755 --- a/stateengine/StateEngineCondition.py +++ b/stateengine/StateEngineCondition.py @@ -44,6 +44,7 @@ def __init__(self, abitem, name: str): self.__item = None self.__status = None self.__eval = None + self.__status_eval = None self.__value = StateEngineValue.SeValue(self._abitem, "value", True) self.__min = StateEngineValue.SeValue(self._abitem, "min") self.__max = StateEngineValue.SeValue(self._abitem, "max") @@ -61,36 +62,94 @@ def __init__(self, abitem, name: str): self.__itemClass = Item def __repr__(self): - return "SeCondition 'item': {}, 'status': {}, 'eval': {}, 'value': {}".format(self.__item, self.__status, self.__eval, self.__value) + return "SeCondition 'item': {}, 'status': {}, 'eval': {}, " \ + "'status_eval': {}, 'value': {}".format(self.__item, self.__status, self.__eval, self.__status_eval, self.__value) + + def check_items(self, check, value=None, item_state=None): + item_issue, status_issue, eval_issue, status_eval_issue = None, None, None, None + item_value, status_value, eval_value, status_eval_value = None, None, None, None + if check == "se_item" or (check == "attribute" and self.__item is None and self.__eval is None): + if value is None: + value = StateEngineTools.find_attribute(self._sh, item_state, "se_item_" + self.__name) + if value is not None: + match = re.match(r'^(.*):', value) + if isinstance(value, str) and value.startswith("eval:"): + _, _, value = value.partition(":") + self.__eval = value + self.__item = None + elif match: + self._log_warning("Your item configuration '{0}' is wrong! Define a plain (relative) " + "item without {1} at the beginning!", value, match.group(1)) + item_issue = "Your eval configuration '{0}' is wrong, remove {1}".format(value, match.group(1)) + self.__item = None + else: + value, issue = self._abitem.return_item(value) + self.__item = value + item_value = value + if check == "se_status" or (check == "attribute" and self.__status is None and self.__status_eval is None): + if value is None: + value = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self.__name) + if value is not None: + match = re.match(r'^(.*):', value) + if isinstance(value, str) and value.startswith("eval:"): + _, _, value = value.partition(":") + self.__status_eval = value + self.__status = None + elif match: + self._log_warning("Your item configuration '{0}' is wrong! Define a plain (relative) " + "item without {1} at the beginning!", value, match.group(1)) + status_issue = "Your eval configuration '{0}' is wrong, remove {1}".format(value, match.group(1)) + self.__status = None + value = None + else: + value, issue = self._abitem.return_item(value) + self.__status = value + status_value = value + if check == "se_eval" or (check == "attribute" and self.__eval is None): + if value is None: + value = StateEngineTools.find_attribute(self._sh, item_state, "se_eval_" + self.__name) + if value is not None: + match = re.match(r'^(.*):', value) + if value.startswith("eval:"): + _, _, value = value.partition("eval:") + elif match: + self._log_warning("Your eval configuration '{0}' is wrong! You have to define " + "a plain eval expression", value) + eval_issue = "Your eval configuration '{0}' is wrong!".format(value) + value = None + self.__eval = value + eval_value = value + if check == "se_status_eval" or (check == "attribute" and self.__status_eval is None): + if value is None: + value = StateEngineTools.find_attribute(self._sh, item_state, "se_status_eval_" + self.__name) + if value is not None: + match = re.match(r'^(.*):', value) + if value.startswith("eval:"): + _, _, value = value.partition("eval:") + elif match: + self._log_warning("Your status eval configuration '{0}' is wrong! You have to define " + "a plain eval expression", value) + status_eval_issue = "Your status eval configuration '{0}' is wrong!".format(value) + value = None + self.__status_eval = value + status_eval_value = value + return item_value, status_value, eval_value, status_eval_value, item_issue, status_issue, eval_issue, status_eval_issue # set a certain function to a given value - # func: Function to set ('item', 'eval', 'value', 'min', 'max', 'negate', 'changedby', 'updatedby', + # func: Function to set ('item', 'eval', 'status_eval', 'value', 'min', 'max', 'negate', 'changedby', 'updatedby', # 'triggeredby','changedbynegate', 'updatedbynegate', 'triggeredbynegate','agemin', 'agemax' or 'agenegate') # value: Value for function def set(self, func, value): issue = None if func == "se_item": - if ":" in value: - self._log_warning("Your item configuration '{0}' is wrong! Define a plain (relative) " - "item without item: at the beginning!", value) - _, _, value = value.partition(":") - self.__item, issue = self._abitem.return_item(value) + value, _, _, _, issue, _, _, _ = self.check_items("se_item", value) elif func == "se_status": - if ":" in value: - self._log_warning("Your status configuration '{0}' is wrong! Define a plain (relative) " - "item without item: at the beginning!", value) - _, _, value = value.partition(":") - self.__status, issue = self._abitem.return_item(value) + _, value, _, _, _, issue, _, _ = self.check_items("se_status", value) elif func == "se_eval": - if value.startswith("eval:"): - _, _, value = value.partition("eval:") - wrong_start = ["item:", "regex:", "value:", "var:"] - if any(value.startswith(wrong_start) for wrong_start in wrong_start): - self._log_warning("Your eval configuration '{0}' is wrong! You have to define " - "a plain eval expression", value) - issue = "Your eval configuration '{0}' is wrong!".format(value) - value = None - self.__eval = value + _, _, value, _, _, _, issue, _ = self.check_items("se_eval", value) + elif func == "se_status_eval": + _, _, _, value, _, _, _, issue = self.check_items("se_status_eval", value) + if func == "se_value": self.__value.set(value, self.__name) elif func == "se_min": @@ -117,18 +176,23 @@ def set(self, func, value): self.__negate = value elif func == "se_agenegate": self.__agenegate = value - elif func != "se_item" and func != "se_eval" and func != "se_status": + elif func != "se_item" and func != "se_eval" and func != "se_status_eval" and func != "se_status": self._log_warning("Function '{0}' is no valid function! Please check item attribute.", func) issue = "Function '{0}' is no valid function!".format(func) return issue def get(self): _eval_result = str(self.__eval) + _status_eval_result = str(self.__status_eval) if 'SeItem' in _eval_result: _eval_result = _eval_result.split('SeItem.')[1].split(' ')[0] if 'SeCurrent' in _eval_result: _eval_result = _eval_result.split('SeCurrent.')[1].split(' ')[0] - _value_result = str(self.__value.get_for_webif()) + if 'SeItem' in _status_eval_result: + _status_eval_result = _status_eval_result.split('SeItem.')[1].split(' ')[0] + if 'SeCurrent' in _status_eval_result: + _status_eval_result = _status_eval_result.split('SeCurrent.')[1].split(' ')[0] + _value_result = self.__value.get_for_webif() try: _item = self.__item.property.path except Exception: @@ -137,7 +201,8 @@ def get(self): _status = self.__status.property.path except Exception: _status = self.__status - result = {'item': _item, 'status': _status, 'eval': _eval_result, 'value': _value_result, + result = {'item': _item, 'status': _status, 'eval': _eval_result, 'status_eval': _status_eval_result, + 'value': _value_result, 'min': str(self.__min), 'max': str(self.__max), 'agemin': str(self.__agemin), 'agemax': str(self.__agemax), 'negate': str(self.__negate), 'agenegate': str(self.__agenegate), @@ -159,7 +224,7 @@ def complete(self, item_state): return False # set 'eval' for some known conditions if item and eval are not set, yet - if self.__item is None and self.__status is None and self.__eval is None: + if all(item is None for item in [self.__item, self.__status, self.__eval, self.__status_eval]): if self.__name == "weekday": self.__eval = StateEngineCurrent.values.get_weekday elif self.__name == "sun_azimut": @@ -211,35 +276,19 @@ def complete(self, item_state): elif self.__name == "original_source": self.__eval = self._abitem.get_update_original_source - # missing item in condition: Try to find it - if self.__item is None: - result = StateEngineTools.find_attribute(self._sh, item_state, "se_item_" + self.__name) - if result is not None: - self.__item, issue = self._abitem.return_item(result) - - # missing status in condition: Try to find it - if self.__status is None: - result = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self.__name) - if result is not None: - self.__status, issue = self._abitem.return_item(result) - - # missing eval in condition: Try to find it - if self.__eval is None: - result = StateEngineTools.find_attribute(self._sh, item_state, "se_eval_" + self.__name) - if result is not None: - self.__eval = result - - # now we should have either 'item' or 'eval' set. If not, raise ValueError - if self.__item is None and self.__status is None and self.__eval is None: - raise ValueError("Neither 'item' nor 'status' nor 'eval' given!") - - if (self.__item is not None or self.__status is not None or self.__eval is not None)\ + self.check_items("attribute", None, item_state) + + # now we should have either 'item' or '(status)eval' set. If not, raise ValueError + if all(item is None for item in [self.__item, self.__status, self.__eval, self.__status_eval]): + 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: self.__changedbynegate = False - if (self.__item is not None or self.__status is not None or self.__eval is not 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 (self.__item is not None or self.__status is not None or self.__eval is not 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 @@ -261,7 +310,7 @@ def complete(self, item_state): elif self.__name == "time": self.__cast_all(StateEngineTools.cast_time) except Exception as ex: - raise ValueError("Condition {0}: Error when casting: {1}".format(self.__name, ex)) + raise ValueError("Error when casting: {0}".format(ex)) # 'agemin' and 'agemax' can only be used for items cond_min_max = self.__agemin.is_empty() and self.__agemax.is_empty() @@ -269,15 +318,22 @@ def complete(self, item_state): cond_evalitem = self.__eval and ("get_relative_item(" in self.__eval or "return_item(" in self.__eval) except Exception: cond_evalitem = False - if self.__item is None and self.__status is None and not cond_min_max and not cond_evalitem: + try: + cond_status_evalitem = self.__status_eval and \ + ("get_relative_item(" in self.__status_eval or "return_item(" in self.__status_eval) + except Exception: + cond_status_evalitem = False + if self.__item is None and self.__status is None and \ + not cond_min_max and not cond_evalitem and not cond_status_evalitem: raise ValueError("Condition {}: 'agemin'/'agemax' can not be used for eval!".format(self.__name)) return True # Check if condition is matching def check(self, state): # Ignore if no current value can be determined (should not happen as we check this earlier, but to be sure ...) - if self.__item is None and self.__status is None and self.__eval is None: - self._log_info("Condition '{0}': No item, status or eval found! Considering condition as matching!", self.__name) + if all(item is None for item in [self.__item, self.__status, self.__eval, self.__status_eval]): + self._log_info("Condition '{0}': No item, status or (status)eval found! " + "Considering condition as matching!", self.__name) return True self._log_debug("Condition '{0}': Checking all relevant stuff", self.__name) self._log_increase_indent() @@ -316,11 +372,17 @@ def write_to_logger(self): else: self._log_info("status item: {0} ({1})", self.__name, self.__status.property.path) if self.__eval is not None: - if isinstance(self.__item, list): - for e in self.__item: + 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() @@ -537,7 +599,7 @@ def __check_value(self, state): for i, _ in enumerate(min_value): min = None if min_value[i] == 'novalue' else min_value[i] max = None if max_value[i] == 'novalue' else max_value[i] - self._log_debug("Checking minvalue {} ({}) and maxvalue {}({}) against current {}({})", min, type(min), max, type(max), current, type(current)) + self._log_debug("Checking minvalue {} ({}) and maxvalue {} ({}) against current {} ({})", min, type(min), max, type(max), current, type(current)) if min is not None and max is not None and min > max: min, max = max, min self._log_warning("Condition {}: min must not be greater than max! " @@ -664,7 +726,7 @@ def __check_age(self, state): return True # Ignore if no current value can be determined - if self.__item is None and self.__status is None and self.__eval is None: + if all(item is None for item in [self.__item, self.__status, self.__eval, self.__status_eval]): self._log_warning("Age of '{0}': No item/eval found! Considering condition as matching!", self.__name) return True @@ -672,17 +734,25 @@ def __check_age(self, state): cond_evalitem = self.__eval and ("get_relative_item(" in self.__eval or "return_item(" in self.__eval) except Exception: cond_evalitem = False + try: + cond_status_evalitem = self.__status_eval and \ + ("get_relative_item(" in self.__status_eval or "return_item(" in self.__status_eval) + except Exception: + cond_status_evalitem = False if self.__item is None and cond_evalitem is False: - self._log_warning("Make sure your se_eval '{}' really contains an item and not an ID. If the age " - "condition does not work though, please check your eval!", self.__eval) - + self._log_warning("Make sure your se_eval/se_item: eval:<..> '{}' really returns an item and not an ID. " + "If the age condition does not work, please check your eval!", self.__eval) + if self.__status is None and cond_status_evalitem is False: + self._log_warning("Make sure your se_status: eval:<..> '{}' really returns an item and not an ID. " + "If the age condition does not work, please check your eval!", self.__status_eval) try: current = self.__get_current(eval_type='age') except Exception as ex: _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'match', 'age'] - self._abitem.update_webif(_key, 'Not possible to get age from eval {}'.format(self.__eval)) - self._log_warning("Age of '{0}': Not possible to get age from eval {1}! " - "Considering condition as matching: {2}", self.__name, self.__eval, ex) + self._abitem.update_webif(_key, 'Not possible to get age from eval {} ' + 'or status_eval {}'.format(self.__eval, self.__status_eval)) + self._log_warning("Age of '{0}': Not possible to get age from eval {1} or status_eval {2}! " + "Considering condition as matching: {3}", self.__name, self.__eval, self.__status_eval, ex) return True agemin = None if self.__agemin.is_empty() else self.__agemin.get() @@ -752,6 +822,32 @@ def __check_age(self, state): # Current value of condition (based on item or eval) def __get_current(self, eval_type='value'): + def check_eval(eval_or_status_eval): + if isinstance(eval_or_status_eval, str): + sh = self._sh + shtime = self._shtime + # noinspection PyUnusedLocal + if "stateengine_eval" in eval_or_status_eval or "se_eval" in eval_or_status_eval: + # noinspection PyUnusedLocal + stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem) + try: + 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 + else: + value = eval_result + except Exception as ex: + text = "Condition {}: problem evaluating {}: {}" + raise ValueError(text.format(self.__name, eval_or_status_eval, ex)) + else: + return value + else: + return 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) @@ -768,30 +864,13 @@ def __get_current(self, eval_type='value'): self.__item.property.last_update_by if eval_type == 'updatedby' else\ self.__item.property.last_trigger_by if eval_type == 'triggeredby' else\ self.__item.property.value - if self.__eval is not None: - # noinspection PyUnusedLocal - self._log_debug("Trying to get {} of eval {}", eval_type, self.__eval) - sh = self._sh - shtime = self._shtime - if isinstance(self.__eval, str): - # noinspection PyUnusedLocal - if "stateengine_eval" in self.__eval or "se_eval" in self.__eval: - # noinspection PyUnusedLocal - stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem) - try: - if isinstance(eval(self.__eval), self.__itemClass): - value = eval(self.__eval).property.last_change_age if eval_type == 'age' else \ - eval(self.__eval).property.last_change_by if eval_type == 'changedby' else \ - eval(self.__eval).property.last_update_by if eval_type == 'updatedby' else \ - eval(self.__eval).property.last_trigger_by if eval_type == 'triggeredby' else \ - eval(self.__eval).property.value - else: - value = eval(self.__eval) - except Exception as ex: - text = "Condition {}: problem evaluating {}: {}" - raise ValueError(text.format(self.__name, self.__eval, ex)) - else: - return value - else: - return self.__eval() - raise ValueError("Condition {}: Neither 'item' nor 'status' nor 'eval' given!".format(self.__name)) + if self.__status_eval is not None: + self._log_debug("Trying to get {} of statuseval {}", eval_type, self.__status_eval) + return_value = check_eval(self.__status_eval) + return return_value + elif self.__eval is not None: + self._log_debug("Trying to get {} of statuseval {}", eval_type, self.__eval) + return_value = check_eval(self.__eval) + return return_value + + raise ValueError("Condition {}: Neither 'item' nor 'status' nor '(status)eval' given!".format(self.__name)) diff --git a/stateengine/StateEngineConditionSet.py b/stateengine/StateEngineConditionSet.py index 256c2379b..8b9d42c7f 100755 --- a/stateengine/StateEngineConditionSet.py +++ b/stateengine/StateEngineConditionSet.py @@ -97,7 +97,7 @@ def update(self, item, grandparent_item): self.__conditions[name] = StateEngineCondition.SeCondition(self._abitem, name) issue = self.__conditions[name].set(func, item.conf[attribute]) self.__conditions.move_to_end(name, last=True) - if issue: + if issue not in [[], None, [None]]: self.__unused_attributes.update({name: {'attribute': attribute, 'issue': issue}}) elif name not in self.__used_attributes.keys(): self.__used_attributes.update({name: {'attribute': attribute}}) @@ -113,7 +113,7 @@ def update(self, item, grandparent_item): if name == "": continue cond1 = name not in self.__used_attributes.keys() - cond2 = func == "se_item" or func == "se_eval" or func == "se_status" + cond2 = func == "se_item" or func == "se_eval" or func == "se_status_eval" or func == "se_status" cond3 = name not in self.__unused_attributes.keys() if cond1: if cond2 and cond3: @@ -125,7 +125,7 @@ def update(self, item, grandparent_item): self.__conditions[name] = StateEngineCondition.SeCondition(self._abitem, name) try: issue = self.__conditions[name].set(func, grandparent_item.conf[attribute]) - if issue: + if issue not in [[], None, [None]]: self.__unused_attributes.update({name: {'attribute': attribute, 'issue': issue}}) except ValueError as ex: self.__unused_attributes.update({name: {'attribute': attribute, 'issue': ex}}) diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 3cd77f8f7..1ef03cfc5 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -31,6 +31,7 @@ from . import StateEngineValue from . import StateEngineStruct from . import StateEngineStructs +from . import StateEngineEval from lib.item import Items from lib.shtime import Shtime @@ -53,6 +54,10 @@ def id(self): def variables(self): return self.__variables + @property + def firstrun(self): + return self.__first_run + @property def templates(self): return self.__templates @@ -93,7 +98,7 @@ def logger(self): @property def instant_leaveaction(self): - return self.__instant_leaveaction + return self.__instant_leaveaction.get() @property def default_instant_leaveaction(self): @@ -101,7 +106,7 @@ def default_instant_leaveaction(self): @default_instant_leaveaction.setter def default_instant_leaveaction(self, value): - self.__default_instant_leaveaction = value + self.__default_instant_leaveaction.set(value) @property def laststate(self): @@ -176,9 +181,8 @@ def __init__(self, smarthome, item, se_plugin): self.__shtime = Shtime.get_instance() self.__se_plugin = se_plugin self.__active_schedulers = [] - self.__default_instant_leaveaction = StateEngineValue.SeValue(self, "Default Instant Leave Action", False, - "bool") - #self.__all_torelease = {} + 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: self.__id = self.__item.property.path except Exception: @@ -186,39 +190,45 @@ def __init__(self, smarthome, item, se_plugin): self.__name = str(self.__item) self.__itemClass = Item # initialize logging - self.__logger.header("") - _log_level = StateEngineValue.SeValue(self, "Log Level", False, "num") + + self.__log_level = StateEngineValue.SeValue(self, "Log Level", False, "num") + _default_log_level = SeLogger.default_log_level.get() - _returnvalue, _returntype, _using_default, _issue = _log_level.set_from_attr(self.__item, "se_log_level", - _default_log_level) + _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: + _returnvalue = _returnvalue[0] + self.__logger.log_level_as_num = 2 + self.__logger.header("") - if len(_returnvalue) > 1: + _startup_log_level = SeLogger.startup_log_level.get() + + if _startup_log_level > 0: + base = self.__sh.get_basedir() + SeLogger.manage_logdirectory(base, SeLogger.log_directory, True) + self.__logger.log_level_as_num = _startup_log_level + self.__logger.info("Set log level to startup log level {}", _startup_log_level) + + if isinstance(_returnvalue, list) and len(_returnvalue) > 1: self.__logger.warning("se_log_level for item {} can not be defined as a list" " ({}). Using default value {}.", self.id, _returnvalue, _default_log_level) - _log_level.set(_default_log_level) - elif len(_returnvalue) == 1 and _returnvalue[0] is None: - _log_level.set(_default_log_level) + self.__log_level.set(_default_log_level) + elif _returnvalue is None: + self.__log_level.set(_default_log_level) self.__logger.header("Initialize Item {0} (Log {1}, Level set" - " to {2} based on default log level {3}" - " because se_log_level has issues)".format(self.id, self.__logger.name, _log_level, + " to {2} based on default log level" + " because se_log_level has issues)".format(self.id, self.__logger.name, _default_log_level)) elif _using_default: self.__logger.header("Initialize Item {0} (Log {1}, Level set" " to {2} based on default log level {3})".format(self.id, self.__logger.name, - _log_level, - _default_log_level)) + _returnvalue, _default_log_level)) else: self.__logger.header("Initialize Item {0} (Log {1}, Level set" - " to {2}, default log level is {3})".format(self.id, self.__logger.name, _log_level, - _default_log_level)) - _startup_log_level = SeLogger.startup_log_level.get() - self.__logger.log_level.set(_startup_log_level) - self.__logger.info("Set log level to startup log level {}", _startup_log_level) - if _startup_log_level > 0: - base = self.__sh.get_basedir() - SeLogger.manage_logdirectory(base, SeLogger.log_directory, True) - self.__log_level = _log_level - self.__instant_leaveaction = StateEngineValue.SeValue(self, "Instant Leave Action", False, "num") + " to {2}, default log level is {3})".format(self.id, self.__logger.name, + _returnvalue, _default_log_level)) # get startup delay self.__startup_delay = StateEngineValue.SeValue(self, "Startup Delay", False, "num") @@ -226,8 +236,9 @@ def __init__(self, smarthome, item, se_plugin): self.__startup_delay_over = False # Init suspend settings + self.__default_suspend_time = StateEngineDefaults.suspend_time.get() self.__suspend_time = StateEngineValue.SeValue(self, "Suspension time on manual changes", False, "num") - self.__suspend_time.set_from_attr(self.__item, "se_suspend_time", StateEngineDefaults.suspend_time.get()) + self.__suspend_time.set_from_attr(self.__item, "se_suspend_time", self.__default_suspend_time) # Init laststate and previousstate items/values self.__config_issues = {} @@ -274,7 +285,7 @@ def __init__(self, smarthome, item, se_plugin): self.__previousstate_conditionset_internal_name = "" if self.__previousstate_conditionset_item_name is None else \ self.__previousstate_conditionset_item_name.property.value self.__config_issues.update(_issue) - filtered_dict = {key: value for key, value in self.__config_issues.items() if value.get('issue') is not None} + filtered_dict = {key: value for key, value in self.__config_issues.items() if value.get('issue') not in [[], [None], None]} self.__config_issues = filtered_dict self.__states = [] @@ -290,7 +301,7 @@ def __init__(self, smarthome, item, se_plugin): self.__repeat_actions = StateEngineValue.SeValue(self, "Repeat actions if state is not changed", False, "bool") self.__repeat_actions.set_from_attr(self.__item, "se_repeat_actions", True) - + self.__first_run = None self._initstate = None self._initactionname = None self.__update_trigger_item = None @@ -352,17 +363,20 @@ def startup(self): startup_delay = 1 if self.__startup_delay.is_empty() or _startup_delay_param == 0 else _startup_delay_param if startup_delay > 0: first_run = self.__shtime.now() + datetime.timedelta(seconds=startup_delay) - self.__logger.info("Will start stateengine evaluation at {}", first_run) + self.__first_run = first_run.strftime('%H:%M:%S, %d.%m.') + self.__logger.info("Will start stateengine evaluation at {}", self.__first_run) scheduler_name = self.__id + "-Startup Delay" value = {"item": self.__item, "caller": "Init"} self.__se_plugin.scheduler_add(scheduler_name, self.__startup_delay_callback, value=value, next=first_run) elif startup_delay == -1: self.__startup_delay_over = True + self.__first_run = None self.__add_triggers() else: self.__startup_delay_callback(self.__item, "Init", None, None) - self.__logger.info("Reset log level to {}", self.__log_level) - self.__logger.log_level = self.__log_level + _log_level = self.__log_level.get() + self.__logger.info("Reset log level to {}", _log_level) + self.__logger.log_level_as_num = _log_level def show_issues_summary(self): # show issues summary @@ -371,8 +385,7 @@ def show_issues_summary(self): self.__unused_attributes = filtered_dict self.__logger.info("".ljust(80, "_")) - #filtered_dict = {key: value for key, value in self.__action_status.items() if value.get('issue') is not None} - #self.__action_status = filtered_dict + if self.__config_issues: self.__log_issues('config entries') if self.__unused_attributes: @@ -385,6 +398,7 @@ def show_issues_summary(self): self.__log_issues('structs') def update_leave_action(self, default_instant_leaveaction): + default_instant_leaveaction_value = default_instant_leaveaction.get() self.__default_instant_leaveaction = default_instant_leaveaction _returnvalue_leave, _returntype_leave, _using_default_leave, _issue = self.__instant_leaveaction.set_from_attr( @@ -392,23 +406,24 @@ def update_leave_action(self, default_instant_leaveaction): if len(_returnvalue_leave) > 1: self.__logger.warning("se_instant_leaveaction for item {} can not be defined as a list" - " ({}). Using default value {}.", self.id, _returnvalue_leave, default_instant_leaveaction) - self.__instant_leaveaction.set(default_instant_leaveaction) - self.__variables.update({"item.instant_leaveaction": default_instant_leaveaction}) + " ({}). Using default value {}.", self.id, _returnvalue_leave, + default_instant_leaveaction_value) + self.__instant_leaveaction = default_instant_leaveaction + self.__variables.update({"item.instant_leaveaction": default_instant_leaveaction_value}) elif len(_returnvalue_leave) == 1 and _returnvalue_leave[0] is None: - self.__instant_leaveaction.set(default_instant_leaveaction) - self.__variables.update({"item.instant_leaveaction": default_instant_leaveaction}) + self.__instant_leaveaction = default_instant_leaveaction + self.__variables.update({"item.instant_leaveaction": default_instant_leaveaction_value}) self.__logger.info("Using default instant_leaveaction {0} " - "as no se_instant_leaveaction is set.".format(default_instant_leaveaction)) + "as no se_instant_leaveaction is set.".format(default_instant_leaveaction_value)) elif _using_default_leave: - self.__variables.update({"item.instant_leaveaction": default_instant_leaveaction}) + self.__variables.update({"item.instant_leaveaction": default_instant_leaveaction_value}) self.__logger.info("Using default instant_leaveaction {0} " - "as no se_instant_leaveaction is set.".format(default_instant_leaveaction)) + "as no se_instant_leaveaction is set.".format(default_instant_leaveaction_value)) else: self.__variables.update({"item.instant_leaveaction": _returnvalue_leave}) self.__logger.info("Using instant_leaveaction {0} " "from attribute se_instant_leaveaction. " - "Default value is {1}".format(_returnvalue_leave, default_instant_leaveaction)) + "Default value is {1}".format(_returnvalue_leave, default_instant_leaveaction_value)) def updatetemplates(self, template, value): if value is None: @@ -434,30 +449,55 @@ def run_queue(self): self.__logger.debug("{} not running (anymore). Queue not activated.", StateEngineDefaults.plugin_identification) return - _current_log_level = self.__logger.get_loglevel() + _current_log_level = self.__log_level.get() _default_log_level = SeLogger.default_log_level.get() + + if _current_log_level <= -1: + self.__using_default_log_level = True + value = SeLogger.default_log_level.get() + else: + value = _current_log_level + self.__using_default_log_level = False + self.__logger.log_level_as_num = value + if _current_log_level > 0: base = self.__sh.get_basedir() SeLogger.manage_logdirectory(base, SeLogger.log_directory, True) - self.__logger.debug("Current log level {}, default {}, currently using default {}", - self.__logger.log_level, _default_log_level, self.__logger.using_default_log_level) - if self.__instant_leaveaction.get() <= -1: + additional_text = ", currently using default" if self.__using_default_log_level is True else "" + self.__logger.info("Current log level {} ({}), default {}{}", + _current_log_level, type(self.__logger.log_level), _default_log_level, additional_text) + _instant_leaveaction = self.__instant_leaveaction.get() + _default_instant_leaveaction_value = self.__default_instant_leaveaction.get() + if _instant_leaveaction <= -1: self.__using_default_instant_leaveaction = True + additional_text = ", currently using default" + elif _instant_leaveaction > 1: + self.__logger.info("Current se_instant_leaveaction {} is invalid. " + "It has to be set to -1, 0 or 1. Setting it to 1 instead.", _instant_leaveaction) + _instant_leaveaction = 1 + self.__using_default_instant_leaveaction = False + additional_text = "" else: self.__using_default_instant_leaveaction = False - self.__logger.debug("Current instant leave action {}, default {}, currently using default {}", - self.__instant_leaveaction, self.__default_instant_leaveaction, - self.__using_default_instant_leaveaction) - if self.__suspend_time.get() < 0: + additional_text = "" + self.__logger.debug("Current instant leave action {}, default {}{}", + _instant_leaveaction, _default_instant_leaveaction_value, additional_text) + _suspend_time = self.__suspend_time.get() + if _suspend_time < 0: self.__using_default_suspendtime = True + additional_text = ", currently using default" else: self.__using_default_suspendtime = False - self.__logger.debug("Current suspend time {}, default {}, currently using default {}", - self.__suspend_time, StateEngineDefaults.suspend_time, - self.__using_default_suspendtime) + additional_text = "" + self.__logger.debug("Current suspend time {}, default {}{}", + _suspend_time, self.__default_suspend_time, additional_text) self.update_lock.acquire(True, 10) all_released_by = {} new_state = None + if self.__using_default_instant_leaveaction: + _instant_leaveaction = _default_instant_leaveaction_value + else: + _instant_leaveaction = True if _instant_leaveaction == 1 else False while not self.__queue.empty() and self.__ab_alive: job = self.__queue.get() new_state = None @@ -506,11 +546,11 @@ def run_queue(self): # Update current values StateEngineCurrent.update() - self.__variables["item.suspend_time"] = StateEngineDefaults.suspend_time.get() \ - if self.__using_default_suspendtime is True else self.__suspend_time.get() + self.__variables["item.suspend_time"] = self.__default_suspend_time \ + if self.__using_default_suspendtime is True else _suspend_time self.__variables["item.suspend_remaining"] = -1 - self.__variables["item.instant_leaveaction"] = self.__default_instant_leaveaction.get() \ - if self.__using_default_instant_leaveaction is True else self.__instant_leaveaction.get() + self.__variables["item.instant_leaveaction"] = _default_instant_leaveaction_value \ + if self.__using_default_instant_leaveaction is True else _instant_leaveaction # get last state last_state = self.__laststate_get() if last_state is not None: @@ -546,6 +586,10 @@ def run_queue(self): # find new state _leaveactions_run = False + if _instant_leaveaction >= 1 and caller != "Released_by Retrigger": + evaluated_instant_leaveaction = True + else: + evaluated_instant_leaveaction = False for state in self.__states: if not self.__ab_alive: self.__logger.debug("StateEngine Plugin not running (anymore). Stop state evaluation.") @@ -554,7 +598,7 @@ def run_queue(self): _key_name = ['{}'.format(state.id), 'name'] self.update_webif(_key_name, state.name) - result = self.__update_check_can_enter(state) + result = self.__update_check_can_enter(state, _instant_leaveaction) _previousstate_conditionset_id = _last_conditionset_id _previousstate_conditionset_name = _last_conditionset_name _last_conditionset_id = self.__lastconditionset_internal_id @@ -563,14 +607,8 @@ 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 - _instant_leaveaction = self.__instant_leaveaction.get() - if self.__using_default_instant_leaveaction: - _instant_leaveaction = self.__default_instant_leaveaction.get() - if _instant_leaveaction >= 1 and caller != "Released_by Retrigger": - _instant_leaveaction = True - else: - _instant_leaveaction = False - if result is False and last_state == state and _instant_leaveaction is True: + + 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()) @@ -602,7 +640,7 @@ def run_queue(self): self.update_lock.release() self.__logger.debug("State evaluation finished") self.__logger.info("State evaluation queue empty.") - self.__handle_releasedby(new_state, last_state) + self.__handle_releasedby(new_state, last_state, _instant_leaveaction) return if new_state.is_copy_for: @@ -615,7 +653,7 @@ def run_queue(self): #self.lastconditionset_set(_original_conditionset_id, _original_conditionset_name) self.__logger.info("Leaving {0} ('{1}'). Condition set was: {2}.", last_state.id, last_state.name, _original_conditionset_id) - self.__update_check_can_enter(last_state, False) + self.__update_check_can_enter(last_state, _instant_leaveaction, False) last_state.run_leave(self.__repeat_actions.get()) _key_leave = ['{}'.format(last_state.id), 'leave'] _key_stay = ['{}'.format(last_state.id), 'stay'] @@ -624,7 +662,7 @@ def run_queue(self): self.update_webif(_key_leave, True) self.update_webif(_key_stay, False) self.update_webif(_key_enter, False) - self.__handle_releasedby(new_state, last_state) + self.__handle_releasedby(new_state, last_state, _instant_leaveaction) if self.update_lock.locked(): self.update_lock.release() self.update_state(self.__item, "Released_by Retrigger", state.id) @@ -693,7 +731,7 @@ def run_queue(self): self.update_webif(_key_enter, False) self.__logger.debug("State evaluation finished") - all_released_by = self.__handle_releasedby(new_state, last_state) + all_released_by = self.__handle_releasedby(new_state, last_state, _instant_leaveaction) self.__logger.info("State evaluation queue empty.") if new_state: @@ -747,9 +785,12 @@ def __update_can_release(self, can_release, new_state=None): else: self.__logger.info("Entry {} in se_released_by of state(s) is not a valid state.", entry) - def __handle_releasedby(self, new_state, last_state): + def __handle_releasedby(self, new_state, last_state, instant_leaveaction): def update_can_release_list(): for e in _returnvalue: + if isinstance(e, list): + self.__logger.warning("Entry {} should not be list. Please check!", e) + e = e[0] e = self.__update_release_item_value(e, new_state) if e and state.id not in can_release.setdefault(e, [state.id]): can_release[e].append(state.id) @@ -834,10 +875,9 @@ def update_can_release_list(): if cond_index or relevant_state not in self.__states: current_log_level = self.__log_level.get() if current_log_level < 3: - self.__logger.log_level.set(0) - can_enter = self.__update_check_can_enter(relevant_state) - #can_enter = relevant_state.can_enter() - self.__logger.log_level.set(current_log_level) + self.__logger.log_level_as_num = 0 + 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 {}, "\ "not copying", relevant_state.id, last_state.id) @@ -882,7 +922,11 @@ def combine_dicts(dict1, dict2): for key, value in dict2.items(): if key in combined_dict: - combined_dict[key]['issueorigin'].extend(value['issueorigin']) + for k, v in combined_dict.items(): + v['issueorigin'].extend( + [item for item in v['issueorigin'] if item not in combined_dict[k]['issueorigin']]) + v['issue'].extend([item for item in v['issue'] if item not in combined_dict[k]['issue']]) + else: combined_dict[key] = value @@ -972,13 +1016,18 @@ def list_issues(v): warn = ', '.join(key for key in self.__config_issues.keys()) else: to_check = self.__unused_attributes.items() - warn = ', '.join(key for key in self.__unused_attributes.keys()) + warn_unused = ', '.join(key for key, value in self.__unused_attributes.items() if 'issue' not in value) + warn_issues = ', '.join(key for key, value in self.__unused_attributes.items() if 'issue' in value) self.__logger.info("") if issue_type == 'attributes': - self.__logger.info("These attributes are not used: {} Please check extended " - "log file for details.", warn) + if warn_unused: + self.__logger.info("These attributes are not used: {}. Please check extended " + "log file for details.", warn_unused) + if warn_issues: + self.__logger.warning("There are attribute issues: {}. Please check extended " + "log file for details.", warn_issues) else: - self.__logger.warning("There are {} issues: {} Please check extended " + self.__logger.warning("There are {} issues: {}. Please check extended " "log file for details.", issue_type, warn) self.__logger.info("") self.__logger.info("The following {} have issues:", issue_type) @@ -1013,8 +1062,15 @@ def list_issues(v): origin_text = 'state {}, action {}, on_{}'.format(origin.get('state'), origin.get('action'), origin.get('type')) elif issue_type == 'states': - origin_text = 'condition {} defined in conditionset {}'.format(origin.get('condition'), - origin.get('conditionset')) + if origin.get('condition') == 'GeneralError' and len(origin_list) == 1: + origin_text = 'there was a general error. The state' + elif origin.get('condition') == 'ValueError' and len(origin_list) == 1: + origin_text = 'there was a value error. The state' + else: + if origin.get('condition') in ['GeneralError', 'ValueError']: + continue + origin_text = 'condition {} defined in conditionset {}'.format(origin.get('condition'), + origin.get('conditionset')) else: origin_text = 'state {}, conditionset {}'.format(origin.get('state'), origin.get('conditionset')) @@ -1041,8 +1097,12 @@ def __initialize_state(self, item_state, _statecount): key not in _state.used_attributes} self.__unused_attributes = filtered_dict except ValueError as ex: + self.update_issues('state', {item_state.property.path: {'issue': ex, 'issueorigin': + [{'conditionset': 'None', 'condition': 'ValueError'}]}}) self.__logger.error("Ignoring state {0} because ValueError: {1}", item_state.property.path, ex) except Exception as ex: + self.update_issues('state', {item_state.property.path: {'issue': ex, 'issueorigin': + [{'conditionset': 'None', 'condition': 'GeneralError'}]}}) self.__logger.error("Ignoring state {0} because: {1}", item_state.property.path, ex) def __finish_states(self): @@ -1064,7 +1124,7 @@ def update_state(self, item, caller=None, source=None, dest=None): # check if state can be entered after setting state-specific variables # state: state to check - def __update_check_can_enter(self, state, refill=True): + def __update_check_can_enter(self, state, instant_leaveaction, refill=True): try: wasreleasedby = state.was_releasedby.id except Exception as ex: @@ -1089,7 +1149,7 @@ def __update_check_can_enter(self, state, refill=True): self.__variables["release.will_release"] = iscopyfor self.__variables["previous.state_id"] = self.__previousstate_internal_id self.__variables["previous.state_name"] = self.__previousstate_internal_name - self.__variables["item.instant_leaveaction"] = self.__instant_leaveaction.get() + self.__variables["item.instant_leaveaction"] = instant_leaveaction self.__variables["current.state_id"] = state.id self.__variables["current.state_name"] = state.name self.__variables["current.conditionset_id"] = self.__lastconditionset_internal_id @@ -1537,32 +1597,32 @@ def write_to_log(self): # log laststate settings if self.__laststate_item_id is not None: - self.__logger.info("Item 'Laststate Id': {0}", self.__laststate_item_id.property.path) + self.__logger.debug("Item 'Laststate Id': {0}", self.__laststate_item_id.property.path) if self.__laststate_item_name is not None: - self.__logger.info("Item 'Laststate Name': {0}", self.__laststate_item_name.property.path) + self.__logger.debug("Item 'Laststate Name': {0}", self.__laststate_item_name.property.path) # log previousstate settings if self.__previousstate_item_id is not None: - self.__logger.info("Item 'Previousstate Id': {0}", self.__previousstate_item_id.property.path) + self.__logger.debug("Item 'Previousstate Id': {0}", self.__previousstate_item_id.property.path) if self.__previousstate_item_name is not None: - self.__logger.info("Item 'Previousstate Name': {0}", self.__previousstate_item_name.property.path) + self.__logger.debug("Item 'Previousstate Name': {0}", self.__previousstate_item_name.property.path) # log lastcondition settings if self.__lastconditionset_item_id is not None: - self.__logger.info("Item 'Lastcondition Id': {0}", self.__lastconditionset_item_id.property.path) + self.__logger.debug("Item 'Lastcondition Id': {0}", self.__lastconditionset_item_id.property.path) if self.__lastconditionset_item_name is not None: - self.__logger.info("Item 'Lastcondition Name': {0}", self.__lastconditionset_item_name.property.path) + self.__logger.debug("Item 'Lastcondition Name': {0}", self.__lastconditionset_item_name.property.path) # log previouscondition settings if self.__previousconditionset_item_id is not None: - self.__logger.info("Item 'Previouscondition Id': {0}", self.__previousconditionset_item_id.property.path) + self.__logger.debug("Item 'Previouscondition Id': {0}", self.__previousconditionset_item_id.property.path) if self.__previousconditionset_item_name is not None: - self.__logger.info("Item 'Previouscondition Name': {0}", self.__previousconditionset_item_name.property.path) + self.__logger.debug("Item 'Previouscondition Name': {0}", self.__previousconditionset_item_name.property.path) if self.__previousstate_conditionset_item_id is not None: - self.__logger.info("Item 'Previousstate condition Id': {0}", self.__previousstate_conditionset_item_id.property.path) + self.__logger.debug("Item 'Previousstate condition Id': {0}", self.__previousstate_conditionset_item_id.property.path) if self.__previousstate_conditionset_item_name is not None: - self.__logger.info("Item 'Previousstate condition Name': {0}", + self.__logger.debug("Item 'Previousstate condition Name': {0}", self.__previousstate_conditionset_item_name.property.path) self.__init_releasedby() @@ -1572,7 +1632,7 @@ def write_to_log(self): state.write_to_log() self._initstate = None - filtered_dict = {key: value for key, value in self.__config_issues.items() if value.get('issue') not in [[], None]} + filtered_dict = {key: value for key, value in self.__config_issues.items() if value.get('issue') not in [[], [None], None]} self.__config_issues = filtered_dict # endregion @@ -1714,6 +1774,7 @@ def __startup_delay_callback(self, item, caller=None, source=None, dest=None): if self.__se_plugin.scheduler_get(scheduler_name): self.__se_plugin.scheduler_remove(scheduler_name) self.__logger.debug('Startup Delay over. Removed scheduler {}', scheduler_name) + self.__first_run = None self.update_state(item, "Startup Delay", source, dest) self.__add_triggers() @@ -1738,11 +1799,11 @@ def return_item(self, item_id): return self.itemsApi.return_item(item_id.id), None if item_id is None: _issue = "item_id is None" - return None, _issue + return None, [_issue] if not isinstance(item_id, str): _issue = "'{0}' is not defined as string.".format(item_id) self.__logger.info("{0} Check your item config!", _issue, item_id) - return None, _issue + return None, [_issue] item_id = item_id.strip() if item_id.startswith("struct:"): item = None @@ -1756,13 +1817,27 @@ def return_item(self, item_id): if item is None: _issue = "Item '{0}' in struct not found.".format(item_id) self.__logger.warning(_issue) - return item, _issue + return item, [_issue] if not item_id.startswith("."): - item = self.itemsApi.return_item(item_id) + match = re.match(r'^(.*):', item_id) + if item_id.startswith("eval:"): + if "stateengine_eval" in item_id or "se_eval" in item_id: + # noinspection PyUnusedLocal + stateengine_eval = se_eval = StateEngineEval.SeEval(self) + item = item_id.replace('sh', 'self._sh') + item = item.replace('shtime', 'self._shtime') + _, _, item = item.partition(":") + return item, None + elif match: + _issue = "Item '{0}' has to be defined as an item path or eval expression without {}.".format(match.group(1), item_id) + self.__logger.warning(_issue) + return None, [_issue] + else: + item = self.itemsApi.return_item(item_id) if item is None: _issue = "Item '{0}' not found.".format(item_id) self.__logger.warning(_issue) - return item, _issue + return item, [_issue] self.__logger.debug("Testing for relative item declaration {}", item_id) parent_level = 0 for c in item_id: @@ -1787,13 +1862,13 @@ def return_item(self, item_id): self.__logger.warning(_issue) else: self.__logger.develop("Determined item '{0}' for id {1}.", item.id, item_id) - return item, _issue + return item, [_issue] # Return an item related to the StateEngine object item # attribute: Name of the attribute of the StateEngine object item, which contains the item_id to read def return_item_by_attribute(self, attribute): if attribute not in self.__item.conf: - _issue = {attribute: {'issue': 'Attribute missing in stateeninge configuration.'}} + _issue = {attribute: {'issue': ['Attribute missing in stateeninge configuration.']}} self.__logger.warning("Attribute '{0}' missing in stateeninge configuration.", attribute) return None, _issue _returnvalue, _issue = self.return_item(self.__item.conf[attribute]) diff --git a/stateengine/StateEngineLogger.py b/stateengine/StateEngineLogger.py index 6e56af86f..acf5460f5 100755 --- a/stateengine/StateEngineLogger.py +++ b/stateengine/StateEngineLogger.py @@ -57,32 +57,17 @@ def log_maxage(self, value): logger.error("The maximum age of the log files has to be an int number.") @property - def using_default_log_level(self): - return self.__using_default_log_level + def log_level_as_num(self): + return self.__log_level_as_num - @using_default_log_level.setter - def using_default_log_level(self, value): - self.__using_default_log_level = value + @log_level_as_num.setter + def log_level_as_num(self, value): + self.__log_level_as_num = value @property def name(self): return self.__name - # Set global log level - # loglevel: current loglevel - @property - def log_level(self): - return self.__log_level.get() - - @log_level.setter - def log_level(self, value): - try: - self.__log_level = int(value) - except ValueError: - self.__log_level = 0 - logger = StateEngineDefaults.logger - logger.error("Loglevel has to be an int number!") - @property def log_directory(self): return SeLogger.__log_directory @@ -147,19 +132,13 @@ def __init__(self, item): self.__name = 'stateengine.{}'.format(item.property.path) self.__section = item.property.path.replace(".", "_").replace("/", "") self.__indentlevel = 0 - self.__default_log_level = None - self.__startup_log_level = None - self.__log_level = None - self.__using_default_log_level = False + self.__log_level_as_num = 0 self.__logmaxage = None self.__date = None self.__logerror = False self.__filename = "" self.update_logfile() - # get current log level of abitem - def get_loglevel(self): - return self.log_level.get() # Update name logfile if required def update_logfile(self): @@ -186,13 +165,7 @@ def decrease_indent(self, by=1): # text: text to log def log(self, level, text, *args): # Section given: Check level - _log_level = self.get_loglevel() - if _log_level <= -1: - self.using_default_log_level = True - _log_level = SeLogger.default_log_level.get() - else: - self.using_default_log_level = False - if level <= _log_level: + if level <= self.__log_level_as_num: indent = "\t" * self.__indentlevel if args: text = text.format(*args) diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index e665a0788..06d17186f 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -362,7 +362,12 @@ def update_unused(used_attributes, type, name): def update_action_status(action_status, actiontype): if action_status is None: return - + action_status = StateEngineTools.flatten_list(action_status) + #self._log_debug("Action status: {}", action_status) + if isinstance(action_status, list): + for e in action_status: + update_action_status(e, actiontype) + return for itm, dct in action_status.items(): if itm not in self.__action_status: self.__action_status.update({itm: dct}) @@ -370,6 +375,9 @@ def update_action_status(action_status, actiontype): for (itm, dct) in action_status.items(): issues = dct.get('issue') 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): @@ -392,7 +400,8 @@ def update_action_status(action_status, 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]} + 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) @@ -506,7 +515,7 @@ def update_action_status(action_status, actiontype): for attribute in parent_item.conf: func, name = StateEngineTools.partition_strip(attribute, "_") cond1 = name and name not in self.__used_attributes - cond2 = func == "se_item" or func == "se_eval" or func == "se_status" + cond2 = func == "se_item" or func == "se_eval" or func == "se_status_eval" or func == "se_status" cond3 = name not in self.__unused_attributes.keys() if cond1 and cond2 and cond3: @@ -523,6 +532,7 @@ def update_action_status(action_status, actiontype): _, _action_status = self.__actions_enter.update(attribute, child_item.conf[attribute]) if _action_status: update_action_status(_action_status, 'enter') + self._abitem.update_action_status(self.__action_status) update_unused(_used_attributes, 'action', child_name) elif child_name == "on_stay": _actioncount += 1 @@ -531,6 +541,7 @@ def update_action_status(action_status, actiontype): _, _action_status = self.__actions_stay.update(attribute, child_item.conf[attribute]) if _action_status: update_action_status(_action_status, 'stay') + self._abitem.update_action_status(self.__action_status) update_unused(_used_attributes, 'action', child_name) elif child_name == "on_enter_or_stay": _actioncount += 1 @@ -539,6 +550,7 @@ def update_action_status(action_status, actiontype): _, _action_status = self.__actions_enter_or_stay.update(attribute, child_item.conf[attribute]) if _action_status: update_action_status(_action_status, 'enter_or_stay') + self._abitem.update_action_status(self.__action_status) update_unused(_used_attributes, 'action', child_name) elif child_name == "on_leave": _actioncount += 1 @@ -547,6 +559,7 @@ def update_action_status(action_status, actiontype): _, _action_status = self.__actions_leave.update(attribute, child_item.conf[attribute]) if _action_status: update_action_status(_action_status, 'leave') + self._abitem.update_action_status(self.__action_status) update_unused(_used_attributes, 'action', child_name) except ValueError as ex: raise ValueError("Condition {0} check for actions error: {1}".format(child_name, ex)) @@ -558,6 +571,7 @@ def update_action_status(action_status, actiontype): _action_status = _result[1] if _action_status: update_action_status(_action_status, 'enter_or_stay') + self._abitem.update_action_status(self.__action_status) _total_actioncount = _enter_actioncount + _stay_actioncount + _enter_stay_actioncount + _leave_actioncount @@ -568,15 +582,19 @@ def update_action_status(action_status, actiontype): _action_status = self.__actions_enter.complete(item_state, self.__conditions.evals_items) if _action_status: update_action_status(_action_status, 'enter') + self._abitem.update_action_status(self.__action_status) _action_status = self.__actions_stay.complete(item_state, self.__conditions.evals_items) if _action_status: update_action_status(_action_status, 'stay') + self._abitem.update_action_status(self.__action_status) _action_status = self.__actions_enter_or_stay.complete(item_state, self.__conditions.evals_items) if _action_status: update_action_status(_action_status, 'enter_or_stay') + self._abitem.update_action_status(self.__action_status) _action_status = self.__actions_leave.complete(item_state, self.__conditions.evals_items) if _action_status: update_action_status(_action_status, 'leave') + self._abitem.update_action_status(self.__action_status) self._abitem.update_action_status(self.__action_status) self._abitem.update_attributes(self.__unused_attributes, self.__used_attributes) _summary = "{} on_enter, {} on_stay , {} on_enter_or_stay, {} on_leave" diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index 4f26c950a..73c01acdd 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -299,15 +299,23 @@ def set(self, value, name="", reset=True, item=None): elif field_value[i] == "": field_value[i] = s s = "value" - cond3 = isinstance(field_value[i], str) and field_value[i].lstrip('-').replace('.','',1).isdigit() - if cond3: - field_value[i] = ast.literal_eval(field_value[i]) - elif isinstance(field_value[i], str) and field_value[i].lower() in ['true', 'yes']: - field_value[i] = True - elif isinstance(field_value[i], str) and field_value[i].lower() in ['false', 'no']: - field_value[i] = False - self.__value = [] if self.__value is None else [self.__value] if not isinstance(self.__value, list) else self.__value - self.__value.append(None if s != "value" else self.__do_cast(field_value[i])) + self.__value = [] if self.__value is None else [self.__value] if not isinstance(self.__value, + list) else self.__value + if s == "value": + cond3 = isinstance(field_value[i], str) and field_value[i].lstrip('-').replace('.','',1).isdigit() + if cond3: + field_value[i] = ast.literal_eval(field_value[i]) + elif isinstance(field_value[i], str) and field_value[i].lower() in ['true', 'yes']: + field_value[i] = True + elif isinstance(field_value[i], str) and field_value[i].lower() in ['false', 'no']: + field_value[i] = False + + _value, _issue = self.__do_cast(field_value[i]) + if _issue: + self.__issues.append(_issue) + self.__value.append(_value) + else: + self.__value.append(None) self.__item = [] if self.__item is None else [self.__item] if not isinstance(self.__item, list) else self.__item if s == "item": _item, _issue = self._abitem.return_item(field_value[i]) @@ -356,7 +364,9 @@ def set(self, value, name="", reset=True, item=None): field_value = True elif isinstance(field_value, str) and field_value.lower() in ['false', 'no']: field_value = False - self.__value = self.__do_cast(field_value) + self.__value, _issue = self.__do_cast(field_value) + if _issue: + self.__issues.append(_issue) else: self.__value = None self.__issues = StateEngineTools.flatten_list(self.__issues) @@ -368,7 +378,8 @@ def set(self, value, name="", reset=True, item=None): # cast_func: cast function def set_cast(self, cast_func): self.__cast_func = cast_func - self.__value = self.__do_cast(self.__value) + self.__value, _issue = self.__do_cast(self.__value) + return [_issue] # determine and return value def get(self, default=None, originalorder=True): @@ -406,7 +417,7 @@ def get(self, default=None, originalorder=True): def get_for_webif(self): returnvalues = self.get() returnvalues = self.__varname if returnvalues == '' else returnvalues - return returnvalues + return str(returnvalues) def get_type(self): if len(self.__listorder) <= 1: @@ -504,8 +515,6 @@ def get_text(self, prefix=None, suffix=None): def cast_item(self, value): try: _returnvalue, _issue = self._abitem.return_item(value) - if _issue: - self.__issues.append(_issue) return _returnvalue except Exception as ex: self._log_error("Can't cast {0} to item/struct! {1}".format(value, ex)) @@ -546,6 +555,7 @@ def __absolute_item(self, value, id=None): # Cast given value, if cast-function is set # value: value to cast def __do_cast(self, value, id=None): + _issue = None if value is not None and self.__cast_func is not None: try: if isinstance(value, list): @@ -556,7 +566,6 @@ def __do_cast(self, value, id=None): except Exception as ex: _newvalue = None _issue = "Problem casting element '{0}' to {1}: {2}.".format(element, self.__cast_func, ex) - self.__issues.append(_issue) self._log_warning(_issue) valuelist.append(_newvalue) if element in self.__listorder: @@ -584,18 +593,15 @@ def __do_cast(self, value, id=None): except Exception as ex: if any(x in value for x in ['sh.', '_eval', '(']): _issue = "You most likely forgot to prefix your expression with 'eval:'" - self.__issues.append(_issue) raise ValueError(_issue) else: - _issue = "Not possible to cast because {}".format(ex) - self.__issues.append(_issue) + _issue = "Not possible to cast '{}' because {}".format(value, ex) raise ValueError(_issue) if value in self.__listorder: self.__listorder[self.__listorder.index(value)] = _newvalue value = _newvalue except Exception as ex: - _issue = "Problem casting '{0}' to {1}: {2}.".format(value, self.__cast_func, ex) - self.__issues.append(_issue) + _issue = "Problem casting '{0}': {1}.".format(value, ex) self._log_warning(_issue) if '_cast_list' in self.__cast_func.__globals__ and self.__cast_func == self.__cast_func.__globals__['_cast_list']: try: @@ -608,9 +614,9 @@ def __do_cast(self, value, id=None): value = [value] self._log_debug("Original casting of {} to {} failed. New cast is now: {}.", value, self.__cast_func, type(value)) - return value - return None - return value + return value, _issue + return None, _issue + return value, _issue # Determine value by using a struct def __get_from_struct(self): @@ -618,13 +624,13 @@ def __get_from_struct(self): if isinstance(self.__struct, list): for val in self.__struct: if val is not None: - _newvalue = self.__do_cast(val) + _newvalue, _issue = self.__do_cast(val) values.append(_newvalue) if 'struct:{}'.format(val.property.path) in self.__listorder: self.__listorder[self.__listorder.index('struct:{}'.format(val.property.path))] = _newvalue else: if self.__struct is not None: - _newvalue = self.__do_cast(self.__struct) + _newvalue, _issue = self.__do_cast(self.__struct) if 'struct:{}'.format(self.__regex) in self.__listorder: self.__listorder[self.__listorder.index('struct:{}'.format(self.__struct))] = _newvalue values = _newvalue @@ -632,14 +638,14 @@ def __get_from_struct(self): return values try: - _newvalue = self.__do_cast(self.__struct) + _newvalue, _issue = self.__do_cast(self.__struct) if 'struct:{}'.format(self.__struct) in self.__listorder: self.__listorder[self.__listorder.index('struct:{}'.format(self.__struct))] = _newvalue values = _newvalue except Exception as ex: values = self.__struct _issue = "Problem while getting from struct '{0}': {1}.".format(values, ex) - self.__issues.append(_issue) + #self.__issues.append(_issue) self._log_info(_issue) return values @@ -669,7 +675,7 @@ def __get_from_regex(self): except Exception as ex: values = self.__regex _issue = "Problem while creating regex '{0}': {1}.".format(values, ex) - self.__issues.append(_issue) + #self.__issues.append(_issue) self._log_info(_issue) return values @@ -686,7 +692,7 @@ def __get_eval(self): self._log_debug("Checking eval: {0}", self.__eval) self._log_increase_indent() try: - _newvalue = self.__do_cast(eval(self.__eval)) + _newvalue, _issue = self.__do_cast(eval(self.__eval)) if 'eval:{}'.format(self.__eval) in self.__listorder: self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = _newvalue values = _newvalue @@ -696,7 +702,7 @@ def __get_eval(self): except Exception as ex: self._log_decrease_indent() _issue = "Problem evaluating '{0}': {1}.".format(StateEngineTools.get_eval_name(self.__eval), ex) - self.__issues.append(_issue) + #self.__issues.append(_issue) self._log_warning(_issue) self._log_increase_indent() values = None @@ -717,7 +723,7 @@ def __get_eval(self): # noinspection PyUnusedLocal stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem) try: - _newvalue = self.__do_cast(eval(val)) + _newvalue, _issue = self.__do_cast(eval(val)) if 'eval:{}'.format(val) in self.__listorder: self.__listorder[self.__listorder.index('eval:{}'.format(val))] = _newvalue value = _newvalue @@ -728,13 +734,13 @@ def __get_eval(self): self._log_decrease_indent() _issue = "Problem evaluating from list '{0}': {1}.".format( StateEngineTools.get_eval_name(val), ex) - self.__issues.append(_issue) + #self.__issues.append(_issue) self._log_warning(_issue) self._log_increase_indent() value = None else: try: - _newvalue = self.__do_cast(val()) + _newvalue, _issue = self.__do_cast(val()) if 'eval:{}'.format(val) in self.__listorder: self.__listorder[self.__listorder.index('eval:{}'.format(val))] = _newvalue value = _newvalue @@ -742,17 +748,18 @@ def __get_eval(self): self._log_decrease_indent() _issue = "Problem evaluating '{0}': {1}.".format( StateEngineTools.get_eval_name(val), ex) - self.__issues.append(_issue) + #self.__issues.append(_issue) self._log_info(_issue) value = None if value is not None: - values.append(self.__do_cast(value)) + _newvalue, _issue = self.__do_cast(value) + values.append(_newvalue) self._log_decrease_indent() else: self._log_debug("Checking eval (no str, no list): {0}.", self.__eval) try: self._log_increase_indent() - _newvalue = self.__do_cast(self.__eval()) + _newvalue, _issue = self.__do_cast(self.__eval()) if 'eval:{}'.format(self.__eval) in self.__listorder: self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = _newvalue values = _newvalue @@ -762,7 +769,7 @@ def __get_eval(self): except Exception as ex: self._log_decrease_indent() _issue = "Problem evaluating '{0}': {1}.".format(StateEngineTools.get_eval_name(self.__eval), ex) - self.__issues.append(_issue) + #self.__issues.append(_issue) self._log_warning(_issue) self._log_increase_indent() return None @@ -771,47 +778,61 @@ def __get_eval(self): # Determine value from item def __get_from_item(self): + _issue_list = [] if isinstance(self.__item, list): values = [] for val in self.__item: if val is None: _newvalue = None else: - _newvalue = self.__do_cast(val.property.value) + _newvalue, _issue = self.__do_cast(val.property.value) + if _issue: + _issue_list.append(_issue) values.append(_newvalue) - if 'item:{}'.format(val) in self.__listorder: - self.__listorder[self.__listorder.index('item:{}'.format(val))] = _newvalue + search_item = 'item:{}'.format(val) + if search_item in self.__listorder: + index = self.__listorder.index(search_item) + self.__listorder[index] = _newvalue else: if self.__item is None: return None - _newvalue = self.__do_cast(self.__item.property.value) - if 'item:{}'.format(self.__item) in self.__listorder: - self.__listorder[self.__listorder.index('item:{}'.format(self.__item))] = _newvalue + _newvalue, _issue = self.__do_cast(self.__item.property.value) + if _issue: + _issue_list.append(_issue) + search_item = 'item:{}'.format(self.__item) + if search_item in self.__listorder: + index = self.__listorder.index(search_item) + self.__listorder[index] = _newvalue values = _newvalue if values is not None: return values try: _newvalue = self.__item.property.path - if 'item:{}'.format(self.__item) in self.__listorder: - self.__listorder[self.__listorder.index('item:{}'.format(self.__item))] = _newvalue + search_item = 'item:{}'.format(self.__item) + if search_item in self.__listorder: + index = self.__listorder.index(search_item) + self.__listorder[index] = _newvalue values = _newvalue except Exception as ex: values = self.__item _issue = "Problem while reading item path '{0}': {1}.".format(values, ex) - self.__issues.append(_issue) + _issue_list.append(_issue) self._log_info(_issue) - return self.__do_cast(values) + _newvalue, _issue = self.__do_cast(values) + if _issue: + _issue_list.append(_issue) + return _newvalue # Determine value from variable def __get_from_variable(self): def update_value(varname): value = self._abitem.get_variable(varname) - new_value = self.__do_cast(value) + new_value, _issue = self.__do_cast(value) new_value = 'var:{}'.format(varname) if new_value == '' else new_value if isinstance(new_value, str) and 'Unknown variable' in new_value: issue = "There is a problem with your variable {}".format(new_value) - self.__issues.append(issue) + #self.__issues.append(issue) self._log_warning(issue) new_value = '' self._log_debug("Checking variable '{0}', value {1} from list {2}", diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index 84399112a..b1b38b36d 100755 --- a/stateengine/StateEngineWebif.py +++ b/stateengine/StateEngineWebif.py @@ -140,16 +140,16 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s else "" action1 = action_dict.get('function') if action1 == 'set': - action2 = action_dict.get('item') - value_check = action_dict.get('value') + action2 = str(action_dict.get('item')) + value_check = str(action_dict.get('value')) value_check = '""' if value_check == "" else value_check - is_number = value_check.lstrip('-').replace('.','',1).isdigit() + is_number = value_check.lstrip('-').replace('.', '', 1).isdigit() if is_number and "." in value_check: value_check = round(float(value_check), 2) action3 = 'to {}'.format(value_check) elif action1 == 'special': - action2 = action_dict.get('special') - action3 = action_dict.get('value') + action2 = str(action_dict.get('special')) + action3 = str(action_dict.get('value')) else: action2 = 'None' action3 = "" @@ -188,10 +188,11 @@ def _conditionlabel(self, state, conditionset, i): current = condition_dict.get('current') match = condition_dict.get('match') - item_none = str(condition_dict.get('item')) == 'None' status_none = str(condition_dict.get('status')) == 'None' - eval_none = condition_dict.get('eval') == 'None' - value_none = condition_dict.get('value') == 'None' + item_none = str(condition_dict.get('item')) == 'None' or not status_none + status_eval_none = condition_dict.get('status_eval') == 'None' + eval_none = condition_dict.get('eval') == 'None' or not status_eval_none + value_none = str(condition_dict.get('value')) == 'None' min_none = condition_dict.get('min') == 'None' max_none = condition_dict.get('max') == 'None' agemin_none = condition_dict.get('agemin') == 'None' @@ -202,17 +203,10 @@ def _conditionlabel(self, state, conditionset, i): for compare in condition_dict: cond1 = not condition_dict.get(compare) == 'None' - cond2 = not compare == 'item' - cond3 = not compare == 'eval' - cond4 = not compare == 'negate' - cond5 = not compare == 'agenegate' - cond6 = not compare == 'changedbynegate' - cond7 = not compare == 'updatedbynegate' - cond8 = not compare == 'triggeredbynegate' - cond9 = not compare == 'status' - cond10 = not compare == 'current' - cond11 = not compare == 'match' - if cond1 and cond2 and cond3 and cond4 and cond5 and cond6 and cond7 and cond8 and cond9 and cond10 and cond11: + excluded_values = ['item', 'eval', 'negate', 'agenegate', 'changedbynegate', + '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: @@ -226,6 +220,7 @@ def _conditionlabel(self, state, conditionset, i): 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_compare = str(condition_dict.get(compare) or '') if not status_none: textlength = len(info_status) @@ -234,13 +229,13 @@ def _conditionlabel(self, state, conditionset, i): condition_tooltip += ' ' tooltip_count += 1 condition_tooltip += '{}'.format(condition_dict.get('status')) - elif not item_none: - textlength = len(info_item) + elif not status_eval_none: + textlength = len(info_status_eval) if textlength > self.__textlimit: if tooltip_count > 0: condition_tooltip += ' ' tooltip_count += 1 - condition_tooltip += '{}'.format(condition_dict.get('item')) + condition_tooltip += '{}'.format(condition_dict.get('status_eval')) elif not eval_none: textlength = len(info_eval) if textlength > self.__textlimit: @@ -248,14 +243,21 @@ def _conditionlabel(self, state, conditionset, i): condition_tooltip += ' ' tooltip_count += 1 condition_tooltip += '{}'.format(condition_dict.get('eval')) + elif not item_none: + textlength = len(info_item) + if textlength > self.__textlimit: + if tooltip_count > 0: + condition_tooltip += ' ' + tooltip_count += 1 + condition_tooltip += '{}'.format(condition_dict.get('item')) else: textlength = 0 info_item = info_item[:self.__textlimit] + '..  ' * int(textlength > self.__textlimit) info_status = info_status[:self.__textlimit] + '..  ' * int(textlength > self.__textlimit) info_eval = info_eval[:self.__textlimit] + '..  ' * int(textlength > self.__textlimit) - info_value = info_compare[:self.__textlimit] + '..  ' * \ - int(len(info_compare) > self.__textlimit) + info_status_eval = info_status_eval[:self.__textlimit] + '..  ' * int(textlength > self.__textlimit) + info_value = info_compare[:self.__textlimit] + '..  ' * int(len(info_compare) > self.__textlimit) textlength = len(info_compare) if textlength > self.__textlimit: if tooltip_count > 0: @@ -265,10 +267,12 @@ def _conditionlabel(self, state, conditionset, i): if not status_none: info = info_status - elif not item_none: - info = info_item + elif not status_eval_none: + info = info_status_eval elif not eval_none: info = info_eval + elif not item_none: + info = info_item else: info = "" conditionlist += '{}'.format(info) @@ -295,12 +299,16 @@ def _conditionlabel(self, state, conditionset, i): else match.get('age') if compare in ["agemin", "agemax", "age"]\ else match.get(compare) conditionlist += '{}'.format(comparison) - conditionlist += '"{}"'.format(info) if not item_none and not status_none and not eval_none else '' + conditionlist += '"{}"'.format(info) if not item_none and not status_none \ + and not eval_none and not status_eval_none else '' info = info_value + cond1 = eval_none and not item_none + cond2 = eval_none and (not status_none or not status_eval_none) + cond3 = not eval_none and item_none + cond4 = not eval_none and status_eval_none and status_none conditionlist += '{}'.format(info) if not condition_dict.get(compare) == 'None' and ( - (eval_none and not item_none) or (eval_none and not status_none) or \ - (not eval_none and item_none) or (not eval_none and status_none)) else '' + cond1 or cond2 or cond3 or cond4) else '' conditionlist += ' (negate)' if condition_dict.get('negate') == 'True' and "age" \ not in compare and not compare == "value" else '' conditionlist += ' (negate)' if condition_dict.get('agenegate') == 'True' and "age" in compare else '' @@ -361,7 +369,6 @@ def drawgraph(self, filename): previousconditionset = '' previousstate = '' previousstate_conditionset = '' - #self._log_debug('STATES {}', self.__states) for i, state in enumerate(self.__states): #self._log_debug('Adding state for webif {}', self.__states[state]) if isinstance(self.__states[state], (OrderedDict, dict)): @@ -422,13 +429,16 @@ def drawgraph(self, filename): 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) + 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) + 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) + 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) diff --git a/stateengine/__init__.py b/stateengine/__init__.py index 7b0598afb..58ae6f5a3 100755 --- a/stateengine/__init__.py +++ b/stateengine/__init__.py @@ -35,6 +35,7 @@ from lib.model.smartplugin import * from lib.item import Items from .webif import WebInterface +from datetime import datetime try: import pydotplus @@ -228,14 +229,23 @@ def get_graph(self, abitem, graphtype='link'): try: if graphtype == 'link': return ''.format(abitem) - else: + elif abitem.firstrun is None: webif.drawgraph(vis_file) - return '\ - '.format(abitem) + try: + change_timestamp = os.path.getmtime(vis_file) + change_datetime = datetime.fromtimestamp(change_timestamp) + formatted_date = change_datetime.strftime('%H:%M:%S, %d. %B') + except Exception: + pass + return f'
{self.translate("Letzte Aktualisierung:")} {formatted_date}
\ + \ + ' + else: + return '' except pydotplus.graphviz.InvocationException as ex: self.logger.error("Problem getting graph for {}. Error: {}".format(abitem, ex)) return '

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

' \ diff --git a/stateengine/locale.yaml b/stateengine/locale.yaml index 229980725..4c8194174 100755 --- a/stateengine/locale.yaml +++ b/stateengine/locale.yaml @@ -18,3 +18,11 @@ plugin_translations: 'SE Item': {'de': '=', 'en': '='} 'Detailvisualisierung': {'de': '=', 'en': 'Detailed Visualization'} 'KeineVisualisierung': {'de': 'Visualisierung nicht verfügbar', 'en': 'Visualization not available'} + 'Zoom +': {'de': '=', 'en': '='} + 'Zoom -': {'de': '=', 'en': '='} + 'Zoom Reset': {'de': '=', 'en': '='} + 'Zoom/Pan aktiv': {'de': '=', 'en': 'Zoom/Pan active'} + 'Klicken zum Öffnen des SVG Files': {'de': '=', 'en': 'Click to open the SVG file'} + 'ist noch nicht initialisiert.': {'de': '=', 'en': 'is not initialized yet.'} + 'Die erste Evaluierung ist geplant für:': {'de': '=', 'en': 'The first evaluation is planned for:'} + 'Letzte Aktualisierung:': {'de': '=', 'en': 'Last Update:'} diff --git a/stateengine/plugin.yaml b/stateengine/plugin.yaml index 377a8b2d2..bd0aa0444 100755 --- a/stateengine/plugin.yaml +++ b/stateengine/plugin.yaml @@ -735,7 +735,6 @@ item_structs: rules: se_item_lock: ..lock eval_trigger: - - merge_unique* - ..lock lock: @@ -871,7 +870,6 @@ item_structs: se_suspend_time: item:..settings.suspendduration.seconds eval_trigger: - - merge_unique* - ..manuell suspend: @@ -1116,14 +1114,14 @@ item_structs: type: str visu_acl: rw cache: True - initial_value: '' + initial_value: 'struct:stateengine.state_standard.rules.standard' additionaluse2: remark: set this value to a struct or (relative) state that should be added to the condition sets if suspendvariant is 2 type: str visu_acl: rw cache: True - initial_value: '' + initial_value: 'struct:stateengine.state_standard.rules.standard' suspendduration: remark: duration of suspend mode in minutes (gets converted automatically) @@ -1175,7 +1173,6 @@ item_structs: se_suspend_time: eval:se_eval.get_relative_itemvalue('..settings.suspendvariant.suspendduration{}.seconds'.format(max(0, min(se_eval.get_relative_itemvalue('..settings.suspendvariant'), 2)))) eval_trigger: - - merge_unique* - ..manuell suspend: @@ -1519,10 +1516,9 @@ item_attribute_prefixes: en: 'Definition wether an action should be repeated or not when reentering the same state (deprecated - use se_action instead)' se_order_: - type: int description: - de: 'Definiert die Reihenfolge einer Aktion (veraltet - Nutze stattdessen se_action)' - en: 'Definition of the running order of an action (deprecated - use se_action instead)' + 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_: description: diff --git a/stateengine/user_doc.rst b/stateengine/user_doc.rst index 58bf3d5ac..b9cdbf3b1 100755 --- a/stateengine/user_doc.rst +++ b/stateengine/user_doc.rst @@ -55,11 +55,5 @@ Das Webinterface bietet folgende Übersichtsinformationen: :alt: Web Interface Overview :align: center -Ein Klick auf das Lupensymbol in der Visu-Spalte öffnet die Detailansicht. Hier ist zu sehen, welcher Zustand eingenommen werden könnte, welcher aktiv ist und welche Aktionen bei welcher Bedingung ausgeführt werden. - - .. image:: user_doc/assets/webif_stateengine_detail.png - :height: 1656px - :width: 3312px - :scale: 25% - :alt: Web Interface Detail - :align: center +Ein Klick auf das Lupensymbol in der Visu-Spalte öffnet die Detailansicht. Hier ist zu sehen, welcher Zustand eingenommen werden könnte, welcher aktiv ist und welche Aktionen bei welcher Bedingung ausgeführt werden. Ein Beispiel ist in der +Sektion zu finden. diff --git a/stateengine/user_doc/01_allgemein.rst b/stateengine/user_doc/01_allgemein.rst index 896d0992a..bcae88653 100755 --- a/stateengine/user_doc/01_allgemein.rst +++ b/stateengine/user_doc/01_allgemein.rst @@ -69,15 +69,23 @@ Webinterface Über das Webinterface lässt sich auf einen Blick erkennen, welche State Engine sich in welchem Zustand befindet. Zusätzlich ist es möglich, durch Klick auf einen Eintrag die komplette State Engine visuell zu betrachten. Dabei ist folgende Farbkodierung zu beachten: + - grau: wurde nicht evaluiert (weil bereits ein höherrangiger Zustand eingenommen wurde) - grün: aktueller Zustand / ausgeführte Aktion - rot: Bedingungen nicht erfüllt +Innerhalb einer Bedingungsgruppe wird bei evaluierten Zuständen ein rotes X angezeigt, +wenn die Bedingung nicht wahr ist oder ein grünes Häkchen, falls die Bedingung erfüllt ist. + Bei den Aktionen sind die einzelnen Zeilen unter Umständen ebenfalls farbkodiert: + - schwarz: Aktion normal ausgeführt - weiß: Aktion nicht ausgeführt, da Bedingungen nicht erfüllt - grau: Aktion wird erst mit Verzögerung ausgeführt - rot: Fehler in der Konfiguration -.. image:: assets/webinterface.png +Zudem wird hinter ausgeführten Aktionen ein grünes Häkchen angezeigt, hinter nicht ausgeführten +(weil beispielsweise Bedingungen nicht erfüllt sind) ein rotes X und hinter Problemen ein Warnsignal. + +.. image:: assets/webif_stateengine_detail.png :class: screenshot diff --git a/stateengine/user_doc/02_konfiguration.rst b/stateengine/user_doc/02_konfiguration.rst index 90ea0f69f..5ae67824e 100755 --- a/stateengine/user_doc/02_konfiguration.rst +++ b/stateengine/user_doc/02_konfiguration.rst @@ -46,10 +46,11 @@ Logging Es gibt zwei Möglichkeiten, den Output des Plugins zu loggen: **intern** -Hierbei werden, sofern das Loglevel 1 oder 2 beträgt, sämtliche Logeinträge in +Hierbei werden, sofern das Loglevel 1 oder mehr beträgt, sämtliche Logeinträge in eigene Dateien in einem selbst definierten Verzeichnis geschrieben. Das Loglevel kann sowohl global in der etc/plugin.yaml Datei deklariert, als auch individuell pro Item mittels ``se_log_level`` (dort wo auch se_plugin: active steht) überschrieben werden. +Wird im Item nichts angegeben oder das Attribut mit dem Wert -1 angegeben, wird der Standardwert herangezogen. **logging.yaml** Sowohl der Output des Plugins generell, als auch der Einträge für bestimmte Items diff --git a/stateengine/user_doc/03_regelwerk.rst b/stateengine/user_doc/03_regelwerk.rst index ab841d13c..90cc18339 100755 --- a/stateengine/user_doc/03_regelwerk.rst +++ b/stateengine/user_doc/03_regelwerk.rst @@ -49,14 +49,18 @@ Diese Items müssen auf Ebene des Regelwerk-Items über das Attribut ``se_item_`` bekannt gemacht werden. Um einfacher zwischen Items, die für Bedingungen und solchen, die für Aktionen genutzt werden, unterscheiden zu können, können Items, die nur für Bedingungen gebraucht werden, mittels ``se_status_`` -deklariert werden. Diese Variante ist auch besonders dann relevant, wenn es zwei separate Items -für "Senden" und "Empfangen" gibt, also z.B. Senden der Jalousiehöhe und Empfangen des aktuellen -Werts vom KNX-Aktor. +deklariert werden. + +Anstatt direkt das Item in Form des absoluten oder relativen Pfades zu setzen, kann auch ein +eval-Ausdruck mittels ``eval:`` angegeben werden. -Anstatt direkt das Item in Form des absoluten oder relativen Pfades mittels ``se_item_`` zu -setzen, kann auch die Angabe ``se_eval_`` genutzt werden. In diesem Fall wird eine beliebige -Funktion anstelle des Itemnamen angegeben. Dies ist primär für das Setzen von "dynamischen" Items -gedacht, allerdings ist es auch möglich, hier einen beliebigen Eval-Ausdruck als Bedingung festzulegen. +.. hint:: + + Aus Kompatibilitätsgründen kann für das Setzen "dynamischer" Items auch die Angabe ``se_eval_`` + oder ``se_status_eval_`` genutzt werden. In diesem Fall wird eine beliebige + Funktion anstelle des Itemnamen angegeben, also beispielsweise + se_eval_height: se_eval.get_relative_item('..test'). Hierzu in den Kapiteln :ref:`Bedingungen` + und :ref:`Aktionen` mehr. Beispiel se_item @@ -64,12 +68,12 @@ Beispiel se_item Im Beispiel wird durch ``se_item_height`` das Item ``beispiel.raffstore1.hoehe`` dem Plugin unter dem Namen "height" bekannt gemacht. Das Item ``beispiel.wetterstation.helligkeit`` -wird durch ``se_item_brightness`` (alternativ via ``se_status_brightness``) als "brightness" referenziert. +wird durch ``se_item_brightness`` als "brightness" referenziert. Auf diese Namen beziehen sich nun in weiterer Folge Bedingungen und Aktionen. Im Beispiel wird im Zustand Nacht das Item ``beispiel.raffstore1.hoehe`` auf den Wert 100 gesetzt, sobald -``beispiel.wetterstation.helligkeit`` den Wert 25 übersteigt. Erklärungen zu Bedingungen -und Aktionen folgen auf den nächsten Seiten. +``beispiel.wetterstation.helligkeit`` den Wert 25 übersteigt. Erklärungen zu Bedingungen und +Aktionen folgen auf den nächsten Seiten. .. code-block:: yaml @@ -91,18 +95,41 @@ und Aktionen folgen auf den nächsten Seiten. enter_toodark: se_max_brightness: 25 +Beispiel se_status +================== + +Wie erwähnt, können Items, die nur für Bedingungen genutzt werden, auch mittels se_status deklariert +werden. Diese Variante ist aber auch besonders dann relevant, wenn es zwei separate Items +für "Senden" und "Empfangen" gibt, also z.B. Senden der Jalousiehöhe und Empfangen des aktuellen +Werts vom KNX-Aktor. + +Im Beispiel wird durch ``se_item_height`` das Item ``beispiel.raffstore1.hoehe`` (das den Befehl an den +KNX Aktor übermittelt) dem Plugin unter dem Namen "height" bekannt gemacht. ``se_status_height`` referenziert auf das +separate Status-Item (das vom KNX Aktor den Rückmeldestatus erhält) ``beispiel.raffstore1.hoehe.status``. +Dies ist aktuell insbesondere dann wichtig, wenn `se_mindelta_height`` genutzt wird (siehe :ref:`Aktionen`). + +.. code-block:: yaml + + raffstore1: + automatik: + struct: stateengine.general + rules: + se_item_height: beispiel.raffstore1.hoehe + se_status_height: beispiel.raffstore1.hoehe.status + se_mindelta_height: 10 + + Standard: + on_enter_or_stay: + se_action_height: + - 'function: set' + - 'to: 100' + + Beispiel se_eval ================ -se_eval ist für Sonderfälle und etwas komplexere Konfigurationen sinnvoll, kann aber -im ersten Durchlauf ignoriert werden. Es wird daher empfohlen, als Beginner -dieses Beispiel einfach zu überspringen ;) - -Im Beispiel wird durch ``se_eval_brightness`` das Item für den Check von -Bedingungen bekannt gemacht. Aufgrund der angegebenen Funktion wird das Item -abhängig vom aktuellen Zustandsnamen eruiert. Da Zustand_Eins den Namen "sueden" -hat, wird somit auch der Wert von wetterstation.helligkeit_sueden abgefragt. -Würde der Zustand "osten" heißen, würde der Helligkeitswert vom Osten getestet werden. +Im Beispiel werden zwei Helligkeitswerte addiert und das Resultat durch 2 geteilt +(also der Mittelwert gebildet). Das Resultat wird dann mit dem Wert 5000 verglichen. .. code-block:: yaml @@ -122,7 +149,7 @@ Würde der Zustand "osten" heißen, würde der Helligkeitswert vom Osten geteste automatik: struct: stateengine.general rules: - se_eval_brightness: se_eval.get_relative_itemvalue('wetterstation.helligkeit_{}'.format(se_eval.get_variable('current.state_name'))) + se_eval_brightness: (se_eval.get_relative_itemvalue('wetterstation.helligkeit_sueden') + se_eval.get_relative_itemvalue('wetterstation.helligkeit_osten'))/2 Zustand_Eins: name: sueden diff --git a/stateengine/user_doc/05_bedingungen.rst b/stateengine/user_doc/05_bedingungen.rst index c72b2e312..62c884579 100755 --- a/stateengine/user_doc/05_bedingungen.rst +++ b/stateengine/user_doc/05_bedingungen.rst @@ -26,6 +26,52 @@ die Helligkeit (über se_item_brightness oder se_status_brightness definiert) ü enter: se_min_brightness: 500 +Name der Bedingung +------------------ + +Der Name einer Bedingung setzt sich aus folgenden drei Teilen zusammen, +die jeweils mit einem Unterstrich "_" getrennt werden: + +- ``se_``: eindeutiger Prefix, um dem Plugin zugeordnet zu werden +- ````: siehe unten. Beispiel: min = der Wert des muss mindestens dem beim Attribut angegebenen Wert entsprechen. +- ````: Hier wird entweder das im Regelwerk-Item mittels ``se_item_`` oder ``se_status_`` deklarierte Item oder eine besondere Bedingung (siehe unten) referenziert. + + +Referenzieren von Items +----------------------- + +Für jede "standardmäßige" Bedingung muss ein Item hinterlegt werden, das geprüft werden soll. +Dies geschieht in der Regel durch ``se_status_``, kann aber auch durch ``se_item_`` +erfolgen, falls z.B. das gleiche Item für Bedingungen und Aktionen gebraucht wird. + +Im Beispiel wird durch ``se_status_brightness`` das Item für den Check von +Bedingungen bekannt gemacht. Aufgrund der angegebenen eval-Funktion wird das Item +abhängig vom aktuellen Zustandsnamen eruiert. Da Zustand_Eins den Namen "sueden" +hat, wird somit der Wert von wetterstation.helligkeit_sueden abgefragt. Ist dieser +mehr als 499, ist die Bedingung erfüllt. Würde der Zustand "osten" heißen (Name von Zustand_Zwei), +würde der Helligkeitswert vom Osten getestet werden. Bedingung wäre dann erfüllt, +wenn die Helligkeit 1500 oder mehr beträge. + +.. code-block:: yaml + + #items/item.yaml + raffstore1: + automatik: + struct: stateengine.general + rules: + se_status_brightness: eval:se_eval.get_relative_itemvalue('wetterstation.helligkeit_{}'.format(se_eval.get_variable('current.state_name'))) + + Zustand_Eins: + name: sueden + enter: + se_min_brightness: 500 + + Zustand_Zwei: + name: osten + enter: + se_min_brightness: 1500 + + Bedingungsgruppen ----------------- @@ -67,18 +113,6 @@ Der zu vergleichende Wert einer Bedingung kann auf folgende Arten definiert werd des StateEngine Items eingesetzt werden kann. Angegeben durch ``template:`` -Name der Bedingung ------------------- - -Der Name einer Bedingung setzt sich aus folgenden drei Teilen zusammen, -die jeweils mit einem Unterstrich "_" getrennt werden: - -- ``se_``: eindeutiger Prefix, um dem Plugin zugeordnet zu werden -- ````: siehe unten. Beispiel: min = der Wert des muss mindestens dem beim Attribut angegebenen Wert entsprechen. -- ````: Hier wird entweder das im Regelwerk-Item mittels ``se_item_`` -oder ``se_status_`` deklarierte Item oder eine besondere Bedingung (siehe unten) referenziert. - - Templates für Bedingungsabfragen -------------------------------- @@ -352,7 +386,7 @@ Freitag, 5 = Samstag, 6 = Sonntag Der Azimut (Horizontalwinkel) ist die Kompassrichtung, in der die Sonne steht. Der Azimut wird von smarthomeNg auf Basis der aktuellen Zeit sowie der konfigurierten geographischen Position -berechnet. Siehe auch `Dokumentation `_ +berechnet. Siehe auch `Dokumentation `_ für Voraussetzungen zur Berechnung der Sonnenposition. Beispielwerte: 0 → Sonne exakt im Norden, 90 → Sonne exakt im Osten, 180 → Sonne exakt im Süden, 270 → Sonne exakt im Westen @@ -363,8 +397,8 @@ Osten, 180 → Sonne exakt im Süden, 270 → Sonne exakt im Westen Die Altitude (Vertikalwikel) ist der Winkel, in dem die Sonne über dem Horizont steht. Die Altitude wird von smarthomeNG auf Basis der aktuellen Zeit sowie der konfigurierten geographischen -Position berechnet. Siehe auch `SmarthomeNG -Dokumentation `_ +Position berechnet. Siehe ebenfalls `SmarthomeNG +Dokumentation `_ für Voraussetzungen zur Berechnung der Sonnenposition. Werte: negativ → Sonne unterhalb des Horizonts, 0 → Sonnenaufgang/Sonnenuntergang, 90 → Sonne exakt im Zenith diff --git a/stateengine/user_doc/06_aktionen.rst b/stateengine/user_doc/06_aktionen.rst index 0182d969b..55d6b187c 100755 --- a/stateengine/user_doc/06_aktionen.rst +++ b/stateengine/user_doc/06_aktionen.rst @@ -9,8 +9,7 @@ Aktionen Es gibt zwei Möglichkeiten, Aktionen zu definieren. Die :ref:`Aktionen - einzeln` Variante wird am Ende der Dokumentation der Vollständigkeit halber beschrieben. Für einfache Aktionen ohne Angabe zusätzlicher Attribute wie delay, order, repeat, etc. -kann diese andere Möglichkeit der Aktionsangabe durchaus Sinn machen. Sie wurde -allerdings in der weiteren Pluginentwicklung nicht mehr getestet. +kann diese andere Möglichkeit der Aktionsangabe durchaus Sinn machen. Bei der hier beschriebenen kombinierten Variante zur Definition von Aktionen werden alle Parameter einer Aktion in einem Attribut definiert. Der Aktionsname ``se_action_`` @@ -29,7 +28,7 @@ das den Wert vom KNX-Aktor empfängt. Außerdem ist es möglich, über ``se_repeat_actions`` generell zu definieren, ob Aktionen für die Stateengine wiederholt ausgeführt werden sollen oder nicht. Diese Konfiguration -kann für einzelne Aktionen individuell über die Angabe ``repeat`` überschrieben werden. Siehe auch :ref:`Aktionen`. +kann für einzelne Aktionen individuell über die Angabe ``repeat`` überschrieben werden. Beispiel zu Aktionen -------------------- @@ -346,20 +345,20 @@ Die einzelnen Angaben einer Liste werden als ``OR`` evaluiert. .. code-block:: yaml -screens: - conditionset_to_check: - type: str - value: "screens.osten_s1.automatik.rules.abend.enter_abend" + screens: + conditionset_to_check: + type: str + value: "screens.osten_s1.automatik.rules.abend.enter_abend" - conditionset: - - regex:enter_(.*)_test - - eval:sh.screens.conditionset_to_check.property.name + conditionset: + - regex:enter_(.*)_test + - eval:sh.screens.conditionset_to_check.property.name Der gesamte Pfad könnte wie folgt evaluiert werden: .. code-block:: yaml - "eval:se_eval.get_relative_itemid('{}.'.format(se_eval.get_relative_itemvalue('..state_id')))" + "eval:se_eval.get_relative_itemid('{}.'.format(se_eval.get_relative_itemvalue('..state_id')))" Eine sinnvolle Anwendung hierfür wäre, anstelle von verschiedenen Zuständen mit leicht anderen Bedingungen, alles in einen Zustand zu packen und anhand des Conditionsets diff --git a/stateengine/user_doc/07_zeitpunkt.rst b/stateengine/user_doc/07_zeitpunkt.rst index dadb59360..98774cb2e 100755 --- a/stateengine/user_doc/07_zeitpunkt.rst +++ b/stateengine/user_doc/07_zeitpunkt.rst @@ -52,7 +52,7 @@ Die Konfiguration von instant_leaveaction bestimmt, ob on_leave Aktionen sofort eines Zustands ausgeführt werden oder erst am Ende der Statusevaluierung. Die Option kann sowohl in der globalen Pluginkonfiguration mittels ``instant_leaveaction`` (boolscher Wert True oder False), als auch pro Item -mittels ``se_instant_leaveaction``festgelegt werden. Letzteres Attribut kann auch +mittels ``se_instant_leaveaction`` festgelegt werden. Letzteres Attribut kann auch auf ein Item verweisen, dem der Wert -1 = Nutzen des Default Wertes, 0 = False, 1 = True zugewiesen werden kann. Im ``general struct`` sind bereits entsprechende Einträge und Items angelegt (mit einem Wert von -1). diff --git a/stateengine/user_doc/08_beispiel.rst b/stateengine/user_doc/08_beispiel.rst index c4767ab0b..8a300d35b 100755 --- a/stateengine/user_doc/08_beispiel.rst +++ b/stateengine/user_doc/08_beispiel.rst @@ -390,14 +390,14 @@ Beim zweiten Durchlauf wird somit der Zustand Sonnenschutz aktiviert. Der Raffst Let's play god. Ändern wir das Wetter ;) Entweder über das CLI, Visu oder Backend-Plugin oder Admin-Interface: -c) up beispiel.wetterstation.helligkeit=35000 +c) beispiel.wetterstation.helligkeit=35000 - Die erste Bedingungsgruppe des Sonnenstandzustands ist nicht mehr "wahr", da die Helligkeit zu niedrig ist. - Es wird ``enter_hysterese`` evaluiert. Da die Helligkeit noch über 25000 und die Sonnenposition gleich wie zuvor ist, ist diese Gruppe wahr. Der Sonnenschutz bleibt somit aktiv, weil trotz der Helligkeitsverringerung der untere Schwellwert noch überschritten wurde. Der Raffstore bleibt unten. -d) up beispiel.wetterstation.helligkeit=15000 +d) beispiel.wetterstation.helligkeit=15000 - Die ersten beiden Bedingungsgruppen sind unwahr, da die Helligkeit zu gering ist. - Durch den Eintrag ``se_agemax_brightnessGt25k: 60`` in der Gruppe ``enter_delay`` wird 60 Sekunden gewartet. @@ -411,13 +411,13 @@ e) Es erfolgt eine weitere Evaluierung des Automaten durch das cycle Attribut: Der Zustand wird verlassen. Gibt es einen nachfolgenden Zustand, der eingenommen werden kann, ist dies der neue aktive Zustand. Gibt es keine Zustände, die aktiviert werden könnten, verbleibt die State Engine beim letzten aktiven Zustand, also beim Sonnenschutz. Im Beispiel gibt es noch einen Standard "Tag" Eintrag, wodurch der Raffstore hoch fährt. -f) up beispiel.raffstore1.aufab = 1 +f) beispiel.raffstore1.aufab = 1 - Durch Triggern des "Manuell" Items wird die Zustandsevaluierung pausiert. Sämtliche Änderungen der Helligkeit, Temperatur, etc. werden für die suspend_time ignoriert. Die Dauer ist im Template auf 60 Minuten festgelegt, kann aber manuell durch Ändern des entsprechenden Items geändert werden. -g) up beispiel.raffstore1.automatik.settings.suspendduration = 1 +g) beispiel.raffstore1.automatik.settings.suspendduration = 1 - Die Suspendzeit wird auf eine Minute verkürzt. - Beim erneuten Durchlauf ist die Suspendzeit abgelaufen, daher dieser Zustand nicht mehr aktiv. @@ -639,7 +639,7 @@ Settings für Itemwerte ---------------------- Das Setup ist besonders flexibel, wenn zu setzende Werte nicht fix in den Zustandsvorgaben -definiert werden, sondern in eigenen Items, die dann jederzeit zur Laufzeit abänderbar +definiert werden, sondern in eigenen Items, die dann jederzeit zur Laufzeit änderbar sind. Das folgende Beispiel zeigt eine Leuchte, die abhängig vom aktuell definierten Lichtmodus (z.B. über die Visu) verschiedene Stati einnimmt und immer wieder dieselben Änderungen vornimmt. Sollte eine Änderung nicht möglich sein, weil das entsprechende @@ -649,8 +649,6 @@ Die Struct-Vorlagen sehen dabei folgendermaßen aus. Besonders ist der Eval Ausd Dieser führt dazu, dass der zu setzende Wert aus dem Item ``automatik.settings..sollwert`` im aktuellen Item gelesen wird. Somit kann diese Vorlage für sämtliche Zustände 1:1 eingesetzt werden, wobei natürlich zu beachten ist, dass sowohl "Settings" als auch Zustand richtig benannt sind. -Das Item state_name wird bis zur Pluginversion 1.5.0 erst nach Ausführen der Aktionen aktualisiert, -weshalb diese Vorgehensweise erst ab 1.5.1 empfohlen wird. .. code-block:: yaml @@ -758,7 +756,7 @@ Letzten Endes wird alles in einem item.yaml auf folgende Art und Weise implement - licht_rules_heimkino - licht_rules_lichtkurve - remark: Das eval_trigger muss vor SmarthomeNG 1.7 noch manuell mit der kompletten Liste überschrieben werden, auch wenn die Structs bereits Einträge enthalten. Ab 1.7 würde licht.modus* ausreichen! + remark: Das eval_trigger muss vor SmarthomeNG 1.7 noch manuell mit der kompletten Liste überschrieben werden, auch wenn die Structs bereits Einträge enthalten. Ab 1.7 würde merge_unique* und licht.modus* ausreichen! eval_trigger: - ..settings_edited - ..lock diff --git a/stateengine/user_doc/09_vorlagen.rst b/stateengine/user_doc/09_vorlagen.rst index bd72ebd68..68ce0409e 100755 --- a/stateengine/user_doc/09_vorlagen.rst +++ b/stateengine/user_doc/09_vorlagen.rst @@ -43,9 +43,7 @@ können wie folgt eingebunden werden: rules: eval_trigger: - - ..lock - - ..supsend - - .. release + - merge_unique* - beispiel.trigger additional_state1: @@ -63,81 +61,17 @@ general Die ``general`` Vorlage enthält die Items, die generell für einen Zustandsautomaten angelegt werden sollten. Das "rules" Item ist das Regelwerk-Item mit aktiviertem -se_plugin. Dieser Codeblock wird zwingend von jedem Zustandsautomaten benötigt. - -.. code-block:: yaml - - #stateengine.general - state_id: - # The id/path of the actual state is assigned to this item by the stateengine - type: str - visu_acl: r - cache: True - - state_name: - # The name of the actual state is assigned to this item by the stateengine - type: str - visu_acl: r - cache: True - - conditionset_id: - remark: The id/path of the actual condition set is assigned to this item by the stateengine - type: str - visu_acl: r - cache: True - - conditionset_name: - remark: The name of the actual condition set is assigned to this item by the stateengine - type: str - visu_acl: r - cache: True - - rules: - name: Regeln und Item Verweise für den Zustandsautomaten - type: bool - se_plugin: active - eval: True - - # se_startup_delay: 30 - # se_repeat_actions: true - # se_suspend_time: 7200 - - se_laststate_item_id: ..state_id - se_laststate_item_name: ..state_name - se_lastconditionset_item_id: ..conditionset_id - se_lastconditionset_item_name: ..conditionset_name +se_plugin. Außerdem werden zwei Settings Items angelegt, um das Log Level und +"Instant Leaveaction" per Item konfigurieren und zur Laufzeit ändern zu können. +Dieser Codeblock wird zwingend von jedem Zustandsautomaten benötigt. lock ==== Die ``state_lock`` Vorlage beinhaltet zum einen den Lock Zustand mit dem Namen "gesperrt", zum anderen ein Item mit dem Namen ``lock``. Wird dieses auf "1/True" gesetzt, wird der -Zustand eingenommen. Der Zustand sollte immer als erster Zustand eingebunden werden. - -.. code-block:: yaml - - #stateengine.state_lock - lock: - type: bool - knx_dpt: 1 - visu_acl: rw - cache: 'on' - - rules: - se_item_lock: ..lock - eval_trigger: - - ..lock - - lock: - name: gesperrt - - on_leave: - se_action_lock: - - 'function: set' - - 'to: False' - - enter: - se_value_lock: True +Zustand so lange eingenommen, bis das Item wieder auf False gestellt wird. So lässt sich zeitweise +die Evaluierung anderer Zustände pausieren. Der Zustand sollte immer als erster Zustand eingebunden werden. suspend ======= @@ -162,198 +96,19 @@ Setzt man das Item ``settings.suspend_active`` auf False, wird der Pause-Zustand deaktiviert und manuelle Betätigungen werden beim nächsten Durchlauf eventuell durch andere Zustände überschrieben. -.. code-block:: yaml +suspend_dynamic +=============== - #stateengine.state_suspend - state_suspend: - name: Zustandsvorlage für manuelles Aussetzen - - suspend: - type: bool - knx_dpt: 1 - visu_acl: rw - cache: True - - visu: - type: bool - knx_dpt: 1 - visu_acl: rw - cache: True - - suspend_end: - type: str - visu_acl: ro - eval: "'' if not any(char.isdigit() for char in sh..self.date_time()) else sh..self.date_time().split(' ')[1].split('.')[0]" - eval_trigger: .date_time - crontab: init - - date_time: - type: str - visu_acl: ro - cache: True - - unix_timestamp: - type: num - visu_acl: ro - eval: "0 if not any(char.isdigit() for char in sh...date_time()) else sh.tools.dt2ts(shtime.datetime_transform(sh...date_time())) * 1000" - eval_trigger: ..date_time - crontab: init - - suspend_start: - type: str - visu_acl: ro - eval: "'' if not any(char.isdigit() for char in sh..self.date_time()) else sh..self.date_time().split(' ')[1].split('.')[0]" - eval_trigger: .date_time - crontab: init - - date_time: - type: str - visu_acl: ro - cache: True - - unix_timestamp: - remark: Can be used for the clock.countdown widget - type: num - visu_acl: ro - eval: "0 if not any(char.isdigit() for char in sh...date_time()) else sh.tools.dt2ts(shtime.datetime_transform(sh...date_time())) * 1000" - eval_trigger: ..date_time - crontab: init - - manuell: - type: bool - name: manuell - se_manual_invert: True - remark: Adapt the se_manual_exclude the way you need it - #se_manual_include: KNX:* Force manual mode based on source - se_manual_exclude: - - database:* - - init:* - - retrigger: - remark: Item to retrigger the rule set evaluation - type: bool - visu_acl: rw - enforce_updates: True - on_update: ..rules = True - - settings: - remark: Use these settings for your condition values - type: foo - 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 - - suspendduration: - remark: duration of suspend mode in minutes (gets converted automatically) - type: num - visu_acl: rw - cache: True - initial_value: 60 - on_change: .seconds = value * 60 if not sh..self.property.last_change_by == "On_Change:{}".format(sh..seconds.property.path) else None - on_update: .seconds = value * 60 if "Init" in sh..self.property.last_update_by else None - - duration_format: - remark: Can be used for the clock.countdown widget - type: str - cache: True - visu_acl: ro - eval: "'{}d {}h {}i {}s'.format(int(sh...seconds()//86400), int((sh...seconds()%86400)//3600), int((sh...seconds()%3600)//60), round((sh...seconds()%3600)%60))" - eval_trigger: - - ..seconds - - .. - - seconds: - remark: duration of suspend mode in seconds (gets converted automatically) - type: num - visu_acl: rw - cache: True - on_change: .. = value / 60 if not sh..self.property.last_change_by in [ "On_Change:{}".format(sh....property.path), "On_Update:{}".format(sh....property.path)] else None - - suspend_active: - remark: Use this to (de)activate suspend mode in general - type: bool - visu_acl: rw - cache: True - initial_value: True - - settings_edited: - type: bool - name: settings editiert - eval_trigger: ...settings.* - eval: not sh..self() - on_update: ...retrigger = True if sh..self.property.prev_update_age > 0.1 else None - - rules: - se_item_suspend: ..suspend - se_item_suspend_visu: ..suspend.visu - se_item_suspend_end: ..suspend_end.date_time - se_item_suspend_start: ..suspend_start.date_time - se_item_suspend_active: ..settings.suspend_active - se_suspend_time: ..settings.suspendduration - - eval_trigger: - - ..manuell - - suspend: - name: ausgesetzt - - on_enter: - se_action_suspend_visu: - - 'function: set' - - 'to: True' - - 'order: 2' - - on_enter_or_stay: - se_action_suspend: - - 'function: special' - - 'value: suspend:..suspend, ..manuell' - - 'repeat: True' - - 'order: 1' - se_action_suspend_end: - - 'function: set' - - "to: eval:se_eval.insert_suspend_time('..suspend', suspend_text='%Y-%m-%d %H:%M:%S.%f%z')" - - 'repeat: True' - - 'order: 3' - se_action_suspend_start: - - 'function: set' - - "to: eval:str(shtime.now())" - - 'repeat: True' - - 'conditionset: enter_manuell' - - 'order: 4' - se_action_retrigger: - - 'function: special' - - 'value: retrigger:..retrigger' - - 'delay: var:item.suspend_remaining' - - 'repeat: True' - - 'order: 5' - - on_leave: - se_action_suspend: - - 'function: set' - - 'to: False' - - 'order: 2' - se_action_suspend_visu: - - 'function: set' - - 'to: False' - - 'order: 3' - se_action_suspend_end: - - 'function: set' - - 'to: ' - - 'order: 4' - se_action_suspend_start: - - 'function: set' - - 'to: ' - - 'order: 5' - - 'delay: 1' - - enter_manuell: - se_value_trigger_source: eval:se_eval.get_relative_itemproperty('..manuell', 'path') - se_value_suspend_active: True - - enter_stay: - se_value_laststate: var:current.state_id - se_agemax_suspend: var:item.suspend_time - se_value_suspend: True - se_value_suspend_active: True +Eine Variante des Suspendmodus, bei dem es möglich ist, bis zu drei verschiedene +Suspendzeiten zu deklarieren. Außerdem kann man definieren, ob noch zusätzliche Zustände +integriert werden sollen. Dabei ist zu beachten, dass standardmäßig der "Standard"-Status +mit eingebunden wird. Da dieser leer ist, wird nichts passieren. Bei Bedarf kann der Wert +in den Items ``automatik.settings.suspendvariant.additionaluse[0-2]`` geändert werden. + +Welche Zeiten und Zustände letztlich genutzt werden, wird durch Setzen des Items +``suspendvariant`` bestimmt. Der Wert muss zwischen 0 und 2 liegen. + +Weitere Informationen sind unter :ref:`Besondere Zustände` zu finden. release ======= @@ -362,54 +117,11 @@ Die ``state_release`` Vorlage ist nicht unbedingt nötig, kann aber dazu genutzt schnell den Sperr- oder Pause-Zustand zu verlassen und die erneute Evaluierung der Zustände anzuleiern. -.. code-block:: yaml - - #stateengine.state_release - release: #triggers the release - type: bool - knx_dpt: 1 - visu_acl: rw - enforce_updates: True - - rules: - se_item_lock: ..lock - se_item_suspend: ..suspend - se_item_retrigger: ..rules - se_item_release: ..release - se_item_suspend_end: ..suspend_end - eval_trigger: - - ..release - - release: - name: release - - on_enter_or_stay: - se_action_suspend: - - 'function: set' - - 'to: False' - - 'order: 1' - se_action_lock: - - 'function: set' - - 'to: False' - - 'order: 2' - se_action_release: - - 'function: set' - - 'to: False' - - 'order: 3' - se_action_suspend_end: - - 'function: set' - - 'to: ' - - 'order: 4' - se_action_retrigger: - - 'function: set' - - 'to: True' - - 'order: 5' - - 'repeat: True' - - 'delay: 1' - - enter: - se_value_release: True +standard +======== +Ein praktisch leerer Status, der immer am Ende angehängt werden sollte. Dieser Status wird +eingenommen, wenn keine Bedingungen der anderen Stati erfüllt sind. Pluginspezifische Templates --------------------------- diff --git a/stateengine/user_doc/11_sonderzustaende.rst b/stateengine/user_doc/11_sonderzustaende.rst index c59f12456..bda77918c 100755 --- a/stateengine/user_doc/11_sonderzustaende.rst +++ b/stateengine/user_doc/11_sonderzustaende.rst @@ -284,8 +284,37 @@ abweichend sein soll, kann dort das Attribut angegeben werden. Der Parameter kann auch durch ein Item oder eval festgelegt werden. Letzteres ermöglicht es, je nach Situation die Suspenddauer von verschiedenen Items -abhängig zu machen. Im struct ``state_suspend_dynamic`` wird hier das Item automatik.settings.suspendduration.seconds verknüpft bzw. -für die verschiedenen "suspendvariants" automatik.settings.suspendvariant.suspendduration[0-2].seconds. +abhängig zu machen. Im struct ``state_suspend_dynamic`` wird hier das +Item automatik.settings.suspendduration.seconds verknüpft bzw. +für die verschiedenen "suspendvariants" die Items automatik.settings.suspendvariant.suspendduration[0-2].seconds. Hierzu ist im struct ein Item settings.suspendvariant integriert, das einen numerischen Wert zwischen 0 und 2 erwartet. 0 ist dabei die "normale" Funktionsweise, eine 1 würde auf die duration1 und eine 2 auf die duration2 verweisen. + +Um diese unterschiedlichen Dauerangaben zu nutzen, ist der Wert von suspendvariant in den entsprechenden +Zuständen zu setzen. Außerdem sollte beim Beenden des Suspendstatus der Wert wieder auf 0 oder den +vorherigen Wert gesetzt werden (was im entsprechenden Struct auch passiert). + +.. code-block:: yaml + + #items/item.yaml + beispiel: + raffstore1: + automatik: + struct: + - stateengine.general + - stateengine.state_release + - stateengine.state_lock + - stateengine.state_suspend_dynamic + - beschattung_se_state_abend + - beschattung_se_state_nacht + - beschattung_se_state_schnee + - beschattung_se_state_standard + + rules: + nacht: + on_leave: + se_set_suspendvariant: 1 + schnee: + on_leave: + se_set_suspendvariant: 2 diff --git a/stateengine/user_doc/12_aktioneneinzeln.rst b/stateengine/user_doc/12_aktioneneinzeln.rst index 62a88a809..9363459ae 100755 --- a/stateengine/user_doc/12_aktioneneinzeln.rst +++ b/stateengine/user_doc/12_aktioneneinzeln.rst @@ -46,12 +46,12 @@ Einziger Unterschied ist, dass die Wertänderung erzwungen wird: Wenn das Item bereits den zu setzenden Wert hat, dann ändert smarthomeNG das Item nicht. Selbst wenn beim Item das Attribut ``enforce_updates: yes`` gesetzt ist, wird zwar der Wert neu -gesetzt, der von smarthomeNG die Änderungszeit nicht neu gesetzt. Mit +gesetzt, aber nicht die Änderungszeit. Mit dem Attribut ``se_force_`` wird das Plugin den Wert des Items bei Bedarf zuerst auf einen anderen Wert ändern und dann auf dem Zielwert setzen. Damit erfolgt auf jeden Fall eine Wertänderung (ggf. sogar zwei) mit allen damit in Zusammenhang -stehenden Änderungen (eval's, Aktualisierung der Änderungszeiten, +stehenden Änderungen (evals, Aktualisierung der Änderungszeiten, etc). **Aktion run: Ausführen einer Funktion** @@ -164,7 +164,7 @@ Aktion ausgeführt werden soll. Die Angabe erfolgt in Sekunden oder mit dem Suffix "m" in Minuten. Der Timer zur Ausführung der Aktion nach der angegebenen -Verzögerung wird entfernt, wenn eine gleichartike Aktion +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 diff --git a/stateengine/user_doc/assets/webif_stateengine_detail.png b/stateengine/user_doc/assets/webif_stateengine_detail.png old mode 100755 new mode 100644 index a3dccc55d..817b27003 Binary files a/stateengine/user_doc/assets/webif_stateengine_detail.png and b/stateengine/user_doc/assets/webif_stateengine_detail.png differ diff --git a/stateengine/user_doc/assets/webinterface.png b/stateengine/user_doc/assets/webinterface.png deleted file mode 100755 index 5427b61c0..000000000 Binary files a/stateengine/user_doc/assets/webinterface.png and /dev/null differ diff --git a/stateengine/webif/__init__.py b/stateengine/webif/__init__.py index de8ba7332..e5d6f224f 100755 --- a/stateengine/webif/__init__.py +++ b/stateengine/webif/__init__.py @@ -84,7 +84,7 @@ def index(self, action=None, item_id=None, item_path=None, reload=None, abitem=N if self.vis_enabled: self.plugin.get_graph(abitem, 'graph') tmpl = self.tplenv.get_template('visu.html') - return tmpl.render(p=self.plugin, item=abitem, + return tmpl.render(p=self.plugin, item=abitem, firstrun=str(abitem.firstrun), language=self.plugin.get_sh().get_defaultlanguage(), now=self.plugin.shtime.now()) # add values to be passed to the Jinja2 template eg: tmpl.render(p=self.plugin, interface=interface, ...) return tmpl.render(p=self.plugin, @@ -103,17 +103,15 @@ def get_data_html(self, dataSet=None): :param dataSet: Dataset for which the data should be returned (standard: None) :return: dict with the data needed to update the web page. """ - if dataSet is None: - # get the new data - data = {} - for item in self.plugin.get_items(): - conditionset = item.lastconditionset_name - conditionset = "-" if conditionset == "" else conditionset - log_level = str(item.logger.log_level) - data.update({item.id: {'laststate': item.laststate_name, - 'lastconditionset': conditionset, 'log_level': log_level}}) + + if dataSet and isinstance(dataSet, str): try: - return json.dumps(data) + dataSet = self.plugin.abitems[dataSet] except Exception as e: - self.logger.error(f"get_data_html exception: {e}") - return {} + self.logger.warning("Item {} not initialized yet. " + "Try again later. Error: {}".format(dataSet, e)) + return json.dumps({"success": "error"}) + if self.vis_enabled and dataSet.firstrun is None: + self.plugin.get_graph(dataSet, 'graph') + return json.dumps({"success": "true"}) + return json.dumps({"success": "false"}) diff --git a/stateengine/webif/static/panzoom.min.js b/stateengine/webif/static/panzoom.min.js new file mode 100644 index 000000000..8d3ad220b --- /dev/null +++ b/stateengine/webif/static/panzoom.min.js @@ -0,0 +1,6 @@ +/** +* Panzoom for panning and zooming elements using CSS transforms +* Copyright Timmy Willison and other contributors +* https://github.com/timmywil/panzoom/blob/main/MIT-License.txt +*/ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Panzoom=e()}(this,function(){"use strict";var Y=function(){return(Y=Object.assign||function(t){for(var e,n=1,o=arguments.length;n{{ _('Übersicht') }} + {% endblock buttons %} +{% block pluginstyles %} + +{% endblock pluginstyles %} +{% block pluginscripts %} + + + + + + +{% endblock pluginscripts %} +{% set update_interval = 0 %} {% set logo_frame = false %} +{% set dataSet = item %} {% set tab1title = "" ~ _('Visualisierung') ~ "" %} {% set tabcount = 1 %} {% block bodytab1 %} - -
-
+{% if firstrun == 'None' %} +
{{ _('Detailvisualisierung') }} - {{ item }} - {{ _('Klicken für volle Größe') }} - {{ p.get_graph(item, 'graph') }} + {{ _('Klicken zum Öffnen des SVG Files') }} +
+ Mittels Buttons und Slider kann jederzeit gezoomt werden. Ist die Zoom-Funktion aktiviert, + kann zusätzlich bei Halten der "Shift"-Taste mittels Mausrad in der Grafik gezoomt werden. + Die linke Maustaste ermöglicht dann auch ein Verschieben des Ausschnitts. Dabei werden + allerdings die Tooltips nicht angezeigt - hierfür ist die Zoom-Funktion zu deaktivieren. + +
+
+ + + + + {{_('Zoom/Pan aktiv')}} +
+ +
+ + {{ p.get_graph(item, 'graph') }} +
+{% else %} +
+ {{ item }} {{ _('ist noch nicht initialisiert.') }} {{ _('Die erste Evaluierung ist geplant für:') }} {{ firstrun }}
-
+
+ +
+{% endif %} + {% endblock bodytab1 %}