0.5.0: riordino come l'app ufficiale (home, Dati, Impianto)

- home: temperatura esterna e in casa/assente in alto, messaggio di stato,
  scheda impianto con temperatura stanza e acqua calda; comandi sotto
- pagina Dati: calore prodotto, elettricita' usata, COP acqua calda e
  riscaldamento, risparmio, CO2 evitata, Smart Tariff Control (mese per mese)
- pagina Impianto: stato, termostato a parete, versioni software, errori
- bridge 0.4.0: rotta /stats (servizio statistiche del cloud) e blocco
  dispositivo in /summary (ID breve, versioni, termostato)
- C++: parseStats, formatNumber, formatMoney, monthLabel (separatore e
  valuta secondo la lingua); 61/61 verifiche del nucleo
- anteprime aggiornate (dati, impianto) in inglese e italiano
This commit is contained in:
2026-10-10 08:59:03 +02:00
parent ec7333a185
commit 4f279a18b0
30 changed files with 2355 additions and 353 deletions
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""Sonda di sola lettura sui dati statistici del cloud Aira (v2).
NON invia comandi all'impianto. Obiettivo: capire cosa si puo' mostrare in una
pagina "Dati" come quella dell'app ufficiale (calore prodotto, elettricita',
efficienza/COP, risparmi).
"""
import json
import os
import sys
from datetime import datetime, timedelta
from pathlib import Path
CFG = Path(os.environ.get("AIRA_CONFIG_DIR", str(Path.home() / ".hermes/profiles/carlo")))
sys.path.insert(0, str(CFG))
from pyairahome import AiraHome
from pyairahome.device.heat_pump.statistics.v1 import service_pb2 as S
def section(t):
print("\n" + "=" * 72)
print(t)
print("=" * 72)
section("messaggi annidati e risposte")
for name in ("Uuid", "YearMonth", "LocalDateTime", "CopEntry", "GetCopResponse",
"MonthlySavings", "GetSavingsResponse", "RollingMonthOptimisation",
"GetOptimisationSavingsResponse", "InsightEntry", "GetHeatPumpInsightsResponse"):
cls = getattr(S, name, None)
if cls is None:
print(f"{name}: assente")
continue
try:
fs = cls.DESCRIPTOR.fields
print(f"{name}: " + ", ".join(
f"{f.name}(type={f.type}, msg={f.message_type and f.message_type.name}, "
f"enum={f.enum_type and f.enum_type.name})" for f in fs))
except Exception as exc: # noqa: BLE001
print(f"{name}: introspezione fallita ({type(exc).__name__}: {exc})")
# --- login ----------------------------------------------------------------- #
tokens = json.loads((CFG / "aira_tokens.json").read_text())
state = json.loads(Path("/tmp/aira_state.json").read_text())["state"]
hp = state["heat_pump_id"]["value"]
aira = AiraHome()
aira.cloud.login_with_tokens(tokens["id_token"], tokens["access_token"], tokens["refresh_token"])
stub = aira.cloud._stats_service_stub # noqa: SLF001
call = aira.cloud.call_service
print(f"\nheat_pump_id: {hp}")
now = datetime.now()
def show(label, method, req):
section(label)
try:
out = call(stub, method, req, timeout=25, raw=True)
print(str(out).replace("\n", " | ")[:1500] or "(risposta vuota)")
except Exception as exc: # noqa: BLE001
print(f"ERRORE {type(exc).__name__}: {str(exc)[:400]}")
# COP mensile, ultimi 6 mesi
req = S.GetCopRequest()
req.heat_pump_id.value = hp
req.granularity = S.COP_GRANULARITY_MONTHLY
start = now - timedelta(days=180)
req.start_date.year, req.start_date.month = start.year, start.month
req.end_date_exclusive.year, req.end_date_exclusive.month = now.year, now.month + 1 if now.month < 12 else 1
if now.month == 12:
req.end_date_exclusive.year = now.year + 1
show("GetCop mensile (6 mesi) -> efficienza", "GetCop", req)
# Risparmi: periodo 180 giorni
from google.protobuf.timestamp_pb2 import Timestamp
req = S.GetSavingsRequest()
req.heat_pump_id.value = hp
ts = Timestamp(); ts.FromDatetime(start); req.start_time.CopyFrom(ts)
te = Timestamp(); te.FromDatetime(now); req.end_time.CopyFrom(te)
show("GetSavings (180 giorni) -> risparmi", "GetSavings", req)
# Risparmi Smart Tariff Control (per nucleo familiare)
household = None
try:
devs = aira.cloud.get_devices()
print("\nget_devices ->", str(devs)[:400])
def find_hh(o):
if isinstance(o, dict):
for k, v in o.items():
if k == "household_id" and v:
return v if isinstance(v, str) else v.get("value")
r = find_hh(v)
if r:
return r
elif isinstance(o, list):
for v in o:
r = find_hh(v)
if r:
return r
return None
household = find_hh(devs)
print("household_id:", household)
except Exception as exc: # noqa: BLE001
print("get_devices fallita:", type(exc).__name__, str(exc)[:200])
if household:
req = S.GetOptimisationSavingsRequest()
req.household_id.value = household
show("GetOptimisationSavings -> risparmio Smart Tariff Control", "GetOptimisationSavings", req)
# Insight giornalieri, ultimi 7 giorni (LocalDateTime)
req = S.GetHeatPumpInsightsRequest()
req.heat_pump_id.value = hp
req.granularity = S.GRANULARITY_DAILY
s = now - timedelta(days=7)
for m in (req.start_time, req.end_time_exclusive):
pass
req.start_time.year, req.start_time.month, req.start_time.day = s.year, s.month, s.day
req.end_time_exclusive.year, req.end_time_exclusive.month, req.end_time_exclusive.day = now.year, now.month, now.day
show("GetHeatPumpInsights giornaliero (7 giorni)", "GetHeatPumpInsights", req)
aira.cloud.close()
print("\nsonda conclusa: nessun comando inviato all'impianto.")