Files
shellydash/shelly_client.py
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

105 lines
3.5 KiB
Python

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