
关键词:楼宇自控、BACnet/IP、Modbus TCP、以太网温湿度传感器、批量组态、设备模板、自动发现、点位映射、BAS集成、边缘网关 标签:#物联网 #Modbus #TCP/IP #POE供电 #Python #InfluxDB #以太网温湿度传感器 #网口温湿度变送器 #机房监控 #边缘计算 #楼宇自控

前面几篇围绕机房动环展开。本篇切到楼宇自控(BAS/IoT接入)场景:把分布在楼层、新风机房、弱电井、变配电室、冷站侧的以太网温湿度传感器,批量纳管进楼控系统。差异先讲清,避免套错架构。
实操主线:南向采Modbus TCP → 边缘网关归一化 → BACnet/IP服务端暴露 → BAS工作站组态引用;或反向,BAS直接Modbus轮询。下面给可落地路径。

楼控网通常分两层,别把传感器直接挂BAS工作站所在网段不管。
管理网/楼控BAS网 (VLAN 100, 10.10.10.0/24)
└── 边缘网关 x2 (HA)
├── Modbus TCP 南向 (VLAN 200, 192.168.200.0/24)
│ ├── 接入交换机/PoE
│ └── 以太网温湿度传感器 N台, port 502
└── BACnet/IP 北向 (VLAN 100, UDP/47808=BIP)规划清单:
先建设备模板,别逐台配置。模板描述:连接参数+寄存器映射+缩放+告警。
# sensor_template.yaml
template_id: eth-th-01
transport: modbus-tcp
port: 502
unit_id: 1 # 多数网口变送器从站地址可设,批量时统一或按IP映射
poll_period_ms: 2000
timeout_ms: 1500
retry: 2
registers:
- {name: temperature, block: holding, offset: 0, type: int16, scale: 0.1, signed: true}
- {name: humidity, block: holding, offset: 1, type: int16, scale: 0.1, signed: false}
- {name: dewpoint, block: holding, offset: 2, type: int16, scale: 0.1, signed: true}
- {name: status_word, block: holding, offset: 3, type: uint16, scale: 1.0}
- {name: uptime_min, block: holding, offset: 4, type: uint32, wordorder: hilo}
alarms:
- {point: temperature, hi: 28.0, lo: 16.0, deadband: 0.5}
- {point: humidity, hi: 65.0, lo: 35.0, deadband: 2.0}
meta:
location_attr: floor_zone # 组态时填充注意点:
工程侧常用CSV清单驱动,避免手工录入:
alias,ip,unit_id,floor,zone,ddc_loop,enable
AHU1-RA-T,192.168.200.11,1,L2,Z1,DL1,1
AHU1-SA-T,192.168.200.12,1,L2,Z2,DL1,1
CRAC-1-T,192.168.200.21,1,L3,CRAC,DL2,1
...生成网关配置 + BACnet对象表:
"""gen_config.py - 由清单+模板生成组态"""
import yaml, csv, json
from jinja2 import Template # 或直接拼dict
tpl = yaml.safe_load(open("sensor_template.yaml"))
devs = []
with open("devices.csv") as f:
for row in csv.DictReader(f):
if row["enable"] != "1":
continue
devs.append({
"alias": row["alias"],
"ip": row["ip"],
"unit_id": int(row["unit_id"]),
"floor": row["floor"],
"zone": row["zone"],
"ddc_loop": row["ddc_loop"],
"poll_period_ms": tpl["poll_period_ms"],
"points": tpl["registers"],
"alarms": tpl["alarms"],
})
config = {
"gateway": {"southbound": {"modbus_tcp": devs}},
"northbound": {"bacnet": {"device_instance_base": 10000, "increment": 1}},
}
json.dump(config, open("gateway_config.json","w"), indent=2)
print(f"generated {len(devs)} devices")用Python服务或现成网关固件。下面是轻量参考,生产建议用成熟运行时(Node-RED、FAST、Volttron、Ignition Edge、KEPServerEX),此处给可移植逻辑。
"""gw_modbus_poller.py"""
import asyncio, logging
from pymodbus.client import AsyncModbusTcpClient
from dataclasses import dataclass, asdict
@dataclass
class DevCfg:
alias: str; ip: str; unit: int; period: float
points: list
floor: str; zone: str; ddc_loop: str
class Southbound:
def __init__(self, devs, cache):
self.devs = devs
self.cache = cache # dict alias->point dict, 也推北向
self.clients = {}
async def client(self, d):
c = self.clients.get(d.ip)
if c is None or not c.connected:
c = AsyncModbusTcpClient(d.ip, port=502)
await c.connect()
self.clients[d.ip] = c
return c
async def poll(self, d):
try:
c = await self.client(d)
# 单次读连续块,offset0..max_off
max_off = max(p["offset"] for p in d.points) + (2 if any(p["type"]=="uint32" for p in d.points) else 1)
rr = await asyncio.wait_for(
c.read_holding_registers(0, max_off, slave=d.unit), timeout=2.0)
if rr.isError():
raise RuntimeError(str(rr))
regs = rr.registers
vals = {}
for p in d.points:
raw = regs[p["offset"]]
if p["type"] == "int16":
v = raw/10.0 if not p.get("signed",True) else (raw-0x10000)/10.0 if raw>=0x8000 else raw/10.0
elif p["type"] == "uint16":
v = raw*p["scale"]
elif p["type"] == "uint32":
hi = regs[p["offset"]]; lo = regs[p["offset"]+1]
v = ((hi<<16)|lo)*p["scale"]
vals[p["name"]] = v
vals["_online"] = True
vals["_ts"] = asyncio.get_event_loop().time()
self.cache[d.alias] = vals
except Exception as e:
logging.warning("poll %s fail: %s", d.alias, e)
cur = self.cache.setdefault(d.alias, {})
cur["_online"] = False
async def run(self):
while True:
for d in self.devs:
asyncio.create_task(self.poll(d))
await asyncio.sleep(min(d.period for d in self.devs)/1000.0)工程化补充:并发限制、退避重连、连接池、缓存TTL、状态字解码、离线保持上次值或置bad质量。
楼控工作站通常消费BACnet对象。边缘网关作为BACnet/IP服务端,每台传感器映射为一个Device或合并为一个结构化设备——后者更实际。
对象属性最小集:
生产可用商业库或网关,开发验证可用bacpypes3/bacpypes:
"""gw_bacnet.py - 简化示例,勿直接上产线"""
from bacpypes3.ipv4.app import Application
from bacpypes3.local.device import DeviceObject
from bacpypes3.local.object import AnalogInputObject
from bacpypes3.primitivedata import CharacterString, Real
import asyncio, json
def build_app(cache, devs, device_instance=10101, addr="10.10.10.50/24"):
d = DeviceObject(objectIdentifier=("device", device_instance),
objectName="ETH-TH-GW",
servicesSupported=[...])
app = Application(d, addr)
# 建对象
inst = 0
for dev in devs:
for pt in ("temperature","humidity"):
inst += 1
ai = AnalogInputObject(
objectIdentifier=("analogInput", inst),
objectName=f"{dev['alias']}.{pt}",
units="degreesCelsius" if pt=="temperature" else "percentRelativeHumidity",
presentValue=Real(0.0),
covIncrement=0.2,
)
app.add_object(ai)
dev[f"_obj_{pt}"] = ai
return app, d
async def push_loop(app, cache, devs):
while True:
for dev in devs:
vals = cache.get(dev["alias"], {})
for pt in ("temperature","humidity"):
obj = dev.get(f"_obj_{pt}")
if obj and pt in vals:
obj.presentValue = Real(float(vals[pt]))
await asyncio.sleep(1.0)注意:bacpypes参考代码需补齐应用上下文、事件循环、COV通知、读写属性服务。交付环境更推荐用成熟网关或OPC UA/Modbus北向,别自研BACnet栈进产线。
很多国产BAS/DDC上位机支持Modbus TCP驱动。此时边缘网关可省,BAS驱动按点表批量导入:
上电后先探测,再组态,别盲信清单。
"""discover.py - Modbus探测+SNMP可选,产出实际清单"""
import asyncio
from pymodbus.client import AsyncModbusTcpClient
async def probe(ip):
c = AsyncModbusTcpClient(ip, port=502)
try:
await asyncio.wait_for(c.connect(), timeout=1.0)
rr = await asyncio.wait_for(c.read_holding_registers(0,4,slave=1), timeout=1.0)
if not rr.isError():
return ip, rr.registers
except Exception:
pass
finally:
c.close()
return ip, None
async def scan(subnet_ips):
res = {}
for ip in subnet_ips:
r = await probe(ip)
if r[1]: res[ip]=r[1]
return resBACnet侧用Who-Is/IAm发现网关设备实例,I-Am绑定;用ReadProperty验证对象表。
"""validate.py - 组态与现场一致性"""
import json, asyncio
from pymodbus.client import AsyncModbusTcpClient
async def check(config, cache):
errs=[]
for d in config["gateway"]["southbound"]["modbus_tcp"]:
c = AsyncModbusTcpClient(d["ip"], port=502)
ok=False
try:
await c.connect();
rr=await c.read_holding_registers(0,1,slave=d["unit_id"])
ok=not rr.isError()
except Exception as e:
errs.append((d["alias"],"conn",str(e)))
finally:
c.close()
cached=cache.get(d["alias"],{})
if cached and not cached.get("_online"):
errs.append((d["alias"],"offline",""))
# 质量检查
v=cached.get("temperature")
if v is not None and (v<-40 or v>85):
errs.append((d["alias"],"range",v))
return errsBAS侧校验:读BACnet present-value与现场比对;COV订阅验证;报警触发测试。
交付时给现场工程师,而非开发文档:
devices.csv 点位清单 sensor_template.yaml 点表模板 gateway_config.json 网关配置 bacnet_object_map.csv object-name, object-identifier, alias, location, units bas_import.csv BAS驱动/图形页导入格式 failover.md 主备切换、IP漂移、回退 test_checklist.md 上电、探测、轮询、北向发现、联动测试 楼控IoT接入的批量组态,本质是配置驱动的工程化交付:标准化点表模板 + 设备清单驱动生成 + 边缘网关归一化 + BACnet/IP北向暴露 + BAS图形页绑定。开发侧提供工具链和校验脚本,工程侧关注下发、发现、联动测试。别把楼控集成做成自研采集系统,复用DDC/BAS既有对象模型和告警调度,网关只做协议桥接与批量纳管,交付才闭环。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。