UNPKG

node-red-contrib-presence-faker

Version:

Simulates home occupancy by intermittently activating and deactivating devices (e.g. lights) at random intervals

282 lines (242 loc) 7.95 kB
const { createSchedule, stripPastBlocks, setDebugFn } = require('./pf') const CronJob = require('cron').CronJob const dayjs = require('dayjs') module.exports = function (RED) { function PresenceFakerNode(config) { RED.nodes.createNode(this, config) const node = this let windowBeginCron let windowEndCron let msgCrons = [] let isEnabled = false const debug = function (debugMsg) { if (config.isLoggingEnabled) { node.warn(debugMsg) } } setDebugFn(debug) const setNodeStatusForBlock = function (block) { const text = `${block.isOn ? 'ON' : 'OFF'} [${block.beginString} - ${ block.endString }]` node.status({ fill: 'yellow', shape: block.isOn ? 'dot' : 'ring', text, }) debug(`new node status: ${text}`) } const setNodeStatus = function (text) { node.status({ fill: 'grey', shape: 'dot', text: text, }) debug(`new node status: ${text}`) } const isNowWithinWindow = function () { const now = dayjs() let begin = dayjs(now.format('YYYY-MM-DD') + 'T' + config.windowBegin) let end = dayjs(now.format('YYYY-MM-DD') + 'T' + config.windowEnd) const duration = end.diff(begin, 'seconds') // handle special case of windowBegin > windowEnd, i.e. windows spanning two days if (duration < 0) { if (now.isBefore(end)) { begin = begin.subtract(1, 'day') } else if (now.isAfter(begin)) { end = end.add(1, 'day') } } const result = (now.isAfter(begin) && now.isBefore(end)) || now.isSame(begin) || now.isSame(end) debug(`isNowWithinWindow? ${result ? 'Yes' : 'No'}`) return result } const scheduleWindowCrons = function () { stopCrons() const now = dayjs() const begin = dayjs(now.format('YYYY-MM-DD') + 'T' + config.windowBegin) const end = dayjs(now.format('YYYY-MM-DD') + 'T' + config.windowEnd) const windowBeginCronCallback = function () { debug('executing windowBeginCronCallback') const schedule = stripPastBlocks(createSchedule(config)) debug('created new schedule: ' + JSON.stringify(schedule)) if (schedule.length > 0) { const currentBlock = schedule.shift() executeBlock(currentBlock) } scheduleMsgCrons(schedule) } if (isNowWithinWindow()) { windowBeginCronCallback() } else { setNodeStatus(`next cycle: ${begin.format('HH:mm')}`) } windowBeginCron = CronJob.from({ cronTime: `0 ${begin.minute()} ${begin.hour()} * * *`, // sec min hour dom month dow onTick: windowBeginCronCallback, start: true, }) windowEndCron = CronJob.from({ cronTime: `0 ${end.minute()} ${end.hour()} * * *`, // sec min hour dom month dow // cronTime: fakeCronTime, onTick: () => { setNodeStatus('cycle completed') }, start: true, }) debug( `window crontabs set up for ${begin.format('HH:mm')} and ${end.format( 'HH:mm' )}` ) } const scheduleMsgCrons = function (schedule) { msgCrons = schedule.map((block) => { const cron = CronJob.from({ cronTime: block.begin.toDate(), onTick: () => { executeBlock(block) }, }) try { cron.start() } catch (e) { debug(e) } return cron }) debug(`installed crons for ${schedule.length} blocks`) } const stopCrons = function () { if (windowBeginCron) { windowBeginCron.stop() windowEndCron.stop() debug(`window crons deleted`) } let deletedCronsCount = 0 if (msgCrons.length > 0) { msgCrons.forEach((cron) => { cron.stop() deletedCronsCount++ }) msgCrons = [] } debug(`${deletedCronsCount} message crons deleted`) } const executeBlock = (block) => { debug('executing block: ' + JSON.stringify(block)) ejectMsg(block.isOn) setNodeStatusForBlock(block) } const ejectMsg = function (isOn, sendHandler) { const castPayload = (payload, payloadType) => { if (payloadType === 'num') { return Number(payload) } else if (payloadType === 'bool') { return payload === 'true' } else if (payloadType === 'json') { return JSON.parse(payload) } else { return payload } } let payload = isOn ? config.onPayload : config.offPayload let payloadType = isOn ? config.onPayloadType : config.offPayloadType let msgToSend = { topic: isOn ? config.onTopic : config.offTopic, payload: castPayload(payload, payloadType), } if (sendHandler) { sendHandler(msgToSend) } else { node.send(msgToSend) } } node.on('input', function (msg, send, done) { send = send || function () { node.send.apply(node, arguments) } if ( msg.payload === true || msg.payload === 'true' || msg.payload === 'enable' || msg.payload === 'activate' ) { // activate! isEnabled = true scheduleWindowCrons() } else if ( msg.payload === false || msg.payload === 'false' || msg.payload === 'disable' || msg.payload === 'deactivate' ) { // deactivate! isEnabled = false stopCrons() if (isNowWithinWindow() && config.actionOnDisable !== 'none') { ejectMsg(config.actionOnDisable === 'on', send) } setNodeStatus('inactive') } else if (typeof msg.payload === 'object') { // config object received as msg.payload debug(`!!!! CONFIG OBJECT RECEIVED !!!`) if (msg.payload.windowBegin) { debug(`new windowBegin: ${msg.payload.windowBegin}`) config.windowBegin = msg.payload.windowBegin this.context().set('windowBegin', config.windowBegin) } if (msg.payload.windowEnd) { debug(`new windowEnd: ${msg.payload.windowEnd}`) config.windowEnd = msg.payload.windowEnd this.context().set('windowEnd', config.windowEnd) } if (msg.payload.minDuration) { debug(`new minDuration: ${msg.payload.minDuration}`) config.minDuration = Number(msg.payload.minDuration) this.context().set('minDuration', config.minDuration) } if (msg.payload.maxDuration) { debug(`new maxDuration: ${msg.payload.maxDuration}`) config.maxDuration = Number(msg.payload.maxDuration) this.context().set('maxDuration', config.maxDuration) } if (msg.payload.minCount) { debug(`new minCount: ${msg.payload.minCount}`) config.minCount = Number(msg.payload.minCount) this.context().set('minCount', config.minCount) } if (msg.payload.maxCount) { debug(`new maxCount: ${msg.payload.maxCount}`) config.maxCount = Number(msg.payload.maxCount) this.context().set('maxCount', config.maxCount) } // force re-scheduling if (isEnabled) { node.emit('input', { payload: true }) } } if (done) { done() } }) node.on('close', function () { stopCrons() }) ;(function () { // on startup: if (config.startupBehavior === 'onStartup') { node.emit('input', { payload: true }) } else { setNodeStatus('starting up...') setTimeout(() => setNodeStatus('inactive upon load'), 1000) } })() } RED.nodes.registerType('presence-faker', PresenceFakerNode) }