Skip to content

Plugin file

Plugin configuration file
{
  "name": "wika-pgw23-100",
  "version": "1.0.0",
  "description": "Bourdon tube pressure gauge with wireless transmission",
  "author": "Thinger.io",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/thinger-io/plugins.git",
    "directory": "wika-pgw23-100"
  },
  "metadata": {
    "name": "Wika PGW23-100 Pressure Sensor",
    "description": "Bourdon tube pressure gauge with wireless transmission",
    "image": "https://www.wika.com/media/Images/Product_700x700/Pressure/pgw23.100_en_co_rs_w410_h410_image.jpg"
  },
  "resources": {
    "products": [
      {
        "description": "Bourdon tube pressure gauge with wireless LoRaWAN transmission",
        "enabled": true,
        "name": "Wika PGW23-100 Pressure Sensor",
        "product": "wika_pgw23_100",
        "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": {
              "device_id_resolver": "get_id",
              "enabled": true,
              "handle_connectivity": true,
              "request": {
                "data": {
                  "payload": "{{payload}}",
                  "payload_function": "",
                  "payload_type": "source_payload",
                  "target": "resource_stream"
                }
              },
              "response": {
                "data": {}
              }
            }
          },
          "autoprovisions": {
            "pgw23_100_autoprovisioning": {
              "config": {
                "mode": "pattern",
                "pattern": "wika_pgw23_100_.*"
              },
              "enabled": true
            }
          },
          "buckets": {
            "pgw23_100_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 PGW23.100.11 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 = 10;\nvar DEVICE_TEMPERATURE_RANGE_START = -40;\nvar DEVICE_TEMPERATURE_RANGE_END = 60;\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 */\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 = \"PGW23.100.11\";\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'];\nvar TECHNICAL_ALARM_CAUSE_OF_FAILURE_NAMES_DICTIONARY = ['', 'general failure'];\nvar DEVICE_ALARM_CAUSE_OF_FAILURE_NAMES_DICTIONARY = ['', 'device dependent'];\nvar DEVICE_ALARM_TYPE_NAMES_DICTIONARY = ['low temperature alarm'];\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 = padStrin\noffline: falseg + tempString;\n    }\n\n    return tempString; \n };\n\n function print(asd) {\n    console.log(\"PPPBDA\" + asd);\n }\n\nfunction decodeUplink(arg) {\n    function makeError(msg) {\n        return addErrorMessage(createOutputObject(), msg);\n    }\n\n    try {\n        let bytes;\n\n        if (arg && Array.isArray(arg.bytes)) {\n            bytes = arg.bytes.slice();\n        } else if (arg && typeof arg.payload === 'string') {\n            let hex = arg.payload.replace(/\\s+/g, '');\n            if (hex.startsWith('0x') || hex.startsWith('0X')) hex = hex.slice(2);\n            bytes = convertHexStringToBytes(hex);\n        } else if (arg && typeof arg.payload_raw === 'string') {\n            bytes = convertBase64StringToBytes(arg.payload_raw);\n        } else {\n            return makeError(\"Missing 'bytes', 'payload' (hex) or 'payload_raw' (base64) in decodeUplink argument\");\n        }\n\n        const fPort = (arg && typeof arg.fPort === 'number') ? arg.fPort : 0;\n\n        const input = {\n            bytes: bytes,\n            fPort: fPort,\n            recvTime: new Date().toISOString()\n        };\n\n        const result = decode(input);\n        print(\"B\" + JSON.stringify(result));\n\n        if (result && result.data && result.data.measurement && Array.isArray(result.data.measurement.channels)) {\n            const flat = {};\n            for (const ch of result.data.measurement.channels) {\n                let key = ch.channelName.toLowerCase().replace(/\\s+/g, '_');\n                if (key === 'device_temperature') key = 'temperature';\n                flat[key] = ch.value;\n            }\n            return flat;\n        }\n\n        return result;\n    } catch (e) {\n        print(\"ERROR\" + (e && e.message ? e.message : String(e)));\n        return addErrorMessage(\n            createOutputObject(),\n            \"Unexpected error in decodeUplink: \" + (e && e.message ? e.message : String(e))\n        );\n    }\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\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 >= 5 && !((input.bytes.length - 2) % 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 == 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 == 41) {\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    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    // 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    \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        // Alarm channel 0 = pressure, 1 = device temperature\n        output.data.technicalAlarms[alarmCounter].channelId = (input.bytes[byteIndex] & 0x78) >> 3;\n        output.data.technicalAlarms[alarmCounter].channelName = ALARM_CHANNEL_NAMES_DICTIONARY[output.data.technicalAlarms[alarmCounter].channelId];\n\n        // Alarm channel 1 = general failure\n        output.data.technicalAlarms[alarmCounter].causeOfFailure = (input.bytes[byteIndex] & 0x07);\n        output.data.technicalAlarms[alarmCounter].causeOfFailureName = TECHNICAL_ALARM_CAUSE_OF_FAILURE_NAMES_DICTIONARY[output.data.technicalAlarms[alarmCounter].causeOfFailure];\n\n        // Alarm value\n        output.data.technicalAlarms[alarmCounter].value = getRealValueByChannelNumberAndAlarmType(output.data.technicalAlarms[alarmCounter].channelId, 0,\n            input.bytes[byteIndex + 1] << 8 | input.bytes[byteIndex + 2]);\n\n        if (output.data.technicalAlarms[alarmCounter].value == ERROR_VALUE) {\n            output = addWarningMessage(output, \"Invalid data for \" + output.data.technicalAlarms[alarmCounter].channelName + \"channel\");\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 temperature alarm\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    // 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;\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] == 10 ? \"PGW23.100.11\" : input.bytes[2];\n\n    // Wireless module firmware version\n    output.data.deviceInformation.wirelessModuleFirmwareVersion = ((input.bytes[3] >> 4) & 0x0f).toString() + \".\" + (input.bytes[3] & 0x0f).toString() + \".\" + (input.bytes[4]).toString();\n\n    // Wireless module hardware version\n    output.data.deviceInformation.wirelessModuleHardwareVersion = ((input.bytes[5] >> 4) & 0x0f).toString() + \".\" + (input.bytes[5] & 0x0f).toString() + \".\" + (input.bytes[6]).toString();\n\n    // Sensor module firmware version\n    output.data.deviceInformation.sensorModuleFirmwareVersion = ((input.bytes[7] >> 4) & 0x0f).toString() + \".\" + (input.bytes[7] & 0x0f).toString() + \".\" + (input.bytes[8]).toString();\n\n    // Sensor module hardware version\n    output.data.deviceInformation.sensorModuleHardwareVersion = ((input.bytes[9] >> 4) & 0x0f).toString() + \".\" + (input.bytes[9] & 0x0f).toString() + \".\" + (input.bytes[10]).toString();\n\n    // Sensor serial number\n    output.data.deviceInformation.serialNumber = \"\";\n    for (var i = 11; i < 22; i++) {\n        if (input.bytes[i] == 0) break;\n        output.data.deviceInformation.serialNumber += String.fromCharCode(input.bytes[i]);\n    }\n\n    // Pressure type\n    switch (input.bytes[22]) {\n        case 1:\n            output.data.deviceInformation.pressureType = \"absolute\";\n            break;\n\n        case 2:\n            output.data.deviceInformation.pressureType = \"relative\";\n            break;\n\n        case 3:\n            output.data.deviceInformation.pressureType = \"differential\";\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[26].toString(16).padStart(2, \"0\") + input.bytes[25].toString(16).padStart(2, \"0\") + input.bytes[24].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[30].toString(16).padStart(2, \"0\") + input.bytes[29].toString(16).padStart(2, \"0\") + input.bytes[28].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[34].toString(16).padStart(2, \"0\") + input.bytes[33].toString(16).padStart(2, \"0\") + input.bytes[32].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[38].toString(16).padStart(2, \"0\") + input.bytes[37].toString(16).padStart(2, \"0\") + input.bytes[36].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[39];\n    output.data.deviceInformation.pressureUnitName = returnPhysicalUnitFromId(input.bytes[39]);\n\n    // Unit pressure\n    output.data.deviceInformation.deviceTemperatureUnit = input.bytes[40];\n    output.data.deviceInformation.deviceTemperatureUnitName = returnPhysicalUnitFromId(input.bytes[40]);\n\n    return output;\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 * Add 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 * Add private to output object\n * @param {output} output\n * @param {string} errorMessage\n * @access private \n */\nfunction addErrorMessage(output, errorMessage) {\n    output.errors = output.errors || [];\n    output.errors.push(DEVICE_NAME + \" (JS): \" + errorMessage);\n    return output;\n}\n\n/**\n * Create 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 * 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\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}",
            "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 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": 13
                        },
                        "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/pgw23.100_en_co_rs_w410_h410_image.jpg",
                            "name": "Source 1",
                            "source": "image_url"
                          }
                        ],
                        "type": "image"
                      },
                      {
                        "layout": {
                          "col": 3,
                          "row": 0,
                          "sizeX": 3,
                          "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"
                      },
                      {
                        "api": {},
                        "layout": {
                          "col": 2,
                          "row": 0,
                          "sizeX": 1,
                          "sizeY": 13
                        },
                        "panel": {
                          "color": "#ffffff",
                          "currentColor": "#ffffff",
                          "showOffline": {
                            "type": "none"
                          },
                          "title": "Temperature",
                          "updateTs": 1752583769912
                        },
                        "properties": {
                          "alignTimeSeries": false,
                          "colors": [
                            {
                              "color": "#ff9500",
                              "max": 80,
                              "min": 70
                            },
                            {
                              "color": "#d40202",
                              "max": 100,
                              "min": 80
                            }
                          ],
                          "dataAppend": false,
                          "majorTicks": 10,
                          "max": 100,
                          "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": "ºC"
                        },
                        "sources": [
                          {
                            "aggregation": {},
                            "bucket": {
                              "backend": "mongodb",
                              "id": "pgw23_100_data_bucket",
                              "mapping": "temperature",
                              "tags": {
                                "device": [],
                                "group": []
                              }
                            },
                            "color": "#1abc9c",
                            "name": "pressure",
                            "source": "bucket",
                            "timespan": {
                              "magnitude": "hour",
                              "mode": "latest",
                              "period": "latest",
                              "value": 6
                            }
                          }
                        ],
                        "type": "tachometer"
                      },
                      {
                        "api": {},
                        "layout": {
                          "col": 1,
                          "row": 0,
                          "sizeX": 1,
                          "sizeY": 13
                        },
                        "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": "pgw23_100_data_bucket",
                              "mapping": "pressure",
                              "tags": {
                                "device": [],
                                "group": []
                              }
                            },
                            "color": "#1abc9c",
                            "name": "pressure",
                            "source": "bucket",
                            "timespan": {
                              "magnitude": "hour",
                              "mode": "latest",
                              "period": "latest",
                              "value": 6
                            }
                          }
                        ],
                        "type": "tachometer"
                      },
                      {
                        "api": {},
                        "layout": {
                          "col": 3,
                          "row": 13,
                          "sizeX": 3,
                          "sizeY": 8
                        },
                        "panel": {
                          "color": "#ffffff",
                          "currentColor": "#ffffff",
                          "showOffline": {
                            "type": "none"
                          },
                          "title": "Temperature"
                        },
                        "properties": {
                          "alignTimeSeries": false,
                          "dataAppend": false,
                          "options": "var options = {\n    chart: {\n        type: 'line',\n        zoom: {\n            type: 'x',\n            enabled: true,\n            autoScaleYaxis: true,\n            allowMouseWheelZoom: false\n        },\n        toolbar: {\n            autoSelected: 'zoom'\n        }\n    },\n    stroke: {\n        curve: 'straight',\n        width: 4\n    },\n    grid: {\n        row: {\n            colors: ['#f3f3f3', 'transparent'],\n            opacity: 0.5\n        },\n    },\n    xaxis: {\n        type: 'datetime',\n        tooltip: {\n            enabled: false\n        },\n        labels: {\n            datetimeUTC: false\n        }\n    },\n    yaxis: {\n        labels: {\n            \"formatter\": function (val) {\n                if ( val !== null && typeof val !== 'undefined' )\n                    return val.toFixed(2);\n            }\n        }\n    },\n    tooltip: {\n        x: {\n            format: 'dd/MM/yyyy HH:mm:ss'\n        }\n    }\n};\n",
                          "realTimeUpdate": true
                        },
                        "sources": [
                          {
                            "aggregation": {},
                            "bucket": {
                              "backend": "mongodb",
                              "id": "pgw23_100_data_bucket",
                              "mapping": "temperature",
                              "tags": {
                                "device": [
                                  "wika_pgw23_100_12345"
                                ],
                                "group": []
                              }
                            },
                            "color": "#d3aa17",
                            "name": "Temperature",
                            "source": "bucket",
                            "timespan": {
                              "magnitude": "hour",
                              "mode": "relative",
                              "period": "latest",
                              "value": 6
                            }
                          }
                        ],
                        "type": "apex_charts"
                      },
                      {
                        "layout": {
                          "col": 0,
                          "row": 13,
                          "sizeX": 3,
                          "sizeY": 8
                        },
                        "panel": {
                          "color": "#ffffff",
                          "currentColor": "#ffffff",
                          "currentSubtitle": "",
                          "currentTitle": "Pressure",
                          "showOffline": {
                            "type": "none"
                          },
                          "title": "Pressure"
                        },
                        "properties": {
                          "alignTimeSeries": false,
                          "dataAppend": false,
                          "options": "var options = {\n    chart: {\n        type: 'line',\n        zoom: {\n            type: 'x',\n            enabled: true,\n            autoScaleYaxis: true,\n            allowMouseWheelZoom: false\n        },\n        toolbar: {\n            autoSelected: 'zoom'\n        }\n    },\n    stroke: {\n        curve: 'straight',\n        width: 4\n    },\n    grid: {\n        row: {\n            colors: ['#f3f3f3', 'transparent'],\n            opacity: 0.5\n        },\n    },\n    xaxis: {\n        type: 'datetime',\n        tooltip: {\n            enabled: false\n        },\n        labels: {\n            datetimeUTC: false\n        }\n    },\n    yaxis: {\n        labels: {\n            \"formatter\": function (val) {\n                if ( val !== null && typeof val !== 'undefined' )\n                    return val.toFixed(2);\n            }\n        }\n    },\n    tooltip: {\n        x: {\n            format: 'dd/MM/yyyy HH:mm:ss'\n        }\n    }\n};\n",
                          "realTimeUpdate": true
                        },
                        "sources": [
                          {
                            "aggregation": {},
                            "bucket": {
                              "backend": "mongodb",
                              "id": "pgw23_100_data_bucket",
                              "mapping": "pressure",
                              "tags": {
                                "device": [
                                  "wika_pgw23_100_12345"
                                ],
                                "group": []
                              }
                            },
                            "color": "#1abc9c",
                            "name": "Pressure",
                            "source": "bucket",
                            "timespan": {
                              "magnitude": "hour",
                              "mode": "relative",
                              "period": "latest",
                              "value": 6
                            }
                          }
                        ],
                        "type": "apex_charts"
                      }
                    ]
                  }
                ]
              }
            }
          ]
        }
      }
    ]
  }
}