Python application & daemon that integrates with a OpenDisplay screen to show custom screens.
  • Python 97.2%
  • Dockerfile 1.4%
  • Shell 1.4%
Find a file
Kamil Marut cf416253d2
All checks were successful
Lint / ruff (push) Successful in 19s
chore: Create a script for releasing new tags
2026-05-29 10:12:52 +02:00
.forgejo/workflows ci: Add linter to CI 2026-05-29 10:12:25 +02:00
scripts chore: Create a script for releasing new tags 2026-05-29 10:12:52 +02:00
src/retheia fix: Verbose logging on all commands 2026-05-28 22:04:33 +02:00
.env.example build(docker): Add Dockerfile and Docker Compose file 2026-05-28 22:44:14 +02:00
.gitignore build(docker): Add Dockerfile and Docker Compose file 2026-05-28 22:44:14 +02:00
AGENTS.md feat: Create MVP 2026-05-28 20:50:18 +02:00
docker-compose.yml build(docker): Add Dockerfile and Docker Compose file 2026-05-28 22:44:14 +02:00
Dockerfile build: Improve Dockerfile builds 2026-05-28 22:49:47 +02:00
pyproject.toml ci: Add linter to CI 2026-05-29 10:12:25 +02:00
README.md docs: Add additional info to README.md 2026-05-28 21:39:31 +02:00
uv.lock ci: Add linter to CI 2026-05-29 10:12:25 +02:00

reTheia

Async daemon that drives a Seeed reTerminal E1001 ePaper display running OpenDisplay firmware. The device deep-sleeps and wakes every ~6 h to broadcast a BLE advertisement; reTheia runs on a home server, scans for that advertisement, renders one of three screens, and pushes the image over BLE before the device sleeps again.

Screens

  • dashboard (default) — current weather + 3-day forecast (Open-Meteo) + EUR/PLN and USD/PLN (NBP) on the left; next 6 Google Calendar events on the right.
  • calendar — full-width week view.
  • http-cat — random http.cat image.

All three carry a 20 px status bar: screen name on the left, estimated battery percent (computed from the BLE advertisement's battery_mv) on the right.

Install

You need uv and libcairo installed.

uv sync

Configure

Either set env vars or pass CLI flags (CLI takes precedence). Env vars:

Variable Purpose
RETHEIA_CITY / RETHEIA_COUNTRY City + ISO country code for weather.
RETHEIA_LAT / RETHEIA_LON Skip geocoding by passing coordinates directly.
RETHEIA_DEVICE_MAC Optional MAC filter — otherwise the daemon listens for any OpenDisplay advertisement.
RETHEIA_DEVICE_NAME Optional name filter.
RETHEIA_CALENDAR_ID Google Calendar id (default primary).
RETHEIA_TIMEZONE IANA tz name for rendering event times (default: $TZ or UTC).
RETHEIA_STATE_FILE Override the active-screen state file.
RETHEIA_GOOGLE_CREDENTIALS / RETHEIA_GOOGLE_TOKEN Override OAuth file paths.

Google Calendar auth

  1. In Google Cloud Console: enable the Calendar API, create an OAuth client of type "Desktop app", download the JSON.
  2. Save it to ~/.config/retheia/google/credentials.json (or set RETHEIA_GOOGLE_CREDENTIALS).
  3. Run uv run retheia auth. A browser window opens; consent. A token is cached at ~/.config/retheia/google/token.json (mode 600).

Notes:

  • The flow needs a desktop browser. On a headless server, run retheia auth on your primary device and copy token.json to the server.
  • In Google Cloud Console "Testing" mode the refresh token expires after 7 days. Either move the project to "Production" (no review needed for the calendar.readonly scope on a personal account) or re-auth weekly.
  • If the token can no longer refresh, the daemon does not crash — the dashboard's calendar column renders a Calendar auth expired — run \retheia auth`` message until you re-run auth.

Run

# pick a screen the next wake should render
uv run retheia screen dashboard      # or calendar | http-cat

# start the BLE scan daemon
uv run retheia run --city Warsaw --country PL

# render a screen to a PNG and open it in the desktop image viewer
uv run retheia preview dashboard --city Warsaw --country PL

# render without opening (CI / iteration)
uv run retheia preview calendar --no-open

The preview pipeline runs the same render + dither chain as the daemon (via opendisplay.prepare_image), so the PNG matches what the panel will actually show.

BLE / BlueZ on Linux

py-opendisplay uses bleak on top of BlueZ over D-Bus. The user running the daemon must be able to talk to BlueZ — easiest is to add them to the bluetooth group:

sudo usermod -aG bluetooth $USER
# log out / back in

Sanity check the adapter with bluetoothctl scan on.

How a wake is processed

  1. BleakScanner runs continuously with manufacturer_data filtering. When the reTerminal advertises, the callback parses it with opendisplay.parse_advertisement and pushes the latest advertisement into a 1-slot async queue (older queued items are dropped — only the most recent wake is processed).
  2. A worker reads the active screen from the state file, fetches the necessary data concurrently (weather + NBP + GCal), composes a 800×460 body image, overlays the 20 px status bar, and uploads via OpenDisplayDevice.upload_image(..., fit=FitMode.CONTAIN). The library handles 4-grayscale dithering.
  3. Duplicate advertisements within the same wake (matching mac + loop_counter) are ignored.
  4. If an upload takes longer than 15s, a warning is logged — the device's awake window is short and a failed upload costs 6 h of staleness.

Installing the OpenDisplay firmware

To install the OpenDisplay firmware, make sure you use a Chromium-based browser. Firefox does not support the required WebUSB and Bluetooth APIs.

If you get a failed to execute 'open' on 'SerialPort': Failed to open serial port. error on Linux, you may need to run the following command to set the correct ACL on the serial port:

sudo setfacl -m u:$USER:rw /dev/ttyUSB0