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
```
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
- **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
- numeri, importi e nomi dei mesi formattati con `QLocale`: separatore decimale e
posizione del simbolo di valuta seguono la lingua (verificato per entrambe)
- `qmllint` pulito su tutti i QML (valida solo la sintassi: senza il modulo
Silica una proprietà inesistente passa in silenzio)
- controlli statici dove la UI non è eseguibile: `qmllint` pulito su tutti i QML
(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`,
13 file): assegnare due volte la stessa proprietà nello stesso oggetto — due
`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
periodo (calore prodotto 70,8 kWh, elettricità 29,3 kWh, COP acqua calda 4,42,
risparmio Smart Tariff Control 2,71 €)
- **l'app non è ancora stata compilata né eseguita su un telefono**: le immagini
in `docs/anteprima/` sono anteprime di layout costruite con i valori veri
dell'impianto, non screenshot
- **compilata ed eseguita su un telefono** (Jolla, Sailfish OS 5.2): è proprio la
prima build ad aver rivelato il difetto della Home, corretto nella 0.6.1. Resta
**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
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/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_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/setup_creds.py salva le credenziali Aira (file 600)
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
```
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
Verified so far:
@@ -158,8 +166,11 @@ Verified so far:
in, Italian out, and back to English when the catalogue is removed
- numbers, amounts and month names formatted through `QLocale`, so the decimal
separator and the currency position follow the language (verified for both)
- `qmllint` clean on every QML file. Note it validates syntax only: without the
Silica module an invalid Silica property passes silently
- static checks where the UI cannot be run: `qmllint` clean on every QML file
(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`,
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
@@ -171,9 +182,11 @@ Verified so far:
4.42, Smart Tariff Control saving €2.71)
- 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
build is up to whoever has the Sailfish SDK. The images in `docs/anteprima/` are
layout previews built from the real plant values, not screenshots.
Verified on a phone too: **built and run on a Sailfish phone** (Jolla, Sailfish
OS 5.2) — that first build is exactly what exposed the home-page bug fixed in
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
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/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_focus_check.py a tap opens only that object's settings
bridge/ local HTTP bridge -> see bridge/README.md
translations/ harbour-aira-it.ts (English is the source language)
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",
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à:
il numero vero spariva proprio dove lo si guarda di sfuggita. L'acqua si chiama
"Acqua calda" anche qui, come in Home: per stare nella colonna stretta
dell'etichetta il valore attuale è più grande e l'obiettivo più piccolo e
attaccato ("51,5/55,0 °C").
il numero vero spariva proprio dove lo si guarda di sfuggita. Per stare nella
colonna stretta dell'etichetta il valore attuale è più grande e l'obiettivo più
piccolo e 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.
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"
subtitle: qsTr("target %1 °C").arg(page.fmt(page.raw("zone1_setpoint")))
valueColor: api.stale ? Theme.secondaryColor : Theme.primaryColor
onTapped: pageStack.push(Qt.resolvedUrl("SetpointsPage.qml"))
onTapped: pageStack.push(Qt.resolvedUrl("SetpointsPage.qml"),
{ "section": "house" })
}
ReadingCell {
@@ -217,7 +218,8 @@ Page {
progress: page.waterProgress()
showBar: true
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.
// Qui lo slider e' la PROPOSTA, il numero grande e' quello vero dell'impianto:
// 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 {
id: page
allowedOrientations: Orientation.All
property string section: ""
property bool mostraCasa: page.section !== "water"
property bool mostraAcqua: page.section !== "house"
property real pendingHouse: 0
property bool houseTouched: false
property real pendingWater: 0
@@ -53,6 +61,8 @@ Page {
function houseChanged() { return Math.abs(page.pendingHouse - page.houseTarget()) > 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: {
page.syncFromPlant()
if (api.configured)
@@ -95,9 +105,13 @@ Page {
PageHeader { title: qsTr("Setpoints") }
// ---- riscaldamento casa ---------------------------------------- //
SectionHeader { text: qsTr("Heating the house") }
SectionHeader {
visible: page.mostraCasa
text: qsTr("Heating the house")
}
ValueRow {
visible: page.mostraCasa
title: qsTr("Current target")
subtitle: qsTr("What the plant is keeping now")
value: page.fmt(page.houseTarget()) + " °C"
@@ -105,6 +119,7 @@ Page {
Slider {
id: casaSlider
visible: page.mostraCasa
width: parent.width
label: qsTr("New house target")
minimumValue: 5
@@ -123,6 +138,7 @@ Page {
}
Row {
visible: page.mostraCasa
x: Theme.horizontalPageMargin
width: col.width - 2 * Theme.horizontalPageMargin
spacing: Theme.paddingSmall
@@ -143,6 +159,7 @@ Page {
}
ActionButton {
visible: page.mostraCasa
text: qsTr("Set house to %1 °C").arg(page.fmt(page.pendingHouse))
confirmText: qsTr("House temperature to %1 °C").arg(page.fmt(page.pendingHouse))
enabled: page.houseChanged()
@@ -150,9 +167,13 @@ Page {
}
// ---- acqua calda sanitaria ------------------------------------- //
SectionHeader { text: qsTr("Domestic hot water") }
SectionHeader {
visible: page.mostraAcqua
text: qsTr("Domestic hot water")
}
ValueRow {
visible: page.mostraAcqua
title: qsTr("DHW target")
subtitle: qsTr("Water now %1 °C").arg(page.fmt(page.raw("dhw_current")))
value: page.fmt(page.waterTarget()) + " °C"
@@ -160,6 +181,7 @@ Page {
Slider {
id: acquaSlider
visible: page.mostraAcqua
width: parent.width
label: qsTr("New target")
minimumValue: 45
@@ -176,6 +198,7 @@ Page {
}
Row {
visible: page.mostraAcqua
x: Theme.horizontalPageMargin
width: col.width - 2 * Theme.horizontalPageMargin
spacing: Theme.paddingSmall
@@ -196,6 +219,7 @@ Page {
}
ActionButton {
visible: page.mostraAcqua
text: qsTr("Set target %1 °C").arg(page.fmt(page.pendingWater))
confirmText: qsTr("DHW target to %1 °C").arg(page.fmt(page.pendingWater))
enabled: page.waterChanged()
@@ -203,15 +227,21 @@ Page {
}
// ---- 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 {
visible: page.mostraAcqua
text: qsTr("Start DHW boost")
confirmText: qsTr("Starting DHW boost")
onTriggered: api.sendCommand("hot_water_boost_on")
}
ActionButton {
visible: page.mostraAcqua
text: qsTr("Stop DHW boost")
confirmText: qsTr("Stopping DHW boost")
onTriggered: api.sendCommand("hot_water_boost_off")
+5 -1
View File
@@ -1,6 +1,6 @@
Name: harbour-aira
Summary: Control your Aira heat pump through a local bridge
Version: 0.6.5
Version: 0.6.6
Release: 1
Group: Qt/Qt
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.
%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
- 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'
+2 -2
View File
@@ -21,7 +21,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
app->setOrganizationName(QStringLiteral("harbour"));
app->setApplicationName(QStringLiteral("aira"));
app->setApplicationVersion(QStringLiteral("0.6.5"));
app->setApplicationVersion(QStringLiteral("0.6.6"));
Settings 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
// 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("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))))