New "Time-based schedule" section on the Rotation settings page: up to
4 rules, each a screen + a daily HH:MM-HH:MM window (crossing midnight
is fine, e.g. 22:00-02:00). While a rule's window is active, that
screen overrides rotation and any manual pin -- without touching
either's underlying state, so things go back to exactly how they were
once the window ends. Navidrome-playing still wins on top of a
schedule rule, same as it already does over everything else.
config.py: new schedules list (get_schedules/set_schedules), each rule
just {screen_id, start, end} -- being in the list *is* being enabled,
no separate flag (a settings row is "disabled" by leaving its screen
dropdown on "-- none --").
scheduler.py: _time_in_window() handles same-day and midnight-wrapping
windows, treats a zero-width window as never-matching (more likely a
blank settings row than an intentional all-day rule).
_active_scheduled_screen() slots into _select_screen() between
Navidrome-priority and the pin/rotation fallback.
Verified: unit tests for _time_in_window's edge cases (wraparound,
zero-width, malformed input) and the priority override, plus an
end-to-end run against the real app -- pinned a screen, added a rule
active right now, confirmed the status page switched to the scheduled
screen within one tick.
Weather's daily forecast starts with "today" as of when check() last
ran -- correct at fetch time, but that cached state sits until the
next poll (up to update_minutes later, 30 min by default). Right
after local midnight, that means the forecast keeps showing the old
day first (e.g. Fr Sa So Mo Di at 00:30 Saturday) until the next
scheduled poll happens to land after the rollover.
ScreenCache now also tracks the calendar date it was last checked on
and treats itself as stale on a date change, independent of
poll_interval -- so the very next tick after midnight re-checks
weather (and any other date-sensitive screen) immediately instead of
waiting out the rest of the interval. Verified with a unit test
simulating a cache populated "yesterday".
config.update_screen_settings() writes to config.json immediately, but
each screen's cached check() result only gets refreshed on its own
poll_interval (30 min for weather by default). So e.g. changing
Weather's forecast_days from 3 to 5 saved correctly but kept showing
the old 3-day forecast for up to 30 minutes, since the cached state
was fetched under the old setting and nothing told the scheduler to
re-check sooner.
Added Scheduler.invalidate_screen(), called from every settings route
right after config.update_screen_settings() succeeds, which resets
that screen's cache timestamp so the very next tick re-checks instead
of reusing stale data. Reproduced the bug and verified the fix with a
direct Scheduler unit test, then confirmed end-to-end through a real
running app (POST forecast_days=5, saw it render 5 days within the
next ~5s tick instead of after 30 minutes).
Screen.poll_interval was a fixed class attribute the scheduler read
directly -- added Screen.get_poll_interval(settings) so a screen can make
it settings-driven instead. WeatherScreen overrides it to read a new
"update_minutes" field (default 30, matching the old hardcoded value).
New number input on the Weather settings page.
Caught a real edge case while testing: `raw or 30` treats an explicit 0
as falsy and silently replaces it with 30 instead of clamping to the
1-minute floor. Not reachable via the UI (min="1" on the input) but the
method should still be correct standalone -- fixed with an explicit
None-check instead.
"Show X now" now persists indefinitely (survives restarts too, it's in
config.json) instead of auto-expiring back to rotation after 5 minutes.
Implemented by reusing the existing rotation.enabled flag rather than a
separate scheduler-side timer: pinning a screen sets rotation.enabled=False
and records which screen in a new pinned_screen config field. Re-enabling
rotation -- either the "Resume automatic" button or just checking
"Enabled" again on the Rotation settings page -- clears the pin and
resumes normal cycling either way.
Navidrome priority is unchanged: still checked first, still preempts a
pin the instant something's playing, same as it already preempted
rotation.
Verified end-to-end: pin persists across many scheduler ticks (previously
would have reverted after 5 min), both resume paths (button and settings
checkbox) clear it correctly.
- scheduler: "Show X now" was a true one-shot -- reverted on the very next
5s tick, causing the flash-to-X-then-flash-back-to-clock behavior. Now
pins for 5 minutes (still preemptable by Navidrome priority, which stays
authoritative). Added a "Resume automatic" action/button and status
display showing what's pinned and until when.
- weather: daily forecast strip (3/5/7 days, selectable in settings) below
current conditions, via Open-Meteo's daily= params in the same request.
Fixed a bad symbol choice along the way -- DejaVu Sans has no glyph for
the partly-cloudy emoji, was rendering as a tofu box; swapped to the
plain cloud symbol, verified glyph coverage for the whole WMO code table.
- Panel is mounted sideways: render landscape (600x400), rotate to the
panel's native portrait orientation right before writing to hardware.
- Web UI: card-based layout, dark mode, active nav highlighting, save
confirmation toasts, error banners.
- Rotation: separate enabled/disabled toggle (off pins to the first screen
in the list) and the interval is now in minutes instead of raw seconds.
Migrates existing config.json from the old interval_seconds field.
- Weather: type a city name instead of hunting down coordinates, resolved
via Open-Meteo's free geocoding API. Manual lat/long still available as
a fallback.