iotagent-node-lib
Version:
IoT Agent library to interface with NGSI Context Broker
922 lines (843 loc) • 30.1 kB
JavaScript
/*
* Copyright 2014 Telefonica Investigación y Desarrollo, S.A.U
*
* This file is part of fiware-iotagent-lib
*
* fiware-iotagent-lib is free software: you can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* fiware-iotagent-lib is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with fiware-iotagent-lib.
* If not, seehttp://www.gnu.org/licenses/.
*
* For those usages not covered by the GNU Affero General Public License
* please contact with::[contacto@tid.es]
*
* Developed by: Federico M. Facca - Martel Innovate
*/
/* jshint camelcase: false */
const iotAgentLib = require('../../../../lib/fiware-iotagent-lib');
const utils = require('../../../tools/utils');
const should = require('should');
const logger = require('logops');
const nock = require('nock');
const timekeeper = require('timekeeper');
let contextBrokerMock;
const iotAgentConfig = {
logLevel: 'FATAL',
contextBroker: {
host: '192.168.1.1',
port: '1026',
ngsiVersion: 'ld',
jsonLdContext: 'http://context.json-ld'
},
server: {
port: 4041
},
types: {
Light: {
commands: [],
type: 'Light',
lazy: [],
active: [
{
object_id: 'p',
name: 'pressure',
type: 'Number'
},
{
object_id: 'e',
name: 'consumption',
type: 'Number'
},
{
object_id: 'a',
name: 'alive',
type: 'None'
},
{
object_id: 'u',
name: 'updated',
type: 'Boolean'
},
{
object_id: 'm',
name: 'manufacturer',
type: 'Object'
},
{
object_id: 'r',
name: 'revisions',
type: 'Array'
},
{
object_id: 'x',
name: 'consumption_x',
type: 'Number',
expression: 'pressure * 20'
}
]
},
LightError: {
commands: [],
type: 'Light',
lazy: [],
active: [
{
object_id: 'p',
name: 'pressure',
type: 'Number',
expression: 'pressure * / 20'
}
]
},
WeatherStation: {
commands: [],
type: 'WeatherStation',
lazy: [],
active: [
{
object_id: 'p',
name: 'pressure',
type: 'Number',
expression: 'pressure * 20'
},
{
object_id: 'e',
name: 'consumption',
type: 'Number',
expression: 'consumption * 20'
},
{
object_id: 'h',
name: 'humidity',
type: 'Percentage'
},
{
name: 'weather',
type: 'Summary',
expression: '"Humidity " + (humidity / 2) + " and pressure " + (pressure * 20)'
},
{
object_id: 'a',
name: 'alive',
type: 'None',
expression: 'alive * 20'
},
{
object_id: 'u',
name: 'updated',
type: 'Boolean',
expression: 'updated * 20'
}
]
},
WeatherStationUndef: {
commands: [],
type: 'WeatherStation',
lazy: [],
active: [
{
object_id: 'u',
name: 'undef',
type: 'json',
expression: 'u["no"]'
},
{
object_id: 'n',
name: 'nil',
type: 'json',
expression: 'u["no"]?u["no"]:null'
},
{
object_id: 'f',
name: 'falsy',
type: 'Boolean',
expression: 'u["no"]?u["no"]:false'
},
{
object_id: 'z',
name: 'zero',
type: 'Number',
expression: 'u["no"]?u["no"]:0'
}
]
},
WeatherStationMultiple: {
commands: [],
type: 'WeatherStation',
lazy: [],
active: [
{
object_id: 'p',
name: 'pressure',
type: 'Number',
expression: 'pressure|trim'
},
{
object_id: 'p25',
name: 'pressure25',
type: 'Number'
},
{
object_id: 'e',
name: 'consumption',
type: 'Number',
expression: 'consumption|trim'
},
{
object_id: 'h',
name: 'humidity12',
type: 'Percentage'
},
{
name: 'weather',
type: 'Summary',
expression: '"Humidity " + (humidity12 / 2) + " and pressure " + (pressure25 * 20)'
},
{
object_id: 'a',
name: 'alive',
type: 'None',
expression: 'alive|trim'
},
{
object_id: 'u',
name: 'updated',
type: 'Boolean',
expression: 'updated|trim'
}
]
},
GPS: {
commands: [],
type: 'GPS',
lazy: [],
active: [
{
name: 'location',
type: 'geo:json',
expression: "{coordinates: [lon,lat], type: 'Point'}"
},
{
name: 'TimeInstant',
type: 'DateTime',
expression: 'ts|toisodate'
}
],
explicitAttrs: true
},
GPS2: {
commands: [],
type: 'GPS',
lazy: [],
active: [
{
name: 'location',
type: 'geo:json',
expression: "{coordinates: [lon,lat], type: 'Point'}"
}
],
explicitAttrs: true
}
},
service: 'smartgondor',
subservice: 'gardens',
providerUrl: 'http://smartgondor.com',
deviceRegistrationDuration: 'P1M'
};
const iotAgentConfigTS = {
logLevel: 'FATAL',
contextBroker: {
host: '192.168.1.1',
port: '1026',
ngsiVersion: 'ld',
jsonLdContext: 'http://context.json-ld'
},
server: {
port: 4041
},
types: {
GPS: {
commands: [],
type: 'GPS',
lazy: [],
active: [
{
name: 'location',
type: 'geo:json',
expression: "{coordinates: [lon,lat], type: 'Point'}"
}
],
explicitAttrs: true
}
},
timestamp: true,
service: 'smartgondor',
subservice: 'gardens',
providerUrl: 'http://smartgondor.com'
};
describe('NGSI-LD: JEXL', function () {
beforeEach(function (done) {
//logger.setLevel('FATAL');
iotAgentLib.activate(iotAgentConfig, function () {
iotAgentLib.clearAll(function () {
done();
});
});
});
afterEach(function (done) {
iotAgentLib.clearAll(function () {
iotAgentLib.deactivate(done);
});
});
describe('When there are expression attributes that are just calculated (not sent by the device)', function () {
// Case: Expression which results is sent as a new attribute
const values = [
{
name: 'p',
type: 'Number',
value: 52
},
{
name: 'h',
type: 'Percentage',
value: '12'
}
];
beforeEach(function () {
nock.cleanAll();
contextBrokerMock = nock('http://192.168.1.1:1026')
.matchHeader('fiware-service', 'smartgondor')
.matchHeader('fiware-servicepath', 'gardens')
.post(
'/ngsi-ld/v1/entityOperations/upsert/?options=update',
utils.readExampleFile(
'./test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin2.json'
)
)
.reply(204);
});
it('should calculate them and add them to the payload', function (done) {
iotAgentLib.update('ws1', 'WeatherStation', '', values, function (error) {
should.not.exist(error);
contextBrokerMock.done();
done();
});
});
});
describe('When an expression with multiple variables with numbers arrive', function () {
// Case: Update for integer and string attributes with expression
const values = [
{
name: 'p25',
type: 'Number',
value: 52
},
{
name: 'h',
type: 'percentage',
value: '12'
}
];
beforeEach(function () {
nock.cleanAll();
contextBrokerMock = nock('http://192.168.1.1:1026')
.matchHeader('fiware-service', 'smartgondor')
.matchHeader('fiware-servicepath', 'gardens')
.post(
'/ngsi-ld/v1/entityOperations/upsert/?options=update',
utils.readExampleFile(
'./test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin4a.json'
)
)
.reply(204);
});
it('should calculate it and add it to the payload', function (done) {
iotAgentLib.update('ws1', 'WeatherStationMultiple', '', values, function (error) {
should.not.exist(error);
contextBrokerMock.done();
done();
});
});
});
describe('When an update comes for attributes without expressions and type integer', function () {
// Case: Update for an integer attribute without expression
const values = [
{
name: 'e',
type: 'Number',
value: 52
}
];
beforeEach(function () {
nock.cleanAll();
contextBrokerMock = nock('http://192.168.1.1:1026')
.matchHeader('fiware-service', 'smartgondor')
.matchHeader('fiware-servicepath', 'gardens')
.post(
'/ngsi-ld/v1/entityOperations/upsert/?options=update',
utils.readExampleFile(
'./test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin11.json'
)
)
.reply(204);
});
it('should apply the expression before sending the values', function (done) {
iotAgentLib.update('light1', 'Light', '', values, function (error) {
should.not.exist(error);
contextBrokerMock.done();
done();
});
});
});
describe('When an update comes for attributes with numeric expressions and type integer', function () {
// Case: Update for an integer attribute with arithmetic expression
const values = [
{
name: 'p',
type: 'Number',
value: 52
}
];
beforeEach(function () {
nock.cleanAll();
contextBrokerMock = nock('http://192.168.1.1:1026')
.matchHeader('fiware-service', 'smartgondor')
.matchHeader('fiware-servicepath', 'gardens')
.post(
'/ngsi-ld/v1/entityOperations/upsert/?options=update',
utils.readExampleFile(
'./test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin1a.json'
)
)
.reply(204);
});
it('should apply the expression before sending the values', function (done) {
iotAgentLib.update('ws1', 'WeatherStation', '', values, function (error) {
should.not.exist(error);
contextBrokerMock.done();
done();
});
});
});
describe('When an update comes for attributes without expressions and type float', function () {
// Case: Update for a Float attribute without expressions
const values = [
{
name: 'e',
type: 'Number',
value: 0.44
}
];
beforeEach(function () {
nock.cleanAll();
contextBrokerMock = nock('http://192.168.1.1:1026')
.matchHeader('fiware-service', 'smartgondor')
.matchHeader('fiware-servicepath', 'gardens')
.post(
'/ngsi-ld/v1/entityOperations/upsert/?options=update',
utils.readExampleFile(
'./test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin3.json'
)
)
.reply(204);
});
it('should apply the expression before sending the values', function (done) {
iotAgentLib.update('light1', 'Light', '', values, function (error) {
should.not.exist(error);
contextBrokerMock.done();
done();
});
});
});
describe('When an update comes for attributes with numeric expressions and type float', function () {
// Case: Update for a Float attribute with arithmetic expression
const values = [
{
name: 'e',
type: 'Number',
value: 0.44
}
];
beforeEach(function () {
nock.cleanAll();
contextBrokerMock = nock('http://192.168.1.1:1026')
.matchHeader('fiware-service', 'smartgondor')
.matchHeader('fiware-servicepath', 'gardens')
.post(
'/ngsi-ld/v1/entityOperations/upsert/?options=update',
utils.readExampleFile(
'./test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin8a.json'
)
)
.reply(204);
});
it('should apply the expression before sending the values', function (done) {
iotAgentLib.update('ws1', 'WeatherStation', '', values, function (error) {
should.not.exist(error);
contextBrokerMock.done();
done();
});
});
});
describe('When an update comes for attributes without expressions and Boolean type', function () {
// Case: Update for a Boolean attribute without expression
const values = [
{
name: 'u',
type: 'Boolean',
value: true
}
];
beforeEach(function () {
nock.cleanAll();
contextBrokerMock = nock('http://192.168.1.1:1026')
.matchHeader('fiware-service', 'smartgondor')
.matchHeader('fiware-servicepath', 'gardens')
.post(
'/ngsi-ld/v1/entityOperations/upsert/?options=update',
utils.readExampleFile(
'./test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin9.json'
)
)
.reply(204);
});
it('should apply the expression before sending the values', function (done) {
iotAgentLib.update('light1', 'Light', '', values, function (error) {
should.not.exist(error);
contextBrokerMock.done();
done();
});
});
});
describe('When an update comes for attributes without expressions and Object type', function () {
// Case: Update for a JSON document attribute without expression
const values = [
{
name: 'm',
type: 'Object',
value: { name: 'Manufacturer1', VAT: 'U12345678' }
}
];
beforeEach(function () {
nock.cleanAll();
contextBrokerMock = nock('http://192.168.1.1:1026')
.matchHeader('fiware-service', 'smartgondor')
.matchHeader('fiware-servicepath', 'gardens')
.post(
'/ngsi-ld/v1/entityOperations/upsert/?options=update',
utils.readExampleFile(
'./test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin6.json'
)
)
.reply(204);
});
it('should apply the expression before sending the values', function (done) {
iotAgentLib.update('light1', 'Light', '', values, function (error) {
should.not.exist(error);
contextBrokerMock.done();
done();
});
});
});
describe('When an update comes for attributes without expressions and Object type', function () {
// Case: Update for a JSON array attribute without expression
const values = [
{
name: 'r',
type: 'Object',
value: ['v0.1', 'v0.2', 'v0.3']
}
];
beforeEach(function () {
nock.cleanAll();
contextBrokerMock = nock('http://192.168.1.1:1026')
.matchHeader('fiware-service', 'smartgondor')
.matchHeader('fiware-servicepath', 'gardens')
.post(
'/ngsi-ld/v1/entityOperations/upsert/?options=update',
utils.readExampleFile(
'./test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin7.json'
)
)
.reply(204);
});
it('should apply the expression before sending the values', function (done) {
iotAgentLib.update('light1', 'Light', '', values, function (error) {
should.not.exist(error);
contextBrokerMock.done();
done();
});
});
});
describe('When there are expressions including other attributes and they are updated', function () {
const values = [
{
name: 'p',
type: 'Number',
value: 10
}
];
beforeEach(function () {
nock.cleanAll();
contextBrokerMock = nock('http://192.168.1.1:1026')
.matchHeader('fiware-service', 'smartgondor')
.matchHeader('fiware-servicepath', 'gardens')
.post(
'/ngsi-ld/v1/entityOperations/upsert/?options=update',
utils.readExampleFile(
'./test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin13.json'
)
)
.reply(204);
});
it('should apply the expression before sending the values', function (done) {
iotAgentLib.update('light1', 'Light', '', values, function (error) {
should.not.exist(error);
contextBrokerMock.done();
done();
});
});
});
describe('When there are expressions including other attributes and they are updated (overriding situation)', function () {
const values = [
{
name: 'x',
type: 'Number',
value: 0.44
},
{
name: 'p',
type: 'Number',
value: 10
}
];
beforeEach(function () {
nock.cleanAll();
contextBrokerMock = nock('http://192.168.1.1:1026')
.matchHeader('fiware-service', 'smartgondor')
.matchHeader('fiware-servicepath', 'gardens')
.post(
'/ngsi-ld/v1/entityOperations/upsert/?options=update',
utils.readExampleFile(
'./test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin13.json'
)
)
.reply(204);
});
it('should apply the expression before sending the values', function (done) {
iotAgentLib.update('light1', 'Light', '', values, function (error) {
should.not.exist(error);
contextBrokerMock.done();
done();
});
});
});
describe('When a measure arrives and there is not enough information to calculate an expression', function () {
const values = [
{
name: 'p',
type: 'centigrades',
value: '52'
}
];
beforeEach(function () {
nock.cleanAll();
contextBrokerMock = nock('http://192.168.1.1:1026')
.matchHeader('fiware-service', 'smartgondor')
.matchHeader('fiware-servicepath', 'gardens')
.post(
'/ngsi-ld/v1/entityOperations/upsert/?options=update',
utils.readExampleFile(
'./test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin29.json'
)
)
.reply(204);
});
it('should not calculate the expression', function (done) {
iotAgentLib.update('ws1', 'WeatherStation', '', values, function (error) {
should.not.exist(error);
contextBrokerMock.done();
done();
});
});
});
describe('When a measure arrives and there is not enough information to calculate an expression', function () {
const values = [
{
name: 'u',
type: 'json',
value: '{}'
}
];
beforeEach(function () {
nock.cleanAll();
contextBrokerMock = nock('http://192.168.1.1:1026')
.matchHeader('fiware-service', 'smartgondor')
.matchHeader('fiware-servicepath', 'gardens')
.post(
'/ngsi-ld/v1/entityOperations/upsert/?options=update',
utils.readExampleFile(
'./test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin31.json'
)
)
.reply(204);
});
it('should not calculate the expression and allow falsy values', function (done) {
iotAgentLib.update('ws1', 'WeatherStationUndef', '', values, function (error) {
should.not.exist(error);
contextBrokerMock.done();
done();
});
});
});
describe('When there are additional attributes sent by the device to be calculated and removed', function () {
// Case: Expression which results is sent as a new attribute
const values = [
{
name: 'lat',
type: 'Number',
value: 52
},
{
name: 'lon',
type: 'Number',
value: 13
},
{
name: 'ts',
type: 'Number',
value: 1
}
];
beforeEach(function () {
nock.cleanAll();
contextBrokerMock = nock('http://192.168.1.1:1026')
.matchHeader('fiware-service', 'smartgondor')
.matchHeader('fiware-servicepath', 'gardens')
.post(
'/ngsi-ld/v1/entityOperations/upsert/?options=update',
utils.readExampleFile(
'./test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin32.json'
)
)
.reply(204);
});
it('should calculate them and remove non-explicitAttrs from the payload', function (done) {
iotAgentLib.update('gps1', 'GPS', '', values, function (error) {
should.not.exist(error);
contextBrokerMock.done();
done();
});
});
});
describe('When there is an extra TimeInstant sent by the device to be removed', function () {
// Case: Expression which results is sent as a new attribute
const values = [
{
name: 'lat',
type: 'Number',
value: 52
},
{
name: 'lon',
type: 'Number',
value: 13
},
{
name: 'TimeInstant',
type: 'DateTime',
value: '2015-08-05T07:35:01.468+00:00'
}
];
beforeEach(function () {
nock.cleanAll();
contextBrokerMock = nock('http://192.168.1.1:1026')
.matchHeader('fiware-service', 'smartgondor')
.matchHeader('fiware-servicepath', 'gardens')
.post(
'/ngsi-ld/v1/entityOperations/upsert/?options=update',
utils.readExampleFile(
'./test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin34.json'
)
)
.reply(204);
});
it('should calculate them and remove non-explicitAttrs from the payload', function (done) {
iotAgentLib.update('gps1', 'GPS2', '', values, function (error) {
should.not.exist(error);
contextBrokerMock.done();
done();
});
});
});
});
describe('NGSI-LD: JEXL - Timestamps', function () {
beforeEach(function (done) {
logger.setLevel('FATAL');
iotAgentLib.activate(iotAgentConfigTS, function () {
iotAgentLib.clearAll(function () {
done();
});
});
});
afterEach(function (done) {
iotAgentLib.clearAll(function () {
iotAgentLib.deactivate(done);
});
});
describe('When timestamps are added but are not explicitly defined', function () {
// Case: Expression which results is sent as a new attribute
const values = [
{
name: 'lat',
type: 'Number',
value: 52
},
{
name: 'lon',
type: 'Number',
value: 13
},
{
name: 'ts',
type: 'Number',
value: 1
}
];
beforeEach(function () {
const time = new Date(1438760101468); // 2015-08-05T07:35:01.468+00:00
timekeeper.freeze(time);
nock.cleanAll();
contextBrokerMock = nock('http://192.168.1.1:1026')
.matchHeader('fiware-service', 'smartgondor')
.matchHeader('fiware-servicepath', 'gardens')
.post(
'/ngsi-ld/v1/entityOperations/upsert/?options=update',
utils.readExampleFile(
'./test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin33.json'
)
)
.reply(204);
});
afterEach(function (done) {
timekeeper.reset();
done();
});
it('should calculate them and not remove the timestamp from the payload', function (done) {
iotAgentLib.update('gps1', 'GPS', '', values, function (error) {
should.not.exist(error);
contextBrokerMock.done();
done();
});
});
});
});