Skip to content

Plugin file

Plugin configuration file
{
    "name": "wika-pew1000",
    "version": "1.0.0",
    "description": "Wireless pressure sensor",
    "author": "Thinger.io",
    "license": "MIT",
    "repository": {
        "type": "git",
        "url": "https://github.com/thinger-io/plugins.git",
        "directory": "wika-pew1000"
    },
    "metadata": {
        "name": "WIKA PEW-1000",
        "description": "Wireless pressure sensor",
        "image": "https://s3.eu-west-1.amazonaws.com/thinger.io.files/plugins/wika-pew1000/img/wikapew1000.png"
    },
    "resources": {
        "products": [
            {
                "description": "Wireless pressure sensor",
                "enabled": true,
                "name": "Wika PEW1000",
                "product": "wika_pew1000",
                "profile": {
                    "api": {
                        "downlink": {
                            "enabled": true,
                            "handle_connectivity": false,
                            "request": {
                                "data": {
                                    "path": "/downlink",
                                    "payload": "{\n    \"data\"    : \"{{payload.data=\"\"}}\",\n    \"port\"    :  {{payload.port=85}},\n    \"priority\":  {{payload.priority=3}},\n    \"confirmed\" :  {{payload.confirmed=false}},\n    \"uplink\"  :  {{property.uplink}} \n}",
                                    "payload_function": "",
                                    "payload_type": "",
                                    "plugin": "{{property.source}}",
                                    "target": "plugin_endpoint"
                                }
                            },
                            "response": {
                                "data": {
                                    "payload": "{{payload}}",
                                    "payload_function": "",
                                    "payload_type": "source_payload",
                                    "source": "request_response"
                                }
                            }
                        },
                        "uplink": {
                            "enabled": true,
                            "handle_connectivity": true,
                            "request": {
                                "data": {
                                    "payload": "{{payload}}",
                                    "payload_function": "",
                                    "payload_type": "source_payload",
                                    "target": "resource_stream"
                                }
                            },
                            "response": {
                                "data": {}
                            }
                        }
                    },
                    "autoprovisions": {
                        "PEW1000_autoprovisioning": {
                            "config": {
                                "mode": "pattern",
                                "pattern": "wika_pew1000_.*"
                            },
                            "enabled": true
                        }
                    },
                    "buckets": {
                        "pew1000_data_bucket": {
                            "backend": "mongodb",
                            "data": {
                                "payload": "{{payload}}",
                                "payload_function": "decodeUplink",
                                "payload_type": "source_payload",
                                "resource": "uplink",
                                "source": "resource",
                                "update": "events"
                            },
                            "enabled": true,
                            "retention": {
                                "period": 3,
                                "unit": "months"
                            },
                            "tags": []
                        }
                    },
                    "code": {
                        "code": "/*&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&#    &&&   \n&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&#    &&&   \n&&&                                                                                               &&#    &&&   \n&&&    .&&&&&      .&&&&&      &&&&&&    &&&&&     .&&&&/     .&&&&&&          &&&&&&&&           &&#    &&&   \n&&&     &&&&&&     &&&&&&&     &&&&&     &&&&&     .&&&&/   &&&&&&&           &&&&&&&&&&          &&#    &&&   \n&&&      &&&&&(   &&&&&&&&#   &&&&&(     &&&&&     .&&&&/ &&&&&&             &&&&& /&&&&&         &&#    &&&   \n&&&       &&&&&  /&&&&,&&&&  ,&&&&&      &&&&&     .&&&&&&&&&&&             &&&&&   &&&&&%        &&#    &&&   \n&&&       %&&&&& &&&&  &&&&& &&&&&       &&&&&     .&&&&&&&&&&&&&          &&&&&%    &&&&&(       &&#    &&&   \n&&&        &&&&&&&&&(   &&&&&&&&&.       &&&&&     .&&&&&   &&&&&&#       &&&&&&&&&&&&&&&&&.      &&#    &&&   \n&&&         &&&&&&&&     &&&&&&&&        &&&&&     .&&&&/     &&&&&&     *&&&&&        &&&&&      &&#    &&&   \n&&&          &&&&&&      /&&&&&&         &&&&&     .&&&&/       &&&&&&   &&&&&         #&&&&&     &&#    &&&   \n&&&                                                                                                      &&&   \n&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&   \n&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&*/\n\n/**\n * General information:   \n * This JavaScript-based payload formatter is a parser to decode data from bytes into\n * JSON Object. It can only parse payload from PEW-1000 devices. \n * This parser follows the specification LoRaWAN® Payload Codec API Specification TS013-1.0.0.\n * \n * \n * SPDX-FileCopyrightText: Copyright (C) 2023 WIKA Alexander Wiegand SE & Co. KG   \n * SPDX-License-Identifier: MIT\n * \n * SPDX-FileName: index.js\n * SPDX-PackageVersion: 2.0.0\n *  \n*/\n\n// ***********************************************************************************\n// Public Decoding Section\n// ***********************************************************************************\n\n/**\n * ATTENTION: You must define the measurement ranges first, otherwise the script will not work.\n * The device configuration defines the measurement ranges for the supported measured variables of your used devices, e.g.\n * var PRESSURE_RANGE_START = 0;\n * var PRESSURE_RANGE_END = 10;\n * var DEVICE_TEMPERATURE_RANGE_START = -40;\n * var DEVICE_TEMPERATURE_RANGE_END = 60; \n */\n\nvar PRESSURE_RANGE_START = 0;\nvar PRESSURE_RANGE_END = 16;\nvar DEVICE_TEMPERATURE_RANGE_START = -20;\nvar DEVICE_TEMPERATURE_RANGE_END = 80;\n\n\n/**\n * Decode uplink entry point\n * @typedef  {Object}    input          - An object provided by the IoT Flow framework\n * @property {number[]}  input.bytes    - Array of bytes represented as numbers as it has been sent from the device\n * @property {number}    input.fPort    - The Port Field on which the uplink has been sent\n * @property {Date}      input.recvTime - The uplink message time recorded by the LoRaWAN network server\n */\n\n/**\n * Decoded uplink data\n * @typedef  {Object}               output     - An object to be returned to the IoT Flow framework\n * @property {Object.<string, *>}   data       - The open JavaScript object representing the decoded uplink payload when no errors occurred\n * @property {string[]}             [errors]   - A list of error messages while decoding the uplink payload\n * @property {string[]}             [warnings] - A list of warning messages that do not prevent the driver from decoding the uplink payload\n */\n\n/**\n * @typedef {Object}  DecodedUplink  - An object that represents the decoded uplink payload\n */\n\n/**\n * To decode the uplink data defined by LoRaWAN\n * @access public\n * @param   {input}     input - The object to decode\n * @returns {output}          - The decoded object\n */\nfunction decodeUplink(input) {\n  // Ignore Gateway mesagges\n  if (input[\"cmd\"] != \"rx\") {\n    return;\n  }\n\n  let battery = input[\"bat\"];\n  let freq = input[\"freq\"]\n\n  console.log(\"PPPBDA \"+JSON.stringify(input));\n\n  const hex = typeof input === 'string' ? input : input.data || '';\n  const bytes = hex.match(/.{1,2}/g).map(b => parseInt(b, 16));\n  input.bytes = bytes;\n\n  const output = decode(input);\n\n  const flat = {};\n\n  for (const key in output.data) {\n    if (typeof output.data[key] !== 'object' || output.data[key] === null) {\n      flat[key] = output.data[key];\n    }\n  }\n\n  if (output.data.measurement && Array.isArray(output.data.measurement.channels)) {\n    output.data.measurement.channels.forEach(channel => {\n      const name = channel.channelName?.toLowerCase().replace(/\\s+/g, '_') || `channel_${channel.channelId}`;\n      flat[name] = channel.value;\n    });\n  }\n\n  if (output.data.deviceInformation && typeof output.data.deviceInformation === 'object') {\n    for (const key in output.data.deviceInformation) {\n      flat[`deviceInformation_${key}`] = output.data.deviceInformation[key];\n    }\n  }\n\n  if (output.data.deviceStatistic && typeof output.data.deviceStatistic === 'object') {\n    for (const key in output.data.deviceStatistic) {\n      flat[`deviceStatistic_${key}`] = output.data.deviceStatistic[key];\n    }\n  }\n\n  if (output.data.deviceAlarm && typeof output.data.deviceAlarm === 'object') {\n    for (const key in output.data.deviceAlarm) {\n      flat[`deviceAlarm_${key}`] = output.data.deviceAlarm[key];\n    }\n  }\n\n  if (output.data.processAlarms && Array.isArray(output.data.processAlarms)) {\n    output.data.processAlarms.forEach((alarm, i) => {\n      for (const key in alarm) {\n        flat[`processAlarm_${i}_${key}`] = alarm[key];\n      }\n    });\n  }\n\n  if (output.data.technicalAlarms && Array.isArray(output.data.technicalAlarms)) {\n    output.data.technicalAlarms.forEach((alarm, i) => {\n      for (const key in alarm) {\n        flat[`technicalAlarm_${i}_${key}`] = alarm[key];\n      }\n    });\n  }\n\n  if (Object.keys(flat).length === 0) {\n    return;\n  }\n\n  //Add potentially interesting values\n  flat[\"battery (%)\"] = parseInt(battery) * (100 / 254);\n  flat[\"frecuency\"] = freq;\n\n  return flat;\n}\n\n/**\n * To decode from hex encoded string\n * @access public\n * @param   {number}    fPort             - The Port Field on which the uplink has been sent\n * @param   {string}    hexEncodedString  - A hex encoded string has been sent from the device\n * @returns {output}                      - The decoded object\n */\nfunction decodeHexString(fPort, hexEncodedString) {\n    /**\n     * @type {input}\n     */\n    var input = {};\n    input.bytes = convertHexStringToBytes(hexEncodedString);\n    input.fPort = fPort;\n\n    return decode(input);\n}\n\n/**\n * To decode from base64 string\n * @access public\n * @param   {number}    fPort                   - The Port Field on which the uplink has been sent\n * @param   {string}    base64EncodedString     - A base64 encoded string has been sent from the device\n * @returns {output}                            - The decoded object\n */\nfunction decodeBase64String(fPort, base64EncodedString) {\n    /**\n     * @type {input}\n     */\n    var input = {};\n    input.bytes = convertBase64StringToBytes(base64EncodedString);\n    input.fPort = fPort;\n\n    return decode(input);\n}\n\n\n// ***********************************************************************************\n// Private Decoding Section\n// ***********************************************************************************\n\n/**\n * Generic Data Channel Values\n */\nvar DEVICE_NAME = \"PEW-1000\";\n\nvar GENERIC_DATA_CHANNEL_RANGE_START = 2500;\nvar GENERIC_DATA_CHANNEL_RANGE_END = 12500;\nvar ERROR_VALUE = 0xffff;\n\nvar CHANNEL_NAMES_DICTIONARY = ['pressure', 'device temperature', 'battery voltage'];\nvar ALARM_EVENT_NAMES_DICTIONARY = ['triggered', 'disappeared'];\nvar ALARM_CHANNEL_NAMES_DICTIONARY = ['pressure', 'device temperature'];\nvar PROCESS_ALARM_TYPE_NAMES_DICTIONARY = ['low threshold', 'high threshold', 'falling slope', 'rising slope', 'low threshold with delay', 'high threshold with delay'];\n// alarmType\n/// Bitmask PEW-1XXX\n/// [0] = ALU saturation error (1 = activated)\n/// [1] = Sensor memory integrity error (1 = activated)\n/// [2] = Sensor busy error (1 = activated)\n/// [3] = Reserved\n/// [4] = Sensor communication error (1 = activated)\n/// [5] = Pressure out of limit (1 = activated)\n/// [6] = Temperature out of limit (1 = activated)\n/**\n * @type {{[key: number]: string}} \n */\nvar TECHNICAL_ALARM_TYPE_NAMES_DICTIONARY = {\n    1: \"ALU saturation error\", 2: \"sensor memory integrity error\", 4: \"sensor busy error\", 8: \"reserved\",\n    16: \"sensor communication error\", 32: \"pressure out of limit\", 64: \"temperature out of limit\"\n};\n/**\n * @type {{[key: number]: string}} \n */\nvar DEVICE_ALARM_TYPE_NAMES_DICTIONARY = { 0: \"battery low\", 4: \"acknowledged message not emitted\" };\nvar DEVICE_ALARM_CAUSE_OF_FAILURE_NAMES_DICTIONARY = ['generic', 'device dependent'];\n\n\n/**\n * The padStart() method of String values pads this string with another string (multiple times, if needed) until the resulting string reaches the given length.\n * The function is reimplemented to support ES5.\n * @access private\n * @param   {number}     targetLength - The length of the returned string\n * @param   {string}     padString - The string to modify\n * @returns {string}          - The decoded object\n */\nString.prototype.padStart = function (targetLength, padString) {\n\n    var tempString = this.valueOf();\n\n    for(var i = this.length; i < targetLength; ++i)\n    {\n        tempString = padString + tempString;\n    }\n\n    return tempString; \n };\n\n/**\n * To decode the uplink data\n * @access private\n * @param   {input}     input - The object to decode\n * @returns {output}          - The decoded object\n */\nfunction decode(input) {\n    // Define output object\n    console.log(\"PPPBDA decoding...\" + JSON.stringify(input));\n\n    var output = createOutputObject();\n    output = checkMeasurementRanges(output);\n    if (output.errors) {\n        return output;\n    }\n\n    /* Select subfunction to decode message */\n    switch (input.bytes[0]) {\n        /* unused */\n        default:\n        case 0x00:\n        case 0x06: // configuration status message is not supported\n        case 0x09: // extended device identification message is not supported\n            // Error, not enough bytes            \n            output = addErrorMessage(output, \"Data message type \" + input.bytes[0].toString(16).padStart(2, \"0\") + \" not supported\");\n            break;\n\n        /* Data message */\n        case 0x01:\n        case 0x02:\n            /* Check if all bytes needed for decoding are there */\n            if (input.bytes.length == 7) {\n                // decode\n                output = decodeDataMessage(input);\n            }\n            else {\n                // Error, not enough bytes\n                output = addErrorMessage(output, \"Data message 01/02 needs 7 bytes but got \" + input.bytes.length);\n            }\n            break;\n\n        /* Process alarm */\n        case 0x03:\n            /* Check if all bytes needed for decoding are there and all bytes for each alarm */\n            if (input.bytes.length >= 5 && !((input.bytes.length - 2) % 3)) {\n                // decode\n                output = decodeProcessAlarm(input);\n            }\n            else {\n                // Error, not enough bytes\n                output = addErrorMessage(output, \"Process alarm 03 needs at least 5 bytes and got \" + input.bytes.length + \". Also all bytes for each alarm needed\");\n            }\n            break;\n\n        /* Technical alarm */\n        case 0x04:\n            /* Check if all bytes needed for decoding are there and all bytes for each alarm */\n            if (input.bytes.length == 3) {\n                // decode\n                output = decodeTechnicalAlarm(input);\n            }\n            else {\n                // Error, not enough bytes\n                output = addErrorMessage(output, \"Technical alarm 04 needs 5 bytes but got \" + input.bytes.length + \". Also all bytes for each alarm needed\");\n            }\n            break;\n\n        /* Device alarm */\n        case 0x05:\n            /* Check if all bytes needed for decoding are there */\n            if (input.bytes.length >= 3 && input.bytes.length <= 4) {\n                // decode\n                output = decodeDeviceAlarm(input);\n            }\n            else {\n                // Error, not enough bytes\n                output = addErrorMessage(output, \"Device alarm 05 needs 4 bytes but got \" + input.bytes.length);\n            }\n            break;\n\n        /* Device identification */\n        case 0x07:\n            /* Check if all bytes needed for decoding are there */\n            if (input.bytes.length >= 8 && input.bytes.length <= 38) {\n                // decode\n                output = decodeDeviceIdentification(input);\n            }\n            else {\n                // Error, not enough bytes\n                output = addErrorMessage(output, \"Identification message 07 needs 41 bytes but got \" + input.bytes.length);\n            }\n            break;\n\n        /* Keep alive */\n        case 0x08:\n            /* Check if all bytes needed for decoding are there */\n            if (input.bytes.length == 3) {\n                // Decode\n                output = decodeKeepAliveMessage(input);\n            }\n            else {\n                // Error, not enough bytes\n                output = addErrorMessage(output, \"Keep alive message 08 needs 3 bytes but got \" + input.bytes.length);\n            }\n            break;\n    }\n\n    console.log(\"PPPBDA Success Decoding (output): \" + JSON.stringify(output));\n\n    return output;\n}\n\n/**\n * Decodes a data message 01, 02 into an object\n * @access private\n * @param {Object}      input           - An object provided by the IoT Flow framework\n * @param {number[]}    input.bytes     - Array of bytes represented as numbers as it has been sent from the device\n * @param {number}      input.fPort     - The Port Field on which the uplink has been sent\n * @param {Date}        input.recvTime  - The uplink message time recorded by the LoRaWAN network server\n * @returns {output}             - The decoded object\n */\nfunction decodeDataMessage(input) {\n    console.log(\"PPPBDA decoding data message...\" + JSON.stringify(input));\n\n    // Output\n    var output = createOutputObject();\n\n    // data message type\n    output.data.messageType = input.bytes[0];\n\n    // current configuration id\n    output.data.configurationId = input.bytes[1];\n\n    output.data.measurement = {};\n    output.data.measurement.channels = [];\n\n    // pressure - channel 0\n    var pressure = {};\n    pressure.channelId = 0;\n    pressure.channelName = CHANNEL_NAMES_DICTIONARY[pressure.channelId];\n    pressure.value = getCalculatedPressure(input.bytes[3] << 8 | input.bytes[4]);\n    output.data.measurement.channels.push(pressure);\n\n    // temperature - channel 1\n    var temperature = {};\n    temperature.channelId = 1;\n    temperature.channelName = CHANNEL_NAMES_DICTIONARY[temperature.channelId];\n    temperature.value = getCalculatedTemperature(input.bytes[5] << 8 | input.bytes[6]);\n    output.data.measurement.channels.push(temperature);\n\n    // battery voltage - channel x\n    var batteryVoltage = {};\n    batteryVoltage.channelId = 2;\n    batteryVoltage.channelName = CHANNEL_NAMES_DICTIONARY[batteryVoltage.channelId];\n    // battery voltage in V as single not double\n    batteryVoltage.value = (input.bytes[2] / 10);\n    output.data.measurement.channels.push(batteryVoltage);\n    return output;\n}\n\n/**\n * Decodes a process alarm 03 into an object\n * @access private\n * @param {Object}          input           - An object provided by the IoT Flow framework\n * @param {number[]}        input.bytes     - Array of bytes represented as numbers as it has been sent from the device\n * @param {number}          input.fPort     - The Port Field on which the uplink has been sent\n * @param {Date}            input.recvTime  - The uplink message time recorded by the LoRaWAN network server\n * @returns {output}                        - The decoded object\n */\nfunction decodeProcessAlarm(input) {\n\n    var output = createOutputObject();\n    output.data.processAlarms = [];\n\n    // data message type\n    output.data.messageType = input.bytes[0];\n\n    // current configuration id\n    output.data.configurationId = input.bytes[1];\n\n    for (var byteIndex = 2, alarmCounter = 0; byteIndex < input.bytes.length; byteIndex += 3, alarmCounter++) {\n        output.data.processAlarms[alarmCounter] = {};\n\n        // Alarm event 0 = triggered, 1 = disappeared\n        output.data.processAlarms[alarmCounter].event = (input.bytes[byteIndex] & 0x80) >> 7;\n        output.data.processAlarms[alarmCounter].eventName = ALARM_EVENT_NAMES_DICTIONARY[output.data.processAlarms[alarmCounter].event];\n\n        // Alarm channel 0 = pressure, 1 = device temperature\n        output.data.processAlarms[alarmCounter].channelId = (input.bytes[byteIndex] & 0x78) >> 3;\n        output.data.processAlarms[alarmCounter].channelName = ALARM_CHANNEL_NAMES_DICTIONARY[output.data.processAlarms[alarmCounter].channelId];\n\n        // Alarm channel 0 = falling thresh, 1 = rising thresh, 2 = fal slope, 3 = rising slope, 4 = fall thresh delay, 5 = rise thresh delay\n        output.data.processAlarms[alarmCounter].alarmType = (input.bytes[byteIndex] & 0x07);\n        output.data.processAlarms[alarmCounter].alarmTypeName = PROCESS_ALARM_TYPE_NAMES_DICTIONARY[output.data.processAlarms[alarmCounter].alarmType];\n\n        // Alarm value\n        output.data.processAlarms[alarmCounter].value = getRealValueByChannelNumberAndAlarmType(output.data.processAlarms[alarmCounter].channelId, \n            output.data.processAlarms[alarmCounter].alarmType,\n            input.bytes[byteIndex + 1] << 8 | input.bytes[byteIndex + 2]);\n            \n        if (output.data.processAlarms[alarmCounter].value == ERROR_VALUE) {\n            output = addWarningMessage(output, \"Invalid data for \" + output.data.processAlarms[alarmCounter].channelName + \"channel\");\n        }\n\n    }\n\n    return output;\n}\n\n/**\n * Decodes a technical alarm 04 into an object\n * @access private\n * @param {Object}          input           - An object provided by the IoT Flow framework\n * @param {number[]}        input.bytes     - Array of bytes represented as numbers as it has been sent from the device\n * @param {number}          input.fPort     - The Port Field on which the uplink has been sent\n * @param {Date}            input.recvTime  - The uplink message time recorded by the LoRaWAN network server\n * @returns {output}                 - The decoded object\n */\nfunction decodeTechnicalAlarm(input) {\n    // Output\n    var output = createOutputObject();\n    output.data.technicalAlarms = [];\n\n    // data message type\n    output.data.messageType = input.bytes[0];\n\n    // current configuration id\n    output.data.configurationId = input.bytes[1];\n\n    for (var byteIndex = 2, alarmCounter = 0; byteIndex < input.bytes.length; byteIndex += 3, alarmCounter++) {\n        output.data.technicalAlarms[alarmCounter] = {};\n\n        // Alarm event 0 = triggered, 1 = disappeared\n        output.data.technicalAlarms[alarmCounter].event = (input.bytes[byteIndex] & 0x80) >> 7;\n        output.data.technicalAlarms[alarmCounter].eventName = ALARM_EVENT_NAMES_DICTIONARY[output.data.technicalAlarms[alarmCounter].event];\n\n        output.data.technicalAlarms[0].alarmType = (input.bytes[2] & 0x7f);\n\n        // Go through each bit and check if set\n        output.data.technicalAlarms[0].alarmTypeNames = [];\n        for (var j = 0, i = 0; j < 7; j++) {\n            // Check if bit is set\n            if (output.data.technicalAlarms[0].alarmType & (1 << j)) {\n                output.data.technicalAlarms[0].alarmTypeNames[i] = TECHNICAL_ALARM_TYPE_NAMES_DICTIONARY[1 << j];\n                i++;\n            }\n        }\n    }\n    return output;\n}\n\n/**\n * Decodes a device alarm 05 into an object\n * @access private\n * @param {Object}              input           - An object provided by the IoT Flow framework\n * @param {number[]}            input.bytes     - Array of bytes represented as numbers as it has been sent from the device\n * @param {number}              input.fPort     - The Port Field on which the uplink has been sent\n * @param {Date}                input.recvTime  - The uplink message time recorded by the LoRaWAN network server\n * @returns {output}                     - The decoded object\n */\nfunction decodeDeviceAlarm(input) {\n    // Output\n    var output = createOutputObject();\n\n    // data message type\n    output.data.messageType = input.bytes[0];\n\n    // current configuration id\n    output.data.configurationId = input.bytes[1];\n\n    // Create deviceAlarm\n    output.data.deviceAlarm = {};\n\n    // Alarm event 0 = triggered, 1 = disappeared\n    output.data.deviceAlarm.event = (input.bytes[2] & 0x80) >> 7;\n    output.data.deviceAlarm.eventName = ALARM_EVENT_NAMES_DICTIONARY[output.data.deviceAlarm.event];\n\n    // Generic or device dependent 1 = device dependent    \n    output.data.deviceAlarm.causeOfFailure = (input.bytes[2] & 0x60) >> 6;\n    output.data.deviceAlarm.causeOfFailureName = DEVICE_ALARM_CAUSE_OF_FAILURE_NAMES_DICTIONARY[output.data.deviceAlarm.causeOfFailure];\n\n    // Alarm type 0 = low battery, 4 = Acknowledged message not emitted\n    output.data.deviceAlarm.alarmType = (input.bytes[2] & 0x1f);\n    output.data.deviceAlarm.alarmTypeName = DEVICE_ALARM_TYPE_NAMES_DICTIONARY[output.data.deviceAlarm.alarmType];\n\n    switch (output.data.deviceAlarm.alarmType) {\n        // low battery alarm has an value\n        case 0:\n            // The alarm value is an int8, but we have an int32, so we shift 24 bits to the left and 24 bits to the right \n            // and as a result we get an 8 bit integer in a 32 bit integer\n            output.data.deviceAlarm.value = (input.bytes[3] << 24 >> 24) / 10;\n            break;\n\n        // All other don't have any alarm value\n        case 4:\n        default:\n            break;\n    }\n    return output;\n}\n\n/**\n * Decodes a keep alive message 08 into an object\n * @access private\n * @param {Object}              input           - An object provided by the IoT Flow framework\n * @param {number[]}            input.bytes     - Array of bytes represented as numbers as it has been sent from the device\n * @param {number}              input.fPort     - The Port Field on which the uplink has been sent\n * @param {Date}                input.recvTime  - The uplink message time recorded by the LoRaWAN network server\n * @returns {output}                     - The decoded object\n */\nfunction decodeKeepAliveMessage(input) {\n    // Output\n    var output = createOutputObject();\n\n    // data message type\n    output.data.messageType = input.bytes[0];\n\n    // current configuration id\n    output.data.configurationId = input.bytes[1];\n\n    output.data.deviceStatistic = {};\n    // Battery level event indicator\n    output.data.deviceStatistic.batteryLevelNewEvent = (input.bytes[2] & 0x80) >> 7 ? true : false;\n\n    // battery level in percent\n    output.data.deviceStatistic.batteryLevelPercent = input.bytes[2] & 0x7f;\n\n    return output;\n}\n\n/**\n * Decodes a device identification message 07 into an object\n * @access private\n * @param {Object}              input           - An object provided by the IoT Flow framework\n * @param {number[]}            input.bytes     - Array of bytes represented as numbers as it has been sent from the device\n * @param {number}              input.fPort     - The Port Field on which the uplink has been sent\n * @param {Date}                input.recvTime  - The uplink message time recorded by the LoRaWAN network server\n * @returns {output}                            - The decoded object\n */\nfunction decodeDeviceIdentification(input) {\n    // Output\n    var output = createOutputObject();\n\n    // Data message type    \n    output.data.messageType = input.bytes[0];\n\n    // Configuration id\n    output.data.configurationId = input.bytes[1];\n\n    output.data.deviceInformation = {};\n    // Product id raw\n    output.data.deviceInformation.productId = input.bytes[2];\n\n    // Wireless module type\n    output.data.deviceInformation.productIdName = input.bytes[2] == 11 ? \"PEW\" : input.bytes[2];\n\n    // Product sub id\n    output.data.deviceInformation.productSubId = input.bytes[3];\n\n    // Product sub id resolved\n    switch (input.bytes[3]) {\n        case 0:\n            output.data.deviceInformation.productSubIdName = \"LoRaWAN\";\n            break;\n\n        case 1:\n            output.data.deviceInformation.productSubIdName = \"MIOTY\";\n            break;\n\n        default:\n            output.data.deviceInformation.productSubIdName = \"Unknown\";\n            break;\n    }\n\n    // Wireless module firmware version\n    output.data.deviceInformation.wirelessModuleFirmwareVersion = ((input.bytes[4] >> 4) & 0x0f).toString() + \".\" + (input.bytes[4] & 0x0f).toString() + \".\" + (input.bytes[5]).toString();\n\n    // Wireless module hardware version\n    output.data.deviceInformation.wirelessModuleHardwareVersion = ((input.bytes[6] >> 4) & 0x0f).toString() + \".\" + (input.bytes[6] & 0x0f).toString() + \".\" + (input.bytes[7]).toString();\n\n    /* return function if no sensor data is available */\n    if (input.bytes.length < 38) {\n        output = addErrorMessage(output, \"Device identification frame 07 has not all bytes included, received \" + input.bytes.length + \"/38 bytes\");\n        return output;\n    }\n\n    // Sensor serial number\n    output.data.deviceInformation.serialNumber = \"\";\n    for (var i = 8; i < 19; i++) {\n        if (input.bytes[i] == 0) {\n            break;\n        }\n        output.data.deviceInformation.serialNumber += String.fromCharCode(input.bytes[i]);\n    }\n\n    // Pressure type\n    switch (input.bytes[19]) {\n        case 1:\n            output.data.deviceInformation.pressureType = \"absolute\";\n            break;\n\n        case 2:\n            output.data.deviceInformation.pressureType = \"gauge / relative\";\n            break;\n\n        default:\n            output.data.deviceInformation.pressureType = \"unknown\";\n            break;\n    }\n\n    // Min range pressure\n    output.data.deviceInformation.measurementRangeStartPressure = convertHexToFloatIEEE754(input.bytes[20].toString(16).padStart(2, \"0\") + input.bytes[21].toString(16).padStart(2, \"0\") + input.bytes[22].toString(16).padStart(2, \"0\") + input.bytes[23].toString(16).padStart(2, \"0\"));\n    output.data.deviceInformation.measurementRangeStartPressure = Number(output.data.deviceInformation.measurementRangeStartPressure.toFixed(6));\n\n    // Max range pressure\n    output.data.deviceInformation.measurementRangeEndPressure = convertHexToFloatIEEE754(input.bytes[24].toString(16).padStart(2, \"0\") + input.bytes[25].toString(16).padStart(2, \"0\") + input.bytes[26].toString(16).padStart(2, \"0\") + input.bytes[27].toString(16).padStart(2, \"0\"));\n    output.data.deviceInformation.measurementRangeEndPressure = Number(output.data.deviceInformation.measurementRangeEndPressure.toFixed(6));\n\n    // Min range device temperature\n    output.data.deviceInformation.measurementRangeStartDeviceTemperature = convertHexToFloatIEEE754(input.bytes[28].toString(16).padStart(2, \"0\") + input.bytes[29].toString(16).padStart(2, \"0\") + input.bytes[30].toString(16).padStart(2, \"0\") + input.bytes[31].toString(16).padStart(2, \"0\"));\n    output.data.deviceInformation.measurementRangeStartDeviceTemperature = Number(output.data.deviceInformation.measurementRangeStartDeviceTemperature.toFixed(6));\n\n    // Max range device temperature\n    output.data.deviceInformation.measurementRangeEndDeviceTemperature = convertHexToFloatIEEE754(input.bytes[32].toString(16).padStart(2, \"0\") + input.bytes[33].toString(16).padStart(2, \"0\") + input.bytes[34].toString(16).padStart(2, \"0\") + input.bytes[35].toString(16).padStart(2, \"0\"));\n    output.data.deviceInformation.measurementRangeEndDeviceTemperature = Number(output.data.deviceInformation.measurementRangeEndDeviceTemperature.toFixed(6));\n\n    // Unit pressure\n    output.data.deviceInformation.pressureUnit = input.bytes[36];\n    output.data.deviceInformation.pressureUnitName = returnPhysicalUnitFromId(input.bytes[36]);\n\n    // Unit pressure\n    output.data.deviceInformation.deviceTemperatureUnit = input.bytes[37];\n    output.data.deviceInformation.deviceTemperatureUnitName = returnPhysicalUnitFromId(input.bytes[37]);\n\n    return output;\n}\n\n// ***********************************************************************************\n//          Additional Functions Section\n// ***********************************************************************************\n/**\n * Converts a hex string number to float number follows the IEEE 754 standard and it's ES5 compatible\n * @access private\n * @param  {string} hexString   - Float as string \"3.141\"\n * @return {number}             - returns a float\n * @see https://gist.github.com/Jozo132/2c0fae763f5dc6635a6714bb741d152f 2022 by Jozo132 \n */\nfunction convertHexToFloatIEEE754(hexString) {\n    var int = parseInt(hexString, 16);\n    if (int > 0 || int < 0) {\n        var sign = (int >>> 31) ? -1 : 1;\n        var exp = (int >>> 23 & 0xff) - 127;\n        var mantissa = ((int & 0x7fffff) + 0x800000).toString(2);\n        var float32 = 0\n        for (var i = 0; i < mantissa.length; i += 1) { float32 += parseInt(mantissa[i]) ? Math.pow(2, exp) : 0; exp-- }\n        return float32 * sign;\n    } else return 0\n}\n\n/**\n * Checks the user defined measurement ranges\n * @access private\n * @param  {output} output - Output object\n * @return {output}        - Returns if the ranges are correct defined\n */\nfunction checkMeasurementRanges(output) {\n    if (typeof PRESSURE_RANGE_START === 'undefined') {\n        output = addErrorMessage(output, \"The PRESSURE_RANGE_START was not set.\");\n    }\n\n    if (typeof PRESSURE_RANGE_END === 'undefined') {\n        output = addErrorMessage(output, \"The PRESSURE_RANGE_END was not set.\");\n    }\n\n    if (typeof DEVICE_TEMPERATURE_RANGE_START === 'undefined') {\n        output = addErrorMessage(output, \"The DEVICE_TEMPERATURE_RANGE_START was not set.\");\n    }\n\n    if (typeof DEVICE_TEMPERATURE_RANGE_END === 'undefined') {\n        output = addErrorMessage(output, \"The DEVICE_TEMPERATURE_RANGE_END was not set.\");\n    }\n\n    if (PRESSURE_RANGE_START >= PRESSURE_RANGE_END) {\n        output = addErrorMessage(output, \"The PRESSURE_RANGE_START must not be greater or equal to PRESSURE_RANGE_END, \" + PRESSURE_RANGE_START + \" >= \" + PRESSURE_RANGE_END + \". \");\n    }\n\n    if (DEVICE_TEMPERATURE_RANGE_START >= DEVICE_TEMPERATURE_RANGE_END) {\n        output = addErrorMessage(output, \"The DEVICE_TEMPERATURE_RANGE_START must not be greater or equal to DEVICE_TEMPERATURE_RANGE_END, \" + DEVICE_TEMPERATURE_RANGE_START + \" >= \" + DEVICE_TEMPERATURE_RANGE_END + \".\");\n    }\n\n    return output;\n}\n\n/**\n * Adds warning to output object\n * @param {output} output\n * @param {string} warningMessage\n * @access private \n */\nfunction addWarningMessage(output, warningMessage) {\n    // use only functional supported by ECMA-262 5th edition. The nullish assign and .at are not supported. output.warnings ??= [];\n    output.warnings = output.warnings || [];\n    output.warnings.push(DEVICE_NAME + \" (JS): \" + warningMessage);\n    return output;\n}\n\n/**\n * Adds private to output object\n * @param {output} output\n * @param {string} errorMessage\n * @access private \n */\nfunction addErrorMessage(output, errorMessage) {\n    console.log(\"PPPBDA ERROR: \" + errorMessage);\n    output.errors = output.errors || [];\n    output.errors.push(DEVICE_NAME + \" (JS): \" + errorMessage);\n    return output;\n}\n\n/**\n * Creates an empty output object\n * @returns {output}        - Returns an output object\n * @access private\n */\nfunction createOutputObject() {\n    return {\n        data: {},\n    }\n}\n\n\n/**\n * Set measurement ranges only for test purposes\n * @access protected\n * @param  {Number} pressureRangeStart   range start\n * @param  {Number} pressureRangeEnd     range end\n * @param  {Number} temperatureRangeStart   range start\n * @param  {Number} temperatureRangeEnd     range end\n */\nfunction setMeasurementRanges(pressureRangeStart, pressureRangeEnd, temperatureRangeStart, temperatureRangeEnd) {\n    PRESSURE_RANGE_START = pressureRangeStart;\n    PRESSURE_RANGE_END = pressureRangeEnd;\n    DEVICE_TEMPERATURE_RANGE_START = temperatureRangeStart;\n    DEVICE_TEMPERATURE_RANGE_END = temperatureRangeEnd;\n}\n\n\n/**\n * Returns the real physical value of the channel value based on measurement range\n * @access private\n * @param  {Number} channelValue    channel value as integer \n * @param  {Number} measurementRangeStart   range start\n * @param  {Number} measurementRangeEnd     range end\n * @param  {Number} measuringRangeStart   range start (MRS)\n * @param  {Number} measuringRangeEnd     range end (MRE)\n * @return {Number} Returns real physical value e.g. 10 °C\n */\nfunction getCalculatedValue(channelValue, measurementRangeStart, measurementRangeEnd, measuringRangeStart, measuringRangeEnd) {\n    var calculatedValue = (channelValue - measuringRangeStart) * ((measurementRangeEnd - measurementRangeStart) / (measuringRangeEnd - measuringRangeStart)) + measurementRangeStart;\n    // round to the third number after the comma\n    return Math.round(calculatedValue * 1000) / 1000;\n}\n\n/**\n * Returns the real physical value of pressure based on measurement range\n * @param {Number} channelValue  \n * @access private\n */\nfunction getCalculatedPressure(channelValue) {\n    return getCalculatedValue(channelValue, PRESSURE_RANGE_START, PRESSURE_RANGE_END, GENERIC_DATA_CHANNEL_RANGE_START, GENERIC_DATA_CHANNEL_RANGE_END);\n}\n\n/**\n * Returns the real physical value of temperature based on measurement range\n * @param {Number} channelValue  \n * @access private\n */\nfunction getCalculatedTemperature(channelValue) {\n    return getCalculatedValue(channelValue, DEVICE_TEMPERATURE_RANGE_START, DEVICE_TEMPERATURE_RANGE_END, GENERIC_DATA_CHANNEL_RANGE_START, GENERIC_DATA_CHANNEL_RANGE_END);\n}\n\n/**\n * Returns the real physical value of the channel number based on measurement range\n * @param {Number} channelValue  \n * @param {Number} alarmType  \n * @param {Number} channelNumber  \n * @access private\n */\nfunction getRealValueByChannelNumberAndAlarmType(channelNumber, alarmType, channelValue) {\n    if (channelNumber == 0) // pressure channel\n    {\n        if (alarmType == 3 || alarmType == 4) {\n            return getSlopeValue(channelValue, PRESSURE_RANGE_START, PRESSURE_RANGE_END);\n        }\n        else {\n            return getThresholdValue(channelValue, PRESSURE_RANGE_START, PRESSURE_RANGE_END);\n        }\n    }\n    else if (channelNumber == 1) // temperature channel\n    {\n        if (alarmType == 3 || alarmType == 4) {\n            return getSlopeValue(channelValue, DEVICE_TEMPERATURE_RANGE_START, DEVICE_TEMPERATURE_RANGE_END);\n        }\n        else {\n            return getThresholdValue(channelValue, DEVICE_TEMPERATURE_RANGE_START, DEVICE_TEMPERATURE_RANGE_END);\n        }\n    }\n\n    return ERROR_VALUE;\n}\n\n/**\n * Returns the real threshold value of pressure based on measurement range (measurend)\n * @param {Number} channelValue  \n * @param  {Number} measurementRangeStart   range start\n * @param  {Number} measurementRangeEnd     range end\n * @access private\n */\nfunction getThresholdValue(channelValue, measurementRangeStart, measurementRangeEnd) {\n    return getCalculatedValue(channelValue, measurementRangeStart, measurementRangeEnd, 2500, 12500);\n}\n\n/**\n * Returns the real physical value of pressure based on measurement range (measurend/minute)\n * @param {Number} channelValue  \n * @param  {Number} measurementRangeStart   range start\n * @param  {Number} measurementRangeEnd     range end\n * @access private\n */\nfunction getSlopeValue(channelValue, measurementRangeStart, measurementRangeEnd) {\n    return getCalculatedValue(channelValue, measurementRangeStart, measurementRangeEnd, 0, 10000);\n}\n\n\n/**\n * To convert a hex encoded string to a integer array\n * @param {string} hexEncodedString\n * @access private\n */\nfunction convertHexStringToBytes(hexEncodedString) {\n    if (hexEncodedString.startsWith(\"0x\")) {\n        hexEncodedString = hexEncodedString.slice(2);\n    }\n\n    // remove spaces\n    hexEncodedString = hexEncodedString.replace(/\\s/g, '');\n\n    var bytes = [];\n\n    // convert byte to byte (2 characters are 1 byte)\n    for (var i = 0; i < hexEncodedString.length; i += 2) {\n        // extract 2 characters\n        var hex = hexEncodedString.substr(i, 2);\n\n        // convert hex pair to integer\n        var intValue = parseInt(hex, 16);\n\n        bytes.push(intValue);\n    }\n\n    return bytes;\n}\n\n/**\n * To convert a base64 encoded string to a integer array\n * @param {string} base64EncodedString\n * @access private\n */\nfunction convertBase64StringToBytes(base64EncodedString) {\n    var bytes = [];\n\n    // convert base64 to string\n    var decodedBytes = Buffer.from(base64EncodedString, 'base64')\n\n    // convert byte to byte (2 characters are 1 byte)\n    for (var i = 0; i < decodedBytes.length; i++) {\n        // convert byte to integer\n        var intValue = decodedBytes[i];\n\n        bytes.push(intValue);\n    }\n\n    return bytes;\n}\n\n/**\n * Returns the printable name of a measurand for a LPP supporting devices e.g.: 1 = \"Temperature\"\n * @access private\n * @param  {Number} id    Identifier as integer \n * @return {string}       Returns the printable name of a physical unit PGW23.100.11 e.g.: 1 = \"mBar\"\n */\nfunction returnPhysicalUnitFromId(id) {\n    switch (id) {\n        case 1:\n            return \"inH2O\";\n        case 2:\n            return \"inHg\";\n        case 3:\n            return \"ftH2O\";\n        case 4:\n            return \"mmH2O\";\n        case 5:\n            return \"mmHg\";\n        case 6:\n            return \"psi\";\n        case 7:\n            return \"bar\";\n        case 8:\n            return \"mbar\";\n        case 9:\n            return \"g/cm²\";\n        case 10:\n            return \"kg/cm²\";\n        case 11:\n            return \"Pa\";\n        case 12:\n            return \"kPa\";\n        case 13:\n            return \"Torr\";\n        case 14:\n            return \"at\";\n        case 145:\n            return \"inH2O (60 °F)\";\n        case 170:\n            return \"cmH2O (4 °C)\";\n        case 171:\n            return \"mH2O (4 °C)\";\n        case 172:\n            return \"cmHg\";\n        case 173:\n            return \"lb/ft²\";\n        case 174:\n            return \"hPa\";\n        case 175:\n            return \"psia\";\n        case 176:\n            return \"kg/m²\";\n        case 177:\n            return \"ftH2O (4 °C)\";\n        case 178:\n            return \"ftH2O (60 °F)\";\n        case 179:\n            return \"mHg\";\n        case 180:\n            return \"Mpsi\";\n        case 237:\n            return \"MPa\";\n        case 238:\n            return \"inH2O (4 °C)\";\n        case 239:\n            return \"mmH2O (4 °C)\";\n        case 32:\n            return \"°C\";\n        case 33:\n            return \"°F\";\n        default:\n            return \"Unknown\";\n    }\n}\n\nfunction getPluginSource(payload) {\n    return payload[\"source\"];\n}\n\n// ***********************************************************************************\n//          Export functions Section\n// ***********************************************************************************\nif (typeof exports !== 'undefined') {\n    exports.decodeUplink = decodeUplink;\n    exports.setMeasurementRanges = setMeasurementRanges;\n    exports.decodeHexString = decodeHexString;\n    exports.decodeBase64String = decodeBase64String;\n}\n\n\n",
                        "environment": "javascript",
                        "storage": "",
                        "version": "1.0"
                    },
                    "properties": {
                        "source": {
                            "data": {
                                "payload": "{{payload}}",
                                "payload_function": "getPluginSource",
                                "payload_type": "source_payload",
                                "resource": "uplink",
                                "source": "resource",
                                "update": "events"
                            },
                            "default": {
                                "source": "value"
                            },
                            "enabled": true
                        },
                        "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": {
                                "name": "Wika PEW1000 Dashboard",
                                "placeholders": {
                                    "sources": []
                                },
                                "properties": {
                                    "background_image": "#132533",
                                    "border_radius": "7",
                                    "hide_header": false,
                                    "template": true
                                },
                                "tabs": [
                                    {
                                        "icon": "fas fa-tachometer-alt",
                                        "widgets": [
                                            {
                                                "layout": {
                                                    "col": 0,
                                                    "row": 0,
                                                    "sizeX": 1,
                                                    "sizeY": 11
                                                },
                                                "panel": {
                                                    "color": "#ffffff",
                                                    "currentColor": "#ffffff",
                                                    "showOffline": {
                                                        "type": "none"
                                                    }
                                                },
                                                "properties": {
                                                    "refresh_interval": 0
                                                },
                                                "sources": [
                                                    {
                                                        "color": "#1abc9c",
                                                        "image_url": "https://www.wika.com/media/Images/Product_700x700/Pressure/pew_1000_pew_1200_en_co_rs_w410_h410_image.jpg",
                                                        "name": "Source 1",
                                                        "source": "image_url"
                                                    }
                                                ],
                                                "type": "image"
                                            },
                                            {
                                                "layout": {
                                                    "col": 3,
                                                    "row": 0,
                                                    "sizeX": 1,
                                                    "sizeY": 5
                                                },
                                                "panel": {
                                                    "color": "#ffffff",
                                                    "currentColor": "#ffffff",
                                                    "showOffline": {
                                                        "type": "none"
                                                    },
                                                    "title": "Device Temperature"
                                                },
                                                "properties": {
                                                    "color": "#1E313E",
                                                    "gradient": true,
                                                    "max": 80,
                                                    "min": 0,
                                                    "unit": "ºC"
                                                },
                                                "sources": [
                                                    {
                                                        "bucket": {
                                                            "backend": "mongodb",
                                                            "id": "pew1000_data_bucket",
                                                            "mapping": "device_temperature",
                                                            "tags": {
                                                                "device": [
                                                                    "wika_pew1000_70B3D597B0000D15"
                                                                ],
                                                                "group": []
                                                            }
                                                        },
                                                        "color": "#1abc9c",
                                                        "name": "Source 1",
                                                        "source": "bucket",
                                                        "timespan": {
                                                            "mode": "latest"
                                                        }
                                                    }
                                                ],
                                                "type": "gauge"
                                            },
                                            {
                                                "layout": {
                                                    "col": 3,
                                                    "row": 5,
                                                    "sizeX": 1,
                                                    "sizeY": 3
                                                },
                                                "panel": {
                                                    "color": "#ffffff",
                                                    "currentColor": "#ffffff",
                                                    "showOffline": {
                                                        "type": "none"
                                                    },
                                                    "title": "Battery Level"
                                                },
                                                "properties": {
                                                    "icon": "",
                                                    "iconSize": "",
                                                    "max": 100,
                                                    "min": 0,
                                                    "unit": "%"
                                                },
                                                "sources": [
                                                    {
                                                        "color": "#1abc9c",
                                                        "device_bucket": {
                                                            "backend": "mongodb",
                                                            "device": "wika_pew1000_70B3D597B0000D15",
                                                            "id": "pew1000_data_bucket",
                                                            "mapping": "battery (%)",
                                                            "tags": {
                                                                "device": [],
                                                                "group": []
                                                            }
                                                        },
                                                        "name": "Source 1",
                                                        "source": "device_bucket",
                                                        "timespan": {
                                                            "mode": "latest"
                                                        }
                                                    }
                                                ],
                                                "type": "progressbar"
                                            },
                                            {
                                                "layout": {
                                                    "col": 4,
                                                    "row": 0,
                                                    "sizeX": 2,
                                                    "sizeY": 13
                                                },
                                                "panel": {
                                                    "color": "#ffffff",
                                                    "currentColor": "#ffffff",
                                                    "showOffline": {
                                                        "type": "none"
                                                    }
                                                },
                                                "properties": {
                                                    "mapType": "satellite",
                                                    "showClustering": true,
                                                    "showConnected": true,
                                                    "showDisconnected": true,
                                                    "showOptions": false,
                                                    "showSearch": true
                                                },
                                                "sources": [
                                                    {
                                                        "color": "#1abc9c",
                                                        "name": "Source 1"
                                                    }
                                                ],
                                                "type": "assets_map"
                                            },
                                            {
                                                "layout": {
                                                    "col": 3,
                                                    "row": 8,
                                                    "sizeX": 1,
                                                    "sizeY": 3
                                                },
                                                "panel": {
                                                    "color": "#ffffff",
                                                    "currentColor": "#ffffff",
                                                    "showOffline": {
                                                        "type": "none"
                                                    },
                                                    "title": "Battery Voltage"
                                                },
                                                "properties": {
                                                    "color": "#1E313E",
                                                    "decimal_places": 1,
                                                    "icon": "",
                                                    "size": "75px",
                                                    "unit": "V",
                                                    "unit_size": "20px",
                                                    "weight": "font-bold"
                                                },
                                                "sources": [
                                                    {
                                                        "bucket": {
                                                            "backend": "mongodb",
                                                            "id": "pew1000_data_bucket",
                                                            "mapping": "battery_voltage",
                                                            "tags": {
                                                                "device": [
                                                                    "wika_pew1000_70B3D597B0000D15"
                                                                ],
                                                                "group": []
                                                            }
                                                        },
                                                        "color": "#1abc9c",
                                                        "name": "Source 1",
                                                        "source": "bucket",
                                                        "timespan": {
                                                            "mode": "latest"
                                                        }
                                                    }
                                                ],
                                                "type": "text"
                                            },
                                            {
                                                "layout": {
                                                    "col": 0,
                                                    "row": 11,
                                                    "sizeX": 4,
                                                    "sizeY": 7
                                                },
                                                "panel": {
                                                    "color": "#ffffff",
                                                    "currentColor": "#ffffff",
                                                    "showOffline": {
                                                        "type": "none"
                                                    },
                                                    "title": "Pressure"
                                                },
                                                "properties": {
                                                    "axis": true,
                                                    "fill": true,
                                                    "legend": false,
                                                    "multiple_axes": false
                                                },
                                                "sources": [
                                                    {
                                                        "aggregation": {},
                                                        "bucket": {
                                                            "backend": "mongodb",
                                                            "id": "pew1000_data_bucket",
                                                            "mapping": "pressure",
                                                            "tags": {
                                                                "device": [
                                                                    "wika_pew1000_70B3D597B0000D15"
                                                                ],
                                                                "group": []
                                                            }
                                                        },
                                                        "color": "#1abc9c",
                                                        "name": "Pressure",
                                                        "source": "bucket",
                                                        "timespan": {
                                                            "magnitude": "hour",
                                                            "mode": "relative",
                                                            "period": "latest",
                                                            "value": 2
                                                        }
                                                    }
                                                ],
                                                "type": "chart"
                                            },
                                            {
                                                "api": {},
                                                "layout": {
                                                    "col": 1,
                                                    "row": 0,
                                                    "sizeX": 2,
                                                    "sizeY": 11
                                                },
                                                "panel": {
                                                    "color": "#ffffff",
                                                    "currentColor": "#ffffff",
                                                    "showOffline": {
                                                        "type": "none"
                                                    },
                                                    "title": "Pressure",
                                                    "updateTs": 1752583769912
                                                },
                                                "properties": {
                                                    "alignTimeSeries": false,
                                                    "colors": [
                                                        {
                                                            "color": "#ff9500",
                                                            "max": 8,
                                                            "min": 6
                                                        },
                                                        {
                                                            "color": "#d40202",
                                                            "max": 10,
                                                            "min": 8
                                                        }
                                                    ],
                                                    "dataAppend": false,
                                                    "majorTicks": 1,
                                                    "max": 10,
                                                    "min": 0,
                                                    "options": "var options = {\n  series: [\n    {\n      type: 'gauge',\n      startAngle: 180,\n      endAngle: 0,\n      min: 0,\n      max: 240,\n      splitNumber: 12,\n      itemStyle: {\n        color: '#58D9F9',\n        shadowColor: 'rgba(0,138,255,0.45)',\n        shadowBlur: 10,\n        shadowOffsetX: 2,\n        shadowOffsetY: 2\n      },\n      progress: {\n        show: true,\n        roundCap: true,\n        width: 18\n      },\n      pointer: {\n        icon: 'path://M2090.36389,615.30999 L2090.36389,615.30999 C2091.48372,615.30999 2092.40383,616.194028 2092.44859,617.312956 L2096.90698,728.755929 C2097.05155,732.369577 2094.2393,735.416212 2090.62566,735.56078 C2090.53845,735.564269 2090.45117,735.566014 2090.36389,735.566014 L2090.36389,735.566014 C2086.74736,735.566014 2083.81557,732.63423 2083.81557,729.017692 C2083.81557,728.930412 2083.81732,728.84314 2083.82081,728.755929 L2088.2792,617.312956 C2088.32396,616.194028 2089.24407,615.30999 2090.36389,615.30999 Z',\n        length: '75%',\n        width: 16,\n        offsetCenter: [0, '5%']\n      },\n      axisLine: {\n        roundCap: true,\n        lineStyle: {\n          width: 18\n        }\n      },\n      axisTick: {\n        splitNumber: 2,\n        lineStyle: {\n          width: 2,\n          color: '#999'\n        }\n      },\n      splitLine: {\n        length: 12,\n        lineStyle: {\n          width: 3,\n          color: '#999'\n        }\n      },\n      axisLabel: {\n        distance: 30,\n        color: '#999',\n        fontSize: 20\n      },\n      title: {\n        show: false\n      },\n      detail: {\n        backgroundColor: '#fff',\n        borderColor: '#999',\n        borderWidth: 2,\n        width: '60%',\n        lineHeight: 40,\n        height: 40,\n        borderRadius: 8,\n        offsetCenter: [0, '35%'],\n        valueAnimation: true,\n        formatter: function (value) {\n          return '{value|' + value.toFixed(0) + '}{unit|km/h}';\n        },\n        rich: {\n          value: {\n            fontSize: 50,\n            fontWeight: 'bolder',\n            color: '#777'\n          },\n          unit: {\n            fontSize: 20,\n            color: '#999',\n            padding: [0, 0, -20, 10]\n          }\n        }\n      },\n      data: [\n        {\n          value: 100\n        }\n      ]\n    }\n  ]\n};",
                                                    "plateColor": "#ffffff",
                                                    "realTimeUpdate": true,
                                                    "showValue": true,
                                                    "textColor": "#1E313E",
                                                    "tickColor": "#000000",
                                                    "unit": "bar"
                                                },
                                                "sources": [
                                                    {
                                                        "aggregation": {},
                                                        "bucket": {
                                                            "backend": "mongodb",
                                                            "id": "pew1000_data_bucket",
                                                            "mapping": "pressure",
                                                            "tags": {
                                                                "device": [
                                                                    "wika_pew1000_70B3D597B0000D15"
                                                                ],
                                                                "group": []
                                                            }
                                                        },
                                                        "color": "#1abc9c",
                                                        "name": "pressure",
                                                        "source": "bucket",
                                                        "timespan": {
                                                            "magnitude": "hour",
                                                            "mode": "latest",
                                                            "period": "latest",
                                                            "value": 6
                                                        }
                                                    }
                                                ],
                                                "type": "tachometer"
                                            }
                                        ]
                                    }
                                ]
                            }
                        }
                    ]
                }
            }
        ]
    }
}