@arduino/node-red-contrib-arduino-iot-cloud
Version:
Node-RED nodes to talk to Arduino IoT Cloud
244 lines (223 loc) • 7.55 kB
JavaScript
/*
* Copyright 2019 ARDUINO SA (http://www.arduino.cc/)
* This file is part of node-red-contrib-arduino-iot-cloud.
* Copyright (c) 2019
*
* This software is released under:
* The GNU General Public License, which covers the main part of
* node-red-contrib-arduino-iot-cloud
* The terms of this license can be found at:
* https://www.gnu.org/licenses/gpl-3.0.en.html
*
* You can be released from the requirements of the above licenses by purchasing
* a commercial license. Buying such a license is mandatory if you want to modify or
* otherwise use the software for commercial activities involving the Arduino
* software without disclosing the source code of your own applications. To purchase
* a commercial license, send an email to license@arduino.cc.
*
*/
const superagent = require('superagent');
const ArduinoClientHttp = require('./arduino-iot-cloud-api-wrapper');
const ArduinoClientMqtt = require('../arduino-iot-client-mqtt/arduino-iot-client-mqtt');
const accessTokenUri = process.env.NODE_RED_ACCESS_TOKEN_URI || 'https://api2.arduino.cc/iot/v1/clients/token';
const accessTokenAudience = process.env.NODE_RED_ACCESS_TOKEN_AUDIENCE || 'https://api2.arduino.cc/iot';
const arduinoIotCloudHost = process.env.NODE_RED_MQTT_HOST || 'wss.iot.arduino.cc';
const Mutex = require('async-mutex').Mutex;
const mqttMutex = new Mutex();
/** mqttConnections elem struct
* {
* clientId: clientId,
* connectionConfig: connectionConfig,
* clientMqtt: clientMqttobj,
* }
*/
var mqttConnections = [];
const httpMutex = new Mutex();
/** httpConnections elem struct
* {
* clientId: clientId,
* connectionConfig: connectionConfig,
* clientHttp: clientHttpobj,
* }
*/
var httpConnections = [];
function getMqttOptions(clientId, token, RED){
async function reconnect() {
console.log('Reconnecting to MQTT');
const releaseMutex = await mqttMutex.acquire();
let id = findUser(mqttConnections, clientId);
if (id !== -1) {
let token = await waitForToken(mqttConnections[id].connectionConfig);
await mqttConnections[id].clientMqtt.updateToken(token);
}
releaseMutex();
}
return {
host: arduinoIotCloudHost,
token: token,
onDisconnect: async () => {
RED.nodes.eachNode((n)=>{
if(n.type === "property in"){
const node = RED.nodes.getNode(n.id);
node.status({ fill: "red", shape: "dot", text: "arduino-iot-cloud.status.connection-error" });
}
});
console.log('Disconnected from MQTT');
await new Promise((resolve) => setTimeout(resolve, 1000));
await reconnect();
},
onOffline: async () => {
RED.nodes.eachNode((n)=>{
if(n.type === "property in"){
const node = RED.nodes.getNode(n.id);
node.status({ fill: "red", shape: "dot", text: "arduino-iot-cloud.status.offline" });
}
});
console.log('Offline from MQTT');
await new Promise((resolve) => setTimeout(resolve, 1000));
await reconnect();
},
onConnected: () =>{
RED.nodes.eachNode((n)=>{
if(n.type === "property in"){
const node = RED.nodes.getNode(n.id);
node.status({});
}
});
console.log('Connected to MQTT');
},
useCloudProtocolV2: true
};
}
async function getClientMqtt(connectionConfig, RED) {
if (!connectionConfig || !connectionConfig.credentials) {
throw new Error("Cannot find connection config or credentials.");
}
const releaseMutex = await mqttMutex.acquire();
try {
let clientMqtt;
let id = findUser(mqttConnections, connectionConfig.credentials.clientid);
if (id === -1) {
let token = await waitForToken(connectionConfig);
clientMqtt = new ArduinoClientMqtt.ArduinoClientMqtt();
mqttConnections.push({
clientId: connectionConfig.credentials.clientid,
connectionConfig: connectionConfig,
clientMqtt: clientMqtt,
});
await clientMqtt.connect(
getMqttOptions(connectionConfig.credentials.clientid, token, RED),
);
} else {
clientMqtt = mqttConnections[id].clientMqtt;
}
return clientMqtt;
} catch (err) {
console.log(err);
} finally {
releaseMutex();
}
}
async function getClientHttp(connectionConfig, organizationID) {
if (!connectionConfig || !connectionConfig.credentials) {
throw new Error("Cannot find cooonection config or credentials.");
}
const releaseMutex = await httpMutex.acquire();
try {
var id = findUser(httpConnections, connectionConfig.credentials.clientid);
var clientHttp;
if (id === -1) {
clientHttp = new ArduinoClientHttp.ArduinoClientHttp(async () => await getToken(connectionConfig, organizationID));
httpConnections.push({
clientId: connectionConfig.credentials.clientid,
connectionConfig: connectionConfig,
clientHttp: clientHttp,
});
} else {
clientHttp = httpConnections[id].clientHttp;
}
return clientHttp;
} catch (err) {
console.log(err);
} finally {
releaseMutex();
}
}
async function deleteClientMqtt(clientId, thing, propertyName, nodeId) {
const releaseMutex = await mqttMutex.acquire();
var id = findUser(mqttConnections, clientId);
if (id !== -1) {
var ret = await mqttConnections[id].clientMqtt.removePropertyValueCallback(thing, propertyName, nodeId);
if (ret === 0) {
await mqttConnections[id].clientMqtt.disconnect();
delete mqttConnections[id].clientMqtt;
mqttConnections[id].clientMqtt = null;
mqttConnections.splice(id, 1);
}
}
releaseMutex();
}
async function deleteClientHttp(clientId) {
const releaseMutex = await httpMutex.acquire();
var id = findUser(httpConnections, clientId);
if (id !== -1) {
if (httpConnections[id].clientHttp !== null) {
httpConnections[id].clientHttp.openConnections--;
if (httpConnections[id].clientHttp.openConnections === 0) {
httpConnections.splice(id, 1);
}
}
}
releaseMutex();
}
function findUser(connections, clientId) {
for (var i = 0; i < connections.length; i++) {
if (connections[i].clientId === clientId) {
return i;
}
}
return -1;
}
async function waitForToken(connectionConfig, organizationID) {
let delay = 200;
while (true) {
let token = await getToken(connectionConfig, organizationID);
if (token) {
return token;
}
await new Promise((resolve) => setTimeout(resolve, delay));
delay = Math.min(delay * 2, 5000);
}
}
async function getToken(connectionConfig, organizationID) {
const dataToSend = {
grant_type: 'client_credentials',
client_id: connectionConfig.credentials.clientid,
client_secret: connectionConfig.credentials.clientsecret,
audience: accessTokenAudience
};
try {
var req = superagent
.post(accessTokenUri)
.set('content-type', 'application/x-www-form-urlencoded')
.set('accept', 'json')
if (organizationID) {
req.set('X-Organization', organizationID)
}
var res = await req.send(dataToSend);
var token = res.body.access_token;
if (token !== undefined) {
return token;
}
} catch (err) {
if(err.response && err.response.res){
console.log("cannot get token: " + err.response.res.statusCode + ' ' + err.response.res.statusMessage);
}else{
console.log(err);
}
}
}
exports.getClientMqtt = getClientMqtt;
exports.getClientHttp = getClientHttp;
exports.deleteClientMqtt = deleteClientMqtt;
exports.deleteClientHttp = deleteClientHttp;