Files
harbour-aira/docs/PIANO.md
T

14 KiB
Raw 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.