From a8454a1f0a3f8fc5e801a9057b4c8ef585d81075 Mon Sep 17 00:00:00 2001 From: r-roca23 Date: Tue, 9 Jun 2026 12:37:17 +0200 Subject: [PATCH] fixed dashboard --- dingtek-dt311/plugin.json | 844 ++++++++++++++++++++++++++++++++------ 1 file changed, 717 insertions(+), 127 deletions(-) diff --git a/dingtek-dt311/plugin.json b/dingtek-dt311/plugin.json index 217ef1916..7cea8b461 100644 --- a/dingtek-dt311/plugin.json +++ b/dingtek-dt311/plugin.json @@ -1,134 +1,724 @@ { - "name": "dingtek_dt311", - "version": "1.0.0", - "description": "The CNDingtek dt311 is temperature and humidity sensor for indoor using.", - "author": "Thinger.io", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/thinger-io/plugins.git", - "directory": "dingtek-dt311" - }, - "metadata": { - "name": "Dingtek DT311", + "name": "dingtek-dt311", + "version": "1.0.0", "description": "The CNDingtek dt311 is temperature and humidity sensor for indoor using.", + "author": "Thinger.io", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/thinger-io/plugins.git", + "directory": "dingtek-dt311" + }, + "metadata": { + "name": "Dingtek DT311", + "description": "The CNDingtek dt311 is temperature and humidity sensor for indoor using.", "image": "assets/dt311.png", "category": "devices", "vendor": "dingtek" - }, - "resources": { - "products": [ - { - "description": "The CNDingtek dt311 is temperature and humidity sensor for indoor using.", - "enabled": true, - "name": "Dingtek DT311", - "product": "dingtek_dt311", - "profile": { - "api": { - "downlink": { - "enabled": true, - "handle_connectivity": false, - "request": { - "data": { - "path": "/downlink", - "payload": "{\n \"data\" : \"{{payload.data=\"\"}}\",\n \"port\" : {{payload.port=3}},\n \"priority\": {{payload.priority=3}},\n \"confirmed\" : {{payload.confirmed=false}},\n \"uplink\" : {{property.uplink}} \n}", - "payload_function": "", - "payload_type": "", - "plugin": "{{property.uplink.source}}", - "target": "plugin_endpoint" - } - } - }, - "uplink": { - "device_id_resolver": "getId", - "enabled": true, - "handle_connectivity": true, - "request": { - "data": { - "payload": "{{payload}}", - "payload_function": "", - "payload_type": "source_payload", - "resource_stream": "uplink", - "target": "resource_stream" + }, + "resources": { + "products": [ + { + "config": { + "icons": [] + }, + "description": "The CNDingtek dt311 is temperature and humidity sensor for indoor using.", + "enabled": true, + "name": "Dingtek DT311", + "product": "dingtek_dt311", + "profile": { + "api": { + "downlink": { + "enabled": true, + "handle_connectivity": false, + "request": { + "data": { + "path": "/downlink", + "payload": "{\n \"data\" : \"{{payload.data=\"\"}}\",\n \"port\" : {{payload.port=3}},\n \"priority\": {{payload.priority=3}},\n \"confirmed\" : {{payload.confirmed=false}},\n \"uplink\" : {{property.uplink}} \n}", + "payload_function": "", + "payload_type": "", + "plugin": "{{property.uplink.source}}", + "target": "plugin_endpoint" + } + } + }, + "uplink": { + "device_id_resolver": "getId", + "enabled": true, + "handle_connectivity": true, + "request": { + "data": { + "payload": "{{payload}}", + "payload_function": "", + "payload_type": "source_payload", + "resource_stream": "uplink", + "target": "resource_stream" + } + } + } + }, + "autoprovisions": { + "device_autoprovisioning": { + "config": { + "mode": "pattern", + "pattern": "dt311_[0-9A-Fa-f]{16}" + }, + "enabled": true + } + }, + "buckets": { + "dingtek_dt311_data": { + "backend": "mongodb", + "data": { + "payload": "{{payload}}", + "payload_function": "", + "payload_type": "source_payload", + "resource_stream": "uplink_decoded", + "source": "resource_stream" + }, + "enabled": true, + "retention": { + "period": 3, + "unit": "months" + }, + "tags": [] + } + }, + "code": { + "code": "function decodeThingerUplink(thingerData) {\n // 0. If data has already been decoded, we will return it\n if (thingerData.decodedPayload) return thingerData.decodedPayload;\n \n // 1. Extract and Validate Input\n // We need 'payload' (hex string) and 'fPort' (integer)\n const hexPayload = thingerData.payload || \"\";\n const port = thingerData.fPort || 1;\n\n // 2. Convert Hex String to Byte Array\n const bytes = [];\n for (let i = 0; i < hexPayload.length; i += 2) {\n bytes.push(parseInt(hexPayload.substr(i, 2), 16));\n }\n\n // 3. Dynamic Function Detection and Execution\n \n // CASE A: (The Things Stack v3)\n if (typeof decodeUplink === 'function') {\n try {\n const input = {\n bytes: bytes,\n fPort: port\n };\n var result = decodeUplink(input);\n \n if (result.data) return result.data;\n\n return result; \n } catch (e) {\n console.error(\"Error inside decodeUplink:\", e);\n throw e;\n }\n }\n\n // CASE B: Legacy TTN (v2)\n else if (typeof Decoder === 'function') {\n try {\n return Decoder(bytes, port);\n } catch (e) {\n console.error(\"Error inside Decoder:\", e);\n throw e;\n }\n }\n\n // CASE C: No decoder found\n else {\n throw new Error(\"No compatible TTN decoder function (decodeUplink or Decoder) found in scope.\");\n }\n}\n\n\n// TTN decoder\n//IEEE754 hex to float convert\nfunction hex2float(num) {\n var sign = num & 0x80000000 ? -1 : 1;\n var exponent = ((num >> 23) & 0xff) - 127;\n var mantissa = 1 + (num & 0x7fffff) / 0x7fffff;\n return sign * mantissa * Math.pow(2, exponent);\n}\n\nfunction decodeUplink(input) {\n if (input.fPort != 3) {\n return {\n errors: ['unknown FPort'],\n };\n }\n switch (input.bytes[3]) {\n case 1:\n case 2:\n var temp_sign = input.bytes[8];\n var temp_abs = (input.bytes[9] >> 4) * 10 + (input.bytes[9] & 0x0F) + (input.bytes[10] >> 4) / 10 + (input.bytes[10] & 0x0F) / 100;\n var humi_abs = (input.bytes[11] >> 4) * 10 + (input.bytes[11] & 0x0F) + (input.bytes[12] >> 4) / 10 + (input.bytes[12] & 0x0F) / 100;\n var temperature_temp=(temp_sign == 0 )? temp_abs : (0 - temp_abs);\n return {\n // Decoded data\n data: {\n temperature: temperature_temp.toFixed(2),\n humidity: humi_abs.toFixed(2),\n alarmHighTemperature: (input.bytes[13] & 0x10) ? true : false,\n alarmLowTemperature: (input.bytes[13] & 0x01) ? true : false,\n alarmHighHumidity: (input.bytes[14] & 0x10) ? true : false,\n alarmLowHumidity: (input.bytes[14] & 0x01) ? true : false,\n alarmBattery: (input.bytes[7] & 0x01) ? true : false,\n volt: ((input.bytes[5] << 8) + input.bytes[6]) / 100,\n frameCounter: (input.bytes[15] << 8) + input.bytes[16],\n },\n };\n case 3:\n var high_temp_abs = input.bytes[10];\n var low_temp_abs = input.bytes[12];\n return {\n // Decoded data\n data: {\n firmware: input.bytes[5] + \".\" + input.bytes[6],\n uploadInterval: input.bytes[7],\n detectInterval: input.bytes[8],\n highTemperatureThreshold: input.bytes[9] ? 256 - high_temp_abs : high_temp_abs,\n lowTemperatureThreshold: input.bytes[11] ? 0 - low_temp_abs : low_temp_abs,\n highHumidityThreshold: input.bytes[13],\n lowHumidityThreshold: input.bytes[14],\n batteryThreshold: input.bytes[16],\n workMode: input.bytes[15],\n },\n };\n default:\n return {\n errors: ['wrong length'],\n };\n }\n}\n\n// Encode downlink function.\n//\n// Input is an object with the following fields:\n// - data = Object representing the payload that must be encoded.\n// - variables = Object containing the configured device variables.\n//\n// Output must be an object with the following fields:\n// - bytes = Byte array containing the downlink payload.\nfunction encodeDownlink(input) {\n if (input.data.uploadInterval != null && !isNaN(input.data.uploadInterval)) {\n var uploadInterval = input.data.uploadInterval;\n var uploadInterval_high = uploadInterval.toString(16).padStart(2, '0').toUpperCase()[0].charCodeAt(0); \n var uploadInterval_low = uploadInterval.toString(16).padStart(2, '0').toUpperCase()[1].charCodeAt(0);\n if (uploadInterval > 255 || uploadInterval < 1) {\n return {\n errors: ['upload interval range 1-255 hours.'],\n };\n } else {\n return {\n // LoRaWAN FPort used for the downlink message\n fPort: 3,\n // Encoded bytes\n bytes: [0x38, 0x30, 0x30, 0x32, 0x39, 0x39, 0x39, 0x39, 0x30, 0x31, uploadInterval_high, uploadInterval_low, 0x38, 0x31],\n };\n }\n }\n if (input.data.detectInterval != null && !isNaN(input.data.detectInterval)) {\n var detectInterval = input.data.detectInterval;\n var detectInterval_high = detectInterval.toString(16).padStart(2, '0').toUpperCase()[0].charCodeAt(0);\n var detectInterval_low = detectInterval.toString(16).padStart(2, '0').toUpperCase()[1].charCodeAt(0);\n if (detectInterval > 255 || detectInterval < 1) {\n return {\n errors: ['detection interval range 1-255 minutes.'],\n };\n } else {\n return {\n // LoRaWAN FPort used for the downlink message\n fPort: 3,\n // Encoded bytes\n bytes: [0x38, 0x30, 0x30, 0x32, 0x39, 0x39, 0x39, 0x39, 0x30, 0x38, detectInterval_high, detectInterval_low, 0x38, 0x31],\n };\n }\n }\n if (input.data.highTemperatureThreshold != null && !isNaN(input.data.highTemperatureThreshold)) {\n var highTemperatureThreshold = input.data.highTemperatureThreshold<0?0-input.data.highTemperatureThreshold:input.data.highTemperatureThreshold;\n var highTemperatureSign=input.data.highTemperatureThreshold<0?0x31:0x30;\n var highTemperatureThreshold_high = highTemperatureThreshold.toString(16).padStart(2, '0').toUpperCase()[0].charCodeAt(0);\n var highTemperatureThreshold_low = highTemperatureThreshold.toString(16).padStart(2, '0').toUpperCase()[1].charCodeAt(0);\n if (input.data.highTemperatureThreshold > 85 || input.data.highTemperatureThreshold < -30) {\n return {\n errors: ['High temperature alarm threshold range -30~+85.'],\n };\n } else {\n return {\n // LoRaWAN FPort used for the downlink message\n fPort: 3,\n // Encoded bytes\n bytes: [0x38, 0x30, 0x30, 0x32, 0x39, 0x39, 0x39, 0x39, 0x30, 0x32, 0x30,highTemperatureSign,highTemperatureThreshold_high, highTemperatureThreshold_low, 0x38, 0x31],\n };\n }\n }\n if (input.data.lowTemperatureThreshold != null && !isNaN(input.data.lowTemperatureThreshold)) {\n var lowTemperatureThreshold = input.data.lowTemperatureThreshold<0?0-input.data.lowTemperatureThreshold:input.data.lowTemperatureThreshold;\n var lowTemperatureSign=input.data.lowTemperatureThreshold<0?0x31:0x30;\n var lowTemperatureThreshold_high = lowTemperatureThreshold.toString(16).padStart(2, '0').toUpperCase()[0].charCodeAt(0);\n var lowTemperatureThreshold_low = lowTemperatureThreshold.toString(16).padStart(2, '0').toUpperCase()[1].charCodeAt(0);\n if (input.data.lowTemperatureThreshold > 85 || input.data.lowTemperatureThreshold < -30) {\n return {\n errors: ['Low temperature alarm threshold range -30~+85.'],\n };\n } else {\n return {\n // LoRaWAN FPort used for the downlink message\n fPort: 3,\n // Encoded bytes\n bytes: [0x38, 0x30, 0x30, 0x32, 0x39, 0x39, 0x39, 0x39, 0x30, 0x33, 0x30,lowTemperatureSign,lowTemperatureThreshold_high, lowTemperatureThreshold_low, 0x38, 0x31],\n };\n }\n }\n \n if (input.data.highHumidityThreshold != null && !isNaN(input.data.highHumidityThreshold)) {\n var highHumidityThreshold = input.data.highHumidityThreshold<0?0-input.data.highHumidityThreshold:input.data.highHumidityThreshold; \n var highHumidityThreshold_high = highHumidityThreshold.toString(16).padStart(2, '0').toUpperCase()[0].charCodeAt(0);\n var highHumidityThreshold_low = highHumidityThreshold.toString(16).padStart(2, '0').toUpperCase()[1].charCodeAt(0);\n if (input.data.highHumidityThreshold > 100 || input.data.highHumidityThreshold < 0) {\n return {\n errors: ['High humidity alarm threshold range 0~100.'],\n };\n } else {\n return {\n // LoRaWAN FPort used for the downlink message\n fPort: 3,\n // Encoded bytes\n bytes: [0x38, 0x30, 0x30, 0x32, 0x39, 0x39, 0x39, 0x39, 0x30, 0x34, highHumidityThreshold_high, highHumidityThreshold_low, 0x38, 0x31],\n };\n }\n }\n if (input.data.lowHumidityThreshold != null && !isNaN(input.data.lowHumidityThreshold)) {\n var lowHumidityThreshold = input.data.lowHumidityThreshold<0?0-input.data.lowHumidityThreshold:input.data.lowHumidityThreshold; \n var lowHumidityThreshold_high = lowHumidityThreshold.toString(16).padStart(2, '0').toUpperCase()[0].charCodeAt(0);\n var lowHumidityThreshold_low = lowHumidityThreshold.toString(16).padStart(2, '0').toUpperCase()[1].charCodeAt(0);\n if (input.data.lowHumidityThreshold > 100 || input.data.lowHumidityThreshold < 0) {\n return {\n errors: ['High humidity alarm threshold range 0~100.'],\n };\n } else {\n return {\n // LoRaWAN FPort used for the downlink message\n fPort: 3,\n // Encoded bytes\n bytes: [0x38, 0x30, 0x30, 0x32, 0x39, 0x39, 0x39, 0x39, 0x30, 0x45, lowHumidityThreshold_high, lowHumidityThreshold_low, 0x38, 0x31],\n };\n }\n }\n if (input.data.batteryThreshold != null && !isNaN(input.data.batteryThreshold)) {\n var batteryThreshold = input.data.batteryThreshold;\n var batteryThreshold_high = batteryThreshold.toString(16).padStart(2, '0').toUpperCase()[0].charCodeAt(0);\n var batteryThreshold_low = batteryThreshold.toString(16).padStart(2, '0').toUpperCase()[1].charCodeAt(0);\n if (batteryThreshold > 99 || batteryThreshold < 1) {\n return {\n errors: ['Battery alarm threshold range 1-99.'],\n };\n } else {\n return {\n // LoRaWAN FPort used for the downlink message\n fPort: 3,\n // Encoded bytes\n bytes: [0x38, 0x30, 0x30, 0x32, 0x39, 0x39, 0x39, 0x39, 0x30, 0x35, batteryThreshold_high, batteryThreshold_low, 0x38, 0x31],\n };\n }\n }\n\n if (input.data.workMode != null && !isNaN(input.data.workMode)) {\n var workMode = input.data.workMode;\n if (workMode === 0) {\n return {\n // LoRaWAN FPort used for the downlink message\n fPort: 3,\n // Encoded bytes\n bytes: [0x38, 0x30, 0x30, 0x32, 0x39, 0x39, 0x39, 0x39, 0x30, 0x39, 0x30, 0x35, 0x38, 0x31],\n };\n } else if (workMode === 1) {\n return {\n // LoRaWAN FPort used for the downlink message\n fPort: 3,\n // Encoded bytes\n bytes: [0x38, 0x30, 0x30, 0x32, 0x39, 0x39, 0x39, 0x39, 0x30, 0x39, 0x30, 0x36, 0x38, 0x31],\n };\n } else {\n return {\n errors: ['Work mode range 0-1.'],\n }\n }\n }\n return {\n errors: ['invalid downlink parameter.'],\n };\n}\n\nfunction decodeDownlink(input) {\n var input_length = input.bytes.length;\n if (input.fPort != 3) {\n return {\n errors: ['invalid FPort.'],\n };\n }\n\n if (\n input_length < 12 ||\n input.bytes[0] != 0x38 ||\n input.bytes[1] != 0x30 ||\n input.bytes[2] != 0x30 ||\n input.bytes[3] != 0x32 ||\n input.bytes[4] != 0x39 ||\n input.bytes[5] != 0x39 ||\n input.bytes[6] != 0x39 ||\n input.bytes[7] != 0x39 ||\n input.bytes[input_length - 2] != 0x38 ||\n input.bytes[input_length - 1] != 0x31\n ) {\n return {\n errors: ['invalid format.'],\n };\n }\n var option = parseInt(String.fromCharCode(input.bytes[8]) + String.fromCharCode(input.bytes[9]), 16);\n var value = parseInt(String.fromCharCode(input.bytes[10]) + String.fromCharCode(input.bytes[11]), 16);\n switch (option) {\n case 1:\n return {\n data: {\n uploadInterval: value,\n },\n };\n case 8:\n return {\n data: {\n detectInterval: value,\n },\n };\n case 2:\n var temp_abs = parseInt(String.fromCharCode(input.bytes[12]) + String.fromCharCode(input.bytes[13]), 16);\n return {\n data: {\n highTemperatureThreshold: value > 0 ? 0 - temp_abs : temp_abs,\n },\n };\n case 3:\n var temp_abs = parseInt(String.fromCharCode(input.bytes[12]) + String.fromCharCode(input.bytes[13]), 16);\n return {\n data: {\n lowTemperatureThreshold: value > 0 ? 0 - temp_abs : temp_abs,\n },\n };\n case 4:\n return {\n data: {\n highHumidityThreshold: value,\n },\n };\n case 5:\n return {\n data: {\n batteryThreshold: value,\n },\n };\n case 14:\n return {\n data: {\n lowHumidityThreshold: value,\n },\n };\n case 9:\n switch (value) {\n case 0x05:\n return {\n data: {\n workMode: 0,\n },\n };\n case 0x06:\n return {\n data: {\n workMode: 1,\n },\n };\n default:\n return {\n errors: ['invalid parameter value.'],\n };\n }\n default:\n return {\n errors: ['invalid parameter key.'],\n };\n }\n}", + "environment": "javascript", + "storage": "", + "version": "1.0" + }, + "flows": { + "dingtek_dt311_decoder": { + "data": { + "payload": "{{payload}}", + "payload_function": "decodeThingerUplink", + "payload_type": "source_payload", + "resource": "uplink", + "source": "resource", + "update": "events" + }, + "enabled": true, + "sink": { + "payload": "{{payload}}", + "payload_function": "", + "payload_type": "source_payload", + "resource_stream": "uplink_decoded", + "target": "resource_stream" + }, + "split_data": false + } + }, + "properties": { + "uplink": { + "data": { + "payload": "{{payload}}", + "payload_function": "", + "payload_type": "source_payload", + "resource": "uplink", + "source": "resource", + "update": "events" + }, + "default": { + "source": "value" + }, + "enabled": true + } + } + }, + "_resources": { + "properties": [ + { + "property": "dashboard", + "value": { + "tabs": [ + { + "name": "Overview", + "widgets": [ + { + "layout": { + "col": 0, + "row": 0, + "sizeX": 1, + "sizeY": 6 + }, + "panel": { + "color": "#ffffff", + "currentColor": "#ffffff", + "showOffline": { + "type": "none" + }, + "title": "Temperature" + }, + "properties": { + "color": "#e74c3c", + "max": 85, + "min": -30, + "unit": "°C" + }, + "sources": [ + { + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "temperature", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#e74c3c", + "name": "Temperature", + "source": "bucket", + "timespan": { + "mode": "latest" + } + } + ], + "type": "donutchart" + }, + { + "layout": { + "col": 1, + "row": 0, + "sizeX": 1, + "sizeY": 6 + }, + "panel": { + "color": "#ffffff", + "currentColor": "#ffffff", + "showOffline": { + "type": "none" + }, + "title": "Humidity" + }, + "properties": { + "color": "#3498db", + "max": 100, + "min": 0, + "unit": "%RH" + }, + "sources": [ + { + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "humidity", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#3498db", + "name": "Humidity", + "source": "bucket", + "timespan": { + "mode": "latest" + } + } + ], + "type": "donutchart" + }, + { + "layout": { + "col": 2, + "row": 0, + "sizeX": 1, + "sizeY": 6 + }, + "panel": { + "color": "#ffffff", + "currentColor": "#ffffff", + "showOffline": { + "type": "none" + }, + "title": "Battery Voltage" + }, + "properties": { + "color": "#f1c40f", + "max": 4.2, + "min": 2.5, + "unit": "V" + }, + "sources": [ + { + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "volt", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#f1c40f", + "name": "Voltage", + "source": "bucket", + "timespan": { + "mode": "latest" + } + } + ], + "type": "donutchart" + }, + { + "layout": { + "col": 3, + "row": 0, + "sizeX": 3, + "sizeY": 6 + }, + "panel": { + "color": "#ffffff", + "currentColor": "#ffffff", + "showOffline": { + "type": "none" + }, + "title": "Alarm Status" + }, + "properties": { + "source": "code", + "template": "
\n
\n
\n
{{ (value && value[0] && value[0].alarmHighTemperature) ? 'đŸ”Ĩ' : '✅' }}
\n
High Temp
\n
{{ (value && value[0] && value[0].alarmHighTemperature) ? 'ALARM' : 'OK' }}
\n
\n
\n
{{ (value && value[0] && value[0].alarmLowTemperature) ? 'â„ī¸' : '✅' }}
\n
Low Temp
\n
{{ (value && value[0] && value[0].alarmLowTemperature) ? 'ALARM' : 'OK' }}
\n
\n
\n
{{ (value && value[0] && value[0].alarmHighHumidity) ? '💧' : '✅' }}
\n
High Hum
\n
{{ (value && value[0] && value[0].alarmHighHumidity) ? 'ALARM' : 'OK' }}
\n
\n
\n
{{ (value && value[0] && value[0].alarmLowHumidity) ? 'đŸŒĩ' : '✅' }}
\n
Low Hum
\n
{{ (value && value[0] && value[0].alarmLowHumidity) ? 'ALARM' : 'OK' }}
\n
\n
\n
{{ (value && value[0] && value[0].alarmBattery) ? 'đŸĒĢ' : '🔋' }}
\n
Battery
\n
{{ (value && value[0] && value[0].alarmBattery) ? 'Low' : 'OK' }}
\n
\n
\n
Last update: {{ (value && value[0] && value[0].ts) | date:'medium' }}
\n
\n" + }, + "sources": [ + { + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "alarmHighTemperature", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#e74c3c", + "name": "alarmHighTemperature", + "source": "bucket", + "timespan": { + "mode": "latest" + } + }, + { + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "alarmLowTemperature", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#3498db", + "name": "alarmLowTemperature", + "source": "bucket", + "timespan": { + "mode": "latest" + } + }, + { + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "alarmHighHumidity", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#1abc9c", + "name": "alarmHighHumidity", + "source": "bucket", + "timespan": { + "mode": "latest" + } + }, + { + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "alarmLowHumidity", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#e67e22", + "name": "alarmLowHumidity", + "source": "bucket", + "timespan": { + "mode": "latest" + } + }, + { + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "alarmBattery", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#f39c12", + "name": "alarmBattery", + "source": "bucket", + "timespan": { + "mode": "latest" + } + }, + { + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "ts", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#95a5a6", + "name": "ts", + "source": "bucket", + "timespan": { + "mode": "latest" + } + } + ], + "type": "html_time" + }, + { + "layout": { + "col": 0, + "row": 6, + "sizeX": 4, + "sizeY": 8 + }, + "panel": { + "color": "#ffffff", + "currentColor": "#ffffff", + "showOffline": { + "type": "none" + }, + "title": "Temperature History" + }, + "properties": { + "axis": true, + "fill": false, + "legend": true, + "multiple_axes": false + }, + "sources": [ + { + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "temperature", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#e74c3c", + "name": "Temperature (°C)", + "source": "bucket", + "timespan": { + "magnitude": "hour", + "mode": "relative", + "period": "latest", + "value": 24 + } + } + ], + "type": "chart" + }, + { + "layout": { + "col": 4, + "row": 6, + "sizeX": 2, + "sizeY": 8 + }, + "panel": { + "color": "#ffffff", + "currentColor": "#ffffff", + "showOffline": { + "type": "none" + }, + "title": "Humidity History" + }, + "properties": { + "axis": true, + "fill": true, + "legend": true, + "multiple_axes": false + }, + "sources": [ + { + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "humidity", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#3498db", + "name": "Humidity (%RH)", + "source": "bucket", + "timespan": { + "magnitude": "hour", + "mode": "relative", + "period": "latest", + "value": 24 + } + } + ], + "type": "chart" + }, + { + "layout": { + "col": 0, + "row": 14, + "sizeX": 6, + "sizeY": 8 + }, + "panel": { + "color": "#ffffff", + "currentColor": "#ffffff", + "showOffline": { + "type": "none" + }, + "title": "Last 24h Records" + }, + "properties": { + "source": "code", + "template": "
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
DateTemp (°C)Hum (%RH)Volt (V)High TempLow TempHigh HumLow HumBatteryFrame #
{{ entry.ts | date:'dd/MM/yy HH:mm' }}{{ entry.temperature !== undefined ? entry.temperature : '—' }}{{ entry.humidity !== undefined ? entry.humidity : '—' }}{{ entry.volt !== undefined ? (entry.volt | number:2) : '—' }}{{ entry.alarmHighTemperature ? 'đŸ”Ĩ YES' : '✅ NO' }}{{ entry.alarmLowTemperature ? 'â„ī¸ YES' : '✅ NO' }}{{ entry.alarmHighHumidity ? '💧 YES' : '✅ NO' }}{{ entry.alarmLowHumidity ? 'đŸŒĩ YES' : '✅ NO' }}{{ entry.alarmBattery ? 'đŸĒĢ Low' : '🔋 OK' }}{{ entry.frameCounter !== undefined ? entry.frameCounter : '—' }}
\n
\n" + }, + "sources": [ + { + "aggregation": {}, + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "ts", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#95a5a6", + "name": "ts", + "source": "bucket", + "timespan": { + "magnitude": "hour", + "mode": "relative", + "period": "latest", + "value": 24 + } + }, + { + "aggregation": {}, + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "temperature", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#e74c3c", + "name": "temperature", + "source": "bucket", + "timespan": { + "magnitude": "hour", + "mode": "relative", + "period": "latest", + "value": 24 + } + }, + { + "aggregation": {}, + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "humidity", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#3498db", + "name": "humidity", + "source": "bucket", + "timespan": { + "magnitude": "hour", + "mode": "relative", + "period": "latest", + "value": 24 + } + }, + { + "aggregation": {}, + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "volt", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#f1c40f", + "name": "volt", + "source": "bucket", + "timespan": { + "magnitude": "hour", + "mode": "relative", + "period": "latest", + "value": 24 + } + }, + { + "aggregation": {}, + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "alarmHighTemperature", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#e74c3c", + "name": "alarmHighTemperature", + "source": "bucket", + "timespan": { + "magnitude": "hour", + "mode": "relative", + "period": "latest", + "value": 24 + } + }, + { + "aggregation": {}, + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "alarmLowTemperature", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#3498db", + "name": "alarmLowTemperature", + "source": "bucket", + "timespan": { + "magnitude": "hour", + "mode": "relative", + "period": "latest", + "value": 24 + } + }, + { + "aggregation": {}, + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "alarmHighHumidity", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#1abc9c", + "name": "alarmHighHumidity", + "source": "bucket", + "timespan": { + "magnitude": "hour", + "mode": "relative", + "period": "latest", + "value": 24 + } + }, + { + "aggregation": {}, + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "alarmLowHumidity", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#e67e22", + "name": "alarmLowHumidity", + "source": "bucket", + "timespan": { + "magnitude": "hour", + "mode": "relative", + "period": "latest", + "value": 24 + } + }, + { + "aggregation": {}, + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "alarmBattery", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#f39c12", + "name": "alarmBattery", + "source": "bucket", + "timespan": { + "magnitude": "hour", + "mode": "relative", + "period": "latest", + "value": 24 + } + }, + { + "aggregation": {}, + "bucket": { + "backend": "mongodb", + "id": "dingtek_dt311_data", + "mapping": "frameCounter", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#1abc9c", + "name": "frameCounter", + "source": "bucket", + "timespan": { + "magnitude": "hour", + "mode": "relative", + "period": "latest", + "value": 24 + } + } + ], + "type": "html_time" + } + ] + } + ] + } + } + ] } - } - } - }, - "autoprovisions": { - "device_autoprovisioning": { - "config": { - "mode": "pattern", - "pattern": "dt311_[0-9A-Fa-f]{16}" - }, - "enabled": true - } - }, - "buckets": { - "dingtek_dt311_data": { - "backend": "mongodb", - "data": { - "payload": "{{payload}}", - "payload_function": "", - "payload_type": "source_payload", - "resource_stream": "uplink_decoded", - "source": "resource_stream" - }, - "enabled": true, - "retention": { - "period": 3, - "unit": "months" - }, - "tags": [] - } - }, - "code": { - "code": "function decodeThingerUplink(thingerData) {\n // 0. If data has already been decoded, we will return it\n if (thingerData.decodedPayload) return thingerData.decodedPayload;\n \n // 1. Extract and Validate Input\n // We need 'payload' (hex string) and 'fPort' (integer)\n const hexPayload = thingerData.payload || \"\";\n const port = thingerData.fPort || 1;\n\n // 2. Convert Hex String to Byte Array\n const bytes = [];\n for (let i = 0; i < hexPayload.length; i += 2) {\n bytes.push(parseInt(hexPayload.substr(i, 2), 16));\n }\n\n // 3. Dynamic Function Detection and Execution\n \n // CASE A: (The Things Stack v3)\n if (typeof decodeUplink === 'function') {\n try {\n const input = {\n bytes: bytes,\n fPort: port\n };\n var result = decodeUplink(input);\n \n if (result.data) return result.data;\n\n return result; \n } catch (e) {\n console.error(\"Error inside decodeUplink:\", e);\n throw e;\n }\n }\n\n // CASE B: Legacy TTN (v2)\n else if (typeof Decoder === 'function') {\n try {\n return Decoder(bytes, port);\n } catch (e) {\n console.error(\"Error inside Decoder:\", e);\n throw e;\n }\n }\n\n // CASE C: No decoder found\n else {\n throw new Error(\"No compatible TTN decoder function (decodeUplink or Decoder) found in scope.\");\n }\n}\n\n\n// TTN decoder\n//IEEE754 hex to float convert\nfunction hex2float(num) {\n var sign = num & 0x80000000 ? -1 : 1;\n var exponent = ((num >> 23) & 0xff) - 127;\n var mantissa = 1 + (num & 0x7fffff) / 0x7fffff;\n return sign * mantissa * Math.pow(2, exponent);\n}\n\nfunction decodeUplink(input) {\n if (input.fPort != 3) {\n return {\n errors: ['unknown FPort'],\n };\n }\n switch (input.bytes[3]) {\n case 1:\n case 2:\n var temp_sign = input.bytes[8];\n var temp_abs = (input.bytes[9] >> 4) * 10 + (input.bytes[9] & 0x0F) + (input.bytes[10] >> 4) / 10 + (input.bytes[10] & 0x0F) / 100;\n var humi_abs = (input.bytes[11] >> 4) * 10 + (input.bytes[11] & 0x0F) + (input.bytes[12] >> 4) / 10 + (input.bytes[12] & 0x0F) / 100;\n var temperature_temp=(temp_sign == 0 )? temp_abs : (0 - temp_abs);\n return {\n // Decoded data\n data: {\n temperature: temperature_temp.toFixed(2),\n humidity: humi_abs.toFixed(2),\n alarmHighTemperature: (input.bytes[13] & 0x10) ? true : false,\n alarmLowTemperature: (input.bytes[13] & 0x01) ? true : false,\n alarmHighHumidity: (input.bytes[14] & 0x10) ? true : false,\n alarmLowHumidity: (input.bytes[14] & 0x01) ? true : false,\n alarmBattery: (input.bytes[7] & 0x01) ? true : false,\n volt: ((input.bytes[5] << 8) + input.bytes[6]) / 100,\n frameCounter: (input.bytes[15] << 8) + input.bytes[16],\n },\n };\n case 3:\n var high_temp_abs = input.bytes[10];\n var low_temp_abs = input.bytes[12];\n return {\n // Decoded data\n data: {\n firmware: input.bytes[5] + \".\" + input.bytes[6],\n uploadInterval: input.bytes[7],\n detectInterval: input.bytes[8],\n highTemperatureThreshold: input.bytes[9] ? 256 - high_temp_abs : high_temp_abs,\n lowTemperatureThreshold: input.bytes[11] ? 0 - low_temp_abs : low_temp_abs,\n highHumidityThreshold: input.bytes[13],\n lowHumidityThreshold: input.bytes[14],\n batteryThreshold: input.bytes[16],\n workMode: input.bytes[15],\n },\n };\n default:\n return {\n errors: ['wrong length'],\n };\n }\n}\n\n// Encode downlink function.\n//\n// Input is an object with the following fields:\n// - data = Object representing the payload that must be encoded.\n// - variables = Object containing the configured device variables.\n//\n// Output must be an object with the following fields:\n// - bytes = Byte array containing the downlink payload.\nfunction encodeDownlink(input) {\n if (input.data.uploadInterval != null && !isNaN(input.data.uploadInterval)) {\n var uploadInterval = input.data.uploadInterval;\n var uploadInterval_high = uploadInterval.toString(16).padStart(2, '0').toUpperCase()[0].charCodeAt(0); \n var uploadInterval_low = uploadInterval.toString(16).padStart(2, '0').toUpperCase()[1].charCodeAt(0);\n if (uploadInterval > 255 || uploadInterval < 1) {\n return {\n errors: ['upload interval range 1-255 hours.'],\n };\n } else {\n return {\n // LoRaWAN FPort used for the downlink message\n fPort: 3,\n // Encoded bytes\n bytes: [0x38, 0x30, 0x30, 0x32, 0x39, 0x39, 0x39, 0x39, 0x30, 0x31, uploadInterval_high, uploadInterval_low, 0x38, 0x31],\n };\n }\n }\n if (input.data.detectInterval != null && !isNaN(input.data.detectInterval)) {\n var detectInterval = input.data.detectInterval;\n var detectInterval_high = detectInterval.toString(16).padStart(2, '0').toUpperCase()[0].charCodeAt(0);\n var detectInterval_low = detectInterval.toString(16).padStart(2, '0').toUpperCase()[1].charCodeAt(0);\n if (detectInterval > 255 || detectInterval < 1) {\n return {\n errors: ['detection interval range 1-255 minutes.'],\n };\n } else {\n return {\n // LoRaWAN FPort used for the downlink message\n fPort: 3,\n // Encoded bytes\n bytes: [0x38, 0x30, 0x30, 0x32, 0x39, 0x39, 0x39, 0x39, 0x30, 0x38, detectInterval_high, detectInterval_low, 0x38, 0x31],\n };\n }\n }\n if (input.data.highTemperatureThreshold != null && !isNaN(input.data.highTemperatureThreshold)) {\n var highTemperatureThreshold = input.data.highTemperatureThreshold<0?0-input.data.highTemperatureThreshold:input.data.highTemperatureThreshold;\n var highTemperatureSign=input.data.highTemperatureThreshold<0?0x31:0x30;\n var highTemperatureThreshold_high = highTemperatureThreshold.toString(16).padStart(2, '0').toUpperCase()[0].charCodeAt(0);\n var highTemperatureThreshold_low = highTemperatureThreshold.toString(16).padStart(2, '0').toUpperCase()[1].charCodeAt(0);\n if (input.data.highTemperatureThreshold > 85 || input.data.highTemperatureThreshold < -30) {\n return {\n errors: ['High temperature alarm threshold range -30~+85.'],\n };\n } else {\n return {\n // LoRaWAN FPort used for the downlink message\n fPort: 3,\n // Encoded bytes\n bytes: [0x38, 0x30, 0x30, 0x32, 0x39, 0x39, 0x39, 0x39, 0x30, 0x32, 0x30,highTemperatureSign,highTemperatureThreshold_high, highTemperatureThreshold_low, 0x38, 0x31],\n };\n }\n }\n if (input.data.lowTemperatureThreshold != null && !isNaN(input.data.lowTemperatureThreshold)) {\n var lowTemperatureThreshold = input.data.lowTemperatureThreshold<0?0-input.data.lowTemperatureThreshold:input.data.lowTemperatureThreshold;\n var lowTemperatureSign=input.data.lowTemperatureThreshold<0?0x31:0x30;\n var lowTemperatureThreshold_high = lowTemperatureThreshold.toString(16).padStart(2, '0').toUpperCase()[0].charCodeAt(0);\n var lowTemperatureThreshold_low = lowTemperatureThreshold.toString(16).padStart(2, '0').toUpperCase()[1].charCodeAt(0);\n if (input.data.lowTemperatureThreshold > 85 || input.data.lowTemperatureThreshold < -30) {\n return {\n errors: ['Low temperature alarm threshold range -30~+85.'],\n };\n } else {\n return {\n // LoRaWAN FPort used for the downlink message\n fPort: 3,\n // Encoded bytes\n bytes: [0x38, 0x30, 0x30, 0x32, 0x39, 0x39, 0x39, 0x39, 0x30, 0x33, 0x30,lowTemperatureSign,lowTemperatureThreshold_high, lowTemperatureThreshold_low, 0x38, 0x31],\n };\n }\n }\n \n if (input.data.highHumidityThreshold != null && !isNaN(input.data.highHumidityThreshold)) {\n var highHumidityThreshold = input.data.highHumidityThreshold<0?0-input.data.highHumidityThreshold:input.data.highHumidityThreshold; \n var highHumidityThreshold_high = highHumidityThreshold.toString(16).padStart(2, '0').toUpperCase()[0].charCodeAt(0);\n var highHumidityThreshold_low = highHumidityThreshold.toString(16).padStart(2, '0').toUpperCase()[1].charCodeAt(0);\n if (input.data.highHumidityThreshold > 100 || input.data.highHumidityThreshold < 0) {\n return {\n errors: ['High humidity alarm threshold range 0~100.'],\n };\n } else {\n return {\n // LoRaWAN FPort used for the downlink message\n fPort: 3,\n // Encoded bytes\n bytes: [0x38, 0x30, 0x30, 0x32, 0x39, 0x39, 0x39, 0x39, 0x30, 0x34, highHumidityThreshold_high, highHumidityThreshold_low, 0x38, 0x31],\n };\n }\n }\n if (input.data.lowHumidityThreshold != null && !isNaN(input.data.lowHumidityThreshold)) {\n var lowHumidityThreshold = input.data.lowHumidityThreshold<0?0-input.data.lowHumidityThreshold:input.data.lowHumidityThreshold; \n var lowHumidityThreshold_high = lowHumidityThreshold.toString(16).padStart(2, '0').toUpperCase()[0].charCodeAt(0);\n var lowHumidityThreshold_low = lowHumidityThreshold.toString(16).padStart(2, '0').toUpperCase()[1].charCodeAt(0);\n if (input.data.lowHumidityThreshold > 100 || input.data.lowHumidityThreshold < 0) {\n return {\n errors: ['High humidity alarm threshold range 0~100.'],\n };\n } else {\n return {\n // LoRaWAN FPort used for the downlink message\n fPort: 3,\n // Encoded bytes\n bytes: [0x38, 0x30, 0x30, 0x32, 0x39, 0x39, 0x39, 0x39, 0x30, 0x45, lowHumidityThreshold_high, lowHumidityThreshold_low, 0x38, 0x31],\n };\n }\n }\n if (input.data.batteryThreshold != null && !isNaN(input.data.batteryThreshold)) {\n var batteryThreshold = input.data.batteryThreshold;\n var batteryThreshold_high = batteryThreshold.toString(16).padStart(2, '0').toUpperCase()[0].charCodeAt(0);\n var batteryThreshold_low = batteryThreshold.toString(16).padStart(2, '0').toUpperCase()[1].charCodeAt(0);\n if (batteryThreshold > 99 || batteryThreshold < 1) {\n return {\n errors: ['Battery alarm threshold range 1-99.'],\n };\n } else {\n return {\n // LoRaWAN FPort used for the downlink message\n fPort: 3,\n // Encoded bytes\n bytes: [0x38, 0x30, 0x30, 0x32, 0x39, 0x39, 0x39, 0x39, 0x30, 0x35, batteryThreshold_high, batteryThreshold_low, 0x38, 0x31],\n };\n }\n }\n\n if (input.data.workMode != null && !isNaN(input.data.workMode)) {\n var workMode = input.data.workMode;\n if (workMode === 0) {\n return {\n // LoRaWAN FPort used for the downlink message\n fPort: 3,\n // Encoded bytes\n bytes: [0x38, 0x30, 0x30, 0x32, 0x39, 0x39, 0x39, 0x39, 0x30, 0x39, 0x30, 0x35, 0x38, 0x31],\n };\n } else if (workMode === 1) {\n return {\n // LoRaWAN FPort used for the downlink message\n fPort: 3,\n // Encoded bytes\n bytes: [0x38, 0x30, 0x30, 0x32, 0x39, 0x39, 0x39, 0x39, 0x30, 0x39, 0x30, 0x36, 0x38, 0x31],\n };\n } else {\n return {\n errors: ['Work mode range 0-1.'],\n }\n }\n }\n return {\n errors: ['invalid downlink parameter.'],\n };\n}\n\nfunction decodeDownlink(input) {\n var input_length = input.bytes.length;\n if (input.fPort != 3) {\n return {\n errors: ['invalid FPort.'],\n };\n }\n\n if (\n input_length < 12 ||\n input.bytes[0] != 0x38 ||\n input.bytes[1] != 0x30 ||\n input.bytes[2] != 0x30 ||\n input.bytes[3] != 0x32 ||\n input.bytes[4] != 0x39 ||\n input.bytes[5] != 0x39 ||\n input.bytes[6] != 0x39 ||\n input.bytes[7] != 0x39 ||\n input.bytes[input_length - 2] != 0x38 ||\n input.bytes[input_length - 1] != 0x31\n ) {\n return {\n errors: ['invalid format.'],\n };\n }\n var option = parseInt(String.fromCharCode(input.bytes[8]) + String.fromCharCode(input.bytes[9]), 16);\n var value = parseInt(String.fromCharCode(input.bytes[10]) + String.fromCharCode(input.bytes[11]), 16);\n switch (option) {\n case 1:\n return {\n data: {\n uploadInterval: value,\n },\n };\n case 8:\n return {\n data: {\n detectInterval: value,\n },\n };\n case 2:\n var temp_abs = parseInt(String.fromCharCode(input.bytes[12]) + String.fromCharCode(input.bytes[13]), 16);\n return {\n data: {\n highTemperatureThreshold: value > 0 ? 0 - temp_abs : temp_abs,\n },\n };\n case 3:\n var temp_abs = parseInt(String.fromCharCode(input.bytes[12]) + String.fromCharCode(input.bytes[13]), 16);\n return {\n data: {\n lowTemperatureThreshold: value > 0 ? 0 - temp_abs : temp_abs,\n },\n };\n case 4:\n return {\n data: {\n highHumidityThreshold: value,\n },\n };\n case 5:\n return {\n data: {\n batteryThreshold: value,\n },\n };\n case 14:\n return {\n data: {\n lowHumidityThreshold: value,\n },\n };\n case 9:\n switch (value) {\n case 0x05:\n return {\n data: {\n workMode: 0,\n },\n };\n case 0x06:\n return {\n data: {\n workMode: 1,\n },\n };\n default:\n return {\n errors: ['invalid parameter value.'],\n };\n }\n default:\n return {\n errors: ['invalid parameter key.'],\n };\n }\n}", - "environment": "javascript", - "storage": "", - "version": "1.0" - }, - "flows": { - "dingtek_dt311_decoder": { - "data": { - "payload": "{{payload}}", - "payload_function": "decodeThingerUplink", - "payload_type": "source_payload", - "resource": "uplink", - "source": "resource", - "update": "events" - }, - "enabled": true, - "sink": { - "payload": "{{payload}}", - "payload_function": "", - "payload_type": "source_payload", - "resource_stream": "uplink_decoded", - "target": "resource_stream" - }, - "split_data": false - } - }, - "properties": { - "uplink": { - "data": { - "payload": "{{payload}}", - "payload_function": "", - "payload_type": "source_payload", - "resource": "uplink", - "source": "resource", - "update": "events" - }, - "default": { - "source": "value" - }, - "enabled": true } - } - }, - "_resources": { - "properties": [] - } - } - ] - } -} \ No newline at end of file + ] + } +}