iotagent-node-lib
Version:
IoT Agent library to interface with NGSI Context Broker
499 lines (448 loc) • 20.3 kB
JavaScript
/*
* Copyright 2015 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, see http://www.gnu.org/licenses/.
*
* For those usages not covered by the GNU Affero General Public License
* please contact with::daniel.moranjimenez@telefonica.com
*
* Modified by: Federico M. Facca - Martel Innovate
* Modified by: Daniel Calvo - ATOS Research & Innovation
*/
/* eslint-disable consistent-return */
const errors = require('../../errors');
const logger = require('logops');
const _ = require('underscore');
const intoTrans = require('../common/domain').intoTrans;
const config = require('../../commonConfig');
const context = {
op: 'IoTAgentNGSI.Registration'
};
const async = require('async');
const utils = require('../northBound/restUtils');
const subscriptionService = require('../ngsi/subscriptionService');
const NGSI_LD_URN = 'urn:ngsi-ld:';
/**
* Generates a handler for the registration requests that checks all the possible errors derived from the registration.
* The parameter information is needed in order to fulfill error information.
*
* @param {Boolen} unregister Indicates whether this registration is an unregistration or register.
* @param {Object} deviceData Object containing all the deviceData needed to send the registration.
* @return {Function} The generated handler.
*/
function createRegistrationHandlerNgsiLD(unregister, deviceData, callback) {
/* eslint-disable-next-line no-unused-vars */
return function handleRegistrationResponse(error, response, body) {
if (error) {
logger.error(context, 'ORION-002: Connection error sending registrations to the Context Broker: %s', error);
callback(error);
} else if (response && response.statusCode === 201 && response.headers.location && unregister === false) {
logger.debug(context, 'Registration success.');
callback(null, {
registrationId: response.headers.location.substr(response.headers.location.lastIndexOf('/') + 1)
});
} else if (response && response.statusCode === 204 && unregister === true) {
logger.debug(context, 'Unregistration success.');
callback(null, null);
} else if (response && response.statusCode && response.statusCode !== 500) {
logger.error(context, 'Registration error connecting to the Context Broker: %j', response.statusCode);
callback(new errors.BadRequest(JSON.stringify(response.statusCode)));
} else {
let errorObj;
logger.error(context, 'ORION-003: Protocol error connecting to the Context Broker: %j', errorObj);
if (unregister) {
errorObj = new errors.UnregistrationError(deviceData.id, deviceData.type);
} else {
errorObj = new errors.RegistrationError(deviceData.id, deviceData.type);
}
callback(errorObj);
}
};
}
/**
* Generates a handler for the registration requests that checks all the possible errors derived from the registration.
* The parameter information is needed in order to fulfill error information.
*
* @param {Boolen} unregister Indicates whether this registration is an unregistration or register.
* @param {Object} deviceData Object containing all the deviceData needed to send the registration.
* @return {Function} The generated handler.
*/
function createRegistrationHandlerNgsi2(unregister, deviceData, callback) {
/* eslint-disable-next-line no-unused-vars */
return function handleRegistrationResponse(error, response, body) {
if (error) {
logger.error(context, 'ORION-002: Connection error sending registrations to the Context Broker: %s', error);
callback(error);
} else if (response && response.statusCode === 201 && response.headers.location && unregister === false) {
logger.debug(context, 'Registration success.');
callback(null, {
registrationId: response.headers.location.substr(response.headers.location.lastIndexOf('/') + 1)
});
} else if (response && response.statusCode === 204 && unregister === true) {
logger.debug(context, 'Unregistration success.');
callback(null, null);
} else if (response && response.statusCode && response.statusCode !== 500) {
logger.error(context, 'Registration error connecting to the Context Broker: %j', response.statusCode);
callback(new errors.BadRequest(JSON.stringify(response.statusCode)));
} else {
let errorObj;
logger.error(context, 'ORION-003: Protocol error connecting to the Context Broker: %j', errorObj);
if (unregister) {
errorObj = new errors.UnregistrationError(deviceData.id, deviceData.type);
} else {
errorObj = new errors.RegistrationError(deviceData.id, deviceData.type);
}
callback(errorObj);
}
};
}
/**
* Sends a Context Provider unregistration request to the Context Broker using NGSIv2.
*
* @param {Object} deviceData Object containing all the deviceData needed to send the registration.
*/
function sendUnregistrationsNgsi2(deviceData, callback) {
let cbHost = config.getConfig().contextBroker.url;
if (deviceData.cbHost && deviceData.cbHost.indexOf('://') !== -1) {
cbHost = deviceData.cbHost;
} else if (deviceData.cbHost && deviceData.cbHost.indexOf('://') === -1) {
cbHost = 'http://' + deviceData.cbHost;
}
const options = {
url: cbHost + '/v2/registrations/' + deviceData.registrationId,
method: 'DELETE',
headers: {
'fiware-service': deviceData.service,
'fiware-servicepath': deviceData.subservice
}
};
if (deviceData.cbHost && deviceData.cbHost.indexOf('://') !== -1) {
options.url = deviceData.cbHost + '/v2/registrations/' + deviceData.registrationId;
} else if (deviceData.cbHost && deviceData.cbHost.indexOf('://') === -1) {
options.url = 'http://' + deviceData.cbHost + '/v2/registrations/' + deviceData.registrationId;
}
if (deviceData.registrationId) {
logger.debug(context, 'Sending v2 device unregistrations to Context Broker at [%s]', options.url);
logger.debug(context, 'Using the following request:\n\n%s\n\n', JSON.stringify(options, null, 4));
return utils.executeWithSecurity(
options,
deviceData,
createRegistrationHandlerNgsi2(true, deviceData, callback)
);
}
logger.debug(context, 'No Context Provider registrations found for unregister');
return callback(null, deviceData);
}
/**
* Sends a Context Provider unregistration request to the Context Broker using NGSI-LD.
*
* @param {Object} deviceData Object containing all the deviceData needed to send the registration.
*/
function sendUnregistrationsNgsiLD(deviceData, callback) {
let cbHost = config.getConfig().contextBroker.url;
if (deviceData.cbHost && deviceData.cbHost.indexOf('://') !== -1) {
cbHost = deviceData.cbHost;
} else if (deviceData.cbHost && deviceData.cbHost.indexOf('://') === -1) {
cbHost = 'http://' + deviceData.cbHost;
}
const options = {
url: cbHost + '/ngsi-ld/v1/csourceRegistrations/' + deviceData.registrationId,
method: 'DELETE',
headers: {
'fiware-service': deviceData.service,
'fiware-servicepath': deviceData.subservice,
'NGSILD-Tenant': deviceData.service,
'NGSILD-Path': deviceData.subservice
}
};
if (deviceData.cbHost && deviceData.cbHost.indexOf('://') !== -1) {
options.url = deviceData.cbHost + '/ngsi-ld/v1/csourceRegistrations/' + deviceData.registrationId;
} else if (deviceData.cbHost && deviceData.cbHost.indexOf('://') === -1) {
options.url = 'http://' + deviceData.cbHost + '/ngsi-ld/v1/csourceRegistrations/' + deviceData.registrationId;
}
if (deviceData.registrationId) {
logger.debug(context, 'Sending device LD unregistrations to Context Broker at [%s]', options.url);
logger.debug(context, 'Using the following request:\n\n%s\n\n', JSON.stringify(options, null, 4));
return utils.executeWithSecurity(
options,
deviceData,
createRegistrationHandlerNgsi2(true, deviceData, callback)
);
}
logger.debug(context, 'No Context Provider registrations found for unregister');
return callback(null, deviceData);
}
function formatAttributes(originalVector) {
const attributeList = [];
if (originalVector && originalVector.length) {
for (let i = 0; i < originalVector.length; i++) {
attributeList.push(originalVector[i].name);
}
}
return attributeList;
}
function mergeWithSameName(old, current) {
if (old.indexOf(current) < 0) {
old.push(current);
}
return old;
}
/**
* Sends a Context Provider registration or unregistration request to the Context Broker using NGSIv2.
*
* @param {Boolen} unregister Indicates whether this registration is an unregistration or register.
* @param {Object} deviceData Object containing all the deviceData needed to send the registration.
*/
function sendRegistrationsNgsi2(unregister, deviceData, callback) {
// FIXME: When https://github.com/telefonicaid/fiware-orion/issues/3007 is merged into master branch,
// this function should use the new API. This is just a temporary solution which implies deleting the
// registration and creating a new one.
function updateRegistrationNgsi2(deviceData, callback) {
const functions = [];
function removeRegistrationId(deviceData, unregistrationResult, callback) {
delete deviceData.registrationId;
return callback(null, deviceData);
}
functions.push(async.apply(sendRegistrationsNgsi2, true, deviceData));
functions.push(async.apply(removeRegistrationId, deviceData));
functions.push(async.apply(sendRegistrationsNgsi2, false));
async.waterfall(functions, callback);
}
if (unregister) {
return sendUnregistrationsNgsi2(deviceData, callback);
}
if (deviceData.registrationId) {
return updateRegistrationNgsi2(deviceData, callback);
}
let cbHost = config.getConfig().contextBroker.url;
if (deviceData.cbHost && deviceData.cbHost.indexOf('://') !== -1) {
cbHost = deviceData.cbHost;
} else if (deviceData.cbHost && deviceData.cbHost.indexOf('://') === -1) {
cbHost = 'http://' + deviceData.cbHost;
}
const options = {
url: cbHost + '/v2/registrations',
method: 'POST',
json: {
dataProvided: {
entities: [
{
type: deviceData.type,
id: String(deviceData.name)
}
],
attrs: []
},
provider: {
http: {
url: config.getConfig().providerUrl
}
}
},
headers: {
'fiware-service': deviceData.service,
'fiware-servicepath': deviceData.subservice
}
};
options.json.dataProvided.attrs = options.json.dataProvided.attrs
.concat(formatAttributes(deviceData.lazy), formatAttributes(deviceData.commands))
.reduce(mergeWithSameName, []);
if (options.json.dataProvided.attrs.length === 0) {
logger.debug(context, 'Registration with Context Provider is not needed. Device without lazy atts or commands');
return callback(null, deviceData);
}
logger.debug(context, 'Sending v2 device registrations to Context Broker at [%s]', options.url);
logger.debug(context, 'Using the following request:\n\n%s\n\n', JSON.stringify(options, null, 4));
utils.executeWithSecurity(options, deviceData, createRegistrationHandlerNgsi2(unregister, deviceData, callback));
}
function sendUnsubscriptionsNgsi2(deviceData, callback) {
if (deviceData.subscriptionId) {
logger.debug(
context,
'Sending v2 device unsubscriptions to Context Broker at [%s]',
config.getConfig().contextBroker.url
);
logger.debug(context, 'Using the following subscriptionId %j', deviceData.subscriptionId);
subscriptionService.unsubscribe(deviceData, deviceData.subscriptionId, callback);
} else {
logger.debug(context, 'No subscription found for unregister');
return callback(null, deviceData);
}
}
function sendSubscriptionsNgsi2(unregister, deviceData, callback) {
function updateSubscriptionNgsi2(deviceData, callback) {
const functions = [];
function removeSubscriptionId(deviceData, unregistrationResult, callback) {
delete deviceData.subscriptionId;
return callback(null, deviceData);
}
functions.push(async.apply(sendSubscriptionsNgsi2, true, deviceData));
functions.push(async.apply(removeSubscriptionId, deviceData));
functions.push(async.apply(sendSubscriptionsNgsi2, false));
async.waterfall(functions, callback);
}
if (unregister) {
return sendUnsubscriptionsNgsi2(deviceData, callback);
}
if (deviceData.subscriptionId) {
return updateSubscriptionNgsi2(deviceData, callback);
}
const attrs = [].concat(formatAttributes(deviceData.commands)).reduce(mergeWithSameName, []);
if (attrs.length === 0) {
logger.debug(context, 'Subscription is not needed. Device without commands');
return callback(null, deviceData);
}
const trigger = attrs; // one subscription for all commands
const content = attrs;
const attrsFormat = 'normalized';
logger.debug(
context,
'Sending v2 device subscriptions to Context Broker at [%s]',
config.getConfig().contextBroker.url
);
logger.debug(context, 'Using the following trigger %j and content %j', trigger, content);
subscriptionService.subscribe(deviceData, trigger, content, attrsFormat, callback);
}
/**
* Sends a Context Provider registration or unregistration request to the Context Broker using NGSI-LD.
*
* @param {Boolen} unregister Indicates whether this registration is an unregistration or register.
* @param {Object} deviceData Object containing all the deviceData needed to send the registration.
*/
function sendRegistrationsNgsiLD(unregister, deviceData, callback) {
if (unregister) {
return sendUnregistrationsNgsiLD(deviceData, callback);
}
const operations = [];
const properties = [];
const lazy = deviceData.lazy || [];
const commands = deviceData.commands || [];
const supportMerge = config.getConfig().server.ldSupport.merge;
lazy.forEach((element) => {
properties.push(element.name);
});
commands.forEach((element) => {
properties.push(element.name);
});
if (lazy.length > 0) {
operations.push('retrieveOps');
}
if (commands.length > 0) {
operations.push('updateOps');
}
if (supportMerge) {
operations.push('mergeEntity');
}
if (properties.length === 0) {
logger.debug(context, 'Registration with Context Provider is not needed. Device without lazy atts or commands');
return callback(null, deviceData);
}
let cbHost = config.getConfig().contextBroker.url;
if (deviceData.cbHost && deviceData.cbHost.indexOf('://') !== -1) {
cbHost = deviceData.cbHost;
} else if (deviceData.cbHost && deviceData.cbHost.indexOf('://') === -1) {
cbHost = 'http://' + deviceData.cbHost;
}
let id = String(deviceData.name);
if (!id.startsWith(NGSI_LD_URN)) {
id = NGSI_LD_URN + deviceData.type + ':' + id;
logger.debug(context, 'Amending id to a valid URN: %s', id);
}
const options = {
url: cbHost + '/ngsi-ld/v1/csourceRegistrations/',
method: 'POST',
json: {
type: 'ContextSourceRegistration',
information: [
{
entities: [{ type: deviceData.type, id }],
propertyNames: properties
}
],
mode: 'exclusive',
operations,
endpoint: config.getConfig().providerUrl,
contextSourceInfo: [
{
key: 'jsonldContext',
value: config.getConfig().contextBroker.jsonLdContext
}
],
'@context': config.getConfig().contextBroker.jsonLdContext
},
headers: {
'fiware-service': deviceData.service,
'fiware-servicepath': deviceData.subservice,
'NGSILD-Tenant': deviceData.service,
'NGSILD-Path': deviceData.subservice,
'Content-Type': 'application/ld+json'
}
};
logger.debug(context, 'Sending LD device registrations to Context Broker at [%s]', options.url);
logger.debug(context, 'Using the following request:\n\n%s\n\n', JSON.stringify(options, null, 4));
utils.executeWithSecurity(options, deviceData, createRegistrationHandlerNgsiLD(unregister, deviceData, callback));
}
/**
* Sends a Context Provider registration or unregistration request to the Context Broker. As registrations cannot be
* removed, an unregistration consists of an update of the existent registration to make reduce its duration to
* 1 second.
*
* The entity type, entity name and lazy attributes list are needed in order to send the registration:
*
* @param {Boolen} unregister Indicates whether this registration is an unregistration or register.
* @param {Object} deviceData Object containing all the deviceData needed to send the registration.
*/
function sendRegistrations(unregister, deviceData, callback) {
switch (config.ngsiVersion()) {
case 'ld':
sendRegistrationsNgsiLD(unregister, deviceData, callback);
break;
default:
if (
(!deviceData.cmdMode && (!config.getConfig().cmdMode || config.getConfig().cmdMode === 'legacy')) ||
(deviceData && deviceData.cmdMode === 'legacy')
) {
sendRegistrationsNgsi2(unregister, deviceData, callback);
} else {
sendSubscriptionsNgsi2(unregister, deviceData, callback);
}
break;
}
}
/**
* Process the response from a Register Context request for a device, extracting the 'registrationId' and creating the
* device object that will be stored in the registry.
*
* @param {Object} deviceData Object containing all the deviceData needed to send the registration.
*
*/
function processContextRegistration(deviceData, body, callback) {
const newDevice = _.clone(deviceData);
if (body) {
if (
(!deviceData.cmdMode && (!config.getConfig().cmdMode || config.getConfig().cmdMode === 'legacy')) ||
(deviceData && deviceData.cmdMode === 'legacy')
) {
newDevice.registrationId = body.registrationId;
} else {
newDevice.subscriptionId = body.subscriptionId;
}
}
callback(null, newDevice);
}
exports.sendRegistrations = intoTrans(context, sendRegistrations);
exports.processContextRegistration = processContextRegistration;