iotagent-node-lib 4.3.0 → 4.5.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/config.js +2 -1
- package/doc/admin.md +16 -7
- package/doc/api.md +230 -45
- package/doc/requirements.txt +1 -1
- package/docker/Mosquitto/Dockerfile +1 -1
- package/lib/commonConfig.js +25 -1
- package/lib/errors.js +4 -1
- package/lib/services/devices/deviceRegistryMemory.js +1 -1
- package/lib/services/devices/deviceRegistryMongoDB.js +3 -10
- package/lib/services/devices/deviceService.js +5 -3
- package/lib/services/devices/devices-NGSI-LD.js +5 -5
- package/lib/services/devices/devices-NGSI-mixed.js +3 -3
- package/lib/services/devices/devices-NGSI-v2.js +5 -5
- package/lib/services/ngsi/entities-NGSI-LD.js +1 -1
- package/lib/services/ngsi/entities-NGSI-v2.js +324 -268
- package/lib/services/ngsi/ngsiService.js +2 -2
- package/lib/services/ngsi/subscription-NGSI-LD.js +2 -2
- package/lib/services/ngsi/subscription-NGSI-v2.js +2 -2
- package/lib/services/northBound/deviceProvisioningServer.js +5 -4
- package/lib/services/northBound/northboundServer.js +2 -2
- package/package.json +2 -2
- package/scripts/legacy_expression_tool/requirements.txt +1 -1
- package/test/functional/README.md +60 -37
- package/test/functional/testCases.js +2206 -722
- package/test/functional/testUtils.js +53 -20
- package/test/unit/examples/deviceProvisioningRequests/updateProvisionDeviceWithApikey.json +4 -0
- package/test/unit/ngsi-ld/provisioning/device-update-registration_test.js +5 -5
- package/test/unit/ngsiv2/plugins/multientity-plugin_test.js +34 -2
- package/test/unit/ngsiv2/provisioning/device-update-registration_test.js +6 -6
- package/test/unit/ngsiv2/provisioning/updateProvisionedDevices-test.js +35 -0
|
@@ -260,7 +260,7 @@ function sendQueryValueNgsi2(entityName, attributes, typeInformation, token, cal
|
|
|
260
260
|
* @param {Object} typeInformation Configuration information for the device.
|
|
261
261
|
* @param {String} token User token to identify against the PEP Proxies (optional).
|
|
262
262
|
*/
|
|
263
|
-
function sendUpdateValueNgsi2(entityName,
|
|
263
|
+
function sendUpdateValueNgsi2(entityName, originMeasures, originTypeInformation, token, callback) {
|
|
264
264
|
//aux function used to builf JEXL context.
|
|
265
265
|
//it returns a flat object from an Attr array
|
|
266
266
|
function reduceAttrToPlainObject(attrs, initObj = {}) {
|
|
@@ -273,315 +273,354 @@ function sendUpdateValueNgsi2(entityName, measures, typeInformation, token, call
|
|
|
273
273
|
return initObj;
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
|
-
|
|
277
|
-
let entities = {}; //{entityName:{entityType:[attrs]}} //SubGoal Populate entoties data striucture
|
|
278
|
-
let jexlctxt = {}; //will store the whole context (not just for JEXL)
|
|
279
|
-
let payload = {}; //will store the final payload
|
|
280
|
-
let timestamp = { type: constants.TIMESTAMP_TYPE_NGSI2 }; //timestamp scafold-attr for insertions.
|
|
281
|
-
let plainMeasures = null; //will contain measures POJO
|
|
282
|
-
let idTypeSSSList = pluginUtils.getIdTypeServSubServiceFromDevice(typeInformation);
|
|
283
|
-
|
|
284
276
|
//Make a clone and overwrite
|
|
285
|
-
|
|
277
|
+
let idTypeSSSList = pluginUtils.getIdTypeServSubServiceFromDevice(originTypeInformation);
|
|
286
278
|
|
|
287
279
|
//Check mandatory information: type
|
|
288
|
-
if (!
|
|
289
|
-
callback(new errors.TypeNotFound(null, entityName,
|
|
280
|
+
if (!originTypeInformation || !originTypeInformation.type) {
|
|
281
|
+
callback(new errors.TypeNotFound(null, entityName, originTypeInformation));
|
|
290
282
|
return;
|
|
291
283
|
}
|
|
292
|
-
//Rename all measures with matches with id and type to measure_id and measure_type
|
|
293
|
-
for (let measure of measures) {
|
|
294
|
-
if (measure.name === 'id' || measure.name === 'type') {
|
|
295
|
-
measure.name = constants.MEASURE + measure.name;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
//Make a copy of measures in an plain object: plainMeasures
|
|
300
|
-
plainMeasures = reduceAttrToPlainObject(measures);
|
|
301
284
|
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
jexlctxt = reduceAttrToPlainObject(typeInformation.staticAttributes, jexlctxt);
|
|
307
|
-
//id type Service and Subservice
|
|
308
|
-
jexlctxt = reduceAttrToPlainObject(idTypeSSSList, jexlctxt);
|
|
285
|
+
let payload = {}; //will store the final payload
|
|
286
|
+
let entities = {};
|
|
287
|
+
payload.actionType = 'append';
|
|
288
|
+
payload.entities = [];
|
|
309
289
|
|
|
290
|
+
const currentIsoDate = new Date().toISOString();
|
|
291
|
+
const currentMoment = moment(currentIsoDate);
|
|
310
292
|
//Managing timestamp (mustInsertTimeInstant flag to decide if we should insert Timestamp later on)
|
|
311
|
-
const mustInsertTimeInstant =
|
|
312
|
-
|
|
313
|
-
? typeInformation.timestamp
|
|
314
|
-
: false;
|
|
315
|
-
|
|
316
|
-
if (mustInsertTimeInstant) {
|
|
317
|
-
//remove TimeInstant from measures
|
|
318
|
-
measures = measures.filter((item) => item.name !== constants.TIMESTAMP_ATTRIBUTE);
|
|
319
|
-
|
|
320
|
-
if (plainMeasures[constants.TIMESTAMP_ATTRIBUTE]) {
|
|
321
|
-
//if it comes from a measure
|
|
322
|
-
if (moment(plainMeasures[constants.TIMESTAMP_ATTRIBUTE], moment.ISO_8601, true).isValid()) {
|
|
323
|
-
timestamp.value = plainMeasures[constants.TIMESTAMP_ATTRIBUTE];
|
|
324
|
-
} else {
|
|
325
|
-
callback(
|
|
326
|
-
new errors.BadTimestamp(plainMeasures[constants.TIMESTAMP_ATTRIBUTE], entityName, typeInformation)
|
|
327
|
-
);
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
} else if (!typeInformation.timezone) {
|
|
331
|
-
timestamp.value = new Date().toISOString();
|
|
332
|
-
jexlctxt[constants.TIMESTAMP_ATTRIBUTE] = timestamp.value;
|
|
333
|
-
} else {
|
|
334
|
-
timestamp.value = moment().tz(typeInformation.timezone).format('YYYY-MM-DD[T]HH:mm:ss.SSSZ');
|
|
335
|
-
jexlctxt[constants.TIMESTAMP_ATTRIBUTE] = timestamp.value;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
logger.debug(
|
|
340
|
-
context,
|
|
341
|
-
'sendUpdateValueNgsi2 called with: entityName=%s, measures=%j, typeInformation=%j, initial jexlContext=%j, timestamp=%j with value=%j',
|
|
342
|
-
entityName,
|
|
343
|
-
plainMeasures,
|
|
344
|
-
typeInformation,
|
|
345
|
-
jexlctxt,
|
|
346
|
-
mustInsertTimeInstant,
|
|
347
|
-
timestamp.value
|
|
348
|
-
);
|
|
293
|
+
const mustInsertTimeInstant =
|
|
294
|
+
originTypeInformation.timestamp !== undefined ? originTypeInformation.timestamp : false;
|
|
349
295
|
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
try {
|
|
354
|
-
logger.debug(context, 'sendUpdateValueNgsi2 entityNameExp %j', typeInformation.entityNameExp);
|
|
355
|
-
entityNameCalc = expressionPlugin.applyExpression(typeInformation.entityNameExp, jexlctxt, typeInformation);
|
|
356
|
-
} catch (e) {
|
|
357
|
-
logger.debug(
|
|
358
|
-
context,
|
|
359
|
-
'Error evaluating expression for entityName: %j with context: %j',
|
|
360
|
-
typeInformation.entityNameExp,
|
|
361
|
-
jexlctxt
|
|
362
|
-
);
|
|
363
|
-
}
|
|
296
|
+
// Check if measures is a single measure or a array of measures (a multimeasure)
|
|
297
|
+
if (originMeasures[0] && !originMeasures[0][0]) {
|
|
298
|
+
originMeasures = [originMeasures];
|
|
364
299
|
}
|
|
300
|
+
for (let measures of originMeasures) {
|
|
301
|
+
entities = {}; //{entityName:{entityType:[attrs]}} //SubGoal Populate entities data structure
|
|
302
|
+
let jexlctxt = {}; //will store the whole context (not just for JEXL)
|
|
303
|
+
|
|
304
|
+
let plainMeasures = null; //will contain measures POJO
|
|
305
|
+
//Make a clone and overwrite
|
|
306
|
+
let typeInformation = JSON.parse(JSON.stringify(originTypeInformation));
|
|
307
|
+
|
|
308
|
+
//Rename all measures with matches with id and type to measure_id and measure_type
|
|
309
|
+
for (let measure of measures) {
|
|
310
|
+
if (measure.name === 'id' || measure.name === 'type') {
|
|
311
|
+
measure.name = constants.MEASURE + measure.name;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
365
314
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
315
|
+
//Make a copy of measures in an plain object: plainMeasures
|
|
316
|
+
plainMeasures = reduceAttrToPlainObject(measures);
|
|
317
|
+
//Build the initital JEXL Context
|
|
318
|
+
//All the measures (avoid references make another copy instead)
|
|
319
|
+
jexlctxt = reduceAttrToPlainObject(measures);
|
|
320
|
+
//All the static
|
|
321
|
+
jexlctxt = reduceAttrToPlainObject(typeInformation.staticAttributes, jexlctxt);
|
|
322
|
+
//id type Service and Subservice
|
|
323
|
+
jexlctxt = reduceAttrToPlainObject(idTypeSSSList, jexlctxt);
|
|
369
324
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
preprocessedAttr = preprocessedAttr.concat(typeInformation.active);
|
|
380
|
-
}
|
|
325
|
+
logger.debug(
|
|
326
|
+
context,
|
|
327
|
+
'sendUpdateValueNgsi2 loop with: entityName=%s, measures=%j, typeInformation=%j, initial jexlContext=%j, timestamp=%j',
|
|
328
|
+
entityName,
|
|
329
|
+
plainMeasures,
|
|
330
|
+
typeInformation,
|
|
331
|
+
jexlctxt,
|
|
332
|
+
mustInsertTimeInstant
|
|
333
|
+
);
|
|
381
334
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
for (let currentAttr of preprocessedAttr) {
|
|
387
|
-
let hitted = false; //any measure, expressiom or value hit the attr (avoid propagate "silent attr" with null values )
|
|
388
|
-
let attrEntityName = entityName;
|
|
389
|
-
let attrEntityType = typeInformation.type;
|
|
390
|
-
let valueExpression = null;
|
|
391
|
-
//manage active attr without object__id (name by default)
|
|
392
|
-
currentAttr.object_id = currentAttr.object_id ? currentAttr.object_id : currentAttr.name;
|
|
393
|
-
//Enrich the attr (skip, hit, value, meta-timeInstant)
|
|
394
|
-
currentAttr.skipValue = currentAttr.skipValue ? currentAttr.skipValue : null;
|
|
395
|
-
|
|
396
|
-
//determine AttrEntityName for multientity
|
|
397
|
-
if (
|
|
398
|
-
currentAttr.entity_name !== null &&
|
|
399
|
-
currentAttr.entity_name !== undefined &&
|
|
400
|
-
currentAttr.entity_name !== '' &&
|
|
401
|
-
typeof currentAttr.entity_name == 'string'
|
|
402
|
-
) {
|
|
335
|
+
//Now we can calculate the EntityName of primary entity
|
|
336
|
+
let entityNameCalc = null;
|
|
337
|
+
if (typeInformation.entityNameExp !== undefined && typeInformation.entityNameExp !== '') {
|
|
403
338
|
try {
|
|
404
|
-
logger.debug(
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
jexlctxt
|
|
339
|
+
logger.debug(context, 'sendUpdateValueNgsi2 entityNameExp %j', typeInformation.entityNameExp);
|
|
340
|
+
entityNameCalc = expressionPlugin.applyExpression(
|
|
341
|
+
typeInformation.entityNameExp,
|
|
342
|
+
jexlctxt,
|
|
343
|
+
typeInformation
|
|
410
344
|
);
|
|
411
|
-
attrEntityName = jexlParser.applyExpression(currentAttr.entity_name, jexlctxt, typeInformation);
|
|
412
|
-
if (!attrEntityName) {
|
|
413
|
-
attrEntityName = currentAttr.entity_name;
|
|
414
|
-
}
|
|
415
345
|
} catch (e) {
|
|
416
346
|
logger.debug(
|
|
417
347
|
context,
|
|
418
|
-
'
|
|
419
|
-
|
|
348
|
+
'Error evaluating expression for entityName: %j with context: %j',
|
|
349
|
+
typeInformation.entityNameExp,
|
|
420
350
|
jexlctxt
|
|
421
351
|
);
|
|
422
|
-
attrEntityName = currentAttr.entity_name;
|
|
423
352
|
}
|
|
424
353
|
}
|
|
425
354
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
currentAttr.entity_type !== undefined &&
|
|
430
|
-
currentAttr.entity_type !== '' &&
|
|
431
|
-
typeof currentAttr.entity_type === 'string'
|
|
432
|
-
) {
|
|
433
|
-
attrEntityType = currentAttr.entity_type;
|
|
434
|
-
}
|
|
355
|
+
entityName = entityNameCalc ? entityNameCalc : entityName;
|
|
356
|
+
//enrich JEXL context
|
|
357
|
+
jexlctxt['entity_name'] = entityName;
|
|
435
358
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (currentAttr.value !== undefined) {
|
|
441
|
-
//static attributes already have a value
|
|
442
|
-
hitted = true;
|
|
443
|
-
valueExpression = currentAttr.value;
|
|
444
|
-
} else if (plainMeasures[currentAttr.object_id] !== undefined) {
|
|
445
|
-
//we have got a meaure for that Attr
|
|
446
|
-
//actives ¿lazis?
|
|
447
|
-
hitted = true;
|
|
448
|
-
valueExpression = plainMeasures[currentAttr.object_id];
|
|
359
|
+
let preprocessedAttr = [];
|
|
360
|
+
//Add Raw Static, Lazy, Command and Actives attr attributes
|
|
361
|
+
if (typeInformation && typeInformation.staticAttributes) {
|
|
362
|
+
preprocessedAttr = preprocessedAttr.concat(typeInformation.staticAttributes);
|
|
449
363
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
currentAttr.expression !== undefined &&
|
|
456
|
-
currentAttr.expression !== '' &&
|
|
457
|
-
typeof currentAttr.expression == 'string'
|
|
458
|
-
) {
|
|
459
|
-
try {
|
|
460
|
-
hitted = true;
|
|
461
|
-
valueExpression = jexlParser.applyExpression(currentAttr.expression, jexlctxt, typeInformation);
|
|
462
|
-
//we fallback to null if anything unexpecte happend
|
|
463
|
-
if (valueExpression === null || valueExpression === undefined || Number.isNaN(valueExpression)) {
|
|
464
|
-
valueExpression = null;
|
|
465
|
-
}
|
|
466
|
-
} catch (e) {
|
|
467
|
-
valueExpression = null;
|
|
468
|
-
}
|
|
469
|
-
logger.debug(
|
|
470
|
-
context,
|
|
471
|
-
'Evaluated attr: %j, with expression: %j, and ctxt: %j resulting: %j',
|
|
472
|
-
currentAttr.name,
|
|
473
|
-
currentAttr.expression,
|
|
474
|
-
jexlctxt,
|
|
475
|
-
valueExpression
|
|
476
|
-
);
|
|
364
|
+
if (typeInformation && typeInformation.lazy) {
|
|
365
|
+
preprocessedAttr = preprocessedAttr.concat(typeInformation.lazy);
|
|
366
|
+
}
|
|
367
|
+
if (typeInformation && typeInformation.active) {
|
|
368
|
+
preprocessedAttr = preprocessedAttr.concat(typeInformation.active);
|
|
477
369
|
}
|
|
478
370
|
|
|
479
|
-
|
|
480
|
-
|
|
371
|
+
//Proccess every proto Attribute to populate entities data steuture
|
|
372
|
+
entities[entityName] = {};
|
|
373
|
+
entities[entityName][typeInformation.type] = [];
|
|
374
|
+
|
|
375
|
+
for (let currentAttr of preprocessedAttr) {
|
|
376
|
+
let hitted = false; //any measure, expressiom or value hit the attr (avoid propagate "silent attr" with null values )
|
|
377
|
+
let attrEntityName = entityName;
|
|
378
|
+
let attrEntityType = typeInformation.type;
|
|
379
|
+
let valueExpression = null;
|
|
380
|
+
//manage active attr without object__id (name by default)
|
|
381
|
+
currentAttr.object_id = currentAttr.object_id ? currentAttr.object_id : currentAttr.name;
|
|
382
|
+
//Enrich the attr (skip, hit, value, meta-timeInstant)
|
|
383
|
+
currentAttr.skipValue = currentAttr.skipValue ? currentAttr.skipValue : null;
|
|
384
|
+
|
|
385
|
+
//determine AttrEntityName for multientity
|
|
386
|
+
if (
|
|
387
|
+
currentAttr.entity_name !== null &&
|
|
388
|
+
currentAttr.entity_name !== undefined &&
|
|
389
|
+
currentAttr.entity_name !== '' &&
|
|
390
|
+
typeof currentAttr.entity_name == 'string'
|
|
391
|
+
) {
|
|
392
|
+
try {
|
|
393
|
+
logger.debug(
|
|
394
|
+
context,
|
|
395
|
+
'Evaluating attribute: %j, for entity_name(exp):%j, with ctxt: %j',
|
|
396
|
+
currentAttr.name,
|
|
397
|
+
currentAttr.entity_name,
|
|
398
|
+
jexlctxt
|
|
399
|
+
);
|
|
400
|
+
attrEntityName = jexlParser.applyExpression(currentAttr.entity_name, jexlctxt, typeInformation);
|
|
401
|
+
if (!attrEntityName) {
|
|
402
|
+
attrEntityName = currentAttr.entity_name;
|
|
403
|
+
}
|
|
404
|
+
} catch (e) {
|
|
405
|
+
logger.debug(
|
|
406
|
+
context,
|
|
407
|
+
'Exception evaluating entityNameExp:%j, with jexlctxt: %j',
|
|
408
|
+
currentAttr.entity_name,
|
|
409
|
+
jexlctxt
|
|
410
|
+
);
|
|
411
|
+
attrEntityName = currentAttr.entity_name;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
481
414
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
currentAttr.
|
|
415
|
+
//determine AttrEntityType for multientity
|
|
416
|
+
if (
|
|
417
|
+
currentAttr.entity_type !== null &&
|
|
418
|
+
currentAttr.entity_type !== undefined &&
|
|
419
|
+
currentAttr.entity_type !== '' &&
|
|
420
|
+
typeof currentAttr.entity_type === 'string'
|
|
421
|
+
) {
|
|
422
|
+
attrEntityType = currentAttr.entity_type;
|
|
486
423
|
}
|
|
487
|
-
currentAttr.metadata[constants.TIMESTAMP_ATTRIBUTE] = timestamp;
|
|
488
|
-
}
|
|
489
424
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
425
|
+
//PRE POPULATE CONTEXT
|
|
426
|
+
jexlctxt[currentAttr.name] = plainMeasures[currentAttr.object_id];
|
|
427
|
+
|
|
428
|
+
//determine Value
|
|
429
|
+
if (currentAttr.value !== undefined) {
|
|
430
|
+
//static attributes already have a value
|
|
431
|
+
hitted = true;
|
|
432
|
+
valueExpression = currentAttr.value;
|
|
433
|
+
} else if (plainMeasures[currentAttr.object_id] !== undefined) {
|
|
434
|
+
//we have got a meaure for that Attr
|
|
435
|
+
//actives ¿lazis?
|
|
436
|
+
hitted = true;
|
|
437
|
+
valueExpression = plainMeasures[currentAttr.object_id];
|
|
494
438
|
}
|
|
495
|
-
|
|
496
|
-
|
|
439
|
+
//remove measures that has been shadowed by an alias (some may be left and managed later)
|
|
440
|
+
//Maybe we must filter object_id if there is name == object_id
|
|
441
|
+
measures = measures.filter((item) => item.name !== currentAttr.object_id && item.name !== currentAttr.name);
|
|
442
|
+
|
|
443
|
+
if (
|
|
444
|
+
currentAttr.expression !== undefined &&
|
|
445
|
+
currentAttr.expression !== '' &&
|
|
446
|
+
typeof currentAttr.expression == 'string'
|
|
447
|
+
) {
|
|
448
|
+
try {
|
|
449
|
+
hitted = true;
|
|
450
|
+
valueExpression = jexlParser.applyExpression(currentAttr.expression, jexlctxt, typeInformation);
|
|
451
|
+
//we fallback to null if anything unexpecte happend
|
|
452
|
+
if (valueExpression === null || valueExpression === undefined || Number.isNaN(valueExpression)) {
|
|
453
|
+
valueExpression = null;
|
|
454
|
+
}
|
|
455
|
+
} catch (e) {
|
|
456
|
+
valueExpression = null;
|
|
457
|
+
}
|
|
458
|
+
logger.debug(
|
|
459
|
+
context,
|
|
460
|
+
'Evaluated attr: %j, with expression: %j, and ctxt: %j resulting: %j',
|
|
461
|
+
currentAttr.name,
|
|
462
|
+
currentAttr.expression,
|
|
463
|
+
jexlctxt,
|
|
464
|
+
valueExpression
|
|
465
|
+
);
|
|
497
466
|
}
|
|
498
|
-
//store de New Attributte
|
|
499
|
-
entities[attrEntityName][attrEntityType].push(currentAttr);
|
|
500
|
-
}
|
|
501
467
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
468
|
+
currentAttr.hitted = hitted;
|
|
469
|
+
currentAttr.value = valueExpression;
|
|
505
470
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
471
|
+
//store de New Attributte in entity data structure
|
|
472
|
+
if (hitted === true) {
|
|
473
|
+
if (entities[attrEntityName] === undefined) {
|
|
474
|
+
entities[attrEntityName] = {};
|
|
475
|
+
}
|
|
476
|
+
if (entities[attrEntityName][attrEntityType] === undefined) {
|
|
477
|
+
entities[attrEntityName][attrEntityType] = [];
|
|
478
|
+
}
|
|
479
|
+
//store de New Attributte
|
|
480
|
+
entities[attrEntityName][attrEntityType].push(currentAttr);
|
|
513
481
|
}
|
|
514
|
-
logger.debug(
|
|
515
|
-
context,
|
|
516
|
-
'Calculated explicitAttrs with expression: %j and ctxt: %j resulting: %j',
|
|
517
|
-
typeInformation.explicitAttrs,
|
|
518
|
-
jexlctxt,
|
|
519
|
-
explicit
|
|
520
|
-
);
|
|
521
|
-
} catch (e) {
|
|
522
|
-
// nothing to do: exception is already logged at info level
|
|
523
|
-
}
|
|
524
|
-
} else if (typeof typeInformation.explicitAttrs == 'boolean') {
|
|
525
|
-
explicit = typeInformation.explicitAttrs;
|
|
526
|
-
}
|
|
527
482
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
483
|
+
//RE-Populate de JEXLcontext (except for null or NaN we preffer undefined)
|
|
484
|
+
jexlctxt[currentAttr.name] = valueExpression;
|
|
485
|
+
|
|
486
|
+
// Expand metadata value expression
|
|
487
|
+
if (currentAttr.metadata) {
|
|
488
|
+
for (var metaKey in currentAttr.metadata) {
|
|
489
|
+
if (currentAttr.metadata[metaKey].expression && metaKey !== constants.TIMESTAMP_ATTRIBUTE) {
|
|
490
|
+
let newAttrMeta = {};
|
|
491
|
+
if (currentAttr.metadata[metaKey].type) {
|
|
492
|
+
newAttrMeta['type'] = currentAttr.metadata[metaKey].type;
|
|
493
|
+
}
|
|
494
|
+
let metaValueExpression;
|
|
495
|
+
try {
|
|
496
|
+
metaValueExpression = jexlParser.applyExpression(
|
|
497
|
+
currentAttr.metadata[metaKey].expression,
|
|
498
|
+
jexlctxt,
|
|
499
|
+
typeInformation
|
|
500
|
+
);
|
|
501
|
+
//we fallback to null if anything unexpecte happend
|
|
502
|
+
if (
|
|
503
|
+
metaValueExpression === null ||
|
|
504
|
+
metaValueExpression === undefined ||
|
|
505
|
+
Number.isNaN(metaValueExpression)
|
|
506
|
+
) {
|
|
507
|
+
metaValueExpression = null;
|
|
508
|
+
}
|
|
509
|
+
} catch (e) {
|
|
510
|
+
metaValueExpression = null;
|
|
511
|
+
}
|
|
512
|
+
newAttrMeta['value'] = metaValueExpression;
|
|
513
|
+
currentAttr.metadata[metaKey] = newAttrMeta;
|
|
514
|
+
}
|
|
535
515
|
}
|
|
536
|
-
currentMeasure.metadata[constants.TIMESTAMP_ATTRIBUTE] = timestamp;
|
|
537
516
|
}
|
|
538
|
-
//If just measures in the principal entity we missed the Timestamp.
|
|
539
517
|
}
|
|
540
|
-
entities[entityName][typeInformation.type] = entities[entityName][typeInformation.type].concat(measures);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
//PRE-PROCESSING FINISHED
|
|
544
|
-
//Explicit ATTRS and SKIPVALUES will be managed while we build NGSI payload
|
|
545
|
-
|
|
546
|
-
//Get ready to build and send NGSI payload (entities-->payload)
|
|
547
|
-
payload.actionType = 'append';
|
|
548
518
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
let isEmpty = true;
|
|
557
|
-
for (let attr of entities[ename][etype]) {
|
|
558
|
-
if (
|
|
559
|
-
attr.name !== 'id' &&
|
|
560
|
-
attr.name !== 'type' &&
|
|
561
|
-
(attr.value !== attr.skipValue || attr.skipValue === undefined) &&
|
|
562
|
-
(attr.hitted || attr.hitted === undefined) && //undefined is for pure measures
|
|
563
|
-
(typeof explicit === 'boolean' || //true and false already handled
|
|
564
|
-
(explicit instanceof Array && //check the array version
|
|
565
|
-
(explicit.includes(attr.name) ||
|
|
566
|
-
explicit.some(
|
|
567
|
-
(item) => attr.object_id !== undefined && item.object_id === attr.object_id
|
|
568
|
-
))))
|
|
569
|
-
) {
|
|
570
|
-
isEmpty = false;
|
|
571
|
-
e[attr.name] = { type: attr.type, value: attr.value, metadata: attr.metadata };
|
|
519
|
+
//now we can compute explicit (Bool or Array) with the complete JexlContext
|
|
520
|
+
let explicit = false;
|
|
521
|
+
if (typeof typeInformation.explicitAttrs === 'string') {
|
|
522
|
+
try {
|
|
523
|
+
explicit = jexlParser.applyExpression(typeInformation.explicitAttrs, jexlctxt, typeInformation);
|
|
524
|
+
if (explicit instanceof Array && explicit.length > 0 && mustInsertTimeInstant) {
|
|
525
|
+
explicit.push(constants.TIMESTAMP_ATTRIBUTE);
|
|
572
526
|
}
|
|
527
|
+
logger.debug(
|
|
528
|
+
context,
|
|
529
|
+
'Calculated explicitAttrs with expression: %j and ctxt: %j resulting: %j',
|
|
530
|
+
typeInformation.explicitAttrs,
|
|
531
|
+
jexlctxt,
|
|
532
|
+
explicit
|
|
533
|
+
);
|
|
534
|
+
} catch (e) {
|
|
535
|
+
// nothing to do: exception is already logged at info level
|
|
573
536
|
}
|
|
574
|
-
|
|
537
|
+
} else if (typeof typeInformation.explicitAttrs == 'boolean') {
|
|
538
|
+
explicit = typeInformation.explicitAttrs;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
//more mesures may be added to the attribute list (unnhandled/left mesaures) l
|
|
542
|
+
if (explicit === false && Object.keys(measures).length > 0) {
|
|
543
|
+
entities[entityName][typeInformation.type] = entities[entityName][typeInformation.type].concat(measures);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
//PRE-PROCESSING FINISHED
|
|
547
|
+
//Explicit ATTRS and SKIPVALUES will be managed while we build NGSI payload
|
|
548
|
+
//Get ready to build and send NGSI payload (entities-->payload)
|
|
549
|
+
|
|
550
|
+
for (let ename in entities) {
|
|
551
|
+
for (let etype in entities[ename]) {
|
|
552
|
+
let e = {};
|
|
553
|
+
e.id = String(ename);
|
|
554
|
+
e.type = String(etype);
|
|
555
|
+
let timestamp = { type: constants.TIMESTAMP_TYPE_NGSI2 }; //timestamp scafold-attr for insertions.
|
|
556
|
+
let timestampAttrs = null;
|
|
575
557
|
if (mustInsertTimeInstant) {
|
|
576
|
-
|
|
558
|
+
// get timestamp for current entity
|
|
559
|
+
|
|
560
|
+
timestampAttrs = entities[ename][etype].filter(
|
|
561
|
+
(item) => item.name === constants.TIMESTAMP_ATTRIBUTE
|
|
562
|
+
);
|
|
563
|
+
if (timestampAttrs && timestampAttrs.length > 0) {
|
|
564
|
+
timestamp.value = timestampAttrs[0]['value'];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (timestamp.value) {
|
|
568
|
+
if (!moment(timestamp.value, moment.ISO_8601, true).isValid()) {
|
|
569
|
+
callback(new errors.BadTimestamp(timestamp.value, entityName, typeInformation));
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
} else {
|
|
573
|
+
if (!typeInformation.timezone) {
|
|
574
|
+
timestamp.value = currentIsoDate;
|
|
575
|
+
jexlctxt[constants.TIMESTAMP_ATTRIBUTE] = timestamp.value;
|
|
576
|
+
} else {
|
|
577
|
+
timestamp.value = currentMoment
|
|
578
|
+
.tz(typeInformation.timezone)
|
|
579
|
+
.format('YYYY-MM-DD[T]HH:mm:ss.SSSZ');
|
|
580
|
+
jexlctxt[constants.TIMESTAMP_ATTRIBUTE] = timestamp.value;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
//extract attributes
|
|
585
|
+
let isEmpty = true;
|
|
586
|
+
for (let attr of entities[ename][etype]) {
|
|
587
|
+
if (
|
|
588
|
+
attr.name !== 'id' &&
|
|
589
|
+
attr.name !== 'type' &&
|
|
590
|
+
(attr.value !== attr.skipValue || attr.skipValue === undefined) &&
|
|
591
|
+
(attr.hitted || attr.hitted === undefined) && //undefined is for pure measures
|
|
592
|
+
(typeof explicit === 'boolean' || //true and false already handled
|
|
593
|
+
(explicit instanceof Array && //check the array version
|
|
594
|
+
(explicit.includes(attr.name) ||
|
|
595
|
+
explicit.some(
|
|
596
|
+
(item) => attr.object_id !== undefined && item.object_id === attr.object_id
|
|
597
|
+
))))
|
|
598
|
+
) {
|
|
599
|
+
isEmpty = false;
|
|
600
|
+
if (mustInsertTimeInstant) {
|
|
601
|
+
// Add TimeInstant to all attribute metadata of all entities
|
|
602
|
+
if (attr.name !== constants.TIMESTAMP_ATTRIBUTE) {
|
|
603
|
+
if (!attr.metadata) {
|
|
604
|
+
attr.metadata = {};
|
|
605
|
+
}
|
|
606
|
+
attr.metadata[constants.TIMESTAMP_ATTRIBUTE] = timestamp;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
e[attr.name] = { type: attr.type, value: attr.value, metadata: attr.metadata };
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (!isEmpty) {
|
|
613
|
+
if (mustInsertTimeInstant) {
|
|
614
|
+
e[constants.TIMESTAMP_ATTRIBUTE] = timestamp;
|
|
615
|
+
}
|
|
616
|
+
payload.entities.push(e);
|
|
577
617
|
}
|
|
578
|
-
payload.entities.push(e);
|
|
579
618
|
}
|
|
580
619
|
}
|
|
581
|
-
}
|
|
620
|
+
} // end for (let measures of originMeasures)
|
|
582
621
|
|
|
583
622
|
let url = '/v2/op/update';
|
|
584
|
-
let options = NGSIUtils.createRequestObject(url,
|
|
623
|
+
let options = NGSIUtils.createRequestObject(url, originTypeInformation, token);
|
|
585
624
|
options.json = payload;
|
|
586
625
|
|
|
587
626
|
// Prevent to update an entity with an empty payload: more than id and type
|
|
@@ -596,13 +635,17 @@ function sendUpdateValueNgsi2(entityName, measures, typeInformation, token, call
|
|
|
596
635
|
// Note that the options object is prepared for the second case (multi entity), so we "patch" it
|
|
597
636
|
// only in the first case
|
|
598
637
|
|
|
599
|
-
//
|
|
600
|
-
|
|
638
|
+
//Multi: multientity (more than one name o more than one type at primary entity)
|
|
639
|
+
// of multimeasure (originMeasures is an array of more than one element)
|
|
640
|
+
let multi =
|
|
641
|
+
Object.keys(entities).length > 1 ||
|
|
642
|
+
Object.keys(entities[entityName]).length > 1 ||
|
|
643
|
+
originMeasures.length > 1;
|
|
601
644
|
|
|
602
|
-
if (!
|
|
645
|
+
if (!multi) {
|
|
603
646
|
// recreate options object to use single entity update
|
|
604
647
|
url = '/v2/entities?options=upsert';
|
|
605
|
-
options = NGSIUtils.createRequestObject(url,
|
|
648
|
+
options = NGSIUtils.createRequestObject(url, originTypeInformation, token);
|
|
606
649
|
delete payload.actionType;
|
|
607
650
|
|
|
608
651
|
let entityAttrs = payload.entities[0];
|
|
@@ -619,7 +662,20 @@ function sendUpdateValueNgsi2(entityName, measures, typeInformation, token, call
|
|
|
619
662
|
transformedObject.type = entityAttrs.type;
|
|
620
663
|
options.json = transformedObject;
|
|
621
664
|
options.method = 'POST';
|
|
622
|
-
}
|
|
665
|
+
} else if (payload.entities.every((entity) => 'TimeInstant' in entity)) {
|
|
666
|
+
// Try sort entities by TimeInstant
|
|
667
|
+
payload.entities.sort(
|
|
668
|
+
(a, b) => new Date(a.TimeInstant.value).getTime() - new Date(b.TimeInstant.value).getTime()
|
|
669
|
+
);
|
|
670
|
+
options.json = payload;
|
|
671
|
+
} else {
|
|
672
|
+
// keep current options object created for a batch update
|
|
673
|
+
logger.debug(
|
|
674
|
+
context,
|
|
675
|
+
"some entities lack the 'TimeInstant' key. Sorting is not feasible: %j ",
|
|
676
|
+
payload.entities
|
|
677
|
+
);
|
|
678
|
+
}
|
|
623
679
|
|
|
624
680
|
//Send the NGSI request
|
|
625
681
|
logger.debug(context, 'Updating device value in the Context Broker at: %j', options.url);
|
|
@@ -627,7 +683,7 @@ function sendUpdateValueNgsi2(entityName, measures, typeInformation, token, call
|
|
|
627
683
|
|
|
628
684
|
request(
|
|
629
685
|
options,
|
|
630
|
-
generateNGSI2OperationHandler('update', entityName,
|
|
686
|
+
generateNGSI2OperationHandler('update', entityName, originTypeInformation, token, options, callback)
|
|
631
687
|
);
|
|
632
688
|
} else {
|
|
633
689
|
logger.debug(
|