@ :~/lab/irrigazione-ai-giardino-autonomo$
← cd ~/lab

./run irrigazione-ai-giardino-autonomo

Un carattere sbagliato ha allagato la betulla: diario di un'irrigazione autonoma

Diario tecnico di un'irrigazione autonoma: modello agronomico FAO-56, un agente AI che rivede il piano ogni notte, e un protocollo proprietario decodificato da 246 flussi di rete.

status
draft
project
irrigazione
updated
2026-07-07
tags
#irrigazione smart#kubernetes#agente ai#reverse engineering#fao-56#cilium
generate cover # alt: Giardino diviso in dieci zone con una timeline notturna e gocce d'acqua calcolate da un modello, betulla evidenziata

Una notte il sistema ha aperto una zona per un’ora invece che diciassette minuti e ha allagato l’aiuola della betulla — la pianta a cui tengo di più. L’ho scoperto dalla telecamera di sicurezza, non da un log. Un carattere sbagliato nel modo in cui comunicavo con il controller. Quel bug è il motivo per cui questo progetto è diventato serio.

L’idea

Un impianto a timer è stupido: innaffia alle 6 che piova o splenda. Volevo qualcosa che ragionasse zona per zona. Il mio giardino sono 2.500 m², 10 zone, 3 controller wireless. Ogni notte il sistema calcola quanta acqua serve a ognuna, un agente AI rivede il piano, e i controller eseguono da soli.

Il modello fisico prima dell’AI

Il cuore non è l’AI: è l’agronomia. Uso l’ET0 secondo FAO-56 (l’evapotraspirazione di riferimento) più i sensori di umidità del suolo. Il fabbisogno per zona è puro bilancio idrico FAO-56 — niente numeri cablati a mano, ogni parametro ha un valore, una confidenza e una provenienza:

@property
def taw_mm(self):   # Total Available Water: acqua totale disponibile
    return max(0.0, (self.field_capacity_pct/100 - self.wilting_pct/100)
                     * self.root_depth_mm)
@property
def raw_mm(self):   # Readily Available Water = p · TAW
    return self.depletion_fraction * self.taw_mm
@property
def max_session_mm(self):   # mai riempire troppo in un colpo
    ceiling = 8.0 if self.soil_texture == "peat" else 12.0
    return min(0.6 * self.raw_mm, ceiling)

I coefficienti pianta (Kc, profondità radici) vengono dalle tabelle FAO-56, le proprietà del suolo dalla classe tessiturale USDA. Il sensore però vince sulla matematica: se una sonda reale legge sotto la soglia di stress, la zona diventa critica e l’urgenza schizza a 80-100, a prescindere dalle previsioni. Con una furbizia: dopo una pioggia forte, una sonda sotto la chioma degli alberi (dove la pioggia non è arrivata) legge “secco” — e va ignorata, altrimenti innaffio inutilmente.

Dove sta l’AI

Il modello fisico calcola i millimetri, i litri, l’urgenza. L’AI decide la strategia: uno o due cicli (dimezzare le durate aiuta l’infiltrazione su terreno argilloso/torboso), i ritardi tra zone per far ricaricare la cisterna, la finestra oraria per il rischio fungino. Riceve tutto il contesto e risponde in JSON stretto. Regole dure la vincolano: non può mai ridurre una zona in stress critico, e solo un rischio Pythium critico può saltare gli irrigatori — ma se una zona è in stress da caldo, un override deterministico la innaffia comunque, perché “un prato morto è irreversibile, il Pythium si cura”.

Ho cambiato modello tre volte. L’ultimo salto è avvenuto quando il modello precedente, in un’ondata di calore a 38°C, continuava a mettere “salta la notte umida, tanto è reversibile” sopra un prato che seccava. Priorità agronomica sbagliata. Costo attuale: una chiamata a notte, ~$0.025, circa 9 dollari l’anno.

La parte tosta: decodificare il controller

I controller sono proprietari, pensati per essere comandati solo dalla loro app. Il bug dell’allagamento nasceva da lì: mandavo la durata come MM:SS, ma il firmware si aspetta HH:MM. Una zona da 17 minuti (1008 secondi) partiva come "16:48" → interpretato come 16 ore e 48 minuti.

Per capirlo, una mattina: telefono Android rootato con l’app ufficiale, mitmproxy a intercettare l’HTTPS, APK decompilato a fianco. 246 flussi API catturati, ogni tap “Salva Programma” decodificato. Scoperte chiave:

  • I moduli sono figli LoRa di un hub; ogni chiamata usa il seriale dell’hub, mai l’ID del figlio.
  • Tre unità diverse sullo stesso filo: orari di partenza in minuti-da-mezzanotte, durate in secondi, il comando manuale come stringa "HH:MM".
  • Il server risponde 200 anche ai body malformati e poi non consegna niente. Devi fare polling dello stato reale del controller finché non conferma.

La lezione vera non è stata il formato giusto: è stato smettere di comandare zona per zona e passare alla programmazione nativa del firmware, iniettata una volta ed eseguita in autonomia. E scoprire che il LoRa ha i suoi tempi: dall’iniezione all’esecuzione passano 19-24 minuti, variabili. Ho messo un margine fisso di 25.

Il deploy e la gotcha di rete

Gira su un cluster Kubernetes self-hosted. Tre CronJob condividono la stessa immagine: il pianificatore (parte la sera, calcola, inietta, arma il watchdog, poi il pod muore in ~90 secondi — è il firmware che esegue, non serve tenere vivo niente), il watchdog (ogni 5 minuti nella finestra, controlla anomalie e alla fine sospende il proprio CronJob da solo), e una passata anti-caldo diurna.

La gotcha che mi ha rubato giorni: con la CNI Cilium, una NetworkPolicy standard non riesce a whitelistare l’API server di Kubernetes — Cilium gli dà un’identità speciale dopo il NAT di kube-proxy. Sei righe hanno risolto timeout silenziosi da 5 minuti:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata: { name: allow-kube-apiserver, namespace: my-garden }
spec:
  endpointSelector: {}            # tutti i pod del namespace
  egress:
    - toEntities: [ kube-apiserver ]

Cosa ho imparato

  1. L’automazione intelligente non è mettere un LLM ovunque. Il modello fisico giusto (FAO-56 esiste da decenni) fa la matematica; l’AI decide solo dove serve giudizio. Nove dollari l’anno.
  2. HTTP 200 mente. Mai fidarsi dello status code con hardware proprietario: verifica lo stato reale.
  3. Il degrado silenzioso è il nemico. Per notti intere un fallback invisibile mascherava un JSON troncato. Ora tutto fallisce a voce alta.

Prossimo capitolo: una piccola parte di visione — una camera che guarda il prato — come segnale in più, mai come verità assoluta.