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.
This commit is contained in:
Pierre committed 2026-11-03 19:14:20 +01:00
1 parent 1a148d4e3f
commit 54c525c843
4 files changed
+228 -9

No files matched your search

+16 -6
View File
@@ -46,6 +46,7 @@ class Evaluator():
self.__state_callbacks_handle = list()
self.__condition_state_change_delayed = list()
self.__condition_state_change_delayed_cb_handle = None
self.__delay_generation = 0
self.__appdaemon_api = appdaemon_api
self.__activated = False
@@ -573,6 +574,10 @@ class Evaluator():
self.__last_evaluation_result = result
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
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")
@@ -607,13 +612,18 @@ class Evaluator():
# self.__update_result()
self.__trigger_callbacks(f"Callback trigger by {entity}({old} => {new})")
else:
if self.__condition_state_change_delayed_cb_handle != None and self.__appdaemon_api.timer_running(self.__condition_state_change_delayed_cb_handle):
self.__appdaemon_api.cancel_timer(self.__condition_state_change_delayed_cb_handle)
self.__delay_generation += 1
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.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.__on_condition_state_change_delayed, self.__callback_delay)
self.__condition_state_change_delayed.append((entity,old,new,datetime.now()))
self.__condition_state_change_delayed_cb_handle = self.__appdaemon_api.run_in(
self.__on_condition_state_change_delayed, self.__callback_delay,
generation=self.__delay_generation,
)
def __log(self,message,**kwargs):
log_string = self.__log_to_string(message)