0.6.0 blocco 3: copertina, versione, documenti e anteprime nelle due lingue

This commit is contained in:
2026-10-10 09:52:32 +02:00
parent 85702880d5
commit 36033daf3f
18 changed files with 385 additions and 123 deletions
+33 -23
View File
@@ -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/)
+36 -25
View File
@@ -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)
```
+49
View File
@@ -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.
+27
View File
@@ -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:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

+13 -1
View File
@@ -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 <carlo@carlobaratto.it> - 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 <carlo@carlobaratto.it> - 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
+225 -72
View File
@@ -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"
+2 -2
View File
@@ -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);