스마트 빌딩 IoT: 센서 통합 및 엣지 컴퓨팅
유럽의 스마트빌딩 시장이 따라잡았다 2025년에는 75억 유로, 에너지 효율에 관한 EU 법안과 부동산 투자자에 대한 ESG 압력. AI 기반 건물 관리 시스템을 갖춘 파일럿 의 에너지 소비 감소를 입증했습니다. 20-50%, 가장 높은 수익을 올린 HVAC 집약적 건물에서.
이 기사에서는 스마트 빌딩을 위한 IoT 시스템의 전체 아키텍처를 구축합니다. 빌딩 자동화 프로토콜(BACnet, Modbus, KNX) 통합부터 처리까지 실시간 에너지 분석부터 점유 기반 HVAC 자동화까지 MQTT를 통한 엣지 ML을 사용한 예측.
무엇을 배울 것인가
- 빌딩 자동화 프로토콜: BACnet, Modbus RTU/TCP, KNX, DALI
- IoT 게이트웨이를 갖춘 엣지 컴퓨팅: Node-RED를 갖춘 Raspberry Pi/산업용 PC
- MQTT 브로커: 센서 데이터에 대한 주제 설계 및 QoS
- InfluxDB 및 Grafana를 사용한 시계열 수집
- ML을 통한 점유율 예측: 자동 에너지 절약
- HVAC 자동화: PID 컨트롤러 및 수요 기반 환기
- 에너지 벤치마킹: ENERGY STAR 점수 및 ISO 50001
- 실시간 업데이트가 가능한 건물의 디지털 트윈
빌딩 자동화 프로토콜
현대 상업용 및 주거용 건물은 특수 통신 프로토콜을 사용합니다. 센서, 액추에이터 및 제어 시스템을 연결합니다. 이러한 표준의 공존 단일 건물(종종 레거시 + 현대)에 통합이라는 주요 과제가 있습니다.
주요 프로토콜
- 백넷/IP: 상업용 건물에 대한 ASHRAE 표준; HVAC, 출입 통제, 화재 안전에 사용됩니다.
- 모드버스 TCP/RTU: 간단한 산업 프로토콜; 에너지 센서, 인버터, PLC
- KNX: 주거용 및 상업용 건물에 대한 유럽 표준; 조명, 셔터, 난방
- 달리: 조명 제어 전용; 고급 밝기 조절 및 그룹화
- 지그비/Z-파: 저전력 센서용 무선 메시(온도, 점유율, CO2)
- MQTT: 최신 IoT 프로토콜; 레거시 프로토콜과 클라우드 프로토콜 간의 브리지
- OPC UA: 산업 상호 운용성; 스마트 빌딩의 채택 증가
엣지 컴퓨팅 아키텍처
기본 패턴 e 3티어 엣지: 센서/액추에이터(필드 레벨) -> 엣지 게이트웨이(로컬 처리, 프로토콜 번역) -> 클라우드(분석, ML, 대시보드). 지연 시간과 대역폭 소비를 줄이고 연속성을 보장하려면 엣지 처리가 중요합니다. 클라우드 연결이 끊어져도 작동됩니다.
// 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 통합: 에너지 미터 및 PLC
모드버스 전기 계량기, 인버터에 대한 주요 프로토콜
광전지 시스템 및 산업용 PLC. 통합을 위해서는 특정 레지스터를 읽어야 합니다.
도서관과 함께 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: 센서 데이터의 시계열
인플럭스DB v3 고주파 IoT에 대한 참조 시계열 데이터베이스. 일괄 쓰기 및 InfluxQL/Flux 쿼리를 통해 탁월한 성능으로 수십억 개의 데이터 포인트를 처리합니다.
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")
`;
HVAC 자동화를 위한 ML을 사용한 점유율 예측
가장 영향력 있는 사용 사례: 건물의 각 구역 점유를 30~60분 동안 예측합니다. 먼저 HVAC 시스템을 사전 조정(또는 종료)하여 편안함을 극대화하고 에너지 소비 최소화.
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',
};
}
에너지 벤치마킹 및 KPI
| KPI | 공식 | 목표 효율적인 건물 |
|---|---|---|
| EUI(에너지 사용 강도) | kWh/년 / m2 | <100kWh/m2(사무실, 지중해성 기후) |
| PUE(전력 사용 효율) | 총 전력 / IT 전력 | 건물 내 데이터 센터의 경우 <1.5 |
| 열적 쾌적 지수 | 편안한 범위(20~24°C)에서 % 시간 | >95% 시간 점유율 |
| CO2 공기질 | PPM 평균 점유 구역 | <800ppm(ASHRAE 62.1) |
| 점유 기반 절감액 | kWh 절감 대 기준선 | HVAC 20-35% 절감 |
OT/IT 보안: 중요 인프라
BACnet 및 Modbus 시스템은 폐쇄형 네트워크용으로 설계되었으며 보안 기능을 갖추고 있습니다. 제한적입니다(종종 인증되지 않음). 빌딩 자동화 시스템을 연결하기 전 IP 네트워크에 대해 다음을 구현합니다. 네트워크 분할(OT 전용 VLAN), 단방향 방화벽 OT -> IT 통신용 (데이터 다이오드), 게이트웨이 원격접속용 VPN, 모니터링 교통 이상현상. 건물의 HVAC 시스템에 대한 공격으로 인해 피해가 발생할 수 있습니다. 중요한 물리학자.
결론
스마트 빌딩의 IoT 통합은 선도적인 기술 및 비즈니스 기회입니다. 20~50%의 에너지 절약, 탑승자의 편안함 향상 및 CO2 배출 감소. 엣지 컴퓨팅 아키텍처는 클라우드와의 연결이 끊긴 경우에도 운영 탄력성을 보장합니다. 점유 예측을 위한 머신러닝은 이전보다 HVAC 결정을 자동화합니다. 지속적인 인간 개입이 필요했습니다.







