From 6071e43500ba38ce7b1797ec48e98177a7801f41 Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Wed, 15 Jan 2025 15:20:20 +0200 Subject: [PATCH] Added HTTP, ChirpStack, Loriot, ThingsStackCommunity and ThingsStackIndustries integrations for EM300-MCS --- .../ChirpStack/uplink/converter.json | 39 +++++++++ .../EM300-MCS/ChirpStack/uplink/metadata.json | 4 + .../EM300-MCS/ChirpStack/uplink/payload.json | 48 +++++++++++ .../EM300-MCS/ChirpStack/uplink/result.json | 28 +++++++ .../EM300-MCS/HTTP/uplink/converter.json | 29 +++++++ .../EM300-MCS/HTTP/uplink/metadata.json | 4 + .../EM300-MCS/HTTP/uplink/payload.json | 31 +++++++ .../EM300-MCS/HTTP/uplink/result.json | 21 +++++ .../EM300-MCS/LORIOT/uplink/converter.json | 29 +++++++ .../EM300-MCS/LORIOT/uplink/metadata.json | 4 + .../EM300-MCS/LORIOT/uplink/payload.json | 17 ++++ .../EM300-MCS/LORIOT/uplink/result.json | 18 ++++ .../uplink/converter.json | 39 +++++++++ .../ThingsStackCommunity/uplink/metadata.json | 4 + .../ThingsStackCommunity/uplink/payload.json | 54 ++++++++++++ .../ThingsStackCommunity/uplink/result.json | 29 +++++++ .../uplink/converter.json | 40 +++++++++ .../uplink/metadata.json | 4 + .../ThingsStackIndustries/uplink/payload.json | 77 ++++++++++++++++++ .../ThingsStackIndustries/uplink/result.json | 29 +++++++ VENDORS/Milesight/EM300-MCS/guide.md | 13 +++ VENDORS/Milesight/EM300-MCS/info.json | 6 ++ VENDORS/Milesight/EM300-MCS/photo.png | Bin 0 -> 26564 bytes 23 files changed, 567 insertions(+) create mode 100644 VENDORS/Milesight/EM300-MCS/ChirpStack/uplink/converter.json create mode 100644 VENDORS/Milesight/EM300-MCS/ChirpStack/uplink/metadata.json create mode 100644 VENDORS/Milesight/EM300-MCS/ChirpStack/uplink/payload.json create mode 100644 VENDORS/Milesight/EM300-MCS/ChirpStack/uplink/result.json create mode 100644 VENDORS/Milesight/EM300-MCS/HTTP/uplink/converter.json create mode 100644 VENDORS/Milesight/EM300-MCS/HTTP/uplink/metadata.json create mode 100644 VENDORS/Milesight/EM300-MCS/HTTP/uplink/payload.json create mode 100644 VENDORS/Milesight/EM300-MCS/HTTP/uplink/result.json create mode 100644 VENDORS/Milesight/EM300-MCS/LORIOT/uplink/converter.json create mode 100644 VENDORS/Milesight/EM300-MCS/LORIOT/uplink/metadata.json create mode 100644 VENDORS/Milesight/EM300-MCS/LORIOT/uplink/payload.json create mode 100644 VENDORS/Milesight/EM300-MCS/LORIOT/uplink/result.json create mode 100644 VENDORS/Milesight/EM300-MCS/ThingsStackCommunity/uplink/converter.json create mode 100644 VENDORS/Milesight/EM300-MCS/ThingsStackCommunity/uplink/metadata.json create mode 100644 VENDORS/Milesight/EM300-MCS/ThingsStackCommunity/uplink/payload.json create mode 100644 VENDORS/Milesight/EM300-MCS/ThingsStackCommunity/uplink/result.json create mode 100644 VENDORS/Milesight/EM300-MCS/ThingsStackIndustries/uplink/converter.json create mode 100644 VENDORS/Milesight/EM300-MCS/ThingsStackIndustries/uplink/metadata.json create mode 100644 VENDORS/Milesight/EM300-MCS/ThingsStackIndustries/uplink/payload.json create mode 100644 VENDORS/Milesight/EM300-MCS/ThingsStackIndustries/uplink/result.json create mode 100644 VENDORS/Milesight/EM300-MCS/guide.md create mode 100644 VENDORS/Milesight/EM300-MCS/info.json create mode 100644 VENDORS/Milesight/EM300-MCS/photo.png diff --git a/VENDORS/Milesight/EM300-MCS/ChirpStack/uplink/converter.json b/VENDORS/Milesight/EM300-MCS/ChirpStack/uplink/converter.json new file mode 100644 index 00000000..9759c886 --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/ChirpStack/uplink/converter.json @@ -0,0 +1,39 @@ +{ + "name": "ChirpStack Uplink Decoder for EM300-MCS", + "type": "UPLINK", + "debugMode": false, + "debugSettings": { + "failuresEnabled": true, + "allEnabled": false, + "allEnabledUntil": 1733331880270 + }, + "configuration": { + "scriptLang": "TBEL", + "decoder": null, + "tbelDecoder": "var data = decodeToJson(payload);\nvar deviceName = \"EM300-MCS \" + data.deviceInfo.devEui;\nvar deviceType = \"EM300-MCS\";\nvar groupName = null; // If groupName is not null - created device will be added to the entity group with such name.\nvar customerName = null; // If customerName is not null - created devices will be assigned to customer with such name. \n\n// use assetName and assetType instead of deviceName and deviceType\n// to automatically create assets instead of devices.\n// var assetName = 'Asset A';\n// var assetType = 'building';\n\n// If you want to parse incoming data somehow, you can add your code to this function.\n// input: bytes\n// expected output:\n// {\n// \"attributes\": {\"attributeKey\": \"attributeValue\"},\n// \"telemetry\": [{\"ts\": 1...1, \"values\": {\"telemetryKey\":\"telemetryValue\"}, {\"ts\": 1...2, \"values\": {\"telemetryKey\":\"telemetryValue\"}}]\n// }\n\nfunction decodePayload(input) {\n var output = {\n attributes: {},\n telemetry: []\n };\n \n // --- Decoding code --- //\n var decoded = {};\n var fPort = data.fPort;\n var historyDataList = [];\n if(fPort == 85) {\n for(var i = 0; i < input.length - 2; ) {\n var channel_id = input[i++] & 0xff;\n var channel_type = input[i++] & 0xff;\n \n // BATTERY\n if (channel_id === 0x01 && channel_type === 0x75) {\n decoded.battery = input[i];\n i += 1;\n }\n // TEMPERATURE\n else if (channel_id === 0x03 && channel_type === 0x67) {\n // ℃\n decoded.temperature = parseBytesToInt(input, i, 2, false) / 10;\n i += 2;\n \n // ℉\n // decoded.temperature = parseBytesToInt(input, i, 2) / 10 * 1.8 + 32;\n // i +=2;\n }\n // HUMIDITY\n else if (channel_id === 0x04 && channel_type === 0x68) {\n decoded.humidity = parseBytesToInt(input, i, 1) / 2;\n i += 1;\n }\n // MAGNET STATUS\n else if (channel_id === 0x06 && channel_type === 0x00) {\n decoded.magnet_status = input[i] === 0 ? \"close\" : \"open\";\n i += 1;\n }\n else if (channel_id === 0x20 && channel_type === 0xce) {\n var magnet_status = input[i + 7] == 0 ? \"close\" : \"open\";\n var historyData = {\n ts : parseBytesToInt(input, i, 4, false) * 1000,\n values : {\n temperature: parseBytesToInt(input, i + 4, 2, false) / 10,\n humidity: parseBytesToInt(input, i + 6, 1, false) / 2,\n magnet_status: magnet_status\n }\n };\n \n historyDataList.add(historyData);\n i += 8;\n }\n }\n }\n\n output.telemetry = [{\n ts: timestamp,\n values: decoded\n }];\n \n output.telemetry.addAll(historyDataList);\n \n // --- Decoding code --- //\n return output;\n}\n\n// --- attributes and telemetry objects ---\nvar telemetry = [];\nvar attributes = {};\n// --- attributes and telemetry objects ---\n\n// --- Timestamp parsing\nvar dateString = data.time;\ntimestamp = parseDateToTimestamp(dateString);\n// --- Timestamp parsing\n\n// Passing incoming bytes to decodePayload function, to get custom decoding\nvar customDecoding = decodePayload(base64ToBytes(data.data));\n\n\nattributes.eui = data.deviceInfo.devEui;\n\n// Collecting data to result\nif (customDecoding.?telemetry.size() > 0) {\n if (customDecoding.telemetry instanceof java.util.ArrayList) {\n foreach(telemetryObj: customDecoding.telemetry) {\n if (telemetryObj.ts != null && telemetryObj.values != null) {\n telemetry.add(telemetryObj);\n }\n }\n } else {\n telemetry.putAll(customDecoding.telemetry);\n }\n}\n\nif (customDecoding.?attributes.size() > 0) {\n attributes.putAll(customDecoding.attributes);\n}\n\n// You can add some keys manually to attributes or telemetry\nattributes.eui = data.deviceInfo.?devEui;\nattributes.devAddr = data.devAddr;\nattributes.fPort = data.fPort;\nattributes.applicationId = data.deviceInfo.?applicationId;\nattributes.applicationName = data.deviceInfo.?applicationName;\nattributes.tenantId = data.deviceInfo.?tenantId;\nattributes.tenantName = data.deviceInfo.?tenantName;\nattributes.deviceProfileId = data.deviceInfo.?deviceProfileId;\nattributes.deviceProfileName = data.deviceInfo.?deviceProfileName;\nattributes.frequency = data.txInfo.?frequency;\nattributes.bandwidth = data.txInfo.?modulation.?lora.?bandwidth;\nattributes.spreadingFactor = data.txInfo.?modulation.?lora.?spreadingFactor;\nattributes.codeRate = data.txInfo.?modulation.?lora.?codeRate;\n\nif(Boolean.parseBoolean(metadata[\"includeGatewayInfo\"])) {\n var gatewayInfo = getGatewayInfo();\n var addDataToTelemetry = {};\n addDataToTelemetry.snr = gatewayInfo.snr;\n addDataToTelemetry.rssi = gatewayInfo.rssi;\n addDataToTelemetry.channel = gatewayInfo.channel;\n addDataToTelemetry.rfChain = gatewayInfo.rfChain;\n addDataToTelemetry.fCnt = data.fCnt;\n \n telemetry = processTelemetryData(telemetry, addDataToTelemetry);\n}\n\nvar result = {\n deviceName: deviceName,\n deviceType: deviceType,\n // assetName: assetName,\n // assetType: assetType,\n attributes: attributes,\n telemetry: telemetry\n};\n\naddAdditionalInfoForDeviceMsg(result, customerName, groupName);\n\nreturn result;\n\nfunction addAdditionalInfoForDeviceMsg(deviceInfo, customerName, groupName) {\n if (customerName != null) {\n deviceInfo.customerName = customerName;\n }\n if (groupName != null) {\n deviceInfo.groupName = groupName;\n }\n}\n\nfunction parseDateToTimestamp(dateString) {\n var date = new Date(dateString);\n var timestamp = date.getTime();\n \n // If we cannot parse timestamp - we will use the current timestamp\n if (timestamp == -1) {\n timestamp = Date.now();\n }\n \n return timestamp;\n}\n\nfunction getGatewayInfo() {\n var gatewayList = data.rxInfo;\n var maxRssi = Integer.MIN_VALUE;\n var gatewayInfo = {};\n \n foreach (gateway : gatewayList) {\n if(gateway.rssi > maxRssi) {\n maxRssi = gateway.rssi;\n gatewayInfo = gateway;\n }\n }\n \n return gatewayInfo;\n}\n\nfunction processTelemetryData(telemetry, addDataToTelemetry) {\n if (telemetry.size >= 1) {\n telemetry = addDataToTelemetries(telemetry, addDataToTelemetry);\n }\n else {\n telemetry.add(addDataToTelemetry);\n }\n \n return telemetry;\n}\n\nfunction addDataToTelemetries(telemetries, addDataToTelemetry) {\n foreach(telemetry : telemetries) {\n foreach(element : addDataToTelemetry.entrySet()) {\n if(!telemetry[\"values\"].keys.contains(element.key)) {\n telemetry[\"values\"][element.key] = element.value;\n }\n } \n }\n \n return telemetries;\n}", + "encoder": null, + "tbelEncoder": null, + "updateOnlyKeys": [ + "tenantId", + "tenantName", + "applicationId", + "applicationName", + "deviceProfileId", + "deviceProfileName", + "devAddr", + "fPort", + "frequency", + "bandwidth", + "spreadingFactor", + "codeRate", + "channel", + "rfChain", + "eui", + "battery" + ] + }, + "additionalInfo": { + "description": "" + }, + "edgeTemplate": false +} \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/ChirpStack/uplink/metadata.json b/VENDORS/Milesight/EM300-MCS/ChirpStack/uplink/metadata.json new file mode 100644 index 00000000..23f54b34 --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/ChirpStack/uplink/metadata.json @@ -0,0 +1,4 @@ +{ + "integrationName": "ChirpStack integration", + "includeGatewayInfo": "false" +} \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/ChirpStack/uplink/payload.json b/VENDORS/Milesight/EM300-MCS/ChirpStack/uplink/payload.json new file mode 100644 index 00000000..060d9d52 --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/ChirpStack/uplink/payload.json @@ -0,0 +1,48 @@ +{ + "deduplicationId": "57433366-50a6-4dc2-8145-2df1bbc70d9e", + "time": "2023-05-22T07:47:05.404859+00:00", + "deviceInfo": { + "tenantId": "52f14cd4-c6f1-4fbd-8f87-4025e1d49242", + "tenantName": "ChirpStack", + "applicationId": "ca739e26-7b67-4f14-b69e-d568c22a5a75", + "applicationName": "Chirpstack application", + "deviceProfileId": "605d08d4-65f5-4d2c-8a5a-3d2457662f79", + "deviceProfileName": "Chirpstack default device profile", + "deviceName": "Device name", + "devEui": "1000000000000001", + "tags": {} + }, + "devAddr": "20000001", + "adr": true, + "dr": 5, + "fCnt": 4, + "fPort": 85, + "confirmed": false, + "data": "AXVcA2c0AQRoZQYAAQ==", + "rxInfo": [{ + "gatewayId": "6a7e111a10000000", + "uplinkId": 24022, + "time": "2023-05-22T07:47:05.404859+00:00", + "rssi": -35, + "snr": 11.5, + "channel": 2, + "rfChain": 1, + "location": {}, + "context": "EFwMtA==", + "metadata": { + "region_common_name": "EU868", + "region_config_id": "eu868" + }, + "crcStatus": "CRC_OK" + }], + "txInfo": { + "frequency": 868500000, + "modulation": { + "lora": { + "bandwidth": 125000, + "spreadingFactor": 7, + "codeRate": "CR_4_5" + } + } + } +} \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/ChirpStack/uplink/result.json b/VENDORS/Milesight/EM300-MCS/ChirpStack/uplink/result.json new file mode 100644 index 00000000..928431b2 --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/ChirpStack/uplink/result.json @@ -0,0 +1,28 @@ +{ + "deviceName": "EM300-MCS 1000000000000001", + "deviceType": "EM300-MCS", + "attributes": { + "eui": "1000000000000001", + "devAddr": "20000001", + "fPort": 85, + "applicationId": "ca739e26-7b67-4f14-b69e-d568c22a5a75", + "applicationName": "Chirpstack application", + "tenantId": "52f14cd4-c6f1-4fbd-8f87-4025e1d49242", + "tenantName": "ChirpStack", + "deviceProfileId": "605d08d4-65f5-4d2c-8a5a-3d2457662f79", + "deviceProfileName": "Chirpstack default device profile", + "frequency": 868500000, + "bandwidth": 125000, + "spreadingFactor": 7, + "codeRate": "CR_4_5" + }, + "telemetry": [{ + "ts": 1684741625404, + "values": { + "battery": 92, + "temperature": 30.8, + "humidity": 50.5, + "magnet_status": "open" + } + }] +} \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/HTTP/uplink/converter.json b/VENDORS/Milesight/EM300-MCS/HTTP/uplink/converter.json new file mode 100644 index 00000000..2e739516 --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/HTTP/uplink/converter.json @@ -0,0 +1,29 @@ +{ + "name": "HTTP Uplink integration for EM300-MCS", + "type": "UPLINK", + "debugMode": false, + "debugSettings": { + "failuresEnabled": true, + "allEnabled": false, + "allEnabledUntil": 1735560006385 + }, + "configuration": { + "scriptLang": "TBEL", + "decoder": null, + "tbelDecoder": "var data = decodeToJson(payload);\nvar deviceName = \"EM300-MCS \" + data.devEUI;\nvar deviceType = \"EM300-MCS\";\nvar groupName = null; // If groupName is not null - created device will be added to the entity group with such name.\nvar customerName = null; // If customerName is not null - created devices will be assigned to customer with such name. \n\n// use assetName and assetType instead of deviceName and deviceType\n// to automatically create assets instead of devices.\n// var assetName = 'Asset A';\n// var assetType = 'building';\n\n// If you want to parse incoming data somehow, you can add your code to this function.\n// input: bytes\n// expected output:\n// {\n// \"attributes\": {\"attributeKey\": \"attributeValue\"},\n// \"telemetry\": [{\"ts\": 1...1, \"values\": {\"telemetryKey\":\"telemetryValue\"}, {\"ts\": 1...2, \"values\": {\"telemetryKey\":\"telemetryValue\"}}]\n// }\n\nfunction decodePayload(input) {\n var output = {\n attributes: {},\n telemetry: []\n };\n \n // --- Decoding code --- //\n var decoded = {};\n var fPort = data.fPort;\n var historyDataList = [];\n if(fPort == 85) {\n for (var i = 0; i < input.length - 2;) {\n var channel_id = input[i++] & 0xff;\n var channel_type = input[i++] & 0xff;\n \n // BATTERY\n if (channel_id === 0x01 && channel_type === 0x75) {\n decoded.battery = input[i];\n i += 1;\n }\n \n // TEMPERATURE\n else if (channel_id === 0x03 && channel_type === 0x67) {\n // ℃\n decoded.temperature = parseBytesToInt(input, i, 2, false) / 10;\n i += 2;\n \n // ℉\n // decoded.temperature = parseBytesToInt(input, i, 2) / 10 * 1.8 + 32;\n // i +=2;\n }\n // HUMIDITY\n else if (channel_id === 0x04 && channel_type === 0x68) {\n decoded.humidity = parseBytesToInt(input, i, 1) / 2;\n i += 1;\n }\n // MAGNET STATUS\n else if (channel_id === 0x06 && channel_type === 0x00) {\n decoded.magnet_status = input[i] === 0 ? \"close\" : \"open\";\n i += 1;\n }\n else if (channel_id === 0x20 && channel_type === 0xce) {\n var magnet_status = input[i + 7] == 0 ? \"close\" : \"open\";\n var historyData = {\n ts : parseBytesToInt(input, i, 4, false) * 1000,\n values : {\n temperature: parseBytesToInt(input, i + 4, 2, false) / 10,\n humidity: parseBytesToInt(input, i + 6, 1, false) / 2,\n magnet_status: magnet_status\n }\n };\n \n historyDataList.add(historyData);\n i += 8;\n }\n }\n }\n\n output.telemetry = [{\n ts: timestamp,\n values: decoded\n }];\n \n output.telemetry.addAll(historyDataList);\n \n // --- Decoding code --- //\n return output;\n}\n\n// --- attributes and telemetry objects ---\nvar telemetry = [];\nvar attributes = {};\n// --- attributes and telemetry objects ---\n\n// --- Timestamp parsing\nvar dateString = data.time;\ntimestamp = parseDateToTimestamp(dateString);\n// --- Timestamp parsing\n\n// Passing incoming bytes to decodePayload function, to get custom decoding\nvar customDecoding = decodePayload(base64ToBytes(data.data));\n\n// Collecting data to result\nif (customDecoding.?telemetry.size() > 0) {\n foreach(telemetryObj: customDecoding.telemetry) {\n if (telemetryObj.ts != null && telemetryObj.values != null) {\n telemetry.add(telemetryObj);\n }\n }\n}\n\nif (customDecoding.?attributes.size() > 0) {\n attributes.putAll(customDecoding.attributes);\n}\n\n// You can add some keys manually to attributes or telemetry\nattributes.eui = data.?devEui;\nattributes.fPort = data.fPort;\nattributes.applicationId = data.?applicationId;\nattributes.applicationName = data.?applicationName;\nattributes.frequency = data.txInfo.?frequency;\nattributes.bandwidth = data.txInfo.?dataRate.?bandwidth;\nattributes.spreadingFactor = data.txInfo.?dataRate.?spreadFactor;\nattributes.codeRate = data.txInfo.?codeRate;\n\nif(Boolean.parseBoolean(metadata[\"includeGatewayInfo\"])) {\n var gatewayInfo = getGatewayInfo();\n var addDataToTelemetry = {};\n addDataToTelemetry.snr = gatewayInfo.?loRaSNR;\n addDataToTelemetry.rssi = gatewayInfo.rssi;\n addDataToTelemetry.fCnt = data.fCnt;\n \n telemetry = processTelemetryData(telemetry, addDataToTelemetry);\n}\n\nvar result = {\n deviceName: deviceName,\n deviceType: deviceType,\n // assetName: assetName,\n // assetType: assetType,\n attributes: attributes,\n telemetry: telemetry\n};\n\naddAdditionalInfoForDeviceMsg(result, customerName, groupName);\n\nreturn result;\n\nfunction addAdditionalInfoForDeviceMsg(deviceInfo, customerName, groupName) {\n if (customerName != null) {\n deviceInfo.customerName = customerName;\n }\n if (groupName != null) {\n deviceInfo.groupName = groupName;\n }\n}\n\nfunction parseDateToTimestamp(dateString) {\n dateString = dateString.replaceFirst(\"(\\\\d{4}-\\\\d{2})(\\\\d{2})\", \"$1-$2\");\n dateString = dateString.replaceFirst(\"\\\\.(\\\\d{3})\\\\d+Z\", \".$1Z\");\n dateString = dateString.replace(\"Z\", \"+0000\");\n var date = new Date(dateString);\n var timestamp = date.getTime();\n \n // If we cannot parse timestamp - we will use the current timestamp\n if (timestamp == -1) {\n timestamp = Date.now();\n }\n \n return timestamp;\n}\n\nfunction getGatewayInfo() {\n var gatewayList = data.rxInfo;\n var maxRssi = Integer.MIN_VALUE;\n var gatewayInfo = {};\n \n foreach (gateway : gatewayList) {\n if(gateway.rssi > maxRssi) {\n maxRssi = gateway.rssi;\n gatewayInfo = gateway;\n }\n }\n \n return gatewayInfo;\n}\n\nfunction processTelemetryData(telemetry, addDataToTelemetry) {\n if (telemetry.size >= 1) {\n telemetry = addDataToTelemetries(telemetry, addDataToTelemetry);\n }\n else {\n telemetry.add(addDataToTelemetry);\n }\n \n return telemetry;\n}\n\nfunction addDataToTelemetries(telemetries, addDataToTelemetry) {\n foreach(telemetry : telemetries) {\n foreach(element : addDataToTelemetry.entrySet()) {\n if(!telemetry[\"values\"].keys.contains(element.key)) {\n telemetry[\"values\"][element.key] = element.value;\n }\n } \n }\n \n return telemetries;\n}", + "encoder": null, + "tbelEncoder": null, + "updateOnlyKeys": [ + "fPort", + "applicationName", + "frequency", + "bandwidth", + "spreadingFactor", + "codeRate" + ] + }, + "additionalInfo": { + "description": "" + }, + "edgeTemplate": false +} \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/HTTP/uplink/metadata.json b/VENDORS/Milesight/EM300-MCS/HTTP/uplink/metadata.json new file mode 100644 index 00000000..87ee6615 --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/HTTP/uplink/metadata.json @@ -0,0 +1,4 @@ +{ + "integrationName": "HTTP integration", + "includeGatewayInfo": "false" +} \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/HTTP/uplink/payload.json b/VENDORS/Milesight/EM300-MCS/HTTP/uplink/payload.json new file mode 100644 index 00000000..5a89ddf2 --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/HTTP/uplink/payload.json @@ -0,0 +1,31 @@ +{ + "applicationID": 1, + "applicationName": "cloud", + "deviceName": "24e1641092176759", + "devEUI": "24e1641092176759", + "time": "2020-0327T12:39:05.547336Z", + "rxInfo": [ + { + "mac": "24e124fffef021be", + "rssi": -57, + "loRaSNR": 10, + "name": "local_gateway", + "latitude": 0, + "longitude": 0, + "altitude": 0 + } + ], + "txInfo": { + "frequency": 868300000, + "dataRate": { + "modulation": "LORA", + "bandwidth": 125, + "spreadFactor": 7 + }, + "adr": false, + "codeRate": "4/5" + }, + "fCnt": 0, + "fPort": 85, + "data": "AXVcA2c0AQRoZQYAAQ==" +} \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/HTTP/uplink/result.json b/VENDORS/Milesight/EM300-MCS/HTTP/uplink/result.json new file mode 100644 index 00000000..0c3c9c76 --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/HTTP/uplink/result.json @@ -0,0 +1,21 @@ +{ + "deviceName": "EM300-MCS 24e1641092176759", + "deviceType": "EM300-MCS", + "attributes": { + "fPort": 85, + "applicationName": "cloud", + "frequency": 868300000, + "bandwidth": 125, + "spreadingFactor": 7, + "codeRate": "4/5" + }, + "telemetry": [{ + "ts": 1585312745547, + "values": { + "battery": 92, + "temperature": 30.8, + "humidity": 50.5, + "magnet_status": "open" + } + }] +} \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/LORIOT/uplink/converter.json b/VENDORS/Milesight/EM300-MCS/LORIOT/uplink/converter.json new file mode 100644 index 00000000..0230fd16 --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/LORIOT/uplink/converter.json @@ -0,0 +1,29 @@ +{ + "name": "Loriot Uplink Decoder for EM300-MCS", + "type": "UPLINK", + "debugMode": false, + "debugSettings": { + "failuresEnabled": true, + "allEnabled": false, + "allEnabledUntil": 1733331880270 + }, + "configuration": { + "scriptLang": "TBEL", + "decoder": "// Decode an uplink message from a buffer\n// payload - array of bytes\n// metadata - key/value object\n\n/** Decoder **/\n\n// decode payload to string\nvar payloadStr = decodeToString(payload);\n\n// decode payload to JSON\n// var data = decodeToJson(payload);\n\nvar deviceName = 'Device A';\nvar deviceType = 'thermostat';\nvar customerName = 'Customer C';\nvar groupName = 'thermostat devices';\nvar manufacturer = 'Example corporation';\n// use assetName and assetType instead of deviceName and deviceType\n// to automatically create assets instead of devices.\n// var assetName = 'Asset A';\n// var assetType = 'building';\n\n// Result object with device/asset attributes/telemetry data\nvar result = {\n// Use deviceName and deviceType or assetName and assetType, but not both.\n deviceName: deviceName,\n deviceType: deviceType,\n// assetName: assetName,\n// assetType: assetType,\n// customerName: customerName,\n groupName: groupName,\n attributes: {\n model: 'Model A',\n serialNumber: 'SN111',\n integrationName: metadata['integrationName'],\n manufacturer: manufacturer\n },\n telemetry: {\n temperature: 42,\n humidity: 80,\n rawData: payloadStr\n }\n};\n\n/** Helper functions **/\n\nfunction decodeToString(payload) {\n return String.fromCharCode.apply(String, payload);\n}\n\nfunction decodeToJson(payload) {\n // covert payload to string.\n var str = decodeToString(payload);\n\n // parse string to JSON\n var data = JSON.parse(str);\n return data;\n}\n\nreturn result;", + "tbelDecoder": "var data = decodeToJson(payload);\nvar deviceName = \"EM300-MCS \" + data.EUI;\nvar deviceType = \"EM300-MCS\";\nvar groupName = null; // If groupName is not null - created device will be added to the entity group with such name.\nvar customerName = null; // If customerName is not null - created devices will be assigned to customer with such name. \n\n// use assetName and assetType instead of deviceName and deviceType\n// to automatically create assets instead of devices.\n// var assetName = 'Asset A';\n// var assetType = 'building';\n\n// If you want to parse incoming data somehow, you can add your code to this function.\n// input: bytes\n// expected output:\n// {\n// \"attributes\": {\"attributeKey\": \"attributeValue\"},\n// \"telemetry\": {\"telemetryKey\": \"telemetryValue\"}\n// }\n\nfunction decodePayload(input) {\n var output = { attributes: {}, telemetry: []};\n \n var decoded = {};\n var fPort = data.port;\n var historyDataList = [];\n if(fPort == 85) {\n for(var i = 0; i < input.length - 2; ) {\n var channel_id = input[i++] & 0xff;\n var channel_type = input[i++] & 0xff;\n \n // BATTERY\n if (channel_id === 0x01 && channel_type === 0x75) {\n decoded.battery = input[i];\n i += 1;\n }\n // TEMPERATURE\n else if (channel_id === 0x03 && channel_type === 0x67) {\n // ℃\n decoded.temperature = parseBytesToInt(input, i, 2, false) / 10;\n i += 2;\n \n // ℉\n // decoded.temperature = parseBytesToInt(input, i, 2) / 10 * 1.8 + 32;\n // i +=2;\n }\n // HUMIDITY\n else if (channel_id === 0x04 && channel_type === 0x68) {\n decoded.humidity = parseBytesToInt(input, i, 1) / 2;\n i += 1;\n }\n // MAGNET STATUS\n else if (channel_id === 0x06 && channel_type === 0x00) {\n decoded.magnet_status = input[i] === 0 ? \"close\" : \"open\";\n i += 1;\n }\n else if (channel_id === 0x20 && channel_type === 0xce) {\n var magnet_status = input[i + 7] == 0 ? \"close\" : \"open\";\n var historyData = {\n ts : parseBytesToInt(input, i, 4, false) * 1000,\n values : {\n temperature: parseBytesToInt(input, i + 4, 2, false) / 10,\n humidity: parseBytesToInt(input, i + 6, 1, false) / 2,\n magnet_status: magnet_status\n }\n };\n \n historyDataList.add(historyData);\n i += 8;\n }\n }\n }\n \n output.telemetry = [{\n ts: timestamp,\n values: decoded\n }];\n \n output.telemetry.addAll(historyDataList);\n \n // --- Decoding code --- //\n return output;\n}\n\n// --- attributes and telemetry objects ---\nvar telemetry = [];\nvar attributes = {};\n// --- attributes and telemetry objects ---\n\n// --- Timestamp parsing\ntimestamp = data.ts;\n// --- Timestamp parsing\n\n// Message parsing\n// To avoid paths in the decoded objects we passing false value to function as \"pathInKey\" argument.\n// Warning: pathInKey can cause already found fields to be overwritten with the last value found.\n\nvar uplinkDataList = [];\n\n// Passing incoming bytes to decodePayload function, to get custom decoding\nvar customDecoding = decodePayload(hexToBytes(data.data));\n\n// Collecting data to result\nif (customDecoding.?telemetry.size() > 0) {\n if (customDecoding.telemetry instanceof java.util.ArrayList) {\n foreach(telemetryObj: customDecoding.telemetry) {\n if (telemetryObj.ts != null && telemetryObj.values != null) {\n telemetry.add(telemetryObj);\n }\n }\n } else {\n telemetry.putAll(customDecoding.telemetry);\n }\n}\n\nif (customDecoding.?attributes.size() > 0) {\n attributes.putAll(customDecoding.attributes);\n}\n\n// You can add some keys manually to attributes or telemetry\nattributes.eui = data.EUI;\nattributes.fPort = data.port;\nattributes.frequency = data.freq;\n\nvar isIncludeGatewayInfo = metadata[\"includeGatewayInfo\"];\nif(isIncludeGatewayInfo == true) {\n var addDataToTelemetry = {};\n addDataToTelemetry.rssi = data.rssi;\n addDataToTelemetry.seqno = data.seqno;\n addDataToTelemetry.snr = data.snr;\n addDataToTelemetry.ack = data.ack;\n addDataToTelemetry.toa = data.toa;\n addDataToTelemetry.fCnt = data.fcnt;\n \n telemetry = processTelemetryData(telemetry, addDataToTelemetry);\n}\n\nvar deviceInfo = {\n deviceName: deviceName,\n deviceType: deviceType,\n// assetName: assetName,\n// assetType: assetType,\n attributes: attributes,\n telemetry: telemetry, \n};\n\naddAdditionalInfoForDeviceMsg(deviceInfo, customerName, groupName);\n\nuplinkDataList.add(deviceInfo);\n\nvar gatewayDeviceNamePrefix = \"Gateway \";\nvar gatewayDeviceType = \"Lora gateway\";\nvar gatewayGroupName = null; // If gatewayGroupName is not null - created device will be added to the entity group with such name.\n\nif (data.cmd == \"gw\") {\n foreach( gatewayInfo : data.gws ) {\n var addGatewayInfo = {};\n\n // You can add some keys manually telemetry\n addGatewayInfo.rssi = gatewayInfo.rssi;\n addGatewayInfo.snr = gatewayInfo.snr;\n // You can add some keys manually telemetry\n \n var gatewayInfoMsg = {\n deviceName: gatewayDeviceNamePrefix + gatewayInfo.gweui,\n deviceType: gatewayDeviceType,\n telemetry: [{\n \"ts\": gatewayInfo.ts,\n \"values\": addGatewayInfo\n }],\n attributes: {\n eui: gatewayInfo.gweui\n }\n };\n addAdditionalInfoForDeviceMsg(gatewayInfoMsg, customerName, gatewayGroupName);\n uplinkDataList.add(gatewayInfoMsg);\n }\n}\n\nreturn uplinkDataList;\n\nfunction addAdditionalInfoForDeviceMsg(deviceInfo, customerName, groupName) {\n if (customerName != null) {\n deviceInfo.customerName = customerName;\n }\n if (groupName != null) {\n deviceInfo.groupName = groupName;\n }\n}\n\nfunction processTelemetryData(telemetry, addDataToTelemetry) {\n if (telemetry.size >= 1) {\n telemetry = addDataToTelemetries(telemetry, addDataToTelemetry);\n }\n else {\n telemetry.add(addDataToTelemetry);\n }\n \n return telemetry;\n}\n\nfunction addDataToTelemetries(telemetries, addDataToTelemetry) {\n foreach(telemetry : telemetries) {\n foreach(element : addDataToTelemetry.entrySet()) {\n if(!telemetry[\"values\"].keys.contains(element.key)) {\n telemetry[\"values\"][element.key] = element.value;\n }\n } \n }\n \n return telemetries;\n}", + "encoder": null, + "tbelEncoder": null, + "updateOnlyKeys": [ + "fPort", + "ack", + "eui", + "frequency", + "dr", + "battery" + ] + }, + "additionalInfo": { + "description": "" + }, + "edgeTemplate": false +} \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/LORIOT/uplink/metadata.json b/VENDORS/Milesight/EM300-MCS/LORIOT/uplink/metadata.json new file mode 100644 index 00000000..ae2ee743 --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/LORIOT/uplink/metadata.json @@ -0,0 +1,4 @@ +{ + "integrationName": "Loriot integration", + "includeGatewayInfo": "false" +} \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/LORIOT/uplink/payload.json b/VENDORS/Milesight/EM300-MCS/LORIOT/uplink/payload.json new file mode 100644 index 00000000..5837fa14 --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/LORIOT/uplink/payload.json @@ -0,0 +1,17 @@ +{ + "cmd": "rx", + "seqno": 3040, + "EUI": "1000000000000001", + "ts": 1684478801936, + "fcnt": 2, + "port": 85, + "freq": 867500000, + "rssi": -21, + "snr": 10, + "toa": 206, + "dr": "SF9 BW125 4/5", + "ack": false, + "bat": 94, + "offline": false, + "data": "01755C03673401046865060001" +} \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/LORIOT/uplink/result.json b/VENDORS/Milesight/EM300-MCS/LORIOT/uplink/result.json new file mode 100644 index 00000000..7b9f9b3c --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/LORIOT/uplink/result.json @@ -0,0 +1,18 @@ +[{ + "deviceName": "EM300-MCS 1000000000000001", + "deviceType": "EM300-MCS", + "attributes": { + "eui": "1000000000000001", + "fPort": 85, + "frequency": 867500000 + }, + "telemetry": [{ + "ts": 1684478801936, + "values": { + "battery": 92, + "temperature": 30.8, + "humidity": 50.5, + "magnet_status": "open" + } + }] +}] \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/ThingsStackCommunity/uplink/converter.json b/VENDORS/Milesight/EM300-MCS/ThingsStackCommunity/uplink/converter.json new file mode 100644 index 00000000..643e0c70 --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/ThingsStackCommunity/uplink/converter.json @@ -0,0 +1,39 @@ +{ + "name": "The Things Stack Community Uplink Decoder for EM300-MCS", + "type": "UPLINK", + "debugMode": false, + "debugSettings": { + "failuresEnabled": true, + "allEnabled": false, + "allEnabledUntil": 1733331880270 + }, + "configuration": { + "scriptLang": "TBEL", + "decoder": null, + "tbelDecoder": "var data = decodeToJson(payload);\n\nvar deviceName = \"EM300-MCS \" + data.end_device_ids.device_id;\nvar deviceType = \"EM300-MCS\";\nvar groupName = null; // If groupName is not null - created device will be added to the entity group with such name.\nvar customerName = null; // If customerName is not null - created devices will be assigned to customer with such name. \n\n// use assetName and assetType instead of deviceName and deviceType\n// to automatically create assets instead of devices.\n// var assetName = 'Asset A';\n// var assetType = 'building';\n\n// If you want to parse incoming data somehow, you can add your code to this function.\n// input: bytes\n// expected output:\n// {\n// \"attributes\": {\"attributeKey\": \"attributeValue\"},\n// \"telemetry\": [{\"ts\": 1...1, \"values\": {\"telemetryKey\":\"telemetryValue\"}, {\"ts\": 1...2, \"values\": {\"telemetryKey\":\"telemetryValue\"}}]\n// }\n\nfunction decodeFrmPayload(input) {\n var output = {\n attributes: {}, \n telemetry: {}\n };\n \n // --- Decoding code --- //\n var decoded = {};\n var fPort = data.uplink_message.f_port;\n var historyDataList = [];\n if(fPort == 85) {\n for(var i = 0; i < input.length - 2; ) {\n var channel_id = input[i++] & 0xff;\n var channel_type = input[i++] & 0xff;\n \n // BATTERY\n if (channel_id === 0x01 && channel_type === 0x75) {\n decoded.battery = input[i];\n i += 1;\n }\n \n // TEMPERATURE\n else if (channel_id === 0x03 && channel_type === 0x67) {\n // ℃\n decoded.temperature = parseBytesToInt(input, i, 2, false) / 10;\n i += 2;\n \n // ℉\n // decoded.temperature = parseBytesToInt(input, i, 2) / 10 * 1.8 + 32;\n // i +=2;\n }\n // HUMIDITY\n else if (channel_id === 0x04 && channel_type === 0x68) {\n decoded.humidity = parseBytesToInt(input, i, 1) / 2;\n i += 1;\n }\n // MAGNET STATUS\n else if (channel_id === 0x06 && channel_type === 0x00) {\n decoded.magnet_status = input[i] === 0 ? \"close\" : \"open\";\n i += 1;\n }\n else if (channel_id === 0x20 && channel_type === 0xce) {\n var magnet_status = input[i + 7] == 0 ? \"close\" : \"open\";\n var historyData = {\n ts : parseBytesToInt(input, i, 4, false) * 1000,\n values : {\n temperature: parseBytesToInt(input, i + 4, 2, false) / 10,\n humidity: parseBytesToInt(input, i + 6, 1, false) / 2,\n magnet_status: magnet_status\n }\n };\n \n historyDataList.add(historyData);\n i += 8;\n }\n }\n }\n \n output.telemetry = [{\n ts: timestamp,\n values: decoded\n }];\n \n output.telemetry.addAll(historyDataList);\n \n // --- Decoding code --- //\n return output;\n}\n\n\n// --- attributes and telemetry objects ---\nvar telemetry = [];\nvar attributes = {};\n// --- attributes and telemetry objects ---\n\n// --- Timestamp parsing\nvar dateString = data.uplink_message.received_at;\n// If data is simulated or device doesn't send his own date string - we will use date from upcoming message, set by network server\nif ((data.simulated != null && data.simulated) || dateString == null) {\n dateString = data.received_at;\n}\ntimestamp = parseDateToTimestamp(dateString);\n// --- Timestamp parsing\n\n// Message parsing\n// To avoid paths in the decoded objects we passing false value to function as \"pathInKey\" argument.\n// Warning: pathInKey can cause already found fields to be overwritten with the last value found, e.g. receive_at from uplink_message will be written receive_at in the root.\n\n// Passing incoming bytes to decodeFrmPayload function, to get custom decoding\nvar customDecoding = {};\nif (data.uplink_message.get(\"frm_payload\") != null) {\n customDecoding = decodeFrmPayload(base64ToBytes(data.uplink_message.frm_payload));\n}\n\n// Collecting data to result\nif (customDecoding.?telemetry.size() > 0) {\n if (customDecoding.telemetry instanceof java.util.ArrayList) {\n foreach(telemetryObj: customDecoding.telemetry) {\n if (telemetryObj.ts != null && telemetryObj.values != null) {\n telemetry.add(telemetryObj);\n }\n }\n } else {\n telemetry.putAll(customDecoding.telemetry);\n }\n}\n\nif (customDecoding.?attributes.size() > 0) {\n attributes.putAll(customDecoding.attributes);\n}\n\n// You can add some keys manually to attributes or telemetry\nvar applicationId = data.end_device_ids.?application_ids.?application_id;\nvar devAddr = data.end_device_ids.?dev_addr;\nvar spreadingFactor = data.uplink_message.?settings.?data_rate.?lora.?spreading_factor;\nvar codeRate = data.uplink_message.?settings.?data_rate.?lora.?coding_rate;\nvar tenantId = data.uplink_message.?network_ids.?tenant_id;\nattributes.eui = data.end_device_ids.dev_eui;\nattributes.fPort = data.uplink_message.f_port;\nattributes.applicationId = applicationId;\nattributes.devAddr = devAddr;\nattributes.spreadingFactor = spreadingFactor;\nattributes.codeRate = codeRate;\nattributes.tenantId = tenantId;\nattributes.device_id = data.end_device_ids.?device_id;\nattributes.join_eui = data.end_device_ids.?join_eui;\nattributes.net_id = data.uplink_message.?network_ids.?net_id;\nattributes.cluster_id = data.uplink_message.?network_ids.?cluster_id;\nattributes.cluster_adress = data.uplink_message.?network_ids.?cluster_address;\nattributes.bandwidth = data.uplink_message.?settings.?data_rate.?lora.?bandwidth;\nattributes.frequency = data.uplink_message.?settings.?frequency;\n\n\nvar gatewayInfo = getGatewayInfo();\nvar addDataToTelemetry = {};\naddDataToTelemetry.snr = gatewayInfo.snr;\naddDataToTelemetry.rssi = gatewayInfo.rssi;\naddDataToTelemetry.channel = gatewayInfo.channel_index;\naddDataToTelemetry.consumed_airtime = data.uplink_message.?consumed_airtime;\naddDataToTelemetry.fCnt = data.uplink_message.?f_cnt;\n\nvar isIncludeGatewayInfo = metadata[\"includeGatewayInfo\"];\nif(isIncludeGatewayInfo == true) {\n telemetry = processTelemetryData(telemetry, addDataToTelemetry);\n}\n\nvar result = {\n deviceName: deviceName,\n deviceType: deviceType,\n// assetName: assetName,\n// assetType: assetType,\n attributes: attributes,\n telemetry: telemetry\n};\n\naddAdditionalInfoForDeviceMsg(result, customerName, groupName);\n\nreturn result;\n\n\nfunction addAdditionalInfoForDeviceMsg(deviceInfo, customerName, groupName) {\n if (customerName != null) {\n deviceInfo.customerName = customerName;\n }\n if (groupName != null) {\n deviceInfo.groupName = groupName;\n }\n}\n\nfunction parseDateToTimestamp(dateString) {\n var date = new Date(dateString);\n var timestamp = date.getTime();\n \n // If we cannot parse timestamp - we will use the current timestamp\n if (timestamp == -1) {\n timestamp = Date.now();\n }\n \n return timestamp;\n}\n\nfunction getGatewayInfo() {\n var gatewayList = data.uplink_message.?rx_metadata;\n var maxRssi = Integer.MIN_VALUE;\n var gatewayInfo = {};\n \n foreach (gateway : gatewayList) {\n if(gateway.rssi > maxRssi) {\n maxRssi = gateway.rssi;\n gatewayInfo = gateway;\n }\n }\n \n return gatewayInfo;\n}\n\nfunction processTelemetryData(telemetry, addDataToTelemetry) {\n if (telemetry.size >= 1) {\n telemetry = addDataToTelemetries(telemetry, addDataToTelemetry);\n }\n else {\n telemetry.add(addDataToTelemetry);\n }\n \n return telemetry;\n}\n\nfunction addDataToTelemetries(telemetries, addDataToTelemetry) {\n foreach(telemetry : telemetries) {\n foreach(element : addDataToTelemetry.entrySet()) {\n if(!telemetry[\"values\"].keys.contains(element.key)) {\n telemetry[\"values\"][element.key] = element.value;\n }\n } \n }\n \n return telemetries;\n}", + "encoder": null, + "tbelEncoder": null, + "updateOnlyKeys": [ + "fPort", + "bandwidth", + "frequency", + "net_id", + "cluster_id", + "cluster_address", + "device_id", + "join_eui", + "battery", + "eui", + "channel", + "applicationId", + "devAddr", + "spreadingFactor", + "codeRate", + "tenantId" + ] + }, + "additionalInfo": { + "description": "" + }, + "edgeTemplate": false +} \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/ThingsStackCommunity/uplink/metadata.json b/VENDORS/Milesight/EM300-MCS/ThingsStackCommunity/uplink/metadata.json new file mode 100644 index 00000000..0d75c374 --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/ThingsStackCommunity/uplink/metadata.json @@ -0,0 +1,4 @@ +{ + "integrationName": "The Things Stack Community integration", + "includeGatewayInfo": "false" +} \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/ThingsStackCommunity/uplink/payload.json b/VENDORS/Milesight/EM300-MCS/ThingsStackCommunity/uplink/payload.json new file mode 100644 index 00000000..9f4bbf66 --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/ThingsStackCommunity/uplink/payload.json @@ -0,0 +1,54 @@ +{ + "end_device_ids": { + "device_id": "eui-1000000000000001", + "application_ids": { + "application_id": "application-tts-name" + }, + "dev_eui": "1000000000000001", + "join_eui": "2000000000000001", + "dev_addr": "20000001" + }, + "correlation_ids": ["as:up:01H0S7ZJQ9MQPMVY49FT3SE07M", "gs:conn:01H03BQZ9342X3Y86DJ2P704E5", "gs:up:host:01H03BQZ99EGAM52KK1300GFKN", "gs:uplink:01H0S7ZJGS6D9TJSKJN8XNTMAV", "ns:uplink:01H0S7ZJGS9KKD4HTTPKFEMWCV", "rpc:/ttn.lorawan.v3.GsNs/HandleUplink:01H0S7ZJGSF3M38ZRZVTM38DEC", "rpc:/ttn.lorawan.v3.NsAs/HandleUplink:01H0S7ZJQ8R2EH5AA269AKM8DX"], + "received_at": "2023-05-19T05:33:35.848446463Z", + "uplink_message": { + "session_key_id": "AYfqmb0pc/1uRZv9xUydgQ==", + "f_port": 85, + "f_cnt": 10335, + "frm_payload": "AXVcA2c0AQRoZQYAAQ==", + "rx_metadata": [{ + "gateway_ids": { + "gateway_id": "eui-6a7e111a10000000", + "eui": "6A7E111A10000000" + }, + "time": "2023-05-19T05:33:35.608982Z", + "timestamp": 3893546133, + "rssi": -35, + "channel_rssi": -35, + "snr": 13.2, + "frequency_offset": "69", + "uplink_token": "CiIKIAoUZXVpLTZhN2UxMTFhMTAwMDAwMDASCCThJP/+9k6eEJWZy8AOGgwIr5ScowYQvNbUsQIgiMy8y6jwpwE=", + "channel_index": 3, + "received_at": "2023-05-19T05:33:35.607383681Z" + }], + "settings": { + "data_rate": { + "lora": { + "bandwidth": 125000, + "spreading_factor": 7, + "coding_rate": "4/5" + } + }, + "frequency": "867100000", + "timestamp": 3893546133, + "time": "2023-05-19T05:33:35.608982Z" + }, + "received_at": "2023-05-19T05:33:35.641841782Z", + "consumed_airtime": "0.056576s", + "network_ids": { + "net_id": "000013", + "tenant_id": "ttn", + "cluster_id": "eu1", + "cluster_address": "eu1.cloud.thethings.network" + } + } +} \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/ThingsStackCommunity/uplink/result.json b/VENDORS/Milesight/EM300-MCS/ThingsStackCommunity/uplink/result.json new file mode 100644 index 00000000..5fd34808 --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/ThingsStackCommunity/uplink/result.json @@ -0,0 +1,29 @@ +{ + "deviceName": "EM300-MCS eui-1000000000000001", + "deviceType": "EM300-MCS", + "attributes": { + "eui": "1000000000000001", + "fPort": 85, + "applicationId": "application-tts-name", + "devAddr": "20000001", + "spreadingFactor": 7, + "codeRate": "4/5", + "tenantId": "ttn", + "device_id": "eui-1000000000000001", + "join_eui": "2000000000000001", + "net_id": "000013", + "cluster_id": "eu1", + "cluster_adress": "eu1.cloud.thethings.network", + "bandwidth": 125000, + "frequency": "867100000" + }, + "telemetry": [{ + "ts": 1684474415641, + "values": { + "battery": 92, + "temperature": 30.8, + "humidity": 50.5, + "magnet_status": "open" + } + }] +} \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/ThingsStackIndustries/uplink/converter.json b/VENDORS/Milesight/EM300-MCS/ThingsStackIndustries/uplink/converter.json new file mode 100644 index 00000000..c064ba12 --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/ThingsStackIndustries/uplink/converter.json @@ -0,0 +1,40 @@ +{ + "name": "The Things Stack Industries Uplink Decoder for EM300-MCS", + "type": "UPLINK", + "debugMode": false, + "debugSettings": { + "failuresEnabled": true, + "allEnabled": false, + "allEnabledUntil": 1733331880270 + }, + "configuration": { + "scriptLang": "TBEL", + "decoder": null, + "tbelDecoder": "var data = decodeToJson(payload);\n\nvar deviceName = \"EM300-MCS \" + data.end_device_ids.device_id;\nvar deviceType = \"EM300-MCS\";\nvar groupName = null; // If groupName is not null - created device will be added to the entity group with such name.\nvar customerName = null; // If customerName is not null - created devices will be assigned to customer with such name. \n\n// use assetName and assetType instead of deviceName and deviceType\n// to automatically create assets instead of devices.\n// var assetName = 'Asset A';\n// var assetType = 'building';\n\n// If you want to parse incoming data somehow, you can add your code to this function.\n// input: bytes\n// expected output:\n// {\n// \"attributes\": {\"attributeKey\": \"attributeValue\"},\n// \"telemetry\": [{\"ts\": 1...1, \"values\": {\"telemetryKey\":\"telemetryValue\"}, {\"ts\": 1...2, \"values\": {\"telemetryKey\":\"telemetryValue\"}}]\n// }\n\nfunction decodeFrmPayload(input) {\n var output = { attributes: {}, telemetry: []};\n \n // --- Decoding code --- //\n var decoded = {};\n var fPort = data.uplink_message.f_port;\n var historyDataList = [];\n if(fPort == 85) {\n for(var i = 0; i < input.length - 2; ) {\n var channel_id = input[i++] & 0xff;\n var channel_type = input[i++] & 0xff;\n \n // BATTERY\n if (channel_id === 0x01 && channel_type === 0x75) {\n decoded.battery = input[i];\n i += 1;\n }\n \n // TEMPERATURE\n else if (channel_id === 0x03 && channel_type === 0x67) {\n // ℃\n decoded.temperature = parseBytesToInt(input, i, 2, false) / 10;\n i += 2;\n \n // ℉\n // decoded.temperature = parseBytesToInt(input, i, 2) / 10 * 1.8 + 32;\n // i +=2;\n }\n // HUMIDITY\n else if (channel_id === 0x04 && channel_type === 0x68) {\n decoded.humidity = parseBytesToInt(input, i, 1) / 2;\n i += 1;\n }\n // MAGNET STATUS\n else if (channel_id === 0x06 && channel_type === 0x00) {\n decoded.magnet_status = input[i] === 0 ? \"close\" : \"open\";\n i += 1;\n }\n else if (channel_id === 0x20 && channel_type === 0xce) {\n var magnet_status = input[i + 7] == 0 ? \"close\" : \"open\";\n var historyData = {\n ts : parseBytesToInt(input, i, 4, false) * 1000,\n values : {\n temperature: parseBytesToInt(input, i + 4, 2, false) / 10,\n humidity: parseBytesToInt(input, i + 6, 1, false) / 2,\n magnet_status: magnet_status\n }\n };\n \n historyDataList.add(historyData);\n i += 8;\n }\n }\n }\n \n output.telemetry = [{\n ts: timestamp,\n values: decoded\n }];\n \n output.telemetry.addAll(historyDataList);\n \n // --- Decoding code --- //\n return output;\n}\n\n// --- attributes and telemetry objects ---\nvar telemetry = [];\nvar attributes = {};\n// --- attributes and telemetry objects ---\n\n// --- Timestamp parsing\nvar dateString = data.uplink_message.received_at;\n\nif ((data.simulated != null && data.simulated) || dateString == null) {\n dateString = data.received_at;\n}\n\ntimestamp = parseDateToTimestamp(dateString);\n// --- Timestamp parsing\n\n// Message parsing\n// To avoid paths in the decoded objects we passing false value to function as \"pathInKey\" argument.\n// Warning: pathInKey can cause already found fields to be overwritten with the last value found, e.g. receive_at from uplink_message will be written receive_at in the root.\n\n// Passing incoming bytes to decodeFrmPayload function, to get custom decoding\nvar customDecoding = {};\nif (data.uplink_message.get(\"frm_payload\") != null) {\n customDecoding = decodeFrmPayload(base64ToBytes(data.uplink_message.frm_payload));\n}\n\n// Collecting data to result\nif (customDecoding.?telemetry.size() > 0) {\n if (customDecoding.telemetry instanceof java.util.ArrayList) {\n foreach(telemetryObj: customDecoding.telemetry) {\n if (telemetryObj.ts != null && telemetryObj.values != null) {\n telemetry.add(telemetryObj);\n }\n }\n } else {\n telemetry.putAll(customDecoding.telemetry);\n }\n}\n\nif (customDecoding.?attributes.size() > 0) {\n attributes.putAll(customDecoding.attributes);\n}\n\n// You can add some keys manually to attributes or telemetry\nvar applicationId = data.end_device_ids.?application_ids.?application_id;\nvar devAddr = data.end_device_ids.?dev_addr;\nvar spreadingFactor = data.uplink_message.?settings.?data_rate.?lora.?spreading_factor;\nvar codeRate = data.uplink_message.?settings.?data_rate.?lora.?coding_rate;\nvar tenantId = data.uplink_message.?network_ids.?tenant_id;\nattributes.eui = data.end_device_ids.dev_eui;\nattributes.fPort = data.uplink_message.f_port;\nattributes.applicationId = applicationId;\nattributes.devAddr = devAddr;\nattributes.spreadingFactor = spreadingFactor;\nattributes.codeRate = codeRate;\nattributes.tenantId = tenantId;\nattributes.device_id = data.end_device_ids.?device_id;\nattributes.join_eui = data.end_device_ids.?join_eui;\nattributes.net_id = data.uplink_message.?network_ids.?net_id;\nattributes.cluster_id = data.uplink_message.?network_ids.?cluster_id;\nattributes.cluster_address = data.uplink_message.?network_ids.?cluster_address;\nattributes.bandwidth = data.uplink_message.?settings.?data_rate.?lora.?bandwidth;\nattributes.frequency = data.uplink_message.?settings.?frequency;\n\nvar isIncludeGatewayInfo = metadata[\"includeGatewayInfo\"];\nif(isIncludeGatewayInfo == true) {\n var gatewayInfo = getGatewayInfo();\n var addDataToTelemetry = {};\n addDataToTelemetry.snr = gatewayInfo.snr;\n addDataToTelemetry.rssi = gatewayInfo.rssi;\n addDataToTelemetry.channel = gatewayInfo.channel_index;\n addDataToTelemetry.consumed_airtime = data.uplink_message.?consumed_airtime;\n addDataToTelemetry.fCnt = data.uplink_message.?f_cnt;\n\n telemetry = processTelemetryData(telemetry, addDataToTelemetry);\n}\n\nvar result = {\n deviceName: deviceName,\n deviceType: deviceType,\n // assetName: assetName,\n // assetType: assetType,\n attributes: attributes,\n telemetry: telemetry\n};\n\naddAdditionalInfoForDeviceMsg(result, customerName, groupName);\n\nreturn result;\n\nfunction addAdditionalInfoForDeviceMsg(deviceInfo, customerName, groupName) {\n if (customerName != null) {\n deviceInfo.customerName = customerName;\n }\n if (groupName != null) {\n deviceInfo.groupName = groupName;\n }\n}\n\nfunction parseDateToTimestamp(dateString) {\n var date = new Date(dateString);\n var timestamp = date.getTime();\n \n // If we cannot parse timestamp - we will use the current timestamp\n if (timestamp == -1) {\n timestamp = Date.now();\n }\n \n return timestamp;\n}\n\nfunction getGatewayInfo() {\n var gatewayList = data.uplink_message.?rx_metadata;\n var maxRssi = Integer. MIN_VALUE;\n var gatewayInfo = {};\n \n foreach (gateway : gatewayList) {\n if(gateway.rssi > maxRssi) {\n maxRssi = gateway.rssi;\n gatewayInfo = gateway;\n }\n }\n \n return gatewayInfo;\n}\n\nfunction processTelemetryData(telemetry, addDataToTelemetry) {\n if (telemetry.size >= 1) {\n telemetry = addDataToTelemetries(telemetry, addDataToTelemetry);\n }\n else {\n telemetry.add(addDataToTelemetry);\n }\n \n return telemetry;\n}\n\nfunction addDataToTelemetries(telemetries, addDataToTelemetry) {\n foreach(telemetry : telemetries) {\n foreach(element : addDataToTelemetry.entrySet()) {\n if(!telemetry[\"values\"].keys.contains(element.key)) {\n telemetry[\"values\"][element.key] = element.value;\n }\n } \n }\n \n return telemetries;\n}", + "encoder": null, + "tbelEncoder": null, + "updateOnlyKeys": [ + "fPort", + "bandwidth", + "frequency", + "net_id", + "cluster_id", + "cluster_address", + "tenant_address", + "device_id", + "join_eui", + "eui", + "channel", + "devAddr", + "spreadingFactor", + "codeRate", + "tenantId", + "applicationId", + "battery" + ] + }, + "additionalInfo": { + "description": "" + }, + "edgeTemplate": false +} \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/ThingsStackIndustries/uplink/metadata.json b/VENDORS/Milesight/EM300-MCS/ThingsStackIndustries/uplink/metadata.json new file mode 100644 index 00000000..904c0fa0 --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/ThingsStackIndustries/uplink/metadata.json @@ -0,0 +1,4 @@ +{ + "integrationName": "The Things Stack Industries integration new", + "includeGatewayInfo": "false" +} \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/ThingsStackIndustries/uplink/payload.json b/VENDORS/Milesight/EM300-MCS/ThingsStackIndustries/uplink/payload.json new file mode 100644 index 00000000..c6a25003 --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/ThingsStackIndustries/uplink/payload.json @@ -0,0 +1,77 @@ +{ + "end_device_ids": { + "device_id": "eui-1000000000000001", + "application_ids": { + "application_id": "application-tti-name" + }, + "dev_eui": "1000000000000001", + "join_eui": "2000000000000001", + "dev_addr": "20000001" + }, + "correlation_ids": ["as:up:01H0PZDGB1NW6NAPD815NGHPF6", "gs:conn:01H0FJRSXSYT7VKNYXJ89F95XT", "gs:up:host:01H0FJRSY3MZMGPPFBQ4FZV4T8", "gs:uplink:01H0PZDG4HHGFRTXRTXD4PFTH7", "ns:uplink:01H0PZDG4JZ3BM0K6J89EQK1J7", "rpc:/ttn.lorawan.v3.GsNs/HandleUplink:01H0PZDG4J02F85RYFPCNSNXCR", "rpc:/ttn.lorawan.v3.NsAs/HandleUplink:01H0PZDGB081PMP806BJHNHX1A"], + "received_at": "2023-05-18T08:25:26.112483370Z", + "uplink_message": { + "session_key_id": "AYfg8rhha5n+FWx0ZaAprA==", + "f_port": 85, + "f_cnt": 5017, + "frm_payload": "AXVcA2c0AQRoZQYAAQ==", + "rx_metadata": [{ + "gateway_ids": { + "gateway_id": "eui-6A7E111A10000000", + "eui": "6A7E111A10000000" + }, + "time": "2023-05-18T08:25:25.885310Z", + "timestamp": 818273765, + "rssi": -24, + "channel_rssi": -24, + "snr": 12, + "frequency_offset": "671", + "uplink_token": "CiIKIAoUZXVpLTZBN0UxMTFBMTAwMDAwMDASCCThJP/+9k6eEOW7l4YDGgwI9cGXowYQ5KPhrwMgiI2rp+jpOA=", + "channel_index": 2, + "received_at": "2023-05-18T08:25:25.869324983Z" + }, { + "gateway_ids": { + "gateway_id": "packetbroker" + }, + "packet_broker": { + "message_id": "01H0PZDG4MF9AYSMNY44MAVTDH", + "forwarder_net_id": "000013", + "forwarder_tenant_id": "ttn", + "forwarder_cluster_id": "eu1.cloud.thethings.network", + "forwarder_gateway_eui": "6A7E111A10000000", + "forwarder_gateway_id": "eui-6a7e111a10000000", + "home_network_net_id": "000013", + "home_network_tenant_id": "tenant", + "home_network_cluster_id": "eu1.cloud.thethings.industries" + }, + "time": "2023-05-18T08:25:25.885310Z", + "rssi": -24, + "channel_rssi": -24, + "snr": 12, + "frequency_offset": "671", + "uplink_token": "eyJnIjoiWlhsS2FHSkhZMmxQYVVwQ1RWUkpORkl3VGs1VE1XTnBURU5LYkdKdFRXbFBhVXBDVFZSSk5GSXdUazVKYVhkcFlWaFphVTlwU201a01uaGhWVlJvZDFSWFVuRmlSM1JtVFcxT2RVbHBkMmxrUjBadVNXcHZhV05ZY0RKT1IyeExaREpSZVZwR1pIUmpNRXBLVlVoR2RFNVZkR3BWVTBvNUxua3paVVJTWVRaM1lXOU1kbTQwVm5sdmIyWmlPWGN1ZUhCZmVrcElaa3hIWlZadGRVUlFVeTVuYlRaVlZXRXdkakpHV0VKMGJUUjZaMjVXUkVoeGVHRjRaMlJKTlVkS1VsbERhemc1VDNCbk5rVk1iM1JDUkVZM1VWbHdZbEJDTkdOblNqWjBlbkphYUV4MFRVMHhZMVZFTTFac01XdExURUo0YURaMFExTnhhMVJsWWw4eE5FdHlVVXcyZUhsRWFFbEhlakJITXpoTE0xaFdlRzR5VUVjMk4wNUViME5WTkhoTmRrazFZVk5oWkUwd2FXVnFjR294VGtoMFduZHlZMDFxVlVGNmRsbERUazlNY2s5eFdVeFpWMk5XTG1WVFFYVkpNVkptT1U5NWRqUTNhSEoxTUZoalYxRT0iLCJhIjp7ImZuaWQiOiIwMDAwMTMiLCJmdGlkIjoidHRuIiwiZmNpZCI6ImV1MS5jbG91ZC50aGV0aGluZ3MubmV0d29yayJ9fQ==", + "received_at": "2023-05-18T08:25:25.906038642Z" + }], + "settings": { + "data_rate": { + "lora": { + "bandwidth": 125000, + "spreading_factor": 7, + "coding_rate": "4/5" + } + }, + "frequency": "868500000", + "timestamp": 818273765, + "time": "2023-05-18T08:25:25.885310Z" + }, + "received_at": "2023-05-18T08:25:25.906399073Z", + "consumed_airtime": "0.097536s", + "network_ids": { + "net_id": "000013", + "tenant_id": "tenant", + "cluster_id": "eu1", + "cluster_address": "eu1.cloud.thethings.industries", + "tenant_address": "tenant.eu1.cloud.thethings.industries" + } + } +} \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/ThingsStackIndustries/uplink/result.json b/VENDORS/Milesight/EM300-MCS/ThingsStackIndustries/uplink/result.json new file mode 100644 index 00000000..3a22511e --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/ThingsStackIndustries/uplink/result.json @@ -0,0 +1,29 @@ +{ + "deviceName": "EM300-MCS eui-1000000000000001", + "deviceType": "EM300-MCS", + "attributes": { + "eui": "1000000000000001", + "fPort": 85, + "applicationId": "application-tti-name", + "devAddr": "20000001", + "spreadingFactor": 7, + "codeRate": "4/5", + "tenantId": "tenant", + "device_id": "eui-1000000000000001", + "join_eui": "2000000000000001", + "net_id": "000013", + "cluster_id": "eu1", + "cluster_address": "eu1.cloud.thethings.industries", + "bandwidth": 125000, + "frequency": "868500000" + }, + "telemetry": [{ + "ts": 1684398325906, + "values": { + "battery": 92, + "temperature": 30.8, + "humidity": 50.5, + "magnet_status": "open" + } + }] +} \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/guide.md b/VENDORS/Milesight/EM300-MCS/guide.md new file mode 100644 index 00000000..85f289ea --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/guide.md @@ -0,0 +1,13 @@ +# Magnetic Contact Switch - Milesight IoT + +The payload decoder function is applicable to EM300-MCS. + +## Payload Definition + +| CHANNEL | ID | TYPE | LENGTH | DESCRIPTION | +| :-------------: | :--: | :--: | :----: | ------------------------------------------------------------------ | +| Battery | 0x01 | 0x75 | 1 | battery(1B)
battery: unit: % | +| Temperature | 0x03 | 0x67 | 2 | temperature(2B)
temperature, unit: ℃ | +| Humidity | 0x04 | 0x68 | 1 | humidity(1B)
humidity, unit: %RH | +| Magnet Status | 0x06 | 0x00 | 1 | magnet_status(1B)
magnet_status, values: (0: close, 1: open) | +| Historical Data | 0x20 | 0XCE | 8 | timestamp(4B) + temperature(2B) + humidity(1B) + magnet_status(1B) | \ No newline at end of file diff --git a/VENDORS/Milesight/EM300-MCS/info.json b/VENDORS/Milesight/EM300-MCS/info.json new file mode 100644 index 00000000..bc6d16e6 --- /dev/null +++ b/VENDORS/Milesight/EM300-MCS/info.json @@ -0,0 +1,6 @@ +{ + "url": "https://www.milesight.com/iot/product/lorawan-sensor/em300-mcs", + "label": "EM300-MCS: Magnetic Contact Switch", + "description": "EM300-MCS is a compact magnet switch sensor for detecting open/close status of objects (such as doors or windows) and transmitting alarm using LoRaWAN® technology." +} + diff --git a/VENDORS/Milesight/EM300-MCS/photo.png b/VENDORS/Milesight/EM300-MCS/photo.png new file mode 100644 index 0000000000000000000000000000000000000000..f57520da0f50a87628bc9dfc7e1e7e0bb278369c GIT binary patch literal 26564 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

tFuq3|YRbYFg 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 kV(42b0{F{1|IdF1RHKs)Q)yYlQ