Smart Building IoT: Integrarea senzorilor și Edge Computing
Piața europeană a clădirilor inteligente a ajuns din urmă 7,5 miliarde de euro în 2025, a crescut cu 6,3 miliarde în 2024, ca urmare a legislației UE privind eficiența energetică și Presiunea ESG asupra investitorilor imobiliari. Piloți cu sisteme de management al clădirilor bazate pe inteligență artificială au demonstrat reduceri ale consumului de energie de 20-50%, cu cele mai mari câștiguri în clădirile cu consum intens de HVAC.
În acest articol construim arhitectura completă a unui sistem IoT pentru clădiri inteligente: de la integrarea protocoalelor de automatizare a clădirilor (BACnet, Modbus, KNX) până la procesare avantaje cu MQTT, de la analiza energiei în timp real la automatizarea HVAC bazată pe ocupare predicție cu ML.
Ce vei învăța
- Protocoale de automatizare a clădirilor: BACnet, Modbus RTU/TCP, KNX, DALI
- Edge computing cu gateway IoT: Raspberry Pi/PC industrial cu Node-RED
- Broker MQTT: proiectarea subiectului și QoS pentru datele senzorilor
- Ingestie în serii de timp cu InfluxDB și Grafana
- Predicția de ocupare cu ML: economisire automată a energiei
- Automatizare HVAC: controler PID și ventilație bazată pe cerere
- Benchmarking energetic: Scorul ENERGY STAR și ISO 50001
- Geamăn digital al clădirii cu actualizare în timp real
Protocoale de automatizare a clădirilor
Clădirile comerciale și rezidențiale moderne folosesc protocoale de comunicare specializate pentru a conecta senzori, actuatoare și sisteme de control. Coexistența acestor standarde într-o singură clădire (de multe ori moștenire + modernă) și principala provocare a integrării.
Principalele protocoale
- BACnet/IP: Standarde ASHRAE pentru clădiri comerciale; folosit pentru HVAC, control acces, securitate la incendiu
- Modbus TCP/RTU: protocol industrial simplu; senzori de energie, invertoare, PLC
- KNX: Standard european pentru clădiri rezidențiale și comerciale; iluminat, obloane, incalzire
- DALI: specific pentru controlul luminii; reglare și grupare avansate
- Zigbee/Z-Wave: plasă fără fir pentru senzori de putere redusă (temperatura, gradul de ocupare, CO2)
- MQTT: protocol IoT modern; punte între protocoalele moștenite și cele cloud
- OPC UA: interoperabilitate industrială; adoptarea în creștere în clădirile inteligente
Arhitectura Edge Computing
Modelul fundamental e Marginea cu 3 niveluri: senzori/actuatori (nivel de câmp) -> edge gateway (procesare locală, traducere protocol) -> cloud (analitică, ML, tablou de bord). Procesarea marginilor este esențială pentru a reduce latența, consumul de lățime de bandă și pentru a asigura continuitatea operațional chiar și atunci când este deconectat de la cloud.
// Edge Gateway: integrazione BACnet e MQTT con Node.js
import * as bacnet from 'node-bacnet';
import Aedes from 'aedes';
import net from 'net';
interface SensorReading {
deviceId: string;
sensorType: 'temperature' | 'humidity' | 'co2' | 'occupancy' | 'energy' | 'illuminance';
value: number;
unit: string;
timestamp: string;
quality: 'good' | 'uncertain' | 'bad';
}
// Configurazione BACnet device scanner
export class BACnetGateway {
private client = new bacnet.Client();
private discoveredDevices = new Map<number, { address: string; objects: any[] }>();
constructor(private readonly mqttBroker: Aedes) {}
startDiscovery(): void {
// Who-Is broadcast: scopri tutti i device BACnet sulla rete
this.client.whoIs();
this.client.on('iAm', (device: any) => {
const deviceId = device.deviceId;
console.log(`Discovered BACnet device: ${deviceId} at ${device.address}`);
this.discoveredDevices.set(deviceId, { address: device.address, objects: [] });
this.readDeviceObjects(deviceId, device.address);
});
}
private async readDeviceObjects(deviceId: number, address: string): Promise<void> {
// Leggi Property List: scopri tutti gli oggetti del device
this.client.readProperty(
{ ip: address },
{ type: 8, instance: deviceId }, // 8 = Device Object
bacnet.enum.PropertyIdentifier.OBJECT_LIST,
(err: Error | null, value: any) => {
if (err) {
console.error(`Error reading objects for device ${deviceId}:`, err);
return;
}
// Schedula polling dei valori ogni 60 secondi
this.schedulePolling(deviceId, address, value.values);
}
);
}
private schedulePolling(deviceId: number, address: string, objects: any[]): void {
const POLL_INTERVAL_MS = 60_000;
const poll = async () => {
for (const obj of objects.slice(0, 50)) { // Max 50 oggetti per device
try {
await this.readAndPublishObject(deviceId, address, obj);
} catch (err) {
console.warn(`Failed to read ${obj.type}:${obj.instance}:`, err);
}
}
};
poll();
setInterval(poll, POLL_INTERVAL_MS);
}
private readAndPublishObject(deviceId: number, address: string, obj: any): Promise<void> {
return new Promise((resolve) => {
this.client.readProperty(
{ ip: address },
{ type: obj.type, instance: obj.instance },
bacnet.enum.PropertyIdentifier.PRESENT_VALUE,
(err: Error | null, data: any) => {
if (err) { resolve(); return; }
const reading: SensorReading = {
deviceId: `bacnet-${deviceId}-${obj.type}-${obj.instance}`,
sensorType: this.inferSensorType(obj.type, obj.description),
value: data.values[0].value,
unit: this.getUnit(obj.type),
timestamp: new Date().toISOString(),
quality: 'good',
};
// Pubblica su MQTT con topic strutturato
const topic = `building/${deviceId}/sensor/${reading.sensorType}/${obj.instance}`;
this.mqttBroker.publish({
cmd: 'publish',
topic,
payload: Buffer.from(JSON.stringify(reading)),
qos: 1, // At least once - ok per telemetria
retain: true, // Ultimo valore disponibile per nuovi subscriber
dup: false,
}, () => resolve());
}
);
});
}
private inferSensorType(bacnetType: number, description?: string): SensorReading['sensorType'] {
// BACnet object type 0 = Analog Input, 2 = Binary Input, etc.
// Usa description per distinguere (es. "Room Temp", "CO2 Level")
const desc = (description ?? '').toLowerCase();
if (desc.includes('temp')) return 'temperature';
if (desc.includes('co2') || desc.includes('carbon')) return 'co2';
if (desc.includes('humid')) return 'humidity';
if (desc.includes('occup') || desc.includes('presence')) return 'occupancy';
if (desc.includes('energy') || desc.includes('power') || desc.includes('kwh')) return 'energy';
if (desc.includes('lux') || desc.includes('illum')) return 'illuminance';
return 'temperature'; // default
}
private getUnit(bacnetType: number): string {
const units: Record<number, string> = {
62: '°C', 64: '°F', 55: 'ppm', 91: '%RH', 83: 'kWh', 37: 'lux'
};
return units[bacnetType] ?? 'unit';
}
}
// Setup MQTT broker edge-side
export function createEdgeMqttBroker(port = 1883): Aedes {
const broker = new Aedes();
const server = net.createServer(broker.handle);
server.listen(port, () => {
console.log(`MQTT broker listening on port ${port}`);
});
return broker;
}
Integrare Modbus: Contoare de energie și PLC
Modbus și protocolul dominant pentru contoare de energie electrică, invertoare
sisteme fotovoltaice și PLC-uri industriale. Integrarea necesită citirea unor registre specifice
cu biblioteca modbus-serial.
import ModbusRTU from 'modbus-serial';
interface ModbusEnergyReading {
activeEnergyKwh: number;
activePowerW: number;
voltageV: number;
currentA: number;
powerFactor: number;
frequencyHz: number;
}
export class ModbusEnergyMeter {
private client = new ModbusRTU();
async connect(port: string, baudRate = 9600, slaveId = 1): Promise<void> {
await this.client.connectRTUBuffered(port, { baudRate });
this.client.setID(slaveId);
this.client.setTimeout(3000);
console.log(`Modbus connected: ${port} @ ${baudRate} baud, slave ${slaveId}`);
}
// Esempio per misuratore Carlo Gavazzi EM340 (Modbus Map comune)
async readEnergyData(): Promise<ModbusEnergyReading> {
// Registro 40001 (0x0000): Tensione L1-N (in 0.1V)
const voltageReg = await this.client.readHoldingRegisters(0x0000, 2);
// Registro 40007 (0x0006): Corrente L1 (in 0.001A)
const currentReg = await this.client.readHoldingRegisters(0x0006, 2);
// Registro 40013 (0x000C): Potenza Attiva Totale (in 0.1W)
const powerReg = await this.client.readHoldingRegisters(0x000C, 2);
// Registro 40013 (0x0034): Energia Attiva Importata (in 0.1Wh)
const energyReg = await this.client.readHoldingRegisters(0x0034, 4);
// Registro 40047 (0x002E): Power Factor (in 0.001)
const pfReg = await this.client.readHoldingRegisters(0x002E, 2);
const toFloat = (regs: number[]) => {
const buf = Buffer.alloc(4);
buf.writeUInt16BE(regs[0], 0);
buf.writeUInt16BE(regs[1], 2);
return buf.readFloatBE(0);
};
return {
voltageV: toFloat(voltageReg.data),
currentA: toFloat(currentReg.data),
activePowerW: toFloat(powerReg.data),
activeEnergyKwh: toFloat(energyReg.data.slice(0, 2)) / 10,
powerFactor: toFloat(pfReg.data),
frequencyHz: 50, // fisso per reti europee
};
}
async startContinuousReading(
intervalMs: number,
onReading: (reading: ModbusEnergyReading) => void
): Promise<void> {
const loop = async () => {
try {
const reading = await this.readEnergyData();
onReading(reading);
} catch (err) {
console.error('Modbus read error:', err);
// Tenta riconnessione
await new Promise(r => setTimeout(r, 5000));
}
};
setInterval(loop, intervalMs);
}
}
InfluxDB: Seria temporală pentru datele senzorilor
InfluxDB v3 și baza de date de referință în serie de timp pentru IoT de înaltă frecvență. Cu scrieri în loturi și interogări InfluxQL/Flux, gestionează miliarde de puncte de date cu performanțe excelente.
import { InfluxDB, Point, WriteApi } from '@influxdata/influxdb-client';
const client = new InfluxDB({
url: process.env['INFLUXDB_URL']!,
token: process.env['INFLUXDB_TOKEN']!,
});
const writeApi = client.getWriteApi(
process.env['INFLUXDB_ORG']!,
process.env['INFLUXDB_BUCKET']!,
'ms' // precisione timestamp
);
// Configurazione batching per performance
writeApi.useDefaultTags({ environment: 'production' });
export async function writeSensorReading(reading: SensorReading, buildingId: string): Promise<void> {
const point = new Point('sensor_reading')
.tag('building_id', buildingId)
.tag('device_id', reading.deviceId)
.tag('sensor_type', reading.sensorType)
.tag('quality', reading.quality)
.floatField('value', reading.value)
.timestamp(new Date(reading.timestamp));
writeApi.writePoint(point);
// Flush ogni 30 punti o ogni 10 secondi (gestito internamente)
}
export async function writeEnergyReading(
energy: ModbusEnergyReading,
buildingId: string,
meterId: string
): Promise<void> {
const point = new Point('energy_meter')
.tag('building_id', buildingId)
.tag('meter_id', meterId)
.floatField('active_power_w', energy.activePowerW)
.floatField('energy_kwh', energy.activeEnergyKwh)
.floatField('voltage_v', energy.voltageV)
.floatField('current_a', energy.currentA)
.floatField('power_factor', energy.powerFactor)
.timestamp(new Date());
writeApi.writePoint(point);
}
// Query Flux: consumo energetico ultimi 7 giorni con rollup orario
export const ENERGY_QUERY_7D = `
from(bucket: "smart-building")
|> range(start: -7d)
|> filter(fn: (r) => r["_measurement"] == "energy_meter")
|> filter(fn: (r) => r["building_id"] == "building-001")
|> filter(fn: (r) => r["_field"] == "active_power_w")
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> yield(name: "mean_power_hourly")
`;
// Query: alert quando potenza supera soglia
export const POWER_ALERT_QUERY = `
from(bucket: "smart-building")
|> range(start: -5m)
|> filter(fn: (r) => r["_measurement"] == "energy_meter")
|> filter(fn: (r) => r["_field"] == "active_power_w")
|> filter(fn: (r) => r["_value"] > 150000) // 150kW - soglia alert
|> yield(name: "power_alerts")
`;
Predicția de ocupare cu ML pentru automatizarea HVAC
Cel mai impactant caz de utilizare: preziceți ocuparea fiecărei zone a clădirii 30-60 de minute mai întâi să precondiționeze (sau să închidă) sistemul HVAC, maximizând confortul și minimizarea consumului de energie.
import { RandomForestClassifier } from 'ml-random-forest';
interface OccupancyFeatures {
hourOfDay: number; // 0-23
dayOfWeek: number; // 0-6 (0=Lunedi)
monthOfYear: number; // 1-12
isHoliday: boolean;
currentCo2Ppm: number; // sensore CO2 (proxy occupancy)
currentPirTriggered: boolean; // sensore PIR motion
currentDoorState: 'open' | 'closed';
temperatureDelta: number; // temp stanza - setpoint
}
export class OccupancyPredictor {
private model: RandomForestClassifier | null = null;
async train(trainingData: Array<{ features: OccupancyFeatures; occupied: boolean }>): Promise<void> {
const X = trainingData.map(d => this.featuresToArray(d.features));
const y = trainingData.map(d => d.occupied ? 1 : 0);
this.model = new RandomForestClassifier({
nEstimators: 100,
maxDepth: 10,
seed: 42,
});
this.model.train(X, y);
console.log('Occupancy prediction model trained');
}
predict(features: OccupancyFeatures): { occupied: boolean; confidence: number } {
if (!this.model) throw new Error('Model not trained');
const X = [this.featuresToArray(features)];
const prediction = this.model.predict(X);
const probabilities = this.model.predictProbability(X);
return {
occupied: prediction[0] === 1,
confidence: Math.max(...probabilities[0]),
};
}
private featuresToArray(f: OccupancyFeatures): number[] {
return [
f.hourOfDay / 23,
f.dayOfWeek / 6,
f.monthOfYear / 12,
f.isHoliday ? 1 : 0,
Math.min(f.currentCo2Ppm / 2000, 1),
f.currentPirTriggered ? 1 : 0,
f.currentDoorState === 'open' ? 1 : 0,
Math.abs(f.temperatureDelta) / 10,
];
}
}
// HVAC Automation basata su occupancy prediction
interface HVACSetpoint {
zoneId: string;
heatingSetpoint: number; // gradi C
coolingSetpoint: number; // gradi C
fanMode: 'auto' | 'on' | 'off';
mode: 'heat' | 'cool' | 'auto' | 'off';
}
export function calculateHVACSetpoint(
zoneId: string,
occupancyPrediction: { occupied: boolean; confidence: number },
currentTemp: number,
currentHour: number
): HVACSetpoint {
// Comfort setpoints (orario lavorativo)
const COMFORT_HEATING = 21.5;
const COMFORT_COOLING = 24.0;
// Setback setpoints (non occupato)
const SETBACK_HEATING = 18.0;
const SETBACK_COOLING = 27.0;
const isOccupied = occupancyPrediction.occupied && occupancyPrediction.confidence > 0.65;
// Pre-conditioning: attiva 30min prima dell'occupancy prevista
const nextHourOccupied = occupancyPrediction.occupied;
if (isOccupied || (nextHourOccupied && occupancyPrediction.confidence > 0.80)) {
return {
zoneId,
heatingSetpoint: COMFORT_HEATING,
coolingSetpoint: COMFORT_COOLING,
fanMode: 'auto',
mode: 'auto',
};
}
// Setback mode - risparmio energetico
return {
zoneId,
heatingSetpoint: SETBACK_HEATING,
coolingSetpoint: SETBACK_COOLING,
fanMode: 'off',
mode: currentTemp < SETBACK_HEATING ? 'heat' : currentTemp > SETBACK_COOLING ? 'cool' : 'off',
};
}
Benchmarking energetic și KPI-uri
| KPI-uri | Formula | Țintă clădire eficientă |
|---|---|---|
| EUI (intensitatea consumului de energie) | kWh/an/m2 | <100 kWh/m2 (birouri, climat mediteranean) |
| PUE (Eficacitatea consumului de energie) | Putere totală / putere IT | <1,5 pentru centrele de date în clădire |
| Indicele de confort termic | % ore în intervalul de confort (20-24°C) | ocupare >95% ore |
| Calitatea aerului CO2 | PPM zone ocupate în medie | <800 ppm (ASHRAE 62.1) |
| Economii bazate pe ocupare | kWh economisiți față de valoarea de bază | Economii de 20-35% HVAC |
Securitate OT/IT: infrastructură critică
Sistemele BACnet și Modbus au fost proiectate pentru rețele închise și au securitate limitat (adesea fără autentificare). Înainte de conectarea sistemelor de automatizare a clădirilor la rețeaua IP, implementează: segmentarea rețelei (OT dedicat VLAN), firewall unidirecțional (diodă de date) pentru comunicare OT -> IT, VPN pentru acces de la distanță la gateway-uri și monitorizare a anomaliilor de trafic. Un atac asupra sistemelor HVAC ale unei clădiri poate provoca daune fizicieni semnificativi.
Concluzii
Integrarea IoT în clădirile inteligente este o oportunitate tehnică și de afaceri de top: economii de energie de 20-50%, îmbunătățirea confortului ocupanților și reducerea emisiilor de CO2. Arhitectura Edge Computing asigură rezistență operațională chiar și în cazul deconectării de la cloud, în timp ce învățarea automată pentru predicția de ocupare automatizează deciziile HVAC decât înainte au necesitat intervenția umană continuă.







