In casa ci sono undici telecamere a batteria: due Arlo e nove Blink. Home Assistant le vede tutte, e da qualche giorno manda gli avvisi su WhatsApp tramite un gateway, OpenWA, che gira in Docker accanto a lui. Mancavano due cose: vedere cosa ha fatto scattare un avviso di movimento senza aprire nessuna app, e poter chiedere a una telecamera «fammi vedere adesso» scrivendo un messaggio.

Ora l'avviso di movimento arriva con la foto, e al numero di casa si può scrivere telecamere, ricevere l'elenco e rispondere con un numero per avere uno scatto nuovo.

Prima condizione: un gateway che sappia mandare foto

Il testo su WhatsApp funzionava da tempo, le foto no. Il motore che OpenWA usava, basato su un browser che pilota WhatsApp Web, non riusciva più a caricare né a scaricare file. Ho fatto passare il gateway al motore Baileys, che parla direttamente con i server di WhatsApp; il racconto completo è in una guida a parte. Da lì l'invio di immagini ha cominciato a funzionare.

Una sola avvertenza pratica: l'immagine va mandata in base64. L'invio per indirizzo web esiste, ma la validazione dell'API richiede un dominio vero e rifiuta un nome di container come homeassistant.

La foto negli avvisi

Tutti gli avvisi passano da un solo script, che impagina il messaggio e decide i canali. Gli ho aggiunto un campo foto: se l'automazione indica una telecamera, lo script scatta uno snapshot una volta sola e lo usa per tutti i canali.

- action: script.avviso
  data:
    evento: movimento_cam1
    foto: camera.cam1
    titolo: "Movimento rilevato"
    messaggio: "📍 Telecamera *CAM1*"

Due dettagli su dove salvare il file. Home Assistant permette di scrivere snapshot solo in cartelle ammesse; la più ovvia, /config/www, è pubblicata senza autenticazione all'indirizzo /local/, e una foto di casa lì non la voglio. In un'installazione Docker è ammessa anche /media, interna al container: per una foto usa e getta va benissimo.

L'app di Home Assistant sul telefono riceve invece l'immagine direttamente dalla telecamera, passando nella notifica il percorso del proxy:

data:
  image: /api/camera_proxy/camera.cam1

E se lo snapshot non riesce? Lo script sovrascrive sempre lo stesso file, quindi un errore lascerebbe lì la foto della volta prima. Lo script che invia controlla l'età del file: se ha più di due minuti non la manda, e il messaggio parte comunque come solo testo. Meglio nessuna foto che la foto di un altro momento.

Le telecamere a richiesta

Il comando funziona in due tempi. Scrivendo telecamere arriva l'elenco numerato:

📷 TELECAMERE
━━━━━━━━━━━━━━
1. CAM1 (Arlo)
2. CAM2 (Arlo)
3. Caldaia (Blink)
…
👉 Rispondi con il numero o il nome.

Rispondendo con un numero, o con il nome, Home Assistant sveglia quella telecamera, le fa scattare una foto nuova e la manda come risposta. Funziona anche in un colpo solo: foto caldaia.

L'elenco viene da un sensore template che raccoglie le telecamere delle due integrazioni, Arlo prima e Blink poi, ciascuna in ordine di nome. L'elenco mandato su WhatsApp e quello usato per interpretare il numero sono lo stesso, quindi la numerazione non può sfasarsi. Per ogni telecamera il sensore calcola anche una chiave senza spazi e simboli, così «fuori magazzino», «Fuori - Magazzino» e «fuorimagazzino» vengono riconosciuti come lo stesso nome; la ricerca prova prima il nome esatto e poi quello contenuto, così «magazzino» non sceglie per sbaglio «fuori magazzino».

Arlo e Blink scattano in modo diverso

«Foto nuova» vuol dire cose diverse per le due marche, e l'ho verificato nel codice delle integrazioni invece di supporlo.

Per Arlo c'è un servizio che sveglia la telecamera, aspetta l'immagine e la scrive su file: quando la chiamata finisce, la foto è pronta.

- action: aarlo.camera_request_snapshot_to_file
  target:
    entity_id: camera.cam1
  data:
    filename: /media/richiesta_cam1.jpg

Per Blink si chiede uno scatto, si aspetta qualche secondo, si aggiorna l'entità e solo allora si salva lo snapshot:

- action: blink.trigger_camera
  target:
    entity_id: camera.caldaia
- delay: "00:00:06"
- action: homeassistant.update_entity
  target:
    entity_id: camera.caldaia
- action: camera.snapshot
  target:
    entity_id: camera.caldaia
  data:
    filename: /media/richiesta_caldaia.jpg

Lo script parte in background: il messaggio WhatsApp viene accettato subito, e la foto arriva dopo una decina di secondi, il tempo che serve a una telecamera a batteria per svegliarsi.

Due bug che vale la pena raccontare

Il numero che non era un numero. Alla prima prova vera, scrivendo telecamere e poi 1, la risposta era «non ho capito». I nomi invece funzionavano, e per questo nei primi test non me n'ero accorto. Quando arriva l'elenco salvo chi l'ha chiesto e quando, come testo: 1|1792059658. Il destinatario lo calcola un template che restituisce «1», e qui sta il punto: Home Assistant converte il risultato dei template nel tipo che gli sembra giusto, quindi la variabile diventava il numero 1. Il confronto tra il testo «1» letto dall'entità e il numero 1 era sempre falso.

# prima: sempre falso
a[0] == dest
# dopo
a[0] == dest | string

L'ho trovato riproducendo il calcolo nell'editor dei template, dove la variabile era testo e tutto risultava vero: la differenza tra i due ambienti era proprio il tipo.

L'attesa che si chiudeva troppo presto. Dopo la prima foto, un secondo numero riceveva di nuovo «non ho capito». Avevo scritto io di azzerare l'attesa dopo ogni scelta, pensando a una richiesta alla volta; chi guarda le telecamere ne vuole vedere tre di fila. Ora ogni scelta rinnova i cinque minuti, e l'elenco vale finché non passano cinque minuti senza richieste.

In sintesi

  • Le foto su WhatsApp dipendono dal gateway: con alcuni motori il testo funziona e i file no.
  • Gli snapshot vanno in una cartella ammessa ma non pubblica; /config/www è visibile da fuori.
  • Meglio un avviso senza foto che con la foto sbagliata: controllare l'età del file.
  • «Foto nuova» per Arlo e per Blink si ottiene in modi diversi: va letto nel codice dell'integrazione.
  • Nei template di Home Assistant un «1» può diventare un numero: nei confronti con il testo, | string.