Skip to main content

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:

FunctionDescription
buildDiscoveryRequestBuilds the request payload for discovering devices
buildDiscoverPointsRequestBuilds the request payload for querying device point information
buildProbeRequestBuilds the request payload for heartbeat detection
buildReadRequestBuilds the request payload for reading point data
buildWriteRequestBuilds the request payload for writing point data
buildReportResponseBuilds the response payload sent back to active device reports
packetProcessConfigDefines how to extract a complete packet from a byte stream
dispatchDispatches packets to the corresponding parser according to message content
parseDiscoveryResponseParses the device discovery response
parseDiscoverPointsResponseParses the device point information response
parseProbeResponseParses the heartbeat response
parseReadResponseParses the point read response
parseWriteResponseParses the point write response
parseReportRequestParses device-initiated report packets
tip

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.

tip

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:

ParameterTypeDescription
msgIdStringRequest message ID
deviceIdStringDevice 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:

ParameterTypeDescription
msgIdStringRequest message ID. If response matching based on msgId is enabled, passing it is recommended.
deviceIdStringDevice 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:

ParameterTypeDescription
msgIdStringRequest message ID
deviceIdStringDevice ID
pointIdStringPoint 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:

ParameterTypeDescription
msgIdStringRequest message ID
deviceIdStringDevice ID
pointIdStringPoint ID
valueString / 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:

ParameterTypeDescription
msgIdStringRequest message ID
pointIdStringPoint ID
codeByteResponse status code, where 0 means success
errorMsgStringError 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:

FieldTypeMeaning
frameStartBytesnumber[]Frame header byte sequence used to identify the start of a packet
lengthKnownAfterBytesnumberThe byte position after the frame header where the length becomes known
remainingBytesnumberThe number of bytes still to read after the length field
totalLengthnumberTotal packet length. If you do not use lengthKnownAfterBytes and remainingBytes, you can define the total length directly
tip

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 msgId and path are returned, Studio locates the transaction by request ID and then validates the parser.
  • When path is empty but msgId is not empty, Studio matches the transaction and uses the parser expected by that request.
  • When both path and msgId are empty, Studio handles the packet as parseReportRequest by default.
  • A path of parseReportRequest is always handled as an active report. It does not consume a waiting transaction even if msgId is also returned.
  • Any other unsupported non-empty path is 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

ParameterTypeDescription
fromAcceptMessageUint8ArrayRaw byte packet returned by the device

Return Value

The returned object may include the following fields:

FieldTypeDescription
msgIdStringRequest message ID extracted from the response. When msgIdMatch is enabled, it must match the ID in the request packet.
pathStringThe 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

ParameterTypeDescription
fromAcceptMessageUint8ArrayRaw 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:

FieldTypeRequiredDescription
deviceNameStringNoDevice name
modelStringNoModel
vendorStringNoVendor
firmwareVersionStringNoFirmware version
ipStringYesDevice IP address
portIntYesUDP/TCP port
localPortIntNoUDP 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.
deviceIdStringYesUnique 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.

note

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:

ParameterTypeDescription
deviceIdStringDevice ID. The platform always passes this as the first parameter. Required
fromAcceptMessageUint8Array | undefinedIf 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:

FieldTypeRequiredDescription
pointIdStringRequiredUnique point ID used in subsequent read/write operations
pointNameStringRequiredPoint name
devTypeStringConditionally requiredCorresponds to deviceType in the Aqara data model specification. Fill this when defining a point based on Aqara Spec. For available values, see Device Types.
functionCodeStringConditionally requiredCorresponds to functionCode in the Aqara data model specification. Fill this when defining a point based on Aqara Spec. For available values, see Function Codes.
traitCodeStringConditionally requiredCorresponds to traitCode in the Aqara data model specification. Fill this when defining a point based on Aqara Spec. For available values, see Trait Codes.
dataTypeStringConditionally requiredRequired for general points. Supported values are enum, bool, numerical, and text
enumRangeArrayConditionally requiredEnumeration 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 / falseTextStringOptionalDisplay 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
minNumberOptionalMinimum value for a general numeric point. Effective only when dataType is numerical. minimum can also be used as an alias
maxNumberOptionalMaximum value for a general numeric point. Effective only when dataType is numerical. maximum can also be used as an alias
stepNumberOptionalStep 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
precisionNumberOptionalPrecision value for a general numeric point. It must be greater than or equal to 0. Effective only when dataType is numerical
decimalsIntOptionalNumber 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 / unitsStringOptionalUnit 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
accessIntConditionally requiredRequired for general points. Access permission bits: bit1 means read, bit2 means write, and bit3 means report
note

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.

tip

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

ParameterTypeDescription
fromAcceptMessageUint8ArrayHeartbeat response packet returned by the device

Return Value

The return value format is as follows:

{
"code": 0,
"errorMsg": ""
}

Field descriptions:

FieldTypeRequiredDescription
codeByteYes0 means success; other values mean failure
errorMsgStringNoError description
deviceIdStringNoWhen 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

ParameterTypeDescription
fromAcceptMessageUint8ArrayResponse packet for reading point values

Return Value

The return value format is as follows:

{
"pointId": "p.1234",
"value": 1
}

Field descriptions:

FieldTypeRequiredDescription
pointIdStringNoWhen msgId matching is not used, returning the point ID is recommended
valueString / Double / Boolean / Int(enum)YesPoint 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

ParameterTypeDescription
fromAcceptMessageUint8ArrayResponse packet after writing point values

Return Value

The return value format is as follows:

{
"pointId": "p.1234",
"code": 0,
"errorMsg": ""
}

Field descriptions:

FieldTypeRequiredDescription
codeByteYes0 means success
errorMsgStringYesError description. Empty means success
pointIdStringNoWhen 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

ParameterTypeDescription
fromAcceptMessageUint8ArrayRaw 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:

FieldTypeRequiredDescription
pointIdStringYesPoint ID
valueString / Double / Boolean / Int(enum)YesPoint 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:

  1. Use Console Logging to check whether the methods in the script are being called correctly.
  2. Review Script Checklist and verify item by item whether the script meets Aqara Studio's validation requirements.
  3. If the issue still cannot be resolved, it may be caused by Aqara Studio's Runtime Resource Limits.

Console Logging

  1. To debug and troubleshoot methods in the script, such as parseDiscoveryResponse and parseDiscoverPointsResponse, you can add any of the following console logging methods anywhere inside a function:

    • console.log
    • console.info
    • console.debug
    • console.warn
    • console.error
    tip

    Do 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.warn and console.error includes the [warn] and [error] prefixes respectively.
  2. 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.

  3. Open the UDP Protocol Configuration page or the TCP Protocol Configuration page and check whether the Console Content field contains log output:

    • If logs appear correctly, the method has been integrated successfully.
    • If no logs appear, check whether the method implementation is incorrect.
    tip
    • Console Content keeps 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 nodeCalled functionMain validation
Script loadingComplete scriptThe 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 dispatchpacketProcessConfig / 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 discoveryparseDiscoveryResponse / parseDiscoverPointsResponseDiscovery 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 writeparseProbeResponse / parseReadResponse / parseWriteResponseThe 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 reportparseReportRequest(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 repliesbuild*Request / buildReportResponseThe 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 packetProcessConfig and 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.