frame/CLAUDE.md
Andras Schmelczer 90b692da39
All checks were successful
lint / ruff (push) Successful in 45s
Move secrets and URLs out of source into .env
Adds a stdlib-only loader (src/lib/env.py) that walks up to find the
nearest .env. display.py and notebooks/_helpers.py now `require()` the
config values; wifi-check.sh sources .env to derive its probe host. The
.env file is gitignored; .env.example documents the required keys. The
existing tokens are still present in git history and will be scrubbed
in the next commit; rotate them after the rewrite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 11:22:54 +01:00

4.2 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What This Is

An e-ink photo frame that runs on a Raspberry Pi Zero 2W. It fetches photos from an Immich server, checks Home Assistant for presence (only displays when someone is home), and renders them on a Waveshare 7.3" 6-color e-Paper display (800x480, ACeP technology with Black/White/Yellow/Red/Blue/Green).

Deployment

./sync.sh          # rsync src/ to andras@192.168.0.81:~/frame/

On the Pi:

cd ~/frame
python3 display.py                          # default: photos of Me,Ruby
python3 display.py --album "Album Name"     # from specific album
python3 display.py -o 90                    # portrait mode (90° or 270°)
python3 display.py --saturation 1.5 --contrast 1.1 --gamma 0.85

Architecture

src/display.py — Entry point. Orchestrates the pipeline:

  1. Checks time (skips between midnight7am)
  2. Checks Home Assistant presence (skips if nobody home)
  3. Fetches a random photo from Immich (by people or album)
  4. Sends to e-ink display driver

src/lib/immich.py — Immich API client. Key behaviors:

  • _load_history() / _save_history() track displayed photos in photo_history.json to avoid repeats (resets after 7 days). Asset is only marked displayed after a successful download.
  • _pick_weighted_random() picks a pool first, then a uniform random asset from it. Weights: on-this-day 0.30 (or 0.10 for the ±3-day fallback), favorites 0.18, recent-30-days 0.36, all 0.36. Empty pools are dropped before sampling.
  • Filters photos by orientation (portrait/landscape) based on EXIF data including rotation tags. Raises if nothing matches the requested orientation.
  • Downloads preview-size thumbnails, not originals
  • Asset lists (people-search and album) are cached on disk in /tmp/frame_cache/ for 1 hour
  • urlopen calls retry transient failures twice (3s, 10s backoff)

src/lib/homeassistant.py — Simple Home Assistant REST client for presence detection.

src/lib/waveshare_epd/epd7in3e.py — Modified Waveshare driver. The getbuffer() method handles the full image pipeline:

  • Falls back to a face-less center crop via face_aware_crop if the input isn't already at target size
  • Enhances saturation/contrast/gamma for e-ink (caller passes values; CLI defaults live in display.py: saturation=1.3, contrast=1.05, gamma=0.90)
  • Atkinson dithering to 6-color palette using numba JIT; produces palette indices directly (no Pillow quantize round-trip)
  • Packs into 4-bit-per-pixel buffer (two pixels per byte) and returns bytes

src/lib/waveshare_epd/epdconfig.py — GPIO/SPI hardware config. Critical: PWR pin is BCM 27 (not default 18).

src/lib/progress.py — Simple terminal progress bar.

notebooks/ — Off-Pi observable comparisons covering pipeline stages. Run via the uv-managed env (uv run jupyter lab notebooks/...). The notebooks share _helpers.py (bootstrap, Immich client, pool fetch, image cache) and _dither.py (migrated from the former dither_test/):

  • crop_compare.ipynb — face-aware crop vs. centre crop on the most-divergent picks
  • dither_compare.ipynb — error-diffusion + ordered dithering algorithms with timing

Key Constraints

  • Always call epd.sleep() after display — the driver uses a try/finally pattern for this
  • Display refresh takes 12-15 seconds — the BUSY pin polling handles this
  • No test suite — this is a hardware project; test by deploying to the Pi
  • Dependencies on Pi: python3-pil python3-numba python3-smbus spidev gpiozero
  • Config via .env (gitignored): IMMICH_URL, IMMICH_API_KEY, HA_URL, HA_TOKEN. Loaded by src/lib/env.py (stdlib-only); require(key) raises if a value is missing. Copy .env.example to .env and fill in. The Pi keeps its own ~/frame/.envsync.sh excludes it.
  • Uses only stdlib urllib — no requests library; the Immich client uses urllib.request directly
  • Single-instance lock at /tmp/frame.lock (fcntl) — overlapping cron runs exit cleanly
  • sys.path.append is used to add lib/ to the path from display.py