"""SQLite database manager for ShellyDash.""" from __future__ import annotations import sqlite3 import time from datetime import datetime, timezone from pathlib import Path DB_PATH = Path(__file__).parent / "data" / "shellydash.db" class Database: """Manage readings storage and queries.""" def __init__(self, db_path: str | Path = DB_PATH): self.db_path = Path(db_path) self.db_path.parent.mkdir(parents=True, exist_ok=True) self._init_schema() def _connect(self) -> sqlite3.Connection: conn = sqlite3.connect(str(self.db_path)) conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode=WAL") return conn def _init_schema(self): with self._connect() as conn: conn.executescript(""" CREATE TABLE IF NOT EXISTS readings ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER NOT NULL, -- unix timestamp ch0_act_power REAL, ch0_aprt_power REAL, ch0_current REAL, ch0_voltage REAL, ch0_pf REAL, ch0_total_act_energy REAL, ch0_total_ret_energy REAL, ch1_act_power REAL, ch1_aprt_power REAL, ch1_current REAL, ch1_voltage REAL, ch1_pf REAL, ch1_total_act_energy REAL, ch1_total_ret_energy REAL, switch_output INTEGER, temperature REAL, wifi_rssi INTEGER, online INTEGER, uptime INTEGER ); CREATE INDEX IF NOT EXISTS idx_readings_ts ON readings(ts); """) def insert_reading(self, r: dict) -> int | None: ts = int(r.get("unixtime") or time.time()) with self._connect() as conn: cur = conn.execute( """INSERT INTO readings ( ts, ch0_act_power, ch0_aprt_power, ch0_current, ch0_voltage, ch0_pf, ch0_total_act_energy, ch0_total_ret_energy, ch1_act_power, ch1_aprt_power, ch1_current, ch1_voltage, ch1_pf, ch1_total_act_energy, ch1_total_ret_energy, switch_output, temperature, wifi_rssi, online, uptime ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", ( ts, r.get("ch0_act_power"), r.get("ch0_aprt_power"), r.get("ch0_current"), r.get("ch0_voltage"), r.get("ch0_pf"), r.get("ch0_total_act_energy"), r.get("ch0_total_ret_energy"), r.get("ch1_act_power"), r.get("ch1_aprt_power"), r.get("ch1_current"), r.get("ch1_voltage"), r.get("ch1_pf"), r.get("ch1_total_act_energy"), r.get("ch1_total_ret_energy"), 1 if r.get("switch_output") else 0, r.get("temperature"), r.get("wifi_rssi"), 1 if r.get("online") else 0, r.get("uptime"), ), ) return cur.lastrowid def get_latest(self) -> dict | None: """Return the most recent reading.""" with self._connect() as conn: row = conn.execute( "SELECT * FROM readings ORDER BY ts DESC LIMIT 1" ).fetchone() return dict(row) if row else None def get_history(self, hours: int = 24) -> list[dict]: """Return readings for the last N hours, sampled ~ every 60s.""" since = int(time.time()) - hours * 3600 with self._connect() as conn: rows = conn.execute( """SELECT * FROM readings WHERE ts >= ? ORDER BY ts ASC""", (since,), ).fetchall() return [dict(r) for r in rows] def get_energy_today(self) -> dict: """Return total energy produced/consumed today.""" t = datetime.now(timezone.utc) start_of_day = int(t.replace(hour=0, minute=0, second=0, microsecond=0).timestamp()) with self._connect() as conn: row = conn.execute( """SELECT MAX(ch0_total_act_energy) - MIN(ch0_total_act_energy) AS ch0_energy, MAX(ch0_total_ret_energy) - MIN(ch0_total_ret_energy) AS ch0_ret, MAX(ch1_total_act_energy) - MIN(ch1_total_act_energy) AS ch1_energy, MAX(ch1_total_ret_energy) - MIN(ch1_total_ret_energy) AS ch1_ret FROM readings WHERE ts >= ?""", (start_of_day,), ).fetchone() return dict(row) if row else {} def get_monthly_energy(self) -> list[dict]: """Return daily energy sums for the current month.""" t = datetime.now(timezone.utc) start_of_month = int(t.replace(day=1, hour=0, minute=0, second=0, microsecond=0).timestamp()) with self._connect() as conn: rows = conn.execute( """SELECT date(ts, 'unixepoch') AS day, MAX(ch0_total_act_energy) - MIN(ch0_total_act_energy) AS ch0_prod, MAX(ch1_total_act_energy) - MIN(ch1_total_act_energy) AS ch1_cons FROM readings WHERE ts >= ? GROUP BY day ORDER BY day ASC""", (start_of_month,), ).fetchall() return [dict(r) for r in rows] def cleanup_old(self, days: int = 365): """Delete readings older than N days.""" cutoff = int(time.time()) - days * 86400 with self._connect() as conn: conn.execute("DELETE FROM readings WHERE ts < ?", (cutoff,))