From e598d5ca91997a2e23c5c0b4dbc8aa236386859e Mon Sep 17 00:00:00 2001 From: j2 Date: Fri, 10 Jan 2025 15:07:02 +0100 Subject: [PATCH] Changed uplink to emit historic samples --- types/miromico/insight/climate.schema.json | 149 -------- types/miromico/insight/co2.schema.json | 13 + types/miromico/insight/humidity.schema.json | 13 + types/miromico/insight/iaq.schema.json | 16 + types/miromico/insight/meta.json | 8 +- types/miromico/insight/pressure.schema.json | 13 + .../miromico/insight/temperature.schema.json | 13 + types/miromico/insight/uplink.js | 98 ++++-- types/miromico/insight/uplink.spec.js | 328 ++++++++++++++++-- .../insightMioty/humidity.schema.json | 13 + types/miromico/insightMioty/meta.json | 3 +- .../insightMioty/temperature.schema.json | 43 +-- types/miromico/insightMioty/uplink.js | 79 +++-- types/miromico/insightMioty/uplink.spec.js | 72 ++-- 14 files changed, 554 insertions(+), 307 deletions(-) delete mode 100644 types/miromico/insight/climate.schema.json create mode 100644 types/miromico/insight/co2.schema.json create mode 100644 types/miromico/insight/humidity.schema.json create mode 100644 types/miromico/insight/iaq.schema.json create mode 100644 types/miromico/insight/pressure.schema.json create mode 100644 types/miromico/insight/temperature.schema.json create mode 100644 types/miromico/insightMioty/humidity.schema.json diff --git a/types/miromico/insight/climate.schema.json b/types/miromico/insight/climate.schema.json deleted file mode 100644 index 32041f55..00000000 --- a/types/miromico/insight/climate.schema.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "$id": "https://akenza.io/miromico/insight/climate.schema.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "processingType": "uplink_decoder", - "topic": "climate", - "title": "Climate", - "properties": { - "temperature": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/temperature/celsius" - }, - "temperature2": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/temperature/celsius", - "title": "Temperature 2" - }, - "temperature3": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/temperature/celsius", - "title": "Temperature 3" - }, - "temperature4": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/temperature/celsius", - "title": "Temperature 4" - }, - "temperature5": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/temperature/celsius", - "title": "Temperature 5" - }, - "humidity": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/humidity/percent" - }, - "humidity2": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/humidity/percent", - "title": "Humidity 2" - }, - "humidity3": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/humidity/percent", - "title": "Humidity 3" - }, - "humidity4": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/humidity/percent", - "title": "Humidity 4" - }, - "humidity5": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/humidity/percent", - "title": "Humidity 5" - }, - "co2": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/co2/ppm" - }, - "co2_2": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/co2/ppm", - "title": "CO2 2" - }, - "co2_3": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/co2/ppm", - "title": "CO2 3" - }, - "co2_4": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/co2/ppm", - "title": "CO2 4" - }, - "co2_5": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/co2/ppm", - "title": "CO2 5" - }, - "iaq": { - "title": "Indoor air quality", - "description": "Indoor air quality", - "type": "integer", - "hideFromKpis": true - }, - "iaq2": { - "title": "Indoor air quality 2", - "description": "Indoor air quality", - "type": "integer", - "hideFromKpis": true - }, - "iaq3": { - "title": "Indoor air quality 3", - "description": "Indoor air quality", - "type": "integer", - "hideFromKpis": true - }, - "iaq4": { - "title": "Indoor air quality 4", - "description": "Indoor air quality", - "type": "integer", - "hideFromKpis": true - }, - "iaq5": { - "title": "Indoor air quality 5", - "description": "Indoor air quality", - "type": "integer", - "hideFromKpis": true - }, - "iaqAccuracy": { - "title": "Indoor air quality accuracy", - "description": "Indoor air quality accuracy", - "type": "integer", - "hideFromKpis": true - }, - "iaqAccuracy2": { - "title": "Indoor air quality accuracy 2", - "description": "Indoor air quality accuracy 2", - "type": "integer", - "hideFromKpis": true - }, - "iaqAccuracy3": { - "title": "Indoor air quality accuracy 3", - "description": "Indoor air quality accuracy 3", - "type": "integer", - "hideFromKpis": true - }, - "iaqAccuracy4": { - "title": "Indoor air quality accuracy 4", - "description": "Indoor air quality accuracy 4", - "type": "integer", - "hideFromKpis": true - }, - "iaqAccuracy5": { - "title": "Indoor air quality accuracy 5", - "description": "Indoor air quality accuracy 5", - "type": "integer", - "hideFromKpis": true - }, - "pressure": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/pressure/kPa" - }, - "pressure2": { - "title": "Pressure 2", - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/pressure/kPa" - }, - "pressure3": { - "title": "Pressure 3", - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/pressure/kPa" - }, - "pressure4": { - "title": "Pressure 4", - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/pressure/kPa" - }, - "pressure5": { - "title": "Pressure 5", - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/pressure/kPa" - }, - "light": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/illuminance/lux" - } - } -} \ No newline at end of file diff --git a/types/miromico/insight/co2.schema.json b/types/miromico/insight/co2.schema.json new file mode 100644 index 00000000..7c7904cb --- /dev/null +++ b/types/miromico/insight/co2.schema.json @@ -0,0 +1,13 @@ +{ + "$id": "https://akenza.io/miromico/insight/co2.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "processingType": "uplink_decoder", + "topic": "co2", + "title": "Co2", + "properties": { + "co2": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/co2/ppm" + } + } +} \ No newline at end of file diff --git a/types/miromico/insight/humidity.schema.json b/types/miromico/insight/humidity.schema.json new file mode 100644 index 00000000..cc8f6ce9 --- /dev/null +++ b/types/miromico/insight/humidity.schema.json @@ -0,0 +1,13 @@ +{ + "$id": "https://akenza.io/miromico/insight/humidity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "processingType": "uplink_decoder", + "topic": "humidity", + "title": "Humidity", + "properties": { + "humidity": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/humidity/percent" + } + } +} \ No newline at end of file diff --git a/types/miromico/insight/iaq.schema.json b/types/miromico/insight/iaq.schema.json new file mode 100644 index 00000000..e16d4460 --- /dev/null +++ b/types/miromico/insight/iaq.schema.json @@ -0,0 +1,16 @@ +{ + "$id": "https://akenza.io/miromico/insight/iaq.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "processingType": "uplink_decoder", + "topic": "iaq", + "title": "Iaq", + "properties": { + "iaq": { + "title": "Indoor air quality", + "description": "Indoor air quality", + "type": "integer", + "hideFromKpis": true + } + } +} \ No newline at end of file diff --git a/types/miromico/insight/meta.json b/types/miromico/insight/meta.json index 6cf5e036..ae5d00e2 100644 --- a/types/miromico/insight/meta.json +++ b/types/miromico/insight/meta.json @@ -13,10 +13,14 @@ "CO2" ], "outputTopics": [ + "temperature", + "humidity", "co2", + "iaq", + "pressure", + "door", "lifecycle", - "settings", - "temperature" + "settings" ], "encoding": "HEX", "connectivity": "LORA" diff --git a/types/miromico/insight/pressure.schema.json b/types/miromico/insight/pressure.schema.json new file mode 100644 index 00000000..d3f59c8a --- /dev/null +++ b/types/miromico/insight/pressure.schema.json @@ -0,0 +1,13 @@ +{ + "$id": "https://akenza.io/miromico/insight/pressure.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "processingType": "uplink_decoder", + "topic": "pressure", + "title": "Pressure", + "properties": { + "pressure": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/pressure/kPa" + } + } +} \ No newline at end of file diff --git a/types/miromico/insight/temperature.schema.json b/types/miromico/insight/temperature.schema.json new file mode 100644 index 00000000..6d1730d2 --- /dev/null +++ b/types/miromico/insight/temperature.schema.json @@ -0,0 +1,13 @@ +{ + "$id": "https://akenza.io/miromico/insight/temperature.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "processingType": "uplink_decoder", + "topic": "temperature", + "title": "Temperature", + "properties": { + "temperature": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/temperature/celsius" + } + } +} \ No newline at end of file diff --git a/types/miromico/insight/uplink.js b/types/miromico/insight/uplink.js index e4331f08..3a1f49ff 100644 --- a/types/miromico/insight/uplink.js +++ b/types/miromico/insight/uplink.js @@ -24,12 +24,44 @@ function deleteUnusedKeys(data) { return keysRetained; } +function climateSamples(values, topic, state) { + let now = new Date().getTime(); + let sampleInterval = 0; + if (state !== undefined) { + sampleInterval = Math.round((now - state) / values.length); + } + + if (sampleInterval !== 0) { + // Give out samples with the right time // Different values can have different intervals :( + values.forEach(datapoint => { + const data = {}; + data[topic] = datapoint; + emit("sample", { data, topic, timestamp: now }); + now -= sampleInterval; + }); + } else { + // If no timestamps are available. Only give out the newest sample + const data = {}; + data[topic] = values[0]; + emit("sample", { data, topic }); + } + + return new Date().getTime() +} + function consume(event) { const payload = event.data.payloadHex; const bytes = Hex.hexToBytes(payload); const { port } = event.data; - const climate = {}; + const temperature = []; + const humidity = []; + const co2 = []; + const iaq = []; + const iaqAccuracy = []; + const pressure = []; + let light; + const door = {}; const lifecycle = {}; const settings = {}; @@ -40,36 +72,20 @@ function consume(event) { while (idx < total) { const length = bytes[idx]; - let iteration = ""; - switch (bytes[idx + 1]) { case 1: { let start = idx + 2; while (start < idx + length) { - climate[`temperature${iteration}`] = signed(bytes[start] + bytes[start + 1] * 256, 2) / 100; - climate[`humidity${iteration}`] = bytes[start + 2] / 2; + temperature.push(signed(bytes[start] + bytes[start + 1] * 256, 2) / 100); + humidity.push(bytes[start + 2] / 2); start += 3; - - if (iteration === "") { - iteration = 2; - } else { - iteration++; - } } break; } case 2: { let start = idx + 2; - let key = "co2"; while (start < idx + length) { - climate[`${key}${iteration}`] = bytes[start] + bytes[start + 1] * 256; + co2.push(bytes[start] + bytes[start + 1] * 256) start += 2; - - if (iteration === "") { - iteration = 2; - key += "_"; - } else { - iteration++; - } } break; } case 5: { @@ -137,39 +153,27 @@ function consume(event) { } case 15: { let start = idx + 2; while (start < idx + length) { - climate[`iaq${iteration}`] = bytes[start] + (bytes[start + 1] & 0x3f) * 256; - climate[`iaqAccuracy${iteration}`] = bytes[start + 1] >> 6; + iaq.push(bytes[start] + (bytes[start + 1] & 0x3f) * 256); + iaqAccuracy.push(bytes[start + 1] >> 6); start += 2; - - if (iteration === "") { - iteration = 2; - } else { - iteration++; - } } break; } case 16: { let start = idx + 2; while (start < idx + length) { - climate[`pressure${iteration}`] = ( + pressure.push( bytes[start] + bytes[start + 1] * 256 + bytes[start + 2] * 256 * 256 ); start += 3; - - if (iteration === "") { - iteration = 2; - } else { - iteration++; - } } break; } case 17: { settings.reportedInterval = bytes[idx + 2] + bytes[idx + 3] * 256; break; } case 20: { // light not buffered atm - climate.light = bytes[idx + 2] + bytes[idx + 3] * 256; + light = bytes[idx + 2] + bytes[idx + 3] * 256; break; } case 21: { settings.condTxCo2Th = bytes[idx + 2] + bytes[idx + 3] * 256; @@ -190,9 +194,27 @@ function consume(event) { } } - if (deleteUnusedKeys(climate)) { - emit("sample", { data: climate, topic: "climate" }); + const state = event.state || {}; + // Temperature & Humidity always comes in pairs + if (temperature.length !== 0) { + climateSamples(temperature, "temperature", state.lastTemperatureSample); + state.lastTemperatureSample = climateSamples(humidity, "humidity", state.lastTemperatureSample); + } + + if (co2.length !== 0) { + state.lastCo2Sample = climateSamples(co2, "co2", state.lastCo2Sample); + } + + if (iaq.length !== 0) { + state.lastIaqSample = climateSamples(iaq, "iaq", state.lastIaqSample); + } + + if (pressure.length !== 0) { + state.lastPressureSample = climateSamples(pressure, "pressure", state.lastPressureSample); } + + emit("state", state); + if (deleteUnusedKeys(door)) { emit("sample", { data: door, topic: "door" }); } diff --git a/types/miromico/insight/uplink.spec.js b/types/miromico/insight/uplink.spec.js index 6fe08fc8..1d0b75be 100644 --- a/types/miromico/insight/uplink.spec.js +++ b/types/miromico/insight/uplink.spec.js @@ -6,17 +6,57 @@ const utils = require("test-utils"); const { assert } = chai; describe("Miromico insight Uplink", () => { - let climateSchema = null; + let temperatureSchema = null; let consume = null; before((done) => { const script = rewire("./uplink.js"); consume = utils.init(script); - utils.loadSchema(`${__dirname}/climate.schema.json`).then((parsedSchema) => { - climateSchema = parsedSchema; + utils.loadSchema(`${__dirname}/temperature.schema.json`).then((parsedSchema) => { + temperatureSchema = parsedSchema; done(); }); }); + let humiditySchema = null; + before((done) => { + utils + .loadSchema(`${__dirname}/humidity.schema.json`) + .then((parsedSchema) => { + humiditySchema = parsedSchema; + done(); + }); + }); + + let co2Schema = null; + before((done) => { + utils + .loadSchema(`${__dirname}/co2.schema.json`) + .then((parsedSchema) => { + co2Schema = parsedSchema; + done(); + }); + }); + + let iaqSchema = null; + before((done) => { + utils + .loadSchema(`${__dirname}/iaq.schema.json`) + .then((parsedSchema) => { + iaqSchema = parsedSchema; + done(); + }); + }); + + let pressureSchema = null; + before((done) => { + utils + .loadSchema(`${__dirname}/pressure.schema.json`) + .then((parsedSchema) => { + pressureSchema = parsedSchema; + done(); + }); + }); + let lifecycleSchema = null; before((done) => { utils @@ -48,7 +88,7 @@ describe("Miromico insight Uplink", () => { }); describe("consume()", () => { - it("should decode Miromico insight standard payload", () => { + it("should decode Miromico insight standard payload without lastSample timestamp", () => { const data = { data: { port: 15, @@ -62,32 +102,270 @@ describe("Miromico insight Uplink", () => { assert.isNotNull(value); assert.typeOf(value.data, "object"); - assert.equal(value.topic, "climate"); - assert.equal(value.data.co2, 643); - assert.equal(value.data.co2_2, 667); - assert.equal(value.data.co2_3, 705); + assert.equal(value.topic, "temperature"); + assert.equal(value.data.temperature, 24.53); + + utils.validateSchema(value.data, temperatureSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + assert.equal(value.topic, "humidity"); assert.equal(value.data.humidity, 40.5); - assert.equal(value.data.humidity2, 50.5); - assert.equal(value.data.humidity3, 45); + utils.validateSchema(value.data, humiditySchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "co2"); + assert.equal(value.data.co2, 643); + + utils.validateSchema(value.data, co2Schema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "iaq"); assert.equal(value.data.iaq, 201); - assert.equal(value.data.iaq2, 245); - assert.equal(value.data.iaq3, 150); - assert.equal(value.data.iaqAccuracy, 3); - assert.equal(value.data.iaqAccuracy2, 3); - assert.equal(value.data.iaqAccuracy3, 2); + utils.validateSchema(value.data, iaqSchema, { throwError: true }); + }); + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "pressure"); assert.equal(value.data.pressure, 96234); - assert.equal(value.data.pressure2, 96115); - assert.equal(value.data.pressure3, 95623); + utils.validateSchema(value.data, pressureSchema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "state"); + assert.isNotNull(value); + + // assert.equal(value.lastTemperatureSample, new Date().timestamp); + // assert.equal(value.lastCo2Sample, new Date().timestamp); + // assert.equal(value.lastIaqSample, new Date().timestamp); + // assert.equal(value.lastPressureSample, new Date().timestamp); + }); + + 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.batteryVoltage, 3.57); + + utils.validateSchema(value.data, lifecycleSchema, { + throwError: true, + }); + }); + + consume(data); + }); + + it("should decode Miromico insight standard payload with lastSample timestamp", () => { + const data = { + state: { + lastTemperatureSample: new Date().setHours(new Date().getHours() - 1), + lastCo2Sample: new Date().setHours(new Date().getHours() - 1), + lastIaqSample: new Date().setHours(new Date().getHours() - 1), + lastPressureSample: new Date().setHours(new Date().getHours() - 1) + }, + data: { + port: 15, + payloadHex: + "0a01950951e60965c9095a070283029b02c102070fc9c0f5c096800a10ea770173770187750103096501", + }, + }; + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "temperature"); assert.equal(value.data.temperature, 24.53); - assert.equal(value.data.temperature2, 25.34); - assert.equal(value.data.temperature3, 25.05); - utils.validateSchema(value.data, climateSchema, { throwError: true }); + utils.validateSchema(value.data, temperatureSchema, { throwError: true }); // Now + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "temperature"); + assert.equal(value.data.temperature, 25.34); + + utils.validateSchema(value.data, temperatureSchema, { throwError: true }); // Now -20 min + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "temperature"); + assert.equal(value.data.temperature, 25.05); + + utils.validateSchema(value.data, temperatureSchema, { throwError: true }); // Now -40 min + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "humidity"); + assert.equal(value.data.humidity, 40.5); + + utils.validateSchema(value.data, humiditySchema, { throwError: true }); // Now + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "humidity"); + assert.equal(value.data.humidity, 50.5); + + utils.validateSchema(value.data, humiditySchema, { throwError: true }); // Now -20 min + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "humidity"); + assert.equal(value.data.humidity, 45); + + utils.validateSchema(value.data, humiditySchema, { throwError: true }); // Now -40 min + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "co2"); + assert.equal(value.data.co2, 643); + + utils.validateSchema(value.data, co2Schema, { throwError: true }); // Now + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "co2"); + assert.equal(value.data.co2, 667); + + utils.validateSchema(value.data, co2Schema, { throwError: true }); // Now -20 min + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "co2"); + assert.equal(value.data.co2, 705); + + utils.validateSchema(value.data, co2Schema, { throwError: true }); // Now -40 min + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "iaq"); + assert.equal(value.data.iaq, 201); + + utils.validateSchema(value.data, iaqSchema, { throwError: true }); // Now + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "iaq"); + assert.equal(value.data.iaq, 245); + + utils.validateSchema(value.data, iaqSchema, { throwError: true }); // Now -20 min + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "iaq"); + assert.equal(value.data.iaq, 150); + + utils.validateSchema(value.data, iaqSchema, { throwError: true }); // Now -40 min + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "pressure"); + assert.equal(value.data.pressure, 96234); + + utils.validateSchema(value.data, pressureSchema, { throwError: true }); // Now + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "pressure"); + assert.equal(value.data.pressure, 96115); + + utils.validateSchema(value.data, pressureSchema, { throwError: true }); // Now -20 min + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "sample"); + assert.isNotNull(value); + assert.typeOf(value.data, "object"); + + assert.equal(value.topic, "pressure"); + assert.equal(value.data.pressure, 95623); + + utils.validateSchema(value.data, pressureSchema, { throwError: true }); // Now -40 min + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "state"); + assert.isNotNull(value); + + // assert.equal(value.lastTemperatureSample, new Date().timestamp); + // assert.equal(value.lastCo2Sample, new Date().timestamp); + // assert.equal(value.lastIaqSample, new Date().timestamp); + // assert.equal(value.lastPressureSample, new Date().timestamp); }); utils.expectEmits((type, value) => { @@ -115,6 +393,11 @@ describe("Miromico insight Uplink", () => { }, }; + utils.expectEmits((type, value) => { + assert.equal(type, "state"); + assert.isNotNull(value); + }); + utils.expectEmits((type, value) => { assert.equal(type, "sample"); assert.isNotNull(value); @@ -151,6 +434,11 @@ describe("Miromico insight Uplink", () => { }, }; + utils.expectEmits((type, value) => { + assert.equal(type, "state"); + assert.isNotNull(value); + }); + utils.expectEmits((type, value) => { assert.equal(type, "sample"); assert.isNotNull(value); diff --git a/types/miromico/insightMioty/humidity.schema.json b/types/miromico/insightMioty/humidity.schema.json new file mode 100644 index 00000000..cc8f6ce9 --- /dev/null +++ b/types/miromico/insightMioty/humidity.schema.json @@ -0,0 +1,13 @@ +{ + "$id": "https://akenza.io/miromico/insight/humidity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "processingType": "uplink_decoder", + "topic": "humidity", + "title": "Humidity", + "properties": { + "humidity": { + "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/humidity/percent" + } + } +} \ No newline at end of file diff --git a/types/miromico/insightMioty/meta.json b/types/miromico/insightMioty/meta.json index 3e025b19..e748fa3c 100644 --- a/types/miromico/insightMioty/meta.json +++ b/types/miromico/insightMioty/meta.json @@ -14,10 +14,11 @@ ], "outputTopics": [ "co2", + "humidity", "lifecycle", "settings", "temperature" ], "encoding": "HEX", "connectivity": "MIOTY" -} +} \ No newline at end of file diff --git a/types/miromico/insightMioty/temperature.schema.json b/types/miromico/insightMioty/temperature.schema.json index a4c6f6ad..6d1730d2 100644 --- a/types/miromico/insightMioty/temperature.schema.json +++ b/types/miromico/insightMioty/temperature.schema.json @@ -1,5 +1,5 @@ { - "$id": "https://akenza.io/miromico/insightmioty/temperature.schema.json", + "$id": "https://akenza.io/miromico/insight/temperature.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "processingType": "uplink_decoder", @@ -8,45 +8,6 @@ "properties": { "temperature": { "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/temperature/celsius" - }, - "temperature2": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/temperature/celsius", - "title": "Temperature 2" - }, - "temperature3": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/temperature/celsius", - "title": "Temperature 3" - }, - "temperature4": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/temperature/celsius", - "title": "Temperature 4" - }, - "temperature5": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/temperature/celsius", - "title": "Temperature 5" - }, - "humidity": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/humidity/percent" - }, - "humidity2": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/humidity/percent", - "title": "Humidity 2" - }, - "humidity3": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/humidity/percent", - "title": "Humidity 3" - }, - "humidity4": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/humidity/percent", - "title": "Humidity 4" - }, - "humidity5": { - "$ref": "https://raw.githubusercontent.com/akenza-io/device-type-library/main/data-models/environment/schema.json#/$defs/humidity/percent", - "title": "Humidity 5" } - }, - "required": [ - "temperature", - "humidity" - ] + } } \ No newline at end of file diff --git a/types/miromico/insightMioty/uplink.js b/types/miromico/insightMioty/uplink.js index 48205096..48a774ed 100644 --- a/types/miromico/insightMioty/uplink.js +++ b/types/miromico/insightMioty/uplink.js @@ -1,13 +1,39 @@ +function climateSamples(values, topic, state) { + let now = new Date().getTime(); + let sampleInterval = 0; + if (state !== undefined) { + sampleInterval = Math.round((now - state) / values.length); + } + + if (sampleInterval !== 0) { + // Give out samples with the right time // Different values can have different intervals :( + values.forEach(datapoint => { + const data = {}; + data[topic] = datapoint; + emit("sample", { data, topic, timestamp: now }); + now -= sampleInterval; + }); + } else { + // If no timestamps are available. Only give out the newest sample + const data = {}; + data[topic] = values[0]; + emit("sample", { data, topic }); + } + + return new Date().getTime() +} + function consume(event) { const payload = event.data.payloadHex; const bits = Bits.hexToBits(payload); const lifecycle = {}; const settings = {}; - const temperature = {}; - const co2 = {}; - for (let pointer = 0; pointer < bits.length; ) { - let measurement = ""; + const temperature = []; + const humidity = []; + const co2 = []; + + for (let pointer = 0; pointer < bits.length;) { let length = (Bits.bitsToUnsigned(bits.substr(pointer, 8)) - 1) * 8; const msgtype = Bits.bitsToUnsigned(bits.substr((pointer += 8), 8)); pointer += 8; @@ -16,23 +42,10 @@ function consume(event) { switch (msgtype) { case 1: while (pointer < length) { - temperature[`temperature${measurement}`] = - Hex.hexLittleEndianToBigEndian( - payload.substr(pointer / 4, 4), - true, - ) * 0.01; + temperature.push(Hex.hexLittleEndianToBigEndian(payload.substr(pointer / 4, 4), true) * 0.01) pointer += 16; - temperature[`humidity${measurement}`] = - Hex.hexLittleEndianToBigEndian( - payload.substr(pointer / 4, 2), - true, - ) * 0.5; + humidity.push(Hex.hexLittleEndianToBigEndian(payload.substr(pointer / 4, 2), true) * 0.5); pointer += 8; - if (measurement === "") { - measurement = 2; - } else { - measurement++; - } } break; case 2: @@ -41,13 +54,7 @@ function consume(event) { pointer += 8; const co2lsb = bits.substr(pointer, 8); pointer += 8; - co2[`co2${measurement}`] = Bits.bitsToUnsigned(co2lsb + co2msb); - - if (measurement === "") { - measurement = 1; - } else { - measurement++; - } + co2.push(Bits.bitsToUnsigned(co2lsb + co2msb)); } break; case 3: @@ -97,19 +104,23 @@ function consume(event) { } } - if (Object.keys(lifecycle).length > 0) { - emit("sample", { data: lifecycle, topic: "lifecycle" }); + const state = event.state || {}; + // Temperature & Humidity always comes in pairs + if (temperature.length !== 0) { + climateSamples(temperature, "temperature", state.lastTemperatureSample); + state.lastTemperatureSample = climateSamples(humidity, "humidity", state.lastTemperatureSample); } - if (Object.keys(settings).length > 0) { - emit("sample", { data: settings, topic: "settings" }); + if (co2.length !== 0) { + state.lastCo2Sample = climateSamples(co2, "co2", state.lastCo2Sample); } + emit("state", state); - if (Object.keys(temperature).length > 0) { - emit("sample", { data: temperature, topic: "temperature" }); + if (Object.keys(lifecycle).length > 0) { + emit("sample", { data: lifecycle, topic: "lifecycle" }); } - if (Object.keys(co2).length > 0) { - emit("sample", { data: co2, topic: "co2" }); + if (Object.keys(settings).length > 0) { + emit("sample", { data: settings, topic: "settings" }); } } diff --git a/types/miromico/insightMioty/uplink.spec.js b/types/miromico/insightMioty/uplink.spec.js index a9dd0153..fb8be689 100644 --- a/types/miromico/insightMioty/uplink.spec.js +++ b/types/miromico/insightMioty/uplink.spec.js @@ -47,6 +47,16 @@ describe("Miromico insight Uplink", () => { }); }); + let humiditySchema = null; + before((done) => { + utils + .loadSchema(`${__dirname}/humidity.schema.json`) + .then((parsedSchema) => { + humiditySchema = parsedSchema; + done(); + }); + }); + describe("consume()", () => { it("should decode Miromico insight payload", () => { const data = { @@ -62,11 +72,12 @@ describe("Miromico insight Uplink", () => { assert.isNotNull(value); assert.typeOf(value.data, "object"); - assert.equal(value.topic, "lifecycle"); - assert.equal(value.data.consumption, 16); - assert.equal(value.data.batteryVoltage, 3.6); + assert.equal(value.topic, "temperature"); + assert.equal(value.data.temperature, 25); - utils.validateSchema(value.data, lifecycleSchema, { throwError: true }); + utils.validateSchema(value.data, temperatureSchema, { + throwError: true, + }); }); utils.expectEmits((type, value) => { @@ -74,14 +85,12 @@ describe("Miromico insight Uplink", () => { assert.isNotNull(value); assert.typeOf(value.data, "object"); - assert.equal(value.topic, "settings"); - assert.equal(value.data.measurementInterval, 1200); - assert.equal(value.data.temperatureSamples, 4); - assert.equal(value.data.co2Subsample, 32); - assert.equal(value.data.abcCalibrationPeriod, 384); - assert.equal(value.data.firmwareHash, "0389A2B9"); + assert.equal(value.topic, "humidity"); + assert.equal(value.data.humidity, 60); - utils.validateSchema(value.data, settingsSchema, { throwError: true }); + utils.validateSchema(value.data, humiditySchema, { + throwError: true, + }); }); utils.expectEmits((type, value) => { @@ -89,15 +98,18 @@ describe("Miromico insight Uplink", () => { assert.isNotNull(value); assert.typeOf(value.data, "object"); - assert.equal(value.topic, "temperature"); - assert.equal(value.data.temperature, 25); - assert.equal(value.data.humidity, 60); - assert.equal(value.data.temperature2, 25.52); - assert.equal(value.data.humidity2, 59.5); + assert.equal(value.topic, "co2"); + assert.equal(value.data.co2, 779); - utils.validateSchema(value.data, temperatureSchema, { - throwError: true, - }); + utils.validateSchema(value.data, co2Schema, { throwError: true }); + }); + + utils.expectEmits((type, value) => { + assert.equal(type, "state"); + assert.isNotNull(value); + + // assert.equal(value.lastTemperatureSample, new Date().timestamp); + // assert.equal(value.lastCo2Sample, new Date().timestamp); }); utils.expectEmits((type, value) => { @@ -105,10 +117,26 @@ describe("Miromico insight Uplink", () => { assert.isNotNull(value); assert.typeOf(value.data, "object"); - assert.equal(value.topic, "co2"); - assert.equal(value.data.co2, 779); + assert.equal(value.topic, "lifecycle"); + assert.equal(value.data.consumption, 16); + assert.equal(value.data.batteryVoltage, 3.6); - utils.validateSchema(value.data, co2Schema, { throwError: true }); + 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, "settings"); + assert.equal(value.data.measurementInterval, 1200); + assert.equal(value.data.temperatureSamples, 4); + assert.equal(value.data.co2Subsample, 32); + assert.equal(value.data.abcCalibrationPeriod, 384); + assert.equal(value.data.firmwareHash, "0389A2B9"); + + utils.validateSchema(value.data, settingsSchema, { throwError: true }); }); consume(data);