Smart Building IoT: Integrace senzorů a Edge Computing
Evropský trh s inteligentními budovami dohnal 7,5 miliardy eur v roce 2025, v roce 2024 vzrostly o 6,3 miliardy na základě právních předpisů EU o energetické účinnosti a Tlak ESG na investory do nemovitostí. Pilotujte se systémy správy budov řízenými umělou inteligencí prokázaly snížení spotřeby energie 20–50 %, s nejvyššími výdělky v budovách náročných na HVAC.
V tomto článku vytváříme kompletní architekturu systému IoT pro chytré budovy: od integrace protokolů automatizace budov (BACnet, Modbus, KNX) až po zpracování s MQTT, od energetické analýzy v reálném čase až po automatizaci HVAC založenou na obsazenosti predikce s ML.
Co se naučíte
- Protokoly automatizace budov: BACnet, Modbus RTU/TCP, KNX, DALI
- Edge computing s IoT bránou: Raspberry Pi/průmyslové PC s Node-RED
- MQTT broker: návrh tématu a QoS pro data senzorů
- Příjem časových řad pomocí InfluxDB a Grafana
- Predikce obsazenosti s ML: automatická úspora energie
- Automatizace HVAC: PID regulátor a ventilace řízená poptávkou
- Energetické srovnávání: Skóre ENERGY STAR a ISO 50001
- Digitální dvojče budovy s aktualizací v reálném čase
Protokoly automatizace budov
Moderní komerční a obytné budovy používají specializované komunikační protokoly pro připojení senzorů, akčních členů a řídicích systémů. Koexistence těchto norem v jediné budově (často starší + moderní) a hlavní výzvou integrace.
Hlavní protokoly
- BACnet/IP: normy ASHRAE pro komerční budovy; používá se pro HVAC, kontrolu přístupu, požární bezpečnost
- Modbus TCP/RTU: jednoduchý průmyslový protokol; senzory energie, invertory, PLC
- KNX: evropský standard pro obytné a komerční budovy; osvětlení, žaluzie, topení
- DALI: specifické pro ovládání osvětlení; pokročilé stmívání a seskupování
- Zigbee/Z-Wave: bezdrátová síť pro nízkopříkonové senzory (teplota, obsazenost, CO2)
- MQTT: moderní protokol IoT; most mezi staršími a cloudovými protokoly
- OPC UA: průmyslová interoperabilita; rostoucí přijetí v inteligentních budovách
Architektura Edge Computing
Základní vzorec e 3-vrstvý okraj: senzory/aktory (úroveň pole) -> edge gateway (lokální zpracování, překlad protokolů) -> cloud (analytics, ML, dashboard). Zpracování okrajů je zásadní pro snížení latence, spotřeby šířky pásma a zajištění kontinuity funkční i po odpojení od cloudu.
// 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;
}
Integrace Modbus: Měřiče energie a PLC
Modbus a dominantní protokol pro elektroměry, střídače
fotovoltaické systémy a průmyslové PLC. Integrace vyžaduje čtení specifických registrů
s knihovnou 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: Časová řada pro data senzorů
InfluxDB v3 a referenční databáze časových řad pro vysokofrekvenční internet věcí. Díky dávkovým zápisům a dotazům InfluxQL/Flux zpracovává miliardy datových bodů s vynikajícím výkonem.
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")
`;
Predikce obsazenosti s ML pro automatizaci HVAC
Nejpůsobivější případ použití: předpovězte obsazenost každé zóny budovy na 30–60 minut nejprve připravit (nebo vypnout) systém HVAC, maximalizovat pohodlí a minimalizace spotřeby 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',
};
}
Energetický benchmarking a KPI
| KPI | Vzorec | Cílová efektivní budova |
|---|---|---|
| EUI (Intenzita využití energie) | kWh/rok/m2 | <100 kWh/m2 (kanceláře, středomořské klima) |
| PUE (účinnost využití energie) | Celkový výkon / výkon IT | <1,5 pro datová centra v budově |
| Index tepelného komfortu | % hodin v komfortním rozsahu (20-24°C) | Obsazenost > 95 % hodin |
| CO2 Kvalita vzduchu | PPM průměrně obsazené zóny | <800 ppm (ASHRAE 62.1) |
| Úspory podle obsazenosti | ušetřená kWh oproti výchozí hodnotě | Úspora 20-35 % HVAC |
Bezpečnost OT/IT: kritická infrastruktura
Systémy BACnet a Modbus byly navrženy pro uzavřené sítě a mají zabezpečení omezené (často bez ověřování). Před připojením systémů automatizace budov do IP sítě, implementuje: segmentaci sítě (OT dedikovaná VLAN), jednosměrný firewall (datová dioda) pro OT -> IT komunikaci, VPN pro vzdálený přístup k bránám a monitoring dopravních anomálií. Útok na systémy HVAC budovy může způsobit poškození významných fyziků.
Závěry
Integrace IoT v chytrých budovách je přední technickou a obchodní příležitostí: úspora energie 20-50 %, zlepšení komfortu cestujících a snížení emisí CO2. Architektura Edge Computing zajišťuje provozní odolnost i v případě odpojení od cloudu, zatímco strojové učení pro predikci obsazenosti automatizuje rozhodování HVAC než dříve vyžadovaly nepřetržitý lidský zásah.







