Quando un lease DHCP “abbandonato” genera record DNS sbagliati: analisi di un caso reale con pfSense, Unbound e Pi‑hole

In molte reti basate su pfSense, è comune utilizzare:

  • pfSense come router/firewall e server DHCP
  • Unbound come DNS Resolver con registrazione dinamica
  • Pi‑hole come DNS primario per filtraggio e caching

Questa combinazione è potente, ma può generare problemi difficili da diagnosticare quando entrano in gioco:

  • hostname duplicati
  • lease DHCP scaduti ma non rimossi
  • record DNS dinamici generati da lease “abandoned”
  • cache DNS persistente di Pi‑hole

In questo articolo analizziamo un caso reale in cui un dispositivo IoT ha causato un conflitto DNS che ha portato a risoluzioni errate e incoerenti tra DHCP, DNS Resolver e Pi‑hole.

Sintomo del problema

Il comportamento osservato era il seguente:

  • DHCP mostrava Mini4 → 192.168.8.101
  • DNS Lookup mostrava Mini4 → 192.168.8.100
  • Pi‑hole rispondeva sempre con 100
  • pfSense rispondeva con 100 anche dopo reboot

Una situazione apparentemente impossibile: il lease attivo era corretto, ma il DNS continuava a servire un IP vecchio.

La causa reale: un lease DHCP “abbandonato” con hostname duplicato

Nel file reale dei lease di pfSense:

/var/dhcpd/var/db/dhcpd.leases

era presente questo blocco:

lease 192.168.8.100 {

  binding state abandoned;

  client-hostname “Mini4”;

}

Questo lease:

  • era scaduto
  • era “abandoned”
  • non compariva nella GUI
  • ma era ancora presente nel file
  • e conteneva l’hostname “Mini4”

Perché è un problema?

Perché Unbound genera i record DNS dinamici leggendo tutti i lease, anche quelli:

  • expired
  • abandoned
  • free
  • rewind

Di conseguenza, Unbound continuava a generare:

A record: mini4.local.lan → 192.168.8.100

PTR record: 100 → mini4.local.lan

anche se il lease attivo era 101.

La causa originale: un dispositivo IoT che si è spacciato per “Mini4”

Il dispositivo incriminato era un Aubess (wlan0), che:

  • non rispetta DHCP Option 12
  • non invia un hostname coerente
  • si presenta con nomi casuali
  • in questo caso si è presentato come “Mini4”

pfSense ha quindi creato un lease per:

192.168.8.100 → Mini4

Quando il Mac mini ha richiesto un IP, ha ottenuto:

192.168.8.101 → Mini4

Risultato: hostname duplicato.

Il secondo problema: la cache DNS di Pi‑hole

Pi‑hole aveva memorizzato:

mini4.local.lan → 192.168.8.100

Quindi:

  • anche dopo aver corretto pfSense
  • anche dopo aver cancellato il lease fantasma
  • anche dopo il reboot di pfSense

Pi‑hole continuava a rispondere con l’IP sbagliato.

Solo dopo:

pihole restartdns

la cache è stata svuotata e il DNS si è riallineato.

Perché il reboot di pfSense non ha risolto?

Perché pfSense non elimina i lease scaduti dal file.

Il file rimane identico.

E Unbound, al boot, ricostruisce i record DNS dinamici anche dai lease “abandoned”.

Quindi il record sbagliato veniva ricreato ogni volta.

Soluzione definitiva

1. Identificare il lease fantasma

cat /var/dhcpd/var/db/dhcpd.leases | grep -i mini4 -A5 -B5

2. Eliminare il blocco incriminato

vi /var/dhcpd/var/db/dhcpd.leases

Cancellare:

lease 192.168.8.100 { … }

3. Rigenerare i lease DHCP

GUI → DHCP Server → Save

4. Reload DNS Resolver

GUI → DNS Resolver → Save

5. Svuotare la cache di Pi‑hole

pihole restartdns

Risultato finale

  • Il dispositivo IoT ha preso un nuovo IP (es. 187)
  • Mini4 è rimasto correttamente su 101
  • Nessun hostname duplicato
  • Nessun record DNS sbagliato
  • pfSense e Pi‑hole perfettamente allineati

Conclusioni

Questo caso dimostra come un semplice dispositivo IoT con hostname errato possa:

  • generare un lease DHCP “abandoned”
  • sporcare il DNS dinamico di pfSense
  • creare record A/PTR sbagliati
  • essere ulteriormente amplificato dalla cache di Pi‑hole

La diagnosi richiede:

  • analisi del file reale dei lease
  • comprensione del comportamento di Unbound
  • gestione della cache DNS di Pi‑hole

È un problema raro, ma estremamente insidioso.

🦁 Fondamenti di Virtualizzazione: Tutto su Proxmox VE

1. Introduzione a Proxmox VE: Il Sistema Operativo Hyperconvergente

  • Cos’è: Proxmox Virtual Environment (VE) non è un semplice hypervisor, ma un sistema operativo completo basato su Debian Linux, progettato specificamente per la virtualizzazione enterprise.
  • Filosofia: Fornisce una piattaforma unificata, open-source e integrata per gestire macchine virtuali (VM) e container dallo stesso pannello di controllo web.
  • Modello di Distribuzione: Si installa direttamente sull’hardware server (bare-metal), posizionandosi tecnicamente nella categoria degli hypervisor Type 1.
  • Componenti Core: La sua potenza deriva dall’integrazione di due tecnologie:
    1. KVM (Kernel-based Virtual Machine): Per la virtualizzazione completa delle macchine virtuali.
    2. LXC (Linux Containers): Per la virtualizzazione leggera dei container.
  • Web Interface: Tutta la gestione avviene attraverso un’interfaccia web moderna (https://server-ip:8006), eliminando la necessità di software client dedicati su PC degli amministratori.

2. Architettura Tecnica e Componenti di Base

  • Base Solida: Poggia su un kernel Linux Debian stabilizzato e a lungo supporto, garantendo compatibilità hardware estrema e stabilità.
  • KVM (Kernel-based Virtual Machine):
    • Trasforma il kernel Linux in un hypervisor a tutti gli effetti (Type 1).
    • Utilizza le estensioni di virtualizzazione hardware della CPU (Intel VT-x o AMD-V) per ottenere performance native.
    • Gestisce direttamente l’accesso alla CPU e alla memoria delle VM.
    • Per l’I/O, può utilizzare:
      • Emulazione completa (e1000 per NIC, ide per dischi – lenta ma compatibile).
      • Paravirtualizzazione (virtio-net per NIC, virtio-blk per dischi – alta performance, richiede driver nel guest).
      • PCI Passthrough (VT-d/AMD-Vi) per assegnare hardware fisico direttamente a una VM.
  • LXC (Linux Containers):
    • Tecnologia di virtualizzazione a livello di sistema operativo.
    • I container condividono il kernel host, risultando in overhead quasi nullo, avvio istantaneo e densità molto elevata.
    • Ideale per servizi isolati che non richiedono un kernel personalizzato o driver specifici (es.: web server, database, reverse proxy, applicazioni in container).
  • Storage Management:
    • Supporta nativamente una vasta gamma di backend di storage:
      • Locale: ZFS (consigliato), LVM-Thin, Directory, XFS.
      • Di Rete: NFS, CIFS/SMB.
      • Shared/Cluster: Ceph (integrato nativamente), iSCSI, Fibre Channel.
    • ZFS Integration: Il supporto integrato per ZFS è un punto di forza, offrendo feature enterprise come copy-on-write, snapshot atomiche, compressione, deduplicazione e riparazione dei dati automatica (self-healing).
  • Networking:
    • Basato su Linux bridging e VLANs (802.1q).
    • Supporta Bonding/LAG (aggregazione di schede di rete) per ridondanza e aumento di banda.
    • Software-Defined Networking (SDN): Dalla versione 7.0, introduce un SDN nativo per gestire reti overlay (VXLan) e sotto-sistemi di rete più complessi in modo centralizzato.

3. Il Cuore del Cluster: High Availability e Gestione Centralizzata

  • Modello a Cluster:
    • Più nodi Proxmox (server fisici) possono essere uniti in un cluster tramite Corosync.
    • Il cluster appare come una singola entità gestibile dall’interfaccia web.
    • Tutte le configurazioni (VM, storage, rete) sono replicate in tempo reale tra i nodi.
  • High Availability (HA) Integrato:
    • Feature fondamentale per l’enterprise.
    • Se un nodo del cluster si spegne o ha un malfunzionamento hardware, le VM e i container contrassegnati come “HA” vengono automaticamente riavviati su un altro nodo del cluster in pochi secondi/minuti.
    • Il sistema monitora lo stato dei nodi e delle VM e prende decisioni senza intervento umano.
  • Live Migration:
    • Migrazione a Caldo (Live Migration): Permette di spostare una VM in esecuzione da un nodo all’altro senza downtime percepibile. Essenziale per la manutenzione dell’hardware e il bilanciamento del carico.
    • Migrazione delle Storage: Permette di spostare i dischi di una VM (anche in esecuzione) da uno storage backend a un altro (es.: da storage locale a Ceph).

4. Vantaggi Chiave di Proxmox VE

  1. Open Source e Costo Zero: La licenza è GNU AGPL, v3. Non ci sono costi di licensing per le funzionalità core. Il modello di subscription esiste solo per l’accesso ai repository enterprise stabili e al supporto tecnico.
  2. Ecosistema Completo: Tutto ciò che serve è integrato: virtualizzazione, storage (ZFS, Ceph), networking, clustering, HA. Niente più “vendor lock-in” o integrazioni complesse tra software diversi.
  3. Alta Performance: L’uso di KVM e LXC garantisce performance pari o molto vicine al bare-metal.
  4. Flessibilità Operativa: La scelta tra VM (per carichi di lavoro che necessitano di un OS completo) e Container (per leggerezza e densità) offre la massima flessibilità di deployment.
  5. Gestione Unificata: L’interfaccia web centrale semplifica enormemente la gestione di intere infrastrutture, riducendo la curva di apprendimento e gli errori di configurazione.
  6. Comunità Attiva e Supporto Enterprise: Una vasta comunità offre supporto gratuito nei forum. Per ambienti critici, è disponibile un supporto professionale a pagamento.

5. Considerazioni e Best Practices

  • Hardware Consigliato:
    • CPU: 64-bit (x86-64) con supporto per virtualizzazione hardware (Intel VT-x/AMD-V). Per il PCI Passthrough, serve IOMMU (Intel VT-d/AMD-Vi).
    • RAM: Più è, meglio è. Calcolare il bisogno per l’OS host + (RAM per VM) + (RAM per Ceph/ZFS se usati).
    • Storage: SSD NVMe sono fortemente consigliati per disco di sistema e per le VM. Gli HDD sono adatti solo per storage di archivio.
    • Rete: Schede multiporta Gigabit o, meglio, 10Gbps+ sono essenziali per il traffico di migrazione, Ceph e VM.
  • Pianificazione dello Storage:
    • ZFS locale è un’ottima scelta per un singolo nodo o per un cluster con storage non condiviso.
    • Ceph è la soluzione per creare un storage iper-convergente e ridondante distribuito su più nodi. Richiede almeno 3 nodi per la produzione.
  • Sicurezza:
    • Cambiare la password di root e la porta SSH di default.
    • Configurare firewall basato su pve-firewall o iptables/nftables.
    • Isolare la rete di management del cluster da quella delle VM.
    • Tenere aggiornato il sistema applicando gli aggiornamenti dai repository.

6. Conclusione: Perché Scegliere Proxmox VE?

Proxmox VE si è affermato come una soluzione enterprise-grade alternativa totalmente percorribile a costosi stack commerciali come VMware vSphere. La sua natura open-source, l’incredibile set di funzionalità integrate e la straordinaria stabilità lo rendono la scelta ideale per:

  • PMI che necessitano di un’infrastruttura virtuale robusta senza costi di licensing proibitivi.
  • Entusiasti e Homelab per imparare tecnologie enterprise a costo zero.
  • Aziende di qualsiasi dimensione che vogliono evitare il vendor lock-in e costruire un’infrastruttura moderna, flessibile e scalabile basata su standard aperti.

È uno degli esempi più riusciti di come il software open-source possa non solo competere, ma spesso superare, le soluzioni commerciali in termini di features, flessibilità e rapporto qualità/prezzo.

Guida Completa ai Gateway Ecowitt GW1100 / GWI100: Installazione, Configurazione e Gestione dei Sensori

I gateway Ecowitt della serie GWI100 / GW1100 rappresentano il cuore di una stazione meteo personale moderna. Questi dispositivi raccolgono i dati provenienti dai sensori wireless Ecowitt e li inviano sia alla rete locale sia ai principali servizi meteorologici online. In questa guida completa scoprirai come configurarli, come collegare i sensori e come sfruttare tutte le funzioni disponibili.


Cos’è il Gateway Ecowitt GW1100 / GWI100

Il GW1100 è un piccolo hub Wi‑Fi che riceve i dati dai sensori Ecowitt (temperatura, umidità, vento, pioggia, UV, luminosità, ecc.) e li rende disponibili:

  • tramite pagina web interna
  • tramite app Ecowitt
  • sul portale ecowitt.net
  • su servizi meteo esterni come Wunderground, WeatherCloud, WOW
  • su server personalizzati

È compatto, facile da configurare e compatibile con un’ampia gamma di sensori Ecowitt.


1. Prima Accensione e Accesso al Dispositivo

1.1 Avvio del gateway

Dopo aver alimentato il dispositivo:

  • attendi l’accensione
  • premi il pulsante RESET per 5 secondi per entrare in modalità configurazione

1.2 Connessione al Wi‑Fi del gateway

Il dispositivo crea un access point temporaneo:

GWI100X-XXXXXXXX

Collegati con il tuo smartphone.

1.3 Accesso alla pagina web

Apri il browser e digita:

192.168.4.1

Al primo accesso non è richiesta alcuna password.


2. Configurazione del Dispositivo

2.1 Impostazioni principali

Dalla pagina Device Setting puoi configurare:

  • Nome del dispositivo
  • Fuso orario
  • Localizzazione
  • Unità di misura
  • Password di accesso (opzionale)

Se abiliti Auto Timezone, ricordati di impostare il fuso orario corretto anche su ecowitt.net.


3. Collegamento alla Rete Wi‑Fi Domestica

3.1 Configurazione rete locale

Nella sezione Local Network:

  1. seleziona la tua rete Wi‑Fi
  2. inserisci la password
  3. premi Apply
  4. verifica che il gateway abbia ottenuto un indirizzo IP

4. Invio dei Dati ai Servizi Meteo

Il gateway può inviare i dati a diversi servizi online.

4.1 Ecowitt.net

È il servizio principale e più completo.

Per configurarlo:

  • copia il MAC address del gateway
  • registrati su ecowitt.net
  • aggiungi il dispositivo
  • imposta l’intervallo di upload (minimo 1 minuto)

4.2 Wunderground, WeatherCloud, WOW

Per ciascun servizio devi inserire:

  • Station ID
  • Station Key

4.3 Server personalizzato

Puoi inviare i dati anche a un tuo server:

  • protocollo Ecowitt o Wunderground
  • IP/hostname
  • path (es. /data/report/)
  • porta
  • intervallo di upload

5. Gestione dei Sensori Ecowitt

Il gateway supporta numerosi sensori. Ecco i principali:

5.1 Sensori di temperatura e umidità

  • WH31: multicanale (fino a 8 canali)
  • WH32: sensore esterno prioritario

5.2 Sensori di pioggia

  • WH40: pluviometro con livella a bolla

5.3 Sensori di vento

  • WS68: anemometro + UV + luce
  • WS80: anemometro ultrasonico

5.4 Sensori combinati

  • WS69: array completo (vento, pioggia, T/H, UV, luce)

6. Installazione dei Sensori

Il manuale raccomanda:

  • installare i sensori in giornate asciutte
  • verificare la ricezione del segnale prima del montaggio definitivo
  • evitare ostacoli metallici
  • montare il sensore del vento in posizione elevata
  • mantenere il pluviometro perfettamente in bolla

7. Avvertenze Importanti

  • Non installare durante un temporale
  • I pali metallici possono attirare fulmini
  • Valuta la messa a terra se monti su un edificio
  • Esegui test preliminari a terra

Conclusione

Il gateway Ecowitt GW1100/GWI100 è uno strumento potente e versatile per creare una stazione meteo completa e affidabile. Con pochi passaggi puoi configurarlo, collegarlo alla rete, aggiungere sensori e inviare i dati ai principali servizi meteorologici.

Questa guida ti permette di sfruttare al massimo tutte le funzioni del dispositivo.

template:
  - sensor:
      - name: "FV Potenza Teorica Reale"
        unique_id: fv_potenza_teorica_reale
        unit_of_measurement: "W"
        device_class: power
        state_class: measurement
        state: >
          {% set G_raw = states('sensor.gw1100a_solar_radiation') %}
          {% set T_raw = states('sensor.gw1100a_outdoor_temperature') %}
          {% if G_raw in ['unknown','unavailable','none'] or T_raw in ['unknown','unavailable','none'] %}
            unavailable
          {% else %}
            {% set modules_count = 4 %}
            {% set P_stc_module = 585.0 %}
            {% set area_module = 2.582 %}
            {% set eta = 0.2266 %}
            {% set noct = 45 %}
            {% set gamma_p = -0.003 %}
            {% set tilt_deg = 30 %}
            {% set albedo = 0.20 %}
            {% set sensor_height_factor = 1.15 %}
            {% set G = (G_raw | float(0)) * sensor_height_factor %}
            {% set T_amb = (T_raw | float(20)) %}
            {% set _ = states('sensor.time') %}
            {% set area_total = area_module * modules_count %}
            {% set clearness = G / 1000.0 %}
            {% set df_raw = 0.45 + (1 - clearness) * 0.35 %}
            {% set diffuse_frac = [df_raw, 0.9] | min %}
            {% set diffuse_frac = [diffuse_frac, 0.1] | max %}
            {% set direct_frac = 1.0 - diffuse_frac %}
            {% set tilt_rad = (tilt_deg * pi / 180.0) %}
            {% set direct_factor = [ (cos(tilt_rad)), 0.0 ] | max %}
            {% set diffuse_factor = (1.0 + cos(tilt_rad)) / 2.0 %}
            {% set ground_reflect_factor = (1.0 - cos(tilt_rad)) / 2.0 %}
            {% set G_poa = G * ( direct_frac * direct_factor + diffuse_frac * diffuse_factor + albedo * ground_reflect_factor ) %}
            {% set P_stc_total = P_stc_module * modules_count %}
            {% set P_from_irradiance = (G_poa / 1000.0) * P_stc_total %}
            {% set T_cell = T_amb + (G_poa / 800.0) * (noct - 20.0) %}
            {% set deltaT = T_cell - 25.0 %}
            {% set P_temp_corr = P_from_irradiance * (1.0 + gamma_p * deltaT) %}
            {{ [ (P_temp_corr) | round(0), 0 ] | max }}
          {% endif %}

Il Motore della Previsione Solare: Un’Analisi Passo-Passo delle Formule dietro i Pannelli Fotovoltaici ☀️🔬


👋 Introduzione: Quando la Tecnologia Prevede il Futuro

Hai mai voluto sapere non solo quanta energia produce oggi il tuo impianto solare, ma quanto potrebbe produrre nel momento esatto? Questo è il potere dei “Template” avanzati che utilizziamo nei sistemi di casa intelligente. Questi Template sono veri e propri mini-calcolatori, scritti in un linguaggio chiamato Jinja2, capaci di trasformare semplici letture meteo (come l’intensità del sole o la temperatura) in una previsione molto dettagliata della potenza reale prodotta dal tuo impianto FV.

In questo articolo, smonteremo questa “ricetta digitale” per capire esattamente quali formule vengono usate e perché ogni piccolo numero conta. Prepariamoci a fare un viaggio didattico tra fisica e tecnologia!

🧱 La Struttura: Ingredienti e Obiettivo

Il template ha due compiti principali:

  1. Controllare la Salute: Verificare che tutti i dati di ingresso siano presenti e validi.
  2. Simulare la Produzione: Usando le leggi della fisica, calcolare quanta energia il pannello riceve e quanta ne riesce a trasformare.

Gli Ingredienti Necessari (I Dati Iniziali)

Il sistema parte da due letture chiave dai sensori esterni:

  • GrawGraw​: L’intensità della luce solare che arriva sull’area di misura (Irradiazione GlobaleIrradiazione Globale).
  • TrawTraw​: La temperatura dell’aria all’esterno.

📐 Analisi Passo-Passo: Le Formule Semplificate (Il Cuore del Template)

Il template non fa un singolo calcolo, ma una catena di trasformazioni. Vediamo i passaggi chiave e le formule che li guidano.

Fase 1: Preparazione della Luce

Prima di tutto, dobbiamo capire quanta luce è davvero utile per il pannello e come questa luce cade su di esso.

  • Aggiustamento dell’Altezza (GG): Il template prende l’intensità grezza (GrawGraw​) e la corregge leggermente con un fattore (sensor_height_factor = 0.98). Cosa significa? È una piccola correzione per tenere conto del fatto che il sensore non è perfettamente allineato o misurato alla stessa altezza dell’impianto reale. G=Graw×0.98G=Graw​×0.98
  • La Distribuzione della Luce (DiffuseDiffuse vs DirectDirect): Il sole non è sempre un fascio dritto e perfetto; a volte la luce viene “sparsa” dall’atmosfera.
    • Il template calcola una frazione diffusa basandosi su quanto forte sta brillando il cielo (clearnessclearness). Questa frazione ci dice quanta luce arriva da tutte le direzioni, non solo dal punto esatto del sole.
    • Successivamente, usa i dati di orientamento (tilt_deg=30tilt_deg=30∘) per capire quanto è efficace la superficie del pannello a catturare quella luce diretta e diffusa.

Fase 2: Calcolare l’Energia Reale sul Pannello (GPOAGPOA​)

Questo è il passaggio fondamentale! Stiamo chiedendo: “Quanta energia colpisce davvero la superficie orientata del pannello?”

Il template mescola le frazioni di luce diretta, diffusa e riflessa dal terreno (l’albedo) con i fattori geometrici legati all’angolo di inclinazione ($30^\circ$). La formula combina questi elementi per ottenere GPOAGPOA​:

GPOA=G×(Frazione Diretta×Fattore Diretto+Frazione Diffusa×Fattore Diffuso+Riflesso Terreno×Fattore Riflesso)GPOA​=G×(Frazione Diretta×Fattore Diretto+Frazione Diffusa×Fattore Diffuso+Riflesso Terreno×Fattore Riflesso)

In parole semplici: Stiamo dando al sistema una mappa precisa di come la luce cade sul pannello, tenendo conto sia dell’orientamento che del tipo di cielo.

Fase 3: La Potenza Iniziale (Dal Calore alla Forza)

Una volta che sappiamo quanta energia colpisce il pannello (GPOAGPOA​), dobbiamo trasformarla in Watt. Usiamo la potenza nominale massima (PSTC_totalPSTC_total​, ovvero quanto produce l’impianto in condizioni perfette di laboratorio) e la confrontiamo con l’irradiazione ricevuta:

Potenza Iniziale=(GPOA1000.0)×PSTC_totalPotenza Iniziale=(1000.0GPOA​​)×PSTC_total​ (Il diviso per 1000 serve perché le nostre misurazioni sono spesso in W/m2W/m2, mentre la potenza nominale è espressa su tutta l’area del modulo).

Fase 4: La Correzione Finale (L’Effetto della Temperatura)

Questo è il tocco di finitura. Anche se tutto va bene, quando i pannelli si scaldano troppo, diventano meno efficienti. Il template simula questo calore e applica una “penalità” basata sul delta termico (ΔT=Temperatura Cella25CΔT=Temperatura Cella−25∘C).

La formula di correzione finale è questa: Potenza Corretta=Potenza Iniziale×(1+γpΔT)Potenza Corretta=Potenza Iniziale×(1+γp​⋅ΔT)

Spiegazione Semplice: Se il pannello si scalda molto (ΔTΔT alto), e γpγp​ è un numero negativo, la parentesi (1+...)(1+…) diventa minore di 1, riducendo artificialmente la potenza calcolata. Questo assicura che la nostra previsione sia realistica.

✅ Conclusione: Un Modello Potente ma Semplificato

Il template è estremamente potente perché automatizza un processo che, se lo facessi a mano, richiederebbe ore di calcoli trigonometrici e termici!

Cosa abbiamo imparato? Abbiamo visto che il valore finale in Watt non dipende solo da “quanto c’è sole”, ma da: come cade la luce sul mio pannello (GPOAGPOA​), e quanto è caldo quel pannello (Correzione Termica).

È un esempio brillante di come i dati, quando vengono incartati con una buona dose di logica scolastica e fisica, possano trasformarsi in strumenti di previsione super diretti per la nostra vita quotidiana.


Codice calcolo potenza presunta pannelli

template:
  - sensor:
      - name: "FV Potenza Teorica Reale"
        unique_id: fv_potenza_teorica_reale
        unit_of_measurement: "W"
        device_class: power
        state_class: measurement
        state: >
          {% set G_raw = states('sensor.gw1100a_solar_radiation') %}
          {% set T_raw = states('sensor.gw1100a_outdoor_temperature') %}
          {% if G_raw in ['unknown','unavailable','none'] or T_raw in ['unknown','unavailable','none'] %}
            unavailable
          {% else %}
            {% set modules_count = 4 %}
            {% set P_stc_module = 585.0 %}
            {% set area_module = 2.582 %}
            {% set eta = 0.2266 %}
            {% set noct = 45 %}
            {% set gamma_p = -0.003 %}
            {% set tilt_deg = 30 %}
            {% set albedo = 0.20 %}
            {% set sensor_height_factor = 0.98 %}
            {% set G = (G_raw | float(0)) * sensor_height_factor %}
            {% set T_amb = (T_raw | float(20)) %}
            {% set _ = states('sensor.time') %}
            {% set area_total = area_module * modules_count %}
            {% set clearness = G / 1000.0 %}
            {% set df_raw = 0.45 + (1 - clearness) * 0.35 %}
            {% set diffuse_frac = [df_raw, 0.9] | min %}
            {% set diffuse_frac = [diffuse_frac, 0.1] | max %}
            {% set direct_frac = 1.0 - diffuse_frac %}
            {% set tilt_rad = (tilt_deg * pi / 180.0) %}
            {% set direct_factor = [ (cos(tilt_rad)), 0.0 ] | max %}
            {% set diffuse_factor = (1.0 + cos(tilt_rad)) / 2.0 %}
            {% set ground_reflect_factor = (1.0 - cos(tilt_rad)) / 2.0 %}
            {% set G_poa = G * ( direct_frac * direct_factor + diffuse_frac * diffuse_factor + albedo * ground_reflect_factor ) %}
            {% set P_stc_total = P_stc_module * modules_count %}
            {% set P_from_irradiance = (G_poa / 1000.0) * P_stc_total %}
            {% set T_cell = T_amb + (G_poa / 800.0) * (noct - 20.0) %}
            {% set deltaT = T_cell - 25.0 %}
            {% set P_temp_corr = P_from_irradiance * (1.0 + gamma_p * deltaT) %}
            {{ [ (P_temp_corr) | round(0), 0 ] | max }}
          {% endif %}

✨ Sistema RFID con ESP32, RC522, OLED, Relè, LED e Buzzer

Una guida completa per realizzare un accesso elettronico affidabile, elegante e totalmente integrato con ESPHome.

🔧 Componenti necessari

  • ESP32 DevKit
  • Lettore RFID RC522 (SPI)
  • Display OLED SSD1306 128×64 (I2C)
  • Modulo relè 5V
  • LED verde + LED rosso (con resistenze 220–330 Ω)
  • Buzzer attivo a 3 pin
  • Alimentazione 5V
  • Cavi dupont

🧩 Collegamenti elettrici

Tabella completa dei collegamenti ESP32 → Componenti

RC522 (SPI)

Funzione RC522Pin RC522Pin ESP32
AlimentazioneVCC3.3V
MassaGNDGND
ResetRSTGPIO22
SS / SDASDAGPIO5
SCKSCKGPIO18
MOSIMOSIGPIO23
MISOMISOGPIO19


Display OLED SSD1306 (I2C)

Funzione OLEDPin OLEDPin ESP32
AlimentazioneVCC3.3V
MassaGNDGND
SDASDAGPIO21
SCLSCLGPIO17


LED di stato

LEDAnodo → ESP32Catodo →
VerdeGPIO14 (via resistenza)GND
RossoGPIO27 (via resistenza)GND


Relè cancello

FunzionePin modulo relèPin ESP32
SegnaleINGPIO32
AlimentazioneVCC5V
MassaGNDGND


Buzzer attivo 3 pin

FunzionePin buzzerPin ESP32
AlimentazioneVCC5V
MassaGNDGND
SegnaleI/OGPIO26

💡 Funzionamento del sistema

  • Quando un tag RFID viene avvicinato, l’ESP32 legge l’UID tramite il modulo RC522.
  • Se il tag è autorizzato:
    • Il relè invia un impulso per aprire il cancello
    • Il LED verde si accende per 7 secondi
    • Il display mostra “ACCESSO CONSENTITO”
    • Il buzzer emette due beep
  • Se il tag non è autorizzato:
    • Il LED rosso si accende per 7 secondi
    • Il display mostra “ACCESSO NEGATO”
    • Il buzzer emette tre beep rapidi

🧠 Codice ESPHome completo e funzionante

esphome:
  name: esphome-web-568b34
  friendly_name: Esp32 RFID
  min_version: 2025.11.0
  name_add_mac_suffix: false
  on_boot:
    priority: -100
    then:
      - switch.turn_on: rele

esp32:
  variant: esp32
  framework:
    type: esp-idf
    advanced:
      minimum_chip_revision: "3.1"
      sram1_as_iram: true    

logger:
  level: INFO

api:

ota:
  platform: esphome

wifi:
  ssid: Xxxx
  password: xxxx

# ---------------- SPI + RC522 ----------------
spi:
  clk_pin: 18
  mosi_pin: 23
  miso_pin: 19

rc522_spi:
  cs_pin: 5
  reset_pin: 22
  update_interval: 500ms #valore iniziale 1 sc
  on_tag:
    then:
      - lambda: |-
          std::string uid = x;

          uint32_t now = millis();
          if (now - id(ultimo_lettura) < 2000) {
            return;
          }
          id(ultimo_lettura) = now;

          ESP_LOGI("rfid", "Tag rilevato: %s", uid.c_str());

          id(ultimo_tag).publish_state(uid);

          for (auto &t : id(tag_autorizzati)) {
            if (t == uid) {
              ESP_LOGI("rfid", "Accesso consentito");

              id(accesso_consentito).publish_state(true);
              id(accesso_negato).publish_state(false);

              id(led_ok).execute();
              id(display_ok).execute();

              return;
            }
          }

          ESP_LOGW("rfid", "Accesso negato");

          id(accesso_consentito).publish_state(false);
          id(accesso_negato).publish_state(true);

          id(led_ko).execute();
          id(display_ko).execute();

# ---------------- Lista badge autorizzati ----------------
globals:
  - id: tag_autorizzati
    type: std::vector<std::string>
    initial_value: '{"67-89-90-C9","04-52-DD-01-7A-3B-03","67-FA-0A-D9"}'

  - id: ultimo_lettura
    type: uint32_t
    initial_value: '0'

# ---------------- OUTPUT (LED) ----------------
output:
  - platform: gpio
    pin: 14
    id: led_verde_pin

  - platform: gpio
    pin: 27
    id: led_rosso_pin
  
# ---------------- Script vari ----------------
script:
  - id: led_ok
    then:
      - switch.turn_on: buzzer
      - delay: 120ms
      - switch.turn_off: buzzer
      - delay: 120ms
      - switch.turn_on: buzzer
      - delay: 120ms
      - switch.turn_off: buzzer

      - switch.turn_off: rele
      - delay: 2s
      - switch.turn_on: rele

      - light.turn_on: led_verde
      - delay: 7s
      - light.turn_off: led_verde
      - lambda: |-
          id(accesso_consentito).publish_state(false);

  - id: led_ko
    then:
      - switch.turn_on: buzzer
      - delay: 80ms
      - switch.turn_off: buzzer
      - delay: 80ms
      - switch.turn_on: buzzer
      - delay: 80ms
      - switch.turn_off: buzzer
      - delay: 80ms
      - switch.turn_on: buzzer
      - delay: 80ms
      - switch.turn_off: buzzer

      - light.turn_on: led_rosso
      - delay: 7s
      - light.turn_off: led_rosso
      - lambda: |-
          id(accesso_negato).publish_state(false);

  - id: display_ok
    then:
      - display.page.show: page_ok
      - delay: 8s
      - display.page.show: page_idle

  - id: display_ko
    then:
      - display.page.show: page_ko
      - delay: 8s
      - display.page.show: page_idle
  
# ---------------- LED verde e rosso ----------------
light:
  - platform: binary
    id: led_verde
    name: "LED Verde RFID"
    output: led_verde_pin

  - platform: binary
    id: led_rosso
    name: "LED Rosso RFID"
    output: led_rosso_pin

# ---------------- RELÈ ----------------
switch:
  - platform: gpio
    id: rele
    name: "Relè Cancello"
    pin:
      number: 32
      inverted: true
    restore_mode: ALWAYS_ON

  - platform: gpio
    id: buzzer
    name: "Buzzer RFID"
    pin:
      number: 26
    restore_mode: ALWAYS_OFF

# ---------------- SENSORI RFID ----------------
text_sensor:
  - platform: template
    id: ultimo_tag
    name: "Ultimo TAG RFID"
  - platform: wifi_info
    ip_address:
      name: "ESP32 IP Address"
    ssid:
      name: "ESP32 WiFi SSID"
  - platform: version
    name: "Firmware ESPHome"           

button:
  - platform: restart
    id: riavvia_esp
    name: "Riavvia Rfid ESP32"  

  - platform: template
    id: apri_cancello
    name: "Apri Cancello"
    on_press:
      - switch.turn_off: rele
      - delay: 2s
      - switch.turn_on: rele 

      - switch.turn_on: buzzer
      - delay: 120ms
      - switch.turn_off: buzzer
      - delay: 120ms
      - switch.turn_on: buzzer
      - delay: 120ms
      - switch.turn_off: buzzer
      - delay: 120ms
      - switch.turn_on: buzzer
      - delay: 120ms
      - switch.turn_off: buzzer

      - light.turn_on: led_verde
      - delay: 7s
      - light.turn_off: led_verde 
          
binary_sensor:
  - platform: template
    id: accesso_consentito
    name: "Accesso RFID Consentito"

  - platform: template
    id: accesso_negato
    name: "Accesso RFID Negato"

# ---------------- OLED SSD1306 ----------------
i2c:
  sda: 21
  scl: 17
  scan: false
  frequency: 100kHz

font:
  - file: "gfonts://Roboto"
    id: font1
    size: 20

display:
  - platform: ssd1306_i2c
    model: "SSD1306 128x64"
    address: 0x3C
    id: oled
    update_interval: 500ms
    pages:
      - id: page_idle
        lambda: |-
          it.printf(0, 0, id(font1), "RFID");
          it.printf(0, 28, id(font1), "READY");

      - id: page_ok
        lambda: |-
          it.printf(0, 0, id(font1), "ACCESSO");
          it.printf(0, 28, id(font1), "CONSENTITO");

      - id: page_ko
        lambda: |-
          it.printf(0, 0, id(font1), "ACCESSO");
          it.printf(0, 28, id(font1), "NEGATO");

sensor:
  - platform: uptime
    name: Uptime Sensor
    filters:
      - lambda: return x / 3600;
    unit_of_measurement: "h"
    accuracy_decimals: 2

📡 Monitor VE.Direct con ESP32 e OLED: il progetto definitivo per il tuo impianto solare ⚡

Realizzare un monitor solare professionale, elegante e completamente personalizzato non è mai stato così semplice. In questo articolo ti mostro come ho costruito un sistema completo basato su ESP32, display OLED e protocollo VE.Direct, capace di leggere in tempo reale tutti i dati del regolatore Victron e mostrarli sia su schermo che su una dashboard web moderna e responsive.

Un progetto che unisce elettronica, programmazione e design… e che porta il tuo impianto solare a un livello superiore.

💙 Firmato: TechConnectHub

🔥 Perché questo progetto è speciale

Questo monitor non è un semplice lettore di dati: è un vero e proprio cruscotto intelligente per il tuo impianto fotovoltaico.

Ecco cosa fa:

  • Legge in tempo reale i dati VE.Direct del regolatore Victron
  • Mostra i valori principali su un display OLED 128×64
  • Offre una dashboard web moderna, scura, elegante e aggiornata ogni secondo
  • Funziona con hostname dedicato: `http://victron-monitor.localMostra:
  • Potenza pannelli (W)
  • Tensione pannelli (V)
  • Tensione batteria (V)
  • Corrente (A)
  • Produzione totale (Wh)
  • Produzione giornaliera (Wh)
  • Stato MPPT (Bulk, Absorption, Float…)
  • Numero di serie del regolatore
  • Versione firmware
  • Include una pagina OLED dedicata con il logo TechConnectHub
  • Sincronizzazione perfetta senza perdere pacchetti VE.Direct

Un progetto pensato per essere affidabile, bello da vedere e semplice da installare.

🧩 Hardware necessario

  • ESP32 DevKit (qualsiasi versione con WiFi)
  • Display OLED 128×64 I2C
  • Cavo VE.Direct → UART (TX → GPIO 32)
  • Alimentazione 5V
  • Qualche cavetto Dupont

🛠️ Collegamenti elettrici

  • Victron TX → ESP32 GPIO 32
  • Victron GND → ESP32 GND
  • OLED SDA → ESP32 GPIO 21
  • OLED SCL → ESP32 GPIO 22
  • OLED VCC → 3.3V
  • OLED GND → GND

💻 Il firmware completo

#include <Arduino.h>
#include <U8g2lib.h>
#include <WiFi.h>
#include <WebServer.h>

// -------------------- DISPLAY --------------------
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

// -------------------- VARIABILI GLOBALI --------------------
float battery_voltage = 0;
float battery_current = 0;
float pv_power = 0;
float pv_voltage = 0;
float yield_total = 0;
float yield_today_real = 0;
String mppt_state = "Off";
float soc_voltage = 0;  // Aggiunto SOC

String serial_number = "";
String firmware_ver = "";

unsigned long lastUpdate = 0;
unsigned long lastPageSwitch = 0;
int lcd_page = 0;
bool soc_ready = false;  // Flag SOC pronto

// -------------------- VE.DIRECT BUFFER --------------------
String veLine = "";
bool packetReady = false;

float new_voltage = 0;
float new_current = 0;
float new_pv_power = 0;
float new_pv_voltage = 0;
float new_yield_total = 0;
float new_yield_today_real = 0;
String new_state = "Off";
float new_soc_voltage = 0;  // Aggiunto SOC

String new_serial = "";
String new_fw = "";

// -------------------- WIFI CONFIG --------------------
const char* ssid     = "Bxxxx";
const char* password = "xx";

// -------------------- WIFI WATCHDOG --------------------
unsigned long lastWiFiCheck = 0;
unsigned long wifiReconnectAttempts = 0;
const unsigned long WIFI_CHECK_INTERVAL = 10000; // 10 secondi
const unsigned long MAX_RECONNECT_ATTEMPTS = 5;
bool wifiConnected = false;

// -------------------- WEB SERVER --------------------
WebServer server(80);

// -------------------- FUNZIONE CALCOLO SOC --------------------
float calculateSOC(float v, String state) {

  // Normalizza lo stato in minuscolo
  state.toLowerCase();

  // 100% SOLO in float e sopra 54.40 V
  if (state.indexOf("float") != -1) {
    if (v > 54.40) return 100;
  }

  const int N = 10;
  float v_points[N]   = {54.4, 53.6, 53.1, 52.8, 52.3, 52.0, 51.5, 51.2, 48.0, 40.0};
  float soc_points[N] = {100,  90,   80,   70,   60,   40,   30,   20,   10,   0};

  // EVITA 100% fuori dal float
  if (v > 54.40) return 99;

  // Sotto il minimo → 0%
  if (v <= 40.0) return 0;

  // Interpolazione lineare
  for (int i = 0; i < N - 1; i++) {
    if (v <= v_points[i] && v > v_points[i + 1]) {
      float t = (v - v_points[i + 1]) / (v_points[i] - v_points[i + 1]);
      return soc_points[i + 1] + t * (soc_points[i] - soc_points[i + 1]);
    }
  }

  return 0;
}


// -------------------- HTML PAGE --------------------
const char* htmlPage = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>VE.Direct Monitor</title>
<style>
  body { font-family: Arial; background:#0d1117; color:#e6edf3; padding:20px; }
  h1 { text-align:center; color:#58a6ff; margin-bottom:25px; }
  .section-title { font-size:22px; margin-top:25px; margin-bottom:10px; color:#f0883e; text-align:center; }
  .card { background:#161b22; padding:18px; border-radius:12px; margin-bottom:15px; border:1px solid #30363d; }
  .label { font-size:18px; color:#8b949e; }
  .value { font-size:26px; font-weight:bold; margin-top:5px; }
  .watt { color:#f0883e; }
  .volt { color:#3fb950; }
  .amp  { color:#d29922; }
  .wh   { color:#a371f7; }
  .state { color:#58a6ff; }
  .soc   { color:#f0883e; }
  .footer { text-align:center; margin-top:30px; font-size:18px; color:#58a6ff; font-weight:bold; }
  .wifi-status { position:fixed; top:10px; right:10px; padding:5px 10px; border-radius:15px; font-size:12px; }
  .wifi-connected { background:#3fb950; color:white; }
  .wifi-disconnected { background:#f85149; color:white; }
</style>
</head>
<body>

<div id="wifiStatus" class="wifi-status wifi-disconnected">WiFi: Disconnesso</div>

<h1>VE.Direct Monitor</h1>

<div class="section-title">Dati in tempo reale</div>

<div class="card"><div class="label">Pannelli</div><div class="value watt" id="pv_power">-- W</div></div>
<div class="card"><div class="label">Tensione Pannelli</div><div class="value volt" id="pv_voltage">-- V</div></div>
<div class="card"><div class="label">Batteria</div><div class="value volt" id="battery_voltage">-- V</div></div>
<div class="card"><div class="label">Corrente</div><div class="value amp" id="battery_current">-- A</div></div>
<div class="card"><div class="label">SOC Batteria</div><div class="value soc" id="soc_voltage">-- %</div></div>
<div class="card"><div class="label">Produzione Totale</div><div class="value wh" id="yield_total">-- Wh</div></div>
<div class="card"><div class="label">Produzione Giornaliera</div><div class="value wh" id="yield_today_real">-- Wh</div></div>
<div class="card"><div class="label">Stato MPPT</div><div class="value state" id="mppt_state">--</div></div>

<div class="section-title">Informazioni Regolatore</div>

<div class="card"><div class="label">Seriale</div><div class="value state" id="serial_number">--</div></div>
<div class="card"><div class="label">Firmware</div><div class="value state" id="firmware_ver">--</div></div>

<div class="footer">By TechConnectHub</div>

<script>
function updateWifiStatus(connected) {
  const element = document.getElementById('wifiStatus');
  element.className = connected ? 'wifi-status wifi-connected' : 'wifi-status wifi-disconnected';
  element.innerText = 'WiFi: ' + (connected ? 'Connesso' : 'Disconnesso');
}

function updateData() {
  fetch('/api')
    .then(response => {
      updateWifiStatus(true);
      return response.json();
    })
    .then(data => {
      document.getElementById('pv_power').innerText        = data.pv_power + " W";
      document.getElementById('pv_voltage').innerText      = data.pv_voltage.toFixed(2) + " V";
      document.getElementById('battery_voltage').innerText = data.battery_voltage.toFixed(2) + " V";
      document.getElementById('battery_current').innerText = data.battery_current.toFixed(2) + " A";
      document.getElementById('soc_voltage').innerText     = data.soc_voltage.toFixed(0) + " %";
      document.getElementById('yield_total').innerText     = data.yield_total + " Wh";
      document.getElementById('yield_today_real').innerText= data.yield_today_real + " Wh";
      document.getElementById('mppt_state').innerText      = data.mppt_state;
      document.getElementById('serial_number').innerText   = data.serial_number;
      document.getElementById('firmware_ver').innerText    = data.firmware_ver;
    })
    .catch(error => {
      updateWifiStatus(false);
      console.error('Errore di connessione:', error);
    });
}

setInterval(updateData, 1000);
updateData();
</script>

</body>
</html>
)rawliteral";

// -------------------- VE.DIRECT PARSER --------------------
String csToState(int cs) {
  switch (cs) {
    case 0: return "Off";
    case 2: return "Fault";
    case 3: return "Bulk";
    case 4: return "Absorption";
    case 5: return "Float";
    case 7: return "Equalize";
    default: return "Unknown";
  }
}

void parseVEDirectLine(const String &line) {
  int sep = line.indexOf('\t');
  if (sep < 0) return;

  String key = line.substring(0, sep);
  String val = line.substring(sep + 1);
  long iv = val.toInt();

  if (key == "V") new_voltage = iv / 1000.0f;
  else if (key == "I") new_current = iv / 1000.0f;
  else if (key == "PPV") new_pv_power = iv;
  else if (key == "VPV") new_pv_voltage = iv / 1000.0f;
  else if (key == "H19") new_yield_total = iv * 10.0f;
  else if (key == "H20") new_yield_today_real = iv * 10.0f;
  else if (key == "CS") new_state = csToState(iv);
  else if (key == "SER#") new_serial = val;
  else if (key == "FW") {
    float fw = iv / 100.0f;
    new_fw = String(fw, 2);
  }
  else if (key == "Checksum") packetReady = true;
}

void readVEDirect() {
  while (Serial2.available()) {
    char c = Serial2.read();
    if (c == '\n') { parseVEDirectLine(veLine); veLine = ""; }
    else if (c != '\r') veLine += c;
  }

  if (packetReady) {
    battery_voltage = new_voltage;
    battery_current = new_current;
    pv_power        = new_pv_power;
    pv_voltage      = new_pv_voltage;
    yield_total     = new_yield_total;
    yield_today_real= new_yield_today_real;
    mppt_state      = new_state;
    
    // Calcola il SOC basato sulla tensione
    new_soc_voltage = calculateSOC(new_voltage, new_state);
    soc_voltage = new_soc_voltage;
    
    serial_number   = new_serial;
    firmware_ver    = new_fw;
    lastUpdate = millis();
    packetReady = false;
    
    // Imposta SOC come pronto se abbiamo dati validi
    if (!soc_ready && soc_voltage > 0 && battery_voltage > 0) {
      soc_ready = true;
    }
  }
}

// -------------------- WIFI MANAGEMENT --------------------
void WiFiEvent(WiFiEvent_t event) {
  switch (event) {
    case ARDUINO_EVENT_WIFI_STA_CONNECTED:
      Serial.println("Connesso all'AP");
      wifiConnected = true;
      wifiReconnectAttempts = 0;
      break;
      
    case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
      Serial.println("Disconnesso dall'AP");
      wifiConnected = false;
      break;
      
    case ARDUINO_EVENT_WIFI_STA_GOT_IP:
      Serial.print("IP ottenuto: ");
      Serial.println(WiFi.localIP());
      break;
      
    default:
      break;
  }
}

void wifiWatchdog() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi disconnesso, tentativo di riconnessione...");
    
    if (wifiReconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
      WiFi.disconnect();
      delay(100);
      WiFi.reconnect();
      wifiReconnectAttempts++;
      Serial.print("Tentativo ");
      Serial.println(wifiReconnectAttempts);
    } else {
      Serial.println("Troppi tentativi falliti, reset completo WiFi...");
      WiFi.disconnect(true);
      delay(1000);
      WiFi.begin(ssid, password);
      wifiReconnectAttempts = 0;
    }
  } else {
    wifiReconnectAttempts = 0;
  }
}

// -------------------- SETUP --------------------
void setup() {
  Serial.begin(115200);
  Serial2.begin(19200, SERIAL_8N1, 32, 33);

  u8g2.begin();
  u8g2.enableUTF8Print();

  // Configurazione WiFi
  WiFi.onEvent(WiFiEvent);
  WiFi.mode(WIFI_STA);
  WiFi.setHostname("victron-monitor");
  WiFi.setAutoReconnect(true);
  WiFi.persistent(true);

  Serial.println("Connessione al WiFi...");
  WiFi.begin(ssid, password);

  // Timeout connessione iniziale (30 secondi)
  unsigned long startTime = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - startTime < 30000) {
    delay(500);
    Serial.print(".");
  }

  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\nConnesso!");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
  } else {
    Serial.println("\nFallito connessione WiFi iniziale");
  }

  // Configurazione server web
  server.on("/", []() {
    server.send(200, "text/html", htmlPage);
  });

  server.on("/api", []() {
    String json = "{";
    json += "\"pv_power\":" + String(pv_power) + ",";
    json += "\"pv_voltage\":" + String(pv_voltage) + ",";
    json += "\"battery_voltage\":" + String(battery_voltage) + ",";
    json += "\"battery_current\":" + String(battery_current) + ",";
    json += "\"soc_voltage\":" + String(soc_voltage) + ",";
    json += "\"yield_total\":" + String(yield_total) + ",";
    json += "\"yield_today_real\":" + String(yield_today_real) + ",";
    json += "\"serial_number\":\"" + serial_number + "\",";
    json += "\"firmware_ver\":\"" + firmware_ver + "\",";
    json += "\"mppt_state\":\"" + mppt_state + "\"";
    json += "}";
    server.send(200, "application/json", json);
  });

  server.begin();
  Serial.println("Server HTTP avviato");
}

// -------------------- LOOP --------------------
void loop() {
  server.handleClient();
  readVEDirect();

  // Gestione WiFi watchdog ogni 10 secondi
  if (millis() - lastWiFiCheck > WIFI_CHECK_INTERVAL) {
    wifiWatchdog();
    lastWiFiCheck = millis();
  }

  // Cambio pagina display ogni 4 secondi
  if (millis() - lastPageSwitch > 4000) {
    lcd_page = (lcd_page + 1) % 11;  // Modificato per 11 pagine (0-10)
    lastPageSwitch = millis();
  }

  // Aggiornamento display
  u8g2.firstPage();
  do {
    if (millis() - lastUpdate > 15000) {
      u8g2.setFont(u8g2_font_ncenB12_tr);
      u8g2.drawStr(0, 30, "No VE.Direct...");
    } else {
      switch (lcd_page) {
        case 0:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Pannelli");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(pv_power); u8g2.print(" W");
          break;

        case 1:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "V Pannelli");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(pv_voltage, 2); u8g2.print(" V");
          break;

        case 2:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Batteria");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(battery_voltage, 2); u8g2.print(" V");
          break;

        case 3:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Corrente");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(battery_current, 2); u8g2.print(" A");
          break;

        case 4:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Totale");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(yield_total); u8g2.print(" Wh");
          break;

        case 5:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Giorno");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(yield_today_real); u8g2.print(" Wh");
          break;

        case 6:
          u8g2.setFont(u8g2_font_ncenB12_tr);
          u8g2.drawStr(0, 16, "SER#");
          u8g2.setFont(u8g2_font_7x14B_tr);
          u8g2.setCursor(0, 55);
          u8g2.print(serial_number);
          break;

        case 7:
          u8g2.setFont(u8g2_font_ncenB12_tr);
          u8g2.drawStr(0, 16, "FW");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(firmware_ver);
          break;

        case 8:
          u8g2.setFont(u8g2_font_7x14B_tr);
          u8g2.drawStr(0, 40, "TechConnectHub");
          break;

        case 9:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Stato MPPT");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(mppt_state);
          break;

        case 10:  // PAGINA 10: SOC BATTERIA
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "SOC Batteria");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          
          if (!soc_ready) {
            u8g2.print("Attesa...");
          } else {
            u8g2.print(soc_voltage, 0); u8g2.print(" %");
          }
          break;
      }
    }
  } while (u8g2.nextPage());
}
  • font migliorati
  • pagina web divisa in sezioni
  • scritta TechConnectHub su OLED e web
  • parsing VE.Direct completo
  • sincronizzazione perfetto

🌐 Dashboard web moderna e responsive

La dashboard web è stata progettata per essere:

  • leggibile anche da smartphone
  • elegante grazie al tema scuro
  • aggiornata ogni secondo
  • divisa in due sezioni:
  • Dati in tempo reale
  • Informazioni del regolatore

Il footer mostra con orgoglio:

By TechConnectHub

🖥️ Display OLED: semplice, chiaro, professionale

Il display OLED mostra i dati in rotazione ogni 4 secondi:

  • Potenza pannelli
  • Tensione pannelli
  • Tensione batteria
  • Corrente
  • Produzione totale
  • Produzione giornaliera
  • Numero di serie
  • Firmware
  • Logo TechConnectHub

I font sono stati calibrati per essere:

  1. grandi dove serve (es. 1.68)
  2. compatti dove necessario (SER#)
  3. sempre leggibili

Versione v2 con integrazione SOC

#include <Arduino.h>
#include <U8g2lib.h>
#include <WiFi.h>
#include <WebServer.h>

// -------------------- DISPLAY --------------------
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

// -------------------- VARIABILI GLOBALI --------------------
float battery_voltage = 0;
float battery_current = 0;
float pv_power = 0;
float pv_voltage = 0;
float yield_total = 0;
float yield_today_real = 0;
String mppt_state = "Off";
float soc_voltage = 0;  // Aggiunto SOC

String serial_number = "";
String firmware_ver = "";

unsigned long lastUpdate = 0;
unsigned long lastPageSwitch = 0;
int lcd_page = 0;
bool soc_ready = false;  // Flag SOC pronto

// -------------------- VE.DIRECT BUFFER --------------------
String veLine = "";
bool packetReady = false;

float new_voltage = 0;
float new_current = 0;
float new_pv_power = 0;
float new_pv_voltage = 0;
float new_yield_total = 0;
float new_yield_today_real = 0;
String new_state = "Off";
float new_soc_voltage = 0;  // Aggiunto SOC

String new_serial = "";
String new_fw = "";

// -------------------- WIFI CONFIG --------------------
const char* ssid     = "xx";
const char* password = "sxxx";

// -------------------- WIFI WATCHDOG --------------------
unsigned long lastWiFiCheck = 0;
unsigned long wifiReconnectAttempts = 0;
const unsigned long WIFI_CHECK_INTERVAL = 10000; // 10 secondi
const unsigned long MAX_RECONNECT_ATTEMPTS = 5;
bool wifiConnected = false;

// -------------------- WEB SERVER --------------------
WebServer server(80);

// -------------------- FUNZIONE CALCOLO SOC --------------------
float calculateSOC(float v, const String& state) {
  // 100% SOLO in FLOAT
  if (state == "Float" || state == "FLOAT" || state == "float") {
    if (v > 53.3) return 100;
  }

  const int N = 10;
  float v_points[N]   = {54.4, 53.6, 53.1, 52.8, 52.3, 52.0, 51.5, 51.2, 48.0, 40.0};
  float soc_points[N] = {100,  90,   80,   70,   60,   45,   30,   20,   10,   0};

  // EVITA 100% fuori dal FLOAT
  if (v >= v_points[0]) {
    return 99;   // massimo SOC fuori dal Float
  }

  if (v <= v_points[N-1]) return soc_points[N-1];

  for (int i = 0; i < N - 1; i++) {
    if (v <= v_points[i] && v > v_points[i + 1]) {
      float t = (v - v_points[i + 1]) / (v_points[i] - v_points[i + 1]);
      return soc_points[i + 1] + t * (soc_points[i] - soc_points[i + 1]);
    }
  }

  return 0;
}

// -------------------- HTML PAGE --------------------
const char* htmlPage = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>VE.Direct Monitor</title>
<style>
  body { font-family: Arial; background:#0d1117; color:#e6edf3; padding:20px; }
  h1 { text-align:center; color:#58a6ff; margin-bottom:25px; }
  .section-title { font-size:22px; margin-top:25px; margin-bottom:10px; color:#f0883e; text-align:center; }
  .card { background:#161b22; padding:18px; border-radius:12px; margin-bottom:15px; border:1px solid #30363d; }
  .label { font-size:18px; color:#8b949e; }
  .value { font-size:26px; font-weight:bold; margin-top:5px; }
  .watt { color:#f0883e; }
  .volt { color:#3fb950; }
  .amp  { color:#d29922; }
  .wh   { color:#a371f7; }
  .state { color:#58a6ff; }
  .soc   { color:#f0883e; }
  .footer { text-align:center; margin-top:30px; font-size:18px; color:#58a6ff; font-weight:bold; }
  .wifi-status { position:fixed; top:10px; right:10px; padding:5px 10px; border-radius:15px; font-size:12px; }
  .wifi-connected { background:#3fb950; color:white; }
  .wifi-disconnected { background:#f85149; color:white; }
</style>
</head>
<body>

<div id="wifiStatus" class="wifi-status wifi-disconnected">WiFi: Disconnesso</div>

<h1>VE.Direct Monitor</h1>

<div class="section-title">Dati in tempo reale</div>

<div class="card"><div class="label">Pannelli</div><div class="value watt" id="pv_power">-- W</div></div>
<div class="card"><div class="label">Tensione Pannelli</div><div class="value volt" id="pv_voltage">-- V</div></div>
<div class="card"><div class="label">Batteria</div><div class="value volt" id="battery_voltage">-- V</div></div>
<div class="card"><div class="label">Corrente</div><div class="value amp" id="battery_current">-- A</div></div>
<div class="card"><div class="label">SOC Batteria</div><div class="value soc" id="soc_voltage">-- %</div></div>
<div class="card"><div class="label">Produzione Totale</div><div class="value wh" id="yield_total">-- Wh</div></div>
<div class="card"><div class="label">Produzione Giornaliera</div><div class="value wh" id="yield_today_real">-- Wh</div></div>
<div class="card"><div class="label">Stato MPPT</div><div class="value state" id="mppt_state">--</div></div>

<div class="section-title">Informazioni Regolatore</div>

<div class="card"><div class="label">Seriale</div><div class="value state" id="serial_number">--</div></div>
<div class="card"><div class="label">Firmware</div><div class="value state" id="firmware_ver">--</div></div>

<div class="footer">By TechConnectHub</div>

<script>
function updateWifiStatus(connected) {
  const element = document.getElementById('wifiStatus');
  element.className = connected ? 'wifi-status wifi-connected' : 'wifi-status wifi-disconnected';
  element.innerText = 'WiFi: ' + (connected ? 'Connesso' : 'Disconnesso');
}

function updateData() {
  fetch('/api')
    .then(response => {
      updateWifiStatus(true);
      return response.json();
    })
    .then(data => {
      document.getElementById('pv_power').innerText        = data.pv_power + " W";
      document.getElementById('pv_voltage').innerText      = data.pv_voltage.toFixed(2) + " V";
      document.getElementById('battery_voltage').innerText = data.battery_voltage.toFixed(2) + " V";
      document.getElementById('battery_current').innerText = data.battery_current.toFixed(2) + " A";
      document.getElementById('soc_voltage').innerText     = data.soc_voltage.toFixed(0) + " %";
      document.getElementById('yield_total').innerText     = data.yield_total + " Wh";
      document.getElementById('yield_today_real').innerText= data.yield_today_real + " Wh";
      document.getElementById('mppt_state').innerText      = data.mppt_state;
      document.getElementById('serial_number').innerText   = data.serial_number;
      document.getElementById('firmware_ver').innerText    = data.firmware_ver;
    })
    .catch(error => {
      updateWifiStatus(false);
      console.error('Errore di connessione:', error);
    });
}

setInterval(updateData, 1000);
updateData();
</script>

</body>
</html>
)rawliteral";

// -------------------- VE.DIRECT PARSER --------------------
String csToState(int cs) {
  switch (cs) {
    case 0: return "Off";
    case 2: return "Fault";
    case 3: return "Bulk";
    case 4: return "Absorption";
    case 5: return "Float";
    case 7: return "Equalize";
    default: return "Unknown";
  }
}

void parseVEDirectLine(const String &line) {
  int sep = line.indexOf('\t');
  if (sep < 0) return;

  String key = line.substring(0, sep);
  String val = line.substring(sep + 1);
  long iv = val.toInt();

  if (key == "V") new_voltage = iv / 1000.0f;
  else if (key == "I") new_current = iv / 1000.0f;
  else if (key == "PPV") new_pv_power = iv;
  else if (key == "VPV") new_pv_voltage = iv / 1000.0f;
  else if (key == "H19") new_yield_total = iv * 10.0f;
  else if (key == "H20") new_yield_today_real = iv * 10.0f;
  else if (key == "CS") new_state = csToState(iv);
  else if (key == "SER#") new_serial = val;
  else if (key == "FW") {
    float fw = iv / 100.0f;
    new_fw = String(fw, 2);
  }
  else if (key == "Checksum") packetReady = true;
}

void readVEDirect() {
  while (Serial2.available()) {
    char c = Serial2.read();
    if (c == '\n') { parseVEDirectLine(veLine); veLine = ""; }
    else if (c != '\r') veLine += c;
  }

  if (packetReady) {
    battery_voltage = new_voltage;
    battery_current = new_current;
    pv_power        = new_pv_power;
    pv_voltage      = new_pv_voltage;
    yield_total     = new_yield_total;
    yield_today_real= new_yield_today_real;
    mppt_state      = new_state;
    
    // Calcola il SOC basato sulla tensione
    new_soc_voltage = calculateSOC(new_voltage, new_state);
    soc_voltage = new_soc_voltage;
    
    serial_number   = new_serial;
    firmware_ver    = new_fw;
    lastUpdate = millis();
    packetReady = false;
    
    // Imposta SOC come pronto se abbiamo dati validi
    if (!soc_ready && soc_voltage > 0 && battery_voltage > 0) {
      soc_ready = true;
    }
  }
}

// -------------------- WIFI MANAGEMENT --------------------
void WiFiEvent(WiFiEvent_t event) {
  switch (event) {
    case ARDUINO_EVENT_WIFI_STA_CONNECTED:
      Serial.println("Connesso all'AP");
      wifiConnected = true;
      wifiReconnectAttempts = 0;
      break;
      
    case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
      Serial.println("Disconnesso dall'AP");
      wifiConnected = false;
      break;
      
    case ARDUINO_EVENT_WIFI_STA_GOT_IP:
      Serial.print("IP ottenuto: ");
      Serial.println(WiFi.localIP());
      break;
      
    default:
      break;
  }
}

void wifiWatchdog() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi disconnesso, tentativo di riconnessione...");
    
    if (wifiReconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
      WiFi.disconnect();
      delay(100);
      WiFi.reconnect();
      wifiReconnectAttempts++;
      Serial.print("Tentativo ");
      Serial.println(wifiReconnectAttempts);
    } else {
      Serial.println("Troppi tentativi falliti, reset completo WiFi...");
      WiFi.disconnect(true);
      delay(1000);
      WiFi.begin(ssid, password);
      wifiReconnectAttempts = 0;
    }
  } else {
    wifiReconnectAttempts = 0;
  }
}

// -------------------- SETUP --------------------
void setup() {
  Serial.begin(115200);
  Serial2.begin(19200, SERIAL_8N1, 32, 33);

  u8g2.begin();
  u8g2.enableUTF8Print();

  // Configurazione WiFi
  WiFi.onEvent(WiFiEvent);
  WiFi.mode(WIFI_STA);
  WiFi.setHostname("victron-monitor");
  WiFi.setAutoReconnect(true);
  WiFi.persistent(true);

  Serial.println("Connessione al WiFi...");
  WiFi.begin(ssid, password);

  // Timeout connessione iniziale (30 secondi)
  unsigned long startTime = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - startTime < 30000) {
    delay(500);
    Serial.print(".");
  }

  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\nConnesso!");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
  } else {
    Serial.println("\nFallito connessione WiFi iniziale");
  }

  // Configurazione server web
  server.on("/", []() {
    server.send(200, "text/html", htmlPage);
  });

  server.on("/api", []() {
    String json = "{";
    json += "\"pv_power\":" + String(pv_power) + ",";
    json += "\"pv_voltage\":" + String(pv_voltage) + ",";
    json += "\"battery_voltage\":" + String(battery_voltage) + ",";
    json += "\"battery_current\":" + String(battery_current) + ",";
    json += "\"soc_voltage\":" + String(soc_voltage) + ",";
    json += "\"yield_total\":" + String(yield_total) + ",";
    json += "\"yield_today_real\":" + String(yield_today_real) + ",";
    json += "\"serial_number\":\"" + serial_number + "\",";
    json += "\"firmware_ver\":\"" + firmware_ver + "\",";
    json += "\"mppt_state\":\"" + mppt_state + "\"";
    json += "}";
    server.send(200, "application/json", json);
  });

  server.begin();
  Serial.println("Server HTTP avviato");
}

// -------------------- LOOP --------------------
void loop() {
  server.handleClient();
  readVEDirect();

  // Gestione WiFi watchdog ogni 10 secondi
  if (millis() - lastWiFiCheck > WIFI_CHECK_INTERVAL) {
    wifiWatchdog();
    lastWiFiCheck = millis();
  }

  // Cambio pagina display ogni 4 secondi
  if (millis() - lastPageSwitch > 4000) {
    lcd_page = (lcd_page + 1) % 11;  // Modificato per 11 pagine (0-10)
    lastPageSwitch = millis();
  }

  // Aggiornamento display
  u8g2.firstPage();
  do {
    if (millis() - lastUpdate > 15000) {
      u8g2.setFont(u8g2_font_ncenB12_tr);
      u8g2.drawStr(0, 30, "No VE.Direct...");
    } else {
      switch (lcd_page) {
        case 0:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Pannelli");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(pv_power); u8g2.print(" W");
          break;

        case 1:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "V Pannelli");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(pv_voltage, 2); u8g2.print(" V");
          break;

        case 2:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Batteria");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(battery_voltage, 2); u8g2.print(" V");
          break;

        case 3:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Corrente");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(battery_current, 2); u8g2.print(" A");
          break;

        case 4:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Totale");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(yield_total); u8g2.print(" Wh");
          break;

        case 5:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Giorno");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(yield_today_real); u8g2.print(" Wh");
          break;

        case 6:
          u8g2.setFont(u8g2_font_ncenB12_tr);
          u8g2.drawStr(0, 16, "SER#");
          u8g2.setFont(u8g2_font_7x14B_tr);
          u8g2.setCursor(0, 55);
          u8g2.print(serial_number);
          break;

        case 7:
          u8g2.setFont(u8g2_font_ncenB12_tr);
          u8g2.drawStr(0, 16, "FW");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(firmware_ver);
          break;

        case 8:
          u8g2.setFont(u8g2_font_7x14B_tr);
          u8g2.drawStr(0, 40, "TechConnectHub");
          break;

        case 9:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Stato MPPT");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(mppt_state);
          break;

        case 10:  // PAGINA 10: SOC BATTERIA
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "SOC Batteria");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          
          if (!soc_ready) {
            u8g2.print("Attesa...");
          } else {
            u8g2.print(soc_voltage, 0); u8g2.print(" %");
          }
          break;
      }
    }
  } while (u8g2.nextPage());
}

🚀 Conclusione

Questo progetto trasforma un semplice ESP32 in un monitor solare professionale, elegante e completamente personalizzato.

È perfetto per chi vuole:

  • monitorare il proprio impianto solare in tempo reale
  • avere una dashboard moderna e accessibile da qualsiasi dispositivo
  • integrare estetica e funzionalità
  • costruire un prodotto degno di essere venduto

Un progetto che unisce tecnica, design e passione.

Firmato con orgoglio:

💙 TechConnectHub

Monitorare un Victron SmartSolar con ESP32, ESPHome e Display OLED SSD1306

Integrare un regolatore Victron SmartSolar con un ESP32 permette di ottenere un monitoraggio locale, immediato e completamente personalizzabile dei parametri principali del proprio impianto fotovoltaico. Questo documento descrive un sistema compatto che legge via BLE i dati del Victron, li elabora con ESPHome e li visualizza su un display OLED SSD1306.

Perché usare un ESP32 con ESPHome

L’ESP32 è un microcontrollore economico, potente e dotato di Bluetooth Low Energy. ESPHome semplifica la configurazione e consente di:

  • Leggere i dati via BLE dal regolatore Victron.
  • Inviarli a Home Assistant.
  • Visualizzarli su un display locale.
  • Creare logiche personalizzate.
  • Aggiornare il firmware OTA.

Il display SSD1306

Il display OLED SSD1306 (128×64 pixel) è ideale per visualizzare informazioni essenziali:

  • Consumo ridotto.
  • Ottima leggibilità.
  • Collegamento semplice tramite I2C.
  • Supporto nativo in ESPHome.

Nel progetto vengono visualizzati ciclicamente:

  1. PV Power (W)
  2. Battery Voltage (V)
  3. Battery Current (A)
  4. Stato MPPT

Collegamenti hardware

SSD1306 → ESP32
SDA     → GPIO 21
SCL     → GPIO 22
VCC     → 3.3V o 5V (in base al modulo)
GND     → GND

Codice completo ESPHome


     esphome:
  name: esphome-web-660f74
  friendly_name: Victron 35
  min_version: 2025.11.0
  name_add_mac_suffix: false

esp32:
  variant: esp32
  framework:
    type: esp-idf
    advanced:
      minimum_chip_revision: "3.1"
      sram1_as_iram: true  

logger:
  level: INFO

api:

ota:
- platform: esphome

wifi:
  networks:
    - ssid:xxx
      password: xxx

esp32_ble_tracker:

external_components:
  - source: github://Fabian-Schmidt/esphome-victron_ble

victron_ble:
  - id: MySmartSolar
    mac_address: "de630e81b151"
    bindkey: "9d9701c7ec7acd40e063725bc6ce5591"

sensor:
  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "Battery Voltage"
    id: battery_voltage
    type: BATTERY_VOLTAGE

  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "Battery Current"
    id: battery_current
    type: BATTERY_CURRENT

  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "Yield Today"
    type: YIELD_TODAY

  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "PV Power"
    id: pv_power
    type: PV_POWER

  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "Load Current"
    type: LOAD_CURRENT

  - platform: wifi_signal
    name: "ESP32 WiFi Strength"
    update_interval: 60s

  - platform: integration
    name: "Solar Energy Produced"
    sensor: pv_power
    unit_of_measurement: "Wh"
    time_unit: h
    accuracy_decimals: 2
    state_class: total_increasing
    device_class: energy

  - platform: uptime
    name: Uptime Sensor
    filters:
      - lambda: return x / 3600.0;
    unit_of_measurement: "h"
    accuracy_decimals: 2

  - platform: internal_temperature
    name: "Temperatura interna"
    update_interval: 60s

  - platform: template
    name: "SOC stimato da tensione"
    id: soc_voltage
    unit_of_measurement: "%"
    accuracy_decimals: 0
    lambda: |-
      float v = id(battery_voltage).state;
      std::string state = id(mppt_state).state;

      // Normalizza in minuscolo
      std::transform(state.begin(), state.end(), state.begin(), ::tolower);

      // 100% SOLO se lo stato è ESATTAMENTE "float"
      if (state == "float" && v >= 54.40) {
        return 100;
      }

      // EVITA 100% fuori dal FLOAT
      if (v >= 54.40) {
        return 99;
      }

      // --- CURVA NORMALE SOTTO 54.40V (tabella LiFePO4) ---
      const int N = 10;
      float v_points[N]   = {54.4, 53.6, 53.1, 52.8, 52.3, 52.2, 52.0, 51.5, 48.0, 40.0};
      float soc_points[N] = {100,  90,   80,   70,   60,   50,   40,   30,   10,   0};

      if (v <= 40.0) return 0;

      for (int i = 0; i < N - 1; i++) {
        if (v <= v_points[i] && v > v_points[i + 1]) {
          float t = (v - v_points[i + 1]) / (v_points[i] - v_points[i + 1]);
          return soc_points[i + 1] + t * (soc_points[i] - soc_points[i + 1]);
        }
      }

      return 0;


binary_sensor:
  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "MPPT is in Fault state"
    type: DEVICE_STATE_FAULT

  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "MPPT has Error"
    type: CHARGER_ERROR

  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "MPPT in FLOAT"
    id: mppt_in_float
    type: DEVICE_STATE_FLOAT

text_sensor:
  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "MPPT state"
    id: mppt_state
    type: DEVICE_STATE

  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "MPPT Error reason"
    type: CHARGER_ERROR

  - platform: wifi_info
    ip_address:
      name: "ESP32 IP Address"
    ssid:
      name: "ESP32 WiFi SSID"

  - platform: version
    name: "Firmware ESPHome"

output:
  - platform: gpio
    pin: 2
    id: led_ident

switch:
  - platform: output
    name: "LED Identificazione ESP32"
    output: led_ident

button:
  - platform: restart
    id: riavvia_esp
    name: "Riavvia V35 ESP32"

i2c:
  sda: 21
  scl: 22
  scan: true

font:
  - file: "gfonts://Roboto"
    id: my_font
    size: 18

globals:
  - id: lcd_page
    type: int
    restore_value: no
    initial_value: '0'

  # FLAG: SOC pronto
  - id: soc_ready
    type: bool
    initial_value: 'false'

interval:
  - interval: 1s
    then:
      - lambda: |-
          if (!id(soc_ready) &&
              id(soc_voltage).state > 0 &&
              id(battery_voltage).state > 0) {
            id(soc_ready) = true;
          }

  - interval: 6s
    then:
      - lambda: |-
          id(lcd_page)++;
          if (id(lcd_page) > 4) id(lcd_page) = 0;

display:
  - platform: ssd1306_i2c
    model: "SSD1306 128x64"
    address: 0x3C
    lambda: |-
      int page = id(lcd_page);

      if (page == 0) {
        it.printf(0, 0, id(my_font), "PV Power:");
        it.printf(0, 20, id(my_font), "%.1f W", id(pv_power).state);
      }

      if (page == 1) {
        it.printf(0, 0, id(my_font), "Batt Volt:");
        it.printf(0, 20, id(my_font), "%.2f V", id(battery_voltage).state);
      }

      if (page == 2) {
        it.printf(0, 0, id(my_font), "Batt Curr:");
        it.printf(0, 20, id(my_font), "%.2f A", id(battery_current).state);
      }

      if (page == 3) {
        it.printf(0, 0, id(my_font), "MPPT State:");
        it.printf(0, 20, id(my_font), "%s", id(mppt_state).state.c_str());
      }

      if (page == 4) {

        if (!id(soc_ready)) {
          it.printf(0, 0, id(my_font), "SOC in attesa...");
          return;
        }

        float soc = id(soc_voltage).state;
        float vbat = id(battery_voltage).state;

        it.printf(0, 0, id(my_font), "SOC Batteria:");
        it.printf(0, 20, id(my_font), "%.0f %%", soc);
        it.printf(0, 40, id(my_font), "%.2f V", vbat);
      }

Risultato finale

Il display mostra ciclicamente:

  • Potenza PV
  • Tensione batteria
  • Corrente batteria
  • Stato MPPT

Il sistema è autonomo, affidabile e perfetto per monitorare un impianto solare senza aprire Home Assistant.

Spiegazione didattica del funzionamento del codice (ID sensori, globals, interval, display)

1. Perché prima servono gli ID dei sensori

Prima di poter usare un sensore nel display o in una lambda, ESPHome deve sapere come si chiama quel sensore. Questo nome è l’id:.

Senza ID, il display non può leggere il valore del sensore e il codice non compila.

Esempio corretto:

id: battery_voltage


Questo permette al display di usare:

id(battery_voltage).state

Gli ID sono quindi etichette obbligatorie che collegano i sensori al codice del display.

2. La variabile globale `lcd_page`

globals:
  - id: lcd_page
    type: int
    restore_value: no
    initial_value: '0'

Questa variabile è un contatore che indica quale pagina del display deve essere mostrata.

  • `type int → è un numero intero
  • `initialvalue: ‘0’ → parte dalla pagina 0
  • `restorevalue: no → al riavvio riparte da 0

È il “segnalibro” del display.

3. Il timer `interval` che cambia pagina ogni 5 secondi

interval:
  - interval: 5s
    then:
      - lambda: |-
          id(lcd_page)++;
          if (id(lcd_page) > 3) id(lcd_page) = 0;

Ogni 5 secondi:

  1. aumenta `cd_page di 1
  2. se supera 3, torna a 0

È un ciclo continuo:

0 → 1 → 2 → 3 → 0 → …

Questo permette al display di cambiare pagina automaticamente senza pulsanti.

4. La sezione `display` che disegna la pagina corretta

int page = id(lcd_page);

l display legge quale pagina deve mostrare.

Ogni blocco if (page == X) rappresenta una pagina:

  • 0 → PV Power
  • 1 → Battery Voltage
  • 2 → Battery Current
  • 3 → MPPT State

Esempio:

if (page == 0) {
  it.printf(0, 0, id(my_font), "PV Power:");
  it.printf(0, 20, id(my_font), "%.1f W", id(pv_power).state);
}


Il display mostra solo la pagina corrispondente al valore di lcd_page.

📌 Riassunto didattico

  • Prima si definiscono gli ID dei sensori, altrimenti il display non può leggerli.
  • `cd_page è la variabile che tiene memoria della pagina corrente.
  • interval cambia pagina ogni 5 secondi.
  • `isplay legge `cd_page e mostra la pagina giusta.

È un sistema semplice, elegante e molto flessibile.

CPU diverse ! Proxmox cluster .

Quando gestisci un cluster Proxmox, pensi sempre che la parte difficile sia l’hardware, i dischi, la rete… e invece a volte il problema arriva da dove meno te lo aspetti 😄. È quello che è successo a me qualche giorno fa, quando una semplice migrazione live ha iniziato a comportarsi in modo strano. Tutto sembrava procedere bene: la RAM veniva trasferita, il tunnel era attivo, nessun errore evidente. Poi, proprio al momento del passaggio finale, boom… “resume failed – client closed connection”. La VM si spegneva sul nodo di destinazione come se qualcuno avesse tirato la spina 😑.

All’inizio ho pensato a un problema di rete, poi a un bug, poi a qualche servizio bloccato. Ho controllato conntrack, dbus‑vmstate, log di QEMU, journal… niente. Tutto sembrava in ordine. Eppure la migrazione continuava a fallire, sempre verso lo stesso nodo. Una cosa che ti fa grattare la testa e dire “ma che diavolo sta succedendo” 🤨.

La svolta è arrivata quando ho iniziato a guardare non i log, non la rete, ma l’hardware. I miei tre nodi non erano affatto gemelli: uno montava un Intel i5, gli altri due erano Xeon, ma di modelli diversi. E lì ho avuto l’illuminazione 💡. Anche se sono tutte CPU Intel, non condividono lo stesso set di istruzioni. Alcune hanno AVX2, altre no. Alcune hanno AES‑NI, altre lo gestiscono in modo diverso. E quando una VM è configurata con “cpu: host”, Proxmox espone al guest tutte le istruzioni della CPU fisica del nodo sorgente. Se il nodo di destinazione non le supporta, la migrazione live non può funzionare. È come cercare di far ripartire un motore diesel su un’auto a benzina… non succederà mai 😅.

A quel punto tutto aveva senso. QEMU provava a ripristinare lo stato della CPU sul nodo target, trovava un’istruzione non supportata e chiudeva la connessione. Da qui l’errore “resume failed”. Una cosa che sembra misteriosa finché non guardi il quadro completo.

La soluzione, alla fine, è stata sorprendentemente semplice 😊. Ho cambiato il modello CPU della VM da “host” a “x86‑64‑v2”, un modello più portabile che espone solo le istruzioni comuni a tutte le CPU moderne. In pratica, un linguaggio CPU che tutti i nodi del cluster capiscono senza problemi. Dopo aver applicato questa modifica, la migrazione live ha iniziato a funzionare immediatamente, senza errori, senza spegnimenti improvvisi, senza sorprese. Una sensazione di sollievo incredibile 😌.

Se tutti i nodi supportano AES‑NI, si può anche usare “x86‑64‑v2‑AES”, che offre un piccolo vantaggio prestazionale. Chi vuole mantenere performance elevate può provare “host‑model”, che è più compatibile di “host” ma comunque non perfetto in cluster molto diversi. “qemu64” resta l’opzione più portabile in assoluto, ma oggi è troppo limitante per la maggior parte dei carichi.

Alla fine, questa esperienza mi ha ricordato una cosa importante: in un cluster eterogeneo, la compatibilità CPU è fondamentale. Se incontri errori di migrazione live come “resume failed”, non farti ingannare da log strani o messaggi fuorvianti. A volte la risposta è molto più semplice: i nodi parlano lingue diverse, e basta scegliere un modello CPU che tutti capiscono per riportare la pace nel cluster 🚀.

ESP32 vs Arduino: quando scegliere cosa (e perché)


Per chi si avvicina al mondo IoT o microcontrolli, le due piattaforme più citate sono Arduino e ESP32. Entrambe costano poco e offrono molta flessibilità, ma hanno punti di forza molto diversi. In questo articolo vediamo come e quando l’ESP32 “prende il sopravvento” rispetto ad Arduino in molti progetti moderni.


1. Cosa sono (breve ripasso)

  • Arduino: una piattaforma open-source basata su varie schede, tipicamente con un microcontrollore AVR (es: ATmega328P nel classico Uno).
  • Facile da programmare (IDE semplice, librerie abbondanti)
  • Ottimo per progetti didattici e semplici interfacce fisiche.
  • ESP32: un chip System-on-Chip (SoC) con:
  • CPU dual-core a 240 MHz
  • Wi-Fi + Bluetooth integrati
  • Più RAM e flash del classico Arduino Uno
  • Supporto per RTOS (FreeRTOS), filesystem, stack TCP/IP.

2. Le differenze chiave in pratica

Ecco come si traducono queste caratteristiche in situazioni reali:

a) Connettività

  • Arduino di base ha solo porte I/O e seriale. Per Wi-Fi o Bluetooth serve un modulo esterno (ESP8266, BT42), che complica cablaggi e dipendenze software.
  • ESP32: Wi-Fi + BT integrati. Questo semplifica enormemente progetti IoT: sensori remoti, home automation, nodi mesh wireless, etc.

b) Potenza di calcolo e memoria

  • Un ESP32 ha tipicamente 4–8 MB di RAM e ~512 KB di flash.
  • L’ESP32 può gestire protocolli pesanti (MQTT, HTTP/HTTPS), server web embedded, caching in memoria e più eventi concorrenti senza bloccarsi.
  • Su un Arduino Uno con ATmega328P la RAM è piccolissima (circa 2 KB) e flash limitata (32 KB): si fanno a pezzi anche programmi medi.

c) Architettura e OS

  • Arduino usa loop infinito (setup(), loop()) che può diventare un collo di bottiglia in progetti complessi.
  • ESP32 supporta RTOS: puoi definire task multipli (es: uno per Wi-Fi, uno per sensori, uno per l’UI) e pianificarli indipendentemente.

d) Flessibilità hardware

Entrambi usano GPIO, SPI, I²C, ADC/DAC.
Le differenze sono più nelle capacità extra dell’ESP32:

  • Più canali di ADC (spesso 12 bit).
  • Periferiche avanzate: USB OTG, CAN bus, ecc.
  • Supporto per touch screen capacitivi integrato su alcune schede ESP32.

3. Dove l’ESP32 è più “diffuso” e perché

Ecco alcuni esempi concreti in cui l’ESP32 ha quasi sostituito Arduino:

  1. Progetti IoT semplici (misuratori di temperatura/umidità Wi-Fi, interruttori remoti)
  • ESP32 è “out of the box”: basta scrivere il codice e si connette senza moduli extra.
  1. Home automation / domotica fai-da-te
  • Nodi Zigbee o Z-Wave via Wi-Fi/BT (ESP32 + firmware compatibile).
  • Pannelli di controllo touchscreen integrati.
  1. Wearable e dispositivi connessi a basso costo
  • Smartwatch, tracker fitness DIY, smartwatch con BT audio: ESP32 offre potenza e radio in un unico chip.
  1. Server web embedded / AP personalizzati
  • Access point Wi-Fi che serve una pagina web per configurazione o controllo (es: router mesh personalizzato).
  • Server HTTP leggero per dashboard interne su LAN.
  1. Prototipazione rapida di applicazioni complesse
  • Con RTOS e librerie TCP/IP, ESP32 permette di simulare piccoli server o client senza PC intermediario (ottimo per test IoT).

4. Arduino resta rilevante dove l’ESP32 è “troppo”

Ci sono ambiti in cui Arduino è ancora una scelta migliore:

  • Semplicità estrema
  • Per progetti educativi o didattici, la versione base (Uno/Nano) è più chiara e meno sovraccarica.
  • Basso costo per alta produzione senza connettività
  • Se ti servono centinaia di nodi che leggono un solo sensore via seriale e non Wi-Fi, un Arduino Nano resta spesso più economico.
  • Ambienti industriali legacy o certificati
  • In alcuni contesti si preferiscono microcontrollori con una storia lunga e supporti certi (es: ATmega), anche se meno potenti.

5. Conclusione: “Quando scegliere cosa”

  • Scegli ESP32 se ti serve:
  • Connettività Wi-Fi + BT integrata
  • Più potenza di calcolo e memoria (per protocolli, GUI, RTOS)
  • Flessibilità per IoT, server embedded, wearable complessi.
  • Scegli Arduino se ti serve:
  • Semplicità didattica estrema
  • Bassissimo costo per progetti semplici senza Wi-Fi/BT
  • Ambiente consolidato e ben documentato per elettronica base.

In pratica: la maggior parte dei nuovi progetti IoT, wearable, smart home, dashboard embedded e prototipi veloci finiscono usando ESP32 al posto di Arduino “puro” proprio per via della connettività e delle risorse extra in un unico chip a basso costo.

ZFS + Proxmox: un mix potente per storage enterprise-grade 🛠️

ZFS (Zettabyte File System) è molto più di un filesystem; è un sistema di storage completo che offre data integrity, pooling flessibile, snapshot efficienti e RAID integrato. Insieme a Proxmox Cluster, ZFS crea una piattaforma solida per virtualizzazione aziendale.

Qui vediamo perché ZFS conviene su Proxmox, quali compromessi ci sono e come configurarlo in modo pratico.


Perché scegliere ZFS? 🧐

1. Data integrity senza pari

  • Checksum a livello di blocco: ZFS calcola e verifica checksum per ogni blocco fisico.
  • Auto-correzione: se un disco si corrompe, ZFS ripristina i dati da ridondanza (mirror, RAIDZ).
  • Zero write-through cache: anche in caso di crash improvviso, le scritture in memoria vengono sempre riportate su disco prima che l’OS segnali il completamento.

In pratica: probabilità minima di data loss dovuta a bit rot o errori hardware.

2. Pooling flessibile e espansibile

  • ZFS raggruppa più dischi fisici (o SSD) in un pool.
  • Puoi aggiungere/rimuovere dischi senza ripartizionare tutto, solo espandendo il pool.
  • Possibilità di avere diversi dataset all’interno dello stesso pool:
    • Ciascuno può avere quota, snapshot policy, compressione e RAID type differenti.

Ideale per ambienti misti (OS/VM + librerie media + backup).

3. Snapshot e rollback rapidi

  • Le snapshot sono copy-on-write: inizialmente puntano ai blocchi esistenti; cambiano solo quelli modificati.
  • Creazione quasi istantanea, con impatto minimo sulle performance (anche su HDD).
  • Serve per backup point-in-time, test software senza paura e rollback in caso di problemi.

Ottimo per VM: puoi fare snapshot prima di upgrade OS o patch e ripristinare rapidamente se qualcosa va storto.

4. RAID integrato ed efficiente

  • ZFS offre RAIDZ (RAID5), RAIDZ2 (RAID6) e RAIDZ3, che combinano ridondanza e capacità in modo flessibile.
  • In pratica: puoi avere 1/2/3 disco(i) di spare per rebuild e protezione dati.
  • Opzione mirror per prestazioni superiori, soprattutto con SSD.

Questo toglie la necessità di hardware RAID (anche se può coesistere).


Proxmox Cluster + ZFS: un matrimonio ideale 🤝

Proxmox Cluster si basa su Ceph o, più comunemente in ambienti piccoli/medi, su storage locale condiviso via NFS/iSCSI. Usare ZFS qui porta benefici concreti:

  • Storage locale per VM:
    • Ogni node ha il proprio pool ZFS con mirror/RAIDZ.
    • Le VM si “vedono” come dischi locali ad alte prestazioni.
    • Cluster è resiliente a guasti singoli (anche di nodo).
  • Facilità di espansione e gestione:
    • Aggiungi dischi ai node uno alla volta, senza downtime.
    • Proxmox usa ZFS via API per gestire snapshot e clonazione VM.
  • Ottimizzazione delle risorse:
    • ZFS può usare cache SSD per accelerare I/O su HDD (L2ARC).
    • Dataset possono avere compressione (LZ4) e deduplicazione (se le esigenze lo giustificano).

Pro e contro di ZFS su Proxmox ⚖️

Pro:

  • Data integrity eccellente.
  • Pool flessibili e scalabili.
  • Snapshot e rollback rapidi, ottimi per VM.
  • RAID integrato che spesso sostituisce hardware RAID dedicato.
  • Integrazione nativa con API Proxmox (gestione snapshot, clonazione).

Contro:

  • Consumo di RAM: ZFS usa memoria per ARC (cache in RAM), L2ARC (SSD cache) e dataset metadata; 8–32 GB di RAM è il minimo ragionevole per pool significativi.
  • Write amplification con SSD: anche se ottimizzata, la scrittura su SSD può consumare più TBW del previsto, soprattutto con RAIDZ/mirror e TRIM disabilitato.
  • Complessità concettuale: ZFS ha molti concetti (pool, vdev, dataset, quota) che richiedono tempo per padroneggiare.

Come configurare ZFS + Proxmox Cluster in pratica ⌨️

Per semplicità assumiamo due node con storage locale condiviso via NFS.

1. Aggiungi dischi ai node

  • Assicurati che i nuovi HDD/SSD siano non-RAIDed e visibili nel BIOS.
  • Nel GUI di Proxmox, vai a Disks -> Add e seleziona il disco.

2. Crea lo ZFS pool via CLI (esempio) Per due dischi da 4 TB su ogni node:

# Esempio con mirror (performance)
zpool create -o ashift=128k \
              -O raidz2,relativedegrade=adaptive \
              data /dev/disk/by-id/..._SATA...*

# Oppure RAIDZ3 per più resilienza su 4+ dischi:
zpool create -o ashift=128k \
              -O raidz3,relativedegrade=adaptive \
              data /dev/disk/by-id/..._SATA...*

# Poi crea dataset per le VM (con quota):
zfs create -o quota=400G data/vms

ashift=128k è ottimale per dischi moderni; relativedegrade=adaptive aiuta a mantenere prestazioni durante rebuild.

3. Condividi il pool via NFS

  • Sul nodo master:zfs export -o nfs_server=4,nfs_port=111 data/vms systemctl restart nginx # Proxmox usa Nginx per NFS
  • Sui node worker, in Datacenter -> Storage:
    • Aggiungi uno storage di tipo NFS.
    • Usa l’indirizzo del master e il path a data/vms (o al pool root).

4. Configura Proxmox per usare lo storage ZFS

  • In Datacenter -> Storage: imposta il nuovo NFS come provider per:
    • CD/DVD
    • HDD
    • SSD
    • Backup
  • Aggiungi VM o template e scegli questo storage.

Best practices per ZFS + Proxmox 🚀

  1. RAM adeguata: minimo 8–32 GB per pool di decente capacità.
  2. Cache SSD (L2ARC): aggiungila su dataset pesanti in I/O se hai dischi costosi e RAM limitata; altrimenti, HDD cache è più conveniente.
  3. Monitoraggio continuo: usa zpool status e Proxmox metrics per rilevare problemi precocemente.
  4. Trim periodico su SSD: abilita TRIM (tramite cron) per mantenere buone performance nel tempo.
  5. Snapshot policy sensata: automatizza snapshot regolari, ma non esagerare; ZFS conserva i dataset più recenti e le loro snapshot finché c’è spazio libero.

Conclusione 💯

ZFS è una scelta eccellente per Proxmox Cluster se cerchi:

  • Protezione dati affidabile (checksum e auto-correzione).
  • Flessibilità nel pooling e nella gestione dello storage.
  • Integrazione nativa con snapshot e clonazione VM.

I compromessi (RAM, write amplification su SSD) sono gestibili con una configurazione attenta e hardware adeguato. Se stai progettando un cluster Proxmox per uso aziendale o semi-aziendale, ZFS è quasi sempre la strada da seguire.