0.6.0 blocco 1: Home di consultazione e pagina Regolazioni

- Home: stato in una riga col pallino, due letture grandi toccabili, tre
  azioni rapide; via i due slider, i sette pulsanti e la scheda separata.
- Nuova pagina Regolazioni: un blocco per grandezza, valore attuale sempre
  visibile, preimpostazioni per le dita fredde, conferma esplicita; il
  pulsante e' attivo solo se c'e' davvero qualcosa da cambiare.
- Messaggi dei comandi spostati in ApiClient (api.notice) con NoticeBar
  ancorata in fondo: prima nascevano fuori dallo schermo.
- Stato breve/esteso/livello in C++ (testabile senza SDK), 18 verifiche
  nuove: 79/79.
- stale + lastUpdate: la Home dice 'aggiornato alle HH:MM' e spegne i
  numeri vecchi invece di farli passare per freschi.
- 192/192 stringhe tradotte (54 nuove).
This commit is contained in:
2026-10-10 09:47:16 +02:00
parent 667ff63907
commit d5b63b57df
10 changed files with 1038 additions and 600 deletions
+42
View File
@@ -0,0 +1,42 @@
import QtQuick 2.2
import Sailfish.Silica 1.0
// Messaggio dell'ultimo comando, ancorato al bordo inferiore della pagina.
//
// Sta FUORI dal contenuto che scorre: prima il messaggio era la prima riga
// della colonna, quindi premendo un pulsante in fondo alla pagina nasceva
// fuori dallo schermo e non si capiva se il comando fosse partito.
//
// Lo stato sta in ApiClient (api.notice): qui si mostra e basta, cosi' il
// messaggio e' lo stesso su qualsiasi pagina.
Rectangle {
id: root
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: message.height + 2 * Theme.paddingMedium
color: Qt.rgba(0, 0, 0, 0.78)
opacity: api.notice.length > 0 ? 1 : 0
visible: opacity > 0
z: 100
Behavior on opacity { NumberAnimation { duration: 200 } }
Label {
id: message
anchors {
left: parent.left
right: parent.right
leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin
verticalCenter: parent.verticalCenter
}
text: api.notice
color: api.noticeIsError ? Theme.errorColor : Theme.highlightColor
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.Wrap
}
}
+62
View File
@@ -0,0 +1,62 @@
import QtQuick 2.2
import Sailfish.Silica 1.0
// Riquadro di una lettura grande nella Home: titolo, valore, riga sotto e la
// barra di avanzamento (facoltativa). Toccabile: porta alle regolazioni.
//
// La barra c'e' sempre, anche quando non serve, e resta trasparente: cosi' i
// due riquadri affiancati hanno la stessa altezza senza vincoli circolari.
BackgroundItem {
id: root
property string title: ""
property string value: ""
property string subtitle: ""
property real progress: 0
property bool showBar: false
property color valueColor: Theme.primaryColor
signal tapped()
height: content.height + 2 * Theme.paddingMedium
onClicked: root.tapped()
Column {
id: content
x: Theme.paddingMedium
y: Theme.paddingMedium
width: parent.width - 2 * Theme.paddingMedium
spacing: Theme.paddingSmall
Label {
width: parent.width
text: root.title
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeSmall
elide: Text.ElideRight
}
Label {
width: parent.width
text: root.value
color: root.valueColor
font.pixelSize: Theme.fontSizeHuge
elide: Text.ElideRight
}
Label {
width: parent.width
text: root.subtitle
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall
wrapMode: Text.Wrap
}
ProgressBar {
width: parent.width
value: Math.max(0, Math.min(1, root.progress))
opacity: root.showBar ? 1 : 0
}
}
}
+174 -376
View File
@@ -2,27 +2,65 @@ import QtQuick 2.2
import Sailfish.Silica 1.0 import Sailfish.Silica 1.0
import "../components" import "../components"
// Dashboard dell'impianto, ordinata come la schermata iniziale dell'app // Home: una schermata da GUARDARE, non un modulo da compilare.
// ufficiale Aira: in alto la temperatura esterna, poi il messaggio di stato, //
// poi la scheda dell'impianto con le due letture principali (casa e acqua // Stato in una riga, le due letture che contano (casa e acqua calda), tre
// calda). I comandi restano piu' sotto, perche' qui si possono dare davvero. // azioni rapide. Le regolazioni stanno in SetpointsPage, gli altri comandi in
// Legge api.summary (QVariantMap dal C++), quindi non dipende dallo scope // CommandsPage, i numeri in DataPage, i dettagli in PlantPage: qui solo quello
// delle property del main qml. // che si guarda molte volte al giorno, senza scorrere (docs/UX-STUDIO.md).
Page { Page {
id: page id: page
allowedOrientations: Orientation.All allowedOrientations: Orientation.All
// obiettivo ACS scelto dall'utente (non sovrascritto dal refresh finche' function raw(key) {
// non lo tocca, altrimenti lo slider "salta" a ogni lettura) return api.summary[key]
property int targetAcs: 55 }
property bool targetAcsTouched: false
// obiettivo della casa (zona 1); parte dalla temperatura attuale e non // I numeri si formattano nel C++ (ApiClient::formatValue): cosi' il
// viene sovrascritto dal refresh finche' l'utente non lo tocca // separatore decimale segue la lingua dell'app, non il toFixed di JS.
property real targetCasa: 20 function fmt(value) { return api.formatValue(value) }
property bool targetCasaTouched: false
property string notice: "" function count(key) {
property bool noticeIsError: false var v = page.raw(key)
if (v === undefined || v === null)
return "—"
return String(Math.round(Number(v)))
}
// Colore del pallino di stato. Il livello (0 regolare, 1 modo attivo,
// 2 errore) lo calcola il C++; qui si sceglie il colore, e l'ambra quando
// i numeri sono vecchi: un dato vecchio non e' un dato buono.
function statusColor() {
if (!api.hasData || api.stale)
return "orange"
var livello = api.statusLevel()
if (livello === 2)
return Theme.errorColor
if (livello === 1)
return Theme.highlightColor
return "#4caf50"
}
// Quanto e' carica l'acqua calda verso l'obiettivo (da 10 °C in su).
function waterProgress() {
var ora = Number(page.raw("dhw_current"))
var obiettivo = Number(page.raw("dhw_target"))
if (!(obiettivo > 10) || isNaN(ora))
return 0
return (ora - 10) / (obiettivo - 10)
}
// Riga sotto il titolo: dice anche quando i numeri sono vecchi.
function updateText() {
if (!api.hasData)
return qsTr("No data yet")
if (api.stale)
return qsTr("Not updated · last %1").arg(api.lastUpdate)
if (api.lastUpdate.length === 0)
return ""
return qsTr("Updated at %1").arg(api.lastUpdate)
}
function openSettings() { function openSettings() {
pageStack.push(Qt.resolvedUrl("SettingsPage.qml")) pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
@@ -40,100 +78,14 @@ Page {
pageStack.push(Qt.resolvedUrl("DataPage.qml")) pageStack.push(Qt.resolvedUrl("DataPage.qml"))
} }
function showNotice(msg, isError) { Component.onCompleted: {
page.notice = msg if (api.configured)
page.noticeIsError = isError === true api.refresh()
noticeTimer.restart()
}
function raw(key) {
return api.summary[key]
}
// I numeri si formattano nel C++ (ApiClient::formatValue): cosi' il
// separatore decimale segue la lingua dell'app, non il toFixed di JS.
function fmt(value) { return api.formatValue(value) }
function temp(key) {
return api.formatValue(page.raw(key))
}
function count(key) {
var v = page.raw(key)
if (v === undefined || v === null)
return "—"
return String(Math.round(Number(v)))
}
// "2026-10-09 18:52:17.199340" -> "18:52"
function shortTime(s) {
if (!s)
return "—"
var parts = String(s).split(" ")
if (parts.length < 2)
return String(s)
return parts[1].substring(0, 5)
}
// Messaggio grande in cima: e' la frase che l'app ufficiale mette al centro
// della schermata iniziale ("Relax, la tua pompa di calore..."). Si sceglie
// in ordine di importanza: errori, modalita' attive, pompa in funzione.
function stateMessage() {
if (!api.configured)
return ""
var errs = Number(page.raw("errors_count"))
if (errs > 0)
return (errs === 1)
? qsTr("Attention: one error on the plant")
: qsTr("Attention: %1 errors on the plant").arg(errs)
if (page.raw("away_mode") === true)
return qsTr("Away mode: the house is saving energy.")
if (page.raw("night_mode") === true)
return qsTr("Night mode is on.")
if (page.raw("force_heating") === true)
return qsTr("Boost: the house is heating at full power.")
if (page.raw("hot_water_heating") === true)
return qsTr("The heat pump is heating the hot water.")
if (String(page.raw("pump_active_state") || "").indexOf("RUNNING") >= 0)
return qsTr("The heat pump is working.")
return qsTr("Relax: the heat pump is running smoothly.")
}
// Riga piccola sotto il messaggio: modalita' e ora dell'ultima lettura.
function statusDetail() {
if (!api.configured)
return ""
return qsTr("%1 · last reading %2")
.arg(api.modeLabel(page.raw("operating_status")))
.arg(page.shortTime(page.raw("pump_updated")))
}
Timer {
id: noticeTimer
interval: 4000
onTriggered: page.notice = ""
} }
// Fuori dal pulsante: dentro il pulsante i figli finiscono nel contentItem.
RemorseItem { id: remorse } RemorseItem { id: remorse }
Connections {
target: api
onErrorOccurred: page.showNotice(message, true)
onCommandFinished: page.showNotice(message, !ok)
onSummaryChanged: {
if (!page.targetAcsTouched) {
var v = page.raw("dhw_target")
if (v !== undefined && v !== null)
page.targetAcs = Math.round(Number(v))
}
if (!page.targetCasaTouched) {
var c = page.raw("zone1_setpoint")
if (c !== undefined && c !== null && Number(c) > 0)
page.targetCasa = Math.round(Number(c) * 2) / 2
}
}
}
SilicaFlickable { SilicaFlickable {
id: flick id: flick
anchors.fill: parent anchors.fill: parent
@@ -154,10 +106,6 @@ Page {
text: qsTr("Plant") text: qsTr("Plant")
onClicked: page.openPlant() onClicked: page.openPlant()
} }
MenuItem {
text: qsTr("Other commands")
onClicked: page.openCommands()
}
MenuItem { MenuItem {
text: qsTr("Settings") text: qsTr("Settings")
onClicked: page.openSettings() onClicked: page.openSettings()
@@ -169,17 +117,9 @@ Page {
width: parent.width width: parent.width
spacing: Theme.paddingSmall spacing: Theme.paddingSmall
PageHeader { title: "Aira" } PageHeader {
title: "Aira"
// ---- avvisi ---------------------------------------------------- // description: page.updateText()
Label {
visible: page.notice.length > 0
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
text: page.notice
color: page.noticeIsError ? Theme.errorColor : Theme.highlightColor
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.Wrap
} }
// ---- bridge non configurato ------------------------------------ // // ---- bridge non configurato ------------------------------------ //
@@ -200,300 +140,158 @@ Page {
onClicked: page.openSettings() onClicked: page.openSettings()
} }
// ---- barra in alto: fuori casa / a casa ------------------------ // // ---- stato in una riga ----------------------------------------- //
Item { Item {
visible: api.configured visible: api.configured
x: Theme.horizontalPageMargin x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin
height: fuori.height height: Math.max(stato.height, pallino.height, attesa.height)
Label { Rectangle {
id: fuori id: pallino
anchors.left: parent.left width: Theme.fontSizeSmall * 0.6
text: qsTr("Outside %1 °C").arg(page.temp("outdoor_temperature")) height: width
color: Theme.secondaryColor radius: width / 2
font.pixelSize: Theme.fontSizeSmall color: page.statusColor()
anchors.verticalCenter: parent.verticalCenter
} }
Label { Label {
id: stato
anchors {
left: pallino.right
leftMargin: Theme.paddingMedium
right: attesa.left
rightMargin: Theme.paddingMedium
verticalCenter: parent.verticalCenter
}
text: api.stateShort()
color: Theme.primaryColor
font.pixelSize: Theme.fontSizeSmall
elide: Text.ElideRight
}
BusyIndicator {
id: attesa
anchors { anchors {
right: parent.right right: parent.right
verticalCenter: parent.verticalCenter verticalCenter: parent.verticalCenter
} }
text: page.raw("away_mode") === true ? qsTr("Away") : qsTr("At home") size: BusyIndicatorSize.Small
color: page.raw("away_mode") === true ? Theme.highlightColor : Theme.secondaryColor running: api.busy
font.pixelSize: Theme.fontSizeExtraSmall visible: api.busy
} }
} }
// ---- messaggio di stato (il "centro" della schermata ufficiale) - // // ---- le due letture che contano -------------------------------- //
Label { Row {
id: letture
visible: api.configured visible: api.configured
x: Theme.horizontalPageMargin x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin
text: page.stateMessage() spacing: Theme.paddingSmall
color: Theme.primaryColor
font.pixelSize: Theme.fontSizeMedium ReadingCell {
horizontalAlignment: Text.AlignHCenter width: (letture.width - letture.spacing) / 2
wrapMode: Text.Wrap title: qsTr("House")
value: page.fmt(page.raw("zone1_temperature")) + " °C"
subtitle: qsTr("target %1 °C").arg(page.fmt(page.raw("zone1_setpoint")))
valueColor: api.stale ? Theme.secondaryColor : Theme.primaryColor
onTapped: pageStack.push(Qt.resolvedUrl("SetpointsPage.qml"))
}
ReadingCell {
width: (letture.width - letture.spacing) / 2
title: qsTr("Hot water")
value: page.fmt(page.raw("dhw_current")) + " °C"
subtitle: qsTr("target %1 °C").arg(page.fmt(page.raw("dhw_target")))
progress: page.waterProgress()
showBar: true
valueColor: api.stale ? Theme.secondaryColor : Theme.primaryColor
onTapped: pageStack.push(Qt.resolvedUrl("SetpointsPage.qml"))
}
} }
Label { Label {
visible: api.configured visible: api.configured
x: Theme.horizontalPageMargin x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin
text: page.statusDetail() text: qsTr("Tap a value to adjust it")
color: Theme.secondaryColor color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall font.pixelSize: Theme.fontSizeExtraSmall
horizontalAlignment: Text.AlignHCenter
} }
// ---- scheda dell'impianto -------------------------------------- // // ---- azioni rapide --------------------------------------------- //
// Come la scheda dispositivo dell'app ufficiale: intestazione con SectionHeader {
// nome e connessione, poi le due letture principali. Toccandola si visible: api.configured
// apre la pagina "Impianto" (accessori, versioni, errori). text: qsTr("Quick actions")
}
Row {
visible: api.configured
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
spacing: Theme.paddingSmall
Button {
id: boostBtn
width: (parent.width - 2 * parent.spacing) / 3
text: qsTr("Boost")
onClicked: remorse.execute(boostBtn, qsTr("Starting DHW boost"),
function() { api.sendCommand("hot_water_boost_on") })
}
Button {
id: assenzaBtn
width: (parent.width - 2 * parent.spacing) / 3
// Il pulsante dice cosa succede premendolo, non lo stato.
text: page.raw("away_mode") ? qsTr("Back home") : qsTr("Away mode")
onClicked: {
if (page.raw("away_mode")) {
remorse.execute(assenzaBtn, qsTr("Away mode off"),
function() { api.sendCommand("away_mode_off") })
} else {
remorse.execute(assenzaBtn, qsTr("Away mode on"),
function() { api.sendCommand("away_mode_on") })
}
}
}
Button {
width: (parent.width - 2 * parent.spacing) / 3
text: qsTr("Commands")
onClicked: page.openCommands()
}
}
// ---- letture di contorno (si toccano: portano a Impianto) ------ //
BackgroundItem { BackgroundItem {
id: scheda
visible: api.configured visible: api.configured
x: Theme.horizontalPageMargin x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin
height: schedaCol.height + 3 * Theme.paddingMedium height: contornoTxt.height + Theme.paddingMedium
onClicked: page.openPlant() onClicked: page.openPlant()
Column { Label {
id: schedaCol id: contornoTxt
x: Theme.paddingMedium anchors {
y: Theme.paddingMedium left: parent.left
width: parent.width - 2 * Theme.paddingMedium right: parent.right
spacing: Theme.paddingMedium verticalCenter: parent.verticalCenter
Item {
width: parent.width
height: intestazione.height
Column {
id: intestazione
anchors {
left: parent.left
right: freccia.left
rightMargin: Theme.paddingSmall
}
Label {
text: qsTr("Aira heat pump")
color: Theme.primaryColor
font.pixelSize: Theme.fontSizeSmall
}
Label {
text: qsTr("Connected through the cloud")
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall
}
}
Label {
id: freccia
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
}
text: "›"
color: Theme.highlightColor
font.pixelSize: Theme.fontSizeLarge
}
}
ValueRow {
title: qsTr("Room temperature")
subtitle: qsTr("Target %1 °C").arg(page.temp("zone1_setpoint"))
value: page.temp("zone1_temperature") + " °C"
}
ValueRow {
title: qsTr("Domestic hot water")
subtitle: qsTr("Target %1 °C").arg(page.temp("dhw_target"))
value: page.temp("dhw_current") + " °C"
} }
text: qsTr("Outside %1 °C · Thermostat %2 °C · Zones %3")
.arg(page.fmt(page.raw("outdoor_temperature")))
.arg(page.fmt(page.raw("thermostat_temperature")))
.arg(page.count("zones"))
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall
wrapMode: Text.Wrap
} }
} }
// ---- riscaldamento della casa ---------------------------------- // Item { width: 1; height: Theme.paddingMedium }
SectionHeader { visible: api.configured; text: qsTr("Heating the house") }
Label {
visible: api.configured
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Heating setpoint %1 °C · %2")
.arg(page.temp("zone1_setpoint"))
.arg(api.modeLabel(page.raw("zone1_mode")))
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall
}
Slider {
id: casaSlider
visible: api.configured
width: parent.width
label: qsTr("New house target")
minimumValue: 5
maximumValue: 30
stepSize: 0.5
value: page.targetCasa
valueText: page.fmt(value) + " °C"
onValueChanged: {
page.targetCasaTouched = true
page.targetCasa = Math.round(value * 2) / 2
}
}
Button {
id: setCasaButton
visible: api.configured
anchors {
left: parent.left
right: parent.right
leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin
}
text: qsTr("Set house to %1 °C").arg(page.fmt(page.targetCasa))
onClicked: remorse.execute(setCasaButton,
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("Domestic hot water") }
ProgressBar {
visible: api.configured
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 4 * Theme.horizontalPageMargin
minimumValue: 20
maximumValue: 65
value: {
var v = page.raw("dhw_current")
return (v === undefined || v === null) ? 0 : Number(v)
}
}
Slider {
visible: api.configured
width: parent.width
label: qsTr("New target")
minimumValue: 45
maximumValue: 60
stepSize: 1
value: page.targetAcs
valueText: page.fmt(value) + " °C"
onValueChanged: {
page.targetAcsTouched = true
page.targetAcs = Math.round(value)
}
}
Button {
id: setAcsButton
visible: api.configured
anchors {
left: parent.left
right: parent.right
leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin
}
text: qsTr("Set target %1 °C").arg(page.targetAcs)
onClicked: remorse.execute(setAcsButton,
qsTr("DHW target to %1 °C").arg(page.targetAcs),
function() { api.sendCommand("set_hot_water_target", page.targetAcs) })
}
Button {
id: boostOnButton
visible: api.configured
anchors {
left: parent.left
right: parent.right
leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin
}
text: qsTr("Start DHW boost")
onClicked: remorse.execute(boostOnButton, qsTr("Starting DHW boost"),
function() { api.sendCommand("hot_water_boost_on") })
}
Button {
id: boostOffButton
visible: api.configured
anchors {
left: parent.left
right: parent.right
leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin
}
text: qsTr("Stop DHW boost")
onClicked: remorse.execute(boostOffButton, qsTr("Stopping DHW boost"),
function() { api.sendCommand("hot_water_boost_off") })
}
// ---- altre pagine ---------------------------------------------- //
SectionHeader { visible: api.configured; text: qsTr("Details") }
Button {
visible: api.configured
anchors {
left: parent.left
right: parent.right
leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin
}
text: qsTr("Plant and accessories")
onClicked: page.openPlant()
}
Button {
visible: api.configured
anchors {
left: parent.left
right: parent.right
leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin
}
text: qsTr("Consumption and savings")
onClicked: page.openData()
}
Button {
visible: api.configured
anchors {
left: parent.left
right: parent.right
leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin
}
text: qsTr("Other commands")
onClicked: page.openCommands()
}
BusyIndicator {
visible: api.busy
anchors.horizontalCenter: parent.horizontalCenter
size: BusyIndicatorSize.Small
running: true
}
Label {
visible: api.configured
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
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
}
} }
} }
Component.onCompleted: { NoticeBar { }
if (api.configured)
api.refresh()
}
} }
+234
View File
@@ -0,0 +1,234 @@
import QtQuick 2.2
import Sailfish.Silica 1.0
import "../components"
// Regolazioni: le grandezze che si cambiano (obiettivo casa, obiettivo acqua
// calda) e il boost, ognuna con il valore attuale sempre visibile, le
// preimpostazioni per le dita fredde e la conferma esplicita.
//
// Pagina a parte perche' la Home deve restare una schermata da guardare: due
// slider dentro la pagina che scorre si contendevano anche il trascinamento.
// Qui lo slider e' la PROPOSTA, il numero grande e' quello vero dell'impianto:
// i due coincidono solo dopo "Imposta".
Page {
id: page
allowedOrientations: Orientation.All
property real pendingHouse: 0
property bool houseTouched: false
property real pendingWater: 0
property bool waterTouched: false
function raw(key) {
return api.summary[key]
}
function fmt(value) { return api.formatValue(value) }
// Obiettivo vero dell'impianto (zona 1), a passi di mezzo grado.
function houseTarget() {
var v = page.raw("zone1_setpoint")
if (v === undefined || v === null || Number(v) <= 0)
return 0
return Math.round(Number(v) * 2) / 2
}
function waterTarget() {
var v = page.raw("dhw_target")
if (v === undefined || v === null)
return 0
return Math.round(Number(v))
}
// Allinea le proposte ai valori dell'impianto, ma non sovrascrive quello
// che l'utente sta scegliendo.
function syncFromPlant() {
if (!page.houseTouched)
page.pendingHouse = page.houseTarget()
if (!page.waterTouched)
page.pendingWater = page.waterTarget()
}
function houseChanged() { return Math.abs(page.pendingHouse - page.houseTarget()) > 0.001 }
function waterChanged() { return Math.abs(page.pendingWater - page.waterTarget()) > 0.001 }
Component.onCompleted: {
page.syncFromPlant()
if (api.configured)
api.refresh()
}
Connections {
target: api
onSummaryChanged: page.syncFromPlant()
}
SilicaFlickable {
anchors.fill: parent
contentHeight: col.height + Theme.paddingLarge
PullDownMenu {
MenuItem {
text: qsTr("Refresh")
onClicked: api.refresh()
}
MenuItem {
text: qsTr("Data")
onClicked: pageStack.push(Qt.resolvedUrl("DataPage.qml"))
}
MenuItem {
text: qsTr("Plant")
onClicked: pageStack.push(Qt.resolvedUrl("PlantPage.qml"))
}
MenuItem {
text: qsTr("Settings")
onClicked: pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
}
}
Column {
id: col
width: parent.width
spacing: Theme.paddingSmall
PageHeader { title: qsTr("Setpoints") }
// ---- riscaldamento casa ---------------------------------------- //
SectionHeader { text: qsTr("Heating the house") }
ValueRow {
title: qsTr("Current target")
subtitle: qsTr("What the plant is keeping now")
value: page.fmt(page.houseTarget()) + " °C"
}
Slider {
id: casaSlider
width: parent.width
label: qsTr("New house target")
minimumValue: 5
maximumValue: 30
stepSize: 0.5
value: page.pendingHouse
valueText: page.fmt(value) + " °C"
onValueChanged: {
// Solo un movimento vero dell'utente segna "toccato": il
// valore che arriva dall'impianto non deve bloccarlo.
if (Math.abs(page.pendingHouse - value) > 0.001) {
page.houseTouched = true
page.pendingHouse = Math.round(value * 2) / 2
}
}
}
Row {
x: Theme.horizontalPageMargin
width: col.width - 2 * Theme.horizontalPageMargin
spacing: Theme.paddingSmall
Repeater {
model: [16, 17, 18, 19, 20, 21]
Button {
width: (col.width - 2 * Theme.horizontalPageMargin
- 5 * Theme.paddingSmall) / 6
text: modelData + "°"
onClicked: {
page.houseTouched = true
page.pendingHouse = modelData
}
}
}
}
ActionButton {
text: qsTr("Set house to %1 °C").arg(page.fmt(page.pendingHouse))
confirmText: qsTr("House temperature to %1 °C").arg(page.fmt(page.pendingHouse))
enabled: page.houseChanged()
onTriggered: api.sendCommand("set_heating_setpoint", page.pendingHouse)
}
// ---- acqua calda sanitaria ------------------------------------- //
SectionHeader { text: qsTr("Domestic hot water") }
ValueRow {
title: qsTr("DHW target")
subtitle: qsTr("Water now %1 °C").arg(page.fmt(page.raw("dhw_current")))
value: page.fmt(page.waterTarget()) + " °C"
}
Slider {
id: acquaSlider
width: parent.width
label: qsTr("New target")
minimumValue: 45
maximumValue: 60
stepSize: 1
value: page.pendingWater
valueText: page.fmt(value) + " °C"
onValueChanged: {
if (Math.abs(page.pendingWater - value) > 0.001) {
page.waterTouched = true
page.pendingWater = Math.round(value)
}
}
}
Row {
x: Theme.horizontalPageMargin
width: col.width - 2 * Theme.horizontalPageMargin
spacing: Theme.paddingSmall
Repeater {
model: [45, 47, 50, 55, 60]
Button {
width: (col.width - 2 * Theme.horizontalPageMargin
- 4 * Theme.paddingSmall) / 5
text: modelData + "°"
onClicked: {
page.waterTouched = true
page.pendingWater = modelData
}
}
}
}
ActionButton {
text: qsTr("Set target %1 °C").arg(page.fmt(page.pendingWater))
confirmText: qsTr("DHW target to %1 °C").arg(page.fmt(page.pendingWater))
enabled: page.waterChanged()
onTriggered: api.sendCommand("set_hot_water_target", page.pendingWater)
}
// ---- boost ------------------------------------------------------ //
SectionHeader { text: qsTr("Hot water boost") }
ActionButton {
text: qsTr("Start DHW boost")
confirmText: qsTr("Starting DHW boost")
onTriggered: api.sendCommand("hot_water_boost_on")
}
ActionButton {
text: qsTr("Stop DHW boost")
confirmText: qsTr("Stopping DHW boost")
onTriggered: api.sendCommand("hot_water_boost_off")
}
Label {
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
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
}
Item { width: 1; height: Theme.paddingMedium }
}
}
NoticeBar { }
}
+78
View File
@@ -198,4 +198,82 @@ QString errorMessage(const QByteArray &body, int httpStatus)
return QCoreApplication::translate("AiraJson", "HTTP error %1").arg(httpStatus); return QCoreApplication::translate("AiraJson", "HTTP error %1").arg(httpStatus);
} }
namespace {
// Flag booleano del riassunto, con falso come ripiego.
bool flagValue(const QVariantMap &summary, const QString &key)
{
return summary.value(key).toBool();
}
int errorCount(const QVariantMap &summary)
{
return summary.value(QStringLiteral("errors_count")).toInt();
}
bool pumpRunning(const QVariantMap &summary)
{
return summary.value(QStringLiteral("pump_active_state")).toString()
.contains(QStringLiteral("RUNNING"));
}
} // namespace
QString stateShort(const QVariantMap &summary)
{
if (summary.isEmpty())
return QCoreApplication::translate("AiraJson", "No data");
const int errs = errorCount(summary);
if (errs > 0)
return (errs == 1)
? QCoreApplication::translate("AiraJson", "Attention: one error")
: QCoreApplication::translate("AiraJson", "Attention: %1 errors").arg(errs);
if (flagValue(summary, QStringLiteral("away_mode")))
return QCoreApplication::translate("AiraJson", "Away mode");
if (flagValue(summary, QStringLiteral("night_mode")))
return QCoreApplication::translate("AiraJson", "Night mode");
if (flagValue(summary, QStringLiteral("force_heating")))
return QCoreApplication::translate("AiraJson", "Heating at full power");
if (flagValue(summary, QStringLiteral("hot_water_heating")))
return QCoreApplication::translate("AiraJson", "Heating the hot water");
if (pumpRunning(summary))
return QCoreApplication::translate("AiraJson", "Working");
return QCoreApplication::translate("AiraJson", "Everything is fine");
}
QString stateLong(const QVariantMap &summary)
{
if (summary.isEmpty())
return QString();
const int errs = errorCount(summary);
if (errs > 0)
return (errs == 1)
? QCoreApplication::translate("AiraJson", "Attention: one error on the plant")
: QCoreApplication::translate("AiraJson", "Attention: %1 errors on the plant").arg(errs);
if (flagValue(summary, QStringLiteral("away_mode")))
return QCoreApplication::translate("AiraJson", "Away mode: the house is saving energy.");
if (flagValue(summary, QStringLiteral("night_mode")))
return QCoreApplication::translate("AiraJson", "Night mode is on.");
if (flagValue(summary, QStringLiteral("force_heating")))
return QCoreApplication::translate("AiraJson", "Boost: the house is heating at full power.");
if (flagValue(summary, QStringLiteral("hot_water_heating")))
return QCoreApplication::translate("AiraJson", "The heat pump is heating the hot water.");
if (pumpRunning(summary))
return QCoreApplication::translate("AiraJson", "The heat pump is working.");
return QCoreApplication::translate("AiraJson", "Relax: the heat pump is running smoothly.");
}
int statusLevel(const QVariantMap &summary)
{
if (summary.isEmpty())
return 1;
if (errorCount(summary) > 0)
return 2;
if (flagValue(summary, QStringLiteral("away_mode"))
|| flagValue(summary, QStringLiteral("night_mode"))
|| flagValue(summary, QStringLiteral("force_heating")))
return 1;
return 0;
}
} // namespace AiraJson } // namespace AiraJson
+12
View File
@@ -30,6 +30,18 @@ QVariantMap parseStats(const QByteArray &body, bool *ok);
// Codice di stato Aira ("OPERATING_STATUS_AUTO") in etichetta italiana. // Codice di stato Aira ("OPERATING_STATUS_AUTO") in etichetta italiana.
QString modeLabel(const QString &operatingStatus); QString modeLabel(const QString &operatingStatus);
// Riga di stato per la Home: breve, per stare in una riga sola
// ("Tutto regolare", "In assenza", "Attenzione: un errore").
QString stateShort(const QVariantMap &summary);
// Frase estesa per la pagina Impianto (quella dell'app ufficiale:
// "Relax: la pompa di calore funziona regolarmente").
QString stateLong(const QVariantMap &summary);
// Livello dello stato, per il colore della riga: 0 regolare, 1 modo attivo
// (assenza, notte, riscaldamento forzato), 2 errore sull'impianto.
int statusLevel(const QVariantMap &summary);
// Temperatura leggibile: "51,0" oppure "—" se il valore manca. // Temperatura leggibile: "51,0" oppure "—" se il valore manca.
QString formatValue(const QVariant &value); QString formatValue(const QVariant &value);
+67
View File
@@ -3,8 +3,10 @@
#include "settings.h" #include "settings.h"
#include <QDebug> #include <QDebug>
#include <QLocale>
#include <QNetworkRequest> #include <QNetworkRequest>
#include <QNetworkReply> #include <QNetworkReply>
#include <QTime>
#include <QTimer> #include <QTimer>
#include <QUrl> #include <QUrl>
@@ -12,7 +14,32 @@ ApiClient::ApiClient(Settings *settings, QObject *parent)
: QObject(parent) : QObject(parent)
, m_settings(settings) , m_settings(settings)
, m_busy(false) , m_busy(false)
, m_stale(false)
, m_noticeIsError(false)
, m_noticeTimer(0)
{ {
// Il messaggio dell'ultimo comando (o dell'ultimo errore) vale per ogni
// pagina: lo stato sta qui, si spegne da solo e la UI lo mostra ancorato
// in fondo alla schermata (NoticeBar), fuori dal contenuto che scorre.
m_noticeTimer = new QTimer(this);
m_noticeTimer->setSingleShot(true);
m_noticeTimer->setInterval(7000);
connect(m_noticeTimer, &QTimer::timeout, this, [this]() {
if (m_notice.isEmpty())
return;
m_notice.clear();
emit noticeChanged();
});
connect(this, &ApiClient::errorOccurred, this, [this](const QString &message) {
setNotice(message, true);
});
connect(this, &ApiClient::commandFinished, this, [this](bool ok, const QString &message) {
setNotice(message, !ok);
});
connect(this, &ApiClient::statsRefreshed, this, [this](bool ok, const QString &message) {
setNotice(message, !ok);
});
if (m_settings) { if (m_settings) {
// La UI deve riflettere subito il passaggio da "non configurato" a // La UI deve riflettere subito il passaggio da "non configurato" a
// "configurato" quando si salvano indirizzo o token. // "configurato" quando si salvano indirizzo o token.
@@ -29,6 +56,30 @@ bool ApiClient::configured() const
&& !m_settings->token().trimmed().isEmpty(); && !m_settings->token().trimmed().isEmpty();
} }
QString ApiClient::stateShort() const
{
return AiraJson::stateShort(m_summary);
}
QString ApiClient::stateLong() const
{
return AiraJson::stateLong(m_summary);
}
int ApiClient::statusLevel() const
{
return AiraJson::statusLevel(m_summary);
}
void ApiClient::setNotice(const QString &message, bool isError)
{
m_notice = message;
m_noticeIsError = isError;
emit noticeChanged();
if (!message.isEmpty())
m_noticeTimer->start();
}
void ApiClient::setBusy(bool value) void ApiClient::setBusy(bool value)
{ {
if (value == m_busy) if (value == m_busy)
@@ -102,6 +153,10 @@ void ApiClient::refresh()
? tr("Server unreachable (timeout)") ? tr("Server unreachable (timeout)")
: AiraJson::errorMessage(body, status); : AiraJson::errorMessage(body, status);
qWarning() << "[api] summary non riuscito:" << status << msg; qWarning() << "[api] summary non riuscito:" << status << msg;
if (!m_stale) {
m_stale = true;
emit staleChanged();
}
emit errorOccurred(msg); emit errorOccurred(msg);
return; return;
} }
@@ -110,10 +165,22 @@ void ApiClient::refresh()
const QVariantMap summary = AiraJson::parseSummary(body, &ok); const QVariantMap summary = AiraJson::parseSummary(body, &ok);
if (!ok) { if (!ok) {
qWarning() << "[api] riassunto non interpretabile"; qWarning() << "[api] riassunto non interpretabile";
if (!m_stale) {
m_stale = true;
emit staleChanged();
}
emit errorOccurred(tr("Unreadable response from the bridge")); emit errorOccurred(tr("Unreadable response from the bridge"));
return; return;
} }
m_summary = summary; m_summary = summary;
if (m_stale) {
m_stale = false;
emit staleChanged();
}
// Ora dell'ultimo aggiornamento riuscito: la Home la mostra, cosi' un
// numero vecchio non passa per un numero fresco.
m_lastUpdate = QLocale::system().toString(QTime::currentTime(), QLocale::ShortFormat);
emit lastUpdateChanged();
emit summaryChanged(summary); emit summaryChanged(summary);
}); });
} }
+33
View File
@@ -7,6 +7,7 @@
#include <QVariantMap> #include <QVariantMap>
class QNetworkReply; class QNetworkReply;
class QTimer;
class Settings; class Settings;
// Client HTTP verso il bridge sul server (~/workspace/Carlo/Aira/aira_bridge.py). // Client HTTP verso il bridge sul server (~/workspace/Carlo/Aira/aira_bridge.py).
@@ -24,6 +25,16 @@ class ApiClient : public QObject
// Statistiche della pagina "Dati" (calore, elettricita', COP, risparmi). // Statistiche della pagina "Dati" (calore, elettricita', COP, risparmi).
// Separata dal riassunto: e' una chiamata piu' lenta, con cache lato bridge. // Separata dal riassunto: e' una chiamata piu' lenta, con cache lato bridge.
Q_PROPERTY(QVariantMap stats READ stats NOTIFY statsChanged) Q_PROPERTY(QVariantMap stats READ stats NOTIFY statsChanged)
// Salute dell'aggiornamento: "stale" = l'ultimo tentativo non e' riuscito,
// quindi i numeri a schermo sono vecchi e vanno mostrati spenti.
Q_PROPERTY(bool stale READ stale NOTIFY staleChanged)
Q_PROPERTY(QString lastUpdate READ lastUpdate NOTIFY lastUpdateChanged)
Q_PROPERTY(bool hasData READ hasData NOTIFY summaryChanged)
// Messaggio dell'ultimo comando (o errore), mostrato in fondo alla pagina:
// sta qui e non nel QML perche' vale per OGNI pagina, e perche' cosi' il
// messaggio e' lo stesso ovunque si stia.
Q_PROPERTY(QString notice READ notice NOTIFY noticeChanged)
Q_PROPERTY(bool noticeIsError READ noticeIsError NOTIFY noticeChanged)
public: public:
explicit ApiClient(Settings *settings, QObject *parent = 0); explicit ApiClient(Settings *settings, QObject *parent = 0);
@@ -32,6 +43,18 @@ public:
bool configured() const; bool configured() const;
QVariantMap summary() const { return m_summary; } QVariantMap summary() const { return m_summary; }
QVariantMap stats() const { return m_stats; } QVariantMap stats() const { return m_stats; }
bool stale() const { return m_stale; }
QString lastUpdate() const { return m_lastUpdate; }
bool hasData() const { return !m_summary.isEmpty(); }
QString notice() const { return m_notice; }
bool noticeIsError() const { return m_noticeIsError; }
// Stato leggibile in una riga (stateShort) e per esteso (stateLong), piu'
// il livello da cui la UI prende il colore. Stanno in C++ perche' sono
// logica pura: verificate in tests/core_test.cpp e non duplicate fra pagine.
Q_INVOKABLE QString stateShort() const;
Q_INVOKABLE QString stateLong() const;
Q_INVOKABLE int statusLevel() const;
// Scarica il riassunto e lo emette con summaryChanged(). // Scarica il riassunto e lo emette con summaryChanged().
Q_INVOKABLE void refresh(); Q_INVOKABLE void refresh();
@@ -56,6 +79,9 @@ public:
signals: signals:
void busyChanged(); void busyChanged();
void configuredChanged(); void configuredChanged();
void staleChanged();
void lastUpdateChanged();
void noticeChanged();
void summaryChanged(const QVariantMap &summary); void summaryChanged(const QVariantMap &summary);
void statsChanged(const QVariantMap &stats); void statsChanged(const QVariantMap &stats);
void statsRefreshed(bool ok, const QString &message); void statsRefreshed(bool ok, const QString &message);
@@ -68,10 +94,17 @@ private:
const QByteArray &body, int timeoutSec); const QByteArray &body, int timeoutSec);
void armTimeout(QNetworkReply *reply, int seconds); void armTimeout(QNetworkReply *reply, int seconds);
void setBusy(bool value); void setBusy(bool value);
// Mostra un messaggio in fondo alla pagina per qualche secondo.
void setNotice(const QString &message, bool isError);
Settings *m_settings; Settings *m_settings;
QNetworkAccessManager m_nam; QNetworkAccessManager m_nam;
bool m_busy; bool m_busy;
bool m_stale;
QString m_lastUpdate;
QString m_notice;
bool m_noticeIsError;
QTimer *m_noticeTimer;
QVariantMap m_summary; QVariantMap m_summary;
QVariantMap m_stats; QVariantMap m_stats;
}; };
+61
View File
@@ -190,6 +190,67 @@ int main(int argc, char *argv[])
} }
} }
// --- stato in una riga e per esteso (Home e pagina Impianto) ----------- //
{
QVariantMap calmo;
calmo.insert("errors_count", 0);
calmo.insert("away_mode", false);
calmo.insert("night_mode", false);
calmo.insert("force_heating", false);
calmo.insert("hot_water_heating", false);
calmo.insert("pump_active_state", "PUMP_ACTIVE_STATE_IDLE");
check(AiraJson::stateShort(calmo) == "Everything is fine",
"stato breve: nessuna attivita'");
check(AiraJson::stateLong(calmo) == "Relax: the heat pump is running smoothly.",
"stato esteso: frase dell'app ufficiale");
check(AiraJson::statusLevel(calmo) == 0, "livello: regolare");
QVariantMap lavora = calmo;
lavora.insert("pump_active_state", "PUMP_ACTIVE_STATE_RUNNING");
check(AiraJson::stateShort(lavora) == "Working", "stato breve: pompa in funzione");
check(AiraJson::statusLevel(lavora) == 0,
"livello: pompa in funzione e' comunque regolare");
QVariantMap acqua = calmo;
acqua.insert("hot_water_heating", true);
check(AiraJson::stateShort(acqua) == "Heating the hot water",
"stato breve: sta scaldando l'acqua");
check(AiraJson::stateLong(acqua) == "The heat pump is heating the hot water.",
"stato esteso: acqua calda in carica");
QVariantMap assente = calmo;
assente.insert("away_mode", true);
check(AiraJson::stateShort(assente) == "Away mode", "stato breve: assenza");
check(AiraJson::statusLevel(assente) == 1, "livello: modo attivo, non regolare");
QVariantMap uno = calmo;
uno.insert("errors_count", 1);
check(AiraJson::stateShort(uno) == "Attention: one error",
"stato breve: un errore (singolare)");
check(AiraJson::stateLong(uno) == "Attention: one error on the plant",
"stato esteso: un errore");
check(AiraJson::statusLevel(uno) == 2, "livello: errore");
QVariantMap due = calmo;
due.insert("errors_count", 2);
check(AiraJson::stateShort(due) == "Attention: 2 errors",
"stato breve: due errori (plurale)");
check(AiraJson::stateLong(due) == "Attention: 2 errors on the plant",
"stato esteso: due errori");
// L'errore ha la precedenza su tutto: e' quello che va visto per primo.
QVariantMap misto = assente;
misto.insert("errors_count", 3);
check(AiraJson::stateShort(misto) == "Attention: 3 errors",
"stato breve: l'errore vince sul modo attivo");
check(AiraJson::statusLevel(misto) == 2, "livello: errore prima di tutto");
check(AiraJson::stateShort(QVariantMap()) == "No data",
"stato breve: nessun dato");
check(AiraJson::stateLong(QVariantMap()).isEmpty(),
"stato esteso: nessun dato = nessuna frase");
}
qInfo() << "----"; qInfo() << "----";
qInfo() << checks - failures << "/" << checks << "verifiche superate"; qInfo() << checks - failures << "/" << checks << "verifiche superate";
return failures == 0 ? 0 : 1; return failures == 0 ? 0 : 1;
+275 -224
View File
@@ -63,70 +63,155 @@
<source>HTTP error %1</source> <source>HTTP error %1</source>
<translation>errore HTTP %1</translation> <translation>errore HTTP %1</translation>
</message> </message>
<message>
<location filename="../src/airajson.cpp" line="225"/>
<source>No data</source>
<translation>Nessun dato</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="229"/>
<source>Attention: one error</source>
<translation>Attenzione: un errore</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="230"/>
<source>Attention: %1 errors</source>
<translation>Attenzione: %1 errori</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="232"/>
<source>Away mode</source>
<translation>Assenza</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="234"/>
<source>Night mode</source>
<translation>Modalità notte</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="236"/>
<source>Heating at full power</source>
<translation>Riscaldamento al massimo</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="238"/>
<source>Heating the hot water</source>
<translation>Sta scaldando l'acqua</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="240"/>
<source>Working</source>
<translation>In funzione</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="241"/>
<source>Everything is fine</source>
<translation>Tutto regolare</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="251"/>
<source>Attention: one error on the plant</source>
<translation>Attenzione: un errore sull'impianto</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="252"/>
<source>Attention: %1 errors on the plant</source>
<translation>Attenzione: %1 errori sull'impianto</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="254"/>
<source>Away mode: the house is saving energy.</source>
<translation>Modalità assenza: la casa sta risparmiando.</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="256"/>
<source>Night mode is on.</source>
<translation>Modalità notte attiva.</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="258"/>
<source>Boost: the house is heating at full power.</source>
<translation>Boost: la casa sta scaldando a pieno regime.</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="260"/>
<source>The heat pump is heating the hot water.</source>
<translation>La pompa di calore sta scaldando l'acqua calda.</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="262"/>
<source>The heat pump is working.</source>
<translation>La pompa di calore sta lavorando.</translation>
</message>
<message>
<location filename="../src/airajson.cpp" line="263"/>
<source>Relax: the heat pump is running smoothly.</source>
<translation>Relax: la pompa di calore funziona regolarmente.</translation>
</message>
</context> </context>
<context> <context>
<name>ApiClient</name> <name>ApiClient</name>
<message> <message>
<location filename="../src/apiclient.cpp" line="60"/> <location filename="../src/apiclient.cpp" line="111"/>
<source>Bridge address not configured</source> <source>Bridge address not configured</source>
<translation>Indirizzo del bridge non configurato</translation> <translation>Indirizzo del bridge non configurato</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="82"/> <location filename="../src/apiclient.cpp" line="133"/>
<location filename="../src/apiclient.cpp" line="178"/> <location filename="../src/apiclient.cpp" line="245"/>
<location filename="../src/apiclient.cpp" line="221"/> <location filename="../src/apiclient.cpp" line="288"/>
<source>Set the bridge address and token</source> <source>Set the bridge address and token</source>
<translation>Configura indirizzo e token del bridge</translation> <translation>Configura indirizzo e token del bridge</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="102"/> <location filename="../src/apiclient.cpp" line="153"/>
<source>Server unreachable (timeout)</source> <source>Server unreachable (timeout)</source>
<translation>Server non raggiungibile (timeout)</translation> <translation>Server non raggiungibile (timeout)</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="113"/> <location filename="../src/apiclient.cpp" line="172"/>
<location filename="../src/apiclient.cpp" line="209"/> <location filename="../src/apiclient.cpp" line="276"/>
<source>Unreadable response from the bridge</source> <source>Unreadable response from the bridge</source>
<translation>Risposta del bridge non interpretabile</translation> <translation>Risposta del bridge non interpretabile</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="125"/> <location filename="../src/apiclient.cpp" line="192"/>
<source>Enter the bridge address</source> <source>Enter the bridge address</source>
<translation>Inserisci l&apos;indirizzo del bridge</translation> <translation>Inserisci l&apos;indirizzo del bridge</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="141"/> <location filename="../src/apiclient.cpp" line="208"/>
<source>Timeout: no response</source> <source>Timeout: no response</source>
<translation>Timeout: nessuna risposta</translation> <translation>Timeout: nessuna risposta</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="142"/> <location filename="../src/apiclient.cpp" line="209"/>
<source>Unreachable (HTTP %1)</source> <source>Unreachable (HTTP %1)</source>
<translation>Irraggiungibile (HTTP %1)</translation> <translation>Irraggiungibile (HTTP %1)</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="146"/> <location filename="../src/apiclient.cpp" line="213"/>
<source>Bridge reachable</source> <source>Bridge reachable</source>
<translation>Bridge raggiungibile</translation> <translation>Bridge raggiungibile</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="186"/> <location filename="../src/apiclient.cpp" line="253"/>
<location filename="../src/apiclient.cpp" line="230"/> <location filename="../src/apiclient.cpp" line="297"/>
<source>Bridge not configured</source> <source>Bridge not configured</source>
<translation>Bridge non configurato</translation> <translation>Bridge non configurato</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="198"/> <location filename="../src/apiclient.cpp" line="265"/>
<source>Timeout: no statistics</source> <source>Timeout: no statistics</source>
<translation>Timeout: statistiche non disponibili</translation> <translation>Timeout: statistiche non disponibili</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="244"/> <location filename="../src/apiclient.cpp" line="311"/>
<source>Timeout: command not sent</source> <source>Timeout: command not sent</source>
<translation>Timeout: comando non inviato</translation> <translation>Timeout: comando non inviato</translation>
</message> </message>
<message> <message>
<location filename="../src/apiclient.cpp" line="251"/> <location filename="../src/apiclient.cpp" line="318"/>
<source>Command sent</source> <source>Command sent</source>
<translation>Comando inviato</translation> <translation>Comando inviato</translation>
</message> </message>
@@ -434,263 +519,111 @@
<context> <context>
<name>MainPage</name> <name>MainPage</name>
<message> <message>
<source>yes</source> <location filename="../qml/pages/MainPage.qml" line="57"/>
<translation type="vanished">sì</translation> <source>No data yet</source>
<translation>Nessun dato</translation>
</message> </message>
<message> <message>
<source>no</source> <location filename="../qml/pages/MainPage.qml" line="59"/>
<translation type="vanished">no</translation> <source>Not updated · last %1</source>
<translation>Non aggiornato · ultimo %1</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="146"/> <location filename="../qml/pages/MainPage.qml" line="62"/>
<source>Updated at %1</source>
<translation>Aggiornato alle %1</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="98"/>
<source>Refresh</source> <source>Refresh</source>
<translation>Aggiorna</translation> <translation>Aggiorna</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="162"/> <location filename="../qml/pages/MainPage.qml" line="110"/>
<location filename="../qml/pages/MainPage.qml" line="199"/> <location filename="../qml/pages/MainPage.qml" line="139"/>
<source>Settings</source> <source>Settings</source>
<translation>Impostazioni</translation> <translation>Impostazioni</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="158"/> <location filename="../qml/pages/MainPage.qml" line="196"/>
<location filename="../qml/pages/MainPage.qml" line="472"/> <source>House</source>
<source>Other commands</source> <translation>Casa</translation>
<translation>Altri comandi</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="87"/> <location filename="../qml/pages/MainPage.qml" line="198"/>
<source>Attention: one error on the plant</source> <location filename="../qml/pages/MainPage.qml" line="207"/>
<translation>Attenzione: un errore sull&apos;impianto</translation> <source>target %1 °C</source>
<translation>obiettivo %1 °C</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="88"/> <location filename="../qml/pages/MainPage.qml" line="205"/>
<source>Attention: %1 errors on the plant</source> <source>Hot water</source>
<translation>Attenzione: %1 errori sull&apos;impianto</translation> <translation>Acqua calda</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="90"/> <location filename="../qml/pages/MainPage.qml" line="219"/>
<source>Away mode: the house is saving energy.</source> <source>Tap a value to adjust it</source>
<translation>Modalità assenza: la casa sta risparmiando.</translation> <translation>Tocca un valore per regolarlo</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="92"/> <location filename="../qml/pages/MainPage.qml" line="227"/>
<source>Night mode is on.</source> <source>Quick actions</source>
<translation>Modalità notte attiva.</translation> <translation>Azioni rapide</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="94"/> <location filename="../qml/pages/MainPage.qml" line="239"/>
<source>Boost: the house is heating at full power.</source> <source>Boost</source>
<translation>Boost: la casa sta scaldando a pieno regime.</translation> <translation>Boost</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="96"/> <location filename="../qml/pages/MainPage.qml" line="248"/>
<source>The heat pump is heating the hot water.</source> <source>Back home</source>
<translation>La pompa di calore sta scaldando l&apos;acqua calda.</translation> <translation>Torna a casa</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="98"/> <location filename="../qml/pages/MainPage.qml" line="251"/>
<source>The heat pump is working.</source> <source>Away mode off</source>
<translation>La pompa di calore sta lavorando.</translation> <translation>Assenza disattivata</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="99"/> <location filename="../qml/pages/MainPage.qml" line="254"/>
<source>Relax: the heat pump is running smoothly.</source> <source>Away mode on</source>
<translation>Relax: la pompa di calore funziona regolarmente.</translation> <translation>Assenza attivata</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="106"/> <location filename="../qml/pages/MainPage.qml" line="262"/>
<source>%1 · last reading %2</source> <source>Commands</source>
<translation>%1 · ultima lettura %2</translation> <translation>Comandi</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="150"/> <location filename="../qml/pages/MainPage.qml" line="282"/>
<source>Outside %1 °C · Thermostat %2 °C · Zones %3</source>
<translation>Esterno %1 °C · Termostato %2 °C · Zone %3</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="102"/>
<source>Data</source> <source>Data</source>
<translation>Dati</translation> <translation>Dati</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="190"/> <location filename="../qml/pages/MainPage.qml" line="130"/>
<source>Bridge not configured. Open the settings and enter the server address and the token.</source> <source>Bridge not configured. Open the settings and enter the server address and the token.</source>
<translation>Bridge non configurato. Apri le impostazioni e inserisci l&apos;indirizzo del server e il token.</translation> <translation>Bridge non configurato. Apri le impostazioni e inserisci l&apos;indirizzo del server e il token.</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="213"/> <location filename="../qml/pages/MainPage.qml" line="240"/>
<source>Outside %1 °C</source>
<translation>Esterno %1 °C</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="223"/>
<source>Away</source>
<translation>Assente</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="223"/>
<source>At home</source>
<translation>In casa</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="283"/>
<source>Aira heat pump</source>
<translation>Aira Pompa di calore</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="288"/>
<source>Connected through the cloud</source>
<translation>Connessa tramite Cloud</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="307"/>
<source>Room temperature</source>
<translation>Temperatura stanza</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="308"/>
<location filename="../qml/pages/MainPage.qml" line="314"/>
<source>Target %1 °C</source>
<translation>Obiettivo %1 °C</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="321"/>
<source>Heating the house</source>
<translation>Riscaldamento della casa</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="326"/>
<source>Heating setpoint %1 °C · %2</source>
<translation>Obiettivo riscaldamento %1 °C · %2</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="438"/>
<source>Details</source>
<translation>Dettagli</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="448"/>
<source>Plant and accessories</source>
<translation>Impianto e accessori</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="460"/>
<source>Consumption and savings</source>
<translation>Consumi e risparmi</translation>
</message>
<message>
<source>House temperature</source>
<translation type="vanished">Temperatura in casa</translation>
</message>
<message>
<source>target %1 °C · %2</source>
<translation type="vanished">obiettivo %1 °C · %2</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="337"/>
<source>New house target</source>
<translation>Nuovo obiettivo casa</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="358"/>
<source>Set house to %1 °C</source>
<translation>Imposta casa a %1 °C</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="360"/>
<source>House temperature to %1 °C</source>
<translation>Temperatura della casa a %1 °C</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="313"/>
<location filename="../qml/pages/MainPage.qml" line="365"/>
<source>Domestic hot water</source>
<translation>Acqua calda sanitaria</translation>
</message>
<message>
<source>DHW target</source>
<translation type="vanished">Obiettivo acqua calda</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="382"/>
<source>New target</source>
<translation>Nuovo obiettivo</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="403"/>
<source>Set target %1 °C</source>
<translation>Imposta obiettivo %1 °C</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="405"/>
<source>DHW target to %1 °C</source>
<translation>Obiettivo ACS a %1 °C</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="418"/>
<source>Start DHW boost</source>
<translation>Avvia boost ACS</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="419"/>
<source>Starting DHW boost</source> <source>Starting DHW boost</source>
<translation>Avvio del boost ACS</translation> <translation>Avvio del boost ACS</translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/MainPage.qml" line="432"/> <location filename="../qml/pages/MainPage.qml" line="106"/>
<source>Stop DHW boost</source>
<translation>Ferma boost ACS</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="433"/>
<source>Stopping DHW boost</source>
<translation>Arresto del boost ACS</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="154"/>
<source>Plant</source> <source>Plant</source>
<translation>Impianto</translation> <translation>Impianto</translation>
</message> </message>
<message> <message>
<source>Outdoor temperature</source> <location filename="../qml/pages/MainPage.qml" line="248"/>
<translation type="vanished">Temperatura esterna</translation>
</message>
<message>
<source>Indoor temperature (pump)</source>
<translation type="vanished">Temperatura interna (pompa)</translation>
</message>
<message>
<source>Mode</source>
<translation type="vanished">Modalità</translation>
</message>
<message>
<source>Away mode</source> <source>Away mode</source>
<translation type="vanished">Assenza</translation> <translation>Assenza</translation>
</message>
<message>
<source>Night mode</source>
<translation type="vanished">Night mode</translation>
</message>
<message>
<source>Forced heating</source>
<translation type="vanished">Riscaldamento forzato</translation>
</message>
<message>
<source>Inline heater</source>
<translation type="vanished">Resistenze integrative</translation>
</message>
<message>
<source>Zone</source>
<translation type="vanished">Zone</translation>
</message>
<message>
<source>Errors</source>
<translation type="vanished">Errori</translation>
</message>
<message>
<source>Last reading</source>
<translation type="vanished">Aggiornato alle</translation>
</message>
<message>
<location filename="../qml/pages/MainPage.qml" line="487"/>
<source>Commands are sent to the bridge and from there to the plant: they may take a few seconds.</source>
<translation>I comandi vengono inviati al bridge e da lì all&apos;impianto: possono richiedere qualche secondo.</translation>
</message> </message>
</context> </context>
<context> <context>
@@ -886,6 +819,124 @@
<translation>Errori segnalati</translation> <translation>Errori segnalati</translation>
</message> </message>
</context> </context>
<context>
<name>SetpointsPage</name>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="73"/>
<source>Refresh</source>
<translation>Aggiorna</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="77"/>
<source>Data</source>
<translation>Dati</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="81"/>
<source>Plant</source>
<translation>Impianto</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="85"/>
<source>Settings</source>
<translation>Impostazioni</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="95"/>
<source>Setpoints</source>
<translation>Regolazioni</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="98"/>
<source>Heating the house</source>
<translation>Riscaldamento della casa</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="101"/>
<source>Current target</source>
<translation>Obiettivo attuale</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="102"/>
<source>What the plant is keeping now</source>
<translation>Quello che l'impianto mantiene adesso</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="109"/>
<source>New house target</source>
<translation>Nuovo obiettivo casa</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="146"/>
<source>Set house to %1 °C</source>
<translation>Imposta casa a %1 °C</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="147"/>
<source>House temperature to %1 °C</source>
<translation>Temperatura della casa a %1 °C</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="153"/>
<source>Domestic hot water</source>
<translation>Acqua calda sanitaria</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="156"/>
<source>DHW target</source>
<translation>Obiettivo acqua calda</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="157"/>
<source>Water now %1 °C</source>
<translation>Acqua ora %1 °C</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="164"/>
<source>New target</source>
<translation>Nuovo obiettivo</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="199"/>
<source>Set target %1 °C</source>
<translation>Imposta obiettivo %1 °C</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="200"/>
<source>DHW target to %1 °C</source>
<translation>Obiettivo ACS a %1 °C</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="206"/>
<source>Hot water boost</source>
<translation>Boost acqua calda</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="209"/>
<source>Start DHW boost</source>
<translation>Avvia boost ACS</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="210"/>
<source>Starting DHW boost</source>
<translation>Avvio del boost ACS</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="215"/>
<source>Stop DHW boost</source>
<translation>Ferma boost ACS</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="216"/>
<source>Stopping DHW boost</source>
<translation>Arresto del boost ACS</translation>
</message>
<message>
<location filename="../qml/pages/SetpointsPage.qml" line="223"/>
<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> <context>
<name>SettingsPage</name> <name>SettingsPage</name>
<message> <message>