0.2.0: riscaldamento - obiettivo temperatura casa per zona, stato zone in copertina
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
# harbour-aira
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
### Verifica locale, senza SDK
|
||||
|
||||
+26
-1
@@ -16,7 +16,7 @@ Nessuna autenticazione: serve a distinguere "server irraggiungibile" da
|
||||
"token sbagliato".
|
||||
|
||||
```json
|
||||
{"ok": true, "version": "0.1.0"}
|
||||
{"ok": true, "version": "0.2.0"}
|
||||
```
|
||||
|
||||
## GET /summary
|
||||
@@ -39,6 +39,14 @@ Valori pronti per la dashboard.
|
||||
"inline_heater": false,
|
||||
"led_pattern": "LED_PATTERN_NORMAL",
|
||||
"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,
|
||||
"pump_updated": "2026-10-09 18:52:17.199340"
|
||||
}
|
||||
@@ -68,6 +76,8 @@ Risposta: `{"ok": true, "id": "...", "result": {...}}`.
|
||||
|---|---|---|
|
||||
| `ping` | — | verifica che l'impianto risponda |
|
||||
| `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_heating_on` / `hot_water_heating_off` | — | riscaldamento ACS |
|
||||
| `away_mode_on` / `away_mode_off` | — | modalità assenza |
|
||||
@@ -76,6 +86,21 @@ Risposta: `{"ok": true, "id": "...", "result": {...}}`.
|
||||
| `heating_on` / `heating_off` | — | funzione riscaldamento |
|
||||
| `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
|
||||
|
||||
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
|
||||
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 {
|
||||
|
||||
+72
-2
@@ -13,6 +13,10 @@ Page {
|
||||
// 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
|
||||
|
||||
@@ -73,6 +77,11 @@ Page {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,11 +176,72 @@ Page {
|
||||
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 ------------------------------------------- //
|
||||
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("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("Assenza"); value: page.flag("away_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")) }
|
||||
|
||||
// ---- comandi --------------------------------------------------- //
|
||||
SectionHeader { visible: api.configured; text: qsTr("Comandi") }
|
||||
SectionHeader { visible: api.configured; text: qsTr("Comandi acqua calda") }
|
||||
|
||||
Slider {
|
||||
visible: api.configured
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Name: harbour-aira
|
||||
Summary: Control your Aira heat pump through a local bridge
|
||||
Version: 0.1.0
|
||||
Version: 0.2.0
|
||||
Release: 1
|
||||
Group: Qt/Qt
|
||||
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)
|
||||
|
||||
|
||||
def slider(d: ImageDraw.ImageDraw, y: int, frac: float, value_text: str) -> int:
|
||||
d.text((M, y), "Obiettivo ACS", font=font(24), fill=SECONDARY)
|
||||
def slider(d: ImageDraw.ImageDraw, y: int, frac: float, value_text: str,
|
||||
label: str = "Obiettivo ACS") -> int:
|
||||
d.text((M, y), label, font=font(24), fill=SECONDARY)
|
||||
vw = d.textlength(value_text, font=font(24))
|
||||
d.text((W - M - vw, y), value_text, font=font(24), fill=PRIMARY)
|
||||
ty = y + 58
|
||||
@@ -129,15 +130,27 @@ def main_screen() -> Image.Image:
|
||||
t = "obiettivo 55,0 °C"
|
||||
d.text(((W - d.textlength(t, font=font(24))) / 2, y), t, font=font(24), fill=SECONDARY)
|
||||
|
||||
y = section_header(d, y + 44, "Impianto")
|
||||
y = label_pair(d, y, "Esterna", "18,0 °C")
|
||||
y = label_pair(d, y, "Interna", "22,5 °C")
|
||||
y = label_pair(d, y, "Modalità", "Auto")
|
||||
y = label_pair(d, y, "Zone", "1")
|
||||
y = label_pair(d, y, "Errori", "0")
|
||||
y = label_pair(d, y, "Aggiornato", "19:07")
|
||||
# ---- riscaldamento: temperatura della casa -------------------------- #
|
||||
y = section_header(d, y + 44, "Riscaldamento")
|
||||
c = "Temperatura in casa"
|
||||
d.text(((W - d.textlength(c, font=font(24))) / 2, y), c, font=font(24), fill=SECONDARY)
|
||||
y += 34
|
||||
big2 = "23,0 °C"
|
||||
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 = button(d, y, "Imposta obiettivo ACS", primary=True)
|
||||
y = button(d, y, "Avvia boost ACS")
|
||||
@@ -202,6 +215,9 @@ def cover_screen() -> Image.Image:
|
||||
vw = d.textlength(val, font=font(24))
|
||||
d.text((card[2] - 34 - vw, y), val, font=font(24), fill=PRIMARY)
|
||||
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)
|
||||
return img
|
||||
|
||||
|
||||
@@ -60,11 +60,25 @@ QString modeLabel(const QString &operatingStatus)
|
||||
return QStringLiteral("Stand-by");
|
||||
if (s == QLatin1String("OPERATING_STATUS_OFF"))
|
||||
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())
|
||||
return QStringLiteral("—");
|
||||
// Sconosciuto: togliamo il prefisso e mostriamo il resto, minuscolo.
|
||||
QString rest = s;
|
||||
rest.remove(QLatin1String("OPERATING_STATUS_"));
|
||||
rest.remove(QLatin1String("PUMP_MODE_STATE_"));
|
||||
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_HOT_WATER") == "Acqua calda", "acqua calda");
|
||||
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",
|
||||
"stato sconosciuto: prefisso tolto, minuscolo");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user