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;
@diegofcornejo
Copy link

Grate job, thanks for sharing!

@PovilasID
Copy link

Looks awesome but... how did you capture the packet in the first place?
What HW are you using?

@Alkarex
Copy link
Author

Alkarex commented Nov 30, 2023

@PovilasID This is the payload sent over LoRaWAN (in my case, tested with The Things Network)

@PovilasID
Copy link

@PovilasID This is the payload sent over LoRaWAN (in my case, tested with The Things Network)

I am trying to capture Lora packet in transit using Software defined radio or aka usb stick with an antena. Manufacturer said that they do not encrypt the packets, so I should be able to just read them and use your scrip to decode them.
So in The Things Network you are not using your own gateway? Just using existing gateways to capture and then get access to the packets via API over the Internet?

@Alkarex
Copy link
Author

Alkarex commented Nov 30, 2023

Own gateway yes, but the important part is to control the application, as LoRa packets are natively encrypted

@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