Skip to content

Commit 4ab034b

Browse files
authored
Merge pull request #2516 from cebtenzzre/fix-next-is-tomorrow
fix run_at when the next occurrence is tomorrow
2 parents a49d10e + a27817c commit 4ab034b

File tree

2 files changed

+46
-1
lines changed

2 files changed

+46
-1
lines changed

appdaemon/adapi.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2975,7 +2975,9 @@ async def run_at(
29752975
_, offset = resolve_time_str(start_str, now=now, location=self.AD.sched.location)
29762976
func = functools.partial(func, *args, repeat=True, offset=offset)
29772977
case _:
2978-
start = await self.AD.sched.parse_datetime(start, aware=True)
2978+
# For run_at, always schedule for the next occurrence (today=False)
2979+
# This ensures that times in the past are scheduled for tomorrow
2980+
start = await self.AD.sched.parse_datetime(start, aware=True, today=False)
29792981
func = functools.partial(
29802982
self.AD.sched.insert_schedule,
29812983
name=self.name,

tests/unit/datetime/test_parse_datetime.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,3 +329,46 @@ def test_exact_sun_event(default_date: date, location: Location, tz: BaseTzInfo)
329329
assert next_sunset.date() != default_date, "Next sunset should be tomorrow"
330330
assert next_sunset.date() != default_date, "Next sunset should be tomorrow"
331331
assert next_sunset.date() != default_date, "Next sunset should be tomorrow"
332+
333+
334+
def test_run_at_time_in_past(default_now: datetime, default_date: date, tomorrow_date: date, parser: partial[datetime]) -> None:
335+
"""Test that run_at schedules for next day when time is in the past.
336+
337+
This test reproduces the bug reported in issue #2491 where run_at() with a time
338+
in the past runs immediately instead of scheduling for the next day.
339+
340+
The fix is to have run_at() explicitly pass today=False to parse_datetime,
341+
which forces times in the past to be scheduled for tomorrow.
342+
"""
343+
from datetime import time
344+
345+
# Current time is 12:00 (default_now is 12:00:00)
346+
# Test with a time object that's 1 hour in the past (11:00)
347+
past_time = time(11, 0, 0)
348+
# run_at should call parse_datetime with today=False
349+
result = parser(past_time, today=False)
350+
351+
# Since the time is in the past and today=False (behavior for run_at),
352+
# it should be scheduled for tomorrow
353+
assert result.date() == tomorrow_date, f"Expected {tomorrow_date}, got {result.date()}"
354+
assert result.time() == past_time
355+
356+
# Test with a time string that's in the past
357+
result_str = parser("11:00:00", today=False)
358+
assert result_str.date() == tomorrow_date, f"Expected {tomorrow_date}, got {result_str.date()}"
359+
360+
# Test with a time that's in the future (should be today)
361+
future_time = time(13, 0, 0)
362+
result_future = parser(future_time, today=False)
363+
assert result_future.date() == default_date, f"Expected {default_date}, got {result_future.date()}"
364+
assert result_future.time() == future_time
365+
366+
# Test with today=True explicitly (should be today even if in the past)
367+
result_today = parser(past_time, today=True)
368+
assert result_today.date() == default_date
369+
assert result_today.time() == past_time
370+
371+
# Test with today=None (default for elevation events - should be today even if past)
372+
result_none = parser(past_time, today=None)
373+
assert result_none.date() == default_date
374+
assert result_none.time() == past_time

0 commit comments

Comments
 (0)