0.3.0: pagina Altri comandi (assenza, night mode, forzato, legionella, ACS)
This commit is contained in:
@@ -76,9 +76,11 @@ src/apiclient.{h,cpp} QNetworkAccessManager + watchdog di timeout
|
|||||||
src/settings.{h,cpp} QSettings su AppConfigLocation (sandbox SailJail)
|
src/settings.{h,cpp} QSettings su AppConfigLocation (sandbox SailJail)
|
||||||
src/main.cpp context properties: appSettings, api
|
src/main.cpp context properties: appSettings, api
|
||||||
qml/harbour-aira.qml ApplicationWindow + copertina
|
qml/harbour-aira.qml ApplicationWindow + copertina
|
||||||
qml/pages/MainPage.qml dashboard e comandi
|
qml/pages/MainPage.qml dashboard e comandi acqua/riscaldamento
|
||||||
|
qml/pages/CommandsPage.qml gli altri comandi, con lo stato attuale in cima
|
||||||
qml/pages/SettingsPage.qml
|
qml/pages/SettingsPage.qml
|
||||||
qml/components/InfoRow.qml
|
qml/components/InfoRow.qml
|
||||||
|
qml/components/ActionButton.qml pulsante di comando, con conferma opzionale
|
||||||
tests/core_test.cpp verifiche del nucleo (headless)
|
tests/core_test.cpp verifiche del nucleo (headless)
|
||||||
docs/PIANO.md piano di progetto e scelte
|
docs/PIANO.md piano di progetto e scelte
|
||||||
docs/PROTOCOL.md API del bridge
|
docs/PROTOCOL.md API del bridge
|
||||||
|
|||||||
@@ -100,6 +100,14 @@ Non verificabile qui (serve il suo PC + il telefono): build RPM, istallazione, U
|
|||||||
- **App 0.2.0**: sezione "Riscaldamento" nella dashboard (temperatura in casa, obiettivo,
|
- **App 0.2.0**: sezione "Riscaldamento" nella dashboard (temperatura in casa, obiettivo,
|
||||||
stato zona, slider 5–30 °C a passi di 0,5 con conferma) e copertina con la temperatura di
|
stato zona, slider 5–30 °C a passi di 0,5 con conferma) e copertina con la temperatura di
|
||||||
casa. Aggiunte le etichette per gli stati `PUMP_MODE_STATE_*`. Nucleo: **29/29**.
|
casa. Aggiunte le etichette per gli stati `PUMP_MODE_STATE_*`. Nucleo: **29/29**.
|
||||||
|
- **Bridge 0.3.0 + App 0.3.0**: pagina **"Altri comandi"** nell'app (dalla tendina in
|
||||||
|
alto o dal pulsante in fondo alla home) con assenza, night mode un'ora, riscaldamento
|
||||||
|
forzato on/off, funzione riscaldamento on/off, ciclo antilegionella, riscaldamento ACS
|
||||||
|
on/off e verifica impianto; i comandi che durano (forzato, legionella, disabilitazioni)
|
||||||
|
chiedono conferma. In cima alla pagina lo **stato attuale**, così non si preme alla
|
||||||
|
cieca. **Bug corretto nello stesso giro**: `force_heating` nello stato è un oggetto
|
||||||
|
`{enabled, remaining_time}`, e il bridge lo leggeva con `bool()` — sempre vero. Ora si
|
||||||
|
legge il campo `enabled`; il test lo copre (27/27). Nucleo app: **31/31**.
|
||||||
- **App 0.2.1** (richiesta dell'utente): la temperatura della casa diventa il dato
|
- **App 0.2.1** (richiesta dell'utente): la temperatura della casa diventa il dato
|
||||||
principale **in cima** alla home (valore grande, barra, obiettivo, slider e pulsante),
|
principale **in cima** alla home (valore grande, barra, obiettivo, slider e pulsante),
|
||||||
l'acqua calda sanitaria scende in una propria sezione sotto, lo stato impianto va in
|
l'acqua calda sanitaria scende in una propria sezione sotto, lo stato impianto va in
|
||||||
|
|||||||
+7
-1
@@ -35,8 +35,14 @@ Valori pronti per la dashboard.
|
|||||||
"away_mode": false,
|
"away_mode": false,
|
||||||
"night_mode": false,
|
"night_mode": false,
|
||||||
"manual_mode": false,
|
"manual_mode": false,
|
||||||
"force_heating": true,
|
"force_heating": false,
|
||||||
|
"force_heating_remaining": "0s",
|
||||||
"inline_heater": false,
|
"inline_heater": false,
|
||||||
|
"hot_water_heating": true,
|
||||||
|
"configured_modes": "PUMP_MODE_STATE_HEATING",
|
||||||
|
"pump_active_state": "PUMP_ACTIVE_STATE_IDLE",
|
||||||
|
"outdoor_defrost": false,
|
||||||
|
"signature_lights": true,
|
||||||
"led_pattern": "LED_PATTERN_NORMAL",
|
"led_pattern": "LED_PATTERN_NORMAL",
|
||||||
"zones": 1,
|
"zones": 1,
|
||||||
"zone1_setpoint": 19.0,
|
"zone1_setpoint": 19.0,
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -0,0 +1,37 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import Sailfish.Silica 1.0
|
||||||
|
|
||||||
|
// Pulsante a tutta larghezza per un comando del bridge.
|
||||||
|
// Se `confirmText` e' valorizzato, l'azione parte solo dopo la conferma
|
||||||
|
// (RemorseItem): la usano i comandi che cambiano lo stato in modo duraturo.
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
width: parent ? parent.width : 0
|
||||||
|
height: btn.height
|
||||||
|
|
||||||
|
property string text: ""
|
||||||
|
property string confirmText: ""
|
||||||
|
signal triggered()
|
||||||
|
|
||||||
|
Button {
|
||||||
|
id: btn
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
leftMargin: Theme.horizontalPageMargin
|
||||||
|
rightMargin: Theme.horizontalPageMargin
|
||||||
|
}
|
||||||
|
text: root.text
|
||||||
|
onClicked: {
|
||||||
|
if (root.confirmText.length > 0) {
|
||||||
|
remorse.execute(btn, root.confirmText, function() { root.triggered() })
|
||||||
|
} else {
|
||||||
|
root.triggered()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fuori dal Button: dentro il pulsante i figli finiscono nel contentItem.
|
||||||
|
RemorseItem { id: remorse }
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import QtQuick 2.2
|
||||||
|
import Sailfish.Silica 1.0
|
||||||
|
import "../components"
|
||||||
|
|
||||||
|
// Comandi secondari: quelli che non stanno nella home. In cima lo stato
|
||||||
|
// attuale, cosi' non si preme alla cieca.
|
||||||
|
Page {
|
||||||
|
id: page
|
||||||
|
|
||||||
|
allowedOrientations: Orientation.All
|
||||||
|
|
||||||
|
property string notice: ""
|
||||||
|
property bool noticeIsError: false
|
||||||
|
|
||||||
|
function raw(key) {
|
||||||
|
return api.summary[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
function flag(key) {
|
||||||
|
return page.raw(key) ? qsTr("sì") : qsTr("no")
|
||||||
|
}
|
||||||
|
|
||||||
|
function stato(key) {
|
||||||
|
return api.modeLabel(page.raw(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotice(msg, isError) {
|
||||||
|
page.notice = msg
|
||||||
|
page.noticeIsError = isError === true
|
||||||
|
noticeTimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: noticeTimer
|
||||||
|
interval: 4000
|
||||||
|
onTriggered: page.notice = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: api
|
||||||
|
onErrorOccurred: page.showNotice(message, true)
|
||||||
|
onCommandFinished: page.showNotice(message, !ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
SilicaFlickable {
|
||||||
|
anchors.fill: parent
|
||||||
|
contentHeight: col.height + Theme.paddingLarge
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: col
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.paddingSmall
|
||||||
|
|
||||||
|
PageHeader { title: qsTr("Altri comandi") }
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- stato attuale -------------------------------------------- //
|
||||||
|
SectionHeader { text: qsTr("Stato attuale") }
|
||||||
|
|
||||||
|
InfoRow { label: qsTr("Assenza"); value: page.flag("away_mode") }
|
||||||
|
InfoRow { label: qsTr("Night mode"); value: page.flag("night_mode") }
|
||||||
|
InfoRow { label: qsTr("Riscaldamento forzato"); value: page.flag("force_heating") }
|
||||||
|
InfoRow { label: qsTr("Acqua calda attiva"); value: page.flag("hot_water_heating") }
|
||||||
|
InfoRow { label: qsTr("Pompa"); value: page.stato("pump_active_state") }
|
||||||
|
InfoRow { label: qsTr("Modalità consentite"); value: page.stato("configured_modes") }
|
||||||
|
|
||||||
|
// ---- assenza --------------------------------------------------- //
|
||||||
|
SectionHeader { text: qsTr("Assenza") }
|
||||||
|
|
||||||
|
ActionButton {
|
||||||
|
text: qsTr("Attiva assenza")
|
||||||
|
onTriggered: api.sendCommand("away_mode_on")
|
||||||
|
}
|
||||||
|
ActionButton {
|
||||||
|
text: qsTr("Disattiva assenza")
|
||||||
|
onTriggered: api.sendCommand("away_mode_off")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- rumore ---------------------------------------------------- //
|
||||||
|
SectionHeader { text: qsTr("Rumore") }
|
||||||
|
|
||||||
|
ActionButton {
|
||||||
|
text: qsTr("Night mode per un'ora")
|
||||||
|
onTriggered: api.sendCommand("night_mode_1h")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- riscaldamento --------------------------------------------- //
|
||||||
|
SectionHeader { text: qsTr("Riscaldamento") }
|
||||||
|
|
||||||
|
ActionButton {
|
||||||
|
text: qsTr("Forza riscaldamento")
|
||||||
|
confirmText: qsTr("Avvio del riscaldamento forzato")
|
||||||
|
onTriggered: api.sendCommand("force_heating_on")
|
||||||
|
}
|
||||||
|
ActionButton {
|
||||||
|
text: qsTr("Ferma riscaldamento forzato")
|
||||||
|
onTriggered: api.sendCommand("force_heating_off")
|
||||||
|
}
|
||||||
|
ActionButton {
|
||||||
|
text: qsTr("Abilita funzione riscaldamento")
|
||||||
|
onTriggered: api.sendCommand("heating_on")
|
||||||
|
}
|
||||||
|
ActionButton {
|
||||||
|
text: qsTr("Disabilita funzione riscaldamento")
|
||||||
|
confirmText: qsTr("Il riscaldamento verrà disabilitato")
|
||||||
|
onTriggered: api.sendCommand("heating_off")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- acqua calda ----------------------------------------------- //
|
||||||
|
SectionHeader { text: qsTr("Acqua calda sanitaria") }
|
||||||
|
|
||||||
|
ActionButton {
|
||||||
|
text: qsTr("Ciclo antilegionella")
|
||||||
|
confirmText: qsTr("Avvio del ciclo antilegionella: richiede tempo")
|
||||||
|
onTriggered: api.sendCommand("legionella_cycle")
|
||||||
|
}
|
||||||
|
ActionButton {
|
||||||
|
text: qsTr("Abilita riscaldamento ACS")
|
||||||
|
onTriggered: api.sendCommand("hot_water_heating_on")
|
||||||
|
}
|
||||||
|
ActionButton {
|
||||||
|
text: qsTr("Disabilita riscaldamento ACS")
|
||||||
|
confirmText: qsTr("L'acqua calda smetterà di essere riscaldata")
|
||||||
|
onTriggered: api.sendCommand("hot_water_heating_off")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- diagnostica ----------------------------------------------- //
|
||||||
|
SectionHeader { text: qsTr("Diagnostica") }
|
||||||
|
|
||||||
|
ActionButton {
|
||||||
|
text: qsTr("Verifica impianto")
|
||||||
|
onTriggered: api.sendCommand("ping")
|
||||||
|
}
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
visible: api.busy
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
size: BusyIndicatorSize.Small
|
||||||
|
running: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
x: Theme.horizontalPageMargin
|
||||||
|
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||||
|
text: qsTr("I comandi passano dal bridge e possono richiedere qualche secondo. Dopo l'invio lo stato si aggiorna da solo.")
|
||||||
|
color: Theme.secondaryColor
|
||||||
|
font.pixelSize: Theme.fontSizeExtraSmall
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (api.configured)
|
||||||
|
api.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,10 @@ Page {
|
|||||||
pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
|
pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openCommands() {
|
||||||
|
pageStack.push(Qt.resolvedUrl("CommandsPage.qml"))
|
||||||
|
}
|
||||||
|
|
||||||
function showNotice(msg, isError) {
|
function showNotice(msg, isError) {
|
||||||
page.notice = msg
|
page.notice = msg
|
||||||
page.noticeIsError = isError === true
|
page.noticeIsError = isError === true
|
||||||
@@ -101,6 +105,10 @@ Page {
|
|||||||
text: qsTr("Impostazioni")
|
text: qsTr("Impostazioni")
|
||||||
onClicked: page.openSettings()
|
onClicked: page.openSettings()
|
||||||
}
|
}
|
||||||
|
MenuItem {
|
||||||
|
text: qsTr("Altri comandi")
|
||||||
|
onClicked: page.openCommands()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -308,6 +316,18 @@ Page {
|
|||||||
InfoRow { visible: api.configured; label: qsTr("Errori"); value: page.count("errors_count") }
|
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")) }
|
InfoRow { visible: api.configured; label: qsTr("Aggiornato alle"); value: page.shortTime(page.raw("pump_updated")) }
|
||||||
|
|
||||||
|
Button {
|
||||||
|
visible: api.configured
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
leftMargin: Theme.horizontalPageMargin
|
||||||
|
rightMargin: Theme.horizontalPageMargin
|
||||||
|
}
|
||||||
|
text: qsTr("Altri comandi")
|
||||||
|
onClicked: page.openCommands()
|
||||||
|
}
|
||||||
|
|
||||||
BusyIndicator {
|
BusyIndicator {
|
||||||
visible: api.busy
|
visible: api.busy
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|||||||
@@ -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.2.1
|
Version: 0.3.0
|
||||||
Release: 1
|
Release: 1
|
||||||
Group: Qt/Qt
|
Group: Qt/Qt
|
||||||
License: MIT
|
License: MIT
|
||||||
|
|||||||
@@ -216,10 +216,36 @@ def cover_screen() -> Image.Image:
|
|||||||
return img
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def commands_screen() -> Image.Image:
|
||||||
|
img = background()
|
||||||
|
d = ImageDraw.Draw(img)
|
||||||
|
status_bar(d)
|
||||||
|
page_header(d, "Altri comandi")
|
||||||
|
|
||||||
|
y = section_header(d, 156, "Stato attuale")
|
||||||
|
for lab, val in (("Assenza", "no"), ("Night mode", "no"),
|
||||||
|
("Riscaldamento forzato", "no"), ("Acqua calda attiva", "sì"),
|
||||||
|
("Pompa", "Ferma"), ("Modalità consentite", "Riscaldamento")):
|
||||||
|
y = label_pair(d, y, lab, val)
|
||||||
|
|
||||||
|
y = section_header(d, y + 24, "Assenza")
|
||||||
|
y = button(d, y, "Attiva assenza")
|
||||||
|
y = button(d, y, "Disattiva assenza")
|
||||||
|
|
||||||
|
y = section_header(d, y + 24, "Rumore")
|
||||||
|
y = button(d, y, "Night mode per un'ora")
|
||||||
|
|
||||||
|
y = section_header(d, y + 24, "Riscaldamento")
|
||||||
|
y = button(d, y, "Forza riscaldamento", primary=True)
|
||||||
|
d.text(((W - d.textlength("(chiede conferma prima di partire)", font=font(22))) / 2, y - 4),
|
||||||
|
"(chiede conferma prima di partire)", font=font(22), fill=SECONDARY)
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
OUT.mkdir(parents=True, exist_ok=True)
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
for name, fn in (("dashboard", main_screen), ("impostazioni", settings_screen),
|
for name, fn in (("dashboard", main_screen), ("comandi", commands_screen),
|
||||||
("copertina", cover_screen)):
|
("impostazioni", settings_screen), ("copertina", cover_screen)):
|
||||||
path = OUT / f"{name}.png"
|
path = OUT / f"{name}.png"
|
||||||
fn().save(path)
|
fn().save(path)
|
||||||
print(f"scritto {path} ({path.stat().st_size} byte)")
|
print(f"scritto {path} ({path.stat().st_size} byte)")
|
||||||
|
|||||||
@@ -73,12 +73,19 @@ QString modeLabel(const QString &operatingStatus)
|
|||||||
return QStringLiteral("Stand-by");
|
return QStringLiteral("Stand-by");
|
||||||
if (s == QLatin1String("PUMP_MODE_STATE_OFF"))
|
if (s == QLatin1String("PUMP_MODE_STATE_OFF"))
|
||||||
return QStringLiteral("Spento");
|
return QStringLiteral("Spento");
|
||||||
|
// Stato operativo della pompa (state.pump_active_state).
|
||||||
|
if (s == QLatin1String("PUMP_ACTIVE_STATE_IDLE"))
|
||||||
|
return QStringLiteral("Ferma");
|
||||||
|
if (s == QLatin1String("PUMP_ACTIVE_STATE_RUNNING"))
|
||||||
|
return QStringLiteral("In funzione");
|
||||||
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_"));
|
rest.remove(QLatin1String("PUMP_MODE_STATE_"));
|
||||||
|
rest.remove(QLatin1String("PUMP_ACTIVE_STATE_"));
|
||||||
|
rest.remove(QLatin1String("POWER_PREFERENCE_"));
|
||||||
return rest.toLower();
|
return rest.toLower();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ int main(int argc, char *argv[])
|
|||||||
"stato zona: in attesa");
|
"stato zona: in attesa");
|
||||||
check(AiraJson::modeLabel("PUMP_MODE_STATE_IGNOTO") == "ignoto",
|
check(AiraJson::modeLabel("PUMP_MODE_STATE_IGNOTO") == "ignoto",
|
||||||
"stato zona sconosciuto -> senza prefisso");
|
"stato zona sconosciuto -> senza prefisso");
|
||||||
|
check(AiraJson::modeLabel("PUMP_ACTIVE_STATE_IDLE") == "Ferma",
|
||||||
|
"pompa ferma");
|
||||||
|
check(AiraJson::modeLabel("PUMP_ACTIVE_STATE_RUNNING") == "In funzione",
|
||||||
|
"pompa in funzione");
|
||||||
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