Files
ad_toolbox/virtualsensors.py

676 lines
36 KiB
Python

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, app_name = None,self_initialize = False):
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]
assert yaml_block ,f"{self.sensor_name} have a null Yaml block"
self.yaml_block = yaml_block
self.templates_library = templates_library
try: self.attributes = self.yaml_block['attributes']
except (TypeError,KeyError): self.attributes = dict()
if app_name:
self.attributes['ad_app'] = app_name
if constants:
self.constants = dict(constants) # we don't want to modify the parent dict
self.constants['self'] = self.sensor_name # we need to set self by ourself
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, app_name = 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,app_name = app_name)
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,app_name = app_name)
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,app_name = app_name)
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,app_name = app_name)
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,app_name = app_name)
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,app_name = app_name)
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,app_name = app_name)
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,app_name = app_name)
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,app_name = app_name)
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,app_name = app_name)
else:
self.logger_interface.log_error(f"Invalid sensor prefix {splitted_sensor[0]}")
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','name','config_path'],app_name = self.name)