diff --git a/dingtek-dc410/plugin.json b/dingtek-dc410/plugin.json index aac281e2a..bceb2cebf 100644 --- a/dingtek-dc410/plugin.json +++ b/dingtek-dc410/plugin.json @@ -1,134 +1,665 @@ { - "name": "dingtek_dc410", - "version": "1.0.0", - "description": "The CNDingtek DC410 is a smart manhole sensor that integrates a liquid/solid level sensor and a movement sensor. Suitable for measuring water and sewage levels in wells and manholes. The data is sent to a LoRaWAN network, then the application server for f", - "author": "Thinger.io", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/thinger-io/plugins.git", - "directory": "dingtek-dc410" - }, - "metadata": { - "name": "Dingtek DC410", + "name": "dingtek-dc410", + "version": "1.0.0", "description": "The CNDingtek DC410 is a smart manhole sensor that integrates a liquid/solid level sensor and a movement sensor. Suitable for measuring water and sewage levels in wells and manholes. The data is sent to a LoRaWAN network, then the application server for f", - "image": "assets/dc410.png", + "author": "Thinger.io", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/thinger-io/plugins.git", + "directory": "dingtek-dc410" + }, + "metadata": { + "name": "Dingtek DC410", + "description": "The CNDingtek DC410 is a smart manhole sensor that integrates a liquid/solid level sensor and a movement sensor. Suitable for measuring water and sewage levels in wells and manholes. The data is sent to a LoRaWAN network, then the application server for f", + "image": "assets/dc410.png", "category": "devices", "vendor": "dingtek" - }, - "resources": { - "products": [ - { - "description": "The CNDingtek DC410 is a smart manhole sensor that integrates a liquid/solid level sensor and a movement sensor. Suitable for measuring water and sewage levels in wells and manholes. The data is sent to a LoRaWAN network, then the application server for f", - "enabled": true, - "name": "Dingtek DC410", - "product": "dingtek_dc410", - "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": [ + { + "description": "The CNDingtek DC410 is a smart manhole sensor that integrates a liquid/solid level sensor and a movement sensor. Suitable for measuring water and sewage levels in wells and manholes. The data is sent to a LoRaWAN network, then the application server for f", + "enabled": true, + "name": "Dingtek DC410", + "product": "dingtek_dc410", + "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": "dc410_.*" + }, + "enabled": true + } + }, + "buckets": { + "dingtek_dc410_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\nvar units = [' ℃', ' hours', ' minutes', ' mm', ' °', ' cm'];\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\n switch (input.bytes.length) {\n case 17:\n return {\n // Decoded data\n data: {\n level: (input.bytes[5] << 8) + input.bytes[6] + units[3],\n full: Boolean(input.bytes[11] >> 4),\n motion: Boolean(input.bytes[12] >> 4),\n battery: Boolean(input.bytes[12] & 0x0f),\n angle: (input.bytes[9] & (0x0f === 0x00) ? input.bytes[10] : 0 - input.bytes[10]) + units[4],\n temperature: input.bytes[8] + units[0],\n frame_counter: (input.bytes[13] << 8) + input.bytes[14],\n },\n };\n case 25:\n var data_type = input.bytes[3];\n if (data_type === 0x03) {\n return {\n // Decoded parameter\n data: {\n periodic_upload_interval: input.bytes[7] + units[1],\n periodic_detection_interval: input.bytes[8] + units[2],\n full_alarm_threshold: input.bytes[9] + units[5],\n motion_alarm_threshold: input.bytes[11] + units[4],\n motion_alarm_enable: Boolean(input.bytes[12]),\n ultrasonic_range: (input.bytes[13] === 0x00 ? 2000 : 5000) + units[3],\n },\n };\n }\n default:\n return {\n errors: ['wrong length'],\n };\n }\n}\n\nfunction encodeDownlink(input) {\n if (input.data.periodic_upload_interval != null && !isNaN(input.data.periodic_upload_interval)) {\n var periodic_interval = input.data.periodic_upload_interval;\n var periodic_interval_high = periodic_interval.toString(16).padStart(2, '0').toUpperCase()[0].charCodeAt(0);\n var periodic_interval_low = periodic_interval.toString(16).padStart(2, '0').toUpperCase()[1].charCodeAt(0);\n if (periodic_interval > 168 || periodic_interval < 1) {\n return {\n errors: ['periodic upload interval range 1-168 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, periodic_interval_high, periodic_interval_low, 0x38, 0x31],\n };\n }\n }\n if (input.data.periodic_detection_interval != null && !isNaN(input.data.periodic_detection_interval)) {\n var detection_interval = input.data.periodic_detection_interval;\n var detection_interval_high = detection_interval.toString(16).padStart(2, '0').toUpperCase()[0].charCodeAt(0);\n var detection_interval_low = detection_interval.toString(16).padStart(2, '0').toUpperCase()[1].charCodeAt(0);\n if (detection_interval > 60 || detection_interval < 1) {\n return {\n errors: ['periodic detection interval range 1-60 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, detection_interval_high, detection_interval_low, 0x38, 0x31],\n };\n }\n }\n if (input.data.full_alarm_threshold != null && !isNaN(input.data.full_alarm_threshold)) {\n var full_alarm_threshold = input.data.full_alarm_threshold;\n var full_alarm_threshold_high = full_alarm_threshold.toString(16).padStart(2, '0').toUpperCase()[0].charCodeAt(0);\n var full_alarm_threshold_low = full_alarm_threshold.toString(16).padStart(2, '0').toUpperCase()[1].charCodeAt(0);\n if (full_alarm_threshold > 255 || full_alarm_threshold < 15) {\n return {\n errors: ['full alarm threshold range 15-255 cm.'],\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, full_alarm_threshold_high, full_alarm_threshold_low, 0x38, 0x31],\n };\n }\n }\n\n if (input.data.motion_alarm_threshold != null && !isNaN(input.data.motion_alarm_threshold)) {\n var tilt_alarm_threshold = input.data.motion_alarm_threshold;\n var tilt_alarm_threshold_high = tilt_alarm_threshold.toString(16).padStart(2, '0').toUpperCase()[0].charCodeAt(0);\n var tilt_alarm_threshold_low = tilt_alarm_threshold.toString(16).padStart(2, '0').toUpperCase()[1].charCodeAt(0);\n if (tilt_alarm_threshold > 90 || tilt_alarm_threshold < 15) {\n return {\n errors: ['tilt alarm threshold range 15-90 °.'],\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, tilt_alarm_threshold_high, tilt_alarm_threshold_low, 0x38, 0x31],\n };\n }\n }\n if (input.data.motion_alarm_enable != null && input.data.motion_alarm_enable === !!input.data.motion_alarm_enable) {\n var tilt_enable = input.data.motion_alarm_enable;\n if (tilt_enable === true) {\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, 0x41, 0x38, 0x31],\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, 0x39, 0x30, 0x39, 0x38, 0x31],\n };\n }\n }\n if (input.data.ultrasonic_range != null && !isNaN(input.data.ultrasonic_range)) {\n var ultrasonic_range = input.data.ultrasonic_range;\n if (ultrasonic_range === 2 || ultrasonic_range === 5) {\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, ultrasonic_range === 2 ? 0x30 : 0x31, ultrasonic_range === 2 ? 0x37 : 0x33, 0x38, 0x31],\n };\n } else {\n return {\n errors: ['ultrasonic range 2 or 5 meters.'],\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 periodic_upload_interval: value,\n },\n };\n case 8:\n return {\n data: {\n periodic_detection_interval: value,\n },\n };\n case 2:\n return {\n data: {\n full_alarm_threshold: value,\n },\n };\n case 4:\n return {\n data: {\n motion_alarm_threshold: value,\n },\n };\n case 9:\n switch (value) {\n case 0x07:\n return {\n data: {\n ultrasonic_range: 2,\n },\n };\n case 0x13:\n return {\n data: {\n ultrasonic_range: 5,\n },\n };\n case 0x09:\n return {\n data: {\n motion_alarm_enable: false,\n },\n };\n case 0x0a:\n return {\n data: {\n motion_alarm_enable: true,\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}\n", + "environment": "javascript", + "storage": "", + "version": "1.0" + }, + "flows": { + "dingtek_dc410_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": "Water Level" + }, + "properties": { + "color": "#3498db", + "max": 2000, + "min": 0, + "unit": "mm" + }, + "sources": [ + { + "bucket": { + "backend": "mongodb", + "id": "dingtek_dc410_data", + "mapping": "level", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#3498db", + "name": "Level", + "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": "Temperature" + }, + "properties": { + "color": "#e74c3c", + "max": 60, + "min": -20, + "unit": "°C" + }, + "sources": [ + { + "bucket": { + "backend": "mongodb", + "id": "dingtek_dc410_data", + "mapping": "temperature", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#e74c3c", + "name": "Temperature", + "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": "Tilt Angle" + }, + "properties": { + "color": "#9b59b6", + "max": 90, + "min": 0, + "unit": "°" + }, + "sources": [ + { + "bucket": { + "backend": "mongodb", + "id": "dingtek_dc410_data", + "mapping": "angle", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#9b59b6", + "name": "Angle", + "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": "Device Status" + }, + "properties": { + "source": "code", + "template": "
| Date | \nLevel (mm) | \nTemperature (°C) | \nAngle (°) | \nFull | \nMotion | \nBattery | \nFrame # | \n
|---|---|---|---|---|---|---|---|
| {{ entry.ts | date:'dd/MM/yy HH:mm' }} | \n{{ entry.level || '—' }} | \n{{ entry.temperature || '—' }} | \n{{ entry.angle || '—' }} | \n{{ entry.full ? '🚨 YES' : '✅ NO' }} | \n{{ entry.motion ? '⚠️ YES' : '🔒 NO' }} | \n{{ entry.battery ? '🪫 Low' : '🔋 OK' }} | \n{{ entry.frame_counter || '—' }} | \n