diff --git a/README.it.md b/README.it.md index b4a0065..e30dbb0 100644 --- a/README.it.md +++ b/README.it.md @@ -15,24 +15,29 @@ e ripiega sull'inglese; la si può scegliere anche nelle impostazioni dell'app. ## Cosa mostra -- **Home**, ordinata come l'app ufficiale: temperatura esterna e *in casa / - assente* in alto, una frase di stato in chiaro ("Relax: la pompa di calore - funziona regolarmente"), poi la **scheda dell'impianto** con le due letture che - contano (temperatura stanza e acqua calda, ognuna con il suo obiettivo). - Toccando la scheda si apre la pagina dell'impianto. Sotto, i comandi: - obiettivo della casa con slider 5–30 °C (passi da 0,5 °C) e conferma prima - dell'invio, obiettivo ACS (45–60 °C) e i pulsanti del boost +- **Home**, una schermata da guardare: pallino colorato con lo stato in una riga + ("Tutto regolare", "Sta scaldando l'acqua"), l'ora dell'ultimo aggiornamento, le + due letture che contano (casa e acqua calda, ognuna con il suo obiettivo) + affiancate e **toccabili**, e tre azioni rapide: boost, assenza, comandi + impianto. Sta in una schermata, senza scorrere: i comandi hanno pagine loro +- **Regolazioni**: obiettivo casa e obiettivo acqua calda, ognuno con il valore + che l'impianto sta tenendo adesso, lo slider (la proposta: il valore vero non si + muove), una fila di valori rapidi per le dita fredde e il pulsante di conferma, + che si accende solo se c'è davvero qualcosa da cambiare. Il boost sta qui - **Dati**: calore prodotto, elettricità usata, efficienza (COP) di acqua calda e riscaldamento, risparmio in bolletta, CO₂ evitata e risparmio Smart Tariff Control, mese per mese. Sono i numeri che Aira calcola nel cloud, gli stessi della scheda *Dati* dell'app ufficiale - **Impianto**: stato (connessione, modalità, assenza/notte/riscaldamento - forzato, temperature esterna e interna, zone, ultima lettura), accessori (il + forzato, modalità manuale, sbrinamento, luci di firma, stato pompa, modi + consentiti, temperature esterna e interna, zone, ultima lettura), accessori (il termostato a parete con temperatura, umidità, segnale, batteria), versioni del - software e numero di errori -- **Altri comandi**: assenza, night mode per un'ora, riscaldamento forzato, - funzione riscaldamento on/off, ciclo legionella, ACS on/off e verifica - impianto, con lo stato attuale in cima + software, numero di errori e verifica impianto. Qui sta anche la frase di stato + estesa ("Relax: la pompa di calore funziona regolarmente") +- **Comandi impianto**: quattro interruttori (assenza, riscaldamento forzato, + riscaldamento, acqua calda attiva) che mostrano lo stato e lo cambiano in un + tocco, più i pulsanti per quello che uno stato non ce l'ha: ciclo legionella e + night mode per un'ora. Gli interruttori seguono l'impianto, non lo anticipano - **Copertina** con la temperatura della casa - Indirizzo e token del bridge stanno nelle impostazioni dell'app (QSettings), mai nel binario @@ -43,13 +48,13 @@ Anteprime del layout (valori veri dell'impianto, **non** screenshot — vedi *Stato*). Queste sono in italiano; quelle in inglese stanno in `docs/anteprima/`: -| Home | Dati | Impianto | +| Home | Regolazioni | Dati | |---|---|---| -| ![home](docs/anteprima/it/dashboard.png) | ![dati](docs/anteprima/it/dati.png) | ![impianto](docs/anteprima/it/impianto.png) | +| ![home](docs/anteprima/it/dashboard.png) | ![regolazioni](docs/anteprima/it/regolazioni.png) | ![dati](docs/anteprima/it/dati.png) | -| Altri comandi | Impostazioni | Copertina | -|---|---|---| -| ![comandi](docs/anteprima/it/comandi.png) | ![impostazioni](docs/anteprima/it/impostazioni.png) | ![copertina](docs/anteprima/it/copertina.png) | +| Impianto | Comandi impianto | Impostazioni | Copertina | +|---|---|---|---| +| ![impianto](docs/anteprima/it/impianto.png) | ![comandi](docs/anteprima/it/comandi.png) | ![impostazioni](docs/anteprima/it/impostazioni.png) | ![copertina](docs/anteprima/it/copertina.png) | ## La lingua dell'interfaccia @@ -151,10 +156,12 @@ python3 tests/ui_contract_check.py http://192.168.3.21:8790 ## Stato -- **61/61** verifiche del nucleo, compilazione senza warning +- **79/79** verifiche del nucleo, compilazione senza warning (comprese le frasi + di stato, breve ed estesa: quello che la Home dice dell'impianto è verificato + senza telefono) - **48/48** verifiche offline del bridge (`bridge/test_bridge_commands.py`), comprese le conversioni Wh→kWh e degli importi delle statistiche -- **170/170** stringhe tradotte nel catalogo italiano: il test carica il `.qm` +- **190/190** stringhe tradotte nel catalogo italiano: il test carica il `.qm` compilato come fa `main.cpp` e verifica che in inglese l'etichetta di stato sia la sorgente e in italiano quella tradotta, e che togliendo il catalogo si torni all'inglese @@ -188,13 +195,16 @@ src/translator.{h,cpp} lingua dell'interfaccia: locale di sistema, ripiego sull'inglese, ricarica della UI al cambio src/main.cpp context properties: appSettings, api, translator qml/harbour-aira.qml ApplicationWindow + copertina -qml/pages/MainPage.qml home: stato, scheda impianto, riscaldamento e acqua +qml/pages/MainPage.qml home: stato in una riga, due letture, azioni rapide +qml/pages/SetpointsPage.qml obiettivi casa e acqua, valori rapidi, boost qml/pages/DataPage.qml energia, efficienza e risparmi -qml/pages/PlantPage.qml stato, accessori, versioni, errori -qml/pages/CommandsPage.qml gli altri comandi, con lo stato attuale in cima +qml/pages/PlantPage.qml stato, accessori, versioni, errori, diagnostica +qml/pages/CommandsPage.qml i comandi impianto, stato negli interruttori qml/pages/SettingsPage.qml qml/components/InfoRow.qml etichetta a sinistra, valore a destra qml/components/ValueRow.qml titolo e sottotitolo a sinistra, valore grande a destra +qml/components/ReadingCell.qml lettura toccabile: titolo, valore grande, barra +qml/components/NoticeBar.qml messaggi dei comandi, ancorati in fondo qml/components/ActionButton.qml pulsante di comando, con conferma opzionale tests/core_test.cpp verifiche del nucleo (headless) tests/ui_contract_check.py chiavi dei QML contro quelle del bridge (bridge attivo) @@ -207,7 +217,7 @@ bridge/harbour-aira-bridge.service unit systemd utente bridge/README.md istruzioni di installazione del bridge (inglese) docs/PIANO.md piano di progetto e scelte docs/PROTOCOL.md API del bridge -docs/UX-STUDIO.md studio UX e proposta per la 0.6.0 +docs/UX-STUDIO.md studio UX e cosa è cambiato nella 0.6.0 docs/ux/ schemi dello studio UX translations/ harbour-aira-it.ts (l'inglese è la lingua sorgente) docs/anteprima/ anteprime di layout in inglese (in italiano: it/) diff --git a/README.md b/README.md index 8402903..4a6ef1a 100644 --- a/README.md +++ b/README.md @@ -9,24 +9,30 @@ status, domestic hot water, and the house target temperature per zone. ## What it does -- **Home**, organised like the official Aira app: outdoor temperature and - *at home / away* at the top, a plain-language status line ("Relax: the heat - pump is running smoothly"), then the **plant card** with the two readings that - matter (room temperature and domestic hot water, each with its target). - Tapping the card opens the plant page. Below that the controls: house target - with a 5–30 °C slider (0.5 °C steps) and a confirmation before the command - leaves, domestic hot water target (45–60 °C) and the boost buttons +- **Home**, one screen to look at: a coloured dot with a one-line status + ("Everything is fine", "Heating the hot water"), the time of the last update, + the two readings that matter (house and domestic hot water, each with its + target) side by side and **tappable**, and three quick actions: boost, away + mode and the plant commands. It fits without scrolling — the controls live in + their own pages +- **Setpoints** page: house target and hot water target, each with the value the + plant is keeping right now, a slider (the proposal, the plant value does not + move), a row of quick values for cold fingers and a confirmation button that + only lights up when there is something to change - **Data** page: heat produced, electricity used, efficiency (COP) for hot water and heating, savings on the bill, CO₂ avoided and the Smart Tariff Control saving, month by month. These are the numbers Aira computes in the cloud, exactly as in the official app's *Data* tab -- **Plant** page: status (connection, mode, away/night/forced heating, outdoor - and indoor temperature, zones, last reading), accessories (the wall thermostat - with temperature, humidity, signal, battery), software versions and the error - count -- **Other commands** page: away mode, night mode for one hour, force heating, - heating function on/off, legionella cycle, DHW heating on/off and a plant - check — with the current state at the top, so nothing is pressed blindly +- **Plant** page: status (connection, mode, away/night/forced heating, manual + mode, defrost, signature lights, pump state, allowed modes, outdoor and indoor + temperature, zones, last reading), accessories (the wall thermostat with + temperature, humidity, signal, battery), software versions, the error count and + the plant check. The extended status sentence ("Relax: the heat pump is running + smoothly") lives here +- **Plant commands** page: four switches (away mode, forced heating, heating, hot + water active) that show the state of the plant and change it in one tap, plus + the buttons for what has no state: legionella cycle and night mode for one + hour. The switches follow the plant — they never anticipate it - **Cover page** showing the house temperature - Address and bridge token live in the app settings (QSettings), never in the binary @@ -39,13 +45,13 @@ status, domestic hot water, and the house target temperature per zone. Layout previews (rendered with the real values the plant returned, **not** screenshots — see *Status*): -| Home | Data | Plant | +| Home | Setpoints | Data | |---|---|---| -| ![home](docs/anteprima/dashboard.png) | ![data](docs/anteprima/dati.png) | ![plant](docs/anteprima/impianto.png) | +| ![home](docs/anteprima/dashboard.png) | ![setpoints](docs/anteprima/regolazioni.png) | ![data](docs/anteprima/dati.png) | -| Other commands | Settings | Cover | -|---|---|---| -| ![commands](docs/anteprima/comandi.png) | ![settings](docs/anteprima/impostazioni.png) | ![cover](docs/anteprima/copertina.png) | +| Plant | Plant commands | Settings | Cover | +|---|---|---|---| +| ![plant](docs/anteprima/impianto.png) | ![commands](docs/anteprima/comandi.png) | ![settings](docs/anteprima/impostazioni.png) | ![cover](docs/anteprima/copertina.png) | ## Why there is a bridge @@ -136,12 +142,14 @@ python3 tests/ui_contract_check.py http://192.168.3.21:8790 Verified so far: -- **61/61** core checks (`tests/core_test.cpp`), compiled with no warnings +- **79/79** core checks (`tests/core_test.cpp`), compiled with no warnings — + including the one-line and extended status sentences, so what the home says + about the plant is verified without a phone - **48/48** bridge checks, offline (`bridge/test_bridge_commands.py`) — including that a zone command always carries every zone, that absurd values are refused (40 °C, zone 3, text), and the Wh→kWh / money conversions of the statistics -- **170/170** strings translated in the Italian catalogue, checked by loading +- **190/190** strings translated in the Italian catalogue, checked by loading the compiled `.qm` in the test the same way `main.cpp` does: source English in, Italian out, and back to English when the catalogue is removed - numbers, amounts and month names formatted through `QLocale`, so the decimal @@ -202,13 +210,16 @@ src/settings.{h,cpp} QSettings in AppConfigLocation (SailJail sandbox) src/translator.{h,cpp} UI language: system locale, English fallback, reload src/main.cpp context properties: appSettings, api, translator qml/harbour-aira.qml ApplicationWindow + cover -qml/pages/MainPage.qml home: status, plant card, heating and water +qml/pages/MainPage.qml home: one-line status, two readings, quick actions +qml/pages/SetpointsPage.qml house and water targets, quick values, boost qml/pages/DataPage.qml energy, efficiency and savings -qml/pages/PlantPage.qml status, accessories, versions, errors -qml/pages/CommandsPage.qml the other commands, current state on top +qml/pages/PlantPage.qml status, accessories, versions, errors, diagnostics +qml/pages/CommandsPage.qml the plant commands, state shown by switches qml/pages/SettingsPage.qml qml/components/InfoRow.qml label on the left, value on the right qml/components/ValueRow.qml title + subtitle left, big value right +qml/components/ReadingCell.qml tappable reading: title, big value, charge bar +qml/components/NoticeBar.qml command messages, anchored to the bottom edge qml/components/ActionButton.qml command button, optional confirmation tests/core_test.cpp core checks (headless) tests/ui_contract_check.py QML keys vs bridge keys (bridge must be running) @@ -216,7 +227,7 @@ bridge/ local HTTP bridge -> see bridge/README.md translations/ harbour-aira-it.ts (English is the source language) docs/PIANO.md project plan and decisions (Italian) docs/PROTOCOL.md bridge API -docs/UX-STUDIO.md UX review + proposal for 0.6.0 (Italian) +docs/UX-STUDIO.md UX review and what changed in 0.6.0 (Italian) docs/ux/ UX study wireframes docs/anteprima/ layout previews (English; docs/anteprima/it/ Italian) ``` diff --git a/docs/PIANO.md b/docs/PIANO.md index d31b3aa..06f1437 100644 --- a/docs/PIANO.md +++ b/docs/PIANO.md @@ -231,3 +231,52 @@ andrebbe disegnata a mano. 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: **190/190** tradotte. Anteprime +rigenerate (nuova `regolazioni.png`) in italiano e inglese. + +- Nessun comando nuovo all'impianto: anche questa versione resta **sola + lettura** finché non si prova l'invio dal telefono. diff --git a/docs/UX-STUDIO.md b/docs/UX-STUDIO.md index 7669626..aafb2ef 100644 --- a/docs/UX-STUDIO.md +++ b/docs/UX-STUDIO.md @@ -8,6 +8,33 @@ Immagini di questo studio (schemi, non anteprime): `docs/ux/`. --- +## 0. Cosa è stato deciso e cosa è stato fatto (0.6.0) + +Le quattro scelte, applicate: + +1. La frase di stato lunga sta nella pagina **Impianto**; in Home resta la riga + compatta con il pallino colorato. +2. "Altri comandi" è diventata **"Comandi impianto"**. +3. Le azioni rapide sono **pulsanti visibili** in Home. +4. Azioni sulla copertina: **rimandate** a dopo la prova di `CoverAction` sul kit. + +Problemi risolti: P1 (Home di consultazione), P2 (feedback ancorato), P3 +(interruttori), P4 (un solo percorso per pagina), P5 (un blocco per +grandezza, valori rapidi), P6 (numeri vecchi spenti + ora), P7 ("esterna"), +P9, P10 (diagnostica in Impianto), P11. + +Rimandato: P8 (azioni sulla copertina), in attesa della prova sul telefono. + +Verifiche fatte qui: **79 verifiche del nucleo** (erano 61), `qmllint` pulito su +tutti i file QML, 190/190 stringhe tradotte, anteprime rigenerate in inglese e +italiano, contratto dei dati fra UI e bridge rispettato +(`tests/ui_contract_check.py`). +Da provare sul telefono: che `TextSwitch` si istanzi davvero, l'altezza reale +della Home (riga di log con `contentHeight` contro l'altezza dello schermo) e +l'uso con una mano sola. + +--- + ## 1. Metodo Tre riferimenti, in quest'ordine: diff --git a/docs/anteprima/comandi.png b/docs/anteprima/comandi.png index 968f33b..9740dc7 100644 Binary files a/docs/anteprima/comandi.png and b/docs/anteprima/comandi.png differ diff --git a/docs/anteprima/copertina.png b/docs/anteprima/copertina.png index da53351..37587ee 100644 Binary files a/docs/anteprima/copertina.png and b/docs/anteprima/copertina.png differ diff --git a/docs/anteprima/dashboard.png b/docs/anteprima/dashboard.png index 40a22ab..511980c 100644 Binary files a/docs/anteprima/dashboard.png and b/docs/anteprima/dashboard.png differ diff --git a/docs/anteprima/impianto.png b/docs/anteprima/impianto.png index cc8485a..e97bf0c 100644 Binary files a/docs/anteprima/impianto.png and b/docs/anteprima/impianto.png differ diff --git a/docs/anteprima/impostazioni.png b/docs/anteprima/impostazioni.png index e6467d8..41af571 100644 Binary files a/docs/anteprima/impostazioni.png and b/docs/anteprima/impostazioni.png differ diff --git a/docs/anteprima/it/comandi.png b/docs/anteprima/it/comandi.png index 5b52ecf..4ad61fc 100644 Binary files a/docs/anteprima/it/comandi.png and b/docs/anteprima/it/comandi.png differ diff --git a/docs/anteprima/it/dashboard.png b/docs/anteprima/it/dashboard.png index 7d2d597..5e8b7ff 100644 Binary files a/docs/anteprima/it/dashboard.png and b/docs/anteprima/it/dashboard.png differ diff --git a/docs/anteprima/it/impianto.png b/docs/anteprima/it/impianto.png index 63bd4fd..3a3e5bc 100644 Binary files a/docs/anteprima/it/impianto.png and b/docs/anteprima/it/impianto.png differ diff --git a/docs/anteprima/it/impostazioni.png b/docs/anteprima/it/impostazioni.png index 45f22a2..09aac36 100644 Binary files a/docs/anteprima/it/impostazioni.png and b/docs/anteprima/it/impostazioni.png differ diff --git a/docs/anteprima/it/regolazioni.png b/docs/anteprima/it/regolazioni.png new file mode 100644 index 0000000..46eabbd Binary files /dev/null and b/docs/anteprima/it/regolazioni.png differ diff --git a/docs/anteprima/regolazioni.png b/docs/anteprima/regolazioni.png new file mode 100644 index 0000000..320ef70 Binary files /dev/null and b/docs/anteprima/regolazioni.png differ diff --git a/rpm/harbour-aira.spec b/rpm/harbour-aira.spec index c5117ff..f7b58b1 100644 --- a/rpm/harbour-aira.spec +++ b/rpm/harbour-aira.spec @@ -1,6 +1,6 @@ Name: harbour-aira Summary: Control your Aira heat pump through a local bridge -Version: 0.5.0 +Version: 0.6.0 Release: 1 Group: Qt/Qt License: MIT @@ -37,6 +37,18 @@ Address and token of the bridge live in the app settings (AppConfigLocation); no personal data is stored in the binary. %changelog +* Sat Oct 10 2026 Carlo - 0.6.0-1 +- Home di sola consultazione: stato in una riga con pallino colorato, le due + letture (casa e acqua calda) grandi e toccabili, tre azioni rapide; le due + regolazioni e i pulsanti di navigazione escono dalla Home +- pagina Regolazioni: obiettivo casa e obiettivo acqua calda, ognuno con il + valore attuale sempre visibile, slider, valori rapidi e conferma; boost ACS +- Comandi impianto: quattro interruttori al posto di otto pulsanti acceso/ + spento, piu' tre pulsanti per le azioni senza stato; diagnostica spostata + nella pagina Impianto +- messaggi dei comandi ancorati in fondo alla pagina, visibili sempre +- numeri vecchi mostrati spenti, con l'ora dell'ultimo aggiornamento +- lo stato si rilegge da solo due secondi dopo un comando * Sat Oct 10 2026 Carlo - 0.5.0-1 - schermata iniziale riordinata come l'app ufficiale: temperatura esterna in alto, messaggio di stato, scheda dell'impianto con casa e acqua calda diff --git a/scripts/render_mockups.py b/scripts/render_mockups.py index 890a0ba..d5468ba 100644 --- a/scripts/render_mockups.py +++ b/scripts/render_mockups.py @@ -327,6 +327,92 @@ TEXTS = { LANG = "en" +# --------------------------------------------------------------------------- # +# Testi aggiunti nella 0.6.0 (Home di consultazione, pagina Regolazioni, +# Comandi con interruttori). Le due lingue in un colpo solo: le anteprime +# restano bilingui come l'app. +TEXTS["en"].update({ + "outside": "Outside {v}", + "house_cell": "House", + "hot_water_cell": "Hot water", + "cell_target": "target {v}", + "status_fine": "Everything is fine", + "status_water": "Heating the hot water", + "updated_at": "Updated at 21:18", + "pull_hint": "Pull down: Aggiorna · Dati · Impianto · Impostazioni", + "tap_adjust": "Tap a value to adjust it", + "quick_actions": "Quick actions", + "boost": "Boost", + "away_btn": "Away mode", + "back_home": "Back home", + "commands": "Commands", + "contorno": "Outside {a} · Thermostat {b} · Zones 1", + "setpoints": "Setpoints", + "current_target": "Current target", + "keep_now": "What the plant is keeping now", + "water_now": "Water now {v}", + "hot_water_boost": "Hot water boost", + "set_house_v": "Set house to {v} °C", + "set_water_v": "Set target {v} °C", + "plant_commands": "Plant commands", + "modes": "Modes", + "sw_away_desc": "Lowers the house and stops the hot water", + "sw_forced_desc": "Heats at full power until you stop it", + "sw_heating_desc": "The heating can turn on", + "sw_water_desc": "The heat pump keeps the water in temperature", + "legionella": "Legionella cycle", + "switches_note": "The switches show the current state of the plant.", + "commands_note": "Commands go through the bridge and may take a few seconds.", + "diagnostics": "Diagnostics", + "plant_check": "Plant check", + "manual_mode": "Manual mode", + "defrost": "Defrost", + "signature_lights": "Signature lights", + "save_and_test": "Save and test", + "cover_outdoor": "outside", +}) + +TEXTS["it"].update({ + "house_cell": "Casa", + "hot_water_cell": "Acqua calda", + "cell_target": "obiettivo {v}", + "status_fine": "Tutto regolare", + "status_water": "Sta scaldando l'acqua", + "updated_at": "Aggiornato alle 21:18", + "pull_hint": "Trascina in giù: Aggiorna · Dati · Impianto · Impostazioni", + "tap_adjust": "Tocca un valore per regolarlo", + "quick_actions": "Azioni rapide", + "boost": "Boost", + "away_btn": "Assenza", + "back_home": "Torna a casa", + "commands": "Comandi", + "contorno": "Esterno {a} · Termostato {b} · Zone 1", + "setpoints": "Regolazioni", + "current_target": "Obiettivo attuale", + "keep_now": "Quello che l'impianto mantiene adesso", + "water_now": "Acqua ora {v}", + "hot_water_boost": "Boost acqua calda", + "set_house_v": "Imposta casa a {v} °C", + "set_water_v": "Imposta obiettivo {v} °C", + "plant_commands": "Comandi impianto", + "modes": "Modalità", + "sw_away_desc": "Abbassa la casa e spegne l'acqua calda", + "sw_forced_desc": "Scalda a pieno regime finché non lo fermi", + "sw_heating_desc": "Il riscaldamento può accendersi", + "sw_water_desc": "Mantiene l'acqua in temperatura", + "legionella": "Ciclo legionella", + "switches_note": "Gli interruttori mostrano lo stato attuale dell'impianto.", + "commands_note": "I comandi passano dal bridge e possono richiedere qualche secondo.", + "diagnostics": "Diagnostica", + "plant_check": "Verifica impianto", + "manual_mode": "Modalità manuale", + "defrost": "Sbrinamento", + "signature_lights": "Luci di firma", + "save_and_test": "Salva e prova", + "cover_outdoor": "esterna", + "night_one_hour": "Modalità notte per un'ora", +}) + def T(key: str) -> str: return TEXTS[LANG][key] @@ -363,65 +449,67 @@ def value_row(d: ImageDraw.ImageDraw, y: int, title: str, subtitle: str, # --------------------------------------------------------------------------- # +def reading_cell(d: ImageDraw.ImageDraw, x: int, y: int, w: int, h: int, + title: str, value: str, subtitle: str, + frac: float = None) -> None: + """Riquadro di una lettura grande (componente ReadingCell dell'app): titolo, + valore, riga sotto e barra di avanzamento facoltativa.""" + d.rounded_rectangle([x, y, x + w, y + h], radius=18, fill=(26, 32, 41), + outline=BTN_EDGE, width=2) + d.text((x + 26, y + 22), title, font=font(24), fill=SECONDARY) + d.text((x + 26, y + 56), value, font=font(56, bold=True), fill=PRIMARY) + d.text((x + 26, y + 132), subtitle, font=font(20), fill=SECONDARY) + if frac is not None: + bx0, bx1 = x + 26, x + w - 26 + by = y + h - 30 + d.rounded_rectangle([bx0, by, bx1, by + 10], radius=5, fill=(46, 54, 66)) + d.rounded_rectangle([bx0, by, bx0 + (bx1 - bx0) * max(0.0, min(1.0, frac)), by + 10], + radius=5, fill=ACCENT) + + def main_screen() -> Image.Image: - """Schermata iniziale, ordinata come quella dell'app ufficiale: temperatura - esterna in alto, messaggio di stato, scheda dell'impianto, poi i comandi.""" + """Home della 0.6.0: una schermata da guardare, non un modulo. Stato in una + riga, le due letture che contano, tre azioni rapide. Niente scorrimento.""" img = background() d = ImageDraw.Draw(img) status_bar(d) page_header(d, T("app")) + d.text((M, 128), T("updated_at"), font=font(22), fill=SECONDARY) - y = 150 - # ---- barra in alto: fuori casa / a casa ------------------------------ # - d.text((M, y), T("outside").format(v=dec(18.0) + " °C"), font=font(24), fill=SECONDARY) - casa = T("at_home") - d.text((W - M - d.textlength(casa, font=font(22)), y + 2), casa, + y = 176 + # ---- stato in una riga ------------------------------------------------ # + d.ellipse([M, y + 7, M + 18, y + 25], fill=OK) + d.text((M + 34, y), T("status_water"), font=font(26), fill=PRIMARY) + y += 54 + + # ---- le due letture che contano --------------------------------------- # + gap = 20 + cw = (W - 2 * M - gap) // 2 + ch = 200 + reading_cell(d, M, y, cw, ch, T("house_cell"), dec(23.0) + " °C", + T("cell_target").format(v=dec(19.0) + " °C")) + reading_cell(d, M + cw + gap, y, cw, ch, T("hot_water_cell"), dec(50.6) + " °C", + T("cell_target").format(v=dec(55.0) + " °C"), + (50.6 - 10) / (55.0 - 10)) + y += ch + 18 + + d.text((M, y), T("tap_adjust"), font=font(22), fill=SECONDARY) + y += 54 + + # ---- azioni rapide ----------------------------------------------------- # + y = section_header(d, y, T("quick_actions")) + bw = (W - 2 * M - 2 * gap) // 3 + for i, testo in enumerate((T("boost"), T("away_btn"), T("commands"))): + x = M + i * (bw + gap) + d.rounded_rectangle([x, y, x + bw, y + 74], radius=18, fill=BTN_BG, + outline=BTN_EDGE, width=2) + tw = d.textlength(testo, font=font(24)) + d.text((x + (bw - tw) / 2, y + 23), testo, font=font(24), fill=PRIMARY) + y += 104 + + # ---- letture di contorno ----------------------------------------------- # + d.text((M, y), T("contorno").format(a=dec(17.8) + " °C", b=dec(22.8) + " °C"), font=font(22), fill=SECONDARY) - y += 58 - - # ---- messaggio di stato ---------------------------------------------- # - fnt = font(30) - righe = wrap(d, T("status_relax"), fnt, W - 2 * M - 20) - for i, riga in enumerate(righe): - d.text(((W - d.textlength(riga, font=fnt)) / 2, y + i * 40), riga, - font=fnt, fill=PRIMARY) - y += 40 * len(righe) + 6 - det = T("status_detail") - d.text(((W - d.textlength(det, font=font(22))) / 2, y), det, font=font(22), fill=SECONDARY) - y += 46 - - # ---- scheda dell'impianto -------------------------------------------- # - card(d, y, 282) - d.text((M + 28, y + 22), T("device_name"), font=font(26), fill=PRIMARY) - d.text((M + 28, y + 58), T("device_link"), font=font(22), fill=SECONDARY) - d.text((W - M - 46, y + 24), "›", font=font(42), fill=ACCENT) - value_row(d, y + 100, T("room_temperature"), - T("target_value").format(v=dec(19.0) + " °C"), dec(22.8) + " °C") - value_row(d, y + 186, T("dhw_row"), - T("target_value").format(v=dec(55.0) + " °C"), dec(51.0) + " °C") - y += 282 + 26 - - # ---- riscaldamento della casa ---------------------------------------- # - y = section_header(d, y, T("heating_house")) - line = T("setpoint_line") - d.text(((W - d.textlength(line, font=font(22))) / 2, y - 8), line, - font=font(22), fill=SECONDARY) - y = slider(d, y + 28, (19.0 - 5) / (30 - 5), dec(19.0) + " °C", T("new_house_target")) - y = button(d, y, T("set_house").format(v=dec(19.0)), primary=True) - - # ---- acqua calda sanitaria ------------------------------------------- # - y = section_header(d, y + 6, T("dhw_section")) - progress(d, y, (51.0 - 20) / (65 - 20)) - y += 22 - y = slider(d, y + 4, (55.0 - 45) / (60 - 45), "55 °C", T("target_label")) - y = button(d, y, T("set_acs"), primary=True) - y = button(d, y, T("start_boost")) - y = button(d, y, T("stop_boost")) - - # ---- pagine collegate (scorrono sotto) ------------------------------- # - y = section_header(d, y + 6, T("details")) - y = button(d, y, T("plant_accessories")) - y = button(d, y, T("consumption")) return img @@ -449,10 +537,9 @@ def settings_screen() -> Image.Image: d.multiline_text((M, y), T("settings_note"), font=font(22), fill=SECONDARY, spacing=10) y += 104 - y = button(d, y, T("test_connection"), primary=True) + # Un pulsante solo: salvare e provare erano due gesti per la stessa cosa. + y = button(d, y, T("save_and_test"), primary=True) d.text((M, y), T("connection_ok"), font=font(24), fill=OK) - y += 58 - y = button(d, y, T("save")) return img @@ -488,32 +575,87 @@ def cover_screen() -> Image.Image: return img +def switch_row(d: ImageDraw.ImageDraw, y: int, text: str, description: str, + checked: bool) -> int: + """Riga con interruttore (componente TextSwitch dell'app): etichetta e + spiegazione a sinistra, interruttore a destra con lo stato dell'impianto.""" + d.text((M, y), text, font=font(28), fill=PRIMARY) + d.text((M, y + 38), description, font=font(22), fill=SECONDARY) + pw, ph = 96, 44 + x0 = W - M - pw + y0 = y + 8 + d.rounded_rectangle([x0, y0, x0 + pw, y0 + ph], radius=ph / 2, + fill=ACCENT if checked else (48, 56, 68)) + cx = x0 + pw - ph / 2 if checked else x0 + ph / 2 + d.ellipse([cx - ph / 2 + 5, y0 + 5, cx + ph / 2 - 5, y0 + ph - 5], + fill=(238, 242, 246)) + return y + 98 + + def commands_screen() -> Image.Image: + """Comandi impianto della 0.6.0: gli interruttori dicono lo stato e si + cambiano in un tocco; i pulsanti restano per le azioni senza stato.""" img = background() d = ImageDraw.Draw(img) status_bar(d) - page_header(d, T("other_commands")) + page_header(d, T("plant_commands")) - y = section_header(d, 156, T("current_state")) - for lab, val in ((T("away"), T("off")), (T("night_mode"), T("off")), - (T("forced_heating"), T("off")), (T("hot_water_active"), T("on")), - (T("pump"), T("stopped")), (T("allowed_modes"), T("heating"))): - y = label_pair(d, y, lab, val) + y = section_header(d, 150, T("modes")) + y = switch_row(d, y, T("away"), T("sw_away_desc"), False) + y = switch_row(d, y, T("forced_heating"), T("sw_forced_desc"), False) + y = switch_row(d, y, T("heating"), T("sw_heating_desc"), True) - y = section_header(d, y + 24, T("away")) - y = button(d, y, T("turn_away_on")) - y = button(d, y, T("turn_away_off")) + y = section_header(d, y + 10, T("dhw_section")) + y = switch_row(d, y, T("hot_water_active"), T("sw_water_desc"), True) + y = button(d, y, T("legionella")) - y = section_header(d, y + 24, T("noise")) + y = section_header(d, y + 10, T("noise")) y = button(d, y, T("night_one_hour")) - y = section_header(d, y + 24, T("heating")) - y = button(d, y, T("force_heating"), primary=True) - hint = T("confirm_hint") - d.text(((W - d.textlength(hint, font=font(22))) / 2, y - 4), hint, font=font(22), fill=SECONDARY) + d.text((M, y + 12), T("switches_note"), font=font(22), fill=SECONDARY) return img +def setpoints_screen() -> Image.Image: + """Regolazioni: un blocco per grandezza, con il valore attuale sempre + visibile, le preimpostazioni e la conferma.""" + img = background() + d = ImageDraw.Draw(img) + status_bar(d) + page_header(d, T("setpoints")) + + y = section_header(d, 150, T("heating_house")) + y = value_row(d, y, T("current_target"), T("keep_now"), dec(19.0) + " °C") + y = slider(d, y, (19.0 - 5) / (30 - 5), dec(19.0) + " °C", T("new_house_target")) + y = preset_row(d, y, ["16°", "17°", "18°", "19°", "20°", "21°"]) + y = button(d, y, T("set_house_v").format(v=dec(19.0)), primary=True) + + y = section_header(d, y + 6, T("dhw_section")) + y = value_row(d, y, T("dhw_target"), T("water_now").format(v=dec(50.6) + " °C"), + dec(55.0) + " °C") + y = slider(d, y, (55.0 - 45) / (60 - 45), dec(55.0) + " °C", T("target_label")) + y = preset_row(d, y, ["45°", "47°", "50°", "55°", "60°"]) + y = button(d, y, T("set_water_v").format(v=dec(55.0)), primary=True) + + y = section_header(d, y + 6, T("hot_water_boost")) + y = button(d, y, T("start_boost")) + y = button(d, y, T("stop_boost")) + return img + + +def preset_row(d: ImageDraw.ImageDraw, y: int, valori: list) -> int: + """Fila di valori rapidi: un tocco invece del cursore, per le dita fredde.""" + gap = 12 + bw = (W - 2 * M - (len(valori) - 1) * gap) // len(valori) + for i, testo in enumerate(valori): + x = M + i * (bw + gap) + d.rounded_rectangle([x, y, x + bw, y + 62], radius=16, fill=BTN_BG, + outline=BTN_EDGE, width=2) + tw = d.textlength(testo, font=font(24)) + d.text((x + (bw - tw) / 2, y + 17), testo, font=font(24), fill=PRIMARY) + return y + 86 + + def data_screen() -> Image.Image: """Pagina "Dati": gli stessi numeri della scheda Dati dell'app ufficiale.""" img = background() @@ -566,11 +708,18 @@ def plant_screen() -> Image.Image: status_bar(d) page_header(d, T("plant")) - y = 150 + # La frase estesa dell'app ufficiale: sta qui, non in Home. + righe = wrap(d, T("status_relax"), font(24), W - 2 * M) + for i, riga in enumerate(righe): + d.text((M, 132 + i * 32), riga, font=font(24), fill=PRIMARY) + + y = 150 + 32 * len(righe) y = section_header(d, y, T("status")) for lab, val in ((T("connection"), T("cloud")), (T("mode"), T("automatic")), (T("away"), T("off")), (T("night_mode"), T("off")), - (T("forced_heating"), T("off"))): + (T("forced_heating"), T("off")), (T("manual_mode"), T("off")), + (T("defrost"), T("off")), (T("signature_lights"), T("on")), + (T("pump"), T("stopped")), (T("allowed_modes"), T("heating"))): y = label_pair(d, y, lab, val) y = section_header(d, y + 20, T("accessories")) @@ -587,6 +736,9 @@ def plant_screen() -> Image.Image: y = section_header(d, y + 20, T("errors")) d.text((M, y), T("no_errors"), font=font(24), fill=OK) + + y = section_header(d, y + 40, T("diagnostics")) + y = button(d, y, T("plant_check")) return img @@ -602,7 +754,8 @@ if __name__ == "__main__": out = pathlib.Path("docs/anteprima") if lang == "en" else pathlib.Path("docs/anteprima") / lang out.mkdir(parents=True, exist_ok=True) - for name, fn in (("dashboard", main_screen), ("dati", data_screen), + for name, fn in (("dashboard", main_screen), ("regolazioni", setpoints_screen), + ("dati", data_screen), ("impianto", plant_screen), ("comandi", commands_screen), ("impostazioni", settings_screen), ("copertina", cover_screen)): path = out / f"{name}.png" diff --git a/src/main.cpp b/src/main.cpp index 70a1827..d4cded0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -21,7 +21,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[]) app->setOrganizationName(QStringLiteral("harbour")); app->setApplicationName(QStringLiteral("aira")); - app->setApplicationVersion(QStringLiteral("0.5.0")); + app->setApplicationVersion(QStringLiteral("0.6.0")); Settings settings; ApiClient api(&settings); @@ -36,7 +36,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[]) // Prima riga del log: dice subito se il device sta girando la build nuova // o una stale (trappola nota del build engine). - qDebug() << "harbour-aira v0.5.0 build" << __DATE__ << __TIME__; + qDebug() << "harbour-aira v0.6.0 build" << __DATE__ << __TIME__; view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings); view->rootContext()->setContextProperty(QStringLiteral("api"), &api);