0.6.2: pallino rosso solo per un allarme che ferma la macchina (avviso = ambra)
Il pallino era rosso per qualunque voce nell'elenco errori: un semplice avviso (compressore limitato dalla temperatura esterna, caso reale del 10/10) sembrava un guasto. Ora il ponte espone in /summary anche error_severity e alarm_stopping, e il livello 3 (rosso) e' riservato agli allarmi che fermano la macchina o di severita' CRITICAL/ERROR; avviso = ambra. Copertina allineata. 6 verifiche nuove nell'harness del nucleo (avviso/stop/critical/error/ precedenza sul modo attivo/gravita' ignota = ambra).
This commit is contained in:
+25
-1
@@ -53,7 +53,7 @@ CREDS = BASE / "aira.json"
|
|||||||
TOKENS = BASE / "aira_tokens.json"
|
TOKENS = BASE / "aira_tokens.json"
|
||||||
POOL_CFG = BASE / "aira_config.json"
|
POOL_CFG = BASE / "aira_config.json"
|
||||||
|
|
||||||
VERSION = "0.4.0"
|
VERSION = "0.6.2"
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
@@ -195,6 +195,23 @@ def summarize(st: dict) -> dict:
|
|||||||
|
|
||||||
errs = st.get("errors") or []
|
errs = st.get("errors") or []
|
||||||
|
|
||||||
|
# ---- gravita' degli errori -------------------------------------------
|
||||||
|
# La severita' arriva come stringa ("SEVERITY_WARNING") e le voci possono
|
||||||
|
# essere piu' di una: si normalizza al nome nudo e si tiene la peggiore.
|
||||||
|
# "allarme che ferma" NON si deduce dalla severita': sta nei metadati
|
||||||
|
# (compreso quello del compressore).
|
||||||
|
_GRAVITA = {"WARNING": 1, "ERROR": 2, "CRITICAL": 3}
|
||||||
|
_meta = st.get("error_metadata") or {}
|
||||||
|
alarm_stopping = bool(_meta.get("hp_has_stopping_alarms")) or bool(
|
||||||
|
_meta.get("compressor_has_stopping_alarm"))
|
||||||
|
severita = ""
|
||||||
|
for _voce in errs if isinstance(errs, list) else []:
|
||||||
|
_nome = str((_voce or {}).get("severity") or "")
|
||||||
|
if _nome.startswith("SEVERITY_"):
|
||||||
|
_nome = _nome[len("SEVERITY_"):]
|
||||||
|
if _GRAVITA.get(_nome, 0) > _GRAVITA.get(severita, 0):
|
||||||
|
severita = _nome
|
||||||
|
|
||||||
# ---- dati del dispositivo (per la pagina "Impianto": accessori + riepilogo,
|
# ---- dati del dispositivo (per la pagina "Impianto": accessori + riepilogo,
|
||||||
# come la scheda "Accessori" e il "Riepilogo impostazioni" dell'app ufficiale)
|
# come la scheda "Accessori" e il "Riepilogo impostazioni" dell'app ufficiale)
|
||||||
hp_id = g("heat_pump_id", "value") or ""
|
hp_id = g("heat_pump_id", "value") or ""
|
||||||
@@ -249,6 +266,13 @@ def summarize(st: dict) -> dict:
|
|||||||
"zone2_mode": g("current_pump_mode_state", "zone_2"),
|
"zone2_mode": g("current_pump_mode_state", "zone_2"),
|
||||||
"zone2_cooling_setpoint": g("zone_setpoints_cooling", "zone_2"),
|
"zone2_cooling_setpoint": g("zone_setpoints_cooling", "zone_2"),
|
||||||
"errors_count": len(errs) if isinstance(errs, list) else 0,
|
"errors_count": len(errs) if isinstance(errs, list) else 0,
|
||||||
|
# Gravita' degli errori, per il pallino di stato dell'app: un semplice
|
||||||
|
# avviso (WARNING) non deve diventare rosso come un allarme che ferma la
|
||||||
|
# macchina. "error_metadata" dice se c'e' un allarme che ferma; le voci
|
||||||
|
# di "errors" portano la severita' di ognuna (normalizzata senza il
|
||||||
|
# prefisso SEVERITY_, cosi' la UI non dipende dal protobuf).
|
||||||
|
"alarm_stopping": alarm_stopping,
|
||||||
|
"error_severity": severita,
|
||||||
"pump_updated": g("aws_iot_received_time") or st.get("time"),
|
"pump_updated": g("aws_iot_received_time") or st.get("time"),
|
||||||
# Identificativo e versioni: l'app ufficiale mostra le prime 6 cifre
|
# Identificativo e versioni: l'app ufficiale mostra le prime 6 cifre
|
||||||
# dell'UUID come "Dispositivo ID".
|
# dell'UUID come "Dispositivo ID".
|
||||||
|
|||||||
+3
-2
@@ -245,8 +245,9 @@ rapide sono pulsanti visibili, le azioni sulla copertina restano per dopo la
|
|||||||
prova di `CoverAction` sul kit.
|
prova di `CoverAction` sul kit.
|
||||||
|
|
||||||
**Home** (`MainPage.qml`, riscritta): una schermata da guardare, non un modulo.
|
**Home** (`MainPage.qml`, riscritta): una schermata da guardare, non un modulo.
|
||||||
Stato in una riga con il pallino colorato (verde regolare, ambra modo attivo o
|
Stato in una riga con il pallino colorato (verde regolare, ambra modo attivo,
|
||||||
numeri vecchi, rosso errore), le due letture che contano — casa e acqua calda,
|
numeri vecchi o avviso, rosso solo allarme che ferma la macchina), le due
|
||||||
|
letture che contano — casa e acqua calda,
|
||||||
con obiettivo e barra di carica — affiancate e **toccabili**, e tre azioni
|
con obiettivo e barra di carica — affiancate e **toccabili**, e tre azioni
|
||||||
rapide: boost, assenza, comandi. Escono dalla Home i due slider, i sette
|
rapide: boost, assenza, comandi. Escono dalla Home i due slider, i sette
|
||||||
pulsanti e i tre pulsanti di navigazione ridondanti col menù a tendina. Sotto,
|
pulsanti e i tre pulsanti di navigazione ridondanti col menù a tendina. Sotto,
|
||||||
|
|||||||
+5
-2
@@ -169,8 +169,11 @@ I richiami numerati dell'immagine:
|
|||||||
Solo navigazione fra pagine, una voce per pagina: i pulsanti in fondo alla
|
Solo navigazione fra pagine, una voce per pagina: i pulsanti in fondo alla
|
||||||
home non ci sono più (risolve P4).
|
home non ci sono più (risolve P4).
|
||||||
2. **Stato in una riga**: pallino colorato + "Tutto regolare · Automatico" +
|
2. **Stato in una riga**: pallino colorato + "Tutto regolare · Automatico" +
|
||||||
ora dell'ultimo aggiornamento. Verde regolare, ambra attenzione, rosso
|
ora dell'ultimo aggiornamento. Verde regolare, ambra attenzione (modo
|
||||||
errore o non raggiungibile (risolve P6). La frase estesa dell'app ufficiale
|
attivo, numeri vecchi o avviso della pompa), rosso solo un allarme che
|
||||||
|
ferma la macchina o non raggiungibile (risolve P6 — la distinzione fra
|
||||||
|
avviso e allarme è arrivata il 10/10: prima qualunque voce nell'elenco
|
||||||
|
errori accendeva il rosso). La frase estesa dell'app ufficiale
|
||||||
("Relax: la pompa funziona regolarmente") è bella ma occupa 2 righe in cima:
|
("Relax: la pompa funziona regolarmente") è bella ma occupa 2 righe in cima:
|
||||||
sta meglio nella pagina Impianto (decisione 1).
|
sta meglio nella pagina Impianto (decisione 1).
|
||||||
3. **Due letture grandi, affiancate**: Casa 22,8 °C (obiettivo 19,0) e Acqua
|
3. **Due letture grandi, affiancate**: Casa 22,8 °C (obiettivo 19,0) e Acqua
|
||||||
|
|||||||
@@ -14,12 +14,16 @@ CoverBackground {
|
|||||||
// Colore del pallino: gli stessi criteri della Home (vedi MainPage.qml).
|
// Colore del pallino: gli stessi criteri della Home (vedi MainPage.qml).
|
||||||
// Un dato vecchio e' ambra come un modo attivo: un dato vecchio non e' un
|
// Un dato vecchio e' ambra come un modo attivo: un dato vecchio non e' un
|
||||||
// dato buono, e la copertina e' il posto dove si guarda di sfuggita.
|
// dato buono, e la copertina e' il posto dove si guarda di sfuggita.
|
||||||
|
// Rosso solo per un allarme che ferma la macchina (livello 3): un avviso
|
||||||
|
// (livello 2) e' ambra.
|
||||||
function statusColor() {
|
function statusColor() {
|
||||||
if (!api.hasData || api.stale)
|
if (!api.hasData || api.stale)
|
||||||
return "orange"
|
return "orange"
|
||||||
var livello = api.statusLevel()
|
var livello = api.statusLevel()
|
||||||
if (livello === 2)
|
if (livello === 3)
|
||||||
return Theme.errorColor
|
return Theme.errorColor
|
||||||
|
if (livello === 2)
|
||||||
|
return "#ffb300"
|
||||||
if (livello === 1)
|
if (livello === 1)
|
||||||
return Theme.highlightColor
|
return Theme.highlightColor
|
||||||
return "#4caf50"
|
return "#4caf50"
|
||||||
|
|||||||
@@ -29,14 +29,19 @@ Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Colore del pallino di stato. Il livello (0 regolare, 1 modo attivo,
|
// Colore del pallino di stato. Il livello (0 regolare, 1 modo attivo,
|
||||||
// 2 errore) lo calcola il C++; qui si sceglie il colore, e l'ambra quando
|
// 2 avviso, 3 allarme serio) lo calcola il C++; qui si sceglie il colore,
|
||||||
// i numeri sono vecchi: un dato vecchio non e' un dato buono.
|
// e l'ambra quando i numeri sono vecchi: un dato vecchio non e' un dato
|
||||||
|
// buono. Rosso solo quando e' la macchina a essere ferma o guasta: un
|
||||||
|
// avviso (per esempio il compressore limitato dalla temperatura esterna)
|
||||||
|
// e' ambra, come nell'app ufficiale.
|
||||||
function statusColor() {
|
function statusColor() {
|
||||||
if (!api.hasData || api.stale)
|
if (!api.hasData || api.stale)
|
||||||
return "orange"
|
return "orange"
|
||||||
var livello = api.statusLevel()
|
var livello = api.statusLevel()
|
||||||
if (livello === 2)
|
if (livello === 3)
|
||||||
return Theme.errorColor
|
return Theme.errorColor
|
||||||
|
if (livello === 2)
|
||||||
|
return "#ffb300"
|
||||||
if (livello === 1)
|
if (livello === 1)
|
||||||
return Theme.highlightColor
|
return Theme.highlightColor
|
||||||
return "#4caf50"
|
return "#4caf50"
|
||||||
|
|||||||
@@ -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.1
|
Version: 0.6.2
|
||||||
Release: 1
|
Release: 1
|
||||||
Group: Qt/Qt
|
Group: Qt/Qt
|
||||||
License: MIT
|
License: MIT
|
||||||
@@ -37,6 +37,13 @@ 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
|
||||||
|
* Sat Oct 10 2026 Carlo <carlo@carlobaratto.it> - 0.6.2-1
|
||||||
|
- pallino di stato: rosso solo per un allarme che ferma la macchina (o di
|
||||||
|
severita' CRITICAL/ERROR); un avviso ora e' ambra. Prima qualunque voce
|
||||||
|
nell'elenco errori accendeva il rosso, e un semplice avviso (compressore
|
||||||
|
limitato dalla temperatura esterna, caso reale del 10/10) sembrava un guasto.
|
||||||
|
Il bridge espone error_severity e alarm_stopping in /summary; l'esito ignoto
|
||||||
|
resta ambra, il rosso si accende solo su un guasto che sappiamo essere serio
|
||||||
* Sat Oct 10 2026 Carlo <carlo@carlobaratto.it> - 0.6.1-1
|
* Sat Oct 10 2026 Carlo <carlo@carlobaratto.it> - 0.6.1-1
|
||||||
- correzione: la Home non si apriva ("Type MainPage unavailable", app senza
|
- correzione: la Home non si apriva ("Type MainPage unavailable", app senza
|
||||||
pagina iniziale). Causa: due assegnazioni di Component.onCompleted nello
|
pagina iniziale). Causa: due assegnazioni di Component.onCompleted nello
|
||||||
|
|||||||
+14
-1
@@ -211,6 +211,19 @@ int errorCount(const QVariantMap &summary)
|
|||||||
return summary.value(QStringLiteral("errors_count")).toInt();
|
return summary.value(QStringLiteral("errors_count")).toInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allarme "serio": quello che ferma la macchina (flag del bridge, dai metadati
|
||||||
|
// della pompa) oppure una voce di severita' CRITICAL/ERROR. Un semplice avviso
|
||||||
|
// (WARNING: per esempio il compressore limitato dalla temperatura esterna) NON
|
||||||
|
// e' serio, e non deve accendere il pallino rosso. Senza i campi (bridge piu'
|
||||||
|
// vecchio) si resta prudenti sul colore: ambra, non rosso.
|
||||||
|
bool alarmSerious(const QVariantMap &summary)
|
||||||
|
{
|
||||||
|
if (flagValue(summary, QStringLiteral("alarm_stopping")))
|
||||||
|
return true;
|
||||||
|
const QString severita = summary.value(QStringLiteral("error_severity")).toString();
|
||||||
|
return severita == QStringLiteral("CRITICAL") || severita == QStringLiteral("ERROR");
|
||||||
|
}
|
||||||
|
|
||||||
bool pumpRunning(const QVariantMap &summary)
|
bool pumpRunning(const QVariantMap &summary)
|
||||||
{
|
{
|
||||||
return summary.value(QStringLiteral("pump_active_state")).toString()
|
return summary.value(QStringLiteral("pump_active_state")).toString()
|
||||||
@@ -268,7 +281,7 @@ int statusLevel(const QVariantMap &summary)
|
|||||||
if (summary.isEmpty())
|
if (summary.isEmpty())
|
||||||
return 1;
|
return 1;
|
||||||
if (errorCount(summary) > 0)
|
if (errorCount(summary) > 0)
|
||||||
return 2;
|
return alarmSerious(summary) ? 3 : 2;
|
||||||
if (flagValue(summary, QStringLiteral("away_mode"))
|
if (flagValue(summary, QStringLiteral("away_mode"))
|
||||||
|| flagValue(summary, QStringLiteral("night_mode"))
|
|| flagValue(summary, QStringLiteral("night_mode"))
|
||||||
|| flagValue(summary, QStringLiteral("force_heating")))
|
|| flagValue(summary, QStringLiteral("force_heating")))
|
||||||
|
|||||||
+4
-2
@@ -38,8 +38,10 @@ QString stateShort(const QVariantMap &summary);
|
|||||||
// "Relax: la pompa di calore funziona regolarmente").
|
// "Relax: la pompa di calore funziona regolarmente").
|
||||||
QString stateLong(const QVariantMap &summary);
|
QString stateLong(const QVariantMap &summary);
|
||||||
|
|
||||||
// Livello dello stato, per il colore della riga: 0 regolare, 1 modo attivo
|
// Livello dello stato, per il colore del pallino in Home:
|
||||||
// (assenza, notte, riscaldamento forzato), 2 errore sull'impianto.
|
// 0 regolare, 1 modo attivo (assenza, notte, riscaldamento forzato),
|
||||||
|
// 2 avviso (errori di severita' WARNING: la macchina lavora),
|
||||||
|
// 3 allarme serio (allarme che ferma la macchina, o severita' CRITICAL/ERROR).
|
||||||
int statusLevel(const QVariantMap &summary);
|
int statusLevel(const QVariantMap &summary);
|
||||||
|
|
||||||
// Temperatura leggibile: "51,0" oppure "—" se il valore manca.
|
// Temperatura leggibile: "51,0" oppure "—" se il valore manca.
|
||||||
|
|||||||
@@ -245,6 +245,42 @@ int main(int argc, char *argv[])
|
|||||||
"stato breve: l'errore vince sul modo attivo");
|
"stato breve: l'errore vince sul modo attivo");
|
||||||
check(AiraJson::statusLevel(misto) == 2, "livello: errore prima di tutto");
|
check(AiraJson::statusLevel(misto) == 2, "livello: errore prima di tutto");
|
||||||
|
|
||||||
|
// Un avviso NON e' un allarme: pallino ambra (livello 2), non rosso.
|
||||||
|
// Caso reale 10/10/2026: compressore limitato dalla temperatura esterna
|
||||||
|
// (SEVERITY_WARNING, nessun allarme che ferma la macchina) -> l'app
|
||||||
|
// accendeva il rosso per un semplice avviso.
|
||||||
|
QVariantMap avviso = calmo;
|
||||||
|
avviso.insert("errors_count", 1);
|
||||||
|
avviso.insert("alarm_stopping", false);
|
||||||
|
avviso.insert("error_severity", "WARNING");
|
||||||
|
check(AiraJson::statusLevel(avviso) == 2,
|
||||||
|
"livello: avviso (WARNING) = ambra, non rosso");
|
||||||
|
|
||||||
|
// Allarme che ferma la macchina: rosso anche con severita' di avviso.
|
||||||
|
QVariantMap fermo = avviso;
|
||||||
|
fermo.insert("alarm_stopping", true);
|
||||||
|
check(AiraJson::statusLevel(fermo) == 3, "livello: allarme che ferma = rosso");
|
||||||
|
|
||||||
|
// Severita' alta: rosso anche senza il flag di stop.
|
||||||
|
QVariantMap critico = avviso;
|
||||||
|
critico.insert("error_severity", "CRITICAL");
|
||||||
|
check(AiraJson::statusLevel(critico) == 3, "livello: CRITICAL = rosso");
|
||||||
|
QVariantMap guasto = avviso;
|
||||||
|
guasto.insert("error_severity", "ERROR");
|
||||||
|
check(AiraJson::statusLevel(guasto) == 3, "livello: ERROR = rosso");
|
||||||
|
|
||||||
|
// Il rosso vince sul modo attivo.
|
||||||
|
QVariantMap fermoAssente = assente;
|
||||||
|
fermoAssente.insert("errors_count", 1);
|
||||||
|
fermoAssente.insert("alarm_stopping", true);
|
||||||
|
check(AiraJson::statusLevel(fermoAssente) == 3,
|
||||||
|
"livello: allarme che ferma prima del modo attivo");
|
||||||
|
|
||||||
|
// Senza i campi di gravita' (bridge piu' vecchio) si resta ambra:
|
||||||
|
// il rosso si accende solo su un guasto che sappiamo essere serio.
|
||||||
|
check(AiraJson::statusLevel(uno) == 2,
|
||||||
|
"livello: gravita' ignota = ambra");
|
||||||
|
|
||||||
check(AiraJson::stateShort(QVariantMap()) == "No data",
|
check(AiraJson::stateShort(QVariantMap()) == "No data",
|
||||||
"stato breve: nessun dato");
|
"stato breve: nessun dato");
|
||||||
check(AiraJson::stateLong(QVariantMap()).isEmpty(),
|
check(AiraJson::stateLong(QVariantMap()).isEmpty(),
|
||||||
|
|||||||
Reference in New Issue
Block a user