node-red-contrib-jewishtimer
Version:
A Timer for node-red which supports jewish dates and Zmanim
235 lines (231 loc) • 7.93 kB
JavaScript
const { DAYS_COUNT, TIMER_COUNT} = require("./settings");
module.exports = function(RED) {
"use strict"
const HeDate = require('he-date');
const kosherZmamin = require('kosher-zmanim');
function getMonth(month){
return ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"][month];
}
function getWeekday(weekday){
return ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][weekday];
}
function getHeDateMonth(hedate) {
let month = hedate.toDateString().match(/^.+?\d\d (.+) \d{4}$/)[1];
// for adar we'll only deal with adar1 and adar2.
if(month == "Adar I") return "Adar1";
if(month.startsWith("Adar")) return "Adar2";
return month;
}
function Timer(config) {
RED.nodes.createNode(this,config);
const node = this;
node.state = {
msg: {
topic: config.topic,
},
timeout: null,
todaysSchedules: [],
active: true,
incomingMsg: null,
}
function sendMsg(){
try{
node.send(node.state.msg);
}catch(e){
console.error("Error sending msg",e)
}
}
function startSchedule(){
// starts the process when node starts
startTodaysScheules();
}
function execSchedule(){
// execeutes a schedule and schedules the next one
clearTimeout(node.state.timeout);
const evt = node.state.todaysSchedules.shift();
node.state.msg.payload = Number(evt.action) ? config.onmsg : config.offmsg;
node.state.action = Number(evt.action);
node.state.msg.time = evt.time;
sendMsg();
scheduleNextEvt(); // will also set the status
}
function scheduleNextEvt(){
// schedules next evt for today or tomorrows day start
if(node.state.todaysSchedules.length){
node.state.timeout = setTimeout(execSchedule, node.state.todaysSchedules[0].time - Date.now() );
} else {
const tom = new Date();
tom.setDate(tom.getDate()+1);
tom.setHours(0);
tom.setMinutes(0);
tom.setSeconds(0);
tom.setMilliseconds(0);
node.state.timeout = setTimeout(startTodaysScheules,tom.getTime() - Date.now());
}
setStatus();
}
function startTodaysScheules(){
// clean up msg obj
node.state.msg = {};
let sendMsgRegardless = false;
if(!node.state.active) { // if it becomes active midday, startTodaysScheules will be called again.
scheduleNextEvt(); // will just set up tomorrows call, for when does not become active today;
return;
}
node.state.todaysSchedules = getTodaysSchedules();
// only keep one past due schedule
while(node.state.todaysSchedules[1] && node.state.todaysSchedules[1].time <= Date.now()){
node.state.todaysSchedules.shift();
}
if(node.state.todaysSchedules.length){
if(config.forceinactive){
if(config.forceinactivefullday){
const date = new Date();
date.setHours(23);
date.setMinutes(59);
date.setSeconds(59);
node.state.msg.forceInactiveUntil = date.getTime();
sendMsgRegardless = true;
} else {
const time = node.state.todaysSchedules[node.state.todaysSchedules.length -1].time + Number(config.inactiveoffset) * Number(config.inactiveoffsettype);
node.state.msg.forceInactiveUntil = time; // TBD MAYBE: allow forcing inactive for x after each schedule.
// not sending msg now, it will go with next schedule;
}
}
}
if(node.state.todaysSchedules[0] && node.state.todaysSchedules[0].time <= Date.now()){
execSchedule();
} else {
scheduleNextEvt();
// force inactive
sendMsgRegardless && sendMsg();
}
}
function getTodaysSchedules(){
const date = new Date();
const jDate = new HeDate();
const weekday = getWeekday(date.getDay());
const gDay = date.getDate();
const gMonth = getMonth(date.getMonth());
const jDay = jDate.getDate();
const jMonth = getHeDateMonth(jDate);
let dayActive = true;
const events = [];
for(let i=0;i<DAYS_COUNT;i++){
if(config[`sc${i}DateActive`]) {
// selected dates set up
const gDateSel = config[`sc${i}Datetype`] === "gmonthday" &&
config[`sc${i}${gMonth}`] &&
config[`sc${i}gmonthdays`].split(",").includes(String(gDay));
const jDateSel = config[`sc${i}Datetype`] === "jmonthday" &&
config[`sc${i}${jMonth}`] &&
config[`sc${i}jmonthdays`].split(",").includes(String(jDay));
const weekdaySel = config[`sc${i}Datetype`] === "weekday" &&
config[`sc${i}${weekday}`];
// Rule is Active
switch(config[`sc${i}DateAction`]){
case "0":
// exclude
dayActive = dayActive && !gDateSel && !jDateSel && !weekdaySel;
break;
case "1":
// include
dayActive = dayActive || gDateSel || jDateSel || weekdaySel;
break;
case "2":
// only include;
dayActive = dayActive && (gDateSel || jDateSel || weekdaySel);
break;
default:
// act as if inactive
continue;
}
}
}
if(dayActive){
// gather events
for(let i = 0; i< TIMER_COUNT; i++){
if(config[`sc${i}TimeActive`]){
if(config[`sc${i}Timetype`] == "time"){
const [hrs,mins] = config[`sc${i}Time`].split(":");
const time = new Date();
time.setHours(hrs);
time.setMinutes(mins);
events.push({
time: time.getTime(),
action: config[`sc${i}TimeAction`]
})
}else if(config[`sc${i}Timetype`] == "zman"){
const zmanim = kosherZmamin.getZmanimJson({
latitude: config.lat,
longitude: config.lon
}).BasicZmanim;
const time = (new Date(zmanim[config[`sc${i}Zman`]])).getTime() + Number(config[`sc${i}Zmanoffset`]) * Number(config[`sc${i}Zmanoffsettype`]);
events.push({
time,
action: config[`sc${i}TimeAction`]
})
}
}
}
}
return events.sort((a,b)=>{return a.time - b.time });
}
function setInactiveUntil(time){
if(time > Date.now()){
node.state.active = false;
node.state.inactiveUntil = time;
clearTimeout(node.state.timeout);
node.state.timeout = setTimeout(()=>{
node.state.active = true;
node.state.inactiveUntil = null;
startSchedule();
}, time - Date.now());
}
setStatus();
}
function setStatus(){
if(!node.state.active){
const until = new Date(node.state.inactiveUntil);
node.status({
fill: "yellow",
shape: "dot",
text: `Passthrough mode until: ${until.toLocaleString()}`
})
return;
}
const nextSchedule = node.state.todaysSchedules[0]
let nextScheduleStr = "";
let currScheduleStr = "";
if(nextSchedule){
nextScheduleStr = `Next Sechedule: ${(new Date(nextSchedule.time)).toLocaleString()}, Action: ${Number(nextSchedule.action)? "ON" : "OFF"}`;
}
if(node.state.msg.payload){
currScheduleStr = Number(node.state.msg.action) ? "ON" : "OFF" + ", "
}
node.status({
fill: currScheduleStr ? (node.state.action ? "green" : "red") : nextSchedule ? (Number(nextSchedule.action) ? "green" : "red") : "grey",
shape: currScheduleStr ? "dot" : nextScheduleStr ? "ring" : "dot",
text: (currScheduleStr + nextScheduleStr) || "No Schedule Today"
})
}
// entry point
// wait 2 seconds so node red has calmed down and can send msgs
setTimeout(startSchedule,2000);
node.on('input', function(msg) {
node.state.incomingMsg = msg;
// TBR: why is this needed
if(!config.topic && msg.topic){
node.state.msg.topic = msg.topic;
}
if(msg.forceInactiveUntil){
setInactiveUntil(msg.forceInactiveUntil);
}
node.send(node.state.incomingMsg);
});
node.on('close', function(){
clearTimeout(node.state.timeout);
});
}
RED.nodes.registerType("jewish-timer",Timer);
}