From 40fd23ff1fc28635cd98a69c0ad1c981aa58d026 Mon Sep 17 00:00:00 2001 From: Phil Rae Date: Wed, 4 Mar 2026 09:33:08 +0000 Subject: [PATCH 1/5] Explicitly force the platform within the `docker pull` action of the Makefile (fixes MacOS issue). --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7fec7a1..9373196 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: test nix-start nix-test serve: - docker pull chirpstack/chirpstack-device-profiles:latest + docker pull --platform=linux/amd64 chirpstack/chirpstack-device-profiles:latest docker run --rm -u $(id -u):$(id -g) -p 8090:8090 -v '$(shell pwd):/chirpstack-device-profiles' chirpstack/chirpstack-device-profiles:latest -p /chirpstack-device-profiles build: From 098becd4719308f440854f59ae3de48b61890fad Mon Sep 17 00:00:00 2001 From: Phil Rae Date: Wed, 4 Mar 2026 09:48:33 +0000 Subject: [PATCH 2/5] Added Milesight AM102 and AM102L devices. --- vendors/milesight/codecs/am102-l.js | 366 ++++++++++++++++ vendors/milesight/codecs/am102.js | 400 ++++++++++++++++++ .../milesight/codecs/test_decode_am102-l.json | 1 + .../milesight/codecs/test_decode_am102.json | 1 + .../milesight/codecs/test_encode_am102-l.json | 1 + .../milesight/codecs/test_encode_am102.json | 1 + .../milesight/devices/milesight-am102-l.toml | 18 + .../milesight/devices/milesight-am102.toml | 18 + 8 files changed, 806 insertions(+) create mode 100644 vendors/milesight/codecs/am102-l.js create mode 100644 vendors/milesight/codecs/am102.js create mode 100644 vendors/milesight/codecs/test_decode_am102-l.json create mode 100644 vendors/milesight/codecs/test_decode_am102.json create mode 100644 vendors/milesight/codecs/test_encode_am102-l.json create mode 100644 vendors/milesight/codecs/test_encode_am102.json create mode 100644 vendors/milesight/devices/milesight-am102-l.toml create mode 100644 vendors/milesight/devices/milesight-am102.toml diff --git a/vendors/milesight/codecs/am102-l.js b/vendors/milesight/codecs/am102-l.js new file mode 100644 index 0000000..c847b2e --- /dev/null +++ b/vendors/milesight/codecs/am102-l.js @@ -0,0 +1,366 @@ +/** + * Payload Decoder + * + * Copyright 2025 Milesight IoT + * + * @product AM102L + */ +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; + } + // TEMPERATURE + else if (channel_id === 0x03 && channel_type === 0x67) { + // °C + decoded.temperature = readInt16LE(bytes.slice(i, i + 2)) / 10; + i += 2; + } + // HUMIDITY + else if (channel_id === 0x04 && channel_type === 0x68) { + decoded.humidity = readUInt8(bytes[i]) / 2; + i += 1; + } + // HISTORY DATA + else if (channel_id === 0x20 && channel_type === 0xce) { + var data = {}; + data.timestamp = readUInt32LE(bytes.slice(i, i + 4)); + data.temperature = readInt16LE(bytes.slice(i + 4, i + 6)) / 10; + data.humidity = readUInt8(bytes[i + 6]) / 2; + i += 7; + decoded.history = decoded.history || []; + decoded.history.push(data); + } + // SENSOR ENABLE + else if (channel_id === 0xff && channel_type === 0x18) { + // skip 1 byte + var data = readUInt8(bytes[i + 1]); + var sensor_bit_offset = { temperature: 0, humidity: 1 }; + decoded.sensor_enable = {}; + for (var key in sensor_bit_offset) { + decoded.sensor_enable[key] = readEnableStatus((data >> sensor_bit_offset[key]) & 0x01); + } + i += 2; + } + // 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 0x06: + decoded.temperature_alarm_config = {}; + var condition = readUInt8(bytes[offset]); + decoded.temperature_alarm_config.condition = readMathCondition(condition & 0x07); + decoded.temperature_alarm_config.threshold_min = readInt16LE(bytes.slice(offset + 1, offset + 3)) / 10; + decoded.temperature_alarm_config.threshold_max = readInt16LE(bytes.slice(offset + 3, offset + 5)) / 10; + // skip 4 bytes + offset += 9; + break; + case 0x10: + decoded.reboot = readYesNoStatus(1); + offset += 1; + break; + case 0x11: + decoded.timestamp = readUInt32LE(bytes.slice(offset, offset + 4)); + offset += 4; + break; + case 0x17: + decoded.time_zone = readTimeZone(readInt16LE(bytes.slice(offset, offset + 2))); + offset += 2; + break; + case 0x27: + decoded.clear_history = readYesNoStatus(1); + offset += 1; + break; + case 0x2f: + decoded.led_indicator_mode = readLedIndicatorStatus(bytes[offset]); + offset += 1; + break; + case 0x3a: + var num = readUInt8(bytes[offset]); + offset += 1; + for (var i = 0; i < num; i++) { + var report_schedule_config = {}; + report_schedule_config.start_time = readUInt8(bytes[offset]) / 10; + report_schedule_config.end_time = readUInt8(bytes[offset + 1]) / 10; + report_schedule_config.report_interval = readUInt16LE(bytes.slice(offset + 2, offset + 4)); + // skip 1 byte + report_schedule_config.collection_interval = readUInt8(bytes[offset + 5]); + offset += 6; + decoded.report_schedule_config = decoded.report_schedule_config || []; + decoded.report_schedule_config.push(report_schedule_config); + } + break; + case 0x3b: + decoded.time_sync_enable = readEnableStatus(bytes[offset]); + offset += 1; + break; + case 0x57: + decoded.clear_report_schedule = readYesNoStatus(1); + offset += 1; + break; + case 0x59: + decoded.reset_battery = readYesNoStatus(1); + offset += 1; + break; + case 0x68: + decoded.history_enable = readEnableStatus(bytes[offset]); + offset += 1; + break; + case 0x69: + decoded.retransmit_enable = readEnableStatus(bytes[offset]); + offset += 1; + break; + case 0x6a: + var interval_type = readUInt8(bytes[offset]); + if (interval_type === 0) { + decoded.retransmit_interval = readUInt16LE(bytes.slice(offset + 1, offset + 3)); + } else if (interval_type === 1) { + decoded.resend_interval = readUInt16LE(bytes.slice(offset + 1, offset + 3)); + } + offset += 3; + 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 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 readTimeZone(time_zone) { + var timezone_map = { "-120": "UTC-12", "-110": "UTC-11", "-100": "UTC-10", "-95": "UTC-9:30", "-90": "UTC-9", "-80": "UTC-8", "-70": "UTC-7", "-60": "UTC-6", "-50": "UTC-5", "-40": "UTC-4", "-35": "UTC-3:30", "-30": "UTC-3", "-20": "UTC-2", "-10": "UTC-1", 0: "UTC", 10: "UTC+1", 20: "UTC+2", 30: "UTC+3", 35: "UTC+3:30", 40: "UTC+4", 45: "UTC+4:30", 50: "UTC+5", 55: "UTC+5:30", 57: "UTC+5:45", 60: "UTC+6", 65: "UTC+6:30", 70: "UTC+7", 80: "UTC+8", 90: "UTC+9", 95: "UTC+9:30", 100: "UTC+10", 105: "UTC+10:30", 110: "UTC+11", 120: "UTC+12", 127: "UTC+12:45", 130: "UTC+13", 140: "UTC+14" }; + return getValue(timezone_map, time_zone); +} + +function readLedIndicatorStatus(status) { + var status_map = { 0: "off", 2: "blink" }; + return getValue(status_map, status); +} + +function readMathCondition(type) { + var condition_map = { 0: "disable", 1: "below", 2: "above", 3: "between", 4: "outside" }; + return getValue(condition_map, type); +} + +/* 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; + }, +}); +//} diff --git a/vendors/milesight/codecs/am102.js b/vendors/milesight/codecs/am102.js new file mode 100644 index 0000000..49b6c46 --- /dev/null +++ b/vendors/milesight/codecs/am102.js @@ -0,0 +1,400 @@ +/** + * Payload Decoder + * + * Copyright 2025 Milesight IoT + * + * @product AM102 + */ +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; + } + // TEMPERATURE + else if (channel_id === 0x03 && channel_type === 0x67) { + // °C + decoded.temperature = readInt16LE(bytes.slice(i, i + 2)) / 10; + i += 2; + } + // HUMIDITY + else if (channel_id === 0x04 && channel_type === 0x68) { + decoded.humidity = readUInt8(bytes[i]) / 2; + i += 1; + } + // HISTORY DATA + else if (channel_id === 0x20 && channel_type === 0xce) { + var data = {}; + data.timestamp = readUInt32LE(bytes.slice(i, i + 4)); + data.temperature = readInt16LE(bytes.slice(i + 4, i + 6)) / 10; + data.humidity = readUInt8(bytes[i + 6]) / 2; + i += 7; + decoded.history = decoded.history || []; + decoded.history.push(data); + } + // SENSOR ENABLE + else if (channel_id === 0xff && channel_type === 0x18) { + // skip 1 byte + var data = readUInt8(bytes[i + 1]); + var sensor_bit_offset = { temperature: 0, humidity: 1 }; + decoded.sensor_enable = {}; + for (var key in sensor_bit_offset) { + decoded.sensor_enable[key] = readEnableStatus((data >> sensor_bit_offset[key]) & 0x01); + } + i += 2; + } + // 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 0x06: + decoded.temperature_alarm_config = {}; + var condition = readUInt8(bytes[offset]); + decoded.temperature_alarm_config.condition = readMathCondition(condition & 0x07); + decoded.temperature_alarm_config.threshold_min = readInt16LE(bytes.slice(offset + 1, offset + 3)) / 10; + decoded.temperature_alarm_config.threshold_max = readInt16LE(bytes.slice(offset + 3, offset + 5)) / 10; + // skip 4 bytes + offset += 9; + break; + case 0x10: + decoded.reboot = readYesNoStatus(1); + offset += 1; + break; + case 0x11: + decoded.timestamp = readUInt32LE(bytes.slice(offset, offset + 4)); + offset += 4; + break; + case 0x17: + decoded.time_zone = readTimeZone(readInt16LE(bytes.slice(offset, offset + 2))); + offset += 2; + break; + case 0x27: + decoded.clear_history = readYesNoStatus(1); + offset += 1; + break; + case 0x2d: + decoded.screen_display_enable = readEnableStatus(bytes[offset]); + offset += 1; + break; + case 0x2f: + decoded.led_indicator_mode = readLedIndicatorStatus(bytes[offset]); + offset += 1; + break; + case 0x3a: + var num = readUInt8(bytes[offset]); + offset += 1; + for (var i = 0; i < num; i++) { + var report_schedule_config = {}; + report_schedule_config.start_time = readUInt8(bytes[offset]) / 10; + report_schedule_config.end_time = readUInt8(bytes[offset + 1]) / 10; + report_schedule_config.report_interval = readUInt16LE(bytes.slice(offset + 2, offset + 4)); + // skip 1 byte + report_schedule_config.collection_interval = readUInt8(bytes[offset + 5]); + offset += 6; + decoded.report_schedule_config = decoded.report_schedule_config || []; + decoded.report_schedule_config.push(report_schedule_config); + } + break; + case 0x3b: + decoded.time_sync_enable = readEnableStatus(bytes[offset]); + offset += 1; + break; + case 0x56: + decoded.screen_intelligent_enable = readEnableStatus(bytes[offset]); + offset += 1; + break; + case 0x57: + decoded.clear_report_schedule = readYesNoStatus(1); + offset += 1; + break; + case 0x59: + decoded.reset_battery = readYesNoStatus(1); + offset += 1; + break; + case 0x5a: + decoded.screen_refresh_interval = readUInt16LE(bytes.slice(offset, offset + 2)); + offset += 2; + break; + case 0x68: + decoded.history_enable = readEnableStatus(bytes[offset]); + offset += 1; + break; + case 0x69: + decoded.retransmit_enable = readEnableStatus(bytes[offset]); + offset += 1; + break; + case 0x6a: + var interval_type = readUInt8(bytes[offset]); + if (interval_type === 0) { + decoded.retransmit_interval = readUInt16LE(bytes.slice(offset + 1, offset + 3)); + } else if (interval_type === 1) { + decoded.resend_interval = readUInt16LE(bytes.slice(offset + 1, offset + 3)); + } + offset += 3; + break; + case 0x75: + decoded.hibernate_config = {}; + decoded.hibernate_config.enable = readEnableStatus(bytes[offset]); + decoded.hibernate_config.lora_uplink_enable = readEnableStatus(bytes[offset + 1]); + decoded.hibernate_config.start_time = readUInt16LE(bytes.slice(offset + 2, offset + 4)); + decoded.hibernate_config.end_time = readUInt16LE(bytes.slice(offset + 4, offset + 6)); + decoded.hibernate_config.weekdays = {}; + var data = readUInt8(bytes[offset + 6]); + var weekday_bit_offset = { monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6, sunday: 7 }; + for (var key in weekday_bit_offset) { + decoded.hibernate_config.weekdays[key] = readEnableStatus((data >> weekday_bit_offset[key]) & 0x01); + } + offset += 7; + break; + case 0x85: + decoded.screen_display_time_enable = readEnableStatus(bytes[offset]); + offset += 1; + break; + case 0x86: + decoded.screen_last_refresh_interval = readUInt8(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 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 readTimeZone(time_zone) { + var timezone_map = { "-120": "UTC-12", "-110": "UTC-11", "-100": "UTC-10", "-95": "UTC-9:30", "-90": "UTC-9", "-80": "UTC-8", "-70": "UTC-7", "-60": "UTC-6", "-50": "UTC-5", "-40": "UTC-4", "-35": "UTC-3:30", "-30": "UTC-3", "-20": "UTC-2", "-10": "UTC-1", 0: "UTC", 10: "UTC+1", 20: "UTC+2", 30: "UTC+3", 35: "UTC+3:30", 40: "UTC+4", 45: "UTC+4:30", 50: "UTC+5", 55: "UTC+5:30", 57: "UTC+5:45", 60: "UTC+6", 65: "UTC+6:30", 70: "UTC+7", 80: "UTC+8", 90: "UTC+9", 95: "UTC+9:30", 100: "UTC+10", 105: "UTC+10:30", 110: "UTC+11", 120: "UTC+12", 127: "UTC+12:45", 130: "UTC+13", 140: "UTC+14" }; + return getValue(timezone_map, time_zone); +} + +function readLedIndicatorStatus(status) { + var status_map = { 0: "off", 2: "blink" }; + return getValue(status_map, status); +} + +function readMathCondition(type) { + var condition_map = { 0: "disable", 1: "below", 2: "above", 3: "between", 4: "outside" }; + return getValue(condition_map, type); +} + +/* 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; + }, +}); +//} diff --git a/vendors/milesight/codecs/test_decode_am102-l.json b/vendors/milesight/codecs/test_decode_am102-l.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/milesight/codecs/test_decode_am102-l.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/milesight/codecs/test_decode_am102.json b/vendors/milesight/codecs/test_decode_am102.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/milesight/codecs/test_decode_am102.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/milesight/codecs/test_encode_am102-l.json b/vendors/milesight/codecs/test_encode_am102-l.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/milesight/codecs/test_encode_am102-l.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/milesight/codecs/test_encode_am102.json b/vendors/milesight/codecs/test_encode_am102.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/milesight/codecs/test_encode_am102.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/milesight/devices/milesight-am102-l.toml b/vendors/milesight/devices/milesight-am102-l.toml new file mode 100644 index 0000000..51f3c0d --- /dev/null +++ b/vendors/milesight/devices/milesight-am102-l.toml @@ -0,0 +1,18 @@ +[device] +id = "45f48720-e4f4-401c-9681-ba063f6c978e" +name = "Milesight AM102L" +description = "2-in-1 IAQ Sensor (Temperature & Humidity)" + +[[device.firmware]] +version = "1.3" +profiles = [ + "AS923-1_0_3.toml", + "AU915-1_0_3.toml", + "EU868-1_0_3.toml", + "US915-1_0_3.toml", +] +codec = "am102-l.js" + +[device.metadata] +product_url = "https://www.milesight.com/iot/product/lorawan-sensor/am102" +documentation_url = "https://www.milesight.com/iot/product/lorawan-sensor/am102" diff --git a/vendors/milesight/devices/milesight-am102.toml b/vendors/milesight/devices/milesight-am102.toml new file mode 100644 index 0000000..d06a614 --- /dev/null +++ b/vendors/milesight/devices/milesight-am102.toml @@ -0,0 +1,18 @@ +[device] +id = "b7bfeca6-1fba-4c72-b8d0-30f5bf143d96" +name = "Milesight AM102" +description = "2-in-1 IAQ Sensor (Temperature & Humidity)" + +[[device.firmware]] +version = "1.3" +profiles = [ + "AS923-1_0_3.toml", + "AU915-1_0_3.toml", + "EU868-1_0_3.toml", + "US915-1_0_3.toml", +] +codec = "am102.js" + +[device.metadata] +product_url = "https://www.milesight.com/iot/product/lorawan-sensor/am102" +documentation_url = "https://www.milesight.com/iot/product/lorawan-sensor/am102" From 7ac1f66f1a57420a105ccf7927850700b2c44e56 Mon Sep 17 00:00:00 2001 From: Phil Rae Date: Tue, 24 Mar 2026 22:14:52 +0000 Subject: [PATCH 3/5] Added Milesight EM400-TLD --- vendors/milesight/codecs/em400-tld.js | 356 ++++++++++++++++++ .../codecs/test_decode_em400-tld.json | 1 + .../codecs/test_encode_em400-tld.json | 1 + .../devices/milesight-em400-tld.toml | 18 + 4 files changed, 376 insertions(+) create mode 100644 vendors/milesight/codecs/em400-tld.js create mode 100644 vendors/milesight/codecs/test_decode_em400-tld.json create mode 100644 vendors/milesight/codecs/test_encode_em400-tld.json create mode 100644 vendors/milesight/devices/milesight-em400-tld.toml diff --git a/vendors/milesight/codecs/em400-tld.js b/vendors/milesight/codecs/em400-tld.js new file mode 100644 index 0000000..6ef7f47 --- /dev/null +++ b/vendors/milesight/codecs/em400-tld.js @@ -0,0 +1,356 @@ +/** + * Payload Decoder + * + * Copyright 2024 Milesight IoT + * + * @product EM400-TLD + */ +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; + } + // TEMPERATURE + else if (channel_id === 0x03 && channel_type === 0x67) { + decoded.temperature = readInt16LE(bytes.slice(i, i + 2)) / 10; + i += 2; + } + // DISTANCE + else if (channel_id === 0x04 && channel_type === 0x82) { + decoded.distance = readUInt16LE(bytes.slice(i, i + 2)); + i += 2; + } + // POSITION + else if (channel_id === 0x05 && channel_type === 0x00) { + decoded.position = readPositionType(bytes[i]); + i += 1; + } + // TEMPERATURE WITH ABNORMAL + else if (channel_id === 0x83 && channel_type === 0x67) { + decoded.temperature = readInt16LE(bytes.slice(i, i + 2)) / 10; + decoded.temperature_alarm = readAlarmType(bytes[i + 2]); + i += 3; + } + // DISTANCE WITH ALARMING + else if (channel_id === 0x84 && channel_type === 0x82) { + decoded.distance = readUInt16LE(bytes.slice(i, i + 2)); + decoded.distance_alarm = readAlarmType(bytes[i + 2]); + i += 3; + } + // 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 0x02: + decoded.collection_interval = readUInt16LE(bytes.slice(offset, offset + 2)); + offset += 2; + break; + case 0x03: + decoded.report_interval = readUInt16LE(bytes.slice(offset, offset + 2)); + offset += 2; + break; + case 0x06: + var value = readUInt8(bytes[offset]); + var alarm_type = (value >>> 3) & 0x07; + var condition_type = value & 0x07; + var config = {}; + config.condition = readMathConditionType(condition_type); + config.alarm_release_enable = readEnableStatus((value >>> 7) & 0x01); + config.threshold_min = readUInt16LE(bytes.slice(offset + 1, offset + 3)); + config.threshold_max = readUInt16LE(bytes.slice(offset + 3, offset + 5)); + // skip 4 bytes + offset += 9; + if (alarm_type === 1) { + decoded.standard_mode_alarm_config = config; + } else if (alarm_type === 2) { + decoded.bin_mode_alarm_config = config; + } + break; + case 0x10: + decoded.reboot = readYesNoStatus(1); + offset += 1; + break; + case 0x13: + decoded.install_height_enable = readEnableStatus(bytes[offset]); + offset += 1; + break; + case 0x1c: + decoded.recollection_config = {}; + decoded.recollection_config.counts = bytes[offset]; + decoded.recollection_config.interval = bytes[offset + 1]; + offset += 2; + break; + case 0x28: + decoded.query_device_status = readYesNoStatus(1); + offset += 1; + break; + case 0x3e: + decoded.tilt_linkage_distance_enable = readEnableStatus(bytes[offset]); + offset += 1; + break; + case 0x4a: + decoded.sync_time = readYesNoStatus(1); + offset += 1; + break; + case 0x56: + decoded.tof_detection_enable = readEnableStatus(bytes[offset]); + offset += 1; + break; + case 0x70: + decoded.people_existing_height = readUInt16LE(bytes.slice(offset, offset + 2)); + offset += 2; + break; + case 0x71: + decoded.working_mode = readWorkingMode(bytes[offset]); + offset += 1; + break; + case 0x77: + decoded.install_height = readUInt16LE(bytes.slice(offset, offset + 2)); + offset += 2; + 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 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 readPositionType(type) { + var type_map = { 0: "normal", 1: "tilt" }; + return getValue(type_map, type); +} + +function readAlarmType(type) { + var type_map = { 0: "threshold_alarm_release", 1: "threshold_alarm" }; + return getValue(type_map, type); +} + +function readMathConditionType(type) { + var type_map = { 0: "disable", 1: "below", 2: "above", 3: "between", 4: "outside" }; + return getValue(type_map, type); +} + +function readWorkingMode(type) { + var type_map = { 0: "standard", 1: "bin" }; + return getValue(type_map, type); +} + +/* 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; + }, + }); +//} diff --git a/vendors/milesight/codecs/test_decode_em400-tld.json b/vendors/milesight/codecs/test_decode_em400-tld.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/milesight/codecs/test_decode_em400-tld.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/milesight/codecs/test_encode_em400-tld.json b/vendors/milesight/codecs/test_encode_em400-tld.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/milesight/codecs/test_encode_em400-tld.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/milesight/devices/milesight-em400-tld.toml b/vendors/milesight/devices/milesight-em400-tld.toml new file mode 100644 index 0000000..bc6e459 --- /dev/null +++ b/vendors/milesight/devices/milesight-em400-tld.toml @@ -0,0 +1,18 @@ +[device] +id = "92b180ce-3d9e-4f10-a617-d441a46a509e" +name = "Milesight EM400-TLD" +description = "ToF Laser Distance Sensor" + +[[device.firmware]] +version = "1.2" +profiles = [ + "EU868-1_0_3.toml", + "US915-1_0_3.toml", + "AU915-1_0_3.toml", + "AS923-1_0_3.toml", +] +codec = "em400-tld.js" + +[device.metadata] +product_url = "https://www.milesight.com/iot/product/lorawan-sensor/em400-tld" +documentation_url = "https://www.milesight.com/iot/product/lorawan-sensor/em400-tld" From 0990586ac33d8e4973e90f365024da91a8b8c013 Mon Sep 17 00:00:00 2001 From: Phil Rae Date: Tue, 24 Mar 2026 22:20:58 +0000 Subject: [PATCH 4/5] Updating vendor.toml. --- vendors/milesight/vendor.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/vendors/milesight/vendor.toml b/vendors/milesight/vendor.toml index 6929c60..7bd3aad 100644 --- a/vendors/milesight/vendor.toml +++ b/vendors/milesight/vendor.toml @@ -4,14 +4,17 @@ name = "Milesight" vendor_id = 601 ouis = ["24e124"] devices = [ + "milesight-am102-l.toml", + "milesight-am102.toml", + "milesight-em300-cl.toml", "milesight-em300-di.toml", "milesight-em300-mcs.toml", - "milesight-em300-th.toml", - "milesight-em300-cl.toml", "milesight-em300-mld.toml", "milesight-em300-sld-zld.toml", + "milesight-em300-th.toml", "milesight-em310-tilt.toml", "milesight-em320-th.toml", + "milesight-em400-tld.toml", ] [vendor.metadata] From 24a0a5b876641b375d2c001bf925601f9da6ff11 Mon Sep 17 00:00:00 2001 From: Phil Rae Date: Wed, 25 Mar 2026 09:33:45 +0000 Subject: [PATCH 5/5] Added MOKOsmart vendor. Added MOKOSmart LW005-MP Smart Plug. --- vendors/mokosmart/codecs/lw005-mp.js | 283 ++++++++++++++++++ .../codecs/test_decode_lw005-mp.json | 1 + .../codecs/test_encode_lw005-mp.json | 1 + vendors/mokosmart/devices/lw005-mp.toml | 18 ++ vendors/mokosmart/profiles/AS923-1_0_3.toml | 25 ++ vendors/mokosmart/profiles/AU915-1_0_3.toml | 25 ++ vendors/mokosmart/profiles/EU868-1_0_3.toml | 25 ++ vendors/mokosmart/profiles/US915-1_0_3.toml | 25 ++ vendors/mokosmart/vendor.toml | 11 + 9 files changed, 414 insertions(+) create mode 100644 vendors/mokosmart/codecs/lw005-mp.js create mode 100644 vendors/mokosmart/codecs/test_decode_lw005-mp.json create mode 100644 vendors/mokosmart/codecs/test_encode_lw005-mp.json create mode 100644 vendors/mokosmart/devices/lw005-mp.toml create mode 100644 vendors/mokosmart/profiles/AS923-1_0_3.toml create mode 100644 vendors/mokosmart/profiles/AU915-1_0_3.toml create mode 100644 vendors/mokosmart/profiles/EU868-1_0_3.toml create mode 100644 vendors/mokosmart/profiles/US915-1_0_3.toml create mode 100644 vendors/mokosmart/vendor.toml diff --git a/vendors/mokosmart/codecs/lw005-mp.js b/vendors/mokosmart/codecs/lw005-mp.js new file mode 100644 index 0000000..2a37865 --- /dev/null +++ b/vendors/mokosmart/codecs/lw005-mp.js @@ -0,0 +1,283 @@ +//LW005-MP Payload Decoder rule +//Creation time:2022-07-20 +//Creator:yujiahang +//Suitable firmware versions:LW005-MP V1.X.X +//Programming languages:Javascript +//Suitable platforms:Chirpstack +var payloadTypeArray = [ + "Switch" + , "Electrical" + , "Electrical" + , "Energy" + , "Over-voltage" + , "Sag-voltage" + , "Over-current" + , "Over-load" + , "Load state" + , "Countdown" +]; +// Decode uplink function. +// +// Input is an object with the following fields: +// - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0] +// - fPort = Uplink fPort. +// - variables = Object containing the configured device variables. +// +// Output must be an object with the following fields: +// - data = Object representing the decoded payload. +function decodeUplink(input) { + var bytes = input.bytes; + var fPort = input.fPort; + var deviceInfo = {}; + var data = {}; + if (fPort == 0) { + deviceInfo.data = data; + return deviceInfo; + } + + data.port = fPort; + data.hex_format_payload = bytesToHexString(bytes, 0, bytes.length); + data.payload_type = payloadTypeArray[fPort - 5]; + if (command_format_check(bytes, fPort) == false) { + data.result = "Format wrong"; + deviceInfo.data = data; + return deviceInfo; + } + var timestamp = bytesToInt(bytes, 0, 4); + if (timestamp < 1000000000){ + data.result = "Timestamp wrong"; + deviceInfo.data = data; + return deviceInfo; + } + data.time = parse_time(timestamp, bytes[4] * 0.5); + data.timestamp = timestamp; + data.timezone = signedHexToInt(bytesToHexString(bytes, 4, 1)) / 2; + switch (fPort) { + case 5: + data.ac_output_state = bytes[5] == 1 ? "ON" : "OFF"; + data.plug_load_status = bytes[6] == 1 ? "There is a load on the plug" : "No load on the plug"; + break; + case 6: + data.instantaneous_voltage = bytesToInt(bytes, 5, 2) / 10 + "V"; + data.instantaneous_current = bytesToInt(bytes, 7, 2) / 1000 + "A"; + data.instantaneous_current_frequency = bytesToInt(bytes, 9, 2) / 1000 + "HZ"; + break; + case 7: + data.instantaneous_active_power = bytesToInt(bytes, 5, 4) / 10 + "W"; + data.instantaneous_power_factor = (bytes[9] & 0xFF) + "%"; + break; + case 8: + data.total_energy = Number(bytesToInt(bytes, 5, 4) / 3200).toFixed(2) + "KWH"; + data.energy_of_last_hour = Number(bytesToInt(bytes, 9, 2) / 3200).toFixed(2) + "KWH"; + break; + case 9: + data.over_voltage_state = bytes[5]; + data.current_instantaneous_voltage = bytesToInt(bytes, 6, 2) / 10 + "V"; + data.over_voltage_threshold = bytesToInt(bytes, 8, 2) / 10 + "V"; + break; + case 10: + data.sag_voltage_state = bytes[5]; + data.current_instantaneous_voltage = bytesToInt(bytes, 6, 2) / 10 + "V"; + data.sag_voltage_threshold = bytesToInt(bytes, 8, 2) / 10 + "V"; + break; + case 11: + data.over_current_state = bytes[5]; + data.current_instantaneous_current = parse_int16(bytesToInt(bytes, 6, 2)) / 1000 + "A"; + data.over_current_threshold = bytesToInt(bytes, 8, 2) / 1000 + "A"; + break; + case 12: + data.over_load_state = bytes[5]; + data.current_instantaneous_power = parse_int24(bytesToInt(bytes, 6, 3)) / 10 + "W"; + data.over_load_threshold = bytesToInt(bytes, 9, 2) / 10 + "W"; + break; + case 13: + data.load_change_state = bytes[5] == 1 ? "load starts working" : "load starts stop"; + break; + case 14: + data.ac_output_state_after_countdown = bytes[5] == 1 ? "ON" : "OFF";; + data.remaining_time_of_countdown_process = bytesToInt(bytes, 6, 4) + "s"; + break; + default: + break; + } + deviceInfo.data = data; + return deviceInfo; +} + +function command_format_check(bytes, port) { + if (port >= 1 || port <= 4) { + return true; + } + switch (port) { + case 5: + if (bytes.length === 7) + return true; + break; + + case 6: + if (bytes.length === 11) + return true; + break; + + case 7: + if (bytes.length === 10) + return true; + break; + + case 8: + if (bytes.length === 11) + return true; + break; + + case 9: + if (bytes.length === 10) + return true; + break; + + case 10: + if (bytes.length === 10) + return true; + break; + + case 11: + if (bytes.length === 10) + return true; + break; + + case 12: + if (bytes.length === 11) + return true; + break; + + case 13: + if (bytes.length === 6) + return true; + break; + + case 14: + if (bytes.length === 10) + return true; + break; + + default: + break; + } + + return false; +} + +function bytesToHexString(bytes, start, len) { + var char = []; + for (var i = 0; i < len; i++) { + var data = bytes[start + i].toString(16); + var dataHexStr = ("0x" + data) < 0x10 ? ("0" + data) : data; + char.push(dataHexStr); + } + return char.join(""); +} + +function parse_int16(num) { + if (num & 0x8000) + return (num - 0x10000); + else + return num; +} + +function parse_int24(num) { + if (num & 0x800000) + return (num - 0x1000000); + else + return num; +} + + +function parse_time(timestamp, timezone) { + timezone = timezone > 64 ? timezone - 128 : timezone; + timestamp = timestamp + timezone * 3600; + if (timestamp < 0) { + timestamp = 0; + } + + var d = new Date(timestamp * 1000); + //d.setUTCSeconds(1660202724); + + var time_str = ""; + time_str += d.getUTCFullYear(); + time_str += "-"; + time_str += formatNumber(d.getUTCMonth() + 1); + time_str += "-"; + time_str += formatNumber(d.getUTCDate()); + time_str += " "; + + time_str += formatNumber(d.getUTCHours()); + time_str += ":"; + time_str += formatNumber(d.getUTCMinutes()); + time_str += ":"; + time_str += formatNumber(d.getUTCSeconds()); + + return time_str; +} + +function formatNumber(number) { + return number < 10 ? "0" + number : number; +} + +function signedHexToInt(hexStr) { + var twoStr = parseInt(hexStr, 16).toString(2); // 将十六转十进制,再转2进制 + var bitNum = hexStr.length * 4; // 1个字节 = 8bit ,0xff 一个 "f"就是4位 + if (twoStr.length < bitNum) { + while (twoStr.length < bitNum) { + twoStr = "0" + twoStr; + } + } + if (twoStr.substring(0, 1) == "0") { + // 正数 + twoStr = parseInt(twoStr, 2); // 二进制转十进制 + return twoStr; + } + // 负数 + var twoStr_unsign = ""; + twoStr = parseInt(twoStr, 2) - 1; // 补码:(负数)反码+1,符号位不变;相对十进制来说也是 +1,但这里是负数,+1就是绝对值数据-1 + twoStr = twoStr.toString(2); + twoStr_unsign = twoStr.substring(1, bitNum); // 舍弃首位(符号位) + // 去除首字符,将0转为1,将1转为0 反码 + twoStr_unsign = twoStr_unsign.replace(/0/g, "z"); + twoStr_unsign = twoStr_unsign.replace(/1/g, "0"); + twoStr_unsign = twoStr_unsign.replace(/z/g, "1"); + twoStr = -parseInt(twoStr_unsign, 2); + return twoStr; +} + +function bytesToInt(bytes, start, len) { + var value = 0; + for (var i = 0; i < len; i++) { + var m = ((len - 1) - i) * 8; + value = value | bytes[start + i] << m; + } + // var value = ((bytes[start] << 24) | (bytes[start + 1] << 16) | (bytes[start + 2] << 8) | (bytes[start + 3])); + return value; +} + +// function getData(hex) { +// var length = hex.length; +// var datas = []; +// for (var i = 0; i < length; i += 2) { +// var start = i; +// var end = i + 2; +// var data = parseInt("0x" + hex.substring(start, end)); +// datas.push(data); +// } +// return datas; +// } + +// var input = {}; +// input.fPort = 8; +// input.bytes = getData("0000000010000216eb0000"); +// console.log(decodeUplink(input)); + +//deviceInfo = Decoder([0x62, 0xF4, 0xBA, 0xDA, 0x10, 0x00, 0x00], 5); +//deviceInfo = Decoder([0x61, 0xAD, 0x6C, 0x62, 0x10, 0x09, 0x2D, 0xF2, 0x0F, 0xC3, 0x65], 6); +//deviceInfo = Decoder([0x61, 0xAD, 0x6C, 0x62, 0x10, 0x00, 0x00, 0x78, 0xF9, 0x26], 7); +// deviceInfo = Decoder([0x61, 0xAD, 0x6C, 0x44, 0x10, 0x00, 0xB4, 0x1F, 0x3F, 0x01, 0x67], 8); +// console.log(deviceInfo); +// console.log(parse_time(1660202724, 0)); diff --git a/vendors/mokosmart/codecs/test_decode_lw005-mp.json b/vendors/mokosmart/codecs/test_decode_lw005-mp.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/mokosmart/codecs/test_decode_lw005-mp.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/mokosmart/codecs/test_encode_lw005-mp.json b/vendors/mokosmart/codecs/test_encode_lw005-mp.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/mokosmart/codecs/test_encode_lw005-mp.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/mokosmart/devices/lw005-mp.toml b/vendors/mokosmart/devices/lw005-mp.toml new file mode 100644 index 0000000..69a8104 --- /dev/null +++ b/vendors/mokosmart/devices/lw005-mp.toml @@ -0,0 +1,18 @@ +[device] +id = "f2b69399-969c-45e6-bef4-898e74332fd2" +name = "LW005-MP" +description = "Smart plug with power and energy monitoring" + +[[device.firmware]] +version = "1.0" +profiles = [ + "EU868-1_0_3.toml", + "AU915-1_0_3.toml", + "AS923-1_0_3.toml", + "US915-1_0_3.toml", +] +codec = "lw005-mp.js" + +[device.metadata] +product_url = "https://www.mokosmart.com/lorawan-meter-plug-lw005-mp/" +documentation_url = "https://www.mokosmart.com/lorawan-meter-plug-lw005-mp/" diff --git a/vendors/mokosmart/profiles/AS923-1_0_3.toml b/vendors/mokosmart/profiles/AS923-1_0_3.toml new file mode 100644 index 0000000..8f86ca7 --- /dev/null +++ b/vendors/mokosmart/profiles/AS923-1_0_3.toml @@ -0,0 +1,25 @@ +[profile] +id = "a36dade4-89e5-4aee-9c16-a2d7d8403c12" +vendor_profile_id = 0 +region = "AS923" +mac_version = "1.0.3" +reg_params_revision = "A" +supports_otaa = true +supports_class_b = false +supports_class_c = false +max_eirp = 16 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 0 diff --git a/vendors/mokosmart/profiles/AU915-1_0_3.toml b/vendors/mokosmart/profiles/AU915-1_0_3.toml new file mode 100644 index 0000000..4623469 --- /dev/null +++ b/vendors/mokosmart/profiles/AU915-1_0_3.toml @@ -0,0 +1,25 @@ +[profile] +id = "9067cb54-7f52-4f37-85d1-851c1e96c4e6" +vendor_profile_id = 0 +region = "AU915" +mac_version = "1.0.3" +reg_params_revision = "A" +supports_otaa = true +supports_class_b = false +supports_class_c = false +max_eirp = 30 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 0 diff --git a/vendors/mokosmart/profiles/EU868-1_0_3.toml b/vendors/mokosmart/profiles/EU868-1_0_3.toml new file mode 100644 index 0000000..3992f5e --- /dev/null +++ b/vendors/mokosmart/profiles/EU868-1_0_3.toml @@ -0,0 +1,25 @@ +[profile] +id = "03d8d816-bf65-42d1-bccf-b3a5451f41b0" +vendor_profile_id = 0 +region = "EU868" +mac_version = "1.0.3" +reg_params_revision = "A" +supports_otaa = true +supports_class_b = false +supports_class_c = false +max_eirp = 16 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 0 diff --git a/vendors/mokosmart/profiles/US915-1_0_3.toml b/vendors/mokosmart/profiles/US915-1_0_3.toml new file mode 100644 index 0000000..07e8a91 --- /dev/null +++ b/vendors/mokosmart/profiles/US915-1_0_3.toml @@ -0,0 +1,25 @@ +[profile] +id = "ce6b613c-8e85-4d36-b931-d42ee0069edd" +vendor_profile_id = 0 +region = "US915" +mac_version = "1.0.3" +reg_params_revision = "A" +supports_otaa = true +supports_class_b = false +supports_class_c = false +max_eirp = 30 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 0 diff --git a/vendors/mokosmart/vendor.toml b/vendors/mokosmart/vendor.toml new file mode 100644 index 0000000..b9bb58e --- /dev/null +++ b/vendors/mokosmart/vendor.toml @@ -0,0 +1,11 @@ +[vendor] +id = "7af20d70-8b33-4eec-8d9d-d0c96c3b1863" +name = "MOKOSmart" +vendor_id = 786 +ouis = [] +devices = [ + "lw005-mp.toml", +] + +[vendor.metadata] +homepage = "https://www.mokosmart.com/"