0.2.0: riscaldamento - obiettivo temperatura casa per zona, stato zone in copertina
This commit is contained in:
@@ -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).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -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
@@ -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 |
@@ -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
@@ -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,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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user