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.
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.
┌──────────────────────────┐
│ [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.
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)
Il cervello del progetto. Wi-Fi integrato, si programma via USB.

Sensore JSN-SR04T
Trasduttore sigillato su cavo, resiste all'umidità. La scelta giusta per un serbatoio chiuso.
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.
Filo sensore Pin Wemos GPIO ───────────────────────────────────────── VCC ─────► 5V — Trig ─────► D1 GPIO5 Echo ─────► D2 GPIO4 GND ─────► G —
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.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.
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.
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"
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.
Se invece preferisci il terminale, la generi con uno di questi due comandi:
# 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:
QwctZqC7QrXxezuV......................EsEmPiO= <- 44 caratteri
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'è.
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.
.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.
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:
[D][ultrasonic.sensor:087]: 'Livello' - Got distance: 0.220 m
[S][sensor]: 'Livello' >> 100 %
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.
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:
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.
(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:
distanza misurata livello
─────────────────────────────────
0,22 m ──► 100%
0,39 m ──► 75%
0,56 m ──► 50%
0,73 m ──► 25%
0,90 m ──► 0%
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:
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
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.
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 percentualesensor.cisterna_segnale_wifi— potenza del segnale in dBmsensor.cisterna_indirizzo_ip— utile perché l'IP è in DHCP e cambiaswitch.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.
- 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
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.8 Troubleshooting
Measurement start timed out[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.
Measurement pulse timed outCausa: 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.
unknownCausa: 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.
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.
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.
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.
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.
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.
