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.
Not needed any more -- the Journey screen already covers the same
regional-transit need via EFA backends, and Departures depended on
the free db-vendo-client API that DB has been actively blocking third
parties from anyway.
Removed screens/departures.py, its settings template, its route, its
registration in screens/__init__.py, and its nav link. Reworded the
docstring/hint references in journey.py and settings_journey.html
that pointed at the now-gone screen. Existing config.json entries
referencing "departures" (rotation list, pinned screen) are harmless
leftovers -- both scheduler lookups already filter to screens that
still exist, so nothing needs a migration step.
Verified: app boots clean, /settings/departures 404s, every other
settings page still loads fine, no leftover references outside of
journey.py's own unrelated "departure time" wording.
New screens/journey.py, ported and simplified from github.com/EiS94/db_info
(a Home Assistant integration). Deliberately uses only the regional EFA
(Elektronische Fahrplanauskunft) backends the reference project queries
(VRR prioritized for NRW routes, falling back to Bavaria/BW instances) and
drops its bahn.de-internal-API source entirely -- that's DB's own
infrastructure, exactly the kind of thing DB has been actively blocking
(see the Departures screen). EFA instances are run by regional transit
authorities, not DB, so they aren't part of that clampdown. Also dropped
the reference project's parallel async fetch-and-score-all-sources
machinery for simple sequential fallback, matching this project's fully
synchronous style.
Verified against live VRR data for a real NRW route (Münster -> Dortmund):
real trains, platforms, transfer counts, even a night bus connection.
Caught a real bug during that live test: EFA returns UTC timestamps (Z
suffix), and datetime.fromisoformat() parses that correctly into an
aware datetime, but strftime() on an aware datetime prints its own
stored hour/minute rather than converting to local time first --
everything displayed 2 hours behind (CEST is UTC+2), which looked like
"next" departures had already left. Fixed by converting with
astimezone() before formatting; re-verified live afterward that
departures now correctly show after the query time.
Settings: origin/destination resolved via Open-Meteo's free geocoding
(same pattern as Weather's city field), how many connections to show
(1/2/3/5), and its own configurable update interval.
New screens/departures.py using the free, unofficial db-vendo-client REST
API (v6.db.transport.rest) -- no key needed, same "no API key" philosophy
as Weather's Open-Meteo integration. Community-run infra though, not
official DB, and it was actually down (503, then timing out entirely) for
the whole time I was building this -- couldn't live-verify against real
data the way Open-Meteo was earlier, so I built the parsing against the
project's documented example response and stress-tested render() with
mocked requests.get() covering on-time/delayed/cancelled/missing-platform/
very-long-direction cases instead. check() fails soft (logs a warning,
returns None) on any request error, matching the "service might be down"
reality of this API -- the screen shows a plain "not available" message
rather than erroring out.
Settings: station name (resolved to a station ID via /locations, same
pattern as Weather's city->coordinates geocoding), how many departures to
show (3/5/8), and its own configurable update interval via the
get_poll_interval() mechanism built for Weather (defaults to 2 min --
departure boards go stale much faster than weather does).
Caught two real layout bugs while stress-testing render() before shipping:
line names ("ICE 1234") and delayed/cancelled time text ("Fällt aus") were
both wider than the columns I'd budgeted for them, with the second one
overlapping the direction text when right-aligned past its column's own
left edge. Fixed by measuring actual worst-case widths at the chosen font
size instead of guessing, and truncating every field to its column as a
defensive backstop regardless.
- 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.
New screen alongside clock/weather/navidrome. Upload goes through the web
UI (validated with PIL before accepting, stored under web/static/uploads/,
gitignored), or point it at a URL re-fetched every 30 minutes. Cover/contain
fit modes (new fit_cover/fit_contain helpers in render_utils.py, generalized
from the square-only cover-art cropping already used by the Navidrome
screen), plus the same contrast-boost/grayscale options that screen has.
- 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.