Compare commits

..
4 Commits
Author SHA1 Message Date
Pierre 86b5441603 removed old vsix 2026-11-10 19:08:21 +01:00
Pierre d5e7193b42 smartobject and virtualevent now correctly use the logger interface for their logs 2026-11-08 11:07:27 +01:00
Pierre 54c525c843 fix: ignore stale callbacks from rescheduled timers
- Add generation counters to SmartCondition and EventSensor timers.
- Preserve pending changes and the retention period when timers are restarted.
- Silently cancel timers that have already expired.
- Add regression tests for stale callbacks and expiry during cancellation.
2026-11-03 19:14:20 +01:00
Pierre 1a148d4e3f New time period virtual sensor 2026-10-25 20:39:52 +01:00
8 changed files with 421 additions and 14 deletions

No files matched your search

+16 -6
View File
@@ -46,6 +46,7 @@ class Evaluator():
self.__state_callbacks_handle = list() self.__state_callbacks_handle = list()
self.__condition_state_change_delayed = list() self.__condition_state_change_delayed = list()
self.__condition_state_change_delayed_cb_handle = None self.__condition_state_change_delayed_cb_handle = None
self.__delay_generation = 0
self.__appdaemon_api = appdaemon_api self.__appdaemon_api = appdaemon_api
self.__activated = False self.__activated = False
@@ -573,6 +574,10 @@ class Evaluator():
self.__last_evaluation_result = result self.__last_evaluation_result = result
def __on_condition_state_change_delayed(self, kwargs): def __on_condition_state_change_delayed(self, kwargs):
# An expired timer may already be queued when a newer change reschedules it.
# Ignore it before touching the current timer handle or pending changes.
if kwargs.get("generation") != self.__delay_generation:
return
self.__condition_state_change_delayed_cb_handle = None self.__condition_state_change_delayed_cb_handle = None
if len(self.__condition_state_change_delayed) == 0: if len(self.__condition_state_change_delayed) == 0:
self.__log(f"Callback has been trigger but self.__condition_state_change_delayed is empty, this shouldn't happen, the callback won't be called", level = "WARNING") self.__log(f"Callback has been trigger but self.__condition_state_change_delayed is empty, this shouldn't happen, the callback won't be called", level = "WARNING")
@@ -607,13 +612,18 @@ class Evaluator():
# self.__update_result() # self.__update_result()
self.__trigger_callbacks(f"Callback trigger by {entity}({old} => {new})") self.__trigger_callbacks(f"Callback trigger by {entity}({old} => {new})")
else: else:
if self.__condition_state_change_delayed_cb_handle != None and self.__appdaemon_api.timer_running(self.__condition_state_change_delayed_cb_handle): self.__delay_generation += 1
self.__appdaemon_api.cancel_timer(self.__condition_state_change_delayed_cb_handle) handle = self.__condition_state_change_delayed_cb_handle
if handle is not None:
# Expiry may already have removed the timer from the scheduler.
self.__appdaemon_api.cancel_timer(handle, silent=True)
self.__condition_state_change_delayed_cb_handle = None self.__condition_state_change_delayed_cb_handle = None
self.__condition_state_change_delayed.append((entity,old,new,datetime.now())) self.__condition_state_change_delayed.append((entity,old,new,datetime.now()))
#self.__log("State changed :" + str(entity) + "(" + str(old) + " => " + str(new) + ") but callback delayed for " + str(self.__callback_delay) + "s") self.__condition_state_change_delayed_cb_handle = self.__appdaemon_api.run_in(
self.__condition_state_change_delayed_cb_handle = self.__appdaemon_api.run_in(self.__on_condition_state_change_delayed, self.__callback_delay) self.__on_condition_state_change_delayed, self.__callback_delay,
generation=self.__delay_generation,
)
def __log(self,message,**kwargs): def __log(self,message,**kwargs):
log_string = self.__log_to_string(message) log_string = self.__log_to_string(message)
+3 -3
View File
@@ -184,15 +184,15 @@ class SmartObject(hass.Hass,LoggerInterface):
def initialize(self): def initialize(self):
self.initialize_logger_interface(self.get_ad_api()) self.initialize_logger_interface(self.get_ad_api())
with self.log_group('App Initialization') as group: with self.log_group('App Initialization') as group:
self.log(f"Initialization started [group={group['kind']}:{group['id']}]") self._write_log(f"Initialization started [group={group['kind']}:{group['id']}]", 'INFO', None)
try: try:
succeeded = self._initialize_smart_object() succeeded = self._initialize_smart_object()
except BaseException: except BaseException:
self.log(f"Initialization failed [group={group['kind']}:{group['id']}]", level='ERROR') self._write_log(f"Initialization failed [group={group['kind']}:{group['id']}]", 'ERROR', None)
raise raise
status = 'completed' if succeeded else 'failed' status = 'completed' if succeeded else 'failed'
level = 'INFO' if succeeded else 'ERROR' level = 'INFO' if succeeded else 'ERROR'
self.log(f"Initialization {status} [group={group['kind']}:{group['id']}]", level=level) self._write_log(f"Initialization {status} [group={group['kind']}:{group['id']}]", level, None)
def _initialize_smart_object(self): def _initialize_smart_object(self):
try: try:
+79
View File
@@ -0,0 +1,79 @@
"""An expired timer queued before reactivation must not end the new window."""
import ast
from pathlib import Path
from types import SimpleNamespace
import unittest
from unittest.mock import Mock
from test_continuous_condition import FakeScheduler
source = Path(__file__).resolve().parents[1] / 'virtualsensors.py'
node = next(node for node in ast.parse(source.read_text()).body
if isinstance(node, ast.ClassDef) and node.name == 'EventSensor')
namespace = {'VirtualSensorBase': object, 'EventHandler': Mock()}
exec(compile(ast.Module(body=[node], type_ignores=[]), str(source), 'exec'), namespace)
EventSensor = namespace['EventSensor']
class EventSensorTests(unittest.TestCase):
def setUp(self):
self.scheduler = FakeScheduler()
self.state = 'off'
entity = Mock()
entity.exists.return_value = True
entity.set_state.side_effect = lambda *, state: setattr(self, 'state', state)
self.sensor = EventSensor()
self.sensor.ad_api = SimpleNamespace(
get_entity=lambda name: entity,
run_in=self.scheduler.run_in,
cancel_timer=self.scheduler.cancel_timer,
)
self.sensor.sensor_name = 'binary_sensor.red_shelly_home'
self.sensor.yaml_block = {'retain_time': 120}
self.sensor.initialize()
def activate(self):
self.sensor.on_activation_event('signal_strength', {}, {})
return self.sensor.cb_handle
def test_current_expiry_turns_sensor_off(self):
handle = self.activate()
self.assertEqual(self.state, 'on')
self.scheduler.expire(handle)()
self.assertEqual(self.state, 'off')
self.assertIsNone(self.sensor.cb_handle)
def test_reactivation_cancels_running_timer(self):
old = self.activate()
current = self.activate()
self.assertNotIn(old, self.scheduler.timers)
self.assertIn(current, self.scheduler.timers)
self.assertEqual(self.state, 'on')
self.assertEqual(self.scheduler.warnings, [])
def test_queued_expiry_preserves_reactivated_sensor_and_timer(self):
queued = self.scheduler.expire(self.activate())
current = self.activate()
queued()
self.assertEqual(self.state, 'on')
self.assertEqual(self.sensor.cb_handle, current)
self.assertEqual(self.scheduler.warnings, [])
self.scheduler.expire(current)()
self.assertEqual(self.state, 'off')
self.assertIsNone(self.sensor.cb_handle)
def test_multiple_queued_expiries_cannot_end_latest_window(self):
first = self.scheduler.expire(self.activate())
second = self.scheduler.expire(self.activate())
current = self.activate()
second()
first()
self.assertEqual(self.state, 'on')
self.assertEqual(self.sensor.cb_handle, current)
self.assertEqual(self.scheduler.warnings, [])
self.scheduler.expire(current)()
self.assertEqual(self.state, 'off')
if __name__ == '__main__':
unittest.main()
+122
View File
@@ -0,0 +1,122 @@
"""Verify debouncing when an expired AppDaemon timer is still queued."""
import sys
import unittest
from pathlib import Path
from unittest.mock import Mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from smartcondition import Evaluator
class FakeScheduler:
def __init__(self):
self.timers = {}
self.next_id = 0
self.cancelled = []
self.warnings = []
self.expire_on_cancel = False
self.queued = []
def run_in(self, callback, delay, **kwargs):
self.next_id += 1
handle = str(self.next_id)
self.timers[handle] = (callback, kwargs)
return handle
def timer_running(self, handle):
return handle in self.timers
def cancel_timer(self, handle, silent=False):
self.cancelled.append(handle)
if self.expire_on_cancel and handle in self.timers:
self.queued.append(self.expire(handle))
cancelled = self.timers.pop(handle, None) is not None
if not cancelled and not silent:
self.warnings.append(handle)
return cancelled
def expire(self, handle):
callback, kwargs = self.timers.pop(handle)
return lambda: callback(kwargs)
class DelayedCallbackTests(unittest.TestCase):
def setUp(self):
self.scheduler = FakeScheduler()
self.evaluator = Evaluator.__new__(Evaluator)
fields = dict(
appdaemon_api=self.scheduler, callback_delay=10,
trigger_callback_on_entity_creation=True,
condition_state_change_delayed=[],
condition_state_change_delayed_cb_handle=None,
delay_generation=0, trigger_callbacks=Mock(), log=Mock(),
)
for name, value in fields.items():
setattr(self.evaluator, '_Evaluator__' + name, value)
def change(self, old, new):
self.evaluator._Evaluator__on_condition_state_change(
'sensor.temperature', 'state', old, new, {})
return self.evaluator._Evaluator__condition_state_change_delayed_cb_handle
def test_queued_obsolete_callback_preserves_new_timer_and_changes(self):
old_handle = self.change('20', '21')
queued_callback = self.scheduler.expire(old_handle)
new_handle = self.change('21', '22')
queued_callback()
self.assertEqual(self.scheduler.warnings, [])
self.assertEqual(self.evaluator._Evaluator__condition_state_change_delayed_cb_handle,
new_handle)
self.assertEqual(len(self.evaluator._Evaluator__condition_state_change_delayed), 2)
self.evaluator._Evaluator__trigger_callbacks.assert_not_called()
self.evaluator._Evaluator__log.assert_not_called()
self.scheduler.expire(new_handle)()
self.evaluator._Evaluator__trigger_callbacks.assert_called_once()
reason = self.evaluator._Evaluator__trigger_callbacks.call_args.args[0]
self.assertIn('20 => 21', reason)
self.assertIn('21 => 22', reason)
self.assertEqual(self.evaluator._Evaluator__condition_state_change_delayed, [])
self.assertIsNone(self.evaluator._Evaluator__condition_state_change_delayed_cb_handle)
self.evaluator._Evaluator__log.assert_not_called()
def test_obsolete_callback_after_current_callback_is_ignored(self):
queued_callback = self.scheduler.expire(self.change('20', '21'))
self.scheduler.expire(self.change('21', '22'))()
queued_callback()
self.evaluator._Evaluator__trigger_callbacks.assert_called_once()
self.evaluator._Evaluator__log.assert_not_called()
def test_running_timer_is_cancelled_when_rescheduled(self):
old_handle = self.change('20', '21')
new_handle = self.change('21', '22')
self.assertEqual(self.scheduler.cancelled, [old_handle])
self.assertFalse(self.scheduler.timer_running(old_handle))
self.scheduler.expire(new_handle)()
self.evaluator._Evaluator__trigger_callbacks.assert_called_once()
def test_expiry_during_cancellation_is_silent_and_preserves_new_batch(self):
self.change('20', '21')
self.scheduler.expire_on_cancel = True
current = self.change('21', '22')
self.assertEqual(self.scheduler.warnings, [])
self.assertEqual(len(self.scheduler.queued), 1)
self.scheduler.queued[0]()
self.evaluator._Evaluator__trigger_callbacks.assert_not_called()
self.assertEqual(self.evaluator._Evaluator__condition_state_change_delayed_cb_handle,
current)
self.assertEqual(len(self.evaluator._Evaluator__condition_state_change_delayed), 2)
self.scheduler.expire(current)()
self.evaluator._Evaluator__trigger_callbacks.assert_called_once()
self.evaluator._Evaluator__log.assert_not_called()
def test_zero_delay_remains_immediate(self):
self.evaluator._Evaluator__callback_delay = 0
self.change('20', '21')
self.evaluator._Evaluator__trigger_callbacks.assert_called_once()
self.assertEqual(self.scheduler.timers, {})
if __name__ == '__main__':
unittest.main()
+101
View File
@@ -0,0 +1,101 @@
"""Time-period boundaries, source updates and timer lifecycle without AppDaemon."""
import ast
import datetime
import re
import unittest
from pathlib import Path
from unittest.mock import Mock
source = Path(__file__).resolve().parents[1] / 'virtualsensors.py'
nodes = [node for node in ast.parse(source.read_text()).body
if isinstance(node, ast.ClassDef) and node.name in ('VirtualSensorBase', 'TimePeriod', 'VirtualSensors')]
from kwargsparser import kwargsParser
namespace = {'datetime': datetime, 're': re, 'kwargsParser': kwargsParser, 'ParsingException': ValueError}
exec(compile(ast.Module(body=nodes, type_ignores=[]), str(source), 'exec'), namespace)
TimePeriod = namespace['TimePeriod']
class TimePeriodTests(unittest.TestCase):
def make_sensor(self, start='08:00:00', end='18:00:00', now='2026-10-25 12:00:00'):
self.states = {'input_datetime.start': start, 'input_datetime.end': end}
self.api = Mock()
self.api.datetime.return_value = datetime.datetime.fromisoformat(now)
self.api.get_state.side_effect = self.states.get
self.api.entity_exists.return_value = True
self.api.get_entity.return_value.get_state.return_value = 'unknown'
self.sensor = TimePeriod(self.api, Mock(), virtual_sensor_name='period',
yaml_block={'start': 'input_datetime.start', 'end': 'input_datetime.end'})
self.sensor._initialize()
return self.sensor
def assert_state(self, expected):
self.assertEqual(self.sensor.entity.set_state.call_args.kwargs['state'], expected)
def test_factory_registration(self):
self.make_sensor()
factory = namespace['VirtualSensors'](ad_api=self.api, logger_interface=Mock(),
yaml_block={'sensors': {'time_period.example': {
'start': 'input_datetime.start', 'end': 'input_datetime.end'}}})
self.assertIsInstance(factory.virtual_sensors['binary_sensor.example'], TimePeriod)
def test_day_boundaries_and_timer(self):
self.make_sensor(now='2026-10-25 07:59:50')
self.assert_state('off')
self.assertEqual(self.api.run_in.call_args.args[1], 10)
for hour, expected in [('08:00:00', 'on'), ('17:59:59', 'on'), ('18:00:00', 'off')]:
self.api.datetime.return_value = datetime.datetime.fromisoformat('2026-10-25 ' + hour)
self.sensor.on_time_elapsed({'generation': self.sensor._timer_generation})
self.assert_state(expected)
def test_overnight_and_equal_boundaries(self):
for hour, expected in [('23:00:00', 'on'), ('02:00:00', 'on'), ('06:00:00', 'off'), ('12:00:00', 'off')]:
self.make_sensor('22:00:00', '06:00:00', '2026-10-25 ' + hour)
self.assert_state(expected)
self.make_sensor('08:00:00', '08:00:00')
self.assert_state('off')
def test_source_change_cancels_timer_and_ignores_stale_callback(self):
self.make_sensor()
self.assert_state('on')
previous_generation = self.sensor._timer_generation
previous_handle = self.sensor.cb_handle
self.states['input_datetime.end'] = '10:00:00'
self.sensor.on_input_change('input_datetime.end', 'state', '18:00:00', '10:00:00', {})
self.assert_state('off')
self.api.cancel_timer.assert_called_with(previous_handle, silent=True)
count = self.api.run_in.call_count
self.sensor.on_time_elapsed({'generation': previous_generation})
self.assertEqual(self.api.run_in.call_count, count)
self.assertEqual(self.api.listen_state.call_count, 2)
def test_unavailable_and_recovery(self):
for invalid in [None, 'unknown', 'unavailable', '25:00:00', '2026-10-25']:
self.make_sensor(start=invalid)
self.assert_state('unavailable')
self.states['input_datetime.start'] = '08:00:00'
self.sensor.update_period()
self.assert_state('on')
def test_dated_periods(self):
for start, end, expected in [
('2026-10-24 22:00:00', '2026-10-26 06:00:00', 'on'),
('2026-10-24 22:00:00', '2026-10-25 12:00:00', 'off'),
('2026-10-26 22:00:00', '2026-10-24 06:00:00', 'off'),
('2026-10-24', '2026-10-26', 'on'),
]:
self.make_sensor(start, end)
self.assert_state(expected)
def test_dependencies_constants_and_validation(self):
self.make_sensor()
self.assertEqual(self.sensor.sensor_name, 'binary_sensor.period')
self.sensor.constants['begin'] = 'input_datetime.start'
self.sensor.yaml_block['start'] = 'begin'
self.assertEqual(self.sensor.generate_dependencies_list(), list(self.states))
self.sensor.yaml_block['end'] = 'sensor.invalid'
with self.assertRaises(ValueError):
self.sensor.generate_dependencies_list()
if __name__ == '__main__':
unittest.main()
+2 -2
View File
@@ -22,9 +22,9 @@ class VirtualEvents(hass.Hass,LoggerInterface):
def on_condition_succeed(self,event_name): def on_condition_succeed(self,event_name):
event_yaml = self.args["virtual_events"][event_name] event_yaml = self.args["virtual_events"][event_name]
if 'event_data' in event_yaml: if 'event_data' in event_yaml:
self.log(f"Sending event {event_yaml['event_name']} data = {event_yaml['event_data']}") self.log_info(f"Sending event {event_yaml['event_name']} data = {event_yaml['event_data']}")
self.fire_event(event_yaml['event_name'],**event_yaml['event_data']) self.fire_event(event_yaml['event_name'],**event_yaml['event_data'])
else: else:
self.log(f"Sending event {event_yaml['event_name']}") self.log_info(f"Sending event {event_yaml['event_name']}")
self.fire_event(event_yaml['event_name']) self.fire_event(event_yaml['event_name'])
+98 -3
View File
@@ -4,6 +4,7 @@ import smartvalue as SmartValue
from kwargsparser import kwargsParser from kwargsparser import kwargsParser
from expressionparser import ParsingException from expressionparser import ParsingException
import time import time
import datetime
import re import re
from logger_interface import LoggerInterface from logger_interface import LoggerInterface
from ad_toolbox.eventhandler import EventHandler from ad_toolbox.eventhandler import EventHandler
@@ -618,6 +619,90 @@ class RetainCondition(VirtualSensorBase):
if self.output_sensor.get_state() != 'off': if self.output_sensor.get_state() != 'off':
self.output_sensor.set_state(state = 'off') self.output_sensor.set_state(state = 'off')
class TimePeriod(VirtualSensorBase):
"""Active in [start, end), using two input_datetime entities.
Time-only inputs repeat daily and may cross midnight. Dated inputs
describe a single interval. Equal boundaries describe an empty interval.
"""
def generate_sensor_name(self, virtual_sensor_name):
return f"binary_sensor.{virtual_sensor_name}"
def generate_dependencies_list(self):
inputs = []
for key in ('start', 'end'):
value = self.yaml_block.get(key)
value = self.constants.get(value, value) if isinstance(value, str) else value
if not self.is_entity_reference(value) or not value.startswith('input_datetime.'):
raise ValueError(f"{self.sensor_name}: {key} must reference an input_datetime")
inputs.append(value)
return inputs
def initialize(self):
self.start_entity, self.end_entity = self.generate_dependencies_list()
self.cb_handle = None
self._timer_generation = 0
for entity in set((self.start_entity, self.end_entity)):
self.ad_api.listen_state(self.on_input_change, entity)
self.update_period()
@staticmethod
def parse_boundary(value):
for fmt, kind in (('%H:%M:%S', 'time'),
('%Y-%m-%d %H:%M:%S', 'datetime'),
('%Y-%m-%d', 'date')):
try:
parsed = datetime.datetime.strptime(value, fmt)
return kind, parsed.time() if kind == 'time' else parsed
except (TypeError, ValueError):
continue
raise ValueError('Unavailable or invalid input_datetime state')
def on_input_change(self, entity, attribute, old, new, kwargs):
self.update_period()
def on_time_elapsed(self, kwargs):
if kwargs.get('generation') != self._timer_generation:
return
self.cb_handle = None
self.update_period()
def update_period(self):
self._timer_generation += 1
if self.cb_handle is not None:
self.ad_api.cancel_timer(self.cb_handle, silent=True)
self.cb_handle = None
now = self.ad_api.datetime()
delay = 60.0
try:
start_kind, start = self.parse_boundary(self.ad_api.get_state(self.start_entity))
end_kind, end = self.parse_boundary(self.ad_api.get_state(self.end_entity))
if start_kind != end_kind:
raise ValueError('Both inputs must have the same date/time format')
if start_kind == 'time':
current = now.time()
active = (start <= current < end if start <= end
else current >= start or current < end)
boundaries = [datetime.datetime.combine(now.date(), value)
for value in (start, end)]
boundaries = [value if value > now else value + datetime.timedelta(days=1)
for value in boundaries]
else:
active = start <= now < end
boundaries = [value for value in (start, end) if value > now]
state = 'on' if active else 'off'
if boundaries:
delay = min(delay, min((value - now).total_seconds() for value in boundaries))
except ValueError:
state = 'unavailable'
if self.entity.get_state() != state:
self.entity.set_state(state=state)
# Refresh at boundaries, and at least every minute to follow clock/DST changes.
self.cb_handle = self.ad_api.run_in(
self.on_time_elapsed, delay, generation=self._timer_generation)
class BinarySensor(VirtualSensorBase): class BinarySensor(VirtualSensorBase):
def initialize(self): def initialize(self):
self.output_sensor = self.ad_api.get_entity(self.sensor_name) self.output_sensor = self.ad_api.get_entity(self.sensor_name)
@@ -672,20 +757,28 @@ class EventSensor(VirtualSensorBase):
self.retain_time = self.yaml_block['retain_time'] self.retain_time = self.yaml_block['retain_time']
self.cb_handle = None self.cb_handle = None
self._timer_generation = 0
self.event_handlers = [] self.event_handlers = []
if "activation_events" in self.yaml_block: if "activation_events" in self.yaml_block:
self.event_handlers.append(EventHandler(self.ad_api, self.yaml_block["activation_events"], self.on_activation_event,logger_interface = self.logger_interface)) self.event_handlers.append(EventHandler(self.ad_api, self.yaml_block["activation_events"], self.on_activation_event,logger_interface = self.logger_interface))
def on_activation_event(self, event_name, data, kwargs): def on_activation_event(self, event_name, data, kwargs):
self._timer_generation += 1
self.output_sensor.set_state(state = 'on') self.output_sensor.set_state(state = 'on')
if self.cb_handle is not None: if self.cb_handle is not None:
self.ad_api.cancel_timer(self.cb_handle) self.ad_api.cancel_timer(self.cb_handle, silent=True)
self.cb_handle = self.ad_api.run_in(self.on_retain_time_elapsed, self.retain_time) self.cb_handle = self.ad_api.run_in(
self.on_retain_time_elapsed, self.retain_time,
generation=self._timer_generation,
)
def on_retain_time_elapsed(self, kwargs): def on_retain_time_elapsed(self, kwargs):
self.output_sensor.set_state(state = 'off') # Cancellation cannot retract a callback already queued by AppDaemon.
if kwargs.get('generation') != self._timer_generation:
return
self.cb_handle = None self.cb_handle = None
self.output_sensor.set_state(state = 'off')
def generate_sensor_name(self,virtual_sensor_name): def generate_sensor_name(self,virtual_sensor_name):
return f"binary_sensor.{virtual_sensor_name}" return f"binary_sensor.{virtual_sensor_name}"
@@ -764,6 +857,8 @@ class VirtualSensors():
if splitted_sensor[0] == 'binary_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) 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] == 'time_period':
self.virtual_sensors[f"binary_sensor.{splitted_sensor[1]}"] = TimePeriod(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': 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) 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': elif splitted_sensor[0] == 'continuous_condition':
Binary file not shown.