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:
Carlo
2026-10-11 12:05:59 +02:00
parent 6ef6f5bfd8
commit d2cb9d692f
17 changed files with 481 additions and 80 deletions
+8
View File
@@ -23,6 +23,14 @@ e ripiega sull'inglese; la si può scegliere anche nelle impostazioni dell'app.
affiancate e **toccabili**, e tre azioni rapide — boost, assenza, comandi affiancate e **toccabili**, e tre azioni rapide — boost, assenza, comandi
impianto — ognuna con la sua icona. Sta in una schermata, senza scorrere: i impianto — ognuna con la sua icona. Sta in una schermata, senza scorrere: i
comandi hanno pagine loro comandi hanno pagine loro
- **Conto alla rovescia del boost**: mentre il boost dell'acqua calda è in corso,
in Home scorre da 30:00 a 00:00 e in copertina prende il posto dell'obiettivo
dell'acqua. I secondi vengono dalla fine del boost letta dall'impianto: il
conto è giusto anche se il boost l'ha avviato qualcun altro
- **Assenza con le date**: toccando *Assenza* si apre una finestra dove si
scelgono il primo e l'ultimo giorno (calendario, un tocco ciascuno). In quel
periodo la casa resta a 15 °C e riprende il programma normale il giorno scelto
come ultimo
- **Regolazioni**: obiettivo casa e obiettivo acqua calda, ognuno con il valore - **Regolazioni**: obiettivo casa e obiettivo acqua calda, ognuno con il valore
che l'impianto sta tenendo adesso, lo slider (la proposta: il valore vero non si che l'impianto sta tenendo adesso, lo slider (la proposta: il valore vero non si
muove), una fila di valori rapidi per le dita fredde e il pulsante di conferma, muove), una fila di valori rapidi per le dita fredde e il pulsante di conferma,
+8
View File
@@ -17,6 +17,14 @@ status, domestic hot water, and the house target temperature per zone.
target) side by side and **tappable**, and three quick actions — boost, away target) side by side and **tappable**, and three quick actions — boost, away
mode and the plant commands, each with its own icon. It fits without scrolling mode and the plant commands, each with its own icon. It fits without scrolling
— the controls live in their own pages — the controls live in their own pages
- **Boost countdown**: while a hot water boost is running the home screen shows
it ticking down from 30:00 to 00:00, and the cover page shows it in place of
the hot water target. The seconds come from the plant's own boost end time, so
the count is right even if the boost was started from somewhere else
- **Away mode with dates**: tapping *Away* opens a dialog where you pick the
first and the last day (calendar, one tap each). The house is kept at 15 °C
for that period and resumes its normal programme on the day you chose as the
last one
- **Setpoints** page: house target and hot water target, each with the value the - **Setpoints** page: house target and hot water target, each with the value the
plant is keeping right now, a slider (the proposal, the plant value does not plant is keeping right now, a slider (the proposal, the plant value does not
move), a row of quick values for cold fingers and a confirmation button that move), a row of quick values for cold fingers and a confirmation button that
+75 -11
View File
@@ -35,9 +35,10 @@ import sys
import threading import threading
import traceback import traceback
import warnings import warnings
from datetime import datetime, timedelta, timezone from datetime import datetime, time, timedelta, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path from pathlib import Path
from zoneinfo import ZoneInfo
from google.protobuf.duration_pb2 import Duration from google.protobuf.duration_pb2 import Duration
from google.protobuf.timestamp_pb2 import Timestamp from google.protobuf.timestamp_pb2 import Timestamp
@@ -236,6 +237,8 @@ def summarize(st: dict) -> dict:
return None return None
return round(float(valore) / 10.0, 1) return round(float(valore) / 10.0, 1)
boost_restano = boost_restanti(st)
return { return {
"outdoor_temperature": g("current_outdoor_temperature"), "outdoor_temperature": g("current_outdoor_temperature"),
"indoor_temperature": g("deprecated_current_indoor_temperature"), "indoor_temperature": g("deprecated_current_indoor_temperature"),
@@ -243,6 +246,8 @@ def summarize(st: dict) -> dict:
"dhw_target": g("target_hot_water_temperature"), "dhw_target": g("target_hot_water_temperature"),
"operating_status": g("operating_status"), "operating_status": g("operating_status"),
"away_mode": bool(st.get("away_mode_enabled")), "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")), "night_mode": bool(st.get("night_mode_enabled")),
"manual_mode": bool(st.get("manual_mode_enabled")), "manual_mode": bool(st.get("manual_mode_enabled")),
# force_heating E' un oggetto {enabled, remaining_time}: leggerlo con # 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. # nemmeno l'app mandava la durata.
# I valori qui sotto sono i default del bridge: sono scelte, non misure. L'app # 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). # 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 ACS_OFF_MIN_DEFAULT = 60 # ACS spento
FORCE_MIN_DEFAULT = 60 # riscaldamento forzato FORCE_MIN_DEFAULT = 60 # riscaldamento forzato
AWAY_DAYS_DEFAULT = 7 # assenza AWAY_DAYS_DEFAULT = 7 # assenza
AWAY_TARGET_TEMP = 15.0 # casa tenuta a questa temperatura in assenza, °C 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 = { COMMANDS = {
"ping": {"help": "verifica che l'impianto risponda", "value": None}, "ping": {"help": "verifica che l'impianto risponda", "value": None},
"set_hot_water_target": {"help": "temperatura ACS obiettivo in °C", "value": "45-60"}, "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_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)"}, "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_boost_off": {"help": "ferma il boost ACS", "value": None},
"hot_water_heating_on": {"help": "abilita il riscaldamento 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)"}, "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}, "away_mode_off": {"help": "disattiva l'assenza", "value": None},
"night_mode_1h": {"help": "night mode per un'ora", "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)"}, "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 """Costruisce l'oggetto comando della libreria. Importata solo qui per non
pagarne il costo all'avvio del bridge. 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)) duration=_durata(value, FORCE_MIN_DEFAULT, 15, 240, cmd_id))
if cmd_id == "away_mode_on": if cmd_id == "away_mode_on":
inizio, fine = _periodo_assenza(params, value)
return SetAwayMode( return SetAwayMode(
current_time=_adesso(), current_time=inizio,
end_time=_adesso(giorni=_giorni(value)), end_time=fine,
target_room_temperature=AWAY_TARGET_TEMP, target_room_temperature=AWAY_TARGET_TEMP,
) )
@@ -625,13 +635,67 @@ def _giorni(value) -> int:
return g return g
def _adesso(giorni: int = 0) -> Timestamp: def _ts(quando: datetime) -> Timestamp:
"""Timestamp UTC di adesso, oppure di adesso + N giorni."""
t = Timestamp() t = Timestamp()
t.FromDatetime(datetime.now(timezone.utc) + timedelta(days=giorni)) t.FromDatetime(quando.astimezone(timezone.utc))
return t 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 # server HTTP
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
@@ -695,7 +759,7 @@ class Handler(BaseHTTPRequestHandler):
body = self._body() body = self._body()
cmd_id = body.get("id", "") cmd_id = body.get("id", "")
cmd = build_command(cmd_id, body.get("value"), body.get("zone") or 1, 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: except ValueError as exc:
return self._send(400, {"ok": False, "error": str(exc)}) return self._send(400, {"ok": False, "error": str(exc)})
except Exception as exc: except Exception as exc:
+43 -2
View File
@@ -149,8 +149,8 @@ check("senza termostato: nessun errore",
from pyairahome.commands import CommandBase # noqa: E402 from pyairahome.commands import CommandBase # noqa: E402
boost = b.build_command("hot_water_boost_on") boost = b.build_command("hot_water_boost_on")
check("boost: un'ora di default", check("boost: mezz'ora di default (richiesta dell'utente)",
boost._message.hot_water_boost_duration.seconds == 3600, boost._message.hot_water_boost_duration.seconds == 1800,
f"{boost._message.hot_water_boost_duration.seconds}s") f"{boost._message.hot_water_boost_duration.seconds}s")
check("boost: durata scelta in minuti (90 -> 5400s)", check("boost: durata scelta in minuti (90 -> 5400s)",
b.build_command("hot_water_boost_on", 90) b.build_command("hot_water_boost_on", 90)
@@ -199,6 +199,47 @@ for cid in b.COMMANDS:
check(f"whitelist: {cid} si costruisce e si incarta", False, check(f"whitelist: {cid} si costruisce e si incarta", False,
f"{type(exc).__name__}: {exc}") f"{type(exc).__name__}: {exc}")
# --- 6) assenza con date, e conto alla rovescia del boost ----------------- #
from datetime import datetime, timedelta, timezone # noqa: E402
from zoneinfo import ZoneInfo # noqa: E402
ROMA = ZoneInfo("Europe/Rome")
def a_roma(t):
return t.ToDatetime(tzinfo=timezone.utc).astimezone(ROMA).strftime("%Y-%m-%d %H:%M")
date_di = b.build_command("away_mode_on", None, 1, ctx,
{"start": "2026-11-01", "end": "2026-11-08"})
m3 = date_di._message
check("assenza con date: inizio = 1 novembre a mezzanotte",
a_roma(m3.current_time) == "2026-11-01 00:00", a_roma(m3.current_time))
check("assenza con date: fine = mezzanotte dell'8 (da li' riparte)",
a_roma(m3.end_time) == "2026-11-08 00:00", a_roma(m3.end_time))
check("assenza con date: casa a 15 °C", abs(m3.target_room_temperature - 15.0) < 1e-6)
rifiuta("assenza: fine prima dell'inizio", "away_mode_on", None, 1, ctx,
{"start": "2026-11-08", "end": "2026-11-01"})
rifiuta("assenza: data illeggibile", "away_mode_on", None, 1, ctx,
{"start": "domani", "end": "2026-11-08"})
rifiuta("assenza: oltre 180 giorni", "away_mode_on", None, 1, ctx,
{"start": "2026-11-01", "end": "2028-11-08"})
fra = (datetime.now(timezone.utc) + timedelta(minutes=20)).isoformat().replace("+00:00", "Z")
check("boost in corso: restano ~1200 secondi",
abs(b.boost_restanti({"hot_water_boost": {"end_time": fra}}) - 1200) <= 3)
check("boost finito: zero secondi",
b.boost_restanti({"hot_water_boost": {"end_time": "2026-01-01T00:00:00Z"}}) == 0)
check("boost mai avviato: zero secondi", b.boost_restanti({}) == 0)
riassunto = b.summarize({"hot_water_boost": {"end_time": fra}})
check("riassunto: dice che il boost e' in corso", riassunto.get("boost_active") is True)
check("riassunto: porta i secondi del boost",
abs(riassunto.get("boost_remaining_seconds", 0) - 1200) <= 3,
str(riassunto.get("boost_remaining_seconds")))
check("riassunto senza boost: spento e zero",
b.summarize({}).get("boost_active") is False
and b.summarize({}).get("boost_remaining_seconds") == 0)
# --- esito ---------------------------------------------------------------- # # --- esito ---------------------------------------------------------------- #
falliti = [n for n, ok, _ in esiti if not ok] falliti = [n for n, ok, _ in esiti if not ok]
print(f"\n{len(esiti) - len(falliti)}/{len(esiti)} controlli superati") print(f"\n{len(esiti) - len(falliti)}/{len(esiti)} controlli superati")
+47 -4
View File
@@ -399,7 +399,50 @@ uno resta senza argomento obbligatorio si vede lì (74/74). Provato dal vivo con
un `ping` (200, con la ricezione dall'impianto) e con un boost fuori intervallo, un `ping` (200, con la ricezione dall'impianto) e con un boost fuori intervallo,
respinto con 400 **prima** di partire. respinto con 400 **prima** di partire.
**Stato attuale (0.6.9).** 85/85 verifiche del nucleo, 74/74 del bridge, ## 11. Assenza con le date e conto alla rovescia del boost — v0.7.0
191/191 stringhe tradotte, `qmllint` pulito, contratto UI↔bridge rispettato.
All'impianto è stato inviato un solo comando, di prova e innocuo: un `ping` **Richieste dell'utente (11/10/2026), da applicare come dati:**
diagnostico, che non cambia nulla. Nessun comando di regolazione è mai partito.
1. il **boost** deve essere di **30 minuti**;
2. l'**assenza** si imposta da un **popup con data di inizio e fine**;
3. durante l'assenza l'impianto **tiene la casa a 15 °C** e **riprende
normalmente il giorno prima del rientro**;
4. mentre il boost è in corso si deve vedere un **conto alla rovescia da 30:00 a
00:00**.
## 12. Conseguenze dell'uso reale (0.7.0)
Deciso con l'uso, non in anticipo:
- il **boost** è di **mezz'ora** (era un'ora);
- l'**assenza** non è più "N giorni": si scelgono **due date** in un popup, e la
casa resta a **15 °C**;
- mentre il boost corre, **conto alla rovescia** in Home e in copertina.
Resta aperto: il popup chiede *inizio* e *fine*, dove "fine" è il giorno in cui
l'impianto ricomincia a scaldare (nell'uso di Carlo: il giorno prima del rientro).
Se preferisce indicare il *rientro* e far togliere un giorno all'app, è una riga.
**Come è stato fatto (0.7.0).**
- Bridge: i comandi con durata hanno il loro ramo e i default in cima al file;
per l'assenza `_periodo_assenza` legge `start` ed `end` (AAAA-MM-GG) e li
traduce in `current_time`/`end_time`. La fine è la **mezzanotte del giorno
indicato nel fuso dell'impianto** (`PLANT_TZ = Europe/Rome`, dichiarato dallo
stato): da lì la pompa riprende il programma. Il fuso della *macchina* non si
usa: alle date di cambio ora sbaglia di un'ora (lo hanno visto i test).
- Bridge: `/summary` espone `boost_active` e `boost_remaining_seconds`, calcolati
dalla fine del boost letta dall'impianto (`hot_water_boost.end_time`). Il conto
alla rovescia dell'app si basa su quelli, non su quando l'app ha visto partire
il comando: se il boost l'ha avviato il telefono di casa o l'app ufficiale, il
numero è comunque giusto.
- App: `AwayDialog.qml` (Dialog + due `ValueButton` + `DatePickerDialog`, come
nell'esempio della guida Silica), il conto alla rovescia in Home sotto le
letture e in copertina al posto dell'obiettivo dell'acqua calda. La formattazione
`mm:ss` sta in `AiraJson::countdown` (C++, verificata in `tests/core_test.cpp`),
i secondi li fa scorrere un timer del client, che si riallinea a ogni rilettura.
- Catalogo: `lupdate harbour-aira.pro` leggeva solo i C++ e marcava 151 voci QML
come obsolete (l'italiano sarebbe rimasto a metà senza accorgersene). Ora:
`lupdate src qml -ts translations/harbour-aira-it.ts`. 197 stringhe, 0 non
tradotte.
+4 -2
View File
@@ -37,6 +37,8 @@ Valori pronti per la dashboard.
"manual_mode": false, "manual_mode": false,
"force_heating": false, "force_heating": false,
"force_heating_remaining": "0s", "force_heating_remaining": "0s",
"boost_active": false,
"boost_remaining_seconds": 0,
"inline_heater": false, "inline_heater": false,
"hot_water_heating": true, "hot_water_heating": true,
"configured_modes": "PUMP_MODE_STATE_HEATING", "configured_modes": "PUMP_MODE_STATE_HEATING",
@@ -153,9 +155,9 @@ Risposta: `{"ok": true, "id": "...", "result": {...}}`.
| `set_hot_water_target` | 45–60 | temperatura ACS obiettivo | | `set_hot_water_target` | 45–60 | temperatura ACS obiettivo |
| `set_heating_setpoint` | 5–30 | temperatura obiettivo della casa (zona, default 1) | | `set_heating_setpoint` | 5–30 | temperatura obiettivo della casa (zona, default 1) |
| `set_cooling_setpoint` | 5–30 | temperatura obiettivo del raffrescamento (zona, default 1) | | `set_cooling_setpoint` | 5–30 | temperatura obiettivo del raffrescamento (zona, default 1) |
| `hot_water_boost_on` / `hot_water_boost_off` | minuti, 15–240 (default 60) | boost ACS una tantum | | `hot_water_boost_on` / `hot_water_boost_off` | minuti, 15–240 (default 30) | boost ACS una tantum |
| `hot_water_heating_on` / `hot_water_heating_off` | minuti, 15–1440 (default 60) | riscaldamento ACS | | `hot_water_heating_on` / `hot_water_heating_off` | minuti, 15–1440 (default 60) | riscaldamento ACS |
| `away_mode_on` / `away_mode_off` | giorni, 1–60 (default 7) | modalità assenza | | `away_mode_on` / `away_mode_off` | `start` ed `end`, AAAA-MM-GG | assenza con date; la casa resta a 15 °C |
| `night_mode_1h` | — | night mode per un'ora | | `night_mode_1h` | — | night mode per un'ora |
| `force_heating_on` / `force_heating_off` | minuti, 15–240 (default 60) | riscaldamento forzato | | `force_heating_on` / `force_heating_off` | minuti, 15–240 (default 60) | riscaldamento forzato |
| `heating_on` / `heating_off` | — | funzione riscaldamento | | `heating_on` / `heating_off` | — | funzione riscaldamento |
+6 -2
View File
@@ -136,8 +136,12 @@ CoverBackground {
} }
Label { Label {
text: cover.obiettivo("dhw_target") // Col boost in corso questo posto lo prende il conto alla
color: Theme.secondaryColor // rovescia: piu' utile del valore obiettivo.
text: api.boostActive
? qsTr("boost %1").arg(api.boostCountdown)
: cover.obiettivo("dhw_target")
color: api.boostActive ? Theme.highlightColor : Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall font.pixelSize: Theme.fontSizeExtraSmall
width: parent.width - acquaOra.width width: parent.width - acquaOra.width
elide: Text.ElideRight elide: Text.ElideRight
+55
View File
@@ -0,0 +1,55 @@
import QtQuick 2.6
import Sailfish.Silica 1.0
// Assenza con le date (richiesta dell'utente, 11/10/2026): si scelgono inizio e
// fine e il comando parte solo quando si accetta. La "fine" e' il giorno in cui
// l'impianto ricomincia a scaldare: nell'uso di Carlo, il giorno prima del
// rientro. Durante l'assenza la casa resta a 15 °C.
Dialog {
id: away
property date inizio: new Date()
property date fine: new Date(Date.now() + 7 * 24 * 3600000)
// Non si accetta una fine che non viene dopo l'inizio: il bridge la
// respingerebbe comunque, ed e' meglio non far partire il comando.
canAccept: away.fine > away.inizio
Column {
width: parent.width
DialogHeader {
title: qsTr("Away mode")
acceptText: qsTr("Activate")
}
ValueButton {
label: qsTr("From")
value: Qt.formatDate(away.inizio, Qt.SystemLocaleShortDate)
onClicked: {
var scelta = pageStack.push("Sailfish.Silica.DatePickerDialog",
{ "date": away.inizio })
scelta.accepted.connect(function() { away.inizio = scelta.date })
}
}
ValueButton {
label: qsTr("To")
value: Qt.formatDate(away.fine, Qt.SystemLocaleShortDate)
onClicked: {
var scelta = pageStack.push("Sailfish.Silica.DatePickerDialog",
{ "date": away.fine })
scelta.accepted.connect(function() { away.fine = scelta.date })
}
}
Label {
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
text: qsTr("The house stays at 15 °C and starts heating again on the day chosen as \"To\".")
wrapMode: Text.WordWrap
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall
}
}
}
+22 -2
View File
@@ -223,6 +223,18 @@ Page {
} }
} }
// Conto alla rovescia del boost, da 30:00 a 00:00 (richiesta
// dell'utente): si vede solo mentre il boost e' in corso.
Label {
visible: api.configured && api.boostActive
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
text: qsTr("Boost · %1").arg(api.boostCountdown)
color: Theme.highlightColor
font.pixelSize: Theme.fontSizeSmall
font.bold: true
}
Label { Label {
visible: api.configured visible: api.configured
x: Theme.horizontalPageMargin x: Theme.horizontalPageMargin
@@ -272,8 +284,16 @@ Page {
remorse.execute(assenzaBtn, qsTr("Away mode off"), remorse.execute(assenzaBtn, qsTr("Away mode off"),
function() { api.sendCommand("away_mode_off") }) function() { api.sendCommand("away_mode_off") })
} else { } else {
remorse.execute(assenzaBtn, qsTr("Away mode on"), // L'assenza si imposta da un popup con le date
function() { api.sendCommand("away_mode_on") }) // (richiesta dell'utente): si apre qui e il comando
// parte solo se si accetta.
var scelta = pageStack.push(Qt.resolvedUrl("AwayDialog.qml"))
scelta.accepted.connect(function() {
api.sendCommand("away_mode_on", null, {
"start": Qt.formatDate(scelta.inizio, "yyyy-MM-dd"),
"end": Qt.formatDate(scelta.fine, "yyyy-MM-dd")
})
})
} }
} }
} }
+11 -1
View File
@@ -1,6 +1,6 @@
Name: harbour-aira Name: harbour-aira
Summary: Control your Aira heat pump through a local bridge Summary: Control your Aira heat pump through a local bridge
Version: 0.6.9 Version: 0.7.0
Release: 1 Release: 1
Group: Qt/Qt Group: Qt/Qt
License: MIT License: MIT
@@ -37,6 +37,16 @@ Address and token of the bridge live in the app settings (AppConfigLocation);
no personal data is stored in the binary. no personal data is stored in the binary.
%changelog %changelog
* Sun Oct 11 2026 Carlo <carlo@carlobaratto.it> - 0.7.0-1
- assenza con le date: popup con inizio e fine (DatePickerDialog, componenti
verificati nella guida Silica dell'SDK). Il bridge manda inizio e fine come
orari veri, l'impianto tiene la casa a 15 °C e riprende il giorno scelto
come fine (per Carlo: il giorno prima del rientro).
- conto alla rovescia del boost da 30:00 a 00:00, in Home e in copertina: i
secondi vengono dalla fine del boost letta dall'impianto, l'app li fa scorrere.
- boost a mezz'ora (era un'ora): richiesta dell'utente.
- catalogo italiano rifatto: lupdate leggeva solo i C++ e marcava 151 voci dei
QML come obsolete. Ora si passano src e qml espliciti: 197 stringhe tradotte.
* Sun Oct 11 2026 Carlo <carlo@carlobaratto.it> - 0.6.9-1 * Sun Oct 11 2026 Carlo <carlo@carlobaratto.it> - 0.6.9-1
- copertina: azione rapida (P8 del piano). Sulla copertina compare l'icona del - copertina: azione rapida (P8 del piano). Sulla copertina compare l'icona del
boost: si avvia il boost dell'acqua calda senza aprire l'app. Una sola azione, boost: si avvia il boost dell'acqua calda senza aprire l'app. Una sola azione,
+17 -1
View File
@@ -175,15 +175,31 @@ QString monthLabel(const QString &yearMonth)
return QStringLiteral("%1 %2").arg(QLocale().monthName(mese, QLocale::LongFormat)).arg(anno); return QStringLiteral("%1 %2").arg(QLocale().monthName(mese, QLocale::LongFormat)).arg(anno);
} }
QByteArray commandPayload(const QString &id, const QVariant &value) QByteArray commandPayload(const QString &id, const QVariant &value, const QVariantMap &extra)
{ {
QJsonObject o; QJsonObject o;
o.insert(QStringLiteral("id"), id); o.insert(QStringLiteral("id"), id);
if (value.isValid() && !value.isNull()) if (value.isValid() && !value.isNull())
o.insert(QStringLiteral("value"), QJsonValue::fromVariant(value)); o.insert(QStringLiteral("value"), QJsonValue::fromVariant(value));
// Campi in piu' (per esempio start ed end dell'assenza): li passa al bridge
// cosi' come sono, saltando quelli vuoti.
for (QVariantMap::const_iterator it = extra.constBegin(); it != extra.constEnd(); ++it) {
const QVariant v = it.value();
if (v.isValid() && !v.isNull() && !v.toString().isEmpty())
o.insert(it.key(), QJsonValue::fromVariant(v));
}
return QJsonDocument(o).toJson(QJsonDocument::Compact); return QJsonDocument(o).toJson(QJsonDocument::Compact);
} }
QString countdown(int seconds)
{
if (seconds < 0)
seconds = 0;
return QStringLiteral("%1:%2")
.arg(seconds / 60, 2, 10, QLatin1Char('0'))
.arg(seconds % 60, 2, 10, QLatin1Char('0'));
}
QString errorMessage(const QByteArray &body, int httpStatus) QString errorMessage(const QByteArray &body, int httpStatus)
{ {
QJsonParseError err; QJsonParseError err;
+8 -2
View File
@@ -59,8 +59,14 @@ QString formatMoney(const QVariant &value, const QString &currencyCode);
QString monthLabel(const QString &yearMonth); QString monthLabel(const QString &yearMonth);
// Corpo JSON per POST /command. value viene omesso se nullo (i comandi // Corpo JSON per POST /command. value viene omesso se nullo (i comandi
// semplici non hanno parametri). // semplici non hanno parametri). `extra` aggiunge campi in piu' (per esempio
QByteArray commandPayload(const QString &id, const QVariant &value); // start/end dell'assenza): passano al bridge saltando i vuoti.
QByteArray commandPayload(const QString &id, const QVariant &value,
const QVariantMap &extra = QVariantMap());
// Conto alla rovescia leggibile da secondi: 1800 -> "30:00", 65 -> "01:05",
// 0 -> "00:00". Oltre l'ora i minuti continuano (240 min -> "240:00").
QString countdown(int seconds);
// Messaggio d'errore leggibile da una risposta del bridge // Messaggio d'errore leggibile da una risposta del bridge
// ({"ok":false,"error":"..."}), con fallback sullo stato HTTP. // ({"ok":false,"error":"..."}), con fallback sullo stato HTTP.
+51 -2
View File
@@ -53,6 +53,14 @@ ApiClient::ApiClient(Settings *settings, QObject *parent)
m_refreshTimer->start(); m_refreshTimer->start();
}); });
// Un secondo alla volta mentre il boost e' in corso: i secondi veri li da'
// il bridge a ogni rilettura (ogni 30 s), qui si scorre in mezzo.
m_boostActive = false;
m_boostRemaining = 0;
m_boostTimer = new QTimer(this);
m_boostTimer->setInterval(1000);
connect(m_boostTimer, &QTimer::timeout, this, &ApiClient::tickBoost);
if (m_settings) { if (m_settings) {
// La UI deve riflettere subito il passaggio da "non configurato" a // La UI deve riflettere subito il passaggio da "non configurato" a
// "configurato" quando si salvano indirizzo o token. // "configurato" quando si salvano indirizzo o token.
@@ -186,6 +194,7 @@ void ApiClient::refresh()
return; return;
} }
m_summary = summary; m_summary = summary;
setBoostFromSummary(summary);
if (m_stale) { if (m_stale) {
m_stale = false; m_stale = false;
emit staleChanged(); emit staleChanged();
@@ -295,14 +304,14 @@ void ApiClient::refreshStats()
}); });
} }
void ApiClient::sendCommand(const QString &id, const QVariant &value) void ApiClient::sendCommand(const QString &id, const QVariant &value, const QVariantMap &extra)
{ {
if (!configured()) { if (!configured()) {
emit commandFinished(false, tr("Set the bridge address and token")); emit commandFinished(false, tr("Set the bridge address and token"));
return; return;
} }
const QByteArray payload = AiraJson::commandPayload(id, value); const QByteArray payload = AiraJson::commandPayload(id, value, extra);
qDebug() << "[api] comando:" << id; // mai i valori: qui potrebbe passare altro qDebug() << "[api] comando:" << id; // mai i valori: qui potrebbe passare altro
QNetworkReply *reply = send("POST", QStringLiteral("/command"), payload, 30); QNetworkReply *reply = send("POST", QStringLiteral("/command"), payload, 30);
@@ -332,3 +341,43 @@ void ApiClient::sendCommand(const QString &id, const QVariant &value)
refresh(); // i valori letti cambiano: rileggo refresh(); // i valori letti cambiano: rileggo
}); });
} }
QString ApiClient::boostCountdown() const
{
return AiraJson::countdown(m_boostRemaining);
}
// I secondi veri arrivano dal riassunto: il bridge li ricava dalla FINE del
// boost letta dall'impianto. Il timer li fa solo scorrere fra una lettura e
// l'altra, cosi' il conto non riparte da capo quando si riapre la app (e non
// sbaglia se il boost e' stato avviato da un'altra parte).
void ApiClient::setBoostFromSummary(const QVariantMap &summary)
{
const int restanti = summary.value(QStringLiteral("boost_remaining_seconds")).toInt();
const bool attivo = restanti > 0;
if (!attivo && !m_boostActive && m_boostRemaining == 0) {
m_boostTimer->stop(); // niente da mostrare: non disturbo
return;
}
m_boostRemaining = restanti;
m_boostActive = attivo;
if (attivo)
m_boostTimer->start();
else
m_boostTimer->stop();
emit boostChanged();
}
void ApiClient::tickBoost()
{
if (m_boostRemaining > 0)
--m_boostRemaining;
if (m_boostRemaining <= 0) {
m_boostRemaining = 0;
m_boostActive = false;
m_boostTimer->stop(); // arrivato a 00:00
}
emit boostChanged();
}
+19 -1
View File
@@ -35,6 +35,13 @@ class ApiClient : public QObject
// messaggio e' lo stesso ovunque si stia. // messaggio e' lo stesso ovunque si stia.
Q_PROPERTY(QString notice READ notice NOTIFY noticeChanged) Q_PROPERTY(QString notice READ notice NOTIFY noticeChanged)
Q_PROPERTY(bool noticeIsError READ noticeIsError NOTIFY noticeChanged) Q_PROPERTY(bool noticeIsError READ noticeIsError NOTIFY noticeChanged)
// Conto alla rovescia del boost ACS (richiesta dell'utente: da 30:00 a
// 00:00). I secondi arrivano dal bridge, che li calcola dalla FINE del
// boost letta dall'impianto: cosi' il numero non dipende dal momento in cui
// l'app ha visto partire il comando. Qui scorrono di secondo in secondo.
Q_PROPERTY(bool boostActive READ boostActive NOTIFY boostChanged)
Q_PROPERTY(int boostRemaining READ boostRemaining NOTIFY boostChanged)
Q_PROPERTY(QString boostCountdown READ boostCountdown NOTIFY boostChanged)
public: public:
explicit ApiClient(Settings *settings, QObject *parent = 0); explicit ApiClient(Settings *settings, QObject *parent = 0);
@@ -48,6 +55,9 @@ public:
bool hasData() const { return !m_summary.isEmpty(); } bool hasData() const { return !m_summary.isEmpty(); }
QString notice() const { return m_notice; } QString notice() const { return m_notice; }
bool noticeIsError() const { return m_noticeIsError; } bool noticeIsError() const { return m_noticeIsError; }
bool boostActive() const { return m_boostActive; }
int boostRemaining() const { return m_boostRemaining; }
QString boostCountdown() const;
// Stato leggibile in una riga (stateShort) e per esteso (stateLong), piu' // Stato leggibile in una riga (stateShort) e per esteso (stateLong), piu'
// il livello da cui la UI prende il colore. Stanno in C++ perche' sono // il livello da cui la UI prende il colore. Stanno in C++ perche' sono
@@ -66,7 +76,9 @@ public:
Q_INVOKABLE void testConnection(); Q_INVOKABLE void testConnection();
// Invia un comando: sendCommand("set_hot_water_target", 55). // Invia un comando: sendCommand("set_hot_water_target", 55).
Q_INVOKABLE void sendCommand(const QString &id, const QVariant &value = QVariant()); // `extra` aggiunge campi al corpo JSON (start/end dell'assenza).
Q_INVOKABLE void sendCommand(const QString &id, const QVariant &value = QVariant(),
const QVariantMap &extra = QVariantMap());
// Formattazione per la UI: unica fonte di verita' in C++ (testata in // Formattazione per la UI: unica fonte di verita' in C++ (testata in
// tests/core_test.cpp), niente duplicazione di logica nel QML. // tests/core_test.cpp), niente duplicazione di logica nel QML.
@@ -82,6 +94,7 @@ signals:
void staleChanged(); void staleChanged();
void lastUpdateChanged(); void lastUpdateChanged();
void noticeChanged(); void noticeChanged();
void boostChanged();
void summaryChanged(const QVariantMap &summary); void summaryChanged(const QVariantMap &summary);
void statsChanged(const QVariantMap &stats); void statsChanged(const QVariantMap &stats);
void statsRefreshed(bool ok, const QString &message); void statsRefreshed(bool ok, const QString &message);
@@ -96,6 +109,8 @@ private:
void setBusy(bool value); void setBusy(bool value);
// Mostra un messaggio in fondo alla pagina per qualche secondo. // Mostra un messaggio in fondo alla pagina per qualche secondo.
void setNotice(const QString &message, bool isError); void setNotice(const QString &message, bool isError);
void setBoostFromSummary(const QVariantMap &summary);
void tickBoost();
Settings *m_settings; Settings *m_settings;
QNetworkAccessManager m_nam; QNetworkAccessManager m_nam;
@@ -108,6 +123,9 @@ private:
QTimer *m_refreshTimer; QTimer *m_refreshTimer;
QVariantMap m_summary; QVariantMap m_summary;
QVariantMap m_stats; QVariantMap m_stats;
bool m_boostActive;
int m_boostRemaining;
QTimer *m_boostTimer;
}; };
#endif // APICLIENT_H #endif // APICLIENT_H
+2 -2
View File
@@ -21,7 +21,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
app->setOrganizationName(QStringLiteral("harbour")); app->setOrganizationName(QStringLiteral("harbour"));
app->setApplicationName(QStringLiteral("aira")); app->setApplicationName(QStringLiteral("aira"));
app->setApplicationVersion(QStringLiteral("0.6.9")); app->setApplicationVersion(QStringLiteral("0.7.0"));
Settings settings; Settings settings;
ApiClient api(&settings); ApiClient api(&settings);
@@ -36,7 +36,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
// Prima riga del log: dice subito se il device sta girando la build nuova // Prima riga del log: dice subito se il device sta girando la build nuova
// o una stale (trappola nota del build engine). // o una stale (trappola nota del build engine).
qDebug() << "harbour-aira v0.6.9 build" << __DATE__ << __TIME__; qDebug() << "harbour-aira v0.7.0 build" << __DATE__ << __TIME__;
view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings); view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings);
view->rootContext()->setContextProperty(QStringLiteral("api"), &api); view->rootContext()->setContextProperty(QStringLiteral("api"), &api);
+20
View File
@@ -114,6 +114,26 @@ int main(int argc, char *argv[])
AiraJson::commandPayload("hot_water_boost_on", QVariant())).object(); AiraJson::commandPayload("hot_water_boost_on", QVariant())).object();
check(c2.contains("value") == false, "comando senza parametri: nessun campo value"); check(c2.contains("value") == false, "comando senza parametri: nessun campo value");
// Il popup dell'assenza manda start ed end: devono arrivare nel corpo, e i
// campi vuoti non devono entrarci.
const QJsonObject c3 = QJsonDocument::fromJson(AiraJson::commandPayload(
"away_mode_on", QVariant(), QVariantMap{
{ QStringLiteral("start"), QStringLiteral("2026-11-01") },
{ QStringLiteral("end"), QStringLiteral("2026-11-08") },
{ QStringLiteral("zone"), QString() } })).object();
check(c3.value("start").toString() == "2026-11-01", "campo start nel corpo");
check(c3.value("end").toString() == "2026-11-08", "campo end nel corpo");
check(c3.contains("value") == false, "assenza con date: nessun campo value");
check(c3.contains("zone") == false, "campo vuoto: non entra nel corpo");
// ---- countdown (conto alla rovescia del boost) ------------------------ //
check(AiraJson::countdown(1800) == "30:00", "mezz'ora -> 30:00");
check(AiraJson::countdown(65) == "01:05", "65 secondi -> 01:05");
check(AiraJson::countdown(59) == "00:59", "59 secondi -> 00:59");
check(AiraJson::countdown(0) == "00:00", "zero -> 00:00");
check(AiraJson::countdown(-5) == "00:00", "negativo -> 00:00");
check(AiraJson::countdown(240 * 60) == "240:00", "quattro ore -> 240:00");
// ---- errorMessage ----------------------------------------------------- // // ---- errorMessage ----------------------------------------------------- //
check(AiraJson::errorMessage(R"({"ok":false,"error":"token mancante o errato"})", 401) check(AiraJson::errorMessage(R"({"ok":false,"error":"token mancante o errato"})", 401)
== "token mancante o errato", "messaggio d'errore dal corpo"); == "token mancante o errato", "messaggio d'errore dal corpo");
+85 -48
View File
@@ -54,97 +54,97 @@
<translation>In funzione</translation> <translation>In funzione</translation>
</message> </message>
<message> <message>
<location filename="../src/airajson.cpp" line="197"/> <location filename="../src/airajson.cpp" line="213"/>
<source>server unreachable</source> <source>server unreachable</source>
<translation>server non raggiungibile</translation> <translation>server non raggiungibile</translation>
</message> </message>
<message> <message>
<location filename="../src/airajson.cpp" line="198"/> <location filename="../src/airajson.cpp" line="214"/>
<source>HTTP error %1</source> <source>HTTP error %1</source>
<translation>errore HTTP %1</translation> <translation>errore HTTP %1</translation>
</message> </message>
<message> <message>
<location filename="../src/airajson.cpp" line="238"/> <location filename="../src/airajson.cpp" line="254"/>
<source>No data</source> <source>No data</source>
<translation>Nessun dato</translation> <translation>Nessun dato</translation>
</message> </message>
<message> <message>
<location filename="../src/airajson.cpp" line="242"/> <location filename="../src/airajson.cpp" line="258"/>
<source>Attention: one error</source> <source>Attention: one error</source>
<translation>Attenzione: un errore</translation> <translation>Attenzione: un errore</translation>
</message> </message>
<message> <message>
<location filename="../src/airajson.cpp" line="243"/> <location filename="../src/airajson.cpp" line="259"/>
<source>Attention: %1 errors</source> <source>Attention: %1 errors</source>
<translation>Attenzione: %1 errori</translation> <translation>Attenzione: %1 errori</translation>
</message> </message>
<message> <message>
<location filename="../src/airajson.cpp" line="245"/> <location filename="../src/airajson.cpp" line="261"/>
<source>Away mode</source> <source>Away mode</source>
<translation>Assenza</translation> <translation>Assenza</translation>
</message> </message>
<message> <message>
<location filename="../src/airajson.cpp" line="247"/> <location filename="../src/airajson.cpp" line="263"/>
<source>Night mode</source> <source>Night mode</source>
<translation>Modalità notte</translation> <translation>Modalità notte</translation>
</message> </message>
<message> <message>
<location filename="../src/airajson.cpp" line="249"/> <location filename="../src/airajson.cpp" line="265"/>
<source>Heating at full power</source> <source>Heating at full power</source>
<translation>Riscaldamento al massimo</translation> <translation>Riscaldamento al massimo</translation>
</message> </message>
<message> <message>
<location filename="../src/airajson.cpp" line="251"/> <location filename="../src/airajson.cpp" line="267"/>
<source>Heating the hot water</source> <source>Heating the hot water</source>
<translation>Sta scaldando l&apos;acqua</translation> <translation>Sta scaldando l&apos;acqua</translation>
</message> </message>
<message> <message>
<location filename="../src/airajson.cpp" line="253"/> <location filename="../src/airajson.cpp" line="269"/>
<source>Working</source> <source>Working</source>
<translation>In funzione</translation> <translation>In funzione</translation>
</message> </message>
<message> <message>
<location filename="../src/airajson.cpp" line="254"/> <location filename="../src/airajson.cpp" line="270"/>
<source>Everything is fine</source> <source>Everything is fine</source>
<translation>Tutto regolare</translation> <translation>Tutto regolare</translation>
</message> </message>
<message> <message>
<location filename="../src/airajson.cpp" line="264"/> <location filename="../src/airajson.cpp" line="280"/>
<source>Attention: one error on the plant</source> <source>Attention: one error on the plant</source>
<translation>Attenzione: un errore sull&apos;impianto</translation> <translation>Attenzione: un errore sull&apos;impianto</translation>
</message> </message>
<message> <message>
<location filename="../src/airajson.cpp" line="265"/> <location filename="../src/airajson.cpp" line="281"/>
<source>Attention: %1 errors on the plant</source> <source>Attention: %1 errors on the plant</source>
<translation>Attenzione: %1 errori sull&apos;impianto</translation> <translation>Attenzione: %1 errori sull&apos;impianto</translation>
</message> </message>
<message> <message>
<location filename="../src/airajson.cpp" line="267"/> <location filename="../src/airajson.cpp" line="283"/>
<source>Away mode: the house is saving energy.</source> <source>Away mode: the house is saving energy.</source>
<translation>Modalità assenza: la casa sta risparmiando.</translation> <translation>Modalità assenza: la casa sta risparmiando.</translation>
</message> </message>
<message> <message>
<location filename="../src/airajson.cpp" line="269"/> <location filename="../src/airajson.cpp" line="285"/>
<source>Night mode is on.</source> <source>Night mode is on.</source>
<translation>Modalità notte attiva.</translation> <translation>Modalità notte attiva.</translation>
</message> </message>
<message> <message>
<location filename="../src/airajson.cpp" line="271"/> <location filename="../src/airajson.cpp" line="287"/>
<source>Boost: the house is heating at full power.</source> <source>Boost: the house is heating at full power.</source>
<translation>Boost: la casa sta scaldando a pieno regime.</translation> <translation>Boost: la casa sta scaldando a pieno regime.</translation>
</message> </message>
<message> <message>
<location filename="../src/airajson.cpp" line="273"/> <location filename="../src/airajson.cpp" line="289"/>
<source>The heat pump is heating the hot water.</source> <source>The heat pump is heating the hot water.</source>
<translation>La pompa di calore sta scaldando l&apos;acqua calda.</translation> <translation>La pompa di calore sta scaldando l&apos;acqua calda.</translation>
</message> </message>
<message> <message>
<location filename="../src/airajson.cpp" line="275"/> <location filename="../src/airajson.cpp" line="291"/>
<source>The heat pump is working.</source> <source>The heat pump is working.</source>
<translation>La pompa di calore sta lavorando.</translation> <translation>La pompa di calore sta lavorando.</translation>
</message> </message>
<message> <message>
<location filename="../src/airajson.cpp" line="276"/> <location filename="../src/airajson.cpp" line="292"/>
<source>Relax: the heat pump is running smoothly.</source> <source>Relax: the heat pump is running smoothly.</source>
<translation>Relax: la pompa di calore funziona regolarmente.</translation> <translation>Relax: la pompa di calore funziona regolarmente.</translation>
</message> </message>
@@ -152,70 +152,98 @@
<context> <context>
<name>ApiClient</name> <name>ApiClient</name>
<message> <message>
<location filename="../src/apiclient.cpp" line="124"/> <location filename="../src/apiclient.cpp" line="132"/>
<source>Bridge address not configured</source> <source>Bridge address not configured</source>
<translation>Indirizzo del bridge non configurato</translation> <translation>Indirizzo del bridge non configurato</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="146"/> <location filename="../src/apiclient.cpp" line="154"/>
<location filename="../src/apiclient.cpp" line="258"/> <location filename="../src/apiclient.cpp" line="267"/>
<location filename="../src/apiclient.cpp" line="301"/> <location filename="../src/apiclient.cpp" line="310"/>
<source>Set the bridge address and token</source> <source>Set the bridge address and token</source>
<translation>Configura indirizzo e token del bridge</translation> <translation>Configura indirizzo e token del bridge</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="166"/> <location filename="../src/apiclient.cpp" line="174"/>
<source>Server unreachable (timeout)</source> <source>Server unreachable (timeout)</source>
<translation>Server non raggiungibile (timeout)</translation> <translation>Server non raggiungibile (timeout)</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="185"/> <location filename="../src/apiclient.cpp" line="193"/>
<location filename="../src/apiclient.cpp" line="289"/> <location filename="../src/apiclient.cpp" line="298"/>
<source>Unreadable response from the bridge</source> <source>Unreadable response from the bridge</source>
<translation>Risposta del bridge non interpretabile</translation> <translation>Risposta del bridge non interpretabile</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="205"/> <location filename="../src/apiclient.cpp" line="214"/>
<source>Enter the bridge address</source> <source>Enter the bridge address</source>
<translation>Inserisci l&apos;indirizzo del bridge</translation> <translation>Inserisci l&apos;indirizzo del bridge</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="221"/> <location filename="../src/apiclient.cpp" line="230"/>
<source>Timeout: no response</source> <source>Timeout: no response</source>
<translation>Timeout: nessuna risposta</translation> <translation>Timeout: nessuna risposta</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="222"/> <location filename="../src/apiclient.cpp" line="231"/>
<source>Unreachable (HTTP %1)</source> <source>Unreachable (HTTP %1)</source>
<translation>Irraggiungibile (HTTP %1)</translation> <translation>Irraggiungibile (HTTP %1)</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="226"/> <location filename="../src/apiclient.cpp" line="235"/>
<source>Bridge reachable</source> <source>Bridge reachable</source>
<translation>Bridge raggiungibile</translation> <translation>Bridge raggiungibile</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="266"/> <location filename="../src/apiclient.cpp" line="275"/>
<location filename="../src/apiclient.cpp" line="310"/> <location filename="../src/apiclient.cpp" line="319"/>
<source>Bridge not configured</source> <source>Bridge not configured</source>
<translation>Bridge non configurato</translation> <translation>Bridge non configurato</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="278"/> <location filename="../src/apiclient.cpp" line="287"/>
<source>Timeout: no statistics</source> <source>Timeout: no statistics</source>
<translation>Timeout: statistiche non disponibili</translation> <translation>Timeout: statistiche non disponibili</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="324"/> <location filename="../src/apiclient.cpp" line="333"/>
<source>Timeout: command not sent</source> <source>Timeout: command not sent</source>
<translation>Timeout: comando non inviato</translation> <translation>Timeout: comando non inviato</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="331"/> <location filename="../src/apiclient.cpp" line="340"/>
<source>Command sent</source> <source>Command sent</source>
<translation>Comando inviato</translation> <translation>Comando inviato</translation>
</message> </message>
</context> </context>
<context>
<name>AwayDialog</name>
<message>
<location filename="../qml/pages/AwayDialog.qml" line="22"/>
<source>Away mode</source>
<translation>Assenza</translation>
</message>
<message>
<location filename="../qml/pages/AwayDialog.qml" line="23"/>
<source>Activate</source>
<translation>Attiva</translation>
</message>
<message>
<location filename="../qml/pages/AwayDialog.qml" line="27"/>
<source>From</source>
<translation>Dal</translation>
</message>
<message>
<location filename="../qml/pages/AwayDialog.qml" line="37"/>
<source>To</source>
<translation>Al</translation>
</message>
<message>
<location filename="../qml/pages/AwayDialog.qml" line="49"/>
<source>The house stays at 15 °C and starts heating again on the day chosen as &quot;To&quot;.</source>
<translation>La casa resta a 15 °C e riprende a scaldare il giorno scelto come "Al".</translation>
</message>
</context>
<context> <context>
<name>CommandsPage</name> <name>CommandsPage</name>
<message> <message>
@@ -347,12 +375,17 @@
<translation>Acqua calda</translation> <translation>Acqua calda</translation>
</message> </message>
<message> <message>
<location filename="../qml/cover/CoverPage.qml" line="159"/> <location filename="../qml/cover/CoverPage.qml" line="142"/>
<source>boost %1</source>
<translation>boost %1</translation>
</message>
<message>
<location filename="../qml/cover/CoverPage.qml" line="163"/>
<source>Not updated · last %1</source> <source>Not updated · last %1</source>
<translation>Non aggiornato · ultimo %1</translation> <translation>Non aggiornato · ultimo %1</translation>
</message> </message>
<message> <message>
<location filename="../qml/cover/CoverPage.qml" line="160"/> <location filename="../qml/cover/CoverPage.qml" line="164"/>
<source>outside %1 °C</source> <source>outside %1 °C</source>
<translation>esterna %1 °C</translation> <translation>esterna %1 °C</translation>
</message> </message>
@@ -530,47 +563,51 @@
<translation>Acqua calda</translation> <translation>Acqua calda</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="230"/> <location filename="../qml/pages/MainPage.qml" line="232"/>
<source>Boost · %1</source>
<translation>Boost · %1</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="242"/>
<source>Tap a value to adjust it</source> <source>Tap a value to adjust it</source>
<translation>Tocca un valore per regolarlo</translation> <translation>Tocca un valore per regolarlo</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="238"/> <location filename="../qml/pages/MainPage.qml" line="250"/>
<source>Quick actions</source> <source>Quick actions</source>
<translation>Azioni rapide</translation> <translation>Azioni rapide</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="250"/> <location filename="../qml/pages/MainPage.qml" line="262"/>
<source>Boost</source> <source>Boost</source>
<translation>Boost</translation> <translation>Boost</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="266"/> <location filename="../qml/pages/MainPage.qml" line="278"/>
<source>Home</source> <source>Home</source>
<translation>Torna</translation> <translation>Torna</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="266"/> <location filename="../qml/pages/MainPage.qml" line="278"/>
<source>Away</source> <source>Away</source>
<translation>Assenza</translation> <translation>Assenza</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="272"/> <location filename="../qml/pages/MainPage.qml" line="284"/>
<source>Away mode off</source> <source>Away mode off</source>
<translation>Assenza disattivata</translation> <translation>Assenza disattivata</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="275"/>
<source>Away mode on</source> <source>Away mode on</source>
<translation>Assenza attivata</translation> <translation type="vanished">Assenza attivata</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="285"/> <location filename="../qml/pages/MainPage.qml" line="305"/>
<source>More</source> <source>More</source>
<translation>Altro</translation> <translation>Altro</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="308"/> <location filename="../qml/pages/MainPage.qml" line="328"/>
<source>Outside %1 °C · Thermostat %2 °C · Zones %3</source> <source>Outside %1 °C · Thermostat %2 °C · Zones %3</source>
<translation>Esterno %1 °C · Termostato %2 °C · Zone %3</translation> <translation>Esterno %1 °C · Termostato %2 °C · Zone %3</translation>
</message> </message>
@@ -585,7 +622,7 @@
<translation>Bridge non configurato. Apri le impostazioni e inserisci l&apos;indirizzo del server e il token.</translation> <translation>Bridge non configurato. Apri le impostazioni e inserisci l&apos;indirizzo del server e il token.</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="256"/> <location filename="../qml/pages/MainPage.qml" line="268"/>
<source>Starting DHW boost</source> <source>Starting DHW boost</source>
<translation>Avvio del boost ACS</translation> <translation>Avvio del boost ACS</translation>
</message> </message>