0.6.6: dalla Home si aprono le regolazioni dell'oggetto toccato, non di tutto l'impianto
This commit is contained in:
+19
-5
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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" })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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))))
|
||||
Reference in New Issue
Block a user