diff --git a/types/sensative/ms-comfort/alarm.schema.json b/types/sensative/ms-comfort/alarm.schema.json new file mode 100644 index 00000000..5cbbb8f1 --- /dev/null +++ b/types/sensative/ms-comfort/alarm.schema.json @@ -0,0 +1,66 @@ +{ + "$id": "https://akenza.io/sensative/ms-comfort/alarm.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "processingType": "uplink_decoder", + "topic": "alarm", + "title": "Alarm", + "properties": { + "highAlarm": { + "title": "High alarm", + "description": "Equals to true if the threshold is exceeded", + "type": "boolean" + }, + "lowAlarm": { + "title": "Low alarm", + "description": "Equals to true if the threshold has fallen below", + "type": "boolean" + }, + "doorAlarm": { + "title": "Door alarm", + "description": "Equals to true if there is a door alarm", + "type": "boolean" + }, + "tamperAlarm": { + "title": "Tamper alarm", + "description": "Equals to true if the strip has been tampered with", + "type": "boolean" + }, + "floodAlarm": { + "title": "Flood alarm", + "description": "Equals to true if a flood is detected", + "type": "boolean" + }, + "oilAlarm": { + "title": "Oil alarm", + "description": "Equals to true if oil is detected", + "type": "boolean" + }, + "foilAlarm": { + "title": "Foil alarm", + "description": "Equals to true if a foil is detected", + "type": "boolean" + }, + "userSwitchAlarm": { + "title": "User switch alarm", + "description": "Equals to true if a user switch is detected", + "type": "boolean" + }, + "closeProximityAlarm": { + "title": "Close proximity Alarm", + "description": "Equals to true if an object in close proximity is detected", + "type": "boolean" + }, + "disinfectAlarm": { + "title": "Disinfect alarm", + "description": "Status of the disinfection", + "type": "string", + "enum": [ + "DIRTY", + "OCCUPIED", + "CLEANING", + "CLEAN" + ] + } + } +} \ No newline at end of file diff --git a/types/sensative/ms-comfort/comfort.png b/types/sensative/ms-comfort/comfort.png new file mode 100644 index 00000000..0beab483 Binary files /dev/null and b/types/sensative/ms-comfort/comfort.png differ diff --git a/types/sensative/ms-comfort/default.schema.json b/types/sensative/ms-comfort/default.schema.json new file mode 100644 index 00000000..c8440139 --- /dev/null +++ b/types/sensative/ms-comfort/default.schema.json @@ -0,0 +1,71 @@ +{ + "$id": "https://akenza.io/sensative/ms-comfort/default.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "processingType": "uplink_decoder", + "topic": "default", + "title": "Default", + "properties": { + "temperature": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/temperature/celsius" + }, + "averageTemperature": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/temperature/celsius", + "description": "Average temperature report in °C.", + "title": "Average temperature report" + }, + "humidity": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/humidity/percent" + }, + "light": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/illuminance/lux" + }, + "light2": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/illuminance/lux" + }, + "closed": { + "title": "Closed", + "description": "Status of the strip. closed = true, open = false", + "type": "boolean" + }, + "tamperReport": { + "title": "Tamper report", + "description": "Status if the device got tampered the strip. open = true, closed = false", + "type": "boolean" + }, + "flood": { + "title": "Flood", + "unit": "%", + "type": "number", + "description": "Flood relative wetness", + "minimum": 0, + "maximum": 100 + }, + "doorCount": { + "title": "Door count", + "type": "integer", + "description": "Door opening count", + "minimum": 0, + "maximum": 65535 + }, + "presence": { + "title": "Presence", + "description": "Status if the device reads presence", + "type": "boolean" + }, + "irProximity": { + "title": "Infrared proximity", + "description": "Count of objects in proximity", + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "irCloseProximity": { + "title": "Infrared close proximity", + "description": "Count of objects in close proximity", + "type": "integer", + "minimum": 0, + "maximum": 255 + } + } +} \ No newline at end of file diff --git a/types/sensative/ms-comfort/lifecycle.schema.json b/types/sensative/ms-comfort/lifecycle.schema.json new file mode 100644 index 00000000..33127378 --- /dev/null +++ b/types/sensative/ms-comfort/lifecycle.schema.json @@ -0,0 +1,62 @@ +{ + "$id": "https://akenza.io/sensative/ms-comfort/lifecycle.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "processingType": "uplink_decoder", + "topic": "lifecycle", + "title": "Lifecycle", + "properties": { + "batteryLevel": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "title": "Battery charge", + "description": "The battery charge in percent.", + "unit": "%" + }, + "error": { + "title": "Error message", + "description": "Error info which contains: wrong length of RX package", + "type": "string", + "hideFromKpis": true + }, + "badConditionsCounter": { + "title": "Bad conditions counter", + "type": "integer", + "description": "Bad conditions counter", + "minimum": 0, + "maximum": 65535, + "hideFromKpis": true + }, + "stackTxFailRebootCount": { + "title": "Stack TX fail reboot count", + "type": "integer", + "description": "Stack TX fail reboot count", + "minimum": 0, + "maximum": 65535, + "hideFromKpis": true + }, + "startupCount": { + "title": "Start up count", + "type": "integer", + "description": "Start up count", + "minimum": 0, + "maximum": 65535, + "hideFromKpis": true + }, + "watchdogCount": { + "title": "Watchdog count", + "type": "integer", + "description": "Watchdog count", + "minimum": 0, + "maximum": 65535, + "hideFromKpis": true + }, + "softwareVersion": { + "title": "Software version", + "type": "string", + "description": "Software version", + "hideFromKpis": true + } + } +} \ No newline at end of file diff --git a/types/sensative/ms-comfort/meta.json b/types/sensative/ms-comfort/meta.json new file mode 100644 index 00000000..7cb40c22 --- /dev/null +++ b/types/sensative/ms-comfort/meta.json @@ -0,0 +1,14 @@ +{ + "name": "Strips Multi-sensor +Comfort", + "version": "1.0.0", + "manufacturer": "Sensative", + "url": "https://sensative.com/sensors/strips-sensors-for-lorawan/strips-lorawan-ms-comfort/", + "description": "MS +Comfort's precise measuring gives you a great overview of your indoor climate. Maybe you want to keep your office environment optimal for the well-being of your staff or keep a close eye on changes in temperature and humidity in remote areas such as cellars or attics.", + "author": "Akenza AG", + "firmwareVersion": "V1.0.0", + "loraDeviceClass": "A", + "availableSensors": ["Magnet", "Temperature"], + "outputTopics": ["alarm", "default", "lifecycle", "occupancy"], + "encoding": "HEX", + "connectivity": "LORA" +} diff --git a/types/sensative/ms-comfort/occupancy.schema.json b/types/sensative/ms-comfort/occupancy.schema.json new file mode 100644 index 00000000..40ca6563 --- /dev/null +++ b/types/sensative/ms-comfort/occupancy.schema.json @@ -0,0 +1,25 @@ +{ + "$id": "https://akenza.io/sensative/ms-comfort/occupancy.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "processingType": "uplink_decoder", + "topic": "occupancy", + "title": "Occupancy", + "properties": { + "occupancy": { + "title": "Occupancy", + "description": "Space occupancy. 0 = Unoccupied / 1 = Occupied.", + "type": "integer", + "minimum": 0, + "maximum": 1 + }, + "occupied": { + "title": "Occupied", + "description": "Space occupancy. false = Unoccupied / true = Occupied.", + "type": "boolean" + } + }, + "required": [ + "occupancy" + ] +} \ No newline at end of file diff --git a/types/sensative/ms-comfort/uplink.js b/types/sensative/ms-comfort/uplink.js new file mode 100644 index 00000000..e1f70ad2 --- /dev/null +++ b/types/sensative/ms-comfort/uplink.js @@ -0,0 +1,277 @@ +function decodeFrame(bytes, type, pos) { + const data = {}; + let pointer = pos; + + switch (type & 0x7f) { + case 0: + data.emptyFrame = {}; + break; + case 1: // Battery 1byte 0-100% + data.batteryLevel = bytes[pointer++]; + break; + case 2: // TempReport 2bytes 0.1degree C + // celcius 0.1 precision + data.temperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 3: + // Temp alarm + // sends alarm after >x< + data.highAlarm = !!(bytes[pointer] & 0x01); // boolean + data.lowAlarm = !!(bytes[pointer] & 0x02); // boolean + pointer++; + break; + case 4: // AvgTempReport 2bytes 0.1degree C + data.averageTemperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 5: + // AvgTemp alarm + // sends alarm after >x< + data.highAlarm = !!(bytes[pointer] & 0x01); // boolean + data.lowAlarm = !!(bytes[pointer] & 0x02); // boolean + pointer++; + break; + case 6: // Humidity 1byte 0-100% in 0.5% + data.humidity = bytes[pointer] / 2; // relativeHumidity percent 0,5 + break; + case 7: // Lux 2bytes 0-65535lux + data.light = (bytes[pointer++] << 8) | bytes[pointer++]; // you can the lux range between two sets (lux1 and 2) + break; + case 8: // Lux 2bytes 0-65535lux + data.light2 = (bytes[pointer++] << 8) | bytes[pointer++]; + break; + case 9: // DoorSwitch 1bytes binary + data.closed = !!bytes[pointer++]; // true = door closed, false = door open + break; + case 10: // DoorAlarm 1bytes binary + data.doorAlarm = !!bytes[pointer++]; // boolean true = alarm + break; + case 11: // TamperReport 1bytes binary (was previously TamperSwitch) + data.tamperReport = !!bytes[pointer++]; + break; + case 12: // TamperAlarm 1bytes binary + data.tamperAlarm = !!bytes[pointer++]; + break; + case 13: // Flood 1byte 0-100% + data.flood = bytes[pointer++]; // percentage, relative wetness + break; + case 14: // FloodAlarm 1bytes binary + data.floodAlarm = !!bytes[pointer++]; // boolean, after >x< + break; + case 15: // FoilAlarm 1bytes binary + data.oilAlarm = !!bytes[pointer]; + data.foilAlarm = !!bytes[pointer++]; + break; + case 16: // UserSwitch1Alarm, 1 byte digital + data.userSwitchAlarm = !!bytes[pointer++]; + break; + case 17: // DoorCountReport, 2 byte analog + data.doorCount = (bytes[pointer++] << 8) | bytes[pointer++]; + break; + case 18: // PresenceReport, 1 byte digital + data.presence = !!bytes[pointer++]; + break; + case 19: // IRProximityReport + data.irProximity = (bytes[pointer++] << 8) | bytes[pointer++]; + break; + case 20: // IRCloseProximityReport, low power + data.irCloseProximity = (bytes[pointer++] << 8) | bytes[pointer++]; + break; + case 21: // CloseProximityAlarm, something very close to presence sensor + data.closeProximityAlarm = !!bytes[pointer++]; + break; + case 22: // DisinfectAlarm + data.disinfectAlarm = bytes[pointer++]; + if (data.disinfectAlarm === 0) data.disinfectAlarm = "DIRTY"; + if (data.disinfectAlarm === 1) data.disinfectAlarm = "OCCUPIED"; + if (data.disinfectAlarm === 2) data.disinfectAlarm = "CLEANING"; + if (data.disinfectAlarm === 3) data.disinfectAlarm = "CLEAN"; + break; + case 80: + data.humidity = bytes[pointer++] / 2; + data.temperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 81: + data.humidity = bytes[pointer++] / 2; + data.averageTemperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 82: + data.closed = !!bytes[pointer++]; // true = door open, false = door closed // Inverted in example decoder + data.temperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 112: // Capacitance Raw Sensor Value 2bytes 0-65535 + data.capacitanceFlood = (bytes[pointer++] << 8) | bytes[pointer++]; // should never trigger anymore + break; + case 113: // Capacitance Raw Sensor Value 2bytes 0-65535 + data.capacitancePad = (bytes[pointer++] << 8) | bytes[pointer++]; // should never trigger anymore + break; + case 110: { + const number = + ((bytes[pointer++] << 24) | + (bytes[pointer++] << 16) | + (bytes[pointer++] << 8) | + bytes[pointer++]) >>> + 0; + data.softwareVersion = number.toString(16); + data.startupCount = bytes[pointer++]; + data.watchdogCount = bytes[pointer++]; + data.stackTxFailRebootCount = bytes[pointer++]; + data.badConditionsCounter = bytes[pointer++]; + break; + } + case 114: // Capacitance Raw Sensor Value 2bytes 0-65535 + data.capacitanceEnd = (bytes[pointer++] << 8) | bytes[pointer++]; // should never trigger anymore + break; + default: + break; + } + return { data, pointer }; +} + +function deleteUnusedKeys(data) { + let keysRetained = false; + Object.keys(data).forEach((key) => { + if (data[key] === undefined) { + delete data[key]; + } else { + keysRetained = true; + } + }); + return keysRetained; +} + +function consume(event) { + const payload = event.data.payloadHex; + const { port } = event.data; + const bytes = Hex.hexToBytes(payload); + + const decoded = {}; + let pos = 2; + let type; + + switch (port) { + case 1: + if (bytes.length < 2) { + emit("log", { error: "Wrong length of RX package" }); + break; + } + while (pos < bytes.length) { + type = bytes[pos++]; + const decodedFrame = decodeFrame(bytes, type, pos); + pos = decodedFrame.pointer; + Object.assign(decoded, decodedFrame.data); + } + break; + + case 2: { + const now = new Date(); + decoded.history = {}; + if (bytes.length < 2) { + decoded.history.error = "Wrong length of RX package"; + break; + } + let seqNr = (bytes[pos++] << 8) | bytes[pos++]; + while (pos < bytes.length) { + decoded.history[seqNr] = {}; + decoded.history.now = now.toUTCString(); + const secondsAgo = + (bytes[pos++] << 24) | + (bytes[pos++] << 16) | + (bytes[pos++] << 8) | + bytes[pos++]; + decoded.history[seqNr].timeStamp = new Date( + now.getTime() - secondsAgo * 1000, + ).toUTCString(); + type = bytes[pos++]; + const decodedFrame = decodeFrame(bytes, type, pos); + pos = decodedFrame.pointer; + Object.assign(decoded, decodedFrame.data); + seqNr++; + } + break; + } + default: + break; + } + + const def = {}; + def.temperature = decoded.temperature; + def.averageTemperature = decoded.averageTemperature; + def.humidity = decoded.humidity; + def.light = decoded.light; + def.light2 = decoded.light2; + def.closed = decoded.closed; + def.tamperReport = decoded.tamperReport; + def.doorCount = decoded.doorCount; + def.presence = decoded.presence; + def.irProximity = decoded.irProximity; + def.irCloseProximity = decoded.irCloseProximity; + + const alarm = {}; + alarm.highAlarm = decoded.highAlarm; + alarm.lowAlarm = decoded.lowAlarm; + alarm.doorAlarm = decoded.doorAlarm; + alarm.tamperAlarm = decoded.tamperAlarm; + alarm.floodAlarm = decoded.floodAlarm; + alarm.oilAlarm = decoded.oilAlarm; + alarm.foilAlarm = decoded.foilAlarm; + alarm.userSwitchAlarm = decoded.userSwitchAlarm; + alarm.closeProximityAlarm = decoded.closeProximityAlarm; + alarm.disinfectAlarm = decoded.disinfectAlarm; + + const lifecycle = {}; + if (decoded.batteryLevel !== 0) { + lifecycle.batteryLevel = decoded.batteryLevel; + } + lifecycle.error = decoded.error; + lifecycle.softwareVersion = decoded.softwareVersion; + lifecycle.startupCount = decoded.startupCount; + lifecycle.watchdogCount = decoded.watchdogCount; + lifecycle.stackTxFailRebootCount = decoded.stackTxFailRebootCount; + lifecycle.badConditionsCounter = decoded.badConditionsCounter; + + if (deleteUnusedKeys(lifecycle)) { + emit("sample", { data: lifecycle, topic: "lifecycle" }); + } + + if (deleteUnusedKeys(alarm)) { + emit("sample", { data: alarm, topic: "alarm" }); + } + + if (deleteUnusedKeys(def)) { + emit("sample", { data: def, topic: "default" }); + } + + if (decoded.presence !== undefined) { + if (decoded.presence === true) { + emit("sample", { + data: { occupancy: 1, occupied: true }, + topic: "occupancy", + }); + } else { + emit("sample", { + data: { occupancy: 0, occupied: false }, + topic: "occupancy", + }); + } + } +} diff --git a/types/sensative/ms-comfort/uplink.spec.js b/types/sensative/ms-comfort/uplink.spec.js new file mode 100644 index 00000000..c8334a0b --- /dev/null +++ b/types/sensative/ms-comfort/uplink.spec.js @@ -0,0 +1,197 @@ +const chai = require("chai"); + +const rewire = require("rewire"); +const utils = require("test-utils"); + +const { assert } = chai; + +describe("Sensative strip", () => { + let defaultSchema = null; + let consume = null; + before((done) => { + const script = rewire("./uplink.js"); + consume = utils.init(script); + utils + .loadSchema(`${__dirname}/default.schema.json`) + .then((parsedSchema) => { + defaultSchema = parsedSchema; + done(); + }); + }); + + let lifecycleSchema = null; + before((done) => { + utils + .loadSchema(`${__dirname}/lifecycle.schema.json`) + .then((parsedSchema) => { + lifecycleSchema = parsedSchema; + done(); + }); + }); + + let alarmSchema = null; + before((done) => { + utils.loadSchema(`${__dirname}/alarm.schema.json`).then((parsedSchema) => { + alarmSchema = parsedSchema; + done(); + }); + }); + + describe("consume()", () => { + it("should decode the sensative strip payload", () => { + const data = { + data: { + port: 1, + payloadHex: "ffff09010a0052010000", + }, + }; + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "alarm"); + assert.equal(value.data.doorAlarm, false); + + utils.validateSchema(value.data, alarmSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "default"); + assert.equal(value.data.closed, true); + assert.equal(value.data.temperature, 0); + + utils.validateSchema(value.data, defaultSchema, { throwError: true }); + }); + + consume(data); + }); + + it("should decode the sensative strip payload", () => { + const data = { + data: { + port: 1, + payloadHex: "ffff01561100001500", + }, + }; + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "lifecycle"); + assert.equal(value.data.batteryLevel, 86); + + utils.validateSchema(value.data, lifecycleSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "alarm"); + assert.equal(value.data.closeProximityAlarm, false); + + utils.validateSchema(value.data, alarmSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "default"); + assert.equal(value.data.doorCount, 0); + + utils.validateSchema(value.data, defaultSchema, { throwError: true }); + }); + + consume(data); + }); + + it("should decode the sensative strip payload", () => { + const data = { + data: { + port: 1, + payloadHex: "ffff01510900110001", + }, + }; + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "lifecycle"); + assert.equal(value.data.batteryLevel, 81); + + utils.validateSchema(value.data, lifecycleSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "default"); + assert.equal(value.data.closed, false); + assert.equal(value.data.doorCount, 1); + + utils.validateSchema(value.data, defaultSchema, { throwError: true }); + }); + + consume(data); + }); + + it("should decode the sensative strip payload", () => { + const data = { + data: { + port: 1, + payloadHex: "ffff01581100001501", + }, + }; + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "lifecycle"); + assert.equal(value.data.batteryLevel, 88); + + utils.validateSchema(value.data, lifecycleSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "alarm"); + assert.equal(value.data.closeProximityAlarm, true); + + utils.validateSchema(value.data, alarmSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "default"); + assert.equal(value.data.doorCount, 0); + + utils.validateSchema(value.data, defaultSchema, { throwError: true }); + }); + + consume(data); + }); + }); +}); diff --git a/types/sensative/ms-drip-oil/alarm.schema.json b/types/sensative/ms-drip-oil/alarm.schema.json new file mode 100644 index 00000000..48e8d085 --- /dev/null +++ b/types/sensative/ms-drip-oil/alarm.schema.json @@ -0,0 +1,66 @@ +{ + "$id": "https://akenza.io/sensative/ms-drip-oil/alarm.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "processingType": "uplink_decoder", + "topic": "alarm", + "title": "Alarm", + "properties": { + "highAlarm": { + "title": "High alarm", + "description": "Equals to true if the threshold is exceeded", + "type": "boolean" + }, + "lowAlarm": { + "title": "Low alarm", + "description": "Equals to true if the threshold has fallen below", + "type": "boolean" + }, + "doorAlarm": { + "title": "Door alarm", + "description": "Equals to true if there is a door alarm", + "type": "boolean" + }, + "tamperAlarm": { + "title": "Tamper alarm", + "description": "Equals to true if the strip has been tampered with", + "type": "boolean" + }, + "floodAlarm": { + "title": "Flood alarm", + "description": "Equals to true if a flood is detected", + "type": "boolean" + }, + "oilAlarm": { + "title": "Oil alarm", + "description": "Equals to true if oil is detected", + "type": "boolean" + }, + "foilAlarm": { + "title": "Foil alarm", + "description": "Equals to true if a foil is detected", + "type": "boolean" + }, + "userSwitchAlarm": { + "title": "User switch alarm", + "description": "Equals to true if a user switch is detected", + "type": "boolean" + }, + "closeProximityAlarm": { + "title": "Close proximity Alarm", + "description": "Equals to true if an object in close proximity is detected", + "type": "boolean" + }, + "disinfectAlarm": { + "title": "Disinfect alarm", + "description": "Status of the disinfection", + "type": "string", + "enum": [ + "DIRTY", + "OCCUPIED", + "CLEANING", + "CLEAN" + ] + } + } +} \ No newline at end of file diff --git a/types/sensative/ms-drip-oil/default.schema.json b/types/sensative/ms-drip-oil/default.schema.json new file mode 100644 index 00000000..51c9970c --- /dev/null +++ b/types/sensative/ms-drip-oil/default.schema.json @@ -0,0 +1,71 @@ +{ + "$id": "https://akenza.io/sensative/ms-drip-oil/default.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "processingType": "uplink_decoder", + "topic": "default", + "title": "Default", + "properties": { + "temperature": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/temperature/celsius" + }, + "averageTemperature": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/temperature/celsius", + "description": "Average temperature report in °C.", + "title": "Average temperature report" + }, + "humidity": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/humidity/percent" + }, + "light": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/illuminance/lux" + }, + "light2": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/illuminance/lux" + }, + "closed": { + "title": "Closed", + "description": "Status of the strip. closed = true, open = false", + "type": "boolean" + }, + "tamperReport": { + "title": "Tamper report", + "description": "Status if the device got tampered the strip. open = true, closed = false", + "type": "boolean" + }, + "flood": { + "title": "Flood", + "unit": "%", + "type": "number", + "description": "Flood relative wetness", + "minimum": 0, + "maximum": 100 + }, + "doorCount": { + "title": "Door count", + "type": "integer", + "description": "Door opening count", + "minimum": 0, + "maximum": 65535 + }, + "presence": { + "title": "Presence", + "description": "Status if the device reads presence", + "type": "boolean" + }, + "irProximity": { + "title": "Infrared proximity", + "description": "Count of objects in proximity", + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "irCloseProximity": { + "title": "Infrared close proximity", + "description": "Count of objects in close proximity", + "type": "integer", + "minimum": 0, + "maximum": 255 + } + } +} \ No newline at end of file diff --git a/types/sensative/ms-drip-oil/dripOil.png b/types/sensative/ms-drip-oil/dripOil.png new file mode 100644 index 00000000..4c873d34 Binary files /dev/null and b/types/sensative/ms-drip-oil/dripOil.png differ diff --git a/types/sensative/ms-drip-oil/lifecycle.schema.json b/types/sensative/ms-drip-oil/lifecycle.schema.json new file mode 100644 index 00000000..ad824ba5 --- /dev/null +++ b/types/sensative/ms-drip-oil/lifecycle.schema.json @@ -0,0 +1,62 @@ +{ + "$id": "https://akenza.io/sensative/ms-drip-oil/lifecycle.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "processingType": "uplink_decoder", + "topic": "lifecycle", + "title": "Lifecycle", + "properties": { + "batteryLevel": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "title": "Battery charge", + "description": "The battery charge in percent.", + "unit": "%" + }, + "error": { + "title": "Error message", + "description": "Error info which contains: wrong length of RX package", + "type": "string", + "hideFromKpis": true + }, + "badConditionsCounter": { + "title": "Bad conditions counter", + "type": "integer", + "description": "Bad conditions counter", + "minimum": 0, + "maximum": 65535, + "hideFromKpis": true + }, + "stackTxFailRebootCount": { + "title": "Stack TX fail reboot count", + "type": "integer", + "description": "Stack TX fail reboot count", + "minimum": 0, + "maximum": 65535, + "hideFromKpis": true + }, + "startupCount": { + "title": "Start up count", + "type": "integer", + "description": "Start up count", + "minimum": 0, + "maximum": 65535, + "hideFromKpis": true + }, + "watchdogCount": { + "title": "Watchdog count", + "type": "integer", + "description": "Watchdog count", + "minimum": 0, + "maximum": 65535, + "hideFromKpis": true + }, + "softwareVersion": { + "title": "Software version", + "type": "string", + "description": "Software version", + "hideFromKpis": true + } + } +} \ No newline at end of file diff --git a/types/sensative/ms-drip-oil/meta.json b/types/sensative/ms-drip-oil/meta.json new file mode 100644 index 00000000..14db336e --- /dev/null +++ b/types/sensative/ms-drip-oil/meta.json @@ -0,0 +1,14 @@ +{ + "name": "Strips Multi-sensor +Drip Oil", + "version": "1.0.0", + "manufacturer": "Sensative", + "url": "https://sensative.com/sensors/strips-sensors-for-lorawan/strips-multi-sensor-oil-for-lorawan/", + "description": "MStrips MS +Drip Oil is an innovative ultra-thin (4mm/0.16in) and wireless multi-sensor for LoRaWAN with water & oil leak detection, developed for smart IoT applications such as heavy machinery, transmission and hydraulics monitoring.", + "author": "Akenza AG", + "firmwareVersion": "V1.0.0", + "loraDeviceClass": "A", + "availableSensors": ["Water Leak", "Oil Leak", "Temperature"], + "outputTopics": ["alarm", "default", "lifecycle", "occupancy"], + "encoding": "HEX", + "connectivity": "LORA" +} diff --git a/types/sensative/ms-drip-oil/occupancy.schema.json b/types/sensative/ms-drip-oil/occupancy.schema.json new file mode 100644 index 00000000..604fe161 --- /dev/null +++ b/types/sensative/ms-drip-oil/occupancy.schema.json @@ -0,0 +1,25 @@ +{ + "$id": "https://akenza.io/sensative/ms-drip-oil/occupancy.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "processingType": "uplink_decoder", + "topic": "occupancy", + "title": "Occupancy", + "properties": { + "occupancy": { + "title": "Occupancy", + "description": "Space occupancy. 0 = Unoccupied / 1 = Occupied.", + "type": "integer", + "minimum": 0, + "maximum": 1 + }, + "occupied": { + "title": "Occupied", + "description": "Space occupancy. false = Unoccupied / true = Occupied.", + "type": "boolean" + } + }, + "required": [ + "occupancy" + ] +} \ No newline at end of file diff --git a/types/sensative/ms-drip-oil/uplink.js b/types/sensative/ms-drip-oil/uplink.js new file mode 100644 index 00000000..e1f70ad2 --- /dev/null +++ b/types/sensative/ms-drip-oil/uplink.js @@ -0,0 +1,277 @@ +function decodeFrame(bytes, type, pos) { + const data = {}; + let pointer = pos; + + switch (type & 0x7f) { + case 0: + data.emptyFrame = {}; + break; + case 1: // Battery 1byte 0-100% + data.batteryLevel = bytes[pointer++]; + break; + case 2: // TempReport 2bytes 0.1degree C + // celcius 0.1 precision + data.temperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 3: + // Temp alarm + // sends alarm after >x< + data.highAlarm = !!(bytes[pointer] & 0x01); // boolean + data.lowAlarm = !!(bytes[pointer] & 0x02); // boolean + pointer++; + break; + case 4: // AvgTempReport 2bytes 0.1degree C + data.averageTemperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 5: + // AvgTemp alarm + // sends alarm after >x< + data.highAlarm = !!(bytes[pointer] & 0x01); // boolean + data.lowAlarm = !!(bytes[pointer] & 0x02); // boolean + pointer++; + break; + case 6: // Humidity 1byte 0-100% in 0.5% + data.humidity = bytes[pointer] / 2; // relativeHumidity percent 0,5 + break; + case 7: // Lux 2bytes 0-65535lux + data.light = (bytes[pointer++] << 8) | bytes[pointer++]; // you can the lux range between two sets (lux1 and 2) + break; + case 8: // Lux 2bytes 0-65535lux + data.light2 = (bytes[pointer++] << 8) | bytes[pointer++]; + break; + case 9: // DoorSwitch 1bytes binary + data.closed = !!bytes[pointer++]; // true = door closed, false = door open + break; + case 10: // DoorAlarm 1bytes binary + data.doorAlarm = !!bytes[pointer++]; // boolean true = alarm + break; + case 11: // TamperReport 1bytes binary (was previously TamperSwitch) + data.tamperReport = !!bytes[pointer++]; + break; + case 12: // TamperAlarm 1bytes binary + data.tamperAlarm = !!bytes[pointer++]; + break; + case 13: // Flood 1byte 0-100% + data.flood = bytes[pointer++]; // percentage, relative wetness + break; + case 14: // FloodAlarm 1bytes binary + data.floodAlarm = !!bytes[pointer++]; // boolean, after >x< + break; + case 15: // FoilAlarm 1bytes binary + data.oilAlarm = !!bytes[pointer]; + data.foilAlarm = !!bytes[pointer++]; + break; + case 16: // UserSwitch1Alarm, 1 byte digital + data.userSwitchAlarm = !!bytes[pointer++]; + break; + case 17: // DoorCountReport, 2 byte analog + data.doorCount = (bytes[pointer++] << 8) | bytes[pointer++]; + break; + case 18: // PresenceReport, 1 byte digital + data.presence = !!bytes[pointer++]; + break; + case 19: // IRProximityReport + data.irProximity = (bytes[pointer++] << 8) | bytes[pointer++]; + break; + case 20: // IRCloseProximityReport, low power + data.irCloseProximity = (bytes[pointer++] << 8) | bytes[pointer++]; + break; + case 21: // CloseProximityAlarm, something very close to presence sensor + data.closeProximityAlarm = !!bytes[pointer++]; + break; + case 22: // DisinfectAlarm + data.disinfectAlarm = bytes[pointer++]; + if (data.disinfectAlarm === 0) data.disinfectAlarm = "DIRTY"; + if (data.disinfectAlarm === 1) data.disinfectAlarm = "OCCUPIED"; + if (data.disinfectAlarm === 2) data.disinfectAlarm = "CLEANING"; + if (data.disinfectAlarm === 3) data.disinfectAlarm = "CLEAN"; + break; + case 80: + data.humidity = bytes[pointer++] / 2; + data.temperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 81: + data.humidity = bytes[pointer++] / 2; + data.averageTemperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 82: + data.closed = !!bytes[pointer++]; // true = door open, false = door closed // Inverted in example decoder + data.temperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 112: // Capacitance Raw Sensor Value 2bytes 0-65535 + data.capacitanceFlood = (bytes[pointer++] << 8) | bytes[pointer++]; // should never trigger anymore + break; + case 113: // Capacitance Raw Sensor Value 2bytes 0-65535 + data.capacitancePad = (bytes[pointer++] << 8) | bytes[pointer++]; // should never trigger anymore + break; + case 110: { + const number = + ((bytes[pointer++] << 24) | + (bytes[pointer++] << 16) | + (bytes[pointer++] << 8) | + bytes[pointer++]) >>> + 0; + data.softwareVersion = number.toString(16); + data.startupCount = bytes[pointer++]; + data.watchdogCount = bytes[pointer++]; + data.stackTxFailRebootCount = bytes[pointer++]; + data.badConditionsCounter = bytes[pointer++]; + break; + } + case 114: // Capacitance Raw Sensor Value 2bytes 0-65535 + data.capacitanceEnd = (bytes[pointer++] << 8) | bytes[pointer++]; // should never trigger anymore + break; + default: + break; + } + return { data, pointer }; +} + +function deleteUnusedKeys(data) { + let keysRetained = false; + Object.keys(data).forEach((key) => { + if (data[key] === undefined) { + delete data[key]; + } else { + keysRetained = true; + } + }); + return keysRetained; +} + +function consume(event) { + const payload = event.data.payloadHex; + const { port } = event.data; + const bytes = Hex.hexToBytes(payload); + + const decoded = {}; + let pos = 2; + let type; + + switch (port) { + case 1: + if (bytes.length < 2) { + emit("log", { error: "Wrong length of RX package" }); + break; + } + while (pos < bytes.length) { + type = bytes[pos++]; + const decodedFrame = decodeFrame(bytes, type, pos); + pos = decodedFrame.pointer; + Object.assign(decoded, decodedFrame.data); + } + break; + + case 2: { + const now = new Date(); + decoded.history = {}; + if (bytes.length < 2) { + decoded.history.error = "Wrong length of RX package"; + break; + } + let seqNr = (bytes[pos++] << 8) | bytes[pos++]; + while (pos < bytes.length) { + decoded.history[seqNr] = {}; + decoded.history.now = now.toUTCString(); + const secondsAgo = + (bytes[pos++] << 24) | + (bytes[pos++] << 16) | + (bytes[pos++] << 8) | + bytes[pos++]; + decoded.history[seqNr].timeStamp = new Date( + now.getTime() - secondsAgo * 1000, + ).toUTCString(); + type = bytes[pos++]; + const decodedFrame = decodeFrame(bytes, type, pos); + pos = decodedFrame.pointer; + Object.assign(decoded, decodedFrame.data); + seqNr++; + } + break; + } + default: + break; + } + + const def = {}; + def.temperature = decoded.temperature; + def.averageTemperature = decoded.averageTemperature; + def.humidity = decoded.humidity; + def.light = decoded.light; + def.light2 = decoded.light2; + def.closed = decoded.closed; + def.tamperReport = decoded.tamperReport; + def.doorCount = decoded.doorCount; + def.presence = decoded.presence; + def.irProximity = decoded.irProximity; + def.irCloseProximity = decoded.irCloseProximity; + + const alarm = {}; + alarm.highAlarm = decoded.highAlarm; + alarm.lowAlarm = decoded.lowAlarm; + alarm.doorAlarm = decoded.doorAlarm; + alarm.tamperAlarm = decoded.tamperAlarm; + alarm.floodAlarm = decoded.floodAlarm; + alarm.oilAlarm = decoded.oilAlarm; + alarm.foilAlarm = decoded.foilAlarm; + alarm.userSwitchAlarm = decoded.userSwitchAlarm; + alarm.closeProximityAlarm = decoded.closeProximityAlarm; + alarm.disinfectAlarm = decoded.disinfectAlarm; + + const lifecycle = {}; + if (decoded.batteryLevel !== 0) { + lifecycle.batteryLevel = decoded.batteryLevel; + } + lifecycle.error = decoded.error; + lifecycle.softwareVersion = decoded.softwareVersion; + lifecycle.startupCount = decoded.startupCount; + lifecycle.watchdogCount = decoded.watchdogCount; + lifecycle.stackTxFailRebootCount = decoded.stackTxFailRebootCount; + lifecycle.badConditionsCounter = decoded.badConditionsCounter; + + if (deleteUnusedKeys(lifecycle)) { + emit("sample", { data: lifecycle, topic: "lifecycle" }); + } + + if (deleteUnusedKeys(alarm)) { + emit("sample", { data: alarm, topic: "alarm" }); + } + + if (deleteUnusedKeys(def)) { + emit("sample", { data: def, topic: "default" }); + } + + if (decoded.presence !== undefined) { + if (decoded.presence === true) { + emit("sample", { + data: { occupancy: 1, occupied: true }, + topic: "occupancy", + }); + } else { + emit("sample", { + data: { occupancy: 0, occupied: false }, + topic: "occupancy", + }); + } + } +} diff --git a/types/sensative/ms-drip-oil/uplink.spec.js b/types/sensative/ms-drip-oil/uplink.spec.js new file mode 100644 index 00000000..c8334a0b --- /dev/null +++ b/types/sensative/ms-drip-oil/uplink.spec.js @@ -0,0 +1,197 @@ +const chai = require("chai"); + +const rewire = require("rewire"); +const utils = require("test-utils"); + +const { assert } = chai; + +describe("Sensative strip", () => { + let defaultSchema = null; + let consume = null; + before((done) => { + const script = rewire("./uplink.js"); + consume = utils.init(script); + utils + .loadSchema(`${__dirname}/default.schema.json`) + .then((parsedSchema) => { + defaultSchema = parsedSchema; + done(); + }); + }); + + let lifecycleSchema = null; + before((done) => { + utils + .loadSchema(`${__dirname}/lifecycle.schema.json`) + .then((parsedSchema) => { + lifecycleSchema = parsedSchema; + done(); + }); + }); + + let alarmSchema = null; + before((done) => { + utils.loadSchema(`${__dirname}/alarm.schema.json`).then((parsedSchema) => { + alarmSchema = parsedSchema; + done(); + }); + }); + + describe("consume()", () => { + it("should decode the sensative strip payload", () => { + const data = { + data: { + port: 1, + payloadHex: "ffff09010a0052010000", + }, + }; + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "alarm"); + assert.equal(value.data.doorAlarm, false); + + utils.validateSchema(value.data, alarmSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "default"); + assert.equal(value.data.closed, true); + assert.equal(value.data.temperature, 0); + + utils.validateSchema(value.data, defaultSchema, { throwError: true }); + }); + + consume(data); + }); + + it("should decode the sensative strip payload", () => { + const data = { + data: { + port: 1, + payloadHex: "ffff01561100001500", + }, + }; + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "lifecycle"); + assert.equal(value.data.batteryLevel, 86); + + utils.validateSchema(value.data, lifecycleSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "alarm"); + assert.equal(value.data.closeProximityAlarm, false); + + utils.validateSchema(value.data, alarmSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "default"); + assert.equal(value.data.doorCount, 0); + + utils.validateSchema(value.data, defaultSchema, { throwError: true }); + }); + + consume(data); + }); + + it("should decode the sensative strip payload", () => { + const data = { + data: { + port: 1, + payloadHex: "ffff01510900110001", + }, + }; + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "lifecycle"); + assert.equal(value.data.batteryLevel, 81); + + utils.validateSchema(value.data, lifecycleSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "default"); + assert.equal(value.data.closed, false); + assert.equal(value.data.doorCount, 1); + + utils.validateSchema(value.data, defaultSchema, { throwError: true }); + }); + + consume(data); + }); + + it("should decode the sensative strip payload", () => { + const data = { + data: { + port: 1, + payloadHex: "ffff01581100001501", + }, + }; + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "lifecycle"); + assert.equal(value.data.batteryLevel, 88); + + utils.validateSchema(value.data, lifecycleSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "alarm"); + assert.equal(value.data.closeProximityAlarm, true); + + utils.validateSchema(value.data, alarmSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "default"); + assert.equal(value.data.doorCount, 0); + + utils.validateSchema(value.data, defaultSchema, { throwError: true }); + }); + + consume(data); + }); + }); +}); diff --git a/types/sensative/ms-drip/alarm.schema.json b/types/sensative/ms-drip/alarm.schema.json new file mode 100644 index 00000000..60e53b66 --- /dev/null +++ b/types/sensative/ms-drip/alarm.schema.json @@ -0,0 +1,66 @@ +{ + "$id": "https://akenza.io/sensative/ms-drip/alarm.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "processingType": "uplink_decoder", + "topic": "alarm", + "title": "Alarm", + "properties": { + "highAlarm": { + "title": "High alarm", + "description": "Equals to true if the threshold is exceeded", + "type": "boolean" + }, + "lowAlarm": { + "title": "Low alarm", + "description": "Equals to true if the threshold has fallen below", + "type": "boolean" + }, + "doorAlarm": { + "title": "Door alarm", + "description": "Equals to true if there is a door alarm", + "type": "boolean" + }, + "tamperAlarm": { + "title": "Tamper alarm", + "description": "Equals to true if the strip has been tampered with", + "type": "boolean" + }, + "floodAlarm": { + "title": "Flood alarm", + "description": "Equals to true if a flood is detected", + "type": "boolean" + }, + "oilAlarm": { + "title": "Oil alarm", + "description": "Equals to true if oil is detected", + "type": "boolean" + }, + "foilAlarm": { + "title": "Foil alarm", + "description": "Equals to true if a foil is detected", + "type": "boolean" + }, + "userSwitchAlarm": { + "title": "User switch alarm", + "description": "Equals to true if a user switch is detected", + "type": "boolean" + }, + "closeProximityAlarm": { + "title": "Close proximity Alarm", + "description": "Equals to true if an object in close proximity is detected", + "type": "boolean" + }, + "disinfectAlarm": { + "title": "Disinfect alarm", + "description": "Status of the disinfection", + "type": "string", + "enum": [ + "DIRTY", + "OCCUPIED", + "CLEANING", + "CLEAN" + ] + } + } +} \ No newline at end of file diff --git a/types/sensative/ms-drip/default.schema.json b/types/sensative/ms-drip/default.schema.json new file mode 100644 index 00000000..446e09d6 --- /dev/null +++ b/types/sensative/ms-drip/default.schema.json @@ -0,0 +1,71 @@ +{ + "$id": "https://akenza.io/sensative/ms-drip/default.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "processingType": "uplink_decoder", + "topic": "default", + "title": "Default", + "properties": { + "temperature": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/temperature/celsius" + }, + "averageTemperature": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/temperature/celsius", + "description": "Average temperature report in °C.", + "title": "Average temperature report" + }, + "humidity": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/humidity/percent" + }, + "light": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/illuminance/lux" + }, + "light2": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/illuminance/lux" + }, + "closed": { + "title": "Closed", + "description": "Status of the strip. closed = true, open = false", + "type": "boolean" + }, + "tamperReport": { + "title": "Tamper report", + "description": "Status if the device got tampered the strip. open = true, closed = false", + "type": "boolean" + }, + "flood": { + "title": "Flood", + "unit": "%", + "type": "number", + "description": "Flood relative wetness", + "minimum": 0, + "maximum": 100 + }, + "doorCount": { + "title": "Door count", + "type": "integer", + "description": "Door opening count", + "minimum": 0, + "maximum": 65535 + }, + "presence": { + "title": "Presence", + "description": "Status if the device reads presence", + "type": "boolean" + }, + "irProximity": { + "title": "Infrared proximity", + "description": "Count of objects in proximity", + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "irCloseProximity": { + "title": "Infrared close proximity", + "description": "Count of objects in close proximity", + "type": "integer", + "minimum": 0, + "maximum": 255 + } + } +} \ No newline at end of file diff --git a/types/sensative/ms-drip/drip.png b/types/sensative/ms-drip/drip.png new file mode 100644 index 00000000..db4726ad Binary files /dev/null and b/types/sensative/ms-drip/drip.png differ diff --git a/types/sensative/ms-drip/lifecycle.schema.json b/types/sensative/ms-drip/lifecycle.schema.json new file mode 100644 index 00000000..636b8f4c --- /dev/null +++ b/types/sensative/ms-drip/lifecycle.schema.json @@ -0,0 +1,62 @@ +{ + "$id": "https://akenza.io/sensative/ms-drip/lifecycle.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "processingType": "uplink_decoder", + "topic": "lifecycle", + "title": "Lifecycle", + "properties": { + "batteryLevel": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "title": "Battery charge", + "description": "The battery charge in percent.", + "unit": "%" + }, + "error": { + "title": "Error message", + "description": "Error info which contains: wrong length of RX package", + "type": "string", + "hideFromKpis": true + }, + "badConditionsCounter": { + "title": "Bad conditions counter", + "type": "integer", + "description": "Bad conditions counter", + "minimum": 0, + "maximum": 65535, + "hideFromKpis": true + }, + "stackTxFailRebootCount": { + "title": "Stack TX fail reboot count", + "type": "integer", + "description": "Stack TX fail reboot count", + "minimum": 0, + "maximum": 65535, + "hideFromKpis": true + }, + "startupCount": { + "title": "Start up count", + "type": "integer", + "description": "Start up count", + "minimum": 0, + "maximum": 65535, + "hideFromKpis": true + }, + "watchdogCount": { + "title": "Watchdog count", + "type": "integer", + "description": "Watchdog count", + "minimum": 0, + "maximum": 65535, + "hideFromKpis": true + }, + "softwareVersion": { + "title": "Software version", + "type": "string", + "description": "Software version", + "hideFromKpis": true + } + } +} \ No newline at end of file diff --git a/types/sensative/ms-drip/meta.json b/types/sensative/ms-drip/meta.json new file mode 100644 index 00000000..3777148b --- /dev/null +++ b/types/sensative/ms-drip/meta.json @@ -0,0 +1,14 @@ +{ + "name": "Strips Multi-sensor +Drip", + "version": "1.0.0", + "manufacturer": "Sensative", + "url": "https://sensative.com/sensors/strips-sensors-for-lorawan/strips-lorawan-ms-drip/", + "description": "Strips MS +Drip is an innovative ultra-thin (3mm/0.12in) and wireless multi-sensor for LoRaWAN with water leak detection, developed for smart IoT applications such as building monitoring and operations.", + "author": "Akenza AG", + "firmwareVersion": "V1.0.0", + "loraDeviceClass": "A", + "availableSensors": ["Water Leak", "Temperature"], + "outputTopics": ["alarm", "default", "lifecycle", "occupancy"], + "encoding": "HEX", + "connectivity": "LORA" +} diff --git a/types/sensative/ms-drip/occupancy.schema.json b/types/sensative/ms-drip/occupancy.schema.json new file mode 100644 index 00000000..aa6298b2 --- /dev/null +++ b/types/sensative/ms-drip/occupancy.schema.json @@ -0,0 +1,25 @@ +{ + "$id": "https://akenza.io/sensative/ms-drip/occupancy.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "processingType": "uplink_decoder", + "topic": "occupancy", + "title": "Occupancy", + "properties": { + "occupancy": { + "title": "Occupancy", + "description": "Space occupancy. 0 = Unoccupied / 1 = Occupied.", + "type": "integer", + "minimum": 0, + "maximum": 1 + }, + "occupied": { + "title": "Occupied", + "description": "Space occupancy. false = Unoccupied / true = Occupied.", + "type": "boolean" + } + }, + "required": [ + "occupancy" + ] +} \ No newline at end of file diff --git a/types/sensative/ms-drip/uplink.js b/types/sensative/ms-drip/uplink.js new file mode 100644 index 00000000..e1f70ad2 --- /dev/null +++ b/types/sensative/ms-drip/uplink.js @@ -0,0 +1,277 @@ +function decodeFrame(bytes, type, pos) { + const data = {}; + let pointer = pos; + + switch (type & 0x7f) { + case 0: + data.emptyFrame = {}; + break; + case 1: // Battery 1byte 0-100% + data.batteryLevel = bytes[pointer++]; + break; + case 2: // TempReport 2bytes 0.1degree C + // celcius 0.1 precision + data.temperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 3: + // Temp alarm + // sends alarm after >x< + data.highAlarm = !!(bytes[pointer] & 0x01); // boolean + data.lowAlarm = !!(bytes[pointer] & 0x02); // boolean + pointer++; + break; + case 4: // AvgTempReport 2bytes 0.1degree C + data.averageTemperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 5: + // AvgTemp alarm + // sends alarm after >x< + data.highAlarm = !!(bytes[pointer] & 0x01); // boolean + data.lowAlarm = !!(bytes[pointer] & 0x02); // boolean + pointer++; + break; + case 6: // Humidity 1byte 0-100% in 0.5% + data.humidity = bytes[pointer] / 2; // relativeHumidity percent 0,5 + break; + case 7: // Lux 2bytes 0-65535lux + data.light = (bytes[pointer++] << 8) | bytes[pointer++]; // you can the lux range between two sets (lux1 and 2) + break; + case 8: // Lux 2bytes 0-65535lux + data.light2 = (bytes[pointer++] << 8) | bytes[pointer++]; + break; + case 9: // DoorSwitch 1bytes binary + data.closed = !!bytes[pointer++]; // true = door closed, false = door open + break; + case 10: // DoorAlarm 1bytes binary + data.doorAlarm = !!bytes[pointer++]; // boolean true = alarm + break; + case 11: // TamperReport 1bytes binary (was previously TamperSwitch) + data.tamperReport = !!bytes[pointer++]; + break; + case 12: // TamperAlarm 1bytes binary + data.tamperAlarm = !!bytes[pointer++]; + break; + case 13: // Flood 1byte 0-100% + data.flood = bytes[pointer++]; // percentage, relative wetness + break; + case 14: // FloodAlarm 1bytes binary + data.floodAlarm = !!bytes[pointer++]; // boolean, after >x< + break; + case 15: // FoilAlarm 1bytes binary + data.oilAlarm = !!bytes[pointer]; + data.foilAlarm = !!bytes[pointer++]; + break; + case 16: // UserSwitch1Alarm, 1 byte digital + data.userSwitchAlarm = !!bytes[pointer++]; + break; + case 17: // DoorCountReport, 2 byte analog + data.doorCount = (bytes[pointer++] << 8) | bytes[pointer++]; + break; + case 18: // PresenceReport, 1 byte digital + data.presence = !!bytes[pointer++]; + break; + case 19: // IRProximityReport + data.irProximity = (bytes[pointer++] << 8) | bytes[pointer++]; + break; + case 20: // IRCloseProximityReport, low power + data.irCloseProximity = (bytes[pointer++] << 8) | bytes[pointer++]; + break; + case 21: // CloseProximityAlarm, something very close to presence sensor + data.closeProximityAlarm = !!bytes[pointer++]; + break; + case 22: // DisinfectAlarm + data.disinfectAlarm = bytes[pointer++]; + if (data.disinfectAlarm === 0) data.disinfectAlarm = "DIRTY"; + if (data.disinfectAlarm === 1) data.disinfectAlarm = "OCCUPIED"; + if (data.disinfectAlarm === 2) data.disinfectAlarm = "CLEANING"; + if (data.disinfectAlarm === 3) data.disinfectAlarm = "CLEAN"; + break; + case 80: + data.humidity = bytes[pointer++] / 2; + data.temperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 81: + data.humidity = bytes[pointer++] / 2; + data.averageTemperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 82: + data.closed = !!bytes[pointer++]; // true = door open, false = door closed // Inverted in example decoder + data.temperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 112: // Capacitance Raw Sensor Value 2bytes 0-65535 + data.capacitanceFlood = (bytes[pointer++] << 8) | bytes[pointer++]; // should never trigger anymore + break; + case 113: // Capacitance Raw Sensor Value 2bytes 0-65535 + data.capacitancePad = (bytes[pointer++] << 8) | bytes[pointer++]; // should never trigger anymore + break; + case 110: { + const number = + ((bytes[pointer++] << 24) | + (bytes[pointer++] << 16) | + (bytes[pointer++] << 8) | + bytes[pointer++]) >>> + 0; + data.softwareVersion = number.toString(16); + data.startupCount = bytes[pointer++]; + data.watchdogCount = bytes[pointer++]; + data.stackTxFailRebootCount = bytes[pointer++]; + data.badConditionsCounter = bytes[pointer++]; + break; + } + case 114: // Capacitance Raw Sensor Value 2bytes 0-65535 + data.capacitanceEnd = (bytes[pointer++] << 8) | bytes[pointer++]; // should never trigger anymore + break; + default: + break; + } + return { data, pointer }; +} + +function deleteUnusedKeys(data) { + let keysRetained = false; + Object.keys(data).forEach((key) => { + if (data[key] === undefined) { + delete data[key]; + } else { + keysRetained = true; + } + }); + return keysRetained; +} + +function consume(event) { + const payload = event.data.payloadHex; + const { port } = event.data; + const bytes = Hex.hexToBytes(payload); + + const decoded = {}; + let pos = 2; + let type; + + switch (port) { + case 1: + if (bytes.length < 2) { + emit("log", { error: "Wrong length of RX package" }); + break; + } + while (pos < bytes.length) { + type = bytes[pos++]; + const decodedFrame = decodeFrame(bytes, type, pos); + pos = decodedFrame.pointer; + Object.assign(decoded, decodedFrame.data); + } + break; + + case 2: { + const now = new Date(); + decoded.history = {}; + if (bytes.length < 2) { + decoded.history.error = "Wrong length of RX package"; + break; + } + let seqNr = (bytes[pos++] << 8) | bytes[pos++]; + while (pos < bytes.length) { + decoded.history[seqNr] = {}; + decoded.history.now = now.toUTCString(); + const secondsAgo = + (bytes[pos++] << 24) | + (bytes[pos++] << 16) | + (bytes[pos++] << 8) | + bytes[pos++]; + decoded.history[seqNr].timeStamp = new Date( + now.getTime() - secondsAgo * 1000, + ).toUTCString(); + type = bytes[pos++]; + const decodedFrame = decodeFrame(bytes, type, pos); + pos = decodedFrame.pointer; + Object.assign(decoded, decodedFrame.data); + seqNr++; + } + break; + } + default: + break; + } + + const def = {}; + def.temperature = decoded.temperature; + def.averageTemperature = decoded.averageTemperature; + def.humidity = decoded.humidity; + def.light = decoded.light; + def.light2 = decoded.light2; + def.closed = decoded.closed; + def.tamperReport = decoded.tamperReport; + def.doorCount = decoded.doorCount; + def.presence = decoded.presence; + def.irProximity = decoded.irProximity; + def.irCloseProximity = decoded.irCloseProximity; + + const alarm = {}; + alarm.highAlarm = decoded.highAlarm; + alarm.lowAlarm = decoded.lowAlarm; + alarm.doorAlarm = decoded.doorAlarm; + alarm.tamperAlarm = decoded.tamperAlarm; + alarm.floodAlarm = decoded.floodAlarm; + alarm.oilAlarm = decoded.oilAlarm; + alarm.foilAlarm = decoded.foilAlarm; + alarm.userSwitchAlarm = decoded.userSwitchAlarm; + alarm.closeProximityAlarm = decoded.closeProximityAlarm; + alarm.disinfectAlarm = decoded.disinfectAlarm; + + const lifecycle = {}; + if (decoded.batteryLevel !== 0) { + lifecycle.batteryLevel = decoded.batteryLevel; + } + lifecycle.error = decoded.error; + lifecycle.softwareVersion = decoded.softwareVersion; + lifecycle.startupCount = decoded.startupCount; + lifecycle.watchdogCount = decoded.watchdogCount; + lifecycle.stackTxFailRebootCount = decoded.stackTxFailRebootCount; + lifecycle.badConditionsCounter = decoded.badConditionsCounter; + + if (deleteUnusedKeys(lifecycle)) { + emit("sample", { data: lifecycle, topic: "lifecycle" }); + } + + if (deleteUnusedKeys(alarm)) { + emit("sample", { data: alarm, topic: "alarm" }); + } + + if (deleteUnusedKeys(def)) { + emit("sample", { data: def, topic: "default" }); + } + + if (decoded.presence !== undefined) { + if (decoded.presence === true) { + emit("sample", { + data: { occupancy: 1, occupied: true }, + topic: "occupancy", + }); + } else { + emit("sample", { + data: { occupancy: 0, occupied: false }, + topic: "occupancy", + }); + } + } +} diff --git a/types/sensative/ms-drip/uplink.spec.js b/types/sensative/ms-drip/uplink.spec.js new file mode 100644 index 00000000..c8334a0b --- /dev/null +++ b/types/sensative/ms-drip/uplink.spec.js @@ -0,0 +1,197 @@ +const chai = require("chai"); + +const rewire = require("rewire"); +const utils = require("test-utils"); + +const { assert } = chai; + +describe("Sensative strip", () => { + let defaultSchema = null; + let consume = null; + before((done) => { + const script = rewire("./uplink.js"); + consume = utils.init(script); + utils + .loadSchema(`${__dirname}/default.schema.json`) + .then((parsedSchema) => { + defaultSchema = parsedSchema; + done(); + }); + }); + + let lifecycleSchema = null; + before((done) => { + utils + .loadSchema(`${__dirname}/lifecycle.schema.json`) + .then((parsedSchema) => { + lifecycleSchema = parsedSchema; + done(); + }); + }); + + let alarmSchema = null; + before((done) => { + utils.loadSchema(`${__dirname}/alarm.schema.json`).then((parsedSchema) => { + alarmSchema = parsedSchema; + done(); + }); + }); + + describe("consume()", () => { + it("should decode the sensative strip payload", () => { + const data = { + data: { + port: 1, + payloadHex: "ffff09010a0052010000", + }, + }; + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "alarm"); + assert.equal(value.data.doorAlarm, false); + + utils.validateSchema(value.data, alarmSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "default"); + assert.equal(value.data.closed, true); + assert.equal(value.data.temperature, 0); + + utils.validateSchema(value.data, defaultSchema, { throwError: true }); + }); + + consume(data); + }); + + it("should decode the sensative strip payload", () => { + const data = { + data: { + port: 1, + payloadHex: "ffff01561100001500", + }, + }; + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "lifecycle"); + assert.equal(value.data.batteryLevel, 86); + + utils.validateSchema(value.data, lifecycleSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "alarm"); + assert.equal(value.data.closeProximityAlarm, false); + + utils.validateSchema(value.data, alarmSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "default"); + assert.equal(value.data.doorCount, 0); + + utils.validateSchema(value.data, defaultSchema, { throwError: true }); + }); + + consume(data); + }); + + it("should decode the sensative strip payload", () => { + const data = { + data: { + port: 1, + payloadHex: "ffff01510900110001", + }, + }; + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "lifecycle"); + assert.equal(value.data.batteryLevel, 81); + + utils.validateSchema(value.data, lifecycleSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "default"); + assert.equal(value.data.closed, false); + assert.equal(value.data.doorCount, 1); + + utils.validateSchema(value.data, defaultSchema, { throwError: true }); + }); + + consume(data); + }); + + it("should decode the sensative strip payload", () => { + const data = { + data: { + port: 1, + payloadHex: "ffff01581100001501", + }, + }; + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "lifecycle"); + assert.equal(value.data.batteryLevel, 88); + + utils.validateSchema(value.data, lifecycleSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "alarm"); + assert.equal(value.data.closeProximityAlarm, true); + + utils.validateSchema(value.data, alarmSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "default"); + assert.equal(value.data.doorCount, 0); + + utils.validateSchema(value.data, defaultSchema, { throwError: true }); + }); + + consume(data); + }); + }); +}); diff --git a/types/sensative/ms-guard/alarm.schema.json b/types/sensative/ms-guard/alarm.schema.json index 812bcfc3..9bc74191 100644 --- a/types/sensative/ms-guard/alarm.schema.json +++ b/types/sensative/ms-guard/alarm.schema.json @@ -31,6 +31,11 @@ "description": "Equals to true if a flood is detected", "type": "boolean" }, + "oilAlarm": { + "title": "Oil alarm", + "description": "Equals to true if oil is detected", + "type": "boolean" + }, "foilAlarm": { "title": "Foil alarm", "description": "Equals to true if a foil is detected", diff --git a/types/sensative/ms-guard/lifecycle.schema.json b/types/sensative/ms-guard/lifecycle.schema.json index d800e537..76426c87 100644 --- a/types/sensative/ms-guard/lifecycle.schema.json +++ b/types/sensative/ms-guard/lifecycle.schema.json @@ -15,22 +15,46 @@ "type": "string", "hideFromKpis": true }, - "historySeqNr": { - "title": "History sequence number", + "badConditionsCounter": { + "title": "Bad conditions counter", "type": "integer", - "description": "History sequence number", + "description": "Bad conditions counter", "minimum": 0, "maximum": 65535, "hideFromKpis": true }, - "prevHistSeqNr": { - "title": "Previous history sequence number", + "stackTxFailRebootCount": { + "title": "Stack TX fail reboot count", "type": "integer", - "description": "Previous history sequence number", + "description": "Stack TX fail reboot count", "minimum": 0, "maximum": 65535, "hideFromKpis": true + }, + "startupCount": { + "title": "Start up count", + "type": "integer", + "description": "Start up count", + "minimum": 0, + "maximum": 65535, + "hideFromKpis": true + }, + "watchdogCount": { + "title": "Watchdog count", + "type": "integer", + "description": "Watchdog count", + "minimum": 0, + "maximum": 65535, + "hideFromKpis": true + }, + "softwareVersion": { + "title": "Software version", + "type": "string", + "description": "Software version", + "hideFromKpis": true } }, - "required": ["batteryLevel", "historySeqNr", "prevHistSeqNr"] -} + "required": [ + "batteryLevel" + ] +} \ No newline at end of file diff --git a/types/sensative/ms-guard/meta.json b/types/sensative/ms-guard/meta.json index 6cdfb753..cd99c88a 100644 --- a/types/sensative/ms-guard/meta.json +++ b/types/sensative/ms-guard/meta.json @@ -2,7 +2,7 @@ "name": "Strips Multi-sensor +Guard", "version": "1.0.0", "manufacturer": "Sensative", - "url": "https://sensative.com/sensors/strips-lorawan-sensors/strips-multi-sensor-guard-for-lorawan/", + "url": "https://sensative.com/sensors/strips-sensors-for-lorawan/strips-multi-sensor-guard-for-lorawan/", "description": "Strips +Guard is a magnet sensor for monitoring windows, doors, and valuables. Strips +Guard can also be used for temperature measurements.", "author": "Akenza AG", "firmwareVersion": "V1.0.0", diff --git a/types/sensative/ms-guard/uplink.js b/types/sensative/ms-guard/uplink.js index 56e99216..e1f70ad2 100644 --- a/types/sensative/ms-guard/uplink.js +++ b/types/sensative/ms-guard/uplink.js @@ -1,145 +1,150 @@ -function parseHexString(str) { - const result = []; - while (str.length >= 2) { - result.push(parseInt(str.substring(0, 2), 16)); +function decodeFrame(bytes, type, pos) { + const data = {}; + let pointer = pos; - str = str.substring(2, str.length); - } - - return result; -} - -function decodeFrame(bytes, type, target, pos) { switch (type & 0x7f) { case 0: - target.emptyFrame = {}; + data.emptyFrame = {}; break; case 1: // Battery 1byte 0-100% - target.batteryLevel = bytes[pos++]; + data.batteryLevel = bytes[pointer++]; break; case 2: // TempReport 2bytes 0.1degree C // celcius 0.1 precision - target.temperature = - ((bytes[pos] & 0x80 ? 0xffff << 16 : 0) | - (bytes[pos++] << 8) | - bytes[pos++]) / + data.temperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / 10; break; case 3: // Temp alarm // sends alarm after >x< - target.highAlarm = !!(bytes[pos] & 0x01); // boolean - target.lowAlarm = !!(bytes[pos] & 0x02); // boolean - pos++; + data.highAlarm = !!(bytes[pointer] & 0x01); // boolean + data.lowAlarm = !!(bytes[pointer] & 0x02); // boolean + pointer++; break; case 4: // AvgTempReport 2bytes 0.1degree C - target.averageTemperature = - ((bytes[pos] & 0x80 ? 0xffff << 16 : 0) | - (bytes[pos++] << 8) | - bytes[pos++]) / + data.averageTemperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / 10; break; case 5: // AvgTemp alarm // sends alarm after >x< - target.highAlarm = !!(bytes[pos] & 0x01); // boolean - target.lowAlarm = !!(bytes[pos] & 0x02); // boolean - pos++; + data.highAlarm = !!(bytes[pointer] & 0x01); // boolean + data.lowAlarm = !!(bytes[pointer] & 0x02); // boolean + pointer++; break; case 6: // Humidity 1byte 0-100% in 0.5% - target.humidity = bytes[pos++] / 2; // relativeHumidity percent 0,5 + data.humidity = bytes[pointer] / 2; // relativeHumidity percent 0,5 break; case 7: // Lux 2bytes 0-65535lux - target.light = (bytes[pos++] << 8) | bytes[pos++]; // you can the lux range between two sets (lux1 and 2) + data.light = (bytes[pointer++] << 8) | bytes[pointer++]; // you can the lux range between two sets (lux1 and 2) break; case 8: // Lux 2bytes 0-65535lux - target.light2 = (bytes[pos++] << 8) | bytes[pos++]; + data.light2 = (bytes[pointer++] << 8) | bytes[pointer++]; break; case 9: // DoorSwitch 1bytes binary - target.closed = !bytes[pos++]; // true = door closed, false = door open + data.closed = !!bytes[pointer++]; // true = door closed, false = door open break; case 10: // DoorAlarm 1bytes binary - target.doorAlarm = !bytes[pos++]; // boolean true = alarm + data.doorAlarm = !!bytes[pointer++]; // boolean true = alarm break; case 11: // TamperReport 1bytes binary (was previously TamperSwitch) - target.tamperReport = !!bytes[pos++]; + data.tamperReport = !!bytes[pointer++]; break; case 12: // TamperAlarm 1bytes binary - target.tamperAlarm = !!bytes[pos++]; + data.tamperAlarm = !!bytes[pointer++]; break; case 13: // Flood 1byte 0-100% - target.flood = bytes[pos++]; // percentage, relative wetness + data.flood = bytes[pointer++]; // percentage, relative wetness break; case 14: // FloodAlarm 1bytes binary - target.floodAlarm = !!bytes[pos++]; // boolean, after >x< + data.floodAlarm = !!bytes[pointer++]; // boolean, after >x< break; case 15: // FoilAlarm 1bytes binary - target.foilAlarm = !!bytes[pos++]; + data.oilAlarm = !!bytes[pointer]; + data.foilAlarm = !!bytes[pointer++]; break; case 16: // UserSwitch1Alarm, 1 byte digital - target.userSwitchAlarm = !!bytes[pos++]; + data.userSwitchAlarm = !!bytes[pointer++]; break; case 17: // DoorCountReport, 2 byte analog - target.doorCount = (bytes[pos++] << 8) | bytes[pos++]; + data.doorCount = (bytes[pointer++] << 8) | bytes[pointer++]; break; case 18: // PresenceReport, 1 byte digital - target.presence = !!bytes[pos++]; + data.presence = !!bytes[pointer++]; break; case 19: // IRProximityReport - target.irProximity = (bytes[pos++] << 8) | bytes[pos++]; + data.irProximity = (bytes[pointer++] << 8) | bytes[pointer++]; break; case 20: // IRCloseProximityReport, low power - target.irCloseProximity = (bytes[pos++] << 8) | bytes[pos++]; + data.irCloseProximity = (bytes[pointer++] << 8) | bytes[pointer++]; break; case 21: // CloseProximityAlarm, something very close to presence sensor - target.closeProximityAlarm = !!bytes[pos++]; + data.closeProximityAlarm = !!bytes[pointer++]; break; case 22: // DisinfectAlarm - target.disinfectAlarm = bytes[pos++]; - if (target.disinfectAlarm === 0) target.disinfectAlarm = "DIRTY"; - if (target.disinfectAlarm === 1) target.disinfectAlarm = "OCCUPIED"; - if (target.disinfectAlarm === 2) target.disinfectAlarm = "CLEANING"; - if (target.disinfectAlarm === 3) target.disinfectAlarm = "CLEAN"; + data.disinfectAlarm = bytes[pointer++]; + if (data.disinfectAlarm === 0) data.disinfectAlarm = "DIRTY"; + if (data.disinfectAlarm === 1) data.disinfectAlarm = "OCCUPIED"; + if (data.disinfectAlarm === 2) data.disinfectAlarm = "CLEANING"; + if (data.disinfectAlarm === 3) data.disinfectAlarm = "CLEAN"; break; case 80: - target.humidity = bytes[pos++] / 2; - target.temperature = - ((bytes[pos] & 0x80 ? 0xffff << 16 : 0) | - (bytes[pos++] << 8) | - bytes[pos++]) / + data.humidity = bytes[pointer++] / 2; + data.temperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / 10; break; case 81: - target.humidity = bytes[pos++] / 2; - target.averageTemperature = - ((bytes[pos] & 0x80 ? 0xffff << 16 : 0) | - (bytes[pos++] << 8) | - bytes[pos++]) / + data.humidity = bytes[pointer++] / 2; + data.averageTemperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / 10; break; case 82: - target.closed = !bytes[pos++]; // true = door closed, false = door open - target.temperature = - ((bytes[pos] & 0x80 ? 0xffff << 16 : 0) | - (bytes[pos++] << 8) | - bytes[pos++]) / + data.closed = !!bytes[pointer++]; // true = door open, false = door closed // Inverted in example decoder + data.temperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / 10; break; case 112: // Capacitance Raw Sensor Value 2bytes 0-65535 - target.capacitanceFlood = (bytes[pos++] << 8) | bytes[pos++]; // should never trigger anymore + data.capacitanceFlood = (bytes[pointer++] << 8) | bytes[pointer++]; // should never trigger anymore break; case 113: // Capacitance Raw Sensor Value 2bytes 0-65535 - target.capacitancePad = (bytes[pos++] << 8) | bytes[pos++]; // should never trigger anymore - break; - case 110: - pos += 8; + data.capacitancePad = (bytes[pointer++] << 8) | bytes[pointer++]; // should never trigger anymore + break; + case 110: { + const number = + ((bytes[pointer++] << 24) | + (bytes[pointer++] << 16) | + (bytes[pointer++] << 8) | + bytes[pointer++]) >>> + 0; + data.softwareVersion = number.toString(16); + data.startupCount = bytes[pointer++]; + data.watchdogCount = bytes[pointer++]; + data.stackTxFailRebootCount = bytes[pointer++]; + data.badConditionsCounter = bytes[pointer++]; break; + } case 114: // Capacitance Raw Sensor Value 2bytes 0-65535 - target.capacitanceEnd = (bytes[pos++] << 8) | bytes[pos++]; // should never trigger anymore + data.capacitanceEnd = (bytes[pointer++] << 8) | bytes[pointer++]; // should never trigger anymore break; default: break; } + return { data, pointer }; } function deleteUnusedKeys(data) { @@ -157,10 +162,10 @@ function deleteUnusedKeys(data) { function consume(event) { const payload = event.data.payloadHex; const { port } = event.data; - const bytes = parseHexString(payload); + const bytes = Hex.hexToBytes(payload); const decoded = {}; - let pos = 0; + let pos = 2; let type; switch (port) { @@ -169,27 +174,26 @@ function consume(event) { emit("log", { error: "Wrong length of RX package" }); break; } - decoded.historySeqNr = (bytes[pos++] << 8) | bytes[pos++]; - decoded.prevHistSeqNr = decoded.historySeqNr; while (pos < bytes.length) { type = bytes[pos++]; - if (type & 0x80) decoded.prevHistSeqNr--; - decodeFrame(bytes, type, decoded, pos); + const decodedFrame = decodeFrame(bytes, type, pos); + pos = decodedFrame.pointer; + Object.assign(decoded, decodedFrame.data); } break; - case 2: - var now = new Date(); + case 2: { + const now = new Date(); decoded.history = {}; if (bytes.length < 2) { decoded.history.error = "Wrong length of RX package"; break; } - var seqNr = (bytes[pos++] << 8) | bytes[pos++]; + let seqNr = (bytes[pos++] << 8) | bytes[pos++]; while (pos < bytes.length) { decoded.history[seqNr] = {}; decoded.history.now = now.toUTCString(); - secondsAgo = + const secondsAgo = (bytes[pos++] << 24) | (bytes[pos++] << 16) | (bytes[pos++] << 8) | @@ -198,11 +202,13 @@ function consume(event) { now.getTime() - secondsAgo * 1000, ).toUTCString(); type = bytes[pos++]; - decodeFrame(bytes, type, decoded.history[seqNr], pos); + const decodedFrame = decodeFrame(bytes, type, pos); + pos = decodedFrame.pointer; + Object.assign(decoded, decodedFrame.data); seqNr++; } break; - + } default: break; } @@ -226,16 +232,22 @@ function consume(event) { alarm.doorAlarm = decoded.doorAlarm; alarm.tamperAlarm = decoded.tamperAlarm; alarm.floodAlarm = decoded.floodAlarm; + alarm.oilAlarm = decoded.oilAlarm; alarm.foilAlarm = decoded.foilAlarm; alarm.userSwitchAlarm = decoded.userSwitchAlarm; alarm.closeProximityAlarm = decoded.closeProximityAlarm; alarm.disinfectAlarm = decoded.disinfectAlarm; const lifecycle = {}; - lifecycle.batteryLevel = decoded.batteryLevel; + if (decoded.batteryLevel !== 0) { + lifecycle.batteryLevel = decoded.batteryLevel; + } lifecycle.error = decoded.error; - lifecycle.historySeqNr = decoded.historySeqNr; - lifecycle.prevHistSeqNr = decoded.prevHistSeqNr; + lifecycle.softwareVersion = decoded.softwareVersion; + lifecycle.startupCount = decoded.startupCount; + lifecycle.watchdogCount = decoded.watchdogCount; + lifecycle.stackTxFailRebootCount = decoded.stackTxFailRebootCount; + lifecycle.badConditionsCounter = decoded.badConditionsCounter; if (deleteUnusedKeys(lifecycle)) { emit("sample", { data: lifecycle, topic: "lifecycle" }); diff --git a/types/sensative/ms-guard/uplink.spec.js b/types/sensative/ms-guard/uplink.spec.js index 7d30cf21..c8334a0b 100644 --- a/types/sensative/ms-guard/uplink.spec.js +++ b/types/sensative/ms-guard/uplink.spec.js @@ -46,14 +46,47 @@ describe("Sensative strip", () => { }, }; + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "alarm"); + assert.equal(value.data.doorAlarm, false); + + utils.validateSchema(value.data, alarmSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "default"); + assert.equal(value.data.closed, true); + assert.equal(value.data.temperature, 0); + + utils.validateSchema(value.data, defaultSchema, { throwError: true }); + }); + + consume(data); + }); + + it("should decode the sensative strip payload", () => { + const data = { + data: { + port: 1, + payloadHex: "ffff01561100001500", + }, + }; + utils.expectEmits((type, value) => { assert.equal(type, "sample"); assert.isNotNull(value); assert.typeOf(value.data, "object"); assert.equal(value.topic, "lifecycle"); - assert.equal(value.data.historySeqNr, 65535); - assert.equal(value.data.prevHistSeqNr, 65535); + assert.equal(value.data.batteryLevel, 86); utils.validateSchema(value.data, lifecycleSchema, { throwError: true }); }); @@ -64,7 +97,7 @@ describe("Sensative strip", () => { assert.typeOf(value.data, "object"); assert.equal(value.topic, "alarm"); - assert.equal(value.data.doorAlarm, true); + assert.equal(value.data.closeProximityAlarm, false); utils.validateSchema(value.data, alarmSchema, { throwError: true }); }); @@ -75,8 +108,85 @@ describe("Sensative strip", () => { assert.typeOf(value.data, "object"); assert.equal(value.topic, "default"); - assert.equal(value.data.temperature, 0); + assert.equal(value.data.doorCount, 0); + + utils.validateSchema(value.data, defaultSchema, { throwError: true }); + }); + + consume(data); + }); + + it("should decode the sensative strip payload", () => { + const data = { + data: { + port: 1, + payloadHex: "ffff01510900110001", + }, + }; + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "lifecycle"); + assert.equal(value.data.batteryLevel, 81); + + utils.validateSchema(value.data, lifecycleSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "default"); assert.equal(value.data.closed, false); + assert.equal(value.data.doorCount, 1); + + utils.validateSchema(value.data, defaultSchema, { throwError: true }); + }); + + consume(data); + }); + + it("should decode the sensative strip payload", () => { + const data = { + data: { + port: 1, + payloadHex: "ffff01581100001501", + }, + }; + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "lifecycle"); + assert.equal(value.data.batteryLevel, 88); + + utils.validateSchema(value.data, lifecycleSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "alarm"); + assert.equal(value.data.closeProximityAlarm, true); + + utils.validateSchema(value.data, alarmSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "default"); + assert.equal(value.data.doorCount, 0); utils.validateSchema(value.data, defaultSchema, { throwError: true }); }); diff --git a/types/sensative/presence/alarm.schema.json b/types/sensative/presence/alarm.schema.json index f0aeda90..ba8638dd 100644 --- a/types/sensative/presence/alarm.schema.json +++ b/types/sensative/presence/alarm.schema.json @@ -31,6 +31,11 @@ "description": "Equals to true if a flood is detected", "type": "boolean" }, + "oilAlarm": { + "title": "Oil alarm", + "description": "Equals to true if oil is detected", + "type": "boolean" + }, "foilAlarm": { "title": "Foil alarm", "description": "Equals to true if a foil is detected", @@ -50,7 +55,12 @@ "title": "Disinfect alarm", "description": "Status of the disinfection", "type": "string", - "enum": ["DIRTY", "OCCUPIED", "CLEANING", "CLEAN"] + "enum": [ + "DIRTY", + "OCCUPIED", + "CLEANING", + "CLEAN" + ] } } -} +} \ No newline at end of file diff --git a/types/sensative/presence/default.schema.json b/types/sensative/presence/default.schema.json index 8340ebe7..35cd28c0 100644 --- a/types/sensative/presence/default.schema.json +++ b/types/sensative/presence/default.schema.json @@ -28,6 +28,11 @@ "description": "Status of the strip. open = true, closed = false", "type": "boolean" }, + "closed": { + "title": "Closed", + "description": "Status of the strip. closed = true, open = false", + "type": "boolean" + }, "tamperReport": { "title": "Tamper report", "description": "Status if the device got tampered the strip. open = true, closed = false", @@ -68,4 +73,4 @@ "maximum": 255 } } -} +} \ No newline at end of file diff --git a/types/sensative/presence/lifecycle.schema.json b/types/sensative/presence/lifecycle.schema.json index 66a364c1..f0eae10b 100644 --- a/types/sensative/presence/lifecycle.schema.json +++ b/types/sensative/presence/lifecycle.schema.json @@ -31,7 +31,7 @@ "maximum": 65535, "hideFromKpis": true }, - "startUpCount": { + "startupCount": { "title": "Start up count", "type": "integer", "description": "Start up count", @@ -54,4 +54,4 @@ "hideFromKpis": true } } -} +} \ No newline at end of file diff --git a/types/sensative/presence/meta.json b/types/sensative/presence/meta.json index 60275f19..272df241 100644 --- a/types/sensative/presence/meta.json +++ b/types/sensative/presence/meta.json @@ -2,8 +2,8 @@ "name": "Strips Presence", "version": "1.0.0", "manufacturer": "Sensative", - "url": "https://sensative.com/sensors/strips-lorawan-sensors/strips-presence-lorawan/", - "description": "Active IR occupancy and magnetic sensor for smart offices and buildings. Strips Presence can also be used for asset monitoring and optional as button or switch.", + "url": "https://sensative.com/sensors/strips-sensors-for-lorawan/strips-presence-lorawan/", + "description": "Active IR occupancy and magnetic sensor for smart offices and buildings. Strips Presence can also be used for asset monitoring and optional as button or switch.", "author": "Akenza AG", "firmwareVersion": "V1.0.0", "loraDeviceClass": "A", diff --git a/types/sensative/presence/uplink.js b/types/sensative/presence/uplink.js index 9ee36fa2..8e92697a 100644 --- a/types/sensative/presence/uplink.js +++ b/types/sensative/presence/uplink.js @@ -1,183 +1,195 @@ -function decode(bytes, port) { - // Decode an uplink message from a buffer - // (array) of bytes to an object of fields. +function decodeFrame(bytes, type, pos) { + const data = {}; + let pointer = pos; - const decodeFrame = (type, target) => { - switch (type & 0x7f) { - case 0: - target.emptyFrame = {}; - break; - case 1: // Battery 1byte 0-100% - target.batteryLevel = bytes[pos++]; - break; - case 2: // TempReport 2bytes 0.1degree C - target.temperature = - ((bytes[pos] & 0x80 ? 0xffff << 16 : 0) | - (bytes[pos++] << 8) | - bytes[pos++]) / - 10; - break; - case 3: - // Temp alarm - target.highAlarm = !!(bytes[pos] & 0x01); // boolean - target.lowAlarm = !!(bytes[pos] & 0x02); // boolean - pos++; - break; - case 4: // AvgTempReport 2bytes 0.1degree C - target.averageTemperature = - ((bytes[pos] & 0x80 ? 0xffff << 16 : 0) | - (bytes[pos++] << 8) | - bytes[pos++]) / - 10; - break; - case 5: - // AvgTemp alarm - target.highAlarm = !!(bytes[pos] & 0x01); // boolean - target.lowAlarm = !!(bytes[pos] & 0x02); // boolean - pos++; - break; - case 6: // Humidity 1byte 0-100% in 0.5% - target.humidity = bytes[pos++] / 2; // relativeHumidity percent 0,5 - break; - case 7: // Lux 2bytes 0-65535lux - target.lux = (bytes[pos++] << 8) | bytes[pos++]; // you can the lux range between two sets (lux1 and 2) - break; - case 8: // Lux 2bytes 0-65535lux - target.lux2 = (bytes[pos++] << 8) | bytes[pos++]; - break; - case 9: // DoorSwitch 1bytes binary - target.open = !!bytes[pos++]; // true = door open, false = door closed - break; - case 10: // DoorAlarm 1bytes binary - target.doorAlarm = !!bytes[pos++]; // boolean true = alarm - break; - case 11: // TamperReport 1bytes binary - target.tamperReport = !!bytes[pos++]; // should never trigger anymore - break; - case 12: // TamperAlarm 1bytes binary - target.tamperAlarm = !!bytes[pos++]; // should never trigger anymore - break; - case 13: // Flood 1byte 0-100% - target.flood = bytes[pos++]; // percentage, relative wetness - break; - case 14: // FloodAlarm 1bytes binary - target.floodAlarm = {}; - target.floodAlarm = !!bytes[pos++]; // boolean, after >x< - break; - case 15: // oilAlarm 1bytes analog - target.oilAlarm = bytes[pos++]; - target.foilAlarm = !!bytes[pos++]; - break; - case 16: // UserSwitch1Alarm, 1 byte digital - target.userSwitch1Alarm = !!bytes[pos++]; - break; - case 17: // DoorCountReport, 2 byte analog - target.doorCount = (bytes[pos++] << 8) | bytes[pos++]; - break; - case 18: // PresenceReport, 1 byte digital - target.presence = !!bytes[pos++]; - break; - case 19: // IRProximityReport - target.IRproximity = (bytes[pos++] << 8) | bytes[pos++]; - break; - case 20: // IRCloseProximityReport, low power - target.IRcloseproximity = (bytes[pos++] << 8) | bytes[pos++]; - break; - case 21: // CloseProximityAlarm, something very close to presence sensor - target.closeProximityAlarm = !!bytes[pos++]; - break; - case 22: // DisinfectAlarm - target.disinfectAlarm = bytes[pos++]; - if (target.disinfectAlarm === 0) { - target.disinfectAlarm.state = "DIRTY"; - } - if (target.disinfectAlarm === 1) { - target.disinfectAlarm.state = "OCCUPIED"; - } - if (target.disinfectAlarm === 2) { - target.disinfectAlarm.state = "CLEANING"; - } - if (target.disinfectAlarm === 3) { - target.disinfectAlarm.state = "CLEAN"; - } - break; - case 80: - target.humidity = bytes[pos++] / 2; - target.temperature = - ((bytes[pos] & 0x80 ? 0xffff << 16 : 0) | - (bytes[pos++] << 8) | - bytes[pos++]) / - 10; - break; - case 81: - target.humidity = bytes[pos++] / 2; - target.averageTemperature = - ((bytes[pos] & 0x80 ? 0xffff << 16 : 0) | - (bytes[pos++] << 8) | - bytes[pos++]) / - 10; - break; - case 82: - target.open = !!bytes[pos++]; // true = door open, false = door closed - target.temperature = - ((bytes[pos] & 0x80 ? 0xffff << 16 : 0) | - (bytes[pos++] << 8) | - bytes[pos++]) / - 10; - break; - case 112: // Capacitance Raw Sensor Value 2bytes 0-65535 - target.capacitanceFlood = (bytes[pos++] << 8) | bytes[pos++]; // should never trigger anymore - break; - case 113: // Capacitance Raw Sensor Value 2bytes 0-65535 - target.capacitancePad = (bytes[pos++] << 8) | bytes[pos++]; // should never trigger anymore - break; - case 110: - const number = - ((bytes[pos++] << 24) | - (bytes[pos++] << 16) | - (bytes[pos++] << 8) | - bytes[pos++]) >>> - 0; - target.softwareVersion = number.toString(16); - target.startUpCount = bytes[pos++]; - target.watchdogCount = bytes[pos++]; - target.stackTxFailRebootCount = bytes[pos++]; - target.badConditionsCounter = bytes[pos++]; - break; - case 114: // Capacitance Raw Sensor Value 2bytes 0-65535 - target.capacitanceEnd = (bytes[pos++] << 8) | bytes[pos++]; // should never trigger anymore - break; - default: + switch (type & 0x7f) { + case 0: + data.emptyFrame = {}; + break; + case 1: // Battery 1byte 0-100% + data.batteryLevel = bytes[pointer++]; + break; + case 2: // TempReport 2bytes 0.1degree C + // celcius 0.1 precision + data.temperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 3: + // Temp alarm + // sends alarm after >x< + data.highAlarm = !!(bytes[pointer] & 0x01); // boolean + data.lowAlarm = !!(bytes[pointer] & 0x02); // boolean + pointer++; + break; + case 4: // AvgTempReport 2bytes 0.1degree C + data.averageTemperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 5: + // AvgTemp alarm + // sends alarm after >x< + data.highAlarm = !!(bytes[pointer] & 0x01); // boolean + data.lowAlarm = !!(bytes[pointer] & 0x02); // boolean + pointer++; + break; + case 6: // Humidity 1byte 0-100% in 0.5% + data.humidity = bytes[pointer] / 2; // relativeHumidity percent 0,5 + break; + case 7: // Lux 2bytes 0-65535lux + data.light = (bytes[pointer++] << 8) | bytes[pointer++]; // you can the lux range between two sets (lux1 and 2) + break; + case 8: // Lux 2bytes 0-65535lux + data.light2 = (bytes[pointer++] << 8) | bytes[pointer++]; + break; + case 9: // DoorSwitch 1bytes binary + data.closed = !!bytes[pointer++]; // true = door closed, false = door open + break; + case 10: // DoorAlarm 1bytes binary + data.doorAlarm = !!bytes[pointer++]; // boolean true = alarm + break; + case 11: // TamperReport 1bytes binary (was previously TamperSwitch) + data.tamperReport = !!bytes[pointer++]; + break; + case 12: // TamperAlarm 1bytes binary + data.tamperAlarm = !!bytes[pointer++]; + break; + case 13: // Flood 1byte 0-100% + data.flood = bytes[pointer++]; // percentage, relative wetness + break; + case 14: // FloodAlarm 1bytes binary + data.floodAlarm = !!bytes[pointer++]; // boolean, after >x< + break; + case 15: // FoilAlarm 1bytes binary + data.oilAlarm = !!bytes[pointer]; + data.foilAlarm = !!bytes[pointer++]; + break; + case 16: // UserSwitch1Alarm, 1 byte digital + data.userSwitchAlarm = !!bytes[pointer++]; + break; + case 17: // DoorCountReport, 2 byte analog + data.doorCount = (bytes[pointer++] << 8) | bytes[pointer++]; + break; + case 18: // PresenceReport, 1 byte digital + data.presence = !!bytes[pointer++]; + break; + case 19: // IRProximityReport + data.irProximity = (bytes[pointer++] << 8) | bytes[pointer++]; + break; + case 20: // IRCloseProximityReport, low power + data.irCloseProximity = (bytes[pointer++] << 8) | bytes[pointer++]; + break; + case 21: // CloseProximityAlarm, something very close to presence sensor + data.closeProximityAlarm = !!bytes[pointer++]; + break; + case 22: // DisinfectAlarm + data.disinfectAlarm = bytes[pointer++]; + if (data.disinfectAlarm === 0) data.disinfectAlarm = "DIRTY"; + if (data.disinfectAlarm === 1) data.disinfectAlarm = "OCCUPIED"; + if (data.disinfectAlarm === 2) data.disinfectAlarm = "CLEANING"; + if (data.disinfectAlarm === 3) data.disinfectAlarm = "CLEAN"; + break; + case 80: + data.humidity = bytes[pointer++] / 2; + data.temperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 81: + data.humidity = bytes[pointer++] / 2; + data.averageTemperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 82: + data.closed = !!bytes[pointer++]; // true = door open, false = door closed // Inverted in example decoder + data.temperature = + ((bytes[pointer] & 0x80 ? 0xffff << 16 : 0) | + (bytes[pointer++] << 8) | + bytes[pointer++]) / + 10; + break; + case 112: // Capacitance Raw Sensor Value 2bytes 0-65535 + data.capacitanceFlood = (bytes[pointer++] << 8) | bytes[pointer++]; // should never trigger anymore + break; + case 113: // Capacitance Raw Sensor Value 2bytes 0-65535 + data.capacitancePad = (bytes[pointer++] << 8) | bytes[pointer++]; // should never trigger anymore + break; + case 110: { + const number = + ((bytes[pointer++] << 24) | + (bytes[pointer++] << 16) | + (bytes[pointer++] << 8) | + bytes[pointer++]) >>> + 0; + data.softwareVersion = number.toString(16); + data.startupCount = bytes[pointer++]; + data.watchdogCount = bytes[pointer++]; + data.stackTxFailRebootCount = bytes[pointer++]; + data.badConditionsCounter = bytes[pointer++]; + break; } - }; + case 114: // Capacitance Raw Sensor Value 2bytes 0-65535 + data.capacitanceEnd = (bytes[pointer++] << 8) | bytes[pointer++]; // should never trigger anymore + break; + default: + break; + } + return { data, pointer }; +} + +function deleteUnusedKeys(data) { + let keysRetained = false; + Object.keys(data).forEach((key) => { + if (data[key] === undefined) { + delete data[key]; + } else { + keysRetained = true; + } + }); + return keysRetained; +} + +function consume(event) { + const payload = event.data.payloadHex; + const { port } = event.data; + const bytes = Hex.hexToBytes(payload); const decoded = {}; - var pos = 0; + let pos = 2; let type; + switch (port) { case 1: if (bytes.length < 2) { - decoded.error = "Wrong length of RX package"; + emit("log", { error: "Wrong length of RX package" }); break; } - decoded.historySeqNr = (bytes[pos++] << 8) | bytes[pos++]; - decoded.prevHistSeqNr = decoded.historySeqNr; while (pos < bytes.length) { type = bytes[pos++]; - if (type & 0x80) { - decoded.prevHistSeqNr--; - } - decodeFrame(type, decoded); + const decodedFrame = decodeFrame(bytes, type, pos); + pos = decodedFrame.pointer; + Object.assign(decoded, decodedFrame.data); } break; - case 2: - var now = new Date(); + + case 2: { + const now = new Date(); decoded.history = {}; if (bytes.length < 2) { - decoded.error = "Wrong length of RX package"; + decoded.history.error = "Wrong length of RX package"; break; } - var seqNr = (bytes[pos++] << 8) | bytes[pos++]; + let seqNr = (bytes[pos++] << 8) | bytes[pos++]; while (pos < bytes.length) { decoded.history[seqNr] = {}; decoded.history.now = now.toUTCString(); @@ -190,50 +202,24 @@ function decode(bytes, port) { now.getTime() - secondsAgo * 1000, ).toUTCString(); type = bytes[pos++]; - decodeFrame(type, decoded.history[seqNr]); + const decodedFrame = decodeFrame(bytes, type, pos); + pos = decodedFrame.pointer; + Object.assign(decoded, decodedFrame.data); seqNr++; } break; - default: - } - return decoded; -} - -function deleteUnusedKeys(data) { - let keysRetained = false; - Object.keys(data).forEach((key) => { - if (data[key] === undefined) { - delete data[key]; - } else { - keysRetained = true; } - }); - return keysRetained; -} - -function parseHexString(str) { - const result = []; - while (str.length >= 2) { - result.push(parseInt(str.substring(0, 2), 16)); - str = str.substring(2, str.length); + default: + break; } - return result; -} - -function consume(event) { - const payload = event.data.payloadHex; - const { port } = event.data; - const bytes = parseHexString(payload); - const decoded = decode(bytes, port); - const def = {}; def.temperature = decoded.temperature; def.averageTemperature = decoded.averageTemperature; def.humidity = decoded.humidity; def.light = decoded.light; def.light2 = decoded.light2; - def.open = decoded.open; + def.closed = decoded.closed; def.tamperReport = decoded.tamperReport; def.doorCount = decoded.doorCount; def.presence = decoded.presence; @@ -246,15 +232,19 @@ function consume(event) { alarm.doorAlarm = decoded.doorAlarm; alarm.tamperAlarm = decoded.tamperAlarm; alarm.floodAlarm = decoded.floodAlarm; + alarm.oilAlarm = decoded.oilAlarm; alarm.foilAlarm = decoded.foilAlarm; alarm.userSwitchAlarm = decoded.userSwitchAlarm; alarm.closeProximityAlarm = decoded.closeProximityAlarm; alarm.disinfectAlarm = decoded.disinfectAlarm; const lifecycle = {}; - lifecycle.batteryLevel = decoded.batteryLevel; + if (decoded.batteryLevel !== 0) { + lifecycle.batteryLevel = decoded.batteryLevel; + } + lifecycle.error = decoded.error; lifecycle.softwareVersion = decoded.softwareVersion; - lifecycle.startUpCount = decoded.startUpCount; + lifecycle.startupCount = decoded.startupCount; lifecycle.watchdogCount = decoded.watchdogCount; lifecycle.stackTxFailRebootCount = decoded.stackTxFailRebootCount; lifecycle.badConditionsCounter = decoded.badConditionsCounter; @@ -268,6 +258,7 @@ function consume(event) { } if (deleteUnusedKeys(def)) { + def.open = !decoded.closed; emit("sample", { data: def, topic: "default" }); } diff --git a/types/sensative/presence/uplink.spec.js b/types/sensative/presence/uplink.spec.js index 3bddf8ea..c68822d2 100644 --- a/types/sensative/presence/uplink.spec.js +++ b/types/sensative/presence/uplink.spec.js @@ -40,7 +40,7 @@ describe("Sensative presence Uplink", () => { assert.equal(value.topic, "lifecycle"); assert.equal(value.data.badConditionsCounter, 0); assert.equal(value.data.stackTxFailRebootCount, 0); - assert.equal(value.data.startUpCount, 72); + assert.equal(value.data.startupCount, 72); assert.equal(value.data.softwareVersion, "2378c84"); assert.equal(value.data.watchdogCount, 0);