node-red-contrib-snap4city-user
Version:
Nodes for Snap4city project, targeted to standard user (no developer)
1,443 lines (1,132 loc) • 64.3 kB
JavaScript
/* NODE-RED-CONTRIB-SNAP4CITY-USER
Copyright (C) 2018 DISIT Lab http://www.disit.org - University of Florence
This program 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.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>. */
var s4cUtility = require("./snap4city-utility.js");
var s4cOrionUtility = require("./snap4city-orion-utility.js");
var bodyParser = require("body-parser");
var getBody = require('raw-body');
var typer = require('media-typer');
var isUtf8 = require('is-utf8');
var http = require("follow-redirects").http;
var https = require("follow-redirects").https;
var urllib = require("url");
var https2 = require('https');
var when = require('when');
var fs = require('fs');
var path = require('path')
var os = require('os')
module.exports = function (RED) {
"use strict";
var subscriptionIDs = new s4cOrionUtility.SubscriptionStore(RED);
var nodeStatus = new s4cOrionUtility.NodeStatus();
var orionHttpRequestOptions = new s4cOrionUtility.OrionHttpRequestOptions();
var jsonParser = bodyParser.json();
var urlencParser = bodyParser.urlencoded({ extended: true });
var token = "";
var LIMIT = 30;
/*
*This node is not shown by NodeRed but is used by any other node
*to make the various http requests (query, subscribe, etc.).
*Configuration nodes doc: https://nodered.org/docs/creating-nodes/config-nodes
*/
RED.nodes.registerType("orion-service-api-v2", OrionServiceV2, {
credentials: {
user: { type: "text" },
password: { type: "password" }
}
});
function OrionServiceV2(config) {
RED.nodes.createNode(this, config);
var serviceNode = this;
this.authentication = config.authentication;
this.url = config.url;
this.port = config.port;
var orionUrl = getOrionUrl(config);
var credentials = { user: '', password: '' };
if (/\/$/.test(this.url)) {
this.url = this.url.substring(0, this.url.length - 1);
}
if (!this.port) {
throw "Missing port";
}
this.init = function (node) {
nodeStatus.initializing(node);
node.selectedContextbroker = config.selectedContextbroker;
return when.promise(function (resolve) {
// get token from context broker
getToken(node, serviceNode.url, credentials).then(function () {
resolve();
});
});
};
this.queryContext = function (node, config, queryParams) {
const logger = s4cUtility.getLogger(RED, node);
const uid = s4cUtility.retrieveAppID(RED);
var orionBrokerService = RED.nodes.getNode(config.service);
var accessToken = s4cUtility.retrieveAccessToken(RED, node, orionBrokerService.authentication, uid);
nodeStatus.querying(node)
logger.debug(`Querying entity: ${config.enid}`);
return when.promise(function (resolve, reject) {
s4cOrionUtility.getContextBrokerListForRegisterActivity(RED, node, orionBrokerService.url, orionBrokerService.port, retrieveDeviceName(node, config.enid, config.tenant, config.servicepath), uid, accessToken);
var [hostname, prefixPath] = s4cOrionUtility.splitUrlInHostnameAndPrefixPath(orionBrokerService.url);
var options = orionHttpRequestOptions.generateForOrionAPIV2Query(hostname, orionBrokerService.port, prefixPath, config, queryParams, accessToken)
options = orionHttpRequestOptions.setHeaderAuthTenantAndTls(options, config, RED)
var msg = {};
var req = https2.request(options, function (res) {
(node.ret === "bin") ? res.setEncoding('binary') : res.setEncoding('utf8');
msg.statusCode = res.statusCode;
msg.headers = res.headers;
msg.payload = "";
res.on('data', function (chunk) {
msg.payload += chunk;
});
res.on('end', function () {
if (res.statusCode === 200) {
resolve(msg);
} else {
reject(msg);
}
});
});
req.on('error', function (err) {
reject(err);
});
req.write(JSON.stringify(queryParams));
req.end();
});
};
this.subscribeExact = function (node, config, payload) {
const logger = s4cUtility.getLogger(RED, node);
function safeStringify(obj) {
try {
return JSON.stringify(obj, null, 2);
} catch (e) {
return "[UNSERIALIZABLE: " + e + "]";
}
}
function formatError(err) {
if (err instanceof Error) {
return err.stack || err.message || String(err);
}
try {
return JSON.stringify(err, null, 2);
} catch (e) {
return String(err);
}
}
try {
const uid = s4cUtility.retrieveAppID(RED);
var orionBrokerService = RED.nodes.getNode(config.service);
if (!orionBrokerService) {
throw new Error("orionBrokerService not found for config.service=" + config.service);
}
var accessToken = s4cUtility.retrieveAccessToken(
RED,
node,
orionBrokerService.authentication,
uid,
false
);
var reference = payload && payload.notification && payload.notification.http
? payload.notification.http.url
: "";
nodeStatus.subscribing(node);
var registerDeviceName = retrieveDeviceName(
node,
"pattern_exact_subscription",
config.tenant,
config.servicepath
);
s4cOrionUtility.getContextBrokerListForRegisterActivity(
RED,
node,
orionBrokerService.url,
orionBrokerService.port,
registerDeviceName,
uid,
accessToken
);
var split = s4cOrionUtility.splitUrlInHostnameAndPrefixPath(orionBrokerService.url);
var hostname = split[0];
var prefixPath = split[1];
var serializedPayload = JSON.stringify(payload);
var options = orionHttpRequestOptions.generateForOrionAPIV2SubscribeExact(
hostname,
orionBrokerService.port,
prefixPath,
config,
Buffer.byteLength(JSON.stringify(payload)),
accessToken
);
options = orionHttpRequestOptions.setHeaderAuthTenantAndTls(options, config, RED);
var msg = {};
var req = https2.request(options, function (res) {
if (node.ret === "bin") {
res.setEncoding("binary");
} else {
res.setEncoding("utf8");
}
msg.statusCode = res.statusCode;
msg.headers = res.headers;
msg.payload = "";
res.on("data", function (chunk) {
msg.payload += chunk;
});
res.on("end", function () {
logger.info("subscribeExact result:" + msg.statusCode + " " + msg.payload);
if (res.statusCode === 201) {
if (msg.headers.location != null) {
var subscriptionID = msg.headers.location.replace("/v2/subscriptions/", "");
var nodeID = (node.id + "").replace(".", "");
listenOnUrl(nodeID, function (req, res) {
var currentSubscriptionId = subscriptionIDs.getSubscriptionOfNode(nodeID);
if (req.body.subscriptionId != currentSubscriptionId) {
logger.warn(
"Ignoring notification for subscription " +
req.body.subscriptionId +
" (current valid: " +
currentSubscriptionId +
")"
);
} else {
node.send({
payload: req.body.data,
statusCode: 201
});
}
res.sendStatus(200);
});
var idToUnsubscribe = subscriptionIDs.getSubscriptionOfNode(nodeID);
subscriptionIDs.setSubscriptionOnNode(subscriptionID, nodeID);
if (idToUnsubscribe != undefined) {
logger.info("unsubscribe old exact subscription:" + idToUnsubscribe);
setTimeout(function () {
unsubscribeFromOrion(node, idToUnsubscribe, null, config);
}, 2000);
}
} else {
logger.error("subscribeExact error: missing Location header " + JSON.stringify(msg));
nodeStatus.getError(node, msg);
}
} else {
logger.error("88888subscribeExact error:" + JSON.stringify(msg));
nodeStatus.getError(node, msg);
}
});
});
req.on("socket", function (socket) {
socket.on("connect", function () {
});
socket.on("secureConnect", function () {
});
socket.on("timeout", function () {
});
socket.on("error", function (err) {
});
socket.on("close", function (hadError) {
});
});
req.on("finish", function () {
});
req.on("close", function () {
});
req.on("error", function (err) {
logger.error("subscribeExact error:" + formatError(err));
nodeStatus.getError(node, err);
});
nodeStatus.listening(node, reference);
if (payload) {
req.write(serializedPayload);
}
req.end();
} catch (err) {
logger.error("subscribeExact error:" + formatError(err));
nodeStatus.getError(node, err);
}
};
this.updateContext = function (node, config, payload, auth) {
const logger = s4cUtility.getLogger(RED, node);
const uid = s4cUtility.retrieveAppID(RED);
var orionBrokerService = RED.nodes.getNode(config.service);
var accessToken = s4cUtility.retrieveAccessToken(RED, node, orionBrokerService.authentication, uid);
nodeStatus.sending(node)
logger.debug(`Updating entity: ${config.enid} with payload: ${JSON.stringify(payload)}`);
return when.promise(function (resolve, reject) {
s4cOrionUtility.getContextBrokerListForRegisterActivity(RED, node, orionBrokerService.url, orionBrokerService.port, retrieveDeviceName(node, config.enid, config.tenant, config.servicepath), uid, accessToken);
var [hostname, prefixPath] = s4cOrionUtility.splitUrlInHostnameAndPrefixPath(orionBrokerService.url);
var options = orionHttpRequestOptions.generateForOrionAPIV2Update(hostname, orionBrokerService.port, prefixPath, config, auth, Buffer.byteLength(JSON.stringify(payload)), accessToken)
options = orionHttpRequestOptions.setHeaderAuthTenantAndTls(options, config, RED, auth)
var msg = {};
var req = https2.request(options, function (res) {
msg.statusCode = res.statusCode;
msg.headers = res.headers;
msg.payload = "";
res.on('data', function (chunk) {
msg.payload += chunk;
});
res.on('end', function () {
if (res.statusCode === 204) {
resolve(msg);
} else {
reject(msg);
}
});
});
req.on('error', function (e) {
reject(e);
});
req.write(JSON.stringify(payload));
req.end();
});
};
this.subscribe = function (node, config, payload) {
const logger = s4cUtility.getLogger(RED, node);
const uid = s4cUtility.retrieveAppID(RED);
var orionBrokerService = RED.nodes.getNode(config.service);
var accessToken = s4cUtility.retrieveAccessToken(RED, node, orionBrokerService.authentication, uid, false);
var reference = payload.notification.http.url;
nodeStatus.subscribing(node)
logger.debug(`Subscribing entity: ${config.enid} with payload: ${JSON.stringify(payload)}`);
var correctPath =os.homedir() + "\\.snap4cityConfig\\";//consider edge scenario
if (RED.settings.APPID != null) {//check edge scenario or cloud scenario
correctPath = "/data/.snap4cityConfig/";//consider cloud scenario
}
var jsonFilePath=correctPath+node.id+".json";
if(!node.enid&&fs.existsSync(jsonFilePath)){
//if(fs.existsSync(jsonFilePath)){
//if(!node.enid){
var subscriptionJson = JSON.parse(fs.readFileSync(jsonFilePath));
config.enid=subscriptionJson[subscriptionJson.length-1]['id'];
}
s4cOrionUtility.getContextBrokerListForRegisterActivity(RED, node, orionBrokerService.url, orionBrokerService.port, retrieveDeviceName(node, config.enid, config.tenant, config.servicepath), uid, accessToken);
var [hostname, prefixPath] = s4cOrionUtility.splitUrlInHostnameAndPrefixPath(orionBrokerService.url);
var options = orionHttpRequestOptions.generateForOrionAPIV2Subscribe(hostname, orionBrokerService.port, prefixPath, config, JSON.stringify(payload).length, accessToken)
options = orionHttpRequestOptions.setHeaderAuthTenantAndTls(options, config, RED)
logger.debug("subscribeContext options:" + JSON.stringify(options));
try {
var msg = {};
var req = https2.request(options, function (res) {
if (node.ret === "bin") res.setEncoding('binary');
else res.setEncoding('utf8');
msg.statusCode = res.statusCode;
msg.headers = res.headers;
msg.payload = "";
res.on('data', function (chunk) {
msg.payload += chunk;
});
res.on('end', function () {
logger.info("subscribeContext result:" + msg.statusCode + " " + msg.payload);
if (res.statusCode === 201) {
if (msg.headers.location != null) {//TODO verify this
var subscriptionID = msg.headers.location.replace('/v2/subscriptions/', '');
var nodeID = (node.id + "").replace('.', '');
//listen subscription just if the return code is 200ok
listenOnUrl(nodeID, function (req, res) {
if (req.body.subscriptionId != subscriptionIDs.getSubscriptionOfNode(nodeID)) {
logger.warn(
"Ignoring notification for subscription " + req.body.subscriptionId +
" (current valid: " + currentId + ")"
);//unsubscribeFromOrion(node, req.body.subscriptionId, orionUrl, config);
} else {
//var payload = formatOutput(node, config, req.body.data);//TODO *1 verify formatOutput
node.send({
payload: req.body.data,//TODO *1
statusCode: 201
});
}
res.sendStatus(200);
});
logger.info("subscribeContext elementId: " + config.enid + " nodeId: " + nodeID + " oldSubId: " + subscriptionIDs.getSubscriptionOfNode(nodeID) + " newSubId: " + subscriptionID);
var idToUnsubscribe = subscriptionIDs.getSubscriptionOfNode(nodeID);//save previous sub for unsub
subscriptionIDs.setSubscriptionOnNode(subscriptionID, nodeID);//update new subs
if (idToUnsubscribe != undefined) {//if there was a previous sub
logger.info("unsubscription:" + idToUnsubscribe);
setTimeout(function () {
unsubscribeFromOrion(node, idToUnsubscribe, orionUrl, config);//unsub previous sub
}, 2000);
}
} else if (parsedResponse.result == false) {
logger.error("subscribeContext error:" + JSON.stringify(msg));
nodeStatus.getError(node, msg)
}
} else {
logger.error("subscribeContext error:" + JSON.stringify(msg));
var deviceType = (msg.payload.deviceType ? msg.payload.deviceType : config.entype);
var deviceId = (msg.payload.deviceId ? msg.payload.deviceId : config.enid);
var correctPath =os.homedir() + "\\.snap4cityConfig\\";//consider edge scenario
if (RED.settings.APPID != null) {//check edge scenario or cloud scenario
correctPath = "/data/.snap4cityConfig/";//consider cloud scenario
}
var nodeID = (node.id + "").replace('.', '');
var jsonFilePath=correctPath+nodeID+".json";
if(fs.existsSync(jsonFilePath)){
var subscriptionJson = JSON.parse(fs.readFileSync(jsonFilePath));
var ind=-1
for(var i = 0; i < subscriptionJson.length; i++){
if(subscriptionJson[i].id === deviceId && subscriptionJson[i].type === deviceType){
ind=i;
break;
}
}
if(ind!=-1){
subscriptionJson.splice(ind, 1);
}
fs.writeFileSync(jsonFilePath, JSON.stringify(subscriptionJson))
}
nodeStatus.getError(node, msg)
}
});
});
req.on('error', function (err) {
logger.error("subscribeContext error:" + err);
nodeStatus.getError(node, err)
});
nodeStatus.listening(node, reference)
if (payload) {
logger.debug("subscribeContext payload:" + JSON.stringify(payload));
req.write(JSON.stringify(payload));
}
req.end();
} catch (err) {
logger.error("subscribeContext error:" + err);
nodeStatus.getError(node, err)
}
};
}
function unsubscribeFromOrion(node, subscriptionId, url, config) {
const logger = s4cUtility.getLogger(RED, node);
const uid = s4cUtility.retrieveAppID(RED);
var orionBrokerService = RED.nodes.getNode(config.service);
var accessToken = s4cUtility.retrieveAccessToken(RED, node, orionBrokerService.authentication, uid);
return when.promise(function (resolve, reject) {
var [hostname, prefixPath] = s4cOrionUtility.splitUrlInHostnameAndPrefixPath(orionBrokerService.url);
var options = orionHttpRequestOptions.generateForOrionAPIV2Unsubscribe(hostname, orionBrokerService.port, prefixPath, config, subscriptionId, accessToken)
options = orionHttpRequestOptions.setHeaderAuthTenantAndTls(options, config, RED)
var req = https2.request(options, function (res) {
(node.ret === "bin") ? res.setEncoding('binary') : res.setEncoding('utf8');
res.on('end', function () {
resolve(res);
});
});
req.on('error', function (err) {
logger.error("Error in unsubscribe: "+err);
console.log(err)
reject(err);
});
req.write(JSON.stringify(
{
"statusCode": {
"code": "200",
"reasonPhrase": "OK"
},
"subscriptionId": subscriptionId
}
));
req.end();
});
}
// retrieve token from context broker
function getToken(node, orionUrl, credentials) {
const logger = s4cUtility.getLogger(RED, node);
var tokenUrl = orionUrl + "/token";
if (tokenUrl.indexOf("http://") >= 0) {
tokenUrl = "https://" + tokenUrl.substring(7);
} else if (orionUrl.indexOf("https://") < 0) {
tokenUrl = "https://" + tokenUrl;
}
var opts = urllib.parse(tokenUrl);
opts.method = "POST";
opts.headers = {};
opts.headers['content-type'] = "application/json";
opts.headers["Accept"] = "application/json";
var payload = {
"username": credentials.user,
"password": credentials.password
};
payload = JSON.stringify(payload);
opts.headers['content-length'] = Buffer.byteLength(payload);
token = "";
return when.promise(function (resolve, reject) {
if (!credentials.user) {
resolve();
} else {
logger.debug("--requesting token using payload: " + payload);
var req = (https).request(opts, function (res) {
(node.ret === "bin") ? res.setEncoding('binary') : res.setEncoding('utf8');
res.on('data', function (chunk) {
token += chunk;
});
res.on('end', function () {
logger.debug("--resolved token: " + token);
resolve(token);
});
});
req.on('error', function (err) {
reject(err);
node.status({
fill: "red",
shape: "ring",
text: err.code
});
node.send({
payload: err.toString() + " : " + tokenUrl,
statusCode: err.code
});
});
req.write(payload);
req.end();
}
});
}
function formatOutput(node, n, msg) {
var contextResponses = msg.contextResponses;
var payload = [];
if (typeof contextResponses != "undefined") {
contextResponses.forEach(function (entry) {
var contextElement = entry.contextElement;
delete contextElement.isPattern;
if (!n.includeattr) {
// removing attribute metadata
contextElement.attributes.forEach(function (entry) {
delete entry.metadatas;
});
}
payload.push(contextElement);
});
}
return payload;
}
function rawBodyParser(req, res, next) {
if (req._body) {
return next();
}
req.body = "";
req._body = true;
var isText = true;
var checkUTF = false;
if (req.headers['content-type']) {
var parsedType = typer.parse(req.headers['content-type'])
if (parsedType.type === "text") {
isText = true;
} else if (parsedType.subtype === "xml" || parsedType.suffix === "xml") {
isText = true;
} else if (parsedType.type !== "application") {
isText = false;
} else if (parsedType.subtype !== "octet-stream") {
checkUTF = true;
}
}
getBody(req, {
length: req.headers['content-length'],
encoding: isText ? "utf8" : null
}, function (err, buf) {
if (err) {
return next(err);
}
if (!isText && checkUTF && isUtf8(buf)) {
buf = buf.toString()
}
req.body = buf;
next();
});
}
function validateInput(node, n) {
var err = null;
n.url = n.url;
n.port = n.port;
n.enid = n.enid;
n.entype = n.entype;
n.ispattern = n.ispattern;
n.userk1 = n.userk1;
n.passk2 = n.passk2;
n.tenant = n.tenant;
n.servicepath = n.servicepath;
n.apikey = n.apikey;
n.basicAuth = n.basicAuth;
n.attributes = n.attributes;
n.condvalues = n.condvalues;
n.includeattr = n.includeattr;
n.port = n.port * 1;
/*if (!n.enid || !n.entype) {
err = "Missing subscription parameters";
}*/
if (err) {
throw err;
}
n.attributes = n.attributes || [];
if (n.attributes.constructor !== Array) {
n.attributes = (n.attributes || "").split(",");
for (var i = 0; i < n.attributes.length; i++) {
n.attributes[i] = n.attributes[i].trim();
}
}
n.condvalues = n.condvalues || [];
if (n.condvalues.constructor !== Array) {
n.condvalues = (n.condvalues || "").split(",");
for (var i = 0; i < n.condvalues.length; i++) {
n.condvalues[i] = n.condvalues[i].trim();
}
}
}
function generateSubscribePayload(node, n) {
// prepare payload for context subscription
// contains node uid and url besides data supplied in node fields
var nodeID = node.id + "";
nodeID = nodeID.replace('.', '');
return when.promise(
function (resolve, reject) {
getMyUri(n).then(function (myUri) {
var correctPath =os.homedir() + "\\.snap4cityConfig\\";//consider edge scenario
if (RED.settings.APPID != null) {//check edge scenario or cloud scenario
correctPath = "/data/.snap4cityConfig/";//consider cloud scenario
}
var jsonFilePath=correctPath+n.id+".json";
if (fs.existsSync(jsonFilePath)) {
var subscriptionJson = JSON.parse(fs.readFileSync(jsonFilePath));
var entities=[]
for (var i = 0; i < subscriptionJson.length; i++) {
entities.push({"id": subscriptionJson[i]['id'],"isPattern": n.ispattern,"type": subscriptionJson[i]['type']})
}
var devid = subscriptionJson[0]['id'];
//if(!n.enid){
// var devid=subscriptionJson[subscriptionJson.length-1]['id'];
//}else{
// var devid=n.enid;
//}
}else{
var entities=[]
entities.push({"id": n.enid,"isPattern": n.ispattern,"type": n.entype})
fs.writeFileSync(jsonFilePath, JSON.stringify(entities));
var devid=n.enid
}
var subject= {"condition": {
"attrs": n.condvals ? [n.condvals] : []
}
};
///////////////////
///////////////////
///////////////////
var orionUrl = node.brokerConn.url;
var prot='http';
if (orionUrl.indexOf("https://") >= 0) {
prot='https';
}
///////////////////
///////////////////
//////////////////
subject["entities"]=entities;
var sub={
"description": `A subscription to get info about ${devid}`,
"notification": {
"http": {
"url": `${prot}://${myUri}/${nodeID}`
},
"attrs": n.attributes
}
};
sub["subject"]=subject;
var expires = new Date();
expires.setDate(expires.getDate()+Number(n.duration));
sub["expires"]= expires.toISOString();
if (n.throttle!=0)
sub["throttling"]=Number(n.throttle);
resolve(sub);
});
}
);
}
function getOrionUrl(n) {
var orionUrl = n.url;
if (!/^((http|https):\/\/)/.test(orionUrl)) {
orionUrl = "https://" + orionUrl + ":" + n.port;
}
return orionUrl;
}
//OrionSubscribeV2 node constructor
RED.nodes.registerType("orion-subscribe-api-v2", OrionSubscribeV2);
function OrionSubscribeV2(n) {
RED.nodes.createNode(this, n);
if (!global.nodeInitLock) global.nodeInitLock = {};
if (global.nodeInitLock[n.id]) {
return;
}
global.nodeInitLock[n.id] = true;
var node = this;
// =============================
// REGISTRAZIONE LISTENER HTTP
// (necessario per rendere il deploy uguale al riavvio)
// =============================
function registerListener() {
const path = "/" + node.id;
RED.httpNode.post(path, function(req, res) {
const body = req.body || {};
let msg = {};
msg.subscriptionId = body.subscriptionId || null;
// Se c’è data[], prendiamo il primo elemento
if (body.data && Array.isArray(body.data) && body.data.length > 0) {
msg.payload = body.data;
} else {
msg.payload = body; // fallback
}
node.send(msg);
res.sendStatus(200);
});
node.httpListener = true;
}
// CREA IL LISTENER SUBITO (deploy + riavvio)
registerListener();
var node = this;
this.service = n.service;
this.brokerConn = RED.nodes.getNode(this.service);
this.noderedhost = n.noderedhost;
this.userk1 = n.userk1;
this.passk2 = n.passk2;
this.apikey = n.apikey;
this.basicAuth = n.basicAuth;
this.on('close', function(removed, done) {
global.nodeInitLock[n.id] = false;
try {
// 1. Rimuovi l’HTTP endpoint listener
if (node.httpListener) {
RED.httpNode._router.stack = RED.httpNode._router.stack.filter(function(r){
return !(r.route && r.route.path === "/" + node.id);
});
node.httpListener = null;
}
// 2. Rimuovi eventuali timeout, interval, pending requests
if (node.pendingTimer) clearTimeout(node.pendingTimer);
// 3. Pulisci cache e contesto
node.context().set('currentSubscriptionId', null);
} catch(e) {
node.error("Error closing node: " + e)
}
delete global.nodeInitLock[n.id];
done();
});
// validate mandatory fields
validateInput(this, n);
this.on('input', function (msg) {
var action = msg.payload.action;
var deviceType = (msg.payload.deviceType ? msg.payload.deviceType : n.entype);
var deviceId = (msg.payload.deviceId ? msg.payload.deviceId : n.enid);
var isPattern = (!(typeof msg.payload.isPattern === "undefined") ? msg.payload.isPattern :n.ispattern);
var correctPath =os.homedir() + "\\.snap4cityConfig\\";//consider edge scenario
if (RED.settings.APPID != null) {//check edge scenario or cloud scenario
correctPath = "/data/.snap4cityConfig/";//consider cloud scenario
}
var jsonFilePath=correctPath+n.id+".json";
if (!fs.existsSync(correctPath)) {
fs.mkdirSync(correctPath);//not exist folder, create folder + empty file
fs.writeFileSync(jsonFilePath, JSON.stringify(msg.payload));
} else if (!fs.existsSync(jsonFilePath)){
//this.subscriptionJson = JSON.parse(fs.readFileSync(jsonFilePath));
fs.writeFileSync(jsonFilePath, '[]',{ flag: 'w' });//not exist file, create empy file
} else //folder and file exist, read from it
this.subscriptionJson = JSON.parse(fs.readFileSync(jsonFilePath));
var subscriptionJson = JSON.parse(fs.readFileSync(jsonFilePath));
if(action==="add"){
var ind=true;
for(var i = 0; i < subscriptionJson.length; i++){
if(subscriptionJson[i].id === deviceId && subscriptionJson[i].type === deviceType){
ind=false;
break;
}
}
if(ind){
subscriptionJson.push({"id": deviceId,"isPattern": n.ispattern,"type": deviceType});
fs.writeFileSync(jsonFilePath, JSON.stringify(subscriptionJson));
}
}else if(action==="remove"){
var ind=-1
for(var i = 0; i < subscriptionJson.length; i++){
if(subscriptionJson[i].id === deviceId && subscriptionJson[i].type === deviceType){
ind=i;
break;
}
}
if(ind!=-1){
subscriptionJson.splice(ind, 1);
}
fs.writeFileSync(jsonFilePath, JSON.stringify(subscriptionJson))
}else{
msg.payload ="possible actions are add or remove"
}
generateSubscribePayload(node, n).then(function (payload) {
node.brokerConn.subscribe(node, n, payload);
});
msg.info=msg.payload;
node.send(msg);
});
node.brokerConn.init(node, n).then(function () {
var correctPath =os.homedir() + "\\.snap4cityConfig\\";//consider edge scenario
if (RED.settings.APPID != null) {//check edge scenario or cloud scenario
correctPath = "/data/.snap4cityConfig/";//consider cloud scenario
}
var jsonFilePath=correctPath+n.id+".json";
if (fs.existsSync(jsonFilePath)){
generateSubscribePayload(node, n).then(function (payload) {
node.brokerConn.subscribe(node, n, payload);
});
}else{
generateSubscribePayload(node, n).then(function (payload) {
node.brokerConn.subscribe(node, n, payload);
});
}
});
}
//OrionSubscribeV2 node constructor
RED.nodes.registerType("orion-subscribe-list-update-api-v2", OrionSubscribeListV2);
function OrionSubscribeListV2(n) {
RED.nodes.createNode(this, n);
if (!global.nodeInitLock) global.nodeInitLock = {};
if (global.nodeInitLock[n.id]) {
return;
}
global.nodeInitLock[n.id] = true;
var node = this;
// =============================
// REGISTRAZIONE LISTENER HTTP
// (necessario per rendere il deploy uguale al riavvio)
// =============================
function registerListener() {
const path = "/" + node.id;
RED.httpNode.post(path, function(req, res){
const body = req.body || {};
node.send({ payload: body });
res.sendStatus(200);
});
node.httpListener = true;
}
// CREA IL LISTENER SUBITO (deploy + riavvio)
registerListener();
// =============================
this.service = n.service;
this.brokerConn = RED.nodes.getNode(this.service);
this.noderedhost = n.noderedhost;
this.userk1 = n.userk1;
this.passk2 = n.passk2;
this.apikey = n.apikey;
this.basicAuth = n.basicAuth;
this.on('close', function(removed, done) {
global.nodeInitLock[n.id] = false;
try {
// 1. Rimuovi l’HTTP endpoint listener
if (node.httpListener) {
RED.httpNode._router.stack = RED.httpNode._router.stack.filter(function(r){
return !(r.route && r.route.path === "/" + node.id);
});
node.httpListener = null;
}
// 2. Rimuovi eventuali timeout, interval, pending requests
if (node.pendingTimer) clearTimeout(node.pendingTimer);
// 3. Pulisci cache e contesto
node.context().set('currentSubscriptionId', null);
} catch(e) {
node.error("Error closing node: " + e)
}
delete global.nodeInitLock[n.id];
done();
});
// validate mandatory fields
validateInput(this, n);
this.on('input', function (msg) {
var action = msg.payload.action;
// Se abbiamo una lista devices la usiamo, altrimenti creiamo una lista con un singolo elemento
var devicesList = Array.isArray(msg.payload.devices)
? msg.payload.devices
: [{
deviceType: msg.payload.deviceType || n.entype,
deviceId: msg.payload.deviceId || n.enid,
isPattern: (typeof msg.payload.isPattern !== "undefined" ? msg.payload.isPattern : n.ispattern)
}];
// Aggiorna comunque gli ultimi valori memorizzati
// n.entype = devicesList[0].deviceType;
// n.enid = devicesList[0].deviceId;
//n.ispattern = devicesList[0].isPattern;
var correctPath = os.homedir() + "\\.snap4cityConfig\\";
if (RED.settings.APPID != null) {
correctPath = "/data/.snap4cityConfig/";
}
var jsonFilePath = correctPath + n.id + ".json";
if (!fs.existsSync(correctPath)) {
fs.mkdirSync(correctPath);
fs.writeFileSync(jsonFilePath, '[]');
} else if (!fs.existsSync(jsonFilePath)) {
fs.writeFileSync(jsonFilePath, '[]');
}
// Carica JSON corrente
var subscriptionJson = JSON.parse(fs.readFileSync(jsonFilePath));
// --- LOGICA SOTTOSCRIZIONI MULTIPLE ---
devicesList.forEach(dev => {
var deviceType = dev.deviceType;
var deviceId = dev.deviceId;
var isPattern = (typeof dev.isPattern !== "undefined" ? dev.isPattern : n.ispattern);
if (action === "add") {
var found = subscriptionJson.some(e =>
e.id === deviceId && e.type === deviceType);
if (!found) {
subscriptionJson.push({
id: deviceId,
type: deviceType,
isPattern: isPattern
});
}
} else if (action === "remove") {
subscriptionJson = subscriptionJson.filter(e =>
!(e.id === deviceId && e.type === deviceType));
}
});
// Salva modifiche
fs.writeFileSync(jsonFilePath, JSON.stringify(subscriptionJson));
if (action !== "add" && action !== "remove") {
msg.payload = "possible actions are add or remove";
}
generateSubscribePayload(node, n).then(function (payload) {
node.brokerConn.subscribe(node, n, payload);
});
msg.info = msg.payload;
node.send(msg);
});
node.brokerConn.init(node, n).then(function () {
var correctPath =os.homedir() + "\\.snap4cityConfig\\";//consider edge scenario
if (RED.settings.APPID != null) {//check edge scenario or cloud scenario
correctPath = "/data/.snap4cityConfig/";//consider cloud scenario
}
var jsonFilePath=correctPath+n.id+".json";
if (fs.existsSync(jsonFilePath)){
generateSubscribePayload(node, n).then(function (payload) {
node.brokerConn.subscribe(node, n, payload);
});
}else{
generateSubscribePayload(node, n).then(function (payload) {
node.brokerConn.subscribe(node, n, payload);
});
}
});
}
/*Cancellazione sottoscrizione*/
RED.nodes.registerType("orion-unsubscribe-api-v2", OrionUnsubscribeV2);
function OrionUnsubscribeV2(n) {
RED.nodes.createNode(this, n);
this.on('input', function (msg) {
var node = this; // Riferimento al nodo
// URL di Orion Broker (può essere parametrizzato)
// Recupera l'ID di iscrizione dal payload del messaggio o dal parametro del nodo
var subscriptionId = (msg.payload.subscriptionId ? msg.payload.subscriptionId : n.subscriptionId);
var deviceId=(msg.payload.id ? msg.payload.id : n.enid);
n.enid=deviceId;
// Chiamata alla funzione di disiscrizione (unsubscribeFromOrion)
if (subscriptionId !== undefined && subscriptionId !== "" && subscriptionId !== null && deviceId !== null && deviceId !== "" && deviceId !== undefined) {
unsubscribeFromOrion(node, subscriptionId, null, n);
msg.payload = "The subscription has been canceled.";
node.send(msg);
} else {
let errorMessage = "Missing required parameters";
node.error(errorMessage);
}
});
}
// OrionPatternSubscribeV2 node constructor
RED.nodes.registerType("orion-pattern-subscribe-api-v2", OrionPatternSubscribeV2);
function OrionPatternSubscribeV2(n) {
RED.nodes.createNode(this, n);
if (!global.nodeInitLock) global.nodeInitLock = {};
if (global.nodeInitLock[n.id]) {
return;
}
global.nodeInitLock[n.id] = true;
var node = this;
this.service = n.service;
this.brokerConn = RED.nodes.getNode(this.service);
this.noderedhost = n.noderedhost;
this.apikey = n.apikey;
this.basicAuth = n.basicAuth;
function getNodeID() {
return (node.id + "").replace(".", "");
}
function buildExactPayload(cfg) {
return getMyUri(cfg).then(function (myUri) {
var idPattern = (cfg.idpattern || "").trim();
var typePattern = (cfg.typepattern || "").trim();
var entitytype = (cfg.entitytype || "").trim();
var description = (cfg.description || "").trim() || ("Pattern subscription for " + idPattern);
if (!idPattern) {
throw new Error("Missing idPattern");
}
if ((typePattern && entitytype) || (!typePattern && !entitytype)) {
throw new Error("Specify exactly one between typePattern and type");
}
var prot = "http";
var orionUrl = (node.brokerConn && node.brokerConn.url) ? node.brokerConn.url : "";
if (orionUrl.indexOf("https://") >= 0) {
prot = "https";
}
var entity = {
idPattern: idPattern
};
if (typePattern) {
entity.typePattern = typePattern;
} else {
entity.type = entitytype;
}
return {
description: description,
subject: {
entities: [entity]
},
notification: {
http: {
url: prot + "://" + myUri + "/" + getNodeID()
}
}
};
});
}
function subscribePatternExact() {
if (!node.brokerConn) {
node.error("Missing service configuration node");
return;
}
buildExactPayload(n)
.then(function (exactPayload) {
var serviceConfig = Object.assign({}, n, {
enid: "pattern_subscribe",
entype: "pattern_type",
ispattern: true
});
node.log("EXACT OUTGOING BODY: " + JSON.stringify(exactPayload));
node.brokerConn.subscribeExact(node, serviceConfig, exactPayload);
})
.catch(function (err) {
node.error("Pattern exact subscribe error: " + err);
});
}
node.brokerConn.init(node, n).then(function () {
subscribePatternExact();
}).catch(function (err) {
node.error("Init error: " + err);
});
this.on("close", function (removed, done) {
try {
var nodeID = getNodeID();
if (RED.httpNode && RED.httpNode._router && RED.httpNode._router.stack) {
RED.httpNode._router.stack = RED.httpNode._router.stack.filter(function (r) {
return !(r.route && r.route.path === "/" + nodeID);
});
}
node.context().set("currentSubscriptionId", null);
} catch (e) {
node.error("Error closing node: " + e);
}
delete global.nodeInitLock[n.id];
done();
});
}
//OrionSubscribeV2 node constructor
RED.nodes.registerType("orion-broker-subscribe-list-entity-api-v2", OrionBrokerSubscribeListEntityV2);
function OrionBrokerSubscribeListEntityV2(n) {
RED.nodes.createNode(this, n);
var node = this;
this.service = n.service;
this.brokerConn = RED.nodes.getNode(this.service);
this.noderedhost = n.noderedhost;
this.userk1 = n.userk1;
this.passk2 = n.passk2;
this.apikey = n.apikey;
this.basicAuth = n.basicAuth;
this.on("close", function () {
var nodeID = node.id + "";
nodeID = nodeID.replace('.', '');
RED.httpNode._router.stack.forEach(function (route, i, routes) {
if (route.route && route.route.path == "/" + nodeID) {
routes.splice(i, 1);
}
});
//cannot invoke unsubscribe from here since the config is empty (and the contextbroker uri is unknown)
// unsubscribeFromOrion(node, subscriptionIDs[nodeID], null, n);
});
var nodeID = (node.id + "").replace('.', '');
listenOnUrl(nodeID, function (req, res) {
if (req.body.subscriptionId != subscriptionIDs.getSubscriptionOfNode(nodeID)) {
logger.warn(
"Ignoring notification for subscription " + req.body.subscriptionId +
" (current valid: " + currentId + ")"
);//unsubscribeFromOrion(node, req.body.subscriptionId, orionUrl, config);
} else {
//var payload = formatOutput(node, config, req.body.data);//TODO *1 verify formatOutput
node.send({
payload: req.body.data,//TODO *1
statusCode: 201
});
}
res.sendStatus(200);
var orionUrl = node.brokerConn.url;
var prot='http';
if (orionUrl.indexOf("https://") >= 0) {
prot='https';
}
getMyUri(n).then(function(myUri) {
var url=`${prot}://${myUri}/${nodeID}`
node.status({
fill: "blue",
shape: "dot",
text: "Listening on: "+url
})
}).catch(function(error) {
console.error("Errore:", error);
});
});
// validate mandatory fields
validateInput(this, n);
this.on('input', function (msg) {
n.ispattern="isPattern";
n.enid=msg.payload
var correctPath =os.homedir() + "\\.snap4cityConfig\\";//consider edge scenario
var created=false
if (RED.settings.APPID != null) {//check edge scenario or cloud scenario
correctPath = "/data/.snap4cityConfig/";//consider cloud scenario
}
var jsonFilePath=correctPath+n.id+".json";
var jsonFileSubscription=correctPath+"subscriptions.json";
if (!fs.existsSync(correctPath)) {
fs.mkdirSync(correctPath);//not exist folder, create folder + empty file
fs.writeFileSync(jsonFilePath, JSON.stringify(msg.payload));
} else if (!fs.existsSync(jsonFilePath)){
//this.subscriptionJson = JSON.parse(fs.readFileSync(jsonFilePath));
fs.writeFileSync(jsonFilePath, '[]',{ flag: 'w' });//not exist file, create empy file
} else{ //folder and file exist, read from it
var subscriptionJsonRead = JSON.parse(fs.readFileSync(jsonFilePath));
if(subscriptionJsonRead.length!=0){
var subscriptionFlow = JSON.parse(fs.readFileSync(jsonFileSubscription));
var msg2={}
msg2.payload={
message:"The subscription has already been created.",
subscription_id:subscriptionFlow[n.id],
devices_entities:n["enid"]
}
created=true
node.send([,msg2]);
}
}
if(!created){
var subscriptionJson = msg.payload;
fs.writeFileSync(jsonFilePath, JSON.stringify(subscriptionJson));
generateSubscribePayload(node, n).then(function (payload) {
node.brokerConn.subscribe(node, n, payload);
});
node.brokerConn.init(node, n).then(function () {
var correctPath =os.homedir() + "\\.snap4cityConfig\\";//consider edge scenario
if (RED.settings.APPID != null) {//check edge scenario or cloud scenario
correctPath = "/data/.snap4cityConfig/";//consider cloud scenario
}
var jsonFilePath=correctPath+n.id+".json";
if (fs.existsSync(jsonFi