node-red-contrib-light-scheduler
Version:
Light Scheduler is a node-red node that provides a weekly schedule, and is mainly focused on controlling light in home automation scenarios.
172 lines (139 loc) • 6.05 kB
JavaScript
module.exports = function(RED) {
'use strict'
var path = require('path')
var util = require('util')
var scheduler = require('./lib/scheduler.js')
var isItDark = require('./lib/isitdark.js')
var LightScheduler = function(n) {
RED.nodes.createNode(this, n)
this.settings = RED.nodes.getNode(n.settings) // Get global settings
this.events = JSON.parse(n.events)
this.runningEvents = JSON.parse(n.events) // With possible randomness.
this.topic = n.topic
this.onPayload = n.onPayload
this.onPayloadType = n.onPayloadType
this.offPayload = n.offPayload
this.offPayloadType = n.offPayloadType
this.onlyWhenDark = n.onlyWhenDark
this.sunElevationThreshold = n.sunElevationThreshold ? n.sunElevationThreshold : 6
this.sunShowElevationInStatus = n.sunShowElevationInStatus | false
this.outputfreq = n.outputfreq ? n.outputfreq : 'output.statechange.startup'
this.scheduleRndMax = !isNaN(parseInt(n.scheduleRndMax)) ? parseInt(n.scheduleRndMax) : 0
this.scheduleRndMax = Math.max(Math.min(this.scheduleRndMax, 60), 0) // 0 -> 60 allowed
this.override = 'auto'
this.prevPayload = null
this.firstEval = true
this.manualTrigger = false
var node = this
function isEqual(a, b) {
// simpler and more what we want compared to RED.utils.compareObjects()
return JSON.stringify(a) === JSON.stringify(b)
}
function randomizeSchedule() {
function offsetMOD() {
var min = 0 - node.scheduleRndMax
var max = node.scheduleRndMax
return Math.floor(Math.random() * (max - min) + min)
}
node.runningEvents = node.events.map(event => {
var minutes = event.end.mod - event.start.mod
let new_event = JSON.parse(JSON.stringify(event))
// Prevent randomization of event that start and end at midnight to
// prevent "gaps" when the schedule continuous past midnight.
// TODO: Handle all events that is back-to-back after each other.
if (event.start.mod !== 0) new_event.start.mod = event.start.mod + offsetMOD()
if (event.end.mod !== 0) {
new_event.end.mod = event.end.mod + offsetMOD()
if (event.end.mod <= event.start.mod)
// Handle "too short events"
new_event.end.mod = event.start.mod + minutes // Events can overlap, but we don't need to care about that
}
return new_event
})
}
function evaluateNodeProperty(propName, propValue, propType, node, msg) {
try {
return RED.util.evaluateNodeProperty(propValue, propType, node, msg)
}
catch(err) {
node.warn('Failed to interpret ' + propName + '. Ignoring it!. Reason:' + err)
return msg.payload
}
}
function setState(out) {
var msg = {
topic: node.topic,
}
if (out) msg.payload = evaluateNodeProperty('onPayload', node.onPayload, node.onPayloadType, node, msg)
else msg.payload = evaluateNodeProperty('offPayload', node.offPayload, node.offPayloadType, node, msg)
var sunElevation = ''
if (node.sunShowElevationInStatus) {
sunElevation = ' Sun: ' + isItDark.getElevation(node).toFixed(1) + '°'
}
var overrideTxt = node.override == 'auto' ? '' : ' Override: ' + node.override
node.status({
fill: out ? 'green' : 'red',
shape: 'dot',
text: (out ? 'ON' : 'OFF') + sunElevation + overrideTxt,
})
// Only send anything if the state have changed, on trigger and when configured to output on a minutely basis.
if (node.manualTrigger || node.outputfreq == 'output.minutely' || !isEqual(msg.payload, node.prevPayload)) {
if (!node.firstEval) node.send(msg)
node.prevPayload = msg.payload
}
node.firstEval = false
node.manualTrigger = false
}
function evaluate() {
// Handle override state, if any.
if (node.override == 'stop') {
node.status({fill: 'gray', shape: 'dot', text: 'Override: Stopped!'})
return
}
if (node.override == 'on') return setState(true)
if (node.override == 'off') return setState(false)
var matchEvent = scheduler.matchSchedule(node)
if (node.override == 'schedule-only') return setState(matchEvent)
if (node.override == 'light-only') return setState(isItDark.isItDark(node))
// node.override == auto
if (!matchEvent) return setState(false)
if (node.onlyWhenDark) return setState(isItDark.isItDark(node))
return setState(true)
}
node.on('input', function(msg) {
msg.payload = msg.payload.toString() // Make sure we have a string.
if (msg.payload.match(/^(1|on|0|off|auto|stop|schedule-only|light-only|trigger)$/i)) {
if (msg.payload == '0') msg.payload = 'off'
if (msg.payload == '1') msg.payload = 'on'
// Store override, unless trigger
if (!msg.payload.match(/^(trigger)$/i)) node.override = msg.payload.toLowerCase()
else node.manualTrigger = true
evaluate()
} else node.warn('Failed to interpret incoming msg.payload. Ignoring it!')
})
// re-evaluate every minute
node.evalInterval = setInterval(evaluate, 60000)
// Run initially directly after start / deploy.
if (node.outputfreq != 'output.statechange') {
node.firstEval = false
setTimeout(evaluate, 1000)
}
node.on('close', function() {
clearInterval(node.evalInterval)
clearInterval(node.rndInterval)
})
if (node.scheduleRndMax > 0) {
randomizeSchedule()
// Randomize schedule every hour for now, could cause problems with large scheduleRndMax.
node.rndInterval = setInterval(randomizeSchedule, 1 * 60 * 60 * 1000)
}
}
RED.nodes.registerType('light-scheduler', LightScheduler)
RED.httpAdmin.get('/light-scheduler/js/*', function(req, res) {
var options = {
root: __dirname + '/static/',
dotfiles: 'deny',
}
res.sendFile(req.params[0], options)
})
}