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__/
*.pyc
# cataloghi compilati: si rigenerano in build da translations/*.ts
translations/*.qm
# temporanei
*~
*.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
(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)
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)
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
- **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`)
- **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
Silica una proprietà inesistente passa in silenzio)
- 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/apiclient.{h,cpp} QNetworkAccessManager + watchdog di timeout
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/pages/MainPage.qml dashboard e comandi acqua/riscaldamento
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)
docs/PIANO.md piano di progetto e scelte
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
+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
- Address and bridge token live in the app settings (QSettings), never in the
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**
screenshots — see *Status*):
| Home | Other commands | Cover |
|---|---|---|
| ![home](docs/anteprima/dashboard.png) | ![commands](docs/anteprima/comandi.png) | ![cover](docs/anteprima/copertina.png) |
| Home | Other commands | Settings | Cover |
|---|---|---|---|
| ![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
@@ -107,10 +112,15 @@ parsing, mode labels, command payloads, error messages.
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`) —
including that a zone command always carries every zone, and that absurd
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
Silica module an invalid Silica property passes silently
- 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
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:
- **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/apiclient.{h,cpp} QNetworkAccessManager + timeout watchdog
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/pages/MainPage.qml dashboard and water/heating commands
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
tests/core_test.cpp core checks (headless)
bridge/ local HTTP bridge -> see bridge/README.md
translations/ harbour-aira-it.ts (English is the source language)
docs/PIANO.md project plan and decisions (Italian)
docs/PROTOCOL.md bridge API
docs/anteprima/ layout previews
docs/anteprima/ layout previews (English; docs/anteprima/it/ Italian)
```
## 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) |
| 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) |
| 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 |
@@ -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
rivela il problema — va provato dalla LAN.
- **App 0.4.0 — localizzazione ita/eng** (richiesta dell'utente): l'interfaccia si
può mettere in italiano o inglese **dalle impostazioni**; di serie segue la lingua
del sistema, con **ripiego sull'inglese**. L'inglese è la lingua sorgente delle
stringhe (restano nel codice), l'italiano sta in `translations/harbour-aira-it.ts`
(**99 stringhe**, aggiornate con `lupdate` e compilate da `lrelease` in build).
Nuova classe `Translator` (`src/translator.{h,cpp}`): risolve la lingua **prima**
di `createView`, così le `qsTr()` del QML nascono già tradotte, e allinea
`QLocale::setDefault` alla lingua scelta — così i numeri seguono la lingua
(`23,0 °C` in italiano, `23.0 °C` in inglese): le temperature si formattano in
`ApiClient::formatValue`, non più con il `toFixed()` di JavaScript, che avrebbe
sempre messo il punto. *Vincolo di piattaforma*: **Qt 5.6 non ha
`QQmlEngine::retranslate()`** (arriva con Qt 5.10), quindi installare il
traduttore non ritraduce ciò che è già a schermo: al cambio lingua l'app
**ricarica la radice QML**. *Trappola di `lupdate`*: riconosce solo le funzioni di
traduzione note (`tr()`, `QCoreApplication::translate()`), **non una funzione
wrapper scritta da noi** — le etichette di `airajson.cpp` sono quindi chiamate
`QCoreApplication::translate("AiraJson", …)` per esteso, altrimenti sparirebbero
dal catalogo. Nucleo app: **39/39** (le attese sono cambiate in inglese e il test
carica il `.qm` vero come fa `main.cpp`, verificando entrambe le lingue, il
ripiego e i formati numerici). Anteprime rigenerate in **due lingue**
(`docs/anteprima/` inglese, `docs/anteprima/it/` italiano), impostazioni comprese
la nuova riga della lingua.
## 6. Rischi noti
- **Dipendenza dal server**: se il bridge è spento, l'app non vede nulla. Mitigazione: unità
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 += \
src/main.cpp \
src/settings.cpp \
src/translator.cpp \
src/apiclient.cpp \
src/airajson.cpp
HEADERS += \
src/settings.h \
src/translator.h \
src/apiclient.h \
src/airajson.h
@@ -31,9 +33,18 @@ OTHER_FILES += \
DISTFILES += $$OTHER_FILES
# Niente catalogo di traduzione in 0.1.0: le stringhe sorgente sono in italiano
# (app personale). Se un giorno serve lo store Harbour si aggiungono i .ts e la
# compilazione con lrelease dentro lo spec, come negli altri progetti.
# Traduzioni: l'inglese e' la lingua sorgente (nessun .qm: Qt lascia le stringhe
# originali quando per il locale non c'e' catalogo = ripiego automatico), mentre
# 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
# path relativi falliscono quando il tar sorgente e' incompleto).
+4 -4
View File
@@ -16,7 +16,7 @@ CoverBackground {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Theme.paddingLarge
text: qsTr("Aira")
text: "Aira"
color: Theme.primaryColor
font.pixelSize: Theme.fontSizeLarge
}
@@ -37,7 +37,7 @@ CoverBackground {
Label {
width: parent.width
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
font.pixelSize: Theme.fontSizeExtraSmall
truncationMode: TruncationMode.Fade
@@ -46,7 +46,7 @@ CoverBackground {
Label {
width: parent.width
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
font.pixelSize: Theme.fontSizeExtraSmall
truncationMode: TruncationMode.Fade
@@ -57,7 +57,7 @@ CoverBackground {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
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
font.pixelSize: Theme.fontSizeExtraSmall
}
+29 -29
View File
@@ -17,7 +17,7 @@ Page {
}
function flag(key) {
return page.raw(key) ? qsTr("sì") : qsTr("no")
return page.raw(key) ? qsTr("yes") : qsTr("no")
}
function stato(key) {
@@ -51,7 +51,7 @@ Page {
width: parent.width
spacing: Theme.paddingSmall
PageHeader { title: qsTr("Altri comandi") }
PageHeader { title: qsTr("Other commands") }
Label {
visible: page.notice.length > 0
@@ -64,80 +64,80 @@ Page {
}
// ---- 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("Riscaldamento forzato"); value: page.flag("force_heating") }
InfoRow { label: qsTr("Acqua calda attiva"); value: page.flag("hot_water_heating") }
InfoRow { label: qsTr("Pompa"); value: page.stato("pump_active_state") }
InfoRow { label: qsTr("Modalità consentite"); value: page.stato("configured_modes") }
InfoRow { label: qsTr("Forced heating"); value: page.flag("force_heating") }
InfoRow { label: qsTr("Hot water active"); value: page.flag("hot_water_heating") }
InfoRow { label: qsTr("Pump"); value: page.stato("pump_active_state") }
InfoRow { label: qsTr("Allowed modes"); value: page.stato("configured_modes") }
// ---- assenza --------------------------------------------------- //
SectionHeader { text: qsTr("Assenza") }
SectionHeader { text: qsTr("Away mode") }
ActionButton {
text: qsTr("Attiva assenza")
text: qsTr("Turn away mode on")
onTriggered: api.sendCommand("away_mode_on")
}
ActionButton {
text: qsTr("Disattiva assenza")
text: qsTr("Turn away mode off")
onTriggered: api.sendCommand("away_mode_off")
}
// ---- rumore ---------------------------------------------------- //
SectionHeader { text: qsTr("Rumore") }
SectionHeader { text: qsTr("Noise") }
ActionButton {
text: qsTr("Night mode per un'ora")
text: qsTr("Night mode for one hour")
onTriggered: api.sendCommand("night_mode_1h")
}
// ---- riscaldamento --------------------------------------------- //
SectionHeader { text: qsTr("Riscaldamento") }
SectionHeader { text: qsTr("Heating") }
ActionButton {
text: qsTr("Forza riscaldamento")
confirmText: qsTr("Avvio del riscaldamento forzato")
text: qsTr("Force heating")
confirmText: qsTr("Starting forced heating")
onTriggered: api.sendCommand("force_heating_on")
}
ActionButton {
text: qsTr("Ferma riscaldamento forzato")
text: qsTr("Stop forced heating")
onTriggered: api.sendCommand("force_heating_off")
}
ActionButton {
text: qsTr("Abilita funzione riscaldamento")
text: qsTr("Enable heating function")
onTriggered: api.sendCommand("heating_on")
}
ActionButton {
text: qsTr("Disabilita funzione riscaldamento")
confirmText: qsTr("Il riscaldamento verrà disabilitato")
text: qsTr("Disable heating function")
confirmText: qsTr("Heating will be disabled")
onTriggered: api.sendCommand("heating_off")
}
// ---- acqua calda ----------------------------------------------- //
SectionHeader { text: qsTr("Acqua calda sanitaria") }
SectionHeader { text: qsTr("Domestic hot water") }
ActionButton {
text: qsTr("Ciclo antilegionella")
confirmText: qsTr("Avvio del ciclo antilegionella: richiede tempo")
text: qsTr("Legionella cycle")
confirmText: qsTr("Starting the legionella cycle: it takes a while")
onTriggered: api.sendCommand("legionella_cycle")
}
ActionButton {
text: qsTr("Abilita riscaldamento ACS")
text: qsTr("Enable DHW heating")
onTriggered: api.sendCommand("hot_water_heating_on")
}
ActionButton {
text: qsTr("Disabilita riscaldamento ACS")
confirmText: qsTr("L'acqua calda smetterà di essere riscaldata")
text: qsTr("Disable DHW heating")
confirmText: qsTr("Domestic hot water will stop being heated")
onTriggered: api.sendCommand("hot_water_heating_off")
}
// ---- diagnostica ----------------------------------------------- //
SectionHeader { text: qsTr("Diagnostica") }
SectionHeader { text: qsTr("Diagnostics") }
ActionButton {
text: qsTr("Verifica impianto")
text: qsTr("Plant check")
onTriggered: api.sendCommand("ping")
}
@@ -151,7 +151,7 @@ Page {
Label {
x: 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
font.pixelSize: Theme.fontSizeExtraSmall
wrapMode: Text.Wrap
+38 -34
View File
@@ -38,12 +38,16 @@ Page {
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) {
return api.formatValue(page.raw(key))
}
function flag(key) {
return page.raw(key) ? qsTr("sì") : qsTr("no")
return page.raw(key) ? qsTr("yes") : qsTr("no")
}
function count(key) {
@@ -98,15 +102,15 @@ Page {
// fanno deref su null (TypeError + crash).
PullDownMenu {
MenuItem {
text: qsTr("Aggiorna")
text: qsTr("Refresh")
onClicked: api.refresh()
}
MenuItem {
text: qsTr("Impostazioni")
text: qsTr("Settings")
onClicked: page.openSettings()
}
MenuItem {
text: qsTr("Altri comandi")
text: qsTr("Other commands")
onClicked: page.openCommands()
}
}
@@ -116,7 +120,7 @@ Page {
width: parent.width
spacing: Theme.paddingSmall
PageHeader { title: qsTr("Aira") }
PageHeader { title: "Aira" }
// ---- avvisi ---------------------------------------------------- //
Label {
@@ -134,7 +138,7 @@ Page {
visible: !api.configured
x: 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
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.Wrap
@@ -143,7 +147,7 @@ Page {
Button {
visible: !api.configured
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Impostazioni")
text: qsTr("Settings")
onClicked: page.openSettings()
}
@@ -151,7 +155,7 @@ Page {
Label {
visible: api.configured
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Temperatura in casa")
text: qsTr("House temperature")
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeSmall
}
@@ -179,7 +183,7 @@ Page {
Label {
visible: api.configured
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
font.pixelSize: Theme.fontSizeExtraSmall
}
@@ -188,12 +192,12 @@ Page {
id: casaSlider
visible: api.configured
width: parent.width
label: qsTr("Nuovo obiettivo casa")
label: qsTr("New house target")
minimumValue: 5
maximumValue: 30
stepSize: 0.5
value: page.targetCasa
valueText: value.toFixed(1) + " °C"
valueText: page.fmt(value) + " °C"
onValueChanged: {
page.targetCasaTouched = true
page.targetCasa = Math.round(value * 2) / 2
@@ -209,14 +213,14 @@ Page {
leftMargin: 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,
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) })
}
// ---- acqua calda sanitaria ------------------------------------- //
SectionHeader { visible: api.configured; text: qsTr("Acqua calda sanitaria") }
SectionHeader { visible: api.configured; text: qsTr("Domestic hot water") }
Label {
visible: api.configured
@@ -240,19 +244,19 @@ Page {
InfoRow {
visible: api.configured
label: qsTr("Obiettivo acqua calda")
label: qsTr("DHW target")
value: page.temp("dhw_target") + " °C"
}
Slider {
visible: api.configured
width: parent.width
label: qsTr("Nuovo obiettivo")
label: qsTr("New target")
minimumValue: 45
maximumValue: 60
stepSize: 1
value: page.targetAcs
valueText: value + " °C"
valueText: page.fmt(value) + " °C"
onValueChanged: {
page.targetAcsTouched = true
page.targetAcs = Math.round(value)
@@ -268,9 +272,9 @@ Page {
leftMargin: 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,
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) })
}
@@ -283,8 +287,8 @@ Page {
leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin
}
text: qsTr("Avvia boost ACS")
onClicked: remorse.execute(boostOnButton, qsTr("Avvio del boost ACS"),
text: qsTr("Start DHW boost")
onClicked: remorse.execute(boostOnButton, qsTr("Starting DHW boost"),
function() { api.sendCommand("hot_water_boost_on") })
}
@@ -297,24 +301,24 @@ Page {
leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin
}
text: qsTr("Ferma boost ACS")
onClicked: remorse.execute(boostOffButton, qsTr("Arresto del boost ACS"),
text: qsTr("Stop DHW boost")
onClicked: remorse.execute(boostOffButton, qsTr("Stopping DHW boost"),
function() { api.sendCommand("hot_water_boost_off") })
}
// ---- 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("Temperatura interna (pompa)"); 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("Assenza"); value: page.flag("away_mode") }
InfoRow { visible: api.configured; label: qsTr("Outdoor temperature"); value: page.temp("outdoor_temperature") + " °C" }
InfoRow { visible: api.configured; label: qsTr("Indoor temperature (pump)"); value: page.temp("indoor_temperature") + " °C" }
InfoRow { visible: api.configured; label: qsTr("Mode"); value: api.modeLabel(page.raw("operating_status")) }
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("Riscaldamento forzato"); value: page.flag("force_heating") }
InfoRow { visible: api.configured; label: qsTr("Resistenze integrative"); value: page.flag("inline_heater") }
InfoRow { visible: api.configured; label: qsTr("Forced heating"); value: page.flag("force_heating") }
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("Errori"); 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("Errors"); value: page.count("errors_count") }
InfoRow { visible: api.configured; label: qsTr("Last reading"); value: page.shortTime(page.raw("pump_updated")) }
Button {
visible: api.configured
@@ -324,7 +328,7 @@ Page {
leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin
}
text: qsTr("Altri comandi")
text: qsTr("Other commands")
onClicked: page.openCommands()
}
@@ -339,7 +343,7 @@ Page {
visible: api.configured
x: 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
font.pixelSize: Theme.fontSizeExtraSmall
wrapMode: Text.Wrap
+50 -8
View File
@@ -21,7 +21,7 @@ Page {
function testAndReport() {
save()
page.testResult = qsTr("verifico…")
page.testResult = qsTr("checking…")
api.testConnection()
}
@@ -47,13 +47,55 @@ Page {
width: parent.width
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 {
id: urlField
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
label: qsTr("Indirizzo del bridge")
label: qsTr("Bridge address")
placeholderText: "192.168.2.50:8790"
text: page.urlText
onTextChanged: if (text !== page.urlText) page.urlText = text
@@ -68,7 +110,7 @@ Page {
id: tokenField
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
label: qsTr("Token del bridge")
label: qsTr("Bridge token")
showEchoModeToggle: true
text: page.tokenText
onTextChanged: if (text !== page.tokenText) page.tokenText = text
@@ -80,7 +122,7 @@ Page {
Label {
x: 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
font.pixelSize: Theme.fontSizeExtraSmall
wrapMode: Text.Wrap
@@ -88,17 +130,17 @@ Page {
Button {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Salva")
text: qsTr("Save")
onClicked: {
page.save()
page.testResult = qsTr("impostazioni salvate")
page.testResult = qsTr("settings saved")
page.testOk = true
}
}
Button {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Prova connessione")
text: qsTr("Test connection")
onClicked: page.testAndReport()
}
+16 -1
View File
@@ -1,6 +1,6 @@
Name: harbour-aira
Summary: Control your Aira heat pump through a local bridge
Version: 0.3.0
Version: 0.4.0
Release: 1
Group: Qt/Qt
License: MIT
@@ -15,6 +15,8 @@ BuildRequires: pkgconfig(Qt5Qml)
BuildRequires: pkgconfig(Qt5Quick)
BuildRequires: pkgconfig(Qt5Network)
BuildRequires: desktop-file-utils
# lupdate/lrelease: i .qm si compilano in build (CONFIG += sailfishapp_i18n).
BuildRequires: qt5-qttools-linguist
%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.
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);
no personal data is stored in the binary.
%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
- 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/172x172/apps/%{name}.png
%{_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
# --------------------------------------------------------------------------- #
# 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:
img = background()
d = ImageDraw.Draw(img)
status_bar(d)
page_header(d, "Aira")
page_header(d, T("app"))
y = 156
# ---- 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)
y += 40
big = "23,0 °C"
big = dec(23.0) + " °C"
d.text(((W - d.textlength(big, font=font(92, bold=True))) / 2, y),
big, font=font(92, bold=True), fill=PRIMARY)
y += 116
progress(d, y, (23.0 - 5) / (30 - 5))
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)
y += 46
y = slider(d, y, (19.0 - 5) / (30 - 5), "19,0 °C", "Nuovo obiettivo casa")
y = button(d, y, "Imposta casa a 19,0 °C", primary=True)
y = slider(d, y, (19.0 - 5) / (30 - 5), dec(19.0) + " °C", T("new_house_target"))
y = button(d, y, T("set_house").format(v=dec(19.0)), primary=True)
# ---- acqua calda sanitaria ------------------------------------------- #
y = section_header(d, y + 24, "Acqua calda sanitaria")
acs = "51,0 °C"
y = section_header(d, y + 24, T("dhw_section"))
acs = dec(51.0) + " °C"
d.text(((W - d.textlength(acs, font=font(56, bold=True))) / 2, y),
acs, font=font(56, bold=True), fill=PRIMARY)
y += 76
progress(d, y, (51.0 - 20) / (65 - 20))
y += 22
y = label_pair(d, y, "Obiettivo acqua calda", "55,0 °C")
y = slider(d, y + 10, (55.0 - 45) / (60 - 45), "55 °C")
y = button(d, y, "Imposta obiettivo ACS", primary=True)
y = button(d, y, "Avvia boost ACS")
y = button(d, y, "Ferma boost ACS")
y = label_pair(d, y, T("dhw_target"), dec(55.0) + " °C")
y = slider(d, y + 10, (55.0 - 45) / (60 - 45), "55 °C", T("target_label"))
y = button(d, y, T("set_acs"), primary=True)
y = button(d, y, T("start_boost"))
y = button(d, y, T("stop_boost"))
# ---- stato impianto (scorre sotto) ----------------------------------- #
y = section_header(d, y + 24, "Impianto")
y = label_pair(d, y, "Esterna", "18,0 °C")
y = label_pair(d, y, "Interna (pompa)", "22,5 °C")
y = section_header(d, y + 24, T("plant"))
y = label_pair(d, y, T("outdoor"), dec(18.0) + " °C")
y = label_pair(d, y, T("indoor_pump"), dec(22.5) + " °C")
d.text(((W - d.textlength("Trascina verso il basso per aggiornare",
font=font(22))) / 2, y + 8),
"Trascina verso il basso per aggiornare", font=font(22), fill=SECONDARY)
hint = T("pull_to_refresh")
d.text(((W - d.textlength(hint, font=font(22))) / 2, y + 8),
hint, font=font(22), fill=SECONDARY)
return img
@@ -163,11 +283,17 @@ def settings_screen() -> Image.Image:
img = background()
d = ImageDraw.Draw(img)
status_bar(d)
page_header(d, "Impostazioni")
page_header(d, T("settings"))
y = 172
for lab, val in (("Indirizzo del bridge", "192.168.3.21:8790"),
("Token del bridge", "••••••••••••••••••••••••••••")):
# ---- lingua ----------------------------------------------------------- #
y = section_header(d, 156, T("language"))
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)
y += 36
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)
y += 44
d.multiline_text((M, y), "Indirizzo e token restano nella configurazione\n"
"locale dell'app, in sola lettura per gli altri.",
font=font(22), fill=SECONDARY, spacing=10)
d.multiline_text((M, y), T("settings_note"), font=font(22), fill=SECONDARY, spacing=10)
y += 104
y = button(d, y, "Prova connessione", primary=True)
d.text((M, y), "Connessione riuscita (bridge 0.1.0)", font=font(24), fill=OK)
y = button(d, y, T("test_connection"), primary=True)
d.text((M, y), T("connection_ok"), font=font(24), fill=OK)
y += 58
y = button(d, y, "Salva")
y = button(d, y, T("save"))
return img
@@ -199,20 +323,22 @@ def cover_screen() -> Image.Image:
# copertina
card = [96, 430, W - 96, 430 + 470]
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)],
fill=BTN_EDGE, width=2)
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),
big, font=font(76, bold=True), fill=PRIMARY)
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)
vw = d.textlength(val, font=font(24))
d.text((card[2] - 34 - vw, y), val, font=font(24), fill=PRIMARY)
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
@@ -220,32 +346,42 @@ def commands_screen() -> Image.Image:
img = background()
d = ImageDraw.Draw(img)
status_bar(d)
page_header(d, "Altri comandi")
page_header(d, T("other_commands"))
y = section_header(d, 156, "Stato attuale")
for lab, val in (("Assenza", "no"), ("Night mode", "no"),
("Riscaldamento forzato", "no"), ("Acqua calda attiva", "sì"),
("Pompa", "Ferma"), ("Modalità consentite", "Riscaldamento")):
y = section_header(d, 156, T("current_state"))
for lab, val in ((T("away"), T("off")), (T("night_mode"), T("off")),
(T("forced_heating"), T("off")), (T("hot_water_active"), T("on")),
(T("pump"), T("stopped")), (T("allowed_modes"), T("heating"))):
y = label_pair(d, y, lab, val)
y = section_header(d, y + 24, "Assenza")
y = button(d, y, "Attiva assenza")
y = button(d, y, "Disattiva assenza")
y = section_header(d, y + 24, T("away"))
y = button(d, y, T("turn_away_on"))
y = button(d, y, T("turn_away_off"))
y = section_header(d, y + 24, "Rumore")
y = button(d, y, "Night mode per un'ora")
y = section_header(d, y + 24, T("noise"))
y = button(d, y, T("night_one_hour"))
y = section_header(d, y + 24, "Riscaldamento")
y = button(d, y, "Forza riscaldamento", primary=True)
d.text(((W - d.textlength("(chiede conferma prima di partire)", font=font(22))) / 2, y - 4),
"(chiede conferma prima di partire)", font=font(22), fill=SECONDARY)
y = section_header(d, y + 24, T("heating"))
y = button(d, y, T("force_heating"), primary=True)
hint = T("confirm_hint")
d.text(((W - d.textlength(hint, font=font(22))) / 2, y - 4), hint, font=font(22), fill=SECONDARY)
return img
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),
("impostazioni", settings_screen), ("copertina", cover_screen)):
path = OUT / f"{name}.png"
path = out / f"{name}.png"
fn().save(path)
print(f"scritto {path} ({path.stat().st_size} byte)")
+22 -17
View File
@@ -1,8 +1,10 @@
#include "airajson.h"
#include <QCoreApplication>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QLocale>
#include <QUrl>
namespace AiraJson {
@@ -49,35 +51,35 @@ QString modeLabel(const QString &operatingStatus)
{
const QString s = operatingStatus;
if (s == QLatin1String("OPERATING_STATUS_AUTO"))
return QStringLiteral("Automatico");
return QCoreApplication::translate("AiraJson", "Automatic");
if (s == QLatin1String("OPERATING_STATUS_HEATING"))
return QStringLiteral("Riscaldamento");
return QCoreApplication::translate("AiraJson", "Heating");
if (s == QLatin1String("OPERATING_STATUS_COOLING"))
return QStringLiteral("Raffrescamento");
return QCoreApplication::translate("AiraJson", "Cooling");
if (s == QLatin1String("OPERATING_STATUS_HOT_WATER"))
return QStringLiteral("Acqua calda");
return QCoreApplication::translate("AiraJson", "Hot water");
if (s == QLatin1String("OPERATING_STATUS_STANDBY"))
return QStringLiteral("Stand-by");
return QCoreApplication::translate("AiraJson", "Standby");
if (s == QLatin1String("OPERATING_STATUS_OFF"))
return QStringLiteral("Spento");
return QCoreApplication::translate("AiraJson", "Off");
// Stato della pompa per zona (state.zoneN_mode).
if (s == QLatin1String("PUMP_MODE_STATE_HEATING"))
return QStringLiteral("Riscaldamento");
return QCoreApplication::translate("AiraJson", "Heating");
if (s == QLatin1String("PUMP_MODE_STATE_COOLING"))
return QStringLiteral("Raffrescamento");
return QCoreApplication::translate("AiraJson", "Cooling");
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"))
return QStringLiteral("In attesa");
return QCoreApplication::translate("AiraJson", "Idle");
if (s == QLatin1String("PUMP_MODE_STATE_STANDBY"))
return QStringLiteral("Stand-by");
return QCoreApplication::translate("AiraJson", "Standby");
if (s == QLatin1String("PUMP_MODE_STATE_OFF"))
return QStringLiteral("Spento");
return QCoreApplication::translate("AiraJson", "Off");
// Stato operativo della pompa (state.pump_active_state).
if (s == QLatin1String("PUMP_ACTIVE_STATE_IDLE"))
return QStringLiteral("Ferma");
return QCoreApplication::translate("AiraJson", "Stopped");
if (s == QLatin1String("PUMP_ACTIVE_STATE_RUNNING"))
return QStringLiteral("In funzione");
return QCoreApplication::translate("AiraJson", "Running");
if (s.isEmpty())
return QStringLiteral("—");
// Sconosciuto: togliamo il prefisso e mostriamo il resto, minuscolo.
@@ -97,7 +99,10 @@ QString formatValue(const QVariant &value)
bool isNumber = false;
const double d = value.toDouble(&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();
return s.isEmpty() ? QStringLiteral("—") : s;
@@ -122,8 +127,8 @@ QString errorMessage(const QByteArray &body, int httpStatus)
return msg;
}
if (httpStatus == 0)
return QStringLiteral("server non raggiungibile");
return QStringLiteral("errore HTTP %1").arg(httpStatus);
return QCoreApplication::translate("AiraJson", "server unreachable");
return QCoreApplication::translate("AiraJson", "HTTP error %1").arg(httpStatus);
}
} // 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());
if (base.isEmpty()) {
emit errorOccurred(QStringLiteral("Indirizzo del bridge non configurato"));
emit errorOccurred(tr("Bridge address not configured"));
return 0;
}
@@ -79,7 +79,7 @@ QNetworkReply *ApiClient::send(const QByteArray &verb, const QString &path,
void ApiClient::refresh()
{
if (!configured()) {
emit errorOccurred(QStringLiteral("Configura indirizzo e token del bridge"));
emit errorOccurred(tr("Set the bridge address and token"));
return;
}
@@ -99,7 +99,7 @@ void ApiClient::refresh()
if (reply->error() != QNetworkReply::NoError || status != 200) {
const QString msg = (reply->error() == QNetworkReply::OperationCanceledError)
? QStringLiteral("Server non raggiungibile (timeout)")
? tr("Server unreachable (timeout)")
: AiraJson::errorMessage(body, status);
qWarning() << "[api] summary non riuscito:" << status << msg;
emit errorOccurred(msg);
@@ -110,7 +110,7 @@ void ApiClient::refresh()
const QVariantMap summary = AiraJson::parseSummary(body, &ok);
if (!ok) {
qWarning() << "[api] riassunto non interpretabile";
emit errorOccurred(QStringLiteral("Risposta del bridge non interpretabile"));
emit errorOccurred(tr("Unreadable response from the bridge"));
return;
}
m_summary = summary;
@@ -122,7 +122,7 @@ void ApiClient::testConnection()
{
const QString base = AiraJson::normalizeBaseUrl(m_settings ? m_settings->baseUrl() : QString());
if (base.isEmpty()) {
emit connectionTested(false, QStringLiteral("Inserisci l'indirizzo del bridge"));
emit connectionTested(false, tr("Enter the bridge address"));
return;
}
@@ -138,12 +138,12 @@ void ApiClient::testConnection()
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (reply->error() != QNetworkReply::NoError || status != 200) {
const QString msg = (reply->error() == QNetworkReply::OperationCanceledError)
? QStringLiteral("Timeout: nessuna risposta")
: QStringLiteral("Irraggiungibile (HTTP %1)").arg(status);
? tr("Timeout: no response")
: tr("Unreachable (HTTP %1)").arg(status);
emit connectionTested(false, msg);
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)
{
if (!configured()) {
emit commandFinished(false, QStringLiteral("Configura indirizzo e token del bridge"));
emit commandFinished(false, tr("Set the bridge address and token"));
return;
}
@@ -169,7 +169,7 @@ void ApiClient::sendCommand(const QString &id, const QVariant &value)
QNetworkReply *reply = send("POST", QStringLiteral("/command"), payload, 30);
if (!reply) {
emit commandFinished(false, QStringLiteral("Bridge non configurato"));
emit commandFinished(false, tr("Bridge not configured"));
return;
}
@@ -183,14 +183,14 @@ void ApiClient::sendCommand(const QString &id, const QVariant &value)
if (reply->error() != QNetworkReply::NoError || status != 200) {
const QString msg = (reply->error() == QNetworkReply::OperationCanceledError)
? QStringLiteral("Timeout: comando non inviato")
? tr("Timeout: command not sent")
: AiraJson::errorMessage(body, status);
qWarning() << "[api] comando" << id << "fallito:" << status << msg;
emit commandFinished(false, msg);
return;
}
emit commandFinished(true, QStringLiteral("Comando inviato"));
emit commandFinished(true, tr("Command sent"));
refresh(); // i valori letti cambiano: rileggo
});
}
+24 -2
View File
@@ -13,6 +13,7 @@
#include "apiclient.h"
#include "settings.h"
#include "translator.h"
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->setApplicationName(QStringLiteral("aira"));
app->setApplicationVersion(QStringLiteral("0.3.0"));
app->setApplicationVersion(QStringLiteral("0.4.0"));
Settings 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();
// Prima riga del log: dice subito se il device sta girando la build nuova
// o una stale (trappola nota del build engine).
qDebug() << "harbour-aira v0.3.0 build" << __DATE__ << __TIME__;
qDebug() << "harbour-aira v0.4.0 build" << __DATE__ << __TIME__;
view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings);
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->show();
+15
View File
@@ -52,3 +52,18 @@ void Settings::setToken(const QString &value)
m_settings.sync();
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.
//
// 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
{
Q_OBJECT
Q_PROPERTY(QString baseUrl READ baseUrl WRITE setBaseUrl NOTIFY baseUrlChanged)
Q_PROPERTY(QString token READ token WRITE setToken NOTIFY tokenChanged)
Q_PROPERTY(QString language READ language WRITE setLanguage NOTIFY languageChanged)
public:
explicit Settings(QObject *parent = 0);
@@ -27,9 +30,13 @@ public:
QString token() const;
void setToken(const QString &value);
QString language() const;
void setLanguage(const QString &value);
signals:
void baseUrlChanged();
void tokenChanged();
void languageChanged();
private:
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 <QJsonObject>
#include <QDebug>
#include <QLocale>
#include <QStringList>
#include <QTranslator>
#include "../src/airajson.h"
@@ -59,25 +61,46 @@ int main(int argc, char *argv[])
"ok=true senza summary -> vuoto");
// ---- modeLabel -------------------------------------------------------- //
check(AiraJson::modeLabel("OPERATING_STATUS_AUTO") == "Automatico", "AUTO -> Automatico");
check(AiraJson::modeLabel("OPERATING_STATUS_HOT_WATER") == "Acqua calda", "acqua calda");
// Attese in inglese: e' la lingua SORGENTE delle stringhe (l'italiano arriva
// 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("PUMP_MODE_STATE_HEATING") == "Riscaldamento",
"stato zona: riscaldamento");
check(AiraJson::modeLabel("PUMP_MODE_STATE_IDLE") == "In attesa",
"stato zona: in attesa");
check(AiraJson::modeLabel("PUMP_MODE_STATE_HEATING") == "Heating",
"zone state: heating");
check(AiraJson::modeLabel("PUMP_MODE_STATE_IDLE") == "Idle",
"zone state: idle");
check(AiraJson::modeLabel("PUMP_MODE_STATE_IGNOTO") == "ignoto",
"stato zona sconosciuto -> senza prefisso");
check(AiraJson::modeLabel("PUMP_ACTIVE_STATE_IDLE") == "Ferma",
"pompa ferma");
check(AiraJson::modeLabel("PUMP_ACTIVE_STATE_RUNNING") == "In funzione",
"pompa in funzione");
"unknown zone state -> prefix stripped");
check(AiraJson::modeLabel("PUMP_ACTIVE_STATE_IDLE") == "Stopped",
"pump stopped");
check(AiraJson::modeLabel("PUMP_ACTIVE_STATE_RUNNING") == "Running",
"pump running");
check(AiraJson::modeLabel("OPERATING_STATUS_DEFROST") == "defrost",
"stato sconosciuto: prefisso tolto, minuscolo");
"unknown state: prefix stripped, lowercase");
// ---- 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");
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(QString())) == "—", "stringa vuota -> trattino");
@@ -94,8 +117,36 @@ int main(int argc, char *argv[])
// ---- errorMessage ----------------------------------------------------- //
check(AiraJson::errorMessage(R"({"ok":false,"error":"token mancante o errato"})", 401)
== "token mancante o errato", "messaggio d'errore dal corpo");
check(AiraJson::errorMessage("", 502) == "errore HTTP 502", "fallback sullo stato HTTP");
check(AiraJson::errorMessage("", 0) == "server non raggiungibile", "stato 0 = irraggiungibile");
check(AiraJson::errorMessage("", 502) == "HTTP error 502", "http status fallback");
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() << 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>