- bridge/ con aira_bridge.py, setup_creds.py, aira_probe.py, test offline - percorsi di configurazione via AIRA_CONFIG_DIR (default invariato) - README del bridge in inglese: installazione, servizio, firewall, sicurezza - README principale aggiornato (il bridge non è più 'fuori dal repo')
170 lines
6.3 KiB
Markdown
170 lines
6.3 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) |
|
|
| `GET /state` | token | full raw plant state, exactly as the cloud reports it |
|
|
| `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
|
|
```
|
|
|
|
27 checks on the protobuf payloads and on the summary parsing — 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 |
|
|
| `test_bridge_commands.py` | offline tests of the command payloads |
|
|
| `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.
|