ADR-010: NUKI Battery Monitoring and Usage Reporting¶
Status: Accepted Date: 2026-03-14
Context¶
The NUKI Smart Lock is battery-powered (4x AA). A dead battery means nobody can enter the hackerspace. We need early warning for admins without spamming them with repeated alerts.
The ESP32 already has a 5-minute sync cycle with heartbeat and event reporting to the backend. BLE connections to the NUKI are expensive (time, radio contention, power), so we should minimize unnecessary BLE connections. However, the door may go unused for days (weekends, holidays), so we cannot rely solely on lock actions for fresh battery data.
Decision¶
Piggyback battery queries on lock actions¶
After each successful execute() call in NukiSmartLock, query requestBatteryReport() on the same BLE connection. This gives fresh battery readings at zero extra connection cost. A typical hackerspace sees 20-50 accesses per day, so battery data stays current during active use.
Daily 3am BLE poll¶
Every day at 03:00, the ESP32 makes one standalone BLE connection (queryBattery()) to get fresh battery data, regardless of whether any lock actions occurred that day. This ensures battery data is never more than 24 hours stale, even on quiet days.
The daily report transmits:
- Fresh battery percent and critical flag
- Accumulated daily counters: unlatch count, lock count, total actions
- Sent as a
nuki_daily_reportaudit event
Battery impact: 1 extra BLE connection per day. Negligible compared to daily lock usage, and the only connection on quiet days.
Three data channels¶
| Channel | Frequency | Data | Purpose |
|---|---|---|---|
| Heartbeat | Every 5 min | nuki_battery_percent, nuki_battery_critical, nuki_paired, nuki_state (cached) |
Continuous monitoring, dashboard |
| Lock event | Per access | battery_percent in event details |
Correlate battery drain with usage |
| Daily summary | Once/day at 3am | Battery + action counts (nuki_daily_report event) |
Trend graphs, usage analytics |
The heartbeat sends cached values over Ethernet/WiFi — no BLE connection. Lock events piggyback on existing BLE. Only the daily summary adds one BLE connection.
Low-battery alert with hysteresis¶
A nuki_battery_low event fires once when battery drops below 20%. The alert does not repeat until battery recovers above 25% (e.g., batteries replaced but not fully charged) and then drops below 20% again. This prevents repeated alerts from readings that fluctuate around the threshold.
BatteryMonitor as pure logic class¶
BatteryMonitor is a standalone class with no hardware dependencies:
- Tracks action counters (unlatch, lock) in RAM
- Caches last battery reading (updated by both lock actions and standalone queries)
- Manages low-battery alert state with hysteresis
- Tracks daily report scheduling by day number
updateBattery() updates the cache without incrementing action counters (used by the 3am standalone query). recordAction() updates both the cache and counters (used after lock actions).
Fully testable on the native platform. Counters reset after each daily report. If the ESP32 reboots mid-day, partial counts are lost — acceptable for a trend graph.
Monitoring endpoints¶
Two unauthenticated endpoints for external monitoring systems:
GET /api/v1/monitoring/nuki-battery— Nagios plugin format with perfdata. Configurable WARN/CRIT thresholds. Compatible with Nagios, Icinga2, Checkmk, Zabbix.GET /api/v1/monitoring/metrics— Prometheus exposition format. Exposes device health, NUKI battery, and daily usage counters as gauges.
See docs/monitoring.md for setup guides covering Icinga2, Nagios, Checkmk, Zabbix, Prometheus+Grafana, and Uptime Kuma.
Backend storage¶
NUKI fields are optional additions to the existing DeviceHeartbeat schema. All data flows into audit_log.details JSON — no schema migration needed. New event types (nuki_daily_report, nuki_battery_low) are just strings handled by the existing audit log.
Alternatives considered¶
No daily BLE poll (piggyback-only): Simpler, but battery data goes stale on quiet days. A weekend without visitors means 48+ hours of stale readings. The daily 3am poll adds negligible drain (1 connection/day) for guaranteed freshness.
Query battery on every heartbeat (every 5 min): ~288 BLE connections/day. Excessive NUKI battery drain and BLE radio contention. Rejected.
Persistent counters in NVS: Saving action counts to flash (NVS) would survive reboots, but adds flash wear for data that's purely informational. RAM counters with occasional loss are fine for trend graphs.
Configurable thresholds via backend: Would require a settings sync protocol. Compile-time constants (20%/25%) are sufficient for now. Can be made configurable later if needed.
Push-based monitoring (email/webhook on low battery): Doesn't integrate with existing monitoring infrastructure. Can be added later as a complement.
Consequences¶
- Admins get exactly one alert when battery is low — no spam
- Battery data is never more than 24 hours stale, even on quiet days
- Battery and usage data accumulates in the audit log for Grafana graphs
- Only 1 extra BLE connection per day (3am poll); all other battery queries are free
- BatteryMonitor is testable on native (19 dedicated tests)
- Backend schema change is additive (optional fields) — no migration needed
- Monitoring works with 6 different systems out of the box