スマート ビルディング IoT: センサー統合とエッジ コンピューティング
ヨーロッパのスマートビルディング市場が追いついてきた 2025年に75億ユーロ、 エネルギー効率とエネルギー効率に関する EU の法律により、2024 年には 63 億人増加 不動産投資家に対する 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
- リアルタイム更新による建物のデジタルツイン
ビルディング オートメーション プロトコル
現代の商業ビルや住宅ビルでは特殊な通信プロトコルが使用されています センサー、アクチュエーター、制御システムを接続します。これらの標準の共存 単一の建物 (多くの場合、レガシー + モダン) 内にあり、統合が主な課題です。
主なプロトコル
- BACnet/IP: 商業ビルに関する ASHRAE 基準。 HVAC、アクセス制御、防火に使用
- Modbus 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
Modbus 電力メーター、インバーターの主要なプロトコル
太陽光発電システムと産業用 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: センサー データの時系列
InfluxDB 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 | Formula | 効率的な建物を目指す |
|---|---|---|
| EUI (エネルギー使用原単位) | kWh/年/m2 | <100 kWh/m2 (オフィス、地中海性気候) |
| PUE (電力使用効率) | 総合力・IT力 | 建物内データセンターの場合は <1.5 |
| 温熱快適性指数 | 快適な範囲 (20 ~ 24°C) で過ごした時間の割合 (%) | >95% 時間稼働率 |
| CO2 空気の質 | PPM 平均占有ゾーン | <800 ppm (ASHRAE 62.1) |
| 占有率に基づく節約 | ベースラインと比較して節約されたkWh | HVAC を 20 ~ 35% 節約 |
OT/IT セキュリティ: 重要なインフラストラクチャ
BACnet および Modbus システムは閉域ネットワーク用に設計されており、セキュリティが備わっています。 制限されています (多くの場合、認証はありません)。ビルディングオートメーションシステムを接続する前に IP ネットワークへの実装: ネットワーク セグメンテーション (OT 専用 VLAN)、一方向ファイアウォール (データ ダイオード) OT -> IT 通信、ゲートウェイへのリモート アクセスおよび監視用の VPN 交通異常のこと。建物の HVAC システムが攻撃されると損害が発生する可能性があります 重要な物理学者。
結論
スマート ビルディングにおける IoT の統合は、技術的およびビジネス上の主要な機会です。 エネルギーを 20 ~ 50% 節約し、居住者の快適性を向上させ、CO2 排出量を削減します。 エッジ コンピューティング アーキテクチャにより、クラウドから切断された場合でも運用上の復元力が保証されます。 一方、占有率予測のための機械学習により、以前よりも HVAC の決定が自動化されています。 継続的な人間の介入が必要でした。







