Misurato in container: la ricarica della radice ri-traduce, il catalogo si trova anche con it_IT. La ricarica ora scarica la radice prima di ricaricarla, cosi' non dipende da un comportamento di setSource che cambia fra versioni di Qt.
38 KiB
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
grpcioebleakcompilati 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 suAppConfigLocation, obbligatorio per la sandbox SailJail) eApiClient(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); qmllintsui 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_setpointeset_cooling_setpoint(5–30 °C, zona opzionale 1 o 2) e campizone1_*/zone2_*nel riassunto. Il messaggioZoneTemperaturesva 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_heatingnello stato è un oggetto{enabled, remaining_time}, e il bridge lo leggeva conbool()— sempre vero. Ora si legge il campoenabled; 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 aLinger=yes,Restart=on-failureverificato con SIGKILL → riparte in ~10 s) e regola52-aira-bridge-lan.confin/etc/nftables.d/. Il firewall YunoHost hapolicy drope 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 da127.0.0.1non 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 conlupdatee compilate dalreleasein build). Nuova classeTranslator(src/translator.{h,cpp}): risolve la lingua prima dicreateView, così leqsTr()del QML nascono già tradotte, e allineaQLocale::setDefaultalla lingua scelta — così i numeri seguono la lingua (23,0 °Cin italiano,23.0 °Cin inglese): le temperature si formattano inApiClient::formatValue, non più con iltoFixed()di JavaScript, che avrebbe sempre messo il punto. Vincolo di piattaforma: Qt 5.6 non haQQmlEngine::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 dilupdate: riconosce solo le funzioni di traduzione note (tr(),QCoreApplication::translate()), non una funzione wrapper scritta da noi — le etichette diairajson.cppsono quindi chiamateQCoreApplication::translate("AiraJson", …)per esteso, altrimenti sparirebbero dal catalogo. Nucleo app: 39/39 (le attese sono cambiate in inglese e il test carica il.qmvero come famain.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.pysul 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
- Come raggiungere il bridge dal telefono: VPN WireGuard (già attiva, zero configurazione nuova) oppure sottodominio HTTPS pubblico con reverse proxy.
- Ambito della v0.1: sola lettura (consigliata) oppure lettura + comandi ACS subito.
- 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.
- Home:
esterno 16,8 °Cin 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. - 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.
- 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:
MainPageriordinata, nuoveDataPage.qmlePlantPage.qml(quest'ultima si è chiamataInfoPagenella prima stesura del piano), nuovo componenteValueRow.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 edocs/PROTOCOL.mdaggiornati. - 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:
- il boost deve essere di 30 minuti;
- l'assenza si imposta da un popup con data di inizio e fine;
- durante l'assenza l'impianto tiene la casa a 15 °C e riprende normalmente il giorno prima del rientro;
- 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.qmldel target Sailfish letto dentro il motore di build, non indovinato); tests/label_fit_check.pyadesso 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 (
Stopcon il glifo di stop quandoboostActive) anche in copertina: una copertina che riavvia il boost quando vuoi fermarlo e' peggio di un tasto che non fa niente; quick-boost-stop.pngdisegnato in casa conscripts/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.pya dirlo, che adesso legge anche le etichette condizionali; tests/cover_action_check.pylegge 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 °Ceraesterna %1 °C(minuscolo e femminile, residuo di quando la riga era un'altra frase): oraEsterno %1 °C, come nella Home. Verificato leggendo il catalogo, non a memoria; - le etichette
CasaeAcqua caldarestano: 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.
appVersiontra le context property dimain.cpp(QCoreApplication::applicationVersion());SettingsPage.qml: rigaqsTr("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. Le misure della Home al bridge — v0.7.5
Carlo: "È la 0.7.4 ma il problema rimane". Fino a qui avevo dedotto la
disposizione: dal vero Button.qml, da uno screenshot letto a occhio, da una
replica della colonna che mi ero costruito io. Tutte e tre le volte la deduzione
ha retto in teoria e ha sbagliato sui fatti, e ogni giro costava un RPM, una
copia sul telefono e una delusione.
MainPage.qml: il timermisura(gia' presente dalla 0.7.1) non si limita piu' a scrivere nel registro del telefono — che nessuno puo' leggere — ma manda le stesse misure al bridge conapi.reportLayout(): versione, altezza dello schermo, altezza del contenuto ey/h/testo di ogni riga della Home;ApiClient::reportLayout(): un POST a/layout, senza attendere risposta;aira_bridge.py: la rotta/layout(protetta da token come le altre) scrive il corpo nel proprio registro:[bridge] layout {...}.
Così la diagnosi smette di passare da un essere umano: Carlo apre l'app, le coordinate arrivano nel registro del bridge, e l'agente le legge. Verificato prima di consegnarlo: POST con token → 200 e riga nel registro, senza token → 401.
17. Il banner del remorse sui tasti rapidi — v0.7.6
Carlo: "Il problema c'e' quando premo un pulsante". Meta' della diagnosi stava in quella frase, e nessuno l'aveva ancora detta: succede alla pressione.
In Silica RemorseItem eredita da RemorseBase, che e' uno SwipeItem e
alla execute() fa parent = item.parent: si ri-parenta dentro il contenitore
dell'oggetto premuto. I tasti rapidi della Home stanno in una Row di tre, e
il riquadro del remorse non ha la larghezza del tasto: il suo BannerBackground
(il messaggio piu' il conto alla rovescia) viene disegnato a margini fissi e
finisce sopra i tasti vicini. Ecco il "copre i pulsanti": non la fila che si
sposta, ma un banner disegnato sopra.
MainPage.qml: tolti i treremorse.execute(...)(boost, assenza-off). Il messaggio lo mostra gia' laNoticeBarin fondo: era un doppione e il danno.NoticeBar.qml: altezza fissa a una riga (elide, nonwrap) e proprieta'banda, cosi' le pagine possono riservarle lo spazio;- le cinque pagine con la barra (
MainPage,CommandsPage,DataPage,PlantPage,SetpointsPage):anchors.bottomMargin: notice.bandasullaSilicaFlickable— il messaggio non copre piu' il contenuto e non lo sposta quando compare.
Lezione di metodo: il comportamento vero dei componenti Silica sta nel loro
sorgente (<target>/usr/lib64/qt5/qml/Sailfish/Silica/…), non in quello che
sembra leggendo il proprio QML. RemorseBase.qml ha dato la risposta in un
minuto, dopo tre diagnosi dedotte e sbagliate.
18. Un solo tasto per il boost, con lo stato della Home — v0.7.7
Carlo: "Unisci i pulsanti boost in modo che ce ne sia solo uno che cambia lo stato. Lo stato deve essere coerente con la home".
In SetpointsPage, sezione Acqua calda sanitaria, c'erano due pulsanti —
«Avvia boost ACS» e «Ferma boost ACS» — entrambi sempre attivi e nessuno dei due
che dicesse cosa sta facendo l'impianto: si poteva chiedere di fermare un boost
che non c'era, e la pagina poteva contraddire la Home, che invece il boost lo
mostra e lo ferma con lo stesso tasto.
- un
ActionButtonsolo, contexte comando scelti daapi.boostActive— lo stesso dato della Home, quindi le due schermate non possono divergere; - riga
Boost running · %1 leftconapi.boostCountdownquando il boost corre, come il conto alla rovescia della Home; - senza
confirmText: la conferma la da' il messaggio in fondo, come in Home (il remorse resta solo dove serve davvero, sui comandi a tutta larghezza).
19. Le diciture della copertina — v0.7.8
Carlo: "Nella cover scrivi T. casa, T. acqua e Temperatura esterna".
La copertina usava le stesse stringhe della Home (qsTr("House"),
qsTr("Hot water")): cambiarne la traduzione avrebbe cambiato anche la Home,
che invece deve restare «Casa» e «Acqua calda» (sua richiesta esplicita, solo per
la copertina). Servono quindi stringhe proprie della copertina:
T. house→ italianoT. casa;T. water→ italianoT. acqua;Outside temperature %1 °C(eraoutside %1 °C, usata solo qui) → italianoTemperatura esterna %1 °C.
Le fonti restano in inglese, come tutto il catalogo; l'italiano sta nel .ts.
La Home non cambia.
20. Via la barra di avanzamento dell'acqua — v0.7.9
Carlo, dopo aver chiesto cosa fosse: "Toglila pure".
La barra diceva quanto l'acqua è carica verso l'obiettivo partendo da 10 °C (non da zero), quindi stava quasi sempre al 99% e non aggiungeva informazione: una striscia che non cambia non dice niente, e non si spiegava da sola (servita la domanda "cos'è la striscia bianca sotto l'acqua calda?").
ReadingCell.qml: toltiprogress,showBare ilProgressBar(era usato solo dalla Home, quindi sparisce dal componente);MainPage.qml: toltoprogress: page.waterProgress()/showBar: truee la funzionewaterProgress(), che non serviva più.
Regola: un indicatore permanente che resta sempre allo stesso valore è rumore. Se un giorno servisse, va ancorato a qualcosa che cambia davvero.
22. Il cambio lingua — v0.7.10
Carlo: "Tutto perfetto. Solo il cambio lingua non sembra funzionare".
Misurato invece che dedotto, con un banco vero in container (Qt 5.15, non 5.6: differenza che conta). Esiti:
- installare il traduttore non ri-traduce le stringhe gia' a schermo
(conferma: Qt 5.6 non ha
retranslate()); setSource()con la stessa url ri-traduce (House -> Casa): la tecnica dell'app e' quella giusta;- il catalogo si trova anche con locale
it_IT:load()ripiega sul nome accorciatoit; e nel pacchetto sta in/usr/share/harbour-aira/translations/harbour-aira-it.qm, la cartella che l'app cerca.
Restava un'unica differenza fra banco e telefono: la versione di Qt. La
ricarica si affidava a un comportamento di setSource(stessa url) che cambia
fra versioni, quindi ora si fa in due mosse — radice scaricata con url vuota,
poi ricaricata: cosi' il componente deve essere ricreato, su qualunque Qt.
E per non indovinare piu': la Home manda al /layout anche lingua, locale e
catalogo, cosi' il registro del bridge dice cosa sta usando davvero il
telefono.
23. 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_assenzaleggestartedend(AAAA-MM-GG) e li traduce incurrent_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:
/summaryesponeboost_activeeboost_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 + dueValueButton+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 formattazionemm:sssta inAiraJson::countdown(C++, verificata intests/core_test.cpp), i secondi li fa scorrere un timer del client, che si riallinea a ogni rilettura. - Catalogo:
lupdate harbour-aira.proleggeva 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.