Node-Red configuration
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

suncron.js 8.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. const CronJob = require('cron').CronJob
  2. const dayjs = require('dayjs')
  3. const SunCalc = require('suncalc')
  4. module.exports = function (RED) {
  5. function SuncronNode(config) {
  6. RED.nodes.createNode(this, config)
  7. const node = this
  8. let schedule
  9. const eventTypes = [
  10. 'sunrise',
  11. 'sunriseEnd',
  12. 'goldenHourEnd',
  13. 'solarNoon',
  14. 'goldenHour',
  15. 'sunsetStart',
  16. 'sunset',
  17. 'dusk',
  18. 'nauticalDusk',
  19. 'night',
  20. 'nadir',
  21. 'nightEnd',
  22. 'nauticalDawn',
  23. 'dawn',
  24. ]
  25. let msgCrons = []
  26. let dailyCron = []
  27. const letsGo = function () {
  28. schedule = calcScheduleForToday()
  29. if (config.replay === true) {
  30. try {
  31. const mostRecentEvent = findMostRecentEvent(schedule)
  32. setTimeout(() => {
  33. ejectMsg(mostRecentEvent, schedule)
  34. }, 500)
  35. } catch (e) {
  36. debug(e)
  37. }
  38. }
  39. installMsgCronjobs(schedule)
  40. setTimeout(() => {
  41. ejectSchedule(schedule)
  42. }, 500)
  43. debug(schedule)
  44. dailyCron = installDailyCronjob()
  45. }
  46. const calcScheduleForToday = function () {
  47. const today = new Date()
  48. const midday = new Date(
  49. today.getFullYear(),
  50. today.getMonth(),
  51. today.getDate(),
  52. 12,
  53. 0,
  54. 0,
  55. 0,
  56. 0
  57. )
  58. const sunTimes = SunCalc.getTimes(midday, config.lat, config.lon)
  59. return eventTypes.reduce((result, eventType) => {
  60. const payload = config[`${eventType}Payload`]
  61. if (payload !== '') {
  62. const payloadType = config[`${eventType}PayloadType`]
  63. const topic = config[`${eventType}Topic`]
  64. const sunEventTime = dayjs(sunTimes[eventType])
  65. const offsetSec = config[`${eventType}Offset`]
  66. const offsetType = config[`${eventType}OffsetType`]
  67. const offset = offsetSec * offsetType
  68. let cronTime
  69. if (offset > 0) {
  70. cronTime = dayjs(sunEventTime).add(offsetSec, 'second')
  71. } else {
  72. cronTime = dayjs(sunEventTime).subtract(offsetSec, 'second')
  73. }
  74. try {
  75. result[eventType] = {
  76. event: eventType,
  77. sunEventTimeUTC: sunEventTime.toISOString(),
  78. sunEventTimeLocal: sunEventTime.format('YYYY-MM-DDTHH:mm:ss'),
  79. offset: offsetSec * offsetType,
  80. cronTime,
  81. cronTimeUTC: cronTime.toISOString(),
  82. cronTimeLocal: cronTime.format('YYYY-MM-DDTHH:mm:ss'),
  83. payload,
  84. payloadType,
  85. topic,
  86. }
  87. } catch (e) {
  88. console.log(
  89. `ignoring event type '${eventType}' as no event time could be determined for current day.`
  90. )
  91. }
  92. }
  93. return result
  94. }, {})
  95. }
  96. const formatSchedule = function (schedule) {
  97. let result = {}
  98. for (let eventType in schedule) {
  99. let event = schedule[eventType]
  100. result[eventType] = {
  101. event: eventType,
  102. sunEventTime: event.sunEventTimeLocal,
  103. cronTime: event.cronTimeLocal,
  104. offset: event.offset,
  105. }
  106. }
  107. return result
  108. }
  109. function findNextEvent(schedule) {
  110. let futureEvents = Object.keys(schedule)
  111. .map((eventType) => ({
  112. eventName: eventType,
  113. eventTime: schedule[eventType].cronTime,
  114. }))
  115. .sort((e1, e2) => e1.eventTime.unix() - e2.eventTime.unix())
  116. .filter((event) => event.eventTime.isAfter(dayjs()))
  117. if (futureEvents.length > 0) {
  118. return futureEvents.shift()
  119. } else {
  120. throw new Error('done for today')
  121. }
  122. }
  123. const installMsgCronjobs = function (schedule) {
  124. stopMsgCrons()
  125. let i = 0
  126. for (let eventType in schedule) {
  127. i++
  128. let event = schedule[eventType]
  129. // override cronTimes for debugging purpose
  130. if (RED.settings.suncronMockTimes) {
  131. event.cronTime = dayjs().add(i * 5, 'second')
  132. }
  133. let cron = new CronJob({
  134. cronTime: event.cronTime.toDate(),
  135. onTick: () => {
  136. ejectMsg(event, schedule)
  137. setNodeStatus(
  138. `now: ${event.event} @ ${event.cronTime.format('HH:mm')}`,
  139. 'green'
  140. )
  141. setTimeout(() => {
  142. setNodeStatusToNextEvent(schedule)
  143. }, 2000)
  144. },
  145. })
  146. try {
  147. cron.start()
  148. msgCrons.push(cron)
  149. } catch (err) {
  150. debug(`${event.event}: ${err.message}`)
  151. }
  152. }
  153. debug(`${i} msg crons installed`)
  154. setNodeStatus(`${i} crons active`)
  155. setTimeout(() => {
  156. setNodeStatusToNextEvent(schedule)
  157. }, 2000)
  158. }
  159. const installDailyCronjob = function () {
  160. // run daily cron 5 seconds past midnight (except for debugging: 5 seconds past the full minute)
  161. const cronTime = RED.settings.suncronMockTimes
  162. ? '5 * * * * *'
  163. : '5 0 0 * * *'
  164. const cron = new CronJob({
  165. cronTime,
  166. onTick: () => {
  167. schedule = calcScheduleForToday()
  168. installMsgCronjobs(schedule)
  169. ejectSchedule(schedule)
  170. setNodeStatusToNextEvent(schedule)
  171. },
  172. })
  173. cron.start()
  174. return cron
  175. }
  176. const debug = function (debugMsg) {
  177. if (RED.settings.suncronDebug) {
  178. node.warn(debugMsg)
  179. }
  180. }
  181. const setNodeStatus = function (text, color = 'grey') {
  182. node.status({
  183. fill: color,
  184. shape: 'dot',
  185. text: text,
  186. })
  187. }
  188. const setNodeStatusToNextEvent = function (schedule) {
  189. try {
  190. const nextEvent = findNextEvent(schedule)
  191. setNodeStatus(
  192. `next: ${nextEvent.eventName} @ ${nextEvent.eventTime.format(
  193. 'HH:mm'
  194. )}`
  195. )
  196. } catch (err) {
  197. setNodeStatus(err.message)
  198. }
  199. }
  200. const findMostRecentEvent = function (schedule) {
  201. let pastEvents = Object.keys(schedule)
  202. .map((eventType) => schedule[eventType])
  203. .sort((e1, e2) => e2.cronTime.unix() - e1.cronTime.unix())
  204. .filter((event) => event.cronTime.isBefore(dayjs()))
  205. if (pastEvents.length > 0) {
  206. return pastEvents.shift()
  207. } else {
  208. throw new Error('no past events')
  209. }
  210. }
  211. const stopMsgCrons = function () {
  212. if (msgCrons.length > 0) {
  213. msgCrons.forEach((cron) => {
  214. cron.stop()
  215. })
  216. debug(`${msgCrons.length} msg crons deleted`)
  217. msgCrons = []
  218. }
  219. }
  220. const ejectMsg = function ({ payload, payloadType, topic }, schedule) {
  221. const castPayload = (payload, payloadType) => {
  222. if (payloadType === 'num') {
  223. return Number(payload)
  224. } else if (payloadType === 'bool') {
  225. return payload === 'true'
  226. } else if (payloadType === 'json') {
  227. return JSON.parse(payload)
  228. } else {
  229. return payload
  230. }
  231. }
  232. let formatedSchedule = formatSchedule(schedule)
  233. let next
  234. try {
  235. next = formatedSchedule[findNextEvent(schedule).eventName]
  236. } catch (e) {
  237. next = null
  238. }
  239. node.send({
  240. topic,
  241. payload: castPayload(payload, payloadType),
  242. schedule: formatedSchedule,
  243. next,
  244. })
  245. }
  246. const ejectSchedule = function (schedule) {
  247. if (!config.ejectScheduleOnUpdate) {
  248. return
  249. }
  250. node.send({
  251. topic: 'suncron:schedule',
  252. payload: formatSchedule(schedule),
  253. })
  254. }
  255. node.on('input', function (msg, send, done) {
  256. send =
  257. send ||
  258. function () {
  259. node.send.apply(node, arguments)
  260. }
  261. if (typeof msg.payload === 'object') {
  262. // config object received as msg.payload
  263. debug(`!!!! CONFIG OBJECT RECEIVED !!!`)
  264. // debug(msg.payload)
  265. eventTypes.forEach((eventType) => {
  266. if (
  267. msg.payload.hasOwnProperty(eventType) &&
  268. Number.isInteger(msg.payload[eventType])
  269. ) {
  270. debug(`new offset for ${eventType}: ${msg.payload[eventType]}`)
  271. config[`${eventType}Offset`] = Math.abs(msg.payload[eventType])
  272. config[`${eventType}OffsetType`] =
  273. msg.payload[eventType] < 0 ? -1 : 1
  274. }
  275. })
  276. letsGo()
  277. } else {
  278. try {
  279. const mostRecentEvent = findMostRecentEvent(schedule)
  280. ejectMsg(mostRecentEvent, schedule)
  281. } catch (e) {
  282. debug(e)
  283. }
  284. }
  285. if (done) {
  286. done()
  287. }
  288. })
  289. node.on('close', function () {
  290. stopMsgCrons()
  291. dailyCron.stop()
  292. })
  293. ;(function () {
  294. // on startup:
  295. try {
  296. letsGo()
  297. } catch (e) {
  298. console.log(e)
  299. }
  300. })()
  301. }
  302. RED.nodes.registerType('suncron', SuncronNode)
  303. }