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
@@ -16,6 +16,9 @@ bridge/.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# cataloghi compilati: si rigenerano in build da translations/*.ts
|
||||
translations/*.qm
|
||||
|
||||
# temporanei
|
||||
*~
|
||||
*.bak
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
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 |
|
||||
|---|---|---|---|
|
||||
|  |  |  |  |
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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 | Other commands | Settings | Cover |
|
||||
|---|---|---|---|
|
||||
|  |  |  |  |
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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à
|
||||
|
||||
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 45 KiB |
@@ -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).
|
||||
|
||||
@@ -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,8 +57,8 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,4 +14,4 @@ ApplicationWindow {
|
||||
|
||||
// Nessun allowedOrientations qui: a livello di window limita TUTTE le
|
||||
// pagine (intersezione), anche quelle che dichiarano Orientation.All.
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -163,4 +163,4 @@ Page {
|
||||
if (api.configured)
|
||||
api.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -351,4 +355,4 @@ Page {
|
||||
if (api.configured)
|
||||
api.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -113,4 +155,4 @@ Page {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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";
|
||||
|
||||
@@ -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'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'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'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'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'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'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'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>
|
||||