# 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`, v**0.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 ` 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. - Nessun comando nuovo all'impianto: anche questa versione resta **sola lettura** finché non si prova l'invio dal telefono.