Statistics¶
Eneru writes UPS history to SQLite. The TUI graphs, event panel, notification retry queue, and offline troubleshooting all use the same per-UPS database files.
Statistics are always enabled. If the database cannot be opened or written, Eneru logs the problem and keeps monitoring.
Storage path¶
statistics:
db_directory: "/var/lib/eneru"
retention:
raw_hours: 24
agg_5min_days: 30
agg_hourly_days: 1825
The package creates /var/lib/eneru. PyPI installs create it on first daemon start if permissions allow.
Each UPS gets its own .db file named from the sanitized UPS label.
Write path¶
UPS poll loop
|
v
append sample to in-memory buffer
|
v
StatsWriter flushes every ~10 seconds
|
v
SQLite samples table
|
v
5-minute and hourly aggregates
Polls do not wait on SQLite writes. The writer batches rows in the background. Event rows are inserted directly, but failures are isolated from the safety-critical monitor path.
Stats write timeline¶
These intervals come from StatsWriter defaults in src/eneru/stats.py: flush_interval=10.0 and maintenance_interval=300.0.
| Time | Action | Result |
|---|---|---|
| Every poll | Monitor appends sample to memory | No SQLite work runs on the hot path |
| About every 10s | Writer flushes buffered samples | One batched SQLite transaction persists recent samples |
| Every 300s | Writer aggregates raw rows | agg_5min receives new buckets |
| Every 300s | Writer rolls 5-minute buckets | agg_hourly receives hourly buckets |
| Every 300s | Retention runs | Old raw and aggregate rows are purged by tier |
| Any SQLite failure | Error is logged and rate-limited | Monitoring continues without stats persistence |
Tables¶
| Table | Purpose |
|---|---|
samples |
Raw poll samples, typically 1 Hz |
agg_5min |
Five-minute aggregate buckets |
agg_hourly |
Hourly aggregate buckets |
events |
Power, self-test power transitions, health, lifecycle, slow-response diagnostics, remote-health transition, and shutdown events. Each row has an id (INTEGER PRIMARY KEY AUTOINCREMENT, schema v5) that the API uses to target individual events for deletion; the id is never reused, so deletion is always safe |
notifications |
Persistent notification queue and delivery history |
battery_health |
One row per periodic battery-health computation (schema v7): composite score plus per-term sub-scores, feeding replacement-prediction trending |
self_tests |
UPS self-test results (schema v7): one row per issued or device-observed test with the normalized result enum and the raw ups.test.result |
meta |
Schema version and lifecycle metadata |
The main sample metrics include status, battery charge, runtime, load, input/output voltage, battery voltage, temperature, frequency, real power and nominal power (schema v7, for energy tracking), depletion rate, time on battery, and connection state.
Self-test state that must survive a restart lives in meta: the active test id
and next poll time, notification deduplication markers, and the hard-failure
latch. On first startup with this behavior, Eneru also performs one conservative
history repair. It relabels only a closed ON_BATTERY / POWER_RESTORED pair
lasting at most 120 seconds when exactly one Eneru-issued self-test began
between 30 seconds before the transfer and 30 seconds after restoration.
Device-observed rows are excluded because their observation timestamp may not
be the test's real start time. Aborted results are also excluded because they do
not establish with enough confidence that the test caused the transfer. Long,
open, or ambiguous outages remain ordinary power events. The
self_test_event_repair_v1 marker prevents repeat scans.
Retention¶
| Tier | Default retention | Use |
|---|---|---|
| Raw samples | 24 hours | Detailed incident review |
| Five-minute aggregates | 30 days | Recent trends |
| Hourly aggregates | 5 years | Long-term battery and load history |
Raw rows are deleted after they have been aggregated. Aggregates have their own retention windows.
Disk usage¶
At a 1-second poll interval, expect roughly 15 to 20 MB per UPS at the default retention settings, plus small growth from events and notification rows.
For SD-card systems, the write pattern is a small transaction about every 10 seconds per UPS. That is usually less write pressure than normal system logging, but you can move the database to better storage:
Make sure the daemon user can write there.
Inspect data¶
List databases:
Show tables:
Recent samples:
sqlite3 /var/lib/eneru/UPS-192-168-1-100.db <<'SQL'
.headers on
.mode column
SELECT datetime(ts, 'unixepoch') AS time,
status,
battery_charge,
battery_runtime,
ups_load,
input_voltage
FROM samples
ORDER BY ts DESC
LIMIT 20;
SQL
Recent events:
sqlite3 /var/lib/eneru/UPS-192-168-1-100.db \
"SELECT datetime(ts, 'unixepoch'), event_type, detail FROM events ORDER BY ts DESC LIMIT 20;"
Slow NUT and remote SSH probes are stored as diagnostics events
(SLOW_NUT_RESPONSE and REMOTE_SSH_SLOW_RESPONSE). They show in the TUI
and /api/v1/events when event verbosity includes Diagnostics (-v /
verbosity=1).
Muted events:
sqlite3 /var/lib/eneru/UPS-192-168-1-100.db \
"SELECT event_type, COUNT(*) FROM events WHERE notification_sent = 0 GROUP BY event_type;"
Pending notifications:
sqlite3 /var/lib/eneru/UPS-192-168-1-100.db \
"SELECT id, attempts, next_attempt_at, title FROM notifications WHERE status='pending';"
TUI graphs¶
eneru monitor reads the stats database read-only. Graphs select the smallest table that covers the requested time range:
| Window | Table | Resolution |
|---|---|---|
| Up to 24 hours | samples |
Per poll |
| Up to 30 days | agg_5min |
Five-minute buckets |
| More than 30 days | agg_hourly |
Hourly buckets |
See Monitor and graphs for TUI usage.
Backup¶
SQLite supports online backup without stopping Eneru:
Schema migrations¶
Eneru stores the schema version in meta.schema_version. New releases migrate existing databases with additive ALTER TABLE statements and preserve old rows. When a column cannot be added in place, the table is rebuilt inside the same transaction. Schema v5 does this for events.id INTEGER PRIMARY KEY AUTOINCREMENT, which SQLite cannot ALTER ADD; existing rows keep their identity as id = old rowid, so the migration is still atomic and replay-safe. Schema v7 adds the real_power / power_nominal sample columns (with matching *_avg on the aggregate tables) plus the battery_health and self_tests tables; all additive, so existing rows are untouched.
Check the version:
sqlite3 /var/lib/eneru/UPS-192-168-1-100.db \
"SELECT key, value FROM meta WHERE key='schema_version';"
Migrations are designed to be replay-safe. If a daemon crashes mid-migration, the next start retries before bumping the schema version.
Failure behavior¶
Stats failures do not stop monitoring or shutdown. You may see logs like:
Fix the storage problem, then restart Eneru so it can reopen the database.