Optocoupler-based UPS power monitor for a Proxmox homelab. Rebuild of the
v1 system (branch legacy) — v1 inferred mains state from
network reachability (TCP probe to 192.168.0.2) which produced constant fake
powercut alerts. v2 measures electricity directly on a GPIO; the network is
used only for actuation (shutdown webhook, WOL) and reporting (Telegram/ntfy).
Read UPGRADE_PLAN.md first — it is the single source
of truth for scope, phased TODOs, acceptance gates, and deployment automation.
Wall mains ──→ 5V USB adapter ──→ PC817 optocoupler ──→ ESP32 GPIO 13 (D13)
│
┌────────────────────┤
▼ ▼
Pi brain Telegram / ntfy
(reconciler, (notification)
webhook, PVE,
countdown cards)
- ESP32 (autonomy authority): GPIO 13 mains detection, event ledger, WOL/shutdown, WiFi BSSID-locked
- Pi (control plane, thin):
/state+/eventsreconciler, Telegram bot, ntfy fallback, PVE verification — never mutates state - Notifications: Telegram primary, ntfy fallback, info coalescing (90s window), live countdown cards
| Branch | Purpose |
|---|---|
main |
v2 rebuild — all work happens here |
legacy |
sanitized v1 snapshot: firmware main.cpp, Pi ups-monitor.py, README.md (v1 system doc) |
| Phase | Title | Status |
|---|---|---|
| 0 | Repo scaffold + deploy tooling | ✅ Done |
| 1 | Firmware v2 core (GPIO mains, event ledger, WOL/shutdown) | ✅ Done |
| 2 | Pi brain (reconciler, Telegram, ntfy, PVE) | ✅ Done — 3/3 drills passed |
| 3 | Optocoupler hardware bring-up | ✅ Done — wired, verified, 20/20 matrix passed |
| 4 | UX polish + observability | ✅ Done — pytest (78), /diag v2, single-voice verify, /missed + /history |
| 5 | Final validation | ✅ Done |
scripts/ sanitize.py (secret scrubber), inject.py (env -> code filler)
firmware/ ESP32 PlatformIO project (GPIO mains sense, WOL, shutdown agent)
pi/ Pi brain: Telegram bot, event reconciler, ntfy fallback, PVE
hardware/ Optocoupler wiring notes (PC817 + 5V adapter, isolated)
deploy/ deploy-pi.sh, ota-esp32.sh (inject creds from .env at push time)
tests/ Pi-side pytest (78 tests, runs in builder LXC)
Secrets live only in a git-ignored .env (see .env.example). Sources carry
__PLACEHOLDER__ tokens; deploy scripts fill them in RAM and push.
Cred-embedded code is never persisted on this machine (built in /dev/shm).
- Pi (control plane):
pi@192.168.0.169(wired LAN) - ESP32 (sensor/actuator):
192.168.0.178(OTA-enabled) - Proxmox node:
192.168.0.50(shutdown agent:9999, API:8006)
Available after typing / in the bot chat:
| Command | Description |
|---|---|
/status |
Live power & server status (live-edits every 5s during a power cut) |
/diag |
Technical diagnostics |
/on |
Wake the server |
/off |
Shut the server down |
/mainsdelay |
Set power-loss shutdown delay (1–720 min) |
/wantimeout |
Set internet-loss shutdown delay (5–120 min) |
/missed |
Show alerts that failed delivery |
/history |
Recent power/internet outages |
- Mains sensed by optocoupler on GPIO 13 (D13), active-LOW — never via network
- ESP32 is the autonomy authority (countdowns, shutdown, WOL); the Pi is control plane only
- Event flow: ESP32 event ledger (seq +
GET /events?since=N) → Pi reconciler; webhooks are fast-path nudges, ledger is authority - All secrets are
__PLACEHOLDER__tokens in source, injected at deploy time from.env - Run
python3 scripts/sanitize.pybefore every commit