# 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://: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.