diff --git a/bin/validate.js b/bin/validate.js index cc203640cd..5a22c93f11 100644 --- a/bin/validate.js +++ b/bin/validate.js @@ -1,6 +1,8 @@ #!/usr/bin/env node -const Ajv = require('ajv'); +const Ajv = require('ajv/dist/2020'); +const addFormats = require('ajv-formats'); + const yargs = require('yargs'); const fs = require('fs'); const yaml = require('js-yaml'); @@ -11,6 +13,7 @@ const readChunk = require('read-chunk'); const imageType = require('image-type'); const ajv = new Ajv({ schemas: [require('../lib/payload.json'), require('../schema.json')] }); +addFormats(ajv); const options = yargs .usage('Usage: --vendor [--vendor-id ]') @@ -26,21 +29,12 @@ const options = yargs type: 'string', }).argv; -let validateVendorsIndex = ajv.compile({ - $ref: 'https://schema.thethings.network/devicerepository/1/schema#/definitions/vendorsIndex', -}); -let validateVendorIndex = ajv.compile({ - $ref: 'https://schema.thethings.network/devicerepository/1/schema#/definitions/vendorIndex', -}); -let validateEndDevice = ajv.compile({ - $ref: 'https://schema.thethings.network/devicerepository/1/schema#/definitions/endDevice', -}); -let validateEndDeviceProfile = ajv.compile({ - $ref: 'https://schema.thethings.network/devicerepository/1/schema#/definitions/endDeviceProfile', -}); -let validateEndDevicePayloadCodec = ajv.compile({ - $ref: 'https://schema.thethings.network/devicerepository/1/schema#/definitions/endDevicePayloadCodec', -}); +const schemaId = 'https://schema.thethings.network/devicerepository/1/schema'; +const validateVendorsIndex = ajv.getSchema(`${schemaId}#/$defs/vendorsIndex`); +const validateVendorIndex = ajv.getSchema(`${schemaId}#/$defs/vendorIndex`); +const validateEndDevice = ajv.getSchema(`${schemaId}#/$defs/endDevice`); +const validateEndDeviceProfile = ajv.getSchema(`${schemaId}#/$defs/endDeviceProfile`); +const validateEndDevicePayloadCodec = ajv.getSchema(`${schemaId}#/$defs/endDevicePayloadCodec`); function requireFile(path) { if (path.toLowerCase() !== path) { @@ -206,7 +200,7 @@ function requireImageDecode(fileName) { } function formatValidationErrors(errors) { - return errors.map((e) => `${e.dataPath} ${e.message}`); + return errors.map((e) => `${e.instancePath} ${e.message}`); } const vendors = yaml.load(fs.readFileSync(options.vendor)); @@ -283,17 +277,18 @@ vendors.vendors.forEach((v) => { console.log(`${v.id}: valid index`); const codecs = {}; - const deviceNames = {}; vendor.endDevices.forEach(async (d) => { const key = `${v.id}: ${d}`; const endDevicePath = `${folder}/${d}.yaml`; const endDevice = yaml.load(fs.readFileSync(endDevicePath)); + if (!validateEndDevice(endDevice)) { console.error(`${key}: invalid: ${formatValidationErrors(validateEndDevice.errors)}`); process.exit(1); } + console.log(`${key}: valid`); // Create a regex to check if the vendor's name is a standalone word in the device name diff --git a/lib/payload.json b/lib/payload.json index 428f4c3d61..03e5a9f225 100644 --- a/lib/payload.json +++ b/lib/payload.json @@ -1,9 +1,9 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schema.thethings.network/devicerepository/1/payload/schema", "title": "LoRaWAN Device Repository Payload", "description": "Payload definitions for the LoRaWAN Device Repository", - "definitions": { + "$defs": { "temperature": { "type": "number", "description": "Temperature (°C)", @@ -56,20 +56,25 @@ "format": "date-time", "description": "Date and time of the measurement (RFC3339)" }, + "battery": { + "type": "number", + "description": "Voltage (V)", + "minimum": 0 + }, "soil": { "type": "object", "properties": { "depth": { "description": "Depth of the soil measurement (cm)", - "$ref": "#/definitions/depth" + "$ref": "#/$defs/depth" }, "moisture": { "description": "Soil moisture (%)", - "$ref": "#/definitions/percentage" + "$ref": "#/$defs/percentage" }, "temperature": { "description": "Soil temperature (°C)", - "$ref": "#/definitions/temperature" + "$ref": "#/$defs/temperature" }, "ec": { "description": "Soil electrical conductivity (dS/m)", @@ -79,19 +84,19 @@ }, "pH": { "description": "Soil pH level", - "$ref": "#/definitions/pH" + "$ref": "#/$defs/pH" }, "n": { "description": "Concentration of Nitrogen in the soil (ppm)", - "$ref": "#/definitions/concentration" + "$ref": "#/$defs/concentration" }, "p": { "description": "Concentration of Phosphorus in the soil (ppm)", - "$ref": "#/definitions/concentration" + "$ref": "#/$defs/concentration" }, "k": { "description": "Concentration of Potassium in the soil (ppm)", - "$ref": "#/definitions/concentration" + "$ref": "#/$defs/concentration" } }, "additionalProperties": false @@ -99,13 +104,18 @@ "air": { "type": "object", "properties": { + "location": { + "type": "string", + "enum": ["indoor", "outdoor"], + "description": "Specifies whether the measurement was taken indoors or outdoors." + }, "temperature": { "description": "Air temperature (°C)", - "$ref": "#/definitions/temperature" + "$ref": "#/$defs/temperature" }, "relativeHumidity": { "description": "Relative humidity (%)", - "$ref": "#/definitions/percentage" + "$ref": "#/$defs/percentage" }, "pressure": { "type": "number", @@ -115,11 +125,11 @@ }, "co2": { "description": "Concentration of CO2 in the air (ppm)", - "$ref": "#/definitions/concentration" + "$ref": "#/$defs/concentration" }, "lightIntensity": { "description": "Light intensity (lux)", - "$ref": "#/definitions/illuminance" + "$ref": "#/$defs/illuminance" } }, "additionalProperties": false @@ -129,11 +139,95 @@ "properties": { "speed": { "description": "Wind speed (m/s)", - "$ref": "#/definitions/speed" + "$ref": "#/$defs/speed" }, "direction": { "description": "Wind direction (°)", - "$ref": "#/definitions/direction" + "$ref": "#/$defs/direction" + } + }, + "additionalProperties": false + }, + "water": { + "type": "object", + "properties": { + "leak": { + "type": ["boolean", "string"], + "description": "Leak detected" + }, + "temperature": { + "type": "object", + "properties": { + "min": { + "description": "Minimum temperature (°C)", + "$ref": "#/$defs/temperature" + }, + "max": { + "description": "Maximum temperature (°C)", + "$ref": "#/$defs/temperature" + }, + "avg": { + "description": "Average temperature (°C)", + "$ref": "#/$defs/temperature" + }, + "current": { + "description": "Current temperature (°C)", + "$ref": "#/$defs/temperature" + } + } + } + }, + "additionalProperties": false + }, + "metering": { + "type": "object", + "properties": { + "water": { + "type": "object", + "properties": { + "total": { + "type": "number", + "description": "Total volume (L)" + } + } + } + } + }, + "action": { + "type": "object", + "properties": { + "motion": { + "type": "object", + "properties": { + "detected": { + "type": "boolean", + "description": "Motion detected" + }, + "count": { + "type": "number", + "description": "Number of motion events (count)" + } + }, + "additionalProperties": false + }, + "contactState": { + "type": "string", + "description": "State of a contact sensor", + "enum": ["OPEN", "CLOSED"] + } + }, + "additionalProperties": false + }, + "position": { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "description": "Horizontal distance from equator (°)" + }, + "longitude": { + "type": "number", + "description": "Vertical distance from prime meridian (°)" } }, "additionalProperties": false @@ -144,13 +238,13 @@ "uplinkPayload": { "type": "array", "items": { - "$ref": "#/definitions/measurement" + "$ref": "#/$defs/measurement" } } }, "oneOf": [ { - "$ref": "#/definitions/uplinkPayload" + "$ref": "#/$defs/uplinkPayload" } ] } diff --git a/package-lock.json b/package-lock.json index 051ff8907b..eb92fe0f2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,7 +1,7 @@ { "name": "lorawan-devices", "version": "1.0.0", - "lockfileVersion": 3, + "lockfileVersion": 2, "requires": true, "packages": { "": { @@ -22,21 +22,22 @@ "validate": "bin/validate.js" }, "devDependencies": { - "ajv": "^6.12.6", + "ajv": "^8.17.1", "ajv-cli": "^5.0.0", - "prettier": "^2.6.2" + "ajv-formats": "^3.0.1", + "prettier": "^2.8.8" } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -69,22 +70,6 @@ } } }, - "node_modules/ajv-cli/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/ajv-cli/node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -107,11 +92,22 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/ajv-cli/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } }, "node_modules/ansi-regex": { "version": "5.0.1", @@ -252,27 +248,15 @@ "dev": true }, "node_modules/fast-json-patch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-2.2.1.tgz", - "integrity": "sha512-4j5uBaTnsYAV5ebkidvxiLUYOwjQ+JSFljeqfTxCrH9bDmlCQaOJFS84oDJ2rAXZq2yskmk3ORfoP9DCwqFNig==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^2.0.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/fast-json-patch/node_modules/fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==", "dev": true }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "node_modules/fast-uri": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", + "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==", "dev": true }, "node_modules/file-type": { @@ -403,34 +387,12 @@ "ajv": "^8.0.0" } }, - "node_modules/json-schema-migrate/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/json-schema-migrate/node_modules/json-schema-traverse": { + "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -549,15 +511,6 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/queue": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", @@ -668,15 +621,6 @@ "xtend": "~4.0.1" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -758,5 +702,535 @@ "node": ">=10" } } + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "ajv-cli": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ajv-cli/-/ajv-cli-5.0.0.tgz", + "integrity": "sha512-LY4m6dUv44HTyhV+u2z5uX4EhPYTM38Iv1jdgDJJJCyOOuqB8KtZEGjPZ2T+sh5ZIJrXUfgErYx/j3gLd3+PlQ==", + "dev": true, + "requires": { + "ajv": "^8.0.0", + "fast-json-patch": "3.1.1", + "glob": "^7.1.0", + "js-yaml": "^3.14.0", + "json-schema-migrate": "^2.0.0", + "json5": "^2.1.3", + "minimist": "^1.2.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + } + } + }, + "ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "requires": { + "ajv": "^8.0.0" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "csv-write-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/csv-write-stream/-/csv-write-stream-2.0.0.tgz", + "integrity": "sha512-QTraH6FOYfM5f+YGwx71hW1nR9ZjlWri67/D4CWtiBkdce0UAa91Vc0yyHg0CjC0NeEGnvO/tBSJkA1XF9D9GQ==", + "requires": { + "argparse": "^1.0.7", + "generate-object-property": "^1.0.0", + "ndjson": "^1.3.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + } + } + }, + "csv-writer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/csv-writer/-/csv-writer-1.6.0.tgz", + "integrity": "sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==", + "dev": true + }, + "fast-uri": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", + "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==", + "dev": true + }, + "file-type": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz", + "integrity": "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha512-TuOwZWgJ2VAMEGJvAyPWvpqxSANF0LDpmyHauMjFYzaACvn+QTT/AZomvPCzVBV7yDN3OmwHQ5OvHaeLKre3JQ==", + "requires": { + "is-property": "^1.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "image-size": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.9.7.tgz", + "integrity": "sha512-KRVgLNZkr00YGN0qn9MlIrmlxbRhsCcEb1Byq3WKGnIV4M48iD185cprRtaoK4t5iC+ym2Q5qlArxZ/V1yzDgA==", + "requires": { + "queue": "6.0.2" + } + }, + "image-type": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/image-type/-/image-type-4.1.0.tgz", + "integrity": "sha512-CFJMJ8QK8lJvRlTCEgarL4ro6hfDQKif2HjSvYCdQZESaIPV4v9imrf7BQHK+sQeTeNeMpWciR9hyC/g8ybXEg==", + "requires": { + "file-type": "^10.10.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "json-schema-migrate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/json-schema-migrate/-/json-schema-migrate-2.0.0.tgz", + "integrity": "sha512-r38SVTtojDRp4eD6WsCqiE0eNDt4v1WalBXb9cyZYw9ai5cGtBwzRNWjHzJl38w6TxFkXAIA7h+fyX3tnrAFhQ==", + "dev": true, + "requires": { + "ajv": "^8.0.0" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "ndjson": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/ndjson/-/ndjson-1.5.0.tgz", + "integrity": "sha512-hUPLuaziboGjNF7wHngkgVc0FOclR8dDk/HfEvTtDr/iUrqBWiRcRSTK3/nLOqKH33th714BrMmTPtObI9gZxQ==", + "requires": { + "json-stringify-safe": "^5.0.1", + "minimist": "^1.2.0", + "split2": "^2.1.0", + "through2": "^2.0.3" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==" + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "requires": { + "inherits": "~2.0.3" + } + }, + "read-chunk": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-3.2.0.tgz", + "integrity": "sha512-CEjy9LCzhmD7nUpJ1oVOE6s/hBkejlcJEgLQHVnQznOSilOPb+kpKktlLfFDK3/WP43+F80xkUTM2VOkYoSYvQ==", + "requires": { + "pify": "^4.0.1", + "with-open-file": "^0.1.6" + } + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "split2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", + "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", + "requires": { + "through2": "^2.0.2" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "with-open-file": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/with-open-file/-/with-open-file-0.1.7.tgz", + "integrity": "sha512-ecJS2/oHtESJ1t3ZfMI3B7KIDKyfN0O16miWxdn30zdh66Yd3LsRFebXZXq6GU4xfxLf6nVxp9kIqElb5fqczA==", + "requires": { + "p-finally": "^1.0.0", + "p-try": "^2.1.0", + "pify": "^4.0.1" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" + } } } diff --git a/package.json b/package.json index 7cfd3e4d65..1c91ef9385 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,10 @@ }, "homepage": "https://github.com/TheThingsNetwork/lorawan-devices#readme", "devDependencies": { - "ajv": "^6.12.6", + "ajv": "^8.17.1", "ajv-cli": "^5.0.0", - "prettier": "^2.6.2" + "ajv-formats": "^3.0.1", + "prettier": "^2.8.8" }, "dependencies": { "csv-write-stream": "^2.0.0", @@ -34,5 +35,8 @@ "lodash.isequal": "^4.5.0", "read-chunk": "^3.2.0", "yargs": "^16.2.0" + }, + "overrides": { + "fast-json-patch": "3.1.1" } } diff --git a/schema.json b/schema.json index e950c2492d..aea052c930 100644 --- a/schema.json +++ b/schema.json @@ -1,9 +1,9 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schema.thethings.network/devicerepository/1/schema", "title": "LoRaWAN Device Repository", "description": "Open source repository for LoRaWAN end devices and gateways", - "definitions": { + "$defs": { "identifier": { "type": "string", "pattern": "^[a-z0-9](?:[-]?[a-z0-9]){2,}$", @@ -15,7 +15,7 @@ }, "logo": { "allOf": [ - { "$ref": "#/definitions/localFile" }, + { "$ref": "#/$defs/localFile" }, { "type": "string", "pattern": "\\.(png|svg)$" @@ -24,7 +24,7 @@ }, "photo": { "allOf": [ - { "$ref": "#/definitions/localFile" }, + { "$ref": "#/$defs/localFile" }, { "type": "string", "pattern": "\\.(png|jpg|jpeg)$" @@ -65,7 +65,7 @@ "vendors": { "type": "array", "items": { - "$ref": "#/definitions/vendor" + "$ref": "#/$defs/vendor" } } }, @@ -77,7 +77,7 @@ "endDevices": { "type": "array", "items": { - "$ref": "#/definitions/identifier" + "$ref": "#/$defs/identifier" } }, "profileIDs": { @@ -89,7 +89,7 @@ "type": "object", "properties": { "endDeviceID": { - "$ref": "#/definitions/identifier" + "$ref": "#/$defs/identifier" }, "hardwareVersion": { "type": "string" @@ -99,7 +99,7 @@ "minLength": 1 }, "region": { - "$ref": "#/definitions/region" + "$ref": "#/$defs/region" } }, "required": ["endDeviceID", "hardwareVersion", "firmwareVersion", "region"] @@ -108,10 +108,10 @@ "type": "object", "properties": { "id": { - "$ref": "#/definitions/identifier" + "$ref": "#/$defs/identifier" }, "codec": { - "$ref": "#/definitions/identifier" + "$ref": "#/$defs/identifier" } }, "required": ["id", "codec"] @@ -129,7 +129,7 @@ "description": "Vendor of a device", "type": "object", "properties": { - "id": { "$ref": "#/definitions/identifier" }, + "id": { "$ref": "#/$defs/identifier" }, "name": { "type": "string", "minLength": 1 @@ -194,7 +194,7 @@ "type": "string", "format": "email" }, - "logo": { "$ref": "#/definitions/logo" } + "logo": { "$ref": "#/$defs/logo" } }, "required": ["id", "name"] }, @@ -247,7 +247,7 @@ "properties": { "fileName": { "allOf": [ - { "$ref": "#/definitions/localFile" }, + { "$ref": "#/$defs/localFile" }, { "type": "string", "pattern": "\\.js$" @@ -362,15 +362,15 @@ "decodeInput": { "allOf": [ { - "$ref": "#/definitions/endDevicePayloadCodec/definitions/scriptInput" + "$ref": "#/$defs/endDevicePayloadCodec/definitions/scriptInput" }, { "type": "object", "properties": { - "bytes": { "$ref": "#/definitions/frmPayload" }, + "bytes": { "$ref": "#/$defs/frmPayload" }, "fPort": { "allOf": [ - { "$ref": "#/definitions/fPort" }, + { "$ref": "#/$defs/fPort" }, { "type": "integer", "minimum": 1 @@ -385,7 +385,7 @@ "decodeOutput": { "allOf": [ { - "$ref": "#/definitions/endDevicePayloadCodec/definitions/scriptOutput" + "$ref": "#/$defs/endDevicePayloadCodec/definitions/scriptOutput" }, { "type": "object", @@ -415,7 +415,7 @@ "encodeInput": { "allOf": [ { - "$ref": "#/definitions/endDevicePayloadCodec/definitions/scriptInput" + "$ref": "#/$defs/endDevicePayloadCodec/definitions/scriptInput" }, { "type": "object", @@ -431,15 +431,15 @@ "encodeOutput": { "allOf": [ { - "$ref": "#/definitions/endDevicePayloadCodec/definitions/scriptOutput" + "$ref": "#/$defs/endDevicePayloadCodec/definitions/scriptOutput" }, { "type": "object", "properties": { - "bytes": { "$ref": "#/definitions/frmPayload" }, + "bytes": { "$ref": "#/$defs/frmPayload" }, "fPort": { "allOf": [ - { "$ref": "#/definitions/fPort" }, + { "$ref": "#/$defs/fPort" }, { "type": "integer", "minimum": 1 @@ -477,12 +477,12 @@ "normalizeUplinkOutput": { "allOf": [ { - "$ref": "#/definitions/endDevicePayloadCodec/definitions/scriptOutput" + "$ref": "#/$defs/endDevicePayloadCodec/definitions/scriptOutput" }, { "type": "object", "properties": { - "data": { "$ref": "https://schema.thethings.network/devicerepository/1/payload/schema#/definitions/uplinkPayload" } + "data": { "$ref": "https://schema.thethings.network/devicerepository/1/payload/schema#/$defs/uplinkPayload" } } }, { @@ -506,7 +506,7 @@ "properties": { "uplinkDecoder": { "allOf": [ - { "$ref": "#/definitions/script" }, + { "$ref": "#/$defs/script" }, { "type": "object", "properties": { @@ -516,13 +516,13 @@ "type": "object", "properties": { "input": { - "$ref": "#/definitions/endDevicePayloadCodec/definitions/decodeInput" + "$ref": "#/$defs/endDevicePayloadCodec/definitions/decodeInput" }, "output": { - "$ref": "#/definitions/endDevicePayloadCodec/definitions/decodeOutput" + "$ref": "#/$defs/endDevicePayloadCodec/definitions/decodeOutput" }, "normalizedOutput": { - "$ref": "#/definitions/endDevicePayloadCodec/definitions/normalizeUplinkOutput" + "$ref": "#/$defs/endDevicePayloadCodec/definitions/normalizeUplinkOutput" } } } @@ -533,7 +533,7 @@ }, "downlinkEncoder": { "allOf": [ - { "$ref": "#/definitions/script" }, + { "$ref": "#/$defs/script" }, { "type": "object", "properties": { @@ -543,10 +543,10 @@ "type": "object", "properties": { "input": { - "$ref": "#/definitions/endDevicePayloadCodec/definitions/encodeInput" + "$ref": "#/$defs/endDevicePayloadCodec/definitions/encodeInput" }, "output": { - "$ref": "#/definitions/endDevicePayloadCodec/definitions/encodeOutput" + "$ref": "#/$defs/endDevicePayloadCodec/definitions/encodeOutput" } } } @@ -557,7 +557,7 @@ }, "downlinkDecoder": { "allOf": [ - { "$ref": "#/definitions/script" }, + { "$ref": "#/$defs/script" }, { "type": "object", "properties": { @@ -567,10 +567,10 @@ "type": "object", "properties": { "input": { - "$ref": "#/definitions/endDevicePayloadCodec/definitions/decodeInput" + "$ref": "#/$defs/endDevicePayloadCodec/definitions/decodeInput" }, "output": { - "$ref": "#/definitions/endDevicePayloadCodec/definitions/decodeOutput" + "$ref": "#/$defs/endDevicePayloadCodec/definitions/decodeOutput" } } } @@ -622,13 +622,13 @@ "description": "Maximum delay for the end device to answer a MAC request or confirmed downlink frame (seconds)" }, "pingSlotPeriod": { - "$ref": "#/definitions/pingSlotPeriod" + "$ref": "#/$defs/pingSlotPeriod" }, "pingSlotDataRateIndex": { - "$ref": "#/definitions/dataRateIndex" + "$ref": "#/$defs/dataRateIndex" }, "pingSlotFrequency": { - "$ref": "#/definitions/frequency" + "$ref": "#/$defs/frequency" }, "supportsClassC": { "type": "boolean", @@ -644,32 +644,32 @@ "description": "LoRaWAN version" }, "regionalParametersVersion": { - "$ref": "#/definitions/regionalParametersVersion" + "$ref": "#/$defs/regionalParametersVersion" }, "supportsJoin": { "type": "boolean", "description": "End device supports join (OTAA) or not (ABP)" }, "rx1Delay": { - "$ref": "#/definitions/rxDelay", + "$ref": "#/$defs/rxDelay", "description": "RX1 delay" }, "rx1DataRateOffset": { - "$ref": "#/definitions/rxDataRateOffset", + "$ref": "#/$defs/rxDataRateOffset", "description": "RX1 data rate offset" }, "rx2DataRateIndex": { - "$ref": "#/definitions/dataRateIndex", + "$ref": "#/$defs/dataRateIndex", "description": "RX2 data rate index" }, "rx2Frequency": { - "$ref": "#/definitions/frequency", + "$ref": "#/$defs/frequency", "description": "RX2 channel frequency" }, "factoryPresetFrequencies": { "type": "array", "items": { - "$ref": "#/definitions/frequency" + "$ref": "#/$defs/frequency" }, "uniqueItems": true, "description": "List of factory preset frequencies" @@ -912,16 +912,16 @@ }, "profiles": { "type": "object", - "propertyNames": { "$ref": "#/definitions/region" }, + "propertyNames": { "$ref": "#/$defs/region" }, "patternProperties": { "": { "type": "object", "properties": { - "id": { "$ref": "#/definitions/identifier" }, + "id": { "$ref": "#/$defs/identifier" }, "lorawanCertified": { "type": "boolean" }, - "codec": { "$ref": "#/definitions/identifier" } + "codec": { "$ref": "#/$defs/identifier" } }, "required": ["id"] } @@ -1212,6 +1212,7 @@ "South Korea", "Turkey", "United States", + "United Kingdom", "Other" ] } @@ -1240,10 +1241,10 @@ "photos": { "type": "object", "properties": { - "main": { "$ref": "#/definitions/photo" }, + "main": { "$ref": "#/$defs/photo" }, "other": { "type": "array", - "items": { "$ref": "#/definitions/photo" } + "items": { "$ref": "#/$defs/photo" } } }, "required": ["main"] @@ -1251,23 +1252,23 @@ "videos": { "type": "object", "properties": { - "main": { "$ref": "#/definitions/video" }, + "main": { "$ref": "#/$defs/video" }, "other": { "type": "array", - "items": { "$ref": "#/definitions/video" } + "items": { "$ref": "#/$defs/video" } } } }, "compliances": { "type": "object", "properties": { - "safety": { "$ref": "#/definitions/compliances" }, - "radioEquipment": { "$ref": "#/definitions/compliances" } + "safety": { "$ref": "#/$defs/compliances" }, + "radioEquipment": { "$ref": "#/$defs/compliances" } } } }, "required": ["name", "firmwareVersions"] } }, - "oneOf": [{ "$ref": "#/definitions/endDevice" }, { "$ref": "#/definitions/endDeviceProfile" }, { "$ref": "#/definitions/vendorsIndex" }, { "$ref": "#/definitions/vendorIndex" }] + "oneOf": [{ "$ref": "#/$defs/endDevice" }, { "$ref": "#/$defs/endDeviceProfile" }, { "$ref": "#/$defs/vendorsIndex" }, { "$ref": "#/$defs/vendorIndex" }] } diff --git a/vendor/accuwatch/3chbatteryvoltagesensor-1.png b/vendor/accuwatch/3chbatteryvoltagesensor-1.png new file mode 100644 index 0000000000..d2436a8f40 Binary files /dev/null and b/vendor/accuwatch/3chbatteryvoltagesensor-1.png differ diff --git a/vendor/accuwatch/3chbatteryvoltagesensor-codec.yaml b/vendor/accuwatch/3chbatteryvoltagesensor-codec.yaml new file mode 100644 index 0000000000..750fa2cfb1 --- /dev/null +++ b/vendor/accuwatch/3chbatteryvoltagesensor-codec.yaml @@ -0,0 +1,34 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://www.thethingsindustries.com/docs/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: 3chbatteryvoltagesensor.js +# # Downlink encoder encodes JSON object into a binary data downlink (optional) +# downlinkEncoder: +# fileName: windsensor.js +# examples: +# - description: Turn green +# input: +# data: +# led: green +# output: +# bytes: [1] +# fPort: 2 +# - description: Invalid color +# input: +# data: +# led: blue +# output: +# errors: +# - invalid LED color + +# # Downlink decoder decodes the encoded downlink message (optional, must be symmetric with downlinkEncoder) +# downlinkDecoder: +# fileName: windsensor.js +# examples: +# - description: Turn green +# input: +# fPort: 2 +# bytes: [1] +# output: +# data: +# led: green diff --git a/vendor/accuwatch/3chbatteryvoltagesensor-profile.yaml b/vendor/accuwatch/3chbatteryvoltagesensor-profile.yaml new file mode 100644 index 0000000000..e6cbc7fc49 --- /dev/null +++ b/vendor/accuwatch/3chbatteryvoltagesensor-profile.yaml @@ -0,0 +1,47 @@ +# LoRaWAN MAC version: 1.0, 1.0.1, 1.0.2, 1.0.3, 1.0.4 or 1.1 +macVersion: '1.0.3' +# LoRaWAN Regional Parameters version. Values depend on the LoRaWAN version: +# 1.0: TS001-1.0 +# 1.0.1: TS001-1.0.1 +# 1.0.2: RP001-1.0.2 or RP001-1.0.2-RevB +# 1.0.3: RP001-1.0.3-RevA +# 1.0.4: RP002-1.0.0 or RP002-1.0.1 +# 1.1: RP001-1.1-RevA or RP001-1.1-RevB +regionalParametersVersion: 'RP001-1.0.3-RevA' + +# Whether the end device supports join (OTAA) or not (ABP) +supportsJoin: true +# If your device is an ABP device (supportsJoin is false), uncomment the following fields: +# RX1 delay +#rx1Delay: 5 +# RX1 data rate offset +#rx1DataRateOffset: 0 +# RX2 data rate index +#rx2DataRateIndex: 0 +# RX2 frequency (MHz) +#rx2Frequency: 869.525 +# Factory preset frequencies (MHz) +#factoryPresetFrequencies: [868.1, 868.3, 868.5, 867.1, 867.3, 867.5, 867.7, 867.9] + +# Maximum EIRP +maxEIRP: 16 +# Whether the end device supports 32-bit frame counters +supports32bitFCnt: true + +# Whether the end device supports class B +supportsClassB: false +# If your device supports class B, uncomment the following fields: +# Maximum delay for the end device to answer a MAC request or confirmed downlink frame (seconds) +#classBTimeout: 60 +# Ping slot period (seconds) +#pingSlotPeriod: 128 +# Ping slot data rate index +#pingSlotDataRateIndex: 0 +# Ping slot frequency (MHz). Set to 0 if the band supports ping slot frequency hopping. +#pingSlotFrequency: 869.525 + +# Whether the end device supports class C +supportsClassC: false +# If your device supports class C, uncomment the following fields: +# Maximum delay for the end device to answer a MAC request or confirmed downlink frame (seconds) +#classCTimeout: 60 diff --git a/vendor/accuwatch/3chbatteryvoltagesensor.js b/vendor/accuwatch/3chbatteryvoltagesensor.js new file mode 100644 index 0000000000..12d5a1f3d8 --- /dev/null +++ b/vendor/accuwatch/3chbatteryvoltagesensor.js @@ -0,0 +1,41 @@ +function decodeUplink(input) { + // Helper function to convert bytes to float + function bytesToFloat(bytes) { + var bits = (bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0]; + var sign = (bits & 0x80000000) ? -1 : 1; + var exponent = ((bits >> 23) & 0xFF) - 127; + var significand = (bits & ~(-1 << 23)); + + if (exponent === 128) + return sign * ((significand) ? NaN : Infinity); + + if (exponent === -127) { + if (significand === 0) return sign * 0.0; + exponent = -126; + significand /= (1 << 22); + } else significand = (significand | (1 << 23)) / (1 << 23); + + return sign * significand * Math.pow(2, exponent); + } + + // Decode each float from the 12-byte payload + var sensor1Voltage = bytesToFloat(input.bytes.slice(0, 4)); + var sensor2Voltage = bytesToFloat(input.bytes.slice(4, 8)); + var sensor3Voltage = bytesToFloat(input.bytes.slice(8, 12)); + + // Round the float values to 2 decimal places + sensor1Voltage = Math.round(sensor1Voltage * 100) / 100; + sensor2Voltage = Math.round(sensor2Voltage * 100) / 100; + sensor3Voltage = Math.round(sensor3Voltage * 100) / 100; + + // Return decoded values + return { + data: { + sensor1Voltage: sensor1Voltage.toFixed(2), + sensor2Voltage: sensor2Voltage.toFixed(2), + sensor3Voltage: sensor3Voltage.toFixed(2) + }, + warnings: [], + errors: [] + }; +} \ No newline at end of file diff --git a/vendor/accuwatch/3chbatteryvoltagesensor.png b/vendor/accuwatch/3chbatteryvoltagesensor.png new file mode 100644 index 0000000000..2fdd89c65f Binary files /dev/null and b/vendor/accuwatch/3chbatteryvoltagesensor.png differ diff --git a/vendor/accuwatch/3chbatteryvoltagesensor.yaml b/vendor/accuwatch/3chbatteryvoltagesensor.yaml new file mode 100644 index 0000000000..62abfbc293 --- /dev/null +++ b/vendor/accuwatch/3chbatteryvoltagesensor.yaml @@ -0,0 +1,143 @@ +name: LoraWan 3ch Voltage Sensor # Device name can not contain the vendor name +description: This custom-designed 3-Channel Battery Voltage Sensor is engineered to monitor the voltage of up to three separate batteries with a shared ground connection, making it ideal for applications where multiple batteries need simultaneous tracking. Constructed with an IP67-rated waterproof enclosure, it is built to withstand harsh environments, including marine and outdoor applications, where water and dust resistance are crucial. + +# Hardware versions (optional, use when you have revisions) +hardwareVersions: + - version: '1.0' + numeric: 1 + +# Firmware versions (at least one is mandatory) +firmwareVersions: + - # Firmware version + version: '1.0' + numeric: 1 + # Corresponding hardware versions (optional) + hardwareVersions: + - '1.0' + + # LoRaWAN Device Profiles per region + # Supported regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, + profiles: + EU863-870: + # Optional identifier of the vendor of the profile. When you specify the vendorID, the profile is loaded from + # the vendorID's folder. This allows you to reuse profiles from module or LoRaWAN end device stack vendors. + # If vendorID is empty, the current vendor ID is used. In this example, the vendorID is the current vendor ID, + # which is verbose. + vendorID: accuwatch + # Identifier of the profile (lowercase, alphanumeric with dashes, max 36 characters) + id: 3chbatteryvoltagesensor-profile + lorawanCertified: true + codec: 3chbatteryvoltagesensor-codec + +# Type of device (optional) +# Valid values are: devkit, module, cots +deviceType: cots + +# Sensors that this device features (optional) +# Valid values are: +# 4-20 ma, accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co, co2, conductivity, current, digital input, +# digital output, dissolved oxygen, distance, dust, energy, gps, gyroscope, h2s, hall effect, humidity, iaq, infrared, leaf wetness, level, +# light, lightning, link, magnetometer, moisture, motion, nfc, no, no2, o3, occupancy, optical meter, particulate matter, ph, pir, +# pm2.5, pm10, potentiometer, power, precipitation, pressure, proximity, pulse count, pulse frequency, radar, rainfall, reed switch, rssi, +# sap flow, smart valve, smoke, snr, so2, solar radiation, sound, strain, surface temperature, switch, temperature, tilt, time, turbidity, +# tvoc, uv, vapor pressure, velocity, vibration, voltage, water potential, water, weight, wifi ssid, wind direction, wind speed. +sensors: + - voltage + +# Additional radios that this device has (optional) +# Valid values are: ble, nfc, wifi, cellular. +additionalRadios: + - wifi + +# Dimensions in mm (optional) +# Use width, height, length and/or diameter +dimensions: + width: 65 + length: 115 + height: 40 + +# Weight in grams (optional) +weight: 190 + +# Battery information (optional) +# battery: +# replaceable: true +# type: AA + +# # Operating conditions (optional) +# operatingConditions: +# # Temperature (Celsius) +# temperature: +# min: -30 +# max: 85 +# # Relative humidity (fraction of 1) +# relativeHumidity: +# min: 0 +# max: 0.97 + +# IP rating (optional) +ipCode: IP67 + +# Key provisioning (optional) +# Valid values are: custom (user can configure keys), join server and manifest. +keyProvisioning: + - join server + +# Key programming (optional) +# Valid values are: bluetooth, nfc, wifi, ethernet (via a webpage), serial (when the user has a serial interface to set the keys) +# and firmware (when the user should change the firmware to set the keys). +# keyProgramming: +# - serial +# - firmware + +# Key security (optional) +# Valid values are: none, read protected and secure element. +keySecurity: read protected + +# Firmware programming (optional) +# Valid values are: serial (when the user has a serial interface to update the firmware), ethernet, fuota lorawan (when the device +# supports LoRaWAN FUOTA via standard interfaces) and fuota other (other wireless update mechanism). +firmwareProgramming: + # - serial + - fuota lorawan + +# Product and data sheet URLs (optional) +productURL: https://accuwatch.nl/LoRaWAN-voltage-sensor +# dataSheetURL: https://accuwatch.nl/LoRaWAN-voltage-sensor/datasheet.pdf +# Link to simple, easy onboarding instructions for the device (optional). +# Please do not use this for marketing or overly technical documents like a data sheet. +# onboardingGuideURL: https://www.thethingsindustries.com/docs/devices/models/windsensor + +# Commercial information +resellerURLs: + - name: 'AccuWatch' + region: # valid regions are: Argentina, Australia, Brazil, Canada, China, European Union, India, Indonesia. + # # Japan, Mexico, Russia, Saudi Arabia, South Africa, South Korea, Turkey, United States, Other + - European Union + url: https://accuwatch.nl/ + +# Photos +photos: + main: 3chbatteryvoltagesensor.png # Image needs to have a transparent background + other: + - 3chbatteryvoltagesensor-1.png # Image needs to have a transparent background + +# Youtube or Vimeo Video (optional) +# videos: +# main: https://www.youtube.com/watch?v=JHzxcD2oEn8 + +# Regulatory compliances (optional) +# compliances: +# safety: +# - body: IEC +# norm: EN +# standard: 62368-1 +# radioEquipment: +# - body: ETSI +# norm: EN +# standard: 301 489-1 +# version: 2.2.0 +# - body: ETSI +# norm: EN +# standard: 301 489-3 +# version: 2.1.0 diff --git a/vendor/accuwatch/accuwatch.png b/vendor/accuwatch/accuwatch.png new file mode 100644 index 0000000000..d2a788e390 Binary files /dev/null and b/vendor/accuwatch/accuwatch.png differ diff --git a/vendor/accuwatch/index.yaml b/vendor/accuwatch/index.yaml new file mode 100644 index 0000000000..53e1c440b3 --- /dev/null +++ b/vendor/accuwatch/index.yaml @@ -0,0 +1,3 @@ +endDevices: + # Unique identifier of the end device (lowercase, alphanumeric with dashes, max 36 characters) + - 3chbatteryvoltagesensor # look in 3chbatteryvoltagesensor.yaml for the end device definition diff --git a/vendor/accuwatch/logo.png b/vendor/accuwatch/logo.png new file mode 100644 index 0000000000..d2a788e390 Binary files /dev/null and b/vendor/accuwatch/logo.png differ diff --git a/vendor/browan/tbdw100-codec.yaml b/vendor/browan/tbdw100-codec.yaml index dc4035d15d..676b0d264a 100644 --- a/vendor/browan/tbdw100-codec.yaml +++ b/vendor/browan/tbdw100-codec.yaml @@ -12,6 +12,14 @@ uplinkDecoder: status: 0 temperatureBoard: 21 time: 0 + normalizedOutput: + data: + - action: + motion: + detected: false + air: + temperature: 21 + battery: 3.6 - description: Unknown FPort input: fPort: 42 diff --git a/vendor/browan/tbdw100.js b/vendor/browan/tbdw100.js index 4edc6ebd79..a8f1e65616 100644 --- a/vendor/browan/tbdw100.js +++ b/vendor/browan/tbdw100.js @@ -24,3 +24,19 @@ function decodeUplink(input) { }; } } + +function normalizeUplink(input) { + return { + data: { + action: { + motion: { + detected: input.data.status > 0, + } + }, + air: { + temperature: input.data.temperatureBoard, + }, + battery: input.data.battery, + } + }; +} diff --git a/vendor/browan/tbms100-codec.yaml b/vendor/browan/tbms100-codec.yaml index 63a1c88d15..d294b1a0f1 100644 --- a/vendor/browan/tbms100-codec.yaml +++ b/vendor/browan/tbms100-codec.yaml @@ -12,6 +12,14 @@ uplinkDecoder: status: 1 temperatureBoard: 20 time: 4 + normalizedOutput: + data: + - action: + motion: + detected: true + air: + temperature: 20 + battery: 3.6 - description: Unknown FPort input: fPort: 42 diff --git a/vendor/browan/tbms100.js b/vendor/browan/tbms100.js index 64bc65693c..9953606462 100644 --- a/vendor/browan/tbms100.js +++ b/vendor/browan/tbms100.js @@ -24,3 +24,19 @@ function decodeUplink(input) { }; } } + +function normalizeUplink(input) { + return { + data: { + action: { + motion: { + detected: input.data.status > 0, + } + }, + air: { + temperature: input.data.temperatureBoard, + }, + battery: input.data.battery, + }, + }; +} diff --git a/vendor/dnt/dnt-lw-dis-back-view.png b/vendor/dnt/dnt-lw-dis-back-view.png new file mode 100644 index 0000000000..a49836fc71 Binary files /dev/null and b/vendor/dnt/dnt-lw-dis-back-view.png differ diff --git a/vendor/dnt/dnt-lw-dis-codec.yaml b/vendor/dnt/dnt-lw-dis-codec.yaml new file mode 100644 index 0000000000..b2f800d142 --- /dev/null +++ b/vendor/dnt/dnt-lw-dis-codec.yaml @@ -0,0 +1,39 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://www.thethingsindustries.com/docs/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: dnt-lw-dis.js + # Examples (optional) + examples: + - description: Button Message + input: + fPort: 10 + bytes: [0x9A, 0x04, 0x00, 0x06, 0x4F] + output: + data: + ToF: + distance: 1615 + status: 0 + distance: 1615 + reason: 'button' + voltage: 3040 + - description: Cyclic Message + input: + fPort: 10 + bytes: [0x9C, 0x01, 0x00, 0x06, 0x47] + output: + data: + ToF: + distance: 1607 + status: 0 + distance: 1607 + reason: 'cycle' + voltage: 3060 + - description: Sabotage Message + input: + fPort: 10 + bytes: [0x9B, 0x03, 0x44] + output: + data: + device_orientation: 'Sideways' + sabotage_notification: 1 + voltage: 3050 diff --git a/vendor/dnt/dnt-lw-dis-front-view.png b/vendor/dnt/dnt-lw-dis-front-view.png new file mode 100644 index 0000000000..5f2c3e8182 Binary files /dev/null and b/vendor/dnt/dnt-lw-dis-front-view.png differ diff --git a/vendor/dnt/dnt-lw-dis-side-view-key.png b/vendor/dnt/dnt-lw-dis-side-view-key.png new file mode 100644 index 0000000000..5bb8751068 Binary files /dev/null and b/vendor/dnt/dnt-lw-dis-side-view-key.png differ diff --git a/vendor/dnt/dnt-lw-dis.js b/vendor/dnt/dnt-lw-dis.js new file mode 100644 index 0000000000..30d0ac6846 --- /dev/null +++ b/vendor/dnt/dnt-lw-dis.js @@ -0,0 +1,301 @@ +// Payloadparser dnt-LW-DIS1 +// v1.0.1 + +// messages +var POWER_ON = 0; +var CYCLE = 1; +var TEMPERATURE_NOTIFICATION = 2; +var SABOTAGE = 3; +var BUTTON = 4; +var DOWNLINK_ERROR = 5; +var TEMPERATURE = 6; + +var FEATURE_TEMPERATURE_ALARM = 1; +var FEATURE_SABOTAGE_ALARM = 2; + +var GET_FEATURES = 21; +var GET_CYCLE = 22; +var GET_TOF_MODE = 23; +var GET_TOF_ROI = 24; +var GET_TEMPERATURE_THRES = 25; +var GET_TEMPERATURE_RISE = 26; +var GET_SABOTAGE_ANGLE = 27; + +var GET_MANUAL_BUTTON_RESET = 245; +var GET_TIME_TILL_REJOIN = 247 +var GET_LORAWAN_DATARATE = 248; +var GET_REJOIN_CONFIG = 250; +var GET_ALL_CONFIG = 252; +var GET_DEVICE_INFO = 255; + + +function decodeUplink(input){ + + bytes = input.bytes; + port = input.fPort; + + var power_on = 0; + var payload = bytes; + var decoded = {}; + var index = 0; + + decoded.voltage = (payload[index++] * 10) + 1500; + + var next_field = 0; + var ToF = {}; + var config = {}; + + while( index < payload.length ){ + + next_field = payload[index++]; + + if( next_field == POWER_ON ) + { + power_on = 1; + decoded.reason = "Start-up"; + + if( payload[index++] == GET_DEVICE_INFO ) + { + decoded.device_info = read_device_info(payload.slice(index, index + 10)); + index += 10; + } + } + else if( next_field == CYCLE || next_field == BUTTON ) + { + if( !power_on ){ + decoded.reason = (next_field == CYCLE) ? "cycle": "button"; + } + + ToF.status = payload[index++]; + ToF.distance = int16(payload.slice(index, index + 2)); + decoded.distance =int16 (payload.slice(index, index + 2)); + index += 2; + + decoded.ToF = ToF; + } + else if( next_field == TEMPERATURE_NOTIFICATION ) + { + decoded.temperature_notification = payload[index++]; + decoded.temperature = int16(payload.slice(index, index + 2)); + index += 2; + + if( decoded.temperature & 0x8000 ){ + decoded.temperature -= 0x10000; + } + + decoded.temperature /= 10; + } + else if( next_field == SABOTAGE ) + { + if( payload[index] & 0x40 ){ + decoded.sabotage_notification = 1; + } + else if( payload[index] & 0x80 ){ + decoded.sabotage_notification = 2; + } + else{ + decoded.sabotage_notification = 0; + } + + var orientation = payload[index++] & ~0x40; + + if( orientation & 0x01 ){ + decoded.device_orientation = "Up"; + } + else if( orientation & 0x02 ){ + decoded.device_orientation = "Down"; + } + else if( orientation & 0x04 ){ + decoded.device_orientation = "Sideways"; + } + + if( orientation & 0x08 ){ + decoded.device_orientation += "|Tilted"; + } + } + else if( next_field == DOWNLINK_ERROR ) + { + var error_field = int16(payload.slice(index, index + 2)); + index += 2; + + decoded.downlink_error = ""; + + for( var i = 0; i < 16; i++ ) + { + if( error_field & (1 << i) ) + { + decoded.downlink_error += ("field_" + i.toString(10)); + } + } + } + else if( next_field == TEMPERATURE ) + { + decoded.temperature = int16(payload.slice(index, index + 2)); + index += 2; + + if( decoded.temperature & 0x8000 ){ + decoded.temperature -= 0x10000; + } + + decoded.temperature /= 10; + } + else if( next_field == GET_FEATURES ) + { + decoded.application_features = read_device_features(bytes[index++]); + } + else if( next_field == GET_CYCLE ) + { + decoded.measurement_cycle = int16(bytes.slice(index, index + 2)); + index += 2; + } + else if( next_field == GET_TOF_MODE ) + { + decoded.TOF_mode = payload[index++]; + } + else if( next_field == GET_TOF_ROI ) + { + decoded.TOF_roi = payload[index++]; + } + else if( next_field == GET_TEMPERATURE_THRES ) + { + decoded.temperature_notification_threshold = payload[index++]; + } + else if( next_field == GET_TEMPERATURE_RISE ) + { + decoded.temperature_notification_rise = payload[index++]; + } + else if( next_field == GET_SABOTAGE_ANGLE ) + { + decoded.sabotage_angle = payload[index++]; + } + else if( next_field == GET_MANUAL_BUTTON_RESET ) + { + decoded.manual_button_reset_enabled = payload[index++]; + } + else if( next_field == GET_TIME_TILL_REJOIN ) + { + decoded.time_till_rejoin = int24(payload.slice(index, index + 3)); + index += 3; + } + else if( next_field == GET_LORAWAN_DATARATE ) + { + decoded.datarate_config = payload[index++]; + } + else if( next_field == GET_REJOIN_CONFIG ) + { + decoded.rejoin_config = int16(payload.slice(index, index + 2)); + index += 2; + } + else if( next_field == GET_ALL_CONFIG ) + { + decoded.config = read_device_config(payload.slice(index, index + 15)); + index += 15; + } + else if( next_field == GET_DEVICE_INFO ) + { + decoded.device_info = read_device_info(payload.slice(index, index + 10)); + index += 10; + } + else + { + break; + } + + } + + return { + data:decoded + }; +} + + +function int16(bytes){ + return ((bytes[0] << 8) | bytes[1]); +} + +function int24(bytes){ + return ((bytes[0] << 16) | (bytes[1] << 8) | bytes[2]); +} + +function int32(bytes){ + return ((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]); +} + +function read_device_info(bytes){ + var index = 0; + var info = {} + + info.device_type = (bytes[index++] << 16); + info.device_type += (bytes[index++] << 8); + info.device_type += bytes[index++]; + + info.fw_version = bytes[index++] + "."; + info.fw_version += bytes[index++] + "."; + info.fw_version += bytes[index++]; + + info.bl_version = bytes[index++] + "."; + info.bl_version += bytes[index++] + "."; + info.bl_version += bytes[index++]; + + info.hw_version = bytes[index++]; + + return info; +} + +function read_device_features(byte){ + var app_features = {}; + + app_features.distance_measurement = 1; + app_features.temperature_notification = 0; + app_features.sabotage_notification = 0; + + if( byte & FEATURE_TEMPERATURE_ALARM ){ + app_features.temperature_notification = 1; + } + if( byte & FEATURE_SABOTAGE_ALARM ){ + app_features.sabotage_notification = 1; + } + + return app_features +} + +function read_device_config(bytes){ + var index = 0; + var config_tmp = {}; + + var lorawan = {}; + var rejoin_config = {}; + var ToF = {}; + var notifications = {}; + + config_tmp.application_features = read_device_features(bytes[index++]); + + config_tmp.measurement_cycle = int16(bytes.slice(index, index + 2)); + index += 2; + + ToF.mode = bytes[index++]; + ToF.roi = bytes[index++]; + + notifications.sabotage_angle = bytes[index++]; + + config_tmp.manual_button_reset_enabled = bytes[index++]; + + lorawan.time_till_rejoin = int24(bytes.slice(index, index + 3)); + index += 3; + + lorawan.datarate_config = bytes[index++]; + + rejoin_config.one_time = int16(bytes.slice(index, index + 2))& 0x8000 ? 1 : 0; + rejoin_config.cycle = int16(bytes.slice(index, index + 2)) & ~0x8000; + index += 2; + + notifications.temperature_notification_threshold = bytes[index++]; + notifications.temperature_notification_rise = bytes[index++]; + + lorawan.rejoin_config = rejoin_config; + config_tmp.notifications = notifications; + config_tmp.tof_config = ToF; + config_tmp.lorawan = lorawan; + + return config_tmp; +} diff --git a/vendor/dnt/dnt-lw-dis.yaml b/vendor/dnt/dnt-lw-dis.yaml new file mode 100644 index 0000000000..52f965a5b5 --- /dev/null +++ b/vendor/dnt/dnt-lw-dis.yaml @@ -0,0 +1,110 @@ +name: LoRaWAN® distance sensor 1 +description: The dnt-LW-DIS1 is a robust LoRaWAN® device for measuring distances outdoors. With its measuring range of 4 to 360 cm and the easy-to-install housing, it can be used for a wide range of applications. + +# Hardware versions (optional, use when you have revisions) +hardwareVersions: + - version: '1.0' + numeric: 1 + +# Firmware versions (at least one is mandatory) +firmwareVersions: + - # Firmware version + version: '1.0.5' + numeric: 1 + # Corresponding hardware versions (optional) + hardwareVersions: + - '1.0' + + # Firmware features (optional) + # Valid values are: remote rejoin (trigger a join from the application layer), transmission interval (configure how + # often he device sends a message). + features: + - transmission interval + + # LoRaWAN Device Profiles per region + # Supported regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, + # RU864-870 + profiles: + EU863-870: + # Optional identifier of the vendor of the profile. When you specify the vendorID, the profile is loaded from + # the vendorID's folder. This allows you to reuse profiles from module or LoRaWAN end device stack vendors. + # If vendorID is empty, the current vendor ID is used. In this example, the vendorID is the current vendor ID, + # which is verbose. + # Identifier of the profile (lowercase, alphanumeric with dashes, max 36 characters) + id: dnt-profile + lorawanCertified: true + codec: dnt-lw-dis-codec + +# Sensors that this device features (optional) +# Valid values are: +# 4-20 ma, accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co, co2, conductivity, +# current, digital input, dissolved oxygen, distance, dust, energy, gps, gyroscope, h2s, humidity, iaq, level, light, +# lightning, link, magnetometer, moisture, motion, no, no2, o3, particulate matter, ph, pir, pm2.5, pm10, potentiometer, +# power, precipitation, pressure, proximity, pulse count, pulse frequency, radar, rainfall, rssi, smart valve, snr, so2, +# solar radiation, sound, strain, surface temperature, temperature, tilt, time, tvoc, uv, vapor pressure, velocity, +# vibration, voltage, water potential, water, weight, wifi ssid, wind direction, wind speed. +sensors: + - distance + - accelerometer + - temperature + +# Dimensions in mm (optional) +# Use width, height, length and/or diameter +dimensions: + width: 116 + length: 23 + height: 70 + +# Weight in grams (optional) +weight: 97 + +# Operating conditions (optional) +operatingConditions: + # Temperature (Celsius) + temperature: + min: -20 + max: 55 + # Relative humidity (fraction of 1) + relativeHumidity: + min: 0 + max: 0.97 + +# IP rating (optional) +ipCode: IP67 + +# Key provisioning (optional) +# Valid values are: custom (user can configure keys), join server and manifest. +keyProvisioning: + - join server + +# Key programming (optional) +# Valid values are: bluetooth, nfc, wifi, serial (when the user has a serial interface to set the keys) +# and firmware (when the user should change the firmware to set the keys). +keyProgramming: + - firmware + +# Firmware programming (optional) +# Valid values are: serial (when the user has a serial interface to update the firmware), fuota lorawan (when the device +# supports LoRaWAN FUOTA via standard interfaces) and fuota other (other wireless update mechanism). +firmwareProgramming: + - serial + - fuota other + +# Product and data sheet URLs (optional) +productURL: https://www.dnt.de/ + +# Commercial information +resellerURLs: + - name: 'ELVshop' + region: + - European Union + url: https://www.dnt.de/Produkte/dnt-LoRaWAN-Distanzsensor-dnt-LW-DIS1/ +msrp: + EUR: 164.95 + +# Photos +photos: + main: dnt-lw-dis-front-view.png + other: + - dnt-lw-dis-back-view.png + - dnt-lw-dis-side-view-key.png diff --git a/vendor/dnt/dnt-lw-etrv-c-codec.yaml b/vendor/dnt/dnt-lw-etrv-c-codec.yaml new file mode 100644 index 0000000000..29c2a23d1b --- /dev/null +++ b/vendor/dnt/dnt-lw-etrv-c-codec.yaml @@ -0,0 +1,42 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://www.thethingsindustries.com/docs/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: dnt-lw-etrv-c.js + # Examples (optional) + examples: + - description: Status Uplink + input: + fPort: 10 + bytes: [0x04, 0xF4, 0xB6, 0x28, 0x28, 0x00, 0x04] + output: + data: + battery_cover_locked: + unit: 'bool' + value: true + battery_voltage: + unit: 'mV' + value: '3320' + heating_control: + mode: + active_mode: + unit: 'string' + value: 'Manu Temp' + holiday: + is_pending: + unit: 'bool' + value: false + window_open_detection: + is_open: + unit: 'bool' + value: false + room_temperature: + unit: '°C' + value: '20.0' + set_point_temperature: + unit: '°C' + value: '20.0' + valve_position: + unit: '%' + value: '0.0' + errors: [] + warnings: [] diff --git a/vendor/dnt/dnt-lw-etrv-c.js b/vendor/dnt/dnt-lw-etrv-c.js new file mode 100644 index 0000000000..6b9fc286ac --- /dev/null +++ b/vendor/dnt/dnt-lw-etrv-c.js @@ -0,0 +1,850 @@ +var payload_parser_version = [1,1,0]; + + +const ERROR_CODE = +{ + 0: "VALVE_DRIVE_READY", + 1: "VALVE_DRIVE_UNINITIALIZED", + 2: "VALVE_TO_TIGHT", + 3: "ADJUST_RANGE_OVERSIZE", + 4: "ADJUST_RANGE_UNDERSIZE", + 5: "ADAPTION_RUN_CALC_ERROR" +}; + +const ACTIVE_MODE = +{ + 0: "Manu Temp", + 1: "Manu_Pos", + 2: "Auto", + 3: "Emergency", + 4: "Frost Protection", + 5: "Boost", + 6: "Window Open", + 7: "Holiday" +}; + +const WEEKDAY = +{ + 0: "SUNDAY", + 1: "MONDAY", + 2: "TUESDAY", + 3: "WEDNESDAY", + 4: "THURSDAY", + 5: "FRIDAY", + 6: "SATURDAY" +}; + +const MONTH = +{ + + 0: "JANUARY", + 1: "FEBRUARY", + 2: "MARCH", + 3: "APRIL", + 4: "MAY", + 5: "JUNE", + 6: "JULY", + 7: "AUGUST", + 8: "SEPTEMBER", + 9: "OCTOBER", + 10: "NOVEMBER", + 11: "DECEMBER" +}; + +const WEEK_OF_MONTH = +{ + 1: "1st", + 2: "2nd", + 3: "3rd", + 4: "4th", + 5: "last" +}; + + +const DATA_RATE = +{ + 0: "Adaptive Data Rate", + 1: "DR 0", + 2: "DR 1", + 3: "DR 2", + 4: "DR 3", + 5: "DR 4", + 6: "DR 5", +}; + + +const REJOIN_BEHAVIOR = +{ + true: "Cyclic Rejoin", + false: "Single Rejoin", +}; + + +const COMMAND_ID = +{ + 0: "COMMAND_ID_GET_STATUS_INTERVAL", + 1: "COMMAND_ID_SET_STATUS_INTERVAL", + 2: "COMMAND_ID_GET_STATUS_PARAMETER_TX_ENABLE_REGISTER", + 3: "COMMAND_ID_SET_STATUS_PARAMETER_TX_ENABLE_REGISTER", + 4: "COMMANF_ID_GET_STATUS", + 5: "COMMAND_ID_GET_BATTERY_VOLTAGE", + 6: "COMMAND_ID_GET_BATTTERY_COVER_LOCK_STATUS", + 7: "COMMAND_ID_GET_ERROR_CODE", + 8: "COMMAND_ID_GET_DEVICE_TIME", + 9: "COMMAND_ID_SET_DEVICE_TIME", + 10: "COMMAND_ID_GET_DEVICE_TIME_CONFIG", + 11: "COMMAND_ID_SET_DEVICE_TIME_CONFIG", + 12: "COMMAND_ID_GET_MODE_STATUS", + 13: "COMMAND_ID_SET_MANU_TEMPERATURE_MODE", + 14: "COMMAND_ID_SET_MANU_POSITIONING_MODE", + 15: "COMMAND_ID_SET_AUTO_MODE", + 16: "COMMAND_ID_SET_HOLIDAY_MODE", + 17: "COMMAND_ID_SET_BOOST_MODE", + 18: "COMMAND_ID_GET_HOLIDAY_MODE_CONFIG", + 19: "COMMAND_ID_DISABLE_HOLIDAY_MODE", + 20: "COMMAND_ID_GET_BOOST_CONFIG", + 21: "COMMAND_ID_SET_BOOST_CONFIG", + 22: "COMMAND_ID_GET_WEEK_PROGRAM", + 23: "COMMAND_ID_SET_WEEK_PROGRAM", + 24: "COMMAND_ID_GET_VALVE_POSITION", + 25: "COMMAND_ID_GET_VALVE_SET_POINT_POSITION", + 26: "COMMAND_ID_SET_VALVE_SET_POINT_POSITION", + 27: "COMMAND_ID_GET_VALVE_OFFSET", + 28: "COMMAND_ID_SET_VALVE_OFFSET", + 29: "COMMAND_ID_GET_VALVE_MAXIMUM_POSITION", + 30: "COMMAND_ID_SET_VALVE_MAXIMUM_POSITION", + 31: "COMMAND_ID_GET_VALVE_EMERGENCY_POSITION", + 32: "COMMAND_ID_SET_VALVE_EMERGENCY_POSITION", + 33: "COMMAND_ID_GET_SET_POINT_TEMPERATURE", + 34: "COMMAND_ID_SET_SET_POINT_TEMPERATURE", + 35: "COMMAND_ID_SET_EXTERNAL_ROOM_TEMPERATURE", + 36: "COMMAND_ID_GET_TEMPERATURE_OFFSET", + 37: "COMMAND_ID_SET_TEMPERATURE_OFFSET", + 38: "COMMAND_ID_GET_HEATING_CNTRL_INPUT_ROOM_TEMPERATURE", + 39: "COMMAND_ID_GET_HEATING_CNTRL_INPUT_SET_POINT_TEMPERATURE", + 40: "COMMAND_ID_GET_HEATING_CNTRL_CONFIG", + 41: "COMMAND_ID_SET_HEATING_CNTRL_CONFIG", + 42: "COMMAND_ID_GET_HEATING_CNTRL_STATIC_GAINS", + 43: "COMMAND_ID_SET_HEATING_CNTRL_STATTC_GAINS", + 44: "COMMAND_ID_GET_HEATING_CNTRL_INPUT_GAINS", + 45: "COMMAND_ID_RESET_HEATING_CONTROLLER_ADAPTIVE_GAINS", + 46: "COMMAND_ID_GET_WINDOW_OPEN_STATUS", + 47: "COMMAND_ID_SET_WINDWO_OPEN_STATUS", + 48: "COMMAND_ID_GET_WINDOW_OPEN_DETECTION_CONFIG", + 49: "COMMAND_ID_SET_WINDOW_OPEN_DETECTION_CONFIG", + 50: "COMMAND_ID_GET_DECALCIFICATION_CONFIG", + 51: "COMMAND_ID_SET_DECALCIFICATION_CONFIG", + 52: "COMMAND_ID_PERFORM_ADAPTION_RUN", + 53: "COMMAND_ID_PERFORM_DECALCIFICATION", + 54: "COMMAND_ID_COMMAND_FAILED", + 55: "COMMAND_ID_SET_BUTTON_ACTION", + 56: "COMMAND_ID_GET_BUTTON_ACTION", + 57: "COMMAND_ID_SET_HARDWARE_FACTORY_RESET_LOCK", + 58: "COMMAND_ID_GET_HARDWARE_FACTORY_RESET_LOCK", + 119: "COMMAND_ID_GET_REMAINING_TIME_UNTIL_REJOIN", + 120: "COMMAND_ID_GET_DATA_RATE", + 121: "COMMAND_ID_SET_DATA_RATE", + 122: "COMMAND_ID_GET_REJOIN_BEHAVIOR", + 123: "COMMAND_ID_SET_REJOIN_BEHAVIOR", + 124: "COMMAND_ID_GET_ALL_CONFIG", + 125: "COMMAND_ID_PERFORM_FACTORY_RESET", + 126: "COMMAND_ID_PERFORM_SOFTWARE_RESET", + 127: "COMMAND_ID_GET_VERSION" +}; + +function decodeUplink(input) { + + var size = input.bytes.length; + + + const data = {}; + + var bufferSize = input.bytes.length; + var idx = 0; + commandId = -1; + + + while(idx < bufferSize) + { + commandId = input.bytes[idx++]; + + switch(COMMAND_ID[commandId]) + { + case "COMMAND_ID_GET_STATUS_INTERVAL": + data.status_report = data.status_report || {}; + data.status_report.interval = data.status_report.interval || {}; + data.status_report.interval.value = input.bytes[idx++] * 30 + 30; + data.status_report.interval.unit ="s"; + break; + + case "COMMAND_ID_GET_STATUS_PARAMETER_TX_ENABLE_REGISTER": + data.radio = data.radio || {}; + data.radio.status_report = data.radio.status_report || {}; + data.radio.status_report.parameter_tx_enable_reg = data.radio.status_report.parameter_tx_enable_reg || {}; + + var status_param_tx_enable_reg = input.bytes[idx++]; + + data.radio.status_report.parameter_tx_enable_reg.battery_voltage_enabled = !!(status_param_tx_enable_reg & (1 << 7)); + data.radio.status_report.parameter_tx_enable_reg.room_temperature_enabled = !!(status_param_tx_enable_reg & (1 << 6)); + data.radio.status_report.parameter_tx_enable_reg.set_point_temperature_enabled = !!(status_param_tx_enable_reg & (1 << 5)); + data.radio.status_report.parameter_tx_enable_reg.valve_position_enabled = !!(status_param_tx_enable_reg & (1 << 4)); + data.radio.status_report.parameter_tx_enable_reg.controller_gains_enabled = !!(status_param_tx_enable_reg & (1 << 3)); + data.radio.status_report.parameter_tx_enable_reg.device_flags_enabled = !!(status_param_tx_enable_reg & (1 << 2)); + data.radio.status_report.parameter_tx_enable_reg.unit = "bool"; + break; + + case "COMMANF_ID_GET_STATUS": + var status_param_tx_enable_reg = input.bytes[idx++]; + + if( !!(status_param_tx_enable_reg & (1 << 7)) ) + { + data.battery_voltage = data.battery_voltage || {}; + data.battery_voltage.value = ( input.bytes[idx++] * 10 + 1500).toFixed(0); + data.battery_voltage.unit = "mV"; + } + + if( !!(status_param_tx_enable_reg & (1 << 6)) ) + { + data.heating_control = data.heating_control || {}; + data.heating_control.room_temperature = {}; + + data.heating_control.room_temperature.value = (input.bytes[idx++] * 0.5).toFixed(1); + data.heating_control.room_temperature.unit = "°C"; + } + + if( !!(status_param_tx_enable_reg & (1 << 5)) ) + { + data.heating_control = data.heating_control || {}; + data.heating_control.set_point_temperature = {}; + + data.heating_control.set_point_temperature.value = (input.bytes[idx++] * 0.5).toFixed(1); + data.heating_control.set_point_temperature.unit = "°C"; + } + + if( !!(status_param_tx_enable_reg & (1 << 4)) ) + { + data.heating_control = data.heating_control || {}; + data.heating_control.valve_position = {}; + + data.heating_control.valve_position.value = (input.bytes[idx++] * 0.5).toFixed(1); + data.heating_control.valve_position.unit = "%"; + } + + if( !!(status_param_tx_enable_reg & (1 << 3)) ) + { + data.heating_control = data.heating_control || {}; + data.heating_control.gain = data.heating_control.gain || {}; + data.heating_control.gain.p = {}; + data.heating_control.gain.i = {}; + + data.heating_control.gain.p.value = (input.bytes[idx++] << 8) + input.bytes[idx++]; + data.heating_control.gain.i.value = (input.bytes[idx++] / 1000000); + data.heating_control.gain.unit = "uint"; + } + + if( !!(status_param_tx_enable_reg & (1 << 2)) ) + { + data.heating_control.mode = data.heating_control.mode || {}; + data.heating_control.mode.holiday = data.heating_control.mode.holiday || {}; + data.heating_control.mode.window_open_detection = data.heating_control.mode.window_open_detection || {}; + + var device_flags = input.bytes[idx++]; + + data.heating_control.mode.active_mode = {}; + data.heating_control.mode.active_mode.value = ACTIVE_MODE[(device_flags & 0xE0) >> 5]; + data.heating_control.mode.active_mode.unit = "string"; + + data.heating_control.mode.holiday.is_pending = {}; + data.heating_control.mode.holiday.is_pending.value = !!(device_flags & (1 << 4)); + data.heating_control.mode.holiday.is_pending.unit = "bool"; + + data.heating_control.mode.window_open_detection.is_open = {}; + data.heating_control.mode.window_open_detection.is_open.value = !!(device_flags & (1 << 3)); + data.heating_control.mode.window_open_detection.is_open.unit = "bool"; + + data.battery_cover_locked = {}; + data.battery_cover_locked.value = !!(device_flags & (1 << 2)); + data.battery_cover_locked.unit = "bool"; + } + break; + + case "COMMAND_ID_GET_BATTERY_VOLTAGE": + data.battery_voltage = {}; + data.battery_voltage.value = ( input.bytes[idx++] * 10 + 1500).toFixed(0); + data.battery_voltage.unit = "mV"; + break; + + case "COMMAND_ID_GET_BATTTERY_COVER_LOCK_STATUS": + data.battery_cover_locked = {}; + data.battery_cover_locked.value = !!(input.bytes[idx++] & 0x01); + data.battery_cover_locked.unit = "bool"; + break; + + case "COMMAND_ID_GET_ERROR_CODE": + data.error_code = {}; + data.error_code.value = ERROR_CODE[input.bytes[idx++]]; + data.error_code.unit = "string"; + break; + + case "COMMAND_ID_GET_DEVICE_TIME": + data.device_time = data.device_time || {}; + data.device_time.local = data.device_time.local || {}; + data.device_time.local.second = {}; + data.device_time.local.minute = {}; + data.device_time.local.hour = {}; + data.device_time.local.day = {}; + data.device_time.local.weekday = {}; + data.device_time.local.month = {}; + data.device_time.local.year = {}; + data.device_time.local.is_dst = {}; + data.device_time.local.utc_offset = {}; + + data.device_time.local.second.value = input.bytes[idx++] & 0x1F; + data.device_time.local.second.unit = "s"; + + data.device_time.local.minute.value = (input.bytes[idx] >> 2) & 0x3F; + data.device_time.local.minute.unit = "min"; + + data.device_time.local.hour.value = ((input.bytes[idx++] & 0x03) << 3) + (input.bytes[idx] >> 5); + data.device_time.local.hour.unit = "h"; + + data.device_time.local.day.value = input.bytes[idx++] & 0x1F; + data.device_time.local.day.unit = "d"; + + data.device_time.local.is_dst.value = !!(input.bytes[idx] & 0x80); + data.device_time.local.is_dst.unit = "bool"; + + data.device_time.local.weekday.value = WEEKDAY[(input.bytes[idx] >> 4) & 0x07]; + data.device_time.local.weekday.unit = "string"; + + data.device_time.local.month.value = MONTH[input.bytes[idx++] & 0x0F]; + data.device_time.local.month.unit = "string"; + + data.device_time.local.year.value = input.bytes[idx++] + 2000; + data.device_time.local.year.unit = "a"; + + data.device_time.local.utc_offset.value = (input.bytes[idx++] * 0.25 - 12).toFixed(2); + data.device_time.local.utc_offset.unit = "h"; + break; + + case "COMMAND_ID_GET_DEVICE_TIME_CONFIG": + data.device_time = data.device_time || {}; + data.device_time.config = data.device_time.config || {}; + data.device_time.config.auto_time_sync_en = {}; + data.device_time.config.utc_offset = {}; + data.device_time.config.utc_dst_offset = {}; + data.device_time.config.utc_dst_begin = {}; + data.device_time.config.utc_dst_end = {}; + data.device_time.config.utc_dst_begin.week_of_month = {}; + data.device_time.config.utc_dst_end.week_of_month = {}; + data.device_time.config.utc_dst_begin.month = {}; + data.device_time.config.utc_dst_end.month = {}; + data.device_time.config.utc_dst_begin.weekday = {}; + data.device_time.config.utc_dst_end.weekday = {}; + data.device_time.config.utc_dst_begin.hour = {}; + data.device_time.config.utc_dst_end.hour = {}; + data.device_time.config.utc_dst_begin.minute = {}; + data.device_time.config.utc_dst_end.minute = {}; + + data.device_time.config.auto_time_sync_en.value = !!(input.bytes[idx] >> 7); + data.device_time.config.auto_time_sync_en.unit = "bool"; + + data.device_time.config.utc_offset.value = ((input.bytes[idx++] & 0x7F) * 0.25 - 12).toFixed(2); + data.device_time.config.utc_offset.unit = "h"; + + data.device_time.config.utc_dst_begin.week_of_month.value = WEEK_OF_MONTH[ (input.bytes[idx] >> 4) & 0x0F ]; + data.device_time.config.utc_dst_begin.week_of_month.unit = "string"; + + data.device_time.config.utc_dst_begin.month.value = MONTH[input.bytes[idx++] & 0x0F]; + data.device_time.config.utc_dst_begin.month.unit = "string"; + + data.device_time.config.utc_dst_begin.weekday.value = WEEKDAY[input.bytes[idx] >> 5]; + data.device_time.config.utc_dst_begin.weekday.unit = "string"; + + data.device_time.config.utc_dst_begin.hour.value = (input.bytes[idx++] & 0x0F); + data.device_time.config.utc_dst_begin.hour.unit = "h"; + + data.device_time.config.utc_dst_offset.value = ((input.bytes[idx++] & 0x7F) * 0.25 - 12).toFixed(2); + data.device_time.config.utc_dst_offset.unit = "h"; + + data.device_time.config.utc_dst_end.week_of_month.value = WEEK_OF_MONTH[ (input.bytes[idx] >> 4) & 0x0F ]; + data.device_time.config.utc_dst_end.week_of_month.unit = "string"; + + data.device_time.config.utc_dst_end.month.value = MONTH[input.bytes[idx++] & 0x0F]; + data.device_time.config.utc_dst_end.month.unit = "string"; + + data.device_time.config.utc_dst_end.weekday.value = WEEKDAY[input.bytes[idx] >> 5]; + data.device_time.config.utc_dst_end.weekday.unit = "string"; + + data.device_time.config.utc_dst_end.hour.value = (input.bytes[idx++] & 0x0F); + data.device_time.config.utc_dst_end.hour.unit = "h"; + + data.device_time.config.utc_dst_begin.minute.value = (input.bytes[idx] >> 4) * 5; + data.device_time.config.utc_dst_begin.minute.unit = "min"; + + data.device_time.config.utc_dst_end.minute.value = (input.bytes[idx++] & 0x0F) * 5; + data.device_time.config.utc_dst_end.minute.unit = "min"; + break; + + case "COMMAND_ID_GET_MODE_STATUS": + data.heating_control = data.heating_control || {}; + data.heating_control.mode = data.heating_control.mode || {}; + data.heating_control.mode.active_main_mode = {}; + data.heating_control.mode.holiday = data.heating_control.mode.holiday || {}; + data.heating_control.mode.holiday.is_active = {}; + data.heating_control.mode.holiday.is_pending = {}; + data.heating_control.mode.boost = data.heating_control.mode.boost || {}; + data.heating_control.mode.boost.is_active = {}; + data.heating_control.mode.frost_protection = data.heating_control.mode.frost_protection || {}; + data.heating_control.mode.frost_protection.is_active = {}; + data.heating_control.mode.window_open_detection = data.heating_control.mode.window_open_detection || {}; + data.heating_control.mode.window_open_detection.is_active = {}; + data.heating_control.mode.emergency = data.heating_control.mode.emergency || {}; + data.heating_control.mode.emergency.is_active = {}; + data.heating_control.mode.auto = data.heating_control.mode.auto || {}; + data.heating_control.mode.auto.selected_week_program = {}; + + data.heating_control.mode.active_main_mode.value = ACTIVE_MODE[input.bytes[idx] >> 6]; + data.heating_control.mode.active_main_mode.unit = "string"; + + data.heating_control.mode.holiday.is_active.value = !!(input.bytes[idx] & (1 << 5)); + data.heating_control.mode.holiday.is_active.unit = "bool"; + + data.heating_control.mode.holiday.is_pending.value = !!(input.bytes[idx] & (1 << 4)); + data.heating_control.mode.holiday.is_pending.unit = "bool"; + + data.heating_control.mode.boost.is_active.value = !!(input.bytes[idx] & (1 << 3)); + data.heating_control.mode.boost.is_active.unit = "bool"; + + data.heating_control.mode.frost_protection.is_active.value = !!(input.bytes[idx] & (1 << 2)); + data.heating_control.mode.frost_protection.is_active.unit = "bool"; + + data.heating_control.mode.window_open_detection.is_active.value = !!(input.bytes[idx] & (1 << 1)); + data.heating_control.mode.window_open_detection.is_active.unit = "bool"; + + data.heating_control.mode.emergency.is_active.value = !!(input.bytes[idx++] & (1 << 0)); + data.heating_control.mode.emergency.is_active.uint = "bool"; + + data.heating_control.mode.auto.selected_week_program.value = (input.bytes[idx++] >> 6); + data.heating_control.mode.auto.selected_week_program.unit = "uint"; + break; + + case "COMMAND_ID_GET_HOLIDAY_MODE_CONFIG": + data.heating_control = data.heating_control || {}; + data.heating_control.mode = data.heating_control.mode || {}; + data.heating_control.mode.holiday = data.heating_control.mode.holiday || {}; + data.heating_control.mode.holiday = {}; + data.heating_control.mode.holiday.begin = data.heating_control.mode.holiday.begin || {}; + data.heating_control.mode.holiday.end = data.heating_control.mode.holiday.end || {}; + data.heating_control.mode.holiday.begin.minute = {}; + data.heating_control.mode.holiday.begin.hour = {}; + data.heating_control.mode.holiday.begin.day = {}; + data.heating_control.mode.holiday.begin.month = {}; + data.heating_control.mode.holiday.begin.year = {}; + data.heating_control.mode.holiday.end.minute = {}; + data.heating_control.mode.holiday.end.hour = {}; + data.heating_control.mode.holiday.end.day = {}; + data.heating_control.mode.holiday.end.month = {}; + data.heating_control.mode.holiday.end.year = {}; + data.heating_control.mode.holiday.set_point_temperature = {}; + + data.heating_control.mode.holiday.begin.minute.value = ((input.bytes[idx] >> 2) & 0x0F) * 5; + data.heating_control.mode.holiday.begin.minute.unit = "min"; + + data.heating_control.mode.holiday.begin.hour.value = ((input.bytes[idx++] & 0x03) << 3) + (input.bytes[idx] >> 5); + data.heating_control.mode.holiday.begin.hour.unit = "h"; + + data.heating_control.mode.holiday.begin.day.value = input.bytes[idx++] & 0x1F; + data.heating_control.mode.holiday.begin.day.unit = "d"; + + data.heating_control.mode.holiday.end.minute.value = ((input.bytes[idx] >> 2) & 0x0F) * 5; + data.heating_control.mode.holiday.end.minute.unit = "min"; + + data.heating_control.mode.holiday.end.hour.value = ((input.bytes[idx++] & 0x03) << 3) + (input.bytes[idx] >> 5); + data.heating_control.mode.holiday.end.hour.unit = "h"; + + data.heating_control.mode.holiday.end.day.value = input.bytes[idx++] & 0x1F; + data.heating_control.mode.holiday.end.day.unit = "d"; + + data.heating_control.mode.holiday.begin.month.value = MONTH[input.bytes[idx] >> 4]; + data.heating_control.mode.holiday.begin.month.unit = "string"; + data.heating_control.mode.holiday.end.month.value = MONTH[input.bytes[idx++] &0x0F]; + data.heating_control.mode.holiday.end.month.unit = "string"; + + data.heating_control.mode.holiday.begin.year.value = input.bytes[idx++] + 2000; + data.heating_control.mode.holiday.begin.year.unit = "a"; + + data.heating_control.mode.holiday.end.year.value = input.bytes[idx++] + 2000; + data.heating_control.mode.holiday.end.year.unit = "a"; + + data.heating_control.mode.holiday.set_point_temperature.value = (input.bytes[idx++] * 0.5).toFixed(1); + data.heating_control.mode.holiday.set_point_temperature.unit = "°C"; + break; + + case "COMMAND_ID_GET_BOOST_CONFIG": + data.heating_control = data.heating_control || {}; + data.heating_control.mode = data.heating_control.mode || {}; + data.heating_control.mode.boost = data.heating_control.mode.boost || {}; + data.heating_control.mode.boost.config = data.heating_control.mode.boost.config || {}; + + data.heating_control.mode.boost.config.duration = {}; + data.heating_control.mode.boost.config.duration.value = input.bytes[idx++] * 15; + data.heating_control.mode.boost.config.duration.unit = "s"; + + data.heating_control.mode.boost.config.valve_position = {}; + data.heating_control.mode.boost.config.valve_position.value = (input.bytes[idx++] * 0.5 ).toFixed(0); + data.heating_control.mode.boost.config.valve_position.unit = "%"; + break; + + case "COMMAND_ID_GET_WEEK_PROGRAM": + data.heating_control = data.heating_control || {}; + data.heating_control.mode = data.heating_control.mode || {}; + data.heating_control.mode.auto = data.heating_control.mode.auto || {}; + data.heating_control.mode.auto.week_program_1 = {}; + + var week_program_nbr = (input.bytes[idx] >> 4) & 0x03; + var nbr_time_switching_points = input.bytes[idx++] & 0x0F; + + const time_switching_point = {}; + + switch(week_program_nbr) + { + case 0: + data.heating_control.mode.auto.week_program_1 = {}; + break; + + case 1: + data.heating_control.mode.auto.week_program_2 = {}; + break; + + case 2: + data.heating_control.mode.auto.week_program_3 = {}; + break; + + default: + + break; + } + + for(let i = 0; i < nbr_time_switching_points; i++) + { + + time_switching_point.minute = (input.bytes[idx] >> 4) * 5; + time_switching_point.hour = ((input.bytes[idx++] & 0x0F) << 1) + (input.bytes[idx] >> 7); + time_switching_point.weekdays = input.bytes[idx++] & 0x7F; + time_switching_point.set_point_temperature = (input.bytes[idx++] * 0.2).toFixed(1); + + switch(week_program_nbr) + { + case 0: + data.heating_control.mode.auto.week_program_1[i] = Object.assign( {}, time_switching_point); + break; + + case 1: + data.heating_control.mode.auto.week_program_2[i] = Object.assign( {}, time_switching_point); + break; + + case 2: + data.heating_control.mode.auto.week_program_3[i] = Object.assign( {}, time_switching_point); + break; + + default: + + break; + } + } + break; + + case "COMMAND_ID_GET_VALVE_POSITION": + data.heating_control = data.heating_control || {}; + data.heating_control.valve_position = {}; + + data.heating_control.valve_position.value = (input.bytes[idx++] * 0.5 ).toFixed(0); + data.heating_control.valve_position.unit = "%"; + break; + + case "COMMAND_ID_GET_VALVE_SET_POINT_POSITION": + data.heating_control = data.heating_control || {}; + data.heating_control.mode = data.heating_control.mode || {}; + data.heating_control.mode.manu_pos = data.heating_control.mode.manu_pos ||{}; + data.heating_control.mode.manu_pos.valve_set_point_position = {}; + + data.heating_control.mode.manu_pos.valve_set_point_position.value = (input.bytes[idx++] * 0.5).toFixed(0); + data.heating_control.mode.manu_pos.valve_set_point_position.unit = "%"; + break; + + case "COMMAND_ID_GET_VALVE_OFFSET": + data.heating_control = data.heating_control || {}; + data.heating_control.config = data.heating_control.config || {}; + data.heating_control.config.valve = data.heating_control.config.valve || {}; + + data.heating_control.config.valve.position_offset = {}; + data.heating_control.config.valve.position_offset.value = (input.bytes[idx++] * 0.5).toFixed(0); + data.heating_control.config.valve.position_offset.unit = "%"; + break; + + case "COMMAND_ID_GET_VALVE_MAXIMUM_POSITION": + data.heating_control = data.heating_control || {}; + data.heating_control.config = data.heating_control.config || {}; + data.heating_control.config.valve = data.heating_control.config.valve || {}; + + data.heating_control.config.valve.max_position = {}; + data.heating_control.config.valve.max_position.value = (input.bytes[idx++] * 0.5).toFixed(0); + data.heating_control.config.valve.max_position.unit = "%"; + break; + case "COMMAND_ID_GET_VALVE_EMERGENCY_POSITION": + data.heating_control = data.heating_control || {}; + data.heating_control.mode = data.heating_control.mode || {}; + data.heating_control.mode.emergency = data.heating_control.mode.emergency || {}; + data.heating_control.mode.emergency.config = data.heating_control.mode.emergency.config || {}; + data.heating_control.mode.emergency.config.valve_set_point_position = data.heating_control.mode.emergency.config.valve_set_point_position || {}; + + data.heating_control.mode.emergency.config.valve_set_point_position.value = (input.bytes[idx++] * 0.5).toFixed(0); + data.heating_control.mode.emergency.config.valve_set_point_position.unit = "%"; + break; + + case "COMMAND_ID_GET_SET_POINT_TEMPERATURE": + data.heating_control = data.heating_control || {}; + data.heating_control.mode = data.heating_control.mode || {}; + data.heating_control.mode.manu_temp = data.heating_control.mode.manu_temp || {}; + + data.heating_control.mode.manu_temp.value = (input.bytes[idx++] * 0.5).toFixed(1); + data.heating_control.mode.manu_temp.unit = "°C"; + break; + + case "COMMAND_ID_GET_TEMPERATURE_OFFSET": + data.heating_control = data.heating_control || {}; + data.heating_control.config = data.heating_control.config || {}; + data.heating_control.config.temperature = data.heating_control.config.temperature || {}; + data.heating_control.config.temperature.offset = data.heating_control.config.temperature.offset || {}; + + data.heating_control.config.temperature.offset.value = ((input.bytes[idx++] * 0.1) - 12.8).toFixed(1); + data.heating_control.config.temperature.offset.unit = "K"; + break; + + case "COMMAND_ID_GET_HEATING_CNTRL_INPUT_ROOM_TEMPERATURE": + data.heating_control = data.heating_control || {}; + data.heating_control.room_temperature = {}; + + data.heating_control.room_temperature.value = (input.bytes[idx++] * 0.5).toFixed(1); + data.heating_control.room_temperature.unit = "°C"; + break; + + case "COMMAND_ID_GET_HEATING_CNTRL_INPUT_SET_POINT_TEMPERATURE": + data.heating_control = data.heating_control || {}; + data.heating_control.set_point_temperature = {}; + + data.heating_control.set_point_temperature.value = (input.bytes[idx++] * 0.5).toFixed(1); + data.heating_control.set_point_temperature.unit = "°C"; + break; + + case "COMMAND_ID_GET_HEATING_CNTRL_CONFIG": + data.heating_control = data.heating_control || {}; + data.heating_control.config = data.heating_control.config || {}; + + data.heating_control.config.adaptive_gain_adjustment_enabled = {}; + data.heating_control.config.adaptive_gain_adjustment_enabled.value = !!(input.bytes[idx] & (1 << 7)); + data.heating_control.config.adaptive_gain_adjustment_enabled.unit = "bool"; + + data.heating_control.config.controller_temperature_input_select = {}; + data.heating_control.config.controller_temperature_input_select.value = !!(input.bytes[idx++] & (1 << 6)); + data.heating_control.config.controller_temperature_input_select.unit = "bool"; + break; + + case "COMMAND_ID_GET_HEATING_CNTRL_STATIC_GAINS": + data.heating_control = data.heating_control || {}; + data.heating_control.config = data.heating_control.config || {}; + data.heating_control.config.static_gain = data.heating_control.config.static_gain || {}; + + data.heating_control.config.static_gain.p = {}; + data.heating_control.config.static_gain.p.value = (input.bytes[idx++] << 8) + input.bytes[idx++]; + data.heating_control.config.static_gain.i = {}; + data.heating_control.config.static_gain.i.value = (input.bytes[idx++] / 1000000); + data.heating_control.config.static_gain.unit = "uint"; + break; + case "COMMAND_ID_GET_HEATING_CNTRL_INPUT_GAINS": + data.heating_control = data.heating_control || {}; + data.heating_control.input_gain = data.heating_control.input_gain || {}; + + data.heating_control.input_gain.p = {}; + data.heating_control.input_gain.p.value = (input.bytes[idx++] << 8) + input.bytes[idx++]; + data.heating_control.input_gain.i = {}; + data.heating_control.input_gain.i.value = (input.bytes[idx++] / 500000); + data.heating_control.input_gain.unit = "uint"; + break; + case "COMMAND_ID_GET_WINDOW_OPEN_STATUS": + data.heating_control = data.heating_control || {}; + data.heating_control.mode = data.heating_control.mode || {}; + data.heating_control.mode.window_open_detection = data.heating_control.mode.window_open_detection || {}; + + data.heating_control.mode.window_open_detection.is_open = {}; + data.heating_control.mode.window_open_detection.is_open.value = !!(input.bytes[idx++] & 0x01); + data.heating_control.mode.window_open_detection.is_open.unit = "bool"; + break; + + case "COMMAND_ID_GET_WINDOW_OPEN_DETECTION_CONFIG": + data.heating_control = data.heating_control || {}; + data.heating_control.mode = data.heating_control.mode || {}; + data.heating_control.mode.window_open_detection = data.heating_control.mode.window_open_detection || {}; + data.heating_control.mode.window_open_detection.config = data.heating_control.mode.window_open_detection.config || {}; + + data.heating_control.mode.window_open_detection.config.source = {}; + data.heating_control.mode.window_open_detection.config.enable_mode = {}; + data.heating_control.mode.window_open_detection.config.open_duration = {}; + data.heating_control.mode.window_open_detection.config.temperature_delta = {}; + data.heating_control.mode.window_open_detection.config.open_temperature = {}; + + data.heating_control.mode.window_open_detection.config.source.value = !!(input.bytes[idx] & (1 << 3)); + data.heating_control.mode.window_open_detection.config.source.unit = "bool"; + + data.heating_control.mode.window_open_detection.config.enable_mode.holiday = {}; + data.heating_control.mode.window_open_detection.config.enable_mode.auto = {}; + data.heating_control.mode.window_open_detection.config.enable_mode.manu_temp = {}; + data.heating_control.mode.window_open_detection.config.enable_mode.holiday.value = !!(input.bytes[idx] & (1 << 0)); + data.heating_control.mode.window_open_detection.config.enable_mode.auto.value = !!(input.bytes[idx] & (1 << 1)); + data.heating_control.mode.window_open_detection.config.enable_mode.manu_temp.value = !!(input.bytes[idx++] & (1 << 2)); + data.heating_control.mode.window_open_detection.config.enable_mode.unit = "bool"; + + data.heating_control.mode.window_open_detection.config.open_duration.value = (input.bytes[idx] >> 5) * 10 + 10; + data.heating_control.mode.window_open_detection.config.open_duration.unit = "min"; + + data.heating_control.mode.window_open_detection.config.temperature_delta.value = ((input.bytes[idx++] & 0x1F) * 0.1 + 0.5).toFixed(1); + data.heating_control.mode.window_open_detection.config.temperature_delta.unit = "K"; + + data.heating_control.mode.window_open_detection.config.open_temperature.value = (input.bytes[idx++] * 0.5).toFixed(1); + data.heating_control.mode.window_open_detection.config.open_temperature.unit = "°C"; + break; + + case "COMMAND_ID_GET_DECALCIFICATION_CONFIG": + data.heating_control = data.heating_control || {}; + data.heating_control.config = data.heating_control.config || {}; + data.heating_control.config.decalcification_time = data.heating_control.config.decalcification_time || {}; + + data.heating_control.config.decalcification_time.weekday = {}; + data.heating_control.config.decalcification_time.week_of_month = {}; + data.heating_control.config.decalcification_time.hour = {}; + data.heating_control.config.decalcification_time.minute = {}; + + data.heating_control.config.decalcification_time.weekday.value = WEEKDAY[(input.bytes[idx] >> 4)]; + data.heating_control.config.decalcification_time.weekday.unit = "string"; + + data.heating_control.config.decalcification_time.week_of_month.value = WEEK_OF_MONTH[((input.bytes[idx] >> 1) & 0x07)]; + data.heating_control.config.decalcification_time.week_of_month.unit = "string"; + + data.heating_control.config.decalcification_time.hour.value = ((input.bytes[idx++] & 0x01) << 4) + (input.bytes[idx] >> 4); + data.heating_control.config.decalcification_time.hour.unit = "h"; + data.heating_control.config.decalcification_time.minute.value = (input.bytes[idx++] & 0x0F) * 5; + break; + + case "COMMAND_ID_COMMAND_FAILED": + data.radio = data.radio || {}; + data.radio.failed_commands = {}; + + var nbr_failed_commands = input.bytes[idx++]; + + data.radio.failed_commands.value = {}; + + for(let i = 0; i < nbr_failed_commands; i++) + { + data.radio.failed_commands.value[i] = COMMAND_ID[input.bytes[idx++]]; + } + + data.radio.failed_commands.unit = "COMMAND_ID"; + break; + + case "COMMAND_ID_GET_BUTTON_ACTION": + data.button_action = data.button_action || {}; + data.button_action.single_tap = data.button_action.single_tap || {}; + data.button_action.double_tap = data.button_action.double_tap || {}; + + data.button_action.single_tap.value = COMMAND_ID[input.bytes[idx++]]; + data.button_action.double_tap.value = COMMAND_ID[input.bytes[idx++]]; + + data.button_action.unit = "COMMAND_ID"; + break; + + case "COMMAND_ID_GET_HARDWARE_FACTORY_RESET_LOCK": + data.button_action = data.button_action || {}; + data.button_action.hw_factory_reset_locked = data.button_action.hw_factory_reset_locked || {}; + + data.button_action.hw_factory_reset_locked.value = !!(input.bytes[idx++] & (0x01)); + data.button_action.hw_factory_reset_locked.unit = "bool"; + break; + + case "COMMAND_ID_GET_DATA_RATE": + data.radio = data.radio || {}; + data.radio.data_rate = {}; + + data.radio.data_rate.value = DATA_RATE[(input.bytes[idx++] & 0x0F)]; + data.radio.data_rate.unit = "string"; + break; + + case "COMMAND_ID_GET_REMAINING_TIME_UNTIL_REJOIN": + data.radio = data.radio || {}; + data.radio.cyclic_rejoin = data.radio.cyclic_rejoin || {}; + data.radio.cyclic_rejoin.remaining_time_until_rejoin = data.radio.cyclic_rejoin.remaining_time_until_rejoin || {}; + + data.radio.cyclic_rejoin.remaining_time_until_rejoin.value = ((input.bytes[idx++] & 0x1F) << 16) + (input.bytes[idx++] << 8) + input.bytes[idx++]; + data.radio.cyclic_rejoin.remaining_time_until_rejoin.unit = "min"; + break; + + case "COMMAND_ID_GET_REJOIN_BEHAVIOR": + data.radio = data.radio || {}; + data.radio.cyclic_rejoin = data.radio.cyclic_rejoin || {}; + data.radio.cyclic_rejoin.conf = data.radio.cyclic_rejoin.conf || {}; + data.radio.cyclic_rejoin.interval = data.radio.cyclic_rejoin.interval || {}; + + data.radio.cyclic_rejoin.conf.value = REJOIN_BEHAVIOR[!(input.bytes[idx] >> 7)]; + data.radio.cyclic_rejoin.conf.unit = "string"; + + data.radio.cyclic_rejoin.interval.value = (((input.bytes[idx++] & 0x7F) * (255)) + input.bytes[idx++]); + data.radio.cyclic_rejoin.interval.unit = "h"; + break; + + case "COMMAND_ID_GET_VERSION": + data.version = data.version || {}; + data.version.hw_revision = {}; + data.version.application = {}; + data.version.bootloader = {}; + data.version.lorawan_l2 = {}; + data.version.payload_parser = {}; + + data.version.hw_revision.value = input.bytes[idx++]; + data.version.hw_revision.unit = "uint"; + + data.version.application.value = {}; + data.version.application.value[0] = input.bytes[idx++]; + data.version.application.value[1] = input.bytes[idx++]; + data.version.application.value[2] = input.bytes[idx++]; + data.version.application.unit = "uint"; + + data.version.bootloader.value = {}; + data.version.bootloader.value[0] = input.bytes[idx++]; + data.version.bootloader.value[1] = input.bytes[idx++]; + data.version.bootloader.value[2] = input.bytes[idx++]; + data.version.bootloader.unit = "uint"; + + data.version.lorawan_l2.value = {}; + data.version.lorawan_l2.value[0] = input.bytes[idx++]; + data.version.lorawan_l2.value[1] = input.bytes[idx++]; + data.version.lorawan_l2.value[2] = input.bytes[idx++]; + data.version.lorawan_l2.unit = "uint"; + + data.version.payload_parser.value = {}; + data.version.payload_parser.value[0] = payload_parser_version[0]; + data.version.payload_parser.value[1] = payload_parser_version[1]; + data.version.payload_parser.value[2] = payload_parser_version[2]; + data.version.payload_parser.unit = "uint"; + break; + + default: + break; + } + } + + return { + data: data, + warnings: [], + errors: [] + }; +} \ No newline at end of file diff --git a/vendor/dnt/dnt-lw-etrv-c.png b/vendor/dnt/dnt-lw-etrv-c.png new file mode 100644 index 0000000000..d8da6a89b0 Binary files /dev/null and b/vendor/dnt/dnt-lw-etrv-c.png differ diff --git a/vendor/dnt/dnt-lw-etrv-c.yaml b/vendor/dnt/dnt-lw-etrv-c.yaml new file mode 100644 index 0000000000..8cec647497 --- /dev/null +++ b/vendor/dnt/dnt-lw-etrv-c.yaml @@ -0,0 +1,105 @@ +name: LoRaWAN® Radiator Thermostat Controllable +description: 'The dnt® LoRaWAN® Radiator thermostat Controllable enables two ways to adjust the room temperature: directly on the device with the integrated buttons or remote control via Downlinks. It features an always-on display to show the current temperature settings, three customizable heating profiles with up to ten switching times per day, a window open detection to automatically reduce temperature and the possibility to perform a dynamic adaptive adjustment of the radiator. Because of very low energy consumption, a battery life up to 3 years can be archived.' + +# Hardware versions (optional, use when you have revisions) +hardwareVersions: + - version: '1.0' + numeric: 1 + +# Firmware versions (at least one is mandatory) +firmwareVersions: + - # Firmware version + version: '0.1.0' + numeric: 1 + # Corresponding hardware versions (optional) + hardwareVersions: + - '1.0' + + # Firmware features (optional) + # Valid values are: remote rejoin (trigger a join from the application layer), transmission interval (configure how + # often he device sends a message). + features: + - transmission interval + + # LoRaWAN Device Profiles per region + # Supported regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, + # RU864-870 + profiles: + EU863-870: + # Optional identifier of the vendor of the profile. When you specify the vendorID, the profile is loaded from + # the vendorID's folder. This allows you to reuse profiles from module or LoRaWAN end device stack vendors. + # If vendorID is empty, the current vendor ID is used. In this example, the vendorID is the current vendor ID, + # which is verbose. + # Identifier of the profile (lowercase, alphanumeric with dashes, max 36 characters) + id: dnt-profile + lorawanCertified: true + codec: dnt-lw-etrv-c-codec + +# Sensors that this device features (optional) +# Valid values are: +# 4-20 ma, accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co, co2, conductivity, +# current, digital input, dissolved oxygen, distance, dust, energy, gps, gyroscope, h2s, humidity, iaq, level, light, +# lightning, link, magnetometer, moisture, motion, no, no2, o3, particulate matter, ph, pir, pm2.5, pm10, potentiometer, +# power, precipitation, pressure, proximity, pulse count, pulse frequency, radar, rainfall, rssi, smart valve, snr, so2, +# solar radiation, sound, strain, surface temperature, temperature, tilt, time, tvoc, uv, vapor pressure, velocity, +# vibration, voltage, water potential, water, weight, wifi ssid, wind direction, wind speed. +sensors: + - temperature + +# Dimensions in mm (optional) +# Use width, height, length and/or diameter +dimensions: + width: 53 + length: 94 + height: 53 + +# Weight in grams (optional) +weight: 179 + +# Operating conditions (optional) +operatingConditions: + # Temperature (Celsius) + temperature: + min: 0 + max: 50 + # Relative humidity (fraction of 1) + relativeHumidity: + min: 0 + max: 0.97 + +# IP rating (optional) +ipCode: IP20 + +# Key provisioning (optional) +# Valid values are: custom (user can configure keys), join server and manifest. +keyProvisioning: + - join server + +# Key programming (optional) +# Valid values are: bluetooth, nfc, wifi, serial (when the user has a serial interface to set the keys) +# and firmware (when the user should change the firmware to set the keys). +keyProgramming: + - firmware + +# Firmware programming (optional) +# Valid values are: serial (when the user has a serial interface to update the firmware), fuota lorawan (when the device +# supports LoRaWAN FUOTA via standard interfaces) and fuota other (other wireless update mechanism). +firmwareProgramming: + - serial + - fuota other + +# Product and data sheet URLs (optional) +productURL: https://www.dnt.de/ + +# Commercial information +resellerURLs: + - name: 'ELVshop' + region: + - European Union + url: https://www.dnt.de/Produkte/dnt-LoRaWAN-Heizkoerperthermostat-dnt-LW-eTRV/ +msrp: + EUR: 89.95 + +# Photos +photos: + main: dnt-lw-etrv-c.png diff --git a/vendor/dnt/dnt-lw-etrv.yaml b/vendor/dnt/dnt-lw-etrv.yaml index 0ec8eadb51..ea11748029 100644 --- a/vendor/dnt/dnt-lw-etrv.yaml +++ b/vendor/dnt/dnt-lw-etrv.yaml @@ -89,7 +89,7 @@ firmwareProgramming: - fuota other # Product and data sheet URLs (optional) -productURL: https://de.dnt.de/ +productURL: https://www.dnt.de/ # Commercial information resellerURLs: diff --git a/vendor/dnt/index.yaml b/vendor/dnt/index.yaml index b4ddecd6f9..b04effba64 100644 --- a/vendor/dnt/index.yaml +++ b/vendor/dnt/index.yaml @@ -4,3 +4,5 @@ endDevices: # Unique identifier of the end device (lowercase, alphanumeric with dashes, max 36 characters) - dnt-lw-ats # look in dnt-lw-ats.yaml for the end device definition - dnt-lw-etrv + - dnt-lw-dis + - dnt-lw-etrv-c diff --git a/vendor/dragino/index.yaml b/vendor/dragino/index.yaml index fb37db16b2..f71664b9a4 100644 --- a/vendor/dragino/index.yaml +++ b/vendor/dragino/index.yaml @@ -35,3 +35,4 @@ endDevices: - sw3l - lht52 - trackerd + - lwl03a diff --git a/vendor/dragino/lht52-codec.yaml b/vendor/dragino/lht52-codec.yaml index f268a0adfa..b18ff12e75 100644 --- a/vendor/dragino/lht52-codec.yaml +++ b/vendor/dragino/lht52-codec.yaml @@ -7,6 +7,25 @@ uplinkDecoder: bytes: [0x0B, 0x88, 0x01, 0x00, 0x25, 0x00, 0x01] output: data: { 'Bat_mV': 1, 'Firmware_Version': '8801', 'Freq_Band': 0, 'Sensor_Model': 11, 'Sub_Band': 37 } + normalizedOutput: + data: + - battery: 1 + + - description: Temperature & Humidity + input: + fPort: 2 + bytes: [0x08, 0xCD, 0x02, 0x20, 0x7F, 0xFF, 0x01, 0x61, 0xCD, 0x4E, 0xDD] + output: + data: { 'Ext': 1, 'Hum_SHT': 54.4, 'Systimestamp': 1640845021, 'TempC_DS': 327.67, 'TempC_SHT': 22.53 } + normalizedOutput: + data: + - air: + location: 'indoor' + temperature: 22.53 + relativeHumidity: 54.4 + - air: + location: 'outdoor' + temperature: 327.67 - description: Unknown FPort input: diff --git a/vendor/dragino/lht52.js b/vendor/dragino/lht52.js index 1f139a19a6..0c78189e89 100644 --- a/vendor/dragino/lht52.js +++ b/vendor/dragino/lht52.js @@ -67,4 +67,35 @@ function decodeUplink(input) { errors: ["unknown FPort"] } } -} \ No newline at end of file +} + +function normalizeUplink(input) { + var data = []; + + if (input.data.TempC_SHT) { + data.push({ + air: { + location: "indoor", + temperature: input.data.TempC_SHT, + relativeHumidity: input.data.Hum_SHT, + } + }); + } + + if (input.data.TempC_DS) { + data.push({ + air: { + location: "outdoor", + temperature: input.data.TempC_DS + } + }); + } + + if (input.data.Bat_mV) { + data.push({ + battery: input.data.Bat_mV + }); + } + + return { data: data }; +} diff --git a/vendor/dragino/lht65-codec.yaml b/vendor/dragino/lht65-codec.yaml index bcfdd9d45e..cf0df18be6 100644 --- a/vendor/dragino/lht65-codec.yaml +++ b/vendor/dragino/lht65-codec.yaml @@ -7,6 +7,16 @@ uplinkDecoder: bytes: [0xCB, 0xF6, 0x0B, 0x0D, 0x03, 0x76, 0x01, 0x0A, 0xDD, 0x7F, 0xFF] output: data: { 'BatV': 3.062, 'Bat_status': 3, 'Ext_sensor': 'Temperature Sensor', 'Hum_SHT': 88.6, 'TempC_DS': 27.81, 'TempC_SHT': 28.29 } + normalizedOutput: + data: + - air: + location: 'indoor' + temperature: 28.29 + relativeHumidity: 88.6 + - air: + location: 'outdoor' + temperature: 27.81 + battery: 3.062 - description: Unknown FPort input: diff --git a/vendor/dragino/lht65.js b/vendor/dragino/lht65.js index 5a48beaa10..76cad8a068 100644 --- a/vendor/dragino/lht65.js +++ b/vendor/dragino/lht65.js @@ -99,4 +99,25 @@ default: } -} \ No newline at end of file +} + +function normalizeUplink(input) { + return { + data: [ + { + air: { + location: "indoor", + temperature: input.data.TempC_SHT, + relativeHumidity: input.data.Hum_SHT, + } + }, + { + air: { + location: "outdoor", + temperature: input.data.TempC_DS, + }, + battery: input.data.BatV + } + ] + }; +} diff --git a/vendor/dragino/lse01-codec.yaml b/vendor/dragino/lse01-codec.yaml index aa14041666..d64df580bd 100644 --- a/vendor/dragino/lse01-codec.yaml +++ b/vendor/dragino/lse01-codec.yaml @@ -7,7 +7,15 @@ uplinkDecoder: bytes: [0xCE, 0x29, 0x00, 0xF1, 0x07, 0xA5, 0x09, 0x9B, 0x6E, 0x28, 0x90] output: data: { 'Bat': 3.625, 'TempC_DS18B20': '24.1', 'conduct_SOIL': 28200, 'temp_SOIL': '24.59', 'water_SOIL': '19.57' } - + normalizedOutput: + data: + - air: + temperature: 24.1 + soil: + temperature: 24.59 + moisture: 19.57 + ec: 28.2 + battery: 3.625 - description: Unknown FPort input: fPort: 42 diff --git a/vendor/dragino/lse01.js b/vendor/dragino/lse01.js index 76086ec091..bc906fb882 100644 --- a/vendor/dragino/lse01.js +++ b/vendor/dragino/lse01.js @@ -32,4 +32,20 @@ switch (input.fPort) { errors: ["unknown FPort"] } } -} \ No newline at end of file +} + +function normalizeUplink(input) { + return { + data: { + air: { + temperature: Number(input.data.TempC_DS18B20), + }, + soil: { + temperature: Number(input.data.temp_SOIL), + moisture: Number(input.data.water_SOIL), + ec: input.data.conduct_SOIL / 1000, + }, + battery: input.data.Bat, + } + }; +} diff --git a/vendor/dragino/lsn50-v2-codec.yaml b/vendor/dragino/lsn50-v2-codec.yaml index 2da95cb942..141944d640 100644 --- a/vendor/dragino/lsn50-v2-codec.yaml +++ b/vendor/dragino/lsn50-v2-codec.yaml @@ -7,6 +7,14 @@ uplinkDecoder: bytes: [0x0B, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x02, 0x05] output: data: { 'ADC_CH0V': 0, 'BatV': 2.9, 'Digital_IStatus': 'L', 'Door_status': 'OPEN', 'EXTI_Trigger': 'FALSE', 'Hum_SHT': 51.7, 'TempC1': 0, 'TempC_SHT': 25.2, 'Work_mode': 'IIC' } + normalizedOutput: + data: + - air: + temperature: 25.2 + relativeHumidity: 51.7 + action: + contactState: 'OPEN' + battery: 2.9 - description: Unknown FPort input: diff --git a/vendor/dragino/lsn50-v2.js b/vendor/dragino/lsn50-v2.js index 49b6a9a761..c26628505c 100644 --- a/vendor/dragino/lsn50-v2.js +++ b/vendor/dragino/lsn50-v2.js @@ -196,4 +196,19 @@ default: errors: ["unknown FPort"] } } -} \ No newline at end of file +} + +function normalizeUplink(input) { + return { + data: { + air: { + temperature: input.data.TempC_SHT, + relativeHumidity: input.data.Hum_SHT, + }, + action: { + contactState: input.data.Door_status === "CLOSE" ? "CLOSED" : input.data.Door_status === "OPEN" ? "OPEN" : undefined + }, + battery: input.data.BatV, + } + }; +} diff --git a/vendor/dragino/lsn50v2-s31-codec.yaml b/vendor/dragino/lsn50v2-s31-codec.yaml index 4bf4c13e62..e8607533d9 100644 --- a/vendor/dragino/lsn50v2-s31-codec.yaml +++ b/vendor/dragino/lsn50v2-s31-codec.yaml @@ -7,7 +7,14 @@ uplinkDecoder: bytes: [0x0B, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x02, 0x05] output: data: { 'ADC_CH0V': 0, 'BatV': 2.9, 'Digital_IStatus': 'L', 'Door_status': 'OPEN', 'EXTI_Trigger': 'FALSE', 'Hum_SHT': 51.7, 'TempC1': 0, 'TempC_SHT': 25.2, 'Work_mode': 'IIC' } - + normalizedOutput: + data: + - air: + temperature: 25.2 + relativeHumidity: 51.7 + action: + contactState: 'OPEN' + battery: 2.9 - description: Unknown FPort input: fPort: 42 diff --git a/vendor/dragino/lsn50v2-s31.js b/vendor/dragino/lsn50v2-s31.js index 577df8e5ed..9fef4d0eb1 100644 --- a/vendor/dragino/lsn50v2-s31.js +++ b/vendor/dragino/lsn50v2-s31.js @@ -97,4 +97,19 @@ default: errors: ["unknown FPort"] } } -} \ No newline at end of file +} + +function normalizeUplink(input) { + return { + data: { + air: { + temperature: input.data.TempC_SHT, + relativeHumidity: input.data.Hum_SHT + }, + action: { + contactState: input.data.Door_status === "CLOSE" ? "CLOSED" : input.data.Door_status === "OPEN" ? "OPEN" : undefined + }, + battery: input.data.BatV, + } + }; +} diff --git a/vendor/dragino/lwl03a-codec.yaml b/vendor/dragino/lwl03a-codec.yaml new file mode 100644 index 0000000000..c8b82784bf --- /dev/null +++ b/vendor/dragino/lwl03a-codec.yaml @@ -0,0 +1,13 @@ +uplinkDecoder: + fileName: lwl03a.js + examples: + - description: Distance Detection + input: + fPort: 2 + bytes: [0x01, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x0A, 0x63, 0x5C, 0xD2, 0xF2] + output: + data: { 'TDC': 'NO', 'ALARM': 'FALSE', 'WATER_LEAK_STATUS': 'LEAK', 'WATER_LEAK_TIMES': 10, 'LAST_WATER_LEAK_DURATION': 10, 'TIME': '2022-10-29 07:14:58' } + normalizedOutput: + data: + - water: + leak: true diff --git a/vendor/dragino/lwl03a.js b/vendor/dragino/lwl03a.js new file mode 100644 index 0000000000..8ce825c681 --- /dev/null +++ b/vendor/dragino/lwl03a.js @@ -0,0 +1,167 @@ +function datalog(i,bytes){ + var aa=(bytes[0+i]&0x02)?"TRUE":"FALSE"; + var bb=(bytes[0+i]&0x01)?"LEAK":"NO LEAK"; + var cc=(bytes[1+i]<<16 | bytes[2+i]<<8 | bytes[3+i]).toString(10); + var dd=(bytes[4+i]<<16 | bytes[5+i]<<8 | bytes[6+i]).toString(10); + var ee= getMyDate((bytes[7+i]<<24 | bytes[8+i]<<16 | bytes[9+i]<<8 | bytes[10+i]).toString(10)); + var string='['+aa+','+bb+','+cc+','+dd+','+ee+']'+','; + return string; +} + +function getzf(c_num){ + if(parseInt(c_num) < 10) + c_num = '0' + c_num; + return c_num; +} + +function getMyDate(str) { + var c_Date; + if (str > 9999999999) { + c_Date = new Date(parseInt(str)); + } else { + c_Date = new Date(parseInt(str) * 1000); + } + var c_Year = c_Date.getUTCFullYear(), + c_Month = c_Date.getUTCMonth() + 1, + c_Day = c_Date.getUTCDate(), + c_Hour = c_Date.getUTCHours(), + c_Min = c_Date.getUTCMinutes(), + c_Sen = c_Date.getUTCSeconds(); + var c_Time = c_Year + '-' + getzf(c_Month) + '-' + getzf(c_Day) + ' ' + getzf(c_Hour) + ':' + getzf(c_Min) + ':' + getzf(c_Sen); + return c_Time; +} + + +function Decoder(bytes, port) { + if(port==0x03) + { + for(var i=0;i>4&0x0f)+'.'+(bytes[2]&0x0f); + var bat= (bytes[5]<<8 | bytes[6])/1000; + + return { + SENSOR_MODEL:sensor, + FIRMWARE_VERSION:firm_ver, + FREQUENCY_BAND:freq_band, + SUB_BAND:sub_band, + BAT:bat, + }; + } + else + { + var tdc_interval=(bytes[0]&0x04)?"YES":"NO"; + var alarm=(bytes[0]&0x02)?"TRUE":"FALSE"; + var leak_status=(bytes[0]&0x01)?"LEAK":"NO LEAK"; + var leak_times=bytes[1]<<16 | bytes[2]<<8 | bytes[3]; + var leak_duration=bytes[4]<<16 | bytes[5]<<8 | bytes[6]; + var data_time= getMyDate((bytes[7]<<24 | bytes[8]<<16 | bytes[9]<<8 | bytes[10]).toString(10)); + + if(bytes.length==11) + { + return { + TDC:tdc_interval, + ALARM:alarm, + WATER_LEAK_STATUS:leak_status, + WATER_LEAK_TIMES:leak_times, + LAST_WATER_LEAK_DURATION:leak_duration, + TIME:data_time + }; + } + } +} + +function decodeUplink(input) { + return { + data : Decoder(input.bytes, input.fPort), + }; +} + +function normalizeUplink(input) { + var data = []; + + if (input.data.WATER_LEAK_STATUS) { + data.push({ + water: { + leak: input.data.WATER_LEAK_STATUS === "LEAK" + } + }); + } + + if (input.data.BAT) { + data.push({ + battery: input.data.BAT + }); + } + + return { data: data }; +} diff --git a/vendor/dragino/lwl03a.png b/vendor/dragino/lwl03a.png new file mode 100644 index 0000000000..1cbd41576f Binary files /dev/null and b/vendor/dragino/lwl03a.png differ diff --git a/vendor/dragino/lwl03a.yaml b/vendor/dragino/lwl03a.yaml new file mode 100644 index 0000000000..9a9dca3911 --- /dev/null +++ b/vendor/dragino/lwl03a.yaml @@ -0,0 +1,112 @@ +name: LWL03A - None-Position Rope Type Water Leak Controller +description: The Dragino LWL03A is a LoRaWAN None-Position Rope Type Water Leak Controller. User can lay the LWL03A + Water Leak Cable on the ground to detect water leakage. + +# Firmware versions (at least one is mandatory) +firmwareVersions: + - # Firmware version + version: '1.0' + numeric: 1 + + # LoRaWAN Device Profiles per region + # Supported regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # Unique identifier of the profile (lowercase, alphanumeric with dashes, max 36 characters) + id: eu868-profile + lorawanCertified: true + codec: lwl03a-codec + EU433: + id: eu433-profile + lorawanCertified: true + codec: lwl03a-codec + US902-928: + id: us915-profile + lorawanCertified: true + codec: lwl03a-codec + KR920-923: + id: kr920-profile + lorawanCertified: true + codec: lwl03a-codec + AU915-928: + id: au915-profile + lorawanCertified: true + codec: lwl03a-codec + RU864-870: + id: ru864-profile + lorawanCertified: true + codec: lwl03a-codec + IN865-867: + id: in865-profile + lorawanCertified: true + codec: lwl03a-codec + CN470-510: + id: cn470-profile + lorawanCertified: true + codec: lwl03a-codec + AS923: + id: as923-profile + lorawanCertified: true + codec: lwl03a-codec + + - version: 'lwl03a abp' + numeric: 2 + profiles: + EU863-870: + id: eu868-a-abp-profile + lorawanCertified: true + codec: lwl03a-codec + EU433: + id: eu433-a-abp-profile + lorawanCertified: true + codec: lwl03a-codec + US902-928: + id: us915-a-abp-profile + lorawanCertified: true + codec: lwl03a-codec + AU915-928: + id: au915-a-abp-profile + lorawanCertified: true + codec: lwl03a-codec + AS923: + id: as923-a-abp-profile + lorawanCertified: true + codec: lwl03a-codec + CN470-510: + id: cn470-a-abp-profile + lorawanCertified: true + codec: lwl03a-codec + IN865-867: + id: in865-a-abp-profile + lorawanCertified: true + codec: lwl03a-codec + KR920-923: + id: kr920-a-abp-profile + lorawanCertified: true + codec: lwl03a-codec + RU864-870: + id: ru864-a-abp-profile + lorawanCertified: true + codec: lwl03a-codec +# Sensors that this device features (optional) +# Valid values are: +# 4-20 ma, accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co, co2, conductivity, +# current, digital input, dissolved oxygen, distance, dust, energy, gps, gyroscope, h2s, humidity, iaq, level, light, +# lightning, link, magnetometer, moisture, motion, no, no2, o3, particulate matter, ph, pir, pm2.5, pm10, potentiometer, +# power, precipitation, pressure, proximity, pulse count, pulse frequency, radar, rainfall, rssi, snr, solar radiation, +# sound, strain, surface temperature, temperature, tilt, time, tvoc, uv, vapor pressure, velocity, vibration, voltage, +# water potential, water, weight, wifi ssid, wind direction, wind speed. +sensors: + - water + +# Product and data sheet URLs (optional) +productURL: https://www.dragino.com/products/lorawan-nb-iot-door-sensor-water-leak/item/241-lwl03a.html +dataSheetURL: http://wiki.dragino.com/xwiki/bin/view/Main/User%20Manual%20for%20LoRaWAN%20End%20Nodes/LWL03A%20%E2%80%93%20LoRaWAN%20None-Position%20Rope%20Type%20Water%20Leak%20Controller%20User%20Manual/ +sellerURLs: + - name: 'Dragino' + region: + - China + url: http://www.dragino.com/ + +# Photos +photos: + main: lwl03a.png diff --git a/vendor/elsys/elsys.js b/vendor/elsys/elsys.js index b992c7130f..9d74d7c1b7 100644 --- a/vendor/elsys/elsys.js +++ b/vendor/elsys/elsys.js @@ -237,3 +237,38 @@ function decodeUplink(input) { "data": DecodeElsysPayload(input.bytes) } } + +function normalizeUplink(input) { + var data = {}; + var air = {}; + var action = {}; + var motion = {}; + + if (input.data.temperature) { + air.temperature = input.data.temperature; + } + + if (input.data.humidity) { + air.relativeHumidity = input.data.humidity; + } + + if (input.data.light) { + air.lightIntensity = input.data.light; + } + + if (input.data.motion) { + motion.detected = input.data.motion > 0; + motion.count = input.data.motion; + action.motion = motion; + } + + if (Object.keys(air).length > 0) { + data.air = air; + } + + if (Object.keys(action).length > 0) { + data.action = action; + } + + return { data: data }; +} diff --git a/vendor/elsys/ems-desk-codec.yaml b/vendor/elsys/ems-desk-codec.yaml index 8ceeb900ea..a73bb270ff 100644 --- a/vendor/elsys/ems-desk-codec.yaml +++ b/vendor/elsys/ems-desk-codec.yaml @@ -16,3 +16,8 @@ uplinkDecoder: y: 39 z: 5 occupancy: 2 + normalizedOutput: + data: + - air: + temperature: 22.6 + relativeHumidity: 41 diff --git a/vendor/elsys/ers-co2-codec.yaml b/vendor/elsys/ers-co2-codec.yaml index 9d585e71d2..3efab2ef0b 100644 --- a/vendor/elsys/ers-co2-codec.yaml +++ b/vendor/elsys/ers-co2-codec.yaml @@ -15,3 +15,13 @@ uplinkDecoder: light: 39 motion: 6 co2: 776 + normalizedOutput: + data: + - air: + temperature: 22.6 + relativeHumidity: 41 + lightIntensity: 39 + action: + motion: + detected: true + count: 6 diff --git a/vendor/elsys/ers-codec.yaml b/vendor/elsys/ers-codec.yaml index dcd5afc555..2ef694561a 100644 --- a/vendor/elsys/ers-codec.yaml +++ b/vendor/elsys/ers-codec.yaml @@ -14,3 +14,13 @@ uplinkDecoder: humidity: 41 light: 39 motion: 6 + normalizedOutput: + data: + - air: + temperature: 22.6 + relativeHumidity: 41 + lightIntensity: 39 + action: + motion: + detected: true + count: 6 diff --git a/vendor/elv/elv-bm-trx1.js b/vendor/elv/elv-bm-trx1.js index 8b1419cc67..a836712dbe 100644 --- a/vendor/elv/elv-bm-trx1.js +++ b/vendor/elv/elv-bm-trx1.js @@ -1,7 +1,7 @@ /* * ELV modular system Payload-Parser * - * Version: V1.9.0 + * Version: V1.10.1 * * */ @@ -786,7 +786,7 @@ function Decoder(bytes, port) { } case 0x14: //6-Axis-Sensor { - index++; + index++; decoded.Acc_x = !!(bytes[index] & 0x01); decoded.Acc_y = !!(bytes[index] & 0x02); decoded.Acc_z = !!(bytes[index] & 0x04); @@ -797,26 +797,65 @@ function Decoder(bytes, port) { } case 0x15: //Window-State { - index++; + index++; data = bytes[index]; if (data < 100) { - decoded.window_state = data; + decoded.Window_State = data; } else if (data == 255) { - decoded.window_state = "Tilted" + decoded.Window_State = "Tilted" } else { - decoded.window_state = "Undefined" + decoded.Window_State = "Undefined" } break; } case 0x16: { index++; - decoded.situation = bytes[index]; + decoded.Situation = bytes[index]; + break; + } + case 0x17: // UV-Index + { + decoded.UVI = bytes[++index]; + break; + } + case 0x18: // UV-A + { + decoded.UVA = ( bytes[++index] << 24 ) | ( bytes[++index] << 16 ) | ( bytes[++index] << 8 ) | bytes[++index]; + decoded.UVA /= 1000000; + + break; + } + case 0x19: // UV-B + { + decoded.UVB = ( bytes[++index] << 24 ) | ( bytes[++index] << 16 ) | ( bytes[++index] << 8 ) | bytes[++index]; + decoded.UVB /= 1000000; + + break; + } + case 0x1A: // UV-C + { + decoded.UVC = ( bytes[++index] << 24 ) | ( bytes[++index] << 16 ) | ( bytes[++index] << 8 ) | bytes[++index]; + decoded.UVC /= 1000000; + + break; + } + case 0x1B: // Irradiance + { + decoded.Irradiance = ( bytes[++index] << 8 ) | bytes[++index]; + + if( decoded.Irradiance == 0xFFFF ) + { + decoded.Irradiance = 0; + } + + decoded.Irradiance /= 10; + break; } // case 0x??: // Further Data Type @@ -853,14 +892,3 @@ function Decoder(bytes, port) { return decoded; } - -function decodeDownlink(input) { - return { - data: { - bytes: input.bytes - }, - warnings: [], - errors: [] - } -} - diff --git a/vendor/example/windsensor.yaml b/vendor/example/windsensor.yaml index 1b9015f5ee..e663722bac 100644 --- a/vendor/example/windsensor.yaml +++ b/vendor/example/windsensor.yaml @@ -153,7 +153,7 @@ onboardingGuideURL: https://www.thethingsindustries.com/docs/devices/models/wind resellerURLs: - name: 'Reseller 1' region: # valid regions are: Argentina, Australia, Brazil, Canada, China, European Union, India, Indonesia. - # Japan, Mexico, Russia, Saudi Arabia, South Africa, South Korea, Turkey, United States, Other + # Japan, Mexico, Russia, Saudi Arabia, South Africa, South Korea, Turkey, United States, United Kingdom, Other - European Union url: https://example.org/reseller1 - name: 'Reseller 2' diff --git a/vendor/fludia/fm432g-10-15mn-decode.js b/vendor/fludia/fm432g-10-15mn-decode.js index de792da889..6a60073c9e 100644 --- a/vendor/fludia/fm432g-10-15mn-decode.js +++ b/vendor/fludia/fm432g-10-15mn-decode.js @@ -115,11 +115,11 @@ function decode_T1(payload,time_step){ function decode_T1_adjustable_step(payload){ var data = {}; data.time_step = payload[1]; - data.index = (payload[2] & 0xFF) << 24 | (payload[3] & 0xFF) << 16 | (payload[4] & 0xFF) << 8 | (payload[5] & 0xFF); + data.index = ((payload[2] & 0xFF) << 24 | (payload[3] & 0xFF) << 16 | (payload[4] & 0xFF) << 8 | (payload[5] & 0xFF))/10; data.increments = [] var nb_values_in_payload = (payload.length-6)/2 for(i=0;i 01 + decodedPayload.push({ "t": 'payloadVersion', "l": 0, "v": (b0 & 48) >> 4 }); + + //Will listen always 1 + parity bit + decodedPayload.push({ "t": 'willListen', "l": 0, "v": ((b0 & 64)) }); + + // the number of TLV elements present in the subsequent data block (0-255) + var payloadLength = parseInt('0x' + hexString[2] + hexString[3]); + + if (payloadLength !== (hexString.length / 2) - 2) { + decodedPayload.push({ "t": 'payloadLength', "l": -1, "v": 'payload length incoherence detected' }); + } + + var i = 4; + while (i < hexString.length) { + var tVal, lVal = 0; + var valVal = ""; + // extracting tag value + tVal = hexString[i] + hexString[i + 1] + ""; + // extracting length value + lVal = parseInt("0x" + hexString[i + 2] + hexString[i + 3]); + // extracting data + for (var j = i + 4; j < i + 4 + (lVal * 2); j = j + 2) { + valVal += "" + hexString[j] + hexString[j + 1]; + } + decodedPayload.push({ "t": tVal, "l": lVal, "v": parseHexString(valVal) }); + i = i + 4 + (lVal * 2); + } + return decodedPayload; +} + +/** + * Interprets an array of TLV objects using a given mapping. + * + * @param {object} tlvmap - A mapping of descriptions to TLV tags. + * @param {Array} tlvs - Array of TLV objects with properties `t` (tag) and `v` (value). + * @returns {object} - An object mapping descriptions to their corresponding values. + */ +function interpretTlv(tlvmap, tlvs) { + // Reverse the tlvmap to map numbers to descriptions + const numToDescription = {}; + for (const key in tlvmap) { + if (tlvmap.hasOwnProperty(key)) { + numToDescription[tlvmap[key]] = key; + } + } + + // Initialize the result object + const result = {}; + + // Iterate over the decodedPayload + tlvs.forEach(entry => { + if (entry.l > 0) { + const tagNum = entry.t; + const value = entry.v; + + // Get the description from the tlvmap + const description = numToDescription[parseHexString(tagNum)]; + + if (description) { + result[description] = value; + } + } + }); + + return result; +} + +/** + * Converts a hex string to a byte array. + * + * @param {string} hex - The hex string to convert. + * @returns {Uint8Array} - The byte array representation of the hex string. + */ +function hexToBytes(hexString) { + // Convert a hex string to a byte array + let bytes = []; + for (let i = 0; i < hexString.length; i += 2) { + bytes.push(parseInt(hexString.substr(i, 2), 16)); + } + return new Uint8Array(bytes); +} +/** + * Decodes a byte array based on specific encoding rules. + * + * @param {Uint8Array} data - The byte array to decode. + * @returns {number | string | object} - The decoded value: + * - A number for single-byte or 16-bit little-endian integers. + * - A boolean as 0 or 1. + * - A string or object for UTF-8 encoded JSON. + * @throws {Error} - If JSON decoding fails. + */ +function decodeValue(data) { + // Decode the value based on the given length rules + if (data.length < 3) { + // Handle 16-bit integers (little-endian format) or 1-byte integers + if (data.length === 1) { + // Single byte integer + return data[0]; + } else if (data.length === 2) { + // Two bytes integer (16-bit) + return data[0] | (data[1] << 8); + } + } else { + // Handle UTF-8 encoded JSON string + try { + const text = new TextDecoder('utf-8').decode(data); + return JSON.parse(text); + } catch (e) { + throw new Error("Failed to decode JSON string"); + } + } +} + +/** + * Encode a value to its hex representation. + * + * @param {any} value - The value to encode. + * @returns {string} - The encoded value as a hex string. + */ +function encodeValueToHex(value) { + if (typeof value === 'number') { + if (Number.isInteger(value) && value >= 0 && value <= 65535) { + if (value < 256) { + // Single byte integer + return value.toString(16).padStart(2, '0').toUpperCase(); + } else { + // Two bytes integer (16-bit, little-endian) + let byte1 = (value & 0xFF).toString(16).padStart(2, '0').toUpperCase(); // Least significant byte + let byte2 = ((value >> 8) & 0xFF).toString(16).padStart(2, '0').toUpperCase(); // Most significant byte + return byte1 + byte2; + } + } + } + + if (typeof value === 'boolean') { + // Boolean as single byte (00 or 01) + return value ? '01' : '00'; + } + + // Encode as UTF-8 JSON string for other data types + let jsonString = JSON.stringify(value); + let encoder = new TextEncoder(); // Use TextEncoder to convert string to UTF-8 bytes + let utf8Array = encoder.encode(jsonString); + + // Convert each byte to a two-character hex string + let hexString = Array.from(utf8Array, byte => byte.toString(16).padStart(2, '0').toUpperCase()).join(''); + return hexString; +} + +/** + * Parses a hex string according to the specified rules. + * + * This function takes a hex string and processes it to decode the value according to specific encoding rules: + * - Integers (0 to 65535) are encoded directly in little-endian format. + * - Booleans are treated as 1-byte integers with values 0 or 1. + * - Strings and other complex types are encoded as UTF-8 JSON strings. + * + * @param {string} hexString - The hex string to be parsed. Each pair of characters in the string represents a byte. + * @returns {number | string | object} - The decoded value, which can be: + * - A number (integer or boolean) if the input represents an integer or boolean. + * - A string or an object if the input represents a UTF-8 encoded JSON string. + */ +function parseHexString(hexString) { + // Parse a hex string according to the specified rules + const byteData = hexToBytes(hexString); + return decodeValue(byteData); +} + +/** + * Recursively flattens a nested JSON object. + * + * @param {object} obj - The object to flatten. + * @param {string} parentKey - The base key string to prepend to each key. + * @param {object} res - The result object to accumulate flattened values. + * @returns {object} - The flattened object. + */ +function flattenObject(obj, parentKey = '', res = {}) { + for (const [key, value] of Object.entries(obj)) { + const newKey = parentKey ? `${parentKey}-${key}` : key; + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + flattenObject(value, newKey, res); + } else { + // Special case for 'msg' key to provide specific value + if (newKey === 'pager-data-msg') { + res[newKey] = 'Help is on the way!'; + } else { + res[newKey] = value; + } + } + } + return res; +} + +/** + * Transforms the input object based on the TLV map to produce an output object. + * + * @param {object} inputObject - The object with keys in the format "prefix-key" and values to be transformed. + * @param {object} tlvMap - The TLV map containing the mapping from key to TLV value. + * @returns {object} - The resulting output object with TLV values as keys and corresponding input values. + */ +function transformBasedOnTLV(inputObject, tlvMap) { + const output = {}; + + for (const [inputKey, value] of Object.entries(inputObject)) { + // Extract the key part from the input key (e.g., "app-dev-power-gauge" => "dev-power-gauge") + const keyPart = inputKey.split('-').slice(1).join('-'); + + // Look up the TLV value for the key part + const tlvKey = tlvMap[keyPart]; + + if (tlvKey !== undefined) { + // Map the TLV key to the corresponding value from the input object + output[tlvKey] = value; + } else { + console.warn(`Key "${keyPart}" not found in TLV map.`); + } + } + + return output; +} + +/** + * Calculates the parity bit for a given byte to ensure even parity. + * + * @param {number} byte - The byte for which to calculate the parity bit. + * @returns {number} - The parity bit (0 or 1). + */ +function calculateParity(byte) { + let parity = 0; + while (byte) { + parity ^= (byte & 1); + byte >>= 1; + } + return parity; +} + +/** + * Creates the first byte (Byte 0) for the packet with the specified downlink ID. + * + * @param {number} downlinkId - The current downlink ID (0-15). + * @returns {number} - The constructed Byte 0. + */ +function createByte0(downlinkId) { + let byte0 = (downlinkId & 0x0F) | (1 << 4); // bits 0-3 for downlinkId, bits 4-5 for protocolVersion (1) + byte0 &= ~(1 << 6); // bit 6 is RFU and set to 0 + let parityBit = calculateParity(byte0) % 2 === 0 ? 0 : 1; // Ensure even parity + byte0 |= (parityBit << 7); // bit 7 for parity bit + return byte0; +} + +/** + * Converts an input array into a hex byte string suitable for sending to a device. + * + * @param {Object} data - The input array containing key-value pairs where keys are tags and values are the data. + * @returns {string} - The resulting hex byte string. + */ +function arrayToHexBytes(data) { + let byte0 = createByte0(downlinkId); + let byte1 = Object.keys(data).length; // Number of TLV elements + + let tlvData = []; + for (const [key, value] of Object.entries(data)) { + let tag = parseInt(key); + let valueBytes; + if (typeof value === 'string') { + valueBytes = Array.from(value).map(c => c.charCodeAt(0)); + } else if (typeof value === 'number') { + valueBytes = [value]; + } else { + throw new Error('Unsupported value type'); + } + let length = valueBytes.length; + tlvData.push(tag, length, ...valueBytes); + } + + // Increment downlinkId for next packet + downlinkId = (downlinkId + 1) & 0x0F; // Keep downlinkId within 4 bits + + // Combine all parts into a single byte array + let resultBytes = [byte0, byte1, ...tlvData]; + + // Convert byte array to hex string + let hexString = resultBytes.map(byte => byte.toString(16).padStart(2, '0')).join(''); + return hexString; +} + +/** + * Takes a JSON object, flattens it, transforms it based on a TLV map, and converts it to a hex byte string. + * + * @param {Object} jsonObj - The JSON object to be processed. + * @param {Object} tlvMap - The map defining how to transform the flattened object. + * @returns {string} - The resulting hex byte string. + */ +function downlink(jsonObj, tlvMap) { + return arrayToHexBytes(transformBasedOnTLV(flattenObject(jsonObj), tlvMap));; +} + +/** + * Takes a hex byte string, parses it using `parsePayload`, and interprets the TLV data using `interpretTlv` to return a JSON object. + * + * @param {string} hexString - The input hex byte string to be processed. + * @returns {Object} - The resulting JSON object after parsing and interpreting the TLV data. + */ +function uplink(hexString) { + return interpretTlv(tlvMap, parsePayload(hexString)); +} + + + +/** + * Encoding of the value part of the TLV (Type-Length-Value) structure. + * + * - If the value is `null`, it is encoded with a length of 0, and no value bits are present. + * - For integers between 0 and 65535: + * - Values less than 256 are encoded as a single byte. + * - Values 256 or greater are encoded as two bytes in little-endian format (least significant byte first). + * - For booleans: + * - Encoded as a single byte with values `0` (false) or `1` (true). + * - For all other value types: + * - Encoded as a UTF-8 JSON string. The entire JSON string is included within the TLV. + * - Note: The TLV cannot be split across multiple packets due to message size limits. + * + + Example JSON message + +const jsonTestMessage = { + "app": { + "dev": { + "power": { + "tempC": 33.29999, + "ext": true, + "charging": false, + "voltage": 3.814, + "gauge": 40 + } + }, + "pager": { + "data": { + "msg": "Help is on the way!" + }, + "actions": { + "gotoPage": 2 + } + } + } +}; + + Example TLV map +const tlvMap = { + "dev-power-gauge": 1, + "pager-data-msg": 2, + "pager-actions-gotoPage": 3 +}; */ \ No newline at end of file diff --git a/vendor/infrafon/cc1.png b/vendor/infrafon/cc1.png new file mode 100644 index 0000000000..a8ef83a7c3 Binary files /dev/null and b/vendor/infrafon/cc1.png differ diff --git a/vendor/infrafon/cc1.yaml b/vendor/infrafon/cc1.yaml new file mode 100644 index 0000000000..b7856d1382 --- /dev/null +++ b/vendor/infrafon/cc1.yaml @@ -0,0 +1,91 @@ +name: CC1 +description: Tiny smart badge with a highres 600x400 pixel interactive e-paper screen and remote management capabilities of smartphone-like functions and user interactions with DataView tasks, while maintaining stability and efficiency by restricting users access to functional setup details. +firmwareVersions: + - version: 'r5' + numeric: 1 + profiles: + EU863-870: + id: cc1-profile + lorawanCertified: false + codec: cc1-codec + US902-928: + id: cc1-profile + lorawanCertified: false + codec: cc1-codec + EU433: + id: cc1-profile + lorawanCertified: false + codec: cc1-codec + AU915-928: + id: cc1-profile + lorawanCertified: false + codec: cc1-codec + CN470-510: + id: cc1-profile + lorawanCertified: false + codec: cc1-codec + KR920-923: + id: cc1-profile + lorawanCertified: false + codec: cc1-codec + IN865-867: + id: cc1-profile + lorawanCertified: false + codec: cc1-codec + RU864-870: + id: cc1-profile + lorawanCertified: false + codec: cc1-codec + AS923: + id: cc1-profile + lorawanCertified: false + codec: cc1-codec + +deviceType: devkit + +sensors: + - accelerometer # Bosch BMA456 + - vibration + - gps # SONY CXD5605 + +additionalRadios: + - ble # Pycom L01 Module + - cellular # Sequans Monarch GM02S + - wifi # Pycom L01 Module + - nfc # NXP PN7150, NXP NTP5210 + +dimensions: + width: 8.3 + length: 50 + height: 90.2 + +weight: 51 + +battery: + replaceable: false + type: LiPo + +keyProvisioning: + - custom + +keyProgramming: + - wifi + +keySecurity: secure element + +firmwareProgramming: + - serial + +productURL: https://www.infrafon.com/technologies +dataSheetURL: https://assets-global.website-files.com/60b10e9934cfe86bb1c56e3e/65439a9a19e4083f0a763116_CC1%20Data%20sheet%20(%2Bcertifications).pdf +onboardingGuideURL: https://www.infrafon.com/get-started + +msrp: + EUR: 300 + +photos: + main: cc1.png + other: + - cc1-developer-kit.png +videos: + main: https://www.youtube.com/watch?v=NnmGxJ9R2F0 diff --git a/vendor/infrafon/index.yaml b/vendor/infrafon/index.yaml new file mode 100644 index 0000000000..c1955b1928 --- /dev/null +++ b/vendor/infrafon/index.yaml @@ -0,0 +1,9 @@ +endDevices: + - cc1 + +profileIDs: + '1': + endDeviceID: 'cc1' + firmwareVersion: 'r5' + hardwareVersion: '1.0' + region: 'EU863-870' diff --git a/vendor/infrafon/infrafon_logo.png b/vendor/infrafon/infrafon_logo.png new file mode 100644 index 0000000000..9ebceda051 Binary files /dev/null and b/vendor/infrafon/infrafon_logo.png differ diff --git a/vendor/infrafon/package-lock.json b/vendor/infrafon/package-lock.json new file mode 100644 index 0000000000..576e9babaf --- /dev/null +++ b/vendor/infrafon/package-lock.json @@ -0,0 +1,166 @@ +{ + "name": "infrafon", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "chai": "^4.3.6", + "moment": "^2.30.1" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "engines": { + "node": "*" + } + }, + "node_modules/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "engines": { + "node": "*" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "engines": { + "node": "*" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "engines": { + "node": ">=4" + } + } + }, + "dependencies": { + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" + }, + "chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, + "check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "requires": { + "get-func-name": "^2.0.2" + } + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "requires": { + "type-detect": "^4.0.0" + } + }, + "get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==" + }, + "loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "requires": { + "get-func-name": "^2.0.1" + } + }, + "moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==" + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==" + }, + "type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==" + } + } +} diff --git a/vendor/infrafon/package.json b/vendor/infrafon/package.json new file mode 100644 index 0000000000..f9470fc5cb --- /dev/null +++ b/vendor/infrafon/package.json @@ -0,0 +1,10 @@ +{ + "type": "module", + "scripts": { + "test": "node testjsfile.js" + }, + "dependencies": { + "chai": "^4.3.6", + "moment": "^2.30.1" + } +} diff --git a/vendor/infrafon/testjsfile.mjs b/vendor/infrafon/testjsfile.mjs new file mode 100644 index 0000000000..29b6420e8a --- /dev/null +++ b/vendor/infrafon/testjsfile.mjs @@ -0,0 +1,41 @@ +import PayloadDecoder from './cc1.mjs'; +import chai from 'chai'; + +const { expect } = chai; + + +function testFunction() { + // Define the uplink message in hexadecimal format + const uplinkHex = '5001010164'; + + // Decode the uplink message using PayloadDecoder + const result = PayloadDecoder.parseAndInterprete(uplinkHex); + + // Print the result to check the output + console.log('Decoded Uplink Message:', result); + + // Test assertions + expect(result).to.have.property('battery'); + expect(result.battery.data).to.equal(100); + + // Define the message to send to the pager in hexadecimal format + const pagerMessageHex = '900202082230313233343522030131'; + + // Decode the pager message using PayloadDecoder + const pagerResult = PayloadDecoder.parseAndInterprete(pagerMessageHex); + + // Print the result to check the output + console.log('Decoded Pager Message:', pagerResult); + + // Test assertions for the pager message + expect(pagerResult).to.have.property('getConfig'); + expect(pagerResult.getConfig.data).to.deep.include({ + key: '02', + value: '23031323343522' + }); + expect(pagerResult).to.have.property('pager'); + expect(pagerResult.pager.data.msg).to.equal('012345'); + expect(pagerResult.pager.actions.gotoPage).to.equal('1'); +} + +testFunction(); \ No newline at end of file diff --git a/vendor/laird/rs1xx-temp-rh-sensor-codec.yaml b/vendor/laird/rs1xx-temp-rh-sensor-codec.yaml index b6af7e7d07..d6725186dd 100644 --- a/vendor/laird/rs1xx-temp-rh-sensor-codec.yaml +++ b/vendor/laird/rs1xx-temp-rh-sensor-codec.yaml @@ -16,6 +16,11 @@ uplinkDecoder: humidity: 52.33 alarmMsgCount: 256 backlogMsgCount: 1 + normalizedOutput: + data: + - air: + temperature: 98.67 + relativeHumidity: 52.33 - description: Send Temp and RH Aggregated Data Notification input: diff --git a/vendor/laird/rs1xx-temp-rh-sensor.js b/vendor/laird/rs1xx-temp-rh-sensor.js index 89485b2a1e..88c8a2b850 100644 --- a/vendor/laird/rs1xx-temp-rh-sensor.js +++ b/vendor/laird/rs1xx-temp-rh-sensor.js @@ -3481,3 +3481,13 @@ function decodeUplink(input) { return(result); } +function normalizeUplink(input) { + return { + data: { + air: { + temperature: input.data.temperature, + relativeHumidity: input.data.humidity, + } + } + } +} diff --git a/vendor/makerfabs/ath20-codec.yaml b/vendor/makerfabs/ath20-codec.yaml new file mode 100644 index 0000000000..e5bd7ad6df --- /dev/null +++ b/vendor/makerfabs/ath20-codec.yaml @@ -0,0 +1,61 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://www.thethingsindustries.com/docs/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: ath20.js + # Examples (optional) + examples: + - description: Temperature + input: + fPort: 2 + bytes: [0x00, 0x01, 0x1F, 0x02, 0x0B, 0x01, 0x32, 0x00, 0x00, 0x00, 0x01] + output: + data: { 'Bat': 3.1, 'Humi': 52.3, 'Temp': 30.6 } + # # Normalized output, uses the normalizeUplink function (optional) + # normalizedOutput: + # data: + # - air: + # location: 'indoor' + # temperature: 30.6 + # relativeHumidity: 52.3 + # - air: + # location: 'outdoor' + # temperature: 27.81 + # battery: 3.062 + + # - description: Unknown FPort + # input: + # fPort: 42 + # bytes: [0x00, 0x01, 0x1F, 0x02, 0x0B, 0x01, 0x32, 0x00, 0x00, 0x00, 0x01] + # output: + # errors: + # - unknown FPort + +# Downlink encoder encodes JSON object into a binary data downlink (optional) +downlinkEncoder: + fileName: ath20.js + examples: + - description: Change Reporting interval + input: + data: + minutes: 10 + output: + bytes: [0x00, 0x00, 0x02, 0x58] + fPort: 1 + # - description: Invalid color + # input: + # data: + # led: blue + # output: + # errors: + # - invalid LED color +# # Downlink decoder decodes the encoded downlink message (optional, must be symmetric with downlinkEncoder) +# downlinkDecoder: +# fileName: Air-Temperature-and-Humidity-Sensor.js +# examples: +# - description: Turn green +# input: +# fPort: 2 +# bytes: [1] +# output: +# data: +# led: green diff --git a/vendor/makerfabs/ath20.jpg b/vendor/makerfabs/ath20.jpg new file mode 100644 index 0000000000..362bdd152f Binary files /dev/null and b/vendor/makerfabs/ath20.jpg differ diff --git a/vendor/makerfabs/ath20.js b/vendor/makerfabs/ath20.js new file mode 100644 index 0000000000..54e5470995 --- /dev/null +++ b/vendor/makerfabs/ath20.js @@ -0,0 +1,54 @@ +function decodeUplink(input) { + + // var num = input.bytes[0] * 256 + input.bytes[1] + var bat = input.bytes[2] / 10.0 + var humi = (input.bytes[3] * 256 + input.bytes[4]) / 10.0 + // var temp = (input.bytes[5] * 256 + input.bytes[6] ) / 10.0 + + var temp = input.bytes[5] * 256 + input.bytes[6] + if (temp >= 0x8000) { + temp -= 0x10000; + } + temp = temp / 10.0 + + + return { + data: { + Bat: bat, + Humi: humi, + Temp: temp, + }, + + }; +} + + + +// Encoder function to be used in the TTN console for downlink payload +function encodeDownlink(input) { + var minutes = input.data.minutes; + + // Converting minutes to seconds + var seconds = minutes * 60; + + // If the number of seconds is less than 300 seconds, set it to 300 seconds + if (seconds < 300) { + seconds = 300; + } + var bytes1 = (seconds >> 24) & 0xFF; + var bytes2 = (seconds >> 16) & 0xFF; + var bytes3 = (seconds >> 8) & 0xFF; + var bytes4 = seconds & 0xFF; + + // var payload = [ + // (seconds >> 24) & 0xFF, + // (seconds >> 16) & 0xFF, + // (seconds >> 8) & 0xFF, + // seconds & 0xFF + // ]; + + return { + bytes: [bytes1,bytes2,bytes3,bytes4], + fPort: 1, + }; +} \ No newline at end of file diff --git a/vendor/makerfabs/ath20.yaml b/vendor/makerfabs/ath20.yaml new file mode 100644 index 0000000000..15d979d8e0 --- /dev/null +++ b/vendor/makerfabs/ath20.yaml @@ -0,0 +1,109 @@ +name: ath20 - air-temperature-and-humidity-sensor # Device name can not contain the vendor name +description: LoRaWAN Temperature & Humidity sensor + +# Hardware versions (optional, use when you have revisions) +hardwareVersions: + - version: '1.0' + numeric: 1 + +# Firmware versions (at least one is mandatory) +firmwareVersions: + - # Firmware version + version: '2.0' + numeric: 1 + + # Corresponding hardware versions (optional) + hardwareVersions: + - '1.0' + + # # Firmware features (optional) + # # Valid values are: remote rejoin (trigger a join from the application layer), transmission interval (configure how + # # often he device sends a message). + # features: + # - remote rejoin + # - transmission interval + + # LoRaWAN Device Profiles per region + # Supported regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, + # RU864-870 + profiles: + EU863-870: + # # Optional identifier of the vendor of the profile. When you specify the vendorID, the profile is loaded from + # # the vendorID's folder. This allows you to reuse profiles from module or LoRaWAN end device stack vendors. + # # If vendorID is empty, the current vendor ID is used. In this example, the vendorID is the current vendor ID, + # # which is verbose. + # vendorID: example + # Identifier of the profile (lowercase, alphanumeric with dashes, max 36 characters) + id: eu868-profile + lorawanCertified: true + codec: ath20-codec + US902-928: + id: us915-profile + lorawanCertified: true + codec: ath20-codec + + # - # You can add more firmware versions and use different profiles per version + # version: '2.0' + # numeric: 2 + # hardwareVersions: + # - '1.0-rev-A' + # profiles: + # EU863-870: + # id: windsensor-profile + # lorawanCertified: true + # codec: windsensor-codec + # US902-928: + # id: windsensor-profile + # lorawanCertified: true + # codec: windsensor-codec + # AS923: + # id: windsensor-profile + # codec: windsensor-codec + +# Sensors that this device features (optional) +# Valid values are: +# 4-20 ma, accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co, co2, conductivity, current, digital input, +# digital output, dissolved oxygen, distance, dust, energy, gps, gyroscope, h2s, hall effect, humidity, iaq, infrared, leaf wetness, level, +# light, lightning, link, magnetometer, moisture, motion, nfc, no, no2, o3, occupancy, optical meter, particulate matter, ph, pir, +# pm2.5, pm10, potentiometer, power, precipitation, pressure, proximity, pulse count, pulse frequency, radar, rainfall, reed switch, rssi, +# sap flow, smart valve, smoke, snr, so2, solar radiation, sound, strain, surface temperature, switch, temperature, tilt, time, turbidity, +# tvoc, uv, vapor pressure, velocity, vibration, voltage, water potential, water, weight, wifi ssid, wind direction, wind speed. +sensors: + - temperature + - humidity + +# Operating conditions (optional) +operatingConditions: + # Temperature (Celsius) + temperature: + min: -40 + max: 85 + # Relative humidity (fraction of 1) + relativeHumidity: + min: 0 + max: 1 + +# # IP rating (optional) +# ipCode: IP68 + +# Product and data sheet URLs (optional) +productURL: https://www.agrosense.cc/pd.jsp?recommendFromPid=0&id=6&fromMid=366 +dataSheetURL: https://github.com/Makerfabs/Agrosense-Decoder/blob/main/AgroSense%20LoRaWAN%40%20Sensor%20Catalogue_V1.0.pdf + +# Commercial information +sellerURLs: + - name: 'makerfabs' + region: # valid regions are: Argentina, Australia, Brazil, Canada, China, European Union, India, Indonesia. + # Japan, Mexico, Russia, Saudi Arabia, South Africa, South Korea, Turkey, United States, United Kingdom, Other + - china + url: http://www.makerfabs.com/ + +# Photos +photos: + main: ath20.jpg # Image needs to have a transparent background + # other: + # - windsensor-package.png # Image needs to have a transparent background + +# Youtube or Vimeo Video (optional) +videos: + main: https://www.youtube.com/watch?v=SXF6HpzQwL0 diff --git a/vendor/makerfabs/eu868-profile.yaml b/vendor/makerfabs/eu868-profile.yaml new file mode 100644 index 0000000000..e6cbc7fc49 --- /dev/null +++ b/vendor/makerfabs/eu868-profile.yaml @@ -0,0 +1,47 @@ +# LoRaWAN MAC version: 1.0, 1.0.1, 1.0.2, 1.0.3, 1.0.4 or 1.1 +macVersion: '1.0.3' +# LoRaWAN Regional Parameters version. Values depend on the LoRaWAN version: +# 1.0: TS001-1.0 +# 1.0.1: TS001-1.0.1 +# 1.0.2: RP001-1.0.2 or RP001-1.0.2-RevB +# 1.0.3: RP001-1.0.3-RevA +# 1.0.4: RP002-1.0.0 or RP002-1.0.1 +# 1.1: RP001-1.1-RevA or RP001-1.1-RevB +regionalParametersVersion: 'RP001-1.0.3-RevA' + +# Whether the end device supports join (OTAA) or not (ABP) +supportsJoin: true +# If your device is an ABP device (supportsJoin is false), uncomment the following fields: +# RX1 delay +#rx1Delay: 5 +# RX1 data rate offset +#rx1DataRateOffset: 0 +# RX2 data rate index +#rx2DataRateIndex: 0 +# RX2 frequency (MHz) +#rx2Frequency: 869.525 +# Factory preset frequencies (MHz) +#factoryPresetFrequencies: [868.1, 868.3, 868.5, 867.1, 867.3, 867.5, 867.7, 867.9] + +# Maximum EIRP +maxEIRP: 16 +# Whether the end device supports 32-bit frame counters +supports32bitFCnt: true + +# Whether the end device supports class B +supportsClassB: false +# If your device supports class B, uncomment the following fields: +# Maximum delay for the end device to answer a MAC request or confirmed downlink frame (seconds) +#classBTimeout: 60 +# Ping slot period (seconds) +#pingSlotPeriod: 128 +# Ping slot data rate index +#pingSlotDataRateIndex: 0 +# Ping slot frequency (MHz). Set to 0 if the band supports ping slot frequency hopping. +#pingSlotFrequency: 869.525 + +# Whether the end device supports class C +supportsClassC: false +# If your device supports class C, uncomment the following fields: +# Maximum delay for the end device to answer a MAC request or confirmed downlink frame (seconds) +#classCTimeout: 60 diff --git a/vendor/makerfabs/index.yaml b/vendor/makerfabs/index.yaml new file mode 100644 index 0000000000..ca0882e933 --- /dev/null +++ b/vendor/makerfabs/index.yaml @@ -0,0 +1,2 @@ +endDevices: + - ath20 diff --git a/vendor/makerfabs/us915-profile.yaml b/vendor/makerfabs/us915-profile.yaml new file mode 100644 index 0000000000..9c10263ddc --- /dev/null +++ b/vendor/makerfabs/us915-profile.yaml @@ -0,0 +1,47 @@ +# LoRaWAN MAC version: 1.0, 1.0.1, 1.0.2, 1.0.3, 1.0.4 or 1.1 +macVersion: 1.0.3 +# LoRaWAN Regional Parameters version. Values depend on the LoRaWAN version: +# 1.0: TS001-1.0 +# 1.0.1: TS001-1.0.1 +# 1.0.2: RP001-1.0.2 or RP001-1.0.2-RevB +# 1.0.3: RP001-1.0.3-RevA +# 1.0.4: RP002-1.0.0 or RP002-1.0.1 +# 1.1: RP001-1.1-RevA or RP001-1.1-RevB +regionalParametersVersion: RP001-1.0.3-RevA + +# Whether the end device supports join (OTAA) or not (ABP) +supportsJoin: true +# If your device is an ABP device (supportsJoin is false), uncomment the following fields: +# RX1 delay +#rx1Delay: 5 +# RX1 data rate offset +#rx1DataRateOffset: 0 +# RX2 data rate index +#rx2DataRateIndex: 0 +# RX2 frequency (MHz) +#rx2Frequency: 869.525 +# Factory preset frequencies (MHz) +#factoryPresetFrequencies: [868.1, 868.3, 868.5, 867.1, 867.3, 867.5, 867.7, 867.9] + +# Maximum EIRP +maxEIRP: 20 +# Whether the end device supports 32-bit frame counters +supports32bitFCnt: true + +# Whether the end device supports class B +supportsClassB: false +# If your device supports class B, uncomment the following fields: +# Maximum delay for the end device to answer a MAC request or confirmed downlink frame (seconds) +#classBTimeout: 60 +# Ping slot period (seconds) +#pingSlotPeriod: 128 +# Ping slot data rate index +#pingSlotDataRateIndex: 0 +# Ping slot frequency (MHz). Set to 0 if the band supports ping slot frequency hopping. +#pingSlotFrequency: 869.525 + +# Whether the end device supports class C +supportsClassC: false +# If your device supports class C, uncomment the following fields: +# Maximum delay for the end device to answer a MAC request or confirmed downlink frame (seconds) +classCTimeout: 60 diff --git a/vendor/mclimate/16aspm-codec.yaml b/vendor/mclimate/16aspm-codec.yaml new file mode 100644 index 0000000000..bcd152574f --- /dev/null +++ b/vendor/mclimate/16aspm-codec.yaml @@ -0,0 +1,15 @@ +uplinkDecoder: + fileName: 16aspm.js + examples: + - description: Periodic uplink + input: + fPort: 2 + bytes: [0x01, 0x1C, 0x03, 0x4A, 0x24, 0x18, 0x05, 0xD9, 0xE7, 0x19, 0x52, 0x01] + output: + data: + internalTemperature: 28 + energy_kWh: 55190.552 + power_W: 1497 + acVoltage_V: 231 + acCurrent_mA: 6482 + relayState: 'ON' diff --git a/vendor/mclimate/16aspm-profile.yaml b/vendor/mclimate/16aspm-profile.yaml new file mode 100644 index 0000000000..ad1fba2485 --- /dev/null +++ b/vendor/mclimate/16aspm-profile.yaml @@ -0,0 +1,8 @@ +supportsClassB: false +supportsClassC: true +macVersion: 1.0.3 +regionalParametersVersion: RP001-1.0.3-RevA +supportsJoin: true +maxEIRP: 16 +supports32bitFCnt: true +classCTimeout: 60 diff --git a/vendor/mclimate/16aspm.js b/vendor/mclimate/16aspm.js new file mode 100644 index 0000000000..686afc05bf --- /dev/null +++ b/vendor/mclimate/16aspm.js @@ -0,0 +1,205 @@ +function decodeUplink(input) { + try { + var bytes = input.bytes; + var data = {}; + + function handleKeepalive(bytes, data) { + data.internalTemperature = bytes[1]; + + // Energy data + var energy = (bytes[2] << 24) | (bytes[3] << 16) | (bytes[4] << 8) | bytes[5]; + data.energy_kWh = energy / 1000; + + // Power data + var power = (bytes[6] << 8) | bytes[7]; + data.power_W = power; + + // AC voltage + data.acVoltage_V = bytes[8]; + + // AC current data + var acCurrent = (bytes[9] << 8) | bytes[10]; + data.acCurrent_mA = acCurrent; + + // Relay state + data.relayState = bytes[11] === 0x01 ? "ON" : "OFF"; + return data; + } + + function handleResponse(bytes, data){ + var commands = bytes.map(function(byte){ + return ("0" + byte.toString(16)).substr(-2); + }); + commands = commands.slice(0,-12); + var command_len = 0; + + commands.map(function (command, i) { + switch (command) { + case '04': + { + command_len = 2; + var hardwareVersion = commands[i + 1]; + var softwareVersion = commands[i + 2]; + data.deviceVersions = { hardware: Number(hardwareVersion), software: Number(softwareVersion) }; + } + break; + case '12': + { + command_len = 1; + data.keepAliveTime = parseInt(commands[i + 1], 16); + } + break; + case '19': + { + command_len = 1; + var commandResponse = parseInt(commands[i + 1], 16); + var periodInMinutes = commandResponse * 5 / 60; + data.joinRetryPeriod = periodInMinutes; + } + break; + case '1b': + { + command_len = 1; + data.uplinkType = parseInt(commands[i + 1], 16) ; + } + break; + case '1d': + { + command_len = 2; + var wdpC = commands[i + 1] == '00' ? false : parseInt(commands[i + 1], 16); + var wdpUc = commands[i + 2] == '00' ? false : parseInt(commands[i + 2], 16); + data.watchDogParams= { wdpC: wdpC, wdpUc: wdpUc } ; + } + break; + case '1f': + { + command_len = 1; + data.overheatingThreshold = parseInt(commands[i + 1], 16); + } + break; + case '21': + { + command_len = 2; + data.overvoltageThreshold = (parseInt(commands[i + 1], 16) << 8) | parseInt(commands[i + 2], 16) ; + } + break; + case '23': + { + command_len = 1; + data.overcurrentThreshold = parseInt(commands[i + 1], 16) ; + } + break; + case '25': + { + command_len = 2; + data.overpowerThreshold = (parseInt(commands[i + 1], 16) << 8) | parseInt(commands[i + 2], 16) ; + } + break; + case '5a': + { + command_len = 1; + data.afterOverheatingProtectionRecovery = parseInt(commands[i + 1], 16) + } + break; + case '5c': + { + command_len = 1; + data.ledIndicationMode = parseInt(commands[i + 1], 16) + } + break; + case '5d': + { + command_len = 1; + data.manualChangeRelayState = parseInt(commands[i + 1], 16) === 0x01 + } + break; + case '5f': + { + command_len = 1; + data.relayRecoveryState = parseInt(commands[i + 1], 16) ; + } + break; + case '60': + { + command_len = 2; + data.overheatingEvents = { events: parseInt(commands[i + 1], 16), temperature: parseInt(commands[i + 2], 16) } ; + } + break; + case '61': + { + command_len = 3; + data.overvoltageEvents = { events: parseInt(commands[i + 1], 16), voltage: (parseInt(commands[i + 2], 16) << 8) | parseInt(commands[i + 3], 16) }; + } + break; + case '62': + { + command_len = 3; + data.overcurrentEvents = { events: parseInt(commands[i + 1], 16), current: (parseInt(commands[i + 2], 16) << 8) | parseInt(commands[i + 3], 16) } + } + break; + case '63': + { + command_len = 3; + data.overpowerEvents = { events: parseInt(commands[i + 1], 16), power: (parseInt(commands[i + 2], 16) << 8) | parseInt(commands[i + 3], 16) }; + } + break; + case '70': + { + command_len = 2; + data.overheatingRecoveryTime = (parseInt(commands[i + 1], 16) << 8) | parseInt(commands[i + 2], 16) ; + } + break; + case '71': + { + command_len = 2; + data.overvoltageRecoveryTime = (parseInt(commands[i + 1], 16) << 8) | parseInt(commands[i + 2], 16); + } + break; + case '72': + { + command_len = 1; + data.overcurrentRecoveryTemp = parseInt(commands[i + 1], 16); + } + break; + case '73': + { + command_len = 1; + data.overpowerRecoveryTemp = parseInt(commands[i + 1], 16); + } + break; + case 'b1': + { + command_len = 1; + data.relayState = parseInt(commands[i + 1], 16) === 0x01 + } + break; + case 'a0': + { + command_len = 4; + let fuota_address = parseInt(`${commands[i + 1]}${commands[i + 2]}${commands[i + 3]}${commands[i + 4]}`, 16) + let fuota_address_raw = `${commands[i + 1]}${commands[i + 2]}${commands[i + 3]}${commands[i + 4]}` + data.fuota = { fuota_address, fuota_address_raw }; + } + break; + default: + break; + } + commands.splice(i,command_len); + }); + return data; + } + + if (bytes[0] == 1) { + data = handleKeepalive(bytes, data); + } else { + data = handleResponse(bytes, data); + // Handle the remaining keepalive data if required after response + bytes = bytes.slice(-12); + data = handleKeepalive(bytes, data); + } + return { data: data }; + } catch (e) { + // console.log(e); + throw new Error('Unhandled data'); + } +} \ No newline at end of file diff --git a/vendor/mclimate/16aspm.png b/vendor/mclimate/16aspm.png new file mode 100644 index 0000000000..e314555688 Binary files /dev/null and b/vendor/mclimate/16aspm.png differ diff --git a/vendor/mclimate/16aspm.yaml b/vendor/mclimate/16aspm.yaml new file mode 100644 index 0000000000..da78728991 --- /dev/null +++ b/vendor/mclimate/16aspm.yaml @@ -0,0 +1,133 @@ +name: 16A Switch & Power Meter (16ASPM) +description: The МClimate 16A Switch & Power Meter LoRaWAN (16ASPM) is a miniature device that features a wet 16A relay and an electricity meter inside. The device has 4 terminals L, N, N, Lout, connecting and disconnecting Lout from Lin. The device operates in LoRaWAN Class C, features FUOTA (Firmware Upgrades Over The Air) and has overheating protection + +# Hardware versions (optional, use when you have revisions) +hardwareVersions: + - version: '1.3' + numeric: 1 + +# Firmware versions (at least one is mandatory) +firmwareVersions: + - # Firmware version + version: '1.2' + numeric: 1 + hardwareVersions: + - '1.3' + + # LoRaWAN Device Profiles per region + # Supported regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # Unique identifier of the profile (lowercase, alphanumeric with dashes, max 36 characters) + id: 16aspm-profile + lorawanCertified: false + codec: 16aspm-codec + +# Sensors that this device features (optional) +# Valid values are: accelerometer, altitude, auxiliary, barometer, battery, button, co2, distance, dust, gps, gyroscope, +# humidity, light, link, magnetometer, moisture, ph, pir, proximity, rssi, snr, sound, temperature, tvoc, velocity, +# vibration, water, wind direction and wind speed. +sensors: + - temperature +# Dimensions in mm (optional) +# Use width, height, length and/or diameter +dimensions: + width: 36 + length: 18 + height: 32 + +# Weight in grams (optional) +weight: 24 + +# Battery information (optional) +battery: + replaceable: false + +# Operating conditions (optional) +operatingConditions: + # Temperature (Celsius) + temperature: + min: -10 + max: 40 + # Relative humidity (fraction of 1) + relativeHumidity: + min: 0.00 + max: 0.80 + +# IP rating (optional) +ipCode: IP30 + +# Key provisioning (optional) +# Valid values are: custom (user can configure keys), join server and manifest. +keyProvisioning: + - custom + - manifest + +# Key security (optional) +# Valid values are: none, read protected and secure element. +keySecurity: read protected + +# Product and data sheet URLs (optional) +productURL: https://docs.mclimate.eu/mclimate-lorawan-devices/devices/mclimate-16a-switch-and-power-meter-lorawan-16aspm +dataSheetURL: https://3940008670-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-McDr-jr9h3qA888r1Yp%2Fuploads%2F6ySZvZUpeLGiEza7qAWz%2FMClimate-16ASPM-Datasheet.pdf?alt=media&token=baf39b06-3946-4f7e-acc3-29faadc0f0e0 +# resellerURLs: +# - name: 'Connected Things' +# region: +# - United Kingdom +# url: https://connectedthings.store/ +# - name: 'Concept13' +# region: +# - United Kingdom +# url: https://www.concept13.co.uk +# - name: 'RG2i' +# region: +# - France +# url: https://connectedthings.store/ +# - name: 'Wideco' +# region: +# - Sweden +# url: https://wideco.se +# - name: 'iioote' +# region: +# - Sweden +# url: https://iiooote.com + +# Photos +photos: + main: 16aspm.png + +# Regulatory compliances (optional) +compliances: + safety: + - body: IEC + norm: EN + standard: 62368-1:2020 + - body: IEC + norm: EN + standard: 61010-1:2010 + electromagneticCompatibility: + - body: ETSI + norm: EN + standard: 301489-1 + version: 'latest' + - body: ETSI + norm: EN + standard: 301489-17 + version: 'latest' + - body: CENELEC + norm: EN + standard: 55032:2015 + - body: CENELEC + norm: EN + standard: 55035:2017 + radioEquipment: + - body: ETSI + norm: EN + standard: 300220-1 + - body: ETSI + norm: EN + standard: 300220-2 + lowVoltageDirective: + - body: IEC + norm: EN + standard: 2014/35/EU diff --git a/vendor/mclimate/co2-display-lite-codec.yaml b/vendor/mclimate/co2-display-lite-codec.yaml new file mode 100644 index 0000000000..4273ef46e9 --- /dev/null +++ b/vendor/mclimate/co2-display-lite-codec.yaml @@ -0,0 +1,15 @@ +uplinkDecoder: + fileName: co2-display-lite.js + examples: + - description: Periodic uplink + input: + fPort: 2 + bytes: [0x01, 0x02, 0x63, 0x87, 0x0A, 0xAC, 0x39, 0x10, 0x01, 0x17] + output: + data: + sensorTemperature: 21.1 + relativeHumidity: 52.73 + batteryVoltage: 2.73 + CO2: 569 + powerSourceStatus: 0 + lux: 279 diff --git a/vendor/mclimate/co2-display-lite-profile.yaml b/vendor/mclimate/co2-display-lite-profile.yaml new file mode 100644 index 0000000000..4b7c151785 --- /dev/null +++ b/vendor/mclimate/co2-display-lite-profile.yaml @@ -0,0 +1,7 @@ +supportsClassB: false +supportsClassC: false +macVersion: 1.0.3 +regionalParametersVersion: RP001-1.0.3-RevA +supportsJoin: true +maxEIRP: 16 +supports32bitFCnt: true diff --git a/vendor/mclimate/co2-display-lite.js b/vendor/mclimate/co2-display-lite.js new file mode 100644 index 0000000000..858e8d6a9a --- /dev/null +++ b/vendor/mclimate/co2-display-lite.js @@ -0,0 +1,170 @@ +function decodeUplink(input) { + try{ + var bytes = input.bytes; + var data = {}; + const toBool = value => value == '1'; + const calculateTemperature = (rawData) => (rawData - 400) / 10; + const calculateHumidity = (rawData) => (rawData * 100) / 256; + + function handleKeepalive(bytes) { + let data = {}; + + // Temperature calculation from two bytes + let temperatureRaw = (bytes[1] << 8) | bytes[2]; // Shift byte[1] left by 8 bits and OR with byte[2] + data.sensorTemperature = Number(calculateTemperature(temperatureRaw).toFixed(2)); + + // Humidity calculation + data.relativeHumidity = Number(calculateHumidity(bytes[3]).toFixed(2)); + + // Battery voltage calculation from two bytes + let batteryVoltageRaw = (bytes[4] << 8) | bytes[5]; + data.batteryVoltage = Number((batteryVoltageRaw / 1000).toFixed(2)); + + // CO2 calculation from bytes 6 and 7 + let co2Low = bytes[6]; // Lower byte of CO2 + let co2High = (bytes[7] & 0xF8) >> 3; // Mask the upper 5 bits and shift them right + data.CO2 = (co2High << 8) | co2Low; // Shift co2High left by 8 bits and combine with co2Low + + // Power source status + data.powerSourceStatus = bytes[7] & 0x07; // Extract the last 3 bits directly + + // Light intensity from two bytes + let lightIntensityRaw = (bytes[8] << 8) | bytes[9]; + data.lux = lightIntensityRaw; + + return data; + } + + function handleResponse(bytes, data){ + var commands = bytes.map(function(byte){ + return ("0" + byte.toString(16)).substr(-2); + }); + commands = commands.slice(0,-8); + var command_len = 0; + + commands.map(function (command, i) { + switch (command) { + case '04': + { + command_len = 2; + var hardwareVersion = commands[i + 1]; + var softwareVersion = commands[i + 2]; + data.deviceVersions = { hardware: Number(hardwareVersion), software: Number(softwareVersion) }; + } + break; + case '12': + { + command_len = 1; + data.keepAliveTime = parseInt(commands[i + 1], 16); + } + break; + case '14': + { + command_len = 1; + data.childLock = toBool(parseInt(commands[i + 1], 16)) ; + } + break; + case '19': + { + command_len = 1; + var commandResponse = parseInt(commands[i + 1], 16); + var periodInMinutes = commandResponse * 5 / 60; + data.joinRetryPeriod = periodInMinutes; + } + break; + case '1b': + { + command_len = 1; + data.uplinkType = parseInt(commands[i + 1], 16) ; + } + break; + case '1f': + { + command_len = 4; + let good_medium = parseInt(`${commands[i + 1]}${commands[i + 2]}`, 16); + let medium_bad = parseInt(`${commands[i + 3]}${commands[i + 4]}`, 16); + + data.boundaryLevels = { good_medium: Number(good_medium), medium_bad: Number(medium_bad) } ; + } + break; + case '1d': + { + command_len = 2; + var deviceKeepAlive = 5; + var wdpC = commands[i + 1] == '00' ? false : commands[i + 1] * deviceKeepAlive + 7; + var wdpUc = commands[i + 2] == '00' ? false : parseInt(commands[i + 2], 16); + data.watchDogParams= { wdpC: wdpC, wdpUc: wdpUc } ; + } + break; + case '21': + { + command_len = 2; + data.autoZeroValue = parseInt(`${commands[i + 1]}${commands[i + 2]}`, 16); + } + break; + case '25': + { + command_len = 3; + let good_zone = parseInt(commands[i + 1], 16); + let medium_zone = parseInt(commands[i + 2], 16); + let bad_zone = parseInt(commands[i + 3], 16); + + data.measurementPeriod = { good_zone: Number(good_zone), medium_zone: Number(medium_zone), bad_zone: Number(bad_zone) } ; + } + break; + case '2b': + { + command_len = 1; + data.autoZeroPeriod = parseInt(commands[i + 1], 16); + } + break; + case '34': + { + command_len = 1; + data.displayRefreshPeriod = parseInt(commands[i + 1], 16) ; + } + break; + case '41': + { + command_len = 1; + data.currentTemperatureVisibility = parseInt(commands[i + 1], 16) ; + } + break; + case '43': + { + command_len = 1; + data.humidityVisibility = parseInt(commands[i + 1], 16) ; + } + break; + case '45': + { + command_len = 1; + data.lightIntensityVisibility = parseInt(commands[i + 1], 16) ; + } + break; + case '80': + { + command_len = 1; + data.measurementBlindTime = parseInt(commands[i + 1], 16) ; + } + break; + default: + break; + } + commands.splice(i,command_len); + }); + return data; + } + if (bytes[0] == 1) { + data = handleKeepalive(bytes, data); + }else{ + data = handleResponse(bytes,data); + bytes = bytes.slice(-10); + data = handleKeepalive(bytes, data); + } + return {data: data}; + } catch (e) { + console.log(e) + throw new Error('Unhandled data'); + } +} \ No newline at end of file diff --git a/vendor/mclimate/co2-display-lite.png b/vendor/mclimate/co2-display-lite.png new file mode 100644 index 0000000000..c61130684a Binary files /dev/null and b/vendor/mclimate/co2-display-lite.png differ diff --git a/vendor/mclimate/co2-display-lite.yaml b/vendor/mclimate/co2-display-lite.yaml new file mode 100644 index 0000000000..58cd4a1d2f --- /dev/null +++ b/vendor/mclimate/co2-display-lite.yaml @@ -0,0 +1,137 @@ +name: CO2 Display Lite +description: MClimate CO2 Display lite LoRaWAN is a stand-alone CO2 sensor powered entirely by solar energy using an organic solar panel. The device features a 1.54" e-ink screen, temperature and humidity sensor, LUX sensor and NDIR CO2 sensor. + +# Hardware versions (optional, use when you have revisions) +hardwareVersions: + - version: '1.2' + numeric: 1 + +# Firmware versions (at least one is mandatory) +firmwareVersions: + - # Firmware version + version: '1.0' + numeric: 1 + hardwareVersions: + - '1.2' + + # LoRaWAN Device Profiles per region + # Supported regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # Unique identifier of the profile (lowercase, alphanumeric with dashes, max 36 characters) + id: co2-display-lite-profile + lorawanCertified: false + codec: co2-display-lite-codec + - # Firmware version + version: '1.1' + numeric: 1 + hardwareVersions: + - '1.2' + + # LoRaWAN Device Profiles per region + # Supported regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # Unique identifier of the profile (lowercase, alphanumeric with dashes, max 36 characters) + id: co2-display-lite-profile + lorawanCertified: false + codec: co2-display-lite-codec + +# Sensors that this device features (optional) +# Valid values are: accelerometer, altitude, auxiliary, barometer, battery, button, co2, distance, dust, gps, gyroscope, +# humidity, light, link, magnetometer, moisture, ph, pir, proximity, rssi, snr, sound, temperature, tvoc, velocity, +# vibration, water, wind direction and wind speed. +sensors: + - temperature + - humidity + - light + - co2 +# Dimensions in mm (optional) +# Use width, height, length and/or diameter +dimensions: + width: 122 + length: 22 + height: 58 + +# Weight in grams (optional) +weight: 80 + +# Operating conditions (optional) +operatingConditions: + # Temperature (Celsius) + temperature: + min: 0 + max: 50 + # Relative humidity (fraction of 1) + relativeHumidity: + min: 0.00 + max: 0.80 + +# IP rating (optional) +ipCode: IP30 + +# Key provisioning (optional) +# Valid values are: custom (user can configure keys), join server and manifest. +keyProvisioning: + - custom + - manifest + +# Key security (optional) +# Valid values are: none, read protected and secure element. +keySecurity: read protected + +# Product and data sheet URLs (optional) +productURL: https://docs.mclimate.eu/mclimate-lorawan-devices/devices/mclimate-co2-display-lite +dataSheetURL: https://3940008670-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-McDr-jr9h3qA888r1Yp%2Fuploads%2FPTIV0nAWncRDDDk5l0aO%2FMClimate-CO2-Display-Lite-Datasheet.pdf?alt=media&token=24575d81-9f79-41f2-af51-628c87fa9489 +# resellerURLs: +# - name: 'Connected Things' +# region: +# - United Kingdom +# url: https://connectedthings.store/ +# - name: 'Concept13' +# region: +# - United Kingdom +# url: https://www.concept13.co.uk +# - name: 'RG2i' +# region: +# - France +# url: https://connectedthings.store/ +# - name: 'Wideco' +# region: +# - Sweden +# url: https://wideco.se +# - name: 'iioote' +# region: +# - Sweden +# url: https://iiooote.com + +# Photos +photos: + main: co2-display-lite.png + +# Regulatory compliances (optional) +compliances: + radioEquipment: + - body: ETSI + norm: EN + standard: 50491-3 + version: '2009' + - body: ETSI + norm: EN + standard: 300220-1 + version: 'V3.1.1:2017' + - body: ETSI + norm: EN + standard: 300220-2 + version: 'V3.1.1:2017' + - body: ETSI + norm: EN + standard: 301489-1 + version: 'V2.1.1:2017' + safety: + - body: IEC + norm: EN + standard: 60950-1 + version: '2006+A11:2009+A1:2010+A12:2011+A2:2013+AC:2015' + generalCompliance: + - directive: '2014/53/EC' diff --git a/vendor/mclimate/co2-display.yaml b/vendor/mclimate/co2-display.yaml index 88510907a6..0255a982d4 100644 --- a/vendor/mclimate/co2-display.yaml +++ b/vendor/mclimate/co2-display.yaml @@ -1,4 +1,4 @@ -name: MClimate CO2 Display +name: CO2 Display description: MClimate CO2 Display is a stand-alone CO2 sensor powered entirely by solar energy using an organic solar panel. The device features a 2.9" e-ink screen, sensor for movement (PIR), temperature and humidity sensor, LUX sensor and NDIR CO2 sensor. # Hardware versions (optional, use when you have revisions) diff --git a/vendor/mclimate/fan-coil-thermostat-profile.yaml b/vendor/mclimate/fan-coil-thermostat-profile.yaml index 0e08692680..ad1fba2485 100644 --- a/vendor/mclimate/fan-coil-thermostat-profile.yaml +++ b/vendor/mclimate/fan-coil-thermostat-profile.yaml @@ -5,3 +5,4 @@ regionalParametersVersion: RP001-1.0.3-RevA supportsJoin: true maxEIRP: 16 supports32bitFCnt: true +classCTimeout: 60 diff --git a/vendor/mclimate/fan-coil-thermostat.yaml b/vendor/mclimate/fan-coil-thermostat.yaml index e56e3288f2..955a7f751a 100644 --- a/vendor/mclimate/fan-coil-thermostat.yaml +++ b/vendor/mclimate/fan-coil-thermostat.yaml @@ -1,4 +1,4 @@ -name: MClimate Fan Coil Thermostat +name: Fan Coil Thermostat description: MClimate Fan Coil Thermostat (FCT) is a thermostat compatible with 2-pipe and 4-pipe Fan Coil Units with 4 SPST relays, 1 DPST dry relay and 0-10V input/output. It features a 4.2" e-ink display, digital temperature and humidity sensor, and 4 buttons. # Hardware versions (optional, use when you have revisions) diff --git a/vendor/mclimate/index.yaml b/vendor/mclimate/index.yaml index 76f0bbde99..23dae896a0 100644 --- a/vendor/mclimate/index.yaml +++ b/vendor/mclimate/index.yaml @@ -5,3 +5,7 @@ endDevices: - ht-sensor - co2-sensor - wireless-thermostat + - co2-display + - co2-display-lite + - fan-coil-thermostat + - 16aspm diff --git a/vendor/mclimate/vicki-codec.yaml b/vendor/mclimate/vicki-codec.yaml index 31b4c5e05c..f2667d7ccc 100644 --- a/vendor/mclimate/vicki-codec.yaml +++ b/vendor/mclimate/vicki-codec.yaml @@ -23,5 +23,13 @@ uplinkDecoder: calibrationFailed: false perceiveAsOnline: false antiFreezeProtection: false - motorOpenness: 17 - targetTemperatureFloat: '29.00' + valveOpenness: 17 + targetTemperatureFloat: 29 + normalizedOutput: + data: + - air: + temperature: 18.01 + relativeHumidity: 46.88 + battery: 3.5 + warnings: + - 'childLock: true' diff --git a/vendor/mclimate/vicki.js b/vendor/mclimate/vicki.js index e83d78f7e1..a0480ca691 100644 --- a/vendor/mclimate/vicki.js +++ b/vendor/mclimate/vicki.js @@ -24,14 +24,14 @@ function decodeUplink(input) { batteryTmp = ("0" + bytes[7].toString(16)).substr(-2)[0]; batteryVoltageCalculated = 2 + parseInt("0x" + batteryTmp, 16) * 0.1; - decbin = function(number) { + let decbin = (number) => { if (number < 0) { number = 0xFFFFFFFF + number + 1 } number = number.toString(2); return "00000000".substr(number.length) + number; } - byte7Bin = decbin(bytes[8]); + byte7Bin = decbin(bytes[7]); openWindow = byte7Bin[4]; highMotorConsumption = byte7Bin[5]; lowMotorConsumption = byte7Bin[6]; @@ -67,9 +67,9 @@ function decodeUplink(input) { data.attachedBackplate = toBool(attachedBackplate); data.perceiveAsOnline = toBool(perceiveAsOnline); data.antiFreezeProtection = toBool(antiFreezeProtection); - data.motorOpenness = Math.round((1-(motorPosition/motorRange))*100); + data.valveOpenness = motorRange != 0 ? Math.round((1-(motorPosition/motorRange))*100) : 0; if(!data.hasOwnProperty('targetTemperatureFloat')){ - data.targetTemperatureFloat = bytes[1].toFixed(2); + data.targetTemperatureFloat = parseFloat(bytes[1]) } return data; } @@ -169,8 +169,7 @@ function decodeUplink(input) { { // get default keepalive if it is not available in data command_len = 2; - var deviceKeepAlive = 5; - var wdpC = commands[i + 1] == '00' ? false : commands[i + 1] * deviceKeepAlive + 7; + var wdpC = commands[i + 1] == '00' ? false : parseInt(commands[i + 1], 16); var wdpUc = commands[i + 2] == '00' ? false : parseInt(commands[i + 2], 16); var dataJ = { watchDogParams: { wdpC: wdpC, wdpUc: wdpUc } }; resultToPass = merge_obj(resultToPass, dataJ); @@ -311,14 +310,14 @@ function decodeUplink(input) { case '4d': { command_len = 2; - var data = { maxAllowedIntegralValue : (parseInt(`${commands[i + 1]}${commands[i + 2]}`, 16))/10 }; + var data = { piMaxIntegratedError : (parseInt(`${commands[i + 1]}${commands[i + 2]}`, 16))/10 }; resultToPass = merge_obj(resultToPass, data); } break; case '50': { command_len = 2; - var data = { valveOpennessRangeInPercentage: { min: parseInt(commands[i + 1], 16), max: parseInt(commands[i + 2], 16) } }; + var data = { effectiveMotorRange: { minValveOpenness: 100 - parseInt(commands[i + 2], 16), maxValveOpenness: 100 - parseInt(commands[i + 1], 16) } }; resultToPass = merge_obj(resultToPass, data); } break; @@ -356,4 +355,55 @@ function decodeUplink(input) { return { data: data }; -} \ No newline at end of file +} + +function normalizeUplink(input) { + const warnings = []; + + if (input.data.openWindow) { + warnings.push("openWindow: true"); + } + + if (input.data.highMotorConsumption) { + warnings.push("highMotorConsumption: true"); + } + + if (input.data.lowMotorConsumption) { + warnings.push("lowMotorConsumption: true"); + } + + if (input.data.brokenSensor) { + warnings.push("brokenSensor: true"); + } + + if (input.data.childLock) { + warnings.push("childLock: true"); + } + + if (input.data.calibrationFailed) { + warnings.push("calibrationFailed: true"); + } + + if (input.data.attachedBackplate) { + warnings.push("attachedBackplate: true"); + } + + if (input.data.perceiveAsOnline) { + warnings.push("perceiveAsOnline: true"); + } + + if (input.data.antiFreezeProtection) { + warnings.push("antiFreezeProtection: true"); + } + + return { + data: { + air: { + temperature: input.data.sensorTemperature, + relativeHumidity: input.data.relativeHumidity, + }, + battery: input.data.batteryVoltage, + }, + warnings: warnings + }; +} diff --git a/vendor/micropelt/mlr003-codec.yaml b/vendor/micropelt/mlr003-codec.yaml index 82dd02e920..2f223ab514 100644 --- a/vendor/micropelt/mlr003-codec.yaml +++ b/vendor/micropelt/mlr003-codec.yaml @@ -15,6 +15,7 @@ uplinkDecoder: Flow_Temperature: 106.00 Ambient_Sensor_Raw: 60.75 Ambient_Temperature: 40.25 + Temperature_Drop_Detection: 0 Energy_Storage: 0 Harvesting_Active: 1 Ambient_Sensor_Failure: 0 @@ -42,6 +43,7 @@ uplinkDecoder: Flow_Temperature: 13.50 Ambient_Sensor_Raw: 15.00 Ambient_Temperature: 15.00 + Temperature_Drop_Detection: 0 Energy_Storage: 0 Harvesting_Active: 0 Ambient_Sensor_Failure: 0 @@ -69,6 +71,7 @@ uplinkDecoder: Flow_Temperature: 33 Ambient_Sensor_Raw: 28.75 Ambient_Temperature: 24 + Temperature_Drop_Detection: 0 Energy_Storage: 0 Harvesting_Active: 1 Ambient_Sensor_Failure: 0 @@ -94,6 +97,7 @@ uplinkDecoder: Flow_Temperature: 22 Ambient_Sensor_Raw: 19.75 Ambient_Temperature: 19.25 + Temperature_Drop_Detection: 0 Energy_Storage: 0 Harvesting_Active: 0 Ambient_Sensor_Failure: 0 diff --git a/vendor/micropelt/mlr003.js b/vendor/micropelt/mlr003.js index aaf20befe7..ba712a5376 100644 --- a/vendor/micropelt/mlr003.js +++ b/vendor/micropelt/mlr003.js @@ -8,6 +8,7 @@ function decodeUplink(input) { Flow_Temperature: input.bytes[2]*0.5, Ambient_Sensor_Raw: input.bytes[3]*0.25, Ambient_Temperature: input.bytes[4]*0.25, + Temperature_Drop_Detection: input.bytes[5]>>7 & 0x01, Energy_Storage: input.bytes[5]>>6 & 0x01, Harvesting_Active: input.bytes[5]>>5 & 0x01, Ambient_Sensor_Failure: input.bytes[5]>>4 & 0x01, diff --git a/vendor/milesight-iot/em300-th-codec.yaml b/vendor/milesight-iot/em300-th-codec.yaml index 2cb65024d5..90f42e5e6a 100644 --- a/vendor/milesight-iot/em300-th-codec.yaml +++ b/vendor/milesight-iot/em300-th-codec.yaml @@ -2,3 +2,19 @@ # For documentation on writing encoders and decoders, see: https://thethingsstack.io/integrations/payload-formatters/javascript/ uplinkDecoder: fileName: em300-th.js + examples: + - description: Example with battery, temperature, and humidity + input: + fPort: 1 + bytes: [0x01, 0x75, 0x32, 0x03, 0x67, 0xC8, 0x00, 0x04, 0x68, 0x3C] + output: + data: + battery: 50 + temperature: 20.0 + humidity: 30.0 + normalizedOutput: + data: + - air: + temperature: 20.0 + relativeHumidity: 30.0 + battery: 50 diff --git a/vendor/milesight-iot/em300-th.js b/vendor/milesight-iot/em300-th.js index 33ae310161..f0f9d7748d 100644 --- a/vendor/milesight-iot/em300-th.js +++ b/vendor/milesight-iot/em300-th.js @@ -1,3 +1,9 @@ +function decodeUplink(input) { + return { + data : Decoder(input.bytes, input.fPort), + }; +} + function Decoder(bytes, port) { var decoded = {}; @@ -51,6 +57,17 @@ function Decoder(bytes, port) { } +function normalizeUplink(input) { + return { + data: { + air: { + temperature: input.data.temperature, + relativeHumidity: input.data.humidity + }, + battery: input.data.battery, + } + }; +} /* ****************************************** @@ -73,4 +90,4 @@ function readInt16LE(bytes) { return ref > 0x7fff ? ref - 0x10000 : ref; -} \ No newline at end of file +} diff --git a/vendor/milesight-iot/ws301-codec.yaml b/vendor/milesight-iot/ws301-codec.yaml index 92a11f78af..fdac30f692 100644 --- a/vendor/milesight-iot/ws301-codec.yaml +++ b/vendor/milesight-iot/ws301-codec.yaml @@ -5,11 +5,17 @@ uplinkDecoder: - description: Magnetic Contact Switch (example 1) - Milesight IoT input: fPort: 85 - bytes: [0x03, 0x00, 0x01] + bytes: [0x01, 0x75, 0x4B, 0x03, 0x00, 0x01] output: data: - door: open - - description: Magnetic Contact Switch (example 2) - Milesgiht IoT + battery: 75 + door: 'open' + normalizedOutput: + data: + - action: + contactState: 'OPEN' + battery: 75 + - description: Magnetic Contact Switch (example 2) - Milesight IoT input: fPort: 85 bytes: [0x04, 0x00, 0x01] diff --git a/vendor/milesight-iot/ws301.js b/vendor/milesight-iot/ws301.js index f98a16c516..4d3c7f4629 100644 --- a/vendor/milesight-iot/ws301.js +++ b/vendor/milesight-iot/ws301.js @@ -43,3 +43,15 @@ function Decoder(bytes, port) { return decoded; } + + +function normalizeUplink(input) { + return { + data: { + action: { + contactState: input.data.door === "close" ? "CLOSED" : input.data.door === "open" ? "OPEN" : undefined + }, + battery: input.data.battery, + } + }; +} diff --git a/vendor/moko/lw001-bgpro-codec.yaml b/vendor/moko/lw001-bgpro-codec.yaml index 131e529f2c..87ef77dbf3 100644 --- a/vendor/moko/lw001-bgpro-codec.yaml +++ b/vendor/moko/lw001-bgpro-codec.yaml @@ -4,44 +4,56 @@ uplinkDecoder: fileName: lw001-bgpro.js # Examples (optional) -# examples: -# - description: heratbeat payload -# input: -# fPort: 1 -# bytes: [0x21, 0x1E, 0xC0, 0x01, 0x47, 0x00, 0x00, 0x00, 0x00] -# output: -# data: -# battery voltage: '3.4V' -# demolish_state: '0' -# idle_state: '0' -# lorawan_downlink_count: '0' -# low_power_state: '0' -# motion_count: '0' -# motion_state: '1' -# pack_type: 'heart' -# last_restart_reason: 'ble_cmd_restart' -# ic_temperature: '30°C' -# firmware ver: 'V1.0.7' -# work_mode: 'period' - -# - description: gps fixed payload -# input: -# fPort: 2 -# bytes: [0x01, 0x1F, 0xC0, 0x02, 0x07, 0xE6, 0x08, 0x03, 0x09, 0x0F, 0x1F, 0x00, 0x09, 0x12, 0x27, 0x88, 0x8E, 0x44, 0x3A, 0xA0, 0x50, 0x0C] -# output: -# data: -# utc_time: '2022-8-3 9:15:31 Timezone:0' -# battery voltage: '3.4V' -# demolish_state: '0' -# fix_tech: 'gps' -# fix_type: 'work_mode_fix' -# idle_state: '0' -# lat: '30.458075' -# lon: '114.4692816' -# lorawan_downlink_count: '0' -# low_power_state: '0' -# motion_state: '0' -# pack_type: 'fix_success' -# pdop: '1.2' -# ic_temperature: '31°C' -# work_mode: 'period' + examples: + - description: heartbeat payload + input: + fPort: 1 + bytes: [0x21, 0x1E, 0xC0, 0x01, 0x47, 0x00, 0x00, 0x00, 0x00] + output: + data: + battery_voltage: 3.4 + demolish_state: 0 + idle_state: 0 + lorawan_downlink_count: 0 + low_power_state: 0 + motion_count: 0 + motion_state: 1 + pack_type: 'heart' + last_restart_reason: 'ble_cmd_restart' + ic_temperature: '30°C' + firmware_ver: 'V1.0.7' + work_mode: 'period mode' + normalizedOutput: + data: + - action: + motion: + detected: true + count: 0 + battery: 3.4 + - description: gps fixed payload + input: + fPort: 2 + bytes: [0x01, 0x1F, 0xC0, 0x02, 0x07, 0xE6, 0x08, 0x03, 0x09, 0x0F, 0x1F, 0x00, 0x09, 0x12, 0x27, 0x88, 0x8E, 0x44, 0x3A, 0xA0, 0x50, 0x0C] + output: + data: + utc_time: '2022-8-3 9:15:31 TZ:0' + battery_voltage: 3.4 + demolish_state: 0 + fix_tech: 'gps' + fix_type: 'work_mode_fix' + idle_state: 0 + lat: 30.458075 + lon: 114.4692816 + lorawan_downlink_count: 0 + low_power_state: 0 + motion_state: 0 + pack_type: 'fix_success' + pdop: 1.2 + ic_temperature: '31°C' + work_mode: 'period mode' + normalizedOutput: + data: + - position: + latitude: 30.458075 + longitude: 114.4692816 + battery: 3.4 diff --git a/vendor/moko/lw001-bgpro.js b/vendor/moko/lw001-bgpro.js index e29f65fde9..21c25c4927 100644 --- a/vendor/moko/lw001-bgpro.js +++ b/vendor/moko/lw001-bgpro.js @@ -41,7 +41,7 @@ function Decoder(bytes, port) } dev_info.lorawan_downlink_count = bytes[2]&0x0f; - dev_info.battery_voltage = (22+((bytes[2]>>4)&0x0f))/10 + "V"; + dev_info.battery_voltage = (22+((bytes[2]>>4)&0x0f))/10; } if(port == 1) { @@ -218,7 +218,7 @@ function Decoder(bytes, port) dev_info.fix_type = dev_fix_type[(bytes[0]>>6)&0x01]; dev_info.lorawan_downlink_count = bytes[1]&0x0f; - dev_info.battery_voltage = (22+((bytes[2]>>4)&0x0f))/10 + "V"; + dev_info.battery_voltage = (22+((bytes[2]>>4)&0x0f))/10; var parse_len = 2; lat =BytestoInt(bytes,parse_len); @@ -237,3 +237,44 @@ function Decoder(bytes, port) } return dev_info; } + +function decodeUplink(input) { + return { + data : Decoder(input.bytes, input.fPort), + }; +} + +function normalizeUplink(input) { + var data = {}; + var action = {}; + var position = {}; + var motion = {}; + + if (input.data.motion_state) { + motion.detected = input.data.motion_state > 0; + motion.count = input.data.motion_count; + action.motion = motion; + } + + if (input.data.lat) { + position.latitude = input.data.lat; + } + + if (input.data.lon) { + position.longitude = input.data.lon; + } + + if (Object.keys(action).length > 0) { + data.action = action; + } + + if (Object.keys(position).length > 0) { + data.position = position; + } + + if (input.data.battery_voltage) { + data.battery = input.data.battery_voltage; + } + + return { data: data }; +} diff --git a/vendor/nexelec/sign-codec.js b/vendor/nexelec/sign-codec.js index ac48d09161..b98073ce86 100644 --- a/vendor/nexelec/sign-codec.js +++ b/vendor/nexelec/sign-codec.js @@ -29,7 +29,7 @@ function decodeUplink(input) function dataOutput(octetTypeMessage) { - outputTypeMessage=["Reserved",periodicDataOutput(stringHex),historicalCO2DataOutput(stringHex),historicalTemperatureDataOutput(stringHex),"Reserved",productStatusDataOutput(stringHex), + outputTypeMessage=["Reserved",periodicDataOutput(stringHex),historicalCO2DataOutput(stringHex),historicalTemperatureDataOutput(stringHex),historicalHumidityDataOutput(stringHex),productStatusDataOutput(stringHex), productConfigurationDataOutput(stringHex)] return outputTypeMessage[octetTypeMessage] } @@ -39,12 +39,13 @@ function decodeUplink(input) if(octetTypeProduit==0xA9){return "Feel LoRa"} if(octetTypeProduit==0xAA){return "Rise LoRa"} if(octetTypeProduit==0xAB){return "Move LoRa"} + if(octetTypeProduit==0xAC){return "Wave LoRa"} if(octetTypeProduit==0xAD){return "Sign LoRa"} } function typeOfMessage(octetTypeMessage) { - const message_name =["Reserved","Periodic data","CO2 Historical Data","Temperature Historical Data" ,"Reserved","Product Status","Product Configuration"] + const message_name =["Reserved","Periodic data","CO2 Historical Data","Temperature Historical Data" ,"Humidity Historical Data","Product Status","Product Configuration"] return message_name[octetTypeMessage] } @@ -272,7 +273,7 @@ function decodeUplink(input) function loraRegion(octetLoRaRegion) { - const message_name =["EU868","US915","AS923","AU915","KR920","IN865","RU864"] + const message_name =["Reserved","EU868","US915","Reserved","Reserved","Reserved","Reserved","Reserved","SF-RC1"] return message_name[octetLoRaRegion] } @@ -335,37 +336,11 @@ function decodeUplink(input) return string_bin; } - /* - function hexToBinary(stringHex) - { - var hex2=stringHex.toUpperCase() // pour convertir des string en hexa il faut qu'ils soient en majuscule - var str1=""; - var string_bin=""; - var str=0; - var buffer=[]; - - // On crée un tableau d'hexa (buffer) - for(i=0; i> 6) & 0x3FF; @@ -398,7 +373,7 @@ function decodeUplink(input) "iziairCo2": iaqGlobalArgument(data_izi_air_co2), "iziairCov": iaqGlobalArgument(data_izi_air_cov), }; - + return data; } @@ -411,7 +386,7 @@ function decodeUplink(input) var offset_octet = 0; var data_nombre_mesures = (parseInt(stringHex.substring(4,6),16)>>2)&0x3F; - var data_time_between_measurement_sec = ((parseInt(stringHex.substring(4,8),16)>>2)&0xFF); + var data_time_between_measurement_min = ((parseInt(stringHex.substring(4,8),16)>>2)&0xFF); var data_repetition = (parseInt(stringHex.substring(7,9),16))&0x3F; var binary=hexToBinary(stringHex) @@ -421,17 +396,17 @@ function decodeUplink(input) mesure[i]= parseInt(binary.substring(offset_binaire,offset_binaire+10),2); if(mesure[i] === 0x3FF){mesure[i] = 0;} - else{mesure[i] = Math.round(mesure[i] * 5)} + else{mesure[i] = parseFloat((mesure[i] * 5).toFixed(2))} } data={ "typeOfProduct": typeOfProduct(octetTypeProduit), "typeOfMessage": typeOfMessage(octetTypeMessage), "numberOfRecord": data_nombre_mesures, - "periodBetweenRecord":{"value":data_time_between_measurement_sec,"unit":"minutes"}, + "periodBetweenRecord":{"value":data_time_between_measurement_min*10,"unit":"minutes"}, "redundancyOfRecord":data_repetition, "co2":{"value":mesure,"unit":"ppm"}, } - + return data } @@ -441,10 +416,10 @@ function decodeUplink(input) var i = 0; var data_nombre_mesures = (parseInt(stringHex.substring(4,6),16)>>2)&0x3F; - var data_time_between_measurement_sec = ((parseInt(stringHex.substring(4,8),16)>>2)&0xFF); + var data_time_between_measurement_min = ((parseInt(stringHex.substring(4,8),16)>>2)&0xFF); var data_repetition = (parseInt(stringHex.substring(7,9),16))&0x3F; var binary=hexToBinary(stringHex) - + for(i=0;i>2)&0x3F; + var data_time_between_measurement_min = ((parseInt(stringHex.substring(4,8),16)>>2)&0xFF); + var data_repetition = (parseInt(stringHex.substring(7,9),16))&0x3F; + var binary=hexToBinary(stringHex) + + for(i=0;i>3) & 0x1F; var data_date_produit_minute = (parseInt(stringHex.substring(32,34),16)>>1) & 0x3F; + var data_datalog_humidity_on_off = (parseInt(stringHex.substring(33,34),16)) & 0x01; data = {"typeOfProduct": typeOfProduct(octetTypeProduit), @@ -574,6 +580,7 @@ function decodeUplink(input) "deltaTemperature": deltaTemp(data_periodic_delta_temp), "historicalCO2DataActivation":active(data_datalog_co2_on_off), "historicalTemperatureDataActivation":active(data_datalog_temperature_on_off), + "historicalHumidityDataActivation":active(data_datalog_humidity_on_off), "numberOfRecordsInADatalogMessage":data_datalog_new_measure, "numberOfMessagesFor1Record":data_datalog_repetition, "transmissionPeriodOfHistoricalMessage":transmissionPeriodHistorical(data_datalog_tx_period), diff --git a/vendor/nexelec/wave-codec.yaml b/vendor/nexelec/wave-codec.yaml new file mode 100644 index 0000000000..3535c0ce28 --- /dev/null +++ b/vendor/nexelec/wave-codec.yaml @@ -0,0 +1,10 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://thethingsstack.io/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: sign-codec.js + +downlinkEncoder: + fileName: sign-codec.js + +downlinkDecoder: + fileName: sign-codec.js diff --git a/vendor/nexelec/wave-profile.yaml b/vendor/nexelec/wave-profile.yaml new file mode 100644 index 0000000000..87105f11a0 --- /dev/null +++ b/vendor/nexelec/wave-profile.yaml @@ -0,0 +1,47 @@ +# LoRaWAN MAC version: 1.0, 1.0.1, 1.0.2, 1.0.3, 1.0.4 or 1.1 +macVersion: 1.0.4 +# LoRaWAN Regional Parameters version. Values depend on the LoRaWAN version: +# 1.0: TS001-1.0 +# 1.0.1: TS001-1.0.1 +# 1.0.2: RP001-1.0.2 or RP001-1.0.2-RevB +# 1.0.3: RP001-1.0.3-RevA +# 1.0.4: RP002-1.0.0 or RP002-1.0.1 +# 1.1: RP001-1.1-RevA or RP001-1.1-RevB +regionalParametersVersion: RP002-1.0.3 + +# Whether the end device supports join (OTAA) or not (ABP) +supportsJoin: true +# If your device is an ABP device (supportsJoin is false), uncomment the following fields: +# RX1 delay +#rx1Delay: 5 +# RX1 data rate offset +#rx1DataRateOffset: 0 +# RX2 data rate index +#rx2DataRateIndex: 0 +# RX2 frequency (MHz) +#rx2Frequency: 869.525 +# Factory preset frequencies (MHz) +#factoryPresetFrequencies: [868.1, 868.3, 868.5, 867.1, 867.3, 867.5, 867.7, 867.9] + +# Maximum EIRP +maxEIRP: 14 +# Whether the end device supports 32-bit frame counters +supports32bitFCnt: true + +# Whether the end device supports class B +supportsClassB: false +# If your device supports class B, uncomment the following fields: +# Maximum delay for the end device to answer a MAC request or confirmed downlink frame (seconds) +#classBTimeout: 60 +# Ping slot period (seconds) +#pingSlotPeriod: 128 +# Ping slot data rate index +#pingSlotDataRateIndex: 0 +# Ping slot frequency (MHz). Set to 0 if the band supports ping slot frequency hopping. +#pingSlotFrequency: 869.525 + +# Whether the end device supports class C +supportsClassC: false +# If your device supports class C, uncomment the following fields: +# Maximum delay for the end device to answer a MAC request or confirmed downlink frame (seconds) +#classCTimeout: 60 diff --git a/vendor/nexelec/wave.yaml b/vendor/nexelec/wave.yaml new file mode 100644 index 0000000000..8b8d163160 --- /dev/null +++ b/vendor/nexelec/wave.yaml @@ -0,0 +1,134 @@ +name: WAVE - Air Quality +description: WAVE is a 5-in-1 LoRaWAN-connected, battery- or USB-powered room sensor that can be rapidly configurable via NFC. Depending on the model, the sensor can measure co2, temperature, humidity, presence and luminosity. + +# Hardware versions (optional, use when you have revisions) +hardwareVersions: + - version: 'V001' + numeric: 1 + +# Firmware versions (at least one is mandatory) +firmwareVersions: + - # Firmware version + version: 'V01.0' + numeric: 1 + # Corresponding hardware versions (optional) + hardwareVersions: + - 'V001' + + # Firmware features (optional) + # Valid values are: remote rejoin (trigger a join from the application layer), transmission interval (configure how + # often he device sends a message). + features: + - remote rejoin + - transmission interval + + # LoRaWAN Device Profiles per region + # Supported regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, + # RU864-870 + profiles: + EU863-870: + # Optional identifier of the vendor of the profile. When you specify the vendorID, the profile is loaded from + # the vendorID's folder. This allows you to reuse profiles from module or LoRaWAN end device stack vendors. + # If vendorID is empty, the current vendor ID is used. In this example, the vendorID is the current vendor ID, + # which is verbose. + #vendorID: example + # Identifier of the profile (lowercase, alphanumeric with dashes, max 36 characters) + id: wave-profile + lorawanCertified: false + codec: sign-codec + +# Sensors that this device features (optional) +# Valid values are: +# 4-20 ma, accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co, co2, conductivity, +# current, digital input, dissolved oxygen, distance, dust, energy, gps, gyroscope, h2s, humidity, iaq, level, light, +# lightning, link, magnetometer, moisture, motion, no, no2, o3, particulate matter, ph, pir, pm2.5, pm10, potentiometer, +# power, precipitation, pressure, proximity, pulse count, pulse frequency, radar, rainfall, rssi, smart valve, snr, so2, +# solar radiation, sound, strain, surface temperature, temperature, tilt, time, tvoc, uv, vapor pressure, velocity, +# vibration, voltage, water potential, water, weight, wifi ssid, wind direction, wind speed. +sensors: + - co2 + - temperature + - humidity + - motion + - light + +# Additional radios that this device has (optional) +# Valid values are: ble, nfc, wifi, cellular. +additionalRadios: + - nfc + +# Dimensions in mm (optional) +# Use width, height, length and/or diameter +dimensions: + width: 87 + length: 87 + height: 23 + +# Weight in grams (optional) +weight: 110 + +# Battery information (optional) +battery: + replaceable: true + type: AA + +# Operating conditions (optional) +operatingConditions: + # Temperature (Celsius) + temperature: + min: -30 + max: 70 + # Relative humidity (fraction of 1) + relativeHumidity: + min: 0 + max: 1 + +# IP rating (optional) +ipCode: IP30 + +# Key security (optional) +# Valid values are: none, read protected and secure element. +keySecurity: read protected + +# Product and data sheet URLs (optional) +productURL: https://nexelec.eu/produit/rise/ +dataSheetURL: https://support.nexelec.fr/en/support/solutions/articles/80001085097-datasheet/ + +# Commercial information +resellerURLs: + - name: 'Nexelec' + region: + - European Union + url: https://www.nexelec.fr/ + +msrp: + EUR: 165 + USD: 181 + +# Photos +photos: + main: sign.png + +# Regulatory compliances (optional) +compliances: + safety: + - body: IEC + norm: EN + standard: 62368-1 + - body: ETSI + norm: EN + standard: 62479:2010 + + radioEquipment: + - body: ETSI + norm: EN + standard: 301 489-1 + version: 2.2.0 + - body: ETSI + norm: EN + standard: 301 489-3 + version: 2.1.0 + - body: ETSI + norm: EN + standard: 300 220-2 + version: 3.2.1 diff --git a/vendor/nke-watteco/ino-sensor.js b/vendor/nke-watteco/ino-sensor.js index bd7fc71f0d..a2b0d9838b 100644 --- a/vendor/nke-watteco/ino-sensor.js +++ b/vendor/nke-watteco/ino-sensor.js @@ -863,7 +863,16 @@ function Decoder(bytes, port) { // multibinary input present value if ( (clusterdID === 0x8005 ) & (attributID === 0x0000)) { - tab.push({label:stdData = "State"+(decoded.zclheader.endpoint+1), value:stdData = (((bytes[index+1]&0x01) === 0x01)?1:0), date:stdData = lDate}); + tab.push({label:stdData = "State1", value:stdData = (((bytes[index+1]&0x01) === 0x01)?1:0), date:stdData = lDate}); + tab.push({label:stdData = "State2", value:stdData = (((bytes[index+1]&0x02) === 0x02)?1:0), date:stdData = lDate}); + tab.push({label:stdData = "State3", value:stdData = (((bytes[index+1]&0x04) === 0x04)?1:0), date:stdData = lDate}); + tab.push({label:stdData = "State4", value:stdData = (((bytes[index+1]&0x08) === 0x08)?1:0), date:stdData = lDate}); + tab.push({label:stdData = "State5", value:stdData = (((bytes[index+1]&0x10) === 0x10)?1:0), date:stdData = lDate}); + tab.push({label:stdData = "State6", value:stdData = (((bytes[index+1]&0x20) === 0x20)?1:0), date:stdData = lDate}); + tab.push({label:stdData = "State7", value:stdData = (((bytes[index+1]&0x40) === 0x40)?1:0), date:stdData = lDate}); + tab.push({label:stdData = "State8", value:stdData = (((bytes[index+1]&0x80) === 0x80)?1:0), date:stdData = lDate}); + tab.push({label:stdData = "State9", value:stdData = (((bytes[index]&0x01) === 0x01)?1:0), date:stdData = lDate}); + tab.push({label:stdData = "State10", value:stdData = (((bytes[index]&0x02) === 0x02)?1:0), date:stdData = lDate}); } //binary input counter diff --git a/vendor/plenom/busylight.js b/vendor/plenom/busylight.js index 786cab94a6..a661822dcd 100644 --- a/vendor/plenom/busylight.js +++ b/vendor/plenom/busylight.js @@ -20,6 +20,68 @@ function decodeUplink(input) { errors: [] }; } + else if (input.bytes.length == 25) + { + switch (input.bytes[24]) + { + case 0x01: + reason="Power On Reset"; + break; + case 0x02: + reason="Brownout 1.2V"; + break; + case 0x04: + reason="Brownout 3.3V"; + break; + case 0x10: + reason="External Reset"; + break; + case 0x20: + reason="WatchDog Timer triggered"; + break; + case 0x40: + reason="Software"; + break; + case 0x80: + reason="Backup"; + break; + } + return { + data: { + RSSI: byteArrayToLong(input.bytes, 0), + SNR: byteArrayToLong(input.bytes, 4), + messages_received: byteArrayToLong(input.bytes, 8), + messages_send: byteArrayToLong(input.bytes, 12), + lastcolor_red: input.bytes[16], + lastcolor_blue: input.bytes[17], + lastcolor_green: input.bytes[18], + lastcolor_ontime: input.bytes[19], + lastcolor_offtime: input.bytes[20], + sw_rev: input.bytes[21], + hw_rev: input.bytes[22], + adr_state: input.bytes[23], + last_reset_reason: reason + }, + warnings: [], + errors: [] + }; + } + else if (input.bytes.length == 10) + { +return { + data: { + messages_send: byteArrayToLong(input.bytes, 0), + lastcolor_red: input.bytes[4], + lastcolor_blue: input.bytes[5], + lastcolor_green: input.bytes[6], + lastcolor_ontime: input.bytes[7], + lastcolor_offtime: input.bytes[8], + last_reset_reason: input.bytes[9] + }, + warnings: [], + errors: [] + }; + } else { return {data: { @@ -48,8 +110,10 @@ function encodeDownlink(input) { } function decodeDownlink(input) { - +if (input.bytes.length == 5) + { return { + data: { red: input.bytes[0], green: input.bytes[2], @@ -60,4 +124,22 @@ function decodeDownlink(input) { warnings: [], errors: [] } + } +else if (input.bytes.length == 6) + { + return { + + data: { + red: input.bytes[0], + green: input.bytes[2], + blue: input.bytes[1], + ontime: input.bytes[3], + offtime: input.bytes[4], + immediate_uplink: input.byte[5] + }, + warnings: [], + errors: [] + } + } } + diff --git a/vendor/quandify/cubicmeter-1-1-copper-codec.yaml b/vendor/quandify/cubicmeter-1-1-copper-codec.yaml new file mode 100644 index 0000000000..9980bdacb6 --- /dev/null +++ b/vendor/quandify/cubicmeter-1-1-copper-codec.yaml @@ -0,0 +1,4 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://thethingsstack.io/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: cubicmeter-1-1-uplink.js diff --git a/vendor/quandify/cubicmeter-1-1-copper.png b/vendor/quandify/cubicmeter-1-1-copper.png new file mode 100644 index 0000000000..3d034667a7 Binary files /dev/null and b/vendor/quandify/cubicmeter-1-1-copper.png differ diff --git a/vendor/quandify/cubicmeter-1-1-copper.yaml b/vendor/quandify/cubicmeter-1-1-copper.yaml new file mode 100644 index 0000000000..24b9516490 --- /dev/null +++ b/vendor/quandify/cubicmeter-1-1-copper.yaml @@ -0,0 +1,122 @@ +name: CubicMeter 1.1 Copper +description: Non-invasive water meter and leakage sensor + +# Hardware versions (optional) +hardwareVersions: + - version: '1.0' + numeric: 1 + +# Firmware versions (at least one is mandatory) +firmwareVersions: + - # Firmware version + version: '1.0' + numeric: 1 + # Supported hardware versions (optional) + hardwareVersions: + - '1.0' # Must refer to hardwareVersions declared above + # LoRaWAN Device Profiles per region + # Supported regions: EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + id: cubicmeter-1-1-profile-eu868 + lorawanCertified: true + codec: cubicmeter-1-1-copper-codec +# Sensors that this device features (optional) +# Valid values are: +# 4-20 ma, accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co, co2, conductivity, +# current, digital input, dissolved oxygen, distance, dust, energy, gps, gyroscope, h2s, humidity, iaq, level, light, +# lightning, link, magnetometer, moisture, motion, no, no2, o3, particulate matter, ph, pir, pm2.5, pm10, potentiometer, +# power, precipitation, pressure, proximity, pulse count, pulse frequency, radar, rainfall, rssi, smart valve, snr, so2, +# solar radiation, sound, strain, surface temperature, temperature, tilt, time, tvoc, uv, vapor pressure, velocity, +# vibration, voltage, water potential, water, weight, wifi ssid, wind direction, wind speed. +sensors: + - battery + - temperature + - water + +# Additional radios that this device has (optional) +# Valid values are: ble, nfc, wifi, cellular. +# additionalRadios: +# - + +# Bridge interfaces (optional) +# Valid values are: modbus, m-bus, can bus, rs-485, sdi-12, analog. +# bridgeInterfaces: +# - + +# Dimensions in mm (optional) +# Use width, height, length and/or diameter +dimensions: + width: 39 + length: 86 + height: 82 + +# Weight in grams (optional) +weight: 260 + +# Battery information (optional) +battery: + replaceable: false + type: 3.6V Li-SOCI2 + +# Operating conditions (optional) +operatingConditions: + # Temperature (Celsius) + temperature: + min: 5 + max: 30 + # Relative humidity (fraction of 1) + # relativeHumidity: + # min: + # max: + +# IP rating (optional) +# ipCode: + +# Key provisioning (optional) +# Valid values are: custom (user can configure keys), join server and manifest. +# keyProvisioning: +# - + +# Key programming (optional) +# Valid values are: bluetooth, nfc, wifi, serial (when the user has a serial interface to set the keys) +# and firmware (when the user should change the firmware to set the keys). +# keyProgramming: +# - + +# Key security (optional) +# Valid values are: none, read protected and secure element. +# keySecurity: + +# Firmware programming (optional) +# Valid values are: serial (when the user has a serial interface to update the firmware), fuota lorawan (when the device +# supports LoRaWAN FUOTA via standard interfaces) and fuota other (other wireless update mechanism). +# firmwareProgramming: +# - + +# Product and data sheet URLs (optional) +productURL: https://quandify.com/cubicmeter +# dataSheetURL: + +# Photos (MAKE SURE THE IMAGE HAS A TRANSPARENT BACKGROUND) +photos: + main: cubicmeter-1-1-copper.png +# Youtube or Vimeo Video (optional) +# videos: +# main: + +# Regulatory compliances (optional) +# compliances: +# safety: +# - body: +# norm: +# standard: +# radioEquipment: +# - body: +# norm: +# standard: +# version: +# - body: +# norm: +# standard: +# version: diff --git a/vendor/quandify/cubicmeter-1-1-plastic-codec.yaml b/vendor/quandify/cubicmeter-1-1-plastic-codec.yaml new file mode 100644 index 0000000000..0bf116efa3 --- /dev/null +++ b/vendor/quandify/cubicmeter-1-1-plastic-codec.yaml @@ -0,0 +1,85 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://thethingsstack.io/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: cubicmeter-1-1-uplink.js + + # Examples + examples: + - description: Normal status report + input: + fPort: 1 + bytes: [73, 251, 223, 1, 0, 0, 222, 20, 0, 0, 0, 0, 0, 0, 70, 237, 223, 1, 6, 252, 139, 7, 2, 226, 230, 83, 84, 85] + output: + data: + fPort: 1 + length: 28 + hexBytes: '49FBDF010000DE1400000000000046EDDF0106FC8B0702E2E6535455' + type: 'statusReport' + decoded: + ambientTemperature: 22.5 + batteryActive: 3608 + batteryRecovered: 3640 + errorCode: 0 + isSensing: true + leakState: 2 + totalVolume: 5342 + waterTemperatureMax: 22 + waterTemperatureMin: 21.5 + warnings: [] + errors: [] + normalizedOutput: + data: + - air: + temperature: 22.5 + water: + temperature: + min: 21.5 + max: 22 + leak: '' + metering: + water: + total: 5342 + battery: 3.64 + warnings: [] + errors: [] + - description: Response from set pipe downlink + input: + fPort: 6 + bytes: [4, 0, 2, 16, 0, 0, 22, 2, 5, 63, 1, 0, 0, 3, 0, 227, 221, 203, 177, 189, 117, 135, 50, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0] + output: + data: + fPort: 6 + length: 38 + hexBytes: '0400021000001602053F0100000300E3DDCBB1BD758732000001000000000000010000000000' + type: 'response' + decoded: + fPort: 4 + status: 'ok' + type: 'hardwareReport' + data: + appState: 'metering' + firmwareVersion: '22.0.16' + hardwareVersion: 2 + pipe: + id: 0 + type: 'Custom' + warnings: [] + errors: [] + - description: Response from set lorawan report interval + input: + fPort: 6 + bytes: [19, 0, 4, 1, 192, 168, 0, 0, 88, 2, 0, 0, 0, 0, 0, 30, 0, 0, 141, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + output: + data: + fPort: 6 + length: 41 + hexBytes: '13000401C0A80000580200000000001E00008D27000000000000000000000000000000000000000000' + type: 'response' + decoded: + fPort: 19 + status: 'ok' + type: 'settingsReport' + data: + lorawanReportInterval: 600 + warnings: [] + errors: [] diff --git a/vendor/quandify/cubicmeter-1-1-plastic.png b/vendor/quandify/cubicmeter-1-1-plastic.png new file mode 100644 index 0000000000..f5d9c19bba Binary files /dev/null and b/vendor/quandify/cubicmeter-1-1-plastic.png differ diff --git a/vendor/quandify/cubicmeter-1-1-plastic.yaml b/vendor/quandify/cubicmeter-1-1-plastic.yaml new file mode 100644 index 0000000000..a0df8a9642 --- /dev/null +++ b/vendor/quandify/cubicmeter-1-1-plastic.yaml @@ -0,0 +1,122 @@ +name: CubicMeter 1.1 Plastic +description: Non-invasive water meter and leakage sensor + +# Hardware versions (optional) +hardwareVersions: + - version: '1.0' + numeric: 1 + +# Firmware versions (at least one is mandatory) +firmwareVersions: + - # Firmware version + version: '1.0' + numeric: 1 + # Supported hardware versions (optional) + hardwareVersions: + - '1.0' # Must refer to hardwareVersions declared above + # LoRaWAN Device Profiles per region + # Supported regions: EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + id: cubicmeter-1-1-profile-eu868 + lorawanCertified: true + codec: cubicmeter-1-1-plastic-codec +# Sensors that this device features (optional) +# Valid values are: +# 4-20 ma, accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co, co2, conductivity, +# current, digital input, dissolved oxygen, distance, dust, energy, gps, gyroscope, h2s, humidity, iaq, level, light, +# lightning, link, magnetometer, moisture, motion, no, no2, o3, particulate matter, ph, pir, pm2.5, pm10, potentiometer, +# power, precipitation, pressure, proximity, pulse count, pulse frequency, radar, rainfall, rssi, smart valve, snr, so2, +# solar radiation, sound, strain, surface temperature, temperature, tilt, time, tvoc, uv, vapor pressure, velocity, +# vibration, voltage, water potential, water, weight, wifi ssid, wind direction, wind speed. +sensors: + - battery + - temperature + - water + +# Additional radios that this device has (optional) +# Valid values are: ble, nfc, wifi, cellular. +# additionalRadios: +# - + +# Bridge interfaces (optional) +# Valid values are: modbus, m-bus, can bus, rs-485, sdi-12, analog. +# bridgeInterfaces: +# - + +# Dimensions in mm (optional) +# Use width, height, length and/or diameter +dimensions: + width: 39 + length: 86 + height: 82 + +# Weight in grams (optional) +weight: 260 + +# Battery information (optional) +battery: + replaceable: false + type: 3.6V Li-SOCI2 + +# Operating conditions (optional) +operatingConditions: + # Temperature (Celsius) + temperature: + min: 5 + max: 30 + # Relative humidity (fraction of 1) + # relativeHumidity: + # min: + # max: + +# IP rating (optional) +# ipCode: + +# Key provisioning (optional) +# Valid values are: custom (user can configure keys), join server and manifest. +# keyProvisioning: +# - + +# Key programming (optional) +# Valid values are: bluetooth, nfc, wifi, serial (when the user has a serial interface to set the keys) +# and firmware (when the user should change the firmware to set the keys). +# keyProgramming: +# - + +# Key security (optional) +# Valid values are: none, read protected and secure element. +# keySecurity: + +# Firmware programming (optional) +# Valid values are: serial (when the user has a serial interface to update the firmware), fuota lorawan (when the device +# supports LoRaWAN FUOTA via standard interfaces) and fuota other (other wireless update mechanism). +# firmwareProgramming: +# - + +# Product and data sheet URLs (optional) +productURL: https://quandify.com/cubicmeter +# dataSheetURL: + +# Photos (MAKE SURE THE IMAGE HAS A TRANSPARENT BACKGROUND) +photos: + main: cubicmeter-1-1-plastic.png +# Youtube or Vimeo Video (optional) +# videos: +# main: + +# Regulatory compliances (optional) +# compliances: +# safety: +# - body: +# norm: +# standard: +# radioEquipment: +# - body: +# norm: +# standard: +# version: +# - body: +# norm: +# standard: +# version: diff --git a/vendor/quandify/cubicmeter-1-1-profile-eu868.yaml b/vendor/quandify/cubicmeter-1-1-profile-eu868.yaml new file mode 100644 index 0000000000..4d76e827dc --- /dev/null +++ b/vendor/quandify/cubicmeter-1-1-profile-eu868.yaml @@ -0,0 +1,47 @@ +# LoRaWAN MAC version: 1.0, 1.0.1, 1.0.2, 1.0.3, 1.0.4 or 1.1 +macVersion: 1.0.2 +# LoRaWAN Regional Parameters version. Values depend on the LoRaWAN version: +# 1.0: TS001-1.0 +# 1.0.1: TS001-1.0.1 +# 1.0.2: RP001-1.0.2 or RP001-1.0.2-RevB +# 1.0.3: RP001-1.0.3-RevA +# 1.0.4: RP002-1.0.0, RP002-1.0.1, RP002-1.0.2, RP002-1.0.3 or RP002-1.0.4 +# 1.1: RP001-1.1-RevA or RP001-1.1-RevB +regionalParametersVersion: RP001-1.0.2-RevB + +# Whether the end device supports join (OTAA) or not (ABP) +supportsJoin: true +# If your device is an ABP device (supportsJoin is false), uncomment the following fields: +# RX1 delay +#rx1Delay: 5 +# RX1 data rate offset +#rx1DataRateOffset: 0 +# RX2 data rate index +#rx2DataRateIndex: 0 +# RX2 frequency (MHz) +#rx2Frequency: 869.525 +# Factory preset frequencies (MHz) +#factoryPresetFrequencies: [868.1, 868.3, 868.5, 867.1, 867.3, 867.5, 867.7, 867.9] + +# Maximum EIRP +maxEIRP: 16 +# Whether the end device supports 32-bit frame counters +supports32bitFCnt: true + +# Whether the end device supports class B +supportsClassB: false +# If your device supports class B, uncomment the following fields: +# Maximum delay for the end device to answer a MAC request or confirmed downlink frame (seconds) +#classBTimeout: 60 +# Ping slot period (seconds) +#pingSlotPeriod: 128 +# Ping slot data rate index +#pingSlotDataRateIndex: 0 +# Ping slot frequency (MHz). Set to 0 if the band supports ping slot frequency hopping. +#pingSlotFrequency: 869.525 + +# Whether the end device supports class C +supportsClassC: false +# If your device supports class C, uncomment the following fields: +# Maximum delay for the end device to answer a MAC request or confirmed downlink frame (seconds) +#classCTimeout: 60 diff --git a/vendor/quandify/cubicmeter-1-1-uplink.js b/vendor/quandify/cubicmeter-1-1-uplink.js new file mode 100644 index 0000000000..cf32182440 --- /dev/null +++ b/vendor/quandify/cubicmeter-1-1-uplink.js @@ -0,0 +1,311 @@ +// Cubicmeter 1.1 uplink decoder + +var appStates = { + 3: 'ready', + 4: 'pipeSelection', + 5: 'metering', +}; + +var uplinkTypes = { + 0: 'ping', + 1: 'statusReport', + 6: 'response', +}; + +var responseStatuses = { + 0: 'ok', + 1: 'commandError', + 2: 'payloadError', + 3: 'valueError', +}; + +// More uplink types only available when using Quandify platform API +var responseTypes = { + 0: 'none', + 1: 'statusReport', + 2: 'hardwareReport', + 4: 'settingsReport', +}; + +/* Smaller water leakages only availble when using Quandify platform API +as it requires cloud analytics */ +var leakStates = { + 3: 'medium', + 4: 'large', +}; + +var pipeTypes = { + 0: 'Custom', + 1: 'Copper 15 mm', + 2: 'Copper 18 mm', + 3: 'Copper 22 mm', + 4: 'Chrome 15 mm', + 5: 'Chrome 18 mm', + 6: 'Chrome 22 mm', + 7: 'Pal 16 mm', + 8: 'Pal 20 mm', + 9: 'Pal 25 mm', + 14: 'Pex 16 mm', + 15: 'Pex 20 mm', + 16: 'Pex 25 mm', + 17: 'Distpipe', +}; + +/** + * 4.1 Uplink Decode + * The 'decodeUplink' function takes a message object and returns a parsed data object. + * @param input Message object + * @param input.fPort int, The uplink message LoRaWAN fPort. + * @param input.bytes int[], The uplink payload byte array, where each byte is represented by an integer between 0 and 255. + * @param input.recvTime Date, The uplink message timestamp recorded by the LoRaWAN network server as a JavaScript Date object. + */ +function decodeUplink(input) { + const buffer = new ArrayBuffer(input.bytes.length); + const data = new DataView(buffer); + for (const index in input.bytes) { + data.setUint8(index, input.bytes[index]); + } + + var decoded = {}; + var errors = []; + var warnings = []; + + try { + switch (input.fPort) { + case 1: // Status report + ({ decoded, warnings } = statusReportDecoder(data)); + break; + case 6: // Response + ({ decoded, warnings } = responseDecoder(data)); + break; + } + } catch (err) { + // Something went terribly wrong + errors.push(err.message); + } + + return { + data: { + fPort: input.fPort, + length: input.bytes.length, + hexBytes: decArrayToStr(input.bytes), + type: uplinkTypes[input.fPort], + decoded, + }, + errors, + warnings, + }; +} + +const LSB = true; + +var statusReportDecoder = function (data) { + if (data.byteLength != 28) { + throw new Error(`Wrong payload length (${data.byteLength}), should be 28 bytes`); + } + + let warnings = []; + const error = data.getUint16(4, LSB); + + // The is sensing value is a bit flag of the error field + const isSensing = !(error & 0x8000); + const errorCode = error & 0x7fff; + + const decoded = { + errorCode: errorCode, // current error code + isSensing: isSensing, // is the ultrasonic sensor sensing water + totalVolume: data.getUint32(6, LSB), // All-time aggregated water usage in litres + leakState: data.getUint8(22), // current water leakage state + batteryActive: decodeBatteryLevel(data.getUint8(23)), // battery mV active + batteryRecovered: decodeBatteryLevel(data.getUint8(24)), // battery mV recovered + waterTemperatureMin: decodeTemperature(data.getUint8(25)), // min water temperature since last statusReport + waterTemperatureMax: decodeTemperature(data.getUint8(26)), // max water temperature since last statusReport + ambientTemperature: decodeTemperature(data.getUint8(27)), // current ambient temperature + }; + + // Warnings + if (decoded.isSensing === false) { + warnings.push('Not sensing water'); + } + + if (decoded.errorCode) { + warnings.push(parseErrorCode(decoded.errorCode)); + } + + if (isLowBattery(decoded.batteryRecovered)) { + warnings.push('Low battery'); + } + + return { + decoded, + warnings, + }; +}; + +var responseDecoder = function (data) { + const status = responseStatuses[data.getUint8(1)]; + if (status === undefined) { + throw new Error(`Invalid response status: ${data.getUint8(1)}`); + } + + const type = responseTypes[data.getUint8(2)]; + if (type === undefined) { + throw new Error(`Invalid response type: ${data.getUint8(2)}`); + } + + const payload = new DataView(data.buffer, 3); + + var response = { + decoded: {}, + warnings: [], + }; + + switch (type) { + case 'statusReport': + response = statusReportDecoder(payload); + break; + case 'hardwareReport': + response = hardwareReportDecoder(payload); + break; + case 'settingsReport': + response = settingsReportDecoder(payload); + break; + } + + return { + decoded: { + fPort: data.getUint8(0), + status: status, + type: type, + data: response.decoded, + }, + warnings: response.warnings, + }; +}; + +var hardwareReportDecoder = function (data) { + if (data.byteLength != 35) { + throw new Error(`Wrong payload length (${data.byteLength}), should be 35 bytes`); + } + + const appState = appStates[data.getUint8(5)]; + if (appState === undefined) { + throw new Error(`Invalid app state (${data.getUint8(5)})`); + } + + const pipeType = pipeTypes[data.getUint8(28)]; + if (pipeType === undefined) { + throw new Error(`Invalid pipe index (${data.getUint8(28)})`); + } + + const firmwareVersion = intToSemver(data.getUint32(0, LSB)); + + return { + decoded: { + firmwareVersion, + hardwareVersion: data.getUint8(4), + appState: appState, + pipe: { + id: data.getUint8(28), + type: pipeType, + }, + }, + warnings: [], + }; +}; + +var settingsReportDecoder = function (data) { + if (data.byteLength != 38) { + throw new Error(`Wrong payload length (${data.byteLength}), should be 38 bytes`); + } + + return { + decoded: { + lorawanReportInterval: data.getUint32(5, LSB), + }, + warnings: [], + }; +}; + +var decodeBatteryLevel = function (input) { + return 1800 + (input << 3); // convert to milliVolt +}; + +var decodeTemperature = function (input) { + return parseFloat(input) * 0.5 - 20.0; // to °C +}; + +var isLowBattery = function (batteryRecovered) { + return batteryRecovered <= 3100; +}; + +var parseErrorCode = function (errorCode) { + switch (errorCode) { + case 384: + return 'Reverse flow'; + default: + return `Contact support, error ${errorCode}`; + } +}; + +var normalizeUplink = function (input) { + if (input.data.type != 'statusReport') { + return {}; + } + + return { + data: { + air: { + temperature: input.data.decoded.ambientTemperature, // °C + }, + water: { + temperature: { + min: input.data.decoded.waterTemperatureMin, // °C + max: input.data.decoded.waterTemperatureMax, // °C + }, + leak: leakStates[input.data.decoded.leak_state] ? leakStates[input.data.decoded.leak_state] : '', // String + }, + metering: { + water: { + total: input.data.decoded.totalVolume, // L + }, + }, + battery: input.data.decoded.batteryRecovered / 1000, // V + }, + warnings: input.warnings, + errors: input.errors, + }; +}; + +// Convert a hex string to decimal array +var hexToDecArray = function (hexString) { + const size = 2; + const length = Math.ceil(hexString.length / size); + const decimalList = new Array(length); + + for (let i = 0, o = 0; i < length; ++i, o += size) { + decimalList[i] = parseInt(hexString.substr(o, size), 16); + } + + return decimalList; +}; + +var base64ToDecArray = function (base64String) { + const buffer = Buffer.from(base64String, 'base64'); + const bufString = buffer.toString('hex'); + + return hexToDecArray(bufString); +}; + +var decArrayToStr = function (byteArray) { + return Array.from(byteArray, function (byte) { + return ('0' + (byte & 0xff).toString(16)).slice(-2).toUpperCase(); + }).join(''); +}; + +var intToSemver = function (version) { + const major = (version >> 24) & 0xff; + const minor = (version >> 16) & 0xff; + const patch = version & 0xffff; + return `${major}.${minor}.${patch}`; +}; diff --git a/vendor/quandify/index.yaml b/vendor/quandify/index.yaml index 41a13527c0..f6804b3052 100644 --- a/vendor/quandify/index.yaml +++ b/vendor/quandify/index.yaml @@ -1,4 +1,6 @@ endDevices: + - cubicmeter-1-1-copper + - cubicmeter-1-1-plastic - cubicmeter profileIDs: '1': diff --git a/vendor/sensative/strips-codec.yaml b/vendor/sensative/strips-codec.yaml index feb32e3c60..e795e99e10 100644 --- a/vendor/sensative/strips-codec.yaml +++ b/vendor/sensative/strips-codec.yaml @@ -1,2 +1,22 @@ uplinkDecoder: fileName: strips.js + examples: + - description: payload + input: + fPort: 1 + bytes: [0xFF, 0xFF, 0x01, 0x50, 0x02, 0x00, 0xEA, 0x03, 0x00, 0x0D, 0x00, 0x0E, 0x00, 0x10, 0x00] + output: + data: + battery: 80 + flood: { value: 0 } + floodAlarm: { value: false } + historySeqNr: 65535 + prevHistSeqNr: 65535 + tempAlarm: { highAlarm: false, lowAlarm: false } + temperature: { value: 23.4 } + userSwitch1Alarm: { value: false } + normalizedOutput: + data: + - air: + temperature: 23.4 + battery: 80 diff --git a/vendor/sensative/strips.js b/vendor/sensative/strips.js index e13d1d717c..5fe6191729 100644 --- a/vendor/sensative/strips.js +++ b/vendor/sensative/strips.js @@ -184,3 +184,20 @@ function Decoder(bytes, port) { } return decoded; } + +function decodeUplink(input) { + return { + data : Decoder(input.bytes, input.fPort), + }; +} + +function normalizeUplink(input) { + return { + data: { + air: { + temperature: input.data.temperature.value, + }, + battery: input.data.battery + } + }; + } diff --git a/vendor/tektelic/decoder_smart_room_sensor.js b/vendor/tektelic/decoder_smart_room_sensor.js index 41f4b79d84..e797f02bcf 100644 --- a/vendor/tektelic/decoder_smart_room_sensor.js +++ b/vendor/tektelic/decoder_smart_room_sensor.js @@ -2864,3 +2864,50 @@ function decodeUplink(input) { }; } + + function normalizeUplink(input) { + var data = {}; + var air = {}; + var action = {}; + var motion = {}; + + if (input.data.ambient_temperature) { + air.temperature = input.data.ambient_temperature; + } + + if (input.data.relative_humidity) { + air.relativeHumidity = input.data.relative_humidity; + } + + if (input.data.light_detected) { + air.lightIntensity = input.data.light_detected; + } + + if (input.data.motion_event_state) { + motion.detected = input.data.motion_event_state > 0; + action.motion = motion; + } + + if (input.data.motion_event_count) { + motion.count = input.data.motion_event_count; + action.motion = motion; + } + + if (input.data.reed_state) { + action.contactState = input.data.reed_state; + } + + if (Object.keys(air).length > 0) { + data.air = air; + } + + if (Object.keys(action).length > 0) { + data.action = action; + } + + if (input.data.battery_voltage) { + data.battery = input.data.battery_voltage; + } + + return { data: data }; + } diff --git a/vendor/tektelic/decoder_smart_room_sensor_pir_base.js b/vendor/tektelic/decoder_smart_room_sensor_pir_base.js index 48d6055a2e..729048a731 100644 --- a/vendor/tektelic/decoder_smart_room_sensor_pir_base.js +++ b/vendor/tektelic/decoder_smart_room_sensor_pir_base.js @@ -782,4 +782,51 @@ function decodeUplink(input) { return { data: decoded_data }; -} \ No newline at end of file +} + +function normalizeUplink(input) { + var data = {}; + var air = {}; + var action = {}; + var motion = {}; + + if (input.data.ambient_temperature) { + air.temperature = input.data.ambient_temperature; + } + + if (input.data.relative_humidity) { + air.relativeHumidity = input.data.relative_humidity; + } + + if (input.data.light_detected) { + air.lightIntensity = input.data.light_detected; + } + + if (input.data.motion_event_state) { + motion.detected = input.data.motion_event_state > 0; + action.motion = motion; + } + + if (input.data.motion_event_count) { + motion.count = input.data.motion_event_count; + action.motion = motion; + } + + if (input.data.reed_state) { + action.contactState = input.data.reed_state; + } + + if (Object.keys(air).length > 0) { + data.air = air; + } + + if (Object.keys(action).length > 0) { + data.action = action; + } + + if (input.data.battery_voltage) { + data.battery = input.data.battery_voltage; + } + + return { data: data }; + } diff --git a/vendor/tektelic/t00048xx-codec.yaml b/vendor/tektelic/t00048xx-codec.yaml index 2aa6d55a0b..73b10e2567 100644 --- a/vendor/tektelic/t00048xx-codec.yaml +++ b/vendor/tektelic/t00048xx-codec.yaml @@ -9,6 +9,11 @@ uplinkDecoder: data: ambient_temperature: 1 relative_humidity: 20 + normalizedOutput: + data: + - air: + temperature: 1 + relativeHumidity: 20 downlinkEncoder: fileName: encoder_smart_room_sensor_pir_base.js diff --git a/vendor/tektelic/t00061xx-codec.yaml b/vendor/tektelic/t00061xx-codec.yaml index ac49039067..755fcec8e6 100644 --- a/vendor/tektelic/t00061xx-codec.yaml +++ b/vendor/tektelic/t00061xx-codec.yaml @@ -12,6 +12,45 @@ uplinkDecoder: raw: '[02, 00, 01]' errors: [] warnings: [] + normalizedOutput: + data: + - air: + lightIntensity: 1 + - description: Temperature & Humidity Uplink + input: + fPort: 10 + bytes: [0x03, 0x67, 0x01, 0x26, 0x04, 0x68, 0x33, 0x00, 0xBA, 0x0B, 0xD1] + output: + data: + ambient_temperature: 29.4 + battery_voltage: 3.025 + port: '10' + raw: '[03, 67, 01, 26, 04, 68, 33, 00, BA, 0B, D1]' + relative_humidity: 25.5 + errors: [] + warnings: [] + normalizedOutput: + data: + - air: + temperature: 29.4 + relativeHumidity: 25.5 + battery: 3.025 + - description: Motion Event Uplink + input: + fPort: 10 + bytes: [0x0A, 0x00, 0xFF] + output: + data: + motion_event_state: 255 + port: '10' + raw: '[0A, 00, FF]' + errors: [] + warnings: [] + normalizedOutput: + data: + - action: + motion: + detected: true downlinkEncoder: fileName: encoder_smart_room_sensor.js diff --git a/vendor/the-things-industries/generic-node-sensor-edition-codec.js b/vendor/the-things-industries/generic-node-sensor-edition-codec.js index c5925fe3c6..155608e45b 100644 --- a/vendor/the-things-industries/generic-node-sensor-edition-codec.js +++ b/vendor/the-things-industries/generic-node-sensor-edition-codec.js @@ -9,3 +9,15 @@ function decodeUplink(input) { data: data, }; } + +function normalizeUplink(input) { + return { + data: { + air: { + temperature: input.data.temperature, + relativeHumidity: input.data.humidity, + }, + battery: input.data.batt_volt, + }, + }; +} diff --git a/vendor/the-things-industries/generic-node-sensor-edition-codec.yaml b/vendor/the-things-industries/generic-node-sensor-edition-codec.yaml index 9200cc19fc..5e44d53a4c 100644 --- a/vendor/the-things-industries/generic-node-sensor-edition-codec.yaml +++ b/vendor/the-things-industries/generic-node-sensor-edition-codec.yaml @@ -2,3 +2,21 @@ # For documentation on writing encoders and decoders, see: https://thethingsstack.io/integrations/payload-formatters/javascript/ uplinkDecoder: fileName: generic-node-sensor-edition-codec.js + # Examples (optional) + examples: + - description: Uplink example + input: + fPort: 1 + bytes: [35, 2, 216, 0, 250, 1] + output: + data: + batt_volt: 3.5 + temperature: 22.8 + humidity: 25.0 + button: 1 + normalizedOutput: + data: + - air: + temperature: 22.8 + relativeHumidity: 25.0 + battery: 3.5 diff --git a/vendor/thermokon/dpa-lrw.yaml b/vendor/thermokon/dpa-lrw.yaml index 5daa0c3589..ec876478c9 100644 --- a/vendor/thermokon/dpa-lrw.yaml +++ b/vendor/thermokon/dpa-lrw.yaml @@ -41,7 +41,7 @@ sensors: - pressure # Product and data sheet URLs (optional) -productURL: https://www.thermokon.de/direct/en-gb?path=%2F1%2F2%2F20&filter_show=1&q=&limit=18&filter_articles=&sbscf%3Asbscf1%2Fattr%2F8470[]=thermokon%3A%2F%2F{r%3D0%2Cb%3D0%2Cl%3Den_UK}%2F1%2F7%2F8%2F338%2F339%2F8470%2F10570 +productURL: https://www.thermokon.de/direct/en-gb/categories/g-dpaplus-lorawan # Photos photos: diff --git a/vendor/thermokon/ls02-lrw.yaml b/vendor/thermokon/ls02-lrw.yaml index 764e381d31..de4b8d0a01 100644 --- a/vendor/thermokon/ls02-lrw.yaml +++ b/vendor/thermokon/ls02-lrw.yaml @@ -33,7 +33,7 @@ sensors: # - lekage - humidity # Product and data sheet URLs (optional) -productURL: https://www.thermokon.de/direct/en-gb?path=%2F1%2F2%2F20&filter_show=1&q=&limit=18&filter_articles=&sbscf%3Asbscf1%2Fattr%2F8470[]=thermokon%3A%2F%2F{r%3D0%2Cb%3D0%2Cl%3Den_UK}%2F1%2F7%2F8%2F338%2F339%2F8470%2F10570 +productURL: https://www.thermokon.de/direct/en-gb/categories/g-ls02plus-flex-lorawan # Photos photos: diff --git a/vendor/thermokon/mcs-lrw.yaml b/vendor/thermokon/mcs-lrw.yaml index 75d07dfef1..16820b5eaf 100644 --- a/vendor/thermokon/mcs-lrw.yaml +++ b/vendor/thermokon/mcs-lrw.yaml @@ -47,7 +47,7 @@ sensors: - light # Product and data sheet URLs (optional) -productURL: https://www.thermokon.de/direct/en-gb?path=%2F1%2F2%2F20&filter_show=1&q=&limit=18&filter_articles=&sbscf%3Asbscf1%2Fattr%2F8470[]=thermokon%3A%2F%2F{r%3D0%2Cb%3D0%2Cl%3Den_UK}%2F1%2F7%2F8%2F338%2F339%2F8470%2F10570 +productURL: https://www.thermokon.de/direct/en-gb/categories/g-mcs-lorawan # Photos photos: diff --git a/vendor/thermokon/novos-3-lrw.yaml b/vendor/thermokon/novos-3-lrw.yaml index 7dc9306f7a..6d89711ce0 100644 --- a/vendor/thermokon/novos-3-lrw.yaml +++ b/vendor/thermokon/novos-3-lrw.yaml @@ -45,7 +45,7 @@ sensors: - potentiometer # Product and data sheet URLs (optional) -productURL: https://www.thermokon.de/direct/en-gb?path=%2F1%2F2%2F20&filter_show=1&q=&limit=18&filter_articles=&sbscf%3Asbscf1%2Fattr%2F8470[]=thermokon%3A%2F%2F{r%3D0%2Cb%3D0%2Cl%3Den_UK}%2F1%2F7%2F8%2F338%2F339%2F8470%2F10570 +productURL: https://www.thermokon.de/direct/en-gb/categories/g-novos-3-x-lorawan # Photos photos: diff --git a/vendor/thermokon/thanos-evo-lrw.yaml b/vendor/thermokon/thanos-evo-lrw.yaml index 03184f0fcc..2d207142b9 100644 --- a/vendor/thermokon/thanos-evo-lrw.yaml +++ b/vendor/thermokon/thanos-evo-lrw.yaml @@ -43,7 +43,7 @@ sensors: - co2 - tvoc # Product and data sheet URLs (optional) -productURL: https://www.thermokon.de/direct/en-gb?path=%2F1%2F2%2F20&filter_show=1&q=&limit=18&filter_articles=&sbscf%3Asbscf1%2Fattr%2F8470[]=thermokon%3A%2F%2F{r%3D0%2Cb%3D0%2Cl%3Den_UK}%2F1%2F7%2F8%2F338%2F339%2F8470%2F10570 +productURL: https://www.thermokon.de/en-gb/highlights/products/thanos-evo # Photos photos: