0.6.6: dalla Home si aprono le regolazioni dell'oggetto toccato, non di tutto l'impianto

This commit is contained in:
2026-10-11 09:59:25 +02:00
parent a788255593
commit afb2878a8c
8 changed files with 167 additions and 23 deletions
+19 -5
View File
@@ -158,6 +158,14 @@ python3 tests/ui_contract_check.py # bridge su 127.0.0.1:8790
python3 tests/ui_contract_check.py http://192.168.3.21:8790 python3 tests/ui_contract_check.py http://192.168.3.21:8790
``` ```
Altri due controlli guardano i QML stessi, visto che qui non se ne può eseguire
nessuno. Impiegano millisecondi e dicono il file di cui non sono contenti:
```sh
python3 tests/qml_check_bindings.py qml # nessuna proprietà assegnata due volte
python3 tests/qml_focus_check.py # un tocco apre solo quello che ha toccato
```
## Stato ## Stato
- **85/85** verifiche del nucleo, compilazione senza warning (comprese le frasi - **85/85** verifiche del nucleo, compilazione senza warning (comprese le frasi
@@ -171,8 +179,11 @@ python3 tests/ui_contract_check.py http://192.168.3.21:8790
all'inglese all'inglese
- numeri, importi e nomi dei mesi formattati con `QLocale`: separatore decimale e - numeri, importi e nomi dei mesi formattati con `QLocale`: separatore decimale e
posizione del simbolo di valuta seguono la lingua (verificato per entrambe) posizione del simbolo di valuta seguono la lingua (verificato per entrambe)
- `qmllint` pulito su tutti i QML (valida solo la sintassi: senza il modulo - controlli statici dove la UI non è eseguibile: `qmllint` pulito su tutti i QML
Silica una proprietà inesistente passa in silenzio) (valida solo la sintassi: senza il modulo Silica una proprietà inesistente
passa in silenzio), le chiavi che i QML leggono sono quelle che il bridge manda
(`tests/ui_contract_check.py`), e toccare una lettura in Home apre solo le
regolazioni di quell'oggetto (`tests/qml_focus_check.py`)
- **nessuna assegnazione duplicata** nei QML (`tests/qml_check_bindings.py`, - **nessuna assegnazione duplicata** nei QML (`tests/qml_check_bindings.py`,
13 file): assegnare due volte la stessa proprietà nello stesso oggetto — due 13 file): assegnare due volte la stessa proprietà nello stesso oggetto — due
`Component.onCompleted` in una pagina, tipicamente — fa fallire la creazione `Component.onCompleted` in una pagina, tipicamente — fa fallire la creazione
@@ -182,9 +193,11 @@ python3 tests/ui_contract_check.py http://192.168.3.21:8790
comprese: coincidono con quelle mostrate dall'app ufficiale per lo stesso comprese: coincidono con quelle mostrate dall'app ufficiale per lo stesso
periodo (calore prodotto 70,8 kWh, elettricità 29,3 kWh, COP acqua calda 4,42, periodo (calore prodotto 70,8 kWh, elettricità 29,3 kWh, COP acqua calda 4,42,
risparmio Smart Tariff Control 2,71 €) risparmio Smart Tariff Control 2,71 €)
- **l'app non è ancora stata compilata né eseguita su un telefono**: le immagini - **compilata ed eseguita su un telefono** (Jolla, Sailfish OS 5.2): è proprio la
in `docs/anteprima/` sono anteprime di layout costruite con i valori veri prima build ad aver rivelato il difetto della Home, corretto nella 0.6.1. Resta
dell'impianto, non screenshot **non verificato l'invio di un comando** a un impianto vero: finora solo
letture. Le immagini in `docs/anteprima/` sono anteprime di layout costruite con
i valori veri dell'impianto, non screenshot
Non c'è ancora la UI per il raffrescamento (il bridge sa farlo), né orari e Non c'è ancora la UI per il raffrescamento (il bridge sa farlo), né orari e
curve; le versioni del software si vedono in sola lettura. La pagina Dati dipende curve; le versioni del software si vedono in sola lettura. La pagina Dati dipende
@@ -218,6 +231,7 @@ 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)
tests/ui_contract_check.py chiavi dei QML contro quelle del bridge (bridge attivo) tests/ui_contract_check.py chiavi dei QML contro quelle del bridge (bridge attivo)
tests/qml_check_bindings.py assegnazioni duplicate di proprieta' nei QML tests/qml_check_bindings.py assegnazioni duplicate di proprieta' nei QML
tests/qml_focus_check.py un tocco apre solo le regolazioni di quell'oggetto
bridge/aira_bridge.py servizio HTTP/1.1 + JSON verso il cloud (gRPC) bridge/aira_bridge.py servizio HTTP/1.1 + JSON verso il cloud (gRPC)
bridge/setup_creds.py salva le credenziali Aira (file 600) bridge/setup_creds.py salva le credenziali Aira (file 600)
bridge/aira_probe.py sonda di sola lettura: login + stato impianto bridge/aira_probe.py sonda di sola lettura: login + stato impianto
+19 -5
View File
@@ -142,6 +142,14 @@ python3 tests/ui_contract_check.py # bridge on 127.0.0.1:8790
python3 tests/ui_contract_check.py http://192.168.3.21:8790 python3 tests/ui_contract_check.py http://192.168.3.21:8790
``` ```
Two checks look at the QML itself, since none of it can be run here. Both take
milliseconds and name the file they are unhappy about:
```sh
python3 tests/qml_check_bindings.py qml # no property assigned twice in one object
python3 tests/qml_focus_check.py # a tap opens only the object it touched
```
## Status ## Status
Verified so far: Verified so far:
@@ -158,8 +166,11 @@ Verified so far:
in, Italian out, and back to English when the catalogue is removed in, Italian out, and back to English when the catalogue is removed
- numbers, amounts and month names formatted through `QLocale`, so the decimal - numbers, amounts and month names formatted through `QLocale`, so the decimal
separator and the currency position follow the language (verified for both) separator and the currency position follow the language (verified for both)
- `qmllint` clean on every QML file. Note it validates syntax only: without the - static checks where the UI cannot be run: `qmllint` clean on every QML file
Silica module an invalid Silica property passes silently (syntax only: without the Silica module an invalid Silica property passes
silently), the data keys the QML reads are the ones the bridge serves
(`tests/ui_contract_check.py`), and a tap on the home screen opens only the
settings of the object it touched (`tests/qml_focus_check.py`)
- **no duplicate property assignment** in the QML (`tests/qml_check_bindings.py`, - **no duplicate property assignment** in the QML (`tests/qml_check_bindings.py`,
13 files): assigning the same property twice in one object — two 13 files): assigning the same property twice in one object — two
`Component.onCompleted` in a page, typically — makes Qt 5.6 fail to create the `Component.onCompleted` in a page, typically — makes Qt 5.6 fail to create the
@@ -171,9 +182,11 @@ Verified so far:
4.42, Smart Tariff Control saving €2.71) 4.42, Smart Tariff Control saving €2.71)
- the bridge runs as a systemd user unit and comes back by itself after a kill - the bridge runs as a systemd user unit and comes back by itself after a kill
Not verified: **the app has not been built or run on a phone yet** — the first Verified on a phone too: **built and run on a Sailfish phone** (Jolla, Sailfish
build is up to whoever has the Sailfish SDK. The images in `docs/anteprima/` are OS 5.2) — that first build is exactly what exposed the home-page bug fixed in
layout previews built from the real plant values, not screenshots. 0.6.1. Not verified: **sending a command to a real plant** (everything so far is
read-only), and the images in `docs/anteprima/` are layout previews built from
the real plant values, not screenshots.
A Qt 5.6 note on the language switch: `QQmlEngine::retranslate()` only exists A Qt 5.6 note on the language switch: `QQmlEngine::retranslate()` only exists
from Qt 5.10, so installing a translator is not enough to re-translate what is from Qt 5.10, so installing a translator is not enough to re-translate what is
@@ -233,6 +246,7 @@ qml/components/ActionButton.qml command button, optional confirmation
tests/core_test.cpp core checks (headless) tests/core_test.cpp core checks (headless)
tests/ui_contract_check.py QML keys vs bridge keys (bridge must be running) tests/ui_contract_check.py QML keys vs bridge keys (bridge must be running)
tests/qml_check_bindings.py duplicate property assignments in the QML tests/qml_check_bindings.py duplicate property assignments in the QML
tests/qml_focus_check.py a tap opens only that object's settings
bridge/ local HTTP bridge -> see bridge/README.md bridge/ local HTTP bridge -> see bridge/README.md
translations/ harbour-aira-it.ts (English is the source language) translations/ harbour-aira-it.ts (English is the source language)
docs/PIANO.md project plan and decisions (Italian) docs/PIANO.md project plan and decisions (Italian)
+16 -5
View File
@@ -328,11 +328,22 @@ logo Aira è ancora in sospeso.
**0.6.4 — la copertina.** Due righe con l'etichetta davanti, "Casa" e "Acqua", **0.6.4 — la copertina.** Due righe con l'etichetta davanti, "Casa" e "Acqua",
ognuna con temperatura attuale / impostata. Le frasi lunghe di prima ("ACS ognuna con temperatura attuale / impostata. Le frasi lunghe di prima ("ACS
53,4 °C · obiettivo 55 °C") sulla cover, che è stretta, venivano tagliate a metà: 53,4 °C · obiettivo 55 °C") sulla cover, che è stretta, venivano tagliate a metà:
il numero vero spariva proprio dove lo si guarda di sfuggita. L'acqua si chiama il numero vero spariva proprio dove lo si guarda di sfuggita. Per stare nella
"Acqua calda" anche qui, come in Home: per stare nella colonna stretta colonna stretta dell'etichetta il valore attuale è più grande e l'obiettivo più
dell'etichetta il valore attuale è più grande e l'obiettivo più piccolo e piccolo e attaccato ("51,5/55,0 °C").
attaccato ("51,5/55,0 °C").
**Stato attuale (0.6.5).** 85/85 verifiche del nucleo, 48/48 del bridge, **0.6.5 — un nome solo per l'acqua.** Sulla copertina l'acqua si chiama "Acqua
calda" come in Home (era "Acqua"): era l'unico punto in cui la stessa cosa aveva
due nomi.
**0.6.6 — le regolazioni dell'oggetto toccato.** Toccando "Casa" si apriva la
pagina con casa e acqua insieme: chi tocca una lettura si aspetta le regolazioni
di quella, non di tutto l'impianto. Ora la Home dice quale oggetto è stato
toccato e la pagina mostra solo quello — l'acqua porta con sé anche il boost, che
è suo. Due lati della stessa cosa, tenuti insieme da un controllo statico
(`tests/qml_focus_check.py`), perché questa parte della UI non è eseguibile senza
il kit Silica.
**Stato attuale (0.6.6).** 85/85 verifiche del nucleo, 48/48 del bridge,
191/191 stringhe tradotte, `qmllint` pulito, contratto UI↔bridge rispettato. 191/191 stringhe tradotte, `qmllint` pulito, contratto UI↔bridge rispettato.
Nessun comando è stato inviato all'impianto: tutto quanto sopra è a sola lettura. Nessun comando è stato inviato all'impianto: tutto quanto sopra è a sola lettura.
+4 -2
View File
@@ -206,7 +206,8 @@ Page {
value: page.fmt(page.raw("zone1_temperature")) + " °C" value: page.fmt(page.raw("zone1_temperature")) + " °C"
subtitle: qsTr("target %1 °C").arg(page.fmt(page.raw("zone1_setpoint"))) subtitle: qsTr("target %1 °C").arg(page.fmt(page.raw("zone1_setpoint")))
valueColor: api.stale ? Theme.secondaryColor : Theme.primaryColor valueColor: api.stale ? Theme.secondaryColor : Theme.primaryColor
onTapped: pageStack.push(Qt.resolvedUrl("SetpointsPage.qml")) onTapped: pageStack.push(Qt.resolvedUrl("SetpointsPage.qml"),
{ "section": "house" })
} }
ReadingCell { ReadingCell {
@@ -217,7 +218,8 @@ Page {
progress: page.waterProgress() progress: page.waterProgress()
showBar: true showBar: true
valueColor: api.stale ? Theme.secondaryColor : Theme.primaryColor valueColor: api.stale ? Theme.secondaryColor : Theme.primaryColor
onTapped: pageStack.push(Qt.resolvedUrl("SetpointsPage.qml")) onTapped: pageStack.push(Qt.resolvedUrl("SetpointsPage.qml"),
{ "section": "water" })
} }
} }
+33 -3
View File
@@ -10,11 +10,19 @@ import "../components"
// slider dentro la pagina che scorre si contendevano anche il trascinamento. // slider dentro la pagina che scorre si contendevano anche il trascinamento.
// Qui lo slider e' la PROPOSTA, il numero grande e' quello vero dell'impianto: // Qui lo slider e' la PROPOSTA, il numero grande e' quello vero dell'impianto:
// i due coincidono solo dopo "Imposta". // i due coincidono solo dopo "Imposta".
//
// La Home passa l'oggetto toccato ("house" o "water") e si vedono solo le sue
// regolazioni: toccando "Acqua calda" non ha senso trovarsi davanti anche la
// casa. Senza parametro si vedono tutte, come in origine.
Page { Page {
id: page id: page
allowedOrientations: Orientation.All allowedOrientations: Orientation.All
property string section: ""
property bool mostraCasa: page.section !== "water"
property bool mostraAcqua: page.section !== "house"
property real pendingHouse: 0 property real pendingHouse: 0
property bool houseTouched: false property bool houseTouched: false
property real pendingWater: 0 property real pendingWater: 0
@@ -53,6 +61,8 @@ Page {
function houseChanged() { return Math.abs(page.pendingHouse - page.houseTarget()) > 0.001 } function houseChanged() { return Math.abs(page.pendingHouse - page.houseTarget()) > 0.001 }
function waterChanged() { return Math.abs(page.pendingWater - page.waterTarget()) > 0.001 } function waterChanged() { return Math.abs(page.pendingWater - page.waterTarget()) > 0.001 }
// UNA sola assegnazione: in Qt 5.6 assegnare Component.onCompleted due volte
// nello stesso oggetto e' un errore di compilazione del file.
Component.onCompleted: { Component.onCompleted: {
page.syncFromPlant() page.syncFromPlant()
if (api.configured) if (api.configured)
@@ -95,9 +105,13 @@ Page {
PageHeader { title: qsTr("Setpoints") } PageHeader { title: qsTr("Setpoints") }
// ---- riscaldamento casa ---------------------------------------- // // ---- riscaldamento casa ---------------------------------------- //
SectionHeader { text: qsTr("Heating the house") } SectionHeader {
visible: page.mostraCasa
text: qsTr("Heating the house")
}
ValueRow { ValueRow {
visible: page.mostraCasa
title: qsTr("Current target") title: qsTr("Current target")
subtitle: qsTr("What the plant is keeping now") subtitle: qsTr("What the plant is keeping now")
value: page.fmt(page.houseTarget()) + " °C" value: page.fmt(page.houseTarget()) + " °C"
@@ -105,6 +119,7 @@ Page {
Slider { Slider {
id: casaSlider id: casaSlider
visible: page.mostraCasa
width: parent.width width: parent.width
label: qsTr("New house target") label: qsTr("New house target")
minimumValue: 5 minimumValue: 5
@@ -123,6 +138,7 @@ Page {
} }
Row { Row {
visible: page.mostraCasa
x: Theme.horizontalPageMargin x: Theme.horizontalPageMargin
width: col.width - 2 * Theme.horizontalPageMargin width: col.width - 2 * Theme.horizontalPageMargin
spacing: Theme.paddingSmall spacing: Theme.paddingSmall
@@ -143,6 +159,7 @@ Page {
} }
ActionButton { ActionButton {
visible: page.mostraCasa
text: qsTr("Set house to %1 °C").arg(page.fmt(page.pendingHouse)) text: qsTr("Set house to %1 °C").arg(page.fmt(page.pendingHouse))
confirmText: qsTr("House temperature to %1 °C").arg(page.fmt(page.pendingHouse)) confirmText: qsTr("House temperature to %1 °C").arg(page.fmt(page.pendingHouse))
enabled: page.houseChanged() enabled: page.houseChanged()
@@ -150,9 +167,13 @@ Page {
} }
// ---- acqua calda sanitaria ------------------------------------- // // ---- acqua calda sanitaria ------------------------------------- //
SectionHeader { text: qsTr("Domestic hot water") } SectionHeader {
visible: page.mostraAcqua
text: qsTr("Domestic hot water")
}
ValueRow { ValueRow {
visible: page.mostraAcqua
title: qsTr("DHW target") title: qsTr("DHW target")
subtitle: qsTr("Water now %1 °C").arg(page.fmt(page.raw("dhw_current"))) subtitle: qsTr("Water now %1 °C").arg(page.fmt(page.raw("dhw_current")))
value: page.fmt(page.waterTarget()) + " °C" value: page.fmt(page.waterTarget()) + " °C"
@@ -160,6 +181,7 @@ Page {
Slider { Slider {
id: acquaSlider id: acquaSlider
visible: page.mostraAcqua
width: parent.width width: parent.width
label: qsTr("New target") label: qsTr("New target")
minimumValue: 45 minimumValue: 45
@@ -176,6 +198,7 @@ Page {
} }
Row { Row {
visible: page.mostraAcqua
x: Theme.horizontalPageMargin x: Theme.horizontalPageMargin
width: col.width - 2 * Theme.horizontalPageMargin width: col.width - 2 * Theme.horizontalPageMargin
spacing: Theme.paddingSmall spacing: Theme.paddingSmall
@@ -196,6 +219,7 @@ Page {
} }
ActionButton { ActionButton {
visible: page.mostraAcqua
text: qsTr("Set target %1 °C").arg(page.fmt(page.pendingWater)) text: qsTr("Set target %1 °C").arg(page.fmt(page.pendingWater))
confirmText: qsTr("DHW target to %1 °C").arg(page.fmt(page.pendingWater)) confirmText: qsTr("DHW target to %1 °C").arg(page.fmt(page.pendingWater))
enabled: page.waterChanged() enabled: page.waterChanged()
@@ -203,15 +227,21 @@ Page {
} }
// ---- boost ------------------------------------------------------ // // ---- boost ------------------------------------------------------ //
SectionHeader { text: qsTr("Hot water boost") } // Il boost e' dell'acqua calda: si vede con lei.
SectionHeader {
visible: page.mostraAcqua
text: qsTr("Hot water boost")
}
ActionButton { ActionButton {
visible: page.mostraAcqua
text: qsTr("Start DHW boost") text: qsTr("Start DHW boost")
confirmText: qsTr("Starting DHW boost") confirmText: qsTr("Starting DHW boost")
onTriggered: api.sendCommand("hot_water_boost_on") onTriggered: api.sendCommand("hot_water_boost_on")
} }
ActionButton { ActionButton {
visible: page.mostraAcqua
text: qsTr("Stop DHW boost") text: qsTr("Stop DHW boost")
confirmText: qsTr("Stopping DHW boost") confirmText: qsTr("Stopping DHW boost")
onTriggered: api.sendCommand("hot_water_boost_off") onTriggered: api.sendCommand("hot_water_boost_off")
+5 -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.6.5 Version: 0.6.6
Release: 1 Release: 1
Group: Qt/Qt Group: Qt/Qt
License: MIT License: MIT
@@ -37,6 +37,10 @@ Address and token of the bridge live in the app settings (AppConfigLocation);
no personal data is stored in the binary. no personal data is stored in the binary.
%changelog %changelog
* Sun Oct 11 2026 Carlo <carlo@carlobaratto.it> - 0.6.6-1
- Home: toccare "Casa" o "Acqua calda" apre le regolazioni di quell'oggetto e
non piu' casa e acqua insieme; l'acqua porta con se' il boost, che e' suo.
Nuovo controllo statico (tests/qml_focus_check.py) su chi apre e chi mostra
* Sun Oct 11 2026 Carlo <carlo@carlobaratto.it> - 0.6.5-1 * Sun Oct 11 2026 Carlo <carlo@carlobaratto.it> - 0.6.5-1
- copertina: l'acqua si chiama "Acqua calda" come in Home (era "Acqua"). Con - copertina: l'acqua si chiama "Acqua calda" come in Home (era "Acqua"). Con
l'etichetta piu' lunga il valore attuale sta piu' grande e l'obiettivo piu' l'etichetta piu' lunga il valore attuale sta piu' grande e l'obiettivo piu'
+2 -2
View File
@@ -21,7 +21,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
app->setOrganizationName(QStringLiteral("harbour")); app->setOrganizationName(QStringLiteral("harbour"));
app->setApplicationName(QStringLiteral("aira")); app->setApplicationName(QStringLiteral("aira"));
app->setApplicationVersion(QStringLiteral("0.6.5")); app->setApplicationVersion(QStringLiteral("0.6.6"));
Settings settings; Settings settings;
ApiClient api(&settings); ApiClient api(&settings);
@@ -36,7 +36,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
// Prima riga del log: dice subito se il device sta girando la build nuova // Prima riga del log: dice subito se il device sta girando la build nuova
// o una stale (trappola nota del build engine). // o una stale (trappola nota del build engine).
qDebug() << "harbour-aira v0.6.5 build" << __DATE__ << __TIME__; qDebug() << "harbour-aira v0.6.6 build" << __DATE__ << __TIME__;
view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings); view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings);
view->rootContext()->setContextProperty(QStringLiteral("api"), &api); view->rootContext()->setContextProperty(QStringLiteral("api"), &api);
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Controlla che la Home apra le regolazioni dell'oggetto toccato.
La UI non e' eseguibile senza il kit Silica, quindi il controllo e' statico:
1. ogni push verso SetpointsPage.qml dichiara una sezione (chi tocca "Casa"
non deve trovarsi davanti le regolazioni di tutto l'impianto);
2. la sezione dichiarata e' fra quelle che la pagina conosce;
3. ogni sezione della pagina e' legata a una visibilita' (mostraCasa /
mostraAcqua), altrimenti "solo quelle dell'oggetto" resta una promessa.
Esce 0 se tutto regge, 1 con l'elenco dei problemi.
"""
import pathlib
import re
import sys
QML = pathlib.Path(__file__).resolve().parent.parent / "qml"
HOME = QML / "pages" / "MainPage.qml"
PAGINA = QML / "pages" / "SetpointsPage.qml"
problemi = []
home = HOME.read_text(encoding="utf-8")
pagina = PAGINA.read_text(encoding="utf-8")
# 1. le chiamate dalla Home, con l'eventuale sezione subito dopo
chiamate = re.findall(
r'SetpointsPage\.qml"\)\s*(?:,\s*\{\s*"section"\s*:\s*"([^"]+)"\s*\})?',
home,
)
if not chiamate:
problemi.append("MainPage: nessun push verso SetpointsPage.qml (rinominato?)")
senza_sezione = chiamate.count("")
if senza_sezione:
problemi.append(
"%d push verso SetpointsPage.qml senza sezione: la pagina si apre intera"
% senza_sezione
)
# 2. le sezioni che la pagina conosce, lette dai confronti su page.section
conosciute = set(re.findall(r'page\.section\s*!==\s*"([^"]+)"', pagina))
if not conosciute:
problemi.append("SetpointsPage: nessuna sezione dichiarata (page.section)")
for sezione in chiamate:
if sezione and sezione not in conosciute:
problemi.append(
"MainPage chiede la sezione %r, che SetpointsPage non conosce (%s)"
% (sezione, ", ".join(sorted(conosciute)) or "nessuna")
)
# 3. ogni sezione deve nascondere l'altra: entrambe le visibilita' usate
for proprieta in ("mostraCasa", "mostraAcqua"):
if ("visible: page.%s" % proprieta) not in pagina:
problemi.append(
"SetpointsPage: nessun blocco legato a %s: la sezione non si nasconde"
% proprieta
)
if problemi:
print("ESITO: problemi trovati")
for p in problemi:
print(" -", p)
sys.exit(1)
print("ESITO: sezioni OK (%d chiamate dalla Home, sezioni %s)"
% (len(chiamate), ", ".join(sorted(conosciute))))