L'inglese è la lingua sorgente delle stringhe; l'italiano sta nel catalogo translations/harbour-aira-it.ts (99 stringhe), che lupdate aggiorna e lrelease compila in build. Di serie la lingua del sistema, con ripiego sull'inglese. - nuova classe Translator: risolve la lingua prima di createView e allinea QLocale::setDefault, così anche i numeri seguono la lingua (23,0 °C in italiano, 23.0 °C in inglese) — le temperature passano da formatValue, non più dal toFixed() di JavaScript che metteva sempre il punto - Qt 5.6 non ha QQmlEngine::retranslate(): al cambio di lingua la radice QML viene ricaricata (setSource ricrea il componente anche a url invariata) - le etichette di stato in airajson.cpp usano QCoreApplication::translate per esteso: lupdate non riconosce una funzione wrapper e le avrebbe perse - selettore della lingua in Impostazioni (Sistema / English / Italiano) - test: 41/41, carica il .qm vero come fa main.cpp e verifica entrambe le lingue, il ripiego e i formati numerici - anteprime bilingui (docs/anteprima/ inglese, it/ italiano), README e PIANO aggiornati, spec RPM con i cataloghi in %files
164 lines
9.8 KiB
Markdown
164 lines
9.8 KiB
Markdown
# 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 <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`).
|