设备自动控制:阈值联动、定时启停与多设备资产
1. 场景说明
ThinkLink(TKL)平台把「感知 → 决策 → 执行」的闭环交给三块原生能力组合实现:触发模型(实时决策)、定时任务 + 设备执行(按时排程)、虚拟设备(资产)(跨设备聚合)。本文用「除湿机 + 湿度传感器」为例,讲清三种最常见的自动控制如何搭:
| 自动控制 | 解决什么 | 核心能力 | 本文 |
|---|---|---|---|
| 阈值联动 | 测量值越过上/下限就自动开/关执行设备 | 触发模型(迟滞死区) | §3 |
| 定时启停 | 到点自动开机、到点自动关机 | 定时任务 + 设备执行 | §4 |
| 多设备联动 | 一台(或多台)传感器驱动另一台执行设备 | 虚拟设备(资产)聚合 | §5 |
三者可以叠加:「工作时段内按湿度自动启停、时段外强制关机、每次切换发通知」就是 §3+§4+§5 的组合,见 §6。
动手前先回答一个问题(§2):感知和执行是不是同一台设备?答案决定要不要建「资产」——这是最容易选错、且选错后全盘不工作的一步。
2. 第一步:要不要建「资产」?
平台有两条底层硬约束(运维侧不可绕过,理解它们就懂了为什么有时必须建资产):
- 一个触发模型只在它所挂设备自己上报时运行,且只能看到这台设备的数据。 把触发器挂在执行设备上,它读不到另一台传感器的测量值。
- 跨设备的数据联动 / 关系(device_relation)只对「虚拟设备(资产)」生效。 把关系连到普通设备上不起作用。
由此得到决策门:
| 范式 A:建资产 | 范式 B:不建资产 | |
|---|---|---|
| 判据 | 感知与执行是两台(或多台)独立设备 | 感知 + 执行同体(一台设备既上报又能被控),或纯按时间排程 |
| 典型 | 湿度传感器 KS52 驱动除湿机 PKO | 除湿机自身既测湿度又能开关;定时灯控 |
| 控制逻辑挂在 | 一个**虚拟设备(资产)**上 | 执行设备自身上 |
| 怎么联动 | 资产聚合传感器数据 → 资产触发器决策 → 按 EUI 下发给执行设备 | 触发器挂执行设备自身,用自身 EUI 回控自己 |
选错的代价:把「同体」做成资产,是白白多一层聚合;把「异体」做成挂在执行设备上的触发器,永远读不到传感器数据、永远不触发。先答这一条,再往下走。
阈值逻辑(§3)和定时逻辑(§4)两种范式都用;区别只在「逻辑挂在资产上还是挂在设备自身上」。§5 专讲范式 A 的资产怎么建。
3. 阈值自动控制(迟滞死区)
最基础的自动控制:测量值越过上限就开机、跌破下限就关机。上限严格大于下限,中间这段「死区」用来防抖——避免测量值在单一阈值附近抖动时反复开关执行设备。
3.1 把阈值与开关写进配置
控制参数统一存在设备(或资产)的 server_attrs 里,由一条配置 RPC 写入,运维不手动改属性:
{
"auto_enabled": true, // 自控总开关;false = 全手动,触发器让位
"hum_high": 70, // 开机阈值
"hum_low": 55, // 关机阈值(high > low = 迟滞死区)
"notify": ["除湿运维组"] // 通知组名
}3.2 触发模型做决策
触发模型(见触发模型)在设备每次上报时运行,结构永远是「门禁 → 迟滞 → 仅在跳变时下发」:
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 挂定时任务。详见定时任务与设备执行。两张表配合:
定时任务(cronTask)——定义「什么时候触发」:
json{ "name": "除湿-08:00开机", "cron": "0 0 8 * * *", "type": "MULTI_DEVICE_RPC", "enabled": true }cron是 6 段(含秒),按服务器本地时区(ManThink 服务器为 UTC+8,即按北京时间写)。- 给它一个人能读懂的名字(
除湿-08:00开机,不要task1)。
设备执行(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)。参考设备管理:
- 建通知组:系统管理 → 通知组,绑定站内 + 邮件渠道。
- 建虚拟资产:设备类型
VIRTUAL,挂上「聚合物模型 + 资产触发器 + 配置 RPC + 通知 RPC」。资产 EUI 按约定取a10+ 5 位模板/业务码 + 执行设备 EUI 后 8 位(a1前缀标识它是资产)。 - 建关系:每台传感器 → 资产、执行设备 → 资产,
notify_fields:["telemetry_data"]。 - 初始化配置:执行一次配置 RPC,写入执行设备 EUI、上/下限、通知组、
control_mode。
5.2 聚合物模型:把传感器数据汇成资产数据
资产的物模型(见物模型)在任一绑定设备变化时运行,按 EUI 区分「执行设备」和「传感器」,把多台传感器的湿度汇成一个「区域湿度」:
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 指到执行设备,并附一条通知:
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(让位) |
叠加时的两条协调规则:
- 时间窗口用定时任务翻转一个标志位(如
in_window),触发器只读这个标志、不读系统时间。开窗 cron 设in_window:1;闭窗 cron 设in_window:0并顺手关机(一条 cron 可挂多条设备执行)。 - 触发器只做:总开关门禁 → 窗口外强制关(安全网)→ 窗口内按 §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 去调另一台设备的 RPC | rpc_script 不能跨设备下发;跨设备动作放触发器的 _eui 路由 |