0.6.9: il bridge corretto (boost con la durata) e i comandi con argomento in PROTOCOL

La copia del bridge dentro l'app va tenuta allineata: contiene la correzione
del boost, dell'assenza, di ACS off e del riscaldamento forzato, che la
libreria accetta solo con un argomento. Il pacchetto Sailfish non cambia (il
bridge non entra nell'RPM), quindi non serve ricostruire l'RPM.
This commit is contained in:
Carlo
2026-10-11 11:49:39 +02:00
parent 2136122398
commit 6ef6f5bfd8
6 changed files with 175 additions and 32 deletions
+78 -9
View File
@@ -35,9 +35,13 @@ import sys
import threading
import traceback
import warnings
from datetime import datetime, timedelta, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from google.protobuf.duration_pb2 import Duration
from google.protobuf.timestamp_pb2 import Timestamp
warnings.filterwarnings("ignore")
# Tutti i file di configurazione stanno in una cartella sola (permessi 600):
@@ -53,7 +57,7 @@ CREDS = BASE / "aira.json"
TOKENS = BASE / "aira_tokens.json"
POOL_CFG = BASE / "aira_config.json"
VERSION = "0.6.2"
VERSION = "0.6.9"
# --------------------------------------------------------------------------- #
@@ -451,19 +455,32 @@ def build_stats(aira, heat_pump_id: str, household_id: str,
# --------------------------------------------------------------------------- #
# comandi ammessi (whitelist: mai esporre l'intera libreria su HTTP)
# --------------------------------------------------------------------------- #
# Quattro comandi NON si possono inviare senza un argomento: la libreria vuole
# una durata (Duration) o, per l'assenza, inizio/fine (Timestamp) e la
# temperatura da tenere in casa. Senza, solleva TypeError e il bridge risponde
# "richiesta non valida" — e' quello che e' successo al primo boost, perche'
# nemmeno l'app mandava la durata.
# I valori qui sotto sono i default del bridge: sono scelte, non misure. L'app
# puo' mandarne altri in `value` (minuti, o giorni per l'assenza).
BOOST_MIN_DEFAULT = 60 # boost ACS
ACS_OFF_MIN_DEFAULT = 60 # ACS spento
FORCE_MIN_DEFAULT = 60 # riscaldamento forzato
AWAY_DAYS_DEFAULT = 7 # assenza
AWAY_TARGET_TEMP = 15.0 # casa tenuta a questa temperatura in assenza, °C
COMMANDS = {
"ping": {"help": "verifica che l'impianto risponda", "value": None},
"set_hot_water_target": {"help": "temperatura ACS obiettivo in °C", "value": "45-60"},
"set_heating_setpoint": {"help": "temperatura obiettivo della casa in °C", "value": "5-30", "zone": "1 o 2 (opzionale, default 1)"},
"set_cooling_setpoint": {"help": "temperatura obiettivo del raffrescamento in °C", "value": "5-30", "zone": "1 o 2 (opzionale, default 1)"},
"hot_water_boost_on": {"help": "avvia il boost ACS (una tantum)", "value": None},
"hot_water_boost_on": {"help": "avvia il boost ACS (una tantum)", "value": "minuti, 15-240 (opzionale, default 60)"},
"hot_water_boost_off": {"help": "ferma il boost ACS", "value": None},
"hot_water_heating_on": {"help": "abilita il riscaldamento ACS", "value": None},
"hot_water_heating_off": {"help": "disabilita il riscaldamento ACS", "value": None},
"away_mode_on": {"help": "modalita' assenza", "value": None},
"hot_water_heating_off": {"help": "disabilita il riscaldamento ACS", "value": "minuti, 15-1440 (opzionale, default 60)"},
"away_mode_on": {"help": "modalita' assenza", "value": "giorni, 1-60 (opzionale, default 7)"},
"away_mode_off": {"help": "disattiva l'assenza", "value": None},
"night_mode_1h": {"help": "night mode per un'ora", "value": None},
"force_heating_on": {"help": "forza il riscaldamento", "value": None},
"force_heating_on": {"help": "forza il riscaldamento", "value": "minuti, 15-240 (opzionale, default 60)"},
"force_heating_off": {"help": "disattiva il riscaldamento forzato", "value": None},
"heating_on": {"help": "abilita la funzione riscaldamento", "value": None},
"heating_off": {"help": "disabilita la funzione riscaldamento", "value": None},
@@ -496,16 +513,14 @@ def build_command(cmd_id: str, value=None, zone=1, context=None):
SetZoneSetpoints,
)
# Solo i comandi che la libreria accetta senza argomenti: gli altri hanno un
# ramo loro qui sotto, perche' senza la durata sollevano TypeError.
simple = {
"ping": Ping,
"hot_water_boost_on": ActivateHotWaterBoosting,
"hot_water_boost_off": DeactivateHotWaterBoosting,
"hot_water_heating_on": EnableHotWaterHeating,
"hot_water_heating_off": DisableHotWaterHeating,
"away_mode_on": SetAwayMode,
"away_mode_off": ClearAwayMode,
"night_mode_1h": ActivateNightModeForOneHour,
"force_heating_on": EnableForceHeating,
"force_heating_off": DisableForceHeating,
"heating_on": EnableHeatingFunction,
"heating_off": DisableHeatingFunction,
@@ -557,12 +572,66 @@ def build_command(cmd_id: str, value=None, zone=1, context=None):
# Kind: 1 = riscaldamento, 2 = raffrescamento (enum del protocollo).
return SetZoneSetpoints(zone_setpoints=zs, kind=1 if heating else 2)
if cmd_id == "hot_water_boost_on":
return ActivateHotWaterBoosting(
hot_water_boost_duration=_durata(value, BOOST_MIN_DEFAULT, 15, 240, cmd_id))
if cmd_id == "hot_water_heating_off":
return DisableHotWaterHeating(
time=_durata(value, ACS_OFF_MIN_DEFAULT, 15, 1440, cmd_id))
if cmd_id == "force_heating_on":
return EnableForceHeating(
duration=_durata(value, FORCE_MIN_DEFAULT, 15, 240, cmd_id))
if cmd_id == "away_mode_on":
return SetAwayMode(
current_time=_adesso(),
end_time=_adesso(giorni=_giorni(value)),
target_room_temperature=AWAY_TARGET_TEMP,
)
cls = simple.get(cmd_id)
if cls is None:
raise ValueError(f"comando non ammesso: {cmd_id}")
return cls()
def _durata(value, default_min: int, min_min: int, max_min: int, nome: str) -> Duration:
"""Durata in minuti: `value` se l'app lo manda, altrimenti il default."""
if value in (None, ""):
minuti = default_min
else:
try:
minuti = int(float(value))
except (TypeError, ValueError):
raise ValueError(f"{nome} richiede i minuti come numero")
if not min_min <= minuti <= max_min:
raise ValueError(
f"{nome}: durata fuori intervallo ({min_min}-{max_min} minuti)")
return Duration(seconds=minuti * 60)
def _giorni(value) -> int:
"""Giorni di assenza: `value` se l'app lo manda, altrimenti il default."""
if value in (None, ""):
return AWAY_DAYS_DEFAULT
try:
g = int(float(value))
except (TypeError, ValueError):
raise ValueError("away_mode_on richiede i giorni come numero")
if not 1 <= g <= 60:
raise ValueError("away_mode_on: durata fuori intervallo (1-60 giorni)")
return g
def _adesso(giorni: int = 0) -> Timestamp:
"""Timestamp UTC di adesso, oppure di adesso + N giorni."""
t = Timestamp()
t.FromDatetime(datetime.now(timezone.utc) + timedelta(days=giorni))
return t
# --------------------------------------------------------------------------- #
# server HTTP
# --------------------------------------------------------------------------- #