0.7.0: assenza con date, conto alla rovescia del boost, catalogo rifatto
- AwayDialog.qml: popup con inizio e fine (Dialog, ValueButton, DatePickerDialog: componenti e proprieta' verificati nella guida Silica dell'SDK). Il tasto Assenza apre il popup invece di mandare il comando; - conto alla rovescia 30:00 -> 00:00 in Home e in copertina, con i secondi dell'impianto (AiraJson::countdown + timer che si riallinea a ogni lettura); - commandPayload accetta campi extra (start/end); - catalogo: lupdate leggeva solo i C++ e marcava 151 voci QML come obsolete. Ora src e qml espliciti: 197 stringhe, 0 non tradotte; - versione 0.7.0 (spec, main.cpp), PROTOCOL, README/PIANO, post per il forum.
This commit is contained in:
+75
-11
@@ -35,9 +35,10 @@ import sys
|
||||
import threading
|
||||
import traceback
|
||||
import warnings
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, time, timedelta, timezone
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from google.protobuf.duration_pb2 import Duration
|
||||
from google.protobuf.timestamp_pb2 import Timestamp
|
||||
@@ -236,6 +237,8 @@ def summarize(st: dict) -> dict:
|
||||
return None
|
||||
return round(float(valore) / 10.0, 1)
|
||||
|
||||
boost_restano = boost_restanti(st)
|
||||
|
||||
return {
|
||||
"outdoor_temperature": g("current_outdoor_temperature"),
|
||||
"indoor_temperature": g("deprecated_current_indoor_temperature"),
|
||||
@@ -243,6 +246,8 @@ def summarize(st: dict) -> dict:
|
||||
"dhw_target": g("target_hot_water_temperature"),
|
||||
"operating_status": g("operating_status"),
|
||||
"away_mode": bool(st.get("away_mode_enabled")),
|
||||
"boost_active": boost_restano > 0,
|
||||
"boost_remaining_seconds": boost_restano,
|
||||
"night_mode": bool(st.get("night_mode_enabled")),
|
||||
"manual_mode": bool(st.get("manual_mode_enabled")),
|
||||
# force_heating E' un oggetto {enabled, remaining_time}: leggerlo con
|
||||
@@ -462,22 +467,26 @@ def build_stats(aira, heat_pump_id: str, household_id: str,
|
||||
# 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
|
||||
BOOST_MIN_DEFAULT = 30 # boost ACS (richiesta dell'utente: mezz'ora)
|
||||
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
|
||||
# Il fuso dell'impianto, NON quello della macchina: il server puo' stare in UTC
|
||||
# e nei giorni di cambio ora si sbaglia di un'ora (e' successo nei test).
|
||||
# Lo stato lo dichiara in `time_zone.time_zone_identifier`.
|
||||
PLANT_TZ = ZoneInfo("Europe/Rome")
|
||||
|
||||
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": "minuti, 15-240 (opzionale, default 60)"},
|
||||
"hot_water_boost_on": {"help": "avvia il boost ACS (una tantum)", "value": "minuti, 15-240 (opzionale, default 30)"},
|
||||
"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": "minuti, 15-1440 (opzionale, default 60)"},
|
||||
"away_mode_on": {"help": "modalita' assenza", "value": "giorni, 1-60 (opzionale, default 7)"},
|
||||
"away_mode_on": {"help": "assenza con date di inizio e fine", "value": "start ed end (AAAA-MM-GG); in alternativa giorni, 1-60"},
|
||||
"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": "minuti, 15-240 (opzionale, default 60)"},
|
||||
@@ -488,7 +497,7 @@ COMMANDS = {
|
||||
}
|
||||
|
||||
|
||||
def build_command(cmd_id: str, value=None, zone=1, context=None):
|
||||
def build_command(cmd_id: str, value=None, zone=1, context=None, params=None):
|
||||
"""Costruisce l'oggetto comando della libreria. Importata solo qui per non
|
||||
pagarne il costo all'avvio del bridge.
|
||||
|
||||
@@ -585,9 +594,10 @@ def build_command(cmd_id: str, value=None, zone=1, context=None):
|
||||
duration=_durata(value, FORCE_MIN_DEFAULT, 15, 240, cmd_id))
|
||||
|
||||
if cmd_id == "away_mode_on":
|
||||
inizio, fine = _periodo_assenza(params, value)
|
||||
return SetAwayMode(
|
||||
current_time=_adesso(),
|
||||
end_time=_adesso(giorni=_giorni(value)),
|
||||
current_time=inizio,
|
||||
end_time=fine,
|
||||
target_room_temperature=AWAY_TARGET_TEMP,
|
||||
)
|
||||
|
||||
@@ -625,13 +635,67 @@ def _giorni(value) -> int:
|
||||
return g
|
||||
|
||||
|
||||
def _adesso(giorni: int = 0) -> Timestamp:
|
||||
"""Timestamp UTC di adesso, oppure di adesso + N giorni."""
|
||||
def _ts(quando: datetime) -> Timestamp:
|
||||
t = Timestamp()
|
||||
t.FromDatetime(datetime.now(timezone.utc) + timedelta(days=giorni))
|
||||
t.FromDatetime(quando.astimezone(timezone.utc))
|
||||
return t
|
||||
|
||||
|
||||
def _periodo_assenza(params, value):
|
||||
"""Inizio e fine dell'assenza.
|
||||
|
||||
Date in `start`/`end` (AAAA-MM-GG, ora locale); se mancano, `value` = giorni
|
||||
da adesso. La fine e' la mezzanotte locale del giorno indicato: da li'
|
||||
l'impianto riprende il programma normale e la casa risale dai 15 °C.
|
||||
"""
|
||||
params = params or {}
|
||||
oggi = datetime.now(PLANT_TZ).date()
|
||||
|
||||
if not str(params.get("start") or "").strip() and not str(params.get("end") or "").strip():
|
||||
# Compatibilita': senza date, solo i giorni da adesso (nessun giorno di
|
||||
# calendario): e' la forma che usava il bridge prima del popup.
|
||||
adesso = datetime.now(PLANT_TZ)
|
||||
return _ts(adesso), _ts(adesso + timedelta(days=_giorni(value)))
|
||||
|
||||
def giorno(chiave, default):
|
||||
testo = str(params.get(chiave) or "").strip()
|
||||
if not testo:
|
||||
return default
|
||||
try:
|
||||
return datetime.strptime(testo[:10], "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
raise ValueError(f"away_mode_on: data non valida in {chiave}: {testo}")
|
||||
|
||||
inizio = giorno("start", oggi)
|
||||
fine = giorno("end", oggi + timedelta(days=_giorni(value)))
|
||||
if fine <= inizio:
|
||||
raise ValueError("away_mode_on: la fine deve venire dopo l'inizio")
|
||||
if (fine - inizio).days > 180:
|
||||
raise ValueError("away_mode_on: periodo troppo lungo (max 180 giorni)")
|
||||
|
||||
adesso = datetime.now(PLANT_TZ)
|
||||
partenza = datetime.combine(inizio, time.min, tzinfo=PLANT_TZ)
|
||||
if partenza < adesso:
|
||||
partenza = adesso
|
||||
return _ts(partenza), _ts(datetime.combine(fine, time.min, tzinfo=PLANT_TZ))
|
||||
|
||||
|
||||
def boost_restanti(st: dict) -> int:
|
||||
"""Secondi che mancano alla fine del boost ACS (0 se non e' in corso).
|
||||
|
||||
L'impianto espone solo start_time/end_time: il conto alla rovescia dell'app
|
||||
si basa su end_time, non sul momento in cui l'app ha visto partire il boost.
|
||||
"""
|
||||
fine = str((st.get("hot_water_boost") or {}).get("end_time") or "")
|
||||
if not fine:
|
||||
return 0
|
||||
try:
|
||||
quando = datetime.fromisoformat(fine.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return 0
|
||||
return max(0, int((quando - datetime.now(timezone.utc)).total_seconds()))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# server HTTP
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -695,7 +759,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
body = self._body()
|
||||
cmd_id = body.get("id", "")
|
||||
cmd = build_command(cmd_id, body.get("value"), body.get("zone") or 1,
|
||||
self.session.last_states())
|
||||
self.session.last_states(), body)
|
||||
except ValueError as exc:
|
||||
return self._send(400, {"ok": False, "error": str(exc)})
|
||||
except Exception as exc:
|
||||
|
||||
Reference in New Issue
Block a user