306 lines
14 KiB
Python
306 lines
14 KiB
Python
#import appdaemon.plugins.hass.hassapi as hass
|
|
from datetime import datetime
|
|
from adexpressioncontext import ADExpressionContext
|
|
from expressionparser import ExpressionParser,ParsingException,EvaluationException
|
|
from kwargsparser import kwargsParser
|
|
|
|
def catch_smartvalue_exception(error_lambda):
|
|
def decorator(function):
|
|
def wrapper(*args, **kwargs):
|
|
try: return function(*args, **kwargs)
|
|
except ParsingException as e: error_lambda(args[0],str(e))
|
|
return wrapper
|
|
return decorator
|
|
|
|
class Evaluator():
|
|
|
|
def __init__(self, appdaemon_api, expression,**kwargs):
|
|
self.__expression_parser = []
|
|
self.__entities_to_listen = {}
|
|
self.__debug_log_prefixes = []
|
|
self.__last_evaluation_result = None
|
|
self.__state_callbacks_handle = list()
|
|
|
|
self.__appdaemon_api = appdaemon_api
|
|
self.__activated = False
|
|
self.__callback_trigger_reason_log_string = None
|
|
self.__evaluation_result_log_string = None
|
|
|
|
parser = kwargsParser(kwargs,lambda string: self.__log(string,level = "ERROR"),lambda string: self.__log(string,level = "WARNING"))
|
|
self.__logger_interface = parser.parse_args('logger_interface',None)
|
|
|
|
self.__no_log_prefix = parser.parse_args('no_log_prefix',False) #as no_log_prefix is used to feedback parser error, it need to be defined first
|
|
expression_name = parser.parse_args('expression_name',None)
|
|
if expression_name:
|
|
self.__debug_log_prefixes.append(f"[{expression_name}]")
|
|
|
|
self.__on_update_callback = parser.parse_args('on_update_cb',None,['callback'])
|
|
self.__on_change_callback = parser.parse_args('on_change_cb',None)
|
|
self.__pass_expression_name_to_cb = parser.parse_args('pass_expression_name_to_cb',None)
|
|
self.__log_init = parser.parse_args('log_init',True)
|
|
self.__unavaibility_value = parser.parse_args('unavaibility_value','unavailable') #None means throw an exception
|
|
self.__trigger_callback_on_activation = parser.parse_args('trigger_callback_on_activation',True)
|
|
self.__trigger_callback_on_entity_creation = parser.parse_args('trigger_callback_on_entity_creation',True)
|
|
self.__log_callback_trigger_reason = parser.parse_args('log_callback_trigger_reason',True)
|
|
self.__constants = parser.parse_args('constants',dict())
|
|
self.__sensors_aliases_default_value = parser.parse_args('sensors_aliases_default_value',dict())
|
|
activated = parser.parse_args('activated',True)
|
|
sensor_existence_validation = parser.parse_args('sensor_existence_validation',True)
|
|
|
|
self.expression_context = ADExpressionContext(self.__appdaemon_api,sensor_existence_validation)
|
|
for constant_name,value in self.__constants.items():
|
|
self.expression_context.declare_constant(constant_name,value)
|
|
for alias,value in self.__sensors_aliases_default_value.items():
|
|
self.expression_context.declare_sensor_alias_default_value(alias,value)
|
|
|
|
parser.validate_args()
|
|
|
|
self.__debug_log_prefixes.append("[Parsing]")
|
|
self.__expression_parser = ExpressionParser(str(expression),self.expression_context)
|
|
self.__add_to_entities_to_listen(self.expression_context.get_entities_to_listen())
|
|
self.__debug_log_prefixes.pop()
|
|
|
|
if activated: self.activate()
|
|
|
|
def __has_user_callback(self):
|
|
return self.__on_update_callback or self.__on_change_callback
|
|
|
|
def __has_user_callback_based_on_result(self):
|
|
return self.__on_change_callback
|
|
|
|
def activate(self):
|
|
if not self.__activated:
|
|
self.__activated = True
|
|
if self.__has_user_callback():
|
|
self.__register_internal_callback()
|
|
if self.__has_user_callback_based_on_result() and self.__trigger_callback_on_activation:
|
|
self.__trigger_callbacks("Callback trigger by condition activation",True)
|
|
|
|
def deactivate(self):
|
|
if self.__activated:
|
|
self.__activated = False
|
|
self.__last_evaluation_result = None
|
|
self.__unregister_internal_callback()
|
|
|
|
def __evaluate(self):
|
|
self.__debug_log_prefixes.append("[Evaluating]")
|
|
try: result = self.__expression_parser.evaluate()
|
|
except EvaluationException as e:
|
|
if self.__unavaibility_value != None and "unavailable" in e.operands:
|
|
self.__log_to_string(f"The expression will be evaluated as '{self.__unavaibility_value}' because of the following exception {e}")
|
|
self.__debug_log_prefixes.pop()
|
|
return self.__unavaibility_value
|
|
else:
|
|
self.__debug_log_prefixes.pop()
|
|
raise e
|
|
self.__debug_log_prefixes.pop()
|
|
return result
|
|
|
|
def evaluate(self,log = True):
|
|
try: result = self.__evaluate()
|
|
except EvaluationException as e:
|
|
self.__log(f"{e}",level = "ERROR")
|
|
return None
|
|
if log:
|
|
self.log_evaluation_result()
|
|
return result
|
|
|
|
def log_callback_trigger_reason(self):
|
|
if self.__callback_trigger_reason_log_string:
|
|
self.__log_info(self.__callback_trigger_reason_log_string)
|
|
self.__callback_trigger_reason_log_string = None
|
|
|
|
def log_evaluation_result(self):
|
|
if self.__evaluation_result_log_string:
|
|
self.__log_info(self.__evaluation_result_log_string)
|
|
self.__evaluation_result_log_string = None
|
|
|
|
def get_evaluation_result_log_string(self):
|
|
return self.__evaluation_result_log_string
|
|
|
|
def log_entities_states(self):
|
|
log_string = "Entities states: "
|
|
for entity,attributes in self.__entities_to_listen.items():
|
|
for attribute in attributes:
|
|
if attribute == "state" or attribute == None:
|
|
log_string += f"{entity} = {self.__appdaemon_api.get_state(entity)},"
|
|
#not sure why it was here, most likely a left over
|
|
#handle = self.__appdaemon_api.listen_state(self.__on_condition_state_change,entity)
|
|
else:
|
|
log_string += f"{entity}[{attribute}] = {self.__appdaemon_api.get_state(entity,attribute = attribute)},"
|
|
|
|
self.__log_info(log_string[:-1])
|
|
|
|
def get_entities_to_listen(self):
|
|
return self.__entities_to_listen
|
|
|
|
def get_entities_to_listen_as_list(self):
|
|
entities_list = list()
|
|
for entity_id,attributes in self.__entities_to_listen.items():
|
|
for attribute in attributes:
|
|
if attribute == None:
|
|
entities_list.append(entity_id)
|
|
else:
|
|
entities_list.append(f"{entity_id}.{attribute}")
|
|
return entities_list
|
|
|
|
def __add_to_entities_to_listen(self,extra_entities_to_listen):
|
|
#__add_to_entities_to_listen support both :
|
|
# ['sensor.a.attribute', 'sensor.a','sensor.a.another_attribute'] and
|
|
# { 'sensor.a' : ['attribute','another_attribute'] }
|
|
if isinstance(extra_entities_to_listen, list):
|
|
for entity_string in extra_entities_to_listen:
|
|
splitted_entity = entity_string.split('.')
|
|
|
|
if len(splitted_entity) == 2:
|
|
entity_id = entity_string
|
|
attribute = None
|
|
elif len(splitted_entity) == 3:
|
|
entity_id = f"{splitted_entity[0]}.{splitted_entity[1]}"
|
|
attribute = splitted_entity[2]
|
|
else:
|
|
self.__log(f"Invalid entity string '{entity_string}' present in extra_entities_to_listen",level = "ERROR")
|
|
continue
|
|
|
|
if not entity_id in self.__entities_to_listen:
|
|
self.__entities_to_listen[entity_id] = []
|
|
if not attribute in self.__entities_to_listen[entity_id]:
|
|
self.__entities_to_listen[entity_id].append(attribute)
|
|
|
|
elif isinstance(extra_entities_to_listen, dict):
|
|
for entity in extra_entities_to_listen:
|
|
if not entity in self.__entities_to_listen:
|
|
self.__entities_to_listen[entity] = []
|
|
|
|
#I support both "my.entity" : "attribute" and "my.entity": ["attribute1","attribute2"]
|
|
if isinstance(extra_entities_to_listen[entity], str):
|
|
attribute = extra_entities_to_listen[entity]
|
|
if not attribute in self.__entities_to_listen[entity]: self.__entities_to_listen[entity].append(attribute)
|
|
else:
|
|
for attribute in extra_entities_to_listen[entity]:
|
|
if not attribute in self.__entities_to_listen[entity]: self.__entities_to_listen[entity].append(attribute)
|
|
else:
|
|
self.__log(f"extra_entities_to_listen is in an invalid format: {extra_entities_to_listen}",level = "ERROR")
|
|
|
|
def __register_internal_callback(self):
|
|
assert len(self.__state_callbacks_handle) == 0, f"self.__state_callbacks_handle is not empy, you are probably trying to call __register_internal_callback() twice"
|
|
logstring = ""
|
|
for entity,attributes in self.__entities_to_listen.items():
|
|
attributes_string = ""
|
|
for attribute in attributes:
|
|
if attribute == "state" or attribute == None: #this is not healthy to use 'state' here as at some point an attribute could actually be called "state"
|
|
assert attribute != 'state', "please don't use 'state' as a keyword to design the actual state of the entity, but use None instead"
|
|
handle = self.__appdaemon_api.listen_state(self.__on_condition_state_change,entity)
|
|
else:
|
|
handle = self.__appdaemon_api.listen_state(self.__on_condition_state_change,entity, attribute = attribute)
|
|
|
|
self.__state_callbacks_handle.append(handle)
|
|
|
|
if attributes_string == "" : attributes_string = f"({attribute if attribute else 'state'}"
|
|
else: attributes_string += f",{attribute if attribute else 'state'}"
|
|
|
|
attributes_string += f")"
|
|
|
|
if attributes_string == "(state)": attributes_string = ""
|
|
|
|
if logstring == "":
|
|
logstring = f"Registering CB for {entity}{attributes_string}"
|
|
else: logstring += f", {entity}{attributes_string}"
|
|
|
|
if self.__log_init:
|
|
if logstring == "":
|
|
logstring = f"No CB has been registered"
|
|
self.__log(logstring)
|
|
|
|
def __unregister_internal_callback(self):
|
|
if len(self.__state_callbacks_handle) > 0:
|
|
self.__log(f"Unregistering {len(self.__state_callbacks_handle)} callbacks")
|
|
for handle in self.__state_callbacks_handle:
|
|
self.__appdaemon_api.cancel_listen_state(handle)
|
|
self.__state_callbacks_handle = list()
|
|
|
|
def __trigger_callbacks(self,trigger_reason,triggered_from_activation = False):
|
|
has_log_trigger_reason = False
|
|
has_evaluation_result = False
|
|
|
|
def log_trigger_reason_once(trigger_reason):
|
|
nonlocal has_log_trigger_reason
|
|
if not has_log_trigger_reason:
|
|
has_log_trigger_reason = True
|
|
self.__callback_trigger_reason_log_string = self.__log_to_string(trigger_reason)
|
|
if self.__log_callback_trigger_reason:
|
|
self.log_callback_trigger_reason()
|
|
|
|
def log_evaluation_result_once():
|
|
nonlocal has_evaluation_result
|
|
if not has_evaluation_result:
|
|
has_evaluation_result = True
|
|
if self.__log_callback_trigger_reason: #in that particular case, the evaluation result is a trigger reason
|
|
self.log_evaluation_result()
|
|
|
|
if self.__on_update_callback and not triggered_from_activation:
|
|
log_trigger_reason_once(trigger_reason)
|
|
if self.__pass_expression_name_to_cb != None:
|
|
self.__on_update_callback(self.__pass_expression_name_to_cb)
|
|
else:
|
|
self.__on_update_callback()
|
|
if self.__has_user_callback_based_on_result():
|
|
try: result = self.__evaluate()
|
|
except EvaluationException as e:
|
|
self.__log(f"{e}",level = "ERROR")
|
|
return
|
|
|
|
if result != self.__last_evaluation_result:
|
|
if self.__on_change_callback:
|
|
log_trigger_reason_once(trigger_reason)
|
|
log_evaluation_result_once()
|
|
if self.__pass_expression_name_to_cb != None:
|
|
self.__on_change_callback(self.__pass_expression_name_to_cb,self.__last_evaluation_result,result)
|
|
else:
|
|
self.__on_change_callback(self.__last_evaluation_result,result)
|
|
|
|
self.__last_evaluation_result = result
|
|
|
|
def __on_condition_state_change(self, entity, attribute, old, new, kwargs):
|
|
if old != new and (old != None or self.__trigger_callback_on_entity_creation):
|
|
self.__trigger_callbacks(f"Callback trigger by {entity}({old} => {new})")
|
|
|
|
def __log(self,message,**kwargs):
|
|
log_string = self.__log_to_string(message)
|
|
|
|
if 'level' in kwargs and kwargs['level'] == 'ERROR':
|
|
self.__log_error(log_string)
|
|
elif 'level' in kwargs and kwargs['level'] == 'WARNING':
|
|
self.__log_warning(log_string)
|
|
else:
|
|
self.__log_info(log_string)
|
|
|
|
def __log_info(self,message):
|
|
if self.__logger_interface:
|
|
self.__logger_interface.log_info(message)
|
|
else:
|
|
try: self.__appdaemon_api.log_info(message)
|
|
except AttributeError: self.__appdaemon_api.log(message,level = "INFO")
|
|
|
|
def __log_warning(self,message):
|
|
if self.__logger_interface:
|
|
self.__logger_interface.log_warning(message)
|
|
else:
|
|
try: self.__appdaemon_api.log_warning(message)
|
|
except AttributeError: self.__appdaemon_api.log(message,level = "WARNING")
|
|
|
|
def __log_error(self,message):
|
|
if self.__logger_interface:
|
|
self.__logger_interface.log_error(message)
|
|
else:
|
|
try: self.__appdaemon_api.log_error(message)
|
|
except AttributeError: self.__appdaemon_api.log(message,level = "ERROR")
|
|
|
|
def __log_to_string(self,message):
|
|
dest_string = ""
|
|
if self.__no_log_prefix == False:
|
|
for prefix in self.__debug_log_prefixes:
|
|
dest_string = dest_string + prefix
|
|
if dest_string: dest_string += " "
|
|
return dest_string + message
|