Skip to content

Commit

Permalink
Added functionality for response messages
Browse files Browse the repository at this point in the history
  • Loading branch information
peter-quandify committed Oct 1, 2024
1 parent ef37fae commit b9a09a6
Show file tree
Hide file tree
Showing 2 changed files with 221 additions and 66 deletions.
1 change: 1 addition & 0 deletions vendor/quandify/cubicmeter-1-1-plastic-codec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ uplinkDecoder:
batteryActive: 3608
batteryRecovered: 3640
errorCode: 0
isSensing: true
leakState: 2
totalVolume: 5342
waterTemperatureMax: 22
Expand Down
286 changes: 220 additions & 66 deletions vendor/quandify/cubicmeter-1-1-uplink.js
Original file line number Diff line number Diff line change
@@ -1,113 +1,234 @@
// Please read here on how to implement the proper codec: https://www.thethingsindustries.com/docs/integrations/payload-formatters/javascript/

// 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 = {
2: 'medium',
3: 'large',
};

var pipeTypes = {
0: 'custom',
1: 'copper15',
2: 'copper18',
3: 'copper22',
4: 'chrome15',
5: 'chrome18',
6: 'chrome22',
7: 'pal16',
8: 'pal20',
9: 'pal25',
14: 'pex16',
15: 'pex20',
16: 'pex25',
17: 'distpipe',
};

/**
* The 'decodeUplink' function takes a message object and returns a parsed data object.
* @param input Message object
* @param input.fPort Fport.
* @param input.bytes Byte array.
*/
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 = {};

switch (input.fPort) {
case 1: // Status report
decoded = statusReportDecoder(input.bytes);
decoded = statusReportDecoder(data).data;
break;
case 6: // Response
decoded = responseDecoder(data);
break;
}

return {
data: {
fPort: input.fPort,
length: input.bytes.length,
hexBytes: toHexString(input.bytes),
type: getPacketType(input.fPort),
hexBytes: decArrayToStr(input.bytes),
type: uplinkTypes[input.fPort],
decoded: decoded,
},
};
}
var LSB = true;

var statusReportDecoder = function (bytes) {
const buffer = new ArrayBuffer(bytes.length);
const data = new DataView(buffer);
if (bytes.length < 28) {
throw new Error('payload too short');
}
for (const index in bytes) {
data.setUint8(index, bytes[index]);
const LSB = true;

var statusReportDecoder = function (data) {
if (data.byteLength != 28) {
throw new Error(`Wrong payload length (${data.byteLength}), should be 28 bytes`);
}

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;

return {
errorCode: data.getUint16(4, LSB), // current error code
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
type: 'statusReport',
data: {
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
},
};
};

function decodeBatteryLevel(input) {
return 1800 + (input << 3); // convert to milliVolt
}
var responseDecoder = function (data) {
const responseStatus = responseStatuses[data.getUint8(1)];
if (responseStatus === undefined) {
throw new Error(`Invalid response status: ${data.getUint8(1)}`);
}

function parseBatteryStatus(input) {
if (input <= 3100) {
return 'Low battery';
const responseType = responseTypes[data.getUint8(2)];
if (responseType === undefined) {
throw new Error(`Invalid response type: ${data.getUint8(2)}`);
}

return '';
}
const dataPayload = new DataView(data.buffer, 3);

function decodeTemperature(input) {
return parseFloat(input) * 0.5 - 20.0; // to °C
}
var responseData = {
type: undefined,
data: {},
};

// More packet types only available when using Quandify platform API
var getPacketType = function (type) {
switch (type) {
case 0:
return 'ping'; // empty ping message
case 1:
return 'statusReport'; // status message
switch (responseType) {
case 'statusReport':
responseData = statusReportDecoder(dataPayload);
break;
case 'hardwareReport':
responseData = hardwareReportDecoder(dataPayload);
break;
case 'settingsReport':
responseData = settingsReportDecoder(dataPayload);
break;
}

return 'Unknown';
return {
fPort: data.getUint8(0),
status: responseStatus,
type: responseData.type,
data: responseData.data,
};
};

/* Smaller water leakages only availble when using Quandify platform API
as it requires cloud analytics */
var parseLeakState = function (input) {
switch (input) {
case 3:
return 'Medium';
case 4:
return 'Large';
default:
return '';
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 {
type: 'hardwareReport',
data: {
firmwareVersion,
hardwareVersion: data.getUint8(4),
appState: appState,
pipe: {
id: data.getUint8(28),
type: pipeType,
},
},
};
};

var settingsReportDecoder = function (data) {
if (data.byteLength != 38) {
throw new Error(`Wrong payload length (${data.byteLength}), should be 38 bytes`);
}

return {
type: 'settingsReport',
data: {
lorawanReportInterval: data.getUint32(5, LSB),
},
};
};

function toHexString(byteArray) {
return Array.from(byteArray, function (byte) {
return ('0' + (byte & 0xff).toString(16)).slice(-2).toUpperCase();
}).join('');
}
var decodeBatteryLevel = function (input) {
return 1800 + (input << 3); // convert to milliVolt
};

function parseErrorCode(errorCode) {
var decodeTemperature = function (input) {
return parseFloat(input) * 0.5 - 20.0; // to °C
};

var parseBatteryStatus = function (input) {
if (input <= 3100) {
return 'Low battery';
}

return '';
};

var parseErrorCode = function (errorCode) {
switch (errorCode) {
case 0:
return '';
case 384:
return 'Reverse flow';
case 419:
case 421:
case 32768:
return 'No sensing';
default:
return 'Contact support';
return `Contact support, error ${errorCode}`;
}
};

var normalizeUplink = function (input) {
if (input.data.type != 'statusReport') {
return {};
}
}

function normalizeUplink(input) {
return {
data: {
air: {
Expand All @@ -118,7 +239,7 @@ function normalizeUplink(input) {
min: input.data.decoded.waterTemperatureMin, // °C
max: input.data.decoded.waterTemperatureMax, // °C
},
leak: parseLeakState(input.data.decoded.leak_state), // String
leak: leakStates[input.data.decoded.leak_state] ? leakStates[input.data.decoded.leak_state] : '', // String
},
metering: {
water: {
Expand All @@ -129,4 +250,37 @@ function normalizeUplink(input) {
},
warnings: [parseErrorCode(input.data.decoded.errorCode), parseBatteryStatus(input.data.decoded.batteryRecovered)].filter((item) => item),
};
}
};

// Convert a hex string to decimal array
var hexToDecArray = function (hexString) {
const size = 2;
const length = Math.ceil(hexString.length / size);
const decimalList = new Array(length);

for (let i = 0, o = 0; i < length; ++i, o += size) {
decimalList[i] = parseInt(hexString.substr(o, size), 16);
}

return decimalList;
};

var base64ToDecArray = function (base64String) {
const buffer = Buffer.from(base64String, 'base64');
const bufString = buffer.toString('hex');

return hexToDecArray(bufString);
};

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}`;
};

0 comments on commit b9a09a6

Please sign in to comment.