Commit Graph
30 Commits
Author SHA1 Message Date
Honey 7601f746fe Weather: default to 5-day forecast, add weekday+date header line
- forecast_days now defaults to 5 (was 3) across default_settings(),
  check()'s fallback, and the settings form's pre-selected option.
- New show_date setting (on by default): weekday + date in the same
  "Samstag, 25. Juli" format Clock already used. Combined with the
  city name into one header line above the icon/temp block when both
  are on ("Münster · Samstag, 25. Juli"), each independently
  toggleable.
- Moved the weekday/month name lists and the "Weekday, D. Month"
  formatting out of clock.py into render_utils.format_date_de(), so
  Weather reuses the exact same formatting instead of a second copy
  that could drift.
2026-07-25 10:38:14 +02:00
Honey 78337a99d2 Fix forecast showing yesterday's days for a while after midnight
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".
2026-07-25 00:33:19 +02:00
Honey 96423e451b Weather: show city name above the current conditions, toggleable
Just the city itself (first segment of the geocode label), not the
full "City, State, Country" string -- too cluttered for a dashboard
header. Off by default for manually-entered coordinates since there's
no resolved place name to show there regardless of the toggle.
New show_city setting, on by default, alongside the other display
toggles in Weather settings.
2026-07-24 23:38:32 +02:00
Honey 322e2882ca Serve the Pi's web UI on port 80 instead of 8080
Just http://<pi-ip> now instead of http://<pi-ip>:8080. Safe since
the systemd service already runs as root (needed for GPIO/SPI access
anyway), so binding the privileged port isn't a new requirement.

Local --mock dev stays on 8080 (README/main.py default unchanged) --
that's normally run without root, and port 80 needs it.

install.sh now does enable + restart instead of enable --now, so
re-running it after a unit-file change (like this one) actually
applies on an already-installed instance, not just a fresh install.
2026-07-24 21:29:55 +02:00
Honey 79641914fd Fix settings changes not taking effect until the poll interval elapsed
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).
2026-07-23 12:24:12 +02:00
Honey 32c98cec16 Weather: graph line yellow instead of blue 2026-07-23 12:19:08 +02:00
Honey 27f4397b04 Weather: icon-left/temp-right layout, feels-like/high-low/rain
chance, and an hourly temperature graph

- Current conditions block redone horizontally (icon left, big temp
  + description + an extras line on the right) instead of stacked
  vertically.
- New extras line: "feels like" (apparent_temperature), today's
  high/low, and rain chance (precipitation_probability_max) -- each
  independently toggleable in settings, all on by default. Switched
  from the legacy current_weather=true param to Open-Meteo's current=
  param, since that's what exposes apparent_temperature; confirmed
  field names/formats live before wiring them in.
- New hourly-today temperature graph (also toggleable): a polyline
  across today's 24 hourly points, min/max temps labeled at their
  points, a red dot marking the current hour, and hour labels along
  the bottom.
- When both the graph and the daily forecast strip are shown, they
  split the remaining vertical space; either can be toggled off to
  give the other more room, matching how forecast_days already
  worked.
2026-07-23 12:06:36 +02:00
Honey 132e133b1d All screens: drop gray text, matching the Journey fix
The epd4in0e panel's palette has no gray at all (just black/white/
yellow/red/blue/green -- see epd4in0e.py's putpalette), so any gray
RGB fill gets Floyd-Steinberg dithered into a black/white checkerboard
across the whole glyph body. At the small font sizes most of these
screens use (Navidrome's artist/album/genre/source lines, Departures'
direction/platform columns, Weather's description/forecast labels,
Clock's date line, the placeholder messages), that reads as blurry,
half-rendered text -- same root cause already fixed in Journey.

Switched every gray fill/outline to solid black across navidrome.py,
weather.py, departures.py, clock.py, and picture.py. Left the
intentionally-faint row-divider hairlines (228,228,228) alone --
those are meant to almost disappear, not readable text. Removed
departures.py's now-unused MUTED constant.
2026-07-23 11:56:24 +02:00
Honey 4c10ff94d2 Journey: show actual transfer/layover time between legs
Replaces the generic "1x umsteigen" summary with the real gap per
transfer (e.g. "SchnellBus S20 ->13min-> Regionalzug RB 67"), computed
from each leg's estimated (falling back to planned) arrival/departure.
Falls back to a plain "umsteigen" label when EFA doesn't supply both
timestamps for a given transfer.
2026-07-23 10:35:54 +02:00
Honey 4380a62524 Journey: real train/bus pictograms instead of letter badges
Vendor Font Awesome Free Solid (fonts/fa-solid-900.ttf, SIL OFL
licensed -- see fonts/FONT-AWESOME-LICENSE.txt) for its actual
train/bus glyphs. Confirmed live these stay sharp and recognizable
even at the tiny size the detail line renders at (~20px), unlike a
hand-drawn shape would be, and unlike emoji (not in DejaVu Sans,
would've rendered as tofu).

render_utils.py gets a load_icon_font() alongside the existing
load_font(), plus named ICON_BUS/ICON_TRAIN codepoint constants so
screens don't need to embed raw Private-Use-Area unicode literals.
2026-07-23 10:13:59 +02:00
Honey 7354517b22 Journey: label mode badges with Z/B letters, not just color
Plain colored dots weren't self-explanatory. Each leg badge now shows
a "Z" (Zug) or "B" (Bus) in white inside the blue/green circle --
confirmed legible at actual render size via zoomed crop inspection.
2026-07-23 10:08:51 +02:00
Honey 080cb11501 Journey: stacked header, bus/train icons, drop gray text
- Origin/destination now on their own lines instead of one truncated
  "A -> B" line, which cut off the destination entirely on the small
  panel.
- Each leg gets a colored dot (blue = train, green = bus), using the
  panel's exact palette colors so they render solid, not dithered.
  Mode is read from EFA's product.name (confirmed live: "StadtBus"/
  "RegioBus"/"SchnellBus" vs "S-Bahn"/"Regionalzug"/"Zug").
- Detail-line text switched from dithered gray to solid black -- the
  gray was hard to read at the small font size the panel renders at.
2026-07-23 10:00:36 +02:00
Honey 06b90ffe6c Journey: add regional-only filter and service notices
- connection_type setting (all/regional/long_distance) restricts EFA
  trip requests via inclMOT_N params, so ICE/IC/EC can be filtered out
  for commutes that only care about regional/local transit.
- Surface leg.infos[].infoLinks titles (real disruption/outage notices
  from EFA) on each journey row, marked with a red warning icon.
2026-07-23 09:51:52 +02:00
Honey d5757a00df Fix Journey location search: use EFA's own stopfinder, not Open-Meteo
Open-Meteo's geocoder is built for city-level weather lookups -- it knows
"Münster" but has no idea what "Münster Hauptbahnhof" means, so any
specific station name failed to resolve. Reused it from Weather without
checking it actually fit this use case.

The db_info reference project sidesteps this entirely by requiring the
user to pick existing Home Assistant person/zone entities, which already
carry precise coordinates from HA's own location system -- no equivalent
here, so this needed real geocoding.

Switched geocode() to query the same EFA instances' XML_STOPFINDER_REQUEST
endpoint instead -- the same system that answers the trip request, so
location resolution is guaranteed consistent with what trip planning
understands. Verified live: correctly resolves specific stations ("Münster
Hauptbahnhof" -> "Münster (Westf), Hbf", even the "Muenster" ue-spelling
the user actually typed), plain city names (falls back to the main
station), and district/neighborhood names (e.g. "Hiltrup" -> the actual
Hiltrup stop, not just the vague suburb entry) -- all via matchQuality
scoring and the API's own isBest flag. Re-verified the full pipeline
end-to-end through the real Flask route afterward.
2026-07-22 23:40:08 +02:00
Honey c182fb2f19 Add Journey screen: next connections for a fixed daily commute
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.
2026-07-22 22:33:36 +02:00
Honey 60acdc3756 Add favicon: inline SVG panel+dot icon matching the nav brand mark, no asset file needed 2026-07-22 21:54:33 +02:00
Honey a9ed1b6fb4 Clarify Departures covers bus/tram/subway too, add clear found/not-found feedback
The API (HAFAS via db-vendo-client) covers all public transport in one
system, not just trains -- verified with a mocked bus-stop response
(Bus 14/Bus 9/Bus 66 mixed with a regional train, all rendering correctly,
no code changes needed since nothing in check()/render() ever filtered by
mode). Fixed the "Train departures" wording in code comments and settings
UI, which was misleadingly train-specific.

Also added an explicit "Found: <station>" confirmation toast on a
successful station lookup, distinct from the generic "Saved." toast --
previously the only feedback was a passive "Currently: X" hint, easy to
miss and not obviously tied to the save you just did.
2026-07-22 21:45:18 +02:00
Honey 8c9b3e1bfa Add Departures screen (train departure board)
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.
2026-07-22 21:38:20 +02:00
Honey 889c062212 Make weather update interval configurable, was hardcoded to 30 min
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.
2026-07-22 20:14:46 +02:00
Honey 15c7c38d86 Make manual pin permanent instead of a 5-minute timeout
"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.
2026-07-22 00:07:25 +02:00
Honey c0192fca7a Make web UI dark mode only, drop prefers-color-scheme light theme 2026-07-21 23:59:23 +02:00
Honey 8798466e5b Redesign weather with colorful hand-drawn icons, bigger forecast
New screens/weather_icons.py: vector icons drawn with PIL primitives using
the panel's actual 6-color palette (black/white/yellow/red/blue/green, from
epd4in0e's own getbuffer() quantization table) -- sun with rays, line-art
clouds, blue rain/snow, yellow lightning bolt, fog bands. Replaces the old
monochrome unicode-glyph approach entirely (also sidesteps font coverage
gaps -- that's what caused the earlier tofu-box bug).

Forecast row given significantly more vertical space per request. Caught
two real bugs while building this:
- Current-conditions block height was guessed independently of the space
  allocated to it, so it overflowed into the forecast row for real content.
  Now derives the space it needs from actual measured text/icon size
  instead of assuming both fit.
- `scale * 0.045 if has_forecast else 0.05` -- conditional-expression
  precedence bug, silently evaluated to a bare 0.05 (not scale * 0.05) in
  the else branch, crashing font loading with size=0 when forecast is
  empty. Only surfaced by testing that specific case.
- Sun icon's rays extended past the nominal icon_size box used for layout
  math, throckwing off vertical centering when no forecast is shown.
2026-07-21 23:42:22 +02:00
Honey a5fd39f2af Make the progress bar purely decorative, drop live position tracking
positionMs changes every poll (~20s), so the old live progress bar/elapsed
time forced a full flash constantly, even mid-song -- this panel has no
partial refresh, so there's no way to show real progress without that.
Now it's an outline + total length only, driven by nothing that changes
while the same track keeps playing. Verified the rendered image is
byte-identical across different positionMs values for the same track.

Also dropped elapsed/elapsed_text/progress_pct from the track dict
entirely now that nothing renders them.
2026-07-21 23:28:27 +02:00
Honey 5baa45904c Fix off-center Navidrome layout
Two separate imbalances:
- Cover art was top-aligned when width-constrained (landscape, cover_size
  capped by width*0.5), leaving dead space below it instead of centering
  in the available height.
- The text column always started drawing at the very top and let the
  source line's bottom-pin create a big empty gap for tracks with
  short/sparse metadata -- looked top-heavy and unbalanced.

Text column is now built as (height, draw_fn) pairs first so the total
height is known before committing to a starting y, then centered in the
space above the reserved source-line area.
2026-07-21 23:25:44 +02:00
Honey a19a7ea49e Fix manual switch reverting instantly, add weather forecast
- 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.
2026-07-21 23:06:16 +02:00
Honey bfe4616f20 Add Picture screen: upload a file or point at a URL
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.
2026-07-21 22:56:32 +02:00
Honey 61348be674 Landscape orientation, redesigned web UI, rotation on/off + minutes, weather by city name
- 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.
2026-07-21 22:51:16 +02:00
Honey 7c197e20a5 fix: correct waveshare_epd import path
Was importing 'waveshare_epd' as a top-level package, but it's nested
under display/waveshare_epd/ -- caused ModuleNotFoundError on the Pi
since only display/ is on sys.path via main.py's imports, not
display/waveshare_epd/ itself. Verified the corrected import resolves
by exercising it directly; the only error left is the driver's own
platform check falling through to its Jetson Nano path on this non-Pi
dev machine, which is expected here and won't happen on the real
Raspberry Pi (it checks /proc/cpuinfo for "Raspberry").
2026-07-21 22:38:48 +02:00
Honey 136495eab6 docs: switch install instructions from SFTP to git clone, add update section 2026-07-21 22:32:02 +02:00
Honey 66464291e1 Initial commit: Paperwall, a standalone e-ink dashboard
Navidrome now-playing, clock, and weather for a Waveshare epd4in0e panel,
with a local web UI and install/uninstall scripts for the Pi.
2026-07-21 22:29:45 +02:00