Skip to content

设备自动控制:阈值联动、定时启停与多设备资产

1. 场景说明

ThinkLink(TKL)平台把「感知 → 决策 → 执行」的闭环交给三块原生能力组合实现:触发模型(实时决策)、定时任务 + 设备执行(按时排程)、虚拟设备(资产)(跨设备聚合)。本文用「除湿机 + 湿度传感器」为例,讲清三种最常见的自动控制如何搭:

自动控制解决什么核心能力本文
阈值联动测量值越过上/下限就自动开/关执行设备触发模型(迟滞死区)§3
定时启停到点自动开机、到点自动关机定时任务 + 设备执行§4
多设备联动一台(或多台)传感器驱动另一台执行设备虚拟设备(资产)聚合§5

三者可以叠加:「工作时段内按湿度自动启停、时段外强制关机、每次切换发通知」就是 §3+§4+§5 的组合,见 §6。

动手前先回答一个问题(§2):感知和执行是不是同一台设备?答案决定要不要建「资产」——这是最容易选错、且选错后全盘不工作的一步。

2. 第一步:要不要建「资产」?

平台有两条底层硬约束(运维侧不可绕过,理解它们就懂了为什么有时必须建资产):

  1. 一个触发模型只在它所挂设备自己上报时运行,且只能看到这台设备的数据。 把触发器挂在执行设备上,它读不到另一台传感器的测量值。
  2. 跨设备的数据联动 / 关系(device_relation)只对「虚拟设备(资产)」生效。 把关系连到普通设备上不起作用。

由此得到决策门:

范式 A:建资产范式 B:不建资产
判据感知与执行是两台(或多台)独立设备感知 + 执行同体(一台设备既上报又能被控),或纯按时间排程
典型湿度传感器 KS52 驱动除湿机 PKO除湿机自身既测湿度又能开关;定时灯控
控制逻辑挂在一个**虚拟设备(资产)**上执行设备自身
怎么联动资产聚合传感器数据 → 资产触发器决策 → 按 EUI 下发给执行设备触发器挂执行设备自身,用自身 EUI 回控自己

选错的代价:把「同体」做成资产,是白白多一层聚合;把「异体」做成挂在执行设备上的触发器,永远读不到传感器数据、永远不触发。先答这一条,再往下走。

阈值逻辑(§3)和定时逻辑(§4)两种范式都用;区别只在「逻辑挂在资产上还是挂在设备自身上」。§5 专讲范式 A 的资产怎么建。

3. 阈值自动控制(迟滞死区)

最基础的自动控制:测量值越过上限就开机、跌破下限就关机。上限严格大于下限,中间这段「死区」用来防抖——避免测量值在单一阈值附近抖动时反复开关执行设备。

3.1 把阈值与开关写进配置

控制参数统一存在设备(或资产)的 server_attrs 里,由一条配置 RPC 写入,运维不手动改属性:

json
{
  "auto_enabled": true,   // 自控总开关;false = 全手动,触发器让位
  "hum_high": 70,         // 开机阈值
  "hum_low": 55,          // 关机阈值(high > low = 迟滞死区)
  "notify": ["除湿运维组"] // 通知组名
}

3.2 触发模型做决策

触发模型(见触发模型)在设备每次上报时运行,结构永远是「门禁 → 迟滞 → 仅在跳变时下发」:

javascript
function trigger_script(device, thingModelId) {
    const sa = device?.server_attrs || {};
    if (sa.auto_enabled !== true) return null;            // 门禁:全手动则让位
    const td = device?.telemetry_data?.[thingModelId];
    if (!td || td.humidity == null || td.power_switch == null) return null;

    const power = td.power_switch;                        // 0/1:用设备真实回报做"状态记忆"
    const { hum_high, hum_low } = sa;
    if (hum_high == null || hum_low == null) return null;

    let desired = null;
    if (td.humidity >= hum_high && power !== 1)      desired = 1;   // 越上限 → 开机
    else if (td.humidity <= hum_low && power === 1)  desired = 0;   // 跌下限 → 关机
    if (desired === null) return null;                   // 落在死区 / 无跳变 → 不动作

    return {
        delayms: 2000, abort_previous_timer: true, should_dispatch: true,
        actions: [
            { method: "pko_power_action", params: { _eui: device.eui, power_switch: desired === 1 } }
        ]
    };
}

要点:

  • 状态记忆来自设备的真实回报power_switch),不是触发器自己存——触发器无法持久化变量。重复下发同一开关指令是幂等的,所以执行设备确认新状态前的短暂窗口无害。
  • 只在跳变时下发:落在死区或状态未变就 return null,不刷指令。
  • 门禁放第一行auto_enabled=false 时整条让位,运维一键切回全手动。

4. 定时启停(定时任务 + 设备执行)

到点自动开关机用平台原生的定时任务 + 设备执行不要在触发模型里读系统时间——触发脚本运行在沙箱里、有约 1 秒超时、不注入 Date、时区不可控。

新版前端可在**设备的 RPC 配置面板里直接「添加到设备执行」**给某条 RPC 挂定时任务。详见定时任务设备执行。两张表配合:

  1. 定时任务(cronTask)——定义「什么时候触发」:

    json
    { "name": "除湿-08:00开机", "cron": "0 0 8 * * *", "type": "MULTI_DEVICE_RPC", "enabled": true }
    • cron6 段(含秒),按服务器本地时区(ManThink 服务器为 UTC+8,即按北京时间写)。
    • 给它一个人能读懂的名字除湿-08:00开机,不要 task1)。
  2. 设备执行(multiDeviceRpc)——把「哪条 RPC + 哪些设备 + 哪个定时任务」绑在一起:

    json
    {
      "name": "除湿-定时开机", "rpc_id": "<开关RPC的id>", "euis": ["<执行设备EUI>"],
      "cron_task": "<上面cronTask的id>", "enable": true, "type": "UNLIMITED",
      "device_interval_ms": 3000, "params": { "rpcParams": { "power_switch": true } }
    }

⚠️ 被定时调用的 RPC 必须自带全部参数。 排程执行时平台只下发 params.rpcParams(外加设备 _eui)——没有弹窗、没有属性回填。所以参数要完整写进 rpcParams;任何依赖「打开表单回填当前值」的 RPC 直接拿去排程会收到空参(细节见 RPC 模型)。缺参的 RPC 脚本应安全兜底 return null,绝不要默认成 0/空(可能误清寄存器)。

「开机」一条 cron(窗口开始时刻)、「关机」一条 cron(窗口结束时刻)即构成一个定时启停窗口。

5. 多设备联动:建虚拟设备(资产)

一台传感器要驱动另一台执行设备时(范式 A),必须用一个**虚拟设备(资产)**作控制中枢——因为 §2 的两条硬约束让逻辑无法直接挂在任何一台真实设备上。

传感器 ──关系──┐
              ├──► 虚拟资产 ──(聚合物模型)──► 区域湿度
执行设备 ──关系─┘            ──(资产触发器)──► 按 EUI 下发开关给执行设备
                            ──(资产触发器)──► 通知 RPC(站内 / 邮件)

5.1 搭建步骤(平台侧)

均通过平台 API / 界面完成(租户级对象,普通管理员即可, PUBLIC)。参考设备管理

  1. 建通知组:系统管理 → 通知组,绑定站内 + 邮件渠道。
  2. 建虚拟资产:设备类型 VIRTUAL,挂上「聚合物模型 + 资产触发器 + 配置 RPC + 通知 RPC」。资产 EUI 按约定取 a10 + 5 位模板/业务码 + 执行设备 EUI 后 8 位(a1 前缀标识它是资产)。
  3. 建关系:每台传感器 → 资产、执行设备 → 资产,notify_fields:["telemetry_data"]
  4. 初始化配置:执行一次配置 RPC,写入执行设备 EUI、上/下限、通知组、control_mode

5.2 聚合物模型:把传感器数据汇成资产数据

资产的物模型(见物模型)在任一绑定设备变化时运行,按 EUI 区分「执行设备」和「传感器」,把多台传感器的湿度汇成一个「区域湿度」:

javascript
    const sa = device?.server_attrs || {};
    const pkoEui = sa.pko_eui;
    const members = { ...(sa.members || {}) };
    let pkoPower = sa.pko_power;

    const srcTd = {};
    const all = msg?.telemetry_data || {};
    for (const k in all) { Object.assign(srcTd, all[k]); }

    if (msg.eui === pkoEui) {
        if (srcTd.power_switch !== undefined) pkoPower = srcTd.power_switch;  // 执行设备 → 状态记忆
    } else if (srcTd.humidity !== undefined) {
        members[msg.eui] = { h: srcTd.humidity };                            // 传感器 → 湿度
    }
    const hums = Object.values(members).map(s => s.h).filter(v => v != null);
    return {
        telemetry_data: { zone_humidity: hums.length ? Math.max(...hums) : null },
        server_attrs: { members, pko_power: pkoPower },
        shared_attrs: null
    };

5.3 资产触发器:决策并下发给执行设备

逻辑同 §3.2 的迟滞,区别是:读的是资产聚合出来的 zone_humidity,下发时用 params._eui 指到执行设备,并附一条通知:

javascript
function trigger_script(device, thingModelId) {
    const sa = device?.server_attrs || {};
    if (sa.control_mode !== "auto") return null;                  // 门禁:仅 auto 闭环
    const td = device?.telemetry_data?.[thingModelId];
    if (!td || td.zone_humidity == null) return null;

    const { hum_high, hum_low, pko_eui } = sa;
    const pkoPower = sa.pko_power;
    if (hum_high == null || hum_low == null || !pko_eui) return null;

    let desired = null;
    if (td.zone_humidity >= hum_high && pkoPower !== 1)      desired = 1;
    else if (td.zone_humidity <= hum_low && pkoPower === 1)  desired = 0;
    if (desired === null) return null;

    const onoff = desired ? "开机" : "关机";
    return {
        delayms: 2000, abort_previous_timer: true, should_dispatch: true,
        actions: [
            { method: "pko_power_action", params: { _eui: pko_eui, power_switch: desired === 1 } },
            { method: "dehum_notify", params: {
                _eui: device.eui,
                title: `[${device.name}] 除湿机已${onoff}`,
                desc: `区域湿度 ${td.zone_humidity}%RH,已自动${onoff}`,
                notice_groups: sa.notify ?? []
            }}
        ]
    };
}

配置 RPC 写的是资产自己的 server_attrs(模式 / 阈值 / 通知组 / 执行设备 EUI),它不能直接调另一台设备的 RPC——跨设备下发只有触发器的 _eui 路由能做。开关机事件用 type:"notify"(站内 + 邮件,不进告警中心、无需手动清除);满水、设备故障等才用告警 RPC

6. 三层叠加:定时窗口 + 窗口内迟滞 + 通知

实际项目常把三者叠起来:工作时段内按湿度自动启停、时段外强制关机、每次切换发通知。用一个 control_mode 状态机协调:

control_mode谁在驱动执行设备触发器
manual无人自动化,设备按自身设定值自调return null(让位)
auto触发器(按测量值闭环)接管
schedule定时任务 + 设备执行return null(让位)

叠加时的两条协调规则:

  1. 时间窗口用定时任务翻转一个标志位(如 in_window),触发器只读这个标志、不读系统时间。开窗 cron 设 in_window:1;闭窗 cron 设 in_window:0 并顺手关机(一条 cron 可挂多条设备执行)。
  2. 触发器只做:总开关门禁 → 窗口外强制关(安全网)→ 窗口内按 §3 迟滞启停。

7. 让配置「人看得懂」(强烈建议)

自动化逻辑要让运维/客户一眼看懂,不靠读代码:

  • 每条自控 RPC / 触发器写一句运维能懂的备注:说清「这条做什么 + 用前提(先跑哪条配置 RPC、配哪个通知组)」,不要写内部字段路径。
  • 配置 RPC 顺手写一条大白话策略摘要server_attrs.auto_policy,例如 "08:00–20:00 运行;湿度≥70%RH 开机、≤55%RH 关机;窗口外强制关机"。它是给人看的镜像,不驱动逻辑——真正生效的是 hum_high/hum_low 等字段;改控制律时记得同步这句摘要。
  • 定时任务 / 设备执行用人能读懂的名字除湿-08:00开机),运维就在设备执行列表里看名字理解排程。

8. 注意事项与常见错误

误区实际
异体设备把触发器挂在执行设备上触发器只见自身设备,读不到传感器数据 → 必须建虚拟资产(§2)
把传感器关系连到普通设备上跨设备关系仅对 VIRTUAL 资产生效,连普通设备无效
在触发器里读系统时间做排程触发脚本约 1s 超时、不注入 Date、时区不可控 → 用定时任务翻标志位(§4/§6)
在触发器里用变量存上次开关状态触发器无法持久化;用设备真实回报的状态字段做记忆
单一阈值开关测量值在阈值附近抖动会反复启停 → 用上限/下限迟滞死区(§3)
被排程的 RPC 依赖表单回填排程只下发 params.rpcParams,无表单、无回填 → 参数写全,缺参 return null 兜底
例行开关机用告警 RPC告警进告警中心需手动清除;事件类用 type:"notify"
配置 RPC 去调另一台设备的 RPCrpc_script 不能跨设备下发;跨设备动作放触发器的 _eui 路由

9. 相关文档