The nightly reschedule cancels the pending sunrise-off, so lights are never turned off automatically #106
Labels
No labels
area/ai
area/backend
area/frontend
area/infra
area/scheduler
area/wled
good-first-issue
priority/high
priority/low
priority/medium
type/bug
type/chore
type/ci-cd
type/docs
type/feature
type/qa
v1.0.0
v1.1.0
v1.2.0
v2.0.0
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/Iris-WLED#106
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while checking whether #7's unattended overnight cycle actually ran on the dev deployment. It did not — and the cause is a scheduling bug, not a one-off.
Symptom
On 2026-09-01,
dim_jobfired correctly at 23:00 CDT.off_jobnever fired at all. There is noRunning job run_off_jobline in the container log, and nooff_jobrow inschedule-logfor that morning — the only two entries are from my manual runs the night before.The lights were off when I checked, which briefly looked like success. They had been turned off by a Home Assistant automation, not by Iris. Without that coincidence the roofline would have burned all day.
Cause
There is exactly one
off_jobslot, and the nightly reschedule overwrites it before it can fire._solar_timescorrectly computes off as the next day's sunrise (scheduler.py/schedule_service.py:78-82):_maybe_scheduleregisters all three under fixed ids withreplace_existing=True:And
reschedule_dailyruns at 00:01 local — which falls between the evening that scheduled the off and the morning it was due:off_jobscheduled for sunrise of D (06:31)reschedule_dailyrecomputes for target D → off = sunrise of D+1, replacing the pending jobConfirmed in the live jobstore: after 00:01 CDT on 2026-09-01,
off_jobsits at2026-09-02 11:32 UTC, and the 2026-09-01 06:31 CDT firing simply never happened.on_jobanddim_jobare unaffected because their correct values always fall on the same calendar day as the reschedule.off_jobis the only one whose value belongs to the next day, which is why it is the only one that collides.Impact
For an outdoor lighting controller this is close to worst-case: the lights are never turned off by Iris. Every night they come on and dim on schedule, then stay lit through the following day until the next evening's cycle. It is silent — every job reports success, and the schedule log looks healthy because the missing job leaves no row.
It also means #7's remaining acceptance criterion — "the configure→approve→push→schedule→sunrise-off loop runs unattended" — cannot pass today.
Possible fixes
reschedule_dailyoff midnight to a time that never has a pending job — local noon, say. It always sits between the morning off and the evening on, so nothing is ever clobbered. One-line cron change, fixes it structurally, and log pruning is just as happy at noon.off_job:2026-09-01), so a new day's jobs cannot replace a pending one. Needs cleanup of stale ids._maybe_schedule. Narrowest change, but the subtlest — the invariant lives in a helper that reads as a pure "add a job".Option 1 is my recommendation: it removes the overlap rather than defending against it. But this is a design call and the "day" boundary for a lighting schedule is genuinely sunset→sunrise, not midnight→midnight, so option 2 may model it more honestly.
Acceptance criteria
off_jobis still registered for the original timeoff_jobpresent inschedule-logand the controller off after sunrise, with no external automation involvedBlocks #7. Related: #10.
Fixed in #107, merged and deployed to the dev host. Staying open until one real overnight confirms it.
Not the fix proposed above
I recommended option 1 — move
reschedule_dailyoff midnight. Implementing it showed that was incomplete. A container restart between midnight and sunrise loses the pending off the same way, because startup callsschedule_today_jobs()too. Moving the cron would have fixed the scheduled path, left the restart path broken, and looked fixed — worse than leaving it.So the fix went to where the wrong assumption lives.
get_active_times()carries a still-pending off forward from the previous cycle; the scheduler and/schedule/statusboth use it, so the reported off is the one that will actually fire.Verified on the deployed image, real settings and coordinates
Row 2 is the case the cron move would have missed.
Jobstore after deploy:
One small diagnostic change
The startup log line now prints the date, not just
HH:MM. Previously it readoff=06:32with no indication of which day, so the log looked identical whether the off was 6 hours away or 30. That ambiguity is part of why this survived so long, and it cost nothing to remove.Acceptance criteria
on_job19:01,dim_job23:00,reschedule_daily00:01,off_job06:32. The proof is anoff_jobrow inschedule-logtomorrow morning plus the controller actually off.The Home Assistant automation that masked the original failure has been disabled, so nothing external can produce that result on Iris's behalf this time.
Verified on real hardware without waiting for morning
Rather than wait for the overnight, the bug condition was reproduced in minutes by manufacturing it with
SUNRISE_OFFSET_MIN.The distinguishing condition is "now is after midnight but before an off that was computed from the previous cycle." Setting
sunrise_offset_min = 334at 12:00 CDT produced exactly that:Side by side on the deployed image:
The app then scheduled it itself on startup:
And it fired:
A
schedule_logrow foroff_job— the row that was missing on the morning of 2026-09-01 — plus the controller actually off. That is the whole failure closed end to end.Two method notes worth keeping
docker exec … run_reschedule_daily_jobs()does nothing. That process has its own_scheduler = None, soschedule_today_jobs()returns immediately and the app's jobstore is untouched. It reports success. This is the same process-isolation trap asinit_controller— a job driven by hand in a fresh process proves nothing about the running scheduler. The reschedule was therefore forced through a container restart, so the app did the scheduling.That restart also covered the case the originally-proposed fix would have missed — startup between midnight and the pending off. Moving the cron off midnight would have left exactly this path broken.
The date on the startup log line earned itself immediately:
off=12:05andoff=12:06are indistinguishable without it, and those were the two candidate answers.Restored
sunrise_offset_min→ 0on=2026-09-01 19:01 CDT, dim=23:00, off=2026-09-02 06:32 CDTAcceptance criteria
off_jobfired unattended, logged, and switched the controller off, with the pending-off condition genuinely reproducedClosing. Tonight's real cycle (19:01 → 23:00 → 00:01 reschedule → 06:32 off) now serves as confirmation in ordinary conditions rather than as the only proof; that observation belongs to #7.