diff --git a/docs/architecture/subsystems/ups.md b/docs/architecture/subsystems/ups.md new file mode 100644 index 0000000..0cedf8b --- /dev/null +++ b/docs/architecture/subsystems/ups.md @@ -0,0 +1,42 @@ +# ups + +## Purpose + +Polls a local NUT (Network UPS Tools) daemon for battery status and publishes power state transitions onto the MQTT bus. When the UPS runs below a configured critical-runtime threshold, triggers a graceful shutdown of the host. + +## Key files + +- `vigilar/ups/monitor.py` — `UPSMonitor`: NUT client, status parser, transition detection, MQTT publisher +- `vigilar/ups/shutdown.py` — `ShutdownSequence`: publishes `SYSTEM_SHUTDOWN` and executes the OS shutdown +- `vigilar/ups/__init__.py` + +## MQTT topics + +**Subscribes:** none +**Publishes:** +- `vigilar/ups/status` (every poll; status, battery charge %, runtime seconds, input voltage, load %) +- `vigilar/ups/power_loss` (transition `OL -> OB`/`LB`) +- `vigilar/ups/restored` (transition back to `OL`) +- `vigilar/ups/low_battery` (charge below `low_battery_threshold_pct`) +- `vigilar/ups/critical` (runtime below `critical_runtime_threshold_s`, triggers shutdown) +- `vigilar/system/shutdown` (published by `ShutdownSequence`) + +## Database tables + +- `events` — writes `POWER_LOSS`, `POWER_RESTORED`, `LOW_BATTERY` rows via `insert_event` +- `system_events` — writes operator-facing notices (`"Power loss detected — running on battery"`, critical shutdown messages) + +## Depends on + +- External NUT daemon reachable at `[ups] nut_host:nut_port` via the `pynut2` client +- `storage` — `insert_event`, `insert_system_event` + +## Consumed by + +- `events` — classifies `vigilar/ups/*` into `POWER_LOSS` / `LOW_BATTERY` / `POWER_RESTORED` +- `alerts` — receives those classified events through rule actions +- `main` supervisor — reacts to `SYSTEM_SHUTDOWN` so all subsystems can drain cleanly + +## Notes + +The critical shutdown trigger fires when the UPS is not online AND `battery.runtime < ups.critical_runtime_threshold_s` AND no shutdown has already been triggered. The low-battery alert fires when `battery.charge < ups.low_battery_threshold_pct` while off mains. NUT reconnection uses exponential backoff capped at 120 seconds, so the monitor survives a temporary NUT restart without the supervisor having to restart it.