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.
This commit is contained in:
commit
b078b850f8
9 files changed
+1039
No files matched your search
+151
@@ -0,0 +1,151 @@
|
||||
"""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,))
|
||||
Reference in new issue
Block a user