TTN Smart Sensor (Quandify)

Sensor

Codec Description

Codec for Quandify

Codec Preview

// Cubicmeter 1.1 uplink decoder var appStates = { 3: 'ready', 4: 'pipeSelection', 5: 'metering', }; var uplinkTypes = { 0: 'ping', 1: 'statusReport', 6: 'response', }; var responseStatuses = { 0: 'ok', 1: 'commandError', 2: 'payloadError', 3: 'valueError', }; // More uplink types only available when using Quandify platform API var responseTypes = { 0: 'none', 1: 'statusReport', 2: 'hardwareReport', 4: 'settingsReport', }; /* Smaller water leakages only availble when using Quandify platform API as it requires cloud analytics */ var leakStates = { 3: 'medium', 4: 'large', }; var pipeTypes = { 0: 'Custom', 1: 'Copper 15 mm', 2: 'Copper 18 mm', 3: 'Copper 22 mm', 4: 'Chrome 15 mm', 5: 'Chrome 18 mm', 6: 'Chrome 22 mm', 7: 'Pal 16 mm', 8: 'Pal 20 mm', 9: 'Pal 25 mm', 14: 'Pex 16 mm', 15: 'Pex 20 mm', 16: 'Pex 25 mm', 17: 'Distpipe', }; /** * 4.1 Uplink Decode * The 'decodeUplink' function takes a message object and returns a parsed data object. * @param input Message object * @param input.fPort int, The uplink message LoRaWAN fPort. * @param input.bytes int[], The uplink payload byte array, where each byte is represented by an integer between 0 and 255. * @param input.recvTime Date, The uplink message timestamp recorded by the LoRaWAN network server as a JavaScript Date object. */ function decodeUplink(input) { const buffer = new ArrayBuffer(input.bytes.length); const data = new DataView(buffer); for (const index in input.bytes) { data.setUint8(index, input.bytes[index]); } var decoded = {}; var errors = []; var warnings = []; try { switch (input.fPort) { case 1: // Status report ({ decoded, warnings } = statusReportDecoder(data)); break; case 6: // Response ({ decoded, warnings } = responseDecoder(data)); break; } } catch (err) { // Something went terribly wrong errors.push(err.message); } return { data: { fPort: input.fPort, length: input.bytes.length, hexBytes: decArrayToStr(input.bytes), type: uplinkTypes[input.fPort], decoded, }, errors, warnings, }; } const LSB = true; var statusReportDecoder = function (data) { if (data.byteLength != 28) { throw new Error(`Wrong payload length (${data.byteLength}), should be 28 bytes`); } let warnings = []; const error = data.getUint16(4, LSB); // The is sensing value is a bit flag of the error field const isSensing = !(error & 0x8000); const errorCode = error & 0x7fff; const decoded = { errorCode: errorCode, // current error code isSensing: isSensing, // is the ultrasonic sensor sensing water totalVolume: data.getUint32(6, LSB), // All-time aggregated water usage in litres leakState: data.getUint8(22), // current water leakage state batteryActive: decodeBatteryLevel(data.getUint8(23)), // battery mV active batteryRecovered: decodeBatteryLevel(data.getUint8(24)), // battery mV recovered waterTemperatureMin: decodeTemperature(data.getUint8(25)), // min water temperature since last statusReport waterTemperatureMax: decodeTemperature(data.getUint8(26)), // max water temperature since last statusReport ambientTemperature: decodeTemperature(data.getUint8(27)), // current ambient temperature }; // Warnings if (decoded.isSensing === false) { warnings.push('Not sensing water'); } if (decoded.errorCode) { warnings.push(parseErrorCode(decoded.errorCode)); } if (isLowBattery(decoded.batteryRecovered)) { warnings.push('Low battery'); } return { decoded, warnings, }; }; var responseDecoder = function (data) { const status = responseStatuses[data.getUint8(1)]; if (status === undefined) { throw new Error(`Invalid response status: ${data.getUint8(1)}`); } const type = responseTypes[data.getUint8(2)]; if (type === undefined) { throw new Error(`Invalid response type: ${data.getUint8(2)}`); } const payload = new DataView(data.buffer, 3); var response = { decoded: {}, warnings: [], }; switch (type) { case 'statusReport': response = statusReportDecoder(payload); break; case 'hardwareReport': response = hardwareReportDecoder(payload); break; case 'settingsReport': response = settingsReportDecoder(payload); break; } return { decoded: { fPort: data.getUint8(0), status: status, type: type, data: response.decoded, }, warnings: response.warnings, }; }; var hardwareReportDecoder = function (data) { if (data.byteLength != 35) { throw new Error(`Wrong payload length (${data.byteLength}), should be 35 bytes`); } const appState = appStates[data.getUint8(5)]; if (appState === undefined) { throw new Error(`Invalid app state (${data.getUint8(5)})`); } const pipeType = pipeTypes[data.getUint8(28)]; if (pipeType === undefined) { throw new Error(`Invalid pipe index (${data.getUint8(28)})`); } const firmwareVersion = intToSemver(data.getUint32(0, LSB)); return { decoded: { firmwareVersion, hardwareVersion: data.getUint8(4), appState: appState, pipe: { id: data.getUint8(28), type: pipeType, }, }, warnings: [], }; }; var settingsReportDecoder = function (data) { if (data.byteLength != 38) { throw new Error(`Wrong payload length (${data.byteLength}), should be 38 bytes`); } return { decoded: { lorawanReportInterval: data.getUint32(5, LSB), }, warnings: [], }; }; var decodeBatteryLevel = function (input) { return 1800 + (input << 3); // convert to milliVolt }; var decodeTemperature = function (input) { return parseFloat(input) * 0.5 - 20.0; // to °C }; var isLowBattery = function (batteryRecovered) { return batteryRecovered <= 3100; }; var parseErrorCode = function (errorCode) { switch (errorCode) { case 384: return 'Reverse flow'; default: return `Contact support, error ${errorCode}`; } }; var normalizeUplink = function (input) { if (input.data.type != 'statusReport') { return {}; } return { data: { air: { temperature: input.data.decoded.ambientTemperature, // °C }, water: { temperature: { min: input.data.decoded.waterTemperatureMin, // °C max: input.data.decoded.waterTemperatureMax, // °C }, leak: input.data.decoded.leakState in leakStates, // Boolean }, metering: { water: { total: input.data.decoded.totalVolume, // L }, }, battery: input.data.decoded.batteryRecovered / 1000, // V }, warnings: input.warnings, errors: input.errors, }; }; var decArrayToStr = function (byteArray) { return Array.from(byteArray, function (byte) { return ('0' + (byte & 0xff).toString(16)).slice(-2).toUpperCase(); }).join(''); }; var intToSemver = function (version) { const major = (version >> 24) & 0xff; const minor = (version >> 16) & 0xff; const patch = version & 0xffff; return `${major}.${minor}.${patch}`; }; 

This codec is sourced from The Things Network. All rights belong to The Things Network.

This codec is licensed under the GNU General Public License v3 (GPL v3). Modifications, if any, are clearly marked. You are free to use, modify, and distribute the codec under the terms of GPL v3.

Community Feedback