From 0bf701d5db685c8519c8c047b309aa8a41003d42 Mon Sep 17 00:00:00 2001 From: AkashM707 Date: Fri, 21 Aug 2026 13:23:59 -0700 Subject: [PATCH 1/2] Add Milesight WS303 mini leak detection sensor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the WS303 with AS923, AU915, EU868 and US915 profiles, reusing the existing Milesight 1.0.3 profiles. The codec is Milesight's published decoder and encoder for this product, combined into a single file. Both halves define a getValue helper with opposite semantics — the decoder maps a numeric key to a label, the encoder maps a label back to its key — so the encoder's has been renamed to getEncodeValue. Concatenating them unchanged lets the encoder's definition win through hoisting, and every decode returns "unknown". Decode and encode tests cover battery, leakage status, device status, reboot and report interval. --- .../milesight/codecs/test_decode_ws303.json | 27 + .../milesight/codecs/test_encode_ws303.json | 12 + vendors/milesight/codecs/ws303.js | 698 ++++++++++++++++++ .../milesight/devices/milesight-ws303.toml | 18 + vendors/milesight/vendor.toml | 1 + 5 files changed, 756 insertions(+) create mode 100644 vendors/milesight/codecs/test_decode_ws303.json create mode 100644 vendors/milesight/codecs/test_encode_ws303.json create mode 100644 vendors/milesight/codecs/ws303.js create mode 100644 vendors/milesight/devices/milesight-ws303.toml diff --git a/vendors/milesight/codecs/test_decode_ws303.json b/vendors/milesight/codecs/test_decode_ws303.json new file mode 100644 index 0000000..6bda9cb --- /dev/null +++ b/vendors/milesight/codecs/test_decode_ws303.json @@ -0,0 +1,27 @@ +[ + { + "name": "Test decode periodic uplink (battery)", + "input": { "bytes": [1, 117, 100] }, + "expected": { "data": { "battery": 100 } } + }, + { + "name": "Test decode leakage alarm", + "input": { "bytes": [3, 0, 1] }, + "expected": { "data": { "leakage_status": "leak" } } + }, + { + "name": "Test decode leakage cleared", + "input": { "bytes": [3, 0, 0] }, + "expected": { "data": { "leakage_status": "normal" } } + }, + { + "name": "Test decode battery and leakage status", + "input": { "bytes": [1, 117, 92, 3, 0, 0] }, + "expected": { "data": { "battery": 92, "leakage_status": "normal" } } + }, + { + "name": "Test decode device status", + "input": { "bytes": [255, 11, 1] }, + "expected": { "data": { "device_status": "on" } } + } +] diff --git a/vendors/milesight/codecs/test_encode_ws303.json b/vendors/milesight/codecs/test_encode_ws303.json new file mode 100644 index 0000000..041c274 --- /dev/null +++ b/vendors/milesight/codecs/test_encode_ws303.json @@ -0,0 +1,12 @@ +[ + { + "name": "Test encode reboot", + "input": { "data": { "reboot": "yes" } }, + "expected": { "bytes": [255, 16, 255] } + }, + { + "name": "Test encode report interval", + "input": { "data": { "report_interval": 3600 } }, + "expected": { "bytes": [255, 3, 16, 14] } + } +] diff --git a/vendors/milesight/codecs/ws303.js b/vendors/milesight/codecs/ws303.js new file mode 100644 index 0000000..3a093af --- /dev/null +++ b/vendors/milesight/codecs/ws303.js @@ -0,0 +1,698 @@ +/** + * Payload Decoder + * + * Copyright 2024 Milesight IoT + * + * @product WS303 + */ +var RAW_VALUE = 0x00; + +/* eslint no-redeclare: "off" */ +/* eslint-disable */ +// Chirpstack v4 +function decodeUplink(input) { + var decoded = milesightDeviceDecode(input.bytes); + return { data: decoded }; +} + +// Chirpstack v3 +function Decode(fPort, bytes) { + return milesightDeviceDecode(bytes); +} + +// The Things Network +function Decoder(bytes, port) { + return milesightDeviceDecode(bytes); +} +/* eslint-enable */ + +function milesightDeviceDecode(bytes) { + var decoded = {}; + + for (var i = 0; i < bytes.length; ) { + var channel_id = bytes[i++]; + var channel_type = bytes[i++]; + + // IPSO VERSION + if (channel_id === 0xff && channel_type === 0x01) { + decoded.ipso_version = readProtocolVersion(bytes[i]); + i += 1; + } + // HARDWARE VERSION + else if (channel_id === 0xff && channel_type === 0x09) { + decoded.hardware_version = readHardwareVersion(bytes.slice(i, i + 2)); + i += 2; + } + // FIRMWARE VERSION + else if (channel_id === 0xff && channel_type === 0x0a) { + decoded.firmware_version = readFirmwareVersion(bytes.slice(i, i + 2)); + i += 2; + } + // TSL VERSION + else if (channel_id === 0xff && channel_type === 0xff) { + decoded.tsl_version = readTslVersion(bytes.slice(i, i + 2)); + i += 2; + } + // SERIAL NUMBER + else if (channel_id === 0xff && channel_type === 0x16) { + decoded.sn = readSerialNumber(bytes.slice(i, i + 8)); + i += 8; + } + // LORAWAN CLASS TYPE + else if (channel_id === 0xff && channel_type === 0x0f) { + decoded.lorawan_class = readLoRaWANClass(bytes[i]); + i += 1; + } + // RESET EVENT + else if (channel_id === 0xff && channel_type === 0xfe) { + decoded.reset_event = readResetEvent(1); + i += 1; + } + // DEVICE STATUS + else if (channel_id === 0xff && channel_type === 0x0b) { + decoded.device_status = readDeviceStatus(1); + i += 1; + } + + // BATTERY + else if (channel_id === 0x01 && channel_type === 0x75) { + decoded.battery = readUInt8(bytes[i]); + i += 1; + } + // WATER LEAK + else if (channel_id === 0x03 && channel_type === 0x00) { + decoded.leakage_status = readLeakageStatus(bytes[i]); + i += 1; + } + // DOWNLINK RESPONSE + else if (channel_id === 0xfe || channel_id === 0xff) { + var result = handle_downlink_response(channel_type, bytes, i); + decoded = Object.assign(decoded, result.data); + i = result.offset; + } else { + break; + } + } + + return decoded; +} + +function handle_downlink_response(channel_type, bytes, offset) { + var decoded = {}; + + switch (channel_type) { + case 0x03: + decoded.report_interval = readUInt16LE(bytes.slice(offset, offset + 2)); + offset += 2; + break; + case 0x10: + decoded.reboot = readYesNoStatus(1); + offset += 1; + break; + case 0x28: + decoded.query_device_status = readYesNoStatus(1); + offset += 1; + break; + case 0x3d: + decoded.stop_alarming = readYesNoStatus(1); + offset += 1; + break; + case 0x3e: + decoded.buzzer_enable = readEnableStatus(bytes[offset]); + offset += 1; + break; + + case 0x7e: + decoded.leakage_alarm_config = {}; + decoded.leakage_alarm_config.enable = readEnableStatus(bytes[offset]); + decoded.leakage_alarm_config.alarm_interval = readUInt16LE(bytes.slice(offset + 1, offset + 3)); + decoded.leakage_alarm_config.alarm_count = readUInt16LE(bytes.slice(offset + 3, offset + 5)); + offset += 5; + break; + case 0x7f: + decoded.find_device_enable = readEnableStatus(bytes[offset]); + offset += 1; + break; + case 0x80: + decoded.find_device_time = readUInt16LE(bytes.slice(offset, offset + 2)); + offset += 2; + break; + case 0x81: + var report_data = readUInt8(bytes[offset + 1]); + decoded.lora_report_settings = {}; + decoded.lora_report_settings.lora_uplink_enable = readEnableStatus((report_data >>> 0) & 0x01); + decoded.lora_report_settings.d2d_uplink_enable = readEnableStatus((report_data >>> 1) & 0x01); + offset += 2; + break; + case 0x83: + var d2d_master_config = {}; + d2d_master_config.mode = readEnableStatus(bytes[offset]); + d2d_master_config.d2d_command = readD2DCommand(bytes.slice(offset + 1, offset + 3)); + offset += 3; + decoded.d2d_master_config = decoded.d2d_master_config || []; + decoded.d2d_master_config.push(d2d_master_config); + break; + case 0x84: + decoded.d2d_enable = readEnableStatus(bytes[offset]); + offset += 1; + break; + default: + throw new Error("unknown downlink response"); + } + + return { data: decoded, offset: offset }; +} + +function readProtocolVersion(bytes) { + var major = (bytes & 0xf0) >> 4; + var minor = bytes & 0x0f; + return "v" + major + "." + minor; +} + +function readHardwareVersion(bytes) { + var major = (bytes[0] & 0xff).toString(16); + var minor = (bytes[1] & 0xff) >> 4; + return "v" + major + "." + minor; +} + +function readFirmwareVersion(bytes) { + var major = (bytes[0] & 0xff).toString(16); + var minor = (bytes[1] & 0xff).toString(16); + return "v" + major + "." + minor; +} + +function readTslVersion(bytes) { + var major = bytes[0] & 0xff; + var minor = bytes[1] & 0xff; + return "v" + major + "." + minor; +} + +function readSerialNumber(bytes) { + var temp = []; + for (var idx = 0; idx < bytes.length; idx++) { + temp.push(("0" + (bytes[idx] & 0xff).toString(16)).slice(-2)); + } + return temp.join(""); +} + +function readLoRaWANClass(type) { + var class_map = { + 0: "Class A", + 1: "Class B", + 2: "Class C", + 3: "Class CtoB", + }; + return getValue(class_map, type); +} + +function readResetEvent(status) { + var status_map = { 0: "normal", 1: "reset" }; + return getValue(status_map, status); +} + +function readDeviceStatus(status) { + var status_map = { 0: "off", 1: "on" }; + return getValue(status_map, status); +} + +function readLeakageStatus(status) { + var status_map = { 0: "normal", 1: "leak" }; + return getValue(status_map, status); +} + +function readYesNoStatus(status) { + var status_map = { 0: "no", 1: "yes" }; + return getValue(status_map, status); +} + +function readEnableStatus(status) { + var status_map = { 0: "disable", 1: "enable" }; + return getValue(status_map, status); +} + +function readD2DCommand(bytes) { + return ("0" + (bytes[1] & 0xff).toString(16)).slice(-2) + ("0" + (bytes[0] & 0xff).toString(16)).slice(-2); +} + +/* eslint-disable */ +function readUInt8(bytes) { + return bytes & 0xff; +} + +function readInt8(bytes) { + var ref = readUInt8(bytes); + return ref > 0x7f ? ref - 0x100 : ref; +} + +function readUInt16LE(bytes) { + var value = (bytes[1] << 8) + bytes[0]; + return value & 0xffff; +} + +function readInt16LE(bytes) { + var ref = readUInt16LE(bytes); + return ref > 0x7fff ? ref - 0x10000 : ref; +} + +function readUInt32LE(bytes) { + var value = (bytes[3] << 24) + (bytes[2] << 16) + (bytes[1] << 8) + bytes[0]; + return (value & 0xffffffff) >>> 0; +} + +function readInt32LE(bytes) { + var ref = readUInt32LE(bytes); + return ref > 0x7fffffff ? ref - 0x100000000 : ref; +} + +function getValue(map, key) { + if (RAW_VALUE) return key; + + var value = map[key]; + if (!value) value = "unknown"; + return value; +} + +//if (!Object.assign) { + Object.defineProperty(Object, "assign", { + enumerable: false, + configurable: true, + writable: true, + value: function (target) { + "use strict"; + if (target == null) { + throw new TypeError("Cannot convert first argument to object"); + } + + var to = Object(target); + for (var i = 1; i < arguments.length; i++) { + var nextSource = arguments[i]; + if (nextSource == null) { + continue; + } + nextSource = Object(nextSource); + + var keysArray = Object.keys(Object(nextSource)); + for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) { + var nextKey = keysArray[nextIndex]; + var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey); + if (desc !== undefined && desc.enumerable) { + // concat array + if (Array.isArray(to[nextKey]) && Array.isArray(nextSource[nextKey])) { + to[nextKey] = to[nextKey].concat(nextSource[nextKey]); + } else { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }, + }); +//} + +/* ---------- downlink encoder ---------- */ +/* eslint-disable */ +// Chirpstack v4 +function encodeDownlink(input) { + var encoded = milesightDeviceEncode(input.data); + return { bytes: encoded }; +} + +// Chirpstack v3 +function Encode(fPort, obj) { + return milesightDeviceEncode(obj); +} + +// The Things Network +function Encoder(obj, port) { + return milesightDeviceEncode(obj); +} +/* eslint-enable */ + +function milesightDeviceEncode(payload) { + var encoded = []; + + if ("reboot" in payload) { + encoded = encoded.concat(reboot(payload.reboot)); + } + if ("query_device_status" in payload) { + encoded = encoded.concat(queryDeviceStatus(payload.query_device_status)); + } + if ("report_interval" in payload) { + encoded = encoded.concat(setReportInterval(payload.report_interval)); + } + if ("leakage_alarm_config" in payload) { + encoded = encoded.concat(setLeakageAlarmSettings(payload.leakage_alarm_config)); + } + if ("buzzer_enable" in payload) { + encoded = encoded.concat(setBuzzerEnable(payload.buzzer_enable)); + } + if ("find_device_enable" in payload) { + encoded = encoded.concat(findDeviceEnable(payload.find_device_enable)); + } + if ("find_device_time" in payload) { + encoded = encoded.concat(setFindDeviceTime(payload.find_device_time)); + } + if ("stop_alarming" in payload) { + encoded = encoded.concat(stopAlarming(payload.stop_alarming)); + } + if ("lora_report_settings" in payload) { + encoded = encoded.concat(setLoRaReportSettings(payload.lora_report_settings)); + } + if ("d2d_enable" in payload) { + encoded = encoded.concat(setD2DEnable(payload.d2d_enable)); + } + if ("d2d_master_config" in payload) { + for (var i = 0; i < payload.d2d_master_config.length; i++) { + encoded = encoded.concat(setD2DMasterSettings(payload.d2d_master_config[i])); + } + } + + return encoded; +} + +/** + * reboot + * @param {number} reboot values: (0: no, 1: yes) + * @example { "reboot": 1 } + */ +function reboot(reboot) { + var yes_no_map = { 0: "no", 1: "yes" }; + var reboot_values = getValues(yes_no_map); + if (reboot_values.indexOf(reboot) === -1) { + throw new Error("reboot must be one of " + reboot_values.join(", ")); + } + + if (getEncodeValue(yes_no_map, reboot) === 0) { + return []; + } + return [0xff, 0x10, 0xff]; +} + +/** + * query device status + * @param {number} query_device_status values: (0: no, 1: yes) + * @example { "query_device_status": 1 } + */ +function queryDeviceStatus(query_device_status) { + var yes_no_map = { 0: "no", 1: "yes" }; + var yes_no_values = getValues(yes_no_map); + if (yes_no_values.indexOf(query_device_status) === -1) { + throw new Error("query_device_status must be one of " + yes_no_values.join(", ")); + } + + if (getEncodeValue(yes_no_map, query_device_status) === 0) { + return []; + } + return [0xff, 0x28, 0xff]; +} + +/** + * set report interval + * @param {number} report_interval unit: second, range: [60, 64800] + * @example { "report_interval": 300 } + */ +function setReportInterval(report_interval) { + if (report_interval < 60 || report_interval > 64800) { + throw new Error("report_interval must be between 60 and 64800"); + } + + var buffer = new Buffer(4); + buffer.writeUInt8(0xff); + buffer.writeUInt8(0x03); + buffer.writeUInt16LE(report_interval); + return buffer.toBytes(); +} + +/** + * set leakage alarm settings + * @param {object} leakage_alarm_config + * @param {number} leakage_alarm_config.enable values: (0: disable, 1: enable) + * @param {number} leakage_alarm_config.alarm_interval unit: second, range: [60, 64800] + * @param {number} leakage_alarm_config.alarm_count unit: count, range: [2, 1000] + * @example { "leakage_alarm_config": { "enable": 1, "alarm_interval": 60, "alarm_count": 2 } } + */ +function setLeakageAlarmSettings(leakage_alarm_config) { + var enable = leakage_alarm_config.enable; + var alarm_interval = leakage_alarm_config.alarm_interval; + var alarm_count = leakage_alarm_config.alarm_count; + + var enable_map = { 0: "disable", 1: "enable" }; + var enable_values = getValues(enable_map); + if (enable_values.indexOf(enable) === -1) { + throw new Error("leakage_alarm_config.enable must be one of " + enable_values.join(", ")); + } + if (alarm_interval < 60 || alarm_interval > 64800) { + throw new Error("leakage_alarm_config.alarm_interval must be between 60 and 64800"); + } + if (alarm_count < 2 || alarm_count > 1000) { + throw new Error("leakage_alarm_config.alarm_count must be between 2 and 1000"); + } + + var buffer = new Buffer(7); + buffer.writeUInt8(0xff); + buffer.writeUInt8(0x7e); + buffer.writeUInt8(getEncodeValue(enable_map, enable)); + buffer.writeUInt16LE(alarm_interval); + buffer.writeUInt16LE(alarm_count); + return buffer.toBytes(); +} + +/** + * set buzzer enable + * @param {number} buzzer_enable values: (0: disable, 1: enable) + * @example { "buzzer_enable": 1 } + */ +function setBuzzerEnable(buzzer_enable) { + var enable_map = { 0: "disable", 1: "enable" }; + var enable_values = getValues(enable_map); + if (enable_values.indexOf(buzzer_enable) === -1) { + throw new Error("buzzer_enable must be one of " + enable_values.join(", ")); + } + + var buffer = new Buffer(3); + buffer.writeUInt8(0xff); + buffer.writeUInt8(0x3e); + buffer.writeUInt8(getEncodeValue(enable_map, buzzer_enable)); + return buffer.toBytes(); +} + +/** + * set find device enable + * @param {number} find_device_enable values: (0: disable, 1: enable) + * @example { "find_device_enable": 1 } + */ +function findDeviceEnable(find_device_enable) { + var enable_map = { 0: "disable", 1: "enable" }; + var enable_values = getValues(enable_map); + if (enable_values.indexOf(find_device_enable) === -1) { + throw new Error("find_device_enable must be one of " + enable_values.join(", ")); + } + + var buffer = new Buffer(3); + buffer.writeUInt8(0xff); + buffer.writeUInt8(0x7f); + buffer.writeUInt8(getEncodeValue(enable_map, find_device_enable)); + return buffer.toBytes(); +} + +/** + * set find device time + * @param {number} find_device_time unit: second, range: [60, 64800] + * @example { "find_device_time": 10 } + */ +function setFindDeviceTime(find_device_time) { + if (find_device_time < 60 || find_device_time > 64800) { + throw new Error("find_device_time must be between 60 and 64800"); + } + + var buffer = new Buffer(4); + buffer.writeUInt8(0xff); + buffer.writeUInt8(0x80); + buffer.writeUInt16LE(find_device_time); + return buffer.toBytes(); +} + +/** + * stop alarming + * @param {number} stop_alarming values: (0: no, 1: yes) + * @example { "stop_alarming": 1 } + */ +function stopAlarming(stop_alarming) { + var yes_no_map = { 0: "no", 1: "yes" }; + var yes_no_values = getValues(yes_no_map); + if (yes_no_values.indexOf(stop_alarming) === -1) { + throw new Error("stop_alarming must be one of " + yes_no_values.join(", ")); + } + + if (getEncodeValue(yes_no_map, stop_alarming) === 0) { + return []; + } + return [0xff, 0x3d, 0xff]; +} + +/** + * set LoRa report settings + * @param {object} lora_report_settings + * @param {number} lora_report_settings.lora_uplink_enable values: (0: disable, 1: enable) + * @param {number} lora_report_settings.d2d_uplink_enable values: (0: disable, 1: enable) + * @example { "lora_report_settings": { "lora_uplink_enable": 1, "d2d_uplink_enable": 1 } } + */ +function setLoRaReportSettings(lora_report_settings) { + var enable_map = { 0: "disable", 1: "enable" }; + var enable_values = getValues(enable_map); + if (enable_values.indexOf(lora_report_settings.lora_uplink_enable) === -1) { + throw new Error("lora_report_settings.lora_uplink_enable must be one of " + enable_values.join(", ")); + } + if (enable_values.indexOf(lora_report_settings.d2d_uplink_enable) === -1) { + throw new Error("lora_report_settings.d2d_uplink_enable must be one of " + enable_values.join(", ")); + } + + var data = 0x00; + data |= getEncodeValue(enable_map, lora_report_settings.lora_uplink_enable) << 0; + data |= getEncodeValue(enable_map, lora_report_settings.d2d_uplink_enable) << 1; + + var buffer = new Buffer(4); + buffer.writeUInt8(0xff); + buffer.writeUInt8(0x81); + buffer.writeUInt8(0x01); // leakage alarm + buffer.writeUInt8(data); + return buffer.toBytes(); +} + +/** + * set D2D enable + * @param {number} d2d_enable values: (0: disable, 1: enable) + * @example { "d2d_enable": 1 } + */ +function setD2DEnable(d2d_enable) { + var enable_map = { 0: "disable", 1: "enable" }; + var enable_values = getValues(enable_map); + if (enable_values.indexOf(d2d_enable) === -1) { + throw new Error("d2d_enable must be one of " + enable_values.join(", ")); + } + + var buffer = new Buffer(3); + buffer.writeUInt8(0xff); + buffer.writeUInt8(0x84); + buffer.writeUInt8(getEncodeValue(enable_map, d2d_enable)); + return buffer.toBytes(); +} + +/** + * set D2D master settings + * @param {object} d2d_master_config + * @param {number} d2d_master_config.mode values: (0: normal, 1: leakage) + * @param {string} d2d_master_config.d2d_command + * @example { "d2d_master_config": { "mode": 1, "d2d_command": "0000" } } + */ +function setD2DMasterSettings(d2d_master_config) { + var mode = d2d_master_config.mode; + var d2d_command = d2d_master_config.d2d_command; + + var mode_map = { 0: "normal", 1: "leakage" }; + var mode_values = getValues(mode_map); + if (mode_values.indexOf(mode) === -1) { + throw new Error("d2d_master_config.mode must be one of " + mode_values.join(", ")); + } + + var buffer = new Buffer(5); + buffer.writeUInt8(0xff); + buffer.writeUInt8(0x83); + buffer.writeUInt8(getEncodeValue(mode_map, mode)); + buffer.writeD2DCommand(d2d_command, "0000"); + return buffer.toBytes(); +} + +function getValues(map) { + var values = []; + for (var key in map) { + values.push(RAW_VALUE ? parseInt(key) : map[key]); + } + return values; +} + +function getEncodeValue(map, value) { + if (RAW_VALUE) return value; + + for (var key in map) { + if (map[key] === value) { + return parseInt(key); + } + } + + throw new Error("not match in " + JSON.stringify(map)); +} + +function Buffer(size) { + this.buffer = new Array(size); + this.offset = 0; + + for (var i = 0; i < size; i++) { + this.buffer[i] = 0; + } +} + +Buffer.prototype._write = function (value, byteLength, isLittleEndian) { + var offset = 0; + for (var index = 0; index < byteLength; index++) { + offset = isLittleEndian ? index << 3 : (byteLength - 1 - index) << 3; + this.buffer[this.offset + index] = (value >> offset) & 0xff; + } +}; + +Buffer.prototype.writeUInt8 = function (value) { + this._write(value, 1, true); + this.offset += 1; +}; + +Buffer.prototype.writeInt8 = function (value) { + this._write(value < 0 ? value + 0x100 : value, 1, true); + this.offset += 1; +}; + +Buffer.prototype.writeUInt16LE = function (value) { + this._write(value, 2, true); + this.offset += 2; +}; + +Buffer.prototype.writeInt16LE = function (value) { + this._write(value < 0 ? value + 0x10000 : value, 2, true); + this.offset += 2; +}; + +Buffer.prototype.writeUInt24LE = function (value) { + this._write(value, 3, true); + this.offset += 3; +}; + +Buffer.prototype.writeInt24LE = function (value) { + this._write(value < 0 ? value + 0x1000000 : value, 3, true); + this.offset += 3; +}; + +Buffer.prototype.writeUInt32LE = function (value) { + this._write(value, 4, true); + this.offset += 4; +}; + +Buffer.prototype.writeInt32LE = function (value) { + this._write(value < 0 ? value + 0x100000000 : value, 4, true); + this.offset += 4; +}; + +Buffer.prototype.writeD2DCommand = function (value, defaultValue) { + if (typeof value !== "string") { + value = defaultValue; + } + if (value.length !== 4) { + throw new Error("d2d_cmd length must be 4"); + } + this.buffer[this.offset] = parseInt(value.substr(2, 2), 16); + this.buffer[this.offset + 1] = parseInt(value.substr(0, 2), 16); + this.offset += 2; +}; + +Buffer.prototype.toBytes = function () { + return this.buffer; +}; diff --git a/vendors/milesight/devices/milesight-ws303.toml b/vendors/milesight/devices/milesight-ws303.toml new file mode 100644 index 0000000..dcce4c8 --- /dev/null +++ b/vendors/milesight/devices/milesight-ws303.toml @@ -0,0 +1,18 @@ +[device] +id = "7fce7699-6b16-4d73-88ad-003120c2656f" +name = "Milesight WS303" +description = "Mini Leak Detection Sensor" + +[[device.firmware]] +version = "1.5" +profiles = [ + "AS923-1_0_3.toml", + "AU915-1_0_3.toml", + "EU868-1_0_3.toml", + "US915-1_0_3.toml", +] +codec = "ws303.js" + +[device.metadata] +product_url = "https://www.milesight.com/iot/product/lorawan-sensor/ws303" +documentation_url = "https://www.milesight.com/iot/product/lorawan-sensor/ws303" diff --git a/vendors/milesight/vendor.toml b/vendors/milesight/vendor.toml index 73461bb..cc0caaa 100644 --- a/vendors/milesight/vendor.toml +++ b/vendors/milesight/vendor.toml @@ -17,6 +17,7 @@ devices = [ "milesight-em320-th.toml", "milesight-em400-tld.toml", "milesight-em500-pt100.toml", + "milesight-ws303.toml", ] [vendor.metadata] From 190896762054297c970d8739fa862ca1789f1ff9 Mon Sep 17 00:00:00 2001 From: AkashM707 <77463115+AkashM707@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:23:51 -0700 Subject: [PATCH 2/2] Replace Buffer function-constructor with a class @brocaar found this was causing the test suite to fail and confirmed a class-based Buffer resolves it. --- vendors/milesight/codecs/ws303.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/vendors/milesight/codecs/ws303.js b/vendors/milesight/codecs/ws303.js index 3a093af..2360104 100644 --- a/vendors/milesight/codecs/ws303.js +++ b/vendors/milesight/codecs/ws303.js @@ -624,12 +624,10 @@ function getEncodeValue(map, value) { throw new Error("not match in " + JSON.stringify(map)); } -function Buffer(size) { - this.buffer = new Array(size); - this.offset = 0; - - for (var i = 0; i < size; i++) { - this.buffer[i] = 0; +class Buffer { + constructor(size) { + this.buffer = new Array(size).fill(0); + this.offset = 0; } }