Put the lights back when the controller reboots (#148) #175
No reviewers
Labels
No labels
area/ai
area/backend
area/frontend
area/infra
area/scheduler
area/wled
good-first-issue
priority/high
priority/low
priority/medium
security
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 milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/Iris-WLED!175
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/148-watchdog"
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?
Closes #148.
Why
When a controller reboots (a GFCI trip, a storm, an OTA update), it comes back in its own boot preset. Iris didn't notice until the next transition, which could be hours later.
What (3 commits)
1.
expected_state(now)(services/expected_state.py): what the lights should be showing at this moment.plan_for_date(which already carries #150's guard) and replays the night's transitions: on, ramps, late-night dim, overnight-off, morning-on and off./remoteoverride is the expected state.Indeterminateduring a ramp, a preview, within 120 s of a transition, with no location, or with no controller.DemoControlleracross 8 configurations × approved, unapproved and no-event nights. After each job it asserts that the controller shows exactlyexpected_state, field for field. A separate test checks that the times it walks are the timesschedule_today_jobsregisters.date.today(), so a 00:30 dim dimmed against the next night's event.2.
last_known_goodnow means "retry when the controller is back". It always re-pushed the failed state anyway, and pushing the previous transition's state after a failed sunset push would put back last night's "off".expected_state()for that moment, so a stale state can't overwrite a newer one._last_known_goodglobal is gone.3. The watchdog (
services/watchdog.py): an interval job, 5 minutes by default, on by default.info.uptimegoing backwards. The last uptime is stored per controller host in the settings table, so an Iris restart is never read as a controller reboot, but a shared power cut is detected.on,bri(±2) and each managed segment'scol/fx/sx/ix/pal.reboot_only(the default): corrects only after a reboot or a reconnect, and reports drift once per episode. It doesn't fight someone using the WLED app.enforcecorrects all drift.state_correctedwebhook (on by default) andstate_drift(opt-in), theiris_watchdog_corrections_total{reason}counter, theiris_watchdog_last_check_timestamp_secondsandiris_watchdog_driftgauges, and aschedule_logrow per correction.increase(iris_watchdog_corrections_total[1d]) > 3, which suggests a flaky circuit or PSU.Verification
tsc, vitest (187 passed) and build.Existing bugs noticed
iris_scheduler_last_success_timestamp_secondsis off by the UTC offset whenTZisn't UTC.enforce, the watchdog undoes a Push now made outside lit hours; this is documented.late_night_timepushes full brightness; with a ramp it lands dimmed. The parity test holds both as they are.🤖 Generated with Claude Code
A controller that loses power -- a tripped GFCI, a storm, a firmware update -- comes back in its own boot preset, and nothing noticed until the next transition. A 3 a.m. brownout left the lights on all day. A watchdog job now reads the controller back every watchdog_interval_min minutes (default 5) and compares it with expected_state. A reboot is info.uptime going down, remembered per controller host in memory and the settings table, so an Iris restart is not read as one and a power cut that takes Iris down too is still caught. A reconnect after an unreachable check counts the same. Either is corrected through push_with_fallback. While the controller is unreachable, or a retry or reconnect watcher is pending, the watchdog leaves recovery to the push fallback. watchdog_policy "reboot_only" (the default) reports any other difference once per episode and leaves it -- a colour picked in the WLED app is not a fault. "enforce" corrects it; "off" stops checking. New webhook events state_corrected (on by default) and state_drift (opt-in), metrics iris_watchdog_corrections_total{reason}, iris_watchdog_last_check_timestamp_seconds and iris_watchdog_drift, a schedule_log row per correction, a Watchdog card in Settings, and a demo-controller hook that simulates a reboot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>