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
@@ -0,0 +1,104 @@
|
||||
"""Shelly Cloud API client."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class ShellyError(Exception):
|
||||
"""Shelly API error."""
|
||||
|
||||
|
||||
class ShellyClient:
|
||||
"""Client for Shelly Cloud Control API v2."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
server: str,
|
||||
auth_key: str,
|
||||
device_id: str,
|
||||
timeout: float = 10,
|
||||
):
|
||||
self.server = server.rstrip("/")
|
||||
self.auth_key = auth_key
|
||||
self.device_id = device_id
|
||||
self.timeout = timeout
|
||||
self._session = requests.Session()
|
||||
|
||||
def _post(self, path: str, body: dict) -> Any:
|
||||
"""POST JSON to API endpoint with auth_key as query param."""
|
||||
url = f"{self.server}/v2/devices/api/{path}"
|
||||
params = {"auth_key": self.auth_key}
|
||||
try:
|
||||
resp = self._session.post(
|
||||
url, json=body, params=params, timeout=self.timeout
|
||||
)
|
||||
except requests.exceptions.ConnectionError as exc:
|
||||
raise ShellyError(f"Connection error: {exc}") from exc
|
||||
except requests.exceptions.Timeout as exc:
|
||||
raise ShellyError(f"Timeout: {exc}") from exc
|
||||
|
||||
if resp.status_code == 429:
|
||||
time.sleep(2)
|
||||
return self._post(path, body)
|
||||
|
||||
if resp.status_code == 401:
|
||||
raise ShellyError("Authentication failed — check auth_key")
|
||||
|
||||
if not resp.ok:
|
||||
raise ShellyError(f"HTTP {resp.status_code}: {resp.text[:200]}")
|
||||
|
||||
if not resp.content:
|
||||
return {}
|
||||
|
||||
return resp.json()
|
||||
|
||||
def get_device_status(self) -> dict:
|
||||
"""Fetch full status of the device."""
|
||||
data = self._post("get", {
|
||||
"ids": [self.device_id],
|
||||
"select": ["status"],
|
||||
})
|
||||
if isinstance(data, list) and len(data) > 0:
|
||||
return data[0]
|
||||
return {}
|
||||
|
||||
def parse_readings(self, status: dict) -> dict:
|
||||
"""Extract useful readings from device status."""
|
||||
s = status.get("status", status)
|
||||
|
||||
em1_0 = s.get("em1:0", {})
|
||||
em1_1 = s.get("em1:1", {})
|
||||
em1data_0 = s.get("em1data:0", {})
|
||||
em1data_1 = s.get("em1data:1", {})
|
||||
switch = s.get("switch:0", {})
|
||||
wifi = s.get("wifi", {})
|
||||
|
||||
return {
|
||||
# Canale 0 — produzione pannelli → inverter
|
||||
"ch0_act_power": em1_0.get("act_power"),
|
||||
"ch0_aprt_power": em1_0.get("aprt_power"),
|
||||
"ch0_current": em1_0.get("current"),
|
||||
"ch0_voltage": em1_0.get("voltage"),
|
||||
"ch0_pf": em1_0.get("pf"),
|
||||
"ch0_total_act_energy": em1data_0.get("total_act_energy"),
|
||||
"ch0_total_ret_energy": em1data_0.get("total_act_ret_energy"),
|
||||
# Canale 1 — inverter → casa/rete
|
||||
"ch1_act_power": em1_1.get("act_power"),
|
||||
"ch1_aprt_power": em1_1.get("aprt_power"),
|
||||
"ch1_current": em1_1.get("current"),
|
||||
"ch1_voltage": em1_1.get("voltage"),
|
||||
"ch1_pf": em1_1.get("pf"),
|
||||
"ch1_total_act_energy": em1data_1.get("total_act_energy"),
|
||||
"ch1_total_ret_energy": em1data_1.get("total_act_ret_energy"),
|
||||
# Switch / stato dispositivo
|
||||
"switch_output": switch.get("output"),
|
||||
"temperature": switch.get("temperature", {}).get("tC"),
|
||||
"wifi_rssi": wifi.get("rssi"),
|
||||
"online": s.get("online", status.get("online")),
|
||||
"unixtime": s.get("sys", {}).get("unixtime"),
|
||||
"uptime": s.get("sys", {}).get("uptime"),
|
||||
}
|
||||
Reference in new issue
Block a user