Skip to content

Instantly share code, notes, and snippets.

@Alkarex
Last active March 4, 2024 11:53
Show Gist options
  • Save Alkarex/4b5d1fef2ff84d483e2793ed009ef607 to your computer and use it in GitHub Desktop.
Save Alkarex/4b5d1fef2ff84d483e2793ed009ef607 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.getTime() - date2.getTime()) > 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;
@Alkarex
Copy link
Author

Alkarex commented Nov 30, 2023

See https://nordiciot.dk/byg-dit-eget-intelligente-vandmalersystem-guide/ (in Danish, but figures and an automated translation should be sufficient)

@Alkarex
Copy link
Author

Alkarex commented Jan 29, 2024

@Mono-Co This decoder is never providing an output with something like {"Time":..., "Water"} as you got, so it is probably something else that was used. Compare with https://gist.github.com/Alkarex/4b5d1fef2ff84d483e2793ed009ef607#file-decodeaxioma-js-L92-L101

@Mono-Co
Copy link

Mono-Co commented Jan 29, 2024

@Alkarex

Apologies, I'm using this decoder.. any suggestions ? https://pastebin.com/raw/35Mif5nk

@Alkarex
Copy link
Author

Alkarex commented Jan 29, 2024

No, it is unrelated to the work here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment