Carlo segnalava la riga che copriva i tasti mentre la correzione era nella 0.7.1: non c'era modo di sapere quale build stesse girando. In Impostazioni ora c'e' 'Versione 0.7.4'. La disposizione l'ho anche misurata, replicandola in QML nel container: con la scatola da 56 px la riga cade a y=240 dentro i tasti (176..286), con quella da 152 a y=336 sotto.
539 lines
32 KiB
Markdown
539 lines
32 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`).
|
||
|
||
## 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,
|
||
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,
|
||
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; **85/85** dalla 0.6.2, con le verifiche sulla
|
||
severità degli allarmi — vedi §10). 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.
|
||
|
||
## 10. Dalla prima build sul telefono alla 0.6.4
|
||
|
||
Il §9 racconta il riordino delle schermate fino alla 0.6.0. Da lì sono venute
|
||
quattro versioni piccole, tutte nate da quello che si è visto usare l'app.
|
||
|
||
**0.6.1 — la Home non si apriva.** La prima build sul telefono ha rivelato il
|
||
difetto descritto al §9: due assegnazioni di `Component.onCompleted` nello stesso
|
||
oggetto, che in Qt 5.6 impediscono la creazione della pagina. Non basta
|
||
correggerlo: `tests/qml_check_bindings.py` ora cerca le assegnazioni duplicate in
|
||
tutti i QML, e gira prima della build.
|
||
|
||
**0.6.2 — il pallino non deve gridare.** Il 10/10 il pallino era rosso per un
|
||
avviso innocuo (compressore limitato dalla temperatura esterna): qualunque voce
|
||
nell'elenco errori accendeva il rosso. Ora il rosso è riservato a un allarme che
|
||
ferma la macchina o di severità CRITICAL/ERROR, l'avviso è ambra, e l'esito
|
||
ignoto resta ambra. Il bridge espone `error_severity` e `alarm_stopping` in
|
||
`/summary`, così la decisione si prende sui dati e non sul testo del messaggio.
|
||
|
||
**0.6.3 — l'icona.** Scelta fra quattro proposte disegnate con Pillow
|
||
(`scripts/icon_proposals.py`, tutte conservate in `docs/icona/proposte/`): casa
|
||
con l'onda calda dentro. È ridisegnata a 344 px e rigenerata alle quattro misure
|
||
(86/108/128/172). Disegno originale, nessun logo di terzi: il permesso per nome e
|
||
logo Aira è ancora in sospeso.
|
||
|
||
**0.6.4 — la copertina.** Due righe con l'etichetta davanti, "Casa" e "Acqua",
|
||
ognuna con temperatura attuale / impostata. Le frasi lunghe di prima ("ACS
|
||
53,4 °C · obiettivo 55 °C") sulla cover, che è stretta, venivano tagliate a metà:
|
||
il numero vero spariva proprio dove lo si guarda di sfuggita. Per stare nella
|
||
colonna stretta dell'etichetta il valore attuale è più grande e l'obiettivo più
|
||
piccolo e attaccato ("51,5/55,0 °C").
|
||
|
||
**0.6.5 — un nome solo per l'acqua.** Sulla copertina l'acqua si chiama "Acqua
|
||
calda" come in Home (era "Acqua"): era l'unico punto in cui la stessa cosa aveva
|
||
due nomi.
|
||
|
||
**0.6.6 — le regolazioni dell'oggetto toccato.** Toccando "Casa" si apriva la
|
||
pagina con casa e acqua insieme: chi tocca una lettura si aspetta le regolazioni
|
||
di quella, non di tutto l'impianto. Ora la Home dice quale oggetto è stato
|
||
toccato e la pagina mostra solo quello — l'acqua porta con sé anche il boost, che
|
||
è suo. Due lati della stessa cosa, tenuti insieme da un controllo statico
|
||
(`tests/qml_focus_check.py`), perché questa parte della UI non è eseguibile senza
|
||
il kit Silica.
|
||
|
||
**0.6.7 — le icone dei tasti.** I tre tasti delle azioni rapide in Home hanno
|
||
un'icona: fiamma per il boost, casa per l'assenza, tre manopole per i comandi.
|
||
Non vengono dal tema Sailfish: nell'elenco ufficiale delle icone non c'è nessuna
|
||
fiamma e nessuna goccia, e "boost" e "assenza" non hanno un simbolo di sistema.
|
||
Sono disegnate con Pillow (`scripts/make_glyphs.py`) in PNG bianche su
|
||
trasparente, in `qml/icons/`: il PNG si vede di sicuro dentro il sandbox, l'SVG
|
||
dipende dal supporto SVG.
|
||
|
||
**0.6.8 — le icone dei tasti, misura vera.** Le icone della 0.6.7 erano grosse:
|
||
in un tasto largo un terzo di schermo il testo veniva tagliato. Dalla
|
||
documentazione Silica risulta che `icon` è un `Image`: senza dimensione prende
|
||
quella del file, e il pulsante la riporta all'icona di tema (misurata sullo
|
||
screenshot: 64 px, `Theme.iconSizeMedium`), lasciando 41 px di testo. Ora il
|
||
tasto dichiara `Theme.iconSizeExtraSmall` e i glifi riempiono il riquadro. Con
|
||
la geometria misurata sul telefono (161 px di tasto, rientri 16, distacco 24)
|
||
le etichette **Assenza**, **Torna**, **Altro** e le inglesi **Away**, **Home**,
|
||
**More** ci stanno con margine; "Comandi" (8 lettere) no, quindi il tasto dice
|
||
"Altro" e la pagina resta "Comandi impianto". Prova ripetibile:
|
||
`tests/label_fit_check.py` (le etichette si leggono dai QML e dal catalogo: se
|
||
una si allunga il controllo lo segnala) e `docs/icona/tasti-alla-misura-vera.png`
|
||
per l'occhio. Il pulsante Silica non espone il carattere, quindi non si può
|
||
rimpicciolire il testo: l'unica leva è l'icona.
|
||
|
||
**0.6.9 — l'azione sulla copertina (P8).** Sulla copertina c'è l'icona del
|
||
boost: si avvia il boost dell'acqua calda senza aprire l'app. Una sola azione,
|
||
perché la documentazione Silica dice che due si possono avere ma che lo stile
|
||
delle app di sistema ne mostra una. Il componente esiste: `CoverActionList` e
|
||
`CoverAction` sono nominati nella guida Silica che sta nell'SDK, e
|
||
`CoverBackground` eredita da `Cover`, quindi l'elenco dichiarato lì si attacca
|
||
(non serve avvolgere la copertina in un `Cover`). L'icona si dichiara con
|
||
`iconSource`, non con `icon.source` del pulsante `Button`: sbagliarlo non dà
|
||
errore, semplicemente l'icona non compare. La riga in fondo (esterna o «non
|
||
aggiornato») si ancora a `Cover.coverActionArea`, così l'icona dell'azione non
|
||
la copre; se l'area non c'è la riga resta dov'era. Nuovo controllo statico
|
||
`tests/cover_action_check.py`: elenco figlio diretto, una sola azione, icona che
|
||
esiste, e l'id del comando confrontato con la tabella del bridge — un id
|
||
sbagliato sarebbe un 400 scoperto solo sul telefono.
|
||
|
||
**Il boost non partiva: mancava un argomento (correzione del bridge).** Il primo
|
||
tocco sul boost ha mostrato in rosso, in fondo alla Home, *«richiesta non
|
||
valida: ActivateHotWaterBoosting.__init__() missing 1 required positional
|
||
argument: 'hot_water_boost_duration'»*. Non era l'impianto a rifiutare: il
|
||
comando non veniva nemmeno costruito. La libreria pretende un argomento per
|
||
quattro comandi della whitelist — `hot_water_boost_on`, `hot_water_heating_off`
|
||
e `force_heating_on` una durata, `away_mode_on` inizio, fine e la temperatura da
|
||
tenere in casa — e il bridge li chiamava senza. Il boost è solo il primo che è
|
||
stato toccato: anche l'assenza, il tasto accanto, sarebbe fallita così. Ora
|
||
ognuno ha il suo ramo in `build_command`, con i default in cima al file (60
|
||
minuti per le tre durate, 7 giorni e 15 °C per l'assenza) e un `value`
|
||
opzionale che l'app può mandare. `test_bridge_commands.py` ora costruisce
|
||
**tutti** i comandi della whitelist e li incarta come li manda la libreria: se
|
||
uno resta senza argomento obbligatorio si vede lì (74/74). Provato dal vivo con
|
||
un `ping` (200, con la ricezione dall'impianto) e con un boost fuori intervallo,
|
||
respinto con 400 **prima** di partire.
|
||
|
||
## 11. Assenza con le date e conto alla rovescia del boost — v0.7.0
|
||
|
||
**Richieste dell'utente (11/10/2026), da applicare come dati:**
|
||
|
||
1. il **boost** deve essere di **30 minuti**;
|
||
2. l'**assenza** si imposta da un **popup con data di inizio e fine**;
|
||
3. durante l'assenza l'impianto **tiene la casa a 15 °C** e **riprende
|
||
normalmente il giorno prima del rientro**;
|
||
4. mentre il boost è in corso si deve vedere un **conto alla rovescia da 30:00 a
|
||
00:00**.
|
||
|
||
## 12. I tasti delle azioni rapide e la riga che li copriva — v0.7.1
|
||
|
||
Carlo: *"Quando premo un bottone la riga sotto si disallinea coprendo i
|
||
pulsanti"*.
|
||
|
||
Non era la riga a spostarsi: era il **tasto** a essere piu' basso del suo
|
||
contenuto. In Silica `Button` dichiara `height: implicitHeight` e
|
||
`implicitHeight: Theme.itemSizeExtraSmall` (~56 px), un'altezza pensata per
|
||
un'icona *accanto* a un'etichetta corta. I tre tasti delle azioni rapide hanno
|
||
icona **e** etichetta (ingombro visibile ~110-160 px, quello che io avevo
|
||
misurato come "tasto 161 px"): il contenuto esce dalla scatola, e la riga
|
||
successiva della colonna -- le letture di contorno -- viene messa subito sotto
|
||
la scatola, cioe' **sopra** l'etichetta che sporge. Da li' l'impressione che si
|
||
"disallinei".
|
||
|
||
**Come è stato fatto (0.7.1).**
|
||
- i tre tasti dichiarano `height: Theme.itemSizeMedium` (`Button.qml` del target
|
||
Sailfish letto dentro il motore di build, non indovinato);
|
||
- `tests/label_fit_check.py` adesso pretende l'altezza dichiarata su ogni tasto
|
||
che porta icona e testo: senza, il tema ne da' ~56 px contro ~110 di
|
||
contenuto;
|
||
- il registro dell'avvio (dopo un secondo) elenca **posizione e altezza di ogni
|
||
riga della Home**, cosi' se la disposizione non torna si legge dal registro
|
||
quale riga finisce dove invece di dedurlo da uno screenshot.
|
||
|
||
## 13. Il tasto del boost diventa Stop, e la riga che spostava i tasti — v0.7.2
|
||
|
||
Carlo: *"Il boost funziona ma la scritta sposta in basso i pulsanti facendoli
|
||
coprire. Il pulsante di avvio boost dovrebbe diventare stop boost se il boost e'
|
||
attivo"*.
|
||
|
||
Due cose distinte, e la prima va detta perche' evita confusione: la riga che
|
||
**copriva** i tasti era il difetto di 0.7.1 (il tasto piu' basso del contenuto),
|
||
che al momento della segnalazione non era ancora installata sul telefono -- il
|
||
bridge si aggiorna da solo sul server, l'app no. La riga che invece li
|
||
**spostava** era nuova e vera: il conto alla rovescia prendeva il posto del
|
||
suggerimento ma con un carattere piu' grande (`fontSizeSmall` contro
|
||
`fontSizeExtraSmall`), quindi la Home cresceva di una decina di pixel e tutto
|
||
scendeva. Ora e' una riga sola, di altezza fissa, con il carattere del
|
||
suggerimento: sostituirsi non sposta piu' niente.
|
||
|
||
**Come è stato fatto (0.7.2).**
|
||
- tasto del boost condizionale (`Stop` con il glifo di stop quando `boostActive`)
|
||
**anche in copertina**: una copertina che riavvia il boost quando vuoi fermarlo
|
||
e' peggio di un tasto che non fa niente;
|
||
- `quick-boost-stop.png` disegnato in casa con `scripts/make_glyphs.py`, come gli
|
||
altri tre glifi;
|
||
- "Stop boost" per intero non ci sta accanto all'icona (92 px contro 73): resta
|
||
`Stop`, il senso lo porta il glifo. E' `tests/label_fit_check.py` a dirlo, che
|
||
adesso legge anche le etichette condizionali;
|
||
- `tests/cover_action_check.py` legge tutti i comandi dell'azione e tutte le
|
||
icone citate: prima si aspettava un solo letterale e su un invio condizionale
|
||
segnalava un problema inesistente.
|
||
|
||
## 14. La copertina: freccia invece di barra, ed "Esterno" maiuscolo — v0.7.3
|
||
|
||
Carlo: *"La copertina ha ancora le vecchie scritte"*. Alle mie domande ha
|
||
risposto: tutte e tre le righe (Casa, Acqua calda, esterna), riscritte corte con
|
||
la freccia.
|
||
|
||
- `obiettivo()` nella copertina restituisce `" → 19,0 °C"` invece di `"/19,0 °C"`:
|
||
la barra era la scrittura vecchia, la freccia si legge di sfuggita anche su una
|
||
tessera stretta;
|
||
- l'italiano di `outside %1 °C` era `esterna %1 °C` (minuscolo e femminile,
|
||
residuo di quando la riga era un'altra frase): ora `Esterno %1 °C`, come nella
|
||
Home. Verificato leggendo il catalogo, non a memoria;
|
||
- le etichette `Casa` e `Acqua calda` restano: erano gia' quelle scelte, era il
|
||
*formato* a essere vecchio.
|
||
|
||
Due domande prima di toccare, e non e' stato tempo perso: "vecchie scritte" poteva
|
||
voler dire il formato, le parole, oppure che sul telefono c'era ancora una
|
||
versione vecchia dell'app. Chiedere ha evitato di rifare l'RPM per niente.
|
||
|
||
## 15. La versione a schermo — v0.7.4
|
||
|
||
Carlo: *"La scritta continua a sovrapporsi ai pulsanti"*, mentre la correzione
|
||
era gia' nella 0.7.1. Non c'era modo di sapere quale build stesse girando: la
|
||
versione non era scritta da nessuna parte nell'app.
|
||
|
||
- `appVersion` tra le context property di `main.cpp` (`QCoreApplication::applicationVersion()`);
|
||
- `SettingsPage.qml`: riga `qsTr("Version %1").arg(appVersion)` in fondo.
|
||
|
||
Prima di aggiungerla ho **misurato** la disposizione invece di dedurla:
|
||
riprodotta la colonna della Home in un file QML con QtQuick puro e fatta girare
|
||
con `qml` e `QT_QPA_PLATFORM=offscreen` nel container Debian (vedi la skill
|
||
`sailfish-app-development`). Con la scatola del tasto a 56 px — la 0.7.0 — la
|
||
riga di contorno cade a y=240, che sta dentro i tasti (176..286): passava sopra,
|
||
come diceva Carlo. Con la scatola a 152 px cade a y=336, sotto: nessuna
|
||
sovrapposizione. La correzione era giusta; mancava l'app aggiornata.
|
||
|
||
## 16. Conseguenze dell'uso reale (0.7.0)
|
||
|
||
Deciso con l'uso, non in anticipo:
|
||
|
||
- il **boost** è di **mezz'ora** (era un'ora);
|
||
- l'**assenza** non è più "N giorni": si scelgono **due date** in un popup, e la
|
||
casa resta a **15 °C**;
|
||
- mentre il boost corre, **conto alla rovescia** in Home e in copertina.
|
||
|
||
Resta aperto: il popup chiede *inizio* e *fine*, dove "fine" è il giorno in cui
|
||
l'impianto ricomincia a scaldare (nell'uso di Carlo: il giorno prima del rientro).
|
||
Se preferisce indicare il *rientro* e far togliere un giorno all'app, è una riga.
|
||
|
||
**Come è stato fatto (0.7.0).**
|
||
- Bridge: i comandi con durata hanno il loro ramo e i default in cima al file;
|
||
per l'assenza `_periodo_assenza` legge `start` ed `end` (AAAA-MM-GG) e li
|
||
traduce in `current_time`/`end_time`. La fine è la **mezzanotte del giorno
|
||
indicato nel fuso dell'impianto** (`PLANT_TZ = Europe/Rome`, dichiarato dallo
|
||
stato): da lì la pompa riprende il programma. Il fuso della *macchina* non si
|
||
usa: alle date di cambio ora sbaglia di un'ora (lo hanno visto i test).
|
||
- Bridge: `/summary` espone `boost_active` e `boost_remaining_seconds`, calcolati
|
||
dalla fine del boost letta dall'impianto (`hot_water_boost.end_time`). Il conto
|
||
alla rovescia dell'app si basa su quelli, non su quando l'app ha visto partire
|
||
il comando: se il boost l'ha avviato il telefono di casa o l'app ufficiale, il
|
||
numero è comunque giusto.
|
||
- App: `AwayDialog.qml` (Dialog + due `ValueButton` + `DatePickerDialog`, come
|
||
nell'esempio della guida Silica), il conto alla rovescia in Home sotto le
|
||
letture e in copertina al posto dell'obiettivo dell'acqua calda. La formattazione
|
||
`mm:ss` sta in `AiraJson::countdown` (C++, verificata in `tests/core_test.cpp`),
|
||
i secondi li fa scorrere un timer del client, che si riallinea a ogni rilettura.
|
||
- Catalogo: `lupdate harbour-aira.pro` leggeva solo i C++ e marcava 151 voci QML
|
||
come obsolete (l'italiano sarebbe rimasto a metà senza accorgersene). Ora:
|
||
`lupdate src qml -ts translations/harbour-aira-it.ts`. 197 stringhe, 0 non
|
||
tradotte.
|
||
|
||
|