Compare commits

...
4 Commits
14 changed files with 242 additions and 26 deletions
+7 -1
View File
@@ -161,7 +161,7 @@ python3 tests/ui_contract_check.py http://192.168.3.21:8790
senza telefono)
- **48/48** verifiche offline del bridge (`bridge/test_bridge_commands.py`),
comprese le conversioni Wh→kWh e degli importi delle statistiche
- **190/190** stringhe tradotte nel catalogo italiano: il test carica il `.qm`
- **191/191** stringhe tradotte nel catalogo italiano: il test carica il `.qm`
compilato come fa `main.cpp` e verifica che in inglese l'etichetta di stato sia
la sorgente e in italiano quella tradotta, e che togliendo il catalogo si torni
all'inglese
@@ -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
+7 -1
View File
@@ -149,13 +149,18 @@ Verified so far:
including that a zone command always carries every zone, that absurd values
are refused (40 °C, zone 3, text), and the Wh→kWh / money conversions of the
statistics
- **190/190** strings translated in the Italian catalogue, checked by loading
- **191/191** strings translated in the Italian catalogue, checked by loading
the compiled `.qm` in the test the same way `main.cpp` does: source English
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
- **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)
+19 -1
View File
@@ -275,8 +275,26 @@ si rilegge da solo (2 s): l'aggiornamento automatico, prima, non esisteva.
**Stato breve ed esteso in C++** (`AiraJson::stateShort/stateLong/statusLevel`):
logica pura, verificabile senza SDK e non duplicata fra Home e Impianto. Test
del nucleo: **79/79** (erano 61). Stringhe: **190/190** tradotte. Anteprime
del nucleo: **79/79** (erano 61). Stringhe: **191/191** tradotte. Anteprime
rigenerate (nuova `regolazioni.png`) in italiano e inglese.
**Copertina** (`qml/cover/CoverPage.qml`): pallino di stato come in Home, la
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.
+11 -3
View File
@@ -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
@@ -26,12 +34,12 @@ P9, P10 (diagnostica in Impianto), P11.
Rimandato: P8 (azioni sulla copertina), in attesa della prova sul telefono.
Verifiche fatte qui: **79 verifiche del nucleo** (erano 61), `qmllint` pulito su
tutti i file QML, 190/190 stringhe tradotte, anteprime rigenerate in inglese e
tutti i file QML, 191/191 stringhe tradotte, anteprime rigenerate in inglese e
italiano, contratto dei dati fra UI e bridge rispettato
(`tests/ui_contract_check.py`).
Da provare sul telefono: che `TextSwitch` si istanzi davvero, l'altezza reale
della Home (riga di log con `contentHeight` contro l'altezza dello schermo) e
l'uso con una mano sola.
della Home (la riga `harbour-aira home: contenuto … schermo …` è già nel codice,
basta leggerla nei log) e l'uso con una mano sola.
---
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 31 KiB

+41 -9
View File
@@ -11,14 +11,40 @@ CoverBackground {
return api.formatValue(v)
}
Label {
id: titleLabel
// Colore del pallino: gli stessi criteri della Home (vedi MainPage.qml).
// Un dato vecchio e' ambra come un modo attivo: un dato vecchio non e' un
// dato buono, e la copertina e' il posto dove si guarda di sfuggita.
function statusColor() {
if (!api.hasData || api.stale)
return "orange"
var livello = api.statusLevel()
if (livello === 2)
return Theme.errorColor
if (livello === 1)
return Theme.highlightColor
return "#4caf50"
}
Row {
id: intestazione
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Theme.paddingLarge
text: "Aira"
color: Theme.primaryColor
font.pixelSize: Theme.fontSizeLarge
spacing: Theme.paddingSmall
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: Theme.paddingSmall
height: width
radius: width / 2
color: cover.statusColor()
}
Label {
text: "Aira"
color: Theme.primaryColor
font.pixelSize: Theme.fontSizeLarge
}
}
Column {
@@ -29,8 +55,9 @@ CoverBackground {
Label {
width: parent.width
horizontalAlignment: Text.AlignHCenter
// Spenta quando i numeri sono vecchi: non deve sembrare fresca.
color: api.stale ? Theme.secondaryColor : Theme.primaryColor
text: cover.t("zone1_temperature") + " °C"
color: Theme.primaryColor
font.pixelSize: Theme.fontSizeHuge
}
@@ -57,8 +84,13 @@ CoverBackground {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.paddingMedium
text: qsTr("outdoor %1 °C").arg(cover.t("outdoor_temperature"))
color: Theme.secondaryColor
// Qui c'e' posto per dire che i numeri non sono freschi.
text: api.stale
? qsTr("Not updated · last %1").arg(api.lastUpdate)
: qsTr("outside %1 °C").arg(cover.t("outdoor_temperature"))
color: api.stale ? "orange" : Theme.secondaryColor
font.pixelSize: Theme.fontSizeExtraSmall
truncationMode: TruncationMode.Fade
width: parent.width - 2 * Theme.paddingMedium
}
}
}
+15
View File
@@ -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.
@@ -294,4 +298,15 @@ Page {
}
NoticeBar { }
// 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
onTriggered: console.log("harbour-aira home: contenuto", flick.contentHeight,
"schermo", height)
}
}
+8 -2
View File
@@ -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 <carlo@carlobaratto.it> - 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 <carlo@carlobaratto.it> - 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
@@ -78,7 +84,7 @@ make %{?_smp_mflags}
# mancante NON fa fallire la compilazione: l'app resterebbe in inglese senza dire
# niente. Qui lo si rigenera comunque; il file finisce nella stessa cartella dove
# lo cerca src/translator.cpp. Il .ts si cerca con find perche' sfdk usa i
# sorgenti live (shadow build) e la cartella puo' chiamarsi harbour-aira-0.5.0
# sorgenti live (shadow build) e la cartella puo' chiamarsi harbour-aira-0.6.0
# (tarball), harbour-aira (clone) o harbour-aira-main (archivio di Gitea).
TS=$(find %{_builddir}/.. -maxdepth 4 -name 'harbour-aira-it.ts' 2>/dev/null | head -1)
if [ -z "$TS" ]; then
+2
View File
@@ -557,6 +557,8 @@ def cover_screen() -> Image.Image:
card = [96, 430, W - 96, 430 + 470]
d.rounded_rectangle(card, radius=34, fill=(14, 18, 24), outline=ACCENT, width=2)
d.text((card[0] + 34, card[1] + 30), T("app"), font=font(40, bold=True), fill=PRIMARY)
# pallino di stato, come nella copertina vera (verde = tutto regolare)
d.ellipse([card[2] - 56, card[1] + 48, card[2] - 32, card[1] + 72], fill=OK)
d.line([(card[0] + 34, card[1] + 100), (card[2] - 34, card[1] + 100)],
fill=BTN_EDGE, width=2)
y = card[1] + 132
+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.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);
+118
View File
@@ -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))
+12 -7
View File
@@ -281,7 +281,7 @@
<message>
<location filename="../qml/pages/CommandsPage.qml" line="75"/>
<source>Lowers the house and stops the hot water</source>
<translation>Abbassa la casa e spegne l'acqua calda</translation>
<translation>Abbassa la casa e spegne l&apos;acqua calda</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="88"/>
@@ -306,7 +306,7 @@
<message>
<location filename="../qml/pages/CommandsPage.qml" line="121"/>
<source>The heat pump keeps the water in temperature</source>
<translation>Mantiene l'acqua in temperatura</translation>
<translation>Mantiene l&apos;acqua in temperatura</translation>
</message>
<message>
<location filename="../qml/pages/CommandsPage.qml" line="135"/>
@@ -337,19 +337,24 @@
<context>
<name>CoverPage</name>
<message>
<location filename="../qml/cover/CoverPage.qml" line="40"/>
<location filename="../qml/cover/CoverPage.qml" line="67"/>
<source>house · target %1 °C</source>
<translation>casa · obiettivo %1 °C</translation>
</message>
<message>
<location filename="../qml/cover/CoverPage.qml" line="49"/>
<location filename="../qml/cover/CoverPage.qml" line="76"/>
<source>DHW %1 °C · target %2 °C</source>
<translation>ACS %1 °C · obiettivo %2 °C</translation>
</message>
<message>
<location filename="../qml/cover/CoverPage.qml" line="60"/>
<source>outdoor %1 °C</source>
<translation>est. %1 °C</translation>
<location filename="../qml/cover/CoverPage.qml" line="89"/>
<source>Not updated · last %1</source>
<translation>Non aggiornato · ultimo %1</translation>
</message>
<message>
<location filename="../qml/cover/CoverPage.qml" line="90"/>
<source>outside %1 °C</source>
<translation>esterna %1 °C</translation>
</message>
</context>
<context>