Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion appdaemon/adapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -2975,7 +2975,9 @@ async def run_at(
_, offset = resolve_time_str(start_str, now=now, location=self.AD.sched.location)
func = functools.partial(func, *args, repeat=True, offset=offset)
case _:
start = await self.AD.sched.parse_datetime(start, aware=True)
# For run_at, always schedule for the next occurrence (today=False)
# This ensures that times in the past are scheduled for tomorrow
start = await self.AD.sched.parse_datetime(start, aware=True, today=False)
func = functools.partial(
self.AD.sched.insert_schedule,
name=self.name,
Expand Down
43 changes: 43 additions & 0 deletions tests/unit/datetime/test_parse_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,3 +329,46 @@ def test_exact_sun_event(default_date: date, location: Location, tz: BaseTzInfo)
assert next_sunset.date() != default_date, "Next sunset should be tomorrow"
assert next_sunset.date() != default_date, "Next sunset should be tomorrow"
assert next_sunset.date() != default_date, "Next sunset should be tomorrow"


def test_run_at_time_in_past(default_now: datetime, default_date: date, tomorrow_date: date, parser: partial[datetime]) -> None:
"""Test that run_at schedules for next day when time is in the past.

This test reproduces the bug reported in issue #2491 where run_at() with a time
in the past runs immediately instead of scheduling for the next day.

The fix is to have run_at() explicitly pass today=False to parse_datetime,
which forces times in the past to be scheduled for tomorrow.
"""
from datetime import time

# Current time is 12:00 (default_now is 12:00:00)
# Test with a time object that's 1 hour in the past (11:00)
past_time = time(11, 0, 0)
# run_at should call parse_datetime with today=False
result = parser(past_time, today=False)

# Since the time is in the past and today=False (behavior for run_at),
# it should be scheduled for tomorrow
assert result.date() == tomorrow_date, f"Expected {tomorrow_date}, got {result.date()}"
assert result.time() == past_time

# Test with a time string that's in the past
result_str = parser("11:00:00", today=False)
assert result_str.date() == tomorrow_date, f"Expected {tomorrow_date}, got {result_str.date()}"

# Test with a time that's in the future (should be today)
future_time = time(13, 0, 0)
result_future = parser(future_time, today=False)
assert result_future.date() == default_date, f"Expected {default_date}, got {result_future.date()}"
assert result_future.time() == future_time

# Test with today=True explicitly (should be today even if in the past)
result_today = parser(past_time, today=True)
assert result_today.date() == default_date
assert result_today.time() == past_time

# Test with today=None (default for elevation events - should be today even if past)
result_none = parser(past_time, today=None)
assert result_none.date() == default_date
assert result_none.time() == past_time
Loading