0.5.0: riordino come l'app ufficiale (home, Dati, Impianto)

- home: temperatura esterna e in casa/assente in alto, messaggio di stato,
  scheda impianto con temperatura stanza e acqua calda; comandi sotto
- pagina Dati: calore prodotto, elettricita' usata, COP acqua calda e
  riscaldamento, risparmio, CO2 evitata, Smart Tariff Control (mese per mese)
- pagina Impianto: stato, termostato a parete, versioni software, errori
- bridge 0.4.0: rotta /stats (servizio statistiche del cloud) e blocco
  dispositivo in /summary (ID breve, versioni, termostato)
- C++: parseStats, formatNumber, formatMoney, monthLabel (separatore e
  valuta secondo la lingua); 61/61 verifiche del nucleo
- anteprime aggiornate (dati, impianto) in inglese e italiano
This commit is contained in:
2026-10-10 08:59:03 +02:00
parent ec7333a185
commit 4f279a18b0
30 changed files with 2355 additions and 353 deletions
+251
View File
@@ -0,0 +1,251 @@
import QtQuick 2.2
import Sailfish.Silica 1.0
import "../components"
// Dati: calore prodotto, elettricita' usata, efficienza (COP) e risparmi, come
// la scheda "Dati" dell'app ufficiale. Sono numeri che Aira calcola nel cloud:
// il bridge li legge in sola lettura e li tiene in cache per 10 minuti.
Page {
id: page
allowedOrientations: Orientation.All
// La pagina non ha solo la lettura: mostra anche cosa sta facendo.
property bool loading: false
property string error: ""
function stats() {
return api.stats
}
function mese() {
return page.stats()["month"] || {}
}
function risparmi() {
return page.stats()["savings"] || {}
}
function tariffa() {
return page.stats()["smart_tariff"] || {}
}
function valuta() {
return page.stats()["currency"] || "EUR"
}
function kwh(valore) {
if (valore === undefined || valore === null)
return "—"
return api.formatValue(valore) + " kWh"
}
// Il COP ha due decimali, la temperatura uno: per questo non basta fmt().
function cop(valore) {
if (valore === undefined || valore === null)
return qsTr("no data")
return api.formatNumber(valore, 2)
}
function soldi(valore) {
if (valore === undefined || valore === null)
return "—"
return api.formatMoney(valore, page.valuta())
}
function meseNome(chiave) {
return api.monthLabel(chiave)
}
Connections {
target: api
onStatsChanged: {
page.error = ""
page.loading = false
}
onStatsRefreshed: {
page.loading = false
if (!ok)
page.error = message
}
onErrorOccurred: page.error = message
}
SilicaFlickable {
anchors.fill: parent
contentHeight: col.height + Theme.paddingLarge
PullDownMenu {
MenuItem {
text: qsTr("Refresh")
onClicked: {
page.loading = true
api.refreshStats()
}
}
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("Data") }
Label {
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
text: qsTr("Numbers calculated by Aira in the cloud: the app only reads them from your own bridge.")
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall
wrapMode: Text.Wrap
}
Label {
visible: page.error.length > 0
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
text: page.error
color: Theme.errorColor
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.Wrap
}
Label {
visible: Object.keys(page.stats()).length === 0 && page.error.length === 0
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
text: qsTr("Reading the data...")
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.Wrap
}
// ---- energia e efficienza del mese ----------------------------- //
SectionHeader {
visible: Object.keys(page.stats()).length > 0
text: page.mese()["label"]
? qsTr("Month of %1").arg(page.meseNome(page.mese()["label"]))
: qsTr("This month")
}
ValueRow {
visible: Object.keys(page.stats()).length > 0
title: qsTr("Heat produced")
subtitle: qsTr("What the heat pump gave the house")
value: page.kwh(page.mese()["heat_kwh"])
}
ValueRow {
visible: Object.keys(page.stats()).length > 0
title: qsTr("Electricity used")
subtitle: qsTr("What the heat pump took from the grid")
value: page.kwh(page.mese()["energy_kwh"])
}
InfoRow {
visible: Object.keys(page.stats()).length > 0
label: qsTr("Efficiency, domestic hot water")
value: page.cop(page.mese()["cop_dhw"])
}
InfoRow {
visible: Object.keys(page.stats()).length > 0
label: qsTr("Efficiency, heating")
value: page.cop(page.mese()["cop_heating"])
}
Label {
visible: Object.keys(page.stats()).length > 0
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
text: qsTr("Efficiency (COP) is how much heat the pump delivers for each unit of electricity: higher is better.")
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall
wrapMode: Text.Wrap
}
// ---- risparmi --------------------------------------------------- //
SectionHeader {
visible: Object.keys(page.risparmi()).length > 0
text: qsTr("Your savings, last %1 days").arg(page.stats()["period_days"] || 30)
}
ValueRow {
visible: Object.keys(page.risparmi()).length > 0
title: qsTr("Saved on the bill")
subtitle: qsTr("Compared with a gas boiler")
value: page.soldi(page.risparmi()["amount"])
valueColor: Theme.highlightColor
}
ValueRow {
visible: Object.keys(page.risparmi()).length > 0
title: qsTr("CO₂ avoided")
subtitle: qsTr("Not released into the air")
value: api.formatValue(page.risparmi()["co2_kg"]) + " kg"
}
ValueRow {
visible: Object.keys(page.tariffa()).length > 0
title: qsTr("Smart Tariff Control")
subtitle: qsTr("Saving from heating when energy is cheaper")
value: page.soldi(page.tariffa()["amount"])
valueColor: Theme.highlightColor
}
// ---- dettaglio mese per mese ------------------------------------ //
SectionHeader {
visible: page.stats()["cop_monthly"] && page.stats()["cop_monthly"].length > 0
text: qsTr("Efficiency month by month")
}
Repeater {
model: page.stats()["cop_monthly"] || []
InfoRow {
label: page.meseNome(modelData["month"])
value: page.cop(modelData["dhw_cop"])
}
}
SectionHeader {
visible: page.stats()["monthly_savings"] && page.stats()["monthly_savings"].length > 0
text: qsTr("Savings month by month")
}
Repeater {
model: page.stats()["monthly_savings"] || []
InfoRow {
label: page.meseNome(modelData["month"])
value: page.soldi(modelData["amount"])
}
}
BusyIndicator {
visible: page.loading
anchors.horizontalCenter: parent.horizontalCenter
size: BusyIndicatorSize.Small
running: true
}
Item { width: 1; height: Theme.paddingMedium }
}
}
Component.onCompleted: {
if (api.configured) {
page.loading = true
api.refreshStats()
}
}
}
+200 -59
View File
@@ -2,8 +2,12 @@ import QtQuick 2.2
import Sailfish.Silica 1.0
import "../components"
// Dashboard dell'impianto. Legge api.summary (QVariantMap dal C++), quindi non
// dipende dallo scope delle property del main qml.
// Dashboard dell'impianto, ordinata come la schermata iniziale dell'app
// 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
// calda). I comandi restano piu' sotto, perche' qui si possono dare davvero.
// Legge api.summary (QVariantMap dal C++), quindi non dipende dallo scope
// delle property del main qml.
Page {
id: page
@@ -28,6 +32,14 @@ Page {
pageStack.push(Qt.resolvedUrl("CommandsPage.qml"))
}
function openPlant() {
pageStack.push(Qt.resolvedUrl("PlantPage.qml"))
}
function openData() {
pageStack.push(Qt.resolvedUrl("DataPage.qml"))
}
function showNotice(msg, isError) {
page.notice = msg
page.noticeIsError = isError === true
@@ -46,10 +58,6 @@ Page {
return api.formatValue(page.raw(key))
}
function flag(key) {
return page.raw(key) ? qsTr("yes") : qsTr("no")
}
function count(key) {
var v = page.raw(key)
if (v === undefined || v === null)
@@ -67,6 +75,39 @@ Page {
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
@@ -106,13 +147,21 @@ Page {
onClicked: api.refresh()
}
MenuItem {
text: qsTr("Settings")
onClicked: page.openSettings()
text: qsTr("Data")
onClicked: page.openData()
}
MenuItem {
text: qsTr("Plant")
onClicked: page.openPlant()
}
MenuItem {
text: qsTr("Other commands")
onClicked: page.openCommands()
}
MenuItem {
text: qsTr("Settings")
onClicked: page.openSettings()
}
}
Column {
@@ -151,39 +200,132 @@ Page {
onClicked: page.openSettings()
}
// ---- casa: temperatura della casa (dato principale) ------------ //
Label {
// ---- barra in alto: fuori casa / a casa ------------------------ //
Item {
visible: api.configured
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("House temperature")
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeSmall
}
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
height: fuori.height
Label {
visible: api.configured
anchors.horizontalCenter: parent.horizontalCenter
text: page.temp("zone1_temperature") + " °C"
color: Theme.primaryColor
font.pixelSize: Theme.fontSizeHuge
}
Label {
id: fuori
anchors.left: parent.left
text: qsTr("Outside %1 °C").arg(page.temp("outdoor_temperature"))
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeSmall
}
ProgressBar {
visible: api.configured
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 4 * Theme.horizontalPageMargin
minimumValue: 5
maximumValue: 30
value: {
var v = page.raw("zone1_temperature")
return (v === undefined || v === null) ? 0 : Number(v)
Label {
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
}
text: page.raw("away_mode") === true ? qsTr("Away") : qsTr("At home")
color: page.raw("away_mode") === true ? Theme.highlightColor : Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall
}
}
// ---- messaggio di stato (il "centro" della schermata ufficiale) - //
Label {
visible: api.configured
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
text: page.stateMessage()
color: Theme.primaryColor
font.pixelSize: Theme.fontSizeMedium
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
}
Label {
visible: api.configured
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
text: page.statusDetail()
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall
horizontalAlignment: Text.AlignHCenter
}
// ---- scheda dell'impianto -------------------------------------- //
// Come la scheda dispositivo dell'app ufficiale: intestazione con
// nome e connessione, poi le due letture principali. Toccandola si
// apre la pagina "Impianto" (accessori, versioni, errori).
BackgroundItem {
id: scheda
visible: api.configured
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
height: schedaCol.height + 3 * Theme.paddingMedium
onClicked: page.openPlant()
Column {
id: schedaCol
x: Theme.paddingMedium
y: Theme.paddingMedium
width: parent.width - 2 * Theme.paddingMedium
spacing: Theme.paddingMedium
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"
}
}
}
// ---- riscaldamento della casa ---------------------------------- //
SectionHeader { visible: api.configured; text: qsTr("Heating the house") }
Label {
visible: api.configured
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("target %1 °C · %2").arg(page.temp("zone1_setpoint")).arg(api.modeLabel(page.raw("zone1_mode")))
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
}
@@ -222,14 +364,6 @@ Page {
// ---- acqua calda sanitaria ------------------------------------- //
SectionHeader { visible: api.configured; text: qsTr("Domestic hot water") }
Label {
visible: api.configured
anchors.horizontalCenter: parent.horizontalCenter
text: page.temp("dhw_current") + " °C"
color: Theme.primaryColor
font.pixelSize: Theme.fontSizeLarge
}
ProgressBar {
visible: api.configured
anchors.horizontalCenter: parent.horizontalCenter
@@ -242,12 +376,6 @@ Page {
}
}
InfoRow {
visible: api.configured
label: qsTr("DHW target")
value: page.temp("dhw_target") + " °C"
}
Slider {
visible: api.configured
width: parent.width
@@ -306,19 +434,32 @@ Page {
function() { api.sendCommand("hot_water_boost_off") })
}
// ---- stato impianto ------------------------------------------- //
SectionHeader { visible: api.configured; text: qsTr("Plant") }
// ---- altre pagine ---------------------------------------------- //
SectionHeader { visible: api.configured; text: qsTr("Details") }
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("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("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
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
@@ -355,4 +496,4 @@ Page {
if (api.configured)
api.refresh()
}
}
}
+187
View File
@@ -0,0 +1,187 @@
import QtQuick 2.2
import Sailfish.Silica 1.0
import "../components"
// Impianto: la scheda "Impianto e accessori" dell'app ufficiale, con in piu'
// le versioni del software e gli errori. Tutto in sola lettura.
Page {
id: page
allowedOrientations: Orientation.All
function raw(key) {
return api.summary[key]
}
function temp(key) {
return api.formatValue(page.raw(key)) + " °C"
}
function flag(key) {
return page.raw(key) ? qsTr("yes") : qsTr("no")
}
function count(key) {
var v = page.raw(key)
if (v === undefined || v === null)
return "—"
return String(Math.round(Number(v)))
}
// Versioni e identificativo: possono mancare, quindi "—" invece di "null".
function testo(valore) {
if (valore === undefined || valore === null || valore === "")
return "—"
return String(valore)
}
// "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)
}
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("Settings")
onClicked: pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
}
}
Column {
id: col
width: parent.width
spacing: Theme.paddingSmall
PageHeader { title: qsTr("Plant") }
// ---- stato ------------------------------------------------------ //
SectionHeader { text: qsTr("Status") }
InfoRow {
label: qsTr("Connection")
value: page.testo(page.raw("connected_via"))
}
InfoRow {
label: qsTr("Mode")
value: api.modeLabel(page.raw("operating_status"))
}
InfoRow { label: qsTr("Away mode"); value: page.flag("away_mode") }
InfoRow { label: qsTr("Night mode"); value: page.flag("night_mode") }
InfoRow { label: qsTr("Forced heating"); value: page.flag("force_heating") }
InfoRow { label: qsTr("Inline heater"); value: page.flag("inline_heater") }
InfoRow { label: qsTr("Outdoor temperature"); value: page.temp("outdoor_temperature") }
InfoRow { label: qsTr("Indoor temperature (pump)"); value: page.temp("indoor_temperature") }
InfoRow { label: qsTr("Zones"); value: page.count("zones") }
InfoRow { label: qsTr("Last reading"); value: page.shortTime(page.raw("pump_updated")) }
// ---- accessori -------------------------------------------------- //
SectionHeader { text: qsTr("Accessories") }
Label {
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
visible: Number(page.count("thermostats_count")) === 0
text: qsTr("No wall thermostat")
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.Wrap
}
Column {
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
spacing: 0
visible: Number(page.count("thermostats_count")) > 0
Label {
text: qsTr("Aira wall thermostat")
color: Theme.primaryColor
font.pixelSize: Theme.fontSizeSmall
}
Label {
text: qsTr("Zone %1 · Connected").arg(page.count("thermostat_zone"))
color: Theme.highlightColor
font.pixelSize: Theme.fontSizeExtraSmall
}
}
InfoRow {
visible: Number(page.count("thermostats_count")) > 0
label: qsTr("Measured temperature")
value: page.temp("thermostat_temperature")
}
InfoRow {
visible: Number(page.count("thermostats_count")) > 0
label: qsTr("Humidity")
value: api.formatNumber(page.raw("thermostat_humidity"), 1) + " %"
}
InfoRow {
visible: Number(page.count("thermostats_count")) > 0
label: qsTr("Signal")
value: page.count("thermostat_rssi") + " dBm"
}
InfoRow {
visible: Number(page.count("thermostats_count")) > 0
label: qsTr("Battery")
value: page.raw("thermostat_battery_low") ? qsTr("low") : qsTr("ok")
}
InfoRow {
visible: Number(page.count("thermostats_count")) > 0
label: qsTr("Article")
value: page.testo(page.raw("thermostat_article"))
}
// ---- versioni --------------------------------------------------- //
SectionHeader { text: qsTr("Versions") }
InfoRow { label: qsTr("Device ID"); value: page.testo(page.raw("device_id")) }
InfoRow { label: qsTr("Platform"); value: page.testo(page.raw("version_platform")) }
InfoRow { label: qsTr("Connectivity"); value: page.testo(page.raw("version_connectivity")) }
InfoRow { label: qsTr("Climate control"); value: page.testo(page.raw("version_climate_control")) }
InfoRow { label: qsTr("Outdoor unit"); value: page.testo(page.raw("version_outdoor_unit")) }
InfoRow { label: qsTr("Outdoor unit EEPROM"); value: page.testo(page.raw("version_outdoor_eeprom")) }
// ---- errori ----------------------------------------------------- //
SectionHeader { text: qsTr("Errors") }
Label {
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
visible: page.count("errors_count") === "0"
text: qsTr("No errors reported by the plant.")
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.Wrap
}
InfoRow {
visible: page.count("errors_count") !== "0"
label: qsTr("Errors reported")
value: page.count("errors_count")
}
Item { width: 1; height: Theme.paddingMedium }
}
}
Component.onCompleted: {
if (api.configured)
api.refresh()
}
}