A new epheleral_condition sensor type + fix issue with dangling callback

This commit is contained in:
Pierre committed 2026-10-16 08:49:57 +02:00
1 parent 3b0c24e795
commit b97445ce92
4 files changed
+369 -13

No files matched your search

+85 -8
View File
@@ -406,6 +406,7 @@ class ValueSelector(VirtualSensorBase):
class ContinuousCondition(VirtualSensorBase):
def initialize(self):
self.cb_handle = None
self._timer_generation = 0
self.time = self.yaml_block['time']
try: self.ignore_unavailable_condition = self.yaml_block['ignore_unavailable_condition']
except KeyError: self.ignore_unavailable_condition = False
@@ -452,14 +453,22 @@ class ContinuousCondition(VirtualSensorBase):
def register_callback(self):
if self.cb_handle == None:
self.cb_handle = self.ad_api.run_in(self.on_time_elapse, self.time)
self._timer_generation += 1
self.cb_handle = self.ad_api.run_in(
self.on_time_elapse, self.time, generation=self._timer_generation)
def cancel_callback(self):
if self.cb_handle != None:
self.ad_api.cancel_timer(self.cb_handle)
self.cb_handle = None
# An expired timer may already have a callback queued in the worker.
# Invalidate it before cancellation, even if it is no longer scheduled.
self._timer_generation += 1
handle = self.cb_handle
self.cb_handle = None
if handle is not None:
self.ad_api.cancel_timer(handle, silent=True)
def on_time_elapse(self, kwargs):
if kwargs.get('generation') != self._timer_generation:
return
self.cb_handle = None
value = 'off' if self.invert else 'on'
if self.output_sensor.get_state() != value:
@@ -474,9 +483,68 @@ class ContinuousCondition(VirtualSensorBase):
if self.output_sensor.get_state() != value:
self.output_sensor.set_state(state = value)
class EphemeralCondition(ContinuousCondition):
"""True during the first `time` seconds of each true condition.
condition_started_at is a Unix timestamp retained after expiry, until
the condition becomes false. Reloads resume this window when the entity
attributes survive; without a timestamp, a true condition starts anew.
"""
def initialize(self):
self._condition_active = False
if self.yaml_block.get('invert', False):
raise ValueError("ephemeral_condition does not support invert; negate conditions instead")
started_at = self.ad_api.get_state(
self.sensor_name, attribute='condition_started_at')
try:
started_at = float(started_at)
if not 0 <= started_at <= time.time():
started_at = None
except (TypeError, ValueError):
started_at = None
self._started_at = started_at
self.attributes['condition_started_at'] = started_at
# Unknown inputs during startup must not erase a restored window.
self.yaml_block = dict(self.yaml_block)
self.yaml_block.setdefault('ignore_unavailable_condition', True)
super().initialize()
def on_condition_succeed(self):
if self._condition_active:
return
self._condition_active = True
if self._started_at is None:
self._started_at = time.time()
self.attributes['condition_started_at'] = self._started_at
remaining = self._started_at + self.time - time.time()
self.output_sensor.set_state(
state='on' if remaining > 0 else 'off',
attributes={'condition_started_at': self._started_at})
if remaining > 0:
self._timer_generation += 1
self.cb_handle = self.ad_api.run_in(
self.on_time_elapse, remaining, generation=self._timer_generation)
def on_condition_fail(self):
self._condition_active = False
self._started_at = None
self.attributes['condition_started_at'] = None
self.cancel_callback()
self.output_sensor.set_state(
state='off', attributes={'condition_started_at': None})
def on_time_elapse(self, kwargs):
if kwargs.get('generation') != self._timer_generation:
return
self.cb_handle = None
self.output_sensor.set_state(state='off')
class RetainCondition(VirtualSensorBase):
def initialize(self):
self.cb_handle = None
self._timer_generation = 0
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
@@ -510,14 +578,21 @@ class RetainCondition(VirtualSensorBase):
def register_callback(self):
if self.cb_handle == None:
self.cb_handle = self.ad_api.run_in(self.on_time_elapse, self.retain_time)
self._timer_generation += 1
self.cb_handle = self.ad_api.run_in(
self.on_time_elapse, self.retain_time, generation=self._timer_generation)
def cancel_callback(self):
if self.cb_handle != None:
self.ad_api.cancel_timer(self.cb_handle)
self.cb_handle = None
# Invalidate callbacks already queued by an expired timer as well.
self._timer_generation += 1
handle = self.cb_handle
self.cb_handle = None
if handle is not None:
self.ad_api.cancel_timer(handle, silent=True)
def on_time_elapse(self, kwargs):
if kwargs.get('generation') != self._timer_generation:
return
self.cb_handle = None
if self.condition.evaluate() == SmartCondition.Result.Failed:
if self.output_sensor.get_state() != 'off':
@@ -697,6 +772,8 @@ class VirtualSensors():
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] == 'ephemeral_condition':
self.virtual_sensors[f"binary_sensor.{splitted_sensor[1]}"] = EphemeralCondition(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)
elif splitted_sensor[0] == 'event_sensor':