A Home Assistant add-on that uses a headless Chromium browser (via Playwright) to screenshot a dashboard URL and serve the result as an e-ink optimised PNG over HTTP on port 3412.
- Python 3.11+ with
uvfor package management - Playwright (sync API) for headless Chromium screenshots
- Pillow + numpy for image processing
- croniter for cron schedule parsing
http.server.HTTPServerfor the HTTP endpoint (no framework)
src/epaper_display/
├── __main__.py # Entry point: HTTPServer, cron loop, cached state
├── capture.py # take_screenshot() — all Playwright/browser logic
├── image.py # process_image() — all Pillow/numpy image processing
└── config.py # load_options() — reads /data/options.json
Other key files:
config.yaml— Home Assistant add-on metadata and config schema (defines the HA UI options)translations/en.yaml— human-readable names and descriptions for every option inconfig.yaml; must be kept in sync whenever options are added, removed, or renamedbuild.yaml— multi-arch Docker base images (amd64,aarch64only — Playwright does not supportarmv7)pyproject.toml+uv.lock— Python dependencies; hatchling is the build backend
Order matters: gamma correction → greyscale → normalize → dither
process_image()inimage.pyowns all image manipulationtake_screenshot()incapture.pyreturns raw PNG bytes from the browser — no image processing_capture()in__main__.pycomposes both calls; used by both the cron loop and the direct HTTP handler
All options are read fresh from /data/options.json on every capture. Changes take effect after the current sleep/request cycle ends — no restart needed.
Releases are managed by release-please. Never manually bump the version in pyproject.toml or config.yaml, and never create tags by hand.
When feat or fix commits land on main, release-please opens a PR that bumps both files and generates CHANGELOG.md. Merging that PR creates the GitHub release and tag automatically.
Use Conventional Commits:
<type>(<optional scope>): <description>
Common types: feat, fix, chore, docs, refactor, test. Examples:
feat(capture): add sidebar hiding via localStorage
fix(image): correct gamma LUT length for RGB images
chore: regenerate uv.lock
docs: update README with hide_sidebar option
Client scripts live in examples/ and are named {device}_{display}.py, e.g. raspberry-pi_waveshare-epd7in5-v2.py. Each file must include a header comment stating the device, display model, resolution, and prerequisites. Scripts should fetch from ADDON_URL and handle IOError and KeyboardInterrupt — see the existing example as a template.
Build and run the add-on exactly as it runs in production using Docker:
# Build (uses the same base image as the real add-on)
# On Apple Silicon add --platform linux/amd64 to both commands
docker build \
--build-arg BUILD_FROM=ghcr.io/home-assistant/amd64-base-debian:bookworm \
-t epaper-display-dev .
# Run — mount a local options.json so the add-on picks up your config
docker run --rm -p 3412:3412 \
-v /path/to/options.json:/data/options.json \
epaper-display-devMinimal options.json:
{
"url": "http://10.4.0.100:8123/e-ink-dashboard/0",
"token": "<long-lived HA token>",
"cron": "*/15 * * * *",
"width": 800,
"height": 480,
"direct": true
}With direct: true the add-on captures on every request — no need to wait for a cron tick. Once running, fetch the screenshot with:
curl http://localhost:3412/screenshot.png -o /tmp/test.png && open /tmp/test.png- Always work on a feature branch — never commit directly to
main - Use
uvfor all dependency management — neverpipdirectly - Regenerate
uv.lockafter any change topyproject.toml - Keep
--no-sandboxand--disable-dev-shm-usagehardcoded intake_screenshot— they are required for Chromium in Docker and must not be removed - Do not add a web framework — the stdlib
HTTPServeris intentional for minimal footprint - Run with
python -m epaper_display