Address incorrect timestamps bug in node-red-node-timeswitch; increase readability (#945)

* Initial fixes before testing and reformatting

* Put sun event offset back in; reformat

* Tabs to spaces

* Fix "Start+X" OFF time; add more comments

* Undo some formatting changes

* Add contributors to package.json
This commit is contained in:
J.D. Mallen 2022-08-23 09:31:18 -04:00 committed by GitHub
parent ecb2849675
commit 29e0bed000
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 169 additions and 82 deletions

View File

@ -25,7 +25,19 @@
}, },
"contributors": [ "contributors": [
{ {
"name": "@pmacostapdi" "name": "@dceejay"
},
{
"name": "@pmacostapdi"
},
{
"name": "@heikokue"
},
{
"name": "@sammachin"
},
{
"name": "@jdmallen"
} }
] ]
} }

View File

@ -1,19 +1,18 @@
module.exports = function (RED) {
module.exports = function(RED) {
"use strict"; "use strict";
var SunCalc = require('suncalc'); var SunCalc = require('suncalc');
const spacetime = require("spacetime") const spacetime = require("spacetime")
const SUNRISE_KEY = "sunrise";
const SUNSET_KEY = "sunset";
function TimeswitchNode(n) { function TimeswitchNode(n) {
RED.nodes.createNode(this, n); RED.nodes.createNode(this, n);
this.lat = n.lat; this.lat = n.lat;
this.lon = n.lon; this.lon = n.lon;
this.start = n.start || "sunrise";
this.end = n.end || "sunset";
this.startt = n.starttime; this.startt = n.starttime;
this.endt = n.endtime; this.endt = n.endtime;
this.duskoff = n.duskoff; this.sunriseOffset = n.dawnoff;
this.dawnoff = n.dawnoff; this.sunsetOffset = n.duskoff;
this.mytopic = n.mytopic; this.mytopic = n.mytopic;
this.timezone = n.timezone || "UTC"; this.timezone = n.timezone || "UTC";
@ -24,6 +23,7 @@ module.exports = function(RED) {
this.thu = n.thu; this.thu = n.thu;
this.fri = n.fri; this.fri = n.fri;
this.sat = n.sat; this.sat = n.sat;
this.jan = n.jan; this.jan = n.jan;
this.feb = n.feb; this.feb = n.feb;
this.mar = n.mar; this.mar = n.mar;
@ -38,117 +38,192 @@ module.exports = function(RED) {
this.dec = n.dec; this.dec = n.dec;
var node = this; var node = this;
var ison = 0;
var newendtime = 0;
this.on("input", function(msg2) { this.on("input", function () {
if (msg2.payload === "reset") { ison = 0; } // current global time
const now = spacetime.now();
const nowNative = now.toNativeDate();
var timeOffset = spacetime(Date.now()).goto(this.timezone.toLowerCase()).timezone().current.offset * 60 * 60 * 1000; // all sun events for the given lat/long
var now = new Date(Date.now() + timeOffset); const sunEvents = SunCalc.getTimes(nowNative, node.lat, node.lon);
var nowMillis = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes(), 0); let sunriseDateTime = spacetime(sunEvents[SUNRISE_KEY]).nearest("minute");
var midnightMillis = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 0, 0); let sunsetDateTime = spacetime(sunEvents[SUNSET_KEY]).nearest("minute");
var today = Math.round((nowMillis - midnightMillis) / 60000) % 1440;
var starttime = Number(node.startt);
var endtime = Number(node.endt);
var tzOff = (new Date()).getTimezoneOffset();
if ((starttime >= 5000) || (endtime == 5000) || (endtime == 6000)) { // add optional sun event offset, if specified
var times = SunCalc.getTimes(now, node.lat, node.lon); sunriseDateTime = sunriseDateTime.add(Number(node.sunriseOffset), "minutes");
var startMillis = Date.UTC(times[node.start].getUTCFullYear(), times[node.start].getUTCMonth(), times[node.start].getUTCDate(), times[node.start].getUTCHours(), times[node.start].getUTCMinutes()); sunsetDateTime = sunsetDateTime.add(Number(node.sunsetOffset), "minutes");
var endMillis = Date.UTC(times[node.end].getUTCFullYear(), times[node.end].getUTCMonth(), times[node.end].getUTCDate(), times[node.end].getUTCHours(), times[node.end].getUTCMinutes());
var dawn = ((startMillis - midnightMillis) / 60000) + Number(node.dawnoff); // check if sun event has already occurred today
var dusk = ((endMillis - midnightMillis) / 60000) + Number(node.duskoff); if (now.isAfter(sunriseDateTime)) {
if (starttime == 5000) { starttime = dawn; } // get tomorrow's sunrise, since it'll be different
if (starttime == 6000) { starttime = dusk; } sunriseDateTime = spacetime(SunCalc.getTimes(now.add(1, "day").toNativeDate(), node.lat, node.lon)[SUNRISE_KEY]).nearest("minute");
if (endtime == 5000) { endtime = dawn; } // add optional sun event offset, if specified (again)
if (endtime == 6000) { endtime = dusk; } sunriseDateTime = sunriseDateTime.add(Number(node.sunriseOffset), "minutes");
if (RED.settings.verbose) { node.log("Dawn " + parseInt(dawn / 60) + ":" + dawn % 60 + " - Dusk " + parseInt(dusk / 60) + ":" + dusk % 60); } }
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");
// add optional sun event offset, if specified (again)
sunsetDateTime = sunsetDateTime.add(Number(node.sunsetOffset), "minutes");
} }
var proceed = 0; // log sun events
switch (now.getDay()) { if (RED.settings.verbose) {
case 0 : { if (node.sun) { proceed++; } break; } node.log(`Sunrise ${sunriseDateTime.format("time")} - Sunset ${sunsetDateTime.format("time")} `);
case 1 : { if (node.mon) { proceed++; } break; }
case 2 : { if (node.tue) { proceed++; } break; }
case 3 : { if (node.wed) { proceed++; } break; }
case 4 : { if (node.thu) { proceed++; } break; }
case 5 : { if (node.fri) { proceed++; } break; }
case 6 : { if (node.sat) { proceed++; } break; }
} }
if (proceed) { // apply selected timezone to selected times (not to sunrise/sunset-- those are based on lat/long)
switch (now.getMonth()) { const currentTimeZone = now.timezone();
case 0 : { if (node.jan) { proceed++; } break; } const selectedTimeZone = spacetime(now.epoch, this.timezone.toLowerCase()).timezone();
case 1 : { if (node.feb) { proceed++; } break; }
case 2 : { if (node.mar) { proceed++; } break; } // handler function to convert minute strings (from <option> tags) to spacetime objects, called below
case 3 : { if (node.apr) { proceed++; } break; } let getSelectedTimeFromMinuteString = minuteString => {
case 4 : { if (node.may) { proceed++; } break; } const selectedTimeInMinutesAfterMidnight = Number(minuteString);
case 5 : { if (node.jun) { proceed++; } break; } let selectedTime = spacetime.now();
case 6 : { if (node.jul) { proceed++; } break; } // if less than 1440, what are the time values for the next start and stop time?
case 7 : { if (node.aug) { proceed++; } break; } if (selectedTimeInMinutesAfterMidnight < 1440) {
case 8 : { if (node.sep) { proceed++; } break; } // determine offset to get from selected time zone to current timezone
case 9 : { if (node.oct) { proceed++; } break; } // e.g. current (EDT) is -4, selected (PDT) is -7
case 10: { if (node.nov) { proceed++; } break; } // in order to get from PDT to EDT, you must add 3
case 11: { if (node.dec) { proceed++; } break; } // (-4) - (-7) = +3
const offset = currentTimeZone.current.offset - selectedTimeZone.current.offset;
const selectedHourValue = Math.floor(selectedTimeInMinutesAfterMidnight / 60);
const selectedMinuteValue = Math.floor(selectedTimeInMinutesAfterMidnight % 60);
selectedTime = selectedTime.hour(selectedHourValue).minute(selectedMinuteValue).second(0).millisecond(0);
selectedTime = selectedTime.add(offset, "hours");
// select the next time if it's in the past
if (now.isAfter(selectedTime)) {
selectedTime = selectedTime.add(1, "day");
}
} else if (selectedTimeInMinutesAfterMidnight == 5000) { // sunrise
selectedTime = sunriseDateTime;
} else if (selectedTimeInMinutesAfterMidnight == 6000) { // sunset
selectedTime = sunsetDateTime;
}
return selectedTime;
};
// our definitive next ON time
let selectedOnTime = getSelectedTimeFromMinuteString(node.startt);
// our definitive next OFF time
let selectedOffTime = getSelectedTimeFromMinuteString(node.endt);
// handle the "Start + X Minutes" cases
if (node.endt >= 10000) {
// even though the next start time might be tomorrow,
// the start time + X minutes might still be coming today,
// so we need to go back a day first
const selectedOnTimeMinus1Day = selectedOnTime.subtract(1, "day");
selectedOffTime = selectedOnTimeMinus1Day.add(node.endt - 10000, "minutes");
// _now_ we can check if the off time is in the past
if (now.isAfter(selectedOffTime)) {
selectedOffTime = selectedOffTime.add(1, "day");
} }
} }
if (proceed >= 2) { proceed = 1; } // handler function for the node payload, called below
else { proceed = 0; } let sendPayload = (payload, nextTime) => {
if (payload == 1) {
newendtime = endtime; node.status({
if (endtime > 10000) { newendtime = starttime + (endtime - 10000); } fill: "yellow",
shape: "dot",
if (proceed) { // have to handle midnight wrap text: `on until ${nextTime.format("time")}`
if (starttime <= newendtime) { });
if ((today >= starttime) && (today <= newendtime)) { proceed++; } } else {
node.status({
fill: "blue",
shape: "dot",
text: `off until ${nextTime.format("time")}`
});
} }
else { var msg = {};
if ((today >= starttime) || (today <= newendtime)) { proceed++; } if (node.mytopic) {
msg.topic = node.mytopic;
} }
msg.payload = payload;
node.send(msg);
};
var proceed = true;
// if today is not among the selected days of the week, stop here
switch (nowNative.getDay()) {
case 0 : { if (!node.sun) { proceed &= false; } break; }
case 1 : { if (!node.mon) { proceed &= false; } break; }
case 2 : { if (!node.tue) { proceed &= false; } break; }
case 3 : { if (!node.wed) { proceed &= false; } break; }
case 4 : { if (!node.thu) { proceed &= false; } break; }
case 5 : { if (!node.fri) { proceed &= false; } break; }
case 6 : { if (!node.sat) { proceed &= false; } break; }
} }
if (proceed >= 2) { if (!proceed) {
node.status({fill:"yellow", shape:"dot", text:"on until " + parseInt((newendtime -tzOff) / 60) + ":" + ("0" + (newendtime - tzOff) % 60).substr(-2)}); sendPayload(0, selectedOnTime);
} return;
else {
node.status({fill:"blue", shape:"dot", text:"off until " + parseInt((starttime - tzOff) / 60) + ":" + ("0" + (starttime - tzOff) % 60).substr(-2)});
} }
var msg = {}; // if this month is not among the selected months, stop here
if (node.mytopic) { msg.topic = node.mytopic; } switch (nowNative.getMonth()) {
msg.payload = (proceed >= 2) ? 1 : 0; case 0 : { if (!node.jan) { proceed &= false; } break; }
node.send(msg); case 1 : { if (!node.feb) { proceed &= false; } break; }
case 2 : { if (!node.mar) { proceed &= false; } break; }
case 3 : { if (!node.apr) { proceed &= false; } break; }
case 4 : { if (!node.may) { proceed &= false; } break; }
case 5 : { if (!node.jun) { proceed &= false; } break; }
case 6 : { if (!node.jul) { proceed &= false; } break; }
case 7 : { if (!node.aug) { proceed &= false; } break; }
case 8 : { if (!node.sep) { proceed &= false; } break; }
case 9 : { if (!node.oct) { proceed &= false; } break; }
case 10: { if (!node.nov) { proceed &= false; } break; }
case 11: { if (!node.dec) { proceed &= false; } break; }
}
if (!proceed) {
sendPayload(0, selectedOnTime);
return;
}
// if the chronological order is NOW --> ON --> OFF, then now should be OFF
if (proceed && selectedOffTime.isAfter(selectedOnTime)) {
sendPayload(0, selectedOnTime);
return;
}
// if the chronological order is NOW --> OFF --> ON, then now should be ON
if (proceed && selectedOffTime.isBefore(selectedOnTime)) {
sendPayload(1, selectedOffTime);
return;
}
// Note: we already ensured that all ON or OFF times would be in the future,
// so there is no midnight wrapping issue.
}); });
var tock = setTimeout(function() { var tock = setTimeout(function () {
node.emit("input", {}); node.emit("input", {});
}, 2000); // wait 2 secs before starting to let things settle down e.g. UI connect }, 2000); // wait 2 secs before starting to let things settle down e.g. UI connect
var tick = setInterval(function() { var tick = setInterval(function () {
node.emit("input", {}); node.emit("input", {});
}, 60000); // trigger every 60 secs }, 60000); // trigger every 60 secs
this.on("close", function() { this.on("close", function () {
if (tock) { clearTimeout(tock); } if (tock) { clearTimeout(tock); }
if (tick) { clearInterval(tick); } if (tick) { clearInterval(tick); }
}); });
} }
RED.httpAdmin.post("/timeswitch/:id", RED.auth.needsPermission("timeswitch.write"), function(req, res) { RED.httpAdmin.post("/timeswitch/:id", RED.auth.needsPermission("timeswitch.write"), function (req, res) {
var node = RED.nodes.getNode(req.params.id); var node = RED.nodes.getNode(req.params.id);
if (node != null) { if (node != null) {
try { try {
node.emit("input", {payload:"reset"}); node.emit("input", { payload: "reset" });
res.sendStatus(200); res.sendStatus(200);
} } catch (err) {
catch (err) {
res.sendStatus(500); res.sendStatus(500);
node.error("Inject failed:" + err); node.error("Inject failed:" + err);
} }
} } else {
else {
res.sendStatus(404); res.sendStatus(404);
} }
}); });