350 рядки
8.7 KiB
JavaScript

const CronJob = require('cron').CronJob
const dayjs = require('dayjs')
const SunCalc = require('suncalc')
module.exports = function (RED) {
function SuncronNode(config) {
RED.nodes.createNode(this, config)
const node = this
let schedule
const eventTypes = [
'sunrise',
'sunriseEnd',
'goldenHourEnd',
'solarNoon',
'goldenHour',
'sunsetStart',
'sunset',
'dusk',
'nauticalDusk',
'night',
'nadir',
'nightEnd',
'nauticalDawn',
'dawn',
]
let msgCrons = []
let dailyCron = []
const letsGo = function () {
schedule = calcScheduleForToday()
if (config.replay === true) {
try {
const mostRecentEvent = findMostRecentEvent(schedule)
setTimeout(() => {
ejectMsg(mostRecentEvent, schedule)
}, 500)
} catch (e) {
debug(e)
}
}
installMsgCronjobs(schedule)
setTimeout(() => {
ejectSchedule(schedule)
}, 500)
debug(schedule)
dailyCron = installDailyCronjob()
}
const calcScheduleForToday = function () {
const today = new Date()
const midday = new Date(
today.getFullYear(),
today.getMonth(),
today.getDate(),
12,
0,
0,
0,
0
)
const sunTimes = SunCalc.getTimes(midday, config.lat, config.lon)
return eventTypes.reduce((result, eventType) => {
const payload = config[`${eventType}Payload`]
if (payload !== '') {
const payloadType = config[`${eventType}PayloadType`]
const topic = config[`${eventType}Topic`]
const sunEventTime = dayjs(sunTimes[eventType])
const offsetSec = config[`${eventType}Offset`]
const offsetType = config[`${eventType}OffsetType`]
const offset = offsetSec * offsetType
let cronTime
if (offset > 0) {
cronTime = dayjs(sunEventTime).add(offsetSec, 'second')
} else {
cronTime = dayjs(sunEventTime).subtract(offsetSec, 'second')
}
try {
result[eventType] = {
event: eventType,
sunEventTimeUTC: sunEventTime.toISOString(),
sunEventTimeLocal: sunEventTime.format('YYYY-MM-DDTHH:mm:ss'),
offset: offsetSec * offsetType,
cronTime,
cronTimeUTC: cronTime.toISOString(),
cronTimeLocal: cronTime.format('YYYY-MM-DDTHH:mm:ss'),
payload,
payloadType,
topic,
}
} catch (e) {
console.log(
`ignoring event type '${eventType}' as no event time could be determined for current day.`
)
}
}
return result
}, {})
}
const formatSchedule = function (schedule) {
let result = {}
for (let eventType in schedule) {
let event = schedule[eventType]
result[eventType] = {
event: eventType,
sunEventTime: event.sunEventTimeLocal,
cronTime: event.cronTimeLocal,
offset: event.offset,
}
}
return result
}
function findNextEvent(schedule) {
let futureEvents = Object.keys(schedule)
.map((eventType) => ({
eventName: eventType,
eventTime: schedule[eventType].cronTime,
}))
.sort((e1, e2) => e1.eventTime.unix() - e2.eventTime.unix())
.filter((event) => event.eventTime.isAfter(dayjs()))
if (futureEvents.length > 0) {
return futureEvents.shift()
} else {
throw new Error('done for today')
}
}
const installMsgCronjobs = function (schedule) {
stopMsgCrons()
let i = 0
for (let eventType in schedule) {
i++
let event = schedule[eventType]
// override cronTimes for debugging purpose
if (RED.settings.suncronMockTimes) {
event.cronTime = dayjs().add(i * 5, 'second')
}
let cron = new CronJob({
cronTime: event.cronTime.toDate(),
onTick: () => {
ejectMsg(event, schedule)
setNodeStatus(
`now: ${event.event} @ ${event.cronTime.format('HH:mm')}`,
'green'
)
setTimeout(() => {
setNodeStatusToNextEvent(schedule)
}, 2000)
},
})
try {
cron.start()
msgCrons.push(cron)
} catch (err) {
debug(`${event.event}: ${err.message}`)
}
}
debug(`${i} msg crons installed`)
setNodeStatus(`${i} crons active`)
setTimeout(() => {
setNodeStatusToNextEvent(schedule)
}, 2000)
}
const installDailyCronjob = function () {
// run daily cron 5 seconds past midnight (except for debugging: 5 seconds past the full minute)
const cronTime = RED.settings.suncronMockTimes
? '5 * * * * *'
: '5 0 0 * * *'
const cron = new CronJob({
cronTime,
onTick: () => {
schedule = calcScheduleForToday()
installMsgCronjobs(schedule)
ejectSchedule(schedule)
setNodeStatusToNextEvent(schedule)
},
})
cron.start()
return cron
}
const debug = function (debugMsg) {
if (RED.settings.suncronDebug) {
node.warn(debugMsg)
}
}
const setNodeStatus = function (text, color = 'grey') {
node.status({
fill: color,
shape: 'dot',
text: text,
})
}
const setNodeStatusToNextEvent = function (schedule) {
try {
const nextEvent = findNextEvent(schedule)
setNodeStatus(
`next: ${nextEvent.eventName} @ ${nextEvent.eventTime.format(
'HH:mm'
)}`
)
} catch (err) {
setNodeStatus(err.message)
}
}
const findMostRecentEvent = function (schedule) {
let pastEvents = Object.keys(schedule)
.map((eventType) => schedule[eventType])
.sort((e1, e2) => e2.cronTime.unix() - e1.cronTime.unix())
.filter((event) => event.cronTime.isBefore(dayjs()))
if (pastEvents.length > 0) {
return pastEvents.shift()
} else {
throw new Error('no past events')
}
}
const stopMsgCrons = function () {
if (msgCrons.length > 0) {
msgCrons.forEach((cron) => {
cron.stop()
})
debug(`${msgCrons.length} msg crons deleted`)
msgCrons = []
}
}
const ejectMsg = function ({ payload, payloadType, topic }, schedule) {
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 formatedSchedule = formatSchedule(schedule)
let next
try {
next = formatedSchedule[findNextEvent(schedule).eventName]
} catch (e) {
next = null
}
node.send({
topic,
payload: castPayload(payload, payloadType),
schedule: formatedSchedule,
next,
})
}
const ejectSchedule = function (schedule) {
if (!config.ejectScheduleOnUpdate) {
return
}
node.send({
topic: 'suncron:schedule',
payload: formatSchedule(schedule),
})
}
node.on('input', function (msg, send, done) {
send =
send ||
function () {
node.send.apply(node, arguments)
}
if (typeof msg.payload === 'object') {
// config object received as msg.payload
debug(`!!!! CONFIG OBJECT RECEIVED !!!`)
// debug(msg.payload)
eventTypes.forEach((eventType) => {
if (
msg.payload.hasOwnProperty(eventType) &&
Number.isInteger(msg.payload[eventType])
) {
debug(`new offset for ${eventType}: ${msg.payload[eventType]}`)
config[`${eventType}Offset`] = Math.abs(msg.payload[eventType])
config[`${eventType}OffsetType`] =
msg.payload[eventType] < 0 ? -1 : 1
}
})
letsGo()
} else {
try {
const mostRecentEvent = findMostRecentEvent(schedule)
ejectMsg(mostRecentEvent, schedule)
} catch (e) {
debug(e)
}
}
if (done) {
done()
}
})
node.on('close', function () {
stopMsgCrons()
dailyCron.stop()
})
;(function () {
// on startup:
try {
letsGo()
} catch (e) {
console.log(e)
}
})()
}
RED.nodes.registerType('suncron', SuncronNode)
}