diff --git a/bridge/aira_bridge.py b/bridge/aira_bridge.py index c900a2b..10a4b4f 100644 --- a/bridge/aira_bridge.py +++ b/bridge/aira_bridge.py @@ -53,7 +53,7 @@ CREDS = BASE / "aira.json" TOKENS = BASE / "aira_tokens.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 [] + # ---- 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, # come la scheda "Accessori" e il "Riepilogo impostazioni" dell'app ufficiale) 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_cooling_setpoint": g("zone_setpoints_cooling", "zone_2"), "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"), # Identificativo e versioni: l'app ufficiale mostra le prime 6 cifre # dell'UUID come "Dispositivo ID". diff --git a/docs/PIANO.md b/docs/PIANO.md index 1304619..579db4f 100644 --- a/docs/PIANO.md +++ b/docs/PIANO.md @@ -245,8 +245,9 @@ rapide sono pulsanti visibili, le azioni sulla copertina restano per dopo la prova di `CoverAction` sul kit. **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 -numeri vecchi, rosso errore), le due letture che contano — casa e acqua calda, +Stato in una riga con il pallino colorato (verde regolare, ambra modo attivo, +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 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, diff --git a/docs/UX-STUDIO.md b/docs/UX-STUDIO.md index f2aea4d..5074087 100644 --- a/docs/UX-STUDIO.md +++ b/docs/UX-STUDIO.md @@ -169,8 +169,11 @@ I richiami numerati dell'immagine: Solo navigazione fra pagine, una voce per pagina: i pulsanti in fondo alla home non ci sono più (risolve P4). 2. **Stato in una riga**: pallino colorato + "Tutto regolare · Automatico" + - ora dell'ultimo aggiornamento. Verde regolare, ambra attenzione, rosso - errore o non raggiungibile (risolve P6). La frase estesa dell'app ufficiale + ora dell'ultimo aggiornamento. Verde regolare, ambra attenzione (modo + 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: sta meglio nella pagina Impianto (decisione 1). 3. **Due letture grandi, affiancate**: Casa 22,8 °C (obiettivo 19,0) e Acqua diff --git a/qml/cover/CoverPage.qml b/qml/cover/CoverPage.qml index 034cb8b..76f6e14 100644 --- a/qml/cover/CoverPage.qml +++ b/qml/cover/CoverPage.qml @@ -14,12 +14,16 @@ CoverBackground { // 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 // 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() { if (!api.hasData || api.stale) return "orange" var livello = api.statusLevel() - if (livello === 2) + if (livello === 3) return Theme.errorColor + if (livello === 2) + return "#ffb300" if (livello === 1) return Theme.highlightColor return "#4caf50" diff --git a/qml/pages/MainPage.qml b/qml/pages/MainPage.qml index 2e6e431..b540751 100644 --- a/qml/pages/MainPage.qml +++ b/qml/pages/MainPage.qml @@ -29,14 +29,19 @@ Page { } // 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 - // i numeri sono vecchi: un dato vecchio non e' un dato buono. + // 2 avviso, 3 allarme serio) lo calcola il C++; qui si sceglie il colore, + // 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() { if (!api.hasData || api.stale) return "orange" var livello = api.statusLevel() - if (livello === 2) + if (livello === 3) return Theme.errorColor + if (livello === 2) + return "#ffb300" if (livello === 1) return Theme.highlightColor return "#4caf50" diff --git a/rpm/harbour-aira.spec b/rpm/harbour-aira.spec index f0dce10..75a9c50 100644 --- a/rpm/harbour-aira.spec +++ b/rpm/harbour-aira.spec @@ -1,6 +1,6 @@ Name: harbour-aira Summary: Control your Aira heat pump through a local bridge -Version: 0.6.1 +Version: 0.6.2 Release: 1 Group: Qt/Qt 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. %changelog +* Sat Oct 10 2026 Carlo - 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 - 0.6.1-1 - correzione: la Home non si apriva ("Type MainPage unavailable", app senza pagina iniziale). Causa: due assegnazioni di Component.onCompleted nello diff --git a/src/airajson.cpp b/src/airajson.cpp index 7ec6cbb..ff642bf 100644 --- a/src/airajson.cpp +++ b/src/airajson.cpp @@ -211,6 +211,19 @@ int errorCount(const QVariantMap &summary) 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) { return summary.value(QStringLiteral("pump_active_state")).toString() @@ -268,7 +281,7 @@ int statusLevel(const QVariantMap &summary) if (summary.isEmpty()) return 1; if (errorCount(summary) > 0) - return 2; + return alarmSerious(summary) ? 3 : 2; if (flagValue(summary, QStringLiteral("away_mode")) || flagValue(summary, QStringLiteral("night_mode")) || flagValue(summary, QStringLiteral("force_heating"))) diff --git a/src/airajson.h b/src/airajson.h index 5bf4759..2dc044a 100644 --- a/src/airajson.h +++ b/src/airajson.h @@ -38,8 +38,10 @@ QString stateShort(const QVariantMap &summary); // "Relax: la pompa di calore funziona regolarmente"). QString stateLong(const QVariantMap &summary); -// Livello dello stato, per il colore della riga: 0 regolare, 1 modo attivo -// (assenza, notte, riscaldamento forzato), 2 errore sull'impianto. +// Livello dello stato, per il colore del pallino in Home: +// 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); // Temperatura leggibile: "51,0" oppure "—" se il valore manca. diff --git a/tests/core_test.cpp b/tests/core_test.cpp index dbb7ecf..ad3368d 100644 --- a/tests/core_test.cpp +++ b/tests/core_test.cpp @@ -245,6 +245,42 @@ int main(int argc, char *argv[]) "stato breve: l'errore vince sul modo attivo"); 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", "stato breve: nessun dato"); check(AiraJson::stateLong(QVariantMap()).isEmpty(),