Da MQTT a InfluxDB: Piattaforma IoT Energetica Real-Time
Il mercato globale dell'IoT energetico industriale supera i 22 miliardi di dollari nel 2025 e cresce con un CAGR del 19.8% verso i 54 miliardi entro il 2030. Ogni impianto fotovoltaico da 10 MW genera oltre 4 milioni di misurazioni al giorno da inverter, sensori di irraggiamento, misuratori di stringa e data logger meteorologici. Ogni parco eolico aggiunge dati vibrazionali, di coppia e di temperatura dalle nacelle. Ogni stazione di ricarica EV pubblica stato di connessione, potenza erogata e stato di salute del connettore ogni 30 secondi.
La sfida non e raccogliere questi dati: e farlo in modo affidabile, scalabile e con latenza sub-secondo, mantenendo la storicizzazione su anni e la capacità di fare query analitiche real-time. Il protocollo MQTT, nato per reti industriali con banda limitata, e diventato il linguaggio comune dell'IIoT energetico. InfluxDB e la soluzione di storage time-series più adottata per questo dominio. Insieme, con Telegraf come ponte e Grafana come layer di visualizzazione, formano uno stack che gestisce da piccoli impianti domestici a utility multi-gigawatt.
Questo articolo costruisce una piattaforma IoT energetica completa partendo dall'architettura end-to-end, passando per la configurazione dettagliata di ogni componente, fino a un Docker Compose funzionante e un case study reale su un parco fotovoltaico da 10 MW con 200 inverter. Ogni sezione include codice testato e strategie di produzione.
Cosa Imparerai in Questo Articolo
- Architettura end-to-end: sensori → gateway → MQTT → Telegraf → InfluxDB → Grafana
- MQTT deep dive: QoS, retain, will messages, topic namespace design per energia
- Confronto broker: Mosquitto vs EMQX vs HiveMQ per scalabilità e clustering
- Telegraf: configurazione MQTT consumer, processor pipeline, output InfluxDB
- InfluxDB 3.x: bucket design, retention policy, Flux queries e task scheduling
- Modbus RTU/TCP → MQTT bridge con Python e pymodbus
- Docker Compose completo per lo stack di produzione
- Sicurezza: TLS mutual, MQTT auth, token InfluxDB, network segmentation
- Case study: parco fotovoltaico 10 MW con 200 inverter, 100K msg/sec
- Alerting avanzato: Grafana, PagerDuty e Slack integration
La Serie EnergyTech - Posizione Articolo
| # | Articolo | Livello | Stato |
|---|---|---|---|
| 1 | Protocollo OCPP 2.x: Costruire Sistemi di Ricarica EV | Advanced | Pubblicato |
| 2 | Architettura DERMS: Aggregare Milioni di Risorse Distribuite | Advanced | Pubblicato |
| 3 | Previsione Energie Rinnovabili con ML: Python LSTM | Advanced | Pubblicato |
| 4 | Battery Management System per Storage Grid-Scale | Advanced | Pubblicato |
| 5 | IEC 61850 per Software Engineer: Comunicazione Smart Grid | Advanced | Pubblicato |
| 6 | EV Charging Load Balancing: Algoritmi Real-Time | Advanced | Pubblicato |
| 7 | Sei qui - Da MQTT a InfluxDB: Piattaforma IoT Energetica Real-Time | Advanced | Corrente |
| 8 | Architettura Software Carbon Accounting: Piattaforme ESG | Advanced | Prossimo |
| 9 | Digital Twin per Infrastruttura Energetica: Simulazione Real-Time | Advanced | In arrivo |
| 10 | Blockchain per Trading Energetico P2P: Smart Contract e Vincoli | Advanced | In arrivo |
Architettura End-to-End: Da Sensore a Dashboard
L'architettura di una piattaforma IoT energetica real-time si articola in cinque livelli distinti, ognuno con responsabilità specifiche e requisiti di affidabilità differenti. La comprensione di questa separazione e fondamentale prima di scrivere la prima riga di configurazione.
Livello 1: Campo (Field Layer)
Al livello più basso troviamo i dispositivi fisici: inverter fotovoltaici, anemometri, pyranometri, misuratori di stringa, PMU (Phasor Measurement Units), smart meter e stazioni di ricarica EV. La maggior parte di questi dispositivi parla protocolli di campo industriali: Modbus RTU su RS-485, Modbus TCP su Ethernet, IEC 61850 nelle sottostazioni, DNP3 per utility grid, o protocolli proprietari come SunSpec per gli inverter.
Questi protocolli non sono nativamente compatibili con IP moderno o con il cloud. Il loro polling cycle tipico va da 100ms (PMU) a 60 secondi (data logger legacy). La frequenza ottimale per un inverter fotovoltaico e 1-5 secondi per grandezze elettriche (tensione, corrente, potenza) e 30-60 secondi per grandezze termiche (temperatura moduli, temperatura inverter).
Livello 2: Gateway / Edge (Edge Layer)
Il gateway edge e il traduttore: legge i protocolli di campo e pubblica su MQTT verso il broker centrale. Può essere un dispositivo embedded (Raspberry Pi 4, BeagleBone, Moxa ioThinx), un PLC con stack MQTT integrato, o un server edge locale (NUC, ODROID) per installazioni più grandi. Il gateway svolge tre funzioni critiche:
- Protocol translation: Modbus → MQTT, IEC 61850 → MQTT, DNP3 → MQTT
- Local buffering: accumula dati durante disconnessioni WAN (store-and-forward)
- Edge preprocessing: filtraggio, aggregazione, rilevamento anomalie locali
Livello 3: Message Broker (Transport Layer)
Il broker MQTT e il cuore del sistema di trasporto. Riceve messaggi dai gateway edge, li distribuisce ai subscriber (Telegraf in primo luogo, ma anche altri consumer come sistemi SCADA o notifiche real-time). La scelta del broker dipende dalla scala: Mosquitto per installazioni singole o piccole (sotto 10K connessioni), EMQX o HiveMQ per deployment enterprise e multi-sito.
Livello 4: Ingestion e Storage (Data Layer)
Telegraf si sottoscrive ai topic MQTT e trasforma i messaggi in metriche InfluxDB usando processor pipeline configurabili. InfluxDB riceve e indicizza le time-series con retention policy differenziate: dati grezzi a alta risoluzione per 30 giorni, aggregati orari per 1 anno, aggregati giornalieri per 5 anni. Le Flux tasks schedulano il downsampling automatico e gli alert calcolati.
Livello 5: Visualizzazione e Alerting (Presentation Layer)
Grafana si connette a InfluxDB via datasource plugin e renderizza dashboard real-time aggiornate ogni 5-30 secondi. Il sistema di alerting di Grafana valuta le query periodicamente e invia notifiche a PagerDuty, Slack, email o webhook custom quando le soglie vengono superate.
Schema Architetturale Completo
Il flusso dati segue questo percorso: Inverter/Sensori (Modbus/SunSpec) → Gateway Edge (Raspberry Pi/NUC con pymodbus) → MQTT Broker (Mosquitto/EMQX) → Telegraf (MQTT Consumer + JSON Parser) → InfluxDB (time-series DB) → Grafana (dashboard + alerting). In parallelo, un Alert Manager in InfluxDB valuta task Flux ogni minuto e può scrivere eventi su un bucket dedicato o chiamare endpoint HTTP.
MQTT Deep Dive: Protocollo e Design per Energia
Fondamenti MQTT 5.0
MQTT (Message Queuing Telemetry Transport) e un protocollo publish-subscribe progettato per reti con banda limitata e alta latenza. La versione 5.0 (RFC standard OASIS 2019) aggiunge feature critiche per applicazioni enterprise: session expiry interval, message expiry interval, reason codes estesi, user properties nei messaggi, flow control con receive maximum, e topic aliases per ridurre la dimensione degli header.
QoS 0, 1 e 2: Scelta Corretta per Dati Energetici
La Quality of Service e la scelta più critica nel design di un sistema MQTT energetico. Le tre opzioni hanno implicazioni molto diverse per throughput, overhead di rete e garanzie di consegna:
| QoS | Garanzia | Overhead | Use Case Energia |
|---|---|---|---|
| QoS 0 | At most once (fire-and-forget) | Minimo (1 RTT) | Telemetria ad alta frequenza (1Hz+), dati dove la perdita occasionale e accettabile: irraggiamento, temperatura ambiente |
| QoS 1 | At least once (con duplicati) | Medio (2 RTT) | Metriche di produzione (kWh, kW), allarmi, stati dispositivo - duplicati gestiti da InfluxDB via timestamp |
| QoS 2 | Exactly once | Alto (4 RTT) | Comandi di controllo (set-point inverter, apertura/chiusura switch), dati finanziari (energia venduta in rete) |
Per un impianto fotovoltaico tipico, la raccomandazione pratica e: QoS 0 per telemetria meteorologica (irraggiamento, temperatura, vento), QoS 1 per produzione e allarmi, QoS 2 solo per comandi di controllo. Questa scelta bilancia affidabilità e throughput, permettendo al broker di gestire 50-100K messaggi/secondo senza saturare la banda WAN.
Retain Messages e Will Messages
Due feature MQTT spesso sottovalutate ma critiche per sistemi energetici:
-
Retain messages: Il broker memorizza l'ultimo messaggio con retain=true
per ogni topic. Quando un nuovo subscriber si connette, riceve immediatamente il valore
corrente senza attendere la prossima pubblicazione. Fondamentale per topic di stato:
plant/PV001/statuscon retain garantisce che ogni nuovo dashboard veda subito lo stato dell'impianto. -
Will messages (Last Will and Testament): Il gateway configura un messaggio
che il broker pubblica automaticamente se il client si disconnette in modo anomalo.
Usato per segnalare disconnessione gateway: se il Raspberry Pi del sito remoto cade,
il broker pubblica
plant/PV001/connectivity offlineimmediatamente, senza aspettare il timeout di sessione.
Topic Namespace Design per Impianti Energetici
Il design del namespace dei topic e una decisione architetturale che impatta ogni aspetto del sistema: routing, filtraggio, sicurezza ACL, e organizzazione in InfluxDB. Una struttura gerarchica ben progettata segue questo schema:
# Schema topic namespace per impianto energetico
# Struttura: {tipo_impianto}/{plant_id}/{subsystem}/{device_id}/{metric_type}
# === FOTOVOLTAICO ===
# Inverter string-level metrics
plant/PV001/inverter/INV001/metrics
plant/PV001/inverter/INV001/alarms
plant/PV001/inverter/INV001/status
# String-level measurements
plant/PV001/string/STR001/metrics
plant/PV001/string/STR001/metrics/voltage
plant/PV001/string/STR001/metrics/current
# Meteorological sensors
plant/PV001/weather/WS001/metrics
# Plant-level aggregates (calcolati dal gateway)
plant/PV001/totals/power_ac
plant/PV001/totals/energy_today
# === EOLICO ===
wind/WF001/turbine/T001/nacelle/metrics
wind/WF001/turbine/T001/blade/metrics
wind/WF001/turbine/T001/gearbox/temperature
wind/WF001/substation/SS001/metrics
# === STAZIONI DI RICARICA EV ===
ev/SITE001/charger/CHRG001/connector/1/metrics
ev/SITE001/charger/CHRG001/session/current
ev/SITE001/charger/CHRG001/status
ev/SITE001/totals/energy_dispensed
# === SISTEMA ===
# Heartbeat e connectivity
plant/PV001/gateway/GW001/heartbeat # ogni 60s
plant/PV001/gateway/GW001/status # retain=true
plant/PV001/gateway/GW001/version # retain=true
# === WILDCARD SUBSCRIPTIONS ===
# Telegraf sottoscrive tutti i metrics:
plant/+/inverter/+/metrics # tutti gli inverter di tutti gli impianti
plant/PV001/+/+/metrics # tutti i metrics dell'impianto PV001
plant/# # tutto il plant PV001 (usare con cautela)
Regole fondamentali per il namespace: ogni livello deve avere significato semantico, i topic di controllo (comandi) devono essere separati da quelli di telemetria (dati), usare snake_case per consistenza, evitare caratteri speciali e spazi, limitare la profondità a massimo 6 livelli per compatibilità con broker e client diversi.
Confronto Broker MQTT: Mosquitto vs EMQX vs HiveMQ
La scelta del broker MQTT e fondamentale per la scalabilità e l'affidabilità del sistema. I tre broker più usati in ambito energetico hanno caratteristiche molto diverse.
| Feature | Mosquitto 2.x | EMQX 5.x | HiveMQ 4.x |
|---|---|---|---|
| Connessioni max | 100K (single node) | 100M (23-node cluster) | 200M (cluster) |
| Clustering | No nativo | Si (Erlang distributed) | Si (enterprise) |
| MQTT versione | 3.1, 3.1.1, 5.0 | 3.1, 3.1.1, 5.0 | 3.1, 3.1.1, 5.0 |
| Rule Engine | No | Si (SQL-based) | Si (enterprise) |
| Bridge | Si | Si | Si |
| Autenticazione | File, TLS, plugin | JWT, OAuth2, LDAP, mTLS | JWT, OAuth2, enterprise |
| Licenza | EPL/EDL (open source) | Apache 2.0 / Enterprise | Community / Enterprise |
| RAM (100K conn) | ~500 MB | ~2 GB | ~3 GB |
| Use case energia | Impianto singolo, edge gateway, lab | Multi-sito, utility-scale, enterprise | Utility-scale, compliance enterprise |
Configurazione Mosquitto per Produzione
# /etc/mosquitto/mosquitto.conf - Produzione con TLS e ACL
# Network listeners
listener 1883 localhost
listener 8883
protocol mqtt
cafile /etc/mosquitto/certs/ca.crt
certfile /etc/mosquitto/certs/server.crt
keyfile /etc/mosquitto/certs/server.key
require_certificate true # mutual TLS
use_identity_as_username true
# WebSocket listener per dashboard web
listener 9001
protocol websockets
cafile /etc/mosquitto/certs/ca.crt
certfile /etc/mosquitto/certs/server.crt
keyfile /etc/mosquitto/certs/server.key
# Autenticazione
allow_anonymous false
password_file /etc/mosquitto/passwd
acl_file /etc/mosquitto/acl
# Persistenza
persistence true
persistence_location /var/lib/mosquitto/
autosave_interval 60
# Logging
log_dest file /var/log/mosquitto/mosquitto.log
log_type error
log_type warning
log_type notice
log_type information
log_timestamp true
log_timestamp_format %Y-%m-%dT%H:%M:%S
# Performance tuning
max_queued_messages 10000
max_packet_size 1048576 # 1MB
message_size_limit 262144 # 256KB per messaggio
max_connections 10000
max_keepalive 300
# /etc/mosquitto/acl - Access Control List per ruoli
# Telegraf: legge tutto
user telegraf_reader
topic read plant/#
topic read wind/#
topic read ev/#
# Gateway PV001: scrive solo per il suo impianto
user gateway_PV001
topic write plant/PV001/#
topic read plant/PV001/commands/#
# Gateway EV SITE001: scrive solo per il suo sito
user gateway_SITE001
topic write ev/SITE001/#
# Dashboard/SCADA: solo lettura
user dashboard_user
topic read plant/#
topic read wind/#
topic read ev/#
# Admin: tutto (solo per maintenance)
user mqtt_admin
topic #
Gateway Edge: Bridge Modbus → MQTT con Python
Il gateway edge e il componente che collega il mondo dei protocolli di campo industriali con l'ecosistema MQTT. Ecco un'implementazione completa per inverter fotovoltaici con protocollo Modbus TCP, incluso buffering locale per gestire disconnessioni WAN.
#!/usr/bin/env python3
"""
gateway_modbus_mqtt.py
Gateway edge per inverter fotovoltaici Modbus TCP → MQTT
Compatibile con registro SunSpec (standard de facto per inverter PV)
"""
import asyncio
import json
import logging
import time
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from typing import Optional
import aiofiles
from pymodbus.client import AsyncModbusTcpClient
from pymodbus.exceptions import ModbusException
import aiomqtt
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('gateway')
@dataclass
class InverterMetrics:
"""Metriche SunSpec inverter (Model 103 - Three Phase Inverter)"""
timestamp: str
plant_id: str
inverter_id: str
# AC output
ac_power_w: float # Potenza AC totale (W)
ac_voltage_l1_v: float # Tensione fase L1 (V)
ac_voltage_l2_v: float # Tensione fase L2 (V)
ac_voltage_l3_v: float # Tensione fase L3 (V)
ac_current_a: float # Corrente totale AC (A)
ac_frequency_hz: float # Frequenza rete (Hz)
# DC input
dc_voltage_v: float # Tensione DC ingresso (V)
dc_current_a: float # Corrente DC ingresso (A)
dc_power_w: float # Potenza DC ingresso (W)
# Energy
energy_wh_total: float # Energia prodotta totale (Wh)
energy_wh_today: float # Energia prodotta oggi (Wh)
# Thermal
temp_cabinet_c: float # Temperatura cabinet inverter (C)
temp_heatsink_c: float # Temperatura heatsink (C)
# Status
operating_state: int # 1=Off, 2=Sleeping, 3=Starting, 4=MPPT, 5=Throttled
error_code: int # Codice errore (0=no error)
# Efficiency
efficiency_pct: float # Efficienza conversione (%)
class ModbusMqttGateway:
"""
Gateway che legge metriche Modbus da inverter SunSpec
e le pubblica su MQTT con buffering locale per disconnessioni WAN
"""
def __init__(self, config: dict):
self.plant_id = config['plant_id']
self.inverters = config['inverters'] # lista di {id, host, port, unit_id}
self.mqtt_host = config['mqtt']['host']
self.mqtt_port = config['mqtt']['port']
self.mqtt_user = config['mqtt']['username']
self.mqtt_password = config['mqtt']['password']
self.poll_interval = config.get('poll_interval_sec', 5)
self.buffer_file = config.get('buffer_file', '/var/lib/gateway/buffer.jsonl')
self.clients: dict[str, AsyncModbusTcpClient] = {}
self._buffer: list[dict] = []
self._mqtt_connected = False
async def connect_inverter(self, inverter_cfg: dict) -> AsyncModbusTcpClient:
"""Crea e connette client Modbus TCP"""
client = AsyncModbusTcpClient(
host=inverter_cfg['host'],
port=inverter_cfg.get('port', 502),
timeout=5,
retries=3,
retry_on_empty=True,
)
await client.connect()
if not client.connected:
raise ConnectionError(
f"Cannot connect to inverter {inverter_cfg['id']} "
f"at {inverter_cfg['host']}:{inverter_cfg.get('port', 502)}"
)
logger.info(f"Connected to inverter {inverter_cfg['id']}")
return client
async def read_sunspec_model103(
self,
client: AsyncModbusTcpClient,
unit_id: int,
plant_id: str,
inverter_id: str
) -> Optional[InverterMetrics]:
"""
Legge registri SunSpec Model 103 (Three Phase Inverter)
SunSpec base address: 40001 (Modbus address 40000)
Model 103 start: 40070 (tipico, dipende da inverter)
"""
try:
# Leggi blocco principale Model 103: 40069 - 40094 (26 registri)
result = await client.read_holding_registers(
address=40069,
count=26,
slave=unit_id
)
if result.isError():
logger.error(f"Modbus error reading INV {inverter_id}: {result}")
return None
regs = result.registers
# Scala con SF (scale factors) SunSpec
ac_current_sf = self._sf(regs[10])
ac_voltage_sf = self._sf(regs[14])
ac_power_sf = self._sf(regs[17])
ac_freq_sf = self._sf(regs[19])
dc_current_sf = self._sf(regs[21])
dc_voltage_sf = self._sf(regs[23])
dc_power_sf = self._sf(regs[25])
# Leggi energia prodotta (registri 40093-40096)
energy_result = await client.read_holding_registers(
address=40093, count=4, slave=unit_id
)
energy_regs = energy_result.registers
energy_total_wh = ((energy_regs[0] << 16) | energy_regs[1]) * 1.0
# Leggi temperatura (registri 40103-40108)
temp_result = await client.read_holding_registers(
address=40103, count=3, slave=unit_id
)
temp_regs = temp_result.registers
ac_power_raw = self._signed(regs[16])
dc_power_w = self._signed(regs[24]) * (10 ** dc_power_sf)
efficiency = (ac_power_raw / dc_power_w * 100) if dc_power_w > 0 else 0.0
return InverterMetrics(
timestamp=datetime.now(timezone.utc).isoformat(),
plant_id=plant_id,
inverter_id=inverter_id,
ac_power_w=ac_power_raw * (10 ** ac_power_sf),
ac_voltage_l1_v=regs[11] * (10 ** ac_voltage_sf),
ac_voltage_l2_v=regs[12] * (10 ** ac_voltage_sf),
ac_voltage_l3_v=regs[13] * (10 ** ac_voltage_sf),
ac_current_a=(regs[7] + regs[8] + regs[9]) * (10 ** ac_current_sf),
ac_frequency_hz=regs[18] * (10 ** ac_freq_sf),
dc_voltage_v=self._signed(regs[22]) * (10 ** dc_voltage_sf),
dc_current_a=self._signed(regs[20]) * (10 ** dc_current_sf),
dc_power_w=dc_power_w,
energy_wh_total=energy_total_wh,
energy_wh_today=0.0, # da registro specifico vendor
temp_cabinet_c=self._signed(temp_regs[0]) * 0.01,
temp_heatsink_c=self._signed(temp_regs[1]) * 0.01,
operating_state=regs[0],
error_code=regs[1],
efficiency_pct=round(efficiency, 2),
)
except ModbusException as e:
logger.error(f"Modbus exception for {inverter_id}: {e}")
return None
def _signed(self, val: int) -> int:
"""Converte uint16 in int16 (signed)"""
return val if val < 32768 else val - 65536
def _sf(self, val: int) -> int:
"""Scale factor SunSpec: int16 nel range -10..10"""
s = self._signed(val)
return s if -10 <= s <= 10 else 0
async def publish_metrics(
self,
mqtt_client: aiomqtt.Client,
metrics: InverterMetrics
) -> None:
"""Pubblica metriche su MQTT con QoS 1"""
topic = f"plant/{metrics.plant_id}/inverter/{metrics.inverter_id}/metrics"
payload = json.dumps(asdict(metrics), default=str)
await mqtt_client.publish(
topic=topic,
payload=payload.encode(),
qos=1,
retain=False,
)
# Pubblica anche stato con retain
status = {
"timestamp": metrics.timestamp,
"operating_state": metrics.operating_state,
"error_code": metrics.error_code,
"online": True,
}
await mqtt_client.publish(
topic=f"plant/{metrics.plant_id}/inverter/{metrics.inverter_id}/status",
payload=json.dumps(status).encode(),
qos=1,
retain=True, # retain per status
)
async def run(self):
"""Loop principale del gateway"""
# Carica buffer da disco se esiste
await self._load_buffer()
# Will message: pubblica offline status se il gateway crolla
will = aiomqtt.Will(
topic=f"plant/{self.plant_id}/gateway/GW001/status",
payload=json.dumps({"online": False, "reason": "unexpected_disconnect"}).encode(),
qos=1,
retain=True,
)
async with aiomqtt.Client(
hostname=self.mqtt_host,
port=self.mqtt_port,
username=self.mqtt_user,
password=self.mqtt_password,
will=will,
keepalive=60,
tls_params=aiomqtt.TLSParameters(
ca_certs='/etc/gateway/certs/ca.crt',
certfile='/etc/gateway/certs/gateway.crt',
keyfile='/etc/gateway/certs/gateway.key',
),
) as mqtt:
self._mqtt_connected = True
logger.info("MQTT connected")
# Pubblica online status con retain
await mqtt.publish(
topic=f"plant/{self.plant_id}/gateway/GW001/status",
payload=json.dumps({"online": True}).encode(),
qos=1, retain=True,
)
# Svuota buffer offline
await self._flush_buffer(mqtt)
# Connetti a tutti gli inverter
for inv_cfg in self.inverters:
try:
self.clients[inv_cfg['id']] = await self.connect_inverter(inv_cfg)
except ConnectionError as e:
logger.error(f"Failed to connect: {e}")
# Loop principale di polling
while True:
poll_start = time.monotonic()
tasks = [
self._poll_and_publish(inv_cfg, mqtt)
for inv_cfg in self.inverters
]
await asyncio.gather(*tasks, return_exceptions=True)
elapsed = time.monotonic() - poll_start
sleep_time = max(0, self.poll_interval - elapsed)
await asyncio.sleep(sleep_time)
async def _poll_and_publish(self, inv_cfg: dict, mqtt: aiomqtt.Client):
"""Polling singolo inverter e pubblicazione"""
inv_id = inv_cfg['id']
client = self.clients.get(inv_id)
if client is None or not client.connected:
try:
client = await self.connect_inverter(inv_cfg)
self.clients[inv_id] = client
except ConnectionError:
return
metrics = await self.read_sunspec_model103(
client, inv_cfg.get('unit_id', 1),
self.plant_id, inv_id
)
if metrics:
try:
await self.publish_metrics(mqtt, metrics)
except Exception as e:
logger.warning(f"MQTT publish failed, buffering: {e}")
self._buffer.append(asdict(metrics))
await self._save_buffer()
async def _load_buffer(self):
"""Carica buffer dal disco"""
try:
async with aiofiles.open(self.buffer_file) as f:
async for line in f:
self._buffer.append(json.loads(line))
logger.info(f"Loaded {len(self._buffer)} buffered messages")
except FileNotFoundError:
pass
async def _save_buffer(self):
"""Salva buffer su disco"""
async with aiofiles.open(self.buffer_file, 'w') as f:
for item in self._buffer:
await f.write(json.dumps(item) + '\n')
async def _flush_buffer(self, mqtt: aiomqtt.Client):
"""Pubblica messaggi bufferizzati"""
if not self._buffer:
return
logger.info(f"Flushing {len(self._buffer)} buffered messages")
published = []
for item in self._buffer:
try:
metrics = InverterMetrics(**item)
await self.publish_metrics(mqtt, metrics)
published.append(item)
except Exception as e:
logger.error(f"Failed to flush buffered message: {e}")
break
self._buffer = [i for i in self._buffer if i not in published]
await self._save_buffer()
if __name__ == '__main__':
config = {
'plant_id': 'PV001',
'poll_interval_sec': 5,
'mqtt': {
'host': '10.0.1.10',
'port': 8883,
'username': 'gateway_PV001',
'password': 'secret',
},
'inverters': [
{'id': 'INV001', 'host': '192.168.1.101', 'port': 502, 'unit_id': 1},
{'id': 'INV002', 'host': '192.168.1.102', 'port': 502, 'unit_id': 1},
# ... fino a INV200 per impianto da 10 MW
],
'buffer_file': '/var/lib/gateway/buffer.jsonl',
}
asyncio.run(ModbusMqttGateway(config).run())
Telegraf: Bridge MQTT → InfluxDB con Pipeline di Trasformazione
Telegraf e l'agente di raccolta dati di InfluxData, scritto in Go, con oltre 300 plugin. Nel nostro stack, Telegraf si sottoscrive ai topic MQTT, decodifica il payload JSON, applica trasformazioni tramite processor, e scrive in InfluxDB. La versione 1.30+ supporta nativamente MQTT 5.0 e il parsing dei topic come tag.
Configurazione Telegraf Completa
# telegraf.conf - Configurazione completa per piattaforma IoT energetica
# ============================================================
# AGENT SETTINGS
# ============================================================
[agent]
interval = "10s"
round_interval = true
metric_batch_size = 10000
metric_buffer_limit = 100000
collection_jitter = "1s"
flush_interval = "10s"
flush_jitter = "1s"
precision = "1ns"
debug = false
quiet = false
logtarget = "file"
logfile = "/var/log/telegraf/telegraf.log"
logfile_rotation_interval = "24h"
logfile_rotation_max_size = "50MB"
logfile_rotation_max_archives = 5
# ============================================================
# OUTPUT: InfluxDB 2.x / 3.x
# ============================================================
[[outputs.influxdb_v2]]
urls = ["http://influxdb:8086"]
token = "$INFLUXDB_TOKEN"
organization = "energy-corp"
bucket = "plant_metrics_raw"
timeout = "5s"
# Tag per routing - usato per bucket separati per plant
[outputs.influxdb_v2.tagpass]
data_type = ["inverter_metrics", "weather_metrics", "string_metrics"]
[[outputs.influxdb_v2]]
urls = ["http://influxdb:8086"]
token = "$INFLUXDB_TOKEN"
organization = "energy-corp"
bucket = "ev_metrics_raw"
timeout = "5s"
[outputs.influxdb_v2.tagpass]
data_type = ["ev_metrics"]
# Output di backup su file locale (per audit e recovery)
[[outputs.file]]
files = ["/var/log/telegraf/metrics.jsonl"]
rotation_interval = "1h"
rotation_max_size = "500MB"
data_format = "json"
[outputs.file.tagpass]
error_code = ["*"] # solo metriche con errori
# ============================================================
# INPUT: MQTT Consumer - Inverter Fotovoltaici
# ============================================================
[[inputs.mqtt_consumer]]
name_override = "pv_inverter"
servers = ["ssl://mosquitto:8883"]
topics = ["plant/+/inverter/+/metrics"]
qos = 1
connection_timeout = "30s"
max_undelivered_messages = 5000
# Autenticazione mTLS
tls_ca = "/etc/telegraf/certs/ca.crt"
tls_cert = "/etc/telegraf/certs/telegraf.crt"
tls_key = "/etc/telegraf/certs/telegraf.key"
username = "telegraf_reader"
password = "$MQTT_PASSWORD"
client_id = "telegraf-energy-01"
persistent_session = true # QoS 1+ richiede sessione persistente
# Parsing topic come tag
# topic: plant/PV001/inverter/INV001/metrics
[[inputs.mqtt_consumer.topic_parsing]]
topic = "plant/+/inverter/+/metrics"
measurement = "_/plant_id/_/inverter_id/_"
tags = "_/plant_id/_/inverter_id/_"
# Estrae plant_id=PV001, inverter_id=INV001 come tag
# Formato payload
data_format = "json_v2"
[[inputs.mqtt_consumer.json_v2]]
[[inputs.mqtt_consumer.json_v2.field]]
path = "ac_power_w"
type = "float"
[[inputs.mqtt_consumer.json_v2.field]]
path = "ac_voltage_l1_v"
type = "float"
[[inputs.mqtt_consumer.json_v2.field]]
path = "ac_voltage_l2_v"
type = "float"
[[inputs.mqtt_consumer.json_v2.field]]
path = "ac_voltage_l3_v"
type = "float"
[[inputs.mqtt_consumer.json_v2.field]]
path = "ac_current_a"
type = "float"
[[inputs.mqtt_consumer.json_v2.field]]
path = "ac_frequency_hz"
type = "float"
[[inputs.mqtt_consumer.json_v2.field]]
path = "dc_voltage_v"
type = "float"
[[inputs.mqtt_consumer.json_v2.field]]
path = "dc_current_a"
type = "float"
[[inputs.mqtt_consumer.json_v2.field]]
path = "dc_power_w"
type = "float"
[[inputs.mqtt_consumer.json_v2.field]]
path = "energy_wh_total"
type = "float"
[[inputs.mqtt_consumer.json_v2.field]]
path = "temp_cabinet_c"
type = "float"
[[inputs.mqtt_consumer.json_v2.field]]
path = "temp_heatsink_c"
type = "float"
[[inputs.mqtt_consumer.json_v2.field]]
path = "operating_state"
type = "int"
[[inputs.mqtt_consumer.json_v2.field]]
path = "error_code"
type = "int"
[[inputs.mqtt_consumer.json_v2.field]]
path = "efficiency_pct"
type = "float"
[[inputs.mqtt_consumer.json_v2.tag]]
path = "plant_id"
[[inputs.mqtt_consumer.json_v2.tag]]
path = "inverter_id"
[[inputs.mqtt_consumer.json_v2.timestamp]]
path = "timestamp"
format = "2006-01-02T15:04:05.999999999Z07:00" # RFC3339Nano
# INPUT: MQTT Consumer - Stazioni Meteo
[[inputs.mqtt_consumer]]
name_override = "pv_weather"
servers = ["ssl://mosquitto:8883"]
topics = ["plant/+/weather/+/metrics"]
qos = 0 # QoS 0 per dati meteo (perdita occasionale accettabile)
tls_ca = "/etc/telegraf/certs/ca.crt"
tls_cert = "/etc/telegraf/certs/telegraf.crt"
tls_key = "/etc/telegraf/certs/telegraf.key"
username = "telegraf_reader"
password = "$MQTT_PASSWORD"
client_id = "telegraf-weather-01"
[[inputs.mqtt_consumer.topic_parsing]]
topic = "plant/+/weather/+/metrics"
tags = "_/plant_id/_/station_id/_"
data_format = "json_v2"
[[inputs.mqtt_consumer.json_v2]]
[[inputs.mqtt_consumer.json_v2.field]]
path = "irradiance_wm2"
type = "float"
[[inputs.mqtt_consumer.json_v2.field]]
path = "temperature_c"
type = "float"
[[inputs.mqtt_consumer.json_v2.field]]
path = "wind_speed_ms"
type = "float"
[[inputs.mqtt_consumer.json_v2.field]]
path = "humidity_pct"
type = "float"
[[inputs.mqtt_consumer.json_v2.timestamp]]
path = "timestamp"
format = "2006-01-02T15:04:05Z07:00"
# INPUT: MQTT Consumer - Stazioni EV
[[inputs.mqtt_consumer]]
name_override = "ev_charger"
servers = ["ssl://mosquitto:8883"]
topics = ["ev/+/charger/+/connector/+/metrics"]
qos = 1
tls_ca = "/etc/telegraf/certs/ca.crt"
tls_cert = "/etc/telegraf/certs/telegraf.crt"
tls_key = "/etc/telegraf/certs/telegraf.key"
username = "telegraf_reader"
password = "$MQTT_PASSWORD"
client_id = "telegraf-ev-01"
[[inputs.mqtt_consumer.topic_parsing]]
topic = "ev/+/charger/+/connector/+/metrics"
tags = "_/site_id/_/charger_id/_/connector_id/_"
data_format = "json_v2"
[[inputs.mqtt_consumer.json_v2]]
[[inputs.mqtt_consumer.json_v2.field]]
path = "power_kw"
type = "float"
[[inputs.mqtt_consumer.json_v2.field]]
path = "energy_kwh"
type = "float"
[[inputs.mqtt_consumer.json_v2.field]]
path = "current_a"
type = "float"
[[inputs.mqtt_consumer.json_v2.field]]
path = "voltage_v"
type = "float"
[[inputs.mqtt_consumer.json_v2.field]]
path = "status"
type = "string"
[[inputs.mqtt_consumer.json_v2.tag]]
path = "site_id"
[[inputs.mqtt_consumer.json_v2.tag]]
path = "charger_id"
[[inputs.mqtt_consumer.json_v2.tag]]
path = "connector_id"
# ============================================================
# PROCESSORS: Trasformazione dati
# ============================================================
# Aggiungi tag data_type per routing output
[[processors.converter]]
namepass = ["pv_inverter"]
[processors.converter.tags]
string = ["data_type"]
[[processors.override]]
namepass = ["pv_inverter"]
[processors.override.tags]
data_type = "inverter_metrics"
[[processors.override]]
namepass = ["ev_charger"]
[processors.override.tags]
data_type = "ev_metrics"
# Filtra metriche con valori anomali (sanity check)
[[processors.dedup]]
dedup_interval = "1s" # evita duplicati QoS 1
[[processors.enum]]
namepass = ["pv_inverter"]
[[processors.enum.mapping]]
field = "operating_state"
dest = "operating_state_str"
[processors.enum.mapping.value_mappings]
1 = "off"
2 = "sleeping"
3 = "starting"
4 = "mppt"
5 = "throttled"
6 = "shutting_down"
7 = "fault"
8 = "standby"
# ============================================================
# MONITORING: Telegraf self-monitoring
# ============================================================
[[inputs.internal]]
collect_memstats = true
[[outputs.influxdb_v2]]
urls = ["http://influxdb:8086"]
token = "$INFLUXDB_TOKEN"
organization = "energy-corp"
bucket = "telegraf_monitoring"
[outputs.influxdb_v2.namepass]
internal = ["*"]
InfluxDB: Storage e Query Time-Series per Energia
Architettura InfluxDB 2.x / 3.x per Impianti Energetici
InfluxDB e il database time-series di riferimento per IoT energetico. La versione 2.x (ancora molto usata in produzione nel 2025) usa Flux come linguaggio di query e introduce il concetto di bucket con retention policy integrata. La versione 3.x, riscritta in Rust con Apache Arrow e DataFusion, introduce SQL come linguaggio primario e migliora drasticamente la scalabilità per alta cardinalita.
Per un impianto da 10 MW con 200 inverter che campionano ogni 5 secondi, il volume di dati e considerevole: ogni inverter genera circa 15 campi, per un totale di 3.000 campi ogni 5 secondi, ovvero 216.000 datapoint al minuto. In una giornata sono circa 311 milioni di datapoint. Una retention di 30 giorni richiede circa 9.3 miliardi di datapoint grezzi. La strategia di downsampling e essenziale.
Setup Iniziale: Buckets e Organizzazione
#!/bin/bash
# setup_influxdb.sh - Configurazione iniziale InfluxDB per piattaforma energetica
INFLUX_HOST="http://localhost:8086"
INFLUX_TOKEN="my-super-secret-admin-token"
INFLUX_ORG="energy-corp"
# Setup iniziale (solo prima volta)
influx setup \
--host "$INFLUX_HOST" \
--username admin \
--password "SecurePassword123!" \
--org "$INFLUX_ORG" \
--bucket plant_metrics_raw \
--retention 720h \ # 30 giorni dati grezzi
--token "$INFLUX_TOKEN" \
--force
# Bucket dati grezzi PV (alta risoluzione: 30 giorni)
influx bucket create \
--host "$INFLUX_HOST" \
--token "$INFLUX_TOKEN" \
--org "$INFLUX_ORG" \
--name "plant_metrics_raw" \
--retention "720h" \
--shard-group-duration "24h" # shard giornalieri per query efficienti
# Bucket downsampled 1 ora (retention 1 anno)
influx bucket create \
--host "$INFLUX_HOST" \
--token "$INFLUX_TOKEN" \
--org "$INFLUX_ORG" \
--name "plant_metrics_1h" \
--retention "8760h" \
--shard-group-duration "168h" # shard settimanali
# Bucket downsampled 1 giorno (retention 5 anni)
influx bucket create \
--host "$INFLUX_HOST" \
--token "$INFLUX_TOKEN" \
--org "$INFLUX_ORG" \
--name "plant_metrics_1d" \
--retention "43800h" \ # ~5 anni
--shard-group-duration "720h" # shard mensili
# Bucket per alerting e eventi
influx bucket create \
--host "$INFLUX_HOST" \
--token "$INFLUX_TOKEN" \
--org "$INFLUX_ORG" \
--name "energy_alerts" \
--retention "8760h" # 1 anno per audit
# Bucket per stazioni EV
influx bucket create \
--host "$INFLUX_HOST" \
--token "$INFLUX_TOKEN" \
--org "$INFLUX_ORG" \
--name "ev_metrics_raw" \
--retention "720h"
# Token con permessi limitati per Telegraf (write only)
influx auth create \
--host "$INFLUX_HOST" \
--token "$INFLUX_TOKEN" \
--org "$INFLUX_ORG" \
--description "Telegraf write-only token" \
--write-bucket plant_metrics_raw \
--write-bucket ev_metrics_raw \
--write-bucket telegraf_monitoring
# Token per Grafana (read only)
influx auth create \
--host "$INFLUX_HOST" \
--token "$INFLUX_TOKEN" \
--org "$INFLUX_ORG" \
--description "Grafana read-only token" \
--read-bucket plant_metrics_raw \
--read-bucket plant_metrics_1h \
--read-bucket plant_metrics_1d \
--read-bucket ev_metrics_raw \
--read-bucket energy_alerts
echo "InfluxDB setup completato"
Flux Tasks: Downsampling e Alerting Automatico
Le Flux Tasks sono job schedulati che Telegraf esegue periodicamente dentro InfluxDB. Le utilizziamo per tre scopi: downsampling automatico dei dati grezzi, calcolo KPI aggregati, e rilevamento anomalie con scrittura su bucket alert.
// task_downsample_1h.flux
// Task: Downsampling dati inverter da 5s a 1h
// Schedulato ogni ora, processa ultima ora
option task = {
name: "downsample_pv_inverter_1h",
every: 1h,
offset: 5m, // aspetta 5 min per assicurarsi che tutti i dati siano arrivati
}
from(bucket: "plant_metrics_raw")
|> range(start: -task.every)
|> filter(fn: (r) => r._measurement == "pv_inverter")
|> filter(fn: (r) =>
r._field == "ac_power_w" or
r._field == "dc_power_w" or
r._field == "energy_wh_total" or
r._field == "ac_voltage_l1_v" or
r._field == "temp_cabinet_c" or
r._field == "efficiency_pct"
)
|> aggregateWindow(
every: 1h,
fn: (tables=<-, column) => tables
|> mean(column: column), // media oraria
createEmpty: false
)
// Aggiungi statistiche aggiuntive per power
|> map(fn: (r) => ({r with _measurement: "pv_inverter_1h"}))
|> to(bucket: "plant_metrics_1h", org: "energy-corp")
// task_plant_kpi.flux
// Task: Calcolo KPI giornalieri per impianto
// Produccion, efficienza, performance ratio
option task = {
name: "calculate_plant_daily_kpi",
every: 15m,
}
// Potenza attiva totale per impianto (somma tutti inverter)
totalPower = from(bucket: "plant_metrics_raw")
|> range(start: -15m)
|> filter(fn: (r) => r._measurement == "pv_inverter")
|> filter(fn: (r) => r._field == "ac_power_w")
|> aggregateWindow(every: 15m, fn: sum, createEmpty: false)
|> group(columns: ["plant_id", "_time"])
|> sum()
|> map(fn: (r) => ({r with
_measurement: "plant_totals",
_field: "total_ac_power_w",
}))
// Energia prodotta totale (max energy_wh_total - min energy_wh_total per periodo)
energyDelta = from(bucket: "plant_metrics_raw")
|> range(start: -15m)
|> filter(fn: (r) => r._measurement == "pv_inverter")
|> filter(fn: (r) => r._field == "energy_wh_total")
|> difference()
|> group(columns: ["plant_id", "_time"])
|> sum()
|> map(fn: (r) => ({r with
_measurement: "plant_totals",
_field: "energy_delta_wh",
}))
// Temperatura media inverter
avgTemp = from(bucket: "plant_metrics_raw")
|> range(start: -15m)
|> filter(fn: (r) => r._measurement == "pv_inverter")
|> filter(fn: (r) => r._field == "temp_cabinet_c")
|> aggregateWindow(every: 15m, fn: mean, createEmpty: false)
|> group(columns: ["plant_id", "_time"])
|> mean()
|> map(fn: (r) => ({r with
_measurement: "plant_totals",
_field: "avg_inverter_temp_c",
}))
union(tables: [totalPower, energyDelta, avgTemp])
|> to(bucket: "plant_metrics_1h", org: "energy-corp")
// task_thermal_alert.flux
// Task: Rilevamento surriscaldamento inverter
// Alert se temperatura > 75°C per più di 5 minuti
option task = {
name: "thermal_alert_inverter",
every: 1m,
}
threshold = 75.0
from(bucket: "plant_metrics_raw")
|> range(start: -6m)
|> filter(fn: (r) => r._measurement == "pv_inverter")
|> filter(fn: (r) => r._field == "temp_cabinet_c")
|> filter(fn: (r) => r._value > threshold)
// Conta quanti valori sopra soglia negli ultimi 6 min (= almeno 5 min)
|> aggregateWindow(every: 6m, fn: count, createEmpty: false)
|> filter(fn: (r) => r._value >= 60) // 60 campioni * 5sec = 5 min
|> map(fn: (r) => ({r with
_measurement: "energy_alert",
_field: "alert_type",
_value: "thermal_overtemperature",
severity: "high",
description: "Inverter temperature exceeds " + string(v: threshold) + "°C for > 5 minutes",
}))
|> to(bucket: "energy_alerts", org: "energy-corp")
Query Flux per Analytics Fotovoltaico
// query_production_analysis.flux
// Analisi produzione giornaliera per impianto PV001
// Confronto con irraggiamento (Performance Ratio)
import "math"
// Produzione AC ogni 5 minuti
production = from(bucket: "plant_metrics_raw")
|> range(start: today(), stop: now())
|> filter(fn: (r) => r._measurement == "pv_inverter")
|> filter(fn: (r) => r.plant_id == "PV001")
|> filter(fn: (r) => r._field == "ac_power_w")
|> aggregateWindow(every: 5m, fn: mean, createEmpty: true)
|> group(columns: ["plant_id"])
|> sum() // somma tutti gli inverter per potenza totale impianto
// Irraggiamento dalla stazione meteo
irradiance = from(bucket: "plant_metrics_raw")
|> range(start: today(), stop: now())
|> filter(fn: (r) => r._measurement == "pv_weather")
|> filter(fn: (r) => r.plant_id == "PV001")
|> filter(fn: (r) => r._field == "irradiance_wm2")
|> aggregateWindow(every: 5m, fn: mean, createEmpty: true)
// Join per calcolo Performance Ratio
// PR = (energia prodotta / (irraggiamento * superficie)) * 100
join(
tables: {production: production, irradiance: irradiance},
on: ["_time", "plant_id"]
)
// PV001: 10 MW peak, superficie pannelli ~50.000 m2
|> map(fn: (r) => ({r with
performance_ratio: if r.irradiance_irradiance_wm2 > 50.0 then
(r.production__value / (r.irradiance_irradiance_wm2 * 50000.0)) * 100.0
else
0.0,
}))
|> keep(columns: ["_time", "production__value", "irradiance_irradiance_wm2", "performance_ratio"])
Docker Compose: Stack Completo di Produzione
Il Docker Compose seguente e il punto di partenza per un deployment di produzione dello stack completo. Ogni servizio ha configurazioni di health check, restart policy, volume persistente e networking isolato per sicurezza.
# docker-compose.yml
# Stack IoT Energetico: Mosquitto + Telegraf + InfluxDB + Grafana
# Produzione-ready con TLS, health checks e persistent volumes
version: '3.9'
networks:
iot_network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/24
# Network separata per accesso esterno (solo Grafana e Mosquitto esposti)
external_network:
driver: bridge
volumes:
mosquitto_data:
mosquitto_logs:
influxdb_data:
influxdb_config:
grafana_data:
grafana_provisioning:
telegraf_buffer:
services:
# ============================================================
# MOSQUITTO MQTT BROKER
# ============================================================
mosquitto:
image: eclipse-mosquitto:2.0.20
container_name: mosquitto
restart: unless-stopped
networks:
- iot_network
- external_network
ports:
- "1883:1883" # MQTT plain (solo per sviluppo/testing locale)
- "8883:8883" # MQTT over TLS
- "9001:9001" # WebSocket over TLS
volumes:
- ./config/mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
- ./config/mosquitto/passwd:/mosquitto/config/passwd:ro
- ./config/mosquitto/acl:/mosquitto/config/acl:ro
- ./certs:/etc/mosquitto/certs:ro
- mosquitto_data:/mosquitto/data
- mosquitto_logs:/mosquitto/log
healthcheck:
test: ["CMD", "mosquitto_pub", "-h", "localhost", "-p", "1883",
"-t", "health", "-m", "ping", "--quiet"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
deploy:
resources:
limits:
memory: 512m
cpus: '1.0'
# ============================================================
# INFLUXDB 2.7 (stable production)
# ============================================================
influxdb:
image: influxdb:2.7-alpine
container_name: influxdb
restart: unless-stopped
networks:
- iot_network
ports:
- "8086:8086" # solo su rete interna, proxy via Nginx se necessario
environment:
DOCKER_INFLUXDB_INIT_MODE: setup
DOCKER_INFLUXDB_INIT_USERNAME: admin
DOCKER_INFLUXDB_INIT_PASSWORD: {{ "$INFLUXDB_ADMIN_PASSWORD" }}
DOCKER_INFLUXDB_INIT_ORG: energy-corp
DOCKER_INFLUXDB_INIT_BUCKET: plant_metrics_raw
DOCKER_INFLUXDB_INIT_RETENTION: 720h
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: {{ "$INFLUXDB_TOKEN" }}
# Performance tuning
INFLUXD_STORAGE_CACHE_MAX_MEMORY_SIZE: 1073741824 # 1GB cache
INFLUXD_STORAGE_COMPACT_THROUGHPUT_BURST: 50331648 # 48MB/s compaction
INFLUXD_HTTP_MAX_BODY_SIZE: 104857600 # 100MB max body
INFLUXD_QUERY_MEMORY_BYTES: 2147483648 # 2GB query memory limit
volumes:
- influxdb_data:/var/lib/influxdb2
- influxdb_config:/etc/influxdb2
healthcheck:
test: ["CMD", "influx", "ping"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
deploy:
resources:
limits:
memory: 4g
cpus: '2.0'
# ============================================================
# TELEGRAF
# ============================================================
telegraf:
image: telegraf:1.34-alpine
container_name: telegraf
restart: unless-stopped
networks:
- iot_network
- external_network # per raggiungere MQTT su rete esterna
depends_on:
influxdb:
condition: service_healthy
mosquitto:
condition: service_healthy
environment:
INFLUXDB_TOKEN: {{ "$INFLUXDB_TOKEN" }}
MQTT_PASSWORD: {{ "$MQTT_PASSWORD" }}
HOSTNAME: telegraf-energy-01
volumes:
- ./config/telegraf/telegraf.conf:/etc/telegraf/telegraf.conf:ro
- ./certs:/etc/telegraf/certs:ro
- telegraf_buffer:/var/lib/telegraf
- /var/log/telegraf:/var/log/telegraf
healthcheck:
test: ["CMD", "telegraf", "--test", "--config", "/etc/telegraf/telegraf.conf"]
interval: 60s
timeout: 30s
retries: 3
deploy:
resources:
limits:
memory: 1g
cpus: '1.0'
# ============================================================
# GRAFANA
# ============================================================
grafana:
image: grafana/grafana-oss:11.4.0
container_name: grafana
restart: unless-stopped
networks:
- iot_network
- external_network
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: {{ "$GRAFANA_ADMIN_PASSWORD" }}
GF_SECURITY_SECRET_KEY: {{ "$GRAFANA_SECRET_KEY" }}
GF_SERVER_ROOT_URL: "https://grafana.energy-corp.it"
GF_SERVER_DOMAIN: "grafana.energy-corp.it"
# Analytics disabilitate per privacy
GF_ANALYTICS_REPORTING_ENABLED: "false"
GF_ANALYTICS_CHECK_FOR_UPDATES: "false"
# SMTP per alerting
GF_SMTP_ENABLED: "true"
GF_SMTP_HOST: {{ "$SMTP_HOST" }}
GF_SMTP_USER: {{ "$SMTP_USER" }}
GF_SMTP_PASSWORD: {{ "$SMTP_PASSWORD" }}
GF_SMTP_FROM_ADDRESS: "alerts@energy-corp.it"
# InfluxDB datasource
GF_DATASOURCES_DEFAULT: InfluxDB
INFLUXDB_TOKEN_GRAFANA: {{ "$INFLUXDB_TOKEN_GRAFANA" }}
volumes:
- grafana_data:/var/lib/grafana
- ./config/grafana/provisioning:/etc/grafana/provisioning:ro
- ./config/grafana/dashboards:/var/lib/grafana/dashboards:ro
depends_on:
influxdb:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
deploy:
resources:
limits:
memory: 512m
cpus: '0.5'
# ============================================================
# NGINX REVERSE PROXY (opzionale ma raccomandato)
# ============================================================
nginx:
image: nginx:1.27-alpine
container_name: nginx
restart: unless-stopped
networks:
- external_network
ports:
- "80:80"
- "443:443"
volumes:
- ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
- /var/log/nginx:/var/log/nginx
depends_on:
- grafana
- mosquitto
healthcheck:
test: ["CMD", "nginx", "-t"]
interval: 30s
timeout: 10s
retries: 3
Provisioning Grafana Automatico
Grafana supporta il provisioning automatico di datasource e dashboard tramite file YAML.
Questo e fondamentale per ambienti containerizzati dove i container vengono ricreati.
Il file config/grafana/provisioning/datasources/influxdb.yml deve contenere
la configurazione del datasource InfluxDB, mentre le dashboard in formato JSON vanno in
config/grafana/dashboards/.
# config/grafana/provisioning/datasources/influxdb.yml
apiVersion: 1
datasources:
- name: InfluxDB-Raw
type: influxdb
access: proxy
url: http://influxdb:8086
jsonData:
version: Flux
organization: energy-corp
defaultBucket: plant_metrics_raw
tlsSkipVerify: false
secureJsonData:
token: $INFLUXDB_TOKEN_GRAFANA
- name: InfluxDB-1h
type: influxdb
access: proxy
url: http://influxdb:8086
jsonData:
version: Flux
organization: energy-corp
defaultBucket: plant_metrics_1h
secureJsonData:
token: $INFLUXDB_TOKEN_GRAFANA
- name: InfluxDB-Alerts
type: influxdb
access: proxy
url: http://influxdb:8086
jsonData:
version: Flux
organization: energy-corp
defaultBucket: energy_alerts
secureJsonData:
token: $INFLUXDB_TOKEN_GRAFANA
Grafana: Dashboard e Alerting per Impianto Fotovoltaico
Struttura Dashboard Impianto PV
Una dashboard efficace per un impianto fotovoltaico deve rispondere alle domande operative in meno di 3 secondi di lettura visiva. La struttura raccomandata e organizzata in righe:
- Riga 1 - KPI Giornalieri: 4 pannelli "stat" con produzione odierna (kWh), potenza attuale (kW), performance ratio (%), numero inverter in fault. Aggiornamento 30s.
- Riga 2 - Produzione Temporale: Grafico a area chart con potenza AC totale nelle ultime 24h, sovrapposta all'irraggiamento normalizzato. Query su InfluxDB-Raw con aggregateWindow di 5 minuti.
- Riga 3 - Heatmap Inverter: Heatmap con asse X = tempo, asse Y = inverter_id, valore = potenza AC. Permette di identificare visivamente inverter che producono meno. Aggiornamento 1 minuto.
- Riga 4 - Temperatura e Allarmi: Gauge chart per temperatura massima inverter, table panel con ultimi 50 allarmi dal bucket energy_alerts ordinati per timestamp.
- Riga 5 - Efficienza e qualità Rete: Scatter plot efficienza vs temperatura, time-series per frequenza rete e tensioni di fase.
Alerting Avanzato: Grafana + PagerDuty + Slack
# config/grafana/provisioning/alerting/rules.yml
apiVersion: 1
groups:
- name: "PV Plant Alerts"
folder: "Energy Alerts"
interval: 1m
rules:
# Alert: Produzione impianto drasticamente ridotta
- uid: pv001-production-drop
title: "PV001 - Produzione ridotta >30%"
condition: C
data:
# Query A: Produzione attuale (15 min rolling mean)
- refId: A
datasourceUid: influxdb-raw-uid
model:
query: |
from(bucket: "plant_metrics_raw")
|> range(start: -15m)
|> filter(fn: (r) => r._measurement == "pv_inverter")
|> filter(fn: (r) => r.plant_id == "PV001")
|> filter(fn: (r) => r._field == "ac_power_w")
|> mean()
|> sum()
# Query B: Irraggiamento attuale (per normalizzare)
- refId: B
datasourceUid: influxdb-raw-uid
model:
query: |
from(bucket: "plant_metrics_raw")
|> range(start: -15m)
|> filter(fn: (r) => r._measurement == "pv_weather")
|> filter(fn: (r) => r.plant_id == "PV001")
|> filter(fn: (r) => r._field == "irradiance_wm2")
|> mean()
# Condition C: se irraggiamento > 200 W/m2 (giorno chiaro)
# e produzione normalizzata < 70% del teorico
- refId: C
datasourceUid: __expr__
model:
type: math
expression: "$A / ($B * 50000) < 0.70 and $B > 200"
for: 10m # alert solo se condizione persiste 10 min
labels:
severity: "warning"
plant_id: "PV001"
category: "production"
annotations:
summary: "PV001: produzione sotto il 70% del teorico"
description: |
Produzione attuale: {{ $A }} W
Irraggiamento: {{ $B }} W/m2
Performance attesa: > 70%
runbook_url: "https://wiki.energy-corp.it/runbook/pv-production-drop"
# Alert: Inverter in fault
- uid: pv001-inverter-fault
title: "PV001 - Inverter Fault Rilevato"
condition: C
data:
- refId: A
datasourceUid: influxdb-raw-uid
model:
query: |
from(bucket: "plant_metrics_raw")
|> range(start: -2m)
|> filter(fn: (r) => r._measurement == "pv_inverter")
|> filter(fn: (r) => r._field == "operating_state")
|> filter(fn: (r) => r._value == 7) // stato fault
|> count()
- refId: C
datasourceUid: __expr__
model:
type: threshold
expression: "$A"
conditions:
- evaluator:
params: [0]
type: gt
for: 1m
labels:
severity: "critical"
plant_id: "PV001"
annotations:
summary: "Inverter in stato FAULT rilevato"
# Alert: Temperatura critica
- uid: pv001-thermal-critical
title: "PV001 - Temperatura Inverter Critica"
condition: C
data:
- refId: A
datasourceUid: influxdb-raw-uid
model:
query: |
from(bucket: "plant_metrics_raw")
|> range(start: -5m)
|> filter(fn: (r) => r._measurement == "pv_inverter")
|> filter(fn: (r) => r._field == "temp_cabinet_c")
|> max()
- refId: C
datasourceUid: __expr__
model:
type: threshold
expression: "$A"
conditions:
- evaluator:
params: [85]
type: gt
for: 5m
labels:
severity: "critical"
category: "thermal"
# Contact points
contactPoints:
- name: "PagerDuty Critical"
receivers:
- uid: pagerduty-critical
type: pagerduty
settings:
integrationKey: $PAGERDUTY_INTEGRATION_KEY
severity: critical
class: "EnergyAlerts"
component: "PV Plant"
- name: "Slack Operations"
receivers:
- uid: slack-ops
type: slack
settings:
url: $SLACK_WEBHOOK_URL
channel: "#energy-ops-alerts"
username: "Grafana Alert Bot"
iconEmoji: ":zap:"
title: "{{ template \"slack.title\" . }}"
text: "{{ template \"slack.message\" . }}"
# Routing
policies:
- receiver: "Slack Operations"
group_by: ["plant_id", "severity"]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- receiver: "PagerDuty Critical"
matchers:
- "severity = critical"
continue: true # invia anche a Slack
Sicurezza: TLS, Autenticazione e Network Segmentation
La sicurezza di una piattaforma IoT energetica e critica: i dati di produzione sono informazioni commerciali sensibili, e la possibilità di inviare comandi agli inverter deve essere protetta da accessi non autorizzati. La strategia di difesa in profondità si articola su quattro livelli.
Generazione Certificati TLS con Script
#!/bin/bash
# generate_certs.sh - Genera CA e certificati per mutual TLS
set -euo pipefail
CERTS_DIR="./certs"
mkdir -p "$CERTS_DIR"
# 1. CA (Certificate Authority) - Root of trust
openssl genrsa -out "$CERTS_DIR/ca.key" 4096
openssl req -x509 -new -nodes \
-key "$CERTS_DIR/ca.key" \
-sha256 -days 3650 \
-out "$CERTS_DIR/ca.crt" \
-subj "/C=IT/ST=Puglia/L=Bari/O=EnergyCorp/CN=Energy IoT CA"
# 2. Server certificate (Mosquitto)
openssl genrsa -out "$CERTS_DIR/server.key" 2048
openssl req -new \
-key "$CERTS_DIR/server.key" \
-out "$CERTS_DIR/server.csr" \
-subj "/C=IT/O=EnergyCorp/CN=mosquitto"
# SAN per tutti i possibili hostname
cat > "$CERTS_DIR/server-ext.cnf" <<EOF
[req]
req_extensions = v3_req
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = mosquitto
DNS.2 = mqtt.energy-corp.it
DNS.3 = localhost
IP.1 = 127.0.0.1
IP.2 = 172.20.0.10
EOF
openssl x509 -req \
-in "$CERTS_DIR/server.csr" \
-CA "$CERTS_DIR/ca.crt" \
-CAkey "$CERTS_DIR/ca.key" \
-CAcreateserial \
-out "$CERTS_DIR/server.crt" \
-days 825 \
-sha256 \
-extfile "$CERTS_DIR/server-ext.cnf" \
-extensions v3_req
# 3. Client certificate per Telegraf
openssl genrsa -out "$CERTS_DIR/telegraf.key" 2048
openssl req -new \
-key "$CERTS_DIR/telegraf.key" \
-out "$CERTS_DIR/telegraf.csr" \
-subj "/C=IT/O=EnergyCorp/CN=telegraf"
openssl x509 -req \
-in "$CERTS_DIR/telegraf.csr" \
-CA "$CERTS_DIR/ca.crt" \
-CAkey "$CERTS_DIR/ca.key" \
-CAcreateserial \
-out "$CERTS_DIR/telegraf.crt" \
-days 825 -sha256
# 4. Client certificate per gateway PV001
openssl genrsa -out "$CERTS_DIR/gateway_PV001.key" 2048
openssl req -new \
-key "$CERTS_DIR/gateway_PV001.key" \
-out "$CERTS_DIR/gateway_PV001.csr" \
-subj "/C=IT/O=EnergyCorp/CN=gateway_PV001"
openssl x509 -req \
-in "$CERTS_DIR/gateway_PV001.csr" \
-CA "$CERTS_DIR/ca.crt" \
-CAkey "$CERTS_DIR/ca.key" \
-CAcreateserial \
-out "$CERTS_DIR/gateway_PV001.crt" \
-days 365 -sha256 # 1 anno per gateway (rotazione annuale)
echo "Certificati generati in $CERTS_DIR/"
echo "CA: ca.crt"
echo "Server (Mosquitto): server.crt + server.key"
echo "Client Telegraf: telegraf.crt + telegraf.key"
echo "Client Gateway PV001: gateway_PV001.crt + gateway_PV001.key"
Sicurezza: Pratiche Obbligatorie in Produzione
- Mutual TLS sempre: Sia client che server devono presentare certificato. Impedisce connessioni da gateway non autorizzati anche se conoscono username/password.
- Segreti tramite variabili d'ambiente o secret manager: Mai hardcodare token InfluxDB, password MQTT o chiavi API nel codice o nei file di configurazione committati su git. Usare Docker secrets, HashiCorp Vault, o AWS Secrets Manager.
- Network segmentation: Il broker MQTT, InfluxDB e Telegraf non devono essere mai esposti direttamente su internet. Solo Grafana tramite reverse proxy HTTPS. Il broker MQTT per i gateway edge può essere esposto su una VPN dedicata.
- Rotazione certificati: I certificati gateway hanno validita di 1 anno (365 giorni). Impostare allarmi Grafana per la scadenza con 30 giorni di anticipo.
- Audit log MQTT: Abilitare logging di tutte le connessioni e i topic in Mosquitto. Integrare con un SIEM per rilevamento accessi anomali.
Performance: Benchmarking e Ottimizzazione per Alta Scala
capacità dello Stack per Impianto da 10 MW
Un parco fotovoltaico da 10 MW tipicamente ha 200 inverter da 50 kW ciascuno. Con polling ogni 5 secondi, il volume di messaggi MQTT e il seguente:
| Componente | Metriche | Volume |
|---|---|---|
| 200 inverter x 5s | 15 campi/messaggio | 40 msg/sec, 600 datapoints/sec |
| 10 stazioni meteo x 10s | 8 campi/messaggio | 1 msg/sec, 8 datapoints/sec |
| Plant totals x 15min | Aggregati Flux | Calcolati in-DB |
| Totale | - | ~650 datapoints/sec, ~56M datapoints/giorno |
| Storage grezzo (30 gg) | ~1.68 miliardi datapoints | ~8-15 GB (con compressione InfluxDB) |
Questo volume e abbondantemente gestibile da un singolo nodo InfluxDB 2.7 con 8 GB RAM e SSD NVMe. EMQX su VM con 4 vCPU e 8 GB RAM gestisce 100K connessioni concorrenti con throughput di 1M messaggi/secondo. Per la singola installazione da 200 inverter, un singolo broker Mosquitto su Raspberry Pi 4 e più che sufficiente (test reali: 10K msg/sec, 500 MB RAM).
Script di Benchmarking MQTT
#!/usr/bin/env python3
"""
mqtt_benchmark.py
Benchmark del broker MQTT per simulare 200 inverter
Verifica throughput, latenza e affidabilità
"""
import asyncio
import json
import time
import random
import statistics
from datetime import datetime, timezone
import aiomqtt
async def simulate_inverter(
plant_id: str,
inverter_id: str,
broker_host: str,
broker_port: int,
duration_sec: int,
poll_interval: float = 5.0
) -> dict:
"""Simula un inverter che pubblica metriche su MQTT"""
messages_sent = 0
latencies = []
errors = 0
start_time = time.monotonic()
async with aiomqtt.Client(
hostname=broker_host,
port=broker_port,
client_id=f"bench_{plant_id}_{inverter_id}",
keepalive=30,
) as client:
while time.monotonic() - start_time < duration_sec:
# Simula metriche realistiche
base_power = 50000 * random.uniform(0.3, 1.0) # 0-50 kW
payload = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"plant_id": plant_id,
"inverter_id": inverter_id,
"ac_power_w": base_power,
"ac_voltage_l1_v": random.uniform(225, 235),
"dc_voltage_v": random.uniform(550, 650),
"dc_current_a": base_power / random.uniform(550, 650),
"temp_cabinet_c": random.uniform(35, 65),
"efficiency_pct": random.uniform(94, 98.5),
"operating_state": 4, # MPPT
"error_code": 0,
"energy_wh_total": messages_sent * base_power * poll_interval / 3600,
}
topic = f"plant/{plant_id}/inverter/{inverter_id}/metrics"
t_send = time.monotonic()
try:
await client.publish(
topic=topic,
payload=json.dumps(payload).encode(),
qos=1,
)
latencies.append((time.monotonic() - t_send) * 1000) # ms
messages_sent += 1
except Exception:
errors += 1
await asyncio.sleep(poll_interval)
return {
"inverter_id": inverter_id,
"messages_sent": messages_sent,
"errors": errors,
"avg_latency_ms": statistics.mean(latencies) if latencies else 0,
"p99_latency_ms": sorted(latencies)[int(len(latencies) * 0.99)] if latencies else 0,
}
async def run_benchmark(
num_inverters: int = 200,
duration_sec: int = 60,
broker_host: str = "localhost",
broker_port: int = 1883,
) -> None:
"""Esegui benchmark con N inverter simulati"""
print(f"Avvio benchmark: {num_inverters} inverter per {duration_sec}s")
print(f"Broker: {broker_host}:{broker_port}")
print("-" * 60)
start = time.monotonic()
tasks = [
simulate_inverter(
plant_id="BENCH001",
inverter_id=f"INV{i:03d}",
broker_host=broker_host,
broker_port=broker_port,
duration_sec=duration_sec,
poll_interval=5.0,
)
for i in range(1, num_inverters + 1)
]
results = await asyncio.gather(*tasks)
elapsed = time.monotonic() - start
# Aggregazione risultati
total_messages = sum(r["messages_sent"] for r in results)
total_errors = sum(r["errors"] for r in results)
all_latencies_avg = [r["avg_latency_ms"] for r in results]
all_latencies_p99 = [r["p99_latency_ms"] for r in results]
print(f"Risultati benchmark:")
print(f" Durata: {elapsed:.1f}s")
print(f" Messaggi inviati: {total_messages:,}")
print(f" Errori: {total_errors} ({total_errors/total_messages*100:.2f}%)")
print(f" Throughput: {total_messages/elapsed:.1f} msg/sec")
print(f" Latenza media: {statistics.mean(all_latencies_avg):.2f} ms")
print(f" Latenza P99: {statistics.mean(all_latencies_p99):.2f} ms")
print(f" Inverter senza errori: {sum(1 for r in results if r['errors'] == 0)}/{num_inverters}")
if __name__ == "__main__":
asyncio.run(run_benchmark(
num_inverters=200,
duration_sec=120,
broker_host="localhost",
broker_port=1883,
))
Case Study: Monitoraggio Parco Fotovoltaico 10 MW
Questo case study descrive l'implementazione reale di una piattaforma IoT energetica per un parco fotovoltaico da 10 MW nel Sud Italia con 200 inverter SMA Sunny Tripower da 50 kW e 8 stazioni di misura meteo.
Configurazione Iniziale e Sfide
Il sito e su una rete WAN 4G con banda garantita di 10 Mbps e latenza variabile tra 30 e 200 ms. Ogni inverter parla Modbus TCP su rete LAN locale (192.168.x.x) tramite switch industriale. Il gateway edge e un Raspberry Pi 4 (8 GB RAM) con SSD da 256 GB per buffering locale, connesso alla WAN tramite router 4G industriale con SIM dati dedicata.
| Parametro | Valore |
|---|---|
| Potenza impianto | 10.2 MW peak (SMA STP50-US-40) |
| Numero inverter | 200 (SMA Sunny Tripower, 50 kW) |
| Stazioni meteo | 8 (Irraggiamento, T, HR, vento) |
| Gateway edge | Raspberry Pi 4 8GB + SSD 256GB |
| Polling interval inverter | 5 secondi |
| Polling interval meteo | 10 secondi |
| Volume messaggi MQTT | ~41 msg/sec (200 inv x 1/5s + 8 meteo x 1/10s) |
| Banda WAN utilizzata | ~180 KB/s (2% dei 10 Mbps disponibili) |
| Broker MQTT | EMQX 5.8 su VM cloud (4 vCPU, 8 GB) |
| InfluxDB | 2.7.10 su VM dedicata (8 vCPU, 32 GB RAM, NVMe) |
Risultati dopo 6 Mesi di Operativita
- Disponibilità piattaforma: 99.94% (downtime totale: 2.6 ore in 6 mesi, tutte per manutenzione pianificata del server cloud)
- Dati persi per disconnessione WAN: 0.002% (grazie al buffering locale del gateway e al persistent session MQTT)
- Alert di fault inverter rilevati automaticamente: 47 eventi in 6 mesi, MTTR (Mean Time To Repair) ridotto da 8 ore a 2.3 ore grazie all'alerting immediato su PagerDuty (vs scoperta manuale il giorno dopo)
- Performance Ratio medio: 82.3% vs 79.1% del periodo precedente (senza monitoraggio real-time): +3.2 punti percentuali = +320 MWh/anno aggiuntivi
- Risparmio da manutenzione predittiva: Rilevamento 12 inverter con degradazione precoce (temperatura heatsink +8°C rispetto alla media) prima del guasto conclamato. Intervento preventivo stimato in 45K EUR di risparmio vs sostituzione emergenziale.
- Storage InfluxDB dopo 6 mesi: 18.7 GB per dati grezzi (30 giorni rolling), 4.2 GB per aggregati 1h (6 mesi), 0.8 GB per aggregati 1d (totale 180 giorni)
Lezione Appresa: Il Buffering Edge e Non Negoziabile
Durante il primo mese di operativita, la connessione 4G ha subito 3 interruzioni prolungate (2-6 ore ciascuna) per problemi dell'operatore telefonico. Senza il buffering locale su SSD del Raspberry Pi, avremmo perso tutte le misurazioni durante quelle finestre. Grazie al buffer da 64 GB e alla logica store-and-forward, ogni volta che la connessione e tornata, tutti i messaggi accumulati sono stati pubblicati su MQTT in ordine cronologico con i timestamp originali, e InfluxDB li ha accettati correttamente per inserimento fuori-ordine.
Best Practices e Anti-Pattern
Best Practices
- Timestamp nel payload, non nel broker: Il gateway deve includere sempre il timestamp preciso di acquisizione nel payload JSON, non affidarsi al timestamp di ricezione del broker MQTT. In caso di buffering offline, i messaggi arrivano con ritardo ma i dati sono temporalmente corretti. InfluxDB accetta inserimenti fuori-ordine.
-
Separare canali telemetria da canali comandi: I topic di telemetria
(
plant/+/inverter/+/metrics) e quelli di comando (plant/+/commands/#) devono essere su namespace separati con ACL distinte. I comandi richiedono QoS 2 e autenticazione più stringente. - Downsampling aggressivo per dati storici: I dati grezzi a 5 secondi sono preziosi per analisi di guasto post-mortem, ma per trend storici bastano aggregati a 15 minuti o orari. Implementare subito le Flux tasks di downsampling e retention.
- Health monitoring del gateway: Usare il heartbeat MQTT del gateway (publish ogni 60s su topic dedicato) e monitorare in Grafana. Se il heartbeat manca per 3 periodi, alert critico: potrebbe essere il gateway caduto, non un problema del broker.
- Metriche di sistema del gateway in InfluxDB: Telegraf su gateway edge può raccogliere metriche CPU, RAM, temperatura CPU, spazio disco disponibile per buffer, e pubblicarle su MQTT. Essenziale per sapere se il Raspberry Pi e sotto stress.
Anti-Pattern da Evitare
Anti-Pattern Critici
-
Topic troppo granulari senza aggregazione: Pubblicare ogni singolo
registro Modbus su topic separato (es.
plant/PV001/INV001/register/40001) genera decine di migliaia di topic e overhead immenso di metadata nel broker. Aggregare sempre i registri correlati in un unico payload JSON per dispositivo. - QoS 2 per telemetria ad alta frequenza: Il four-way handshake di QoS 2 quadruplica il numero di messaggi di controllo. Per dati a 1-5Hz, usare QoS 1 (duplicati gestiti da InfluxDB via idempotenza del timestamp). QoS 2 solo per comandi.
-
Retain su topic di telemetria: Il retain e utile per topic di stato
(l'ultimo valore e significativo), ma su topic di telemetria ad alta frequenza
il broker deve aggiornare il messaggio retained ad ogni pubblicazione, con overhead
di storage e CPU. Usare retain solo per
/statuse/config. -
Cardinalita illimitata in InfluxDB: Evitare tag con valori ad alta
cardinalita (es. usare
session_idcome tag per stazioni EV crea un nuovo serie per ogni sessione). Usare invece come field, non come tag. - Nessun rate limiting su InfluxDB: Senza limit di query concorrenti, una singola query Grafana pesante (es. export di 6 mesi di dati grezzi) può saturare la memoria del DB e causare OOM killer. Configurare sempre i limiti di query.
Risorse e Riferimenti Tecnici
- MQTT 5.0 Specification (OASIS): La specifica ufficiale del protocollo MQTT 5.0 descrive in dettaglio QoS, retain, will messages, session management e user properties. Fondamentale per implementazioni enterprise.
- Telegraf MQTT Consumer Plugin: La documentazione ufficiale InfluxData descrive tutti i parametri di configurazione del plugin mqtt_consumer, incluso il topic parsing e i formati di dato supportati. URL: docs.influxdata.com/telegraf
- InfluxDB Flux Documentation: Il linguaggio Flux e potente ma ha una curva di apprendimento. La documentazione ufficiale include reference completo di tutte le funzioni, incluse aggregateWindow, join, pivot e map.
- SunSpec Alliance: Lo standard SunSpec definisce il mappa dei registri Modbus per inverter fotovoltaici, batterie e misuratori. Disponibile su sunspec.org. Assicura interoperabilità tra dispositivi di produttori diversi.
- EMQX Documentation: La documentazione EMQX include guide per clustering, rule engine, sicurezza e integrazione con sistemi di storage esterni incluso InfluxDB.
- Grafana Alerting: La documentazione Grafana per il sistema di alerting unificato (v9+) include guide per il provisioning via YAML, configurazione contact points e routing.
- IIoT Security: IEC 62443: Lo standard IEC 62443 definisce i requisiti di sicurezza per sistemi di automazione e controllo industriale, inclusi impianti energetici. Obbligatorio per certificazioni NIS2 (recepita in Italia con D.Lgs. 138/2024).
Conclusioni e Prossimi Passi
Abbiamo costruito una piattaforma IoT energetica completa e production-ready: dal bridge Modbus-MQTT con buffering locale fino al alerting multi-canale via Grafana, PagerDuty e Slack. Lo stack MQTT + Telegraf + InfluxDB + Grafana e collaudato, documentato e scalabile: gestisce 650 datapoints/secondo per un impianto da 10 MW con margine abbondante, e con EMQX in clustering e più nodi InfluxDB scala linearmente fino a deployment utility-scale da centinaia di MW.
I numeri del case study parlano chiaro: disponibilità del 99.94%, riduzione del MTTR da 8 a 2.3 ore, +3.2 punti di Performance Ratio, e 45K EUR di risparmio da manutenzione predittiva in 6 mesi. Il monitoraggio real-time non e un lusso per impianti energetici rinnovabili: e un moltiplicatore diretto di ricavi e di vita utile degli asset.
Il prossimo articolo della serie EnergyTech affronta il tema della carbon accounting: come costruire piattaforme ESG che calcolano emissioni evitate, certificati verdi e carbon credit partendo proprio dai dati di produzione raccolti dalla piattaforma IoT che abbiamo appena costruito.
Articoli Correlati
- Serie EnergyTech: Questo articolo fa parte della serie dedicata all'ingegneria software per l'energia rinnovabile. L'articolo precedente su EV Load Balancing e il successivo su Carbon Accounting completano il quadro dell'infrastruttura software per la transizione energetica.
- Serie MLOps: Il downsampling e il rilevamento anomalie implementati con Flux sono soltanto il primo passo. La serie MLOps descrive come integrare modelli ML (LSTM per previsione produzione, Isolation Forest per anomaly detection) con dati provenienti da questa stessa piattaforma InfluxDB.
- Serie Data & AI Business: L'architettura MQTT-InfluxDB e un esempio di data lakehouse IoT. La serie Data & AI Business approfondisce come portare questi dati time-series in pipeline analitiche enterprise con dbt e Airflow.
- Serie PostgreSQL AI: Per analisi più complesse che richiedono JOIN con dati relazionali (es. dati contratti, anagrafica impianti, storico manutenzioni), l'integrazione InfluxDB-PostgreSQL tramite Foreign Data Wrapper e descritta nella serie PostgreSQL AI.

Commenti
Caricamento commenti...
Accedi per commentare