Skip to main content
Skip table of contents

Creating Device Templates & Codec Editors for Custom Sensor Management

Introduction

The Internet of Things (IoT) is continuously evolving, driving the need for more adaptable and sophisticated sensor management systems. Recognizing this, myDevices has introduced an innovative feature in their platform: the Codec Editor and Device Template Editor. These tools offer unprecedented customization of sensor data handling, enabling device manufacturers and partners to fine-tune their IoT solutions for various applications.

Add device templates and codecs for various devices, such as sensors, actuators, gateways, tags, and more, as well as services for storing data, setting up alerts, visualizing data using custom dashboards, and transmitting data to other business intelligence (BI), artificial intelligence (AI), computerized maintenance management system (CMMS), or asset management software. The possibilities are limitless.

What is a Device Template?
A Device Template is a configuration blueprint that lets users define the representation, capabilities and management settings for an IoT device, sensor, or gateway.  This includes configuring device attributes, setting up data channels, and defining alert conditions. It provides a structured way to define and manage the characteristics and capabilities of that device within myDevices' application.

A Device Template is comprised of the following:

  1. General: Information about the device, such as name, description, and manufacturer, etc.

  2. Codec: used to decode or parse the payload of the device. It allows users to create and modify the logic (in JavaScript) that interprets sensor data.

  3. Capabilities: Sensors or Actuators the device supports.

  4. Attributes: UI settings conditions for specific use cases.

  5. Alert Types: Alerts are generated based on capabilities.

  6. Device Uses: used to define the use case of the device.


What is a Codec?
A codec for a device template is a script or set of instructions used to decode the raw data payload from IoT devices into a more meaningful and structured format. This can include encoding and decoding data packets, adapting to different communication protocols, and handling data in various formats.

Why do I need a Device Template and Codec?
Device templates and codecs are required to onboard devices into myDevices. They are used to define the capabilities of the device, how the data is decoded, and how the data is displayed in the UI. Device templates and codecs are also used to define the use case of the device, which is used to determine the appropriate alert types and attributes for the device.

Get Started Steps

  1. Log into console.mydevices.com

  2. Create a Device Template Go to the Device Templates Tab. Here, you can access all templates you've created for your applications and organization. You'll also find example templates that can serve as starting points. To begin, click on "Add Device Template" in the top-right corner.

Adding a Device Template: General Tab

  • Fill out all the fields such as Manufacturer, model, category, sub-category, codec, general device Icon, any certifications, IP rating, description and keywords. This information will be visible to end-users when they access device details or view the device on sensor maps.

  • When adding a device template, you will define the device category and subcategory.

    • If you're adding a Gateway, select "Gateway" as the Category, and do not choose a codec. Currently, we support LoRa Gateways for the subcategory.

    • For End Devices like sensors or actuators, a codec is required, and you should choose either LoRa or MQTT as the subcategory.

    • For End Devices like BLE tags or beacons, a codec is not necessary, and you should use the subcategory "BLE."

Adding a Device Template: Defining the Capabilities

Data capabilities represent the data transmitted by the device and are decoded by the codec. Add capabilities like temperature reading, motion detection, etc., ensuring each capability has a unique data channel. These capabilities determine how data appears in the management screen, gets stored in history, and enables alert creation. Click "Add Capability" to begin defining capabilities.

Fill out all fields including Template Type, Data Type, Name (Data Label in app), Channel ID, Chart, Icon and Widget.

  • Template Type supports different data representations, such as (e.g. 72.4 F), status (Open/Close), Tracking (GPS, BLE), Commands: Button (e.g. Stop Alert), Commands: Toggle On/Off (e.g. Water Valve), Commands: List (e.g. IR Blaster), and Image.

  • Data Type corresponds to the type of data capability you're adding, e.g., temperature.

  • Name is the label that appears in the user interface.

  • Channel ID must match between the device template and codec.

  • Chart defines the chart displayed in device details (e.g., line chart for value devices, status chart for status-based devices).

  • Icon represents the icon for this data capability in sensor maps.

  • Widget specifies the default widget added when this capability is included in a custom dashboard.

  • Depending on whether you choose a value or status data type, you'll have options for Units or Status Labels.

Adding a Device Template: Adding Attributes: Attributes are used to set UI conditions for specific use cases (e.g., displaying device location on a map for tracking devices, adding resources or help links for the device, setting firmware configuration setting options for device, and providing sample payloads for sensor simulators).

  • Not all attributes are required, with only the Broadcast interval and Device Type being mandatory.

    • Broadcast interval should be set to match how often the device periodically checks in. If a device checks in only once a day, its broadcast interval should be set to 1440. By default we would recommend at least 60 minutes and no greater then 1440 minutes.

    • Device Type appears next to the device name in the UI.

    • Testing the Template: Use the simulator attribute to test how your template behaves with simulated sensor data.

Alert Types: Whenever you add a capability using either a Value or Status template type, the corresponding alert is generated. The alert notification text is editable, but the alert template is not editable currently.

  • For capabilities using Value template type, a Min/Max Threshold alert will automatically be created for that particular capability.

  • For capabilities using Status template type, distinct alert types are generated for each unique status. For instance, if you have a Status-based Door sensor that transmits 0 for "closed" and 1 for "open," an alert type is created for both "Open" and "Closed" states.

  • Personalizing Alert Notification Text: You have the ability to modify the text of alert notifications that will be transmitted for a specific alert type.

  • Enabling/Disabling Alert Type Settings: In certain scenarios, there may be data points where the end-user does not require an alert. In such cases, you can deactivate the display of the alert type (e.g., the "Panic Button Press" alert is visible in the user interface, while the "Not Pressed" alert can be toggled to a hidden status and will not be displayed).

Device Uses: This feature allows you to set up pre defined alerts that get added automatically during the device setup process. This is particularly valuable when you already have specific alert requirements in mind for a given use case.

  1. Import or create a Codec into Codec Editor

The Codec Editor provides a flexible environment to write and edit JavaScript for custom codec creation.
Key functions include setting up data interpretation rules, handling different data types, and integrating various sensor models. Example Use Case: Tailoring a codec for a specific temperature sensor to interpret data for a greenhouse environment, converting raw readings into actionable insights like temperature trends and anomaly detection.


Getting Started with the Codec Editor
Creating a New Codec: Click on 'Create New Codec'. Enter a name for your codec and it will automatically generate an ID.


Editing the Codec: Use the JavaScript-based interface to input or modify the code. This may include setting up data decoding functions for incoming data from sensors.


Testing Your Codec: Utilize the built-in debugger to test your codec. Input sample data payloads to see how your code interprets them.


Saving and Applying the Codec: Once satisfied, save your codec. It can now be linked to a device template for use.

Sample and Minimal Codec

Here is a minimal source code for the decoder.js file, with included comments explaining things.

Since it’s a JavaScript file running in a Node.JS sandbox, it allows us define a special context with dedicated data and functions.

In opposite to LoRaWAN type-of Codec, myDevices Engine doesn’t expect any specific function (although it could use a LoRaWAN type-of Codec), but provides tools to write Codec like any Node.JS script instead.

JS
// Example Payload Decoder
// Test with 00F000C8
//    Temp = 24°C
//    Lum  = 200lux

const multiplier = 0.1;

// Could use console.log to help debugging Codec
console.log("multiplier = %d", multiplier);

// Decoder.data.buffer contains a Node.JS Buffer object with the payload data
// See https://nodejs.org/docs/latest-v18.x/api/buffer.html for more information
console.log("Decoder.data.buffer = %s", Decoder.data.buffer.toString("hex"));
const buffer = Decoder.data.buffer;

// Decoder.data.fport contains LoRaWAN fPort
// Could be used to filter uplink or adapt data decoding accordingly
console.log("Decoder.data.fport = %d", Decoder.data.fport);

// Decoder must send a Sensor Object or Sensor Object Array
Decoder.sendSensors({
    channel: 0,                               // Data Channel, must matches with Template Capability
    type: DataTypes.TYPE.TEMPERATURE,         // Data Type
    unit: DataTypes.UNIT.CELSIUS,             // Data Unit
    value: buffer.readInt16BE(0) * multiplier // Data Value
});

Decoder.sendSensors({
    channel: 2,
    type: DataTypes.TYPE.LUMINOSITY,
    unit: DataTypes.UNIT.LUX,
    value: buffer.readUInt16BE(2)
});

// Complete Decoding process (optional, allows for faster response and prevent Codec timeout)
Decoder.done();


Our Engine main global object is Decoder one, which hold:

  • data Object

    • buffer Object with payload using Node.JS Buffer interface

    • fport numerical value

    • format string value (set to "buffer" with LoRaWAN payload)

  • sendSensors function

    • Used to send sensor data, either atomically, or grouped in an array.
      Each sensor data must be an object with a few attributes:

      • channel sensor data channel (eg. unique identifier)

      • type sensor data type

      • unit sensor data unit

      • value actual sensor data value

      • timestamp (optional) used for historical data, should be provided as UTC timestamp in milliseconds

  • done function

    • Although it’s optional, it’s recommend to end your decoding process by calling Decoder.done().
      This allows to complete Decoding process before context timeout, speeding up processing.
      By default codecs are granted a maximum execution time of 100ms, which should be enough for most of use cases. It could be increased by opening a ticket in our support portal.

The second important global object is DataTypes, which have definition for all TYPE and UNIT available.
You can use editor completion to find out existing types and unit available (shortcut CTRL-SPACE).

Since it’s running in a sandbox, you can also use usual console.log and console.error JavaScript function, and see the result directly in the editor. However those functions will be ignored when decoding real data (from real device) in order to speed up execution time.

Using LoRaWAN Codec API

In order to use LoRaWAN type-of Codec, you just have to include the function in your myDevices codec, call it with proper arguments, and then map and send the result into sensor objects (with channel/type/unit/value).

JS
// Past your decodeUplink function
function decodeUplink(input) {
  // ... do something with input
  return output;
}

// Call it
const result = decodeUplink({
  bytes: Decoder.data.buffer,
  fPort: Decoder.data.fport,
  recvTime: new Date(Decoder.data.timestamp)
});

// Map and send sensor data
Decoder.sendSensors({
    channel: 1,
    type: DataTypes.TYPE.TEMPERATURE,
    unit: DataTypes.UNIT.CELSIUS,
    value: result.temperature
});

Decoder.done();

  1. Registry Device IDs: After creating a Device Template & Codec, you’ll next want to register device ID(s) to the new Device Template. Go to Registry Tab, click Upload Devices to begin.

  • Template Catalog

    • If you’re adding device IDs to a public template, choose Public.

    • If you’re adding device IDs to a template you created, choose Application.

  • Device Template

    • Select the appropriate template

  • Registry Type

    • You can choose to register the IDs to a specific application or make it globally available

  • Upload with Text input or CSV File

  1. Add the device to your app and visualize the data

6. Best Practices and Tips
  • Gain a fundamental understanding of JavaScript and familiarize yourself with basic IoT communication protocols for effective use of the Codec Editor.

  • Regularly review and update device templates to stay aligned with evolving sensor technologies and varying data requirements.

  • Addressing Common Concerns: Prioritize data security and privacy when customizing codecs and templates, ensuring that the data handling complies with industry standards and regulations.

  • Iterative Development: Start with a basic setup and gradually add complexity as you become more familiar with the tools.

  • Regular Updates: Continuously revisit and update your codecs and templates to cater to new sensor models and evolving requirements.

  • Documentation and Support: Leverage the myDevices documentation and community forums for troubleshooting and best practices.

By following these steps and tips, users can fully utilize the Codec and Device Template Editors to create customized, efficient IoT solutions tailored to their specific needs.Conclusion

The introduction of the Codec and Device Template Editors in the myDevices platform marks a significant advancement in custom sensor management. By harnessing these features, users can significantly enhance their IoT deployments, ensuring they are robust, adaptable, and tailored to their specific needs.

Real Codec Examples

Dragino LHT65 Temp & Humidity Sensor 2.0 Codec Used

CODE
//Sample data, external DS18B20 sensor: 0B4501050248010105
//Sample data, external soil moisture sensor: 0B49FF3F024802
//Sample data, external tilting sensor: 0B450105024803

"use strict";

const DS18B20_SENSOR = 0x01;
const SOIL_MOISTURE_SENSOR = 0x01;
const TILTING_SENSOR = 0x01;

var buffer = Decoder.data.buffer;
var temperature = 23;

const {probe_offset = 0, internal_offset = 0, offset_unit = 'c', humidity_offset = 0} = Decoder.data.options;

function getTemperatureOffset(offset) {
    const offset_number = Number(offset);
    if (offset_unit == 'c') {
        return offset_number;
    }
    else if (offset_unit == 'f') {
        // we want to solve CtoF(temp) + OFFSET_F = CtoF(temp + OFFSET_C)
        // and compute OFFSET_C based on OFFSET_F provided by setting ...
        // CtoF(x) = x * 1.8 + 32
        // temp * 1.8 + 32 + OFFSET_F = (temp + OFFSET_C) * 1.8 + 32
        // temp * 1.8 + 32 + OFFSET_F = temp * 1.8 + 32 + OFFSET_C * 1.8
        // OFFSET_F = OFFSET_C * 1.8
        // OFFSET_C = OFFSET_F / 1.8
        return offset_number / 1.8;
    }
}

function decodeTemperature(offset, channel, name, temperature_offset = 0) {
    let value = buffer.readInt16BE(offset);
    let tempValue = Number((value / 100).toFixed(2));
    
    /** 
    * @author: Adrian
  * @comment: Do not send probe reading if value is equal to 327
    * @date: 09/04/2019
    */
    if (Math.floor(tempValue) != 327) {
        Decoder.send({
          channel: channel,
          type: DataTypes.TYPE.TEMPERATURE,
          unit: DataTypes.UNIT.CELSIUS,
          value: Number((value / 100).toFixed(2)) + temperature_offset,
          name: name
      });
        //Save the internal temperature
        if (channel == 3) {
            temperature = tempValue;
        }
    }   
      
}
decodeTemperature(2, 3, 'Internal Temp', getTemperatureOffset(internal_offset));
Decoder.send({
    channel: 4,
    type: DataTypes.TYPE.RELATIVE_HUMIDITY,
    unit: DataTypes.UNIT.PERCENT,
    value: Number((buffer.readUInt16BE(4) / 10).toFixed(1)) + humidity_offset,
    name: 'Humidity'
});
var external_sensor = buffer.readUInt8(6);
switch(external_sensor){
    case DS18B20_SENSOR:
        decodeTemperature(7, 7, 'Probe Temp', getTemperatureOffset(probe_offset));
        break
    
    case SOIL_MOISTURE_SENSOR:
        //Format not yet defined
        break
        
    case TILTING_SENSOR:
        //Format not yet defined
        break
}

//Calculate battery percent using trendline from real world temperature/voltage data
let maxValue = 2914 + 6.25 * temperature + -0.0793 * Math.pow(temperature, 2);
if (temperature > 39) {
  maxValue = 3040;
}
if (temperature < -40) {
  maxValue = 2540;
}
console.log(maxValue)
let minValue = 2400;
let range = maxValue - minValue;
let battery = (buffer.readUInt16BE(0) & 0x3FFF) - minValue;
let batteryPercent = Number((Math.min(Math.max(battery, 0), range) / range * 100).toFixed(2));
Decoder.send({
    channel: 5,
    type: DataTypes.TYPE.BATTERY,
    unit: DataTypes.UNIT.PERCENT,
    value: batteryPercent,
    name: 'Battery'
});
console.log("Voltage: " + (buffer.readUInt16BE(0) & 0x3FFF) / 1000);
Decoder.send({
    channel: 500,
    type: DataTypes.TYPE.VOLTAGE,
    unit: DataTypes.UNIT.MILLIVOLTS,
    value: buffer.readUInt16BE(0) & 0x3FFF,
    name: 'Battery Voltage'
});
Decoder.done();

Dragino LDS03 Door Sensor 2.0 Codec Used

CODE
// here goes the decoder source code
//sample payload 0100000400000362f15ee2

/*
0000010c000000630dfe31
0000010d00000e630e057f

{"reset":8.30, "timezone":"America/New_York", "vol_unit":"m3","prox_unit":"m"}

*/
const buffer = Decoder.data.buffer;
const port = Decoder.data.fport;
const session = Decoder.data.session;
const options = Object.assign({ timezone: "UTC", reset: 0}, Decoder.data.options);
const SESSION_EXPIRES = 60 * 20;
if (!session.hasOwnProperty("battery")) {
    session.battery = 100;
}

const ALARM = {
    channel: 500,
    type: DataTypes.TYPE.ALARM,
    unit: DataTypes.UNIT.undefined,
    name: "Alarm"
};

const DOOR = {
    type: DataTypes.TYPE.OPENCLOSED,
    unit: DataTypes.UNIT.undefined,
};

const VALUE = {
    type: DataTypes.TYPE.VALUE_NULL,
    unit: DataTypes.UNIT.NULL,
}

const TIME = {
    type: DataTypes.TYPE.TIME,
    unit: DataTypes.UNIT.MINUTES,
};

const BATTERY = {
    channel: 5,
    type: DataTypes.TYPE.BATTERY,
    unit: DataTypes.UNIT.PERCENT,
    name: "Battery"
};

function sendData(dataType, value, channel, name) {
    let reading = {
        channel: dataType.channel ? dataType.channel : channel,
        type: dataType.type,
        unit: dataType.unit,
        name: dataType.name ? dataType.name : name,
        value: value,
    };
    console.log(reading);
    Decoder.send(reading);
}

function getLastReset(hour) {
    if (hour == 24) {
        hour = 0;
    }

    const [h, m=0] = hour.toString().replace(/\./g, ':').split(':')
        
    const now = new Date();

    // compute offset 
    const utcTime = now.toLocaleString("en-US", { timeZone: "UTC" });
    const localTime = now.toLocaleString("en-US", { timeZone: options.timezone });
    const offset = Date.parse(localTime) - Date.parse(utcTime);

    // get local day
    const fakeTime = now.getTime() - h * 3600000 - m * 60000 + offset;
    const localDay = Math.floor(fakeTime / (24 * 3600000));

    // substract offset to get UTC time from local
    const resetTimestamp = (localDay) * 24 * 3600000 + h * 3600000 + m * 60000 - offset;
    
    console.log({
        utcTime,
        utcResetTime: new Date(resetTimestamp).toLocaleString("en-US", { timeZone: "UTC" }),
        localTime,
        localResetTime: new Date(resetTimestamp).toLocaleString("en-US", { timeZone: options.timezone }),
    });

    return resetTimestamp;
}

function sendDoorStats(open_status){
    const now = Date.now();
    const last_reset = getLastReset(options.reset);
    const last_hour = now - 3600000;

    const last_open_at = session.last_open_at || now;
    const last_open_status = session.last_open_at > 0;
    let open_count = session.open_count || 0;
    let last_open_duration = now - last_open_at;
    
    function getFilter(reset_time) {
        return ({ closed_at }) => closed_at > reset_time;
    }

    function getDuration(reset_time) {
        return (duration, { open_at, closed_at }) => duration + closed_at - Math.max(open_at, reset_time);
    }
    
    if (!session.open_data) {
        session.open_data = [];
    }

    if (!open_status) {
        if (last_open_status) {
            // now closed
            open_count++;
            session.open_data.push({
                open_at: last_open_at,
                closed_at: now,
            });
            session.last_open_at = null;
            session.open_count = open_count;
        }
        else {
            // still closed
            const { open_at=0, closed_at=0 } = session.open_data.slice(-1).pop() || {};
            last_open_duration = closed_at - open_at;
        }
    }

    const last_reset_data = session.open_data.filter(getFilter(last_reset));
    const last_hour_data = session.open_data.filter(getFilter(last_hour));
    
    let daily_count = last_reset_data.length;
    let hourly_count = last_hour_data.length;
    let daily_duration = last_reset_data.reduce(getDuration(last_reset), 0);
    let hourly_duration = last_hour_data.reduce(getDuration(last_hour), 0);
    let daily_critical_count = last_reset_data.filter(({ open_at, closed_at }) => closed_at - open_at >= 60000).length;

    if (open_status) {
        const current_duration = now - last_open_at;

        daily_duration += now - Math.max(last_open_at, last_reset);
        hourly_duration += now - Math.max(last_open_at, last_hour);
        daily_critical_count += (now - Math.max(last_open_at, last_reset) >= 60000) ? 1 : 0;
        daily_count++;
        hourly_count++;
        open_count++;
        
        if (!last_open_status) {
            // now open
            session.last_open_at = now;
        }
        else {
            // still open
        }
    }
    
    sendData(VALUE, open_count, 144, "Door Open Count");
    sendData(VALUE, daily_critical_count, 150, "Last 24Hrs Door Open Count (+1min)");
    sendData(DOOR, daily_critical_count > 0 ? 1 : 0, 151, "Last 24Hrs Door Open for +1min");
    sendData(VALUE, hourly_count, 501, "Last Hour Door Open Count");
    sendData(VALUE, daily_count, 503, "Last 24Hrs Door Open Count");
    sendData(TIME, getFixed(last_open_duration/60000), 168, "Last Open Duration");
    sendData(TIME, getFixed(hourly_duration/60000), 502, "Last Hour Door Open Duration");
    sendData(TIME, getFixed(daily_duration/60000), 504, "Last 24Hrs Door Open Duration");

    session.open_data = last_reset_data;
}

function datalog(i, bytes) {
    var aa = bytes[0 + i] & 0x02 ? "TRUE" : "FALSE";
    var bb = bytes[0 + i] & 0x01 ? "OPEN" : "CLOSE";
    var cc = (
        (bytes[1 + i] << 16) |
        (bytes[2 + i] << 8) |
        bytes[3 + i]
    ).toString(10);
    var dd = (
        (bytes[4 + i] << 16) |
        (bytes[5 + i] << 8) |
        bytes[6 + i]
    ).toString(10);
    var ee = getMyDate(
        (
            (bytes[7 + i] << 24) |
            (bytes[8 + i] << 16) |
            (bytes[9 + i] << 8) |
            bytes[10 + i]
        ).toString(10)
    );
    var string =
        "[" + aa + "," + bb + "," + cc + "," + dd + "," + ee + "]" + ",";

    return string;
}

function getzf(c_num) {
    if (parseInt(c_num) < 10) c_num = "0" + c_num;

    return c_num;
}

function getMyDate(str) {
    var c_Date;
    if (str > 9999999999) c_Date = new Date(parseInt(str));
    else c_Date = new Date(parseInt(str) * 1000);

    var c_Year = c_Date.getFullYear(),
        c_Month = c_Date.getMonth() + 1,
        c_Day = c_Date.getDate(),
        c_Hour = c_Date.getHours(),
        c_Min = c_Date.getMinutes(),
        c_Sen = c_Date.getSeconds();
    var c_Time =
        c_Year +
        "-" +
        getzf(c_Month) +
        "-" +
        getzf(c_Day) +
        " " +
        getzf(c_Hour) +
        ":" +
        getzf(c_Min) +
        ":" +
        getzf(c_Sen);

    return c_Time;
}

function decoder(bytes, port) {
    if (port == 0x02) {
        var alarm = bytes[0] & 0x02 ? 1 : 0;
        var door_open_status = bytes[0] & 0x01 ? 1 : 0;
        var open_times = (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];
        var open_duration = (bytes[4] << 16) | (bytes[5] << 8) | bytes[6];
        var data_time = getMyDate(
            (
                (bytes[7] << 24) |
                (bytes[8] << 16) |
                (bytes[9] << 8) |
                bytes[10]
            ).toString(10)
        );
        //console.log(bytes.length);
        if (bytes.length == 11) {
            return {
                ALARM: alarm,
                DOOR_OPEN_STATUS: door_open_status,
                DOOR_OPEN_TIMES: open_times,
                LAST_DOOR_OPEN_DURATION: open_duration,
                TIME: data_time,
            };
        }
    } else if (port == 0x03) {
        for (var i = 0; i < bytes.length; i = i + 11) {
            var data = datalog(i, bytes);
            if (i == "0") data_sum = data;
            else data_sum += data;
        }
        return {
            DATALOG: data_sum,
        };
    } else if (port == 0x04) {
        var tdc = (bytes[0] << 16) | (bytes[1] << 8) | bytes[2];
        var disalarm = bytes[3] & 0x01;
        var keep_status = bytes[4] & 0x01;
        var keep_time = (bytes[5] << 8) | bytes[6];

        return {
            TDC: tdc,
            DISALARM: disalarm,
            KEEP_STATUS: keep_status,
            KEEP_TIME: keep_time,
        };
    } else if (port == 0x05) {
        var sub_band;
        var freq_band;

        if (bytes[0] == 0x0a) var sensor = "LDS03A";

        if (bytes[4] == 0xff) sub_band = "NULL";
        else sub_band = bytes[4];

        if (bytes[3] == 0x01) freq_band = "EU868";
        else if (bytes[3] == 0x02) freq_band = "US915";
        else if (bytes[3] == 0x03) freq_band = "IN865";
        else if (bytes[3] == 0x04) freq_band = "AU915";
        else if (bytes[3] == 0x05) freq_band = "KZ865";
        else if (bytes[3] == 0x06) freq_band = "RU864";
        else if (bytes[3] == 0x07) freq_band = "AS923";
        else if (bytes[3] == 0x08) freq_band = "AS923_1";
        else if (bytes[3] == 0x09) freq_band = "AS923_2";
        else if (bytes[3] == 0x0a) freq_band = "AS923_3";
        else if (bytes[3] == 0x0b) freq_band = "CN470";
        else if (bytes[3] == 0x0c) freq_band = "EU433";
        else if (bytes[3] == 0x0d) freq_band = "KR920";
        else if (bytes[3] == 0x0e) freq_band = "MA869";

        var firm_ver =
            (bytes[1] & 0x0f) +
            "." +
            ((bytes[2] >> 4) & 0x0f) +
            "." +
            (bytes[2] & 0x0f);
        var bat = ((bytes[5] << 8) | bytes[6]) / 1000;

        return {
            SENSOR_MODEL: sensor,
            FIRMWARE_VERSION: firm_ver,
            FREQUENCY_BAND: freq_band,
            SUB_BAND: sub_band,
            BAT: bat,
        };
    }
}

function getBat(bat) {
    let minValue = 3;
    let range = 0.5;
    let battery = bat - minValue;
    return Number(
        ((Math.min(Math.max(battery, 0), range) / range) * 100).toFixed(2)
    );
}
function getFixed(value, decimal = 2) {
    try {
        return Number(value.toFixed(decimal));
    } catch (error) {}
    return value;
}


if(port == 2){
    const data = decoder(buffer, port);
console.log("data", data)
    if (data.ALARM != null) {
        sendData(ALARM, data.ALARM);
    }
    if (data.DOOR_OPEN_STATUS != null) {
        sendData(DOOR, data.DOOR_OPEN_STATUS, 244, "Door Open Status");
        sendData(DOOR, data.DOOR_OPEN_STATUS, 245, "Door Open Status FP");
        sendData(DOOR, data.DOOR_OPEN_STATUS, 246, "Door Open Status AH");
    }
    sendDoorStats(data.DOOR_OPEN_STATUS);
    sendData(BATTERY, session.battery);
}
else if (port == 5){
    const data = decoder(buffer, port);
console.log("data", data)
    if (data.BAT) {
        session.battery = getBat(data.BAT);
        sendData(BATTERY, session.battery);
    }
}
Decoder.saveSession(session, SESSION_EXPIRES);

Decoder.done();

Dragino LDDS45 Distance Detection (Rolling Average) Codec Used

CODE
//This is a version of the Dragino LDDS75 Distance Detection (Level) codec that uses a rolling average for the level.
//Sample data: 4b3a02000009000000
//Sample options for level/volume calculations
//Height: {"height": 1, "measurement": "HEIGHT", "orientation": "horizontal", "name": "rectangle"}
//Volume: {"measurement": "VOLUME", "orientation": "horizontal", "name": "cylinder", "eq": "PI * l * r^2", "eqp": "l * (r^2 * acos((r-x)/r) - (r-x)* sqrt(2*r*x - x^2) )", "capacity": 1.571, "equation_unit": "cm", "d": 1, "l": 2, "vol_unit": "m3", "prox_unit": "m"}

"use strict";

const READINGS = 30;
const SESSION_EXPIRES = 60 * 60 * 24 * 7;

let buffer = Decoder.data.buffer;
const options = Object.assign({}, Decoder.data.options);
let session = Decoder.data.session;
let sensors = [];
//LandCommand requested a 6 inch (15.24 cm) reset distance so that is the default unless a template overrides it.
let resetDistance = options.resetDistance ? options.resetDistance : 15.24; 

if (!session.levels) {
    session.levels = [];
}

//Newly added distance to container in all equations.
if (!options.dtc) {
    options.dtc = 0;
}

function getFixed(value, decimal = 2) {
  try {
    return Number(value.toFixed(decimal));
  } catch (error) {}
  return value;
}

function getAverage() {
    return session.levels.reduce((a, b) => a + b) / session.levels.length;
}

function evaluate(options) {
  try {
    const math = require("mathjs");
    const _ = require("lodash");
    const parser = math.parser();
    _.forEach(options, (v, k) => {
      parser.set(k, v);
    });
    let result = parser.eval(options.eqp);
    return result;
  } catch (e) {
    console.error(e);
  }
}

function computeVerticalLevel(options) {
  //we have x and we need to calculate the difference

  //if ht was not specified usually is the same as basis
  if (!options.ht) options.ht = options.hb;

  //for the rest of the cases the difference is embeded in equations
  options.hT =
    options.x - options.hb - options.hl > 0
      ? options.x - options.hb - options.hl
      : 0;
  options.hB = options.x - options.hb > 0 ? options.hb : options.x;
  options.hL =
    options.x - options.hb < 0
      ? 0
      : options.x - options.hb - options.hl > 0
      ? options.hl
      : options.x - options.hb;

  return options;
}

//set options.x when computing volume
function computeVolume(options) {
  let total = 0,
    partial = 0;
  //if there is a diameter compute radius from it
  if (options.d) {
    options.r = options.d / 2;
  }

  if (
    (options.orientation === "vertical" &&
      options.name.startsWith("cylinder")) ||
    options.name === "oval"
  ) {
    options.hl = options.h;
    if (options.name === "oval") {
      //do here some tricks on the variables to be able to compute as a vertical level complex equation
      if (options.orientation === "vertical") {
        options.a = options.h - options.w;
        options.r = options.w / 2;
        options.d = options.w;
        //now doing some workarounds for devices
        options.hb = options.ht = options.r;
        options.hl = options.a;
        options = computeVerticalLevel(options);
      } else {
        options.r = options.h / 2;
        options.a = options.w - options.h;
      }
    } else {
      options = computeVerticalLevel(options);
    }
  }
  partial = evaluate(options);
  if (typeof partial !== "number") {
    throw new Error("Invalid number");
  }
  return {
    total,
    partial,
  };
}

function sendLevel(distance) {
  let percent = null;
    if (options.dtc) {
        options.dtc *= 100;
        distance = Math.max(distance - options.dtc, 0);
    }
  
  if (options.measurement === "VOLUME") {
    //all other prox data from options is in meters
    //level comes in cm
    //in order to normalize data
    //we need to convert it to m
    options.x = distance / 100;
    var computedVolume = 0;
    try {
      const { total, partial } = computeVolume(options);
      percent = options.capacity
        ? (partial * 100) / options.capacity
        : null;
      computedVolume = partial;
    } catch (e) {
      //input data comes in meters so change height to cm
      console.error(e);
    }
    sensors.push({
      channel: 26,
      type: DataTypes.TYPE.VOLUME,
      unit: DataTypes.UNIT.CUBIC_METER,
      value: getFixed(computedVolume < 0 ? 0 : computedVolume, 3),
      name: "Volume",
    });
  } else {
    //input data comes in meters so change height to cm
    options.height = options.height ? options.height * 100 : null;
    percent = options.height
      ? ((options.height - distance) * 100) / options.height
      : null;
  }

    sensors.push({
    channel: 23,
    type: DataTypes.TYPE.PROXIMITY,
    unit: DataTypes.UNIT.CENTIMETER,
    value: getFixed(distance),
    name: "Distance",
  });
    if (options.height) {
    let level = options.height >= distance ? options.height - distance : 0;
        if (session.lastDistance < resetDistance && distance < resetDistance) {
            //Reset the level average to 100% if we get two readings in a row that are less than the reset difference.
            level = options.height;
            session.levels = [];
        }
        session.lastDistance = distance;
        session.levels.push(level);
        while(session.levels.length > READINGS) {
            session.levels.shift();
        }
        level = getAverage(session.levels);
      percent = level * 100 / options.height;
        
      sensors.push({
      channel: 24,
      type: DataTypes.TYPE.PROXIMITY,
      unit: DataTypes.UNIT.CENTIMETER,
      value: getFixed(level, 3),
      name: "Level",
    });
  }
    percent = Math.ceil(percent/10)*10;
  sensors.push({
    channel: 25,
    type: DataTypes.TYPE.PERCENTAGE,
    unit: DataTypes.UNIT.PERCENT,
    value: getFixed(percent < 0 ? 0 : percent),
    name: "Percentage",
  });
  sensors.push({
    channel: 500,
    type: DataTypes.TYPE.ANALOG_SENSOR,
    unit: DataTypes.UNIT.ANALOG,
    value: percent < 20 ? 0 : 1,
    name: "Refill Status",
  });    
}

let distance = buffer.readUInt16BE(2) / 10;
//If distance is 2cm it indicates an invalid reading so we drop it.
if (distance != 2) {
    //Use range from 2.5 volts to 2.8 volts to calculate battery percent
    let minValue = 2500;
    let range = 300;
    let battery = (buffer.readUInt16BE(0) & 0x3FFF) - minValue;
    let batteryPercent = Number((Math.min(Math.max(battery, 0), range) / range * 100).toFixed(2));
    sensors.push({
        channel: 5,
        type: DataTypes.TYPE.BATTERY,
        unit: DataTypes.UNIT.PERCENT,
        value: batteryPercent,
        name: 'Battery'
    });
    sendLevel(distance);
  Decoder.send(sensors);
}

Decoder.saveSession(session, SESSION_EXPIRES);
Decoder.done();

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.