-
-
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)