import appdaemon.plugins.hass.hassapi as hass import smartcondition as SmartCondition import smartvalue as SmartValue from kwargsparser import kwargsParser from expressionparser import ParsingException import time from logger_interface import LoggerInterface class VirtualSensorBase: def __init__(self,ad_api,logger_interface, virtual_sensor_name = None, sensor_name = None,super_entity_id = None, yaml_block = None,templates_library = None, constants = None,self_initialize = False): assert yaml_block self.ad_api = ad_api self.logger_interface = logger_interface # sensor_name should be something like 'domain.name' whereas virtual_sensor_name should be only 'name' # only one should be provided and the other one will be deduced. # the regular use case is giving virtual_sensor_name. # Giving a specific sensor_name is usefull when the entity exist before with a different domain if virtual_sensor_name: assert sensor_name == None self.virtual_sensor_name = virtual_sensor_name self.sensor_name = self.generate_sensor_name(virtual_sensor_name) else: assert virtual_sensor_name == None self.sensor_name = sensor_name self.virtual_sensor_name = sensor_name.split('.')[1] self.sensor_domain = self.sensor_name.split(".")[0] self.yaml_block = yaml_block self.templates_library = templates_library try: self.attributes = self.yaml_block['attributes'] except (TypeError,KeyError): self.attributes = dict() if constants: self.constants = dict(constants) # we don't want to modify the parent dict else: self.constants = {'self' : self.sensor_name } if super_entity_id != None: self.constants['super'] = super_entity_id if self_initialize: self._initialize() def generate_sensor_name(self,virtual_sensor_name): return f"sensor.{virtual_sensor_name}" # TODO : restore that code once we rename the sensor # splitted_name = virtual_sensor_name.split(".") # assert len(splitted_name) == 2,f"Invalid virtual sensor name : {virtual_sensor_name}" # return f"sensor.{splitted_name[1]}" def log(self,message): self.logger_interface.log_info(f"[{self.sensor_name}]{message}") def log_info(self,message): self.logger_interface.log_info(f"[{self.sensor_name}]{message}") def log_warning(self,message): self.logger_interface.log_warning(f"[{self.sensor_name}]{message}") def log_error(self,message): self.logger_interface.log_error(f"[{self.sensor_name}]{message}") def generate_dependencies_list(self): return [] def generate_attributes_dependencies_list(self): dependencies = list() if 'sensor_attributes' in self.yaml_block: for key,entity in self.yaml_block['sensor_attributes'].items(): dependencies.append(entity) return dependencies def _initialize(self): if 'sensor_attributes' in self.yaml_block: for attribute_name,sensor_name in self.yaml_block['sensor_attributes'].items(): if not self.ad_api.entity_exists(sensor_name): self.log_error(f"Error in sensor_attribute. {attribute_name}:{sensor_name}. {sensor_name} doesn't exist") else: self.attributes[attribute_name] = self.ad_api.get_state(sensor_name) self.ad_api.listen_state(self.on_attribute_sensor_state_change,sensor_name) try: self.initialize() except ParsingException as e: self.log_error(e) self.ad_api.set_state(self.sensor_name, attributes = self.attributes) def on_attribute_sensor_state_change(self, entity, attribute, old, new, kwargs): attributes_update = False for attribute_name,sensor_name in self.yaml_block['sensor_attributes'].items(): if sensor_name == entity: self.attributes[attribute_name] = new attributes_update = True if attributes_update: self.ad_api.set_state(self.sensor_name, attributes = self.attributes) def initialize(self): assert False def extract_dependencies_from_entities_to_listen(self,entities_to_listen,dependencies): for entity_with_attribute in entities_to_listen: splitted_entity = entity_with_attribute.split('.') assert len(splitted_entity) == 3 or len(splitted_entity) == 2 if len(splitted_entity) == 3: entity = f"{splitted_entity[0]}.{splitted_entity[1]}" else: entity = entity_with_attribute if entity != self.sensor_name and not entity in dependencies: dependencies.append(entity) class Averager(VirtualSensorBase): def initialize(self): self.round = None self.confirm_steady_cb_handle = None self.confirm_tendency_cb_handle = None self.icons_config = None self.sensors_list = list() self.average_sensor_attributes = dict() self.all_sensors_are_mandatory = True if isinstance(self.yaml_block,dict): if 'all_sensors_are_mandatory' in self.yaml_block: self.all_sensors_are_mandatory = self.yaml_block['all_sensors_are_mandatory'] if 'round' in self.yaml_block: self.round = self.yaml_block['round'] if 'sensors' in self.yaml_block: self.sensors_list = self.yaml_block['sensors'] if isinstance(self.yaml_block['sensors'],list) else [self.yaml_block['sensors']] if 'icons' in self.yaml_block: self.icons_config = self.yaml_block['icons'] if 'attributes' in self.yaml_block: self.average_sensor_attributes = self.yaml_block['attributes'] else: self.average_sensor_attributes = { 'tendency' : 'steady' } else: self.sensors_list = [self.yaml_block] for sensor in self.sensors_list: self.ad_api.listen_state(self.on_sensor_state_change,sensor) self.average_sensor = self.ad_api.get_entity(self.sensor_name) if not self.average_sensor.exists(): average = self.compute_average() self.average_sensor.set_state(state = average if average else 'unavailable',attributes = self.average_sensor_attributes) self.update_average() def generate_dependencies_list(self): if isinstance(self.yaml_block,dict): if 'sensors' in self.yaml_block: return self.yaml_block['sensors'] if isinstance(self.yaml_block['sensors'],list) else [self.yaml_block['sensors']] else: return [] else: return [ self.yaml_block ] def on_sensor_state_change(self, *args): self.update_average() def compute_average(self): result_count = 0 average = 0 for sensor in self.sensors_list: try: value = float(self.ad_api.get_state(sensor)) result_count += 1 average += value except (TypeError,ValueError): if self.all_sensors_are_mandatory: return None if result_count == 0: return None average /= result_count if self.round != None: average = round(average,self.round) return average def update_average(self): try: prev_average = float(self.average_sensor.get_state()) except (TypeError,ValueError): prev_average = None new_average = self.compute_average() if new_average and new_average != prev_average: #if we don't change the value for one hour, let's go back to the default icon if self.confirm_steady_cb_handle != None: self.ad_api.cancel_timer(self.confirm_steady_cb_handle) self.confirm_steady_cb_handle = None self.confirm_steady_cb_handle = self.ad_api.run_in(self.on_confirm_steady, 60 * 60) if prev_average: if self.confirm_tendency_cb_handle: self.ad_api.cancel_timer(self.confirm_tendency_cb_handle) self.confirm_tendency_cb_handle = None prev_tendency = self.average_sensor.get_state(attribute = 'tendency') if new_average > prev_average: if prev_tendency == 'down': self.average_sensor_attributes['tendency'] = 'steady' self.confirm_tendency_cb_handle = self.ad_api.run_in(self.on_confirm_tendency, 30 * 60,tendency = 'up') else: self.average_sensor_attributes['tendency'] = 'up' else: if prev_tendency == 'up': self.average_sensor_attributes['tendency'] = 'steady' self.confirm_tendency_cb_handle = self.ad_api.run_in(self.on_confirm_tendency, 30 * 60,tendency = 'down') else: self.average_sensor_attributes['tendency'] = 'down' self.update_icon_attribute() self.average_sensor.set_state(state = new_average,attributes = self.average_sensor_attributes) def update_icon_attribute(self): if self.icons_config: if self.average_sensor_attributes['tendency'] == 'up' and 'up' in self.icons_config: self.average_sensor_attributes['icon'] = self.icons_config['up'] if self.average_sensor_attributes['tendency'] == 'down' and 'down' in self.icons_config: self.average_sensor_attributes['icon'] = self.icons_config['down'] if self.average_sensor_attributes['tendency'] == 'steady' and 'default' in self.icons_config: self.average_sensor_attributes['icon'] = self.icons_config['default'] def on_confirm_tendency(self, kwargs): tendency = kwargs['tendency'] self.confirm_tendency_cb_handle = None self.average_sensor_attributes['tendency'] = tendency self.update_icon_attribute() self.average_sensor.set_state(attributes = self.average_sensor_attributes) def on_confirm_steady(self, kwargs): self.confirm_steady_cb_handle = None self.average_sensor_attributes['tendency'] = 'steady' self.update_icon_attribute() self.average_sensor.set_state(attributes = self.average_sensor_attributes) class ValueSelector(VirtualSensorBase): class SelectorConfig: def __init__(self,name,conditions,value,is_smart_value): self.conditions = conditions self.value = str(value) #value like "17" are deserialized as int self.name = str(name) self.is_smart_value = is_smart_value class Selector: def __init__(self,config, ad_api = None,logger_interface = None, constants = None, templates_library = None): self.config = config self.conditions = SmartCondition.Evaluator(ad_api,self.config.conditions, logger_interface = logger_interface, unavaibility_result = SmartCondition.Result.Failed,condition_name = self.config.name, log_callback_trigger_reason = False, constants = constants, templates_library = templates_library) if config.is_smart_value: self.value = SmartValue.Evaluator(ad_api,self.config.value, logger_interface = logger_interface,expression_name = self.config.name, log_callback_trigger_reason = False, constants = constants) else: self.value = None def compute_value(self): if self.value: return self.value.evaluate(False) else: return self.config.value def get_entities_to_listen(self): if self.value: entities_to_listen = list(self.conditions.get_entities_to_listen_as_list()) entities_to_listen.extend(self.value.get_entities_to_listen_as_list()) return entities_to_listen else: return self.conditions.get_entities_to_listen_as_list() def initialize(self): self.value_selectors = list() selectors_config = self.process_config() self.output_sensor = self.ad_api.get_entity(self.sensor_name) if not self.output_sensor.exists(): default_config = selectors_config[-1] #the last config is the default config if default_config.is_smart_value: default_value = SmartValue.Evaluator(self.ad_api,default_config.value,expression_name = default_config.name, logger_interface = self.logger_interface, log_callback_trigger_reason = False, constants = self.constants).evaluate() else: default_value = default_config.value self.set_value(default_value) entities_to_listen = set() for config in selectors_config: new_selector = self.Selector(config,ad_api = self.ad_api, logger_interface = self.logger_interface, constants = self.constants, templates_library = self.templates_library) self.value_selectors.append(new_selector) entities_to_listen.update(new_selector.get_entities_to_listen()) for entity_to_listen in entities_to_listen: slitted_entity = entity_to_listen.split(".") if len(slitted_entity) == 3: self.ad_api.listen_state(self.on_entities_to_listen_changed,f"{slitted_entity[0]}.{slitted_entity[1]}",attribute = slitted_entity[2]) else: self.ad_api.listen_state(self.on_entities_to_listen_changed,entity_to_listen) self.update_value() def on_entities_to_listen_changed(self, entity, attribute, old, new, kwargs): self.update_value() def process_config(self) -> list[SelectorConfig]: configs = list() #self.log(f"yaml_block:{self.yaml_block}") if 'values' in self.yaml_block: values_block = self.yaml_block['values'] else: values_block = self.yaml_block for key,item in values_block.items(): try: conditions_block = item['conditions'] except (TypeError,KeyError): conditions_block = item try: value = item['value'] is_smart_value = True except (TypeError,KeyError): value = key is_smart_value = False #SmartValue are not supported for 'inline' value like "17: some_condition" configs.append(self.SelectorConfig(key,conditions_block,value,is_smart_value)) return configs def generate_dependencies_list(self): dependencies = list() selectors_config = self.process_config() for config in selectors_config: #self.log(f"config: name = {config.name},conditions = {config.conditions},value = {config.value}") conditions = SmartCondition.Evaluator(self.ad_api,config.conditions, logger_interface = self.logger_interface,condition_name = config.name, sensor_existence_validation = False, constants = self.constants, log_init = False) self.extract_dependencies_from_entities_to_listen(conditions.get_entities_to_listen_as_list(),dependencies) if config.is_smart_value: value = SmartValue.Evaluator(self.ad_api,config.value,expression_name = config.name, sensor_existence_validation = False, constants = self.constants, log_init = False) self.extract_dependencies_from_entities_to_listen(value.get_entities_to_listen_as_list(),dependencies) return dependencies def on_value_condition_change(self, *kwargs): self.update_value() def set_value(self, value): if self.sensor_domain == 'number': self.ad_api.call_service("number/set_value", entity_id = self.sensor_name,value = value) else: self.output_sensor.set_state(state = value) def update_value(self): previous_value = str(self.output_sensor.get_state()) for selector in self.value_selectors: if selector.conditions.evaluate(False) == SmartCondition.Result.Succeeded: new_value = selector.compute_value() if previous_value != str(new_value): #TODO: find a way to know wether if it's the condition or the value that trigger the update selector.conditions.log_callback_trigger_reason() selector.conditions.log_evaluation_result() if selector.value: selector.value.log_callback_trigger_reason() selector.value.log_evaluation_result() self.set_value(new_value) break class ContinuousCondition(VirtualSensorBase): def initialize(self): self.cb_handle = None self.time = self.yaml_block['time'] try: self.ignore_unavailable_condition = self.yaml_block['ignore_unavailable_condition'] except KeyError: self.ignore_unavailable_condition = False try: self.invert = self.yaml_block['invert'] except KeyError: self.invert = False self.log(f"Initializing continuous condition for {self.sensor_name}") self.output_sensor = self.ad_api.get_entity(self.sensor_name) if not self.output_sensor.exists(): self.log(f"{self.sensor_name} does not exist") try: self.output_sensor.set_state(state = self.yaml_block['default_value']) self.log(f"adding {self.sensor_name} with default state {self.yaml_block['default_value']}") except KeyError: self.output_sensor.set_state(state = 'off') self.log(f"adding {self.sensor_name} with default state off") else: self.log(f"{self.sensor_name} already exist with state = {self.output_sensor.get_state()}") if self.invert: succeed_cb = self.on_condition_fail fail_cb = self.on_condition_succeed else: succeed_cb = self.on_condition_succeed fail_cb = self.on_condition_fail self.condition = SmartCondition.Evaluator(self.ad_api,self.yaml_block['conditions'], unavaibility_result = SmartCondition.Result.Unavailable if self.ignore_unavailable_condition else None, logger_interface = self.logger_interface,condition_name = self.sensor_name,on_succeed_cb = succeed_cb, on_fail_cb = fail_cb, constants = self.constants, templates_library = self.templates_library) # new_value = 'on' if self.condition.evaluate() == SmartCondition.Result.Succeeded else 'off' # self.output_sensor.set_state(state = new_value) def generate_sensor_name(self,virtual_sensor_name): return f"binary_sensor.{virtual_sensor_name}" #TODO : restore that code once we rename the sensor splitted_name = virtual_sensor_name.split(".") assert len(splitted_name) == 2,f"Invalid virtual sensor name : {virtual_sensor_name}" return f"binary_sensor.{splitted_name[1]}" def generate_dependencies_list(self): dependencies = list() condition = SmartCondition.Evaluator(self.ad_api,self.yaml_block['conditions'], logger_interface = self.logger_interface, constants = self.constants, templates_library = self.templates_library, sensor_existence_validation = False, log_init = False) self.extract_dependencies_from_entities_to_listen(condition.get_entities_to_listen_as_list(),dependencies) return dependencies def register_callback(self): if self.cb_handle == None: self.cb_handle = self.ad_api.run_in(self.on_time_elapse, self.time) def cancel_callback(self): if self.cb_handle != None: self.ad_api.cancel_timer(self.cb_handle) self.cb_handle = None def on_time_elapse(self, kwargs): self.cb_handle = None value = 'off' if self.invert else 'on' if self.output_sensor.get_state() != value: self.output_sensor.set_state(state = value) def on_condition_succeed(self): self.register_callback() def on_condition_fail(self): self.cancel_callback() value = 'on' if self.invert else 'off' if self.output_sensor.get_state() != value: self.output_sensor.set_state(state = value) class RetainCondition(VirtualSensorBase): def initialize(self): self.cb_handle = None self.retain_time = self.yaml_block['retain_time'] try: self.reset_retain_time_on_new_success = self.yaml_block['reset_retain_time_on_new_success'] except KeyError: self.reset_retain_time_on_new_success = True self.log(f"Initializing retain condition for {self.sensor_name}") self.output_sensor = self.ad_api.get_entity(self.sensor_name) if not self.output_sensor.exists(): self.log(f"{self.sensor_name} does not exist") try: self.output_sensor.set_state(state = self.yaml_block['default_value']) self.log(f"adding {self.sensor_name} with default state {self.yaml_block['default_value']}") except KeyError: self.output_sensor.set_state(state = 'off') self.log(f"adding {self.sensor_name} with default state off") else: self.log(f"{self.sensor_name} already exist with state = {self.output_sensor.get_state()}") self.condition = SmartCondition.Evaluator(self.ad_api,self.yaml_block['conditions'], logger_interface = self.logger_interface,condition_name = self.sensor_name,on_succeed_cb = self.on_condition_succeed, on_fail_cb = self.on_condition_fail, constants = self.constants, templates_library = self.templates_library) # new_value = 'on' if self.condition.evaluate() == SmartCondition.Result.Succeeded else 'off' # self.output_sensor.set_state(state = new_value) def generate_sensor_name(self,virtual_sensor_name): return f"binary_sensor.{virtual_sensor_name}" def generate_dependencies_list(self): dependencies = list() condition = SmartCondition.Evaluator(self.ad_api,self.yaml_block['conditions'], logger_interface = self.logger_interface, constants = self.constants, templates_library = self.templates_library, sensor_existence_validation = False, log_init = False) self.extract_dependencies_from_entities_to_listen(condition.get_entities_to_listen_as_list(),dependencies) return dependencies def register_callback(self): if self.cb_handle == None: self.cb_handle = self.ad_api.run_in(self.on_time_elapse, self.retain_time) def cancel_callback(self): if self.cb_handle != None: self.ad_api.cancel_timer(self.cb_handle) self.cb_handle = None def on_time_elapse(self, kwargs): self.cb_handle = None if self.condition.evaluate() == SmartCondition.Result.Failed: if self.output_sensor.get_state() != 'off': self.output_sensor.set_state(state = 'off') def on_condition_succeed(self): if self.output_sensor.get_state() != 'on': self.output_sensor.set_state(state = 'on') if self.reset_retain_time_on_new_success: self.cancel_callback() self.register_callback() def on_condition_fail(self): # we don't want to set the sensor to off during the timer if self.cb_handle == None: if self.output_sensor.get_state() != 'off': self.output_sensor.set_state(state = 'off') class BinarySensor(VirtualSensorBase): def initialize(self): self.output_sensor = self.ad_api.get_entity(self.sensor_name) if not self.output_sensor.exists(): self.output_sensor.set_state(state = 'off',attributes = self.attributes) #self.ad_api.log(f"Creating Binary Sensor: binary_sensor = {self.virtual_sensor_name}, yaml_block = {self.yaml_block}") self.condition = SmartCondition.Evaluator(self.ad_api,self.get_expression_yaml(), logger_interface = self.logger_interface, unavaibility_result = SmartCondition.Result.Unavailable,condition_name = self.virtual_sensor_name,on_change_cb = self.on_sensor_change, constants = self.constants, templates_library = self.templates_library) new_value = 'on' if self.condition.evaluate() == SmartCondition.Result.Succeeded else 'off' self.output_sensor.set_state(state = new_value) def on_sensor_change(self,prev_result,result): if result == SmartCondition.Result.Unavailable: self.output_sensor.set_state(state = 'unavailable') elif result == SmartCondition.Result.Succeeded: self.output_sensor.set_state(state = 'on') elif result == SmartCondition.Result.Failed: self.output_sensor.set_state(state = 'off') # else: Disabled means we keep the previous value def generate_dependencies_list(self): dependencies = list() condition = SmartCondition.Evaluator(self.ad_api,self.get_expression_yaml(), logger_interface = self.logger_interface, condition_name = [self.virtual_sensor_name,"generate_dependencies_list"],constants = self.constants, templates_library = self.templates_library, sensor_existence_validation = False, log_init = False) self.extract_dependencies_from_entities_to_listen(condition.get_entities_to_listen_as_list(),dependencies) return dependencies def get_expression_yaml(self): try: return self.yaml_block['conditions'] except (TypeError,KeyError): return self.yaml_block def generate_sensor_name(self,virtual_sensor_name): return f"binary_sensor.{virtual_sensor_name}" #TODO : restore that code once we rename the sensor splitted_name = virtual_sensor_name.split(".") assert len(splitted_name) == 2,f"Invalid virtual sensor name : {virtual_sensor_name}" return f"binary_sensor.{splitted_name[1]}" class ValueSensor(VirtualSensorBase): def initialize(self): # should be done in the base class # try: self.sensor_attributes = self.yaml_block['attributes'] # except TypeError: self.sensor_attributes = {} self.output_sensor = self.ad_api.get_entity(self.sensor_name) if not self.output_sensor.exists(): self.output_sensor.set_state(state = 0, attributes = self.attributes) self.smart_value = SmartValue.Evaluator(self.ad_api,self.get_expression_yaml(), logger_interface = self.logger_interface, expression_name = self.virtual_sensor_name,on_change_cb = self.on_sensor_change, constants = self.constants) new_value = self.smart_value.evaluate() self.output_sensor.set_state(state = new_value) def on_sensor_change(self,prev_result,result): self.output_sensor.set_state(state = result, attributes = self.attributes) def get_expression_yaml(self): try: return self.yaml_block['value'] except (TypeError,KeyError): return self.yaml_block def generate_dependencies_list(self): dependencies = list() smart_value = SmartValue.Evaluator(self.ad_api,self.get_expression_yaml(), logger_interface = self.logger_interface, expression_name = self.virtual_sensor_name, constants = self.constants, sensor_existence_validation = False, log_init = False) self.extract_dependencies_from_entities_to_listen(smart_value.get_entities_to_listen_as_list(),dependencies) return dependencies def generate_sensor_name(self,virtual_sensor_name): return f"sensor.{virtual_sensor_name}" class VirtualSensors(): def __init__(self, ad_api = None,logger_interface = None, super_entity_id = None, yaml_block = None ,args_to_ignore_in_validation = [], constants = None, templates_library = None): assert ad_api assert yaml_block assert logger_interface self.ad_api = ad_api self.virtual_sensors = dict() self.logger_interface = logger_interface parser = kwargsParser(yaml_block,lambda string: logger_interface.log_error(string),lambda string: logger_interface.log_warning(string)) default_values = parser.parse_args('default_values',{}) for sensor_name,default_value in default_values.items(): if not self.ad_api.entity_exists(sensor_name): self.logger_interface.log_info(f"Creating sensor {sensor_name} = {default_value}") self.ad_api.set_state(sensor_name, state = str(default_value)) averagers = parser.parse_args('averagers',{}) for averager in averagers: self.virtual_sensors[f"sensor.{averager}"] = Averager(self.ad_api,self.logger_interface,averager,super_entity_id = super_entity_id,yaml_block = yaml_block['averagers'][averager],constants = constants, templates_library = templates_library) continuous_conditions = parser.parse_args('continuous_conditions',{}) for continuous_condition in continuous_conditions: self.virtual_sensors[f"binary_sensor.{continuous_condition}"] = ContinuousCondition(self.ad_api,self.logger_interface,continuous_condition,super_entity_id = super_entity_id,yaml_block = yaml_block['continuous_conditions'][continuous_condition],templates_library = templates_library) binary_sensors = parser.parse_args('binary_sensors',{}) for binary_sensor in binary_sensors: #self.logger_interface.log_info(f"Creating Binary Sensor: binary_sensor = {binary_sensor}, super_entity_id = {super_entity_id}, yaml_block = {yaml_block['binary_sensors']}") self.virtual_sensors[f"binary_sensor.{binary_sensor}"] = BinarySensor(self.ad_api,self.logger_interface,binary_sensor,super_entity_id = super_entity_id,yaml_block = yaml_block['binary_sensors'][binary_sensor],constants = constants,templates_library = templates_library) value_selectors = parser.parse_args('value_selectors',{}) for value_selector in value_selectors: self.virtual_sensors[f"sensor.{value_selector}"] = ValueSelector(self.ad_api,self.logger_interface,value_selector,super_entity_id = super_entity_id,yaml_block = yaml_block['value_selectors'][value_selector],constants = constants,templates_library = templates_library) sensors = parser.parse_args('sensors',{}) for sensor in sensors: splitted_sensor = sensor.split('.') if len(splitted_sensor) != 2: self.logger_interface.log_error(f"Invalid sensor name {splitted_sensor}") if splitted_sensor[0] == 'binary_sensor': self.virtual_sensors[f"binary_sensor.{splitted_sensor[1]}"] = BinarySensor(self.ad_api,self.logger_interface,splitted_sensor[1],super_entity_id = super_entity_id,yaml_block = yaml_block['sensors'][sensor],constants = constants,templates_library = templates_library) elif splitted_sensor[0] == 'sensor': self.virtual_sensors[f"sensor.{splitted_sensor[1]}"] = ValueSensor(self.ad_api,self.logger_interface,splitted_sensor[1],super_entity_id = super_entity_id,yaml_block = yaml_block['sensors'][sensor],constants = constants,templates_library = templates_library) elif splitted_sensor[0] == 'continuous_condition': self.virtual_sensors[f"binary_sensor.{splitted_sensor[1]}"] = ContinuousCondition(self.ad_api,self.logger_interface,splitted_sensor[1],super_entity_id = super_entity_id,yaml_block = yaml_block['sensors'][sensor],constants = constants,templates_library = templates_library) elif splitted_sensor[0] == 'averager': self.virtual_sensors[f"sensor.{splitted_sensor[1]}"] = Averager(self.ad_api,self.logger_interface,splitted_sensor[1],super_entity_id = super_entity_id,yaml_block = yaml_block['sensors'][sensor],constants = constants,templates_library = templates_library) elif splitted_sensor[0] == 'value_selector': self.virtual_sensors[f"sensor.{splitted_sensor[1]}"] = ValueSelector(self.ad_api,self.logger_interface,splitted_sensor[1],super_entity_id = super_entity_id,yaml_block = yaml_block['sensors'][sensor],constants = constants,templates_library = templates_library) elif splitted_sensor[0] == 'retain_condition': self.virtual_sensors[f"binary_sensor.{splitted_sensor[1]}"] = RetainCondition(self.ad_api,self.logger_interface,splitted_sensor[1],super_entity_id = super_entity_id,yaml_block = yaml_block['sensors'][sensor],constants = constants,templates_library = templates_library) else: self.logger_interface.log_error(f"Invalid sensor prefix {splitted_sensor[0]}") parser.validate_args(args_to_ignore_in_validation) dependencies_graph = dict() for sensor_name, virtual_sensor in self.virtual_sensors.items(): try: dependencies_graph[sensor_name] = virtual_sensor.generate_dependencies_list() # add the dependencies from the attributes dependencies_graph[sensor_name].extend(virtual_sensor.generate_attributes_dependencies_list()) #self.logger_interface.log_info(f"Dependencies = {dependencies_graph}") except ParsingException as e: self.logger_interface.log_error(f"[Dependencies][{sensor_name}] {e}") self.remove_obsolete_dependencies(dependencies_graph) while len(dependencies_graph) > 0: virtual_sensor_initialized = list() #self.logger_interface.log_info(f"Dependencies = {dependencies_graph}") #initialize sensor with no dependencies for sensor_name, dependencies_list in dependencies_graph.items(): if len(dependencies_list) == 0: virtual_sensor_initialized.append(sensor_name) self.virtual_sensors[sensor_name]._initialize() #removing initialized sensors from dependencies_graph for sensor_name in virtual_sensor_initialized: del dependencies_graph[sensor_name] #if didn't initialized any sensors, we are in a circular dependencies ignore_sensor = None if len(virtual_sensor_initialized) == 0: foundInDefaultValues = False for sensor_name in dependencies_graph: if sensor_name in default_values: self.logger_interface.log_info(f"Found circular dependencies in the following sensors : {dependencies_graph}. The default values of {sensor_name} will be use to break it.") ignore_sensor = sensor_name foundInDefaultValues = True break if not foundInDefaultValues: self.logger_interface.log_error(f"Found circular dependencies in the following sensors : {dependencies_graph}. You can give a default value with 'default_values' to one of them to break it.") break #remove all dependencies towards sensors that are already been initialized self.remove_obsolete_dependencies(dependencies_graph,ignore_sensor) def remove_obsolete_dependencies(self, dependencies_graph, ignore_sensor = None): for sensor_name, dependencies_list in dependencies_graph.items(): current_dependencies = list() for dependency in dependencies_list: if dependency in dependencies_graph and dependency != ignore_sensor: current_dependencies.append(dependency) dependencies_graph[sensor_name] = current_dependencies def on_sensor_change(self,sensor_name,prev_result,result): if result == SmartCondition.Result.Succeeded: self.ad_api.set_state(f'binary_sensor.{sensor_name}',state = 'on') else : self.ad_api.set_state(f'binary_sensor.{sensor_name}',state = 'off') class VirtualSensorsApp(hass.Hass): def initialize(self): self.logger_interface = LoggerInterface(self.get_ad_api(),default_log = "virtualsensors_log") self.virtual_sensors = VirtualSensors(ad_api = self.get_ad_api(),logger_interface = self.logger_interface,yaml_block = self.args,args_to_ignore_in_validation = ['module','class','global_dependencies','priority'])