diff --git a/hardware/intel/mraa-gpio-din.html b/hardware/intel/mraa-gpio-din.html
index df76b93e..83ef75fe 100644
--- a/hardware/intel/mraa-gpio-din.html
+++ b/hardware/intel/mraa-gpio-din.html
@@ -34,6 +34,9 @@
if (data === 7) { t = "Banana"; }
if (data === 26) { t = "IOT2050"; }
$('#type-tip').text(t);
+ if (data === 26) {
+ $('#node-input-pin').append($("").attr("value",20).text("USER button"));
+ }
$('#node-input-pin').val(pinnow);
});
$.getJSON('mraa-version/'+this.id,function(data) {
@@ -68,7 +71,6 @@
-
diff --git a/hardware/intel/package.json b/hardware/intel/package.json
index 6238514b..2c45353c 100644
--- a/hardware/intel/package.json
+++ b/hardware/intel/package.json
@@ -1,6 +1,6 @@
{
"name" : "node-red-node-intel-gpio",
- "version" : "0.3.0",
+ "version" : "0.4.0",
"description" : "A Node-RED node to talk to an Intel Galileo, Edison or Siemens IOT2050 board using mraa",
"dependencies" : {
},
diff --git a/hardware/sensehat/package.json b/hardware/sensehat/package.json
index c9845b99..d2805955 100644
--- a/hardware/sensehat/package.json
+++ b/hardware/sensehat/package.json
@@ -1,6 +1,6 @@
{
"name" : "node-red-node-pi-sense-hat",
- "version" : "0.1.4",
+ "version" : "0.1.5",
"description" : "A Node-RED node to interact with a Raspberry Pi Sense HAT",
"repository" : {
"type":"git",
diff --git a/hardware/sensehat/sensehat.py b/hardware/sensehat/sensehat.py
index 2b53248b..f5a82deb 100644
--- a/hardware/sensehat/sensehat.py
+++ b/hardware/sensehat/sensehat.py
@@ -66,7 +66,7 @@ hf_interval = 0.09 # Approx 10/s
lf_interval = 1
hf_enabled = False
-lf_enabled = False
+lf_enabled = True
scroll = None
diff --git a/hardware/sensehatsim/examples/Clock.json b/hardware/sensehatsim/examples/Clock.json
index 54525109..a067aab6 100644
--- a/hardware/sensehatsim/examples/Clock.json
+++ b/hardware/sensehatsim/examples/Clock.json
@@ -1 +1,56 @@
-[{"id":"8b346a30.651a88","type":"rpi-sensehat out","z":"7756eff1.08d0a","name":"","x":530,"y":220,"wires":[]},{"id":"c2e7a15d.237bd","type":"function","z":"7756eff1.08d0a","name":"Simple graphical clock","func":"\n// array to hold \"random\" pixels\nvar ranNums = [];\n\n// create a non-overlapping array of random numbers 0-8\nfunction rerand() {\n var nums = [0,1,2,3,4,5,6,7,8];\n var i = nums.length;\n var j;\n ranNums = [];\n while (i--) {\n j = Math.floor(Math.random() * (i+1));\n ranNums.push(nums[j]);\n nums.splice(j,1);\n }\n}\n\n// Get the hours and minutes and split into tens and units\nvar d = new Date();\nvar s = d.getSeconds();\nvar su = s%4;\nif (su === 0) {\n var h = d.getHours();\n var m = d.getMinutes();\n var hu = h%10;\n h = parseInt(h/10);\n var mu = m%10;\n m = parseInt(m/10);\n \n // Do the tens of hours (red)\n rerand();\n node.send({payload:\"1-3,1-3,0,0,0\"});\n for (var i=0; i Destination
+
+
+
+
+
+ Enabling the ACK (acknowledgment) will set the ack header to client while subscribing to topics.
+ This means the items on the broker queue will not be dequeue'd unless an ACK message is sent using the ack node.
+
The Destination field is optional. If not set uses the msg.topic
+
The Destination field is optional. If not set uses the msg.topic
property of the message.
@@ -134,6 +147,9 @@
+
+ Reconnection timings are calculated using exponential backoff. The first reconnection happens immediately, the second reconnection happens at +delay ms, the third at + 2*delay ms, etc.
+
@@ -148,8 +164,8 @@
port: {value:61613,required:true,validate:RED.validators.number()},
protocolversion: {value:"1.0",required:true},
vhost: {},
- reconnectretries: {value:"0",required:true,validate:RED.validators.number()},
- reconnectdelay: {value:"0.5",required:true,validate:RED.validators.number()},
+ reconnectretries: {value:0,required:true,validate:RED.validators.number()},
+ reconnectdelay: {value:1,required:true,validate:RED.validators.number()},
name: {}
},
credentials: {
@@ -161,3 +177,57 @@
}
});
+
+
+
+
+
+
diff --git a/io/stomp/18-stomp.js b/io/stomp/18-stomp.js
index a35da1a7..97a2ec22 100644
--- a/io/stomp/18-stomp.js
+++ b/io/stomp/18-stomp.js
@@ -2,94 +2,415 @@
module.exports = function(RED) {
"use strict";
var StompClient = require('stomp-client');
- var querystring = require('querystring');
+ // ----------------------------------------------
+ // ------------------- State --------------------
+ // ----------------------------------------------
+ function updateStatus(node, allNodes) {
+ let setStatus = setStatusDisconnected;
+
+ if (node.connecting) {
+ setStatus = setStatusConnecting;
+ } else if (node.connected) {
+ setStatus = setStatusConnected;
+ }
+
+ setStatus(node, allNodes);
+ }
+
+ function setStatusDisconnected(node, allNodes) {
+ if (allNodes) {
+ for (let id in node.users) {
+ if (hasProperty(node.users, id)) {
+ node.users[id].status({ fill: "red", shape: "ring", text: "node-red:common.status.disconnected"});
+ }
+ }
+ } else {
+ node.status({ fill: "red", shape: "ring", text: "node-red:common.status.disconnected" })
+ }
+ }
+
+ function setStatusConnecting(node, allNodes) {
+ if(allNodes) {
+ for (var id in node.users) {
+ if (hasProperty(node.users, id)) {
+ node.users[id].status({ fill: "yellow", shape: "ring", text: "node-red:common.status.connecting" });
+ }
+ }
+ } else {
+ node.status({ fill: "yellow", shape: "ring", text: "node-red:common.status.connecting" });
+ }
+ }
+
+ function setStatusConnected(node, allNodes) {
+ if(allNodes) {
+ for (var id in node.users) {
+ if (hasProperty(node.users, id)) {
+ node.users[id].status({ fill: "green", shape: "dot", text: "node-red:common.status.connected" });
+ }
+ }
+ } else {
+ node.status({ fill: "green", shape: "dot", text: "node-red:common.status.connected" });
+ }
+ }
+
+ function setStatusError(node, allNodes) {
+ if(allNodes) {
+ for (var id in node.users) {
+ if (hasProperty(node.users, id)) {
+ node.users[id].status({ fill: "red", shape: "dot", text: "error" });
+ }
+ }
+ } else {
+ node.status({ fill: "red", shape: "dot", text: "error" });
+ }
+ }
+
+ // ----------------------------------------------
+ // ------------------- Nodes --------------------
+ // ----------------------------------------------
function StompServerNode(n) {
RED.nodes.createNode(this,n);
- this.server = n.server;
- this.port = n.port;
- this.protocolversion = n.protocolversion;
- this.vhost = n.vhost;
- this.reconnectretries = n.reconnectretries || 999999;
- this.reconnectdelay = (n.reconnectdelay || 15) * 1000;
- this.name = n.name;
- this.username = this.credentials.user;
- this.password = this.credentials.password;
+ const node = this;
+ // To keep track of processing nodes that use this config node for their connection
+ node.users = {};
+ // Config node state
+ node.connected = false;
+ node.connecting = false;
+ /** Flag to avoid race conditions between `deregister` and the `close` event of the config node (ex. on redeploy) */
+ node.closing = false;
+ /** Options to pass to the stomp-client API */
+ node.options = {};
+ node.sessionId = null;
+ node.subscribtionIndex = 1;
+ node.subscriptionIds = {};
+ /** Array of callbacks to be called once the connection to the broker has been made */
+ node.connectedCallbacks = [];
+ /** @type { StompClient } */
+ node.client;
+ node.setOptions = function(options, init) {
+ if (!options || typeof options !== "object") {
+ return; // Nothing to change
+ }
+
+ // Apply property changes (only if the property exists in the options object)
+ setIfHasProperty(options, node, "server", init);
+ setIfHasProperty(options, node, "port", init);
+ setIfHasProperty(options, node, "protocolversion", init);
+ setIfHasProperty(options, node, "vhost", init);
+ setIfHasProperty(options, node, "reconnectretries", init);
+ setIfHasProperty(options, node, "reconnectdelay", init);
+
+ if (node.credentials) {
+ node.username = node.credentials.user;
+ node.password = node.credentials.password;
+ }
+ if (!init && hasProperty(options, "username")) {
+ node.username = options.username;
+ }
+ if (!init && hasProperty(options, "password")) {
+ node.password = options.password;
+ }
+
+ // Build options for passing to the stomp-client API
+ node.options = {
+ address: node.server,
+ port: node.port * 1,
+ user: node.username,
+ pass: node.password,
+ protocolVersion: node.protocolversion,
+ reconnectOpts: {
+ retries: node.reconnectretries * 1,
+ delay: node.reconnectdelay * 1000
+ },
+ vhost: node.vhost
+ };
+ }
+
+ node.setOptions(n, true);
+
+ /**
+ * Register a STOMP processing node to the connection.
+ * @param { StompInNode | StompOutNode | StompAckNode } stompNode The STOMP processing node to register
+ * @param { Function } callback
+ */
+ node.register = function(stompNode, callback = () => {}) {
+ node.users[stompNode.id] = stompNode;
+
+ if (!node.connected) {
+ node.connectedCallbacks.push(callback);
+ }
+
+ // Auto connect when first STOMP processing node is added
+ if (Object.keys(node.users).length === 1) {
+ node.connect(() => {
+ while (node.connectedCallbacks.length) {
+ node.connectedCallbacks.shift().call();
+ }
+ });
+ } else if (node.connected) {
+ // Execute callback directly as the connection to the STOMP server has already been made
+ callback();
+ }
+ }
+
+ /**
+ * Remove registered STOMP processing nodes from the connection.
+ * @param { StompInNode | StompOutNode | StompAckNode } stompNode The STOMP processing node to unregister
+ * @param { Boolean } autoDisconnect Automatically disconnect from the STOM server when no processing nodes registered to the connection
+ * @param { Function } callback
+ */
+ node.deregister = function(stompNode, autoDisconnect, callback = () => {}) {
+ delete node.users[stompNode.id];
+ if (autoDisconnect && !node.closing && node.connected && Object.keys(node.users).length === 0) {
+ node.disconnect(callback);
+ } else {
+ callback();
+ }
+ }
+
+ /**
+ * Wether a new connection can be made.
+ * @returns `true` or `false`
+ */
+ node.canConnect = function() {
+ return !node.connected && !node.connecting;
+ }
+
+ /**
+ * Connect to the STOMP server.
+ * @param {Function} callback
+ */
+ node.connect = function(callback = () => {}) {
+ if (node.canConnect()) {
+ node.closing = false;
+ node.connecting = true;
+ setStatusConnecting(node, true);
+
+ try {
+ // Remove left over client if needed
+ if (node.client) {
+ node.client.disconnect();
+ node.client = null;
+ }
+
+ node.client = new StompClient(node.options);
+
+ node.client.on("connect", function(sessionId) {
+ node.closing = false;
+ node.connecting = false;
+ node.connected = true;
+ node.sessionId = sessionId;
+
+ node.log(`Connected to STOMP server, sessionId: ${node.sessionId}, url: ${node.options.address}:${node.options.port}, protocolVersion: ${node.options.protocolVersion}`);
+ setStatusConnected(node, true);
+ callback();
+ });
+
+ node.client.on("reconnect", function(sessionId, numOfRetries) {
+ node.closing = false;
+ node.connecting = false;
+ node.connected = true;
+ node.sessionId = sessionId;
+
+ node.log(`Reconnected to STOMP server, sessionId: ${node.sessionId}, url: ${node.options.address}:${node.options.port}, protocolVersion: ${node.options.protocolVersion}, retries: ${numOfRetries}`);
+ setStatusConnected(node, true);
+ callback();
+ });
+
+ node.client.on("reconnecting", function() {
+ node.warn("reconnecting");
+ node.connecting = true;
+ node.connected = false;
+
+ node.log("Reconnecting to STOMP server...", {url: `${node.options.address}:${node.options.port}`, protocolVersion: node.options.protocolVersion});
+ setStatusConnecting(node, true);
+ });
+
+ node.client.on("error", function(err) {
+ node.error(err);
+ setStatusError(node, true);
+ });
+
+ node.client.connect();
+ } catch (err) {
+ node.error(err);
+ }
+ } else {
+ node.log("Not connecting to STOMP server, already connected");
+ callback();
+ }
+ }
+
+ /**
+ * Disconnect from the STOMP server.
+ * @param {Function} callback
+ */
+ node.disconnect = function(callback = () => {}) {
+ const waitDisconnect = (client, timeout) => {
+ return new Promise((resolve, reject) => {
+ // Set flag to avoid race conditions for disconnect as every node tries to call it directly or indirectly using deregister
+ node.closing = true;
+ const t = setTimeout(() => {
+ reject();
+ }, timeout);
+ client.disconnect(() => {
+ clearTimeout(t);
+ resolve();
+ });
+ });
+ }
+
+ if (!node.client) {
+ node.warn("Can't disconnect, connection not initialized.");
+ callback();
+ } else if (node.closing || !node.connected) {
+ // Disconnection already in progress or not connected
+ callback();
+ } else {
+ const subscribedQueues = Object.keys(node.subscriptionIds);
+ subscribedQueues.forEach(function(queue) {
+ node.unsubscribe(queue);
+ });
+ node.log('Disconnecting from STOMP server...');
+ waitDisconnect(node.client, 2000).then(() => {
+ node.log(`Disconnected from STOMP server, sessionId: ${node.sessionId}, url: ${node.options.address}:${node.options.port}, protocolVersion: ${node.options.protocolVersion}`)
+ }).catch(() => {
+ node.log("Disconnect timeout closing node...");
+ }).finally(() => {
+ node.sessionId = null;
+ node.subscribtionIndex = 1;
+ node.subscriptionIds = {};
+ node.connected = false;
+ node.connecting = false;
+ setStatusDisconnected(node, true);
+ callback();
+ });
+ }
+ }
+
+ /**
+ * Subscribe to a given STOMP queue.
+ * @param { String} queue The queue to subscribe to
+ * @param { "auto" | "client" | "client-individual" } clientAck Can be `auto`, `client` or `client-individual` (the latter only starting from STOMP v1.1)
+ * @param { Function } callback
+ */
+ node.subscribe = function(queue, acknowledgment, callback) {
+ node.log(`Subscribe to: ${queue}`);
+
+ if (node.connected && !node.closing) {
+ if (!node.subscriptionIds[queue]) {
+ node.subscriptionIds[queue] = node.subscribtionIndex++;
+ }
+
+ const headers = {
+ id: node.subscriptionIds[queue],
+ // Only set client-individual if not v1.0
+ ack: acknowledgment === "client-individual" && node.options.protocolVersion === "1.0" ? "client" : acknowledgment
+ }
+
+ node.client.subscribe(queue, headers, function(body, responseHeaders) {
+ let msg = { headers: responseHeaders, topic: queue };
+ try {
+ msg.payload = JSON.parse(body);
+ } catch {
+ msg.payload = body;
+ }
+ callback(msg);
+ });
+ } else {
+ node.error("Can't subscribe, not connected");
+ }
+ }
+
+ /**
+ * Unsubscribe from a STOMP queue.
+ * @param {String} queue The STOMP queue to unsubscribe from
+ * @param {Object} headers Headers to add to the unsubscribe message
+ */
+ node.unsubscribe = function(queue, headers = {}) {
+ delete node.subscriptionIds[queue];
+ if (node.connected && !node.closing) {
+ node.client.unsubscribe(queue, headers);
+ node.log(`Unsubscribed from ${queue}, headers: ${JSON.stringify(headers)}`);
+ }
+ }
+
+ /**
+ * Publish a STOMP message on a queue.
+ * @param {String} queue The STOMP queue to publish to
+ * @param {any} message The message to send
+ * @param {Object} headers STOMP headers to add to the SEND command
+ */
+ node.publish = function(queue, message, headers = {}) {
+ if (node.connected && !node.closing) {
+ node.client.publish(queue, message, headers);
+ } else {
+ node.error("Can't publish, not connected");
+ }
+ }
+
+ /**
+ * Acknowledge (a) message(s) that was received from the specified queue.
+ * @param {String} queue The queue/topic to send an acknowledgment for
+ * @param {String} messageId ID of the message that was received from the server, which can be found in the reponse header as `message-id`
+ * @param {String} transaction Optional transaction name
+ */
+ node.ack = function(queue, messageId, transaction = undefined) {
+ if (node.connected && !node.closing) {
+ node.client.ack(messageId, node.subscriptionIds[queue], transaction);
+ } else {
+ node.error("Can't send acknowledgment, not connected");
+ }
+ }
+
+ node.on("close", function(done) {
+ node.disconnect(function() { done (); });
+ });
}
- RED.nodes.registerType("stomp-server",StompServerNode,{
+ RED.nodes.registerType("stomp-server", StompServerNode, {
credentials: {
- user: {type:"text"},
- password: {type: "password"}
+ user: { type: "text" },
+ password: { type: "password" }
}
});
function StompInNode(n) {
RED.nodes.createNode(this,n);
- this.server = n.server;
- this.topic = n.topic;
+ /** @type { StompInNode } */
+ const node = this;
+ node.server = n.server;
+ /** @type { StompServerNode } */
+ node.serverConnection = RED.nodes.getNode(node.server);
+ node.topic = n.topic;
+ node.ack = n.ack;
- this.serverConfig = RED.nodes.getNode(this.server);
- this.stompClientOpts = {
- address: this.serverConfig.server,
- port: this.serverConfig.port * 1,
- user: this.serverConfig.username,
- pass: this.serverConfig.password,
- protocolVersion: this.serverConfig.protocolversion,
- reconnectOpts: {
- retries: this.serverConfig.reconnectretries * 1,
- delay: this.serverConfig.reconnectdelay * 1
+ if (node.serverConnection) {
+ setStatusDisconnected(node);
+
+ if (node.topic) {
+ node.serverConnection.register(node, function() {
+ node.serverConnection.subscribe(node.topic, node.ack, function(msg) {
+ node.send(msg);
+ });
+ });
+
+ if (node.serverConnection.connected) {
+ setStatusConnected(node);
+ }
}
- };
- if (this.serverConfig.vhost) {
- this.stompClientOpts.vhost = this.serverConfig.vhost;
+ } else {
+ node.error("Missing server config");
}
- var node = this;
- var msg = {topic:this.topic};
- node.client = new StompClient(node.stompClientOpts);
-
- node.client.on("connect", function() {
- node.status({fill:"green",shape:"dot",text:"connected"});
- });
-
- node.client.on("reconnecting", function() {
- node.status({fill:"red",shape:"ring",text:"reconnecting"});
- node.warn("reconnecting");
- });
-
- node.client.on("reconnect", function() {
- node.status({fill:"green",shape:"dot",text:"connected"});
- });
-
- node.client.on("error", function(error) {
- node.status({fill:"grey",shape:"dot",text:"error"});
- node.warn(error);
- });
-
- node.status({fill:"grey",shape:"ring",text:"connecting"});
- node.client.connect(function(sessionId) {
- node.log('subscribing to: '+node.topic);
- node.client.subscribe(node.topic, function(body, headers) {
- var newmsg={"headers":headers,"topic":node.topic}
- try {
- newmsg.payload = JSON.parse(body);
- }
- catch(e) {
- newmsg.payload = body;
- }
- node.send(newmsg);
- });
- }, function(error) {
- node.status({fill:"grey",shape:"dot",text:"error"});
- node.warn(error);
- });
-
- node.on("close", function(done) {
- if (node.client) {
- // disconnect can accept a callback - but it is not always called.
- node.client.disconnect();
+ node.on("close", function(removed, done) {
+ if (node.serverConnection) {
+ node.serverConnection.unsubscribe(node.topic);
+ node.serverConnection.deregister(node, true, done);
+ node.serverConnection = null;
+ } else {
+ done();
}
- done();
});
}
RED.nodes.registerType("stomp in",StompInNode);
@@ -97,65 +418,128 @@ module.exports = function(RED) {
function StompOutNode(n) {
RED.nodes.createNode(this,n);
- this.server = n.server;
- this.topic = n.topic;
+ /** @type { StompOutNode } */
+ const node = this;
+ node.server = n.server;
+ /** @type { StompServerNode } */
+ node.serverConnection = RED.nodes.getNode(node.server);
+ node.topic = n.topic;
- this.serverConfig = RED.nodes.getNode(this.server);
- this.stompClientOpts = {
- address: this.serverConfig.server,
- port: this.serverConfig.port * 1,
- user: this.serverConfig.username,
- pass: this.serverConfig.password,
- protocolVersion: this.serverConfig.protocolversion,
- reconnectOpts: {
- retries: this.serverConfig.reconnectretries * 1,
- delay: this.serverConfig.reconnectdelay * 1
+ if (node.serverConnection) {
+ setStatusDisconnected(node);
+
+ node.on("input", function(msg, send, done) {
+ const topic = node.topic || msg.topic;
+ if (topic.length > 0 && msg.payload) {
+ try {
+ msg.payload = JSON.stringify(msg.payload);
+ } catch {
+ msg.payload = `${msg.payload}`;
+ }
+ node.serverConnection.publish(topic, msg.payload, msg.headers || {});
+ } else if (!topic.length > 0) {
+ node.warn('No valid publish topic');
+
+ } else {
+ node.warn('Payload or topic is undefined/null')
+ }
+ done();
+ });
+
+ node.serverConnection.register(node);
+ if (node.serverConnection.connected) {
+ setStatusConnected(node);
}
- };
- if (this.serverConfig.vhost) {
- this.stompClientOpts.vhost = this.serverConfig.vhost;
+ } else {
+ node.error("Missing server config");
}
- var node = this;
- node.client = new StompClient(node.stompClientOpts);
-
- node.client.on("connect", function() {
- node.status({fill:"green",shape:"dot",text:"connected"});
- });
-
- node.client.on("reconnecting", function() {
- node.status({fill:"red",shape:"ring",text:"reconnecting"});
- node.warn("reconnecting");
- });
-
- node.client.on("reconnect", function() {
- node.status({fill:"green",shape:"dot",text:"connected"});
- });
-
- node.client.on("error", function(error) {
- node.status({fill:"grey",shape:"dot",text:"error"});
- node.warn(error);
- });
-
- node.status({fill:"grey",shape:"ring",text:"connecting"});
- node.client.connect(function(sessionId) {
- }, function(error) {
- node.status({fill:"grey",shape:"dot",text:"error"});
- node.warn(error);
- });
-
- node.on("input", function(msg) {
- node.client.publish(node.topic || msg.topic, msg.payload, msg.headers);
- });
-
- node.on("close", function(done) {
- if (node.client) {
- // disconnect can accept a callback - but it is not always called.
- node.client.disconnect();
+ node.on("close", function(removed, done) {
+ if (node.serverConnection) {
+ node.serverConnection.deregister(node, true, done);
+ node.serverConnection = null;
+ } else {
+ done();
}
- done();
});
}
RED.nodes.registerType("stomp out",StompOutNode);
+ function StompAckNode(n) {
+ RED.nodes.createNode(this,n);
+ /** @type { StompOutNode } */
+ const node = this;
+ node.server = n.server;
+ /** @type { StompServerNode } */
+ node.serverConnection = RED.nodes.getNode(node.server);
+ node.topic = n.topic;
+
+ if (node.serverConnection) {
+ setStatusDisconnected(node);
+
+ node.on("input", function(msg, send, done) {
+ const topic = node.topic || msg.topic;
+ if (topic.length > 0) {
+ node.serverConnection.ack(topic, msg.messageId, msg.transaction);
+ } else if (!topic.length > 0) {
+ node.warn('No valid publish topic');
+
+ } else {
+ node.warn('Payload or topic is undefined/null')
+ }
+ done();
+ });
+
+ node.serverConnection.register(node);
+ if (node.serverConnection.connected) {
+ setStatusConnected(node);
+ }
+ } else {
+ node.error("Missing server config");
+ }
+
+ node.on("close", function(removed, done) {
+ if (node.serverConnection) {
+ node.serverConnection.deregister(node, true, done);
+ node.serverConnection = null;
+ } else {
+ done();
+ }
+ });
+ }
+ RED.nodes.registerType("stomp ack",StompAckNode);
+
};
+
+
+// ----------------------------------------------
+// ----------------- Helpers --------------------
+// ----------------------------------------------
+ /**
+ * Helper function for applying changes to an objects properties ONLY when the src object actually has the property.
+ * This avoids setting a `dst` property null/undefined when the `src` object doesnt have the named property.
+ * @param {object} src Source object containing properties
+ * @param {object} dst Destination object to set property
+ * @param {string} propName The property name to set in the Destination object
+ * @param {boolean} force force the dst property to be updated/created even if src property is empty
+ */
+ function setIfHasProperty(src, dst, propName, force) {
+ if (src && dst && propName) {
+ const ok = force || hasProperty(src, propName);
+ if (ok) {
+ dst[propName] = src[propName];
+ }
+ }
+ }
+
+ /**
+ * Helper function to test an object has a property
+ * @param {object} obj Object to test
+ * @param {string} propName Name of property to find
+ * @returns true if object has property `propName`
+ */
+ function hasProperty(obj, propName) {
+ //JavaScript does not protect the property name hasOwnProperty
+ //Object.prototype.hasOwnProperty.call is the recommended/safer test
+ return Object.prototype.hasOwnProperty.call(obj, propName);
+ }
\ No newline at end of file
diff --git a/io/stomp/package.json b/io/stomp/package.json
index 1f1d1329..5508ba7f 100644
--- a/io/stomp/package.json
+++ b/io/stomp/package.json
@@ -1,6 +1,6 @@
{
"name" : "node-red-node-stomp",
- "version" : "0.0.14",
+ "version" : "1.0.4",
"description" : "A Node-RED node to publish and subscribe to/from a Stomp server",
"dependencies" : {
"stomp-client" : "^0.9.0"
diff --git a/social/email/README.md b/social/email/README.md
index bac85577..3d6ee45b 100644
--- a/social/email/README.md
+++ b/social/email/README.md
@@ -12,8 +12,9 @@ getting an application password if you have two-factor authentication enabled.
For Exchange and Outlook 365 you must use OAuth2.0.
**Notes **:
- Version 2.x of this node required **Node.js v12** or newer.
- Version 1.x of this node requires **Node.js v8** or newer.
+ Version 2.x of this node requires **Node.js v14** or newer.
+ Version 1.91 of this node required **Node.js v14** or newer.
+ Previous versions of this node required **Node.js v8** or newer.
Install
-------
diff --git a/social/email/package.json b/social/email/package.json
index 2d98b4c6..e94aec55 100644
--- a/social/email/package.json
+++ b/social/email/package.json
@@ -1,13 +1,13 @@
{
"name": "node-red-node-email",
- "version": "2.0.0",
+ "version": "2.0.1",
"description": "Node-RED nodes to send and receive simple emails.",
"dependencies": {
"imap": "^0.8.19",
"node-pop3": "^0.8.0",
"mailparser": "^3.6.4",
- "nodemailer": "^6.9.1",
- "smtp-server": "^3.11.0"
+ "nodemailer": "^6.9.3",
+ "smtp-server": "^3.12.0"
},
"bundledDependencies": [
"imap",
@@ -40,10 +40,10 @@
},
"author": {
"name": "Dave Conway-Jones",
- "email": "ceejay@vnet.ibm.com",
+ "email": "dceejay@gmail.com",
"url": "http://nodered.org"
},
"engines": {
- "node": ">=8.0.0"
+ "node": ">=14.0.0"
}
}
diff --git a/social/pushover/package.json b/social/pushover/package.json
index d913c923..1b327f38 100644
--- a/social/pushover/package.json
+++ b/social/pushover/package.json
@@ -1,6 +1,6 @@
{
"name": "node-red-node-pushover",
- "version": "0.1.1",
+ "version": "0.2.0",
"description": "A Node-RED node to send alerts via Pushover",
"dependencies": {
"pushover-notifications": "^1.2.2"
diff --git a/time/timeswitch/package.json b/time/timeswitch/package.json
index 71917d79..1c0b3de5 100644
--- a/time/timeswitch/package.json
+++ b/time/timeswitch/package.json
@@ -1,10 +1,10 @@
{
"name" : "node-red-node-timeswitch",
- "version" : "1.0.0",
+ "version" : "1.1.1",
"description" : "A Node-RED node to provide a simple timeswitch to schedule daily on/off events.",
"dependencies" : {
- "spacetime": "^7.4.0",
- "suncalc": "^1.8.0"
+ "spacetime": "^7.4.3",
+ "suncalc": "^1.9.0"
},
"repository" : {
"type":"git",
diff --git a/time/timeswitch/timeswitch.js b/time/timeswitch/timeswitch.js
index fc5e3c87..744fca44 100644
--- a/time/timeswitch/timeswitch.js
+++ b/time/timeswitch/timeswitch.js
@@ -46,8 +46,19 @@ module.exports = function (RED) {
// all sun events for the given lat/long
const sunEvents = SunCalc.getTimes(nowNative, node.lat, node.lon);
- let sunriseDateTime = spacetime(sunEvents[SUNRISE_KEY]).nearest("minute");
- let sunsetDateTime = spacetime(sunEvents[SUNSET_KEY]).nearest("minute");
+ const sunAlt = SunCalc.getPosition(nowNative, node.lat, node.lon).altitude;
+ var sunriseDateTime = spacetime(sunEvents[SUNRISE_KEY]).nearest("minute");
+ var sunsetDateTime = spacetime(sunEvents[SUNSET_KEY]).nearest("minute");
+
+ if (sunEvents[SUNRISE_KEY] == "Invalid Date") {
+ if (sunAlt >= 0) { sunriseDateTime = now.startOf("day"); }
+ else { sunriseDateTime = now.endOf("day"); }
+ }
+
+ if (sunEvents[SUNSET_KEY] == "Invalid Date") {
+ if (sunAlt >= 0) { sunsetDateTime = now.endOf("day"); }
+ else { sunsetDateTime = now.startOf("day"); }
+ }
// add optional sun event offset, if specified
sunriseDateTime = sunriseDateTime.add(Number(node.sunriseOffset), "minutes");
@@ -56,13 +67,15 @@ module.exports = function (RED) {
// check if sun event has already occurred today
if (now.isAfter(sunriseDateTime)) {
// get tomorrow's sunrise, since it'll be different
- sunriseDateTime = spacetime(SunCalc.getTimes(now.add(1, "day").toNativeDate(), node.lat, node.lon)[SUNRISE_KEY]).nearest("minute");
+ // sunriseDateTime = spacetime(SunCalc.getTimes(now.add(1, "day").toNativeDate(), node.lat, node.lon)[SUNRISE_KEY]).nearest("minute");
+ sunriseDateTime = sunriseDateTime.add(1, "day");
// add optional sun event offset, if specified (again)
sunriseDateTime = sunriseDateTime.add(Number(node.sunriseOffset), "minutes");
}
if (now.isAfter(sunsetDateTime)) {
// get tomorrow's sunset, since it'll be different
- sunsetDateTime = spacetime(SunCalc.getTimes(now.add(1, "day").toNativeDate(), node.lat, node.lon)[SUNSET_KEY]).nearest("minute");
+ // sunsetDateTime = spacetime(SunCalc.getTimes(now.add(1, "day").toNativeDate(), node.lat, node.lon)[SUNSET_KEY]).nearest("minute");
+ sunsetDateTime = sunsetDateTime.add(1, "day");
// add optional sun event offset, if specified (again)
sunsetDateTime = sunsetDateTime.add(Number(node.sunsetOffset), "minutes");
}
@@ -126,19 +139,36 @@ module.exports = function (RED) {
// var o = nextTime.goto(selectedTimeZone.name).offset()/60;
// if (o > 0) { o = "+" + o; }
// else {o = "-" + o; }
- if (payload == 1) {
- node.status({
- fill: "yellow",
- shape: "dot",
- text: `on until ${nextTime.goto(selectedTimeZone.name).format("time-24")}`
- });
+ if (nextTime) {
+ if (payload == 1) {
+ node.status({
+ fill: "yellow",
+ shape: "dot",
+ text: `on until ${nextTime.goto(selectedTimeZone.name).format("time-24")}`
+ });
+ } else {
+ node.status({
+ fill: "blue",
+ shape: "dot",
+ text: `off until ${nextTime.goto(selectedTimeZone.name).format("time-24")}`
+ });
+ }
} else {
- node.status({
- fill: "blue",
- shape: "dot",
- text: `off until ${nextTime.goto(selectedTimeZone.name).format("time-24")}`
- });
+ if (payload == 1) {
+ node.status({
+ fill: "yellow",
+ shape: "dot",
+ text: `on`
+ });
+ } else {
+ node.status({
+ fill: "blue",
+ shape: "dot",
+ text: `off`
+ });
+ }
}
+
var msg = {};
if (node.mytopic) {
msg.topic = node.mytopic;