From 522b43a7ec245309066e6faa6e3dc307174fbbe1 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 13 Sep 2023 22:27:58 +0200 Subject: [PATCH 01/11] stateengine plugin: fix potential issue with releasedby because of deepcopy. --- stateengine/StateEngineValue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index 329125021..d01011ab6 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -156,7 +156,7 @@ def __resetvalue(self): # value: string indicating value or source of value # name: name of object ("time" is being handled differently) def set(self, value, name="", reset=True, item=None): - value = copy.deepcopy(value) + value = copy.copy(value) if reset: self.__resetvalue() if isinstance(value, list): @@ -372,6 +372,7 @@ def set(self, value, name="", reset=True, item=None): self.__issues = StateEngineTools.flatten_list(self.__issues) self.__listorder = StateEngineTools.flatten_list(self.__listorder) self.__type_listorder = StateEngineTools.flatten_list(self.__type_listorder) + del value return self.__listorder, self.__type_listorder, self.__issues # Set cast function From c503df4efa2276943d0785a59364314875941ac5 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 14 Sep 2023 16:30:12 +0200 Subject: [PATCH 02/11] stateengine plugin: use get_fullname as plugin identification (used as caller, etc.) --- stateengine/StateEngineDefaults.py | 3 +++ stateengine/__init__.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/stateengine/StateEngineDefaults.py b/stateengine/StateEngineDefaults.py index 5486edeca..6b4a24500 100755 --- a/stateengine/StateEngineDefaults.py +++ b/stateengine/StateEngineDefaults.py @@ -36,8 +36,11 @@ logger = None +plugin_version = 0 + def write_to_log(logger): + logger.info("Plugin {0} Version {1}".format(plugin_identification, plugin_version)) logger.info("StateEngine default suntracking offset = {0}".format(suntracking_offset)) logger.info("StateEngine default suntracking lamella open value = {0}".format(lamella_open_value)) logger.info("StateEngine default startup delay = {0}".format(startup_delay)) diff --git a/stateengine/__init__.py b/stateengine/__init__.py index 58ae6f5a3..dd10a4dad 100755 --- a/stateengine/__init__.py +++ b/stateengine/__init__.py @@ -96,6 +96,8 @@ def __init__(self, sh): StateEngineDefaults.suntracking_offset = self.get_parameter_value("lamella_offset") StateEngineDefaults.lamella_open_value = self.get_parameter_value("lamella_open_value") + StateEngineDefaults.plugin_identification = self.get_fullname() + StateEngineDefaults.plugin_version = self.PLUGIN_VERSION StateEngineDefaults.write_to_log(self.logger) StateEngineCurrent.init(self.get_sh()) @@ -236,7 +238,7 @@ def get_graph(self, abitem, graphtype='link'): change_datetime = datetime.fromtimestamp(change_timestamp) formatted_date = change_datetime.strftime('%H:%M:%S, %d. %B') except Exception: - pass + formatted_date = "Unbekannt" return f'
{self.translate("Letzte Aktualisierung:")} {formatted_date}
\ \ From 4905a0e150e607f4a47d8a721ba9429a7bd84830 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 14 Sep 2023 16:48:26 +0200 Subject: [PATCH 03/11] stateengine plugin: fix log level in web interface --- stateengine/webif/__init__.py | 3 ++- stateengine/webif/templates/index.html | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/stateengine/webif/__init__.py b/stateengine/webif/__init__.py index 1c3021bc5..7bc847f22 100755 --- a/stateengine/webif/__init__.py +++ b/stateengine/webif/__init__.py @@ -109,7 +109,8 @@ def get_data_html(self, dataSet=None): for item in self.plugin.get_items(): conditionset = item.lastconditionset_name conditionset = "-" if conditionset == "" else conditionset - data.update({item.id: {'laststate': item.laststate_name, 'lastconditionset': conditionset}}) + ll = item.logger.log_level_as_num + data.update({item.id: {'laststate': item.laststate_name, 'lastconditionset': conditionset, 'log_level': ll}}) try: return json.dumps(data) except Exception as e: diff --git a/stateengine/webif/templates/index.html b/stateengine/webif/templates/index.html index 183d5d6f1..1213ca83f 100755 --- a/stateengine/webif/templates/index.html +++ b/stateengine/webif/templates/index.html @@ -143,7 +143,7 @@ {% endif %} - {{ item.logger.log_level }} + {{ item.logger.log_level_as_num }} {% for cond in item.webif_infos.keys() %}{% if not p.itemsApi.return_item(cond) == None %}{% if loop.index > 1 %},{% endif %}{{ p.itemsApi.return_item(cond)._name.split('.')[-1] }}{% endif %}{% endfor %} From 032573f50791c0f2f852a6d963e5add97d2eaee9 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Fri, 15 Sep 2023 00:21:50 +0200 Subject: [PATCH 04/11] stateengine plugin: make releasedby feature work again ;) --- stateengine/StateEngineState.py | 18 +++++++++--------- stateengine/StateEngineValue.py | 5 +++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index 06d17186f..3edf622fb 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -84,7 +84,7 @@ def releasedby(self): @releasedby.setter def releasedby(self, value): - self.__releasedby.set(value) + self.__releasedby.set(value, "", True, None, False) @property def can_release(self): @@ -92,7 +92,7 @@ def can_release(self): @can_release.setter def can_release(self, value): - self.__can_release.set(value) + self.__can_release.set(value, "", True, None, False) @property def has_released(self): @@ -100,7 +100,7 @@ def has_released(self): @has_released.setter def has_released(self, value): - self.__has_released.set(value) + self.__has_released.set(value, "", True, None, False) @property def was_releasedby(self): @@ -108,7 +108,7 @@ def was_releasedby(self): @was_releasedby.setter def was_releasedby(self, value): - self.__was_releasedby.set(value) + self.__was_releasedby.set(value, "", True, None, False) @property def is_copy_for(self): @@ -116,7 +116,7 @@ def is_copy_for(self): @is_copy_for.setter def is_copy_for(self, value): - self.__is_copy_for.set(value) + self.__is_copy_for.set(value, "", True, None, False) # Constructor # abitem: parent SeItem instance @@ -313,10 +313,10 @@ def refill(self): def update_releasedby_internal(self, states=None): if states == []: - _returnvalue, _returntype, _issue = self.__releasedby.set([None]) + _returnvalue, _returntype, _issue = self.__releasedby.set([None], "", True, None, False) elif states: self._log_develop("Setting releasedby to {}", states) - _returnvalue, _returntype, _issue = self.__releasedby.set(states) + _returnvalue, _returntype, _issue = self.__releasedby.set(states, "", True, None, False) self._log_develop("returnvalue {}", _returnvalue) else: _returnvalue, _returntype, _, _issue = self.__releasedby.set_from_attr(self.__item, "se_released_by") @@ -324,9 +324,9 @@ def update_releasedby_internal(self, states=None): def update_can_release_internal(self, states): if states == []: - _returnvalue, _returntype, _issue = self.__can_release.set([None]) + _returnvalue, _returntype, _issue = self.__can_release.set([None], "", True, None, False) elif states: - _returnvalue, _returntype, _issue = self.__can_release.set(states) + _returnvalue, _returntype, _issue = self.__can_release.set(states, "", True, None, False) else: _returnvalue, _returntype, _issue = [None], [None], None return _returnvalue, _returntype, _issue diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index d01011ab6..1ca824350 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -155,8 +155,9 @@ def __resetvalue(self): # Set value # value: string indicating value or source of value # name: name of object ("time" is being handled differently) - def set(self, value, name="", reset=True, item=None): - value = copy.copy(value) + def set(self, value, name="", reset=True, item=None, copyvalue=True): + if copyvalue is True: + value = copy.copy(value) if reset: self.__resetvalue() if isinstance(value, list): From cd7167c1595c3d728d6b8246017672156a9d5618 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Fri, 15 Sep 2023 00:23:03 +0200 Subject: [PATCH 05/11] stateengine plugin: check if caller is plugin corrected --- stateengine/StateEngineFunctions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stateengine/StateEngineFunctions.py b/stateengine/StateEngineFunctions.py index dc66feee0..e8e417940 100755 --- a/stateengine/StateEngineFunctions.py +++ b/stateengine/StateEngineFunctions.py @@ -24,6 +24,7 @@ import re from . import StateEngineLogger from . import StateEngineTools +from . import StateEngineDefaults from lib.item import Items @@ -97,7 +98,7 @@ def manual_item_update_eval(self, item_id, caller=None, source=None): elog.info("get_caller({0}, {1}): original trigger by {2}:{3}", caller, source, original_caller, original_source) original = "{}:{}".format(original_caller, original_source) - entry = re.compile("Stateengine Plugin", re.IGNORECASE) + entry = re.compile(StateEngineDefaults.plugin_identification, re.IGNORECASE) result = entry.match(original) if result is not None: elog.info("Manual item updated by Stateengine Plugin. Ignoring change and writing value {}", From 3af85b52d1fdde846cdc4081e7eca4e926c80e75 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Fri, 15 Sep 2023 10:52:39 +0200 Subject: [PATCH 06/11] stateengine plugin: fix and improve manual item check --- stateengine/StateEngineFunctions.py | 103 ++++++++++------------------ 1 file changed, 35 insertions(+), 68 deletions(-) diff --git a/stateengine/StateEngineFunctions.py b/stateengine/StateEngineFunctions.py index e8e417940..a5de0835b 100755 --- a/stateengine/StateEngineFunctions.py +++ b/stateengine/StateEngineFunctions.py @@ -26,6 +26,7 @@ from . import StateEngineTools from . import StateEngineDefaults from lib.item import Items +from ast import literal_eval class SeFunctions: @@ -65,6 +66,37 @@ def __get_lock(self, lock_id): # If the original caller/source should be considered, the method returns the inverted value of the item. # Otherwise, the method returns the current value of the item, so that no change will be made def manual_item_update_eval(self, item_id, caller=None, source=None): + def check_include_exclude(entry_type): + conf_entry = item.conf["se_manual_{}".format(entry_type)] + if isinstance(conf_entry, str): + if ',' in conf_entry: + try: + new_conf_entry = literal_eval(conf_entry) + if isinstance(new_conf_entry, list): + conf_entry = new_conf_entry + except Exception: + conf_entry = [conf_entry, ] + else: + conf_entry = [conf_entry, ] + elif not isinstance(conf_entry, list): + elog.error("Item '{0}', Attribute 'se_manual_exclude': Value must be a string or a list!", item_id) + return retval_no_trigger + elog.info("checking {0} values: {1}", entry_type, conf_entry) + elog.increase_indent() + + # If current value is in list -> Return "Trigger" + for entry in conf_entry: + entry = re.compile(entry, re.IGNORECASE) + result = entry.match(original) + elog.info("Checking regex result {}", result) + if result is not None: + elog.info("{0}: matching.", entry) + elog.decrease_indent() + elog.info("Writing value {0}", retval_no_trigger) + return retval_trigger + elog.info("{0}: not matching", entry) + elog.decrease_indent() + item = self.itemsApi.return_item(item_id) if item is None: self.logger.error("manual_item_update_eval: item {0} not found!".format(item_id)) @@ -106,78 +138,13 @@ def manual_item_update_eval(self, item_id, caller=None, source=None): return retval_no_trigger if "se_manual_on" in item.conf: - # get list of include entries - include = item.conf["se_manual_on"] - if isinstance(include, str): - include = [include, ] - elif not isinstance(include, list): - elog.error("Item '{0}', Attribute 'se_manual_on': Value must be a string or a list!", item_id) - return retval_no_trigger - elog.info("checking manual on values: {0}", include) - elog.increase_indent() - - # If current value is in list -> Return "Trigger" - for entry in include: - entry = re.compile(entry, re.IGNORECASE) - result = entry.match(original) - elog.info("Checking regex result {}", result) - if result is not None: - elog.info("{0}: matching.", entry) - elog.decrease_indent() - elog.info("Writing value {0}", retval_no_trigger) - return retval_no_trigger - elog.info("{0}: not matching", entry) - elog.decrease_indent() + check_include_exclude("on") if "se_manual_exclude" in item.conf: - # get list of exclude entries - exclude = item.conf["se_manual_exclude"] - - if isinstance(exclude, str): - exclude = [exclude, ] - elif not isinstance(exclude, list): - elog.error("Item '{0}', Attribute 'se_manual_exclude': Value must be a string or a list!", item_id) - return retval_no_trigger - elog.info("checking exclude values: {0}", exclude) - elog.increase_indent() - - # If current value is in list -> Return "NoTrigger" - for entry in exclude: - entry = re.compile(entry, re.IGNORECASE) - result = entry.match(original) - elog.info("Checking regex result {}", result) - if result is not None: - elog.info("{0}: matching.", entry) - elog.decrease_indent() - elog.info("Writing value {0}", retval_no_trigger) - return retval_no_trigger - elog.info("{0}: not matching", entry) - elog.decrease_indent() + check_include_exclude("exclude") if "se_manual_include" in item.conf: - # get list of include entries - include = item.conf["se_manual_include"] - if isinstance(include, str): - include = [include, ] - elif not isinstance(include, list): - elog.error("Item '{0}', Attribute 'se_manual_include': Value must be a string or a list!", item_id) - return retval_no_trigger - elog.info("checking include values: {0}", include) - elog.increase_indent() - - # If current value is in list -> Return "Trigger" - for entry in include: - entry = re.compile(entry, re.IGNORECASE) - result = entry.match(original) - elog.info("Checking regex result {}", result) - if result is not None: - elog.info("{0}: matching.", entry) - elog.decrease_indent() - elog.info("Writing value {0}", retval_no_trigger) - return retval_trigger - elog.info("{0}: not matching", entry) - elog.decrease_indent() - + check_include_exclude("include") # Current value not in list -> Return "No Trigger elog.info("No include values matching. Writing value {0}", retval_no_trigger) return retval_no_trigger From e2d17c17dc98eec84afce1c5918476bde614c1f7 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Fri, 15 Sep 2023 10:53:10 +0200 Subject: [PATCH 07/11] stateengine plugin: improve and extend web interface and visu --- stateengine/StateEngineAction.py | 64 +++++++++++++----- stateengine/StateEngineItem.py | 10 +++ stateengine/StateEngineState.py | 9 ++- stateengine/StateEngineWebif.py | 34 +++++++--- stateengine/locale.yaml | 1 + stateengine/webif/__init__.py | 12 +++- .../static/img/visualisations/sign_delay.png | Bin 0 -> 1965 bytes .../static/img/visualisations/sign_false.png | Bin 1778 -> 2350 bytes .../img/visualisations/sign_scheduled.png | Bin 0 -> 2405 bytes .../static/img/visualisations/sign_true.png | Bin 1760 -> 2345 bytes .../static/img/visualisations/sign_warn.png | Bin 1524 -> 2158 bytes stateengine/webif/templates/index.html | 23 +++---- 12 files changed, 110 insertions(+), 43 deletions(-) create mode 100644 stateengine/webif/static/img/visualisations/sign_delay.png create mode 100644 stateengine/webif/static/img/visualisations/sign_scheduled.png diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index e2b7f41dc..9b2ae9a9e 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -82,6 +82,8 @@ def __init__(self, abitem, name: str): self._function = None self.__template = None self._action_status = {} + self._retrigger_issue = None + self._suspend_issue = None self.__queue = abitem.queue def update_delay(self, value): @@ -260,7 +262,6 @@ def check_getitem_fromeval(self, check_item, check_value=None, check_mindelta=No 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: @@ -273,14 +274,12 @@ def check_getitem_fromeval(self, check_item, check_value=None, check_mindelta=No 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 @@ -359,7 +358,6 @@ def check_complete(self, item_state, check_item, check_status, check_mindelta, c 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, @@ -440,7 +438,6 @@ def _update_repeat_webif(value: bool): _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() @@ -561,6 +558,7 @@ def _waitforexecute(self, state, actionname: str, namevar: str = "", repeat_text else: value = None self._abitem.add_scheduler_entry(self._scheduler_name) + self.update_webif_actionstatus(state, self._name, 'Scheduled') self._se_plugin.scheduler_add(self._scheduler_name, self._delayed_execute, value={'actionname': actionname, 'namevar': self._name, 'repeat_text': repeat_text, 'value': value, @@ -791,6 +789,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if returnvalue: return value self._log_info("{0}: Setting values by attribute '{1}'.{2}", actionname, self.__byattr, repeat_text) + self.update_webif_actionstatus(state, self._name, 'True') source = self.set_source(current_condition, previous_condition, previousstate_condition) for item in self.itemsApi.find_items(self.__byattr): self._log_info("\t{0} = {1}", item.property.path, item.conf[self.__byattr]) @@ -855,6 +854,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if returnvalue: return value + self.update_webif_actionstatus(state, self._name, 'True') self._log_info("{0}: Triggering logic '{1}' using value '{2}'.{3}", actionname, self.__logic, value, repeat_text) add_logics = 'logics.{}'.format(self.__logic) if not self.__logic.startswith('logics.') else self.__logic self._sh.trigger(add_logics, by=self._caller, source=self._name, value=value) @@ -937,10 +937,12 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if previousstate_condition: self._log_debug("Running eval {0} based on previous state's conditionset {1}", self.__eval, previousstate_condition) eval(self.__eval) + self.update_webif_actionstatus(state, self._name, 'True') self._log_decrease_indent() except Exception as ex: self._log_decrease_indent() text = "{0}: Problem evaluating '{1}': {2}." + self.update_webif_actionstatus(state, self._name, 'False', 'Problem evaluating: {}'.format(ex)) self._log_error(text.format(actionname, StateEngineTools.get_eval_name(self.__eval), ex)) else: try: @@ -954,9 +956,11 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if previousstate_condition: self._log_debug("Running eval {0} based on previous state's conditionset {1}", self.__eval, previousstate_condition) self.__eval() + self.update_webif_actionstatus(state, self._name, 'True') self._log_decrease_indent() except Exception as ex: self._log_decrease_indent() + self.update_webif_actionstatus(state, self._name, 'False', 'Problem calling: {}'.format(ex)) text = "{0}: Problem calling '{0}': {1}." self._log_error(text.format(actionname, StateEngineTools.get_eval_name(self.__eval), ex)) @@ -1057,6 +1061,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if value is None: self._log_debug("{0}: Value is None", actionname) + self.update_webif_actionstatus(state, self._name, 'False', 'Value is None') return if returnvalue: @@ -1068,6 +1073,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s # noinspection PyCallingNonCallable delta = float(abs(self.__item() - value)) if delta < mindelta: + self.update_webif_actionstatus(state, self._name, 'False') text = "{0}: Not setting '{1}' to '{2}' because delta '{3:.2}' is lower than mindelta '{4}'" self._log_debug(text, actionname, self.__item.property.path, value, delta, mindelta) return @@ -1097,6 +1103,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._log_debug("{0}: New value differs from old value, no force required.", actionname) self._log_decrease_indent() self._log_debug("{0}: Set '{1}' to '{2}'.{3}", actionname, self.__item.property.path, value, repeat_text) + self.update_webif_actionstatus(state, self._name, 'True') # noinspection PyCallingNonCallable self.__item(value, caller=self._caller, source=source) @@ -1190,61 +1197,84 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._log_increase_indent() if self.__special == "suspend": self.suspend_execute(state, current_condition, previous_condition, previousstate_condition) + if self._suspend_issue in ["", [], None, [None]]: + self.update_webif_actionstatus(state, self._name, 'True') + else: + self.update_webif_actionstatus(state, self._name, 'False', self._suspend_issue) self._log_decrease_indent() elif self.__special == "retrigger": + if self._retrigger_issue in ["", [], None, [None]]: + self.update_webif_actionstatus(state, self._name, 'True') + else: + self.update_webif_actionstatus(state, self._name, 'False', self._retrigger_issue) # noinspection PyCallingNonCallable self._abitem.update_state(self.__value, self._caller) #self.__value(True, caller=self._caller) self._log_decrease_indent() else: self._log_decrease_indent() + self.update_webif_actionstatus(state, self._name, 'False', 'Unknown special value {}'.format(self.__special)) raise ValueError("{0}: Unknown special value '{1}'!".format(actionname, self.__special)) self._log_debug("Special action {0}: done", self.__special) def suspend_get_value(self, value): _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'}]}} + text = 'Special action suspend requires arguments' + _issue = {self._name: {'issue': text, 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} self._action_status = _issue - raise ValueError("Action {0}: Special action 'suspend' requires arguments!".format(self._name)) + self._suspend_issue = text + raise ValueError("Action {0}: {1}".format(self._name, text)) 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'}]}} + text = "Special action 'suspend' requires two arguments (separated by a comma)!" + _issue = {self._name: {'issue': text, 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} self._action_status = _issue - raise ValueError("Action {0}: Special action 'suspend' requires two arguments (separated by a comma)!".format(self._name)) + self._suspend_issue = text + raise ValueError("Action {0}: {1}".format(self._name, text)) 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'}]}} + text = "Suspend item '{}' not found!".format(suspend) + _issue = {self._name: {'issue': text, 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} self._action_status = _issue - raise ValueError("Action {0}: Suspend item '{1}' not found!".format(self._name, suspend)) + self._suspend_issue = text + raise ValueError("Action {0}: {1}".format(self._name, text)) manual_item, _issue = self._abitem.return_item(manual) + self._suspend_issue = _issue _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'}]}} + text = 'Manual item {} not found'.format(manual) + _issue = {self._name: {'issue': text, 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} self._action_status = _issue - raise ValueError("Action {0}: Manual item '{1}' not found!".format(self._name, manual)) + self._suspend_issue = text + raise ValueError("Action {0}: {1}".format(self._name, text)) 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'}]}} + text = 'Special action retrigger requires item' + _issue = {self._name: {'issue': text, 'issueorigin': [{'state': 'retrigger', 'action': 'retrigger'}]}} self._action_status = _issue - raise ValueError("Action {0}: Special action 'retrigger' requires item".format(self._name)) + self._retrigger_issue = text + raise ValueError("Action {0}: {1}".format(self._name, text)) se_item, __ = StateEngineTools.partition_strip(value, ",") se_item, _issue = self._abitem.return_item(se_item) + self._retrigger_issue = _issue _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'retrigger', 'action': 'retrigger'}]}} 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'}]}} + text = 'Retrigger item {} not found'.format(se_item) + _issue = {self._name: {'issue': text, 'issueorigin': [{'state': 'retrigger', 'action': 'retrigger'}]}} self._action_status = _issue - raise ValueError("Action {0}: Retrigger item '{1}' not found!".format(self._name, se_item)) + self._retrigger_issue = text + raise ValueError("Action {0}: {1}".format(self._name, text)) return se_item def suspend_execute(self, state=None, current_condition=None, previous_condition=None, previousstate_condition=None): diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 669898e55..516918e7c 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -113,6 +113,12 @@ def laststate(self): _returnvalue = None if self.__laststate_item_id is None else self.__laststate_item_id.property.value return _returnvalue + @property + def laststate_releasedby(self): + _returnvalue = None if self.__laststate_item_id is None \ + else self.__release_info.get(self.__laststate_item_id.property.value) + return _returnvalue + @property def previousstate(self): _returnvalue = None if self.__previousstate_item_id is None else self.__previousstate_item_id.property.value @@ -181,6 +187,7 @@ def __init__(self, smarthome, item, se_plugin): self.__shtime = Shtime.get_instance() self.__se_plugin = se_plugin self.__active_schedulers = [] + self.__release_info = {} 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: @@ -892,6 +899,9 @@ def update_can_release_list(): self.__logger.debug("Inserted copy of state {}", relevant_state.id) _checkedentries.append(entry) self.__logger.info("State {} can currently get released by: {}", new_state.id, _can_release_list) + self.__release_info = {new_state.id: _can_release_list} + _key_releasedby = ['{}'.format(new_state.id), 'releasedby'] + self.update_webif(_key_releasedby, _can_release_list) self.__logger.info("".ljust(80, "_")) return all_released_by diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index 3edf622fb..a45c744c2 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -116,6 +116,12 @@ def is_copy_for(self): @is_copy_for.setter def is_copy_for(self, value): + if value: + webif_id = value.id + else: + webif_id = None + _key_copy = ['{}'.format(self.id), 'is_copy_for'] + self._abitem.update_webif(_key_copy, webif_id) self.__is_copy_for.set(value, "", True, None, False) # Constructor @@ -199,7 +205,8 @@ def write_to_log(self): 'actions_enter_or_stay': {}, 'actions_stay': {}, 'actions_leave': {}, - 'leave': False, 'enter': False, 'stay': False}) + 'leave': False, 'enter': False, 'stay': False, + 'is_copy_for': None, 'releasedby': None}) self._log_decrease_indent() self._log_info("Finished Web Interface Update") diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index b1b38b36d..a40a5a300 100755 --- a/stateengine/StateEngineWebif.py +++ b/stateengine/StateEngineWebif.py @@ -135,7 +135,8 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s else " ({} not met)".format(condition_info) if not condition_met\ else " (no repeat)" if _repeat is False and originaltype == 'actions_stay'\ else " (delay: {})".format(_delay) if _delay > 0\ - else " (wrong delay!)" if _delay < 0\ + else " (cancel delay!)" if _delay == -1 \ + else " (wrong delay!)" if _delay < -1 \ else " (delta {} < {})".format(_delta, _mindelta) if cond_delta and cond1 and cond2\ else "" action1 = action_dict.get('function') @@ -157,13 +158,17 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s cond_enter = originaltype == 'actions_enter' and self.__states[state].get('enter') is True cond_stay = originaltype == 'actions_stay' and self.__states[state].get('stay') is True active = True if (cond_enter or cond_stay) and cond1 else False - success_info = '' \ + success_info = '' \ if _issue is not None and active \ - else '' \ + else '' \ if (_success == 'False' or not condition_met) and active \ - else '' \ + else '' \ + if _success == 'Scheduled' and active \ + else '' \ + if _success == 'True' and active and _delay > 0 \ + else '' \ if _success == 'True' and active \ - else '' + else '' if not action2 == 'None': actionlabel += '{} {} {} {}'.format(fontcolor, action1, action2, action3, additionaltext) actionlabel += '{}'.format(success_info) @@ -342,7 +347,7 @@ def _add_actioncondition(self, state, conditionset, action_type, new_y, cond1, c if self.__nodes.get('{}_{}_state_actions_enter_edge'.format(state, conditionset)) is None: self.__nodes['{}_{}_state_{}_edge'.format(state, conditionset, action_type)] = \ pydotplus.Edge(self.__nodes['{}_{}'.format(state, conditionset)], self.__nodes['{}_{}_state_{}'.format( - state, conditionset, action_type)], style='bold', taillabel=" True", tooltip='first enter') + state, conditionset, action_type)], style='bold', taillabel=" True", tooltip='first enter') self.__graph.add_edge(self.__nodes['{}_{}_state_{}_edge'.format(state, conditionset, action_type)]) else: self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_{}_state_actions_enter'.format(state, conditionset)], @@ -405,10 +410,12 @@ def drawgraph(self, filename): new_y -= 1 * self.__scalefactor position = '{},{}!'.format(0, new_y) #self._log_debug('state: {} {}',state, position) + self.__nodes[state] = pydotplus.Node(state, pos=position, pin=True, notranslate=True, style="filled", fillcolor=color, shape="ellipse", label='<
' - '
{}
{}
>'.format(state, self.__states[state]['name'])) + '{}>'.format( + state, self.__states[state]['name'])) position = '{},{}!'.format(0.5, new_y) self.__nodes['{}_right'.format(state)] = pydotplus.Node('{}_right'.format(state), pos=position, shape="square", width="0", label="") @@ -522,11 +529,16 @@ def drawgraph(self, filename): self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_{}'.format(state, conditionset)], self.__nodes['{}_{}_right'.format(state, conditionset)], style='bold', taillabel=" True", tooltip='action on enter')) - + if self.__states[state].get('is_copy_for'): + xlabel = "can currently release {}\n\r".format(self.__states[state].get('is_copy_for')) + elif self.__states[state].get('releasedby'): + xlabel = "can currently get released by {}\n\r".format(self.__states[state].get('releasedby')) + else: + xlabel = "" if j == 0: self.__graph.add_edge(pydotplus.Edge(self.__nodes[state], self.__nodes['{}_right'.format(state)], - style='bold', color='black', dir='none', - edgetooltip='check first conditionset')) + style='bold', color='black', dir='none', + xlabel=xlabel, edgetooltip='check first conditionset')) self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_right'.format(state)], self.__nodes['{}_{}'.format(state, conditionset)], style='bold', color='black', tooltip='check first conditionset')) @@ -572,7 +584,7 @@ def drawgraph(self, filename): self.__graph.add_node(self.__nodes['{}_actions_leave'.format(state)]) self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_leave'.format(state)], self.__nodes['{}_actions_leave'.format(state)], style='bold', - taillabel=" True", tooltip='run leave actions')) + taillabel=" True", tooltip='run leave actions')) previous_state = state diff --git a/stateengine/locale.yaml b/stateengine/locale.yaml index 4c8194174..3e79465ce 100755 --- a/stateengine/locale.yaml +++ b/stateengine/locale.yaml @@ -26,3 +26,4 @@ plugin_translations: '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:'} + 'Potenziell Released': {'de': '=', 'en': 'Potential Released'} diff --git a/stateengine/webif/__init__.py b/stateengine/webif/__init__.py index 7bc847f22..b53855a6e 100755 --- a/stateengine/webif/__init__.py +++ b/stateengine/webif/__init__.py @@ -107,10 +107,18 @@ def get_data_html(self, dataSet=None): # get the new data data = {} for item in self.plugin.get_items(): + laststate = item.laststate_name + laststate = "-" if laststate in ["", None] else laststate conditionset = item.lastconditionset_name - conditionset = "-" if conditionset == "" else conditionset + conditionset = "-" if conditionset in ["", None] else conditionset ll = item.logger.log_level_as_num - data.update({item.id: {'laststate': item.laststate_name, 'lastconditionset': conditionset, 'log_level': ll}}) + if item.laststate_releasedby in [None, []]: + lsr = "-" + else: + lsr = [entry.split('.')[-1] for entry in item.laststate_releasedby] + data.update({item.id: {'laststate': laststate, + 'lastconditionset': conditionset, 'log_level': ll, + 'laststate_releasedby': lsr}}) try: return json.dumps(data) except Exception as e: diff --git a/stateengine/webif/static/img/visualisations/sign_delay.png b/stateengine/webif/static/img/visualisations/sign_delay.png new file mode 100644 index 0000000000000000000000000000000000000000..eaa0cda825d50100d135e689cf434c5969d9a402 GIT binary patch literal 1965 zcmb_dYfKzf6kcr6KCBR%G=y3mr~N@wXXmjGn2{~7#RBb8gjJz`b7t#=l76+>gwxXInQ zbMJS~`Mz_{J=0iJxMkIf!K!OIq!cs`qxv&e5_~p>tTqbcW0x>FFc$OIu zEAuJ$(lM+W zR18HAD)(?5+g=a=rPt$$#aNV!B*jALMs5|7F`tk{(Ym)rg=8_*l}e3=xmAef`luR1 z^#7gC1=i+HjF-ygXb>3CjfM+1&x@c~f_8W*mZNEoVmDC?M^UC%mGdZq=&xA}>Tq%n z=K?4y7y%gIKfwae`4vq9NUA7-03=m8fMc-)ayd#!(NJK-&K6VW$C203QnpXiH-q05XsQLe$=^@RjD7M(qV;pXwBN#+eET z-odbhokyj{@^%~H5GWr5oixMRVAOuQCZc-K zidHj!K}z7x`B_NBB1ORge$GY*RS=1qTND>b%KeF&v#$<9L}OtE*zFt>tAII}uK0~= zpuwyF;vQ`TMbYbkF?K71rFZ}@%H{>x7-h!?fgFIS1(7)Uz;f>IjPEUHL6!cWa!9l9 z%pDZ@?=y_-{i8=A^^qmi_{?dn>!O!f--a^El!m%;jct3q#S*9GyR*D$=U*%7DHzC1 z9J##V&eDsA`kV3GZtvq~PcS$8LFbx=(ItaD_22X~ZtfrYqPL;p$idxSXYU#IW#;tT zwe)1?gwT6PdHuzWZC!QGp9|c!W|Rz9&YbVNbwj9-65qVi8A#HX45oN4zJb>#lpj0x zQ&Qe<^?4^J`W_lf8~I?GUe>TR>9ZH=y_dFR-%M$GZ22tqLrvx@g>Tu3Cf}Zr(or(` z>{0io_O1JWj(_5v4VT)c8z(m&sJPHwm+;4qq;Gy_Npm$?@>7p^rP-#O`x@kL+w*{rSGu_VN!8?;BX!eyUMwft_pV*-sDJ zlFxKZ1o~HQN=~4P8?yZuewVHlsNV)pW$j389!fhN9Q$#4Ac+|Mvh>7PuCi8aw( zk=ETtb5m_x%jFxvs~Ky?XV=Z_89UG=?~QMnx&Es09#9L^E$@CW)~)^M?yhZ5j(vS+ e{A~BtKjWu9DQ+BmdUVqKpyuZmy4$nM8~y^{&#xi? literal 0 HcmV?d00001 diff --git a/stateengine/webif/static/img/visualisations/sign_false.png b/stateengine/webif/static/img/visualisations/sign_false.png index 6d6433cc1777cb3e23d9bb3cab86457557d9698d..8f86cde9580220bb7e1e359780b8a02e716c0ec4 100644 GIT binary patch literal 2350 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 diff --git a/stateengine/webif/static/img/visualisations/sign_scheduled.png b/stateengine/webif/static/img/visualisations/sign_scheduled.png new file mode 100644 index 0000000000000000000000000000000000000000..a59cc00b7747c11baf7001b82870656b976e2789 GIT binary patch literal 2405 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 literal 1760 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBDAAG{;hE;^%b*2hb1<+n z3NbJPS&Tr)z$nE4G7ZRL@M4sPvx68lplX;H7}_%#SfFa6fHVkr05M1pgl1mAh%j*h z6I`{x0%imor0saV%ts)_S>O>_%)r1c48n{Iv*t)JFfg6S42dX-@b$4u&d=3LOvz75 z)vL%Y0O?||sjvbvb5lza6)JLb@`|l0Y?Z(&tblBgu)dN4SV>8?tx|+H?jfSfFg)+ zA4CH}eP~{0i5*M}nlQSq2!uSs8e~Cq4gN)$slb2)yUP&jEMzex^&sC_1!PvF=0vz; z=BDNqgZyF)bQo3%bdApWIr&Aw1&PV2c0hZ;T9Aa$RfDaG#AOYV1XQDqJ}9n{;vSN) zz@osEWyfWs4^O3bT#H{kng>kNpFCY0Lo5VcC!h8Yb`&{w-m?7N(rFDLkF|MQ-I8N= z7-p;xy3clHY5xhn4sLTP!;kz0j}QIe=nTBr>QcT>=&r|N?@mUS;4O>tCP)Z5(AI0 z$a#g^g{5}C`g?Luz@4>+yBAlk)4iU^shz0gw)yhUyc+Ax{w^LYZQa*#R~_s8BN8VRuy)i zS>=A(pLuqP`Ysj;arnFcUidZLJZ%HhC)T+^o4n`$ zVtgkh-O%>1?8b|A4F_hun9)_+(_DDn@3-;A zjZd@FjhN-V^$*WFy3!+{Ol(Kp3fXNHs~v7%h;r?4QJ!M%c>da^rxz@$wD%Zakz`d@ z%GO?`e0tFahV?A-*K@>AnvkToz_2#O>BR|gt_@pNTziwZKYX{Rqv6DS%O6tj&luc( zw)d!2r0gfIU7q<_`YAhitrC{Y{T%d*qq{>mll#Le%ZLE(Td_jV_ur0_zf)4zkT0|S zU`+Ogg*mb+NxO}LJuh4QyHMi0w|G~Y`^ICja&sedKJRr%&%DFE?EkkXYIbu(LgEx% z`J3zB^QM(lKM=ecsuS#gs4(WGPFK+ey@kPHiwu^@e2%_J~ZL zd-IxAL1=gPme|({s*Q`>mFKvx|LS{u^_in}k>M%-7}JCkc3cSPN(7bLp00i_>zopr E0Eg^$3jhEB diff --git a/stateengine/webif/static/img/visualisations/sign_warn.png b/stateengine/webif/static/img/visualisations/sign_warn.png index e58ab170ecd3e1c1e1473104c517af2365cc938b..3084bec5898c464305748adec6965b82c8f6fa78 100644 GIT binary patch literal 2158 zcmb_e32YQq7+wsNLQ`6f0CLD=iV=|6x%QfY70Pa*1!n2Cr4(wE>CD^i(4B*s+3pqu z1<`85VHG?|Ni;=3BO02B1}Xxg5>Z6tia@N9pe8^Q2q{t&-|Ti**FYr1P2SF%_kZs{ zzW;y!n-@z1vvLQH8fdfGa!ZQ+W$>AAjqEJ=KknLX9zF(W#S0CaEoZ1T`q)}pN7`&z z5viietm5Xff-2j2QLO>?xU506%{FyfT;qj0U?Mf3R#LoZ*WOQ2L=wH|0w+gsnh%7f z;$=E0UlynkmemQ2h)#P9nHp!I0vVV*5|<;2!N$F4LN5#F)-sMF35Z$eMGGy1NEKI# z_*5MrjGe#)f*=t$V<#OzsBussN|=mL1V!TniIX%&QY__U9S)@TheC_G7-Gx(Gkfj9 zH!m7CO^wCzSS)6b(RNj@#Yu)?aDu`q3WEsDXi!W(jwwb#N`oI5f-Y&Mq$-G|k*`ss zrWb|YCc}`mv|7dJr3#XU$9WAW?SvIp0w@Y;oEFt13FD%Gg9wm;Vj2)jrmRfmS5cXYxFj$=y{ z!{ikKl=!_U^vf?iUdiaP{c}Dl818G0w56(n^-HA;viDxmtZVLLJ6HhCnO6qB~~}E zij^&v3{%w`de3S(SlT;_NJyeSSzbu`WbpL>l9pVQ?m+szk(S&a3j-Kts=|_G3t~k{7QBS|>?rXU zR(RHJ3UYHE2oBrw)s{18P75`x9nA^O7{QOvp1C-G)0MWTkLCR;j2-5ed(P~62f1l` zINUt0Ew>F!3}QAW;l@G1?8~Pe{Q@2As1F|5rj6@+CHv|tKR?-c%NVuhjjYG}9c|8e#=n{zlr#5_ z+vklxA3V2y#@yczb#LtQ9bL*$J5SzHD!Z^7*UL^-?_ZZU<;d>i_EoK~^*uQD?Da)* zwEOGM$pd}+pPO}V(WOJKKJ5=R>su?MP3_AM%4<4T+PcvGAI}+Wgd#^bO9AG<^$B$= zxnTW>G1m^~O`mXKz1Mc4_GGOUD+_Z5;;+%L@Y4 x)%oPe=F!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBDAAG{;hE;^%b*2hb1<+n z3NbJPS&Tr)z$nE4G7ZRL@M4sPvx68lplX;H7}_%#SfFa6fHVkr05M1pgl1mAh%j*h z6I`{x0%imor0saV%ts)_S>O>_%)r1c48n{Iv*t)JFfg6S42dX-@b$4u&d=3LOvz75 z)vL%Y0O?||sjvbvb5lza6)JLb@`|l0Y?Z(&tblBgu)dN4SV>8?tx|+H?jfSfFg)+ zA4CH}eP~{0i5*M}nlQSq2!uSs8e~Cq4gN)$slb2)yUP&jEMzex^&sC_1!PvF=0vz; z=BDNqgZyF)bQo3%bdApWIr&Aw1&PV2c0hZ;T9Aa$RfDaG#AOYV1XQDqJ}9n{;vSN) zz@osEWyfWs4^O3bT#H{kng>kNY@RNTAr^wA!H(I&jw1i=`OG?dN$XaqqORO)$5jFw zuFn2=iAzj7i+|piYuz0;>?iDB9CyTN{Wld4(aZCq_EZRbQCO?C&a*82xux;!&e=<2 zZ_G8n|NCC`{n_T+b3Pa}?^&HueP+FA{*-VfgYC0KW9-hmtlzxp{`-F41npZa6(xz6 zUu-z2t$fCvFFf#8^LqCDo3A02x|9!jCAL}+TDldWt-_Wp03D_{@8w$V@;gGJLzzxl4T3F8d(WHH1IRmY1b*Q@GoMuOV+LI ziZVNW(m~Pt=ow2VTO(hI+li*O%?7n$A&&dx>*_Pb5>v1H*goo+sdDk*`al1C=1=?} zp(2~8u>Ml--uydF0w1nA*X8-yHF*WS5SgI4{k!!YyB8a_%BIyeT6Hhj{xIZD4p%vU z#q&otEq_?fh8q9=qj`UVl3qg864t0cx6VC3C*!Ctt8v(9vq^p9PnpS$9dq*!nP?TB z3Nt!kkdpn&WZ5dlNfFQW9>0B-bnM$z&qn2|%dgIlwdY*DD<)5(^3$W{|BT8!9}GTe R9Q6W~WuC5nF6*2UngDTR3xxmx diff --git a/stateengine/webif/templates/index.html b/stateengine/webif/templates/index.html index 1213ca83f..e52c915f4 100755 --- a/stateengine/webif/templates/index.html +++ b/stateengine/webif/templates/index.html @@ -26,12 +26,6 @@ }; } -