首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >档案库房温湿度稳态维持:多点位传感数据驱动设备启停策略

档案库房温湿度稳态维持:多点位传感数据驱动设备启停策略

原创
作者头像
盛世宏博小可
发布于 2026-10-09 09:08:36
发布于 2026-10-09 09:08:36
210
举报

档案库房温湿度稳态维持:多点位传感数据驱动设备启停策略

档案库房环境稳态维持的本质矛盾是:设备有惯性,空间有梯度,控制有延迟。恒湿机从启动压缩机到出风温度稳定需要 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 次/台。


一、多点位感知与空间分区模型

1. 感知密度设计

代码语言:javascript
复制
档案库房感知密度推荐标准:

┌─────────────────────────────────────────────────────────────────────────┐
│  库房类型           │ 单点覆盖面积 │ 推荐密度    │ 备注                │
├─────────────────────┼──────────────┼─────────────┼─────────────────────┤
│ 普通纸质档案库房    │ 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(热空气聚集,空调回风参考)

2. 空间聚合算法

代码语言:javascript
复制
"""
多点位温湿度空间聚合
输入: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',
        }

二、设备启停策略核心算法

1. 传统阈值控制的缺陷

代码语言:javascript
复制
传统方案(单点阈值 + 回差):

  设定值:50%RH,回差 ±3%RH
  启动除湿:湿度 ≥ 53%RH
  停止除湿:湿度 ≤ 50%RH
  启动加湿:湿度 ≤ 47%RH
  停止加湿:湿度 ≥ 50%RH

问题:
  ① 传感器在空调出风口附近 → 读数偏低 → 设备频繁加湿 → 深处过湿
  ② 单点失效(漂移/离线)→ 整个分区失控
  ③ 回差太小(±3%)→ 设备 5 分钟启停一次
  ④ 回差太大(±8%)→ 库房湿度波动 ±8%RH,超标
  ⑤ 不考虑设备惯性 → 压缩机启动 3 分钟后才出冷风,此时湿度已自然下降
     系统误判"设备无效"或"过度制冷"

2. 多点位驱动的启停决策引擎

代码语言:javascript
复制
启停决策逻辑(分层判断):

  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 → 启动定向调控而非全局启停

3. 启停决策核心代码

代码语言:javascript
复制
#!/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()

三、关键参数整定

1. 启停偏移量整定

代码语言:javascript
复制
启动偏移(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%RH

2. 持续确认时间(hold_time)

代码语言:javascript
复制
hold_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 才确认为真实需求

四、多设备协同启停

1. 负载自适应设备数量

代码语言:javascript
复制
设备数量选择矩阵(以 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

    原因:避免多台压缩机同时启动造成电网冲击
         同时让先启动的设备先发挥作用,后启动的作为补充

2. 冷却锁定与最小停机时间

代码语言:javascript
复制
压缩机保护逻辑(必须遵守):

  最小停机时间: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 次/台。


一、多点位感知与空间分区模型

1. 感知密度设计

代码语言:javascript
复制
档案库房感知密度推荐标准:

┌─────────────────────────────────────────────────────────────────────────┐
│  库房类型           │ 单点覆盖面积 │ 推荐密度    │ 备注                │
├─────────────────────┼──────────────┼─────────────┼─────────────────────┤
│ 普通纸质档案库房    │ 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(热空气聚集,空调回风参考)

2. 空间聚合算法

代码语言:javascript
复制
"""
多点位温湿度空间聚合
输入: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',
        }

二、设备启停策略核心算法

1. 传统阈值控制的缺陷

代码语言:javascript
复制
传统方案(单点阈值 + 回差):

  设定值:50%RH,回差 ±3%RH
  启动除湿:湿度 ≥ 53%RH
  停止除湿:湿度 ≤ 50%RH
  启动加湿:湿度 ≤ 47%RH
  停止加湿:湿度 ≥ 50%RH

问题:
  ① 传感器在空调出风口附近 → 读数偏低 → 设备频繁加湿 → 深处过湿
  ② 单点失效(漂移/离线)→ 整个分区失控
  ③ 回差太小(±3%)→ 设备 5 分钟启停一次
  ④ 回差太大(±8%)→ 库房湿度波动 ±8%RH,超标
  ⑤ 不考虑设备惯性 → 压缩机启动 3 分钟后才出冷风,此时湿度已自然下降
     系统误判"设备无效"或"过度制冷"

2. 多点位驱动的启停决策引擎

代码语言:javascript
复制
启停决策逻辑(分层判断):

  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 → 启动定向调控而非全局启停

3. 启停决策核心代码

代码语言:javascript
复制
#!/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()

三、关键参数整定

1. 启停偏移量整定

代码语言:javascript
复制
启动偏移(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%RH

2. 持续确认时间(hold_time)

代码语言:javascript
复制
hold_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 才确认为真实需求

四、多设备协同启停

1. 负载自适应设备数量

代码语言:javascript
复制
设备数量选择矩阵(以 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

    原因:避免多台压缩机同时启动造成电网冲击
         同时让先启动的设备先发挥作用,后启动的作为补充

2. 冷却锁定与最小停机时间

代码语言:javascript
复制
压缩机保护逻辑(必须遵守):

  最小停机时间: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 删除。

目录
  • 档案库房温湿度稳态维持:多点位传感数据驱动设备启停策略
    • 一、多点位感知与空间分区模型
      • 1. 感知密度设计
      • 2. 空间聚合算法
    • 二、设备启停策略核心算法
      • 1. 传统阈值控制的缺陷
      • 2. 多点位驱动的启停决策引擎
      • 3. 启停决策核心代码
    • 三、关键参数整定
      • 1. 启停偏移量整定
      • 2. 持续确认时间(hold_time)
    • 四、多设备协同启停
      • 1. 负载自适应设备数量
      • 2. 冷却锁定与最小停机时间
    • 五、验收测试
    • 六、一句话总结
    • 一、多点位感知与空间分区模型
      • 1. 感知密度设计
      • 2. 空间聚合算法
    • 二、设备启停策略核心算法
      • 1. 传统阈值控制的缺陷
      • 2. 多点位驱动的启停决策引擎
      • 3. 启停决策核心代码
    • 三、关键参数整定
      • 1. 启停偏移量整定
      • 2. 持续确认时间(hold_time)
    • 四、多设备协同启停
      • 1. 负载自适应设备数量
      • 2. 冷却锁定与最小停机时间
    • 五、验收测试
    • 六、一句话总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档