JavaScript Script for UDP and TCP Integration
This document explains how to use JavaScript scripts in Aqara Studio to add custom UDP and TCP devices, points, and parsing functions.
Script Overview
In UDP and TCP integration scenarios, Aqara Studio uses your JavaScript script to complete the following tasks:
- Build request payloads for device discovery, point discovery, heartbeat detection, reading, and writing.
- Parse byte data returned by devices.
- Convert raw byte packets into standard object structures recognized by Aqara Studio.
- Parse point value changes actively reported by devices.
Depending on the integration method and device protocol design, you will typically work with the following 14 functions in the script, although some of them can be implemented selectively based on the protocol:
| Function | Description |
|---|---|
| buildDiscoveryRequest | Builds the request payload for discovering devices |
| buildDiscoverPointsRequest | Builds the request payload for querying device point information |
| buildProbeRequest | Builds the request payload for heartbeat detection |
| buildReadRequest | Builds the request payload for reading point data |
| buildWriteRequest | Builds the request payload for writing point data |
| buildReportResponse | Builds the response payload sent back to active device reports |
| packetProcessConfig | Defines how to extract a complete packet from a byte stream |
| dispatch | Dispatches packets to the corresponding parser according to message content |
| parseDiscoveryResponse | Parses the device discovery response |
| parseDiscoverPointsResponse | Parses the device point information response |
| parseProbeResponse | Parses the heartbeat response |
| parseReadResponse | Parses the point read response |
| parseWriteResponse | Parses the point write response |
| parseReportRequest | Parses device-initiated report packets |
Do not change function names or parameter definitions. build*Request functions need to return device protocol payloads. You can use Uint8Array, number[], hexadecimal strings, or plain strings. The platform converts the return value to a byte array and sends it through TCP/UDP.
Not every function must be implemented in every device scenario. For example, if your device does not require a reply to active reports, buildReportResponse can be omitted. If there is no dedicated point discovery request, parseDiscoverPointsResponse can return point information directly.
Script Global Variables
A protocol script can use globalThis.studioScriptState to share a small amount of temporary state across multiple function calls in the same Context. This variable is initialized as an empty object by default:
globalThis.studioScriptState = {
requestCount: 0,
lastValue: null
};
Keep this variable as a plain object. Its properties may contain strings, finite numbers, booleans, null, plain objects, and arrays, including nested combinations of those types. Convert dates to strings first. Do not store undefined, functions, Symbol, BigInt, NaN, Infinity, circular references, Map, Set, Java/host objects, or custom class instances; such values may be ignored, lose information, or cause serialization failures.
State size is measured by the serialized UTF-8 JSON byte length. The warning threshold is 2 MiB. Studio performs a rate-limited check after script calls. When the state first exceeds the threshold, Studio writes one [warn] entry to both the service log and Console Content on the protocol configuration page. Repeated overflow does not produce periodic warnings, does not delete the state automatically, and does not terminate the current call. Keep this variable below 2 MiB during normal development and operation.
globalThis.studioScriptState is reinitialized after idle Context reclamation, memory-pressure reclamation, script updates, or execution timeouts. Do not use it in place of packetProcessConfig for cross-packet framing, and do not use it for durable device state, credentials, or an unbounded message history.
Prerequisites
Before writing the script, make sure you fully understand the UDP or TCP protocol details of the target device, including but not limited to:
- Device discovery packet format
- Point discovery packet format
- Heartbeat packet format
- Point read and write packet format
- Device active report packet format
- Stream framing rules, frame header rules, and packet length rules
- The field structure and meaning of the returned device data
Only by fully understanding the protocol above can you ensure that your JavaScript script correctly builds packets and parses data according to the device specification.
buildDiscoveryRequest
Return a device protocol payload that can be converted into a byte array. It tells Aqara Studio how to send the byte packet for device discovery.
Parameters
This function has no parameters. Device discovery does not use msgIdMatch.
Return Value
To ensure Aqara Studio can send the request properly, make sure buildDiscoveryRequest returns a byte array, for example:
Uint8Array.from("001#DEVICE_SCAN#\r\n", c => c.charCodeAt(0))
Example
function buildDiscoveryRequest() {
return Uint8Array.from("001#DEVICE_SCAN#\r\n", c => c.charCodeAt(0));
}
buildDiscoverPointsRequest
Return a device protocol payload that can be converted into a byte array. It tells Aqara Studio how to send the point discovery packet to the specified device. If device points are static, this function can be omitted or return an empty value, and the platform calls parseDiscoverPointsResponse(deviceId, undefined).
Parameters
It is recommended that this function accept the following parameters:
| Parameter | Type | Description |
|---|---|---|
| msgId | String | Request message ID |
| deviceId | String | Device ID |
Return Value
The return value must be a byte array representing the point discovery request packet.
Example
function buildDiscoverPointsRequest(msgId, deviceId) {
return Uint8Array.from(msgId + "#DEVICE_POINT_SCAN#" + deviceId, c => c.charCodeAt(0));
}
buildProbeRequest
Return a device protocol payload that can be converted into a byte array. It tells Aqara Studio how to send a heartbeat packet to the device.
Parameters
It is recommended that this function accept the following parameters:
| Parameter | Type | Description |
|---|---|---|
| msgId | String | Request message ID. If response matching based on msgId is enabled, passing it is recommended. |
| deviceId | String | Device ID |
Return Value
The return value must be a byte array representing the heartbeat packet.
Example
function buildProbeRequest(msgId, deviceId) {
return Uint8Array.from(msgId + "#SHAKEHANDS#\r\n", c => c.charCodeAt(0));
}
buildReadRequest
Return a device protocol payload that can be converted into a byte array. It tells Aqara Studio how to send the packet for reading point values.
Parameters
It is recommended that this function accept the following parameters:
| Parameter | Type | Description |
|---|---|---|
| msgId | String | Request message ID |
| deviceId | String | Device ID |
| pointId | String | Point ID |
Return Value
The return value must be a byte array representing the point read packet.
Example
function buildReadRequest(msgId, deviceId, pointId) {
return Uint8Array.from(msgId + "#POLL#" + pointId + "\r\n", c => c.charCodeAt(0));
}
buildWriteRequest
Return a device protocol payload that can be converted into a byte array. It tells Aqara Studio how to send the control packet for writing point values.
Parameters
It is recommended that this function accept the following parameters:
| Parameter | Type | Description |
|---|---|---|
| msgId | String | Request message ID |
| deviceId | String | Device ID |
| pointId | String | Point ID |
| value | String / Number / Boolean / Int(enum) | Control value to write |
Return Value
The return value must be a byte array representing the control packet.
Example
function buildWriteRequest(msgId, deviceId, pointId, value) {
return Uint8Array.from(msgId + "#WRITE#" + "#" + pointId + "#" + value, c => c.charCodeAt(0));
}
buildReportResponse
Return a device protocol payload that can be converted into a byte array. It tells Aqara Studio how to reply to the active report packet sent by the device.
If your protocol does not require a reply to report packets, this function can be omitted.
Parameters
It is recommended that this function accept the following parameters:
| Parameter | Type | Description |
|---|---|---|
| msgId | String | Request message ID |
| pointId | String | Point ID |
| code | Byte | Response status code, where 0 means success |
| errorMsg | String | Error reason |
Return Value
The return value must be a byte array representing the report reply packet.
Example
function buildReportResponse(msgId, pointId, code, errorMsg) {
return Uint8Array.from(msgId + "#EVT_REPLY#OK", c => c.charCodeAt(0));
}
packetProcessConfig
Return an object in this function to define how Aqara Studio should extract one complete packet from a UDP/TCP byte stream.
If you do not define this function, Aqara Studio reads 128 bytes each time as the response value by default, which is usually suitable only for very simple packet scenarios.
Return Value
The return value format is as follows:
{
"frameStartBytes": [0, 216],
"lengthKnownAfterBytes": 4,
"remainingBytes": 1
}
Field descriptions:
| Field | Type | Meaning |
|---|---|---|
| frameStartBytes | number[] | Frame header byte sequence used to identify the start of a packet |
| lengthKnownAfterBytes | number | The byte position after the frame header where the length becomes known |
| remainingBytes | number | The number of bytes still to read after the length field |
| totalLength | number | Total packet length. If you do not use lengthKnownAfterBytes and remainingBytes, you can define the total length directly |
frameStartBytes, lengthKnownAfterBytes, and remainingBytes are required, while totalLength is optional. Usually you configure either lengthKnownAfterBytes + remainingBytes or totalLength, depending on the device's frame structure protocol.
Example
function packetProcessConfig() {
return {
frameStartBytes: [0x00, 0xD8],
lengthKnownAfterBytes: 4,
remainingBytes: 1
};
}
dispatch
This function is required in TCP/UDP scripts.
Implement packet dispatch logic in this function so Aqara Studio can determine which parser should be called for the byte packet returned by the device.
Behavior
After Aqara Studio receives a packet, it calls this function first. You need to decide in this function:
- Which request the current packet is responding to
- Or which parser function should handle the packet
msgId and path are independent: msgId is used for transaction matching, while path validates and selects the parser.
- When both
msgIdandpathare returned, Studio locates the transaction by request ID and then validates the parser. - When
pathis empty butmsgIdis not empty, Studio matches the transaction and uses the parser expected by that request. - When both
pathandmsgIdare empty, Studio handles the packet asparseReportRequestby default. - A
pathofparseReportRequestis always handled as an active report. It does not consume a waiting transaction even ifmsgIdis also returned. - Any other unsupported non-empty
pathis rejected and logged as a warning.
The protocol configuration field msgIdMatch is the request ID validation switch and is disabled by default. When disabled, the first msgId argument passed to buildDiscoverPointsRequest, buildProbeRequest, buildReadRequest, and buildWriteRequest is an empty string. When enabled, Studio generates a non-empty ID for each request. The script should include it in the request packet and return the same ID from the response through dispatch. Studio uses the ID to match waiting transactions and out-of-order responses. A missing, mismatched, or expired ID does not complete the current transaction, so the operation may retry or time out. This field does not decide whether a request waits for a response, does not replace path, and does not apply to device discovery or buildReportResponse.
Parameters
| Parameter | Type | Description |
|---|---|---|
| fromAcceptMessage | Uint8Array | Raw byte packet returned by the device |
Return Value
The returned object may include the following fields:
| Field | Type | Description |
|---|---|---|
| msgId | String | Request message ID extracted from the response. When msgIdMatch is enabled, it must match the ID in the request packet. |
| path | String | The corresponding parser function name, such as parseProbeResponse |
Example Return Value
{
"msgId": "msg.1234",
"path": "parseProbeResponse"
}
Example
function dispatch(fromAcceptMessage) {
const dataStr = String.fromCharCode.apply(null, fromAcceptMessage)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
const reply = parts[1];
if (reply == "SHAKEHANDS_REPLY") {
return {
msgId: parts[0],
path: "parseProbeResponse"
};
} else if (reply == "EVT_REPLY") {
return {
msgId: parts[0],
path: "parseWriteResponse"
};
} else if (reply == "EVT") {
return {
msgId: parts[0],
path: "parseReportRequest"
};
}
}
parseDiscoveryResponse
Parse the device discovery response packet in this function and return an array of device information objects.
If buildDiscoveryRequest is not defined, Aqara Studio may also use the data returned by this function directly as the basis for device discovery, so make sure the returned structure is stable.
Parameters
| Parameter | Type | Description |
|---|---|---|
| fromAcceptMessage | Uint8Array | Raw byte packet of the device discovery response |
Return Value
The return value format is as follows:
[
{
"deviceName": "dn1",
"model": "model-x",
"vendor": "simensA",
"firmwareVersion": "1.1.0",
"deviceId": "uuid1234",
"ip": "device ip",
"port": 1102,
"localPort": 1103
}
]
Field descriptions:
| Field | Type | Required | Description |
|---|---|---|---|
| deviceName | String | No | Device name |
| model | String | No | Model |
| vendor | String | No | Vendor |
| firmwareVersion | String | No | Firmware version |
| ip | String | Yes | Device IP address |
| port | Int | Yes | UDP/TCP port |
| localPort | Int | No | UDP only. The local port that Studio binds to receive responses and unsolicited reports. If omitted or set to 0, Studio uses port; TCP ignores this field. |
| deviceId | String | Yes | Unique device identifier |
Use localPort when the device's remote service port differs from Studio's receive port, when the device requires a fixed client source port, or when multiple UDP devices need distinct local listening ports. Most request-response devices that use the same port do not need this field.
Each UDP device currently binds its own local listening port. Make sure concurrently running UDP devices use available, non-conflicting localPort values. This integration mode does not currently support multiple devices sharing one Studio listening port for unsolicited reports.
Example
function parseDiscoveryResponse(msg) {
if (!msg || msg.length === 0) {
return null;
}
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const snMatch = dataStr.match(/SN=([^&]+)/);
const ipMatch = dataStr.match(/IP=([^\r\n&]+)/);
return [
{
deviceId: snMatch ? snMatch[1].trim() : "",
ip: ipMatch ? ipMatch[1].trim() : "",
port: 8820
}
];
}
parseDiscoverPointsResponse
Parse device point information in this function and return an array of point objects.
Parameters
This function accepts the following parameters:
| Parameter | Type | Description |
|---|---|---|
| deviceId | String | Device ID. The platform always passes this as the first parameter. Required |
| fromAcceptMessage | Uint8Array | undefined | If buildDiscoverPointsRequest is defined and the device response is received, this is passed as the second parameter. It may be empty when there is no separate point discovery request |
Return Value
The return value should be an array of point objects, for example:
[
{
"pointId": "p1",
"pointName": "pn1",
"devType": "Light",
"functionCode": "Output",
"traitCode": "OnOff"
},
{
"pointId": "p2",
"pointName": "pn2",
"dataType": "numerical",
"min": -20,
"max": 50,
"step": 0.1,
"precision": 0.1,
"decimals": 1,
"access": 3
}
]
Field descriptions:
| Field | Type | Required | Description |
|---|---|---|---|
| pointId | String | Required | Unique point ID used in subsequent read/write operations |
| pointName | String | Required | Point name |
| devType | String | Conditionally required | Corresponds to deviceType in the Aqara data model specification. Fill this when defining a point based on Aqara Spec. For available values, see Device Types. |
| functionCode | String | Conditionally required | Corresponds to functionCode in the Aqara data model specification. Fill this when defining a point based on Aqara Spec. For available values, see Function Codes. |
| traitCode | String | Conditionally required | Corresponds to traitCode in the Aqara data model specification. Fill this when defining a point based on Aqara Spec. For available values, see Trait Codes. |
| dataType | String | Conditionally required | Required for general points. Supported values are enum, bool, numerical, and text |
| enumRange | Array | Conditionally required | Enumeration option array. Required only when the general point dataType is enum. The array must not be empty. Each item contains an integer value and a string label. Both value and label must be unique, and label must not be empty |
| trueText / falseText | String | Optional | Display labels for a general Boolean point. Effective only when dataType is bool. If not configured, trueText defaults to On and falseText defaults to Off. Configure them only when custom labels such as Enabled / Disabled are needed. activeText / onText and inactiveText / offText can also be used as aliases |
| min | Number | Optional | Minimum value for a general numeric point. Effective only when dataType is numerical. minimum can also be used as an alias |
| max | Number | Optional | Maximum value for a general numeric point. Effective only when dataType is numerical. maximum can also be used as an alias |
| step | Number | Optional | Step value for a general numeric point. It must be greater than 0. Effective only when dataType is numerical. resolution can also be used as an alias |
| precision | Number | Optional | Precision value for a general numeric point. It must be greater than or equal to 0. Effective only when dataType is numerical |
| decimals | Int | Optional | Number of decimal places for a general numeric point. It must be an integer greater than or equal to 0. Effective only when dataType is numerical |
| unit / units | String | Optional | Unit for a general numeric point. Effective only when dataType is numerical. It is recommended to use registered unit names such as kilowatt hour, watt, volt, ampere, celsius, and percent. Studio displays the corresponding symbols such as kWh, W, V, A, °C, and %. See Unit Names and Symbols Supported by Scripts for the full mapping. The system can also match registered unit symbols |
| access | Int | Conditionally required | Required for general points. Access permission bits: bit1 means read, bit2 means write, and bit3 means report |
For numerical points, when Aqara Studio calls buildWriteRequest, it formats the write value by decimals first. If decimals is not configured, it infers the number of decimal places from step / resolution. To avoid binary floating-point artifacts such as 0.30000030249357224, explicitly configure decimals or step for points that write fractional values. precision only describes precision and is not used as the decimal-place truncation rule for outgoing write payloads.
If a numeric point is used by energy statistics or other unit-dependent features, configure unit or units. For example, use kilowatt hour (displayed as kWh) for accumulated energy, watt (W) for current power, volt (V) for voltage, ampere (A) for current, and celsius (°C) for temperature. Enter unit names exactly as shown in Unit Names and Symbols Supported by Scripts, including case and spaces.
If you have already defined devType, functionCode, and traitCode based on Aqara Spec, you do not need to rely on the general dataType and access mode. If you do not use Aqara Spec, then dataType and access must be provided.
Example
function parseDiscoverPointsResponse(deviceId, fromAcceptMessage) {
return [
{
pointId: "sceneWrite",
pointName: "Scene Trigger",
dataType: "text",
access: 2
},
{
pointId: "sceneListen",
pointName: "Scene Listen",
dataType: "text",
access: 1
},
{
pointId: "setTemperature",
pointName: "Set Temperature",
dataType: "numerical",
min: -20,
max: 50,
step: 0.1,
precision: 0.1,
decimals: 1,
unit: "celsius",
access: 3
},
{
pointId: "operatingMode",
pointName: "Operating Mode",
dataType: "enum",
enumRange: [
{ value: 0, label: "Off" },
{ value: 1, label: "Auto" }
],
access: 3
}
];
}
parseProbeResponse
Parse the device heartbeat response in this function and return a standard status object.
Parameters
| Parameter | Type | Description |
|---|---|---|
| fromAcceptMessage | Uint8Array | Heartbeat response packet returned by the device |
Return Value
The return value format is as follows:
{
"code": 0,
"errorMsg": ""
}
Field descriptions:
| Field | Type | Required | Description |
|---|---|---|---|
| code | Byte | Yes | 0 means success; other values mean failure |
| errorMsg | String | No | Error description |
| deviceId | String | No | When msgId matching is not used, you can return the device ID |
Example
function parseProbeResponse(msg) {
if (!msg || msg.length === 0) {
return null;
}
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
if (parts.length == 3 && parts[2] == "SUCCEED") {
return {
code: 0,
errorMsg: ""
};
} else {
return {
code: 1,
errorMsg: "no reason"
};
}
}
parseReadResponse
Parse the device point read response in this function and return a point value object.
Parameters
| Parameter | Type | Description |
|---|---|---|
| fromAcceptMessage | Uint8Array | Response packet for reading point values |
Return Value
The return value format is as follows:
{
"pointId": "p.1234",
"value": 1
}
Field descriptions:
| Field | Type | Required | Description |
|---|---|---|---|
| pointId | String | No | When msgId matching is not used, returning the point ID is recommended |
| value | String / Double / Boolean / Int(enum) | Yes | Point value |
Example
function parseReadResponse(msg) {
if (!msg || msg.length === 0) {
return null;
}
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
return {
pointId: parts[0],
value: parts[1]
};
}
parseWriteResponse
Parse the device control response in this function and return a standard write result object.
Parameters
| Parameter | Type | Description |
|---|---|---|
| fromAcceptMessage | Uint8Array | Response packet after writing point values |
Return Value
The return value format is as follows:
{
"pointId": "p.1234",
"code": 0,
"errorMsg": ""
}
Field descriptions:
| Field | Type | Required | Description |
|---|---|---|---|
| code | Byte | Yes | 0 means success |
| errorMsg | String | Yes | Error description. Empty means success |
| pointId | String | No | When msgId matching is not used, returning the point ID is recommended |
Example
function parseWriteResponse(msg) {
if (!msg || msg.length === 0) {
return null;
}
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
if (parts[2] == "OK") {
return {
code: 0,
errorMsg: ""
};
} else {
return {
code: 1,
errorMsg: parts[2]
};
}
}
parseReportRequest
Parse the device-initiated point value report packet in this function and return all point values in that packet at once. This function is called only once per complete packet, and returning multiple points does not execute the script once per point.
Prerequisites
You only need to focus on implementing this function when the device supports active reporting or when your integration depends on device-pushed point value changes.
Parameters
| Parameter | Type | Description |
|---|---|---|
| fromAcceptMessage | Uint8Array | Raw byte packet actively reported by the device |
Return Value
The return value must be an object containing a points array:
{
points: [
{ pointId: "temperature", value: 23.5 },
{ pointId: "humidity", value: 60 }
]
}
Field descriptions for items in points:
| Field | Type | Required | Description |
|---|---|---|---|
| pointId | String | Yes | Point ID |
| value | String / Double / Boolean / Int(enum) | Yes | Point value |
Example
function parseReportRequest(msg) {
if (!msg || msg.length === 0) {
return { points: [] };
}
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
return {
points: [
{
pointId: parts[0],
value: parts[1]
}
]
};
}
Complete Example
The following example shows a simplified UDP/TCP integration script structure. You can adjust it according to the device protocol:
The globalThis registrations in the example are dynamic: register only the functions you actually implement. Functions that are not implemented do not need to be registered.
(function(){
// Store only JSON-serializable temporary state. It is cleared when the Context is rebuilt.
const scriptState = globalThis.studioScriptState;
scriptState.reportCount = 0;
scriptState.lastReportAt = null;
function buildDiscoveryRequest() {
return Uint8Array.from("001#DEVICE_SCAN#\r\n", c => c.charCodeAt(0));
}
function buildDiscoverPointsRequest(msgId, deviceId) {
return Uint8Array.from(msgId + "#DEVICE_POINT_SCAN#" + deviceId, c => c.charCodeAt(0));
}
function buildProbeRequest(msgId, deviceId) {
return Uint8Array.from(msgId + "#SHAKEHANDS#\r\n", c => c.charCodeAt(0));
}
function buildReadRequest(msgId, deviceId, pointId) {
return Uint8Array.from(msgId + "#POLL#" + pointId + "\r\n", c => c.charCodeAt(0));
}
function buildWriteRequest(msgId, deviceId, pointId, value) {
return Uint8Array.from(msgId + "#WRITE#" + "#" + pointId + "#" + value, c => c.charCodeAt(0));
}
function buildReportResponse(msgId, pointId, code, errorMsg) {
return Uint8Array.from(msgId + "#EVT_REPLY#OK", c => c.charCodeAt(0));
}
function packetProcessConfig() {
return { frameStartBytes: [0x00, 0xD8], lengthKnownAfterBytes: 4, remainingBytes: 1 };
}
function dispatch(fromAcceptMessage) {
const dataStr = String.fromCharCode.apply(null, fromAcceptMessage)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
const reply = parts[1];
if (reply == "SHAKEHANDS_REPLY") {
return { msgId: parts[0], path: "parseProbeResponse" };
} else if (reply == "EVT_REPLY") {
return { msgId: parts[0], path: "parseWriteResponse" };
} else if (reply == "EVT") {
return { msgId: parts[0], path: "parseReportRequest" };
}
}
function parseDiscoveryResponse(msg) {
if (!msg || msg.length === 0) return null;
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const snMatch = dataStr.match(/SN=([^&]+)/);
const ipMatch = dataStr.match(/IP=([^\r\n&]+)/);
return [
{
deviceId: snMatch ? snMatch[1].trim() : "",
ip: ipMatch ? ipMatch[1].trim() : "",
port: 8820
}
];
}
function parseDiscoverPointsResponse(deviceId, fromAcceptMessage) {
return [
{
pointId: "sceneWrite",
pointName: "Scene Trigger",
dataType: "text",
access: 2
},
{
pointId: "sceneListen",
pointName: "Scene Listen",
dataType: "text",
access: 1
}
];
}
function parseProbeResponse(msg) {
if (!msg || msg.length === 0) return null;
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
if (parts.length == 3 && parts[2] == "SUCCEED") {
return { code: 0, errorMsg: "" };
} else {
return { code: 1, errorMsg: "no reason" };
}
}
function parseReadResponse(msg) {
if (!msg || msg.length === 0) return null;
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
return {
pointId: parts[0],
value: parts[1]
};
}
function parseWriteResponse(msg) {
if (!msg || msg.length === 0) return null;
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
if (parts[2] == "OK") {
return { code: 0, errorMsg: "" };
} else {
return { code: 1, errorMsg: parts[2] };
}
}
function parseReportRequest(msg) {
scriptState.reportCount += 1;
scriptState.lastReportAt = new Date().toISOString();
if (!msg || msg.length === 0) return { points: [] };
const dataStr = String.fromCharCode.apply(null, msg)
.split('\0')[0]
.replace(/[^\x20-\x7E]/g, '');
const parts = dataStr.split("#");
return {
points: [
{
pointId: parts[0],
value: parts[1]
}
]
};
}
globalThis.buildDiscoveryRequest = buildDiscoveryRequest;
globalThis.buildDiscoverPointsRequest = buildDiscoverPointsRequest;
globalThis.buildProbeRequest = buildProbeRequest;
globalThis.buildReadRequest = buildReadRequest;
globalThis.buildWriteRequest = buildWriteRequest;
globalThis.buildReportResponse = buildReportResponse;
globalThis.packetProcessConfig = packetProcessConfig;
globalThis.dispatch = dispatch;
globalThis.parseDiscoveryResponse = parseDiscoveryResponse;
globalThis.parseDiscoverPointsResponse = parseDiscoverPointsResponse;
globalThis.parseProbeResponse = parseProbeResponse;
globalThis.parseReadResponse = parseReadResponse;
globalThis.parseWriteResponse = parseWriteResponse;
globalThis.parseReportRequest = parseReportRequest;
})()
Troubleshooting Script Issues
After you finish developing the script, if Aqara Studio still cannot complete device discovery or point discovery, follow the steps below to check whether the script is running correctly:
- Use Console Logging to check whether the methods in the script are being called correctly.
- Review Script Checklist and verify item by item whether the script meets Aqara Studio's validation requirements.
- If the issue still cannot be resolved, it may be caused by Aqara Studio's Runtime Resource Limits.
Console Logging
-
To debug and troubleshoot methods in the script, such as
parseDiscoveryResponseandparseDiscoverPointsResponse, you can add any of the following console logging methods anywhere inside a function:console.logconsole.infoconsole.debugconsole.warnconsole.error
tipDo not print logs unconditionally inside loops or high-frequency packet-processing functions. Use conditional switches or sampled logging during debugging.
Example:
function parseDiscoveryResponse() {
console.info("Start parsing discovery response", arguments);
return [
{
deviceName: "xxx",
// ...
}
];
}tip- When multiple arguments are passed, the log output joins them with spaces.
- Output from
console.warnandconsole.errorincludes the[warn]and[error]prefixes respectively.
-
Trigger the related action again in Aqara Studio, such as device discovery, point discovery, point reading, writing, or reporting, to execute the method that contains logs.
-
Open the UDP Protocol Configuration page or the TCP Protocol Configuration page and check whether the
Console Contentfield contains log output:- If logs appear correctly, the method has been integrated successfully.
- If no logs appear, check whether the method implementation is incorrect.
tipConsole Contentkeeps at most 2048 characters. When the limit is exceeded, older content is replaced.- Each script parser accepts at most 20 log entries per second. Excess logs are ignored and summarized in the next time window.
- Each log entry keeps at most 512 characters and converts only the first 16 arguments.
Script Checklist
It is recommended that you review the script item by item according to the validation flow of each Aqara Studio business node so that all key checks pass and the script remains reliable and compatible.
| Business node | Called function | Main validation |
|---|---|---|
| Script loading | Complete script | The script must not exceed 1,048,576 characters and must be executable. After loading, Studio checks dispatch and the protocol-expected functions. The validation result is used for diagnostics but does not block script loading. |
| Packet framing and dispatch | packetProcessConfig / dispatch(bytes) | A single complete packet passed into JavaScript must not exceed 64 KiB. dispatch should return path, msgId, or both. If only msgId is returned, the message is handed to transaction matching. If both are empty, it is treated as an active report. |
| Device and point discovery | parseDiscoveryResponse / parseDiscoverPointsResponse | Discovery results must be arrays. At most 128 devices or 512 points are processed in a single call. Invalid entries are ignored item by item. |
| Probe, read, and write | parseProbeResponse / parseReadResponse / parseWriteResponse | The function is selected according to dispatch.path. The response structure, point matching, and value type must be valid. Otherwise, only the current operation fails and other Contexts are not affected. |
| Active device report | parseReportRequest(bytes) | The function is called once per packet. The return value must contain a points array. At most 512 items are processed in a single call, and each item is validated for pointId, value, point matching, and type conversion. |
| Request packets and report replies | build*Request / buildReportResponse | The return value must be convertible to a byte array. An empty value means no packet is sent for this operation. |
Runtime Resource Limits
- A single TCP/UDP binary packet passed into JavaScript must not exceed 64 KiB. Oversized packets are not passed to JavaScript.
- A single device discovery call processes at most 128 devices, and a single point discovery call processes at most 512 points. Additional entries are ignored.
- A single active report processes at most 512 point values. Additional entries are ignored.
- A script Context may be reclaimed after a long idle period or when the system is under memory pressure, and it is rebuilt on the next call. Do not rely on global variables to permanently store device state. Cross-packet framing should be handled by
packetProcessConfigand the transport layer. - A script call may be rejected when the execution queue is full or system memory pressure is high. Active device reports should support reasonable retries or later state retransmission.