Move secrets and URLs out of source into .env
All checks were successful
lint / ruff (push) Successful in 45s

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>
This commit is contained in:
Andras Schmelczer 2026-05-03 11:22:54 +01:00
parent 8609b4a884
commit 90b692da39
8 changed files with 69 additions and 19 deletions

4
.env.example Normal file
View file

@ -0,0 +1,4 @@
IMMICH_URL=https://immich.example.com
IMMICH_API_KEY=your-immich-api-key
HA_URL=https://homeassistant.example.com
HA_TOKEN=your-home-assistant-long-lived-token

2
.gitignore vendored
View file

@ -5,3 +5,5 @@ __pycache__/
*.egg-info/
.ipynb_checkpoints/
photo_history.json
.env

View file

@ -62,7 +62,7 @@ former `dither_test/`):
- **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 environment variables**: `IMMICH_URL`, `IMMICH_API_KEY`, `HA_URL`, `HA_TOKEN` (with hardcoded defaults in display.py)
- **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/.env``sync.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

View file

@ -9,7 +9,6 @@ from __future__ import annotations
import contextlib
import io
import os
import random
import sys
import tempfile
@ -21,8 +20,6 @@ REPO = Path(__file__).resolve().parent.parent
CACHE_DIR = Path(tempfile.gettempdir()) / "frame_notebook"
DEFAULT_PEOPLE = ("Me", "Ruby")
DEFAULT_IMMICH_URL = "https://immich.example.com"
DEFAULT_IMMICH_API_KEY = "REDACTED_IMMICH_API_KEY"
def bootstrap() -> None:
@ -32,15 +29,16 @@ def bootstrap() -> None:
if sp not in sys.path:
sys.path.insert(0, sp)
sys.modules.setdefault("waveshare_epd.epdconfig", ModuleType("waveshare_epd.epdconfig"))
from env import load_env
load_env()
def immich_client():
from env import require
from immich import ImmichClient
return ImmichClient(
os.environ.get("IMMICH_URL", DEFAULT_IMMICH_URL),
os.environ.get("IMMICH_API_KEY", DEFAULT_IMMICH_API_KEY),
)
return ImmichClient(require("IMMICH_URL"), require("IMMICH_API_KEY"))
def is_landscape(asset: dict) -> bool:

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python3
import argparse
import fcntl
import os
import sys
from datetime import datetime
from pathlib import Path
@ -10,6 +9,7 @@ from PIL import Image
sys.path.append(str(Path(__file__).parent / "lib"))
from crop import face_aware_crop
from env import load_env, require
from homeassistant import HomeAssistantClient
from immich import ImmichClient, get_random_photo_from_album, get_random_photo_of_people
from overlay import format_age, format_location
@ -18,14 +18,11 @@ from overlay import format_age, format_location
# GPIO pins at import time, so two overlapping invocations would both crash
# on "GPIO busy" before reaching the flock below.
IMMICH_URL = os.environ.get("IMMICH_URL", "https://immich.example.com")
IMMICH_API_KEY = os.environ.get("IMMICH_API_KEY", "REDACTED_IMMICH_API_KEY")
HA_URL = os.environ.get("HA_URL", "https://homeassistant.example.com")
HA_TOKEN = os.environ.get(
"HA_TOKEN",
"REDACTED_HA_TOKEN",
)
load_env()
IMMICH_URL = require("IMMICH_URL")
IMMICH_API_KEY = require("IMMICH_API_KEY")
HA_URL = require("HA_URL")
HA_TOKEN = require("HA_TOKEN")
HA_PRESENCE = {"Andras": "person.andras", "Ruby": "person.ruby"}

43
src/lib/env.py Normal file
View file

@ -0,0 +1,43 @@
"""Minimal stdlib .env loader.
Reads KEY=VALUE lines from a .env file (project root by default) into
`os.environ`, leaving already-set variables untouched.
"""
from __future__ import annotations
import os
from pathlib import Path
def _find_env() -> Path | None:
# Walk up from this file and from cwd; first .env wins. Handles both the
# dev layout (src/lib/env.py with .env at repo root) and the Pi layout
# (lib/env.py with .env at ~/frame/.env).
for start in (Path(__file__).resolve().parent, Path.cwd().resolve()):
for d in (start, *start.parents):
candidate = d / ".env"
if candidate.is_file():
return candidate
return None
def load_env(path: Path | None = None) -> None:
env_path = path or _find_env()
if env_path is None or not env_path.exists():
return
for raw in env_path.read_text().splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
key = key.strip()
value = value.strip().strip('"').strip("'")
os.environ.setdefault(key, value)
def require(key: str) -> str:
value = os.environ.get(key)
if not value:
raise RuntimeError(f"missing required env var: {key} (set it in .env or the environment)")
return value

View file

@ -4,7 +4,13 @@
# brcmfmac chip on the Pi Zero 2W.
CONNECTION="netplan-wlan0-HiddenPlace"
PROBE_HOST="homeassistant.example.com"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091
[ -f "$SCRIPT_DIR/.env" ] && set -a && . "$SCRIPT_DIR/.env" && set +a
PROBE_HOST="${HA_URL#http*://}"
PROBE_HOST="${PROBE_HOST%%/*}"
: "${PROBE_HOST:?HA_URL must be set in .env}"
probe() {
ping -c 1 -W 5 192.168.0.1 >/dev/null 2>&1 \

View file

@ -1,2 +1,2 @@
#!/bin/bash
rsync -avz --progress src/ andras@192.168.0.81:~/frame/
rsync -avz --progress --exclude=.env src/ andras@192.168.0.81:~/frame/