Intermedio Nuovo

Sensore di livello per cisterna con ESPHome

Quanta acqua è rimasta nel serbatoio? Con pochi componenti e un Wemos D1 mini costruisci un sensore a ultrasuoni che porta il livello in percentuale dentro Home Assistant, senza cloud e senza abbonamenti. Codice pronto da copiare, e soprattutto la parte che quasi nessuna guida spiega: come tarare i valori sul tuo serbatoio.

Sensore di livello per cisterna con ESPHome e Home Assistant
Immagine generata con intelligenza artificiale
~45 minuti Wemos D1 mini Ultrasuoni Home Assistant letture

1 Cosa costruiamo

Un sensore a ultrasuoni montato sulla parte superiore del serbatoio misura la distanza fino al pelo dell'acqua. Più acqua c'è, più la superficie è vicina al sensore. Un Wemos D1 mini legge quella distanza, la converte in percentuale e la pubblica in Home Assistant via Wi-Fi.

Principio di funzionamento
        ┌──────────────────────────┐
        │  [D1 mini] + [ultrasuoni]│  ← in alto
        └────────────┬─────────────┘
                     │
              22 cm  │  ← distanza a serbatoio PIENO  = 100%
                     ▼
        ~~~~~~~~ pelo dell'acqua ~~~~~~~~
                     │
                     │  corsa utile 68 cm
                     │
        ─────────────▼─────────────
              90 cm = fondo asciutto = 0%

        percentuale = (vuoto - misura) / (vuoto - pieno) x 100

I due numeri 22 e 90 sono quelli del mio serbatoio: i tuoi saranno diversi, e il capitolo Taratura spiega come misurarli. È il passaggio che fa la differenza tra un sensore che funziona e uno che mostra numeri plausibili ma sbagliati.

Il progetto funziona con qualsiasi contenitore a sezione costante: cisterne d'acqua piovana, serbatoi da irrigazione, taniche. Per contenitori a sezione variabile la percentuale resta corretta come altezza, ma non è proporzionale al volume.

2 Lista della spesa

Poche cose, tutte facili da trovare. I link sono affiliati: se acquisti da lì supporti queste guide senza costi aggiuntivi per te.

Wemos D1 mini (ESP8266)

Wemos D1 mini (ESP8266)

Il cervello del progetto. Wi-Fi integrato, si programma via USB.

Sensore JSN-SR04T
Consigliato

Sensore JSN-SR04T

Trasduttore sigillato su cavo, resiste all'umidità. La scelta giusta per un serbatoio chiuso.

Cavetti Dupont femmina
Consigliato

Cavetti Dupont femmina

Per collegare sensore e scheda senza saldare.

Il JSN-SR04T ha il trasduttore sigillato e impermeabile, montato su un cavo separato dalla scheda. È la caratteristica che conta in una cisterna, dove l'umidità è al 100% e la condensa si deposita su tutto: è il sensore adatto a questo progetto e a qualsiasi installazione dentro un contenitore chiuso o all'aperto.
Cosa do per scontato. Che tu abbia gia Home Assistant installato e funzionante, e ESPHome installato e integrato in Home Assistant (come add-on, container o su macchina dedicata, indifferente). Se parti da zero su questi due, sistemali prima: qui si parte dal dashboard ESPHome gia aperto. Guida scritta e testata su ESPHome 2026.5.2.
Guarda i prodotti che ho recensito

Su teotek.me trovi tutti i componenti del mio setup Home Assistant, con recensioni e link affiliati che supportano queste guide gratuitamente.

3 Cablaggio

Quattro fili. Il sensore ha i pin serigrafati nell'ordine VCC · Trig · Echo · GND.

Collegamenti
  Filo sensore      Pin Wemos      GPIO
  ─────────────────────────────────────────
  VCC        ─────►    5V            —
  Trig       ─────►    D1          GPIO5
  Echo       ─────►    D2          GPIO4
  GND        ─────►    G             —
Wemos D1 mini cablato al sensore dentro una scatola stampata in 3D
Il D1 mini cablato, in una scatola stampata in 3D. Il LED blu acceso è l'heartbeat.
Trasduttore a ultrasuoni montato attraverso il coperchio della cisterna
Il trasduttore fissato in alto, rivolto verso il basso.
VCC va sui 5V, non sui 3V3. Il sensore a ultrasuoni ha bisogno di 5 volt: alimentandolo a 3,3 V o non parte affatto, o dà letture erratiche e intermittenti che sembrano un guasto del sensore.
Trig ed Echo non sono intercambiabili, e invertirli è il primo errore da cercare. Trig è un ingresso del sensore (lo pilota l'ESP per far partire la misura), Echo è un'uscita (risponde con un impulso lungo quanto il tempo di volo). Se li scambi, il pin echo non riceve mai alcun segnale e nei log leggi Measurement start timed out — che è esattamente lo stesso messaggio che daresti con il sensore scollegato o rotto. Ho perso mezza giornata su questo: se non legge, prima di sospettare l'hardware, scambia i due fili.
Perché D1 e D2. Sull'ESP8266, GPIO4 e GPIO5 sono gli unici due pin senza alcun ruolo di strapping all'avvio: usarli evita che il livello del filo al momento dell'accensione impedisca al chip di partire. Puoi usare altri pin, ma allora ricordati di cambiarli anche nel codice.

3.1 Dove montarlo

  • Il sensore va in alto, rivolto verso il basso, il più possibile perpendicolare alla superficie dell'acqua.
  • Deve vedere l'acqua senza ostacoli: tubi, galleggianti o pareti nel cono di emissione producono echi falsi.
  • Lascia almeno 20-25 cm tra sensore e livello massimo: sotto quella soglia i sensori a ultrasuoni hanno una zona cieca e non misurano.
Cisterna da 500 litri del progetto
La cisterna del progetto: 500 litri nominali.

4 Preparare ESPHome

Se non hai ancora ESPHome, il modo più semplice è l'add-on ufficiale dentro Home Assistant: Impostazioni → Add-on → ESPHome Device Builder → Installa. In alternativa gira come container Docker o in un LXC dedicato.

4.1 Il file secrets.yaml

Prima del device, crea il file secrets.yaml dal pulsante Secrets del dashboard. Serve a tenere password e chiavi fuori dal file di configurazione, così puoi condividere o pubblicare lo YAML senza esporre nulla.

secrets.yaml
wifi_ssid: "NomeDellaTuaReteWiFi"
wifi_password: "LaTuaPasswordWiFi"

# Password della rete di emergenza che il device crea se non trova il Wi-Fi.
# Minimo 8 caratteri.
wifi_ap_password: "UnaPasswordDiRiserva"

# Chiave di cifratura dell'API. GENERANE UNA TUA, non copiare questa riga:
# il dashboard ESPHome te ne propone una nuova a ogni device creato.
key_cisterna: "INCOLLA_QUI_LA_TUA_CHIAVE_GENERATA"
La chiave API deve essere tua e unica. Non copiarla da guide o forum: è ciò che cifra il traffico tra il device e Home Assistant. E se un domani la cambi, dovrai riconfigurare l'integrazione in Home Assistant.

4.2 Come si genera la chiave

Non è una password che puoi inventare battendo sulla tastiera. ESPHome pretende una stringa base64 che decodifichi in esattamente 32 byte: in pratica 44 caratteri che finiscono con =. Qualsiasi altra cosa viene rifiutata in validazione con Encryption key must be base64 and 32 bytes long.

Il modo più semplice è non generarla affatto: quando crei un nuovo device dal dashboard, ESPHome ne produce una da solo e te la scrive già nella configurazione. Ti basta spostarla in secrets.yaml.

Se preferisci un tool pronto, la documentazione ufficiale di ESPHome ne ha uno integrato: apri esphome.io/components/api e nella sezione sull'encryption trovi una chiave già generata, con i pulsanti Copy e Regenerate. La pagina la produce nel tuo browser a ogni caricamento, quindi non transita da nessun server.

Usa quello ufficiale, non un generatore qualsiasi trovato in rete. Se la chiave viene prodotta da un server remoto, quel server la conosce — e una chiave che conosce anche qualcun altro non sta cifrando più niente. Vale per questa come per qualsiasi chiave crittografica.

Se invece preferisci il terminale, la generi con uno di questi due comandi:

bash
# con openssl (Linux, macOS, WSL su Windows)
openssl rand -base64 32

# in alternativa con python, se openssl non e' disponibile
python3 -c "import secrets,base64;print(base64.b64encode(secrets.token_bytes(32)).decode())"

Il risultato ha questa forma, ed è quello che incolli in secrets.yaml:

output
QwctZqC7QrXxezuV......................EsEmPiO=   <- 44 caratteri
Quella qui sopra è volutamente troncata e non utilizzabile. Una chiave pubblicata in una guida non è più segreta per definizione: generane sempre una tua con i comandi qui sopra.

4.3 Creare il device

Dal dashboard ESPHome premi + New Device, dai un nome (io ho usato tank-500l), scegli ESP8266 come piattaforma e lascia che generi la configurazione base. Poi apri Edit e sostituisci tutto con il codice del capitolo seguente.

5 Il codice, commentato

Questo è il file completo, pronto da incollare. Le uniche due righe che devi cambiare sono dist_pieno e dist_vuoto in cima: sono i due numeri che descrivono il tuo serbatoio, e il capitolo successivo spiega come misurarli. Tutto il resto funziona così com'è.

cisterna.yaml
substitutions:
  # ─────────────────────────────────────────────────────────────
  #  GEOMETRIA DEL SERBATOIO — LE UNICHE DUE RIGHE DA CAMBIARE
  #  Distanze in METRI dal sensore al pelo dell'acqua.
  #  Come misurarle: vedi il capitolo "Taratura" della guida.
  # ─────────────────────────────────────────────────────────────
  dist_pieno: "0.22"   # distanza quando il serbatoio e' PIENO  -> 100%
  dist_vuoto: "0.90"   # distanza quando il serbatoio e' VUOTO  ->   0%

esphome:
  name: cisterna
  friendly_name: Cisterna
  # Blocca la compilazione con un errore chiaro se la tua versione di ESPHome
  # e' troppo vecchia per la sintassi "ota:" a lista usata piu' sotto.
  min_version: 2024.6.0

esp8266:
  board: d1_mini

logger:

api:
  encryption:
    key: !secret key_cisterna

ota:
  - platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Rete di emergenza: se il Wi-Fi non risponde entro 90 secondi il device
  # crea questo access point. E' l'unico modo per recuperarlo senza smontarlo
  # e ricollegarlo al PC via USB. Non ometterlo.
  ap:
    ssid: "Fallback Cisterna"
    password: !secret wifi_ap_password

captive_portal:

web_server:
  port: 80

sensor:
  - platform: ultrasonic
    id: distanza_grezza
    name: "Livello"
    icon: "mdi:water-percent"
    unit_of_measurement: "%"
    accuracy_decimals: 0
    update_interval: 12s

    filters:
      # 1) MEDIANA su 5 campioni (finestra da 60 s).
      #    L'acqua si muove: increspature e schiuma producono letture sballate.
      #    La mediana le scarta. Pubblica un valore al minuto, come prima,
      #    ma ogni valore e' il "centro" degli ultimi 5.
      #    I campioni falliti vengono ignorati: perche' il sensore risulti
      #    guasto devono fallire tutti e 5.
      - median:
          window_size: 5
          send_every: 5

      # 2) CONVERSIONE distanza -> percentuale, sui tuoi due numeri.
      - lambda: |-
          return (${dist_vuoto} - x) / (${dist_vuoto} - ${dist_pieno}) * 100.0;

      # 3) LIMITE 0-100. Senza questo, una lettura oltre il fondo
      #    produrrebbe percentuali negative in Home Assistant.
      - clamp:
          min_value: 0
          max_value: 100

    trigger_pin: GPIO5   # D1
    echo_pin: GPIO4      # D2
    timeout: 2.0m        # distanza massima misurabile: tienila sopra dist_vuoto

  # ── Diagnostica Wi-Fi ──
  - platform: wifi_signal
    name: "Segnale WiFi"
    id: segnale_wifi
    update_interval: 60s
    entity_category: "diagnostic"

  - platform: copy
    source_id: segnale_wifi
    name: "Segnale WiFi percentuale"
    filters:
      - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
    unit_of_measurement: "%"
    entity_category: "diagnostic"
    device_class: ""

# ── LED di stato ──
# Il LED blu saldato sulla scheda (GPIO2) diventa un "battito cardiaco".
# Non e' uno switch in Home Assistant: lo pilota solo l'interval qui sotto.
output:
  - platform: gpio
    id: led_onboard
    pin:
      number: GPIO2
      inverted: true

interval:
  #   1 lampo breve ogni 3s = tutto ok, sta parlando con Home Assistant
  #   2 lampi       ogni 3s = e' sul Wi-Fi ma HA non risponde
  #   1 lampo lungo ogni 3s = non e' connesso al Wi-Fi
  #   buio                  = device spento o guasto
  - interval: 3s
    then:
      - if:
          condition:
            api.connected:
          then:
            - output.turn_on: led_onboard
            - delay: 60ms
            - output.turn_off: led_onboard
          else:
            - if:
                condition:
                  wifi.connected:
                then:
                  - output.turn_on: led_onboard
                  - delay: 60ms
                  - output.turn_off: led_onboard
                  - delay: 200ms
                  - output.turn_on: led_onboard
                  - delay: 60ms
                  - output.turn_off: led_onboard
                else:
                  - output.turn_on: led_onboard
                  - delay: 600ms
                  - output.turn_off: led_onboard

switch:
  - platform: restart
    name: "Riavvia"

text_sensor:
  - platform: wifi_info
    ip_address:
      name: "Indirizzo IP"
    ssid:
      name: "Rete connessa"

Premi Install. Il device è vergine e non è ancora sulla rete, quindi il primo caricamento va fatto via USB. Da lì in poi ogni modifica si carica via Wi-Fi (OTA), senza più toccarlo fisicamente.

Quale opzione scegliere per il primo flash. Plug into this computer funziona solo se il browser gira sulla stessa macchina a cui hai collegato il Wemos. Se ESPHome è un add-on sul tuo server e tu stai lavorando dal portatile, quel pulsante non vede la porta USB: scegli Manual download, salva il file .bin e caricalo da web.esphome.io con Chrome o Edge (Firefox e Safari non supportano la Web Serial API).

Due dettagli che fanno perdere un'ora: serve il driver CH340 su Windows e macOS, e il cavo USB deve essere dati — molti cavi da ricarica non hanno i fili dati e il Wemos non compare da nessuna parte.
Perché il LED è un heartbeat e non l'indicatore standard. ESPHome ha un componente status_led che lampeggia quando c'è un problema e resta spento quando tutto va bene. Il difetto è che un LED spento diventa ambiguo: significa "tutto ok" oppure "device morto"? Con l'heartbeat il buio ha un solo significato, e ti accorgi che qualcosa non va semplicemente guardandolo.

6 Taratura: i tuoi due numeri

Qui si decide se il sensore ti dice la verità o solo qualcosa di verosimile. Ti servono due misure, prese dal sensore stesso e non dal metro: il trasduttore misura dal proprio corpo, e quel riferimento include qualche centimetro che il metro non vede.

6.1 Come leggere la distanza in metri

La percentuale non ti serve adesso: serve il dato grezzo. ESPHome lo scrive nei log a ogni ciclo, prima di qualsiasi filtro. Apri i Logs del device dal dashboard e cerca le righe:

log
[D][ultrasonic.sensor:087]: 'Livello' - Got distance: 0.220 m
[S][sensor]: 'Livello' >> 100 %
Log di ESPHome con le righe Got distance in metri
Le righe DEBUG con la misura grezza, una ogni 12 secondi

Got distance è la misura fisica in metri, ed è il numero che ti interessa. La riga sotto è il valore già convertito e filtrato che finisce in Home Assistant.

6.2 Misura a serbatoio pieno

Con il serbatoio al livello massimo, guarda i log e annota il valore di Got distance. Nel mio caso 0,220 m. Quel numero va in dist_pieno.

Col metro avevo misurato circa 20 cm, il sensore diceva 22. Ha ragione il sensore: è il suo sistema di riferimento quello che conta, non il tuo.

6.3 Misura a serbatoio vuoto

Serve la distanza dal sensore al fondo asciutto, e ci sono due modi.

Modo diretto, il più preciso: aspetta che il serbatoio sia vuoto (o svuotalo), leggi Got distance nei log e usa quel numero. Fine.

Modo indiretto, se non puoi svuotarlo. Il trucco è calcolare di quanto il sensore "sbaglia" rispetto al tuo metro, e riportare quella differenza sulla misura del fondo:

Ricavare dist_vuoto senza svuotare
1. A serbatoio pieno, leggi il sensore nei log        S_pieno = 0,220 m
2. Sempre a pieno, misura col metro dal sensore
   al pelo dell'acqua                                 M_pieno = 0,200 m
3. Calcola lo scarto del sensore                      offset  = S_pieno - M_pieno
                                                              = 0,020 m
4. Col metro (o un filo con un peso) misura dal
   sensore al fondo del serbatoio                     M_vuoto = 0,880 m
5. Somma lo scarto                                    dist_vuoto = M_vuoto + offset
                                                                 = 0,900 m

Nel mio caso è venuto 0,90 m, ed è il numero che va in dist_vuoto. Il passaggio 3 è quello che conta: senza correggere lo scarto ti porti dietro un errore fisso su tutta la scala.

Non usare la formula che gira nei forum. Molti esempi propongono qualcosa come (1 - x/2) * 100, che assume implicitamente due cose quasi sempre false: che il fondo sia a 2 metri esatti, e che a serbatoio pieno il sensore tocchi l'acqua. Nel mio caso quella formula dava 89% a serbatoio pieno e — molto peggio — 55% a serbatoio completamente asciutto, senza poter mai scendere sotto. Il livello sembrava plausibile e non lo era.

Con i due valori corretti la scala diventa quella vera:

Scala risultante (con i miei valori)
  distanza misurata        livello
  ─────────────────────────────────
      0,22 m        ──►     100%
      0,39 m        ──►      75%
      0,56 m        ──►      50%
      0,73 m        ──►      25%
      0,90 m        ──►       0%
Il timeout: 2.0m nel codice è la distanza massima misurabile, non un tempo. Deve restare comodamente sopra il tuo dist_vuoto: se il serbatoio è più profondo di 2 metri, alza quel valore, altrimenti a serbatoio quasi vuoto il sensore smette di rispondere e Home Assistant mostra unknown invece di 0%.

6.4 La prova del nove

Per confermare che la taratura sia corretta, apri un rubinetto o fai partire l'irrigazione per un paio di minuti tenendo i log aperti. Devi vedere la distanza crescere in modo regolare. Nel mio caso, 137 secondi di irrigazione hanno prodotto questo:

log
16:05:33  Got distance: 0.220 m     <- baseline, 11 letture identiche
16:05:45  Got distance: 0.225 m     <- apertura
16:06:09  Got distance: 0.241 m
16:06:45  Got distance: 0.267 m
16:07:21  Got distance: 0.283 m
16:07:57  Got distance: 0.305 m     <- chiusura
16:11:09  Got distance: 0.305 m     <- fermo, nessun rimbalzo
Entita del sensore in Home Assistant con lo storico del livello
Lo stesso svuotamento visto dallo storico di Home Assistant

Calo di 8,5 cm, pari al 12,5% della corsa utile, a una velocità costante di 3,55 cm al minuto. Da qui si ricava un dato pratico che vale più di molte dashboard: quel serbatoio si svuota in circa 18 minuti di irrigazione continua. Se conosci la portata in litri/minuto del tuo impianto, moltiplicandola per i minuti ottieni anche il volume reale del serbatoio — che quasi mai coincide con quello dichiarato sull'etichetta, perché il galleggiante chiude prima del tappo.

Seguimi su Telegram

Iscriviti al canale per non perderti le prossime guide, aggiornamenti su Home Assistant e le ultime novità dal mondo smart home.

7 Integrazione in Home Assistant

Appena il device si collega, Home Assistant lo trova da solo: Impostazioni → Dispositivi e servizi, comparirà una scheda ESPHome da configurare. Ti chiederà la chiave di cifratura — è quella che hai messo in secrets.yaml.

Le entità che ottieni:

  • sensor.cisterna_livello — il livello in percentuale
  • sensor.cisterna_segnale_wifi — potenza del segnale in dBm
  • sensor.cisterna_indirizzo_ip — utile perché l'IP è in DHCP e cambia
  • switch.cisterna_riavvia — riavvio remoto

7.1 L'automazione che conta davvero

Un livello sul cruscotto è carino, ma il vero motivo per costruire questo sensore è spegnere la pompa prima che vada a secco. Una pompa che gira senza acqua si danneggia in pochi minuti, e costa molto più di tutto questo progetto.

automations.yaml
- alias: "Cisterna quasi vuota - stop irrigazione"
  triggers:
    - trigger: numeric_state
      entity_id: sensor.cisterna_livello
      below: 10
  conditions:
    - condition: state
      entity_id: switch.valvola_irrigazione
      state: "on"
  actions:
    - action: switch.turn_off
      target:
        entity_id: switch.valvola_irrigazione
    - action: notify.persistent_notification
      data:
        title: "Cisterna quasi vuota"
        message: "Irrigazione interrotta: livello sceso sotto il 10%."
  mode: single
Sostituisci switch.valvola_irrigazione con l'entità vera del tuo impianto. È un nome di esempio che ho inventato: se incolli l'automazione così com'è, non farà nulla e non riceverai alcun errore. Il nome corretto lo trovi in Strumenti per sviluppatori → Stati.
Questa automazione funziona solo se la taratura è giusta, ed è il motivo per cui ho insistito tanto sul capitolo precedente. Con la formula sbagliata che avevo all'inizio, il sensore non scendeva mai sotto il 55%: la soglia del 10% non sarebbe mai scattata, e me ne sarei accorto solo con la pompa bruciata. Una protezione mal tarata è peggio di nessuna protezione, perché ti fa sentire al sicuro.

8 Troubleshooting

Nei log: Measurement start timed out
log
[W][ultrasonic.sensor:063]: 'Livello' - Measurement start timed out

Causa: il pin Echo non riceve alcun segnale. Nell'ordine: Trig ed Echo invertiti, un filo staccato, oppure VCC collegato a 3V3 invece che a 5V. Prima di sostituire il sensore, scambia i due fili di segnale: è il caso di gran lunga più frequente e il messaggio è identico a quello di un sensore rotto.

Nei log: Measurement pulse timed out

Causa: tutt'altra cosa. L'eco è partita ma non è tornata entro la distanza di timeout. Il cablaggio è a posto: o il serbatoio è più profondo del timeout impostato, o il fascio non trova una superficie da cui riflettersi. Alza timeout oppure verifica che il sensore punti davvero sull'acqua.

In Home Assistant il livello è unknown

Causa: con il filtro median attivo servono cinque letture fallite di fila perché compaia. Non è un glitch passeggero: è un guasto che dura da almeno un minuto. Vai direttamente ai log del device.

Il livello sembra bloccato troppo in alto

Causa: quasi sempre la taratura, non l'hardware. Se il valore non scende mai sotto una certa soglia per quanto si svuoti il serbatoio, i tuoi dist_pieno / dist_vuoto sono sbagliati. Rifai le misure del capitolo 6.

Letture instabili, che ballano di continuo

Causa: il sensore vede qualcosa che non è l'acqua — un tubo, un galleggiante, la parete — oppure è troppo vicino alla superficie e lavora nella zona cieca. Spostalo o allontanalo. In seconda battuta puoi allargare la finestra del median da 5 a 7-9 campioni.

Il device non si trova più in rete

Causa: l'IP è assegnato in DHCP e cambia dopo ogni riflash. Non fidarti dell'indirizzo che ricordi: controllalo nella scheda del dispositivo in Home Assistant, oppure guarda il LED. Se lampeggia una volta ogni 3 secondi è vivo e connesso, e il problema è solo che lo stai cercando all'indirizzo sbagliato.

Dopo qualche mese le letture peggiorano

Causa: quasi sempre la condensa depositata sui trasduttori, e succede se il sensore montato non è di tipo stagno. Non si recupera pulendolo: va sostituito con un sensore impermeabile come il JSN-SR04T. Il codice della guida resta identico, non cambia una riga.

Hai trovato utile questa guida?

Supporta il progetto visitando teotek.me per i link ai prodotti consigliati, e unisciti al canale Telegram per non perderti le prossime guide.

Il progetto, il montaggio e le misure sul campo sono miei: sensore installato sulla mia cisterna, taratura e prova di svuotamento fatte sul mio impianto. Il testo e la verifica del codice sono stati sviluppati con l'aiuto di Claude Code, con revisione finale mia.