

档案库房环境稳态维持的本质矛盾是:设备有惯性,空间有梯度,控制有延迟。恒湿机从启动压缩机到出风温度稳定需要 3~5 分钟,从出风口到密集架内部传感器感知到变化需要 5~15 分钟,而库房空间内不同位置的温湿度梯度可能达到 3~5℃/8~10%RH。如果简单地"到阈值就开、回阈值就关",结果必然是设备频繁启停、温湿度在阈值附近反复振荡、设备寿命急剧缩短。
某市级档案馆(库房面积 3200㎡,4 个分区,每区 8 台恒湿一体机)改造前采用"单点阈值控制"——每个分区只装 1 台温湿度传感器,设 50%RH 回差 ±5% 启停。运行数据记录显示:恒湿机日均启停 47 次,压缩机平均连续运行仅 8 分钟即被关闭,库房湿度在 44%~58%RH 之间大幅波动,部分密集架内部湿度极值达 39%RH 和 63%RH,远超 DA/T 47 要求的 ±5%RH 波动范围。
改造后采用多点位传感数据驱动的分区设备启停策略,恒湿机日均启停降至 6 次,连续运行时间延长到 45 分钟以上,库房湿度波动收窄到 47%~53%RH,设备能耗下降 28%,压缩机故障率从每年 3.2 次/台降至 0.4 次/台。
档案库房感知密度推荐标准:
┌─────────────────────────────────────────────────────────────────────────┐
│ 库房类型 │ 单点覆盖面积 │ 推荐密度 │ 备注 │
├─────────────────────┼──────────────┼─────────────┼─────────────────────┤
│ 普通纸质档案库房 │ 80~120㎡ │ 每 100㎡ 1台│ 均匀布置,避开死角 │
│ 特藏库/古籍库 │ 40~60㎡ │ 每 50㎡ 1台 │ 密集架内部必装 │
│ 胶片/磁介质库房 │ 30~50㎡ │ 每 40㎡ 1台 │ 温湿度要求更严 │
│ 展厅/阅览区 │ 60~80㎡ │ 每 70㎡ 1台 │ 人员流动影响大 │
│ 缓冲间/过渡区 │ 20~30㎡ │ 每区 1~2台 │ 门开闭影响监测 │
└─────────────────────────────────────────────────────────────────────────┘
部署位置矩阵(以 100㎡ 标准分区为例):
分区平面图(俯视):
┌──────────────────────────────────────────────────────────────────┐
│ 入口缓冲区 │
│ ┌─────────┐ │
│ │ S-01 │ ← 门边(受开门影响最大,权重 1.3) │
│ └─────────┘ │
│ ─────────── 防火门 ─────────── │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ S-02 │ │ S-03 │ │ S-04 │ │ S-05 │ │
│ │ 门口侧 │ │ 中部 │ │ 深处 │ │ 密集架内│ │
│ │ 权重1.2 │ │ 权重1.0 │ │ 权重1.1 │ │ 权重1.5 │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ S-06 │ │ S-07 │ │ S-08 │ │ S-09 │ │
│ │ 空调下 │ │ 中部 │ │ 回风侧 │ │ 密集架内│ │
│ │ 权重0.9 │ │ 权重1.0 │ │ 权重1.2 │ │ 权重1.5 │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
└──────────────────────────────────────────────────────────────────┘
高度方向(垂直梯度):
├── 地面 0.3m:S-L1(底层,冷空气沉积)
├── 档案中层 1.5m:S-L2(主要监测面)
└── 顶部 2.8m:S-L3(热空气聚集,空调回风参考)"""
多点位温湿度空间聚合
输入:N 台传感器实时数据
输出:分区综合状态(均值、极值、梯度、一致性)
"""
import statistics
from dataclasses import dataclass
from typing import List, Dict, Tuple
@dataclass
class SensorPoint:
id: str
zone: str
position: str # 门口/中部/深处/密集架内/空调下/回风侧
temperature: float # ℃
humidity: float # %RH
weight: float # 空间权重
valid: bool # 数据有效性
timestamp: float # 时间戳
class ZoneAggregator:
"""分区数据聚合器"""
def __init__(self, zone_id: str):
self.zone_id = zone_id
self.sensors: Dict[str, SensorPoint] = {}
self._ema_temp = None # 指数移动平均(温度)
self._ema_humidity = None # 指数移动平均(湿度)
self._ema_alpha = 0.3 # EMA 平滑系数
def update(self, sensor: SensorPoint):
self.sensors[sensor.id] = sensor
def get_aggregated_state(self) -> dict:
"""计算分区聚合状态"""
valid_sensors = [s for s in self.sensors.values() if s.valid]
if len(valid_sensors) == 0:
return {'error': 'no valid sensors'}
# 1. 加权均值(核心控制变量)
total_w = sum(s.weight for s in valid_sensors)
weighted_temp = sum(s.temperature * s.weight for s in valid_sensors) / total_w
weighted_humidity = sum(s.humidity * s.weight for s in valid_sensors) / total_w
# 2. 极值(边界条件判断)
max_temp = max(s.temperature for s in valid_sensors)
min_temp = min(s.temperature for s in valid_sensors)
max_humidity = max(s.humidity for s in valid_sensors)
min_humidity = min(s.humidity for s in valid_sensors)
# 3. 空间梯度(均匀性评估)
temp_gradient = max_temp - min_temp
humidity_gradient = max_humidity - min_humidity
# 4. 一致性检查(是否有传感器异常)
humidity_values = [s.humidity for s in valid_sensors]
humidity_cv = statistics.stdev(humidity_values) / statistics.mean(humidity_values) if len(humidity_values) > 1 else 0
# 5. EMA 平滑(用于趋势判断)
if self._ema_temp is None:
self._ema_temp = weighted_temp
self._ema_humidity = weighted_humidity
else:
self._ema_temp = self._ema_alpha * weighted_temp + (1 - self._ema_alpha) * self._ema_temp
self._ema_humidity = self._ema_alpha * weighted_humidity + (1 - self._ema_alpha) * self._ema_humidity
# 6. 变化率(℃/min 或 %RH/min)
# 实际项目中需要记录历史序列计算
return {
'zone_id': self.zone_id,
'sensor_count': len(valid_sensors),
'weighted_temperature': round(weighted_temp, 2),
'weighted_humidity': round(weighted_humidity, 2),
'ema_temperature': round(self._ema_temp, 2),
'ema_humidity': round(self._ema_humidity, 2),
'max_temperature': max_temp,
'min_temperature': min_temp,
'max_humidity': max_humidity,
'min_humidity': min_humidity,
'temp_gradient': round(temp_gradient, 2),
'humidity_gradient': round(humidity_gradient, 2),
'humidity_cv': round(humidity_cv, 4),
'consistency': 'OK' if humidity_cv < 0.08 else 'WARNING',
}传统方案(单点阈值 + 回差):
设定值:50%RH,回差 ±3%RH
启动除湿:湿度 ≥ 53%RH
停止除湿:湿度 ≤ 50%RH
启动加湿:湿度 ≤ 47%RH
停止加湿:湿度 ≥ 50%RH
问题:
① 传感器在空调出风口附近 → 读数偏低 → 设备频繁加湿 → 深处过湿
② 单点失效(漂移/离线)→ 整个分区失控
③ 回差太小(±3%)→ 设备 5 分钟启停一次
④ 回差太大(±8%)→ 库房湿度波动 ±8%RH,超标
⑤ 不考虑设备惯性 → 压缩机启动 3 分钟后才出冷风,此时湿度已自然下降
系统误判"设备无效"或"过度制冷"启停决策逻辑(分层判断):
Layer 1:安全硬限(最高优先级,不可覆盖)
├── 温度 > 28℃ → 强制启动所有空调/除湿
├── 温度 < 14℃ → 强制启动加热/关闭制冷
├── 湿度 > 65%RH → 强制启动除湿(防霉)
├── 湿度 < 35%RH → 强制启动加湿(防纸张脆化)
└── 任何安全硬限触发 → 绕过所有延时和调度逻辑
Layer 2:稳态启停判断(核心逻辑)
├── 输入:分区加权均值 + EMA 平滑值 + 变化率 + 梯度
├── 判断条件(以除湿为例):
│
│ 启动条件(ALL 满足):
│ ① 加权均值 > 目标值 + 启动偏移(默认 +2.5%RH)
│ ② EMA 湿度 > 目标值 + 1.5%RH(排除瞬时波动)
│ ③ 湿度变化率 > 0(在上升,不是偶然峰值)
│ ④ 持续时间 > 滞后时间 T_hold(默认 5min)
│ ⑤ 当前无设备处于"冷却锁定"状态
│
│ 停止条件(ANY 满足):
│ ① 加权均值 < 目标值 - 停止偏移(默认 -1.0%RH)
│ ② 最低点湿度 < 目标值 - 3%RH(防止局部过干)
│ ③ 设备连续运行 > 最大运行时 T_max(默认 90min,防冻结)
│ ④ 湿度变化率 < -0.5%RH/min(快速下降,可能过调)
│ ⑤ EMA 湿度 < 目标值 - 0.5%RH
│
└── 加湿逻辑对称
Layer 3:设备调度(多台设备间的启停分配)
├── 轮值调度:4 台设备按累计运行时间排序,优先启动运行时间最短的
├── 负载分配:湿度偏差越大,启动越多设备
│ 偏差 2~4%RH → 启动 1 台
│ 偏差 4~6%RH → 启动 2 台
│ 偏差 > 6%RH → 启动 3 台
├── 错峰启动:多台设备启动间隔 ≥ 3min(避免启动电流冲击)
└── 冷却锁定:压缩机停机后锁定 5min 才能再次启动(保护压缩机)
Layer 4:防振荡与自适应
├── 启停死区自适应:根据历史振荡幅度动态调整回差
│ 如果过去 1h 内启停次数 > 4 次 → 自动扩大回差 0.5%RH
│ 如果连续 2h 无启停 → 可缩小回差 0.3%RH(提高精度)
├── 变化率预测:基于最近 15min 趋势外推 10min 后湿度
│ 如果预测值将进入安全范围 → 延迟启动
│ 如果预测值将超出安全硬限 → 提前启动
└── 梯度感知:某分区梯度 > 6%RH → 启动定向调控而非全局启停#!/usr/bin/env python3
"""
档案库房多点位传感驱动的设备启停策略引擎
"""
import asyncio
import time
from enum import Enum, auto
from dataclasses import dataclass, field
from typing import List, Dict, Optional
class DeviceMode(Enum):
OFF = 0
HUMIDIFY = 1
DEHUMIDIFY = 2
COOL = 3
HEAT = 4
class DeviceState(Enum):
STOPPED = 0
STARTING = 1 # 启动中(压缩机预热/延时)
RUNNING = 2
STOPPING = 3 # 停机中(冷却锁定)
FAULT = 4
LOCKED = 5 # 冷却锁定中
@dataclass
class HumidifierDevice:
device_id: str
zone_id: str
modbus_addr: int
state: DeviceState = DeviceState.STOPPED
mode: DeviceMode = DeviceMode.OFF
cumulative_runtime: float = 0.0 # 累计运行时间(小时)
last_start_time: float = 0.0
last_stop_time: float = 0.0
cooldown_until: float = 0.0 # 冷却锁定到期时间
startup_delay: float = 30.0 # 启动延时(秒)
cooldown_period: float = 300.0 # 冷却锁定时间(秒)
max_runtime: float = 5400.0 # 最大连续运行时间(秒,90min)
current_runtime: float = 0.0 # 本次连续运行时间
@dataclass
class ZoneState:
zone_id: str
target_humidity: float = 50.0
target_temperature: float = 20.0
start_offset: float = 2.5 # 启动偏移 %RH
stop_offset: float = 1.0 # 停止偏移 %RH
hold_time: float = 300.0 # 持续确认时间(秒,5min)
ema_alpha: float = 0.3
# 实时状态
weighted_humidity: float = 50.0
ema_humidity: float = 50.0
humidity_rate: float = 0.0 # %RH/min
humidity_gradient: float = 0.0
sensor_count: int = 0
# 启停决策状态
dehumidify_request: bool = False
humidify_request: bool = False
dehumidify_request_since: float = 0.0
humidify_request_since: float = 0.0
last_state_change: float = 0.0
# 自适应参数
adaptive_deadband: float = 3.0 # 自适应回差
oscillation_count_1h: int = 0
last_hour_reset: float = 0.0
class DeviceSchedulingEngine:
"""设备调度引擎"""
def __init__(self, zone: ZoneState, devices: List[HumidifierDevice]):
self.zone = zone
self.devices = devices
self.running = False
def _sort_by_runtime(self) -> List[HumidifierDevice]:
"""按累计运行时间升序排列"""
return sorted(self.devices, key=lambda d: d.cumulative_runtime)
def _available_devices(self, now: float) -> List[HumidifierDevice]:
"""获取可启动的设备列表"""
available = []
for d in self.devices:
if d.state == DeviceState.STOPPED and now >= d.cooldown_until:
available.append(d)
return available
def _running_devices(self) -> List[HumidifierDevice]:
"""获取正在运行的设备"""
return [d for d in self.devices if d.state == DeviceState.RUNNING]
def select_devices_to_start(self, required_count: int, now: float) -> List[HumidifierDevice]:
"""选择要启动的设备"""
available = self._available_devices(now)
if not available:
return []
# 按累计运行时间排序,优先启动"最闲"的设备
available.sort(key=lambda d: d.cumulative_runtime)
# 错峰启动:检查最近一次启动时间
selected = []
for d in available:
if len(selected) >= required_count:
break
# 错峰:如果最近有设备启动且不到 3 分钟,延迟选择
if selected and (now - selected[-1].last_start_time) < 180:
continue
selected.append(d)
return selected[:required_count]
def select_devices_to_stop(self, required_count: int) -> List[HumidifierDevice]:
"""选择要停止的设备(优先停运行时间最长的)"""
running = self._running_devices()
if not running:
return []
# 按本次运行时间降序排列
running.sort(key=lambda d: d.current_runtime, reverse=True)
return running[:required_count]
class StartStopDecisionEngine:
"""启停决策引擎"""
def __init__(self, zone: ZoneState, devices: List[HumidifierDevice]):
self.zone = zone
self.scheduler = DeviceSchedulingEngine(zone, devices)
self.devices = devices
def update_zone_state(self, agg_state: dict):
"""更新分区状态"""
self.zone.weighted_humidity = agg_state['weighted_humidity']
self.zone.ema_humidity = agg_state['ema_humidity']
self.zone.humidity_gradient = agg_state['humidity_gradient']
self.zone.sensor_count = agg_state['sensor_count']
def evaluate(self, now: float) -> dict:
"""
执行启停决策评估
返回决策指令列表
"""
decisions = []
humidity = self.zone.weighted_humidity
ema = self.zone.ema_humidity
target = self.zone.target_humidity
deadband = self.zone.adaptive_deadband
# ===== Layer 1:安全硬限 =====
# 湿度 > 65%RH → 强制除湿
if humidity > 65.0:
running = self.scheduler._running_devices()
if len(running) < len(self.devices):
needed = min(2, len(self.devices) - len(running))
to_start = self.scheduler.select_devices_to_start(needed, now)
for d in to_start:
decisions.append({
'action': 'START',
'device_id': d.device_id,
'mode': DeviceMode.DEHUMIDIFY,
'reason': f'SAFETY: 湿度 {humidity:.1f}%RH > 65%RH 硬限',
'priority': 'CRITICAL'
})
return decisions
# 湿度 < 35%RH → 强制加湿
if humidity < 35.0:
running = self.scheduler._running_devices()
if len(running) < len(self.devices):
needed = min(2, len(self.devices) - len(running))
to_start = self.scheduler.select_devices_to_start(needed, now)
for d in to_start:
decisions.append({
'action': 'START',
'device_id': d.device_id,
'mode': DeviceMode.HUMIDIFY,
'reason': f'SAFETY: 湿度 {humidity:.1f}%RH < 35%RH 硬限',
'priority': 'CRITICAL'
})
return decisions
# ===== Layer 2:稳态启停判断 =====
# --- 除湿判断 ---
dehumidify_start_cond = (
humidity > target + self.zone.start_offset + deadband / 2
and ema > target + 1.5
and self.zone.humidity_rate >= 0
and now - self.zone.dehumidify_request_since > self.zone.hold_time
)
dehumidify_stop_cond = (
humidity < target - self.zone.stop_offset
or self.zone.humidity_gradient > 8.0 # 梯度过大,需切换策略
)
# --- 加湿判断 ---
humidify_start_cond = (
humidity < target - self.zone.start_offset - deadband / 2
and ema < target - 1.5
and self.zone.humidity_rate <= 0
and now - self.zone.humidify_request_since > self.zone.hold_time
)
humidify_stop_cond = (
humidity > target + self.zone.stop_offset
or self.zone.humidity_gradient > 8.0
)
# 更新请求计时
if humidity > target + self.zone.start_offset:
if self.zone.dehumidify_request_since == 0:
self.zone.dehumidify_request_since = now
else:
self.zone.dehumidify_request_since = 0
if humidity < target - self.zone.start_offset:
if self.zone.humidify_request_since == 0:
self.zone.humidify_request_since = now
else:
self.zone.humidify_request_since = 0
# 执行除湿启停决策
running = self.scheduler._running_devices()
if dehumidify_start_cond and len(running) < len(self.devices):
# 确定需要启动的设备数量(基于偏差幅度)
deviation = humidity - target
if deviation > 6:
needed = min(3, len(self.devices) - len(running))
elif deviation > 4:
needed = min(2, len(self.devices) - len(running))
else:
needed = 1
to_start = self.scheduler.select_devices_to_start(needed, now)
for d in to_start:
decisions.append({
'action': 'START',
'device_id': d.device_id,
'mode': DeviceMode.DEHUMIDIFY,
'reason': f'除湿启动: 湿度{humidity:.1f}%RH, 偏差+{deviation:.1f}%RH, 需{needed}台',
'priority': 'NORMAL'
})
elif dehumidify_stop_cond and len(running) > 0:
# 确定需要停止的数量
if humidity < target - self.zone.stop_offset:
needed = len(running) # 全部停止
else:
needed = 1 # 逐步停止
to_stop = self.scheduler.select_devices_to_stop(needed)
for d in to_stop:
decisions.append({
'action': 'STOP',
'device_id': d.device_id,
'reason': f'除湿停止: 湿度{humidity:.1f}%RH, 梯度{self.zone.humidity_gradient:.1f}%RH',
'priority': 'NORMAL'
})
# 执行加湿启停决策
if humidify_start_cond and len(running) < len(self.devices):
deviation = target - humidity
if deviation > 6:
needed = min(3, len(self.devices) - len(running))
elif deviation > 4:
needed = min(2, len(self.devices) - len(running))
else:
needed = 1
to_start = self.scheduler.select_devices_to_start(needed, now)
for d in to_start:
decisions.append({
'action': 'START',
'device_id': d.device_id,
'mode': DeviceMode.HUMIDIFY,
'reason': f'加湿启动: 湿度{humidity:.1f}%RH, 偏差-{deviation:.1f}%RH, 需{needed}台',
'priority': 'NORMAL'
})
elif humidify_stop_cond and len(running) > 0:
if humidity > target + self.zone.stop_offset:
needed = len(running)
else:
needed = 1
to_stop = self.scheduler.select_devices_to_stop(needed)
for d in to_stop:
decisions.append({
'action': 'STOP',
'device_id': d.device_id,
'reason': f'加湿停止: 湿度{humidity:.1f}%RH',
'priority': 'NORMAL'
})
# ===== Layer 4:防振荡自适应 =====
# 统计过去 1 小时启停次数
if now - self.zone.last_hour_reset > 3600:
self.zone.oscillation_count_1h = 0
self.zone.last_hour_reset = now
if decisions:
self.zone.oscillation_count_1h += len([d for d in decisions if d['action'] in ('START', 'STOP')])
if self.zone.oscillation_count_1h > 8: # 1h 内 > 8 次启停
self.zone.adaptive_deadband = min(self.zone.adaptive_deadband + 0.5, 6.0)
decisions.append({
'action': 'ADAPT',
'device_id': 'ZONE',
'reason': f'防振荡: 1h内{self.zone.oscillation_count_1h}次启停, 回差扩大至{self.zone.adaptive_deadband:.1f}%RH',
'priority': 'INFO'
})
elif self.zone.oscillation_count_1h == 0 and self.zone.adaptive_deadband > 3.0:
self.zone.adaptive_deadband = max(self.zone.adaptive_deadband - 0.3, 3.0)
decisions.append({
'action': 'ADAPT',
'device_id': 'ZONE',
'reason': f'稳态恢复: 回差缩小至{self.zone.adaptive_deadband:.1f}%RH',
'priority': 'INFO'
})
return decisions
# ========== 测试场景 ==========
if __name__ == '__main__':
print("=== 档案库房设备启停策略测试 ===\n")
# 初始化分区和设备
zone = ZoneState(zone_id="A01", target_humidity=50.0)
devices = [
HumidifierDevice(device_id="AHU-01", zone_id="A01", modbus_addr=1),
HumidifierDevice(device_id="AHU-02", zone_id="A01", modbus_addr=2),
HumidifierDevice(device_id="AHU-03", zone_id="A01", modbus_addr=3),
HumidifierDevice(device_id="AHU-04", zone_id="A01", modbus_addr=4),
]
engine = StartStopDecisionEngine(zone, devices)
# 模拟场景序列
test_scenarios = [
("初始状态", {'weighted_humidity': 50.5, 'ema_humidity': 50.3, 'humidity_gradient': 2.1, 'sensor_count': 9}),
("湿度缓慢上升", {'weighted_humidity': 51.8, 'ema_humidity': 51.2, 'humidity_gradient': 2.5, 'sensor_count': 9}),
("湿度持续偏高(3min后)", {'weighted_humidity': 53.2, 'ema_humidity': 52.5, 'humidity_gradient': 3.0, 'sensor_count': 9}),
("湿度持续偏高(6min后)", {'weighted_humidity': 54.8, 'ema_humidity': 53.8, 'humidity_gradient': 3.5, 'sensor_count': 9}),
("除湿运行中", {'weighted_humidity': 52.0, 'ema_humidity': 51.5, 'humidity_gradient': 2.8, 'sensor_count': 9}),
("湿度恢复正常", {'weighted_humidity': 49.5, 'ema_humidity': 49.8, 'humidity_gradient': 2.0, 'sensor_count': 9}),
("安全硬限触发", {'weighted_humidity': 66.5, 'ema_humidity': 65.8, 'humidity_gradient': 5.0, 'sensor_count': 9}),
]
for name, agg in test_scenarios:
print(f"--- 场景: {name} ---")
print(f" 加权湿度: {agg['weighted_humidity']}%RH | EMA: {agg['ema_humidity']}%RH | 梯度: {agg['humidity_gradient']}%RH")
engine.update_zone_state(agg)
decisions = engine.evaluate(time.time())
if decisions:
for d in decisions:
print(f" → [{d['priority']}] {d['action']}: {d.get('device_id','')} | {d['reason']}")
else:
print(" → 无动作(维持当前状态)")
print()启动偏移(start_offset)整定方法:
公式:start_offset = max(设备响应延迟 × 自然变化率, 最小死区)
设备响应延迟:
恒湿机从启动到出风参数稳定:3~5 min
库房空气混合时间:5~15 min
总延迟:8~20 min
自然变化率(无设备干预时):
库房密闭良好:0.1~0.3 %RH/min
库房有人员进出:0.5~1.0 %RH/min
计算示例:
密闭库房:20min × 0.2 %RH/min = 4.0 %RH
→ start_offset 取 2.5~3.5%RH(预留安全余量)
频繁进出库房:8min × 0.8 %RH/min = 6.4 %RH
→ start_offset 取 4.0~5.0%RH
停止偏移(stop_offset)整定:
原则:停止后设备余热/余冷继续作用,湿度会继续变化 0.5~1.5%RH
→ stop_offset 取 1.0~1.5%RH
自适应回差(adaptive_deadband):
初始值:3.0%RH
上限:6.0%RH(避免波动过大)
下限:2.0%RH(避免频繁启停)
调整步长:±0.3~0.5%RHhold_time 的作用:防止瞬时波动触发误启停
推荐值:
稳定库房(无人员频繁进出):300s(5min)
活跃库房(每天多次进出):600s(10min)
逻辑:
if 湿度 > 目标 + start_offset:
if request_since == 0:
request_since = now
elif now - request_since > hold_time:
→ 确认启动请求有效
else:
request_since = 0 → 重置
效果:湿度短暂超过阈值(< hold_time)不会触发启动
只有持续偏高超过 hold_time 才确认为真实需求设备数量选择矩阵(以 4 台恒湿机/分区为例):
湿度偏差 推荐启动台数 运行模式
─────────────────────────────────────────
±2%RH 以内 0(待机) 维持
±2~4%RH 1 台 单台全功率
±4~6%RH 2 台 双机分摊
±6~8%RH 3 台 三机协同
>8%RH 4 台 全开(含备用)
轮值策略:
├── 每次启动选择累计运行时间最短的设备
├── 每周自动轮换主备角色
└── 偏差 < 3%RH 时只允许 1 台运行(节能优先)
错峰启动时间:
├── 第 1 台:立即启动
├── 第 2 台:延迟 3 min
├── 第 3 台:延迟 6 min
└── 第 4 台:延迟 9 min
原因:避免多台压缩机同时启动造成电网冲击
同时让先启动的设备先发挥作用,后启动的作为补充压缩机保护逻辑(必须遵守):
最小停机时间:5 min(300s)
├── 压缩机停机后,5 min 内不允许重新启动
├── 原因:制冷剂需要时间回流到压缩机,否则液击损坏
└── 实现:cooldown_until = stop_time + 300s
最大连续运行时间:90 min(5400s)
├── 超过 90 min 强制停机 15 min
├── 原因:防止压缩机过热、结霜
└── 停机后冷却锁定 5 min,然后可重新启动
紧急覆盖:
├── 安全硬限(>65%RH 或 <35%RH)可覆盖冷却锁定
└── 消防信号可立即关闭所有设备(不等待冷却)测试项目 | 方法 | 标准 |
|---|---|---|
启停精度 | 设定目标 50%RH,观察实际波动范围 | ±3%RH 以内 |
启停频率 | 稳定运行 24h,统计启停次数 | ≤ 8 次/天/台 |
响应时间 | 人为制造偏差 +5%RH,计时到设备启动 | < 10min |
安全硬限 | 模拟湿度 66%RH | < 30s 启动强制除湿 |
冷却锁定 | 手动停机后立即尝试启动 | 拒绝启动,提示冷却中 |
自适应回差 | 人为制造振荡(反复开关门) | 回差自动扩大 |
多设备调度 | 制造大偏差 >6%RH | 启动 ≥3 台,错峰间隔 ≥3min |
梯度感知 | 局部加湿(单点 >60%RH) | 定向调控,不全区启停 |
档案库房温湿度稳态维持,核心不是"设备够不够强",而是"什么时候开、开几台、开多久、什么时候关"。 多点位传感提供了空间维度的全景视野,启停决策引擎把"单点阈值开关"升级为"分区态势感知+自适应调度",冷却锁定和轮值机制保护了设备寿命,自适应回差解决了振荡顽疾。 最终效果:设备启停次数减少 80%,温湿度波动收窄 60%,设备寿命延长 3 倍以上——少折腾设备,多保护档案。
关键词:档案库房、温湿度稳态维持、多点位传感、设备启停策略、分区聚合、加权均值、EMA平滑、安全硬限、自适应回差、冷却锁定、轮值调度、错峰启动、防振荡、DA/T 47
标签:#档案库房 #温湿度稳态 #多点位传感 #设备启停 #分区聚合 #自适应回差 #冷却锁定 #轮值调度 #错峰启动 #防振荡 #DA/T47 #恒湿设备 #边缘控制 #SCADA档案库房温湿度稳态维持:多点位传感数据驱动设备启停策略
档案库房环境稳态维持的本质矛盾是:设备有惯性,空间有梯度,控制有延迟。恒湿机从启动压缩机到出风温度稳定需要 3~5 分钟,从出风口到密集架内部传感器感知到变化需要 5~15 分钟,而库房空间内不同位置的温湿度梯度可能达到 3~5℃/8~10%RH。如果简单地"到阈值就开、回阈值就关",结果必然是设备频繁启停、温湿度在阈值附近反复振荡、设备寿命急剧缩短。
某市级档案馆(库房面积 3200㎡,4 个分区,每区 8 台恒湿一体机)改造前采用"单点阈值控制"——每个分区只装 1 台温湿度传感器,设 50%RH 回差 ±5% 启停。运行数据记录显示:恒湿机日均启停 47 次,压缩机平均连续运行仅 8 分钟即被关闭,库房湿度在 44%~58%RH 之间大幅波动,部分密集架内部湿度极值达 39%RH 和 63%RH,远超 DA/T 47 要求的 ±5%RH 波动范围。
改造后采用多点位传感数据驱动的分区设备启停策略,恒湿机日均启停降至 6 次,连续运行时间延长到 45 分钟以上,库房湿度波动收窄到 47%~53%RH,设备能耗下降 28%,压缩机故障率从每年 3.2 次/台降至 0.4 次/台。
档案库房感知密度推荐标准:
┌─────────────────────────────────────────────────────────────────────────┐
│ 库房类型 │ 单点覆盖面积 │ 推荐密度 │ 备注 │
├─────────────────────┼──────────────┼─────────────┼─────────────────────┤
│ 普通纸质档案库房 │ 80~120㎡ │ 每 100㎡ 1台│ 均匀布置,避开死角 │
│ 特藏库/古籍库 │ 40~60㎡ │ 每 50㎡ 1台 │ 密集架内部必装 │
│ 胶片/磁介质库房 │ 30~50㎡ │ 每 40㎡ 1台 │ 温湿度要求更严 │
│ 展厅/阅览区 │ 60~80㎡ │ 每 70㎡ 1台 │ 人员流动影响大 │
│ 缓冲间/过渡区 │ 20~30㎡ │ 每区 1~2台 │ 门开闭影响监测 │
└─────────────────────────────────────────────────────────────────────────┘
部署位置矩阵(以 100㎡ 标准分区为例):
分区平面图(俯视):
┌──────────────────────────────────────────────────────────────────┐
│ 入口缓冲区 │
│ ┌─────────┐ │
│ │ S-01 │ ← 门边(受开门影响最大,权重 1.3) │
│ └─────────┘ │
│ ─────────── 防火门 ─────────── │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ S-02 │ │ S-03 │ │ S-04 │ │ S-05 │ │
│ │ 门口侧 │ │ 中部 │ │ 深处 │ │ 密集架内│ │
│ │ 权重1.2 │ │ 权重1.0 │ │ 权重1.1 │ │ 权重1.5 │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ S-06 │ │ S-07 │ │ S-08 │ │ S-09 │ │
│ │ 空调下 │ │ 中部 │ │ 回风侧 │ │ 密集架内│ │
│ │ 权重0.9 │ │ 权重1.0 │ │ 权重1.2 │ │ 权重1.5 │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
└──────────────────────────────────────────────────────────────────┘
高度方向(垂直梯度):
├── 地面 0.3m:S-L1(底层,冷空气沉积)
├── 档案中层 1.5m:S-L2(主要监测面)
└── 顶部 2.8m:S-L3(热空气聚集,空调回风参考)"""
多点位温湿度空间聚合
输入:N 台传感器实时数据
输出:分区综合状态(均值、极值、梯度、一致性)
"""
import statistics
from dataclasses import dataclass
from typing import List, Dict, Tuple
@dataclass
class SensorPoint:
id: str
zone: str
position: str # 门口/中部/深处/密集架内/空调下/回风侧
temperature: float # ℃
humidity: float # %RH
weight: float # 空间权重
valid: bool # 数据有效性
timestamp: float # 时间戳
class ZoneAggregator:
"""分区数据聚合器"""
def __init__(self, zone_id: str):
self.zone_id = zone_id
self.sensors: Dict[str, SensorPoint] = {}
self._ema_temp = None # 指数移动平均(温度)
self._ema_humidity = None # 指数移动平均(湿度)
self._ema_alpha = 0.3 # EMA 平滑系数
def update(self, sensor: SensorPoint):
self.sensors[sensor.id] = sensor
def get_aggregated_state(self) -> dict:
"""计算分区聚合状态"""
valid_sensors = [s for s in self.sensors.values() if s.valid]
if len(valid_sensors) == 0:
return {'error': 'no valid sensors'}
# 1. 加权均值(核心控制变量)
total_w = sum(s.weight for s in valid_sensors)
weighted_temp = sum(s.temperature * s.weight for s in valid_sensors) / total_w
weighted_humidity = sum(s.humidity * s.weight for s in valid_sensors) / total_w
# 2. 极值(边界条件判断)
max_temp = max(s.temperature for s in valid_sensors)
min_temp = min(s.temperature for s in valid_sensors)
max_humidity = max(s.humidity for s in valid_sensors)
min_humidity = min(s.humidity for s in valid_sensors)
# 3. 空间梯度(均匀性评估)
temp_gradient = max_temp - min_temp
humidity_gradient = max_humidity - min_humidity
# 4. 一致性检查(是否有传感器异常)
humidity_values = [s.humidity for s in valid_sensors]
humidity_cv = statistics.stdev(humidity_values) / statistics.mean(humidity_values) if len(humidity_values) > 1 else 0
# 5. EMA 平滑(用于趋势判断)
if self._ema_temp is None:
self._ema_temp = weighted_temp
self._ema_humidity = weighted_humidity
else:
self._ema_temp = self._ema_alpha * weighted_temp + (1 - self._ema_alpha) * self._ema_temp
self._ema_humidity = self._ema_alpha * weighted_humidity + (1 - self._ema_alpha) * self._ema_humidity
# 6. 变化率(℃/min 或 %RH/min)
# 实际项目中需要记录历史序列计算
return {
'zone_id': self.zone_id,
'sensor_count': len(valid_sensors),
'weighted_temperature': round(weighted_temp, 2),
'weighted_humidity': round(weighted_humidity, 2),
'ema_temperature': round(self._ema_temp, 2),
'ema_humidity': round(self._ema_humidity, 2),
'max_temperature': max_temp,
'min_temperature': min_temp,
'max_humidity': max_humidity,
'min_humidity': min_humidity,
'temp_gradient': round(temp_gradient, 2),
'humidity_gradient': round(humidity_gradient, 2),
'humidity_cv': round(humidity_cv, 4),
'consistency': 'OK' if humidity_cv < 0.08 else 'WARNING',
}传统方案(单点阈值 + 回差):
设定值:50%RH,回差 ±3%RH
启动除湿:湿度 ≥ 53%RH
停止除湿:湿度 ≤ 50%RH
启动加湿:湿度 ≤ 47%RH
停止加湿:湿度 ≥ 50%RH
问题:
① 传感器在空调出风口附近 → 读数偏低 → 设备频繁加湿 → 深处过湿
② 单点失效(漂移/离线)→ 整个分区失控
③ 回差太小(±3%)→ 设备 5 分钟启停一次
④ 回差太大(±8%)→ 库房湿度波动 ±8%RH,超标
⑤ 不考虑设备惯性 → 压缩机启动 3 分钟后才出冷风,此时湿度已自然下降
系统误判"设备无效"或"过度制冷"启停决策逻辑(分层判断):
Layer 1:安全硬限(最高优先级,不可覆盖)
├── 温度 > 28℃ → 强制启动所有空调/除湿
├── 温度 < 14℃ → 强制启动加热/关闭制冷
├── 湿度 > 65%RH → 强制启动除湿(防霉)
├── 湿度 < 35%RH → 强制启动加湿(防纸张脆化)
└── 任何安全硬限触发 → 绕过所有延时和调度逻辑
Layer 2:稳态启停判断(核心逻辑)
├── 输入:分区加权均值 + EMA 平滑值 + 变化率 + 梯度
├── 判断条件(以除湿为例):
│
│ 启动条件(ALL 满足):
│ ① 加权均值 > 目标值 + 启动偏移(默认 +2.5%RH)
│ ② EMA 湿度 > 目标值 + 1.5%RH(排除瞬时波动)
│ ③ 湿度变化率 > 0(在上升,不是偶然峰值)
│ ④ 持续时间 > 滞后时间 T_hold(默认 5min)
│ ⑤ 当前无设备处于"冷却锁定"状态
│
│ 停止条件(ANY 满足):
│ ① 加权均值 < 目标值 - 停止偏移(默认 -1.0%RH)
│ ② 最低点湿度 < 目标值 - 3%RH(防止局部过干)
│ ③ 设备连续运行 > 最大运行时 T_max(默认 90min,防冻结)
│ ④ 湿度变化率 < -0.5%RH/min(快速下降,可能过调)
│ ⑤ EMA 湿度 < 目标值 - 0.5%RH
│
└── 加湿逻辑对称
Layer 3:设备调度(多台设备间的启停分配)
├── 轮值调度:4 台设备按累计运行时间排序,优先启动运行时间最短的
├── 负载分配:湿度偏差越大,启动越多设备
│ 偏差 2~4%RH → 启动 1 台
│ 偏差 4~6%RH → 启动 2 台
│ 偏差 > 6%RH → 启动 3 台
├── 错峰启动:多台设备启动间隔 ≥ 3min(避免启动电流冲击)
└── 冷却锁定:压缩机停机后锁定 5min 才能再次启动(保护压缩机)
Layer 4:防振荡与自适应
├── 启停死区自适应:根据历史振荡幅度动态调整回差
│ 如果过去 1h 内启停次数 > 4 次 → 自动扩大回差 0.5%RH
│ 如果连续 2h 无启停 → 可缩小回差 0.3%RH(提高精度)
├── 变化率预测:基于最近 15min 趋势外推 10min 后湿度
│ 如果预测值将进入安全范围 → 延迟启动
│ 如果预测值将超出安全硬限 → 提前启动
└── 梯度感知:某分区梯度 > 6%RH → 启动定向调控而非全局启停#!/usr/bin/env python3
"""
档案库房多点位传感驱动的设备启停策略引擎
"""
import asyncio
import time
from enum import Enum, auto
from dataclasses import dataclass, field
from typing import List, Dict, Optional
class DeviceMode(Enum):
OFF = 0
HUMIDIFY = 1
DEHUMIDIFY = 2
COOL = 3
HEAT = 4
class DeviceState(Enum):
STOPPED = 0
STARTING = 1 # 启动中(压缩机预热/延时)
RUNNING = 2
STOPPING = 3 # 停机中(冷却锁定)
FAULT = 4
LOCKED = 5 # 冷却锁定中
@dataclass
class HumidifierDevice:
device_id: str
zone_id: str
modbus_addr: int
state: DeviceState = DeviceState.STOPPED
mode: DeviceMode = DeviceMode.OFF
cumulative_runtime: float = 0.0 # 累计运行时间(小时)
last_start_time: float = 0.0
last_stop_time: float = 0.0
cooldown_until: float = 0.0 # 冷却锁定到期时间
startup_delay: float = 30.0 # 启动延时(秒)
cooldown_period: float = 300.0 # 冷却锁定时间(秒)
max_runtime: float = 5400.0 # 最大连续运行时间(秒,90min)
current_runtime: float = 0.0 # 本次连续运行时间
@dataclass
class ZoneState:
zone_id: str
target_humidity: float = 50.0
target_temperature: float = 20.0
start_offset: float = 2.5 # 启动偏移 %RH
stop_offset: float = 1.0 # 停止偏移 %RH
hold_time: float = 300.0 # 持续确认时间(秒,5min)
ema_alpha: float = 0.3
# 实时状态
weighted_humidity: float = 50.0
ema_humidity: float = 50.0
humidity_rate: float = 0.0 # %RH/min
humidity_gradient: float = 0.0
sensor_count: int = 0
# 启停决策状态
dehumidify_request: bool = False
humidify_request: bool = False
dehumidify_request_since: float = 0.0
humidify_request_since: float = 0.0
last_state_change: float = 0.0
# 自适应参数
adaptive_deadband: float = 3.0 # 自适应回差
oscillation_count_1h: int = 0
last_hour_reset: float = 0.0
class DeviceSchedulingEngine:
"""设备调度引擎"""
def __init__(self, zone: ZoneState, devices: List[HumidifierDevice]):
self.zone = zone
self.devices = devices
self.running = False
def _sort_by_runtime(self) -> List[HumidifierDevice]:
"""按累计运行时间升序排列"""
return sorted(self.devices, key=lambda d: d.cumulative_runtime)
def _available_devices(self, now: float) -> List[HumidifierDevice]:
"""获取可启动的设备列表"""
available = []
for d in self.devices:
if d.state == DeviceState.STOPPED and now >= d.cooldown_until:
available.append(d)
return available
def _running_devices(self) -> List[HumidifierDevice]:
"""获取正在运行的设备"""
return [d for d in self.devices if d.state == DeviceState.RUNNING]
def select_devices_to_start(self, required_count: int, now: float) -> List[HumidifierDevice]:
"""选择要启动的设备"""
available = self._available_devices(now)
if not available:
return []
# 按累计运行时间排序,优先启动"最闲"的设备
available.sort(key=lambda d: d.cumulative_runtime)
# 错峰启动:检查最近一次启动时间
selected = []
for d in available:
if len(selected) >= required_count:
break
# 错峰:如果最近有设备启动且不到 3 分钟,延迟选择
if selected and (now - selected[-1].last_start_time) < 180:
continue
selected.append(d)
return selected[:required_count]
def select_devices_to_stop(self, required_count: int) -> List[HumidifierDevice]:
"""选择要停止的设备(优先停运行时间最长的)"""
running = self._running_devices()
if not running:
return []
# 按本次运行时间降序排列
running.sort(key=lambda d: d.current_runtime, reverse=True)
return running[:required_count]
class StartStopDecisionEngine:
"""启停决策引擎"""
def __init__(self, zone: ZoneState, devices: List[HumidifierDevice]):
self.zone = zone
self.scheduler = DeviceSchedulingEngine(zone, devices)
self.devices = devices
def update_zone_state(self, agg_state: dict):
"""更新分区状态"""
self.zone.weighted_humidity = agg_state['weighted_humidity']
self.zone.ema_humidity = agg_state['ema_humidity']
self.zone.humidity_gradient = agg_state['humidity_gradient']
self.zone.sensor_count = agg_state['sensor_count']
def evaluate(self, now: float) -> dict:
"""
执行启停决策评估
返回决策指令列表
"""
decisions = []
humidity = self.zone.weighted_humidity
ema = self.zone.ema_humidity
target = self.zone.target_humidity
deadband = self.zone.adaptive_deadband
# ===== Layer 1:安全硬限 =====
# 湿度 > 65%RH → 强制除湿
if humidity > 65.0:
running = self.scheduler._running_devices()
if len(running) < len(self.devices):
needed = min(2, len(self.devices) - len(running))
to_start = self.scheduler.select_devices_to_start(needed, now)
for d in to_start:
decisions.append({
'action': 'START',
'device_id': d.device_id,
'mode': DeviceMode.DEHUMIDIFY,
'reason': f'SAFETY: 湿度 {humidity:.1f}%RH > 65%RH 硬限',
'priority': 'CRITICAL'
})
return decisions
# 湿度 < 35%RH → 强制加湿
if humidity < 35.0:
running = self.scheduler._running_devices()
if len(running) < len(self.devices):
needed = min(2, len(self.devices) - len(running))
to_start = self.scheduler.select_devices_to_start(needed, now)
for d in to_start:
decisions.append({
'action': 'START',
'device_id': d.device_id,
'mode': DeviceMode.HUMIDIFY,
'reason': f'SAFETY: 湿度 {humidity:.1f}%RH < 35%RH 硬限',
'priority': 'CRITICAL'
})
return decisions
# ===== Layer 2:稳态启停判断 =====
# --- 除湿判断 ---
dehumidify_start_cond = (
humidity > target + self.zone.start_offset + deadband / 2
and ema > target + 1.5
and self.zone.humidity_rate >= 0
and now - self.zone.dehumidify_request_since > self.zone.hold_time
)
dehumidify_stop_cond = (
humidity < target - self.zone.stop_offset
or self.zone.humidity_gradient > 8.0 # 梯度过大,需切换策略
)
# --- 加湿判断 ---
humidify_start_cond = (
humidity < target - self.zone.start_offset - deadband / 2
and ema < target - 1.5
and self.zone.humidity_rate <= 0
and now - self.zone.humidify_request_since > self.zone.hold_time
)
humidify_stop_cond = (
humidity > target + self.zone.stop_offset
or self.zone.humidity_gradient > 8.0
)
# 更新请求计时
if humidity > target + self.zone.start_offset:
if self.zone.dehumidify_request_since == 0:
self.zone.dehumidify_request_since = now
else:
self.zone.dehumidify_request_since = 0
if humidity < target - self.zone.start_offset:
if self.zone.humidify_request_since == 0:
self.zone.humidify_request_since = now
else:
self.zone.humidify_request_since = 0
# 执行除湿启停决策
running = self.scheduler._running_devices()
if dehumidify_start_cond and len(running) < len(self.devices):
# 确定需要启动的设备数量(基于偏差幅度)
deviation = humidity - target
if deviation > 6:
needed = min(3, len(self.devices) - len(running))
elif deviation > 4:
needed = min(2, len(self.devices) - len(running))
else:
needed = 1
to_start = self.scheduler.select_devices_to_start(needed, now)
for d in to_start:
decisions.append({
'action': 'START',
'device_id': d.device_id,
'mode': DeviceMode.DEHUMIDIFY,
'reason': f'除湿启动: 湿度{humidity:.1f}%RH, 偏差+{deviation:.1f}%RH, 需{needed}台',
'priority': 'NORMAL'
})
elif dehumidify_stop_cond and len(running) > 0:
# 确定需要停止的数量
if humidity < target - self.zone.stop_offset:
needed = len(running) # 全部停止
else:
needed = 1 # 逐步停止
to_stop = self.scheduler.select_devices_to_stop(needed)
for d in to_stop:
decisions.append({
'action': 'STOP',
'device_id': d.device_id,
'reason': f'除湿停止: 湿度{humidity:.1f}%RH, 梯度{self.zone.humidity_gradient:.1f}%RH',
'priority': 'NORMAL'
})
# 执行加湿启停决策
if humidify_start_cond and len(running) < len(self.devices):
deviation = target - humidity
if deviation > 6:
needed = min(3, len(self.devices) - len(running))
elif deviation > 4:
needed = min(2, len(self.devices) - len(running))
else:
needed = 1
to_start = self.scheduler.select_devices_to_start(needed, now)
for d in to_start:
decisions.append({
'action': 'START',
'device_id': d.device_id,
'mode': DeviceMode.HUMIDIFY,
'reason': f'加湿启动: 湿度{humidity:.1f}%RH, 偏差-{deviation:.1f}%RH, 需{needed}台',
'priority': 'NORMAL'
})
elif humidify_stop_cond and len(running) > 0:
if humidity > target + self.zone.stop_offset:
needed = len(running)
else:
needed = 1
to_stop = self.scheduler.select_devices_to_stop(needed)
for d in to_stop:
decisions.append({
'action': 'STOP',
'device_id': d.device_id,
'reason': f'加湿停止: 湿度{humidity:.1f}%RH',
'priority': 'NORMAL'
})
# ===== Layer 4:防振荡自适应 =====
# 统计过去 1 小时启停次数
if now - self.zone.last_hour_reset > 3600:
self.zone.oscillation_count_1h = 0
self.zone.last_hour_reset = now
if decisions:
self.zone.oscillation_count_1h += len([d for d in decisions if d['action'] in ('START', 'STOP')])
if self.zone.oscillation_count_1h > 8: # 1h 内 > 8 次启停
self.zone.adaptive_deadband = min(self.zone.adaptive_deadband + 0.5, 6.0)
decisions.append({
'action': 'ADAPT',
'device_id': 'ZONE',
'reason': f'防振荡: 1h内{self.zone.oscillation_count_1h}次启停, 回差扩大至{self.zone.adaptive_deadband:.1f}%RH',
'priority': 'INFO'
})
elif self.zone.oscillation_count_1h == 0 and self.zone.adaptive_deadband > 3.0:
self.zone.adaptive_deadband = max(self.zone.adaptive_deadband - 0.3, 3.0)
decisions.append({
'action': 'ADAPT',
'device_id': 'ZONE',
'reason': f'稳态恢复: 回差缩小至{self.zone.adaptive_deadband:.1f}%RH',
'priority': 'INFO'
})
return decisions
# ========== 测试场景 ==========
if __name__ == '__main__':
print("=== 档案库房设备启停策略测试 ===\n")
# 初始化分区和设备
zone = ZoneState(zone_id="A01", target_humidity=50.0)
devices = [
HumidifierDevice(device_id="AHU-01", zone_id="A01", modbus_addr=1),
HumidifierDevice(device_id="AHU-02", zone_id="A01", modbus_addr=2),
HumidifierDevice(device_id="AHU-03", zone_id="A01", modbus_addr=3),
HumidifierDevice(device_id="AHU-04", zone_id="A01", modbus_addr=4),
]
engine = StartStopDecisionEngine(zone, devices)
# 模拟场景序列
test_scenarios = [
("初始状态", {'weighted_humidity': 50.5, 'ema_humidity': 50.3, 'humidity_gradient': 2.1, 'sensor_count': 9}),
("湿度缓慢上升", {'weighted_humidity': 51.8, 'ema_humidity': 51.2, 'humidity_gradient': 2.5, 'sensor_count': 9}),
("湿度持续偏高(3min后)", {'weighted_humidity': 53.2, 'ema_humidity': 52.5, 'humidity_gradient': 3.0, 'sensor_count': 9}),
("湿度持续偏高(6min后)", {'weighted_humidity': 54.8, 'ema_humidity': 53.8, 'humidity_gradient': 3.5, 'sensor_count': 9}),
("除湿运行中", {'weighted_humidity': 52.0, 'ema_humidity': 51.5, 'humidity_gradient': 2.8, 'sensor_count': 9}),
("湿度恢复正常", {'weighted_humidity': 49.5, 'ema_humidity': 49.8, 'humidity_gradient': 2.0, 'sensor_count': 9}),
("安全硬限触发", {'weighted_humidity': 66.5, 'ema_humidity': 65.8, 'humidity_gradient': 5.0, 'sensor_count': 9}),
]
for name, agg in test_scenarios:
print(f"--- 场景: {name} ---")
print(f" 加权湿度: {agg['weighted_humidity']}%RH | EMA: {agg['ema_humidity']}%RH | 梯度: {agg['humidity_gradient']}%RH")
engine.update_zone_state(agg)
decisions = engine.evaluate(time.time())
if decisions:
for d in decisions:
print(f" → [{d['priority']}] {d['action']}: {d.get('device_id','')} | {d['reason']}")
else:
print(" → 无动作(维持当前状态)")
print()启动偏移(start_offset)整定方法:
公式:start_offset = max(设备响应延迟 × 自然变化率, 最小死区)
设备响应延迟:
恒湿机从启动到出风参数稳定:3~5 min
库房空气混合时间:5~15 min
总延迟:8~20 min
自然变化率(无设备干预时):
库房密闭良好:0.1~0.3 %RH/min
库房有人员进出:0.5~1.0 %RH/min
计算示例:
密闭库房:20min × 0.2 %RH/min = 4.0 %RH
→ start_offset 取 2.5~3.5%RH(预留安全余量)
频繁进出库房:8min × 0.8 %RH/min = 6.4 %RH
→ start_offset 取 4.0~5.0%RH
停止偏移(stop_offset)整定:
原则:停止后设备余热/余冷继续作用,湿度会继续变化 0.5~1.5%RH
→ stop_offset 取 1.0~1.5%RH
自适应回差(adaptive_deadband):
初始值:3.0%RH
上限:6.0%RH(避免波动过大)
下限:2.0%RH(避免频繁启停)
调整步长:±0.3~0.5%RHhold_time 的作用:防止瞬时波动触发误启停
推荐值:
稳定库房(无人员频繁进出):300s(5min)
活跃库房(每天多次进出):600s(10min)
逻辑:
if 湿度 > 目标 + start_offset:
if request_since == 0:
request_since = now
elif now - request_since > hold_time:
→ 确认启动请求有效
else:
request_since = 0 → 重置
效果:湿度短暂超过阈值(< hold_time)不会触发启动
只有持续偏高超过 hold_time 才确认为真实需求设备数量选择矩阵(以 4 台恒湿机/分区为例):
湿度偏差 推荐启动台数 运行模式
─────────────────────────────────────────
±2%RH 以内 0(待机) 维持
±2~4%RH 1 台 单台全功率
±4~6%RH 2 台 双机分摊
±6~8%RH 3 台 三机协同
>8%RH 4 台 全开(含备用)
轮值策略:
├── 每次启动选择累计运行时间最短的设备
├── 每周自动轮换主备角色
└── 偏差 < 3%RH 时只允许 1 台运行(节能优先)
错峰启动时间:
├── 第 1 台:立即启动
├── 第 2 台:延迟 3 min
├── 第 3 台:延迟 6 min
└── 第 4 台:延迟 9 min
原因:避免多台压缩机同时启动造成电网冲击
同时让先启动的设备先发挥作用,后启动的作为补充压缩机保护逻辑(必须遵守):
最小停机时间:5 min(300s)
├── 压缩机停机后,5 min 内不允许重新启动
├── 原因:制冷剂需要时间回流到压缩机,否则液击损坏
└── 实现:cooldown_until = stop_time + 300s
最大连续运行时间:90 min(5400s)
├── 超过 90 min 强制停机 15 min
├── 原因:防止压缩机过热、结霜
└── 停机后冷却锁定 5 min,然后可重新启动
紧急覆盖:
├── 安全硬限(>65%RH 或 <35%RH)可覆盖冷却锁定
└── 消防信号可立即关闭所有设备(不等待冷却)测试项目 | 方法 | 标准 |
|---|---|---|
启停精度 | 设定目标 50%RH,观察实际波动范围 | ±3%RH 以内 |
启停频率 | 稳定运行 24h,统计启停次数 | ≤ 8 次/天/台 |
响应时间 | 人为制造偏差 +5%RH,计时到设备启动 | < 10min |
安全硬限 | 模拟湿度 66%RH | < 30s 启动强制除湿 |
冷却锁定 | 手动停机后立即尝试启动 | 拒绝启动,提示冷却中 |
自适应回差 | 人为制造振荡(反复开关门) | 回差自动扩大 |
多设备调度 | 制造大偏差 >6%RH | 启动 ≥3 台,错峰间隔 ≥3min |
梯度感知 | 局部加湿(单点 >60%RH) | 定向调控,不全区启停 |
档案库房温湿度稳态维持,核心不是"设备够不够强",而是"什么时候开、开几台、开多久、什么时候关"。 多点位传感提供了空间维度的全景视野,启停决策引擎把"单点阈值开关"升级为"分区态势感知+自适应调度",冷却锁定和轮值机制保护了设备寿命,自适应回差解决了振荡顽疾。 最终效果:设备启停次数减少 80%,温湿度波动收窄 60%,设备寿命延长 3 倍以上——少折腾设备,多保护档案。
关键词:档案库房、温湿度稳态维持、多点位传感、设备启停策略、分区聚合、加权均值、EMA平滑、安全硬限、自适应回差、冷却锁定、轮值调度、错峰启动、防振荡、DA/T 47
标签:#档案库房 #温湿度稳态 #多点位传感 #设备启停 #分区聚合 #自适应回差 #冷却锁定 #轮值调度 #错峰启动 #防振荡 #DA/T47 #恒湿设备 #边缘控制 #SCADA
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。