diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index d6653982a..43b704888 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -22,10 +22,10 @@ from . import StateEngineEval from . import StateEngineValue from . import StateEngineDefaults -from . import StateEngineCurrent import datetime from lib.shtime import Shtime import re +from lib.item import Items # Base class from which all action classes are derived @@ -67,6 +67,7 @@ def __cast_seconds(value): def __init__(self, abitem, name: str): super().__init__(abitem) self._se_plugin = abitem.se_plugin + self.itemsApi = Items.get_instance() self._parent = self._abitem.id self._caller = StateEngineDefaults.plugin_identification self.shtime = Shtime.get_instance() @@ -431,7 +432,7 @@ def _check_condition(condition: str): try: _matching = cond.fullmatch(_updated_current_condition) if _matching: - self._log_debug("Given {} '{}' matches current one: '{}'", condition, _orig_cond.pattern, _updated_current_condition) + self._log_debug("Given {} '{}' matching current one: '{}'", condition, _orig_cond.pattern, _updated_current_condition) _condition_met.append(_updated_current_condition) _conditions_met_count += 1 _conditions_met_type.append(condition) @@ -927,7 +928,7 @@ def __repr__(self): # value: Value of the set_(action_name) attribute def update(self, value): self.__byattr = value - _issue = {self._name: {'issue': None, 'attribute': self.__byattr, + _issue = {self._name: {'issue': None, 'attribute': [self.__byattr], 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} return _issue @@ -938,7 +939,7 @@ def complete(self, evals_items=None, use=None): self._abitem.set_variable('current.action_name', self._name) self._abitem.set_variable('current.state_name', self._state.name) self._scheduler_name = "{}-SeByAttrDelayTimer".format(self.__byattr) - _issue = {self._name: {'issue': None, 'attribute': self.__byattr, + _issue = {self._name: {'issue': None, 'attribute': [self.__byattr], 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} self._abitem.set_variable('current.action_name', '') self._abitem.set_variable('current.state_name', '') @@ -964,7 +965,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._log_info("{0}: Setting values by attribute '{1}'.{2}", actionname, self.__byattr, repeat_text) self.update_webif_actionstatus(state, self._name, 'True') source = self.set_source(current_condition, previous_condition, previousstate_condition, next_condition) - for item in self._sh.find_items(self.__byattr): + for item in self.itemsApi.find_items(self.__byattr): self._log_info("\t{0} = {1}", item.property.path, item.conf[self.__byattr]) item(item.conf[self.__byattr], caller=self._caller, source=source) self._abitem.last_run = {self._name: datetime.datetime.now()} diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 75a64de47..e7d2a0eb4 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -164,10 +164,10 @@ def update(self, attribute, value): # If we do not have the action yet (mode-attribute before action-attribute), ... self.__unassigned_modes[name] = value else: - _val, _issue = self.__actions[name].update_mode(value) + _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]) + _issue, _action = self.__check_mode_setting(name, value, self.__actions[name].function, self.__actions[name]) if _issue: _issue_list.append(_issue) if _action: @@ -194,7 +194,7 @@ def update(self, attribute, value): _count += 1 _issue = StateEngineTools.flatten_list(_issue_list) except ValueError as ex: - _issue = {name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': self.__actions[name].function}], 'ignore': True}} + _issue = {name: {'issue': [str(ex)], 'attribute': [func], 'issueorigin': [{'state': self.__state.id, 'action': self.__actions[name].function}], 'ignore': True}} if name in self.__actions: del self.__actions[name] self._log_warning("Ignoring action {0} because: {1}", attribute, ex) @@ -208,8 +208,7 @@ def __check_force_setting(self, name, value, function): 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" + 'attribute': ['force'], 'issueorigin': [{'state': self.__state.id, 'action': function}]}} self._log_warning("Attribute 'se_action_{0}': Parameter 'force' not supported " "for function '{1}'", name, function) elif value and function == "set": @@ -230,8 +229,8 @@ def __check_mode_setting(self, name, value, function, action): _issue = None # Parameter mode is supported only for type "remove" if "remove" not in function: - _issue = {name: {'issue': ['Parameter mode only supported for remove function'], 'attribute': 'mode', - 'issueorigin': [{'state': 'unknown', 'action': function}]}} + _issue = {name: {'issue': ['Parameter mode only supported for remove function'], 'attribute': ['mode'], + 'issueorigin': [{'state': self.__state.id, '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"]: @@ -246,8 +245,8 @@ def __check_mode_setting(self, name, value, function, action): 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}]}} + name: {'issue': ['Parameter {} not allowed for mode!'.format(value)], 'attribute': ['mode'], + 'issueorigin': [{'state': self.__state.id, 'action': function}]}} self._log_warning( "Attribute 'se_action_{0}': Parameter '{1}' for 'mode' is wrong - can only be {2}", name, value, possible_mode_list) @@ -365,9 +364,9 @@ def __handle_combined_action_attribute(self, name, value_list): def remove_action(e): if name in self.__actions: del self.__actions[name] - i = {name: {'issue': [e], 'issueorigin': [{'state': 'unknown', 'action': parameter['function']}], 'ignore': True}} + i = {name: {'issue': [str(e)], 'attribute': [f'se_action_{name}'], 'issueorigin': [{'state': self.__state.id, 'action': parameter['function']}], 'ignore': True}} _issue_list.append(i) - self._log_warning("Ignoring action {0} because: {1}", name, e) + self._log_warning("Removed action {0} because: {1}.", name, e) parameter = {'function': None, 'force': None, 'repeat': None, 'delay': 0, 'order': None, 'nextconditionset': None, 'conditionset': None, 'previousconditionset': None, 'previousstate_conditionset': None, 'mode': None, 'instanteval': None, 'mindelta': None, 'minagedelta': None} @@ -395,18 +394,18 @@ def remove_action(e): else: parameter[key] = val except Exception as ex: - remove_action("Problem with entry {} for action {}: {}".format(entry, name, ex)) + remove_action("Problem with entry {}: {}".format(entry, ex)) if _issue_list: return _issue_list parameter['action'] = name # function given and valid? if parameter['function'] is None: - remove_action("Attribute 'se_action_{0}: Parameter 'function' must be set!".format(name)) + remove_action("Parameter 'function' must be set!") return _issue_list if parameter['function'] not in ('set', 'force', 'run', 'byattr', 'trigger', 'special', 'add', 'remove', 'removeall', 'removefirst', 'removelast'): - remove_action("Attribute 'se_action_{0}: Invalid value '{1}' for parameter 'function'!".format(name, parameter['function'])) + remove_action("Invalid value '{}' for parameter 'function'!".format(parameter['function'])) return _issue_list _issue, parameter['function'] = self.__check_force_setting(name, parameter['force'], parameter['function']) @@ -571,8 +570,7 @@ def remove_action(e): # noinspection PyMethodMayBeStatic def __raise_missing_parameter_error(self, parameter, param_name): if param_name not in parameter or parameter[param_name] is None: - raise ValueError("Attribute 'se_action_{0}: Parameter '{1}' must be set for " - "function '{2}'!".format(parameter['action'], param_name, parameter['function'])) + raise ValueError("Parameter '{0}' must be set for function '{1}'!".format(param_name, parameter['function'])) # Check the actions optimize and complete them # state: state (item) to read from @@ -584,7 +582,7 @@ def complete(self, evals_items=None, use=None): try: _status.update(self.__actions[name].complete(evals_items, use)) except ValueError as ex: - _status.update({name: {'issue': ex, 'issueorigin': {'state': self.__state.id, 'action': 'unknown'}}}) + _status.update({name: {'issue': [str(ex)], 'issueorigin': {'state': self.__state.id, 'action': name}}}) raise ValueError("Completing State '{0}', Action '{1}': {2}".format(self.__state.id, name, ex)) return _status diff --git a/stateengine/StateEngineFunctions.py b/stateengine/StateEngineFunctions.py index 1e869d033..9e632cbce 100755 --- a/stateengine/StateEngineFunctions.py +++ b/stateengine/StateEngineFunctions.py @@ -19,13 +19,13 @@ # You should have received a copy of the GNU General Public License # along with this plugin. If not, see . ######################################################################### -import logging import threading import re from . import StateEngineLogger from . import StateEngineTools from . import StateEngineDefaults from ast import literal_eval +from lib.item import Items class SeFunctions: @@ -44,6 +44,7 @@ def __init__(self, smarthome=None, logger=None): self.__locks = {} self.__global_struct = {} self.__ab_alive = False + self.itemsApi = Items.get_instance() def __repr__(self): return "SeFunctions" @@ -97,7 +98,7 @@ def check_include_exclude(entry_type): elog.decrease_indent() return None - item = self.__sh.return_item(item_id) + 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)) @@ -111,7 +112,7 @@ def check_include_exclude(entry_type): if "se_manual_logitem" in item.conf: elog_item_id = item.conf["se_manual_logitem"] - elog_item = self.__sh.return_item(elog_item_id) + elog_item = self.itemsApi.return_item(elog_item_id) if elog_item is None: self.logger.error("manual_item_update_item: se_manual_logitem {0} not found!".format(elog_item_id)) elog = StateEngineLogger.SeLoggerDummy() diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index b171a8823..a9609b371 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -34,7 +34,7 @@ from lib.shtime import Shtime from lib.item.item import Item -from lib.item.items import Items +from lib.item import Items import copy import threading import queue @@ -201,6 +201,7 @@ def ab_alive(self, value): # se_plugin: smartplugin instance def __init__(self, smarthome, item, se_plugin): self.__item = item + self.itemsApi = Items.get_instance() self.__logger = SeLogger.create(self.__item) self.__logging_off = False self.update_lock = threading.Lock() @@ -2162,7 +2163,7 @@ def return_item(self, item_id): if isinstance(item_id, (StateEngineStruct.SeStruct, self.__itemClass)): return item_id, None if isinstance(item_id, StateEngineState.SeState): - return self.__sh.return_item(item_id.id), None + return self.itemsApi.return_item(item_id.id), None if item_id is None: _issue = "item_id is None" return None, [_issue] @@ -2203,7 +2204,7 @@ def return_item(self, item_id): self.__logger.warning(_issue) return None, [_issue] else: - item = self.__sh.return_item(item_id) + item = self.itemsApi.return_item(item_id) if item is None: _issue = "Item '{0}' not found.".format(item_id) self.__logger.warning(_issue) @@ -2226,7 +2227,7 @@ def return_item(self, item_id): rel_item_id = item_id[parent_level:] if rel_item_id != "": result += "." + rel_item_id - item = self.__sh.return_item(result) + item = self.itemsApi.return_item(result) if item is None: _issue = "Determined item '{0}' does not exist.".format(item_id) self.__logger.warning(_issue) diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index b2ab0221a..43f7fb626 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -592,19 +592,29 @@ def filter_issues(input_dict): return for itm, dct in action_status.items(): if itm not in self.__action_status[actn_type]: - self.__action_status[actn_type].update({itm: dct}) + self.__action_status[actn_type][itm] = dct + else: + for key, value in dct.items(): + if key not in self.__action_status[actn_type][itm]: + self.__action_status[actn_type][itm][key] = value for (itm, dct) in action_status.items(): issues = dct.get('issue') attributes = dct.get('attribute') + if not isinstance(attributes, list): + attributes = [attributes] + if not isinstance(self.__action_status[actn_type][itm].get('attribute'), list): + self.__action_status[actn_type][itm]['attribute'] = [self.__action_status[actn_type][itm].get('attribute')] if issues: if isinstance(issues, list): + attributes = (attributes + [None] * len(issues))[:len(issues)] for i, issue in enumerate(issues): if issue not in self.__action_status[actn_type][itm]['issue']: self.__action_status[actn_type][itm]['issue'].append(issue) self.__action_status[actn_type][itm]['attribute'].append(attributes[i]) flattened_dict = {} + for key, action_type_dict in self.__action_status.items(): # Iterate through the inner dictionaries for inner_key, nested_dict in action_type_dict.items(): @@ -616,6 +626,7 @@ def filter_issues(input_dict): flattened_dict[inner_key].update(nested_dict) self.__used_attributes = deepcopy(flattened_dict) + self.__action_status = filter_issues(self.__action_status) self._abitem.update_attributes(self.__unused_attributes, self.__used_attributes) diff --git a/stateengine/StateEngineTools.py b/stateengine/StateEngineTools.py index b423c9c6a..290fdaef3 100755 --- a/stateengine/StateEngineTools.py +++ b/stateengine/StateEngineTools.py @@ -23,7 +23,7 @@ import datetime from ast import literal_eval import re -from lib.item.items import Items +from lib.item import Items # General class for everything that is below the SeItem Class @@ -410,7 +410,7 @@ def get_original_caller(smarthome, elog, caller, source, item=None, eval_keyword else: original_source = "None" while partition_strip(original_caller, ":")[0] in eval_keyword: - original_item = smarthome.return_item(original_source) + original_item = smarthome.items.return_item(original_source) if original_item is None: elog.info("get_caller({0}, {1}): original item not found", caller, source) break diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index 040ff6209..e745d12e7 100755 --- a/stateengine/StateEngineWebif.py +++ b/stateengine/StateEngineWebif.py @@ -33,6 +33,23 @@ class WebInterface(StateEngineTools.SeItemChild): # Constructor # abitem: parent SeItem instance + + @property + def width(self): + return self.__widthfactor + + @width.setter + def width(self, value): + self.__widthfactor = value + + @property + def height(self): + return self.__heightfactor + + @height.setter + def height(self, value): + self.__heightfactor = value + def __init__(self, abitem): super().__init__(abitem) @@ -46,13 +63,15 @@ def __init__(self, abitem): self.__active_state = abitem.laststate self.__graph = pydotplus.Dot('StateEngine', graph_type='digraph', splines='false', - overlap='scale', compound='false', imagepath='{}'.format(self.__img_path)) + overlap='compress', compound='false', imagepath='{}'.format(self.__img_path)) self.__graph.set_node_defaults(color='lightgray', style='filled', shape='box', - fontname='Helvetica', fontsize='10') - self.__graph.set_edge_defaults(color='darkgray', style='filled', shape='box', - fontname='Helvetica', fontsize='10') + fontname='Helvetica', fontsize='10', pin='true') + self.__graph.set_edge_defaults(color='darkgray', style='filled', shape='box', labeldistance=2.5, + fontname='Helvetica', fontsize='10', len=0.1) + self.__graph.set_graph_defaults(sep=+0.15) self.__nodes = {} - self.__scalefactor = 0.1 + self.__heightfactor = 1 + self.__widthfactor = 1 self.__textlimit = 105 self.__conditionset_count = 0 @@ -81,7 +100,7 @@ def _actionlabel(self, state, label_type, conditionset, active): # label_type: on_enter, on_stay, on_leave, on_pass # active: if action is currently run - actionlabel = actionstart = '<' + actionlabel = actionstart = '<
' action_tooltip = '' types = [label_type] if label_type in ['actions_leave', 'actions_pass'] else ['actions_enter_or_stay', label_type] tooltip_count = 0 @@ -146,7 +165,7 @@ def _actionlabel(self, state, label_type, conditionset, active): if _success == 'True' and active \ else '' if not action2 == 'None': - actionlabel += ''.format(fontcolor, action1, action2, action3, additionaltext) + actionlabel += ''.format(fontcolor, action1, action2, action3, additionaltext) actionlabel += '{}'.format(success_info) actionlabel += '
{} {} {} {}
{} {} {} {}
>' @@ -163,7 +182,7 @@ def _conditionlabel(self, state, conditionset): if _empty_set: return '', '', 0 conditionlist = '<' - conditionlist += ''.format(conditionset) + conditionlist += ''.format(conditionset) tooltip_count = 0 for k, condition in enumerate(self.__states[state]['conditionsets'].get(conditionset)): condition_dict = self.__states[state]['conditionsets'][conditionset].get(condition) @@ -191,12 +210,13 @@ def _conditionlabel(self, state, conditionset): if condition not in conditions_done: current_clean = ", ".join(f"{k} = {v}" for k, v in current.items()) text = " Current {}".format(current_clean) if current is not None and len(current) > 0 else " Not evaluated." - conditionlist += ('
{}
{}
' + conditionlist += ('').format(condition.upper(), text) conditions_done.append(condition) - conditionlist += ''.format(info) + conditionlist += '{}'.format(info) comparison = ">=" if not min_none and compare == "min"\ else "<=" if not max_none and compare == "max"\ else "older" if not agemin_none and compare == "agemin"\ @@ -280,7 +300,7 @@ def _conditionlabel(self, state, conditionset): match_info = match.get('value') if compare in ["min", "max", "value"]\ else match.get('age') if compare in ["agemin", "agemax", "age"]\ else match.get(compare) - conditionlist += ''.format(match_info) - conditionlist += '
{}:{}
' + '' '' '
{}:{}
' + conditionlist += '
' info_status = str(condition_dict.get('status') or 'n/a') info_item = str(condition_dict.get('item') or 'n/a') info_eval = str(condition_dict.get('eval') or 'n/a') @@ -257,7 +277,7 @@ def _conditionlabel(self, state, conditionset): else: info = "n/a" - conditionlist += '{}{}'.format(comparison) + conditionlist += '      {}   '.format(comparison) conditionlist += '"{}"'.format(info) if not item_none and not status_none \ and not eval_none and not status_eval_none else '' @@ -297,9 +317,9 @@ def _conditionlabel(self, state, conditionset): match_info = '' if match_info == 'yes'\ else '' if match_info == 'no'\ else '' if match_info and len(match_info) > 0 \ - else '' + else '' conditionlist += '{}
>' + conditionlist += '
>' return conditionlist, condition_tooltip, tooltip_count def _add_actioncondition(self, state, conditionset, action_type, new_y, cond1, cond2): @@ -312,9 +332,9 @@ def _add_actioncondition(self, state, conditionset, action_type, new_y, cond1, c color_stay = "gray" if (cond1 and cond2 and cond5) or \ (cond_stay and cond4 and cond5) else "olivedrab" if cond4 else "indianred2" - label = 'first enter' if action_type in ['actions_enter', 'actions_enter_or_stay'] else 'staying at state' + label = 'first enter' if action_type in ['actions_enter', 'actions_enter_or_stay'] else 'staying' - position = '{},{}!'.format(0.38, new_y) + position = '{},{}!'.format(12.6 * self.__widthfactor, new_y) color = color_enter if label == 'first enter' else color_stay self.__nodes['{}_{}_state_{}'.format(state, conditionset, action_type)] = \ pydotplus.Node('{}_{}_state_{}'.format(state, conditionset, action_type), style="filled", fillcolor=color, @@ -323,7 +343,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)], @@ -331,7 +351,7 @@ def _add_actioncondition(self, state, conditionset, action_type, new_y, cond1, c style='bold', label="False ")) self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_{}_state_{}'.format(state, conditionset, action_type)], self.__nodes['{}_{}_{}'.format(state, conditionset, action_type)], - style='bold', taillabel=" True")) + style='bold', taillabel="True")) try: if action_type == 'actions_enter': self.__nodes['{}_{}_actions_enter'.format(state, conditionset)].obj_dict['attributes']['fillcolor'] = color @@ -344,277 +364,349 @@ def _add_actioncondition(self, state, conditionset, action_type, new_y, cond1, c pass def drawgraph(self, filename): - new_y = 2 + new_y = 0 previous_state = '' previous_conditionset = '' + above_nodeheights = [] + last_action_nodeheight = 0 for i, state in enumerate(self.__states): #self._log_debug('Adding state for webif {}', self.__states[state]) - if isinstance(self.__states[state], (OrderedDict, dict)): - self.__conditionset_count = len(self.__states[state].get('conditionsets')) - if self.__conditionset_count == 0: - self.__states[state]['conditionsets'][''] = '' - try: - list_index = list(self.__states.keys()).index(self.__active_state) - except Exception: - list_index = 0 - color = "olivedrab" if state == self.__active_state \ - else "gray" if i > list_index else "indianred2" - - new_y -= 1 * self.__scalefactor - position = '{},{}!'.format(0, new_y) - if not i == 0: - condition_node = 'pass' if self.__nodes.get('{}_pass'.format(previous_state)) \ - else 'leave' if self.__nodes.get('{}_leave'.format(previous_state)) \ - else list(self.__states[previous_state]['conditionsets'].keys())[-1] - lastnode = self.__nodes['{}_{}'.format(previous_state, condition_node)] - self.__nodes['{}_above'.format(state)] = pydotplus.Node('{}_above'.format(state), pos=position, - shape="square", width="0", label="") - self.__graph.add_node(self.__nodes['{}_above'.format(state)]) - position = '{},{}!'.format(0.3, new_y) - self.__nodes['{}_above_right'.format(state)] = pydotplus.Node('{}_above_right'.format(state), - pos=position, shape="square", width="0", label="") - self.__graph.add_node(self.__nodes['{}_above_right'.format(state)]) - self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_above'.format(state)], - self.__nodes['{}_above_right'.format(state)], style='bold', - color='black', label="", dir="none")) - self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_above_right'.format(state)], lastnode, - style='bold', color='black', label="False ", dir="none")) - self.__graph.add_edge(pydotplus.Edge(state, self.__nodes['{}_above'.format(state)], style='bold', - color='black', label="", dir="back")) - new_y -= 1 * self.__scalefactor + if not isinstance(self.__states[state], (OrderedDict, dict)): + continue + self.__conditionset_count = len(self.__states[state].get('conditionsets')) + if self.__conditionset_count == 0: + self.__states[state]['conditionsets'][''] = '' + try: + list_index = list(self.__states.keys()).index(self.__active_state) + except Exception: + list_index = 0 + color = "olivedrab" if state == self.__active_state \ + else "gray" if i > list_index else "indianred2" + + if not i == 0: + new_y -= max(float(above_nodeheights[-1] + 0.3), 0.46, float(last_action_nodeheight)) * self.__heightfactor # half elipse added at end 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'])) - position = '{},{}!'.format(0.3, new_y) - self.__nodes['{}_right'.format(state)] = pydotplus.Node('{}_right'.format(state), pos=position, + + condition_node = 'pass' if self.__nodes.get('{}_pass'.format(previous_state)) \ + else 'leave' if self.__nodes.get('{}_leave'.format(previous_state)) \ + else list(self.__states[previous_state]['conditionsets'].keys())[-1] + lastnode = self.__nodes['{}_{}'.format(previous_state, condition_node)] + self.__nodes['{}_above'.format(state)] = pydotplus.Node('{}_above'.format(state), pos=position, shape="square", width="0", label="") - self.__graph.add_node(self.__nodes[state]) - self.__graph.add_node(self.__nodes['{}_right'.format(state)]) - conditionset_positions = [] - actionlist_enter = '' - actionlist_stay = '' - actionlist_leave = '' - actionlist_pass = '' - condition_tooltip = '' - action_tooltip = '' - j = 0 - new_x = 0.55 - actions_enter = self.__states[state].get('actions_enter') or [] - actions_enter_or_stay = self.__states[state].get('actions_enter_or_stay') or [] - actions_stay = self.__states[state].get('actions_stay') or [] - actions_leave = self.__states[state].get('actions_leave') or [] - actions_pass = self.__states[state].get('actions_pass') or [] - action_tooltip_count_enter = 0 - action_tooltip_count_stay = 0 - action_tooltip_count_leave = 0 - action_tooltip_count_pass = 0 - action_tooltip_enter = "" - action_tooltip_stay = "" - action_tooltip_leave = "" - action_tooltip_pass = "" - for j, conditionset in enumerate(self.__states[state]['conditionsets']): - cond3 = conditionset == '' - try: - cond1 = i >= list(self.__states.keys()).index(self.__active_state) - except Exception: - cond1 = True - try: - cond4 = i == list(self.__states.keys()).index(self.__active_state) - except Exception: - cond4 = True - #self._log_debug('i {}, index of active state {}', i, list(self.__states.keys()).index(self.__active_state)) - try: - cond2 = (j > list(self.__states[state]['conditionsets'].keys()).index(self.__active_conditionset) - or i > list(self.__states.keys()).index(self.__active_state)) - except Exception: - cond2 = False if cond3 and cond4 else True - color = "gray" if cond1 and cond2 else "olivedrab" \ - if (conditionset == self.__active_conditionset or cond3) and state == self.__active_state else "indianred2" - try: - cond5 = i >= list(self.__states.keys()).index(self.__active_state) - except Exception: - cond5 = True - - cond6 = conditionset in ['', self.__active_conditionset] and state == self.__active_state - cond_enter = True if self.__states[state].get('enter') is True else False - cond_stay = True if self.__states[state].get('stay') is True else False - active = True if cond_enter and cond6 else False - - if len(actions_enter) > 0 or len(actions_enter_or_stay) > 0: - actionlist_enter, action_tooltip_enter, action_tooltip_count_enter = \ - self._actionlabel(state, 'actions_enter', conditionset, active) - active = True if cond_stay and cond6 else False - if len(actions_stay) > 0 or len(actions_enter_or_stay) > 0: - actionlist_stay, action_tooltip_stay, action_tooltip_count_stay = \ - self._actionlabel(state, 'actions_stay', conditionset, active) - cond_leave = True if self.__states[state].get('leave') is True else False - active = True if cond_leave else False - - if len(actions_leave) > 0: - actionlist_leave, action_tooltip_leave, action_tooltip_count_leave = \ - self._actionlabel(state, 'actions_leave', conditionset, active) - cond_pass = True if self.__states[state].get('pass') is True else False - active = False if (cond5 and not cond_pass) or cond_leave else True - if len(actions_pass) > 0: - actionlist_pass, action_tooltip_pass, action_tooltip_count_pass = \ - self._actionlabel(state, 'actions_pass', conditionset, active) - - new_y -= 1 * self.__scalefactor if j == 0 else 2 * self.__scalefactor - position = '{},{}!'.format(0.3, new_y) - conditionset_positions.append(new_y) - conditionlist, condition_tooltip, condition_tooltip_count = self._conditionlabel(state, conditionset) - - label = 'no condition' if conditionset == '' else conditionset - self.__nodes['{}_{}'.format(state, conditionset)] = pydotplus.Node( - '{}_{}'.format(state, conditionset), style="filled", fillcolor=color, shape="diamond", - label=label, pos=position) - #self._log_debug('Node {} {} drawn. Conditionlist {}', state, conditionset, conditionlist) - position = '{},{}!'.format(0.1, new_y) - xlabel = '1 tooltip' if condition_tooltip_count == 1\ - else '{} tooltips'.format(condition_tooltip_count)\ - if condition_tooltip_count > 1 else '' - if not conditionlist == '': - self.__nodes['{}_{}_conditions'.format(state, conditionset)] = pydotplus.Node( - '{}_{}_conditions'.format(state, conditionset), style="filled", fillcolor=color, - shape="rect", label=conditionlist, pos=position, tooltip=condition_tooltip, xlabel=xlabel) - self.__graph.add_node(self.__nodes['{}_{}_conditions'.format(state, conditionset)]) - # Create a dotted line between conditionlist and conditionset name - parenthesis_edge = pydotplus.Edge(self.__nodes['{}_{}_conditions'.format(state, conditionset)], self.__nodes['{}_{}'.format(state, conditionset)], arrowhead="none", color="black", style="dotted", constraint="false") - self.__graph.add_edge(parenthesis_edge) - self.__graph.add_node(self.__nodes['{}_{}'.format(state, conditionset)]) - - new_x = 0.55 - if not actionlist_enter == '': - position = '{},{}!'.format(new_x, new_y) - xlabel = '1 tooltip' if action_tooltip_count_enter == 1\ - else '{} tooltips'.format(action_tooltip_count_enter)\ - if action_tooltip_count_enter > 1 else '' - #self._log_debug('action enter: {}', position) - self.__nodes['{}_{}_actions_enter'.format(state, conditionset)] = pydotplus.Node( - '{}_{}_actions_enter'.format(state, conditionset), style="filled", fillcolor=color, - shape="rectangle", label=actionlist_enter, pos=position, tooltip=action_tooltip_enter, - xlabel=xlabel) - self.__graph.add_node(self.__nodes['{}_{}_actions_enter'.format(state, conditionset)]) - self._add_actioncondition(state, conditionset, 'actions_enter', new_y, cond1, cond2) - - if not actionlist_stay == '': - new_y -= 0.05 if not actionlist_enter == '' else 0 - position = '{},{}!'.format(new_x, new_y) - xlabel = '1 tooltip' if action_tooltip_count_stay == 1\ - else '{} tooltips'.format(action_tooltip_count_stay)\ - if action_tooltip_count_stay > 1 else '' - #self._log_debug('action stay: {}', position) - self.__nodes['{}_{}_actions_stay'.format(state, conditionset)] = pydotplus.Node( - '{}_{}_actions_stay'.format(state, conditionset), style="filled", fillcolor=color, - shape="rectangle", label=actionlist_stay, pos=position, tooltip=action_tooltip_stay, - xlabel=xlabel) - self.__graph.add_node(self.__nodes['{}_{}_actions_stay'.format(state, conditionset)]) - self._add_actioncondition(state, conditionset, 'actions_stay', new_y, cond1, cond2) - - position = '{},{}!'.format(0.55, new_y) - cond1 = self.__nodes.get('{}_{}_actions_enter'.format(state, conditionset)) is None - cond2 = self.__nodes.get('{}_{}_actions_stay'.format(state, conditionset)) is None - cond3 = self.__nodes.get('{}_{}_actions_leave'.format(state, conditionset)) is None - cond4 = self.__nodes.get('{}_{}_actions_pass'.format(state, conditionset)) is None - if cond1 and cond2 and cond3 and cond4: - self.__nodes['{}_{}_right'.format(state, conditionset)] = pydotplus.Node('{}_{}_right'.format( - state, conditionset), shape="circle", width="0.7", pos=position, label="", fillcolor="black", - style="filled", tooltip="No Action") - self.__graph.add_node(self.__nodes['{}_{}_right'.format(state, conditionset)]) - 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', - 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')) - #self._log_debug('Drew line from state') - else: - self.__graph.add_edge(pydotplus.Edge(previous_conditionset, - self.__nodes['{}_{}'.format(state, conditionset)], - style='bold', color='black', tooltip='check next conditionset')) - previous_conditionset = self.__nodes['{}_{}'.format(state, conditionset)] + self.__graph.add_node(self.__nodes['{}_above'.format(state)]) + position = '{},{}!'.format(10 * self.__widthfactor, new_y) + self.__nodes['{}_above_right'.format(state)] = pydotplus.Node('{}_above_right'.format(state), + pos=position, shape="square", width="0", label="") + self.__graph.add_node(self.__nodes['{}_above_right'.format(state)]) + self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_above'.format(state)], + self.__nodes['{}_above_right'.format(state)], style='bold', + color='black', label="", dir="none")) + self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_above_right'.format(state)], lastnode, + style='bold', color='black', label="False ", dir="none")) + self.__graph.add_edge(pydotplus.Edge(state, self.__nodes['{}_above'.format(state)], style='bold', + color='black', label="", dir="back")) + new_y -= 1.5 * self.__heightfactor + 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'])) + position = '{},{}!'.format(10 * self.__widthfactor, new_y) + self.__nodes['{}_right'.format(state)] = pydotplus.Node('{}_right'.format(state), pos=position, + shape="square", width="0", label="") + self.__graph.add_node(self.__nodes[state]) + self.__graph.add_node(self.__nodes['{}_right'.format(state)]) + conditionset_nodeheights = [] + actionlist_enter = '' + actionlist_stay = '' + actionlist_leave = '' + actionlist_pass = '' + condition_tooltip = '' + action_tooltip = '' + j = 0 + new_x = 0.55 + actions_enter = self.__states[state].get('actions_enter') or [] + actions_enter_or_stay = self.__states[state].get('actions_enter_or_stay') or [] + actions_stay = self.__states[state].get('actions_stay') or [] + actions_leave = self.__states[state].get('actions_leave') or [] + actions_pass = self.__states[state].get('actions_pass') or [] + action_tooltip_count_enter = 0 + action_tooltip_count_stay = 0 + action_tooltip_count_leave = 0 + action_tooltip_count_pass = 0 + action_tooltip_enter = "" + action_tooltip_stay = "" + action_tooltip_leave = "" + action_tooltip_pass = "" + action_y = new_y + actions_nodeheights = {} + nodeheight = 0 + + for j, conditionset in enumerate(self.__states[state]['conditionsets']): + cond3 = conditionset == '' + try: + cond1 = i >= list(self.__states.keys()).index(self.__active_state) + except Exception: + cond1 = True + try: + cond4 = i == list(self.__states.keys()).index(self.__active_state) + except Exception: + cond4 = True + #self._log_debug('i {}, index of active state {}', i, list(self.__states.keys()).index(self.__active_state)) + try: + cond2 = (j > list(self.__states[state]['conditionsets'].keys()).index(self.__active_conditionset) + or i > list(self.__states.keys()).index(self.__active_state)) + except Exception: + cond2 = False if cond3 and cond4 else True + color = "gray" if cond1 and cond2 else "olivedrab" \ + if (conditionset == self.__active_conditionset or cond3) and state == self.__active_state else "indianred2" + try: + cond5 = i >= list(self.__states.keys()).index(self.__active_state) + except Exception: + cond5 = True + + cond6 = conditionset in ['', self.__active_conditionset] and state == self.__active_state + cond_enter = True if self.__states[state].get('enter') is True else False + cond_stay = True if self.__states[state].get('stay') is True else False + active = True if cond_enter and cond6 else False + + if len(actions_enter) > 0 or len(actions_enter_or_stay) > 0: + actionlist_enter, action_tooltip_enter, action_tooltip_count_enter = \ + self._actionlabel(state, 'actions_enter', conditionset, active) + active = True if cond_stay and cond6 else False + if len(actions_stay) > 0 or len(actions_enter_or_stay) > 0: + actionlist_stay, action_tooltip_stay, action_tooltip_count_stay = \ + self._actionlabel(state, 'actions_stay', conditionset, active) + cond_leave = True if self.__states[state].get('leave') is True else False + active = True if cond_leave else False if len(actions_leave) > 0: - new_y -= 1 * self.__scalefactor if j == 0 else 2 * self.__scalefactor - position = '{},{}!'.format(0.3, new_y) - try: - cond2 = i >= list(self.__states.keys()).index(self.__active_state) - except Exception as ex: - cond2 = True - cond3 = True if self.__states[state].get('leave') is True else False - color = "gray" if cond2 and not cond3 else "olivedrab" if cond3 else "indianred2" - self.__nodes['{}_leave'.format(state)] = pydotplus.Node('{}_leave'.format(state), - style="filled", fillcolor=color, shape="diamond", - label='leave', pos=position) - self.__graph.add_node(self.__nodes['{}_leave'.format(state)]) - self.__graph.add_edge(pydotplus.Edge(previous_conditionset, self.__nodes['{}_leave'.format(state)], - style='bold', color='black', tooltip='check leave')) - - position = '{},{}!'.format(new_x, new_y) - xlabel = '1 tooltip' if action_tooltip_count_leave == 1\ - else '{} tooltips'.format(action_tooltip_count_leave)\ - if action_tooltip_count_leave > 1 else '' - #self._log_debug('action leave: {}', position) - self.__nodes['{}_actions_leave'.format(state)] = pydotplus.Node('{}_actions_leave'.format(state), - style="filled", fillcolor=color, - shape="rectangle", label=actionlist_leave, - pos=position, align="center", - tooltip=action_tooltip_leave, - xlabel=xlabel) - self.__graph.add_node(self.__nodes['{}_actions_leave'.format(state)]) - self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_leave'.format(state)], - self.__nodes['{}_actions_leave'.format(state)], style='bold', - taillabel=" True", tooltip='run leave actions')) - previous_conditionset = self.__nodes['{}_leave'.format(state)] - + actionlist_leave, action_tooltip_leave, action_tooltip_count_leave = \ + self._actionlabel(state, 'actions_leave', conditionset, active) + cond_pass = True if self.__states[state].get('pass') is True else False + active = False if (cond5 and not cond_pass) or cond_leave else True if len(actions_pass) > 0: - new_y -= 1 * self.__scalefactor if j == 0 else 2 * self.__scalefactor - position = '{},{}!'.format(0.3, new_y) - try: - cond2 = i >= list(self.__states.keys()).index(self.__active_state) - except Exception: - cond2 = True - cond3 = True if self.__states[state].get('pass') is True else False - color = "gray" if cond2 and not cond3 else "olivedrab" if cond3 else "indianred2" - self.__nodes['{}_pass'.format(state)] = pydotplus.Node('{}_pass'.format(state), - style="filled", fillcolor=color, shape="diamond", - label='pass', pos=position) - self.__graph.add_node(self.__nodes['{}_pass'.format(state)]) - self.__graph.add_edge(pydotplus.Edge(previous_conditionset, self.__nodes['{}_pass'.format(state)], - style='bold', color='black', tooltip='check pass')) - - position = '{},{}!'.format(new_x, new_y) - xlabel = '1 tooltip' if action_tooltip_count_pass == 1\ - else '{} tooltips'.format(action_tooltip_count_pass)\ - if action_tooltip_count_pass > 1 else '' - #self._log_debug('action pass: {}', position) - self.__nodes['{}_actions_pass'.format(state)] = pydotplus.Node('{}_actions_pass'.format(state), - style="filled", fillcolor=color, - shape="rectangle", label=actionlist_pass, - pos=position, align="center", - tooltip=action_tooltip_pass, - xlabel=xlabel) - self.__graph.add_node(self.__nodes['{}_actions_pass'.format(state)]) - self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_pass'.format(state)], - self.__nodes['{}_actions_pass'.format(state)], style='bold', - taillabel=" True", tooltip='run pass actions')) - - previous_state = state - - result = self.__graph.write_svg(filename, prog='fdp') + actionlist_pass, action_tooltip_pass, action_tooltip_count_pass = \ + self._actionlabel(state, 'actions_pass', conditionset, active) + + conditionlist, condition_tooltip, condition_tooltip_count = self._conditionlabel(state, conditionset) + titles = len(re.findall(r'', conditionlist)) + entries = len(re.findall(r'', conditionlist)) + nodeheight = 0.958 + 0.5 + 0.479 # main header, last line, spacing on top and bottom + nodeheight += titles * 1.292 # each title + nodeheight += entries * 1 #each entry + nodeheight /= 5.7 + nodeheight = round(nodeheight, 4) + conditionset_nodeheights.append(nodeheight) + new_y -= nodeheight * self.__heightfactor + if j > 0: + new_y -= max(float(actions_nodeheights.get(j-1, 0.0) + 0.2), conditionset_nodeheights[j-1] + 0.2) * self.__heightfactor + else: + new_y -= 0.46 * self.__heightfactor # half ellipse on top + position = '{},{}!'.format(10 * self.__widthfactor, new_y) + label = 'no condition' if conditionset == '' else conditionset + self.__nodes['{}_{}'.format(state, conditionset)] = pydotplus.Node( + '{}_{}'.format(state, conditionset), style="filled", fillcolor=color, shape="diamond", + label=label, pos=position) + #self._log_debug('Node {} {} drawn. Conditionlist {}', state, conditionset, conditionlist) + position = '{},{}!'.format(3 * self.__widthfactor, new_y) + xlabel = '1 tooltip' if condition_tooltip_count == 1\ + else '{} tooltips'.format(condition_tooltip_count)\ + if condition_tooltip_count > 1 else '' + if not conditionlist == '': + self.__nodes['{}_{}_conditions'.format(state, conditionset)] = pydotplus.Node( + '{}_{}_conditions'.format(state, conditionset), style="filled", fillcolor=color, + shape="rect", label=conditionlist, pos=position, tooltip=condition_tooltip, xlabel=xlabel) + self.__graph.add_node(self.__nodes['{}_{}_conditions'.format(state, conditionset)]) + # Create a dotted line between conditionlist and conditionset name + parenthesis_edge = pydotplus.Edge(self.__nodes['{}_{}_conditions'.format(state, conditionset)], + self.__nodes['{}_{}'.format(state, conditionset)], + arrowhead="none", color="black", style="dotted", constraint="false") + self.__graph.add_edge(parenthesis_edge) + self.__graph.add_node(self.__nodes['{}_{}'.format(state, conditionset)]) + action_y = new_y + new_x = 17.5 * self.__widthfactor + nodeheights = 0 + nodeheight = 0 + + if not actionlist_enter == '': + nodeheight = 0.644 + nodeheight += len(re.findall(r'', actionlist_enter)) * 0.67 + nodeheight /= 5.7 + nodeheight = round(nodeheight, 4) + nodeheight = max(0.4, nodeheight) + nodeheights += nodeheight + last_action_nodeheight = nodeheight + position = '{},{}!'.format(new_x, action_y) + xlabel = '1 tooltip' if action_tooltip_count_enter == 1\ + else '{} tooltips'.format(action_tooltip_count_enter)\ + if action_tooltip_count_enter > 1 else '' + #self._log_debug('action enter: {}', position) + self.__nodes['{}_{}_actions_enter'.format(state, conditionset)] = pydotplus.Node( + '{}_{}_actions_enter'.format(state, conditionset), style="filled", fillcolor=color, + shape="rectangle", label=actionlist_enter, pos=position, tooltip=action_tooltip_enter, + xlabel=xlabel) + self.__graph.add_node(self.__nodes['{}_{}_actions_enter'.format(state, conditionset)]) + self._add_actioncondition(state, conditionset, 'actions_enter', action_y, cond1, cond2) + + if not actionlist_stay == '': + add_nodeheight = nodeheight + action_y -= nodeheight * self.__heightfactor + nodeheight = 0.644 + nodeheight += len(re.findall(r'', actionlist_stay)) * 0.67 + nodeheight /= 5.7 + nodeheight = round(nodeheight, 4) + nodeheight = max(0.4, nodeheight) + add_nodeheight += nodeheight + if action_y != new_y: + nodeheight += 0.2 + action_y -= nodeheight * self.__heightfactor + elif j <= len(self.__states[state]['conditionsets']) - 1: + add_nodeheight = 0 + nodeheights += nodeheight * 2 - 0.2 + last_action_nodeheight = nodeheight + add_nodeheight + + position = '{},{}!'.format(new_x, action_y) + + xlabel = '1 tooltip' if action_tooltip_count_stay == 1\ + else '{} tooltips'.format(action_tooltip_count_stay)\ + if action_tooltip_count_stay > 1 else '' + #self._log_debug('action stay: {}', position) + self.__nodes['{}_{}_actions_stay'.format(state, conditionset)] = pydotplus.Node( + '{}_{}_actions_stay'.format(state, conditionset), style="filled", fillcolor=color, + shape="rectangle", label=actionlist_stay, pos=position, tooltip=action_tooltip_stay, + xlabel=xlabel) + self.__graph.add_node(self.__nodes['{}_{}_actions_stay'.format(state, conditionset)]) + self._add_actioncondition(state, conditionset, 'actions_stay', action_y, cond1, cond2) + + actions_nodeheights[j] = nodeheights + position = '{},{}!'.format(17.5 * self.__widthfactor, action_y) + cond1 = self.__nodes.get('{}_{}_actions_enter'.format(state, conditionset)) is None + cond2 = self.__nodes.get('{}_{}_actions_stay'.format(state, conditionset)) is None + cond3 = self.__nodes.get('{}_{}_actions_leave'.format(state, conditionset)) is None + cond4 = self.__nodes.get('{}_{}_actions_pass'.format(state, conditionset)) is None + if cond1 and cond2 and cond3 and cond4: + actions_nodeheights[j] = 0.5 + last_action_nodeheight = 0.5 + nodeheight= 0.5 + self.__nodes['{}_{}_right'.format(state, conditionset)] = pydotplus.Node('{}_{}_right'.format( + state, conditionset), shape="circle", width="0.7", pos=position, label="", fillcolor="black", + style="filled", tooltip="No Action") + self.__graph.add_node(self.__nodes['{}_{}_right'.format(state, conditionset)]) + 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', + 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')) + #self._log_debug('Drew line from state') + else: + self.__graph.add_edge(pydotplus.Edge(previous_conditionset, + self.__nodes['{}_{}'.format(state, conditionset)], + style='bold', color='black', tooltip='check next conditionset')) + previous_conditionset = self.__nodes['{}_{}'.format(state, conditionset)] + + end_node_heights = 0 + if len(actions_leave) > 0: + action_y -= nodeheight * self.__heightfactor + nodeheight = 0.8 + nodeheight += len(re.findall(r'', actionlist_leave)) * 0.67 + nodeheight /= 5.7 + nodeheight = round(nodeheight, 4) + end_node_heights += max(0.5, nodeheight) + last_action_nodeheight + last_action_nodeheight = max(1.0, nodeheight) + action_y -= max(0.5, nodeheight) * self.__heightfactor + new_y = action_y + position = '{},{}!'.format(10 * self.__widthfactor, action_y) + try: + cond2 = i >= list(self.__states.keys()).index(self.__active_state) + except Exception: + cond2 = True + cond3 = True if self.__states[state].get('leave') is True else False + color = "gray" if cond2 and not cond3 else "olivedrab" if cond3 else "indianred2" + self.__nodes['{}_leave'.format(state)] = pydotplus.Node('{}_leave'.format(state), + style="filled", fillcolor=color, shape="diamond", + label='leave', pos=position) + self.__graph.add_node(self.__nodes['{}_leave'.format(state)]) + self.__graph.add_edge(pydotplus.Edge(previous_conditionset, self.__nodes['{}_leave'.format(state)], + style='bold', color='black', tooltip='check leave')) + + position = '{},{}!'.format(new_x, action_y) + xlabel = '1 tooltip' if action_tooltip_count_leave == 1\ + else '{} tooltips'.format(action_tooltip_count_leave)\ + if action_tooltip_count_leave > 1 else '' + #self._log_debug('action leave: {}', position) + self.__nodes['{}_actions_leave'.format(state)] = pydotplus.Node('{}_actions_leave'.format(state), + style="filled", fillcolor=color, + shape="rectangle", label=actionlist_leave, + pos=position, align="center", + tooltip=action_tooltip_leave, + xlabel=xlabel) + self.__graph.add_node(self.__nodes['{}_actions_leave'.format(state)]) + self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_leave'.format(state)], + self.__nodes['{}_actions_leave'.format(state)], style='bold', + taillabel="True", tooltip='run leave actions')) + previous_conditionset = self.__nodes['{}_leave'.format(state)] + nodeheight += 0.2 + + if len(actions_pass) > 0: + action_y -= nodeheight * self.__heightfactor + nodeheight = 0.8 + nodeheight += len(re.findall(r'', actionlist_pass)) * 0.67 + nodeheight /= 5.7 + nodeheight = round(nodeheight, 4) + if end_node_heights == 0: + end_node_heights += last_action_nodeheight + last_action_nodeheight = max(1.0, nodeheight) + end_node_heights += max(0.5, nodeheight) + action_y -= max(0.5, nodeheight) * self.__heightfactor + new_y = action_y + position = '{},{}!'.format(10 * self.__widthfactor, action_y) + try: + cond2 = i >= list(self.__states.keys()).index(self.__active_state) + except Exception: + cond2 = True + cond3 = True if self.__states[state].get('pass') is True else False + color = "gray" if cond2 and not cond3 else "olivedrab" if cond3 else "indianred2" + self.__nodes['{}_pass'.format(state)] = pydotplus.Node('{}_pass'.format(state), + style="filled", fillcolor=color, shape="diamond", + label='pass', pos=position) + self.__graph.add_node(self.__nodes['{}_pass'.format(state)]) + self.__graph.add_edge(pydotplus.Edge(previous_conditionset, self.__nodes['{}_pass'.format(state)], + style='bold', color='black', tooltip='check pass')) + + position = '{},{}!'.format(new_x, action_y) + xlabel = '1 tooltip' if action_tooltip_count_pass == 1\ + else '{} tooltips'.format(action_tooltip_count_pass)\ + if action_tooltip_count_pass > 1 else '' + #self._log_debug('action pass: {}', position) + self.__nodes['{}_actions_pass'.format(state)] = pydotplus.Node('{}_actions_pass'.format(state), + style="filled", fillcolor=color, + shape="rectangle", label=actionlist_pass, + pos=position, align="center", + tooltip=action_tooltip_pass, + xlabel=xlabel) + self.__graph.add_node(self.__nodes['{}_actions_pass'.format(state)]) + self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_pass'.format(state)], + self.__nodes['{}_actions_pass'.format(state)], style='bold', + taillabel=" True", tooltip='run pass actions')) + last_height = conditionset_nodeheights[-1] - end_node_heights + 0.1 + conditionset_nodeheights.append(last_height) + above_nodeheights = conditionset_nodeheights + previous_state = state + + result = self.__graph.write_svg(filename, prog='neato') return result diff --git a/stateengine/__init__.py b/stateengine/__init__.py index 5b5296938..60134a690 100755 --- a/stateengine/__init__.py +++ b/stateengine/__init__.py @@ -33,6 +33,7 @@ import os import copy from lib.model.smartplugin import * +from lib.item import Items from .webif import WebInterface from datetime import datetime @@ -47,7 +48,7 @@ class StateEngine(SmartPlugin): - PLUGIN_VERSION = '2.2.0' + PLUGIN_VERSION = '2.2.1' # Constructor # noinspection PyUnusedLocal,PyMissingConstructor @@ -55,6 +56,7 @@ def __init__(self, sh): super().__init__() StateEngineDefaults.logger = self.logger self._items = self.abitems = {} + self.itemsApi = Items.get_instance() self.mod_http = None self.__sh = sh self.alive = False @@ -130,9 +132,9 @@ def parse_item(self, item): # Initialization of plugin def run(self): # Initialize - StateEngineStructs.global_struct = copy.deepcopy(self.__sh.items.return_struct_definitions()) + StateEngineStructs.global_struct = copy.deepcopy(self.itemsApi.return_struct_definitions()) self.logger.info("Init StateEngine items") - for item in self.__sh.find_items("se_plugin"): + for item in self.itemsApi.find_items("se_plugin"): if item.conf["se_plugin"] == "active": try: abitem = StateEngineItem.SeItem(self.__sh, item, self) @@ -207,10 +209,12 @@ def get_items(self): finallist.append(self._items[i]) return finallist - def get_graph(self, abitem, graphtype='link'): + def get_graph(self, abitem, graphtype='link', width=1, height=1): if isinstance(abitem, str): abitem = self._items[abitem] webif = StateEngineWebif.WebInterface(abitem) + webif.width = width + webif.height = height try: os.makedirs(self.path_join(self.get_plugin_dir(), 'webif/static/img/visualisations/')) except OSError: @@ -229,11 +233,9 @@ def get_graph(self, abitem, graphtype='link'): except Exception: formatted_date = "Unbekannt" return f'
{self.translate("Letzte Aktualisierung:")} {formatted_date}
\ - \ + \ ' else: return '' diff --git a/stateengine/plugin.yaml b/stateengine/plugin.yaml index be3495046..d92705241 100755 --- a/stateengine/plugin.yaml +++ b/stateengine/plugin.yaml @@ -35,11 +35,11 @@ plugin: - Python Modul pydotplus: ``pip3 install pydotplus``\n ' maintainer: onkelandy - tester: 'Morg42' + tester: 'Morg42, bmxp' state: ready support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1303071-stateengine-plugin-support - version: '2.2.0' + version: '2.2.1' sh_minversion: '1.6' multi_instance: False classname: StateEngine diff --git a/stateengine/webif/__init__.py b/stateengine/webif/__init__.py index 809f8084b..ee758ad5c 100755 --- a/stateengine/webif/__init__.py +++ b/stateengine/webif/__init__.py @@ -26,7 +26,6 @@ ######################################################################### import json - from lib.model.smartplugin import SmartPluginWebIf @@ -63,8 +62,6 @@ def index(self, action=None, item_id=None, item_path=None, reload=None, abitem=N :return: contents of the template after beeing rendered """ - item = self.plugin.get_sh().return_item(item_path) - tmpl = self.tplenv.get_template('{}.html'.format(page)) pagelength = self.plugin.get_parameter_value('webif_pagelength') if action == "get_graph" and abitem is not None: diff --git a/stateengine/webif/static/img/visualisations/sign_empty.png b/stateengine/webif/static/img/visualisations/sign_empty.png new file mode 100644 index 000000000..b6354e6c8 Binary files /dev/null and b/stateengine/webif/static/img/visualisations/sign_empty.png differ diff --git a/stateengine/webif/static/svg-pan-zoom.min.js b/stateengine/webif/static/svg-pan-zoom.min.js new file mode 100644 index 000000000..cadc36e33 --- /dev/null +++ b/stateengine/webif/static/svg-pan-zoom.min.js @@ -0,0 +1,3 @@ +// svg-pan-zoom v3.6.2 +// https://github.com/bumbu/svg-pan-zoom +!function s(r,a,l){function u(e,t){if(!a[e]){if(!r[e]){var o="function"==typeof require&&require;if(!t&&o)return o(e,!0);if(h)return h(e,!0);var n=new Error("Cannot find module '"+e+"'");throw n.code="MODULE_NOT_FOUND",n}var i=a[e]={exports:{}};r[e][0].call(i.exports,function(t){return u(r[e][1][t]||t)},i,i.exports,s,r,a,l)}return a[e].exports}for(var h="function"==typeof require&&require,t=0;tthis.options.maxZoom*n.zoom&&(t=this.options.maxZoom*n.zoom/this.getZoom());var i=this.viewport.getCTM(),s=e.matrixTransform(i.inverse()),r=this.svg.createSVGMatrix().translate(s.x,s.y).scale(t).translate(-s.x,-s.y),a=i.multiply(r);a.a!==i.a&&this.viewport.setCTM(a)},i.prototype.zoom=function(t,e){this.zoomAtPoint(t,a.getSvgCenterPoint(this.svg,this.width,this.height),e)},i.prototype.publicZoom=function(t,e){e&&(t=this.computeFromRelativeZoom(t)),this.zoom(t,e)},i.prototype.publicZoomAtPoint=function(t,e,o){if(o&&(t=this.computeFromRelativeZoom(t)),"SVGPoint"!==r.getType(e)){if(!("x"in e&&"y"in e))throw new Error("Given point is invalid");e=a.createSVGPoint(this.svg,e.x,e.y)}this.zoomAtPoint(t,e,o)},i.prototype.getZoom=function(){return this.viewport.getZoom()},i.prototype.getRelativeZoom=function(){return this.viewport.getRelativeZoom()},i.prototype.computeFromRelativeZoom=function(t){return t*this.viewport.getOriginalState().zoom},i.prototype.resetZoom=function(){var t=this.viewport.getOriginalState();this.zoom(t.zoom,!0)},i.prototype.resetPan=function(){this.pan(this.viewport.getOriginalState())},i.prototype.reset=function(){this.resetZoom(),this.resetPan()},i.prototype.handleDblClick=function(t){var e;if((this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),this.options.controlIconsEnabled)&&-1<(t.target.getAttribute("class")||"").indexOf("svg-pan-zoom-control"))return!1;e=t.shiftKey?1/(2*(1+this.options.zoomScaleSensitivity)):2*(1+this.options.zoomScaleSensitivity);var o=a.getEventPoint(t,this.svg).matrixTransform(this.svg.getScreenCTM().inverse());this.zoomAtPoint(e,o)},i.prototype.handleMouseDown=function(t,e){this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),r.mouseAndTouchNormalize(t,this.svg),this.options.dblClickZoomEnabled&&r.isDblClick(t,e)?this.handleDblClick(t):(this.state="pan",this.firstEventCTM=this.viewport.getCTM(),this.stateOrigin=a.getEventPoint(t,this.svg).matrixTransform(this.firstEventCTM.inverse()))},i.prototype.handleMouseMove=function(t){if(this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),"pan"===this.state&&this.options.panEnabled){var e=a.getEventPoint(t,this.svg).matrixTransform(this.firstEventCTM.inverse()),o=this.firstEventCTM.translate(e.x-this.stateOrigin.x,e.y-this.stateOrigin.y);this.viewport.setCTM(o)}},i.prototype.handleMouseUp=function(t){this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),"pan"===this.state&&(this.state="none")},i.prototype.fit=function(){var t=this.viewport.getViewBox(),e=Math.min(this.width/t.width,this.height/t.height);this.zoom(e,!0)},i.prototype.contain=function(){var t=this.viewport.getViewBox(),e=Math.max(this.width/t.width,this.height/t.height);this.zoom(e,!0)},i.prototype.center=function(){var t=this.viewport.getViewBox(),e=.5*(this.width-(t.width+2*t.x)*this.getZoom()),o=.5*(this.height-(t.height+2*t.y)*this.getZoom());this.getPublicInstance().pan({x:e,y:o})},i.prototype.updateBBox=function(){this.viewport.simpleViewBoxCache()},i.prototype.pan=function(t){var e=this.viewport.getCTM();e.e=t.x,e.f=t.y,this.viewport.setCTM(e)},i.prototype.panBy=function(t){var e=this.viewport.getCTM();e.e+=t.x,e.f+=t.y,this.viewport.setCTM(e)},i.prototype.getPan=function(){var t=this.viewport.getState();return{x:t.x,y:t.y}},i.prototype.resize=function(){var t=a.getBoundingClientRectNormalized(this.svg);this.width=t.width,this.height=t.height;var e=this.viewport;e.options.width=this.width,e.options.height=this.height,e.processCTM(),this.options.controlIconsEnabled&&(this.getPublicInstance().disableControlIcons(),this.getPublicInstance().enableControlIcons())},i.prototype.destroy=function(){var e=this;for(var t in this.beforeZoom=null,this.onZoom=null,this.beforePan=null,this.onPan=null,(this.onUpdatedCTM=null)!=this.options.customEventsHandler&&this.options.customEventsHandler.destroy({svgElement:this.svg,eventsListenerElement:this.options.eventsListenerElement,instance:this.getPublicInstance()}),this.eventListeners)(this.options.eventsListenerElement||this.svg).removeEventListener(t,this.eventListeners[t],!this.options.preventMouseEventsDefault&&h);this.disableMouseWheelZoom(),this.getPublicInstance().disableControlIcons(),this.reset(),c=c.filter(function(t){return t.svg!==e.svg}),delete this.options,delete this.viewport,delete this.publicInstance,delete this.pi,this.getPublicInstance=function(){return null}},i.prototype.getPublicInstance=function(){var o=this;return this.publicInstance||(this.publicInstance=this.pi={enablePan:function(){return o.options.panEnabled=!0,o.pi},disablePan:function(){return o.options.panEnabled=!1,o.pi},isPanEnabled:function(){return!!o.options.panEnabled},pan:function(t){return o.pan(t),o.pi},panBy:function(t){return o.panBy(t),o.pi},getPan:function(){return o.getPan()},setBeforePan:function(t){return o.options.beforePan=null===t?null:r.proxy(t,o.publicInstance),o.pi},setOnPan:function(t){return o.options.onPan=null===t?null:r.proxy(t,o.publicInstance),o.pi},enableZoom:function(){return o.options.zoomEnabled=!0,o.pi},disableZoom:function(){return o.options.zoomEnabled=!1,o.pi},isZoomEnabled:function(){return!!o.options.zoomEnabled},enableControlIcons:function(){return o.options.controlIconsEnabled||(o.options.controlIconsEnabled=!0,s.enable(o)),o.pi},disableControlIcons:function(){return o.options.controlIconsEnabled&&(o.options.controlIconsEnabled=!1,s.disable(o)),o.pi},isControlIconsEnabled:function(){return!!o.options.controlIconsEnabled},enableDblClickZoom:function(){return o.options.dblClickZoomEnabled=!0,o.pi},disableDblClickZoom:function(){return o.options.dblClickZoomEnabled=!1,o.pi},isDblClickZoomEnabled:function(){return!!o.options.dblClickZoomEnabled},enableMouseWheelZoom:function(){return o.enableMouseWheelZoom(),o.pi},disableMouseWheelZoom:function(){return o.disableMouseWheelZoom(),o.pi},isMouseWheelZoomEnabled:function(){return!!o.options.mouseWheelZoomEnabled},setZoomScaleSensitivity:function(t){return o.options.zoomScaleSensitivity=t,o.pi},setMinZoom:function(t){return o.options.minZoom=t,o.pi},setMaxZoom:function(t){return o.options.maxZoom=t,o.pi},setBeforeZoom:function(t){return o.options.beforeZoom=null===t?null:r.proxy(t,o.publicInstance),o.pi},setOnZoom:function(t){return o.options.onZoom=null===t?null:r.proxy(t,o.publicInstance),o.pi},zoom:function(t){return o.publicZoom(t,!0),o.pi},zoomBy:function(t){return o.publicZoom(t,!1),o.pi},zoomAtPoint:function(t,e){return o.publicZoomAtPoint(t,e,!0),o.pi},zoomAtPointBy:function(t,e){return o.publicZoomAtPoint(t,e,!1),o.pi},zoomIn:function(){return this.zoomBy(1+o.options.zoomScaleSensitivity),o.pi},zoomOut:function(){return this.zoomBy(1/(1+o.options.zoomScaleSensitivity)),o.pi},getZoom:function(){return o.getRelativeZoom()},setOnUpdatedCTM:function(t){return o.options.onUpdatedCTM=null===t?null:r.proxy(t,o.publicInstance),o.pi},resetZoom:function(){return o.resetZoom(),o.pi},resetPan:function(){return o.resetPan(),o.pi},reset:function(){return o.reset(),o.pi},fit:function(){return o.fit(),o.pi},contain:function(){return o.contain(),o.pi},center:function(){return o.center(),o.pi},updateBBox:function(){return o.updateBBox(),o.pi},resize:function(){return o.resize(),o.pi},getSizes:function(){return{width:o.width,height:o.height,realZoom:o.getZoom(),viewBox:o.viewport.getViewBox()}},destroy:function(){return o.destroy(),o.pi}}),this.publicInstance};var c=[];e.exports=function(t,e){var o=r.getSvg(t);if(null===o)return null;for(var n=c.length-1;0<=n;n--)if(c[n].svg===o)return c[n].instance.getPublicInstance();return c.push({svg:o,instance:new i(o,e)}),c[c.length-1].instance.getPublicInstance()}},{"./control-icons":1,"./shadow-viewport":2,"./svg-utilities":5,"./uniwheel":6,"./utilities":7}],5:[function(t,e,o){var l=t("./utilities"),s="unknown";document.documentMode&&(s="ie"),e.exports={svgNS:"http://www.w3.org/2000/svg",xmlNS:"http://www.w3.org/XML/1998/namespace",xmlnsNS:"http://www.w3.org/2000/xmlns/",xlinkNS:"http://www.w3.org/1999/xlink",evNS:"http://www.w3.org/2001/xml-events",getBoundingClientRectNormalized:function(t){if(t.clientWidth&&t.clientHeight)return{width:t.clientWidth,height:t.clientHeight};if(t.getBoundingClientRect())return t.getBoundingClientRect();throw new Error("Cannot get BoundingClientRect for SVG.")},getOrCreateViewport:function(t,e){var o=null;if(!(o=l.isElement(e)?e:t.querySelector(e))){var n=Array.prototype.slice.call(t.childNodes||t.children).filter(function(t){return"defs"!==t.nodeName&&"#text"!==t.nodeName});1===n.length&&"g"===n[0].nodeName&&null===n[0].getAttribute("transform")&&(o=n[0])}if(!o){var i="viewport-"+(new Date).toISOString().replace(/\D/g,"");(o=document.createElementNS(this.svgNS,"g")).setAttribute("id",i);var s=t.childNodes||t.children;if(s&&0 {% endblock pluginstyles %} {% block pluginscripts %} - + + {% endblock pluginscripts %} {% set update_interval = 0 %} {% set logo_frame = false %} @@ -165,23 +288,21 @@ {{ _('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. + Mittels Buttons und Slider kann jederzeit gezoomt werden. Sofern "Scrollen" nicht aktiv ist, kann + das Mausrad zum zielgerichteten Zoomen und die linke Maustaste zum Verschieben des Ausschnitts genutzt werden.
- + - {{_('Zoom/Pan aktiv')}} + {{_('Scrollen aktiv')}}
- {{ p.get_graph(item, 'graph') }} + {{ p.get_graph(item, 'graph', 1, 1) }}
{% else %}