harbour-aira 0.1.0: client Sailfish per pompa di calore Aira (bridge HTTP + stato e comandi ACS)

This commit is contained in:
Carlo
2026-10-09 21:00:59 +02:00
commit 15963039ea
27 changed files with 1686 additions and 0 deletions
+268
View File
@@ -0,0 +1,268 @@
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.
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
property string notice: ""
property bool noticeIsError: false
function openSettings() {
pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
}
function showNotice(msg, isError) {
page.notice = msg
page.noticeIsError = isError === true
noticeTimer.restart()
}
function raw(key) {
return api.summary[key]
}
function temp(key) {
return api.formatValue(page.raw(key))
}
function flag(key) {
return page.raw(key) ? qsTr("sì") : qsTr("no")
}
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)
}
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))
}
}
}
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("Aggiorna")
onClicked: api.refresh()
}
MenuItem {
text: qsTr("Impostazioni")
onClicked: page.openSettings()
}
}
Column {
id: col
width: parent.width
spacing: Theme.paddingSmall
PageHeader { title: qsTr("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 non configurato. Apri le impostazioni e inserisci l'indirizzo del server e il token.")
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.Wrap
}
Button {
visible: !api.configured
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Impostazioni")
onClicked: page.openSettings()
}
// ---- acqua calda ---------------------------------------------- //
Label {
visible: api.configured
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Acqua calda sanitaria")
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeSmall
}
Label {
visible: api.configured
anchors.horizontalCenter: parent.horizontalCenter
text: page.temp("dhw_current") + " °C"
color: Theme.primaryColor
font.pixelSize: Theme.fontSizeHuge
}
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)
}
}
Label {
visible: api.configured
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("obiettivo %1 °C").arg(page.temp("dhw_target"))
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall
}
// ---- stato impianto ------------------------------------------- //
SectionHeader { visible: api.configured; text: qsTr("Impianto") }
InfoRow { visible: api.configured; label: qsTr("Temperatura esterna"); value: page.temp("outdoor_temperature") + " °C" }
InfoRow { visible: api.configured; label: qsTr("Temperatura interna"); value: page.temp("indoor_temperature") + " °C" }
InfoRow { visible: api.configured; label: qsTr("Modalità"); value: api.modeLabel(page.raw("operating_status")) }
InfoRow { visible: api.configured; label: qsTr("Assenza"); value: page.flag("away_mode") }
InfoRow { visible: api.configured; label: qsTr("Night mode"); value: page.flag("night_mode") }
InfoRow { visible: api.configured; label: qsTr("Riscaldamento forzato"); value: page.flag("force_heating") }
InfoRow { visible: api.configured; label: qsTr("Resistenze integrative"); value: page.flag("inline_heater") }
InfoRow { visible: api.configured; label: qsTr("Zone"); value: page.count("zones") }
InfoRow { visible: api.configured; label: qsTr("Errori"); value: page.count("errors_count") }
InfoRow { visible: api.configured; label: qsTr("Aggiornato alle"); value: page.shortTime(page.raw("pump_updated")) }
// ---- comandi --------------------------------------------------- //
SectionHeader { visible: api.configured; text: qsTr("Comandi") }
Slider {
visible: api.configured
width: parent.width
label: qsTr("Obiettivo acqua calda")
minimumValue: 45
maximumValue: 60
stepSize: 1
value: page.targetAcs
valueText: 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("Imposta obiettivo %1 °C").arg(page.targetAcs)
onClicked: remorse.execute(setAcsButton,
qsTr("Obiettivo ACS a %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("Avvia boost ACS")
onClicked: remorse.execute(boostOnButton, qsTr("Avvio del boost ACS"),
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("Ferma boost ACS")
onClicked: remorse.execute(boostOffButton, qsTr("Arresto del boost ACS"),
function() { api.sendCommand("hot_water_boost_off") })
}
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("I comandi vengono inviati al bridge e da lì all'impianto: possono richiedere qualche secondo.")
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall
wrapMode: Text.Wrap
}
}
}
Component.onCompleted: {
if (api.configured)
api.refresh()
}
}
+116
View File
@@ -0,0 +1,116 @@
import QtQuick 2.2
import Sailfish.Silica 1.0
// Indirizzo e token del bridge. Si salvano all'uscita dalla pagina, alla
// pressione di Invio e col pulsante Salva: NON a ogni tasto (QSettings.sync()
// su ogni carattere consuma scritture senza motivo).
Page {
id: page
allowedOrientations: Orientation.All
property string urlText: appSettings.baseUrl
property string tokenText: appSettings.token
property string testResult: ""
property bool testOk: false
function save() {
appSettings.baseUrl = urlText.trim()
appSettings.token = tokenText.trim()
}
function testAndReport() {
save()
page.testResult = qsTr("verifico…")
api.testConnection()
}
onStatusChanged: {
if (status === PageStatus.Inactive)
save()
}
Connections {
target: api
onConnectionTested: {
page.testOk = ok
page.testResult = message
}
}
SilicaFlickable {
anchors.fill: parent
contentHeight: col.height + Theme.paddingLarge
Column {
id: col
width: parent.width
spacing: Theme.paddingMedium
PageHeader { title: qsTr("Impostazioni") }
TextField {
id: urlField
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
label: qsTr("Indirizzo del bridge")
placeholderText: "192.168.2.50:8790"
text: page.urlText
onTextChanged: if (text !== page.urlText) page.urlText = text
EnterKey.enabled: true
EnterKey.iconSource: "image://theme/icon-m-enter-accept"
EnterKey.onClicked: page.save()
}
// PasswordField: mette l'icona nativa mostra/nascondi, niente
// TextField con echoMode gestito a mano.
PasswordField {
id: tokenField
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
label: qsTr("Token del bridge")
showEchoModeToggle: true
text: page.tokenText
onTextChanged: if (text !== page.tokenText) page.tokenText = text
EnterKey.enabled: true
EnterKey.iconSource: "image://theme/icon-m-enter-accept"
EnterKey.onClicked: page.save()
}
Label {
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
text: qsTr("Indirizzo e token si leggono sul server: il token è in ~/.hermes/profiles/carlo/aira_bridge.json. Senza schema viene usato http://.")
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall
wrapMode: Text.Wrap
}
Button {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Salva")
onClicked: {
page.save()
page.testResult = qsTr("impostazioni salvate")
page.testOk = true
}
}
Button {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Prova connessione")
onClicked: page.testAndReport()
}
Label {
visible: page.testResult.length > 0
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
text: page.testResult
color: page.testOk ? Theme.highlightColor : Theme.errorColor
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.Wrap
}
}
}
}