import QtQuick 2.2 import Sailfish.Silica 1.0 import "../components" // 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 allowedOrientations: Orientation.All // obiettivo ACS scelto dall'utente (non sovrascritto dal refresh finche' // non lo tocca, altrimenti lo slider "salta" a ogni lettura) property int targetAcs: 55 property bool targetAcsTouched: false // obiettivo della casa (zona 1); parte dalla temperatura attuale e non // viene sovrascritto dal refresh finche' l'utente non lo tocca property real targetCasa: 20 property bool targetCasaTouched: false property string notice: "" property bool noticeIsError: false function openSettings() { pageStack.push(Qt.resolvedUrl("SettingsPage.qml")) } function openCommands() { 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 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 = "" } 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 { id: flick anchors.fill: parent contentHeight: col.height + Theme.paddingLarge // I menu pulley stanno DENTRO la vista: come figli diretti della Page // fanno deref su null (TypeError + crash). PullDownMenu { MenuItem { text: qsTr("Refresh") onClicked: api.refresh() } MenuItem { 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 { id: col width: parent.width spacing: Theme.paddingSmall PageHeader { title: "Aira" } // ---- avvisi ---------------------------------------------------- // 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 ------------------------------------ // Label { visible: !api.configured x: Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin 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 } Button { visible: !api.configured anchors.horizontalCenter: parent.horizontalCenter text: qsTr("Settings") onClicked: page.openSettings() } // ---- barra in alto: fuori casa / a casa ------------------------ // Item { visible: api.configured x: Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin height: fuori.height Label { id: fuori anchors.left: parent.left text: qsTr("Outside %1 °C").arg(page.temp("outdoor_temperature")) color: Theme.secondaryColor font.pixelSize: Theme.fontSizeSmall } 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("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: { if (api.configured) api.refresh() } }