Skip to content

Instantly share code, notes, and snippets.

@suprnrdy
Forked from Alkarex/decodeAxioma.js
Created March 25, 2022 04:27
Show Gist options
  • Save suprnrdy/53811826d0d96e2eb2d68f676cd2396b to your computer and use it in GitHub Desktop.
Save suprnrdy/53811826d0d96e2eb2d68f676cd2396b to your computer and use it in GitHub Desktop.
Node-RED function to decode Axioma water meter payloads.
/* jshint esversion:6, bitwise:false, node:true, strict:true */
/* globals msg */
"use strict";
/**
* Node-RED function to decode Axioma water meter payloads.
* Example assuming that msg.req.body contains an HTTP POST callback from The Things Networks.
*/
function statusAxiomaShort(s) {
const messages = [];
switch(s) {
case 0x00: messages.push('OK'); break;
case 0x04: messages.push('Low battery'); break;
case 0x08: messages.push('Permanent error'); break;
case 0x10: messages.push('Dry'); break;
case 0x70: messages.push('Backflow'); break;
case 0xD0: messages.push('Manipulation'); break;
case 0xB0: messages.push('Burst'); break;
case 0x30: messages.push('Leakage'); break;
case 0x90: messages.push('Low temperature'); break;
}
return messages;
}
function decodeAxiomaShort(raw64) {
const b = Buffer.from(raw64, 'base64');
let epoch, state, volume, pastPeriod;
let pastVolumes = [];
let i = 0;
let error;
try {
epoch = b.readUInt32LE(i); i += 4;
state = b.readUInt8(i); i += 1;
volume = b.readUInt32LE(i); i += 4;
while (i + 8 <= b.length) {
pastVolumes.push(b.readUInt32LE(i)); i += 4;
}
pastPeriod = b.readUInt32LE(i); i += 4;
} catch (ex) {
error = true;
}
return {
date: state == 0 ? (new Date(epoch * 1000)).toISOString() : undefined,
state: state,
stateMessages: statusAxiomaShort(state),
volume: state == 0 && volume ? volume / 1000.0 : undefined,
pastVolumes: state == 0 && pastVolumes.length > 0 ? pastVolumes.map(v => v / 1000.0) : undefined,
pastPeriod: state == 0 && pastPeriod ? pastPeriod : undefined,
error: error ? error : undefined,
};
}
function statusAxiomaExtended(s) {
const messages = [];
if (s === 0x00) {
messages.push('OK'); //No error; Normal work; normal
} else {
if (s & 0x04) messages.push('Low battery'); //Power low
if (s & 0x08) messages.push('Permanent error'); //Hardware error; tamper; manipulation
if (s & 0x10) messages.push('Temporary error'); //Dry; Empty spool; negative flow; leakage; burst; freeze
if (s === 0x10) messages.push('Dry'); //Empty spool;
if ((s & 0x60) === 0x60) messages.push('Backflow'); //Negative flow
if ((s & 0xA0) === 0xA0) messages.push('Burst');
if ((s & 0x20) && !(s & 0x40) && !(s & 0x80)) messages.push('Leakage'); //Leak
if ((s & 0x80) && !(s & 0x20)) messages.push('Low temperature'); //Freeze
}
return messages;
}
function decodeAxiomaExtended(raw64) {
const b = Buffer.from(raw64, 'base64');
let epoch, state, volume, logEpoch, logVolume;
let deltaVolumes = [];
let i = 0;
let error;
try {
epoch = b.readUInt32LE(i); i += 4;
state = b.readUInt8(i); i += 1;
volume = b.readUInt32LE(i); i += 4;
logEpoch = b.readUInt32LE(i); i += 4;
logVolume = b.readUInt32LE(i); i += 4;
while (i + 2 <= b.length) {
deltaVolumes.push(b.readUInt16LE(i)); i += 2;
}
} catch (ex) {
error = true;
}
return {
date: state == 0 ? (new Date(epoch * 1000)).toISOString() : undefined,
state: state,
stateMessages: statusAxiomaExtended(state),
volume: state == 0 && volume ? volume / 1000.0 : undefined,
logDate: state == 0 && logEpoch ? (new Date(logEpoch * 1000)).toISOString() : undefined,
logVolume: state == 0 && logVolume ? logVolume / 1000.0 : undefined,
deltaVolumes: state == 0 && deltaVolumes.length > 0 ? deltaVolumes.map(v => v / 1000.0) : undefined,
error: error ? error : undefined,
};
}
function autoDecode(raw64, body) {
if (body.port == 101) {
//Configuration frame
return {};
}
//TODO: Adjust here if there is a good way to discriminate "Short" or "Extended" payloads,
//for instance if all your sensors are of one type, or have different naming conventions.
let rawLength;
try {
rawLength = Buffer.from(raw64, 'base64').length;
} catch (ex) {
rawLength = 0;
}
if (rawLength > 42) {
return decodeAxiomaExtended(raw64);
} else if (rawLength <= 9) {
return decodeAxiomaShort(raw64);
} else {
//Might be a short or extended payload, so perform more sniffing on some fields to guess
let snifAxiomaExtended;
try {
snifAxiomaExtended = decodeAxiomaExtended(raw64);
//Test valid date difference in extended payload
const maxValidDateDifferenceMs = 1000 * 86400 * 15;
const date1 = new Date(snifAxiomaExtended.date);
const date2 = new Date(snifAxiomaExtended.logDate);
if (Math.abs(date1 - date2) > maxValidDateDifferenceMs) {
return decodeAxiomaShort(raw64);
}
} catch (ex) {
return decodeAxiomaShort(raw64);
}
//Fallback to extended payload
return snifAxiomaExtended;
}
}
const result = {};
try {
if (msg.req && msg.req.body) {
result.decoded = autoDecode(msg.req.body.payload_raw, msg.req.body);
} else {
result.decoded = autoDecode(msg.payload, {});
}
} catch (ex) {
result.error = ex.message;
}
if (typeof msg.payload !== 'object') {
msg.payload = {
input: msg.payload,
};
}
Object.assign(msg.payload, result);
return msg;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment