Small Python service that renders dashboard images and uploads them to OpenEPaperLink tags through the AP /imgupload endpoint.
The first scaffold includes:
- Docker image and Compose service
- YAML config loading
- OpenEPaperLink image upload client
- CalDAV calendar/task fetching for Nextcloud and other CalDAV servers
- RSS/Atom feed fetching
- Train departure fetching through RMV HAPI
- Placeholder data fallback
- Pillow-based 7.5 inch dashboard renderer
- Change-hash check to avoid uploading identical images
-
Edit
config/config.yamlwith your AP IP and tag MAC. -
Export credentials if you enable CalDAV:
export NEXTCLOUD_USERNAME="your-user" export NEXTCLOUD_APP_PASSWORD="your-app-password"
You can also copy
.env.exampleto.env; Docker Compose will read it automatically. -
Build and run:
docker compose up --build
Generated images and upload state are stored in ./data.
To render once without uploading to the AP:
docker compose --profile debug up --build debugThe rendered image will be written to ./data/<tag-name>.jpg.
By default, the service uploads only when dashboard content changes:
efficiency:
upload_only_on_content_change: true
skip_upload_on_source_error: trueThe content hash is based on calendar events, tasks, departures, and news. Render-only values such as the clock and tag battery status are intentionally ignored, so they do not wake tags by themselves.
Dashboard metadata that changes the actual layout, such as dashboard name, title, dimensions, and layout mode, is part of the hash. This means switching a tag from an overview dashboard to a transportation dashboard triggers one upload even if the source data itself did not change.
When skip_upload_on_source_error is enabled, production mode renders an error image into ./data but does not send it to the tag. Debug mode still renders the problem panel locally.
Battery status is read from the AP tag database endpoint and rendered in the header when enabled:
dashboards:
overview:
rendering:
show_battery: trueOpenEPaperLink LUT behavior can be set per tag:
tags:
- name: hallway
lut: ""Leave lut empty unless you have confirmed a mode that works well with your exact tag type. Common UI/API modes include default, no-repeat, fast-no-reds, and fast.
OpenEPaperLink also has lower-level diff image and LUT concepts, but for this container's /imgupload workflow the reliable battery saver is avoiding uploads entirely when content is unchanged. For display refresh behavior, configure the tag's LUT in the OpenEPaperLink tag settings or the per-tag lut field above.
config/ Example and local configuration
data/ Runtime output/cache directory
src/epaper_dashboard/ Python package
sources/ Calendar, task, transit, and RSS source adapters
config.py YAML/env config loading
models.py Shared data models
render.py Pillow dashboard rendering
upload.py OpenEPaperLink /imgupload client
main.py Scheduler loop and orchestration
- Set
sources.caldav.enabled: trueafter adding valid Nextcloud credentials. - Leave
discover: truefor the first run; usecalendar_urlsandtask_urlslater if you want explicit collection selection. - Set
sources.rss.enabled: trueand add feed URLs for news. - Set
sources.transit.enabled: trueand add RMV HAPI station IDs. - Tune the renderer to the exact detected Nebular resolution.
Create an RMV HAPI access ID and put it in .env:
RMV_ACCESS_ID=your-rmv-hapi-access-idShow all departures from one station:
sources:
transit:
enabled: true
provider: "rmv_hapi"
mode: "departure_board"
src_station_id: "3000010"
all_departures: true
excluded_products: ["ICE", "IC", "EC"]Show departures from one station only toward another station/direction:
sources:
transit:
enabled: true
provider: "rmv_hapi"
mode: "departure_board"
src_station_id: "3004801"
direction: "3000010"
all_departures: false
included_products: ["S", "RB", "RE"]Show route options from source to destination:
sources:
transit:
enabled: true
provider: "rmv_hapi"
mode: "trip"
src_station_id: "3004801"
dst_station_id: "3000010"
excluded_products: ["ICE", "IC", "EC"]For RMV trip mode the service sends originExtId and destExtId by default. Plain station IDs like 3000912 are padded automatically to RMV's expected external ID form, e.g. 003000912.
For a dedicated grouped transport dashboard, point a tag at the transportation dashboard and configure multiple directions:
tags:
- name: transport
dashboard: "transportation"
dashboards:
transportation:
title: "Transit"
layout: "transportation"
sections:
transit:
max_items: 12
directions:
- name: "FfM Hbf -> Darmstadt Hbf"
mode: "trip"
src_station_id: "..."
src_label: "FfM Hbf"
dst_station_id: "..."
dst_label: "Darmstadt Hbf"
max_items: 2
excluded_products: ["ICE", "IC", "EC"]
- name: "FfM Hbf -> Offenbach Marktplatz"
mode: "trip"
src_station_id: "..."
src_label: "FfM Hbf"
dst_station_id: "..."
dst_label: "Offenbach Marktplatz"
max_items: 2
- name: "Willy-Brandt-Platz -> Suedbahnhof"
mode: "trip"
src_station_id: "..."
src_label: "FfM Willy-Brandt-Platz"
dst_station_id: "..."
dst_label: "FfM Suedbahnhof"
max_items: 2
sources:
transit:
enabled: true
provider: "rmv_hapi"
access_id: "${RMV_ACCESS_ID}"The renderer keeps the configured order, so put directions from the same source station next to each other. Directions can also live under sources.transit.directions if you want one global transport setup, but dashboard-local directions are clearer once you have multiple dashboards.
For a compact transport-only config, this alias is also supported:
dashboards:
oeffis:
title: "Oeffis"
sections:
directions:
enabled: true
max_items: 10
sources:
transit:
enabled: false
provider: "rmv_hapi"
access_id: "${RMV_ACCESS_ID}"
directions:
- name: "FfM Sued -> Darmstadt Hbf"
mode: "trip"
src_station_id: "3000912"
dst_station_id: "3004734"
max_items: 2Use RMV's location.name endpoint to look up station IDs:
https://www.rmv.de/hapi/location.name?input=Frankfurt%20Hauptbahnhof&format=json&accessId=...