# 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: 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.4.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": false, "force_heating_remaining": "0s", "inline_heater": false, "hot_water_heating": true, "configured_modes": "PUMP_MODE_STATE_HEATING", "pump_active_state": "PUMP_ACTIVE_STATE_IDLE", "outdoor_defrost": false, "signature_lights": true, "led_pattern": "LED_PATTERN_NORMAL", "zones": 1, "zone1_setpoint": 19.0, "zone1_temperature": 23.0, "zone1_mode": "PUMP_MODE_STATE_HEATING", "zone1_cooling_setpoint": 22.0, "zone2_setpoint": 0.0, "zone2_temperature": 0.0, "zone2_mode": "PUMP_MODE_STATE_IDLE", "zone2_cooling_setpoint": 0.0, "errors_count": 0, "pump_updated": "2026-10-09 18:52:17.199340", "device_id": "1f4b26", "device_uuid": "1f4b26c4-1800-4b21-8ea2-236925a92aeb", "version_platform": "6.10.0", "version_connectivity": "3.30.1", "version_climate_control": "3.9.12", "version_outdoor_unit": "1.30.0", "version_outdoor_eeprom": "0.114.0", "connected_via": "Cloud", "timezone": "Europe/Rome", "thermostats_count": 1, "thermostat_zone": 1, "thermostat_article": "201884", "thermostat_temperature": 22.8, "thermostat_humidity": 73.3, "thermostat_rssi": -79, "thermostat_battery_low": false } } ``` Il blocco del dispositivo serve alle pagine "Impianto" e "Accessori" dell'app: `device_id` sono le prime 6 cifre dell'UUID (come le mostra l'app ufficiale), `connected_via` è la via di connessione, le `version_*` sono le versioni del software e i campi `thermostat_*` descrivono il termostato a parete (assenti se l'impianto non ne ha: in quel caso `thermostats_count` è 0 e gli altri sono `null`). Il termostato manda i decimali come interi (228 = 22,8 °C): la conversione la fa il bridge, non l'app. ## GET /stats I numeri che Aira calcola nel cloud: sono gli stessi della scheda "Dati" dell'app ufficiale (verificati sullo stesso periodo). Il bridge li tiene in cache per **10 minuti** (`ttl`), perché sono quattro chiamate e cambiano lentamente. ```json { "ok": true, "stats": { "period_days": 30, "currency": "EUR", "month": { "label": "2026-10", "heat_kwh": 70.8, "energy_kwh": 29.3, "cop_heating": null, "cop_dhw": 4.42 }, "cop_monthly": [ {"month": "2026-09", "heating_cop": null, "dhw_cop": 4.42, "avg_outdoor": 21.8} ], "insights": [ {"date": "2026-10-01", "energy_kwh": 29.3, "heat_kwh": 70.8} ], "savings": {"amount": 21.78, "co2_kg": 38.1, "show": true, "points": 31}, "smart_tariff": {"amount": 2.71, "hours": 180}, "monthly_savings": [{"month": "2026-09", "amount": 3.52}] } } ``` - `month`: l'ultimo periodo con dati — calore prodotto (`heat_kwh`), elettricità usata (`energy_kwh`) ed efficienza (COP) di acqua calda e riscaldamento. **`null` significa "nessun dato"**: su un impianto recente il riscaldamento può non averne ancora (mentre l'acqua calda sì). `label` è il mese `AAAA-MM`. - `cop_monthly` e `monthly_savings`: le serie mese per mese, per il dettaglio. - `savings`: risparmio in bolletta sugli ultimi `period_days` giorni (30), CO₂ evitata e numero di punti usati dal calcolo; `show` è il permesso di Aira a mostrare il dato. - `smart_tariff`: il risparmio del mese mobile dovuto allo Smart Tariff Control. - Questa rotta è di **sola lettura**: non invia nessun comando all'impianto. ## 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 | | `set_heating_setpoint` | 5–30 | temperatura obiettivo della casa (zona, default 1) | | `set_cooling_setpoint` | 5–30 | temperatura obiettivo del raffrescamento (zona, default 1) | | `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 | ### Riscaldamento: come si invia il comando per zona Il messaggio `ZoneTemperatures` contiene **sempre entrambe** le zone: ```json {"id": "set_heating_setpoint", "value": 21.5, "zone": 1} ``` `zone` è opzionale (default 1) e accetta 1 o 2. La zona che non si sta modificando viene rimandata con il **suo valore attuale**, letto dall'ultimo stato disponibile: inviarla a zero la spegnerebbe. Per questo il bridge tiene in memoria l'ultima lettura, e il comando va costruito con quel contesto. Il campo `kind` distingue riscaldamento (`1`) da raffrescamento (`2`). ### 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": "..."}`.