Skip to content

Commit

Permalink
Merge pull request #962 from onkelandy/stateengine
Browse files Browse the repository at this point in the history
Stateengine Plugin: fixes and improvements
  • Loading branch information
onkelandy authored Sep 28, 2024
2 parents b61eab4 + cb431df commit 44460f9
Show file tree
Hide file tree
Showing 7 changed files with 425 additions and 249 deletions.
160 changes: 100 additions & 60 deletions stateengine/StateEngineAction.py

Large diffs are not rendered by default.

8 changes: 3 additions & 5 deletions stateengine/StateEngineActions.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,7 @@ def update(self, attribute, value):
return _count, _issue
elif isinstance(value, str):
value = ":".join(map(str.strip, value.split(":")))
if value[:1] == '[' and value[-1:] == ']':
value = StateEngineTools.convert_str_to_list(value, False)
value = StateEngineTools.convert_str_to_list(value, False)
if name in self.__actions:
self.__actions[name].update_action_details(self.__state, self.__action_type)
if func == "se_delay":
Expand Down Expand Up @@ -388,8 +387,7 @@ def remove_action(e):
entry = list("{!s}:{!s}".format(k, v) for (k, v) in entry.items())[0]
key, val = StateEngineTools.partition_strip(entry, ":")
val = ":".join(map(str.strip, val.split(":")))
if val[:1] == '[' and val[-1:] == ']':
val = StateEngineTools.convert_str_to_list(val, False)
val = StateEngineTools.convert_str_to_list(val, False)
if key == "function":
parameter[key] = StateEngineTools.cast_str(val)
elif key == "force":
Expand Down Expand Up @@ -567,7 +565,7 @@ def remove_action(e):
_issue_list.append(_issue)
if _action:
self.__actions[name] = _action
self._log_debug("Handle combined issuelist {}", _issue_list)
# self._log_develop("Handle combined issuelist {}", _issue_list)
return _issue_list

# noinspection PyMethodMayBeStatic
Expand Down
43 changes: 23 additions & 20 deletions stateengine/StateEngineItem.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,18 +223,17 @@ def __init__(self, smarthome, item, se_plugin):
self.__name = str(self.__item)
self.__itemClass = Item
# initialize logging

self.__logger.log_level_as_num = 2
self.__log_level = StateEngineValue.SeValue(self, "Log Level", False, "num")

_default_log_level = self.__logger.default_log_level.get()
_returnvalue, _returntype, _using_default, _issue, _ = self.__log_level.set_from_attr(self.__item,
"se_log_level",
_default_log_level)
default_value=_default_log_level)
self.__using_default_log_level = _using_default
_returnvalue = self.__log_level.get()
if isinstance(_returnvalue, list) and len(_returnvalue) == 1:
_returnvalue = _returnvalue[0]
self.__logger.log_level_as_num = 2

_startup_log_level = self.__logger.startup_log_level.get()

Expand Down Expand Up @@ -266,13 +265,13 @@ def __init__(self, smarthome, item, se_plugin):

# get startup delay
self.__startup_delay = StateEngineValue.SeValue(self, "Startup Delay", False, "num")
self.__startup_delay.set_from_attr(self.__item, "se_startup_delay", StateEngineDefaults.startup_delay)
self.__startup_delay.set_from_attr(self.__item, "se_startup_delay", default_value=StateEngineDefaults.startup_delay)
self.__startup_delay_over = False

# Init suspend settings
self.__default_suspend_time = StateEngineDefaults.suspend_time.get()
self.__suspend_time = StateEngineValue.SeValue(self, "Suspension time on manual changes", False, "num")
self.__suspend_time.set_from_attr(self.__item, "se_suspend_time", self.__default_suspend_time)
self.__suspend_time.set_from_attr(self.__item, "se_suspend_time", default_value=self.__default_suspend_time)

# Init laststate and previousstate items/values
self.__config_issues = {}
Expand Down Expand Up @@ -459,7 +458,7 @@ def update_leave_action(self, default_instant_leaveaction):
self.__default_instant_leaveaction = default_instant_leaveaction

_returnvalue_leave, _returntype_leave, _using_default_leave, _issue, _ = self.__instant_leaveaction.set_from_attr(
self.__item, "se_instant_leaveaction", default_instant_leaveaction)
self.__item, "se_instant_leaveaction", default_value=default_instant_leaveaction)

if len(_returnvalue_leave) > 1:
self.__logger.warning("se_instant_leaveaction for item {} can not be defined as a list"
Expand Down Expand Up @@ -704,8 +703,8 @@ def update_current_to_empty(d):
_leaveactions_run = True
elif result is False and last_state != state and state.actions_pass.count() > 0:
_pass_state = state
state.run_pass(self.__pass_repeat.get(state, False), self.__repeat_actions.get())
_key_pass = ['{}'.format(last_state.id), 'pass']
_pass_state.run_pass(self.__pass_repeat.get(state, False), self.__repeat_actions.get())
_key_pass = ['{}'.format(_pass_state.id), 'pass']
self.update_webif(_key_pass, True)
self.__pass_repeat.update({state: True})
if result is True:
Expand Down Expand Up @@ -761,11 +760,6 @@ def update_current_to_empty(d):
"State is a copy and therefore just releasing {}. Skipping state actions, running leave actions "
"of last state, then retriggering.", new_state.is_copy_for.id)
if last_state is not None and self.__ab_alive:
_, self.__nextconditionset_name = self.__update_check_can_enter(last_state, _instant_leaveaction, False)
if self.__nextconditionset_name:
self.__nextconditionset_id = f"{state.state_item.property.path}.{self.__nextconditionset_name}"
else:
self.__nextconditionset_id = ""
self.set_variable('next.conditionset_id', self.__nextconditionset_id)
self.set_variable('next.conditionset_name', self.__nextconditionset_name)
self.__logger.develop("Current variables: {}", self.__variables)
Expand Down Expand Up @@ -941,6 +935,7 @@ def update_can_release_list():

self.__logger.info("".ljust(80, "_"))
self.__logger.info("Handling released_by attributes")
self.__logger.increase_indent("Handling released_by... ")
can_release = {}
all_released_by = {}
skip_copy = True
Expand Down Expand Up @@ -1042,7 +1037,7 @@ def update_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.increase_indent("")
self.__logger.info("".ljust(80, "_"))
return all_released_by

Expand Down Expand Up @@ -1204,7 +1199,7 @@ def list_issues(v):
self.__logger.info("has the following issues:")
self.__logger.increase_indent()
for i, e in enumerate(_issuelist):
_attr = "" if _attrlist is None or not _attrlist[i] else "attribute {}: ".format(_attrlist[i])
_attr = "" if _attrlist is None or not isinstance(_attrlist, list) or not _attrlist[i] else "attribute {}: ".format(_attrlist[i])
if isinstance(e, dict):
print_readable_dict(_attr, e)
else:
Expand All @@ -1214,11 +1209,11 @@ def list_issues(v):
if isinstance(_issuelist[0], dict):
self.__logger.info("has the following issues:")
self.__logger.increase_indent()
_attr = "" if _attrlist is None or not _attrlist[0] else "attribute {}: ".format(_attrlist[0])
_attr = "" if _attrlist is None or not isinstance(_attrlist, list) or not _attrlist[0] else "attribute {}: ".format(_attrlist[0])
print_readable_dict(_attr, _issuelist[0])
self.__logger.decrease_indent()
else:
_attr = "" if _attrlist is None or not _attrlist[0] else " for attribute {}".format(_attrlist[0])
_attr = "" if _attrlist is None or not isinstance(_attrlist, list) or not _attrlist[0] else " for attribute {}".format(_attrlist[0])
self.__logger.info("has the following issue{}: {}", _attr, _issuelist[0])
else:
if isinstance(_issuelist, dict):
Expand Down Expand Up @@ -1330,14 +1325,15 @@ def list_issues(v):
self.__logger.info("")
for entry, value in to_check:
if 'issue' not in value:
text = "Definition {} not used in any action or condition.".format(entry)
text = "Definition {} not used in any action or condition.".format(value.get('attribute', entry))
self.__logger.info("{}", text)
self.__logger.decrease_indent()

def __reorder_states(self, init=True):
_reordered_states = []
self.__logger.info("".ljust(80, "_"))
self.__logger.info("Recalculating state order. Current order: {}", self.__states)
self.__logger.increase_indent()
_copied_states = {}
_add_order = 0
_changed_orders = []
Expand Down Expand Up @@ -1403,6 +1399,7 @@ def __reorder_states(self, init=True):
else:
_reorder_webif[state.id] = self.__webif_infos[state.id]
self.__webif_infos = _reorder_webif
self.__logger.decrease_indent()
self.__logger.info("Recalculated state order. New order: {}", self.__states)
self.__logger.info("".ljust(80, "_"))

Expand Down Expand Up @@ -1900,6 +1897,7 @@ def update_can_release_list():

self.__logger.info("".ljust(80, "_"))
self.__logger.info("Initializing released_by attributes")
self.__logger.increase_indent()
can_release = {}
state_dict = {state.id: state for state in self.__states}
for state in self.__states:
Expand All @@ -1925,7 +1923,7 @@ def update_can_release_list():
self.__config_issues.update({state.id: {'issue': _issuelist, 'attribute': 'se_released_by'}})
state.update_releasedby_internal(_convertedlist)
self.__update_can_release(can_release, state)

self.__logger.decrease_indent()
self.__logger.info("".ljust(80, "_"))

# log item data
Expand Down Expand Up @@ -2112,7 +2110,12 @@ def get_update_original_source(self):

# return value of variable
def get_variable(self, varname):
return self.__variables[varname] if varname in self.__variables else "(Unknown variable '{0}'!)".format(varname)
if varname not in self.__variables:
returnvalue = "(Unknown variable '{0}'!)".format(varname)
self.__logger.warning("Issue when getting variable {}".format(returnvalue))
else:
returnvalue = self.__variables[varname]
return returnvalue

# set value of variable
def set_variable(self, varname, value):
Expand Down
18 changes: 11 additions & 7 deletions stateengine/StateEngineLogger.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def __init__(self, item, manual=False):
self.__name = 'stateengine.{}'.format(item.property.path)
self.__section = item.property.path.replace(".", "_").replace("/", "")
self.__indentlevel = 0
self.__indentprefix = ""
if manual:
self.__log_level_as_num = 2
else:
Expand All @@ -151,7 +152,10 @@ def update_logfile(self):
# Increase indentation level
# by: number of levels to increase
def increase_indent(self, by=1):
self.__indentlevel += by
if isinstance(by, int):
self.__indentlevel += by
else:
self.__indentprefix = by

# Decrease indentation level
# by: number of levels to decrease
Expand All @@ -170,7 +174,7 @@ def log(self, level, text, *args):
indent = "\t" * self.__indentlevel
if args:
text = text.format(*args)
logtext = "{0}{1} {2}\r\n".format(datetime.datetime.now(), indent, text)
logtext = "{0} {1}{2}{3}\r\n".format(datetime.datetime.now(), self.__indentprefix, indent, text)
try:
with open(self.__filename, mode="a", encoding="utf-8") as f:
f.write(logtext)
Expand All @@ -194,7 +198,7 @@ def header(self, text):
def info(self, text, *args):
self.log(1, text, *args)
indent = "\t" * self.__indentlevel
text = '{}{}'.format(indent, text)
text = '{}{}{}'.format(self.__indentprefix, indent, text)
if args:
text = text.format(*args)
self.logger.info(text)
Expand All @@ -205,7 +209,7 @@ def info(self, text, *args):
def debug(self, text, *args):
self.log(2, text, *args)
indent = "\t" * self.__indentlevel
text = '{}{}'.format(indent, text)
text = '{}{}{}'.format(self.__indentprefix, indent, text)
if args:
text = text.format(*args)
self.logger.debug(text)
Expand All @@ -216,7 +220,7 @@ def debug(self, text, *args):
def develop(self, text, *args):
self.log(3, "DEV: " + text, *args)
indent = "\t" * self.__indentlevel
text = '{}{}'.format(indent, text)
text = '{}{}{}'.format(self.__indentprefix, indent, text)
if args:
text = text.format(*args)
self.logger.log(StateEngineDefaults.VERBOSE, text)
Expand All @@ -228,7 +232,7 @@ def develop(self, text, *args):
def warning(self, text, *args):
self.log(1, "WARNING: " + text, *args)
indent = "\t" * self.__indentlevel
text = '{}{}'.format(indent, text)
text = '{}{}{}'.format(self.__indentprefix, indent, text)
if args:
text = text.format(*args)
self.logger.warning(text)
Expand All @@ -240,7 +244,7 @@ def warning(self, text, *args):
def error(self, text, *args):
self.log(1, "ERROR: " + text, *args)
indent = "\t" * self.__indentlevel
text = '{}{}'.format(indent, text)
text = '{}{}{}'.format(self.__indentprefix, indent, text)
if args:
text = text.format(*args)
self.logger.error(text)
Expand Down
12 changes: 8 additions & 4 deletions stateengine/StateEngineState.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ def write_to_log(self):
'actions_stay': {},
'actions_leave': {},
'actions_pass': {},
'leave': False, 'enter': False, 'stay': False,
'leave': False, 'enter': False, 'stay': False, 'pass': False,
'is_copy_for': None, 'releasedby': None})
self._log_decrease_indent()
self._log_info("Finished Web Interface Update")
Expand Down Expand Up @@ -573,6 +573,7 @@ def update_unused(used_attributes, attrib_type, attrib_name):
used_attributes[nested_entry].update({attrib_type: attrib_name})
used_attributes[nested_entry].update(nested_dict)
self.__used_attributes.update(used_attributes)
self._abitem.update_attributes(self.__unused_attributes, self.__used_attributes)

def update_action_status(actn_type, action_status):
def filter_issues(input_dict):
Expand Down Expand Up @@ -613,8 +614,10 @@ def filter_issues(input_dict):
# Add 'used in' and update with existing data
flattened_dict[inner_key]['used in'] = key
flattened_dict[inner_key].update(nested_dict)

self.__used_attributes = deepcopy(flattened_dict)
self.__action_status = filter_issues(self.__action_status)
self._abitem.update_attributes(self.__unused_attributes, self.__used_attributes)

if isinstance(state, SeState):
item_state = state.state_item
Expand Down Expand Up @@ -695,8 +698,9 @@ def filter_issues(input_dict):
for attribute in child_item.conf:
self._log_develop("Filling state with {} action named {} for state {} with config {}", child_name, attribute, state.id, child_item.conf)
action_method.update_action_details(self, action_type)
_action_counts[action_name] += 1
_, _action_status = action_method.update(attribute, child_item.conf.get(attribute))
_result = action_method.update(attribute, child_item.conf.get(attribute))
_action_counts[action_name] += _result[0] if _result else 0
_action_status = _result[1]
if _action_status:
update_action_status(action_name, _action_status)
self._abitem.update_action_status(self.__action_status)
Expand All @@ -716,7 +720,7 @@ def filter_issues(input_dict):
update_action_status("enter_or_stay", _action_status)
self._abitem.update_action_status(self.__action_status)

_total_actioncount = _action_counts["enter"] + _action_counts["stay"] + _action_counts["enter_or_stay"] + _action_counts["leave"]
_total_actioncount = _action_counts["enter"] + _action_counts["stay"] + _action_counts["enter_or_stay"] + _action_counts["leave"] + _action_counts["pass"]

self.update_name(item_state, recursion_depth)
# Complete condition sets and actions at the end
Expand Down
28 changes: 22 additions & 6 deletions stateengine/StateEngineTools.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,14 +317,30 @@ def partition_strip(value, splitchar):
# value: list as string
# returns: list or original value
def convert_str_to_list(value, force=True):
if isinstance(value, str) and (value[:1] == '[' and value[-1:] == ']'):
value = value.strip("[]")
if isinstance(value, str):
orig_value = value
value = value.strip()
if value.startswith('[') and value.endswith(']'):
value = value[1:-1].strip()
else:
return orig_value

if isinstance(value, str) and "," in value:
try:
elements = re.findall(r"'([^']+)'|([^,]+)", value)
flattened_elements = [element[0] if element[0] else element[1] for element in elements]
formatted_str = "[" + ", ".join(
["'" + element.strip(" '\"") + "'" for element in flattened_elements]) + "]"
elements = re.findall(r"'([^']*)'|\"([^\"]*)\"|([^,]+)", value)
flattened_elements = [element[0] if element[0] else (element[1] if element[1] else element[2].strip()) for
element in elements]
flattened_elements = [element.strip() for element in flattened_elements]
formatted_elements = []
for element in flattened_elements:
element = element.strip(" '\"")
if "'" in element:
formatted_elements.append(f'"{element}"') # If element contains single quote, wrap in double quotes
elif '"' in element:
formatted_elements.append(f"'{element}'") # If element contains double quote, wrap in single quotes
else:
formatted_elements.append(f"'{element}'") # Default case, wrap in single quotes
formatted_str = "[" + ", ".join(formatted_elements) + "]"
return literal_eval(formatted_str)
except Exception as ex:
raise ValueError("Problem converting string to list: {}".format(ex))
Expand Down
Loading

0 comments on commit 44460f9

Please sign in to comment.