-
-
Save Alkarex/4b5d1fef2ff84d483e2793ed009ef607 to your computer and use it in GitHub Desktop.
/* 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; |
Looks awesome but... how did you capture the packet in the first place?
What HW are you using?
@PovilasID This is the payload sent over LoRaWAN (in my case, tested with The Things Network)
@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?
Own gateway yes, but the important part is to control the application, as LoRa packets are natively encrypted
See https://nordiciot.dk/byg-dit-eget-intelligente-vandmalersystem-guide/ (in Danish, but figures and an automated translation should be sufficient)
@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
Apologies, I'm using this decoder.. any suggestions ? https://pastebin.com/raw/35Mif5nk
No, it is unrelated to the work here
Hello, I am new to this LoRaWAN. I am interested in using your work to decode the LoRaWAN frame of a QALCOSONIC W1 Axioma Watermeter. For this I use an RTL_SDR USB Dongle listening at 868.1MHz
_rtl_433 -f 868.1M -F json | mosquitto_pub -t AXIOMA -l_
In Node-RED I will apply your code to the output of the MQTT json converter
Do you think it will work? Have I missed something important?
Any help/recommendation is welcome
Thank you very much in advance
@ramon2k10 I do not know whether the same format is used, so you can just try
Grate job, thanks for sharing!