From 7cc1d207399656f9b6294db9db61150db5d98414 Mon Sep 17 00:00:00 2001 From: thingsboard017 Date: Mon, 28 Oct 2024 15:26:40 +0200 Subject: [PATCH] Added 4 integrations for AM104 device: Loriot, Things Stack Community, Things Stack Industry and ChirpStack --- .../AM104/ChirpStack/uplink/converter.json | 35 ++++++++ .../AM104/ChirpStack/uplink/metadata.json | 4 + .../AM104/ChirpStack/uplink/payload.json | 48 +++++++++++ .../AM104/ChirpStack/uplink/result.json | 31 +++++++ .../AM104/Loriot/uplink/converter.json | 25 ++++++ .../AM104/Loriot/uplink/metadata.json | 4 + .../AM104/Loriot/uplink/payload.json | 17 ++++ .../Milesight/AM104/Loriot/uplink/result.json | 21 +++++ .../uplink/converter.json | 35 ++++++++ .../ThingsStackCommunity/uplink/metadata.json | 4 + .../ThingsStackCommunity/uplink/payload.json | 54 ++++++++++++ .../ThingsStackCommunity/uplink/result.json | 32 ++++++++ .../uplink/converter.json | 36 ++++++++ .../uplink/metadata.json | 4 + .../ThingsStackIndustries/uplink/payload.json | 77 ++++++++++++++++++ .../ThingsStackIndustries/uplink/result.json | 32 ++++++++ VENDORS/Milesight/AM104/guide.md | 24 ++++++ VENDORS/Milesight/AM104/info.json | 5 ++ VENDORS/Milesight/AM104/photo.png | Bin 0 -> 14907 bytes 19 files changed, 488 insertions(+) create mode 100644 VENDORS/Milesight/AM104/ChirpStack/uplink/converter.json create mode 100644 VENDORS/Milesight/AM104/ChirpStack/uplink/metadata.json create mode 100644 VENDORS/Milesight/AM104/ChirpStack/uplink/payload.json create mode 100644 VENDORS/Milesight/AM104/ChirpStack/uplink/result.json create mode 100644 VENDORS/Milesight/AM104/Loriot/uplink/converter.json create mode 100644 VENDORS/Milesight/AM104/Loriot/uplink/metadata.json create mode 100644 VENDORS/Milesight/AM104/Loriot/uplink/payload.json create mode 100644 VENDORS/Milesight/AM104/Loriot/uplink/result.json create mode 100644 VENDORS/Milesight/AM104/ThingsStackCommunity/uplink/converter.json create mode 100644 VENDORS/Milesight/AM104/ThingsStackCommunity/uplink/metadata.json create mode 100644 VENDORS/Milesight/AM104/ThingsStackCommunity/uplink/payload.json create mode 100644 VENDORS/Milesight/AM104/ThingsStackCommunity/uplink/result.json create mode 100644 VENDORS/Milesight/AM104/ThingsStackIndustries/uplink/converter.json create mode 100644 VENDORS/Milesight/AM104/ThingsStackIndustries/uplink/metadata.json create mode 100644 VENDORS/Milesight/AM104/ThingsStackIndustries/uplink/payload.json create mode 100644 VENDORS/Milesight/AM104/ThingsStackIndustries/uplink/result.json create mode 100644 VENDORS/Milesight/AM104/guide.md create mode 100644 VENDORS/Milesight/AM104/info.json create mode 100644 VENDORS/Milesight/AM104/photo.png diff --git a/VENDORS/Milesight/AM104/ChirpStack/uplink/converter.json b/VENDORS/Milesight/AM104/ChirpStack/uplink/converter.json new file mode 100644 index 00000000..66e8b21d --- /dev/null +++ b/VENDORS/Milesight/AM104/ChirpStack/uplink/converter.json @@ -0,0 +1,35 @@ +{ + "name": "ChirpStack uplink converter for Milesight AM104", + "type": "UPLINK", + "debugMode": true, + "configuration": { + "scriptLang": "TBEL", + "decoder": null, + "tbelDecoder": "var data = decodeToJson(payload);\nvar deviceName = \"AM104 \" + data.deviceInfo.deviceName;\nvar deviceType = \"AM104\";\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 historyData = {};\n for (var i = 0; i < input.length - 2;) {\n var channel_id = input[i++] & 0xff;\n var channel_type = input[i++] & 0xff;\n // BATTERY\n if (channel_id === 0x01 && channel_type === 0x75) {\n decoded.battery = input[i];\n i += 1;\n }\n // TEMPERATURE\n if (channel_id === 0x03 && channel_type === 0x67) {\n // ℃\n decoded.temperature = parseBytesToInt(input, i,\n 2, false) / 10;\n i += 2;\n // ℉\n // decoded.temperature = parseBytesToInt(input, i, 2, false) / 10 * 1.8 + 32;\n // i +=2;\n }\n // HUMIDITY\n if (channel_id === 0x04 && channel_type === 0x68) {\n decoded.humidity = parseBytesToInt(input, i, 1, false) / 2;\n i += 1;\n }\n // PIR\n if (channel_id === 0x05 && channel_type === 0x6a) {\n decoded.activity = parseBytesToInt(input, i, 2, false);\n i += 2;\n }\n \n // LIGHT\n if (channel_id === 0x06 && channel_type === 0x65) {\n decoded.illumination = parseBytesToInt(input, i, 2, false);\n decoded.infrared_and_visible = parseBytesToInt(input, i+2, 2, false);\n decoded.infrared = parseBytesToInt(input, i+4, 2, false);\n i += 6;\n }\n }\n\n output.telemetry = [{\n ts: timestamp,\n values: decoded\n }, historyData];\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\n.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\nvar gatewayInfo = getGatewayInfo();\nvar addDataToTelemetry = {};\naddDataToTelemetry.snr = gatewayInfo.snr;\naddDataToTelemetry.rssi = gatewayInfo.rssi;\naddDataToTelemetry.channel = gatewayInfo.channel;\naddDataToTelemetry.rfChain = gatewayInfo.rfChain;\naddDataToTelemetry.fCnt = data.fCnt;\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\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 timestamp = -1;\n if (dateString != null) {\n timestamp = new Date(dateString).getTime();\n if (timestamp == -1) {\n var secondsSeparatorIndex = dateString.lastIndexOf(\n '.') + 1;\n var millisecondsEndIndex = dateString.lastIndexOf(\n '+');\n if (millisecondsEndIndex == -1) {\n millisecondsEndIndex = dateString.lastIndexOf(\n 'Z');\n }\n if (millisecondsEndIndex == -1) {\n millisecondsEndIndex = dateString.lastIndexOf(\n '-');\n }\n if (millisecondsEndIndex == -1) {\n if (dateString.length >= secondsSeparatorIndex +\n 3) {\n dateString = dateString.substring(0,\n secondsSeparatorIndex + 3);\n }\n } else {\n dateString = dateString.substring(0,\n secondsSeparatorIndex + 3) +\n dateString.substring(millisecondsEndIndex,\n dateString.length);\n }\n timestamp = new Date(dateString).getTime();\n }\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 = addDataToMultipleTelemetries(telemetry, addDataToTelemetry);\n }\n else if (telemetry.size == 1) {\n telemetry = addDataToSingleTelemetry(telemetry, addDataToTelemetry);\n }\n else {\n telemetry.add(addDataToTelemetry);\n }\n \n return telemetry;\n}\n\nfunction addDataToMultipleTelemetries(telemetry, addDataToTelemetry) {\n foreach(element : addDataToTelemetry.entrySet()) {\n if(!telemetry[0][\"values\"].keys.contains(element.key)) {\n telemetry[0][\"values\"][element.key] = element.value;\n }\n if (!telemetry[1][\"values\"].keys.contains(element.key)) {\n telemetry[1][\"values\"][element.key] = element.value;\n }\n }\n \n return telemetry;\n}\n\nfunction addDataToSingleTelemetry(telemetry, addDataToTelemetry) {\n foreach(element : addDataToTelemetry.entrySet()) {\n if(!telemetry[0][\"values\"].keys.contains(element.key)) {\n telemetry[0][\"values\"][element.key] = element.value;\n }\n }\n \n return telemetry;\n}", + "encoder": null, + "tbelEncoder": null, + "updateOnlyKeys": [ + "tenantId", + "tenantName", + "applicationId", + "applicationName", + "deviceProfileId", + "deviceProfileName", + "devAddr", + "fPort", + "frequency", + "bandwidth", + "spreadingFactor", + "codeRate", + "battery", + "channel", + "rfChain", + "eui", + "beep" + ] + }, + "additionalInfo": { + "description": "" + }, + "edgeTemplate": false +} \ No newline at end of file diff --git a/VENDORS/Milesight/AM104/ChirpStack/uplink/metadata.json b/VENDORS/Milesight/AM104/ChirpStack/uplink/metadata.json new file mode 100644 index 00000000..db001d8c --- /dev/null +++ b/VENDORS/Milesight/AM104/ChirpStack/uplink/metadata.json @@ -0,0 +1,4 @@ +{ + "integrationName": "ChirpStack integration", + "includeGatewayInfo" : false +} \ No newline at end of file diff --git a/VENDORS/Milesight/AM104/ChirpStack/uplink/payload.json b/VENDORS/Milesight/AM104/ChirpStack/uplink/payload.json new file mode 100644 index 00000000..e6954e78 --- /dev/null +++ b/VENDORS/Milesight/AM104/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": "AXVcA2c0AQRoZQVqSQAGZRwAeQAUAA==", + "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/AM104/ChirpStack/uplink/result.json b/VENDORS/Milesight/AM104/ChirpStack/uplink/result.json new file mode 100644 index 00000000..67f0ee99 --- /dev/null +++ b/VENDORS/Milesight/AM104/ChirpStack/uplink/result.json @@ -0,0 +1,31 @@ +{ + "deviceName": "AM104 Device name", + "deviceType": "AM104", + "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, + "activity": 73, + "illumination": 28, + "infrared_and_visible": 121, + "infrared": 20 + } + }] +} \ No newline at end of file diff --git a/VENDORS/Milesight/AM104/Loriot/uplink/converter.json b/VENDORS/Milesight/AM104/Loriot/uplink/converter.json new file mode 100644 index 00000000..06bc9d1b --- /dev/null +++ b/VENDORS/Milesight/AM104/Loriot/uplink/converter.json @@ -0,0 +1,25 @@ +{ + "name": "Uplink data converter for Loriot integration AM-103", + "type": "UPLINK", + "debugMode": true, + "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 = \"AM104 \" + data.EUI;\nvar deviceType = \"AM104\";\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 // --- Decoding code --- //\n var decoded = {};\n var historyData = {};\n for (var i = 0; i < input.length - 2; ) {\n var channel_id = input[i++] & 0xff;\n var channel_type = input[i++] & 0xff;\n // BATTERY\n if(channel_id === 0x01 && channel_type === 0x75) {\n decoded.battery = input[i]; \n i += 1;\n }\n // TEMPERATURE\n if (channel_id === 0x03 && channel_type === 0x67) {\n // ℃\n decoded.temperature = parseBytesToInt(input, i, 2, false) / 10;\n // ℉\n // decoded.temperature = parseBytesToInt(input, i, 2, false) / 10 * 1.8 + 32;\n i += 2;\n }\n // HUMIDITY\n if (channel_id === 0x04 && channel_type === 0x68) {\n decoded.humidity = parseBytesToInt(input, i, 1, false) / 2;\n i += 1;\n }\n \n // PIR\n if (channel_id === 0x05 && channel_type === 0x6a) {\n decoded.activity = parseBytesToInt(input, i, 2, false);\n i += 2;\n }\n \n // LIGHT\n if (channel_id === 0x06 && channel_type === 0x65) {\n decoded.illumination = parseBytesToInt(input, i, 2, false);\n decoded.infrared_and_visible = parseBytesToInt(input, i+2, 2, false);\n decoded.infrared = parseBytesToInt(input, i+4, 2, false);\n i += 6;\n }\n }\n \n output.telemetry = [{\n ts: timestamp,\n values: decoded\n }, historyData];\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 addDataToTelemetry = {};\naddDataToTelemetry.rssi = data.rssi;\naddDataToTelemetry.seqno = data.seqno;\naddDataToTelemetry.snr = data.snr;\naddDataToTelemetry.ack = data.ack;\naddDataToTelemetry.toa = data.toa;\naddDataToTelemetry.fCnt = data.fcnt;\n\nvar isIncludeGatewayInfo = metadata[\"includeGatewayInfo\"];\nif(isIncludeGatewayInfo == true) {\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 includeGatewayInfo = [\"ts\", \"gweui\", \"rssi\"];\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 gatewayInfoMsg = {\n deviceName: gatewayDeviceNamePrefix + gatewayInfo.gweui,\n deviceType: gatewayDeviceType,\n telemetry: [{\n \"ts\": parseDateToTimestamp(gatewayInfo.ts),\n \"values\": getDataList(gatewayInfo, includeGatewayInfo)\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 parseDateToTimestamp(dateString) {\n var timestamp = -1;\n if (dateString != null) {\n \n timestamp = new Date(dateString).getTime();\n if (timestamp == -1) {\n var secondsSeparatorIndex = dateString.lastIndexOf('.') + 1;\n var millisecondsEndIndex = dateString.lastIndexOf('+');\n if (millisecondsEndIndex == -1) {\n millisecondsEndIndex = dateString.lastIndexOf('Z');\n }\n if (millisecondsEndIndex == -1) {\n millisecondsEndIndex = dateString.lastIndexOf('-');\n }\n if (millisecondsEndIndex == -1) {\n if (dateString.length >= secondsSeparatorIndex + 3) {\n dateString = dateString.substring(0, secondsSeparatorIndex + 3);\n }\n } else {\n dateString = dateString.substring(0, secondsSeparatorIndex + 3) +\n dateString.substring(millisecondsEndIndex, dateString.length);\n }\n \n timestamp = new Date(dateString).getTime();\n }\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 processTelemetryData(telemetry, addDataToTelemetry) {\n if (telemetry.size > 1) {\n telemetry = addDataToMultipleTelemetries(telemetry, addDataToTelemetry);\n }\n else if (telemetry.size == 1) {\n telemetry = addDataToSingleTelemetry(telemetry, addDataToTelemetry);\n }\n else {\n telemetry.add(addDataToTelemetry);\n }\n \n return telemetry;\n}\n\nfunction addDataToMultipleTelemetries(telemetry, addDataToTelemetry) {\n foreach(element : addDataToTelemetry.entrySet()) {\n if(!telemetry[0][\"values\"].keys.contains(element.key)) {\n telemetry[0][\"values\"][element.key] = element.value;\n }\n if (!telemetry[1][\"values\"].keys.contains(element.key)) {\n telemetry[1][\"values\"][element.key] = element.value;\n }\n }\n \n return telemetry;\n}\n\nfunction addDataToSingleTelemetry(telemetry, addDataToTelemetry) {\n foreach(element : addDataToTelemetry.entrySet()) {\n if(!telemetry[0][\"values\"].keys.contains(element.key)) {\n telemetry[0][\"values\"][element.key] = element.value;\n }\n }\n \n return telemetry;\n}", + "encoder": null, + "tbelEncoder": null, + "updateOnlyKeys": [ + "fPort", + "battery", + "ack", + "beep", + "eui", + "frequency", + "dr" + ] + }, + "additionalInfo": { + "description": "" + }, + "edgeTemplate": false +} \ No newline at end of file diff --git a/VENDORS/Milesight/AM104/Loriot/uplink/metadata.json b/VENDORS/Milesight/AM104/Loriot/uplink/metadata.json new file mode 100644 index 00000000..e76cdcfb --- /dev/null +++ b/VENDORS/Milesight/AM104/Loriot/uplink/metadata.json @@ -0,0 +1,4 @@ +{ + "integrationName": "Loriot integration", + "includeGatewayInfo" : false +} \ No newline at end of file diff --git a/VENDORS/Milesight/AM104/Loriot/uplink/payload.json b/VENDORS/Milesight/AM104/Loriot/uplink/payload.json new file mode 100644 index 00000000..27e2e0a6 --- /dev/null +++ b/VENDORS/Milesight/AM104/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": "01755C03673401046865056A490006651C0079001400" +} \ No newline at end of file diff --git a/VENDORS/Milesight/AM104/Loriot/uplink/result.json b/VENDORS/Milesight/AM104/Loriot/uplink/result.json new file mode 100644 index 00000000..99ddaea4 --- /dev/null +++ b/VENDORS/Milesight/AM104/Loriot/uplink/result.json @@ -0,0 +1,21 @@ +[{ + "deviceName": "AM104 1000000000000001", + "deviceType": "AM104", + "attributes": { + "eui": "1000000000000001", + "fPort": 85, + "frequency": 867500000 + }, + "telemetry": [{ + "ts": 1684478801936, + "values": { + "battery": 92, + "temperature": 30.8, + "humidity": 50.5, + "activity": 73, + "illumination": 28, + "infrared_and_visible": 121, + "infrared": 20 + } + }] +}] \ No newline at end of file diff --git a/VENDORS/Milesight/AM104/ThingsStackCommunity/uplink/converter.json b/VENDORS/Milesight/AM104/ThingsStackCommunity/uplink/converter.json new file mode 100644 index 00000000..29aab1dd --- /dev/null +++ b/VENDORS/Milesight/AM104/ThingsStackCommunity/uplink/converter.json @@ -0,0 +1,35 @@ +{ + "name": "Uplink data converter for The Things Stack Community integration AM-104", + "type": "UPLINK", + "debugMode": true, + "configuration": { + "scriptLang": "TBEL", + "decoder": null, + "tbelDecoder": "var data = decodeToJson(payload);\n\nvar deviceName = \"AM104 \" + data.end_device_ids.device_id;\nvar deviceType = \"AM104\";\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: {}, telemetry: {}\n };\n \n // --- Decoding code --- //\n var decoded = {};\n var historyData = {};\n for (var i = 0; i < input.length - 2; ) {\n var channel_id = input[i++] & 0xff;\n var channel_type = input[i++] & 0xff;\n // BATTERY\n if (channel_id === 0x01 && channel_type === 0x75) {\n decoded.battery = input[i];\n i += 1;\n }\n // TEMPERATURE\n if (channel_id === 0x03 && channel_type === 0x67) {\n // ℃\n decoded.temperature = parseBytesToInt(input, i, 2, false) / 10;\n i += 2;\n // ℉\n // decoded.temperature = parseBytesToInt(input, i, 2, false) / 10 * 1.8 + 32;\n // i +=2;\n }\n // HUMIDITY\n if (channel_id === 0x04 && channel_type === 0x68) {\n decoded.humidity = parseBytesToInt(input, i, 1, false) / 2;\n i += 1;\n }\n // PIR\n if (channel_id === 0x05 && channel_type === 0x6a) {\n decoded.activity = parseBytesToInt(input, i, 2, false);\n i += 2;\n }\n // LIGHT\n if (channel_id === 0x06 && channel_type === 0x65) {\n decoded.illumination = parseBytesToInt(input, i, 2, false);\n decoded.infrared_and_visible = parseBytesToInt(input, i+2, 2, false);\n decoded.infrared = parseBytesToInt(input, i+4, 2, false);\n i += 6;\n }\n }\n \n output.telemetry = [{\n ts: timestamp,\n values: decoded\n }, historyData];\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 timestamp = new Date(dateString).getTime();\n var timestamp = -1;\n if (dateString != null) {\n timestamp = new Date(dateString).getTime();\n if (timestamp == -1) {\n var secondsSeparatorIndex = dateString.lastIndexOf('.') + 1;\n var millisecondsEndIndex = dateString.lastIndexOf('+');\n if (millisecondsEndIndex == -1) {\n millisecondsEndIndex = dateString.lastIndexOf('Z');\n }\n if (millisecondsEndIndex == -1) {\n millisecondsEndIndex = dateString.lastIndexOf('-');\n }\n if (millisecondsEndIndex == -1) {\n if (dateString.length >= secondsSeparatorIndex + 3) {\n dateString = dateString.substring(0, secondsSeparatorIndex + 3);\n }\n } else {\n dateString = dateString.substring(0, secondsSeparatorIndex + 3) +\n dateString.substring(millisecondsEndIndex, dateString.length);\n }\n timestamp = new Date(dateString).getTime();\n }\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 = addDataToMultipleTelemetries(telemetry, addDataToTelemetry);\n }\n else if (telemetry.size == 1) {\n telemetry = addDataToSingleTelemetry(telemetry, addDataToTelemetry);\n }\n else {\n telemetry.add(addDataToTelemetry);\n }\n \n return telemetry;\n}\n\nfunction addDataToMultipleTelemetries(telemetry, addDataToTelemetry) {\n foreach(element : addDataToTelemetry.entrySet()) {\n if(!telemetry[0][\"values\"].keys.contains(element.key)) {\n telemetry[0][\"values\"][element.key] = element.value;\n }\n if (!telemetry[1][\"values\"].keys.contains(element.key)) {\n telemetry[1][\"values\"][element.key] = element.value;\n }\n }\n \n return telemetry;\n}\n\nfunction addDataToSingleTelemetry(telemetry, addDataToTelemetry) {\n foreach(element : addDataToTelemetry.entrySet()) {\n if(!telemetry[0][\"values\"].keys.contains(element.key)) {\n telemetry[0][\"values\"][element.key] = element.value;\n }\n }\n \n return telemetry;\n}", + "encoder": null, + "tbelEncoder": null, + "updateOnlyKeys": [ + "fPort", + "bandwidth", + "frequency", + "net_id", + "cluster_id", + "cluster_address", + "device_id", + "join_eui", + "battery", + "eui", + "beep", + "channel", + "applicationId", + "devAddr", + "spreadingFactor", + "codeRate", + "tenantId" + ] + }, + "additionalInfo": { + "description": "" + }, + "edgeTemplate": false +} \ No newline at end of file diff --git a/VENDORS/Milesight/AM104/ThingsStackCommunity/uplink/metadata.json b/VENDORS/Milesight/AM104/ThingsStackCommunity/uplink/metadata.json new file mode 100644 index 00000000..bba449cd --- /dev/null +++ b/VENDORS/Milesight/AM104/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/AM104/ThingsStackCommunity/uplink/payload.json b/VENDORS/Milesight/AM104/ThingsStackCommunity/uplink/payload.json new file mode 100644 index 00000000..000513a5 --- /dev/null +++ b/VENDORS/Milesight/AM104/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": "AXVcA2c0AQRoZQVqSQAGZRwAeQAUAA==", + "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/AM104/ThingsStackCommunity/uplink/result.json b/VENDORS/Milesight/AM104/ThingsStackCommunity/uplink/result.json new file mode 100644 index 00000000..ace35d73 --- /dev/null +++ b/VENDORS/Milesight/AM104/ThingsStackCommunity/uplink/result.json @@ -0,0 +1,32 @@ +{ + "deviceName": "AM104 eui-1000000000000001", + "deviceType": "AM104", + "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, + "activity": 73, + "illumination": 28, + "infrared_and_visible": 121, + "infrared": 20 + } + }] +} \ No newline at end of file diff --git a/VENDORS/Milesight/AM104/ThingsStackIndustries/uplink/converter.json b/VENDORS/Milesight/AM104/ThingsStackIndustries/uplink/converter.json new file mode 100644 index 00000000..a1122e3d --- /dev/null +++ b/VENDORS/Milesight/AM104/ThingsStackIndustries/uplink/converter.json @@ -0,0 +1,36 @@ +{ + "name": "Uplink data converter for The Things Stack Industries integration AM-104", + "type": "UPLINK", + "debugMode": true, + "configuration": { + "scriptLang": "TBEL", + "decoder": null, + "tbelDecoder": "var data = decodeToJson(payload);\n\nvar deviceName = \"AM104 \" + data.end_device_ids.device_id;\nvar deviceType = \"AM104\";\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 // --- Timestamp parsing\n var decoded = {};\n var historyData = {};\n for (var i = 0; i < input.length - 2; ) {\n var channel_id = input[i++] & 0xff;\n var channel_type = input[i++] & 0xff;\n // BATTERY\n if(channel_id === 0x01 && channel_type === 0x75) {\n decoded.battery = input[i]; \n i += 1;\n }\n // TEMPERATURE\n if (channel_id === 0x03 && channel_type === 0x67) {\n // ℃\n decoded.temperature = parseBytesToInt(input, i, 2, false) / 10;\n // ℉\n // decoded.temperature = parseBytesToInt(input, i, 2, false) / 10 * 1.8 + 32;\n i += 2;\n }\n // HUMIDITY\n if (channel_id === 0x04 && channel_type === 0x68) {\n decoded.humidity = parseBytesToInt(input, i, 1, false) / 2;\n i += 1;\n }\n // PIR\n if (channel_id === 0x05 && channel_type === 0x6a) {\n decoded.activity = parseBytesToInt(input, i, 2, false);\n i += 2;\n }\n // LIGHT\n if (channel_id === 0x06 && channel_type === 0x65) {\n decoded.illumination = parseBytesToInt(input, i, 2, false);\n decoded.infrared_and_visible = parseBytesToInt(input, i+2, 2, false);\n decoded.infrared = parseBytesToInt(input, i+4, 2, false);\n i += 6;\n }\n }\n \n output.telemetry = [{\n ts: timestamp,\n values: decoded\n }, historyData];\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_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\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\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 timestamp = new Date(dateString).getTime();\n var timestamp = -1;\n if (dateString != null) {\n timestamp = new Date(dateString).getTime();\n if (timestamp == -1) {\n var secondsSeparatorIndex = dateString.lastIndexOf('.') + 1;\n var millisecondsEndIndex = dateString.lastIndexOf('+');\n if (millisecondsEndIndex == -1) {\n millisecondsEndIndex = dateString.lastIndexOf('Z');\n }\n if (millisecondsEndIndex == -1) {\n millisecondsEndIndex = dateString.lastIndexOf('-');\n }\n if (millisecondsEndIndex == -1) {\n if (dateString.length >= secondsSeparatorIndex + 3) {\n dateString = dateString.substring(0, secondsSeparatorIndex + 3);\n }\n } else {\n dateString = dateString.substring(0, secondsSeparatorIndex + 3) +\n dateString.substring(millisecondsEndIndex, dateString.length);\n }\n timestamp = new Date(dateString).getTime();\n }\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 = addDataToMultipleTelemetries(telemetry, addDataToTelemetry);\n }\n else if (telemetry.size == 1) {\n telemetry = addDataToSingleTelemetry(telemetry, addDataToTelemetry);\n }\n else {\n telemetry.add(addDataToTelemetry);\n }\n \n return telemetry;\n}\n\nfunction addDataToMultipleTelemetries(telemetry, addDataToTelemetry) {\n foreach(element : addDataToTelemetry.entrySet()) {\n if(!telemetry[0][\"values\"].keys.contains(element.key)) {\n telemetry[0][\"values\"][element.key] = element.value;\n }\n if (!telemetry[1][\"values\"].keys.contains(element.key)) {\n telemetry[1][\"values\"][element.key] = element.value;\n }\n }\n \n return telemetry;\n}\n\nfunction addDataToSingleTelemetry(telemetry, addDataToTelemetry) {\n foreach(element : addDataToTelemetry.entrySet()) {\n if(!telemetry[0][\"values\"].keys.contains(element.key)) {\n telemetry[0][\"values\"][element.key] = element.value;\n }\n }\n \n return telemetry;\n}", + "encoder": null, + "tbelEncoder": null, + "updateOnlyKeys": [ + "fPort", + "bandwidth", + "frequency", + "net_id", + "cluster_id", + "cluster_address", + "tenant_address", + "device_id", + "join_eui", + "battery", + "eui", + "beep", + "channel", + "devAddr", + "spreadingFactor", + "codeRate", + "tenantId", + "applicationId" + ] + }, + "additionalInfo": { + "description": "" + }, + "edgeTemplate": false +} \ No newline at end of file diff --git a/VENDORS/Milesight/AM104/ThingsStackIndustries/uplink/metadata.json b/VENDORS/Milesight/AM104/ThingsStackIndustries/uplink/metadata.json new file mode 100644 index 00000000..f4e6219c --- /dev/null +++ b/VENDORS/Milesight/AM104/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/AM104/ThingsStackIndustries/uplink/payload.json b/VENDORS/Milesight/AM104/ThingsStackIndustries/uplink/payload.json new file mode 100644 index 00000000..a5f948c0 --- /dev/null +++ b/VENDORS/Milesight/AM104/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": "AXVcA2c0AQRoZQVqSQAGZRwAeQAUAA==", + "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/AM104/ThingsStackIndustries/uplink/result.json b/VENDORS/Milesight/AM104/ThingsStackIndustries/uplink/result.json new file mode 100644 index 00000000..282d6150 --- /dev/null +++ b/VENDORS/Milesight/AM104/ThingsStackIndustries/uplink/result.json @@ -0,0 +1,32 @@ +{ + "deviceName": "AM104 eui-1000000000000001", + "deviceType": "AM104", + "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_adress": "eu1.cloud.thethings.industries", + "bandwidth": 125000, + "frequency": "868500000" + }, + "telemetry": [{ + "ts": 1684398325906, + "values": { + "battery": 92, + "temperature": 30.8, + "humidity": 50.5, + "activity": 73, + "illumination": 28, + "infrared_and_visible": 121, + "infrared": 20 + } + }] +} \ No newline at end of file diff --git a/VENDORS/Milesight/AM104/guide.md b/VENDORS/Milesight/AM104/guide.md new file mode 100644 index 00000000..7a070d85 --- /dev/null +++ b/VENDORS/Milesight/AM104/guide.md @@ -0,0 +1,24 @@ +## 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 | +| Activity | 0x05 | 0x6A | 2 | activity(2B) | +| Illumination | 0x06 | 0x65 | 6 | illumination(6B)
illumination, unit: lux | + +## Example + +```json +// 01755C 03673401 046865 056A4900 06651C0079001400 +{ + "battery": 92, + "temperature": 30.8, + "humidity": 50.5, + "activity": 73, + "illumination": 28, + "infrared": 20, + "infrared_and_visible": 121 +} +``` \ No newline at end of file diff --git a/VENDORS/Milesight/AM104/info.json b/VENDORS/Milesight/AM104/info.json new file mode 100644 index 00000000..afeb6262 --- /dev/null +++ b/VENDORS/Milesight/AM104/info.json @@ -0,0 +1,5 @@ +{ + "url": "https://www.milesight.com/iot/product/lorawan-sensor/am104", + "label": "Milesight 4-in-1 Indoor Air Quality Sensor for Healthier Buildings", + "description": "Milesight AM104 is a compact indoor ambience monitoring sensor designed to measure motion, humidity, temperature, and light. With its long-lasting battery life, wall-mounted installation." +} \ No newline at end of file diff --git a/VENDORS/Milesight/AM104/photo.png b/VENDORS/Milesight/AM104/photo.png new file mode 100644 index 0000000000000000000000000000000000000000..76870594e0325ff5ad22e0d2c3320962f16703fe GIT binary patch literal 14907 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 literal 0 HcmV?d00001