Files
truetype b078b850f8 Initial commit: ShellyDash dashboard fotovoltaico
Flask app con gauge Chart.js custom, polling Shelly Cloud API v2,
storico SQLite, due widget gauge (produzione/consumo) con scala graduata.
2026-07-14 20:38:18 +02:00

152 lines
6.1 KiB
Python

"""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,))