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

Paperwall

A standalone dashboard for one Waveshare epd4in0e (4", 6-color) e-ink panel: Navidrome now-playing, clock, weather, departure boards, a fixed-route journey planner, and pictures (upload or URL), switchable/controllable from a local web UI. Started as an InkyPi replacement, but doesn't depend on InkyPi at all -- own venv, own vendored copy of the Waveshare driver files.

Confirmed by reading the vendor driver directly: this panel has no partial-refresh mode at all -- every update is a full flash. So the design here isn't about avoiding that flash, it's about only triggering it when something real actually changed (song change, minute tick on the clock, weather update), via hash-comparing each rendered frame against what's currently on screen.

Local development (no hardware needed)

python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python3 main.py --mock --port 8080

--mock saves each frame to mock_output/ as a PNG instead of writing to real hardware. The web UI (http://localhost:8080) works identically either way -- same status page, same settings forms, same "switch screen now" buttons. requirements-pi.txt (gpiozero/lgpio/RPi.GPIO) is only needed on the real Pi, so local dev doesn't need it -- and those generally won't even build off a Pi anyway.

Installing on the Pi

git clone https://gitea.honeyy.duckdns.org/Honey/Paperwall.git /root/paperwall
cd /root/paperwall
sudo bash install.sh

That's the whole thing. It installs the few system packages needed to build lgpio/spidev (liblgpio-dev, swig, python3-dev), enables SPI/I2C in config.txt if not already on, creates its own venv right next to the project and installs everything into it (requirements.txt + requirements-pi.txt), and installs+starts paperwall.service. If it finds InkyPi (or anything else) already running, it stops it first -- two things can't drive the same panel at once -- but nothing is copied from or reused out of it. Safe to re-run.

Then open http://<pi-ip>, fill in Navidrome credentials under Navidrome and a city under Weather. Everything saves immediately and takes effect within a few seconds -- no restart needed.

Updating

cd /root/paperwall
git pull
sudo systemctl restart paperwall.service

venv/ and config.json are gitignored, so git pull never touches your installed dependencies or your Navidrome/weather settings -- just the code. A plain restart is enough for most changes. Only re-run sudo bash install.sh when requirements.txt or requirements-pi.txt changed (new dependency) or you need SPI/I2C re-checked -- it's idempotent, safe to run any time.

Uninstalling

sudo bash uninstall.sh

Stops the service, removes the systemd unit and the venv install.sh created. Leaves the project folder and config.json alone -- delete those yourself if you want it fully gone. Doesn't touch SPI/I2C or anything else on the system.

(If you're migrating away from InkyPi specifically and want to remove that too, see uninstall_inkypi.sh -- separate script, since it's InkyPi's install that needs reversing, not Paperwall's.)

Deploying to a different e-ink screen

Everything panel-specific lives behind the Display protocol in display/__init__.py (init(), show(image), sleep()) -- screens never talk to hardware directly, they just return a PIL.Image sized to display.resolution. To support a different panel:

  1. Waveshare panel, different model (e.g. a monochrome one for fast partial refresh): vendor that model's driver files into display/waveshare_epd/ the same way (check the Waveshare e-Paper GitHub repo for whether it's in the standard RaspberryPi_JetsonNano/python/lib/waveshare_epd/ path or, like epd4in0e, needs the "Separate Program" path instead). Write a new driver class mirroring display/epd4in0e_driver.py, with the panel's real pixel resolution. If the model exposes displayPartial() (common on B/W and B/W/red panels, unlike this 6-color one), that's a real opportunity: add a supports_partial flag and an optional show_partial(image, region) to the Display protocol, and have the scheduler use it for screens that only change a small region (e.g. just the clock's time text) instead of always doing a full flash.
  2. Pimoroni Inky panel: add inky to requirements-pi.txt and write a driver using inky.auto.auto() instead of the Waveshare EPD classes -- simpler, since Pimoroni's library handles the hardware differences for you.
  3. Either way, update resolution in the new driver, and swap which driver main.py's build_display() constructs.

Nothing in scheduler.py, config.py, web/, or any screen needs to change -- that's the point of the Display boundary.

Layout

main.py                  entrypoint: init display, load config, start scheduler + web app
config.py                 config.json load/save
scheduler.py               rotation / Navidrome-preemption / hash-dedup logic
display/                    Display protocol + epd4in0e driver (vendored) + mock driver for dev
screens/                     one module per screen (navidrome.py, clock.py, weather.py)
web/                          Flask status page + settings forms
install.sh                    sets up venv + service on the Pi, standalone
uninstall.sh                   removes what install.sh set up
uninstall_inkypi.sh             separate: removes InkyPi itself, if you were migrating from it
S
Description
Raspberry Eink Screen Dashboard
Readme
1.1 MiB
0 Stars 0 Watchers 0 Forks
Languages
Python 79.8%
HTML 14.7%
Shell 5.5%