An async Python daemon that logs energy data from HomeWizard P1 meters via WebSocket, stores it in SQLite with 5-minute and 30-second aggregates, and provides a REST API for querying.
- Real-time WebSocket connection to HomeWizard P1 APIv2 with proper authentication
- Windowed aggregates: 5-minute blocks with weighted averages and maximums (indefinite storage), plus 30-second snapshots (24h rolling)
- Automatic gap filling: Detects outages and backfills missing blocks with interpolated energy data for accurate reporting
- Minimal REST API for querying meter data by timestamp range
- Async I/O throughout using
asyncioandaiosqlite - Command-line setup:
--hostand--tokenarguments with persistent storage in database - Automatic user registration: Button-click registration on P1 meter
- Clean logging: INFO for connection events/errors, DEBUG for normal data flow
- Python 3.13+
- HomeWizard P1 meter on your network
pip install -r requirements.txt
python run_daemon.py --host <P1_IP> --token <API_TOKEN>Or register automatically:
python run_daemon.py --host <P1_IP>
# Press the registration button on the meter when promptedThe daemon stores data in meter_data.db:
- Energy:
timestamp,tariff,import_kwh,export_kwh,import_t1_kwh,import_t2_kwh - Power:
w(total),l1_w,l2_w,l3_w(phase watts) - Voltage:
l1_v,l2_v,l3_v - Aggregates:
*_avg(1 decimal),*_max - Metadata:
reading_count(0 = synthetic/backfilled block)
Same schema as meter_data, rotates after 24 hours.
Query meter data:
GET http://localhost:8080/api/v1/meter?start=2026-01-13T14:00:00&end=2026-01-13T15:00:00- WebSocket stream → Real-time measurements from P1 meter (~1/sec)
- 30-second boundaries → Calculate weighted averages, store snapshot
- 5-minute boundaries → Calculate weighted averages from accumulated readings, store permanently
- Hourly cleanup → Remove 30-second data older than 24 hours
- Gap detection → On startup, detect outages and backfill with synthetic blocks
- Energy interpolation → When first real reading arrives after gap, interpolate actual consumption backwards into synthetic blocks
Each reading is weighted by its duration until the next reading. This smooths out measurement intervals and provides realistic average power consumption.
If the daemon was offline for 1-30 minutes:
- Creates synthetic 5-minute blocks with last-known energy values
- On first real reading, calculates actual energy consumed during gap
- Interpolates consumption proportionally across synthetic blocks
- Marks synthetic blocks with
reading_count=0for identification
P1 device host and API token are stored in SQLite config table after first setup:
# Setup with command line
python run_daemon.py --host 192.168.1.100 --token <token>
# Use stored config (no args needed)
python run_daemon.py- Python 3.13+ with asyncio
- websockets 15.0.1 - WebSocket client with HomeWizard auth protocol
- aiohttp 3.11+ - REST API server
- aiosqlite 0.18+ - Async SQLite operations
# Validate syntax
python -m py_compile hwLogger/*.py
# Run with verbose logging
LOGLEVEL=DEBUG python run_daemon.py
# Query data directly
sqlite3 meter_data.db "SELECT timestamp, w_avg, w_max FROM meter_data LIMIT 10;"22 columns in both meter_data and meter_data_30s:
| Column | Type | NOT NULL | Notes |
|---|---|---|---|
| timestamp | TEXT | ✓ | Window-end time (ISO8601) |
| tariff | INT | Current tariff (1 or 2) | |
| import_kwh | REAL | ✓ | Total import energy (kWh) |
| import_t1_kwh | REAL | ✓ | Tariff 1 import (kWh) |
| import_t2_kwh | REAL | ✓ | Tariff 2 import (kWh) |
| export_kwh | REAL | ✓ | Total export energy (kWh) |
| w | REAL | ✓ | Current power (W) |
| l1_w, l2_w, l3_w | REAL | Phase power (W) | |
| l1_v, l2_v, l3_v | REAL | Phase voltage (V) | |
| w_avg, l1_w_avg, l2_w_avg, l3_w_avg | REAL | Weighted average power (1 decimal) | |
| w_max, l1_w_max, l2_w_max, l3_w_max | REAL | Max power (2 decimals) | |
| reading_count | INT | ✓ | Samples in block (0 = synthetic) |
See LICENSE file.