`run_at` running immediately if `start` in past
What happened?
According to the documentation
If the time specified is in the past, the callback will occur the next day at the specified time.
A run with a time that lies in the past should be scheduled for the next day, but that doesn't seem to be the case.
Issue
It is 13:46 now.
When I schedule this run (one minute into the future) everything works as expected:
self.run_at(self.run_actions_cb, "13:47:00")
But when I schedule it one minute "too late"/one minute in the past it fires immediately
self.run_at(self.run_actions_cb, "13:45:00")
Am I misunderstanding the doc or should the second run be scheduled for tomorrow's 13:45?
Version
4.5.12
Installation type
Docker container
Relevant log output
Relevant code in the app or config file that caused the issue
Anything else?
No response
Slight correction, the same happens when using datetime.time
def initialize(self):
run_at_time_object = datetime.time.fromisoformat(run_at_time)
self.run_at(self.run_actions, run_at_time_object)
This yields the same outcome: run_at triggers immediately when the time is in the past.
Minimal non-working example:
import hassapi as hass
from datetime import time
class TimeTriggeredActions(hass.Hass):
def initialize(self):
run_at_time_object = time.fromisoformat("01:00:00")
self.log(f"{run_at_time_object=}")
self.run_at(self.run_actions, run_at_time_object)
self.log("initialized")
def run_actions(self, **kwargs):
self.log("Called run_at")
Log output:
2025-11-27 13:20:05.779307 INFO timetrigeredactionstestapp: run_at_time_object=datetime.time(1, 0)
2025-11-27 13:20:05.783002 INFO timetrigeredactionstestapp: Called run_at
2025-11-27 13:20:05.783445 INFO timetrigeredactionstestapp: initialized
Am I really the only one with this issue? Is there a workaround? Or am I just using it incorrectly?
You are not the only one. I don't use run_at in any of my apps, but I just tested it with a run_daily callback (replacing run_daily with run_at). With run_daily and times in the past, the app does not run until the next future callback time. However, with run_at, it runs immediately.
I am also running AD v4.5.12 (via the Home Assistant add-on v0.17.12), but since I've never used run_at before, I can't say if behaved differently in earlier versions of AD in my environment.
Edit: My simple callback that reproduced your issue:
self.run_at(self.warn_of_upcoming_freezing_weather, "08:00:00")
As a workaround I've created a file in helpers called time_functions.py with the following content:
from datetime import time
from datetime import datetime, date
def seconds_to(run_at_time: time | str) -> float:
"""Seconds from now() until run_at_time. Negative if run_at_time is in the past."""
if isinstance(run_at_time, str):
nr_delimiters = run_at_time.count(":")
match nr_delimiters:
case 1:
format = "%H:%M"
case 2:
format = "%H:%M:%S"
case _:
raise ValueError(
f"run_at_time should contain one or two colons. Contains {nr_delimiters}"
)
run_at_time = datetime.strptime(run_at_time, format).time()
now = datetime.now()
diff = datetime.combine(date.today(), run_at_time) - now
return diff.total_seconds()
def time_in_window(run_at_time: time | str, window_size_seconds: int) -> bool:
"""Checks if run_at_time is happening or has happened within the last `window_size_seconds` seconds.
Example 1:
run_at_time: 00:59:55
now: 01:00:00
if window_size_seconds <=5 this function will return True, otherwise false
Example 2:
run_at_time: 00:01:05
now: 01:00:00
if window_size_seconds <=5 this function will return True, otherwise false
"""
abs_seconds = abs(seconds_to(run_at_time))
return abs_seconds < window_size_seconds
I can then use it like this to abort the callback:
from datetime import time
import hassapi as hass
from helpers.time_functions import time_in_window
class TimeTriggeredActions(hass.Hass):
def initialize(self):
self.run_at_time_object = time.fromisoformat("14:52:00")
self.log(f"{self.run_at_time_object=}")
self.run_at(self.run_actions, self.run_at_time_object)
self.log("initialized")
def run_actions(self, **kwargs):
self.log("Before check")
if not time_in_window(self.run_at_time_object,3):
return
self.log("Called run_at")
"If the callback didn't happen within 3 seconds of the requested time then abort the callback".