@hello.nrfcloud.com/proto-map 5.6.3 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -16
- package/dist/generator/generateModels.js +0 -85
- package/dist/generator/models.js +1 -83
- package/dist/markdown/getCodeBlock.js +0 -26
- package/dist/models/check-model-rules.js +1 -91
- package/dist/models/models.js +5 -114
- package/dist/models/types.js +0 -12
- package/dist/senml/lwm2mToSenML.js +52 -26
- package/dist/senml/lwm2mToSenML.spec.js +3 -1
- package/dist/senml/senMLtoLwM2M.js +24 -12
- package/dist/senml/senMLtoLwM2M.spec.js +8 -4
- package/models/README.md +0 -4
- package/models/check-model-rules.ts +1 -90
- package/models/models.ts +1 -8
- package/models/types.ts +0 -16
- package/package.json +2 -3
- package/senml/lwm2mToSenML.spec.ts +7 -1
- package/senml/lwm2mToSenML.ts +42 -22
- package/senml/senMLtoLwM2M.spec.ts +8 -4
- package/senml/senMLtoLwM2M.ts +33 -16
- package/dist/generator/isDir.js +0 -163
- package/dist/generator/isDir.spec.js +0 -212
- package/dist/markdown/getFrontMatter.js +0 -15
- package/dist/models/asset_tracker_v2+AWS/examples/examples.spec.js +0 -489
- package/models/PCA20035+solar/README.md +0 -10
- package/models/PCA20035+solar/transforms/airQuality.md +0 -48
- package/models/PCA20035+solar/transforms/battery.md +0 -46
- package/models/PCA20035+solar/transforms/button.md +0 -45
- package/models/PCA20035+solar/transforms/deviceInfo.md +0 -72
- package/models/PCA20035+solar/transforms/gain.md +0 -45
- package/models/PCA20035+solar/transforms/geolocationFromGroundfix.md +0 -67
- package/models/PCA20035+solar/transforms/geolocationFromMessage.md +0 -80
- package/models/PCA20035+solar/transforms/humidity.md +0 -43
- package/models/PCA20035+solar/transforms/networkInfo.md +0 -84
- package/models/PCA20035+solar/transforms/pressure.md +0 -43
- package/models/PCA20035+solar/transforms/temperature.md +0 -43
- package/models/asset_tracker_v2+AWS/README.md +0 -6
- package/models/asset_tracker_v2+AWS/examples/examples.spec.ts +0 -229
- package/models/asset_tracker_v2+AWS/examples/shadow/example-1.json +0 -24
- package/models/asset_tracker_v2+AWS/examples/shadow/example-2.json +0 -30
- package/models/asset_tracker_v2+AWS/examples/shadow/example-3.json +0 -37
- package/models/asset_tracker_v2+AWS/examples/shadow/example-4.json +0 -48
- package/models/asset_tracker_v2+AWS/examples/shadow/example-5.json +0 -43
- package/models/asset_tracker_v2+AWS/transforms/GNSS.md +0 -66
- package/models/asset_tracker_v2+AWS/transforms/battery-voltage.md +0 -50
- package/models/asset_tracker_v2+AWS/transforms/device-info.md +0 -61
- package/models/asset_tracker_v2+AWS/transforms/env.md +0 -69
- package/models/asset_tracker_v2+AWS/transforms/fuel-gauge.md +0 -62
- package/models/asset_tracker_v2+AWS/transforms/roam.md +0 -100
- package/models/asset_tracker_v2+AWS/transforms/solar.md +0 -58
|
@@ -6,9 +6,6 @@ function _array_like_to_array(arr, len) {
|
|
|
6
6
|
function _array_with_holes(arr) {
|
|
7
7
|
if (Array.isArray(arr)) return arr;
|
|
8
8
|
}
|
|
9
|
-
function _array_without_holes(arr) {
|
|
10
|
-
if (Array.isArray(arr)) return _array_like_to_array(arr);
|
|
11
|
-
}
|
|
12
9
|
function _define_property(obj, key, value) {
|
|
13
10
|
if (key in obj) {
|
|
14
11
|
Object.defineProperty(obj, key, {
|
|
@@ -28,9 +25,6 @@ function _iterable_to_array(iter) {
|
|
|
28
25
|
function _non_iterable_rest() {
|
|
29
26
|
throw new TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
|
|
30
27
|
}
|
|
31
|
-
function _non_iterable_spread() {
|
|
32
|
-
throw new TypeError("Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
|
|
33
|
-
}
|
|
34
28
|
function _object_spread(target) {
|
|
35
29
|
for(var i = 1; i < arguments.length; i++){
|
|
36
30
|
var source = arguments[i] != null ? arguments[i] : {};
|
|
@@ -73,9 +67,6 @@ function _object_spread_props(target, source) {
|
|
|
73
67
|
function _to_array(arr) {
|
|
74
68
|
return _array_with_holes(arr) || _iterable_to_array(arr) || _unsupported_iterable_to_array(arr) || _non_iterable_rest();
|
|
75
69
|
}
|
|
76
|
-
function _to_consumable_array(arr) {
|
|
77
|
-
return _array_without_holes(arr) || _iterable_to_array(arr) || _unsupported_iterable_to_array(arr) || _non_iterable_spread();
|
|
78
|
-
}
|
|
79
70
|
function _unsupported_iterable_to_array(o, minLen) {
|
|
80
71
|
if (!o) return;
|
|
81
72
|
if (typeof o === "string") return _array_like_to_array(o, minLen);
|
|
@@ -91,11 +82,6 @@ import { ResourceType } from '../lwm2m/LWM2MObjectInfo.js';
|
|
|
91
82
|
/**
|
|
92
83
|
* Convert LwM2M Object Instances to senML
|
|
93
84
|
*/ export var lwm2mToSenML = function(lwm2m) {
|
|
94
|
-
return lwm2m.map(asSenML).flat().filter(function(v) {
|
|
95
|
-
return v !== null;
|
|
96
|
-
});
|
|
97
|
-
};
|
|
98
|
-
var asSenML = function(lwm2m) {
|
|
99
85
|
var def = definitions[lwm2m.ObjectID];
|
|
100
86
|
var i = instanceTs(lwm2m);
|
|
101
87
|
var tsResourceId = timestampResources[lwm2m.ObjectID]// All registered objects must have a timestamp resource
|
|
@@ -104,19 +90,59 @@ var asSenML = function(lwm2m) {
|
|
|
104
90
|
.filter(function(r) {
|
|
105
91
|
return r[1] !== undefined;
|
|
106
92
|
})), first = _Object_entries_filter[0], rest = _Object_entries_filter.slice(1);
|
|
107
|
-
if (first === undefined) return
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
93
|
+
if (first === undefined) return {
|
|
94
|
+
errors: [
|
|
95
|
+
new Error("No valid LwM2M object found")
|
|
96
|
+
]
|
|
97
|
+
};
|
|
98
|
+
var senML = [];
|
|
99
|
+
var errors = [];
|
|
100
|
+
var resourceId = parseInt(first[0], 10);
|
|
101
|
+
var firstKey = toKey(def, resourceId);
|
|
102
|
+
if (firstKey === null) {
|
|
103
|
+
errors.push(new Error("Unknown ResourceID ".concat(resourceId, " for LwM2M Object ").concat(def.ObjectID, "!")));
|
|
104
|
+
} else {
|
|
105
|
+
var _lwm2m_ObjectInstanceID;
|
|
106
|
+
var _obj;
|
|
107
|
+
senML.push((_obj = {
|
|
112
108
|
bn: "".concat(lwm2m.ObjectID, "/").concat((_lwm2m_ObjectInstanceID = lwm2m.ObjectInstanceID) !== null && _lwm2m_ObjectInstanceID !== void 0 ? _lwm2m_ObjectInstanceID : 0, "/"),
|
|
113
109
|
n: first[0]
|
|
114
|
-
}, _define_property(_obj,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
110
|
+
}, _define_property(_obj, firstKey, first[1]), _define_property(_obj, "bt", i.getTime()), _obj));
|
|
111
|
+
}
|
|
112
|
+
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
113
|
+
try {
|
|
114
|
+
for(var _iterator = rest[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
|
|
115
|
+
var r = _step.value;
|
|
116
|
+
var resourceId1 = parseInt(r[0], 10);
|
|
117
|
+
var key = toKey(def, resourceId1);
|
|
118
|
+
if (key === null) {
|
|
119
|
+
errors.push(new Error("Unknown ResourceID ".concat(resourceId1, " for LwM2M Object ").concat(def.ObjectID, "!")));
|
|
120
|
+
} else {
|
|
121
|
+
senML.push(_define_property({
|
|
122
|
+
n: r[0]
|
|
123
|
+
}, key, r[1]));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
_didIteratorError = true;
|
|
128
|
+
_iteratorError = err;
|
|
129
|
+
} finally{
|
|
130
|
+
try {
|
|
131
|
+
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
132
|
+
_iterator.return();
|
|
133
|
+
}
|
|
134
|
+
} finally{
|
|
135
|
+
if (_didIteratorError) {
|
|
136
|
+
throw _iteratorError;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (errors.length > 0) return {
|
|
141
|
+
errors: errors
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
senML: senML
|
|
145
|
+
};
|
|
120
146
|
};
|
|
121
147
|
var toKey = function(def, resourceId) {
|
|
122
148
|
var _def_Resources_resourceId;
|
|
@@ -132,6 +158,6 @@ var toKey = function(def, resourceId) {
|
|
|
132
158
|
case ResourceType.Opaque:
|
|
133
159
|
return 'vd';
|
|
134
160
|
default:
|
|
135
|
-
|
|
161
|
+
return null;
|
|
136
162
|
}
|
|
137
163
|
};
|
|
@@ -99,6 +99,8 @@ void describe('lwm2mToSenML()', function() {
|
|
|
99
99
|
vs: 'AES'
|
|
100
100
|
}
|
|
101
101
|
];
|
|
102
|
-
assert.deepEqual(lwm2mToSenML(
|
|
102
|
+
assert.deepEqual(lwm2m.map(lwm2mToSenML).map(function(res) {
|
|
103
|
+
return 'senML' in res && res.senML;
|
|
104
|
+
}).flat(), expected);
|
|
103
105
|
});
|
|
104
106
|
});
|
|
@@ -53,14 +53,12 @@ function _object_spread_props(target, source) {
|
|
|
53
53
|
import { timestampResources } from '../lwm2m/timestampResources.js';
|
|
54
54
|
import { parseResourceId } from './parseResourceId.js';
|
|
55
55
|
import { hasValue } from './hasValue.js';
|
|
56
|
-
var isInfoForDifferentInstance = function(currentObject, resourceID, currentBaseTime) {
|
|
56
|
+
var isInfoForDifferentInstance = function(currentObject, resourceID, currentBaseTime, tsRes) {
|
|
57
57
|
var _currentObject_Resources_tsRes, _currentObject_Resources;
|
|
58
58
|
if (currentObject === undefined) return true;
|
|
59
59
|
if (currentObject.ObjectID !== resourceID.ObjectID) return true;
|
|
60
60
|
var _currentObject_ObjectInstanceID;
|
|
61
61
|
if (((_currentObject_ObjectInstanceID = currentObject.ObjectInstanceID) !== null && _currentObject_ObjectInstanceID !== void 0 ? _currentObject_ObjectInstanceID : 0) !== resourceID.ObjectInstanceID) return true;
|
|
62
|
-
var tsRes = timestampResources[resourceID.ObjectID];
|
|
63
|
-
if (tsRes === undefined) throw new Error("Unknown LwM2M Object ID: ".concat(resourceID.ObjectID, "!"));
|
|
64
62
|
if (currentBaseTime !== ((_currentObject_Resources = currentObject.Resources) === null || _currentObject_Resources === void 0 ? void 0 : (_currentObject_Resources_tsRes = _currentObject_Resources[tsRes]) === null || _currentObject_Resources_tsRes === void 0 ? void 0 : _currentObject_Resources_tsRes.getTime())) return true;
|
|
65
63
|
return false;
|
|
66
64
|
};
|
|
@@ -77,7 +75,7 @@ export var senMLtoLwM2M = function(senML) {
|
|
|
77
75
|
var currentBaseName = '';
|
|
78
76
|
var currentBaseTime = undefined;
|
|
79
77
|
var currentObject = undefined;
|
|
80
|
-
var
|
|
78
|
+
var lwm2m = [];
|
|
81
79
|
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
82
80
|
try {
|
|
83
81
|
for(var _iterator = senML[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
|
|
@@ -88,12 +86,24 @@ export var senMLtoLwM2M = function(senML) {
|
|
|
88
86
|
var _item_n;
|
|
89
87
|
var itemResourceId = "".concat(currentBaseName).concat((_item_n = item.n) !== null && _item_n !== void 0 ? _item_n : '', "/0");
|
|
90
88
|
var resourceId = parseResourceId(itemResourceId);
|
|
91
|
-
if (resourceId === null)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
89
|
+
if (resourceId === null) {
|
|
90
|
+
return {
|
|
91
|
+
error: new Error("Invalid resource ID: ".concat(itemResourceId))
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
var tsRes = timestampResources[resourceId.ObjectID];
|
|
95
|
+
if (tsRes === undefined) {
|
|
96
|
+
return {
|
|
97
|
+
error: new Error("No timestamp resource defined for object: ".concat(resourceId.ObjectID))
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (currentObject === undefined || currentBaseTime !== undefined && isInfoForDifferentInstance(currentObject, resourceId, currentBaseTime, tsRes)) {
|
|
101
|
+
if (currentObject !== undefined) lwm2m.push(currentObject);
|
|
102
|
+
if (currentBaseTime === undefined) {
|
|
103
|
+
return {
|
|
104
|
+
error: new Error("No base time defined for object: ".concat(resourceId.ObjectID, "!"))
|
|
105
|
+
};
|
|
106
|
+
}
|
|
97
107
|
currentObject = {
|
|
98
108
|
ObjectID: resourceId.ObjectID,
|
|
99
109
|
Resources: _define_property({}, tsRes, new Date(currentBaseTime))
|
|
@@ -121,6 +131,8 @@ export var senMLtoLwM2M = function(senML) {
|
|
|
121
131
|
}
|
|
122
132
|
}
|
|
123
133
|
}
|
|
124
|
-
if (currentObject !== undefined)
|
|
125
|
-
return
|
|
134
|
+
if (currentObject !== undefined) lwm2m.push(currentObject);
|
|
135
|
+
return {
|
|
136
|
+
lwm2m: lwm2m
|
|
137
|
+
};
|
|
126
138
|
};
|
|
@@ -48,7 +48,8 @@ void describe('senMLtoLwM2M()', function() {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
];
|
|
51
|
-
|
|
51
|
+
var res = senMLtoLwM2M(input);
|
|
52
|
+
assert.deepEqual('lwm2m' in res && res.lwm2m, expected);
|
|
52
53
|
});
|
|
53
54
|
void it('should drop empty resources', function() {
|
|
54
55
|
var input = [
|
|
@@ -101,7 +102,8 @@ void describe('senMLtoLwM2M()', function() {
|
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
];
|
|
104
|
-
|
|
105
|
+
var res = senMLtoLwM2M(input);
|
|
106
|
+
assert.deepEqual('lwm2m' in res && res.lwm2m, expected);
|
|
105
107
|
});
|
|
106
108
|
void it('should ignore repeated base properties', function() {
|
|
107
109
|
var input = [
|
|
@@ -160,7 +162,8 @@ void describe('senMLtoLwM2M()', function() {
|
|
|
160
162
|
}
|
|
161
163
|
}
|
|
162
164
|
];
|
|
163
|
-
|
|
165
|
+
var res = senMLtoLwM2M(input);
|
|
166
|
+
assert.deepEqual('lwm2m' in res && res.lwm2m, expected);
|
|
164
167
|
});
|
|
165
168
|
void it('should handle multiple measurements for the same resource', function() {
|
|
166
169
|
var input = [
|
|
@@ -221,6 +224,7 @@ void describe('senMLtoLwM2M()', function() {
|
|
|
221
224
|
}
|
|
222
225
|
}
|
|
223
226
|
];
|
|
224
|
-
|
|
227
|
+
var res = senMLtoLwM2M(input);
|
|
228
|
+
assert.deepEqual('lwm2m' in res && res.lwm2m, expected);
|
|
225
229
|
});
|
|
226
230
|
});
|
package/models/README.md
CHANGED
|
@@ -4,7 +4,3 @@ Models are defined in the subdirectories of this folder.
|
|
|
4
4
|
|
|
5
5
|
A model definition consists of a `README.md` that provides a human-friendly
|
|
6
6
|
description of the model.
|
|
7
|
-
|
|
8
|
-
Optionally, if the device does not send senML directly,
|
|
9
|
-
[transforms can be defined](../README.md#model-transform-definitions) that
|
|
10
|
-
convert the payload sent by the model to senML.
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk'
|
|
2
|
-
import jsonata from 'jsonata'
|
|
3
2
|
import assert from 'node:assert/strict'
|
|
4
3
|
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
5
4
|
import path from 'node:path'
|
|
6
|
-
import {
|
|
7
|
-
import { senMLtoLwM2M } from '../senml/senMLtoLwM2M.js'
|
|
8
|
-
import { getCodeBlock } from '../markdown/getCodeBlock.js'
|
|
9
|
-
import { getFrontMatter } from '../markdown/getFrontMatter.js'
|
|
10
|
-
import { validateSenML } from '../senml/validateSenML.js'
|
|
11
|
-
import { isRegisteredLwM2MObject } from '../lwm2m/isRegisteredLwM2MObject.js'
|
|
12
|
-
import { hasValue } from '../senml/hasValue.js'
|
|
5
|
+
import { ModelIDRegExp } from './types.js'
|
|
13
6
|
import { parseREADME } from 'markdown/parseREADME.js'
|
|
14
7
|
|
|
15
8
|
console.log(chalk.gray('Models rules check'))
|
|
@@ -40,86 +33,4 @@ for (const model of await readdir(modelsDir)) {
|
|
|
40
33
|
throw new Error(`README is not valid for ${model}!`)
|
|
41
34
|
}
|
|
42
35
|
console.log(chalk.green('✔'), chalk.gray(`README.md is valid`))
|
|
43
|
-
|
|
44
|
-
// Validate jsonata expressions
|
|
45
|
-
let hasTransforms = false
|
|
46
|
-
const transformsFolder = path.join(modelDir, 'transforms')
|
|
47
|
-
try {
|
|
48
|
-
await stat(transformsFolder)
|
|
49
|
-
hasTransforms = true
|
|
50
|
-
console.log(' ', chalk.gray('Transforms:'))
|
|
51
|
-
} catch {
|
|
52
|
-
console.log(' ', chalk.gray('No transforms found.'))
|
|
53
|
-
}
|
|
54
|
-
if (hasTransforms) {
|
|
55
|
-
for (const transform of (await readdir(transformsFolder)).filter((f) =>
|
|
56
|
-
f.endsWith('.md'),
|
|
57
|
-
)) {
|
|
58
|
-
console.log(' ', chalk.white('·'), chalk.white.bold(transform))
|
|
59
|
-
const markdown = await readFile(
|
|
60
|
-
path.join(modelDir, 'transforms', transform),
|
|
61
|
-
'utf-8',
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
// Validate front-matter
|
|
65
|
-
const type = getFrontMatter(markdown, FrontMatter).type
|
|
66
|
-
console.log(' ', chalk.green('✔'), chalk.gray(`Type ${type} is valid`))
|
|
67
|
-
const findBlock = getCodeBlock(markdown)
|
|
68
|
-
const matchExpression = findBlock('jsonata', 'Match Expression')
|
|
69
|
-
const transformExpression = findBlock('jsonata', 'Transform Expression')
|
|
70
|
-
const inputExample = JSON.parse(findBlock('json', 'Input Example'))
|
|
71
|
-
const resultExample = JSON.parse(findBlock('json', 'Result Example'))
|
|
72
|
-
|
|
73
|
-
const selectResult = await jsonata(matchExpression).evaluate(inputExample)
|
|
74
|
-
if (selectResult !== true) {
|
|
75
|
-
throw new Error(
|
|
76
|
-
`The select expression did not evaluate to true with the given example.`,
|
|
77
|
-
)
|
|
78
|
-
}
|
|
79
|
-
console.log(
|
|
80
|
-
' ',
|
|
81
|
-
chalk.green('✔'),
|
|
82
|
-
chalk.gray('Select expression evaluated to true for the example input'),
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
const transformResult = await jsonata(
|
|
86
|
-
// For testing purposes this function call result is hardcoded
|
|
87
|
-
transformExpression.replace('$millis()', '1699999999999'),
|
|
88
|
-
).evaluate(inputExample)
|
|
89
|
-
|
|
90
|
-
const maybeValidSenML = validateSenML(transformResult.filter(hasValue))
|
|
91
|
-
if ('errors' in maybeValidSenML) {
|
|
92
|
-
console.error(maybeValidSenML.errors)
|
|
93
|
-
throw new Error('The JSONata expression must produce valid SenML')
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
assert.deepEqual(maybeValidSenML.value, resultExample)
|
|
97
|
-
console.log(
|
|
98
|
-
' ',
|
|
99
|
-
chalk.green('✔'),
|
|
100
|
-
chalk.gray('Transformation result is valid SenML'),
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
assert.deepEqual(maybeValidSenML.value, resultExample)
|
|
104
|
-
console.log(
|
|
105
|
-
' ',
|
|
106
|
-
chalk.green('✔'),
|
|
107
|
-
chalk.gray('The transformation result matches the example'),
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
// Validate
|
|
111
|
-
for (const object of senMLtoLwM2M(maybeValidSenML.value)) {
|
|
112
|
-
if (!isRegisteredLwM2MObject(object, console.error)) {
|
|
113
|
-
throw new Error(
|
|
114
|
-
'The LwM2M object must follow LwM2M schema definition',
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
console.log(
|
|
118
|
-
' ',
|
|
119
|
-
chalk.green('✔'),
|
|
120
|
-
chalk.gray('SenML object is valid LwM2M'),
|
|
121
|
-
)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
36
|
}
|
package/models/models.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import { type Transform, TransformType } from "./types.js";
|
|
2
1
|
/**
|
|
3
2
|
* The Model IDs defined in this repo.
|
|
4
3
|
*/
|
|
5
4
|
export enum ModelID {
|
|
6
|
-
PCA20035_solar = "PCA20035+solar",
|
|
7
|
-
Asset_tracker_v2_AWS = "asset_tracker_v2+AWS",
|
|
8
5
|
Kartverket_vasstandsdata = "kartverket-vasstandsdata"
|
|
9
6
|
}
|
|
10
7
|
export type Model = {
|
|
@@ -12,10 +9,6 @@ export type Model = {
|
|
|
12
9
|
* The Model ID
|
|
13
10
|
*/
|
|
14
11
|
"id": ModelID;
|
|
15
|
-
/**
|
|
16
|
-
* The transforms defined for this model.
|
|
17
|
-
*/
|
|
18
|
-
"transforms": Array<Transform>;
|
|
19
12
|
/**
|
|
20
13
|
* Description of the Model from the README.md
|
|
21
14
|
*/
|
|
@@ -33,4 +26,4 @@ export type Model = {
|
|
|
33
26
|
/**
|
|
34
27
|
* The models defined for hello.nrfcloud.com
|
|
35
28
|
*/
|
|
36
|
-
export const models: Readonly<Record<ModelID, Model>> = { [ModelID.
|
|
29
|
+
export const models: Readonly<Record<ModelID, Model>> = { [ModelID.Kartverket_vasstandsdata]: { "id": ModelID.Kartverket_vasstandsdata, "about": { "title": "Kartverket Vasstandsdata", "description": "A simulated device reporting the current sea level as provided by the Kartverket's (Norwegian Mapping Authority) API for vasstandsdata (API for water level data).\nReports sea water level using the Object 14230.\nThe data is licensed by the Norwegian Mapping Authority\u2019s under the Creative Commons Attribution 4.0 International (CC BY 4.0) license." } } } as const;
|
package/models/types.ts
CHANGED
|
@@ -1,17 +1 @@
|
|
|
1
|
-
import { Type } from '@sinclair/typebox'
|
|
2
|
-
|
|
3
1
|
export const ModelIDRegExp = /^[A-Za-z0-9+_-]+$/
|
|
4
|
-
|
|
5
|
-
export enum TransformType {
|
|
6
|
-
Shadow = 'shadow',
|
|
7
|
-
Messages = 'messages',
|
|
8
|
-
}
|
|
9
|
-
export type Transform = {
|
|
10
|
-
type: TransformType
|
|
11
|
-
match: string
|
|
12
|
-
transform: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const FrontMatter = Type.Object({
|
|
16
|
-
type: Type.Union([Type.Literal('shadow'), Type.Literal('messages')]),
|
|
17
|
-
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hello.nrfcloud.com/proto-map",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "Documents the communication protocol between devices, the hello.nrfcloud.com/map backend and web application",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -105,7 +105,6 @@
|
|
|
105
105
|
],
|
|
106
106
|
"peerDependencies": {
|
|
107
107
|
"@sinclair/typebox": "^0.32.22",
|
|
108
|
-
"ajv": "^8.12.0"
|
|
109
|
-
"jsonata": "^2.0.4"
|
|
108
|
+
"ajv": "^8.12.0"
|
|
110
109
|
}
|
|
111
110
|
}
|
|
@@ -69,6 +69,12 @@ void describe('lwm2mToSenML()', () => {
|
|
|
69
69
|
{ n: '1', vs: 'AES' },
|
|
70
70
|
]
|
|
71
71
|
|
|
72
|
-
assert.deepEqual(
|
|
72
|
+
assert.deepEqual(
|
|
73
|
+
lwm2m
|
|
74
|
+
.map(lwm2mToSenML)
|
|
75
|
+
.map((res) => 'senML' in res && res.senML)
|
|
76
|
+
.flat(),
|
|
77
|
+
expected,
|
|
78
|
+
)
|
|
73
79
|
})
|
|
74
80
|
})
|
package/senml/lwm2mToSenML.ts
CHANGED
|
@@ -9,14 +9,8 @@ import { ResourceType, type LWM2MObjectInfo } from '../lwm2m/LWM2MObjectInfo.js'
|
|
|
9
9
|
* Convert LwM2M Object Instances to senML
|
|
10
10
|
*/
|
|
11
11
|
export const lwm2mToSenML = (
|
|
12
|
-
lwm2m:
|
|
13
|
-
): SenMLType =>
|
|
14
|
-
lwm2m
|
|
15
|
-
.map(asSenML)
|
|
16
|
-
.flat()
|
|
17
|
-
.filter((v) => v !== null) as SenMLType
|
|
18
|
-
|
|
19
|
-
const asSenML = (lwm2m: LwM2MObjectInstance<any>): SenMLType | null => {
|
|
12
|
+
lwm2m: LwM2MObjectInstance<any>,
|
|
13
|
+
): { senML: SenMLType } | { errors: Array<Error> } => {
|
|
20
14
|
const def = definitions[lwm2m.ObjectID]
|
|
21
15
|
const i = instanceTs(lwm2m)
|
|
22
16
|
const tsResourceId = timestampResources[lwm2m.ObjectID] as number // All registered objects must have a timestamp resource
|
|
@@ -27,22 +21,50 @@ const asSenML = (lwm2m: LwM2MObjectInstance<any>): SenMLType | null => {
|
|
|
27
21
|
// Filter out undefined values (and timestamp resource)
|
|
28
22
|
.filter((r): r is [string, LwM2MResourceValue] => r[1] !== undefined)
|
|
29
23
|
|
|
30
|
-
if (first === undefined)
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
if (first === undefined)
|
|
25
|
+
return { errors: [new Error(`No valid LwM2M object found`)] }
|
|
26
|
+
|
|
27
|
+
const senML: SenMLType = []
|
|
28
|
+
const errors: Array<Error> = []
|
|
29
|
+
const resourceId = parseInt(first[0], 10)
|
|
30
|
+
const firstKey = toKey(def, resourceId)
|
|
31
|
+
if (firstKey === null) {
|
|
32
|
+
errors.push(
|
|
33
|
+
new Error(
|
|
34
|
+
`Unknown ResourceID ${resourceId} for LwM2M Object ${def.ObjectID}!`,
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
} else {
|
|
38
|
+
senML.push({
|
|
33
39
|
bn: `${lwm2m.ObjectID}/${lwm2m.ObjectInstanceID ?? 0}/`,
|
|
34
40
|
n: first[0],
|
|
35
|
-
[
|
|
41
|
+
[firstKey]: first[1],
|
|
36
42
|
bt: i.getTime(),
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const r of rest) {
|
|
47
|
+
const resourceId = parseInt(r[0], 10)
|
|
48
|
+
const key = toKey(def, resourceId)
|
|
49
|
+
if (key === null) {
|
|
50
|
+
errors.push(
|
|
51
|
+
new Error(
|
|
52
|
+
`Unknown ResourceID ${resourceId} for LwM2M Object ${def.ObjectID}!`,
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
} else {
|
|
56
|
+
senML.push({
|
|
57
|
+
n: r[0],
|
|
58
|
+
[key]: r[1],
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (errors.length > 0) return { errors }
|
|
64
|
+
return { senML }
|
|
43
65
|
}
|
|
44
66
|
|
|
45
|
-
const toKey = (def: LWM2MObjectInfo, resourceId: number) => {
|
|
67
|
+
const toKey = (def: LWM2MObjectInfo, resourceId: number): string | null => {
|
|
46
68
|
switch (def.Resources[resourceId]?.Type) {
|
|
47
69
|
case ResourceType.String:
|
|
48
70
|
return 'vs'
|
|
@@ -55,8 +77,6 @@ const toKey = (def: LWM2MObjectInfo, resourceId: number) => {
|
|
|
55
77
|
case ResourceType.Opaque:
|
|
56
78
|
return 'vd'
|
|
57
79
|
default:
|
|
58
|
-
|
|
59
|
-
`Unknown ResourceID ${resourceId} for LwM2M Object ${def.ObjectID}!`,
|
|
60
|
-
)
|
|
80
|
+
return null
|
|
61
81
|
}
|
|
62
82
|
}
|
|
@@ -50,7 +50,8 @@ void describe('senMLtoLwM2M()', () => {
|
|
|
50
50
|
},
|
|
51
51
|
},
|
|
52
52
|
]
|
|
53
|
-
|
|
53
|
+
const res = senMLtoLwM2M(input)
|
|
54
|
+
assert.deepEqual('lwm2m' in res && res.lwm2m, expected)
|
|
54
55
|
})
|
|
55
56
|
|
|
56
57
|
void it('should drop empty resources', () => {
|
|
@@ -104,7 +105,8 @@ void describe('senMLtoLwM2M()', () => {
|
|
|
104
105
|
},
|
|
105
106
|
},
|
|
106
107
|
]
|
|
107
|
-
|
|
108
|
+
const res = senMLtoLwM2M(input)
|
|
109
|
+
assert.deepEqual('lwm2m' in res && res.lwm2m, expected)
|
|
108
110
|
})
|
|
109
111
|
|
|
110
112
|
void it('should ignore repeated base properties', () => {
|
|
@@ -134,7 +136,8 @@ void describe('senMLtoLwM2M()', () => {
|
|
|
134
136
|
},
|
|
135
137
|
},
|
|
136
138
|
]
|
|
137
|
-
|
|
139
|
+
const res = senMLtoLwM2M(input)
|
|
140
|
+
assert.deepEqual('lwm2m' in res && res.lwm2m, expected)
|
|
138
141
|
})
|
|
139
142
|
|
|
140
143
|
void it('should handle multiple measurements for the same resource', () => {
|
|
@@ -176,6 +179,7 @@ void describe('senMLtoLwM2M()', () => {
|
|
|
176
179
|
},
|
|
177
180
|
},
|
|
178
181
|
]
|
|
179
|
-
|
|
182
|
+
const res = senMLtoLwM2M(input)
|
|
183
|
+
assert.deepEqual('lwm2m' in res && res.lwm2m, expected)
|
|
180
184
|
})
|
|
181
185
|
})
|
package/senml/senMLtoLwM2M.ts
CHANGED
|
@@ -42,14 +42,12 @@ const isInfoForDifferentInstance = (
|
|
|
42
42
|
currentObject: LwM2MObjectInstance,
|
|
43
43
|
resourceID: ResourceID,
|
|
44
44
|
currentBaseTime: number,
|
|
45
|
+
tsRes: number,
|
|
45
46
|
): boolean => {
|
|
46
47
|
if (currentObject === undefined) return true
|
|
47
48
|
if (currentObject.ObjectID !== resourceID.ObjectID) return true
|
|
48
49
|
if ((currentObject.ObjectInstanceID ?? 0) !== resourceID.ObjectInstanceID)
|
|
49
50
|
return true
|
|
50
|
-
const tsRes = timestampResources[resourceID.ObjectID]
|
|
51
|
-
if (tsRes === undefined)
|
|
52
|
-
throw new Error(`Unknown LwM2M Object ID: ${resourceID.ObjectID}!`)
|
|
53
51
|
if (
|
|
54
52
|
currentBaseTime !==
|
|
55
53
|
(currentObject.Resources?.[tsRes] as Date | undefined)?.getTime()
|
|
@@ -70,11 +68,13 @@ const getValue = (
|
|
|
70
68
|
if ('vd' in measurement) return measurement.vd
|
|
71
69
|
return undefined
|
|
72
70
|
}
|
|
73
|
-
export const senMLtoLwM2M = (
|
|
71
|
+
export const senMLtoLwM2M = (
|
|
72
|
+
senML: SenMLType,
|
|
73
|
+
): { lwm2m: Array<LwM2MObjectInstance> } | { error: Error } => {
|
|
74
74
|
let currentBaseName: string = ''
|
|
75
75
|
let currentBaseTime: number | undefined = undefined
|
|
76
76
|
let currentObject: LwM2MObjectInstance | undefined = undefined
|
|
77
|
-
const
|
|
77
|
+
const lwm2m: Array<LwM2MObjectInstance> = []
|
|
78
78
|
|
|
79
79
|
for (const item of senML) {
|
|
80
80
|
if ('bn' in item && item.bn !== undefined) currentBaseName = item.bn
|
|
@@ -82,20 +82,37 @@ export const senMLtoLwM2M = (senML: SenMLType): Array<LwM2MObjectInstance> => {
|
|
|
82
82
|
if (!hasValue(item)) continue
|
|
83
83
|
const itemResourceId = `${currentBaseName}${item.n ?? ''}/0`
|
|
84
84
|
const resourceId = parseResourceId(itemResourceId)
|
|
85
|
-
if (resourceId === null)
|
|
86
|
-
|
|
85
|
+
if (resourceId === null) {
|
|
86
|
+
return { error: new Error(`Invalid resource ID: ${itemResourceId}`) }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const tsRes = timestampResources[resourceId.ObjectID]
|
|
90
|
+
if (tsRes === undefined) {
|
|
91
|
+
return {
|
|
92
|
+
error: new Error(
|
|
93
|
+
`No timestamp resource defined for object: ${resourceId.ObjectID}`,
|
|
94
|
+
),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
87
97
|
|
|
88
98
|
if (
|
|
89
99
|
currentObject === undefined ||
|
|
90
100
|
(currentBaseTime !== undefined &&
|
|
91
|
-
isInfoForDifferentInstance(
|
|
101
|
+
isInfoForDifferentInstance(
|
|
102
|
+
currentObject,
|
|
103
|
+
resourceId,
|
|
104
|
+
currentBaseTime,
|
|
105
|
+
tsRes,
|
|
106
|
+
))
|
|
92
107
|
) {
|
|
93
|
-
if (currentObject !== undefined)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
108
|
+
if (currentObject !== undefined) lwm2m.push(currentObject)
|
|
109
|
+
if (currentBaseTime === undefined) {
|
|
110
|
+
return {
|
|
111
|
+
error: new Error(
|
|
112
|
+
`No base time defined for object: ${resourceId.ObjectID}!`,
|
|
113
|
+
),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
99
116
|
currentObject = {
|
|
100
117
|
ObjectID: resourceId.ObjectID,
|
|
101
118
|
Resources: {
|
|
@@ -115,7 +132,7 @@ export const senMLtoLwM2M = (senML: SenMLType): Array<LwM2MObjectInstance> => {
|
|
|
115
132
|
}
|
|
116
133
|
}
|
|
117
134
|
}
|
|
118
|
-
if (currentObject !== undefined)
|
|
135
|
+
if (currentObject !== undefined) lwm2m.push(currentObject)
|
|
119
136
|
|
|
120
|
-
return
|
|
137
|
+
return { lwm2m }
|
|
121
138
|
}
|