首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >楼宇自控IoT接入:基于TCP/IP的以太网温湿度传感器批量组态实操

楼宇自控IoT接入:基于TCP/IP的以太网温湿度传感器批量组态实操

原创
作者头像
HONSOR盛世宏博
发布于 2026-09-24 14:06:30
发布于 2026-09-24 14:06:30
1080
举报

楼宇自控IoT接入:基于TCP/IP的以太网温湿度传感器批量组态实操

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

一、场景边界:楼控接入和机房动环不是一回事

前面几篇围绕机房动环展开。本篇切到楼宇自控(BAS/IoT接入)场景:把分布在楼层、新风机房、弱电井、变配电室、冷站侧的以太网温湿度传感器,批量纳管进楼控系统。差异先讲清,避免套错架构。

  • 协议面不同:机房动环常自研采集+InfluxDB+Grafana;楼控侧主流是BACnet/IP做系统集成,Modbus TCP做设备南向,上层NMS/BAS工作站(如Honeywell EBI、Siemens Desigo、Johnson CCT、施耐德EcoStruxure、开放平台Node-RED/FAST/Volttron)。
  • 组态对象不同:楼控关心的是点(point/object)→ 报警、调度、联动风阀水阀,不是单纯时序曲线。
  • 交付物不同:工程交付物是组态库、点位表、网关配置、BAS图形页引用,不是开发文档。
  • 批量性不同:一栋楼几十到几百个测点,跨多个DDC子网,IP分配、模板下发、批量校核是重点。

实操主线:南向采Modbus TCP → 边缘网关归一化 → BACnet/IP服务端暴露 → BAS工作站组态引用;或反向,BAS直接Modbus轮询。下面给可落地路径。

二、网络与地址规划

楼控网通常分两层,别把传感器直接挂BAS工作站所在网段不管。

代码语言:javascript
复制
管理网/楼控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)

规划清单:

  • IP分配:DHCP保留或静态。传感器支持DCU/配置文件下发时,建议DHCP+预留表;否则出厂固化,批量烧录前做IP表。
  • 端口:Modbus TCP 502/TCP;BACnet/IP 47808/UDP(BIP)。跨子网时需BBMD或路由重定向。
  • PoE预算:每端口Class 1~2,接入交换机按机柜分区,留30%余量。
  • 采集周期:楼控常规1~5s轮询DDC侧,网关侧建议1~2s轮询设备,BAS呈现周期1s~10s。
  • 分区:按DDC回路/Floor/Zone分组,每组一个网关实例或虚拟设备实例,避免单BACnet设备实例点位过多。

三、南向批量接入:Modbus TCP组态

3.1 点表标准化

先建设备模板,别逐台配置。模板描述:连接参数+寄存器映射+缩放+告警。

代码语言:javascript
复制
# 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   # 组态时填充

注意点:

  • 寄存器偏移0-based还是1-based,mbpoll/pymodbus与点表一致。
  • 32位值跨寄存器,确认厂家字序(high-word first/low-word first)。
  • 单位缩放:很多设备内部×10,返回原始值,网关侧统一除10。
  • unit_id不一致时,用IP→unit映射表,不破坏模板。

3.2 设备清单驱动批量生成

工程侧常用CSV清单驱动,避免手工录入:

代码语言:javascript
复制
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对象表:

代码语言:javascript
复制
"""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")

3.3 边缘网关轮询实现(参考)

用Python服务或现成网关固件。下面是轻量参考,生产建议用成熟运行时(Node-RED、FAST、Volttron、Ignition Edge、KEPServerEX),此处给可移植逻辑。

代码语言:javascript
复制
"""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/IP组态

楼控工作站通常消费BACnet对象。边缘网关作为BACnet/IP服务端,每台传感器映射为一个Device或合并为一个结构化设备——后者更实际。

4.1 对象模型

  • 每台传感器 → 1个BACnet Device对象(device-instance = base + i),其下AnalogInput对象温度/湿度/露点,BinaryInput或MultistateInput状态。
  • 或:单个网关Device,下挂AnalogInput对象,object-identifier全局唯一,(type,instance)。推荐后者运维更简单,但BAS侧导航不如前者直观。交付时按客户BAS习惯定。

对象属性最小集:

  • object-name = alias(如 AHU1-RA-T)
  • present-value = 归一化后工程值
  • units = degreesCelsius / percentRelativeHumidity
  • cov-increment / notification-class
  • location = floor.zone
  • reliability = FAULT/NO_FAULT_DETECTED

4.2 用bacpypes暴露(参考)

生产可用商业库或网关,开发验证可用bacpypes3/bacpypes:

代码语言:javascript
复制
"""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栈进产线。

4.3 若BAS直接Modbus轮询

很多国产BAS/DDC上位机支持Modbus TCP驱动。此时边缘网关可省,BAS驱动按点表批量导入:

  • 驱动配置:TCP连接表、变量表(地址=IP:502:unit:reg)
  • 批量生成变量标签,绑定图形页
  • 缺点:DDC工作站轮询规模大时性能差,建议经网关归一化后再暴露Modbus TCP服务端(从站模拟)或BACnet。

五、批量组态下发与校验

5.1 自动发现 + 一致性校核

上电后先探测,再组态,别盲信清单。

代码语言:javascript
复制
"""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 res

BACnet侧用Who-Is/IAm发现网关设备实例,I-Am绑定;用ReadProperty验证对象表。

5.2 批量下发流程

  1. 导入设备清单CSV → 生成网关配置 → 推送到边缘网关(Ansible/REST/文件挂载)。
  2. 网关加载配置,建立南向连接池,启动轮询。
  3. 北向BACnet设备/对象注册,BAS工作站发现或手动导入EDF/CSV点表。
  4. 图形页绑定:BAS工程导入点位CSV,关联动态图块。
  5. 联动逻辑:温度越限→开新风阀/启备用机组;湿度高→启除湿/关加湿。

5.3 校验脚本

代码语言:javascript
复制
"""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 errs

BAS侧校验:读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 上电、探测、轮询、北向发现、联动测试

七、HA与运维

  • 边缘网关双机:主备或双活。BACnet设备实例在备机预热,VRRP/BACnet冗余需谨慎——BACnet无原生HA,做法是在BAS侧配双数据源或网关侧主备+VIP,故障切换时重新I-Am广播。
  • 配置版本化:Git或CMDB,模板变更热加载,回滚。
  • 观测:南向poll_err、北向bind状态、BACnet I-Am计数、对象present-value质量位、PoE端口状态。
  • 日志:网关运行日志+审计,联动操作留痕。
  • 安全:楼控网隔离,Modbus/BACnet端口不暴露业务网;固件升级签名;配置下发鉴权。

八、典型坑

  1. BACnet BIP跨子网:未配BBMD,工作站发现不到网关。边缘侧起BBMD或静态设备绑定。
  2. 对象实例冲突:批量生成时instance碰撞,BAS读错点。用中心化分配表。
  3. 单位/缩放双重处理:网关除10,BAS再除10,曲线错。约定北向暴露工程值。
  4. COV风暴:cov-increment过小,网络抖动时大量通知。按点设增量+节流。
  5. DDC轮询周期与网关缓存TTL不匹配:BAS读到陈旧值。明确缓存年龄字段,BAS侧超时判定。
  6. Modbus unit_id混淆:跨IP相同unit,网关连接池按(ip,unit)分键。
  7. 批量下发部分失败:清单IP冲突、PoE未上电。先探测再下发,灰度分区。
  8. 告警死锁:联动启设备后温度回落,振荡。加死区+最小动作间隔。
  9. 固件点表版本:同型号不同批次寄存器布局变。模板按固件版本分支。
  10. 网关单点:进程挂BAS失联。systemd守护+健康检查+自动重启,备机待命。

九、小结

楼控IoT接入的批量组态,本质是配置驱动的工程化交付:标准化点表模板 + 设备清单驱动生成 + 边缘网关归一化 + BACnet/IP北向暴露 + BAS图形页绑定。开发侧提供工具链和校验脚本,工程侧关注下发、发现、联动测试。别把楼控集成做成自研采集系统,复用DDC/BAS既有对象模型和告警调度,网关只做协议桥接与批量纳管,交付才闭环。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 楼宇自控IoT接入:基于TCP/IP的以太网温湿度传感器批量组态实操
    • 一、场景边界:楼控接入和机房动环不是一回事
    • 二、网络与地址规划
    • 三、南向批量接入:Modbus TCP组态
      • 3.1 点表标准化
      • 3.2 设备清单驱动批量生成
      • 3.3 边缘网关轮询实现(参考)
    • 四、北向暴露:BACnet/IP组态
      • 4.1 对象模型
      • 4.2 用bacpypes暴露(参考)
      • 4.3 若BAS直接Modbus轮询
    • 五、批量组态下发与校验
      • 5.1 自动发现 + 一致性校核
      • 5.2 批量下发流程
      • 5.3 校验脚本
    • 六、工程交付包
    • 七、HA与运维
    • 八、典型坑
    • 九、小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档