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. +
@@ -33,6 +45,7 @@ defaults: { name: {value:""}, server: {type:"stomp-server",required:true}, + ack: {value: "auto", required: true}, topic: {value:"",required:true} }, inputs:0, @@ -60,7 +73,7 @@
-
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;