123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349 |
- 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)
- }
|