From 4050b3b5d79f3473e458200714c7ffae0cf74c0a Mon Sep 17 00:00:00 2001 From: Carlo Baratto Date: Sat, 10 Oct 2026 12:22:15 +0200 Subject: [PATCH] 0.6.1: la Home non si apriva (doppio Component.onCompleted); controllo automatico delle assegnazioni duplicate --- README.it.md | 6 ++ README.md | 6 ++ docs/PIANO.md | 13 ++++ docs/UX-STUDIO.md | 8 +++ qml/pages/MainPage.qml | 14 +++-- rpm/harbour-aira.spec | 8 ++- src/main.cpp | 4 +- tests/qml_check_bindings.py | 118 ++++++++++++++++++++++++++++++++++++ 8 files changed, 168 insertions(+), 9 deletions(-) create mode 100644 tests/qml_check_bindings.py diff --git a/README.it.md b/README.it.md index 133557f..6c33740 100644 --- a/README.it.md +++ b/README.it.md @@ -169,6 +169,11 @@ python3 tests/ui_contract_check.py http://192.168.3.21:8790 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) +- **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 + dell'oggetto in Qt 5.6, e sul telefono si vede solo `Type MainPage unavailable` + senza il nome del file. Il controllo impiega millisecondi e dice la riga - provata dal vivo in **sola lettura** su un impianto Aira Home, statistiche 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, @@ -208,6 +213,7 @@ qml/components/NoticeBar.qml messaggi dei comandi, ancorati in fondo 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 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 034a490..ef4ea33 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,11 @@ Verified so far: 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 +- **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 + object, and all you see on the device is `Type MainPage unavailable` with no + file named. The check runs in milliseconds and names the line - read-only queries against one real Aira Home unit, through the bridge — including the cloud statistics, which match what the official app shows for the same period (heat produced 70.8 kWh, electricity 29.3 kWh, hot-water COP @@ -223,6 +228,7 @@ qml/components/NoticeBar.qml command messages, anchored to the bottom edge 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 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 346cf01..1304619 100644 --- a/docs/PIANO.md +++ b/docs/PIANO.md @@ -283,5 +283,18 @@ temperatura grande si spegne quando i numeri sono vecchi e l'etichetta finale è diventata "esterna 18,0 °C" (era "est.", sigla che non diceva niente), con la riga dei numeri vecchi al posto della temperatura esterna quando serve. +**Prima build sul telefono (0.6.0): la Home non si apriva.** Il log diceva +`harbour-aira.qml:9:30: Type MainPage unavailable`, cioè l'app si avviava senza +la pagina iniziale, e non nominava nessun file dei miei. Causa: due assegnazioni +di `Component.onCompleted` nello stesso oggetto (`MainPage.qml`), una vecchia e +una aggiunta con la riga di misura dell'altezza. In Qt 5.6 non è un warning +innocuo: la pagina non si crea più. Il codice è stato unito in una sola +assegnazione e, perché non ricapiti, `tests/qml_check_bindings.py` cerca le +assegnazioni duplicate in tutti i QML (verificato: intercetta il caso +`70fe229`, il codice attuale è pulito). Da questa prima esecuzione sappiamo +anche che il resto dell'avvio funziona: configurazione letta +(`baseUrl=[192.168.3.21:8790]`, token presente), catalogo italiano caricato +(`it_IT`), versione corretta in log. + - Nessun comando nuovo all'impianto: anche questa versione resta **sola lettura** finché non si prova l'invio dal telefono. diff --git a/docs/UX-STUDIO.md b/docs/UX-STUDIO.md index ad2dd20..f2aea4d 100644 --- a/docs/UX-STUDIO.md +++ b/docs/UX-STUDIO.md @@ -10,6 +10,14 @@ Immagini di questo studio (schemi, non anteprime): `docs/ux/`. ## 0. Cosa è stato deciso e cosa è stato fatto (0.6.0) +**Prima build sul telefono:** la Home non si apriva — `Type MainPage unavailable`. +Causa: due assegnazioni di `Component.onCompleted` nello stesso oggetto, che in +Qt 5.6 impediscono la creazione della pagina. Corretto nella 0.6.1 e coperto da +`tests/qml_check_bindings.py`. Tutto il resto dell'avvio funziona (configurazione +letta, catalogo italiano caricato). Il primo giro sul device ha quindi già dato +la sua risposta più importante: la riga `harbour-aira home: contenuto … schermo …` +arriva nei log e si può leggere. + Le quattro scelte, applicate: 1. La frase di stato lunga sta nella pagina **Impianto**; in Home resta la riga diff --git a/qml/pages/MainPage.qml b/qml/pages/MainPage.qml index 0608ac2..2e6e431 100644 --- a/qml/pages/MainPage.qml +++ b/qml/pages/MainPage.qml @@ -78,9 +78,13 @@ Page { pageStack.push(Qt.resolvedUrl("DataPage.qml")) } + // UNA sola assegnazione: in Qt 5.6 assegnare Component.onCompleted due volte + // e' un errore di compilazione del file, e la pagina non si crea piu' + // ("Type MainPage unavailable" e app senza Home). Component.onCompleted: { if (api.configured) api.refresh() + misura.start() } // Fuori dal pulsante: dentro il pulsante i figli finiscono nel contentItem. @@ -295,12 +299,10 @@ Page { NoticeBar { } - Component.onCompleted: misura.start() - - // La disposizione non e' definitiva in Component.onCompleted: si misura un - // secondo dopo. Questa e' la riga da leggere alla prima build sul telefono: - // la Home deve stare in una schermata sola, se "contenuto" supera "schermo" - // c'e' ancora qualcosa da togliere (o da accettare, ma sapendolo). + // La disposizione non e' definitiva subito: si misura un secondo dopo. + // Questa e' la riga da leggere alla prima build sul telefono: la Home deve + // stare in una schermata sola, se "contenuto" supera "schermo" c'e' ancora + // qualcosa da togliere (o da accettare, ma sapendolo). Timer { id: misura interval: 1000 diff --git a/rpm/harbour-aira.spec b/rpm/harbour-aira.spec index 4257275..f0dce10 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.0 +Version: 0.6.1 Release: 1 Group: Qt/Qt License: MIT @@ -37,6 +37,12 @@ Address and token of the bridge live in the app settings (AppConfigLocation); no personal data is stored in the binary. %changelog +* Sat Oct 10 2026 Carlo - 0.6.1-1 +- correzione: la Home non si apriva ("Type MainPage unavailable", app senza + pagina iniziale). Causa: due assegnazioni di Component.onCompleted nello + stesso oggetto; in Qt 5.6 la pagina non si crea piu' e il messaggio non dice + quale sia il file. Ora un controllo automatico le intercetta in pochi + millisecondi (tests/qml_check_bindings.py), prima della build * Sat Oct 10 2026 Carlo - 0.6.0-1 - Home di sola consultazione: stato in una riga con pallino colorato, le due letture (casa e acqua calda) grandi e toccabili, tre azioni rapide; le due diff --git a/src/main.cpp b/src/main.cpp index d4cded0..b778ff6 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.0")); + app->setApplicationVersion(QStringLiteral("0.6.1")); 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.0 build" << __DATE__ << __TIME__; + qDebug() << "harbour-aira v0.6.1 build" << __DATE__ << __TIME__; view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings); view->rootContext()->setContextProperty(QStringLiteral("api"), &api); diff --git a/tests/qml_check_bindings.py b/tests/qml_check_bindings.py new file mode 100644 index 0000000..2da3363 --- /dev/null +++ b/tests/qml_check_bindings.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Assegnazioni duplicate di proprieta' negli stessi oggetti QML. + +Perche' esiste: in Qt 5.6 assegnare due volte la stessa proprieta' di un oggetto +QML (tipico: due `Component.onCompleted` nella stessa pagina) non e' un errore +innocuo: il file compila con un warning "Property value set multiple times" e +l'oggetto NON si crea piu'. Dall'esterno si vede solo + + harbour-aira.qml:9:30: Type MainPage unavailable + +cioe' l'app che si apre senza la pagina iniziale, senza dire il perche'. +Il kit Silica non vede niente di tutto questo (qmllint conosce il QML, non il +significato di questi binding), quindi il controllo sta qui e gira in pochi +millisecondi. + +Uso: + python3 tests/qml_check_bindings.py [cartella_qml] + python3 tests/qml_check_bindings.py --dettagli # elenca anche l'ok +Esce 1 se trova duplicati, 0 se e' pulito. +""" + +import pathlib +import re +import sys + +# Riga che apre un oggetto QML: un componente (maiuscola) oppure un gruppo di +# proprieta' noto. Il corpo di un gestore (onClicked:) o di una funzione e' JS, +# e li' dentro i "duplicati" sono normali (ternari, oggetti letterali, rami). +APRE_OGGETTO = re.compile(r"^\s*([A-Z][A-Za-z0-9_]*)\s*\{") +APRE_GRUPPO = re.compile(r"^\s*(anchors|font|Layout\.[A-Za-z]+)\s*\{") +APRE_JS = re.compile(r"^\s*(function\s+\w+|if\b|else\b|for\b|while\b|switch\b|do\b|try\b|catch\b)\s*[\({]") +APRE_GESTORE = re.compile(r"^\s*on[A-Z][A-Za-z0-9_]*\s*:") + +PROPRIETA = re.compile(r"^\s*((?:readonly\s+)?property\s+\S+\s+([A-Za-z_]\w*)|signal\s+(\w+)|function\s+(\w+)|([A-Za-z_][\w.]*))\s*:") +SENZA_COMMENTO = re.compile(r"//.*$") + + +def chiave(riga: str): + """Nome della proprieta' assegnata dalla riga, o None.""" + m = PROPRIETA.match(SENZA_COMMENTO.sub("", riga)) + if not m: + return None + return m.group(2) or m.group(3) or m.group(4) or m.group(5) + + +def controlla(testo: str): + """Ritorna la lista di (riga, chiave) duplicate nello stesso oggetto.""" + pila = [("qml", {})] # (tipo ambiente, chiavi viste) + problemi = [] + for numero, riga in enumerate(testo.splitlines(), 1): + pulita = SENZA_COMMENTO.sub("", riga) + if not pulita.strip(): + continue + + # Chiusura: si esce dall'ambiente corrente. + while pulita.strip().startswith("}") and len(pila) > 1: + pila.pop() + pulita = pulita.strip()[1:] + if not pulita.strip(): + break + if not pulita.strip(): + continue + + ambiente, chiavi = pila[-1] + if ambiente == "qml": + k = chiave(pulita) + if k: + if k in chiavi: + problemi.append((numero, k)) + else: + chiavi[k] = numero + + # Apertura di un nuovo ambiente: il contenuto e' QML solo se la riga + # apre un oggetto o un gruppo; gestori e funzioni aprono JS. + apre_js = bool(APRE_JS.match(pulita)) or bool(APRE_GESTORE.match(pulita)) + apre_qml = (bool(APRE_OGGETTO.match(pulita)) or bool(APRE_GRUPPO.match(pulita))) \ + and not apre_js + # Una proprieta' il cui valore e' un oggetto letterale JS non e' QML. + if ":" in pulita and "{" in pulita and not apre_qml and not apre_js: + apre_js = True + if pulita.count("{") > pulita.count("}"): + pila.append(("qml" if apre_qml else "js", {})) + return problemi + + +def main(argv): + argomenti = [a for a in argv[1:] if not a.startswith("--")] + radice = pathlib.Path(argomenti[0] if argomenti else "qml") + dettagli = "--dettagli" in argv + + file_qml = sorted(radice.rglob("*.qml")) + if not file_qml: + print("nessun file QML in", radice) + return 1 + + totale = 0 + for percorso in file_qml: + problemi = controlla(percorso.read_text(encoding="utf-8")) + relative = percorso.relative_to(radice.parent if radice.name == "qml" else radice) + if problemi: + totale += len(problemi) + print("%s: %d assegnazioni duplicate" % (relative, len(problemi))) + for numero, k in problemi: + print(" riga %d: '%s' gia' assegnata in questo oggetto" % (numero, k)) + elif dettagli: + print("%s: pulito" % relative) + + print("---") + if totale: + print("ESITO: %d assegnazioni duplicate (l'oggetto non si crea: " + "vedi Qt 5.6, 'Property value set multiple times')" % totale) + return 1 + print("ESITO: nessuna assegnazione duplicata in %d file" % len(file_qml)) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv))