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
|
||||
impianto — ognuna con la sua icona. Sta in una schermata, senza scorrere: i
|
||||
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
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
mode and the plant commands, each with its own icon. It fits without scrolling
|
||||
— 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
|
||||
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
|
||||
|
||||
+75
-11
@@ -35,9 +35,10 @@ import sys
|
||||
import threading
|
||||
import traceback
|
||||
import warnings
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, time, timedelta, timezone
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from google.protobuf.duration_pb2 import Duration
|
||||
from google.protobuf.timestamp_pb2 import Timestamp
|
||||
@@ -236,6 +237,8 @@ def summarize(st: dict) -> dict:
|
||||
return None
|
||||
return round(float(valore) / 10.0, 1)
|
||||
|
||||
boost_restano = boost_restanti(st)
|
||||
|
||||
return {
|
||||
"outdoor_temperature": g("current_outdoor_temperature"),
|
||||
"indoor_temperature": g("deprecated_current_indoor_temperature"),
|
||||
@@ -243,6 +246,8 @@ def summarize(st: dict) -> dict:
|
||||
"dhw_target": g("target_hot_water_temperature"),
|
||||
"operating_status": g("operating_status"),
|
||||
"away_mode": bool(st.get("away_mode_enabled")),
|
||||
"boost_active": boost_restano > 0,
|
||||
"boost_remaining_seconds": boost_restano,
|
||||
"night_mode": bool(st.get("night_mode_enabled")),
|
||||
"manual_mode": bool(st.get("manual_mode_enabled")),
|
||||
# force_heating E' un oggetto {enabled, remaining_time}: leggerlo con
|
||||
@@ -462,22 +467,26 @@ def build_stats(aira, heat_pump_id: str, household_id: str,
|
||||
# nemmeno l'app mandava la durata.
|
||||
# I valori qui sotto sono i default del bridge: sono scelte, non misure. L'app
|
||||
# puo' mandarne altri in `value` (minuti, o giorni per l'assenza).
|
||||
BOOST_MIN_DEFAULT = 60 # boost ACS
|
||||
BOOST_MIN_DEFAULT = 30 # boost ACS (richiesta dell'utente: mezz'ora)
|
||||
ACS_OFF_MIN_DEFAULT = 60 # ACS spento
|
||||
FORCE_MIN_DEFAULT = 60 # riscaldamento forzato
|
||||
AWAY_DAYS_DEFAULT = 7 # assenza
|
||||
AWAY_TARGET_TEMP = 15.0 # casa tenuta a questa temperatura in assenza, °C
|
||||
# Il fuso dell'impianto, NON quello della macchina: il server puo' stare in UTC
|
||||
# e nei giorni di cambio ora si sbaglia di un'ora (e' successo nei test).
|
||||
# Lo stato lo dichiara in `time_zone.time_zone_identifier`.
|
||||
PLANT_TZ = ZoneInfo("Europe/Rome")
|
||||
|
||||
COMMANDS = {
|
||||
"ping": {"help": "verifica che l'impianto risponda", "value": None},
|
||||
"set_hot_water_target": {"help": "temperatura ACS obiettivo in °C", "value": "45-60"},
|
||||
"set_heating_setpoint": {"help": "temperatura obiettivo della casa in °C", "value": "5-30", "zone": "1 o 2 (opzionale, default 1)"},
|
||||
"set_cooling_setpoint": {"help": "temperatura obiettivo del raffrescamento in °C", "value": "5-30", "zone": "1 o 2 (opzionale, default 1)"},
|
||||
"hot_water_boost_on": {"help": "avvia il boost ACS (una tantum)", "value": "minuti, 15-240 (opzionale, default 60)"},
|
||||
"hot_water_boost_on": {"help": "avvia il boost ACS (una tantum)", "value": "minuti, 15-240 (opzionale, default 30)"},
|
||||
"hot_water_boost_off": {"help": "ferma il boost ACS", "value": None},
|
||||
"hot_water_heating_on": {"help": "abilita il riscaldamento ACS", "value": None},
|
||||
"hot_water_heating_off": {"help": "disabilita il riscaldamento ACS", "value": "minuti, 15-1440 (opzionale, default 60)"},
|
||||
"away_mode_on": {"help": "modalita' assenza", "value": "giorni, 1-60 (opzionale, default 7)"},
|
||||
"away_mode_on": {"help": "assenza con date di inizio e fine", "value": "start ed end (AAAA-MM-GG); in alternativa giorni, 1-60"},
|
||||
"away_mode_off": {"help": "disattiva l'assenza", "value": None},
|
||||
"night_mode_1h": {"help": "night mode per un'ora", "value": None},
|
||||
"force_heating_on": {"help": "forza il riscaldamento", "value": "minuti, 15-240 (opzionale, default 60)"},
|
||||
@@ -488,7 +497,7 @@ COMMANDS = {
|
||||
}
|
||||
|
||||
|
||||
def build_command(cmd_id: str, value=None, zone=1, context=None):
|
||||
def build_command(cmd_id: str, value=None, zone=1, context=None, params=None):
|
||||
"""Costruisce l'oggetto comando della libreria. Importata solo qui per non
|
||||
pagarne il costo all'avvio del bridge.
|
||||
|
||||
@@ -585,9 +594,10 @@ def build_command(cmd_id: str, value=None, zone=1, context=None):
|
||||
duration=_durata(value, FORCE_MIN_DEFAULT, 15, 240, cmd_id))
|
||||
|
||||
if cmd_id == "away_mode_on":
|
||||
inizio, fine = _periodo_assenza(params, value)
|
||||
return SetAwayMode(
|
||||
current_time=_adesso(),
|
||||
end_time=_adesso(giorni=_giorni(value)),
|
||||
current_time=inizio,
|
||||
end_time=fine,
|
||||
target_room_temperature=AWAY_TARGET_TEMP,
|
||||
)
|
||||
|
||||
@@ -625,13 +635,67 @@ def _giorni(value) -> int:
|
||||
return g
|
||||
|
||||
|
||||
def _adesso(giorni: int = 0) -> Timestamp:
|
||||
"""Timestamp UTC di adesso, oppure di adesso + N giorni."""
|
||||
def _ts(quando: datetime) -> Timestamp:
|
||||
t = Timestamp()
|
||||
t.FromDatetime(datetime.now(timezone.utc) + timedelta(days=giorni))
|
||||
t.FromDatetime(quando.astimezone(timezone.utc))
|
||||
return t
|
||||
|
||||
|
||||
def _periodo_assenza(params, value):
|
||||
"""Inizio e fine dell'assenza.
|
||||
|
||||
Date in `start`/`end` (AAAA-MM-GG, ora locale); se mancano, `value` = giorni
|
||||
da adesso. La fine e' la mezzanotte locale del giorno indicato: da li'
|
||||
l'impianto riprende il programma normale e la casa risale dai 15 °C.
|
||||
"""
|
||||
params = params or {}
|
||||
oggi = datetime.now(PLANT_TZ).date()
|
||||
|
||||
if not str(params.get("start") or "").strip() and not str(params.get("end") or "").strip():
|
||||
# Compatibilita': senza date, solo i giorni da adesso (nessun giorno di
|
||||
# calendario): e' la forma che usava il bridge prima del popup.
|
||||
adesso = datetime.now(PLANT_TZ)
|
||||
return _ts(adesso), _ts(adesso + timedelta(days=_giorni(value)))
|
||||
|
||||
def giorno(chiave, default):
|
||||
testo = str(params.get(chiave) or "").strip()
|
||||
if not testo:
|
||||
return default
|
||||
try:
|
||||
return datetime.strptime(testo[:10], "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
raise ValueError(f"away_mode_on: data non valida in {chiave}: {testo}")
|
||||
|
||||
inizio = giorno("start", oggi)
|
||||
fine = giorno("end", oggi + timedelta(days=_giorni(value)))
|
||||
if fine <= inizio:
|
||||
raise ValueError("away_mode_on: la fine deve venire dopo l'inizio")
|
||||
if (fine - inizio).days > 180:
|
||||
raise ValueError("away_mode_on: periodo troppo lungo (max 180 giorni)")
|
||||
|
||||
adesso = datetime.now(PLANT_TZ)
|
||||
partenza = datetime.combine(inizio, time.min, tzinfo=PLANT_TZ)
|
||||
if partenza < adesso:
|
||||
partenza = adesso
|
||||
return _ts(partenza), _ts(datetime.combine(fine, time.min, tzinfo=PLANT_TZ))
|
||||
|
||||
|
||||
def boost_restanti(st: dict) -> int:
|
||||
"""Secondi che mancano alla fine del boost ACS (0 se non e' in corso).
|
||||
|
||||
L'impianto espone solo start_time/end_time: il conto alla rovescia dell'app
|
||||
si basa su end_time, non sul momento in cui l'app ha visto partire il boost.
|
||||
"""
|
||||
fine = str((st.get("hot_water_boost") or {}).get("end_time") or "")
|
||||
if not fine:
|
||||
return 0
|
||||
try:
|
||||
quando = datetime.fromisoformat(fine.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return 0
|
||||
return max(0, int((quando - datetime.now(timezone.utc)).total_seconds()))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# server HTTP
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -695,7 +759,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
body = self._body()
|
||||
cmd_id = body.get("id", "")
|
||||
cmd = build_command(cmd_id, body.get("value"), body.get("zone") or 1,
|
||||
self.session.last_states())
|
||||
self.session.last_states(), body)
|
||||
except ValueError as exc:
|
||||
return self._send(400, {"ok": False, "error": str(exc)})
|
||||
except Exception as exc:
|
||||
|
||||
@@ -149,8 +149,8 @@ check("senza termostato: nessun errore",
|
||||
from pyairahome.commands import CommandBase # noqa: E402
|
||||
|
||||
boost = b.build_command("hot_water_boost_on")
|
||||
check("boost: un'ora di default",
|
||||
boost._message.hot_water_boost_duration.seconds == 3600,
|
||||
check("boost: mezz'ora di default (richiesta dell'utente)",
|
||||
boost._message.hot_water_boost_duration.seconds == 1800,
|
||||
f"{boost._message.hot_water_boost_duration.seconds}s")
|
||||
check("boost: durata scelta in minuti (90 -> 5400s)",
|
||||
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,
|
||||
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 ---------------------------------------------------------------- #
|
||||
falliti = [n for n, ok, _ in esiti if not ok]
|
||||
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,
|
||||
respinto con 400 **prima** di partire.
|
||||
|
||||
**Stato attuale (0.6.9).** 85/85 verifiche del nucleo, 74/74 del bridge,
|
||||
191/191 stringhe tradotte, `qmllint` pulito, contratto UI↔bridge rispettato.
|
||||
All'impianto è stato inviato un solo comando, di prova e innocuo: un `ping`
|
||||
diagnostico, che non cambia nulla. Nessun comando di regolazione è mai partito.
|
||||
## 11. Assenza con le date e conto alla rovescia del boost — v0.7.0
|
||||
|
||||
**Richieste dell'utente (11/10/2026), da applicare come dati:**
|
||||
|
||||
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,
|
||||
"force_heating": false,
|
||||
"force_heating_remaining": "0s",
|
||||
"boost_active": false,
|
||||
"boost_remaining_seconds": 0,
|
||||
"inline_heater": false,
|
||||
"hot_water_heating": true,
|
||||
"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_heating_setpoint` | 5–30 | temperatura obiettivo della casa (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 |
|
||||
| `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 |
|
||||
| `force_heating_on` / `force_heating_off` | minuti, 15–240 (default 60) | riscaldamento forzato |
|
||||
| `heating_on` / `heating_off` | — | funzione riscaldamento |
|
||||
|
||||
@@ -136,8 +136,12 @@ CoverBackground {
|
||||
}
|
||||
|
||||
Label {
|
||||
text: cover.obiettivo("dhw_target")
|
||||
color: Theme.secondaryColor
|
||||
// Col boost in corso questo posto lo prende il conto alla
|
||||
// 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
|
||||
width: parent.width - acquaOra.width
|
||||
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 {
|
||||
visible: api.configured
|
||||
x: Theme.horizontalPageMargin
|
||||
@@ -272,8 +284,16 @@ Page {
|
||||
remorse.execute(assenzaBtn, qsTr("Away mode off"),
|
||||
function() { api.sendCommand("away_mode_off") })
|
||||
} else {
|
||||
remorse.execute(assenzaBtn, qsTr("Away mode on"),
|
||||
function() { api.sendCommand("away_mode_on") })
|
||||
// L'assenza si imposta da un popup con le date
|
||||
// (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
|
||||
Summary: Control your Aira heat pump through a local bridge
|
||||
Version: 0.6.9
|
||||
Version: 0.7.0
|
||||
Release: 1
|
||||
Group: Qt/Qt
|
||||
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.
|
||||
|
||||
%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
|
||||
- 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,
|
||||
|
||||
+17
-1
@@ -175,15 +175,31 @@ QString monthLabel(const QString &yearMonth)
|
||||
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;
|
||||
o.insert(QStringLiteral("id"), id);
|
||||
if (value.isValid() && !value.isNull())
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
QJsonParseError err;
|
||||
|
||||
+8
-2
@@ -59,8 +59,14 @@ QString formatMoney(const QVariant &value, const QString ¤cyCode);
|
||||
QString monthLabel(const QString &yearMonth);
|
||||
|
||||
// Corpo JSON per POST /command. value viene omesso se nullo (i comandi
|
||||
// semplici non hanno parametri).
|
||||
QByteArray commandPayload(const QString &id, const QVariant &value);
|
||||
// semplici non hanno parametri). `extra` aggiunge campi in piu' (per esempio
|
||||
// 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
|
||||
// ({"ok":false,"error":"..."}), con fallback sullo stato HTTP.
|
||||
|
||||
+51
-2
@@ -53,6 +53,14 @@ ApiClient::ApiClient(Settings *settings, QObject *parent)
|
||||
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) {
|
||||
// La UI deve riflettere subito il passaggio da "non configurato" a
|
||||
// "configurato" quando si salvano indirizzo o token.
|
||||
@@ -186,6 +194,7 @@ void ApiClient::refresh()
|
||||
return;
|
||||
}
|
||||
m_summary = summary;
|
||||
setBoostFromSummary(summary);
|
||||
if (m_stale) {
|
||||
m_stale = false;
|
||||
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()) {
|
||||
emit commandFinished(false, tr("Set the bridge address and token"));
|
||||
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
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
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.
|
||||
Q_PROPERTY(QString notice READ notice 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:
|
||||
explicit ApiClient(Settings *settings, QObject *parent = 0);
|
||||
@@ -48,6 +55,9 @@ public:
|
||||
bool hasData() const { return !m_summary.isEmpty(); }
|
||||
QString notice() const { return m_notice; }
|
||||
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'
|
||||
// il livello da cui la UI prende il colore. Stanno in C++ perche' sono
|
||||
@@ -66,7 +76,9 @@ public:
|
||||
Q_INVOKABLE void testConnection();
|
||||
|
||||
// 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
|
||||
// tests/core_test.cpp), niente duplicazione di logica nel QML.
|
||||
@@ -82,6 +94,7 @@ signals:
|
||||
void staleChanged();
|
||||
void lastUpdateChanged();
|
||||
void noticeChanged();
|
||||
void boostChanged();
|
||||
void summaryChanged(const QVariantMap &summary);
|
||||
void statsChanged(const QVariantMap &stats);
|
||||
void statsRefreshed(bool ok, const QString &message);
|
||||
@@ -96,6 +109,8 @@ private:
|
||||
void setBusy(bool value);
|
||||
// Mostra un messaggio in fondo alla pagina per qualche secondo.
|
||||
void setNotice(const QString &message, bool isError);
|
||||
void setBoostFromSummary(const QVariantMap &summary);
|
||||
void tickBoost();
|
||||
|
||||
Settings *m_settings;
|
||||
QNetworkAccessManager m_nam;
|
||||
@@ -108,6 +123,9 @@ private:
|
||||
QTimer *m_refreshTimer;
|
||||
QVariantMap m_summary;
|
||||
QVariantMap m_stats;
|
||||
bool m_boostActive;
|
||||
int m_boostRemaining;
|
||||
QTimer *m_boostTimer;
|
||||
};
|
||||
|
||||
#endif // APICLIENT_H
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||
|
||||
app->setOrganizationName(QStringLiteral("harbour"));
|
||||
app->setApplicationName(QStringLiteral("aira"));
|
||||
app->setApplicationVersion(QStringLiteral("0.6.9"));
|
||||
app->setApplicationVersion(QStringLiteral("0.7.0"));
|
||||
|
||||
Settings 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
|
||||
// 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("api"), &api);
|
||||
|
||||
@@ -114,6 +114,26 @@ int main(int argc, char *argv[])
|
||||
AiraJson::commandPayload("hot_water_boost_on", QVariant())).object();
|
||||
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 ----------------------------------------------------- //
|
||||
check(AiraJson::errorMessage(R"({"ok":false,"error":"token mancante o errato"})", 401)
|
||||
== "token mancante o errato", "messaggio d'errore dal corpo");
|
||||
|
||||
@@ -54,97 +54,97 @@
|
||||
<translation>In funzione</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/airajson.cpp" line="197"/>
|
||||
<location filename="../src/airajson.cpp" line="213"/>
|
||||
<source>server unreachable</source>
|
||||
<translation>server non raggiungibile</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/airajson.cpp" line="198"/>
|
||||
<location filename="../src/airajson.cpp" line="214"/>
|
||||
<source>HTTP error %1</source>
|
||||
<translation>errore HTTP %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/airajson.cpp" line="238"/>
|
||||
<location filename="../src/airajson.cpp" line="254"/>
|
||||
<source>No data</source>
|
||||
<translation>Nessun dato</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/airajson.cpp" line="242"/>
|
||||
<location filename="../src/airajson.cpp" line="258"/>
|
||||
<source>Attention: one error</source>
|
||||
<translation>Attenzione: un errore</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/airajson.cpp" line="243"/>
|
||||
<location filename="../src/airajson.cpp" line="259"/>
|
||||
<source>Attention: %1 errors</source>
|
||||
<translation>Attenzione: %1 errori</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/airajson.cpp" line="245"/>
|
||||
<location filename="../src/airajson.cpp" line="261"/>
|
||||
<source>Away mode</source>
|
||||
<translation>Assenza</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/airajson.cpp" line="247"/>
|
||||
<location filename="../src/airajson.cpp" line="263"/>
|
||||
<source>Night mode</source>
|
||||
<translation>Modalità notte</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/airajson.cpp" line="249"/>
|
||||
<location filename="../src/airajson.cpp" line="265"/>
|
||||
<source>Heating at full power</source>
|
||||
<translation>Riscaldamento al massimo</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/airajson.cpp" line="251"/>
|
||||
<location filename="../src/airajson.cpp" line="267"/>
|
||||
<source>Heating the hot water</source>
|
||||
<translation>Sta scaldando l'acqua</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/airajson.cpp" line="253"/>
|
||||
<location filename="../src/airajson.cpp" line="269"/>
|
||||
<source>Working</source>
|
||||
<translation>In funzione</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/airajson.cpp" line="254"/>
|
||||
<location filename="../src/airajson.cpp" line="270"/>
|
||||
<source>Everything is fine</source>
|
||||
<translation>Tutto regolare</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/airajson.cpp" line="264"/>
|
||||
<location filename="../src/airajson.cpp" line="280"/>
|
||||
<source>Attention: one error on the plant</source>
|
||||
<translation>Attenzione: un errore sull'impianto</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/airajson.cpp" line="265"/>
|
||||
<location filename="../src/airajson.cpp" line="281"/>
|
||||
<source>Attention: %1 errors on the plant</source>
|
||||
<translation>Attenzione: %1 errori sull'impianto</translation>
|
||||
</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>
|
||||
<translation>Modalità assenza: la casa sta risparmiando.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/airajson.cpp" line="269"/>
|
||||
<location filename="../src/airajson.cpp" line="285"/>
|
||||
<source>Night mode is on.</source>
|
||||
<translation>Modalità notte attiva.</translation>
|
||||
</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>
|
||||
<translation>Boost: la casa sta scaldando a pieno regime.</translation>
|
||||
</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>
|
||||
<translation>La pompa di calore sta scaldando l'acqua calda.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/airajson.cpp" line="275"/>
|
||||
<location filename="../src/airajson.cpp" line="291"/>
|
||||
<source>The heat pump is working.</source>
|
||||
<translation>La pompa di calore sta lavorando.</translation>
|
||||
</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>
|
||||
<translation>Relax: la pompa di calore funziona regolarmente.</translation>
|
||||
</message>
|
||||
@@ -152,70 +152,98 @@
|
||||
<context>
|
||||
<name>ApiClient</name>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="124"/>
|
||||
<location filename="../src/apiclient.cpp" line="132"/>
|
||||
<source>Bridge address not configured</source>
|
||||
<translation>Indirizzo del bridge non configurato</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="146"/>
|
||||
<location filename="../src/apiclient.cpp" line="258"/>
|
||||
<location filename="../src/apiclient.cpp" line="301"/>
|
||||
<location filename="../src/apiclient.cpp" line="154"/>
|
||||
<location filename="../src/apiclient.cpp" line="267"/>
|
||||
<location filename="../src/apiclient.cpp" line="310"/>
|
||||
<source>Set the bridge address and token</source>
|
||||
<translation>Configura indirizzo e token del bridge</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="166"/>
|
||||
<location filename="../src/apiclient.cpp" line="174"/>
|
||||
<source>Server unreachable (timeout)</source>
|
||||
<translation>Server non raggiungibile (timeout)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="185"/>
|
||||
<location filename="../src/apiclient.cpp" line="289"/>
|
||||
<location filename="../src/apiclient.cpp" line="193"/>
|
||||
<location filename="../src/apiclient.cpp" line="298"/>
|
||||
<source>Unreadable response from the bridge</source>
|
||||
<translation>Risposta del bridge non interpretabile</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="205"/>
|
||||
<location filename="../src/apiclient.cpp" line="214"/>
|
||||
<source>Enter the bridge address</source>
|
||||
<translation>Inserisci l'indirizzo del bridge</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="221"/>
|
||||
<location filename="../src/apiclient.cpp" line="230"/>
|
||||
<source>Timeout: no response</source>
|
||||
<translation>Timeout: nessuna risposta</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="222"/>
|
||||
<location filename="../src/apiclient.cpp" line="231"/>
|
||||
<source>Unreachable (HTTP %1)</source>
|
||||
<translation>Irraggiungibile (HTTP %1)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="226"/>
|
||||
<location filename="../src/apiclient.cpp" line="235"/>
|
||||
<source>Bridge reachable</source>
|
||||
<translation>Bridge raggiungibile</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="266"/>
|
||||
<location filename="../src/apiclient.cpp" line="310"/>
|
||||
<location filename="../src/apiclient.cpp" line="275"/>
|
||||
<location filename="../src/apiclient.cpp" line="319"/>
|
||||
<source>Bridge not configured</source>
|
||||
<translation>Bridge non configurato</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="278"/>
|
||||
<location filename="../src/apiclient.cpp" line="287"/>
|
||||
<source>Timeout: no statistics</source>
|
||||
<translation>Timeout: statistiche non disponibili</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="324"/>
|
||||
<location filename="../src/apiclient.cpp" line="333"/>
|
||||
<source>Timeout: command not sent</source>
|
||||
<translation>Timeout: comando non inviato</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="331"/>
|
||||
<location filename="../src/apiclient.cpp" line="340"/>
|
||||
<source>Command sent</source>
|
||||
<translation>Comando inviato</translation>
|
||||
</message>
|
||||
</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>
|
||||
<name>CommandsPage</name>
|
||||
<message>
|
||||
@@ -347,12 +375,17 @@
|
||||
<translation>Acqua calda</translation>
|
||||
</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>
|
||||
<translation>Non aggiornato · ultimo %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/cover/CoverPage.qml" line="160"/>
|
||||
<location filename="../qml/cover/CoverPage.qml" line="164"/>
|
||||
<source>outside %1 °C</source>
|
||||
<translation>esterna %1 °C</translation>
|
||||
</message>
|
||||
@@ -530,47 +563,51 @@
|
||||
<translation>Acqua calda</translation>
|
||||
</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>
|
||||
<translation>Tocca un valore per regolarlo</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/MainPage.qml" line="238"/>
|
||||
<location filename="../qml/pages/MainPage.qml" line="250"/>
|
||||
<source>Quick actions</source>
|
||||
<translation>Azioni rapide</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/MainPage.qml" line="250"/>
|
||||
<location filename="../qml/pages/MainPage.qml" line="262"/>
|
||||
<source>Boost</source>
|
||||
<translation>Boost</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/MainPage.qml" line="266"/>
|
||||
<location filename="../qml/pages/MainPage.qml" line="278"/>
|
||||
<source>Home</source>
|
||||
<translation>Torna</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/MainPage.qml" line="266"/>
|
||||
<location filename="../qml/pages/MainPage.qml" line="278"/>
|
||||
<source>Away</source>
|
||||
<translation>Assenza</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/MainPage.qml" line="272"/>
|
||||
<location filename="../qml/pages/MainPage.qml" line="284"/>
|
||||
<source>Away mode off</source>
|
||||
<translation>Assenza disattivata</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/MainPage.qml" line="275"/>
|
||||
<source>Away mode on</source>
|
||||
<translation>Assenza attivata</translation>
|
||||
<translation type="vanished">Assenza attivata</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/MainPage.qml" line="285"/>
|
||||
<location filename="../qml/pages/MainPage.qml" line="305"/>
|
||||
<source>More</source>
|
||||
<translation>Altro</translation>
|
||||
</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>
|
||||
<translation>Esterno %1 °C · Termostato %2 °C · Zone %3</translation>
|
||||
</message>
|
||||
@@ -585,7 +622,7 @@
|
||||
<translation>Bridge non configurato. Apri le impostazioni e inserisci l'indirizzo del server e il token.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/MainPage.qml" line="256"/>
|
||||
<location filename="../qml/pages/MainPage.qml" line="268"/>
|
||||
<source>Starting DHW boost</source>
|
||||
<translation>Avvio del boost ACS</translation>
|
||||
</message>
|
||||
|
||||
Reference in New Issue
Block a user