bridge nel repo: sorgenti, unit systemd e istruzioni per i tester
- bridge/ con aira_bridge.py, setup_creds.py, aira_probe.py, test offline - percorsi di configurazione via AIRA_CONFIG_DIR (default invariato) - README del bridge in inglese: installazione, servizio, firewall, sicurezza - README principale aggiornato (il bridge non è più 'fuori dal repo')
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verifica offline dei comandi del bridge. NON invia nulla all'impianto:
|
||||
costruisce i messaggi protobuf e ne controlla il contenuto.
|
||||
|
||||
.venv/bin/python test_bridge_commands.py
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
import aira_bridge as b # noqa: E402
|
||||
|
||||
esiti = []
|
||||
|
||||
|
||||
def check(nome, cond, extra=""):
|
||||
esiti.append((nome, bool(cond), extra))
|
||||
print(f"{'OK ' if cond else 'FALLITO'} {nome}{(' -> ' + extra) if extra else ''}")
|
||||
|
||||
|
||||
# --- contesto: come lo stato reale dell'impianto --------------------------- #
|
||||
ctx = {"zone_setpoints_heating": {"zone_1": 19.0, "zone_2": 0.0},
|
||||
"zone_setpoints_cooling": {"zone_1": 22.0, "zone_2": 0.0}}
|
||||
|
||||
# --- 1) riscaldamento, zona 1 --------------------------------------------- #
|
||||
cmd = b.build_command("set_heating_setpoint", 21.5, 1, ctx)
|
||||
msg = cmd._message
|
||||
check("zona 1: obiettivo applicato", abs(msg.zone_setpoints.zone_1 - 21.5) < 1e-6,
|
||||
f"zone_1={msg.zone_setpoints.zone_1}")
|
||||
check("zona 1: zona 2 preservata", msg.zone_setpoints.zone_2 == 0.0,
|
||||
f"zone_2={msg.zone_setpoints.zone_2}")
|
||||
check("zona 1: kind = riscaldamento (1)", msg.kind == 1, f"kind={msg.kind}")
|
||||
check("messaggio serializzabile", len(msg.SerializeToString()) > 0,
|
||||
f"{len(msg.SerializeToString())} byte")
|
||||
|
||||
# --- 2) raffrescamento, zona 2 (con zona 1 da preservare) ------------------ #
|
||||
ctx2 = {"zone_setpoints_cooling": {"zone_1": 22.0, "zone_2": 24.0},
|
||||
"zone_setpoints_heating": {"zone_1": 19.0, "zone_2": 0.0}}
|
||||
cmd = b.build_command("set_cooling_setpoint", 23.0, 2, ctx2)
|
||||
msg = cmd._message
|
||||
check("zona 2: obiettivo applicato", abs(msg.zone_setpoints.zone_2 - 23.0) < 1e-6,
|
||||
f"zone_2={msg.zone_setpoints.zone_2}")
|
||||
check("zona 2: zona 1 preservata (22.0)", msg.zone_setpoints.zone_1 == 22.0,
|
||||
f"zone_1={msg.zone_setpoints.zone_1}")
|
||||
check("raffrescamento: kind = 2", msg.kind == 2, f"kind={msg.kind}")
|
||||
|
||||
# --- 3) senza contesto: non deve rompersi -------------------------------- #
|
||||
cmd = b.build_command("set_heating_setpoint", 20.0)
|
||||
check("senza contesto: nessuna eccezione", cmd._message.zone_setpoints.zone_1 == 20.0)
|
||||
|
||||
# --- 4) valori rifiutati -------------------------------------------------- #
|
||||
def rifiuta(nome, *args, **kw):
|
||||
try:
|
||||
b.build_command(*args, **kw)
|
||||
except ValueError as exc:
|
||||
check(nome, True, str(exc))
|
||||
except Exception as exc: # inatteso
|
||||
check(nome, False, f"eccezione sbagliata: {type(exc).__name__}: {exc}")
|
||||
else:
|
||||
check(nome, False, "accettato, doveva essere rifiutato")
|
||||
|
||||
|
||||
rifiuta("rifiuta 40 °C (troppo alta)", "set_heating_setpoint", 40.0, 1, ctx)
|
||||
rifiuta("rifiuta 3 °C (troppo bassa)", "set_heating_setpoint", 3.0, 1, ctx)
|
||||
rifiuta("rifiuta zona 3", "set_heating_setpoint", 20.0, 3, ctx)
|
||||
rifiuta("rifiuta valore non numerico", "set_heating_setpoint", "caldo", 1, ctx)
|
||||
rifiuta("rifiuta comando sconosciuto", "factory_reset", None, 1, ctx)
|
||||
|
||||
# --- 5) il riassunto espone i nuovi campi --------------------------------- #
|
||||
stato_file = Path("/tmp/state.json")
|
||||
if stato_file.exists():
|
||||
st = json.loads(stato_file.read_text())["state"]
|
||||
s = b.summarize(st)
|
||||
for k in ("zone1_setpoint", "zone1_temperature", "zone1_mode",
|
||||
"zone2_setpoint", "zones"):
|
||||
check(f"riassunto contiene {k}", k in s, repr(s.get(k)))
|
||||
check("zona 1: obiettivo presente", s.get("zone1_setpoint") is not None,
|
||||
f"{s.get('zone1_setpoint')} °C")
|
||||
|
||||
# force_heating e' un oggetto {enabled, remaining_time}: leggerlo con bool()
|
||||
# darebbe sempre vero (era il bug). Qui si verifica la lettura del campo.
|
||||
spento = b.summarize(dict(st, force_heating={"enabled": False, "remaining_time": "0s"}))
|
||||
acceso = b.summarize(dict(st, force_heating={"enabled": True, "remaining_time": "1h"}))
|
||||
check("riassunto: forzato spento -> falso", spento.get("force_heating") is False,
|
||||
repr(spento.get("force_heating")))
|
||||
check("riassunto: forzato acceso -> vero", acceso.get("force_heating") is True,
|
||||
repr(acceso.get("force_heating")))
|
||||
check("riassunto: durata forzato riportata", acceso.get("force_heating_remaining") == "1h",
|
||||
repr(acceso.get("force_heating_remaining")))
|
||||
for k in ("hot_water_heating", "configured_modes", "pump_active_state",
|
||||
"outdoor_defrost", "signature_lights"):
|
||||
check(f"riassunto contiene {k}", k in s, repr(s.get(k)))
|
||||
else:
|
||||
print("(salto il controllo del riassunto: /tmp/state.json assente)")
|
||||
|
||||
# --- esito ---------------------------------------------------------------- #
|
||||
falliti = [n for n, ok, _ in esiti if not ok]
|
||||
print(f"\n{len(esiti) - len(falliti)}/{len(esiti)} controlli superati")
|
||||
sys.exit(1 if falliti else 0)
|
||||
Reference in New Issue
Block a user