Skip to content

Commit

Permalink
stateengine plugin: introduce caching for evals. This way the value i…
Browse files Browse the repository at this point in the history
…s assured to be the same throughout the whole item evaluation. Evaluation is also faster now.
  • Loading branch information
onkelandy committed Aug 1, 2024
1 parent 768eb38 commit 267623b
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 103 deletions.
22 changes: 20 additions & 2 deletions stateengine/StateEngineItem.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,22 @@ def ab_alive(self):
def ab_alive(self, value):
self.__ab_alive = value

@property
def eval_cache(self):
return self.__eval_cache

@eval_cache.setter
def eval_cache(self, value):
def contains_any_key(dict_list, new_dict):
new_dict_keys = set(new_dict.keys())
for item in dict_list:
if not new_dict_keys.isdisjoint(item.keys()): # Checks for any common key
return True
return False

if not contains_any_key(self.__eval_cache, value):
self.__eval_cache.append(value)

# Constructor
# smarthome: instance of smarthome.py
# item: item to use
Expand All @@ -191,6 +207,7 @@ def __init__(self, smarthome, item, se_plugin):
self.__se_plugin = se_plugin
self.__active_schedulers = []
self.__release_info = {}
self.__eval_cache = []
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:
Expand Down Expand Up @@ -239,7 +256,6 @@ def __init__(self, smarthome, item, se_plugin):
self.__logger.header("Initialize Item {0} (Log {1}, Level set"
" to {2}, default log level is {3})".format(self.id, self.__logger.name,
_returnvalue, _default_log_level))

# get startup delay
self.__startup_delay = StateEngineValue.SeValue(self, "Startup Delay", False, "num")
self.__startup_delay.set_from_attr(self.__item, "se_startup_delay", StateEngineDefaults.startup_delay)
Expand Down Expand Up @@ -541,6 +557,7 @@ def run_queue(self):
(_, item, caller, source, dest) = job
item_id = item.property.path if item is not None else "(no item)"
self.__logger.update_logfile()
self.__eval_cache = []
self.__logger.header("Update state of item {0}".format(self.__name))
if caller:
self.__logger.debug("Update triggered by {0} (item={1} source={2} dest={3})", caller, item_id,
Expand Down Expand Up @@ -667,6 +684,7 @@ def run_queue(self):
last_state.run_stay(self.__repeat_actions.get())
if self.update_lock.locked():
self.update_lock.release()
self.__eval_cache = []
self.__logger.decrease_indent(50)
self.__logger.debug("State evaluation finished")
self.__logger.info("State evaluation queue empty.")
Expand Down Expand Up @@ -763,7 +781,7 @@ def run_queue(self):

self.__logger.debug("State evaluation finished")
all_released_by = self.__handle_releasedby(new_state, last_state, _instant_leaveaction)

self.__eval_cache = []
self.__logger.decrease_indent(50)
self.__logger.info("State evaluation queue empty.")
if new_state:
Expand Down
182 changes: 81 additions & 101 deletions stateengine/StateEngineValue.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def __init__(self, abitem, name, allow_value_list=False, value_type=None):
except Exception:
pass
self.__name = name
self._abitem = abitem
self.__allow_value_list = allow_value_list
self.__value = None
self.__item = None
Expand Down Expand Up @@ -416,6 +417,7 @@ def set_cast(self, cast_func):
# determine and return value
def get(self, default=None, originalorder=True):
returnvalues = []
_original_listorder = []
try:
_original_listorder = self.__listorder.copy()
except Exception as ex:
Expand Down Expand Up @@ -744,118 +746,96 @@ def __get_from_regex(self):
def __get_eval(self):
# noinspection PyUnusedLocal
sh = self._sh
# noinspection PyUnusedLocal
shtime = self._shtime
if isinstance(self.__eval, str):
self.__eval = StateEngineTools.parse_relative(self.__eval, 'sh.', ['()', '.property.'])
if "stateengine_eval" in self.__eval or "se_eval" in self.__eval:

def contains_get_variable(eval_str):
return 'get_variable("current.' in eval_str or "get_variable('current." in eval_str

def handle_eval_cache(eval_str):
for item in self._abitem.eval_cache:
if eval_str in item:
values = item.get(eval_str)
if f'eval:{eval_str}' in self.__listorder:
self.__listorder[self.__listorder.index(f'eval:{eval_str}')] = [values]
self._log_debug("Got value {} ({}) for eval {} from cache.", values, type(values), eval_str)
return values
return None

def handle_eval(eval_str):
nonlocal sh, shtime
if "stateengine_eval" in eval_str or "se_eval" in eval_str:
# noinspection PyUnusedLocal
stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem)
self._log_debug("Checking eval: {0}", self.__eval)

self._log_debug("Checking eval: {}", eval_str)
self._log_increase_indent()
try:
_newvalue, _issue = self.__do_cast(eval(self.__eval))
_issue_dict = {StateEngineTools.get_eval_name(self.__eval): _issue}
if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['eval']:
self.__get_issues['eval'].append(_issue_dict)
if 'eval:{}'.format(self.__eval) in self.__listorder:
self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = [_newvalue]
values = _newvalue
new_value, issue = self.__do_cast(eval(eval_str))
if not isinstance(eval_str, str):
eval_str = str(eval_str)
issue_dict = {eval_str: issue}
else:
issue_dict = {StateEngineTools.get_eval_name(eval_str): issue}
if issue not in [[], None, [None]] and issue_dict not in self.__get_issues['eval']:
self.__get_issues['eval'].append(issue_dict)
if f'eval:{eval_str}' in self.__listorder:
self.__listorder[self.__listorder.index(f'eval:{eval_str}')] = [new_value]
self._log_decrease_indent()
self._log_debug("Eval result: {0} ({1}).", values, type(values))
self._log_debug("Eval result: {} ({}).", new_value, type(new_value))
if not contains_get_variable(eval_str):
self._abitem.eval_cache = {eval_str: new_value}
self._log_increase_indent()
return new_value
except Exception as ex:
self._log_decrease_indent()
_name = StateEngineTools.get_eval_name(self.__eval)
_issue = "Problem evaluating '{0}': {1}.".format(_name, ex)
_issue_dict = {_name: _issue}
if _issue_dict not in self.__get_issues['eval']:
self.__get_issues['eval'].append(_issue_dict)
self._log_warning(_issue)
self._log_increase_indent()
values = None
if not isinstance(eval_str, str):
eval_str = str(eval_str)
else:
eval_str = StateEngineTools.get_eval_name(eval_str)
issue = f"Problem evaluating '{eval_str}': {ex}."
issue_dict = {eval_str: issue}
if issue_dict not in self.__get_issues['eval']:
self.__get_issues['eval'].append(issue_dict)
self._log_warning(issue)
return None
finally:
self._log_decrease_indent()
else:
if isinstance(self.__eval, list):
values = []
for val in self.__eval:
try:
val = val.replace("\n", "")
except Exception:
pass
self._log_debug("Checking eval {0} from list {1}.", val, self.__eval)
self._log_increase_indent()
if isinstance(val, str):
if "stateengine_eval" in val or "se_eval" in val:
# noinspection PyUnusedLocal
stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem)
try:
_newvalue, _issue = self.__do_cast(eval(val))
_issue_dict = {val: _issue}
if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['eval']:
self.__get_issues['eval'].append(_issue_dict)
if 'eval:{}'.format(val) in self.__listorder:
self.__listorder[self.__listorder.index('eval:{}'.format(val))] = [_newvalue]
value = _newvalue
self._log_decrease_indent()
self._log_debug("Eval result from list: {0}.", value)
self._log_increase_indent()
except Exception as ex:
self._log_decrease_indent()
_issue = "Problem evaluating from list '{0}': {1}.".format(
StateEngineTools.get_eval_name(val), ex)
_issue_dict = {val: _issue}
if _issue_dict not in self.__get_issues['eval']:
self.__get_issues['eval'].append(_issue_dict)
self._log_warning(_issue)
self._log_increase_indent()
value = None
else:
try:
_newvalue, _issue = self.__do_cast(val())
_issue_dict = {str(val): _issue}
if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['eval']:
self.__get_issues['eval'].append(_issue_dict)
if 'eval:{}'.format(val) in self.__listorder:
self.__listorder[self.__listorder.index('eval:{}'.format(val))] = [_newvalue]
value = _newvalue
except Exception as ex:
self._log_decrease_indent()
_issue = "Problem evaluating '{0}': {1}.".format(
StateEngineTools.get_eval_name(val), ex)
_issue_dict = {str(val): _issue}
if _issue_dict not in self.__get_issues['eval']:
self.__get_issues['eval'].append(_issue_dict)
self._log_info(_issue)
value = None
if value is not None:
values.append(value)
self._log_decrease_indent()
else:
self._log_debug("Checking eval (no str, no list): {0}.", self.__eval)

def process_eval_list(eval_list):
values = []
self._log_debug("Processing eval list {}", eval_list)
self._log_increase_indent()
for val in eval_list:
try:
self._log_increase_indent()
_newvalue, _issue = self.__do_cast(self.__eval())
_issue_dict = {_newvalue: _issue}
if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['eval']:
self.__get_issues['eval'].append(_issue_dict)
if 'eval:{}'.format(self.__eval) in self.__listorder:
self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = [_newvalue]
values = _newvalue
self._log_decrease_indent()
self._log_debug("Eval result (no str, no list): {0}.", values)
self._log_increase_indent()
except Exception as ex:
self._log_decrease_indent()
_name = StateEngineTools.get_eval_name(self.__eval)
_issue = "Problem evaluating '{0}': {1}.".format(_name, ex)
self._log_warning(_issue)
self._log_increase_indent()
_issue_dict = {_name: _issue}
if _issue_dict not in self.__get_issues['eval']:
self.__get_issues['eval'].append(_issue_dict)
return None
return values
val = val.replace("\n", "")
except Exception:
pass
cached_value = handle_eval_cache(val)
if cached_value is not None:
values.append(cached_value)
continue

eval_result = handle_eval(val)
if eval_result is not None:
values.append(eval_result)
self._log_decrease_indent()
return values

if isinstance(self.__eval, str):
values = handle_eval_cache(self.__eval)
if values is not None:
return values

self.__eval = StateEngineTools.parse_relative(self.__eval, 'sh.', ['()', '.property.'])
return handle_eval(self.__eval)

elif isinstance(self.__eval, list):
return process_eval_list(self.__eval)
else:
values = handle_eval_cache(str(self.__eval))
if values is not None:
return values
return handle_eval(str(self.__eval))

# Determine value from item
def __get_from_item(self):
Expand Down

0 comments on commit 267623b

Please sign in to comment.