0.2.0: riscaldamento - obiettivo temperatura casa per zona, stato zone in copertina

This commit is contained in:
Carlo
2026-10-10 07:12:17 +02:00
parent d25da7a91f
commit 7ef9d1e999
10 changed files with 157 additions and 16 deletions
+3 -2
View File
@@ -1,7 +1,8 @@
# harbour-aira # harbour-aira
App nativa Sailfish OS per una pompa di calore **Aira Home**: mostra lo stato App nativa Sailfish OS per una pompa di calore **Aira Home**: mostra lo stato
dell'impianto e invia i comandi dell'acqua calda sanitaria. dell'impianto e invia i comandi dell'acqua calda sanitaria e del riscaldamento
(temperatura obiettivo della casa, per zona).
![architettura](docs/architettura.svg) ![architettura](docs/architettura.svg)
@@ -52,7 +53,7 @@ sfdk build # RPM in RPMS/
sfdk deploy # sul dispositivo collegato sfdk deploy # sul dispositivo collegato
``` ```
Sorgenti dal tarball: `tar xzf harbour-aira-0.1.0.tar.gz` e compilare in una Sorgenti dal tarball: `tar xzf harbour-aira-0.2.0.tar.gz` e compilare in una
cartella nuova (mai sopra una estrazione precedente: i Makefile restano stale). cartella nuova (mai sopra una estrazione precedente: i Makefile restano stale).
### Verifica locale, senza SDK ### Verifica locale, senza SDK
+26 -1
View File
@@ -16,7 +16,7 @@ Nessuna autenticazione: serve a distinguere "server irraggiungibile" da
"token sbagliato". "token sbagliato".
```json ```json
{"ok": true, "version": "0.1.0"} {"ok": true, "version": "0.2.0"}
``` ```
## GET /summary ## GET /summary
@@ -39,6 +39,14 @@ Valori pronti per la dashboard.
"inline_heater": false, "inline_heater": false,
"led_pattern": "LED_PATTERN_NORMAL", "led_pattern": "LED_PATTERN_NORMAL",
"zones": 1, "zones": 1,
"zone1_setpoint": 19.0,
"zone1_temperature": 23.0,
"zone1_mode": "PUMP_MODE_STATE_HEATING",
"zone1_cooling_setpoint": 22.0,
"zone2_setpoint": 0.0,
"zone2_temperature": 0.0,
"zone2_mode": "PUMP_MODE_STATE_IDLE",
"zone2_cooling_setpoint": 0.0,
"errors_count": 0, "errors_count": 0,
"pump_updated": "2026-10-09 18:52:17.199340" "pump_updated": "2026-10-09 18:52:17.199340"
} }
@@ -68,6 +76,8 @@ Risposta: `{"ok": true, "id": "...", "result": {...}}`.
|---|---|---| |---|---|---|
| `ping` | — | verifica che l'impianto risponda | | `ping` | — | verifica che l'impianto risponda |
| `set_hot_water_target` | 45–60 | temperatura ACS obiettivo | | `set_hot_water_target` | 45–60 | temperatura ACS obiettivo |
| `set_heating_setpoint` | 5–30 | temperatura obiettivo della casa (zona, default 1) |
| `set_cooling_setpoint` | 5–30 | temperatura obiettivo del raffrescamento (zona, default 1) |
| `hot_water_boost_on` / `hot_water_boost_off` | — | boost ACS una tantum | | `hot_water_boost_on` / `hot_water_boost_off` | — | boost ACS una tantum |
| `hot_water_heating_on` / `hot_water_heating_off` | — | riscaldamento ACS | | `hot_water_heating_on` / `hot_water_heating_off` | — | riscaldamento ACS |
| `away_mode_on` / `away_mode_off` | — | modalità assenza | | `away_mode_on` / `away_mode_off` | — | modalità assenza |
@@ -76,6 +86,21 @@ Risposta: `{"ok": true, "id": "...", "result": {...}}`.
| `heating_on` / `heating_off` | — | funzione riscaldamento | | `heating_on` / `heating_off` | — | funzione riscaldamento |
| `legionella_cycle` | — | ciclo antilegionella | | `legionella_cycle` | — | ciclo antilegionella |
### Riscaldamento: come si invia il comando per zona
Il messaggio `ZoneTemperatures` contiene **sempre entrambe** le zone:
```json
{"id": "set_heating_setpoint", "value": 21.5, "zone": 1}
```
`zone` è opzionale (default 1) e accetta 1 o 2. La zona che non si sta
modificando viene rimandata con il **suo valore attuale**, letto dall'ultimo
stato disponibile: inviarla a zero la spegnerebbe. Per questo il bridge tiene
in memoria l'ultima lettura, e il comando va costruito con quel contesto.
Il campo `kind` distingue riscaldamento (`1`) da raffrescamento (`2`).
### Perché una whitelist ### Perché una whitelist
La libreria espone ~90 comandi, fra cui `FactoryReset`, `InstallFirmware`, La libreria espone ~90 comandi, fra cui `FactoryReset`, `InstallFirmware`,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 78 KiB

+9
View File
@@ -42,6 +42,15 @@ CoverBackground {
font.pixelSize: Theme.fontSizeExtraSmall font.pixelSize: Theme.fontSizeExtraSmall
truncationMode: TruncationMode.Fade truncationMode: TruncationMode.Fade
} }
Label {
width: parent.width
horizontalAlignment: Text.AlignHCenter
text: qsTr("casa %1 °C · obiettivo %2 °C").arg(cover.t("zone1_temperature")).arg(cover.t("zone1_setpoint"))
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall
truncationMode: TruncationMode.Fade
}
} }
Label { Label {
+72 -2
View File
@@ -13,6 +13,10 @@ Page {
// non lo tocca, altrimenti lo slider "salta" a ogni lettura) // non lo tocca, altrimenti lo slider "salta" a ogni lettura)
property int targetAcs: 55 property int targetAcs: 55
property bool targetAcsTouched: false 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 string notice: ""
property bool noticeIsError: false property bool noticeIsError: false
@@ -73,6 +77,11 @@ Page {
if (v !== undefined && v !== null) if (v !== undefined && v !== null)
page.targetAcs = Math.round(Number(v)) 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
}
} }
} }
@@ -167,11 +176,72 @@ Page {
font.pixelSize: Theme.fontSizeExtraSmall font.pixelSize: Theme.fontSizeExtraSmall
} }
// ---- riscaldamento: temperatura della casa --------------------- //
SectionHeader { visible: api.configured; text: qsTr("Riscaldamento") }
Label {
visible: api.configured
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Temperatura in casa")
color: Theme.secondaryColor
font.pixelSize: Theme.fontSizeSmall
}
Label {
visible: api.configured
anchors.horizontalCenter: parent.horizontalCenter
text: page.temp("zone1_temperature") + " °C"
color: Theme.primaryColor
font.pixelSize: Theme.fontSizeLarge
}
InfoRow {
visible: api.configured
label: qsTr("Obiettivo casa")
value: page.temp("zone1_setpoint") + " °C"
}
InfoRow {
visible: api.configured
label: qsTr("Stato riscaldamento")
value: api.modeLabel(page.raw("zone1_mode"))
}
Slider {
id: casaSlider
visible: api.configured
width: parent.width
label: qsTr("Nuovo obiettivo")
minimumValue: 5
maximumValue: 30
stepSize: 0.5
value: page.targetCasa
valueText: value.toFixed(1) + " °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("Imposta casa a %1 °C").arg(page.targetCasa.toFixed(1))
onClicked: remorse.execute(setCasaButton,
qsTr("Temperatura della casa a %1 °C").arg(page.targetCasa.toFixed(1)),
function() { api.sendCommand("set_heating_setpoint", page.targetCasa) })
}
// ---- stato impianto ------------------------------------------- // // ---- stato impianto ------------------------------------------- //
SectionHeader { visible: api.configured; text: qsTr("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 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("Temperatura interna (pompa)"); value: page.temp("indoor_temperature") + " °C" }
InfoRow { visible: api.configured; label: qsTr("Modalità"); value: api.modeLabel(page.raw("operating_status")) } InfoRow { visible: api.configured; label: qsTr("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("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("Night mode"); value: page.flag("night_mode") }
@@ -182,7 +252,7 @@ Page {
InfoRow { visible: api.configured; label: qsTr("Aggiornato alle"); value: page.shortTime(page.raw("pump_updated")) } InfoRow { visible: api.configured; label: qsTr("Aggiornato alle"); value: page.shortTime(page.raw("pump_updated")) }
// ---- comandi --------------------------------------------------- // // ---- comandi --------------------------------------------------- //
SectionHeader { visible: api.configured; text: qsTr("Comandi") } SectionHeader { visible: api.configured; text: qsTr("Comandi acqua calda") }
Slider { Slider {
visible: api.configured visible: api.configured
+1 -1
View File
@@ -1,6 +1,6 @@
Name: harbour-aira Name: harbour-aira
Summary: Control your Aira heat pump through a local bridge Summary: Control your Aira heat pump through a local bridge
Version: 0.1.0 Version: 0.2.0
Release: 1 Release: 1
Group: Qt/Qt Group: Qt/Qt
License: MIT License: MIT
+26 -10
View File
@@ -87,8 +87,9 @@ def progress(d: ImageDraw.ImageDraw, y: int, frac: float) -> None:
d.rounded_rectangle([M, y, end, y + 3], radius=2, fill=ACCENT) d.rounded_rectangle([M, y, end, y + 3], radius=2, fill=ACCENT)
def slider(d: ImageDraw.ImageDraw, y: int, frac: float, value_text: str) -> int: def slider(d: ImageDraw.ImageDraw, y: int, frac: float, value_text: str,
d.text((M, y), "Obiettivo ACS", font=font(24), fill=SECONDARY) label: str = "Obiettivo ACS") -> int:
d.text((M, y), label, font=font(24), fill=SECONDARY)
vw = d.textlength(value_text, font=font(24)) vw = d.textlength(value_text, font=font(24))
d.text((W - M - vw, y), value_text, font=font(24), fill=PRIMARY) d.text((W - M - vw, y), value_text, font=font(24), fill=PRIMARY)
ty = y + 58 ty = y + 58
@@ -129,15 +130,27 @@ def main_screen() -> Image.Image:
t = "obiettivo 55,0 °C" t = "obiettivo 55,0 °C"
d.text(((W - d.textlength(t, font=font(24))) / 2, y), t, font=font(24), fill=SECONDARY) d.text(((W - d.textlength(t, font=font(24))) / 2, y), t, font=font(24), fill=SECONDARY)
y = section_header(d, y + 44, "Impianto") # ---- riscaldamento: temperatura della casa -------------------------- #
y = label_pair(d, y, "Esterna", "18,0 °C") y = section_header(d, y + 44, "Riscaldamento")
y = label_pair(d, y, "Interna", "22,5 °C") c = "Temperatura in casa"
y = label_pair(d, y, "Modalità", "Auto") d.text(((W - d.textlength(c, font=font(24))) / 2, y), c, font=font(24), fill=SECONDARY)
y = label_pair(d, y, "Zone", "1") y += 34
y = label_pair(d, y, "Errori", "0") big2 = "23,0 °C"
y = label_pair(d, y, "Aggiornato", "19:07") d.text(((W - d.textlength(big2, font=font(64, bold=True))) / 2, y),
big2, font=font(64, bold=True), fill=PRIMARY)
y += 88
y = label_pair(d, y, "Obiettivo casa", "19,0 °C")
y = label_pair(d, y, "Stato riscaldamento", "Riscaldamento")
y = slider(d, y + 14, (19.0 - 5) / (30 - 5), "19,0 °C", "Nuovo obiettivo")
y = button(d, y, "Imposta casa a 19,0 °C", primary=True)
y = section_header(d, y + 24, "Comandi") y = section_header(d, y + 24, "Impianto")
y = label_pair(d, y, "Esterna", "18,0 °C")
y = label_pair(d, y, "Interna (pompa)", "22,5 °C")
y = label_pair(d, y, "Modalità", "Automatico")
y = label_pair(d, y, "Errori", "0")
y = section_header(d, y + 24, "Comandi acqua calda")
y = slider(d, y, (55.0 - 45) / (60 - 45), "55 °C") y = slider(d, y, (55.0 - 45) / (60 - 45), "55 °C")
y = button(d, y, "Imposta obiettivo ACS", primary=True) y = button(d, y, "Imposta obiettivo ACS", primary=True)
y = button(d, y, "Avvia boost ACS") y = button(d, y, "Avvia boost ACS")
@@ -202,6 +215,9 @@ def cover_screen() -> Image.Image:
vw = d.textlength(val, font=font(24)) vw = d.textlength(val, font=font(24))
d.text((card[2] - 34 - vw, y), val, font=font(24), fill=PRIMARY) d.text((card[2] - 34 - vw, y), val, font=font(24), fill=PRIMARY)
y += 52 y += 52
vw = d.textlength("23,0 °C", font=font(24))
d.text((card[0] + 34, y), "casa", font=font(24), fill=SECONDARY)
d.text((card[2] - 34 - vw, y), "23,0 °C", font=font(24), fill=PRIMARY)
d.text((card[0] + 34, card[3] - 60), "acqua calda sanitaria", font=font(22), fill=SECONDARY) d.text((card[0] + 34, card[3] - 60), "acqua calda sanitaria", font=font(22), fill=SECONDARY)
return img return img
+14
View File
@@ -60,11 +60,25 @@ QString modeLabel(const QString &operatingStatus)
return QStringLiteral("Stand-by"); return QStringLiteral("Stand-by");
if (s == QLatin1String("OPERATING_STATUS_OFF")) if (s == QLatin1String("OPERATING_STATUS_OFF"))
return QStringLiteral("Spento"); return QStringLiteral("Spento");
// Stato della pompa per zona (state.zoneN_mode).
if (s == QLatin1String("PUMP_MODE_STATE_HEATING"))
return QStringLiteral("Riscaldamento");
if (s == QLatin1String("PUMP_MODE_STATE_COOLING"))
return QStringLiteral("Raffrescamento");
if (s == QLatin1String("PUMP_MODE_STATE_HOT_WATER"))
return QStringLiteral("Acqua calda");
if (s == QLatin1String("PUMP_MODE_STATE_IDLE"))
return QStringLiteral("In attesa");
if (s == QLatin1String("PUMP_MODE_STATE_STANDBY"))
return QStringLiteral("Stand-by");
if (s == QLatin1String("PUMP_MODE_STATE_OFF"))
return QStringLiteral("Spento");
if (s.isEmpty()) if (s.isEmpty())
return QStringLiteral("—"); return QStringLiteral("—");
// Sconosciuto: togliamo il prefisso e mostriamo il resto, minuscolo. // Sconosciuto: togliamo il prefisso e mostriamo il resto, minuscolo.
QString rest = s; QString rest = s;
rest.remove(QLatin1String("OPERATING_STATUS_")); rest.remove(QLatin1String("OPERATING_STATUS_"));
rest.remove(QLatin1String("PUMP_MODE_STATE_"));
return rest.toLower(); return rest.toLower();
} }
+6
View File
@@ -62,6 +62,12 @@ int main(int argc, char *argv[])
check(AiraJson::modeLabel("OPERATING_STATUS_AUTO") == "Automatico", "AUTO -> Automatico"); check(AiraJson::modeLabel("OPERATING_STATUS_AUTO") == "Automatico", "AUTO -> Automatico");
check(AiraJson::modeLabel("OPERATING_STATUS_HOT_WATER") == "Acqua calda", "acqua calda"); check(AiraJson::modeLabel("OPERATING_STATUS_HOT_WATER") == "Acqua calda", "acqua calda");
check(AiraJson::modeLabel("") == "—", "stato vuoto -> trattino"); check(AiraJson::modeLabel("") == "—", "stato vuoto -> trattino");
check(AiraJson::modeLabel("PUMP_MODE_STATE_HEATING") == "Riscaldamento",
"stato zona: riscaldamento");
check(AiraJson::modeLabel("PUMP_MODE_STATE_IDLE") == "In attesa",
"stato zona: in attesa");
check(AiraJson::modeLabel("PUMP_MODE_STATE_IGNOTO") == "ignoto",
"stato zona sconosciuto -> senza prefisso");
check(AiraJson::modeLabel("OPERATING_STATUS_DEFROST") == "defrost", check(AiraJson::modeLabel("OPERATING_STATUS_DEFROST") == "defrost",
"stato sconosciuto: prefisso tolto, minuscolo"); "stato sconosciuto: prefisso tolto, minuscolo");