Files
harbour-aira/bridge
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
..

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

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

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:

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.

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.

# ufw
sudo ufw allow from 192.168.1.0/24 to any port 8790 proto tcp
# 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

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:

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.