Files
kaneda 4f279a18b0 0.5.0: riordino come l'app ufficiale (home, Dati, Impianto)
- home: temperatura esterna e in casa/assente in alto, messaggio di stato,
  scheda impianto con temperatura stanza e acqua calda; comandi sotto
- pagina Dati: calore prodotto, elettricita' usata, COP acqua calda e
  riscaldamento, risparmio, CO2 evitata, Smart Tariff Control (mese per mese)
- pagina Impianto: stato, termostato a parete, versioni software, errori
- bridge 0.4.0: rotta /stats (servizio statistiche del cloud) e blocco
  dispositivo in /summary (ID breve, versioni, termostato)
- C++: parseStats, formatNumber, formatMoney, monthLabel (separatore e
  valuta secondo la lingua); 61/61 verifiche del nucleo
- anteprime aggiornate (dati, impianto) in inglese e italiano
2026-10-10 08:59:03 +02:00

173 lines
6.9 KiB
Markdown

# Aira bridge — let the Sailfish app talk to your heat pump
The app cannot reach Aira directly: Aira's cloud API is **gRPC over HTTP/2**, and
Sailfish's Qt 5.6 speaks HTTP/1.1 only. This small service sits on a Linux box in
your LAN, answers the phone in plain HTTP/1.1 + JSON and speaks gRPC to Aira.
A side effect worth having: your Aira credentials and session tokens never leave
this machine, so nothing sensitive is stored on the phone.
Tested against an Aira Home heat pump (Europe). Python 3.9+.
## What you need
- a machine that stays on, on the same LAN as the phone (a Raspberry Pi is plenty)
- an Aira account — the same one you use in the Aira app
- outbound internet access to the Aira cloud
- **no** port forwarding, no public exposure: this is a LAN service
## Install
```bash
git clone https://git.hackatoniclife.com/kaneda/harbour-aira.git
cd harbour-aira/bridge
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
# where credentials and bridge configuration will live
export AIRA_CONFIG_DIR="$HOME/.config/aira"
mkdir -p "$AIRA_CONFIG_DIR"
# your Aira email + password: typed without echo, saved in a file readable
# only by your user (600). Nothing is printed, nothing is logged.
python setup_creds.py
# sanity check: logs in and prints your plant state (read-only)
python aira_probe.py
```
If `aira_probe.py` prints your temperatures and modes, the hard part is done.
## Run
```bash
python aira_bridge.py --host 0.0.0.0 --port 8790
```
On first start the bridge creates `$AIRA_CONFIG_DIR/aira_bridge.json` with a
random token (600). To read it:
```bash
python -c "import json,pathlib,os; print(json.load(open(pathlib.Path(os.environ['AIRA_CONFIG_DIR'])/'aira_bridge.json'))['token'])"
```
Then in the app: pull the menu down, *Impostazioni*, and enter
`http://<LAN-IP-of-this-machine>:8790` plus that token.
## Run as a service (recommended)
```bash
mkdir -p ~/.config/systemd/user
cp harbour-aira-bridge.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now harbour-aira-bridge.service
loginctl enable-linger "$USER" # keeps it up after logout and at boot
journalctl --user -u harbour-aira-bridge.service -f
```
The unit uses `%h` and expects the repo in `~/harbour-aira`:
edit `WorkingDirectory` / `ExecStart` if you cloned it elsewhere, and make sure
`Environment=AIRA_CONFIG_DIR=...` matches what you used with `setup_creds.py`.
Verified resilience: killing the process brings it back in about ten seconds.
## Opening the port (LAN only)
The bridge listens on 8790 and checks a bearer token on every route except
`/health`, but it is meant for a trusted network. Do **not** forward this port
from the internet.
```bash
# ufw
sudo ufw allow from 192.168.1.0/24 to any port 8790 proto tcp
```
```nftables
# nftables: inside your input chain
ip saddr 192.168.1.0/24 tcp dport 8790 accept
```
On a host with a default-drop firewall the bridge looks dead from the phone even
though it is running: check the firewall before debugging anything else, and
test from the phone, not from the bridge machine itself (loopback traffic does
not traverse the input chain, so it always works).
## Security
- Credentials (`aira.json`) and Cognito session tokens (`aira_tokens.json`) live
in `$AIRA_CONFIG_DIR` with mode 600, on this machine only.
- `aira_bridge.json` holds the bearer token, which is the only credential stored
on the phone. To rotate it: stop the bridge, delete the file, start it again,
and paste the new token in the app.
- Traffic goes to Aira's own API and nowhere else. Nothing is collected, nothing
is sent to the author.
- Only a whitelist of commands is exposed (see below). Factory reset, firmware
updates, reboots and Wi-Fi provisioning are deliberately not reachable.
## Endpoints
| Route | Auth | Purpose |
|---|---|---|
| `GET /health` | none | `{ok, version}` — liveness, used above all for debugging |
| `GET /summary` | token | dashboard values (house/DHW temperatures, targets, modes, errors), the device block (short ID, software versions, connection) and the wall thermostat (zone, temperature, humidity, signal, battery) |
| `GET /state` | token | full raw plant state, exactly as the cloud reports it |
| `GET /stats` | token | the numbers Aira computes in the cloud: hot-water and heating COP month by month, heat produced, electricity used, savings on the bill, CO₂ avoided, Smart Tariff Control saving. Cached for 10 minutes |
| `GET /commands` | token | allowed commands with their parameters and ranges |
| `POST /command` | token | `{"id": "...", "value": 21.5, "zone": 1}` |
Commands exposed: hot water target and boost, heating/cooling setpoint per zone,
away mode, night mode for one hour, force heating, heating function, legionella
cycle, plant check. Protocol details and field names: `../docs/PROTOCOL.md`.
Two things learned on a real pump, both worth knowing:
- **A zone command always carries every zone.** `SetZoneSetpoints` takes the full
set of zones, so sending only the zone you want to change would zero the other
one. The bridge remembers the last states it read and sends the untouched zone
back with its current value.
- **`force_heating` is not a boolean** in the state but an object
`{enabled, remaining_time}`. Reading it as a truthy value makes "forced heating:
yes" appear while the pump is idle. Same shape for `hot_water.heating_enabled`.
## Offline tests
```bash
python test_bridge_commands.py
```
48 checks on the protobuf payloads, the summary parsing (including the device
block) and the statistics conversions — no network, and
**no command is sent to the pump**.
## If login fails
Your account may live in a different Cognito user pool: a wrong pool answers
"credentials not valid" even with the right password. Try:
```bash
python aira_probe.py --pool 0 # then 1, then 2
```
or write `{"user_pool_index": N}` into `$AIRA_CONFIG_DIR/aira_config.json`.
Try one pool at a time: repeated failures trigger a temporary lockout.
## Files
| File | What it is |
|---|---|
| `aira_bridge.py` | the service: HTTP/1.1 + JSON in, gRPC to Aira out |
| `setup_creds.py` | asks for the Aira credentials and saves them (600) |
| `aira_probe.py` | read-only check that login and the cloud channel work |
| `aira_stats_probe.py` | read-only dump of the cloud statistics (COP, savings, insights), no command sent |
| `test_bridge_commands.py` | offline tests of the command payloads, the summary parsing and the statistics conversions |
| `harbour-aira-bridge.service` | systemd user unit |
| `requirements.txt` | `pyairahome`, pinned to the tested version |
## Reporting your pump
If you are testing this on your own unit, the useful things to send me are: pump
model and firmware versions, number of heating zones (1 or 2), whether cooling is
configured, the output of `python aira_probe.py --json`, and anything that looks
wrong next to what the Aira app shows at that moment. The probe output contains
no credentials.