@tbowmo/node-red-small-timer
Version:
Small timer node for Node-RED with support for sunrise, sunset etc. timers
301 lines (300 loc) • 10.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SmallTimerRunner = void 0;
const util_1 = require("@node-red/util");
const time_calculation_1 = require("./time-calculation");
const timer_1 = require("./timer");
const pad = (n) => n < 10 ? `0${n.toFixed(0)}` : `${n.toFixed(0)}`;
const SecondsTick = 1000;
class SmallTimerRunner {
constructor(position, configuration, node) {
this.node = node;
this.startupTock = undefined;
this.tickTimer = undefined;
this.tickTimerInterval = 0;
this.lastPublish = 0;
this.override = 'auto';
this.currentState = false;
this.timer = new timer_1.Timer();
this.defaultTickTimer = SecondsTick * 20;
this.timeCalc = new time_calculation_1.TimeCalc(position ? Number(position.latitude) : undefined, position ? Number(position.longitude) : undefined, configuration.wrapMidnight, Number(configuration.startTime), Number(configuration.endTime), Number(configuration.startOffset), Number(configuration.endOffset), Number(configuration.minimumOnTime));
this.topic = configuration.topic;
this.onMsg = configuration.onMsg;
this.offMsg = configuration.offMsg;
this.onMsgType = configuration.onMsgType;
this.offMsgType = configuration.offMsgType;
this.rules = configuration.rules;
this.repeat = configuration.repeat;
this.repeatInterval = this.repeat
? Number(configuration.repeatInterval || 60)
: 60;
this.defaultTickTimer = this.repeatInterval * SecondsTick / 3;
if (this.defaultTickTimer < SecondsTick) {
this.defaultTickTimer = SecondsTick;
}
this.debugMode = configuration.debugEnable;
this.onTimeout = Number(configuration.onTimeout);
this.offTimeout = Number(configuration.offTimeout);
this.sendEmptyPayload = configuration.sendEmptyPayload ?? true;
if (configuration.injectOnStartup) {
this.startupTock = setTimeout(this.forceSend.bind(this), 2 * SecondsTick);
}
else {
this.calcState();
this.updateNodeStatus();
}
this.startTickTimer(this.defaultTickTimer);
}
getCurrentState() {
return this.override === 'auto'
? this.currentState
: (this.override === 'tempOn');
}
generateDebug() {
return {
...this.timeCalc.debug(),
override: this.override,
weekNumber: this.getWeekNumber(),
topic: 'debug',
};
}
generateMsg(trigger) {
const status = this.getCurrentState();
const payload = status
? util_1.util.evaluateNodeProperty(this.onMsg, this.onMsgType, this.node, {})
: util_1.util.evaluateNodeProperty(this.offMsg, this.offMsgType, this.node, {});
return {
state: this.override,
stamp: Date.now(),
autoState: this.override === 'auto',
duration: 0,
temporaryManual: this.override !== 'auto',
timeout: this.timer.timeLeft(),
payload: payload,
topic: this.topic,
trigger,
};
}
publishState(trigger) {
const status = this.generateMsg(trigger);
const shouldSendStatus = this.sendEmptyPayload || status.payload !== '';
if (this.debugMode) {
this.node.send([
shouldSendStatus ? status : null,
this.generateDebug(),
]);
return;
}
if (shouldSendStatus) {
this.node.send(status);
}
}
getWeekNumber() {
const currentDate = new Date();
currentDate.setHours(0, 0, 0, 0);
currentDate.setDate(currentDate.getDate() + 3 - (currentDate.getDay() + 6) % 7);
const week1 = new Date(currentDate.getFullYear(), 0, 4);
return 1 + Math.round(((currentDate.getTime() - week1.getTime()) / 86400000
- 3 + (week1.getDay() + 6) % 7) / 7);
}
isDayOk(date = new Date()) {
const month = date.getMonth() + 1;
const week = this.getWeekNumber();
const dayOfMonth = date.getDate();
const dayOfWeek = date.getDay();
const oddOrEvenWeek = Math.floor(week / 2) === week / 2
? 200
: 201;
const validMonths = [0, month, week + 100, oddOrEvenWeek];
const validDays = [0, dayOfMonth, dayOfWeek + 100];
let isOk = false;
this.rules.forEach((item) => {
const ruleMonth = Number(item.month);
const ruleDay = Number(item.day);
if (validMonths.includes(ruleMonth)
&& validDays.includes(ruleDay)) {
isOk = item.type === 'include';
}
});
return isOk;
}
calcState() {
const date = new Date();
const newState = this.timeCalc.getOnState(date);
if ((this.isDayOk(date) || this.currentState) && (newState !== this.currentState)) {
this.currentState = newState;
return true;
}
return false;
}
shouldRepeatPublish() {
const seconds = Date.now() / SecondsTick;
if (this.repeat && (seconds - this.lastPublish >= this.repeatInterval)) {
this.lastPublish = seconds;
return true;
}
return false;
}
timerEvent() {
const change = this.calcState();
const nextChange = this.updateNodeStatus();
if (nextChange < 60) {
this.startTickTimer(SecondsTick);
}
else {
this.startTickTimer(this.defaultTickTimer);
}
if (change || this.shouldRepeatPublish()) {
this.publishState('timer');
}
}
forceSend(trigger = 'timer') {
this.calcState();
this.updateNodeStatus();
this.publishState(trigger);
}
getHumanTime(time) {
const hour = Math.floor(time / 60);
const minutes = time % 60;
const seconds = Math.floor((time - Math.floor(time)) * 60);
const str = [];
if (hour) {
str.push(`${pad(hour)}hrs`);
}
if (minutes >= 1 || hour) {
str.push(`${pad(minutes)}mins`);
}
else {
str.push(`${pad(seconds)}secs`);
}
return str.join(' ');
}
updateNodeStatus() {
let fill = 'yellow';
const text = [];
const activeToday = this.timeCalc.operationToday() === 'normal' && (this.isDayOk() || this.currentState);
if (!activeToday) {
text.push('No action today');
}
switch (this.timeCalc.operationToday()) {
case 'noMidnightWrap':
text.push('off time is before on time');
break;
case 'minimumOnTimeNotMet':
text.push('minimum on time not met');
}
let nextTimeoutOrAuto = Number.MAX_SAFE_INTEGER;
if (activeToday || this.override !== 'auto') {
fill = 'red';
let state = 'OFF';
let nextAutoChange = this.timeCalc.getTimeToNextStartEvent();
if (this.getCurrentState()) {
fill = 'green';
state = 'ON';
nextAutoChange = this.timeCalc.getTimeToNextEndEvent();
}
const timeout = this.timer.timeLeft();
nextTimeoutOrAuto = timeout && (timeout < nextAutoChange)
? timeout
: nextAutoChange;
if (this.override !== 'auto') {
text.length = 0;
text.push(`Temporary ${state} for ${this.getHumanTime(nextTimeoutOrAuto)}`);
}
else {
text.push(`${state} for ${this.getHumanTime(nextTimeoutOrAuto)}`);
}
}
const status = {
fill,
shape: this.override !== 'auto' ? 'ring' : 'dot',
text: text.join(' - '),
};
this.node.status(status);
return nextTimeoutOrAuto;
}
doOverride(override, timeout) {
this.override = override;
if (override === 'auto') {
this.timer.stop();
return;
}
if ((override === 'tempOn') === this.currentState) {
this.override = 'auto';
this.timer.stop();
return;
}
const timerTimeout = override === 'tempOn'
? (timeout ?? this.onTimeout)
: (timeout ?? this.offTimeout);
this.timer.start(timerTimeout, () => {
this.override = 'auto';
this.forceSend();
});
}
onMessage(incomingMsg) {
if (incomingMsg.reset !== undefined) {
this.doOverride('auto');
this.forceSend('input');
return;
}
const payload = typeof incomingMsg.payload === 'string'
? incomingMsg.payload.toLocaleLowerCase()
: incomingMsg.payload;
const timeout = incomingMsg.timeout !== undefined ? Number(incomingMsg.timeout) : undefined;
if (timeout !== undefined && isNaN(timeout)) {
throw new Error(`Timeout value "${incomingMsg.timeout}" can not be converted to a number`);
}
switch (payload) {
case 0:
case '0':
case 'off':
case 'false':
case false:
this.doOverride('tempOff', timeout);
break;
case 1:
case '1':
case 'on':
case 'true':
case true:
this.doOverride('tempOn', timeout);
break;
case 'toggle':
this.doOverride(this.getCurrentState()
? 'tempOff'
: 'tempOn', timeout);
break;
case 'auto':
case 'default':
this.doOverride('auto');
break;
case 'sync':
break;
default:
throw new Error(`Did not understand the command '${incomingMsg.payload}' supplied in payload`);
}
this.forceSend('input');
}
cleanup() {
if (this.startupTock) {
clearTimeout(this.startupTock);
}
this.stopTickTimer();
}
stopTickTimer() {
if (this.tickTimer) {
clearInterval(this.tickTimer);
this.tickTimer = undefined;
}
}
startTickTimer(newInterval) {
if (this.tickTimer && (newInterval === this.tickTimerInterval)) {
return;
}
this.stopTickTimer();
this.tickTimerInterval = newInterval;
this.tickTimer = setInterval(this.timerEvent.bind(this), newInterval);
}
}
exports.SmallTimerRunner = SmallTimerRunner;