harbour-aira 0.1.0: client Sailfish per pompa di calore Aira (bridge HTTP + stato e comandi ACS)
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
# harbour-aira — app Sailfish per la pompa di calore Aira
|
||||
|
||||
Piano scritto il 9 ottobre 2026, prima di scrivere la UI (convenzione di lavoro).
|
||||
|
||||
## 1. Stato verificato sul campo
|
||||
|
||||
| Cosa | Esito |
|
||||
|---|---|
|
||||
| Accesso cloud Aira | **funziona** — `pyairahome` 2.3.0, pool Cognito `eu-north-1_cnqyjWtbz` (indice 0), token salvati in `~/.hermes/profiles/carlo/aira_tokens.json` (600) |
|
||||
| Lettura impianto | **funziona** — esterna, interna, ACS attuale/obiettivo, modalità, curve zone, firmware, errori |
|
||||
| Bridge HTTP sul server | **scritto e provato** — `~/workspace/Carlo/Aira/aira_bridge.py`, v0.1.0 |
|
||||
| Bluetooth sul server | **assente** — nessun adattatore HCI (esclude la via locale BLE su questa macchina) |
|
||||
| SDK Sailfish (`sfdk`) | **assente qui** — build e deploy li fa Carlo dal suo PC |
|
||||
| Jolla Phone 2026 | non raggiungibile dalla rete del server: deploy solo dal PC |
|
||||
|
||||
## 2. Perché serve un bridge e non una chiamata diretta
|
||||
|
||||
Il cloud Aira espone **gRPC su HTTP/2**. Qt 5.6 — il toolkit su cui gira Sailfish —
|
||||
**non ha HTTP/2**, quindi un'app nativa non può parlarci direttamente. Vie alternative
|
||||
valutate e scartate:
|
||||
|
||||
- **PyOtherSide + pyairahome dentro l'app**: servirebbe impacchettare `grpcio` e `bleak`
|
||||
compilati per aarch64/SFOS, con l'RPM che diventa fragile a ogni aggiornamento.
|
||||
- **Cognito + gRPC riscritti in C++**: HTTP/2 e protobuf da zero. Molto lavoro, molto rischio.
|
||||
|
||||
Scelta: **la logica resta sul server** (dove Python e i token già funzionano), l'app è un
|
||||
client sottile. Vantaggio collaterale: credenziali e token non stanno mai sul telefono.
|
||||
|
||||
```
|
||||
Jolla (QML/C++ Qt 5.6) --HTTP/1.1 + JSON--> bridge sul server --gRPC--> cloud Aira
|
||||
(pyairahome + token)
|
||||
```
|
||||
|
||||
## 3. Protocollo del bridge (v1 — già implementato)
|
||||
|
||||
Autenticazione: `Authorization: Bearer <token>` su tutto tranne `/health`.
|
||||
Token generato al primo avvio in `~/.hermes/profiles/carlo/aira_bridge.json` (600).
|
||||
|
||||
| Metodo | Rotta | Risposta |
|
||||
|---|---|---|
|
||||
| GET | `/health` | `{ok, version}` — nessuna auth, per la diagnostica |
|
||||
| GET | `/summary` | valori leggibili: temperature, ACS, modalità, flag, errori |
|
||||
| GET | `/state` | stato completo grezzo (42 campi) |
|
||||
| GET | `/commands` | elenco comandi ammessi + parametri |
|
||||
| POST | `/command` | `{"id": "...", "value": ...}` |
|
||||
|
||||
Comandi ammessi in v1: `ping`, `set_hot_water_target` (45–60 °C), `hot_water_boost_on/off`,
|
||||
`hot_water_heating_on/off`, `away_mode_on/off`, `night_mode_1h`, `force_heating_on/off`,
|
||||
`heating_on/off`, `legionella_cycle`.
|
||||
|
||||
**Whitelist voluta**: la libreria espone ~90 comandi, inclusi `FactoryReset`,
|
||||
`InstallFirmware`, `RebootDevice`, `RotateCertificate`. Quelli restano fuori di proposito:
|
||||
un client mobile non deve poter riformattare l'impianto per un tap sbagliato.
|
||||
|
||||
## 4. Architettura dell'app
|
||||
|
||||
- `harbour-aira.pro` / `rpm/` / `icons/` dai template consolidati della skill.
|
||||
- **C++ minimo**: `Settings` (QSettings su `AppConfigLocation`, obbligatorio per la sandbox
|
||||
SailJail) e `ApiClient` (QNetworkAccessManager + watchdog di timeout 15 s, JSON parsing).
|
||||
Esposti come context properties: `appSettings`, `api`.
|
||||
- **QML**: `MainPage` (dashboard), `SettingsPage` (indirizzo bridge + token), `CoverPage`.
|
||||
- Nessun dato personale nel binario: indirizzo e token si configurano nell'app
|
||||
(le repo sono pubbliche su Gitea).
|
||||
|
||||
## 5. Fasi
|
||||
|
||||
- **F0 — ponte (fatto)**: bridge scritto, provato su `/health`, `/summary`, `/commands`,
|
||||
errori 400/401 verificati. Nessun comando inviato all'impianto.
|
||||
- **F1 — v0.1 in sola lettura**: dashboard con esterna/interna/ACS, modalità, stato ACS,
|
||||
pull-to-refresh, copertina con le temperature. È già utile e non può fare danni.
|
||||
- **F2 — comandi**: ACS obiettivo (slider 45–60), boost, away, night mode. Conferme con
|
||||
`RemorseItem`/`RemorsePopup` per le azioni che durano.
|
||||
- **F3 — servizio e rete**: unità systemd per il bridge (avvio automatico), poi la via di
|
||||
accesso dal telefono (VPN WireGuard già attiva, oppure sottodominio HTTPS dietro reverse
|
||||
proxy: in quel caso servono DNS e certificato, che Carlo applica dal pannello).
|
||||
|
||||
## 6. Rischi noti
|
||||
|
||||
- **Dipendenza dal server**: se il bridge è spento, l'app non vede nulla. Mitigazione: unità
|
||||
systemd con riavvio automatico (F3).
|
||||
- **Scadenza credenziali**: i token Cognito si rinnovano da soli per settimane; quando il
|
||||
refresh token scade serve rilanciare `setup_creds.py` sul server. L'app deve mostrare un
|
||||
errore chiaro, non un retry infinito.
|
||||
- **Assenza di test sulla UI**: senza SDK né device raggiungibile da qui, la UI Silica si
|
||||
verifica solo sul telefono di Carlo (ciclo consolidato: io consegno il tarball, lui compila
|
||||
e incolla i log).
|
||||
- **Token del bridge**: è l'unica credenziale sul telefono; va trattato come una password e
|
||||
revocabile rigenerando `aira_bridge.json`.
|
||||
|
||||
## 7. Decisioni aperte
|
||||
|
||||
1. **Come raggiungere il bridge dal telefono**: VPN WireGuard (già attiva, zero configurazione
|
||||
nuova) oppure sottodominio HTTPS pubblico con reverse proxy.
|
||||
2. **Ambito della v0.1**: sola lettura (consigliata) oppure lettura + comandi ACS subito.
|
||||
3. **Nome visibile dell'app**: default proposto "Aira" (pacchetto `harbour-aira`).
|
||||
@@ -0,0 +1,95 @@
|
||||
# API del bridge
|
||||
|
||||
Servizio: `~/workspace/Carlo/Aira/aira_bridge.py` (Python, solo `pyairahome`).
|
||||
Traduce HTTP/1.1 + JSON in gRPC verso il cloud Aira. Tutte le rotte tranne
|
||||
`/health` richiedono:
|
||||
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
Token: generato al primo avvio in `~/.hermes/profiles/carlo/aira_bridge.json`.
|
||||
|
||||
## GET /health
|
||||
|
||||
Nessuna autenticazione: serve a distinguere "server irraggiungibile" da
|
||||
"token sbagliato".
|
||||
|
||||
```json
|
||||
{"ok": true, "version": "0.1.0"}
|
||||
```
|
||||
|
||||
## GET /summary
|
||||
|
||||
Valori pronti per la dashboard.
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"summary": {
|
||||
"outdoor_temperature": 18.1,
|
||||
"indoor_temperature": 22.5,
|
||||
"dhw_current": 51.0,
|
||||
"dhw_target": 55.0,
|
||||
"operating_status": "OPERATING_STATUS_AUTO",
|
||||
"away_mode": false,
|
||||
"night_mode": false,
|
||||
"manual_mode": false,
|
||||
"force_heating": true,
|
||||
"inline_heater": false,
|
||||
"led_pattern": "LED_PATTERN_NORMAL",
|
||||
"zones": 1,
|
||||
"errors_count": 0,
|
||||
"pump_updated": "2026-10-09 18:52:17.199340"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## GET /state
|
||||
|
||||
Stato completo e grezzo dal cloud (42 campi: curve climatiche per zona,
|
||||
versioni firmware, schedulazioni, errori). Per diagnostica.
|
||||
|
||||
## GET /commands
|
||||
|
||||
Elenco dei comandi ammessi con i loro parametri.
|
||||
|
||||
## POST /command
|
||||
|
||||
```json
|
||||
{"id": "set_hot_water_target", "value": 55}
|
||||
```
|
||||
|
||||
Risposta: `{"ok": true, "id": "...", "result": {...}}`.
|
||||
|
||||
### Comandi ammessi
|
||||
|
||||
| id | value | effetto |
|
||||
|---|---|---|
|
||||
| `ping` | — | verifica che l'impianto risponda |
|
||||
| `set_hot_water_target` | 45–60 | temperatura ACS obiettivo |
|
||||
| `hot_water_boost_on` / `hot_water_boost_off` | — | boost ACS una tantum |
|
||||
| `hot_water_heating_on` / `hot_water_heating_off` | — | riscaldamento ACS |
|
||||
| `away_mode_on` / `away_mode_off` | — | modalità assenza |
|
||||
| `night_mode_1h` | — | night mode per un'ora |
|
||||
| `force_heating_on` / `force_heating_off` | — | riscaldamento forzato |
|
||||
| `heating_on` / `heating_off` | — | funzione riscaldamento |
|
||||
| `legionella_cycle` | — | ciclo antilegionella |
|
||||
|
||||
### Perché una whitelist
|
||||
|
||||
La libreria espone ~90 comandi, fra cui `FactoryReset`, `InstallFirmware`,
|
||||
`RebootDevice`, `RotateCertificate`, `SetWifiCredentials`. Restano **fuori di
|
||||
proposito**: un client mobile non deve poter riformattare l'impianto per un tap
|
||||
sbagliato.
|
||||
|
||||
## Errori
|
||||
|
||||
| stato | significato |
|
||||
|---|---|
|
||||
| 400 | comando non ammesso, oppure valore fuori intervallo |
|
||||
| 401 | token mancante o errato |
|
||||
| 404 | rotta sconosciuta |
|
||||
| 502 | il bridge non ha potuto parlare con il cloud (dettaglio in `error`) |
|
||||
|
||||
Formato: `{"ok": false, "error": "..."}`.
|
||||
@@ -0,0 +1,35 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="200" viewBox="0 0 720 200">
|
||||
<style>
|
||||
.box { fill: #10243c; stroke: #3f6d8f; stroke-width: 2; rx: 10; }
|
||||
.title { fill: #eaf2f8; font-family: sans-serif; font-size: 16px; font-weight: bold; }
|
||||
.sub { fill: #9fb6c9; font-family: sans-serif; font-size: 12px; }
|
||||
.arrow { stroke: #7fc8e8; stroke-width: 2; fill: none; marker-end: url(#a); }
|
||||
.lbl { fill: #7fc8e8; font-family: sans-serif; font-size: 11px; }
|
||||
</style>
|
||||
<defs>
|
||||
<marker id="a" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto">
|
||||
<path d="M0,0 L10,4 L0,8 z" fill="#7fc8e8"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect class="box" x="20" y="60" width="180" height="80"/>
|
||||
<text class="title" x="110" y="92" text-anchor="middle">Jolla · Sailfish</text>
|
||||
<text class="sub" x="110" y="112" text-anchor="middle">harbour-aira (QML/C++</text>
|
||||
<text class="sub" x="110" y="128" text-anchor="middle">Qt 5.6, solo HTTP/1.1)</text>
|
||||
|
||||
<rect class="box" x="270" y="60" width="180" height="80"/>
|
||||
<text class="title" x="360" y="92" text-anchor="middle">Bridge (server)</text>
|
||||
<text class="sub" x="360" y="112" text-anchor="middle">aira_bridge.py</text>
|
||||
<text class="sub" x="360" y="128" text-anchor="middle">pyairahome + token</text>
|
||||
|
||||
<rect class="box" x="520" y="60" width="180" height="80"/>
|
||||
<text class="title" x="610" y="92" text-anchor="middle">Cloud Aira</text>
|
||||
<text class="sub" x="610" y="112" text-anchor="middle">gRPC su HTTP/2</text>
|
||||
<text class="sub" x="610" y="128" text-anchor="middle">Cognito + engagementbff</text>
|
||||
|
||||
<line class="arrow" x1="200" y1="100" x2="266" y2="100"/>
|
||||
<text class="lbl" x="233" y="90" text-anchor="middle">JSON</text>
|
||||
|
||||
<line class="arrow" x1="450" y1="100" x2="516" y2="100"/>
|
||||
<text class="lbl" x="483" y="90" text-anchor="middle">gRPC</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
Reference in New Issue
Block a user