-
-
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; |
@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
See https://nordiciot.dk/byg-dit-eget-intelligente-vandmalersystem-guide/ (in Danish, but figures and an automated translation should be sufficient)