Smart Building IoT: sensorintegratie en edge computing
De Europese markt voor slimme gebouwen heeft een inhaalslag gemaakt 7,5 miljard euro in 2025, in 2024 met 6,3 miljard gegroeid, gedreven door de EU-wetgeving inzake energie-efficiëntie en de ESG-druk op vastgoedbeleggers. Pilots met AI-gestuurde gebouwbeheersystemen hebben een vermindering van het energieverbruik aangetoond van 20-50%, met de hoogste verdiensten in HVAC-intensieve gebouwen.
In dit artikel bouwen we de volledige architectuur van een IoT-systeem voor slimme gebouwen: van de integratie van gebouwautomatiseringsprotocollen (BACnet, Modbus, KNX) tot verwerking edge met MQTT, van realtime energieanalyses tot op bezetting gebaseerde HVAC-automatisering voorspelling met ML.
Wat je gaat leren
- Protocollen voor gebouwautomatisering: BACnet, Modbus RTU/TCP, KNX, DALI
- Edge computing met IoT-gateway: Raspberry Pi/industriële pc met Node-RED
- MQTT-makelaar: onderwerpontwerp en QoS voor sensorgegevens
- Tijdreeksopname met InfluxDB en Grafana
- Bezettingsvoorspelling met ML: automatische energiebesparing
- HVAC-automatisering: PID-regelaar en vraaggestuurde ventilatie
- Energiebenchmarking: ENERGY STAR-score en ISO 50001
- Digitale tweeling van het gebouw met realtime updates
Protocollen voor gebouwautomatisering
Moderne commerciële en residentiële gebouwen gebruiken gespecialiseerde communicatieprotocollen om sensoren, actuatoren en besturingssystemen aan te sluiten. Het naast elkaar bestaan van deze normen in één gebouw (vaak oud + modern) en de belangrijkste uitdaging van integratie.
Belangrijkste protocollen
- BACnet/IP: ASHRAE-normen voor commerciële gebouwen; gebruikt voor HVAC, toegangscontrole, brandveiligheid
- ModbusTCP/RTU: eenvoudig industrieel protocol; energiesensoren, omvormers, PLC
- KNX: Europese norm voor residentiële en commerciële gebouwen; verlichting, rolluiken, verwarming
- DALI: specifiek voor lichtregeling; geavanceerd dimmen en groeperen
- Zigbee/Z-golf: draadloos mesh voor sensoren met laag vermogen (temperatuur, aanwezigheid, CO2)
- MQTT: modern IoT-protocol; brug tussen oudere en cloudprotocollen
- OPC UA: industriële interoperabiliteit; groeiende adoptie in slimme gebouwen
Edge Computing-architectuur
Het fundamentele patroon e Rand met 3 lagen: sensoren/actoren (veldniveau) -> edge gateway (lokale verwerking, protocolvertaling) -> cloud (analyse, ML, dashboard). Edge-verwerking is van cruciaal belang om de latentie en het bandbreedteverbruik te verminderen en de continuïteit te garanderen operationeel, zelfs als de verbinding met de cloud is verbroken.
// 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;
}
Modbus-integratie: energiemeters en PLC
Modbus en het dominante protocol voor elektriciteitsmeters, omvormers
fotovoltaïsche systemen en industriële PLC's. Integratie vereist het lezen van specifieke registers
met de bibliotheek 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: tijdreeksen voor sensorgegevens
InfluxDB v3 en de referentietijdreeksdatabase voor hoogfrequente IoT. Met batchschrijfbewerkingen en InfluxQL/Flux-query's verwerkt het miljarden datapunten met uitstekende prestaties.
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")
`;
Bezettingsvoorspelling met ML voor HVAC-automatisering
De meest impactvolle use case: voorspel de bezetting van elke zone van het gebouw gedurende 30-60 minuten eerst het HVAC-systeem vooraf conditioneren (of uitschakelen), waardoor het comfort wordt gemaximaliseerd en het minimaliseren van het energieverbruik.
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',
};
}
Energiebenchmarking en KPI's
| KPI's | Formule | Doelgericht bouwen |
|---|---|---|
| EUI (Energiegebruiksintensiteit) | kWh/jaar / m2 | <100 kWh/m2 (kantoren, Mediterraan klimaat) |
| PUE (effectiviteit van energieverbruik) | Totaal vermogen / IT-vermogen | <1,5 voor inpandige datacenters |
| Thermische comfortindex | % uren in comfortbereik (20-24°C) | >95% uurbezetting |
| CO2-luchtkwaliteit | PPM gemiddelde bezette zones | <800 ppm (ASHRAE 62.1) |
| Op bezetting gebaseerde besparingen | kWh bespaard versus basislijn | 20-35% HVAC-besparingen |
OT/IT-beveiliging: kritieke infrastructuur
BACnet- en Modbus-systemen zijn ontworpen voor gesloten netwerken en beschikken over beveiliging beperkt (vaak geen authenticatie). Voordat u gebouwautomatiseringssystemen aansluit naar het IP-netwerk, implementeert: netwerksegmentatie (OT dedicated VLAN), eenrichtingsfirewall (datadiode) voor OT -> IT-communicatie, VPN voor externe toegang tot gateways en monitoring van verkeersafwijkingen. Een aanval op de HVAC-systemen van een gebouw kan schade veroorzaken belangrijke natuurkundigen.
Conclusies
IoT-integratie in slimme gebouwen is een toonaangevende technische en zakelijke kans: energiebesparing van 20-50%, verbetering van het comfort voor de bewoners en reductie van de CO2-uitstoot. Edge computing-architectuur zorgt voor operationele veerkracht, zelfs als de verbinding met de cloud wordt verbroken. terwijl machinaal leren voor het voorspellen van de bezetting HVAC-beslissingen automatiseert dan voorheen ze vereisten voortdurend menselijk ingrijpen.







