跳到主要内容

JavaScript 脚本(适用于 MQTT)

本文件介绍了如何通过 JavaScript 脚本在 Aqara Studio 中自定义添加 MQTT 设备功能点及其解析方法。

脚本介绍

本脚本必须包含以下 4 个 JavaScript 方法:

提示

请勿更改方法的参数及其输入输出结构。

脚本全局变量

协议脚本可以使用 globalThis.studioScriptState 在同一个 Context 的多次函数调用之间共享少量临时状态。该变量默认初始化为空对象:

globalThis.studioScriptState = {
reportCount: 0,
lastValue: null
};

请保持该变量为普通对象。对象属性可保存字符串、有限数值、布尔值、null、普通对象和数组,并可使用这些类型相互嵌套。日期应先转换为字符串。不要保存 undefined、函数、SymbolBigIntNaNInfinity、循环引用、MapSet、Java/Host 对象或自定义类实例;这些值可能被忽略、转换失真或导致序列化失败。

状态大小按序列化后的 UTF-8 JSON 字节数计算,告警阈值为 2 MiB。Studio 在脚本调用后限频检查,首次越界时会同时向服务日志和协议配置页的 Console Content 写入一条 [warn];持续超限不会定期重复打印,也不会自动删除状态或终止本次调用。开发脚本时应确保该变量长期保持在 2 MiB 以内。

Context 被空闲回收、内存压力回收、脚本更新或执行超时后,globalThis.studioScriptState 会重新初始化。请勿用它保存必须持久化的设备状态、凭据或无限增长的历史消息。

parseDiscoveryResponse

该方法 parseDiscoveryResponse 主要用于向 Aqara Studio 声明需要接入的 MQTT 设备的基本配置信息。

您需要以对象数组的形式返回待接入设备的基本信息,详情请参考 设备对象结构

Aqara Studio 会调用此方法以发现所有可用设备,实现自动接入与管理。

设备对象结构

字段名称是否必填说明
deviceName可选设备名称。
deviceId必填设备 ID。
brokerEndpoint必填MQTT Broker 的地址。
brokerPort必填MQTT Broker 的端口。
connectionType必填连接类型。支持以下取值:
  • 0:匿名
  • 1:通过 SSL 匿名
  • 2:通过 SSL 用户登录
  • 3:用户登录无 SSL
username条件必填用户名,按 connectionType 决定是否需要。
password条件必填密码,按 connectionType 决定是否需要。

字段必填说明:

  • deviceIdbrokerEndpointbrokerPortconnectionType 必填。
  • brokerPort 默认端口为 1883connectionType 默认值为 3
  • username/passwordconnectionType 决定是否需要。
  • deviceName 可选,为空时使用空字符串。

示例

function parseDiscoveryResponse() {
return [
{
deviceName: "TestDevice",
deviceId: "device-001",
brokerEndpoint: "127.0.0.1",
brokerPort: 1883,
connectionType: 3,
username: "admin",
password: "public"
}
];
}

parseDiscoverPointsResponse

该方法负责向 Aqara Studio 声明“该设备具备哪些功能点”。Aqara Studio 调用该函数时会把 deviceId 作为第一个参数传入。

您需要在返回数组中填写一个或多个待发现功能点结构(如 pointId 等,详情请见 point 对象结构)。这样,Aqara Studio 才能在功能点发现流程中会调用该函数,解析返回值,从而识别设备具备的所有功能点,实现功能点无缝接入和控制。

point 对象结构

字段名称是否必填说明
pointId必填功能点 ID。请咨询设备厂商获取功能点的真实 ID,以便 Aqara Studio 发现该点。
pointName必填您自定义的功能点名称。
devType条件必填对应 Aqara 物模型规范的 deviceType。基于 Aqara Spec 定义功能点时填写,具体可用值请阅读 设备类型
functionCode条件必填对应 Aqara 物模型规范的 functionCode。基于 Aqara Spec 定义功能点时填写,具体可用值请阅读 功能代码
traitCode条件必填对应 Aqara 物模型规范的 traitCode。基于 Aqara Spec 定义功能点时填写,具体可用值请阅读 功能点代码
dataType条件必填通用功能点必填。功能点数据类型,支持以下取值:
  • bool:布尔型
  • numerical:数值型
  • enum:枚举型
  • text:文本型
enumRange条件必填枚举选项数组。仅当通用功能点的 dataTypeenum 时必填。数组不能为空;每项包含整数类型的 value 和字符串类型的 labelvaluelabel 均不可重复,且 label 不可为空。
trueText / falseText可选布尔型通用功能点的显示文案。仅当 dataTypebool 时生效;未配置时默认 trueTextfalseText。仅在需要自定义文案时配置,例如 启用 / 禁用。也可使用 activeText / onTextinactiveText / offText 作为别名。
min可选数值型通用功能点的最小值。仅当 dataTypenumerical 时生效;也可使用 minimum 作为别名。
max可选数值型通用功能点的最大值。仅当 dataTypenumerical 时生效;也可使用 maximum 作为别名。
step可选数值型通用功能点的步长,必须大于 0。仅当 dataTypenumerical 时生效;也可使用 resolution 作为别名。
precision可选数值型通用功能点的精度值,必须大于等于 0。仅当 dataTypenumerical 时生效。
decimals可选数值型通用功能点的小数位数,必须为大于等于 0 的整数。仅当 dataTypenumerical 时生效。
unit / units可选数值型通用功能点的单位。仅当 dataTypenumerical 时生效;建议填写系统已注册的单位名称,例如 kilowatt hourwattvoltamperecelsiuspercent。Studio 会展示对应符号,例如 kWhWVA°C%。完整映射请参见 脚本支持的单位名称与符号。系统也可匹配已注册的单位符号。
access条件必填通用功能点必填。读写能力标识,
  • 1:可读
  • 2:可写
  • 3:可读可写
subTopic条件必填仅适用于 MQTT 设备)可读或可上报功能点需配置。Aqara Studio 通过订阅该 Topic 获取功能点的最新数据。具体 Topic 请根据设备协议或厂商文档填写;需要同时接收多个上行主题时,可按 MQTT 规范填写 +# 通配符。
pubTopic条件必填仅适用于 MQTT 设备)可写功能点需配置。Aqara Studio 通过向此 Topic 发送消息数据控制功能点。具体 Topic 请根据设备协议或厂商文档填写。(对于仅可读的功能点,此字段留空)
备注

对于 numerical 点,Aqara Studio 在写值调用 buildWriteRequest 时会优先依据 decimals 格式化数值;未配置 decimals 时会依据 step / resolution 推导小数位。为了避免 0.3 被二进制浮点误差表现为 0.30000030249357224,建议需要小数写入的点显式配置 decimalsstepprecision 仅用于描述精度,不作为写值报文的小数位截断规则。

如果数值点会被能耗统计或其他依赖单位的功能使用,请配置 unitunits,例如累计电量使用 kilowatt hour(显示 kWh),当前功率使用 watt(显示 W),电压使用 volt(显示 V),电流使用 ampere(显示 A),温度使用 celsius(显示 °C)。单位名称请按 脚本支持的单位名称与符号 中的原文填写,大小写和空格保持一致。

提示

您可以根据实际情况选择以下任意方式构造功能点的数据结构:

  • 基于 Aqara Spec:需填写 devTypefunctionCodetraitCode 等字段,这可以将设备的功能点关联到 Aqara 物模型规范协议中的对应功能点。
  • 通用方式:需填写 dataTypeaccess 等基本字段。

示例

function parseDiscoverPointsResponse(deviceId) {
return [
// 基于 Aqara 物模型规范接入功能点
{
pointId: "1#冷机运行状态",
pointName: "1#冷机运行状态",
devType: "AirConditioner",
functionCode: "Output",
traitCode: "OnOff",
subTopic: "/WEI-iEdge/realdata/7a1dd258f32940d95a542c68",
pubTopic: "/WEI-iEdge/write/req/7a1dd258f32940d95a542c68"
},
// 基于通用方式接入功能点
{
pointId: "1#冷机关机/开机读",
pointName: "1#冷机关机/开机读",
dataType: "bool",
access: 1,
subTopic: "/WEI-iEdge/realdata/7a1dd258f32940d95a542c68",
pubTopic: "/WEI-iEdge/write/req/7a1dd258f32940d95a542c68"
},
{
pointId: "1#冷机运行模式",
pointName: "1#冷机运行模式",
dataType: "enum",
enumRange: [
{ value: 0, label: "关闭" },
{ value: 1, label: "制冷" }
],
access: 3,
subTopic: "/WEI-iEdge/realdata/7a1dd258f32940d95a542c68",
pubTopic: "/WEI-iEdge/write/req/7a1dd258f32940d95a542c68"
},
{
pointId: "1#冷机出水温度设定读",
pointName: "1#冷机出水温度设定读",
dataType: "numerical",
min: -20,
max: 50,
step: 0.1,
precision: 0.1,
decimals: 1,
unit: "celsius",
access: 2,
subTopic: "/WEI-iEdge/realdata/7a1dd258f32940d95a542c68",
pubTopic: "/WEI-iEdge/write/req/7a1dd258f32940d95a542c68"
},
{
pointId: "1#冷机制冷量百分比",
pointName: "1#冷机制冷量百分比",
dataType: "numerical",
min: 0,
max: 100,
step: 1,
precision: 1,
decimals: 0,
unit: "percent",
access: 2,
subTopic: "/WEI-iEdge/realdata/7a1dd258f32940d95a542c68",
pubTopic: "/WEI-iEdge/write/req/7a1dd258f32940d95a542c68"
}
];
}

parseReportRequest

该方法负责解析一条 MQTT 设备上报消息,并一次返回消息中包含的所有功能点值。同一条消息只调用一次该函数,不会按订阅点重复执行脚本。

参数类型说明
topicString收到消息的 MQTT Topic
jsonStrStringMQTT 消息的原始 payload 字符串

返回值必须是包含 points 数组的对象。数组中的每一项包含 pointIdvalue;只有返回的功能点会被更新。

示例

function parseReportRequest(topic, jsonStr) {
try {
const obj = JSON.parse(jsonStr);
const values = obj.values || {};
return {
points: Object.keys(values).map(function (pointId) {
return {
pointId: pointId,
value: String(values[pointId])
};
})
};
} catch (e) {
return { points: [] };
}
}

buildWriteRequest

该方法负责根据要控制的功能点 ID 和设定的值,构造向设备下发控制指令的字符串。

当 Aqara Studio 需要向设备发送控制命令(如开关、设定温度)时,会调用此函数,传入设备 ID、目标功能点 ID 和要设置的数值。函数需返回一个字符串,该字符串需符合设备协议的控制指令格式,通常包含设备标识、时间戳和具体的值字段。

请根据设备实际可接受的报文格式,在函数内部合理构建对象结构。例如,以 MQTT 设备为例,可包含 nodegrouptimestamp 等字段,并将 pointId 对应的值被写入 values 字段。

示例

// 以通过 JSON 字符串控制 MQTT 设备为例
// value 参数由 Aqara Studio 自动传入,无需手动声明或赋值
function buildWriteRequest(deviceId, pointId, value) {
try {
const obj = {
"node": "冷机",
"group": "1#冷机",
"timestamp": 1768204054654,
"values": {

},
"errors": {

},
"metas": {

}
};
obj.values[pointId] = parseFloat(value);
return JSON.stringify(obj);
} catch (e) {
return null;
}
}

完整示例

下面是一个添加 MQTT 设备功能点的完整示例:

示例中的 globalThis 注册是动态的:脚本实际实现了哪些函数,就只注册哪些函数;未实现的函数不需要注册。

备注

下面示例只展示常见 MQTT 接入流程:订阅一个包含多类上行消息的 Topic、按消息类型解析状态/事件/指令回复,并在下发命令时用请求 ID 关联回复。实际 Topic、字段名和枚举值请按设备协议替换。

(function() {
// 仅保存可 JSON 序列化的临时状态;Context 重建后会清空。
var scriptState = globalThis.studioScriptState;
scriptState.reportCount = 0;
scriptState.lastReportAt = null;

var DEVICE_ID = "device-001";
var SUB_TOPIC = "/example/" + DEVICE_ID + "/+/json";
var PUB_TOPIC = "/example/" + DEVICE_ID + "/cmd/json";
var nextRequestIdValue = 1;
var pendingWrites = {};

function parseDiscoveryResponse() {
return [
{
deviceName: "示例 MQTT 设备",
deviceId: DEVICE_ID,
brokerEndpoint: "127.0.0.1",
brokerPort: 1883,
connectionType: 3,
username: "username",
password: "password"
}
];
}

function parseDiscoverPointsResponse(deviceId) {
return [
{
pointId: "switch.on",
pointName: "开关",
dataType: "bool",
access: 3,
subTopic: SUB_TOPIC,
pubTopic: PUB_TOPIC
}
];
}

function parseReportRequest(topic, jsonStr) {
scriptState.reportCount += 1;
scriptState.lastReportAt = new Date().toISOString();
try {
var obj = JSON.parse(jsonStr);
var value = parseStatusReport(obj)
|| parseEventReport(obj)
|| parseCommandResponse(obj);
return {
points: value === null
? []
: [{ pointId: "switch.on", value: value }]
};
} catch (e) {
return { points: [] };
}
}

function parseStatusReport(obj) {
if (obj.type !== "status") {
return null;
}
return toBoolString(obj.values && obj.values["switch.on"]);
}

function parseEventReport(obj) {
if (obj.type !== "event" || obj.pointId !== "switch.on") {
return null;
}
return toBoolString(obj.value);
}

function parseCommandResponse(obj) {
if (obj.type !== "response" || obj.requestId === undefined) {
return null;
}

var key = String(obj.requestId);
var pending = pendingWrites[key];
delete pendingWrites[key];

if (!pending || obj.success !== true) {
return null;
}
return pending.value;
}

function buildWriteRequest(deviceId, pointId, value) {
try {
if (pointId !== "switch.on") {
return null;
}

var writeValue = toBoolString(value);
if (writeValue === null) {
return null;
}

var requestId = nextRequestId();
pendingWrites[String(requestId)] = {
pointId: pointId,
value: writeValue
};

var values = {};
values[pointId] = writeValue === "1";

return JSON.stringify({
type: "command",
requestId: requestId,
values: values
});
} catch (e) {
return null;
}
}

function toBoolString(value) {
if (value === true || value === 1) {
return "1";
}
if (value === false || value === 0) {
return "0";
}

var text = String(value).toLowerCase();
if (text === "true" || text === "on" || text === "1") {
return "1";
}
if (text === "false" || text === "off" || text === "0") {
return "0";
}
return null;
}

function nextRequestId() {
nextRequestIdValue += 1;
return nextRequestIdValue;
}

globalThis.parseDiscoveryResponse = parseDiscoveryResponse;
globalThis.parseDiscoverPointsResponse = parseDiscoverPointsResponse;
globalThis.buildWriteRequest = buildWriteRequest;
globalThis.parseReportRequest = parseReportRequest;
return null;
})();

排查脚本问题

在完成脚本开发后,如果您无法在 Aqara Studio 完成发现设备或功能点等操作,请参考以下步骤检查脚本是否正常运行:

  1. 使用 控制台日志 检查脚本内各方法是否被正常调用;
  2. 阅读 脚本检查要点,逐项检查脚本是否满足 Aqara Studio 的校验要求。
  3. 如果以上步骤都无法解决问题,可能是因为 Aqara Studio 的 运行资源限制 导致脚本无法正确运行。

控制台日志

  1. 为调试和排查脚本各方法(例如 parseDiscoveryResponseparseDiscoverPointsResponse 等),可在函数内部任意位置添加如下控制台日志方法:

    • console.log
    • console.info
    • console.debug
    • console.warn
    • console.error
    提示

    请勿在循环或高频报文处理函数中无条件输出日志,调试时应使用条件开关或采样输出。

    示例代码:

    function parseDiscoveryResponse() {
    console.info("开始解析发现响应", arguments);
    return [
    {
    deviceName: "xxx",
    // ...
    }
    ];
    }
    提示
    • 多参数时,日志内容将使用空格拼接输出。
    • console.warnconsole.error 的输出分别带有 [warn][error] 前缀。
  2. 在 Aqara Studio 中重新发起相关操作(如设备发现、功能点发现、功能点读取、写入、上报等),即可触发包含日志的方法。

  3. 进入 MQTT 协议配置 页面,查看 Console Content 字段是否有日志输出:

    • 若日志正常显示,说明方法已正确接入;
    • 若没有日志,请检查方法实现是否有误。
    提示
    • Console Content 最多保留 2048 个字符;超出限制时旧内容会被替换。
    • 每个脚本解析器每秒最多接收 20 条日志,超出部分会被忽略并在下一时间窗口输出汇总提示。
    • 单条日志最多保留 512 个字符并转换前 16 个参数。
    查看 Console Content 日志输出

脚本检查要点

建议您结合 Aqara Studio 各业务节点的校验流程,逐项检查脚本是否满足平台要求,确保各关键节点通过校验,提升脚本可靠性与兼容性。

业务节点调用函数主要校验
脚本加载完整脚本脚本不超过 1,048,576 个字符,并且能够被 JavaScript 引擎执行;业务函数在实际调用时还会检查是否存在且可执行。
设备发现parseDiscoveryResponse()返回值必须是数组;单次最多处理 128 个设备;无效设备项会被忽略,其中 deviceId 必须有效。
功能点发现parseDiscoverPointsResponse(deviceId)返回值必须是数组;单次最多处理 512 个功能点;逐项校验 pointId、数据类型、访问能力以及枚举或数值配置。
设备上报parseReportRequest(topic, msg)消息按 UTF-8 计算不得超过 256 KiB;函数每条消息只调用一次;返回值必须包含 points 数组,单次最多处理 512 项,并逐项校验 pointIdvalue
功能点写入buildWriteRequest(deviceId, pointId, value)函数必须返回非空字符串;生成的内容通过功能点配置的发布 Topic 发送。

运行资源限制

  • 传入 parseReportRequest 的单条 MQTT 消息按 UTF-8 计算不得超过 256 KiB;超过限制的消息不会交给 JavaScript 处理。
  • 单次设备发现最多处理 128 个设备,单次功能点发现最多处理 512 个功能点;超出部分会被忽略。
  • parseReportRequest 单次最多返回 512 个功能点值;超出部分会被忽略。
  • 脚本 Context 在长时间无调用或系统内存压力较高时可能被回收并在下次调用时重建。请勿依赖全局变量永久保存设备状态,持久状态应保存在设备或 Studio 功能点中。
  • 当脚本执行队列已满或系统内存压力较高时,本次脚本调用可能被拒绝。设备主动上报应具备合理的重试或后续状态补报机制。