Page cover image

Aquastream LoRaWAN

Product Datasheet

Device Profile for Tesenso IoT Cloud

Decoder/Payload Converter for Tesenso IoT Cloud

https://github.com/Tesenso-GmbH/Device-Decoder/blob/main/PMK%2BAQS-L8_Aquastream_LoRaWAN.js
// var msg = {
//     "data" : "2D44B42570940310050E7202463904B42501070100002004138B0E17008410139B03000002FD17000002FD74BA16",
//     "data2": "2D 44 B4 25 09 93 03 10 05 0E 72 21 53 51 04 B4 25 01 07 01 00 00 20 04 13 E0 2E 00 00 84 10 13 00 00 00 00 02 FD 17 00 00 02 FD 74 CB 16",
// }



var decodedMsg = msg;
if (msg && msg.data) {
    decodedMsg = main(msg.data)
}

if (msg && msg.ts) {
    decodedMsg.ts = msg.ts
}

return {
    msg: decodedMsg,
    metadata: metadata,
    msgType: msgType
};

// console.log(decodedMsg)

function main(data) {
    var CI_FIELD = '78';
    var ADDRESS_INDEX_START = 4;
    var ADDRESS_INDEX_END = 7;
    var VERSION_INDEX = 8;
    var MEDIUM_FIELD = 'SystemComponent';
    var MANUFACTURER_FIELD = 'IMT';

    var VALUE_DIF_VIF = {
        waterMeter: '04 13',
        reverseWaterMeter: '84 10 13',
        errorCodes: '02 fd 17',
        batteryLifetime: '02 fd 74',
        meterAddress: "72"
    }
    var STATUS_AND_EVENT_LIST_FIELD = [
        // Don,t remove those following empty fields.
        // Those are required since this full list is required for a 16-bit incoming status and event handling data
        '', '', '', '', '',
        'alarmOverFlow',
        'alarmReverseFlow',
        'alarmBatteryLow',
        'alarmNoConsumption',
        '', '', '',
        'alarmLeak',
        'alarmBurst',
        '',
        'alarmTamper'
    ]

    var ERROR_LIST_FIELD = [
        'overFlow',
        'reverseFlow',
        'batteryLow',
        'noConsumption',
        'leak',
        'burst',
        'tamper'
    ]
    if (!String.prototype.includes) {
        String.prototype.includes = function(search, start) {
            'use strict';
            if (typeof start !== 'number') {
                start = 0;
            }

            if (start + search.length > this.length) {
                return false;
            } else {
                return this.indexOf(search, start) !== -1;
            }
        };
    }

    function format(value)  {
        var bytes = value.replaceAll(' ', '').toLowerCase();
        var byteArr = []
        for (var i = 0; i < bytes.length; i+=2) {
            byteArr.push(bytes[i] + "" + bytes[i + 1])
        }
        return byteArr;
    }
    function isUnparsed(value) {
        return value && !!value.length && value[0] === CI_FIELD;
    }

    function getBodyPayloadStr(value) {
        var payloadStartIndex = isUnparsed(value) ? 1 : 10;
        if (value.length > payloadStartIndex) {
            return value.slice(payloadStartIndex).join(' ');
        }
        return '';
    }
    function getRevHexValue(value, startIndex, endIndex, convertToDecimal, devider) {
        endIndex = endIndex || startIndex;
        convertToDecimal = convertToDecimal || false;
        devider = devider || 1;
        var result = Array.isArray(value) && !!value.length && value.slice(startIndex, endIndex + 1).reverse().join('') || null;
        if (!!result && convertToDecimal) {
            result = parseInt(result, 16) / devider
        }
        return result;
    }

    function getAddressAndHeaders(value) {
        var res = {};
        if (isUnparsed(value)) {
            res.ciField = CI_FIELD;
        } else {
            res = {
                secondaryAddress: getRevHexValue(value, ADDRESS_INDEX_START, ADDRESS_INDEX_END),
                medium: MEDIUM_FIELD,
                length: getRevHexValue(value, 0,0, true),
                manufacturer: MANUFACTURER_FIELD,
                version: getRevHexValue(value, VERSION_INDEX, VERSION_INDEX, true)
            }
        }
        return res
    }
    function getDeviceValue(key) {
        var result = 1;
        if (key === 'waterMeter' || key === 'reverseWaterMeter') {
            result = 1000;
        } else if (key === 'flowTemperature') {
            result = 10;
        }
        return result;
    }
    function getValuesByDifVif(valueStr, result) {
        result = result || {};
        if (!valueStr) {
            return result;
        }
        var newValueStr;
        var foundResKey;
        for (var key in VALUE_DIF_VIF) {
            if ( valueStr.startsWith(VALUE_DIF_VIF[key])) {
                foundResKey = key;
                break;
            }
        }
        if (!!foundResKey) {
            var difVifArr = VALUE_DIF_VIF[foundResKey].split(' ');
            if (foundResKey === 'reverseWaterMeter' || foundResKey === 'meterAddress') {
                difVifArr[0] = '04'
            }
            var dataIndexLength = parseInt(difVifArr[0]);
            var payloadLength = difVifArr.length + dataIndexLength;
            result[foundResKey] = getRevHexValue(
                valueStr.split(' '),
                payloadLength-dataIndexLength, payloadLength-1, foundResKey !== 'errorCodes' && foundResKey !== 'meterAddress', getDeviceValue(foundResKey));
            newValueStr = valueStr.split(' ').slice(payloadLength).join(' ');
            return getValuesByDifVif(newValueStr, result);
        }
        newValueStr = valueStr.split(' ').slice(1).join(' ');
        return getValuesByDifVif(newValueStr, result);

    }

    function getDeviceValues(value) {
        var bodyPayloadStr = getBodyPayloadStr(value);
        return getValuesByDifVif(bodyPayloadStr);
    }

    function getStatusAndEvents(errorCode) {
        var errorCodeInt = parseInt(errorCode, 16);
        var errorCodeBin = errorCodeInt.toString(2)

        var shortLength  = 16 - errorCodeBin.length;
        for( var i = 0; i < shortLength; i++) {
            errorCodeBin = '0' + errorCodeBin;
        }
        var result = {
            error: !!errorCodeInt,
            errors: ''
        }
        for (var index = 0; index <= STATUS_AND_EVENT_LIST_FIELD.length; index ++) {
            var field = STATUS_AND_EVENT_LIST_FIELD[index];
            if (!!field) {
                result[field] = index<=3 ? parseInt(errorCodeBin[index]) : !!parseInt(errorCodeBin[index]);
                var errorVal;
                if (!!result[field]) {
                    for (var j = 0; j < ERROR_LIST_FIELD.length; j++ ) {
                        if (field.toLowerCase().includes(ERROR_LIST_FIELD[j].toLowerCase())) {
                            errorVal = ERROR_LIST_FIELD[j];
                            result.errors += !!errorVal ? !!result.errors ? ', ' + errorVal : errorVal : '';
                            break;
                        }
                    }
                }
            }
        }
        return result;
    }
    function decode(rowValue) {
        var formattedValue = format(rowValue);

        var addressAndHeaders = getAddressAndHeaders(formattedValue);
        var deviceValues = getDeviceValues(formattedValue);
        var statusAndEvents = getStatusAndEvents(deviceValues.errorCodes)
        return Object.assign({}, addressAndHeaders, deviceValues, statusAndEvents);
    }
    return decode(data);

}

Device Labels

in manual
dataKey
dataType
dataFormat

address

secondaryAdress

serverAttribute

string

manufacturer

manufacturer

serverAttribute

string

device_type

medium

serverAttribute

string

Main volume

waterMeter

telemetry

integer

reverse volume

reverse WaterMeter

telemetry

integer

remaining battery lifetime

batteryLifetime

telemetry

integer

error indicator

error

telemetry -> use later for alarming (boolean) boolean value if there is any error at all

boolean

errors

string of errors if any --> if possible, please list them seperate as below dry,overflow,reverse_flow,battery_low,over_temperature,heat,frost,leak,burst,air_bubble", "status_no_consumption

--> list them separate as datapoint as boolean value

string

AlarmTamper

alarmTamper

telemetry --> use later for alarming

boolean

AlarmBurst

alarmBurst

telemetry --> use later for alarming

boolean

AlarmLeak

alarmLeak

telemetry --> use later for alarming

boolean

AlarmNoConsumption

alarmNoConsumption

telemetry --> use later for alarming

boolean

BatteryLow

alarmBatteryLow

telemetry --> use later for alarming

boolean

AlarmReverseFlow

alarmReverseFlow

telemetry --> use later for alarming

boolean

AlarmOverFlow

alarmOverFlow

telemetry --> use later for alarming

boolean

Last updated