Files

18 KiB
Raw Permalink Blame History

harbour-aira — app Sailfish per la pompa di calore Aira

Piano scritto il 9 ottobre 2026, prima di scrivere la UI (convenzione di lavoro).

1. Stato verificato sul campo

Cosa Esito
Accesso cloud Aira funziona — pyairahome 2.3.0, pool Cognito eu-north-1_cnqyjWtbz (indice 0), token salvati in ~/.hermes/profiles/carlo/aira_tokens.json (600)
Lettura impianto funziona — esterna, interna, ACS attuale/obiettivo, modalità, curve zone, firmware, errori
Bridge HTTP sul server scritto e provato — ~/workspace/Carlo/Aira/aira_bridge.py, v0.3.0 (servizio systemd utente attivo)
Bluetooth sul server assente — nessun adattatore HCI (esclude la via locale BLE su questa macchina)
SDK Sailfish (sfdk) assente qui — build e deploy li fa Carlo dal suo PC
Jolla Phone 2026 non raggiungibile dalla rete del server: deploy solo dal PC

2. Perché serve un bridge e non una chiamata diretta

Il cloud Aira espone gRPC su HTTP/2. Qt 5.6 — il toolkit su cui gira Sailfish — non ha HTTP/2, quindi un'app nativa non può parlarci direttamente. Vie alternative valutate e scartate:

  • PyOtherSide + pyairahome dentro l'app: servirebbe impacchettare grpcio e bleak compilati per aarch64/SFOS, con l'RPM che diventa fragile a ogni aggiornamento.
  • Cognito + gRPC riscritti in C++: HTTP/2 e protobuf da zero. Molto lavoro, molto rischio.

Scelta: la logica resta sul server (dove Python e i token già funzionano), l'app è un client sottile. Vantaggio collaterale: credenziali e token non stanno mai sul telefono.

 Jolla (QML/C++ Qt 5.6)  --HTTP/1.1 + JSON-->  bridge sul server  --gRPC-->  cloud Aira
                                              (pyairahome + token)

3. Protocollo del bridge (v1 — già implementato)

Autenticazione: Authorization: Bearer <token> su tutto tranne /health. Token generato al primo avvio in ~/.hermes/profiles/carlo/aira_bridge.json (600).

Metodo Rotta Risposta
GET /health {ok, version} — nessuna auth, per la diagnostica
GET /summary valori leggibili: temperature, ACS, modalità, flag, errori
GET /state stato completo grezzo (42 campi)
GET /commands elenco comandi ammessi + parametri
POST /command {"id": "...", "value": ...}

Comandi ammessi in v1: ping, set_hot_water_target (45–60 °C), hot_water_boost_on/off, hot_water_heating_on/off, away_mode_on/off, night_mode_1h, force_heating_on/off, heating_on/off, legionella_cycle.

Whitelist voluta: la libreria espone ~90 comandi, inclusi FactoryReset, InstallFirmware, RebootDevice, RotateCertificate. Quelli restano fuori di proposito: un client mobile non deve poter riformattare l'impianto per un tap sbagliato.

4. Architettura dell'app

  • harbour-aira.pro / rpm/ / icons/ dai template consolidati della skill.
  • C++ minimo: Settings (QSettings su AppConfigLocation, obbligatorio per la sandbox SailJail) e ApiClient (QNetworkAccessManager + watchdog di timeout 15 s, JSON parsing). Esposti come context properties: appSettings, api.
  • QML: MainPage (dashboard), SettingsPage (indirizzo bridge + token), CoverPage.
  • Nessun dato personale nel binario: indirizzo e token si configurano nell'app (le repo sono pubbliche su Gitea).

5. Fasi

  • F0 — ponte (fatto): bridge scritto, provato su /health, /summary, /commands, errori 400/401 verificati. Nessun comando inviato all'impianto.
  • F1 — v0.1 in sola lettura (fatto): dashboard con esterna/interna/ACS, modalità, stato ACS, pull-to-refresh, copertina con le temperature.
  • F2 — comandi (fatto in v0.1): obiettivo ACS (slider 45–60), boost on/off, con conferma RemorseItem. Gli altri comandi della whitelist restano raggiungibili dal bridge ma non hanno ancora un pulsante nell'app.
  • F3 — servizio e rete: unità systemd per il bridge (avvio automatico), accesso dal telefono. Decisione dell'utente: per ora solo rete locale — quindi l'indirizzo del bridge è l'IP del server in LAN, e la copertura fuori casa si affronta più avanti (VPN WireGuard già attiva, oppure sottodominio HTTPS).

5-bis. Consegna del 9 ottobre 2026 (sera)

Stato: progetto completo in ~/workspace/Carlo/AppSailfish/harbour-aira (commit iniziale, 27 file) e tarball AppSailfish/pacchetti/harbour-aira-0.1.0.tar.gz.

Verificato qui, senza SDK:

  • nucleo puro (src/airajson.cpp): 26/26 verifiche superate con l'harness Qt 5.15;
  • compilazione del nucleo senza warning (-Wall -Wextra);
  • qmllint sui 6 file QML: nessun errore oltre ai moduli Silica assenti (atteso senza SDK).

Non verificabile qui (serve il suo PC + il telefono): build RPM, istallazione, UI reale.

5-ter. Aggiornamento del 10 ottobre 2026 — v0.2.0 (riscaldamento)

  • Bridge 0.2.0: comandi set_heating_setpoint e set_cooling_setpoint (5–30 °C, zona opzionale 1 o 2) e campi zone1_* / zone2_* nel riassunto. Il messaggio ZoneTemperatures va inviato completo: la zona non toccata viaggia col suo valore attuale (letto dall'ultimo stato, tenuto in cache nella sessione), altrimenti a zero verrebbe spenta. Verifica: test_bridge_commands.py, 19/19, nessun comando inviato all'impianto.

  • App 0.2.0: sezione "Riscaldamento" nella dashboard (temperatura in casa, obiettivo, stato zona, slider 5–30 °C a passi di 0,5 con conferma) e copertina con la temperatura di casa. Aggiunte le etichette per gli stati PUMP_MODE_STATE_*. Nucleo: 29/29.

  • Bridge 0.3.0 + App 0.3.0: pagina "Altri comandi" nell'app (dalla tendina in alto o dal pulsante in fondo alla home) con assenza, night mode un'ora, riscaldamento forzato on/off, funzione riscaldamento on/off, ciclo antilegionella, riscaldamento ACS on/off e verifica impianto; i comandi che durano (forzato, legionella, disabilitazioni) chiedono conferma. In cima alla pagina lo stato attuale, così non si preme alla cieca. Bug corretto nello stesso giro: force_heating nello stato è un oggetto {enabled, remaining_time}, e il bridge lo leggeva con bool() — sempre vero. Ora si legge il campo enabled; il test lo copre (27/27). Nucleo app: 31/31.

  • App 0.2.1 (richiesta dell'utente): la temperatura della casa diventa il dato principale in cima alla home (valore grande, barra, obiettivo, slider e pulsante), l'acqua calda sanitaria scende in una propria sezione sotto, lo stato impianto va in fondo. Stessa gerarchia nella copertina: casa in grande, ACS in una riga secondaria.

  • F3 fatto: harbour-aira-bridge.service (systemd utente, enable --now, parte al boot grazie a Linger=yes, Restart=on-failure verificato con SIGKILL → riparte in ~10 s) e regola 52-aira-bridge-lan.conf in /etc/nftables.d/. Il firewall YunoHost ha policy drop e apriva solo 22/25/53/80/443/587/993/5349/5350: la 8790 va aperta a mano, solo per LAN e VPN. Lezione: un test da 127.0.0.1 non attraversa quelle regole e non rivela il problema — va provato dalla LAN.

  • App 0.4.0 — localizzazione ita/eng (richiesta dell'utente): l'interfaccia si può mettere in italiano o inglese dalle impostazioni; di serie segue la lingua del sistema, con ripiego sull'inglese. L'inglese è la lingua sorgente delle stringhe (restano nel codice), l'italiano sta in translations/harbour-aira-it.ts (99 stringhe, aggiornate con lupdate e compilate da lrelease in build). Nuova classe Translator (src/translator.{h,cpp}): risolve la lingua prima di createView, così le qsTr() del QML nascono già tradotte, e allinea QLocale::setDefault alla lingua scelta — così i numeri seguono la lingua (23,0 °C in italiano, 23.0 °C in inglese): le temperature si formattano in ApiClient::formatValue, non più con il toFixed() di JavaScript, che avrebbe sempre messo il punto. Vincolo di piattaforma: Qt 5.6 non ha QQmlEngine::retranslate() (arriva con Qt 5.10), quindi installare il traduttore non ritraduce ciò che è già a schermo: al cambio lingua l'app ricarica la radice QML. Trappola di lupdate: riconosce solo le funzioni di traduzione note (tr(), QCoreApplication::translate()), non una funzione wrapper scritta da noi — le etichette di airajson.cpp sono quindi chiamate QCoreApplication::translate("AiraJson", …) per esteso, altrimenti sparirebbero dal catalogo. Nucleo app: 39/39 (le attese sono cambiate in inglese e il test carica il .qm vero come fa main.cpp, verificando entrambe le lingue, il ripiego e i formati numerici). Anteprime rigenerate in due lingue (docs/anteprima/ inglese, docs/anteprima/it/ italiano), impostazioni comprese la nuova riga della lingua.

6. Rischi noti

  • Dipendenza dal server: se il bridge è spento, l'app non vede nulla. Mitigazione: unità systemd con riavvio automatico (F3).
  • Scadenza credenziali: i token Cognito si rinnovano da soli per settimane; quando il refresh token scade serve rilanciare setup_creds.py sul server. L'app deve mostrare un errore chiaro, non un retry infinito.
  • Assenza di test sulla UI: senza SDK né device raggiungibile da qui, la UI Silica si verifica solo sul telefono di Carlo (ciclo consolidato: io consegno il tarball, lui compila e incolla i log).
  • Token del bridge: è l'unica credenziale sul telefono; va trattato come una password e revocabile rigenerando aira_bridge.json.

7. Decisioni aperte

  1. Come raggiungere il bridge dal telefono: VPN WireGuard (già attiva, zero configurazione nuova) oppure sottodominio HTTPS pubblico con reverse proxy.
  2. Ambito della v0.1: sola lettura (consigliata) oppure lettura + comandi ACS subito.
  3. Nome visibile dell'app: default proposto "Aira" (pacchetto harbour-aira).

8. Riordino come l'app ufficiale — v0.5.0 (richiesta dell'utente, 10/10/2026)

L'utente ha allegato le schermate dell'app ufficiale (Home, dettaglio impianto, Accessori, Impostazioni, Aira Intelligence, Dati) e ha chiesto di riordinare le informazioni allo stesso modo. Piano scritto prima di toccare la UI, come da convenzione.

Cosa si può replicare (verificato con sonda di sola lettura)

Sonda aira_stats_probe.py (nessun comando inviato all'impianto): il cloud Aira espone anche un servizio statistiche (HeatPumpStatisticsService) che la libreria usava solo in parte. I numeri coincidono con quelli dell'app ufficiale:

Dato dell'app ufficiale Fonte Valore reale letto Coincide
Calore prodotto (questo mese) 70.8 kWh GetHeatPumpInsights → delivered_heat_wh 70 844 Wh sì
Elettricità usata 29.3 kWh GetHeatPumpInsights → energy_consumption_wh 29 300 Wh sì
Efficienza acqua calda 4.4 GetCop mensile → dhw_cop_value 4.42 (settembre) sì
Efficienza riscaldamento: Nessun dato GetCop → room_heating_cop_value vuoto (solo ACS) sì
Risparmio Smart Tariff Control €2.7 GetOptimisationSavings → rolling_month_optimisation € 2,71 sì
Risparmi (ultimi 30 giorni) GetSavings (180 gg) € 95,46 + 153,9 kg CO₂ sì
Dispositivo ID 1f4b26 heat_pump_id (UUID) 1f4b26c4-… sì
Versione software 3.30.1 versions.connectivity_manager 3.30.1 sì
Versione piattaforma 6.10.0 versions.linux_build_id 6.10.0 sì
Termostato a parete Aira · Zone 1 thermostats[] articolo 201884, zona 1, 22,8 °C, 73% UR sì
Modalità Vacanza away_mode_enabled inattivo sì

Resta fuori: Wi-Fi, messaggi luminosi/LED, nucleo familiare, tariffe energia, Aira Intelligence (comfort/risparmio) e le notifiche push. Sono funzioni che dipendono da impostazioni dell'app ufficiale o da dati che il cloud non espone in lettura (le tariffe servono per i suoi calcoli, non li leggiamo).

Struttura nuova dell'app

Come l'app ufficiale: una Home con il dato esterno in alto e la scheda dell'impianto, poi Dati e Impianto (al posto di Accessori + Riepilogo impostazioni). La navigazione resta con il menu pulley di Silica e non con la barra a tre tab in basso: la tab bar non è un pattern Sailfish e su Qt 5.6/Silica 5.1 andrebbe disegnata a mano.

  1. Home: esterno 16,8 °C in alto a sinistra e stato assenza a destra; messaggio di stato grande (derivato: errori, assenza, acqua calda in corso, riscaldamento in corso, tutto regolare); scheda impianto — "Aira Pompa di calore · Connesso tramite Cloud" con due righe (Temperatura interna con l'obiettivo, Acqua calda) e la freccia che apre la pagina Impianto; sotto, i comandi (obiettivo casa, obiettivo ACS e boost) che l'app ufficiale non ha e che restano il valore aggiunto nostro.
  2. Dati: "Il tuo riscaldamento (questo mese)" con calore prodotto, elettricità usata, efficienza (COP riscaldamento / acqua calda); "I tuoi risparmi" con il risparmio totale del periodo, la CO₂ evitata e il risparmio Smart Tariff Control.
  3. Impianto: stato (connessione, modalità, assenza, notte, riscaldamento forzato, resistenza interna, temperature, zone, ultima lettura), accessori (termostato: zona, temperatura, umidità, segnale, batteria, articolo), versioni (ID dispositivo, piattaforma, connettività, controllo climatico, unità esterna, EEPROM) ed errori. Le luci di firma (signature_lights, led_pattern) restano per ora solo in /summary: la pagina non le mostra.

Lavoro

  • Bridge 0.4.0: nuova rotta /stats (COP, risparmi, ottimizzazione, insight) e campi del dispositivo in /summary (device_*, version_*, thermostat_*, connected_via, timezone); cache di 10 minuti lato bridge, perché sono quattro chiamate al cloud, più lente di /state.
  • App 0.5.0: MainPage riordinata, nuove DataPage.qml e PlantPage.qml (quest'ultima si è chiamata InfoPage nella prima stesura del piano), nuovo componente ValueRow.qml, 86 stringhe nuove tradotte (170 in totale), anteprime rigenerate (dati, impianto) in italiano e inglese, test del nucleo estesi al parsing delle statistiche e alla formattazione di importi e mesi (61/61), versione, README e docs/PROTOCOL.md aggiornati.
  • Nessun comando nuovo all'impianto: tutto questo è sola lettura.

9. Studio UX/UI e riordino delle schermate — v0.6.0

Prima di scrivere codice è stato fatto lo studio in docs/UX-STUDIO.md: dieci problemi rilevati nelle schermate della 0.5.0, la proposta, i wireframe (docs/ux/, generati da scripts/render_wireframes.py) e il piano di verifica. Le quattro scelte di design sono state prese dall'autore: la frase di stato lunga va in Impianto, "Altri comandi" diventa "Comandi impianto", le azioni 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, 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, in una riga toccabile che porta a Impianto: esterna, termostato, zone.

Regolazioni (SetpointsPage.qml, nuova): un blocco per grandezza, con il valore vero dell'impianto sempre visibile, slider (la proposta), fila di valori rapidi per le dita fredde e pulsante di conferma che si attiva solo se c'è davvero qualcosa da cambiare. Il boost ha qui i suoi due pulsanti.

Comandi impianto (CommandsPage.qml): da 12 pulsanti in quattro sezioni (otto in coppie acceso/spento) a 4 interruttori TextSwitch + 3 pulsanti. Gli interruttori non anticipano l'impianto: automaticCheck: false, checked sull'ultimo dato vero, e se il comando non riesce restano dov'erano. La diagnostica (verifica impianto) si sposta in Impianto, insieme alle letture che non avevano posto: pompa, modi consentiti, modalità manuale, sbrinamento, luci di firma.

Feedback e dati vecchi: i messaggi dei comandi passano in ApiClient (api.notice) e si mostrano nel nuovo NoticeBar, ancorato al bordo inferiore della pagina invece che dentro il contenuto che scorre: prima, premendo un pulsante in fondo alla pagina, il messaggio nasceva fuori schermo. In più stale e lastUpdate: la Home scrive "aggiornato alle HH:MM" e spegne i numeri quando l'ultima lettura non è recente. Dopo un comando riuscito lo stato si rilegge da solo (2 s): l'aggiornamento automatico, prima, non esisteva.

Stato breve ed esteso in C++ (AiraJson::stateShort/stateLong/statusLevel): logica pura, verificabile senza SDK e non duplicata fra Home e Impianto. Test del nucleo: 79/79 (erano 61). Stringhe: 191/191 tradotte. Anteprime rigenerate (nuova regolazioni.png) in italiano e inglese.

Copertina (qml/cover/CoverPage.qml): pallino di stato come in Home, la temperatura grande si spegne quando i numeri sono vecchi e l'etichetta finale è diventata "esterna 18,0 °C" (era "est.", sigla che non diceva niente), con la riga dei numeri vecchi al posto della temperatura esterna quando serve.

Prima build sul telefono (0.6.0): la Home non si apriva. Il log diceva harbour-aira.qml:9:30: Type MainPage unavailable, cioè l'app si avviava senza la pagina iniziale, e non nominava nessun file dei miei. Causa: due assegnazioni di Component.onCompleted nello stesso oggetto (MainPage.qml), una vecchia e una aggiunta con la riga di misura dell'altezza. In Qt 5.6 non è un warning innocuo: la pagina non si crea più. Il codice è stato unito in una sola assegnazione e, perché non ricapiti, tests/qml_check_bindings.py cerca le assegnazioni duplicate in tutti i QML (verificato: intercetta il caso 70fe229, il codice attuale è pulito). Da questa prima esecuzione sappiamo anche che il resto dell'avvio funziona: configurazione letta (baseUrl=[192.168.3.21:8790], token presente), catalogo italiano caricato (it_IT), versione corretta in log.

  • Nessun comando nuovo all'impianto: anche questa versione resta sola lettura finché non si prova l'invio dal telefono.