0.4.0: interfaccia in italiano e inglese, scelta nelle impostazioni

L'inglese è la lingua sorgente delle stringhe; l'italiano sta nel catalogo
translations/harbour-aira-it.ts (99 stringhe), che lupdate aggiorna e lrelease
compila in build. Di serie la lingua del sistema, con ripiego sull'inglese.

- nuova classe Translator: risolve la lingua prima di createView e allinea
  QLocale::setDefault, così anche i numeri seguono la lingua (23,0 °C in
  italiano, 23.0 °C in inglese) — le temperature passano da formatValue, non
  più dal toFixed() di JavaScript che metteva sempre il punto
- Qt 5.6 non ha QQmlEngine::retranslate(): al cambio di lingua la radice QML
  viene ricaricata (setSource ricrea il componente anche a url invariata)
- le etichette di stato in airajson.cpp usano QCoreApplication::translate per
  esteso: lupdate non riconosce una funzione wrapper e le avrebbe perse
- selettore della lingua in Impostazioni (Sistema / English / Italiano)
- test: 41/41, carica il .qm vero come fa main.cpp e verifica entrambe le
  lingue, il ripiego e i formati numerici
- anteprime bilingui (docs/anteprima/ inglese, it/ italiano), README e PIANO
  aggiornati, spec RPM con i cataloghi in %files
This commit is contained in:
Carlo
2026-10-10 08:19:20 +02:00
parent e0dcf1f830
commit fddd21e16a
29 changed files with 1230 additions and 190 deletions
+3
View File
@@ -16,6 +16,9 @@ bridge/.venv/
__pycache__/ __pycache__/
*.pyc *.pyc
# cataloghi compilati: si rigenerano in build da translations/*.ts
translations/*.qm
# temporanei # temporanei
*~ *~
*.bak *.bak
+41 -3
View File
@@ -10,8 +10,37 @@ App nativa Sailfish OS per una pompa di calore **Aira Home**: mostra lo stato
dell'impianto e invia i comandi dell'acqua calda sanitaria e del riscaldamento dell'impianto e invia i comandi dell'acqua calda sanitaria e del riscaldamento
(temperatura obiettivo della casa, per zona). (temperatura obiettivo della casa, per zona).
L'interfaccia è in **inglese e italiano**: di serie segue la lingua del telefono
e ripiega sull'inglese; la si può scegliere anche nelle impostazioni dell'app.
![architettura](docs/architettura.svg) ![architettura](docs/architettura.svg)
Anteprime del layout (valori veri dell'impianto, **non** screenshot — vedi
*Stato*). Queste sono in italiano; quelle in inglese stanno in
`docs/anteprima/`:
| Home | Altri comandi | Impostazioni | Copertina |
|---|---|---|---|
| ![home](docs/anteprima/it/dashboard.png) | ![comandi](docs/anteprima/it/comandi.png) | ![impostazioni](docs/anteprima/it/impostazioni.png) | ![copertina](docs/anteprima/it/copertina.png) |
## La lingua dell'interfaccia
L'inglese è la **lingua sorgente** delle stringhe (sta direttamente nel codice);
l'italiano sta nel catalogo `translations/harbour-aira-it.ts`, che `lupdate`
aggiorna e `lrelease` compila in build.
La scelta fatta nelle impostazioni è salvata in `QSettings` con il codice della
lingua; vuoto (o lingua assente) significa **lingua di sistema**, con ripiego
sull'inglese quando non esiste il catalogo corrispondente.
Qt 5.6 non ha `QQmlEngine::retranslate()` (arriva con Qt 5.10): installare un
traduttore non basta a ritradurre quello che è già a schermo. L'app quindi, al
cambio di lingua, **ricarica la radice QML** — la scelta ha effetto subito, al
prezzo di tornare alla pagina iniziale.
Anche i numeri seguono la lingua: le temperature passano da `QLocale`, quindi in
italiano si scrive `23,0 °C` e in inglese `23.0 °C`.
## Perché c'è un bridge (e non una chiamata diretta al cloud) ## Perché c'è un bridge (e non una chiamata diretta al cloud)
Il cloud Aira espone **gRPC su HTTP/2**. Qt 5.6 — il toolkit su cui gira ogni Il cloud Aira espone **gRPC su HTTP/2**. Qt 5.6 — il toolkit su cui gira ogni
@@ -82,8 +111,14 @@ d'errore. **La UI Silica si verifica solo sul dispositivo.**
## Stato ## Stato
- **31/31** verifiche del nucleo, compilazione senza warning - **39/39** verifiche del nucleo, compilazione senza warning
- **27/27** verifiche offline del bridge (`bridge/test_bridge_commands.py`) - **27/27** verifiche offline del bridge (`bridge/test_bridge_commands.py`)
- **99/99** 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
- numeri formattati con `QLocale`: il separatore decimale segue la lingua
(verificato per entrambe)
- `qmllint` pulito su tutti i QML (valida solo la sintassi: senza il modulo - `qmllint` pulito su tutti i QML (valida solo la sintassi: senza il modulo
Silica una proprietà inesistente passa in silenzio) Silica una proprietà inesistente passa in silenzio)
- provata dal vivo in **sola lettura** su un impianto Aira Home - provata dal vivo in **sola lettura** su un impianto Aira Home
@@ -102,7 +137,9 @@ harbour-aira.pro progetto qmake (icona, qml, docs)
src/airajson.{h,cpp} logica pura, testabile senza SDK src/airajson.{h,cpp} logica pura, testabile senza SDK
src/apiclient.{h,cpp} QNetworkAccessManager + watchdog di timeout src/apiclient.{h,cpp} QNetworkAccessManager + watchdog di timeout
src/settings.{h,cpp} QSettings su AppConfigLocation (sandbox SailJail) src/settings.{h,cpp} QSettings su AppConfigLocation (sandbox SailJail)
src/main.cpp context properties: appSettings, api 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/harbour-aira.qml ApplicationWindow + copertina
qml/pages/MainPage.qml dashboard e comandi acqua/riscaldamento qml/pages/MainPage.qml dashboard e comandi acqua/riscaldamento
qml/pages/CommandsPage.qml gli altri comandi, con lo stato attuale in cima qml/pages/CommandsPage.qml gli altri comandi, con lo stato attuale in cima
@@ -118,7 +155,8 @@ bridge/harbour-aira-bridge.service unit systemd utente
bridge/README.md istruzioni di installazione del bridge (inglese) bridge/README.md istruzioni di installazione del bridge (inglese)
docs/PIANO.md piano di progetto e scelte docs/PIANO.md piano di progetto e scelte
docs/PROTOCOL.md API del bridge docs/PROTOCOL.md API del bridge
docs/anteprima/ anteprime di layout translations/ harbour-aira-it.ts (l'inglese è la lingua sorgente)
docs/anteprima/ anteprime di layout in inglese (in italiano: it/)
``` ```
## Licenza ## Licenza
+24 -6
View File
@@ -21,13 +21,18 @@ status, domestic hot water, and the house target temperature per zone.
- **Cover page** showing the house temperature - **Cover page** showing the house temperature
- Address and bridge token live in the app settings (QSettings), never in the - Address and bridge token live in the app settings (QSettings), never in the
binary binary
- **Languages: English and Italian.** The interface follows the phone language by
default and falls back to English when there is no catalogue for it. The
language can also be picked in the settings; the app reloads to apply it, and
numbers follow the chosen language too (decimal comma in Italian, dot in
English)
Layout previews (rendered with the real values the plant returned, **not** Layout previews (rendered with the real values the plant returned, **not**
screenshots — see *Status*): screenshots — see *Status*):
| Home | Other commands | Cover | | Home | Other commands | Settings | Cover |
|---|---|---| |---|---|---|---|
| ![home](docs/anteprima/dashboard.png) | ![commands](docs/anteprima/comandi.png) | ![cover](docs/anteprima/copertina.png) | | ![home](docs/anteprima/dashboard.png) | ![commands](docs/anteprima/comandi.png) | ![settings](docs/anteprima/impostazioni.png) | ![cover](docs/anteprima/copertina.png) |
## Why there is a bridge ## Why there is a bridge
@@ -107,10 +112,15 @@ parsing, mode labels, command payloads, error messages.
Verified so far: Verified so far:
- **31/31** core checks (`tests/core_test.cpp`), compiled with no warnings - **39/39** core checks (`tests/core_test.cpp`), compiled with no warnings
- **27/27** bridge checks, offline (`bridge/test_bridge_commands.py`) — - **27/27** bridge checks, offline (`bridge/test_bridge_commands.py`) —
including that a zone command always carries every zone, and that absurd including that a zone command always carries every zone, and that absurd
values are refused (40 °C, zone 3, text) values are refused (40 °C, zone 3, text)
- **99/99** 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 formatted through `QLocale`, so the decimal separator follows the
language (verified for both)
- `qmllint` clean on every QML file. Note it validates syntax only: without the - `qmllint` clean on every QML file. Note it validates syntax only: without the
Silica module an invalid Silica property passes silently Silica module an invalid Silica property passes silently
- read-only queries against one real Aira Home unit, through the bridge - read-only queries against one real Aira Home unit, through the bridge
@@ -120,6 +130,12 @@ Not verified: **the app has not been built or run on a phone yet** — the first
build is up to whoever has the Sailfish SDK. The images in `docs/anteprima/` are build is up to whoever has the Sailfish SDK. The images in `docs/anteprima/` are
layout previews built from the real plant values, not screenshots. layout previews built from the real plant values, not screenshots.
A Qt 5.6 note on the language switch: `QQmlEngine::retranslate()` only exists
from Qt 5.10, so installing a translator is not enough to re-translate what is
already on screen. The app therefore reloads the QML root when the language
changes: the setting applies immediately, at the cost of landing back on the home
page.
Caveats: Caveats:
- **Unofficial client**, not in the Jolla Store: it talks to Aira's API with - **Unofficial client**, not in the Jolla Store: it talks to Aira's API with
@@ -152,7 +168,8 @@ harbour-aira.pro qmake project (icons, qml, docs)
src/airajson.{h,cpp} pure logic, testable without the SDK src/airajson.{h,cpp} pure logic, testable without the SDK
src/apiclient.{h,cpp} QNetworkAccessManager + timeout watchdog src/apiclient.{h,cpp} QNetworkAccessManager + timeout watchdog
src/settings.{h,cpp} QSettings in AppConfigLocation (SailJail sandbox) src/settings.{h,cpp} QSettings in AppConfigLocation (SailJail sandbox)
src/main.cpp context properties: appSettings, api 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/harbour-aira.qml ApplicationWindow + cover
qml/pages/MainPage.qml dashboard and water/heating commands qml/pages/MainPage.qml dashboard and water/heating commands
qml/pages/CommandsPage.qml the other commands, current state on top qml/pages/CommandsPage.qml the other commands, current state on top
@@ -161,9 +178,10 @@ qml/components/InfoRow.qml
qml/components/ActionButton.qml command button, optional confirmation qml/components/ActionButton.qml command button, optional confirmation
tests/core_test.cpp core checks (headless) tests/core_test.cpp core checks (headless)
bridge/ local HTTP bridge -> see bridge/README.md 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/PIANO.md project plan and decisions (Italian)
docs/PROTOCOL.md bridge API docs/PROTOCOL.md bridge API
docs/anteprima/ layout previews docs/anteprima/ layout previews (English; docs/anteprima/it/ Italian)
``` ```
## License ## License
+24 -1
View File
@@ -8,7 +8,7 @@ Piano scritto il 9 ottobre 2026, prima di scrivere la UI (convenzione di lavoro)
|---|---| |---|---|
| 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) | | 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 | | 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.1.0 | | Bridge HTTP sul server | **scritto e provato** — `~/workspace/Carlo/Aira/aira_bridge.py`, v**0.3.0** (servizio systemd utente attivo) |
| Bluetooth sul server | **assente** — nessun adattatore HCI (esclude la via locale BLE su questa macchina) | | 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 | | 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 | | Jolla Phone 2026 | non raggiungibile dalla rete del server: deploy solo dal PC |
@@ -119,6 +119,29 @@ Non verificabile qui (serve il suo PC + il telefono): build RPM, istallazione, U
solo per LAN e VPN. *Lezione*: un test da `127.0.0.1` non attraversa quelle regole e non solo per LAN e VPN. *Lezione*: un test da `127.0.0.1` non attraversa quelle regole e non
rivela il problema — va provato dalla LAN. rivela il problema — va provato dalla LAN.
- **App 0.4.0 — localizzazione ita/eng** (richiesta dell'utente): l'interfaccia si
può mettere in italiano o inglese **dalle impostazioni**; di serie segue la lingua
del sistema, con **ripiego sull'inglese**. L'inglese è la lingua sorgente delle
stringhe (restano nel codice), l'italiano sta in `translations/harbour-aira-it.ts`
(**99 stringhe**, aggiornate con `lupdate` e compilate da `lrelease` in build).
Nuova classe `Translator` (`src/translator.{h,cpp}`): risolve la lingua **prima**
di `createView`, così le `qsTr()` del QML nascono già tradotte, e allinea
`QLocale::setDefault` alla lingua scelta — così i numeri seguono la lingua
(`23,0 °C` in italiano, `23.0 °C` in inglese): le temperature si formattano in
`ApiClient::formatValue`, non più con il `toFixed()` di JavaScript, che avrebbe
sempre messo il punto. *Vincolo di piattaforma*: **Qt 5.6 non ha
`QQmlEngine::retranslate()`** (arriva con Qt 5.10), quindi installare il
traduttore non ritraduce ciò che è già a schermo: al cambio lingua l'app
**ricarica la radice QML**. *Trappola di `lupdate`*: riconosce solo le funzioni di
traduzione note (`tr()`, `QCoreApplication::translate()`), **non una funzione
wrapper scritta da noi** — le etichette di `airajson.cpp` sono quindi chiamate
`QCoreApplication::translate("AiraJson", …)` per esteso, altrimenti sparirebbero
dal catalogo. Nucleo app: **39/39** (le attese sono cambiate in inglese e il test
carica il `.qm` vero come fa `main.cpp`, verificando entrambe le lingue, il
ripiego e i formati numerici). Anteprime rigenerate in **due lingue**
(`docs/anteprima/` inglese, `docs/anteprima/it/` italiano), impostazioni comprese
la nuova riga della lingua.
## 6. Rischi noti ## 6. Rischi noti
- **Dipendenza dal server**: se il bridge è spento, l'app non vede nulla. Mitigazione: unità - **Dipendenza dal server**: se il bridge è spento, l'app non vede nulla. Mitigazione: unità
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

+14 -3
View File
@@ -12,11 +12,13 @@ QT += network
SOURCES += \ SOURCES += \
src/main.cpp \ src/main.cpp \
src/settings.cpp \ src/settings.cpp \
src/translator.cpp \
src/apiclient.cpp \ src/apiclient.cpp \
src/airajson.cpp src/airajson.cpp
HEADERS += \ HEADERS += \
src/settings.h \ src/settings.h \
src/translator.h \
src/apiclient.h \ src/apiclient.h \
src/airajson.h src/airajson.h
@@ -31,9 +33,18 @@ OTHER_FILES += \
DISTFILES += $$OTHER_FILES DISTFILES += $$OTHER_FILES
# Niente catalogo di traduzione in 0.1.0: le stringhe sorgente sono in italiano # Traduzioni: l'inglese e' la lingua sorgente (nessun .qm: Qt lascia le stringhe
# (app personale). Se un giorno serve lo store Harbour si aggiungono i .ts e la # originali quando per il locale non c'e' catalogo = ripiego automatico), mentre
# compilazione con lrelease dentro lo spec, come negli altri progetti. # l'italiano sta in translations/harbour-aira-it.ts.
# Con sailfishapp_i18n qmake compila i .ts in .qm e li installa in
# /usr/share/<TARGET>/translations, che e' dove li cerca src/translator.cpp.
# NIENTE .qm committati: si rigenerano in build.
CONFIG += sailfishapp_i18n
TRANSLATIONS += \
translations/harbour-aira-it.ts
OTHER_FILES += translations/*.ts
# Icone: struttura standard hicolor, path assoluti (gli install manuali con # Icone: struttura standard hicolor, path assoluti (gli install manuali con
# path relativi falliscono quando il tar sorgente e' incompleto). # path relativi falliscono quando il tar sorgente e' incompleto).
+5 -5
View File
@@ -16,7 +16,7 @@ CoverBackground {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: Theme.paddingLarge anchors.topMargin: Theme.paddingLarge
text: qsTr("Aira") text: "Aira"
color: Theme.primaryColor color: Theme.primaryColor
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
} }
@@ -37,7 +37,7 @@ CoverBackground {
Label { Label {
width: parent.width width: parent.width
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: qsTr("casa · obiettivo %1 °C").arg(cover.t("zone1_setpoint")) text: qsTr("house · target %1 °C").arg(cover.t("zone1_setpoint"))
color: Theme.secondaryColor color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall font.pixelSize: Theme.fontSizeExtraSmall
truncationMode: TruncationMode.Fade truncationMode: TruncationMode.Fade
@@ -46,7 +46,7 @@ CoverBackground {
Label { Label {
width: parent.width width: parent.width
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: qsTr("ACS %1 °C · obiettivo %2 °C").arg(cover.t("dhw_current")).arg(cover.t("dhw_target")) text: qsTr("DHW %1 °C · target %2 °C").arg(cover.t("dhw_current")).arg(cover.t("dhw_target"))
color: Theme.secondaryColor color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall font.pixelSize: Theme.fontSizeExtraSmall
truncationMode: TruncationMode.Fade truncationMode: TruncationMode.Fade
@@ -57,8 +57,8 @@ CoverBackground {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.paddingMedium anchors.bottomMargin: Theme.paddingMedium
text: qsTr("est. %1 °C").arg(cover.t("outdoor_temperature")) text: qsTr("outdoor %1 °C").arg(cover.t("outdoor_temperature"))
color: Theme.secondaryColor color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall font.pixelSize: Theme.fontSizeExtraSmall
} }
} }
+1 -1
View File
@@ -14,4 +14,4 @@ ApplicationWindow {
// Nessun allowedOrientations qui: a livello di window limita TUTTE le // Nessun allowedOrientations qui: a livello di window limita TUTTE le
// pagine (intersezione), anche quelle che dichiarano Orientation.All. // pagine (intersezione), anche quelle che dichiarano Orientation.All.
} }
+30 -30
View File
@@ -17,7 +17,7 @@ Page {
} }
function flag(key) { function flag(key) {
return page.raw(key) ? qsTr("sì") : qsTr("no") return page.raw(key) ? qsTr("yes") : qsTr("no")
} }
function stato(key) { function stato(key) {
@@ -51,7 +51,7 @@ Page {
width: parent.width width: parent.width
spacing: Theme.paddingSmall spacing: Theme.paddingSmall
PageHeader { title: qsTr("Altri comandi") } PageHeader { title: qsTr("Other commands") }
Label { Label {
visible: page.notice.length > 0 visible: page.notice.length > 0
@@ -64,80 +64,80 @@ Page {
} }
// ---- stato attuale -------------------------------------------- // // ---- stato attuale -------------------------------------------- //
SectionHeader { text: qsTr("Stato attuale") } SectionHeader { text: qsTr("Current state") }
InfoRow { label: qsTr("Assenza"); value: page.flag("away_mode") } InfoRow { label: qsTr("Away mode"); value: page.flag("away_mode") }
InfoRow { label: qsTr("Night mode"); value: page.flag("night_mode") } InfoRow { label: qsTr("Night mode"); value: page.flag("night_mode") }
InfoRow { label: qsTr("Riscaldamento forzato"); value: page.flag("force_heating") } InfoRow { label: qsTr("Forced heating"); value: page.flag("force_heating") }
InfoRow { label: qsTr("Acqua calda attiva"); value: page.flag("hot_water_heating") } InfoRow { label: qsTr("Hot water active"); value: page.flag("hot_water_heating") }
InfoRow { label: qsTr("Pompa"); value: page.stato("pump_active_state") } InfoRow { label: qsTr("Pump"); value: page.stato("pump_active_state") }
InfoRow { label: qsTr("Modalità consentite"); value: page.stato("configured_modes") } InfoRow { label: qsTr("Allowed modes"); value: page.stato("configured_modes") }
// ---- assenza --------------------------------------------------- // // ---- assenza --------------------------------------------------- //
SectionHeader { text: qsTr("Assenza") } SectionHeader { text: qsTr("Away mode") }
ActionButton { ActionButton {
text: qsTr("Attiva assenza") text: qsTr("Turn away mode on")
onTriggered: api.sendCommand("away_mode_on") onTriggered: api.sendCommand("away_mode_on")
} }
ActionButton { ActionButton {
text: qsTr("Disattiva assenza") text: qsTr("Turn away mode off")
onTriggered: api.sendCommand("away_mode_off") onTriggered: api.sendCommand("away_mode_off")
} }
// ---- rumore ---------------------------------------------------- // // ---- rumore ---------------------------------------------------- //
SectionHeader { text: qsTr("Rumore") } SectionHeader { text: qsTr("Noise") }
ActionButton { ActionButton {
text: qsTr("Night mode per un'ora") text: qsTr("Night mode for one hour")
onTriggered: api.sendCommand("night_mode_1h") onTriggered: api.sendCommand("night_mode_1h")
} }
// ---- riscaldamento --------------------------------------------- // // ---- riscaldamento --------------------------------------------- //
SectionHeader { text: qsTr("Riscaldamento") } SectionHeader { text: qsTr("Heating") }
ActionButton { ActionButton {
text: qsTr("Forza riscaldamento") text: qsTr("Force heating")
confirmText: qsTr("Avvio del riscaldamento forzato") confirmText: qsTr("Starting forced heating")
onTriggered: api.sendCommand("force_heating_on") onTriggered: api.sendCommand("force_heating_on")
} }
ActionButton { ActionButton {
text: qsTr("Ferma riscaldamento forzato") text: qsTr("Stop forced heating")
onTriggered: api.sendCommand("force_heating_off") onTriggered: api.sendCommand("force_heating_off")
} }
ActionButton { ActionButton {
text: qsTr("Abilita funzione riscaldamento") text: qsTr("Enable heating function")
onTriggered: api.sendCommand("heating_on") onTriggered: api.sendCommand("heating_on")
} }
ActionButton { ActionButton {
text: qsTr("Disabilita funzione riscaldamento") text: qsTr("Disable heating function")
confirmText: qsTr("Il riscaldamento verrà disabilitato") confirmText: qsTr("Heating will be disabled")
onTriggered: api.sendCommand("heating_off") onTriggered: api.sendCommand("heating_off")
} }
// ---- acqua calda ----------------------------------------------- // // ---- acqua calda ----------------------------------------------- //
SectionHeader { text: qsTr("Acqua calda sanitaria") } SectionHeader { text: qsTr("Domestic hot water") }
ActionButton { ActionButton {
text: qsTr("Ciclo antilegionella") text: qsTr("Legionella cycle")
confirmText: qsTr("Avvio del ciclo antilegionella: richiede tempo") confirmText: qsTr("Starting the legionella cycle: it takes a while")
onTriggered: api.sendCommand("legionella_cycle") onTriggered: api.sendCommand("legionella_cycle")
} }
ActionButton { ActionButton {
text: qsTr("Abilita riscaldamento ACS") text: qsTr("Enable DHW heating")
onTriggered: api.sendCommand("hot_water_heating_on") onTriggered: api.sendCommand("hot_water_heating_on")
} }
ActionButton { ActionButton {
text: qsTr("Disabilita riscaldamento ACS") text: qsTr("Disable DHW heating")
confirmText: qsTr("L'acqua calda smetterà di essere riscaldata") confirmText: qsTr("Domestic hot water will stop being heated")
onTriggered: api.sendCommand("hot_water_heating_off") onTriggered: api.sendCommand("hot_water_heating_off")
} }
// ---- diagnostica ----------------------------------------------- // // ---- diagnostica ----------------------------------------------- //
SectionHeader { text: qsTr("Diagnostica") } SectionHeader { text: qsTr("Diagnostics") }
ActionButton { ActionButton {
text: qsTr("Verifica impianto") text: qsTr("Plant check")
onTriggered: api.sendCommand("ping") onTriggered: api.sendCommand("ping")
} }
@@ -151,7 +151,7 @@ Page {
Label { Label {
x: Theme.horizontalPageMargin x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin
text: qsTr("I comandi passano dal bridge e possono richiedere qualche secondo. Dopo l'invio lo stato si aggiorna da solo.") text: qsTr("Commands go through the bridge and may take a few seconds. The status refreshes by itself afterwards.")
color: Theme.secondaryColor color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall font.pixelSize: Theme.fontSizeExtraSmall
wrapMode: Text.Wrap wrapMode: Text.Wrap
@@ -163,4 +163,4 @@ Page {
if (api.configured) if (api.configured)
api.refresh() api.refresh()
} }
} }
+39 -35
View File
@@ -38,12 +38,16 @@ Page {
return api.summary[key] return api.summary[key]
} }
// I numeri si formattano nel C++ (ApiClient::formatValue): cosi' il
// separatore decimale segue la lingua dell'app, non il toFixed di JS.
function fmt(value) { return api.formatValue(value) }
function temp(key) { function temp(key) {
return api.formatValue(page.raw(key)) return api.formatValue(page.raw(key))
} }
function flag(key) { function flag(key) {
return page.raw(key) ? qsTr("sì") : qsTr("no") return page.raw(key) ? qsTr("yes") : qsTr("no")
} }
function count(key) { function count(key) {
@@ -98,15 +102,15 @@ Page {
// fanno deref su null (TypeError + crash). // fanno deref su null (TypeError + crash).
PullDownMenu { PullDownMenu {
MenuItem { MenuItem {
text: qsTr("Aggiorna") text: qsTr("Refresh")
onClicked: api.refresh() onClicked: api.refresh()
} }
MenuItem { MenuItem {
text: qsTr("Impostazioni") text: qsTr("Settings")
onClicked: page.openSettings() onClicked: page.openSettings()
} }
MenuItem { MenuItem {
text: qsTr("Altri comandi") text: qsTr("Other commands")
onClicked: page.openCommands() onClicked: page.openCommands()
} }
} }
@@ -116,7 +120,7 @@ Page {
width: parent.width width: parent.width
spacing: Theme.paddingSmall spacing: Theme.paddingSmall
PageHeader { title: qsTr("Aira") } PageHeader { title: "Aira" }
// ---- avvisi ---------------------------------------------------- // // ---- avvisi ---------------------------------------------------- //
Label { Label {
@@ -134,7 +138,7 @@ Page {
visible: !api.configured visible: !api.configured
x: Theme.horizontalPageMargin x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin
text: qsTr("Bridge non configurato. Apri le impostazioni e inserisci l'indirizzo del server e il token.") text: qsTr("Bridge not configured. Open the settings and enter the server address and the token.")
color: Theme.secondaryColor color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.Wrap wrapMode: Text.Wrap
@@ -143,7 +147,7 @@ Page {
Button { Button {
visible: !api.configured visible: !api.configured
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Impostazioni") text: qsTr("Settings")
onClicked: page.openSettings() onClicked: page.openSettings()
} }
@@ -151,7 +155,7 @@ Page {
Label { Label {
visible: api.configured visible: api.configured
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Temperatura in casa") text: qsTr("House temperature")
color: Theme.secondaryColor color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
} }
@@ -179,7 +183,7 @@ Page {
Label { Label {
visible: api.configured visible: api.configured
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("obiettivo %1 °C · %2").arg(page.temp("zone1_setpoint")).arg(api.modeLabel(page.raw("zone1_mode"))) text: qsTr("target %1 °C · %2").arg(page.temp("zone1_setpoint")).arg(api.modeLabel(page.raw("zone1_mode")))
color: Theme.secondaryColor color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall font.pixelSize: Theme.fontSizeExtraSmall
} }
@@ -188,12 +192,12 @@ Page {
id: casaSlider id: casaSlider
visible: api.configured visible: api.configured
width: parent.width width: parent.width
label: qsTr("Nuovo obiettivo casa") label: qsTr("New house target")
minimumValue: 5 minimumValue: 5
maximumValue: 30 maximumValue: 30
stepSize: 0.5 stepSize: 0.5
value: page.targetCasa value: page.targetCasa
valueText: value.toFixed(1) + " °C" valueText: page.fmt(value) + " °C"
onValueChanged: { onValueChanged: {
page.targetCasaTouched = true page.targetCasaTouched = true
page.targetCasa = Math.round(value * 2) / 2 page.targetCasa = Math.round(value * 2) / 2
@@ -209,14 +213,14 @@ Page {
leftMargin: Theme.horizontalPageMargin leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin rightMargin: Theme.horizontalPageMargin
} }
text: qsTr("Imposta casa a %1 °C").arg(page.targetCasa.toFixed(1)) text: qsTr("Set house to %1 °C").arg(page.fmt(page.targetCasa))
onClicked: remorse.execute(setCasaButton, onClicked: remorse.execute(setCasaButton,
qsTr("Temperatura della casa a %1 °C").arg(page.targetCasa.toFixed(1)), qsTr("House temperature to %1 °C").arg(page.fmt(page.targetCasa)),
function() { api.sendCommand("set_heating_setpoint", page.targetCasa) }) function() { api.sendCommand("set_heating_setpoint", page.targetCasa) })
} }
// ---- acqua calda sanitaria ------------------------------------- // // ---- acqua calda sanitaria ------------------------------------- //
SectionHeader { visible: api.configured; text: qsTr("Acqua calda sanitaria") } SectionHeader { visible: api.configured; text: qsTr("Domestic hot water") }
Label { Label {
visible: api.configured visible: api.configured
@@ -240,19 +244,19 @@ Page {
InfoRow { InfoRow {
visible: api.configured visible: api.configured
label: qsTr("Obiettivo acqua calda") label: qsTr("DHW target")
value: page.temp("dhw_target") + " °C" value: page.temp("dhw_target") + " °C"
} }
Slider { Slider {
visible: api.configured visible: api.configured
width: parent.width width: parent.width
label: qsTr("Nuovo obiettivo") label: qsTr("New target")
minimumValue: 45 minimumValue: 45
maximumValue: 60 maximumValue: 60
stepSize: 1 stepSize: 1
value: page.targetAcs value: page.targetAcs
valueText: value + " °C" valueText: page.fmt(value) + " °C"
onValueChanged: { onValueChanged: {
page.targetAcsTouched = true page.targetAcsTouched = true
page.targetAcs = Math.round(value) page.targetAcs = Math.round(value)
@@ -268,9 +272,9 @@ Page {
leftMargin: Theme.horizontalPageMargin leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin rightMargin: Theme.horizontalPageMargin
} }
text: qsTr("Imposta obiettivo %1 °C").arg(page.targetAcs) text: qsTr("Set target %1 °C").arg(page.targetAcs)
onClicked: remorse.execute(setAcsButton, onClicked: remorse.execute(setAcsButton,
qsTr("Obiettivo ACS a %1 °C").arg(page.targetAcs), qsTr("DHW target to %1 °C").arg(page.targetAcs),
function() { api.sendCommand("set_hot_water_target", page.targetAcs) }) function() { api.sendCommand("set_hot_water_target", page.targetAcs) })
} }
@@ -283,8 +287,8 @@ Page {
leftMargin: Theme.horizontalPageMargin leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin rightMargin: Theme.horizontalPageMargin
} }
text: qsTr("Avvia boost ACS") text: qsTr("Start DHW boost")
onClicked: remorse.execute(boostOnButton, qsTr("Avvio del boost ACS"), onClicked: remorse.execute(boostOnButton, qsTr("Starting DHW boost"),
function() { api.sendCommand("hot_water_boost_on") }) function() { api.sendCommand("hot_water_boost_on") })
} }
@@ -297,24 +301,24 @@ Page {
leftMargin: Theme.horizontalPageMargin leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin rightMargin: Theme.horizontalPageMargin
} }
text: qsTr("Ferma boost ACS") text: qsTr("Stop DHW boost")
onClicked: remorse.execute(boostOffButton, qsTr("Arresto del boost ACS"), onClicked: remorse.execute(boostOffButton, qsTr("Stopping DHW boost"),
function() { api.sendCommand("hot_water_boost_off") }) function() { api.sendCommand("hot_water_boost_off") })
} }
// ---- stato impianto ------------------------------------------- // // ---- stato impianto ------------------------------------------- //
SectionHeader { visible: api.configured; text: qsTr("Impianto") } SectionHeader { visible: api.configured; text: qsTr("Plant") }
InfoRow { visible: api.configured; label: qsTr("Temperatura esterna"); value: page.temp("outdoor_temperature") + " °C" } InfoRow { visible: api.configured; label: qsTr("Outdoor temperature"); value: page.temp("outdoor_temperature") + " °C" }
InfoRow { visible: api.configured; label: qsTr("Temperatura interna (pompa)"); value: page.temp("indoor_temperature") + " °C" } InfoRow { visible: api.configured; label: qsTr("Indoor temperature (pump)"); value: page.temp("indoor_temperature") + " °C" }
InfoRow { visible: api.configured; label: qsTr("Modalità"); value: api.modeLabel(page.raw("operating_status")) } InfoRow { visible: api.configured; label: qsTr("Mode"); value: api.modeLabel(page.raw("operating_status")) }
InfoRow { visible: api.configured; label: qsTr("Assenza"); value: page.flag("away_mode") } InfoRow { visible: api.configured; label: qsTr("Away mode"); value: page.flag("away_mode") }
InfoRow { visible: api.configured; label: qsTr("Night mode"); value: page.flag("night_mode") } InfoRow { visible: api.configured; label: qsTr("Night mode"); value: page.flag("night_mode") }
InfoRow { visible: api.configured; label: qsTr("Riscaldamento forzato"); value: page.flag("force_heating") } InfoRow { visible: api.configured; label: qsTr("Forced heating"); value: page.flag("force_heating") }
InfoRow { visible: api.configured; label: qsTr("Resistenze integrative"); value: page.flag("inline_heater") } InfoRow { visible: api.configured; label: qsTr("Inline heater"); value: page.flag("inline_heater") }
InfoRow { visible: api.configured; label: qsTr("Zone"); value: page.count("zones") } InfoRow { visible: api.configured; label: qsTr("Zone"); value: page.count("zones") }
InfoRow { visible: api.configured; label: qsTr("Errori"); value: page.count("errors_count") } InfoRow { visible: api.configured; label: qsTr("Errors"); value: page.count("errors_count") }
InfoRow { visible: api.configured; label: qsTr("Aggiornato alle"); value: page.shortTime(page.raw("pump_updated")) } InfoRow { visible: api.configured; label: qsTr("Last reading"); value: page.shortTime(page.raw("pump_updated")) }
Button { Button {
visible: api.configured visible: api.configured
@@ -324,7 +328,7 @@ Page {
leftMargin: Theme.horizontalPageMargin leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin rightMargin: Theme.horizontalPageMargin
} }
text: qsTr("Altri comandi") text: qsTr("Other commands")
onClicked: page.openCommands() onClicked: page.openCommands()
} }
@@ -339,7 +343,7 @@ Page {
visible: api.configured visible: api.configured
x: Theme.horizontalPageMargin x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin
text: qsTr("I comandi vengono inviati al bridge e da lì all'impianto: possono richiedere qualche secondo.") text: qsTr("Commands are sent to the bridge and from there to the plant: they may take a few seconds.")
color: Theme.secondaryColor color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall font.pixelSize: Theme.fontSizeExtraSmall
wrapMode: Text.Wrap wrapMode: Text.Wrap
@@ -351,4 +355,4 @@ Page {
if (api.configured) if (api.configured)
api.refresh() api.refresh()
} }
} }
+51 -9
View File
@@ -21,7 +21,7 @@ Page {
function testAndReport() { function testAndReport() {
save() save()
page.testResult = qsTr("verifico…") page.testResult = qsTr("checking…")
api.testConnection() api.testConnection()
} }
@@ -47,13 +47,55 @@ Page {
width: parent.width width: parent.width
spacing: Theme.paddingMedium spacing: Theme.paddingMedium
PageHeader { title: qsTr("Impostazioni") } PageHeader { title: qsTr("Settings") }
// ---- lingua ---------------------------------------------------- //
SectionHeader { text: qsTr("Language") }
// Le voci si scelgono dal menu: il valore mostrato segue la scelta
// salvata (translator.language), non l'ultimo tocco.
ComboBox {
id: langBox
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
label: qsTr("Language")
value: translator.language === "it" ? "Italiano"
: translator.language === "en" ? "English"
: qsTr("System")
menu: ContextMenu {
MenuItem {
text: qsTr("System")
onClicked: translator.language = ""
}
// I nomi delle lingue restano nella lingua che nominano.
MenuItem {
text: "English"
onClicked: translator.language = "en"
}
MenuItem {
text: "Italiano"
onClicked: translator.language = "it"
}
}
}
Label {
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
text: qsTr("Changing the language reloads the app.")
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall
wrapMode: Text.Wrap
}
// ---- bridge ---------------------------------------------------- //
SectionHeader { text: qsTr("Bridge") }
TextField { TextField {
id: urlField id: urlField
x: Theme.horizontalPageMargin x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin
label: qsTr("Indirizzo del bridge") label: qsTr("Bridge address")
placeholderText: "192.168.2.50:8790" placeholderText: "192.168.2.50:8790"
text: page.urlText text: page.urlText
onTextChanged: if (text !== page.urlText) page.urlText = text onTextChanged: if (text !== page.urlText) page.urlText = text
@@ -68,7 +110,7 @@ Page {
id: tokenField id: tokenField
x: Theme.horizontalPageMargin x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin
label: qsTr("Token del bridge") label: qsTr("Bridge token")
showEchoModeToggle: true showEchoModeToggle: true
text: page.tokenText text: page.tokenText
onTextChanged: if (text !== page.tokenText) page.tokenText = text onTextChanged: if (text !== page.tokenText) page.tokenText = text
@@ -80,7 +122,7 @@ Page {
Label { Label {
x: Theme.horizontalPageMargin x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin
text: qsTr("Indirizzo e token si leggono sul server: il token è in ~/.hermes/profiles/carlo/aira_bridge.json. Senza schema viene usato http://.") text: qsTr("Address and token live on the server: the token is in aira_bridge.json next to the bridge. Without a scheme, http:// is used.")
color: Theme.secondaryColor color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall font.pixelSize: Theme.fontSizeExtraSmall
wrapMode: Text.Wrap wrapMode: Text.Wrap
@@ -88,17 +130,17 @@ Page {
Button { Button {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Salva") text: qsTr("Save")
onClicked: { onClicked: {
page.save() page.save()
page.testResult = qsTr("impostazioni salvate") page.testResult = qsTr("settings saved")
page.testOk = true page.testOk = true
} }
} }
Button { Button {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Prova connessione") text: qsTr("Test connection")
onClicked: page.testAndReport() onClicked: page.testAndReport()
} }
@@ -113,4 +155,4 @@ Page {
} }
} }
} }
} }
+16 -1
View File
@@ -1,6 +1,6 @@
Name: harbour-aira Name: harbour-aira
Summary: Control your Aira heat pump through a local bridge Summary: Control your Aira heat pump through a local bridge
Version: 0.3.0 Version: 0.4.0
Release: 1 Release: 1
Group: Qt/Qt Group: Qt/Qt
License: MIT License: MIT
@@ -15,6 +15,8 @@ BuildRequires: pkgconfig(Qt5Qml)
BuildRequires: pkgconfig(Qt5Quick) BuildRequires: pkgconfig(Qt5Quick)
BuildRequires: pkgconfig(Qt5Network) BuildRequires: pkgconfig(Qt5Network)
BuildRequires: desktop-file-utils BuildRequires: desktop-file-utils
# lupdate/lrelease: i .qm si compilano in build (CONFIG += sailfishapp_i18n).
BuildRequires: qt5-qttools-linguist
%global debug_package %{nil} %global debug_package %{nil}
@@ -26,10 +28,22 @@ and Qt 5.6 has no HTTP/2. A small bridge on the user's server (Python,
pyairahome) keeps the credentials and exposes the plant as JSON over HTTP/1.1. pyairahome) keeps the credentials and exposes the plant as JSON over HTTP/1.1.
The app shows the current state and sends the hot-water commands. The app shows the current state and sends the hot-water commands.
The interface is in English and Italian: by default it follows the phone
language and falls back to English, and the language can also be picked in the
app settings.
Address and token of the bridge live in the app settings (AppConfigLocation); Address and token of the bridge live in the app settings (AppConfigLocation);
no personal data is stored in the binary. no personal data is stored in the binary.
%changelog %changelog
* Sat Oct 10 2026 Carlo <carlo@carlobaratto.it> - 0.4.0-1
- interfaccia in inglese e italiano, scelta nelle impostazioni con ripiego
sull'inglese; numeri con il separatore decimale della lingua
* Sat Oct 10 2026 Carlo <carlo@carlobaratto.it> - 0.3.0-1
- pagina Altri comandi: assenza, night mode, riscaldamento forzato, legionella,
ACS on/off, verifica impianto; stato attuale in cima alla pagina
* Fri Oct 09 2026 Carlo <carlo@carlobaratto.it> - 0.2.0-1
- temperatura della casa e obiettivo per zona (slider 5-30 gradi)
* Fri Oct 09 2026 Carlo <carlo@carlobaratto.it> - 0.1.0-1 * Fri Oct 09 2026 Carlo <carlo@carlobaratto.it> - 0.1.0-1
- prima release: stato impianto, obiettivo ACS, boost ACS - prima release: stato impianto, obiettivo ACS, boost ACS
@@ -65,3 +79,4 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/%{name}.desktop || tr
%{_datadir}/icons/hicolor/128x128/apps/%{name}.png %{_datadir}/icons/hicolor/128x128/apps/%{name}.png
%{_datadir}/icons/hicolor/172x172/apps/%{name}.png %{_datadir}/icons/hicolor/172x172/apps/%{name}.png
%{_datadir}/%{name}/qml %{_datadir}/%{name}/qml
%{_datadir}/%{name}/translations/%{name}*.qm
+185 -49
View File
@@ -110,52 +110,172 @@ def button(d: ImageDraw.ImageDraw, y: int, text: str, primary: bool = False) ->
return y + h + 16 return y + h + 16
# --------------------------------------------------------------------------- #
# Testi delle anteprime. L'app e' in inglese (lingua sorgente) e italiano, quindi
# le anteprime esistono nelle due lingue: quelle di docs/anteprima/ sono in
# inglese, quelle di docs/anteprima/it/ in italiano.
TEXTS = {
"en": {
"app": "Aira",
"house_temperature": "House temperature",
"heating": "Heating",
"target": "target",
"new_house_target": "New house target",
"set_house": "Set house to {v} °C",
"dhw_section": "Domestic hot water",
"dhw_target": "DHW target",
"target_label": "Target",
"set_acs": "Set DHW target",
"start_boost": "Start DHW boost",
"stop_boost": "Stop DHW boost",
"plant": "Plant",
"outdoor": "Outdoor",
"indoor_pump": "Indoor (pump)",
"pull_to_refresh": "Pull down to refresh",
"settings": "Settings",
"language": "Language",
"system": "System",
"bridge": "Bridge",
"address_label": "Bridge address",
"token_label": "Bridge token",
"settings_note": "The address and the token stay in the local\n"
"configuration of the app, read-only for others.",
"test_connection": "Test connection",
"connection_ok": "Bridge reachable",
"save": "Save",
"cover_target": "target",
"cover_acs": "DHW",
"cover_outdoor": "outdoor",
"cover_footer": "house temperature",
"other_commands": "Other commands",
"current_state": "Current state",
"away": "Away mode",
"night_mode": "Night mode",
"forced_heating": "Forced heating",
"hot_water_active": "Hot water active",
"pump": "Pump",
"stopped": "Stopped",
"allowed_modes": "Allowed modes",
"on": "yes",
"off": "no",
"turn_away_on": "Turn away mode on",
"turn_away_off": "Turn away mode off",
"noise": "Noise",
"night_one_hour": "Night mode for one hour",
"force_heating": "Force heating",
"confirm_hint": "(asks for confirmation first)",
},
"it": {
"app": "Aira",
"house_temperature": "Temperatura in casa",
"heating": "Riscaldamento",
"target": "obiettivo",
"new_house_target": "Nuovo obiettivo casa",
"set_house": "Imposta casa a {v} °C",
"dhw_section": "Acqua calda sanitaria",
"dhw_target": "Obiettivo acqua calda",
"target_label": "Obiettivo ACS",
"set_acs": "Imposta obiettivo ACS",
"start_boost": "Avvia boost ACS",
"stop_boost": "Ferma boost ACS",
"plant": "Impianto",
"outdoor": "Esterna",
"indoor_pump": "Interna (pompa)",
"pull_to_refresh": "Trascina verso il basso per aggiornare",
"settings": "Impostazioni",
"language": "Lingua",
"system": "Sistema",
"bridge": "Bridge",
"address_label": "Indirizzo del bridge",
"token_label": "Token del bridge",
"settings_note": "Indirizzo e token restano nella configurazione\n"
"locale dell'app, in sola lettura per gli altri.",
"test_connection": "Prova connessione",
"connection_ok": "Bridge raggiungibile",
"save": "Salva",
"cover_target": "obiettivo",
"cover_acs": "ACS",
"cover_outdoor": "esterna",
"cover_footer": "temperatura in casa",
"other_commands": "Altri comandi",
"current_state": "Stato attuale",
"away": "Assenza",
"night_mode": "Night mode",
"forced_heating": "Riscaldamento forzato",
"hot_water_active": "Acqua calda attiva",
"pump": "Pompa",
"stopped": "Ferma",
"allowed_modes": "Modalità consentite",
"on": "sì",
"off": "no",
"turn_away_on": "Attiva assenza",
"turn_away_off": "Disattiva assenza",
"noise": "Rumore",
"night_one_hour": "Night mode per un'ora",
"force_heating": "Forza riscaldamento",
"confirm_hint": "(chiede conferma prima di partire)",
},
}
LANG = "en"
def T(key: str) -> str:
return TEXTS[LANG][key]
def dec(value: float) -> str:
"""Numero a un decimale con il separatore della lingua, come fa l'app."""
out = f"{value:.1f}"
return out.replace(".", ",") if LANG == "it" else out
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
def main_screen() -> Image.Image: def main_screen() -> Image.Image:
img = background() img = background()
d = ImageDraw.Draw(img) d = ImageDraw.Draw(img)
status_bar(d) status_bar(d)
page_header(d, "Aira") page_header(d, T("app"))
y = 156 y = 156
# ---- casa: il dato principale, in cima ------------------------------- # # ---- casa: il dato principale, in cima ------------------------------- #
c = "Temperatura in casa" c = T("house_temperature")
d.text(((W - d.textlength(c, font=font(24))) / 2, y), c, font=font(24), fill=SECONDARY) d.text(((W - d.textlength(c, font=font(24))) / 2, y), c, font=font(24), fill=SECONDARY)
y += 40 y += 40
big = "23,0 °C" big = dec(23.0) + " °C"
d.text(((W - d.textlength(big, font=font(92, bold=True))) / 2, y), d.text(((W - d.textlength(big, font=font(92, bold=True))) / 2, y),
big, font=font(92, bold=True), fill=PRIMARY) big, font=font(92, bold=True), fill=PRIMARY)
y += 116 y += 116
progress(d, y, (23.0 - 5) / (30 - 5)) progress(d, y, (23.0 - 5) / (30 - 5))
y += 22 y += 22
t = "obiettivo 19,0 °C · Riscaldamento" t = f'{T("target")} {dec(19.0)} °C · {T("heating")}'
d.text(((W - d.textlength(t, font=font(24))) / 2, y), t, font=font(24), fill=SECONDARY) d.text(((W - d.textlength(t, font=font(24))) / 2, y), t, font=font(24), fill=SECONDARY)
y += 46 y += 46
y = slider(d, y, (19.0 - 5) / (30 - 5), "19,0 °C", "Nuovo obiettivo casa") y = slider(d, y, (19.0 - 5) / (30 - 5), dec(19.0) + " °C", T("new_house_target"))
y = button(d, y, "Imposta casa a 19,0 °C", primary=True) y = button(d, y, T("set_house").format(v=dec(19.0)), primary=True)
# ---- acqua calda sanitaria ------------------------------------------- # # ---- acqua calda sanitaria ------------------------------------------- #
y = section_header(d, y + 24, "Acqua calda sanitaria") y = section_header(d, y + 24, T("dhw_section"))
acs = "51,0 °C" acs = dec(51.0) + " °C"
d.text(((W - d.textlength(acs, font=font(56, bold=True))) / 2, y), d.text(((W - d.textlength(acs, font=font(56, bold=True))) / 2, y),
acs, font=font(56, bold=True), fill=PRIMARY) acs, font=font(56, bold=True), fill=PRIMARY)
y += 76 y += 76
progress(d, y, (51.0 - 20) / (65 - 20)) progress(d, y, (51.0 - 20) / (65 - 20))
y += 22 y += 22
y = label_pair(d, y, "Obiettivo acqua calda", "55,0 °C") y = label_pair(d, y, T("dhw_target"), dec(55.0) + " °C")
y = slider(d, y + 10, (55.0 - 45) / (60 - 45), "55 °C") y = slider(d, y + 10, (55.0 - 45) / (60 - 45), "55 °C", T("target_label"))
y = button(d, y, "Imposta obiettivo ACS", primary=True) y = button(d, y, T("set_acs"), primary=True)
y = button(d, y, "Avvia boost ACS") y = button(d, y, T("start_boost"))
y = button(d, y, "Ferma boost ACS") y = button(d, y, T("stop_boost"))
# ---- stato impianto (scorre sotto) ----------------------------------- # # ---- stato impianto (scorre sotto) ----------------------------------- #
y = section_header(d, y + 24, "Impianto") y = section_header(d, y + 24, T("plant"))
y = label_pair(d, y, "Esterna", "18,0 °C") y = label_pair(d, y, T("outdoor"), dec(18.0) + " °C")
y = label_pair(d, y, "Interna (pompa)", "22,5 °C") y = label_pair(d, y, T("indoor_pump"), dec(22.5) + " °C")
d.text(((W - d.textlength("Trascina verso il basso per aggiornare", hint = T("pull_to_refresh")
font=font(22))) / 2, y + 8), d.text(((W - d.textlength(hint, font=font(22))) / 2, y + 8),
"Trascina verso il basso per aggiornare", font=font(22), fill=SECONDARY) hint, font=font(22), fill=SECONDARY)
return img return img
@@ -163,11 +283,17 @@ def settings_screen() -> Image.Image:
img = background() img = background()
d = ImageDraw.Draw(img) d = ImageDraw.Draw(img)
status_bar(d) status_bar(d)
page_header(d, "Impostazioni") page_header(d, T("settings"))
y = 172 # ---- lingua ----------------------------------------------------------- #
for lab, val in (("Indirizzo del bridge", "192.168.3.21:8790"), y = section_header(d, 156, T("language"))
("Token del bridge", "••••••••••••••••••••••••••••")): y = label_pair(d, y, T("language"), T("system"))
y += 16
# ---- bridge ----------------------------------------------------------- #
y = section_header(d, y, T("bridge"))
for lab, val in ((T("address_label"), "192.168.3.21:8790"),
(T("token_label"), "••••••••••••••••••••••••••••")):
d.text((M, y), lab, font=font(24), fill=SECONDARY) d.text((M, y), lab, font=font(24), fill=SECONDARY)
y += 36 y += 36
d.text((M, y), val, font=font(30), fill=PRIMARY) d.text((M, y), val, font=font(30), fill=PRIMARY)
@@ -175,14 +301,12 @@ def settings_screen() -> Image.Image:
d.line([(M, y), (W - M, y)], fill=BTN_EDGE, width=2) d.line([(M, y), (W - M, y)], fill=BTN_EDGE, width=2)
y += 44 y += 44
d.multiline_text((M, y), "Indirizzo e token restano nella configurazione\n" d.multiline_text((M, y), T("settings_note"), font=font(22), fill=SECONDARY, spacing=10)
"locale dell'app, in sola lettura per gli altri.",
font=font(22), fill=SECONDARY, spacing=10)
y += 104 y += 104
y = button(d, y, "Prova connessione", primary=True) y = button(d, y, T("test_connection"), primary=True)
d.text((M, y), "Connessione riuscita (bridge 0.1.0)", font=font(24), fill=OK) d.text((M, y), T("connection_ok"), font=font(24), fill=OK)
y += 58 y += 58
y = button(d, y, "Salva") y = button(d, y, T("save"))
return img return img
@@ -199,20 +323,22 @@ def cover_screen() -> Image.Image:
# copertina # copertina
card = [96, 430, W - 96, 430 + 470] card = [96, 430, W - 96, 430 + 470]
d.rounded_rectangle(card, radius=34, fill=(14, 18, 24), outline=ACCENT, width=2) d.rounded_rectangle(card, radius=34, fill=(14, 18, 24), outline=ACCENT, width=2)
d.text((card[0] + 34, card[1] + 30), "Aira", font=font(40, bold=True), fill=PRIMARY) d.text((card[0] + 34, card[1] + 30), T("app"), font=font(40, bold=True), fill=PRIMARY)
d.line([(card[0] + 34, card[1] + 100), (card[2] - 34, card[1] + 100)], d.line([(card[0] + 34, card[1] + 100), (card[2] - 34, card[1] + 100)],
fill=BTN_EDGE, width=2) fill=BTN_EDGE, width=2)
y = card[1] + 132 y = card[1] + 132
big = "23,0 °C" big = dec(23.0) + " °C"
d.text(((W - d.textlength(big, font=font(76, bold=True))) / 2, y), d.text(((W - d.textlength(big, font=font(76, bold=True))) / 2, y),
big, font=font(76, bold=True), fill=PRIMARY) big, font=font(76, bold=True), fill=PRIMARY)
y += 108 y += 108
for lab, val in (("obiettivo", "19,0 °C"), ("ACS", "51,0 °C"), ("esterna", "18,0 °C")): for lab, val in ((T("cover_target"), dec(19.0) + " °C"),
(T("cover_acs"), dec(51.0) + " °C"),
(T("cover_outdoor"), dec(18.0) + " °C")):
d.text((card[0] + 34, y), lab, font=font(24), fill=SECONDARY) d.text((card[0] + 34, y), lab, font=font(24), fill=SECONDARY)
vw = d.textlength(val, font=font(24)) vw = d.textlength(val, font=font(24))
d.text((card[2] - 34 - vw, y), val, font=font(24), fill=PRIMARY) d.text((card[2] - 34 - vw, y), val, font=font(24), fill=PRIMARY)
y += 52 y += 52
d.text((card[0] + 34, card[3] - 60), "temperatura in casa", font=font(22), fill=SECONDARY) d.text((card[0] + 34, card[3] - 60), T("cover_footer"), font=font(22), fill=SECONDARY)
return img return img
@@ -220,32 +346,42 @@ def commands_screen() -> Image.Image:
img = background() img = background()
d = ImageDraw.Draw(img) d = ImageDraw.Draw(img)
status_bar(d) status_bar(d)
page_header(d, "Altri comandi") page_header(d, T("other_commands"))
y = section_header(d, 156, "Stato attuale") y = section_header(d, 156, T("current_state"))
for lab, val in (("Assenza", "no"), ("Night mode", "no"), for lab, val in ((T("away"), T("off")), (T("night_mode"), T("off")),
("Riscaldamento forzato", "no"), ("Acqua calda attiva", "sì"), (T("forced_heating"), T("off")), (T("hot_water_active"), T("on")),
("Pompa", "Ferma"), ("Modalità consentite", "Riscaldamento")): (T("pump"), T("stopped")), (T("allowed_modes"), T("heating"))):
y = label_pair(d, y, lab, val) y = label_pair(d, y, lab, val)
y = section_header(d, y + 24, "Assenza") y = section_header(d, y + 24, T("away"))
y = button(d, y, "Attiva assenza") y = button(d, y, T("turn_away_on"))
y = button(d, y, "Disattiva assenza") y = button(d, y, T("turn_away_off"))
y = section_header(d, y + 24, "Rumore") y = section_header(d, y + 24, T("noise"))
y = button(d, y, "Night mode per un'ora") y = button(d, y, T("night_one_hour"))
y = section_header(d, y + 24, "Riscaldamento") y = section_header(d, y + 24, T("heating"))
y = button(d, y, "Forza riscaldamento", primary=True) y = button(d, y, T("force_heating"), primary=True)
d.text(((W - d.textlength("(chiede conferma prima di partire)", font=font(22))) / 2, y - 4), hint = T("confirm_hint")
"(chiede conferma prima di partire)", font=font(22), fill=SECONDARY) d.text(((W - d.textlength(hint, font=font(22))) / 2, y - 4), hint, font=font(22), fill=SECONDARY)
return img return img
if __name__ == "__main__": if __name__ == "__main__":
OUT.mkdir(parents=True, exist_ok=True) import sys
lang = "en"
if "--lang" in sys.argv:
lang = sys.argv[sys.argv.index("--lang") + 1]
if lang not in TEXTS:
raise SystemExit(f"lingua non disponibile: {lang}")
LANG = lang
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), ("comandi", commands_screen), for name, fn in (("dashboard", main_screen), ("comandi", commands_screen),
("impostazioni", settings_screen), ("copertina", cover_screen)): ("impostazioni", settings_screen), ("copertina", cover_screen)):
path = OUT / f"{name}.png" path = out / f"{name}.png"
fn().save(path) fn().save(path)
print(f"scritto {path} ({path.stat().st_size} byte)") print(f"scritto {path} ({path.stat().st_size} byte)")
+22 -17
View File
@@ -1,8 +1,10 @@
#include "airajson.h" #include "airajson.h"
#include <QCoreApplication>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QJsonValue> #include <QJsonValue>
#include <QLocale>
#include <QUrl> #include <QUrl>
namespace AiraJson { namespace AiraJson {
@@ -49,35 +51,35 @@ QString modeLabel(const QString &operatingStatus)
{ {
const QString s = operatingStatus; const QString s = operatingStatus;
if (s == QLatin1String("OPERATING_STATUS_AUTO")) if (s == QLatin1String("OPERATING_STATUS_AUTO"))
return QStringLiteral("Automatico"); return QCoreApplication::translate("AiraJson", "Automatic");
if (s == QLatin1String("OPERATING_STATUS_HEATING")) if (s == QLatin1String("OPERATING_STATUS_HEATING"))
return QStringLiteral("Riscaldamento"); return QCoreApplication::translate("AiraJson", "Heating");
if (s == QLatin1String("OPERATING_STATUS_COOLING")) if (s == QLatin1String("OPERATING_STATUS_COOLING"))
return QStringLiteral("Raffrescamento"); return QCoreApplication::translate("AiraJson", "Cooling");
if (s == QLatin1String("OPERATING_STATUS_HOT_WATER")) if (s == QLatin1String("OPERATING_STATUS_HOT_WATER"))
return QStringLiteral("Acqua calda"); return QCoreApplication::translate("AiraJson", "Hot water");
if (s == QLatin1String("OPERATING_STATUS_STANDBY")) if (s == QLatin1String("OPERATING_STATUS_STANDBY"))
return QStringLiteral("Stand-by"); return QCoreApplication::translate("AiraJson", "Standby");
if (s == QLatin1String("OPERATING_STATUS_OFF")) if (s == QLatin1String("OPERATING_STATUS_OFF"))
return QStringLiteral("Spento"); return QCoreApplication::translate("AiraJson", "Off");
// Stato della pompa per zona (state.zoneN_mode). // Stato della pompa per zona (state.zoneN_mode).
if (s == QLatin1String("PUMP_MODE_STATE_HEATING")) if (s == QLatin1String("PUMP_MODE_STATE_HEATING"))
return QStringLiteral("Riscaldamento"); return QCoreApplication::translate("AiraJson", "Heating");
if (s == QLatin1String("PUMP_MODE_STATE_COOLING")) if (s == QLatin1String("PUMP_MODE_STATE_COOLING"))
return QStringLiteral("Raffrescamento"); return QCoreApplication::translate("AiraJson", "Cooling");
if (s == QLatin1String("PUMP_MODE_STATE_HOT_WATER")) if (s == QLatin1String("PUMP_MODE_STATE_HOT_WATER"))
return QStringLiteral("Acqua calda"); return QCoreApplication::translate("AiraJson", "Hot water");
if (s == QLatin1String("PUMP_MODE_STATE_IDLE")) if (s == QLatin1String("PUMP_MODE_STATE_IDLE"))
return QStringLiteral("In attesa"); return QCoreApplication::translate("AiraJson", "Idle");
if (s == QLatin1String("PUMP_MODE_STATE_STANDBY")) if (s == QLatin1String("PUMP_MODE_STATE_STANDBY"))
return QStringLiteral("Stand-by"); return QCoreApplication::translate("AiraJson", "Standby");
if (s == QLatin1String("PUMP_MODE_STATE_OFF")) if (s == QLatin1String("PUMP_MODE_STATE_OFF"))
return QStringLiteral("Spento"); return QCoreApplication::translate("AiraJson", "Off");
// Stato operativo della pompa (state.pump_active_state). // Stato operativo della pompa (state.pump_active_state).
if (s == QLatin1String("PUMP_ACTIVE_STATE_IDLE")) if (s == QLatin1String("PUMP_ACTIVE_STATE_IDLE"))
return QStringLiteral("Ferma"); return QCoreApplication::translate("AiraJson", "Stopped");
if (s == QLatin1String("PUMP_ACTIVE_STATE_RUNNING")) if (s == QLatin1String("PUMP_ACTIVE_STATE_RUNNING"))
return QStringLiteral("In funzione"); return QCoreApplication::translate("AiraJson", "Running");
if (s.isEmpty()) if (s.isEmpty())
return QStringLiteral("—"); return QStringLiteral("—");
// Sconosciuto: togliamo il prefisso e mostriamo il resto, minuscolo. // Sconosciuto: togliamo il prefisso e mostriamo il resto, minuscolo.
@@ -97,7 +99,10 @@ QString formatValue(const QVariant &value)
bool isNumber = false; bool isNumber = false;
const double d = value.toDouble(&isNumber); const double d = value.toDouble(&isNumber);
if (isNumber) if (isNumber)
return QString::number(d, 'f', 1); // QLocale() = locale predefinito, che translator.cpp allinea alla lingua
// dell'app: separatore decimale col punto in inglese, con la virgola in
// italiano (QString::number darebbe sempre il punto).
return QLocale().toString(d, 'f', 1);
const QString s = value.toString(); const QString s = value.toString();
return s.isEmpty() ? QStringLiteral("—") : s; return s.isEmpty() ? QStringLiteral("—") : s;
@@ -122,8 +127,8 @@ QString errorMessage(const QByteArray &body, int httpStatus)
return msg; return msg;
} }
if (httpStatus == 0) if (httpStatus == 0)
return QStringLiteral("server non raggiungibile"); return QCoreApplication::translate("AiraJson", "server unreachable");
return QStringLiteral("errore HTTP %1").arg(httpStatus); return QCoreApplication::translate("AiraJson", "HTTP error %1").arg(httpStatus);
} }
} // namespace AiraJson } // namespace AiraJson
+12 -12
View File
@@ -57,7 +57,7 @@ QNetworkReply *ApiClient::send(const QByteArray &verb, const QString &path,
{ {
const QString base = AiraJson::normalizeBaseUrl(m_settings ? m_settings->baseUrl() : QString()); const QString base = AiraJson::normalizeBaseUrl(m_settings ? m_settings->baseUrl() : QString());
if (base.isEmpty()) { if (base.isEmpty()) {
emit errorOccurred(QStringLiteral("Indirizzo del bridge non configurato")); emit errorOccurred(tr("Bridge address not configured"));
return 0; return 0;
} }
@@ -79,7 +79,7 @@ QNetworkReply *ApiClient::send(const QByteArray &verb, const QString &path,
void ApiClient::refresh() void ApiClient::refresh()
{ {
if (!configured()) { if (!configured()) {
emit errorOccurred(QStringLiteral("Configura indirizzo e token del bridge")); emit errorOccurred(tr("Set the bridge address and token"));
return; return;
} }
@@ -99,7 +99,7 @@ void ApiClient::refresh()
if (reply->error() != QNetworkReply::NoError || status != 200) { if (reply->error() != QNetworkReply::NoError || status != 200) {
const QString msg = (reply->error() == QNetworkReply::OperationCanceledError) const QString msg = (reply->error() == QNetworkReply::OperationCanceledError)
? QStringLiteral("Server non raggiungibile (timeout)") ? tr("Server unreachable (timeout)")
: AiraJson::errorMessage(body, status); : AiraJson::errorMessage(body, status);
qWarning() << "[api] summary non riuscito:" << status << msg; qWarning() << "[api] summary non riuscito:" << status << msg;
emit errorOccurred(msg); emit errorOccurred(msg);
@@ -110,7 +110,7 @@ void ApiClient::refresh()
const QVariantMap summary = AiraJson::parseSummary(body, &ok); const QVariantMap summary = AiraJson::parseSummary(body, &ok);
if (!ok) { if (!ok) {
qWarning() << "[api] riassunto non interpretabile"; qWarning() << "[api] riassunto non interpretabile";
emit errorOccurred(QStringLiteral("Risposta del bridge non interpretabile")); emit errorOccurred(tr("Unreadable response from the bridge"));
return; return;
} }
m_summary = summary; m_summary = summary;
@@ -122,7 +122,7 @@ void ApiClient::testConnection()
{ {
const QString base = AiraJson::normalizeBaseUrl(m_settings ? m_settings->baseUrl() : QString()); const QString base = AiraJson::normalizeBaseUrl(m_settings ? m_settings->baseUrl() : QString());
if (base.isEmpty()) { if (base.isEmpty()) {
emit connectionTested(false, QStringLiteral("Inserisci l'indirizzo del bridge")); emit connectionTested(false, tr("Enter the bridge address"));
return; return;
} }
@@ -138,12 +138,12 @@ void ApiClient::testConnection()
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (reply->error() != QNetworkReply::NoError || status != 200) { if (reply->error() != QNetworkReply::NoError || status != 200) {
const QString msg = (reply->error() == QNetworkReply::OperationCanceledError) const QString msg = (reply->error() == QNetworkReply::OperationCanceledError)
? QStringLiteral("Timeout: nessuna risposta") ? tr("Timeout: no response")
: QStringLiteral("Irraggiungibile (HTTP %1)").arg(status); : tr("Unreachable (HTTP %1)").arg(status);
emit connectionTested(false, msg); emit connectionTested(false, msg);
return; return;
} }
emit connectionTested(true, QStringLiteral("Bridge raggiungibile")); emit connectionTested(true, tr("Bridge reachable"));
}); });
} }
@@ -160,7 +160,7 @@ QString ApiClient::formatValue(const QVariant &value) const
void ApiClient::sendCommand(const QString &id, const QVariant &value) void ApiClient::sendCommand(const QString &id, const QVariant &value)
{ {
if (!configured()) { if (!configured()) {
emit commandFinished(false, QStringLiteral("Configura indirizzo e token del bridge")); emit commandFinished(false, tr("Set the bridge address and token"));
return; return;
} }
@@ -169,7 +169,7 @@ void ApiClient::sendCommand(const QString &id, const QVariant &value)
QNetworkReply *reply = send("POST", QStringLiteral("/command"), payload, 30); QNetworkReply *reply = send("POST", QStringLiteral("/command"), payload, 30);
if (!reply) { if (!reply) {
emit commandFinished(false, QStringLiteral("Bridge non configurato")); emit commandFinished(false, tr("Bridge not configured"));
return; return;
} }
@@ -183,14 +183,14 @@ void ApiClient::sendCommand(const QString &id, const QVariant &value)
if (reply->error() != QNetworkReply::NoError || status != 200) { if (reply->error() != QNetworkReply::NoError || status != 200) {
const QString msg = (reply->error() == QNetworkReply::OperationCanceledError) const QString msg = (reply->error() == QNetworkReply::OperationCanceledError)
? QStringLiteral("Timeout: comando non inviato") ? tr("Timeout: command not sent")
: AiraJson::errorMessage(body, status); : AiraJson::errorMessage(body, status);
qWarning() << "[api] comando" << id << "fallito:" << status << msg; qWarning() << "[api] comando" << id << "fallito:" << status << msg;
emit commandFinished(false, msg); emit commandFinished(false, msg);
return; return;
} }
emit commandFinished(true, QStringLiteral("Comando inviato")); emit commandFinished(true, tr("Command sent"));
refresh(); // i valori letti cambiano: rileggo refresh(); // i valori letti cambiano: rileggo
}); });
} }
+24 -2
View File
@@ -13,6 +13,7 @@
#include "apiclient.h" #include "apiclient.h"
#include "settings.h" #include "settings.h"
#include "translator.h"
Q_DECL_EXPORT int main(int argc, char *argv[]) Q_DECL_EXPORT int main(int argc, char *argv[])
{ {
@@ -20,19 +21,40 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
app->setOrganizationName(QStringLiteral("harbour")); app->setOrganizationName(QStringLiteral("harbour"));
app->setApplicationName(QStringLiteral("aira")); app->setApplicationName(QStringLiteral("aira"));
app->setApplicationVersion(QStringLiteral("0.3.0")); app->setApplicationVersion(QStringLiteral("0.4.0"));
Settings settings; Settings settings;
ApiClient api(&settings); ApiClient api(&settings);
// Lingua dell'interfaccia. Va risolta PRIMA di createView: le qsTr() del QML
// si valutano alla creazione dei componenti, quindi un traduttore installato
// dopo non avrebbe effetto sulla prima schermata.
Translator translator(&settings);
translator.applySaved();
QQuickView *view = SailfishApp::createView(); QQuickView *view = SailfishApp::createView();
// Prima riga del log: dice subito se il device sta girando la build nuova // Prima riga del log: dice subito se il device sta girando la build nuova
// o una stale (trappola nota del build engine). // o una stale (trappola nota del build engine).
qDebug() << "harbour-aira v0.3.0 build" << __DATE__ << __TIME__; qDebug() << "harbour-aira v0.4.0 build" << __DATE__ << __TIME__;
view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings); view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings);
view->rootContext()->setContextProperty(QStringLiteral("api"), &api); view->rootContext()->setContextProperty(QStringLiteral("api"), &api);
view->rootContext()->setContextProperty(QStringLiteral("translator"), &translator);
// Qt 5.6 non ha QQmlEngine::retranslate() (arriva con Qt 5.10): installare un
// traduttore nuovo NON ri-traduce le stringhe gia' mostrate, quindi la lingua
// scelta nelle impostazioni si applica ricaricando la radice QML. Conseguenza
// voluta e attesa: si torna alla home, nella lingua nuova.
//
// setSource() con la STESSA url ricrea davvero il componente: in Qt 5.6
// QQuickViewPrivate::execute() cancella root e component e ne costruisce uno
// nuovo (documentato: "the QML component being instantiated, even if the URL
// is unchanged"), quindi ogni qsTr() viene rivalutato. Non serve
// clearComponentCache(): i valori delle stringhe non stanno nella cache.
QObject::connect(&translator, &Translator::languageChanged, view, [view]() {
view->setSource(SailfishApp::pathTo(QStringLiteral("qml/harbour-aira.qml")));
});
view->setSource(SailfishApp::pathTo(QStringLiteral("qml/harbour-aira.qml"))); view->setSource(SailfishApp::pathTo(QStringLiteral("qml/harbour-aira.qml")));
view->show(); view->show();
+15
View File
@@ -52,3 +52,18 @@ void Settings::setToken(const QString &value)
m_settings.sync(); m_settings.sync();
emit tokenChanged(); emit tokenChanged();
} }
QString Settings::language() const
{
// Vuoto = non deciso dall'utente: si usa la lingua del sistema.
return m_settings.value(QStringLiteral("language"), QString()).toString();
}
void Settings::setLanguage(const QString &value)
{
if (value == language())
return;
m_settings.setValue(QStringLiteral("language"), value);
m_settings.sync();
emit languageChanged();
}
+8 -1
View File
@@ -10,13 +10,16 @@
// sparivano quando l'app veniva avviata dal launcher. // sparivano quando l'app veniva avviata dal launcher.
// //
// baseUrl indirizzo del bridge, es. "192.168.2.50:8790" (schema opzionale) // baseUrl indirizzo del bridge, es. "192.168.2.50:8790" (schema opzionale)
// token token del bridge (Authorization: Bearer ...) // token token del bridge (Authorization: Bearer ***)
// language lingua dell'interfaccia: "" = lingua del sistema (ripiego
// sull'inglese), "it" = italiano.
class Settings : public QObject class Settings : public QObject
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QString baseUrl READ baseUrl WRITE setBaseUrl NOTIFY baseUrlChanged) Q_PROPERTY(QString baseUrl READ baseUrl WRITE setBaseUrl NOTIFY baseUrlChanged)
Q_PROPERTY(QString token READ token WRITE setToken NOTIFY tokenChanged) Q_PROPERTY(QString token READ token WRITE setToken NOTIFY tokenChanged)
Q_PROPERTY(QString language READ language WRITE setLanguage NOTIFY languageChanged)
public: public:
explicit Settings(QObject *parent = 0); explicit Settings(QObject *parent = 0);
@@ -27,9 +30,13 @@ public:
QString token() const; QString token() const;
void setToken(const QString &value); void setToken(const QString &value);
QString language() const;
void setLanguage(const QString &value);
signals: signals:
void baseUrlChanged(); void baseUrlChanged();
void tokenChanged(); void tokenChanged();
void languageChanged();
private: private:
static QString configPath(); static QString configPath();
+74
View File
@@ -0,0 +1,74 @@
#include "translator.h"
#include "settings.h"
#include <QCoreApplication>
#include <QDebug>
#include <QLocale>
namespace {
// qmake installa i cataloghi sotto il TARGET, non sotto org/app: e' li' che
// l'app deve cercarli (vedi CONFIG += sailfishapp_i18n nel .pro).
const char *kTranslationsDir = "/usr/share/harbour-aira/translations";
const char *kCatalogBase = "harbour-aira";
}
Translator::Translator(Settings *settings, QObject *parent)
: QObject(parent)
, m_settings(settings)
{
}
QString Translator::language() const
{
return m_settings ? m_settings->language() : QString();
}
void Translator::setLanguage(const QString &value)
{
const QString before = language();
if (m_settings)
m_settings->setLanguage(value); // persiste subito
if (value == before)
return; // nessun cambio: niente ricarica
apply(value);
emit languageChanged();
}
void Translator::applySaved()
{
// All'avvio non si ricarica nulla (la view non esiste ancora): nessun segnale.
apply(language());
}
void Translator::apply(const QString &value)
{
if (m_translator) {
QCoreApplication::removeTranslator(m_translator);
delete m_translator;
}
// Lingua vuota = lingua del telefono. Attenzione: QLocale() sarebbe il locale
// PREDEFINITO, che qui sotto cambiamo noi con setDefault(): per tornare alla
// lingua di sistema serve QLocale::system(), che non risente di setDefault.
const QLocale locale = value.isEmpty() ? QLocale::system() : QLocale(value);
QTranslator *translator = new QTranslator(this);
// Anche il locale predefinito segue la lingua scelta: da questo dipendono i
// formati dei numeri (virgola o punto) e le date.
QLocale::setDefault(locale);
if (translator->load(locale, QString::fromLatin1(kCatalogBase),
QStringLiteral("-"), QString::fromLatin1(kTranslationsDir))) {
QCoreApplication::installTranslator(translator);
m_translator = translator;
} else {
// Nessun catalogo (es. locale inglese, o .qm non installati): si resta
// sulle stringhe sorgente inglesi. Il traduttore non va installato.
delete translator;
}
qDebug() << "Translator: richiesta ["
<< (value.isEmpty() ? QStringLiteral("sistema") : value)
<< "] locale" << locale.name()
<< "catalogo caricato:" << !m_translator.isNull();
}
+48
View File
@@ -0,0 +1,48 @@
#ifndef TRANSLATOR_H
#define TRANSLATOR_H
#include <QObject>
#include <QPointer>
#include <QTranslator>
class Settings;
// Lingua dell'interfaccia.
//
// "" (vuoto) -> lingua del sistema (QLocale::system(), non QLocale(), che e' il
// locale predefinito e viene cambiato da setDefault), con ripiego
// sull'inglese: l'inglese e' la lingua sorgente delle stringhe,
// quindi se per il locale del telefono non esiste un catalogo Qt
// lascia il testo originale inglese.
// "it" -> italiano, dal catalogo installato in
// /usr/share/harbour-aira/translations (i .qm li genera qmake in
// build con CONFIG += sailfishapp_i18n).
//
// Qt 5.6 (Sailfish) non ha QQmlEngine::retranslate(): esiste da Qt 5.10, e prima
// il QML non si ri-traduce da solo quando cambia il traduttore. Per questo
// main.cpp collega languageChanged() alla ricarica della radice QML: l'effetto
// visibile e' che l'app torna alla home, ma nella lingua nuova.
class Translator : public QObject
{
Q_OBJECT
Q_PROPERTY(QString language READ language WRITE setLanguage NOTIFY languageChanged)
public:
explicit Translator(Settings *settings, QObject *parent = 0);
QString language() const;
void setLanguage(const QString &value); // salva nelle impostazioni e applica
void applySaved(); // all'avvio, PRIMA di creare la view
signals:
void languageChanged();
private:
void apply(const QString &value);
Settings *m_settings;
QPointer<QTranslator> m_translator;
};
#endif // TRANSLATOR_H
+66 -15
View File
@@ -8,7 +8,9 @@
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QDebug> #include <QDebug>
#include <QLocale>
#include <QStringList> #include <QStringList>
#include <QTranslator>
#include "../src/airajson.h" #include "../src/airajson.h"
@@ -59,25 +61,46 @@ int main(int argc, char *argv[])
"ok=true senza summary -> vuoto"); "ok=true senza summary -> vuoto");
// ---- modeLabel -------------------------------------------------------- // // ---- modeLabel -------------------------------------------------------- //
check(AiraJson::modeLabel("OPERATING_STATUS_AUTO") == "Automatico", "AUTO -> Automatico"); // Attese in inglese: e' la lingua SORGENTE delle stringhe (l'italiano arriva
check(AiraJson::modeLabel("OPERATING_STATUS_HOT_WATER") == "Acqua calda", "acqua calda"); // dal catalogo caricato piu' avanti).
check(AiraJson::modeLabel("OPERATING_STATUS_AUTO") == "Automatic", "AUTO -> Automatic");
check(AiraJson::modeLabel("OPERATING_STATUS_HOT_WATER") == "Hot water", "hot water");
check(AiraJson::modeLabel("") == "—", "stato vuoto -> trattino"); check(AiraJson::modeLabel("") == "—", "stato vuoto -> trattino");
check(AiraJson::modeLabel("PUMP_MODE_STATE_HEATING") == "Riscaldamento", check(AiraJson::modeLabel("PUMP_MODE_STATE_HEATING") == "Heating",
"stato zona: riscaldamento"); "zone state: heating");
check(AiraJson::modeLabel("PUMP_MODE_STATE_IDLE") == "In attesa", check(AiraJson::modeLabel("PUMP_MODE_STATE_IDLE") == "Idle",
"stato zona: in attesa"); "zone state: idle");
check(AiraJson::modeLabel("PUMP_MODE_STATE_IGNOTO") == "ignoto", check(AiraJson::modeLabel("PUMP_MODE_STATE_IGNOTO") == "ignoto",
"stato zona sconosciuto -> senza prefisso"); "unknown zone state -> prefix stripped");
check(AiraJson::modeLabel("PUMP_ACTIVE_STATE_IDLE") == "Ferma", check(AiraJson::modeLabel("PUMP_ACTIVE_STATE_IDLE") == "Stopped",
"pompa ferma"); "pump stopped");
check(AiraJson::modeLabel("PUMP_ACTIVE_STATE_RUNNING") == "In funzione", check(AiraJson::modeLabel("PUMP_ACTIVE_STATE_RUNNING") == "Running",
"pompa in funzione"); "pump running");
check(AiraJson::modeLabel("OPERATING_STATUS_DEFROST") == "defrost", check(AiraJson::modeLabel("OPERATING_STATUS_DEFROST") == "defrost",
"stato sconosciuto: prefisso tolto, minuscolo"); "unknown state: prefix stripped, lowercase");
// ---- formatValue ------------------------------------------------------ // // ---- formatValue ------------------------------------------------------ //
check(AiraJson::formatValue(QVariant(51.0)) == "51.0", "numero a un decimale"); // Il separatore decimale segue il locale predefinito (che translator.cpp
// allinea alla lingua scelta): qui si fissa per rendere l'esito deterministico
// e si verifica che cambi davvero con la lingua.
QLocale::setDefault(QLocale::c());
check(AiraJson::formatValue(QVariant(51.0)) == "51.0", "numero a un decimale (locale C)");
check(AiraJson::formatValue(QVariant(18.14)) == "18.1", "arrotondamento a un decimale"); check(AiraJson::formatValue(QVariant(18.14)) == "18.1", "arrotondamento a un decimale");
QLocale::setDefault(QLocale(QLocale::Italian, QLocale::Italy));
check(AiraJson::formatValue(QVariant(23.0)) == "23,0", "in italiano separatore virgola");
check(AiraJson::formatValue(QVariant(18.14)) == "18,1", "in italiano arrotonda con la virgola");
QLocale::setDefault(QLocale(QLocale::English, QLocale::UnitedKingdom));
check(AiraJson::formatValue(QVariant(23.0)) == "23.0", "in inglese separatore punto");
QLocale::setDefault(QLocale::c());
// Invariante su cui poggia Translator::apply(""): QLocale::system() non risente
// di setDefault(), quindi "lingua vuota" torna sempre alla lingua del telefono
// anche dopo che l'utente ha scelto l'italiano. (QLocale() invece cambierebbe.)
const QLocale systemBefore = QLocale::system();
QLocale::setDefault(QLocale(QLocale::Italian, QLocale::Italy));
check(QLocale::system() == systemBefore, "setDefault non tocca QLocale::system()");
check(QLocale() == QLocale(QLocale::Italian, QLocale::Italy), "QLocale() segue setDefault");
QLocale::setDefault(QLocale::c());
check(AiraJson::formatValue(QVariant()) == "—", "valore assente -> trattino"); check(AiraJson::formatValue(QVariant()) == "—", "valore assente -> trattino");
check(AiraJson::formatValue(QVariant(QString())) == "—", "stringa vuota -> trattino"); check(AiraJson::formatValue(QVariant(QString())) == "—", "stringa vuota -> trattino");
@@ -94,8 +117,36 @@ int main(int argc, char *argv[])
// ---- errorMessage ----------------------------------------------------- // // ---- errorMessage ----------------------------------------------------- //
check(AiraJson::errorMessage(R"({"ok":false,"error":"token mancante o errato"})", 401) check(AiraJson::errorMessage(R"({"ok":false,"error":"token mancante o errato"})", 401)
== "token mancante o errato", "messaggio d'errore dal corpo"); == "token mancante o errato", "messaggio d'errore dal corpo");
check(AiraJson::errorMessage("", 502) == "errore HTTP 502", "fallback sullo stato HTTP"); check(AiraJson::errorMessage("", 502) == "HTTP error 502", "http status fallback");
check(AiraJson::errorMessage("", 0) == "server non raggiungibile", "stato 0 = irraggiungibile"); check(AiraJson::errorMessage("", 0) == "server unreachable", "status 0 = unreachable");
// ---- catalogo italiano: il .qm caricato davvero ------------------------ //
// Non basta che il .ts esista: qui si installa il .qm come fa main.cpp e si
// controlla che le stringhe sorgente inglesi diventino italiane. Il percorso
// si puo' passare come argv[1]; il default e' quello relativo alla cartella
// di build del test (tests/build).
{
const QString qm = (argc > 1) ? QString::fromLocal8Bit(argv[1])
: QStringLiteral("../../translations/harbour-aira-it.qm");
QTranslator translator;
if (translator.load(qm)) {
QCoreApplication::installTranslator(&translator);
check(AiraJson::modeLabel("PUMP_MODE_STATE_HEATING") == "Riscaldamento",
"catalogo it: Heating -> Riscaldamento");
check(AiraJson::modeLabel("PUMP_ACTIVE_STATE_IDLE") == "Ferma",
"catalogo it: Stopped -> Ferma");
check(AiraJson::modeLabel("PUMP_MODE_STATE_IDLE") == "In attesa",
"catalogo it: Idle -> In attesa");
check(AiraJson::errorMessage("", 0) == "server non raggiungibile",
"catalogo it: messaggio d'errore tradotto");
QCoreApplication::removeTranslator(&translator);
check(AiraJson::modeLabel("PUMP_MODE_STATE_HEATING") == "Heating",
"catalogo rimosso: si torna all'inglese (ripiego)");
} else {
qWarning() << "SALTO il controllo del catalogo: .qm non trovato in" << qm
<< "-> lancialo con lrelease, oppure passa il percorso come argomento";
}
}
qInfo() << "----"; qInfo() << "----";
qInfo() << checks - failures << "/" << checks << "verifiche superate"; qInfo() << checks - failures << "/" << checks << "verifiche superate";
+528
View File
@@ -0,0 +1,528 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="it_IT">
<context>
<name>AiraJson</name>
<message>
<location filename="../src/airajson.cpp" line="54"/>
<source>Automatic</source>
<translation>Automatico</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="56"/>
<location filename="../src/airajson.cpp" line="67"/>
<source>Heating</source>
<translation>Riscaldamento</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="58"/>
<location filename="../src/airajson.cpp" line="69"/>
<source>Cooling</source>
<translation>Raffrescamento</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="60"/>
<location filename="../src/airajson.cpp" line="71"/>
<source>Hot water</source>
<translation>Acqua calda</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="62"/>
<location filename="../src/airajson.cpp" line="75"/>
<source>Standby</source>
<translation>Stand-by</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="64"/>
<location filename="../src/airajson.cpp" line="77"/>
<source>Off</source>
<translation>Spento</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="73"/>
<source>Idle</source>
<translation>In attesa</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="80"/>
<source>Stopped</source>
<translation>Ferma</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="82"/>
<source>Running</source>
<translation>In funzione</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="130"/>
<source>server unreachable</source>
<translation>server non raggiungibile</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="131"/>
<source>HTTP error %1</source>
<translation>errore HTTP %1</translation>
</message>
</context>
<context>
<name>ApiClient</name>
<message>
<location filename="../src/apiclient.cpp" line="60"/>
<source>Bridge address not configured</source>
<translation>Indirizzo del bridge non configurato</translation>
</message>
<message>
<location filename="../src/apiclient.cpp" line="82"/>
<location filename="../src/apiclient.cpp" line="163"/>
<source>Set the bridge address and token</source>
<translation>Configura indirizzo e token del bridge</translation>
</message>
<message>
<location filename="../src/apiclient.cpp" line="102"/>
<source>Server unreachable (timeout)</source>
<translation>Server non raggiungibile (timeout)</translation>
</message>
<message>
<location filename="../src/apiclient.cpp" line="113"/>
<source>Unreadable response from the bridge</source>
<translation>Risposta del bridge non interpretabile</translation>
</message>
<message>
<location filename="../src/apiclient.cpp" line="125"/>
<source>Enter the bridge address</source>
<translation>Inserisci l&apos;indirizzo del bridge</translation>
</message>
<message>
<location filename="../src/apiclient.cpp" line="141"/>
<source>Timeout: no response</source>
<translation>Timeout: nessuna risposta</translation>
</message>
<message>
<location filename="../src/apiclient.cpp" line="142"/>
<source>Unreachable (HTTP %1)</source>
<translation>Irraggiungibile (HTTP %1)</translation>
</message>
<message>
<location filename="../src/apiclient.cpp" line="146"/>
<source>Bridge reachable</source>
<translation>Bridge raggiungibile</translation>
</message>
<message>
<location filename="../src/apiclient.cpp" line="172"/>
<source>Bridge not configured</source>
<translation>Bridge non configurato</translation>
</message>
<message>
<location filename="../src/apiclient.cpp" line="186"/>
<source>Timeout: command not sent</source>
<translation>Timeout: comando non inviato</translation>
</message>
<message>
<location filename="../src/apiclient.cpp" line="193"/>
<source>Command sent</source>
<translation>Comando inviato</translation>
</message>
</context>
<context>
<name>CommandsPage</name>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="20"/>
<source>yes</source>
<translation>sì</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="20"/>
<source>no</source>
<translation>no</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="54"/>
<source>Other commands</source>
<translation>Altri comandi</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="67"/>
<source>Current state</source>
<translation>Stato attuale</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="69"/>
<location filename="../qml/pages/CommandsPage.qml" line="77"/>
<source>Away mode</source>
<translation>Assenza</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="70"/>
<source>Night mode</source>
<translation>Night mode</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="71"/>
<source>Forced heating</source>
<translation>Riscaldamento forzato</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="72"/>
<source>Hot water active</source>
<translation>Acqua calda attiva</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="73"/>
<source>Pump</source>
<translation>Pompa</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="74"/>
<source>Allowed modes</source>
<translation>Modalità consentite</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="80"/>
<source>Turn away mode on</source>
<translation>Attiva assenza</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="84"/>
<source>Turn away mode off</source>
<translation>Disattiva assenza</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="89"/>
<source>Noise</source>
<translation>Rumore</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="92"/>
<source>Night mode for one hour</source>
<translation>Night mode per un&apos;ora</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="97"/>
<source>Heating</source>
<translation>Riscaldamento</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="100"/>
<source>Force heating</source>
<translation>Forza riscaldamento</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="101"/>
<source>Starting forced heating</source>
<translation>Avvio del riscaldamento forzato</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="105"/>
<source>Stop forced heating</source>
<translation>Ferma riscaldamento forzato</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="109"/>
<source>Enable heating function</source>
<translation>Abilita funzione riscaldamento</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="113"/>
<source>Disable heating function</source>
<translation>Disabilita funzione riscaldamento</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="114"/>
<source>Heating will be disabled</source>
<translation>Il riscaldamento verrà disabilitato</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="119"/>
<source>Domestic hot water</source>
<translation>Acqua calda sanitaria</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="122"/>
<source>Legionella cycle</source>
<translation>Ciclo antilegionella</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="123"/>
<source>Starting the legionella cycle: it takes a while</source>
<translation>Avvio del ciclo antilegionella: richiede tempo</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="127"/>
<source>Enable DHW heating</source>
<translation>Abilita riscaldamento ACS</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="131"/>
<source>Disable DHW heating</source>
<translation>Disabilita riscaldamento ACS</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="132"/>
<source>Domestic hot water will stop being heated</source>
<translation>L&apos;acqua calda smetterà di essere riscaldata</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="137"/>
<source>Diagnostics</source>
<translation>Diagnostica</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="140"/>
<source>Plant check</source>
<translation>Verifica impianto</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="154"/>
<source>Commands go through the bridge and may take a few seconds. The status refreshes by itself afterwards.</source>
<translation>I comandi passano dal bridge e possono richiedere qualche secondo. Dopo l&apos;invio lo stato si aggiorna da solo.</translation>
</message>
</context>
<context>
<name>CoverPage</name>
<message>
<location filename="../qml/cover/CoverPage.qml" line="40"/>
<source>house · target %1 °C</source>
<translation>casa · obiettivo %1 °C</translation>
</message>
<message>
<location filename="../qml/cover/CoverPage.qml" line="49"/>
<source>DHW %1 °C · target %2 °C</source>
<translation>ACS %1 °C · obiettivo %2 °C</translation>
</message>
<message>
<location filename="../qml/cover/CoverPage.qml" line="60"/>
<source>outdoor %1 °C</source>
<translation>est. %1 °C</translation>
</message>
</context>
<context>
<name>MainPage</name>
<message>
<location filename="../qml/pages/MainPage.qml" line="50"/>
<source>yes</source>
<translation>sì</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="50"/>
<source>no</source>
<translation>no</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="105"/>
<source>Refresh</source>
<translation>Aggiorna</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="109"/>
<location filename="../qml/pages/MainPage.qml" line="150"/>
<source>Settings</source>
<translation>Impostazioni</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="113"/>
<location filename="../qml/pages/MainPage.qml" line="331"/>
<source>Other commands</source>
<translation>Altri comandi</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="141"/>
<source>Bridge not configured. Open the settings and enter the server address and the token.</source>
<translation>Bridge non configurato. Apri le impostazioni e inserisci l&apos;indirizzo del server e il token.</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="158"/>
<source>House temperature</source>
<translation>Temperatura in casa</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="186"/>
<source>target %1 °C · %2</source>
<translation>obiettivo %1 °C · %2</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="195"/>
<source>New house target</source>
<translation>Nuovo obiettivo casa</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="216"/>
<source>Set house to %1 °C</source>
<translation>Imposta casa a %1 °C</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="218"/>
<source>House temperature to %1 °C</source>
<translation>Temperatura della casa a %1 °C</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="223"/>
<source>Domestic hot water</source>
<translation>Acqua calda sanitaria</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="247"/>
<source>DHW target</source>
<translation>Obiettivo acqua calda</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="254"/>
<source>New target</source>
<translation>Nuovo obiettivo</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="275"/>
<source>Set target %1 °C</source>
<translation>Imposta obiettivo %1 °C</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="277"/>
<source>DHW target to %1 °C</source>
<translation>Obiettivo ACS a %1 °C</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="290"/>
<source>Start DHW boost</source>
<translation>Avvia boost ACS</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="291"/>
<source>Starting DHW boost</source>
<translation>Avvio del boost ACS</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="304"/>
<source>Stop DHW boost</source>
<translation>Ferma boost ACS</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="305"/>
<source>Stopping DHW boost</source>
<translation>Arresto del boost ACS</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="310"/>
<source>Plant</source>
<translation>Impianto</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="312"/>
<source>Outdoor temperature</source>
<translation>Temperatura esterna</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="313"/>
<source>Indoor temperature (pump)</source>
<translation>Temperatura interna (pompa)</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="314"/>
<source>Mode</source>
<translation>Modalità</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="315"/>
<source>Away mode</source>
<translation>Assenza</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="316"/>
<source>Night mode</source>
<translation>Night mode</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="317"/>
<source>Forced heating</source>
<translation>Riscaldamento forzato</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="318"/>
<source>Inline heater</source>
<translation>Resistenze integrative</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="319"/>
<source>Zone</source>
<translation>Zone</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="320"/>
<source>Errors</source>
<translation>Errori</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="321"/>
<source>Last reading</source>
<translation>Aggiornato alle</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="346"/>
<source>Commands are sent to the bridge and from there to the plant: they may take a few seconds.</source>
<translation>I comandi vengono inviati al bridge e da lì all&apos;impianto: possono richiedere qualche secondo.</translation>
</message>
</context>
<context>
<name>SettingsPage</name>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="24"/>
<source>checking…</source>
<translation>verifico…</translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="50"/>
<source>Settings</source>
<translation>Impostazioni</translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="53"/>
<location filename="../qml/pages/SettingsPage.qml" line="61"/>
<source>Language</source>
<translation>Lingua</translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="64"/>
<location filename="../qml/pages/SettingsPage.qml" line="67"/>
<source>System</source>
<translation>Sistema</translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="85"/>
<source>Changing the language reloads the app.</source>
<translation>Cambiando la lingua l&apos;app si ricarica.</translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="92"/>
<source>Bridge</source>
<translation>Bridge</translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="98"/>
<source>Bridge address</source>
<translation>Indirizzo del bridge</translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="113"/>
<source>Bridge token</source>
<translation>Token del bridge</translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="125"/>
<source>Address and token live on the server: the token is in aira_bridge.json next to the bridge. Without a scheme, http:// is used.</source>
<translation>Indirizzo e token si leggono sul server: il token è in aira_bridge.json accanto al bridge. Senza schema viene usato http://.</translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="133"/>
<source>Save</source>
<translation>Salva</translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="136"/>
<source>settings saved</source>
<translation>impostazioni salvate</translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="143"/>
<source>Test connection</source>
<translation>Prova connessione</translation>
</message>
</context>
</TS>