UNPKG

@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
"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;