diff --git a/README.it.md b/README.it.md index e0b93ad..8b97a91 100644 --- a/README.it.md +++ b/README.it.md @@ -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 diff --git a/README.md b/README.md index b999ca4..e5e8f0f 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/PIANO.md b/docs/PIANO.md index 75cbd5d..6cee800 100644 --- a/docs/PIANO.md +++ b/docs/PIANO.md @@ -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. diff --git a/qml/pages/MainPage.qml b/qml/pages/MainPage.qml index b540751..895f1cc 100644 --- a/qml/pages/MainPage.qml +++ b/qml/pages/MainPage.qml @@ -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" }) } } diff --git a/qml/pages/SetpointsPage.qml b/qml/pages/SetpointsPage.qml index 975990c..bc92752 100644 --- a/qml/pages/SetpointsPage.qml +++ b/qml/pages/SetpointsPage.qml @@ -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") diff --git a/rpm/harbour-aira.spec b/rpm/harbour-aira.spec index cc0e1da..cdd4674 100644 --- a/rpm/harbour-aira.spec +++ b/rpm/harbour-aira.spec @@ -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 - 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 - 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' diff --git a/src/main.cpp b/src/main.cpp index fe91e30..4d880f1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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); diff --git a/tests/qml_focus_check.py b/tests/qml_focus_check.py new file mode 100644 index 0000000..0c67ac8 --- /dev/null +++ b/tests/qml_focus_check.py @@ -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))))