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:
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 |
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -59,8 +59,14 @@ QString formatMoney(const QVariant &value, const QString ¤cyCode);
|
|||||||
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
@@ -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
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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'acqua</translation>
|
<translation>Sta scaldando l'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'impianto</translation>
|
<translation>Attenzione: un errore sull'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'impianto</translation>
|
<translation>Attenzione: %1 errori sull'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'acqua calda.</translation>
|
<translation>La pompa di calore sta scaldando l'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'indirizzo del bridge</translation>
|
<translation>Inserisci l'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 "To".</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'indirizzo del server e il token.</translation>
|
<translation>Bridge non configurato. Apri le impostazioni e inserisci l'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>
|
||||||
|
|||||||
Reference in New Issue
Block a user