Files
ArchiveCostWebapp/logic/node-red-data/node_modules/node-red-contrib-cron-plus/cronplus.js
T
huesser b518ae8edb
Build and Publish Site / docker (push) Successful in 23s
Workflow funktioniert nun wieder. Es gab Probleme nach Aenderungen.
ABER: Die Applikation funktioniert nur lokal. Die deployte Version geht noch nicht.
2026-07-03 13:24:08 +02:00

2440 lines
126 KiB
JavaScript

/*
MIT License
Copyright (c) 2019, 2020, 2021, 2022 Steve-Mcl
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
const cronstrue = require('cronstrue')
const cronosjs = require('cronosjs')
const prettyMs = require('pretty-ms')
const coordParser = require('coord-parser')
const SunCalc = require('suncalc2')
const path = require('path')
const fs = require('fs')
SunCalc.addTime(-18, 'nightEnd', 'nightStart')
SunCalc.addTime(-6, 'civilDawn', 'civilDusk')
SunCalc.addTime(6, 'morningGoldenHourEnd', 'eveningGoldenHourStart')
const PERMITTED_SOLAR_EVENTS = [
'nightEnd',
// "astronomicalDawn",
'nauticalDawn',
'civilDawn',
// "morningGoldenHourStart",
'sunrise',
'sunriseEnd',
'morningGoldenHourEnd',
'solarNoon',
'eveningGoldenHourStart',
'sunsetStart',
'sunset',
// "eveningGoldenHourEnd",
'civilDusk',
'nauticalDusk',
// "astronomicalDusk",
'nightStart',
'nadir'
]
// accepted commands using topic as the command & (in compatible cases, the payload is the schedule name)
// commands not supported by topic are : add/update & describe
const controlTopics = [
{ command: 'trigger', payloadIsName: true },
{ command: 'status', payloadIsName: true },
{ command: 'list', payloadIsName: true },
{ command: 'export', payloadIsName: true },
{ command: 'stop', payloadIsName: true },
{ command: 'stop-all', payloadIsName: false },
{ command: 'stop-all-dynamic', payloadIsName: false },
{ command: 'stop-all-static', payloadIsName: false },
{ command: 'pause', payloadIsName: true },
{ command: 'pause-all', payloadIsName: false },
{ command: 'pause-all-dynamic', payloadIsName: false },
{ command: 'pause-all-static', payloadIsName: false },
{ command: 'start', payloadIsName: true },
{ command: 'start-all', payloadIsName: false },
{ command: 'start-all-dynamic', payloadIsName: false },
{ command: 'start-all-static', payloadIsName: false },
{ command: 'clear', payloadIsName: false },
{ command: 'remove', payloadIsName: true },
{ command: 'delete', payloadIsName: true },
{ command: 'debug', payloadIsName: true },
{ command: 'next', payloadIsName: false }
]
const addExtendedControlTopics = function (baseCommand) {
controlTopics.push({ command: `${baseCommand}-all`, payloadIsName: false })
controlTopics.push({ command: `${baseCommand}-all-dynamic`, payloadIsName: false })
controlTopics.push({ command: `${baseCommand}-all-static`, payloadIsName: false })
controlTopics.push({ command: `${baseCommand}-active`, payloadIsName: false })
controlTopics.push({ command: `${baseCommand}-active-dynamic`, payloadIsName: false })
controlTopics.push({ command: `${baseCommand}-active-static`, payloadIsName: false })
controlTopics.push({ command: `${baseCommand}-inactive`, payloadIsName: false })
controlTopics.push({ command: `${baseCommand}-inactive-dynamic`, payloadIsName: false })
controlTopics.push({ command: `${baseCommand}-inactive-static`, payloadIsName: false })
}
addExtendedControlTopics('trigger')
addExtendedControlTopics('status')
addExtendedControlTopics('export')
addExtendedControlTopics('list')
addExtendedControlTopics('remove')
addExtendedControlTopics('delete')
addExtendedControlTopics('debug')
/**
* Humanize a cron express
* @param {string} expression the CRON expression to humanize
* @returns {string}
* A human readable version of the expression
*/
const humanizeCron = function (expression, locale) {
try {
const opt = { use24HourTimeFormat: true }
if (locale) opt.locale = locale
return cronstrue.toString(expression, opt)
} catch (error) {
return `Cannot parse expression '${expression}'`
}
}
/**
* Validate a schedule options. Returns true if OK otherwise throws an appropriate error
* @param {object} opt the options object to validate
* @param {boolean} permitDefaults allow certain items to be a default (missing value)
* @returns {boolean}
*/
function validateOpt (opt, permitDefaults = true) {
if (!opt) {
throw new Error('Schedule options are undefined')
}
if (!opt.name) {
throw new Error('Schedule name property missing')
}
if (!opt.expressionType || opt.expressionType === 'cron' || opt.expressionType === 'dates') { // cron
if (!opt.expression) {
throw new Error(`Schedule '${opt.name}' - expression property missing`)
}
let valid = false
try {
valid = cronosjs.validate(opt.expression)
if (valid) { opt.expressionType = 'cron' }
} catch (error) {
console.debug(error)
}
try {
if (!valid) {
valid = isDateSequence(opt.expression)
if (valid) { opt.expressionType = 'dates' }
}
} catch (error) {
console.debug(error)
}
if (!valid) {
throw new Error(`Schedule '${opt.name}' - expression '${opt.expression}' must be either a cron expression, a date, an a array of dates or a CSV of dates`)
}
} else if (opt.expressionType === 'solar') {
if (!opt.offset) {
opt.offset = 0
}
if (opt.locationType === 'fixed' || opt.locationType === 'env') {
// location comes from node
} else {
if (!opt.location) {
throw new Error(`Schedule '${opt.name}' - location property missing`)
}
}
if (opt.solarType !== 'selected' && opt.solarType !== 'all') {
throw new Error(`Schedule '${opt.name}' - solarType property invalid or mising. Must be either "all" or "selected"`)
}
if (opt.solarType === 'selected') {
if (!opt.solarEvents) {
throw new Error(`Schedule '${opt.name}' - solarEvents property missing`)
}
let solarEvents
if (typeof opt.solarEvents === 'string') {
solarEvents = opt.solarEvents.split(',')
} else if (Array.isArray(opt.solarEvents)) {
solarEvents = opt.solarEvents
} else {
throw new Error(`Schedule '${opt.name}' - solarEvents property is invalid`)
}
if (!solarEvents.length) {
throw new Error(`Schedule '${opt.name}' - solarEvents property is empty`)
}
for (let index = 0; index < solarEvents.length; index++) {
const element = solarEvents[index].trim()
if (!PERMITTED_SOLAR_EVENTS.includes(element)) {
throw new Error(`Schedule '${opt.name}' - solarEvents entry '${element}' is invalid`)
}
}
}
} else {
throw new Error(`Schedule '${opt.name}' - invalid schedule type '${opt.expressionType}'. Expected expressionType to be 'cron', 'dates' or 'solar'`)
}
if (permitDefaults) {
opt.payload = ((opt.payload === null || opt.payload === '') && opt.payloadType === 'num') ? 0 : opt.payload
opt.payload = ((opt.payload === null || opt.payload === '') && opt.payloadType === 'str') ? '' : opt.payload
opt.payload = ((opt.payload === null || opt.payload === '') && opt.payloadType === 'bool') ? false : opt.payload
}
if (!opt.payloadType === 'default' && opt.payload === null) {
throw new Error(`Schedule '${opt.name}' - payload property missing`)
}
const okTypes = ['default', 'flow', 'global', 'str', 'num', 'bool', 'json', 'jsonata', 'bin', 'date', 'env']
// eslint-disable-next-line eqeqeq
const typeOK = okTypes.find(el => { return el == opt.payloadType })
if (!typeOK) {
throw new Error(`Schedule '${opt.name}' - type property '${opt.payloadType}' is not valid. Must be one of the following... ${okTypes.join(',')}`)
}
return true
}
/**
* Tests if a string or array of date like items are a date or date sequence
* @param {String|Array} data An array of date like entries or a CSV string of dates
*/
function isDateSequence (data) {
try {
const ds = parseDateSequence(data)
return (ds && ds.isDateSequence)
// eslint-disable-next-line no-empty
} catch (error) { }
return false
}
/**
* Returns an object describing the parameters.
* @param {string} expression The expressions or coordinates to use
* @param {string} expressionType The expression type ("cron" | "solar" | "dates")
* @param {string} timeZone An optional timezone to use
* @param {number} offset An optional offset to apply
* @param {string} solarType Specifies either "all" or "selected" - related to solarEvents property
* @param {string} solarEvents a CSV of solar events to be included
* @param {date} time Optional time to use (defaults to Date.now() if excluded)
*/
function _describeExpression (expression, expressionType, timeZone, offset, solarType, solarEvents, time, opts) {
const now = time ? new Date(time) : new Date()
opts = opts || {}
let result = { description: undefined, nextDate: undefined, nextDescription: undefined, prettyNext: 'Never' }
const cronOpts = timeZone ? { timezone: timeZone } : undefined
let ds = null
let dsOk = false
let exOk = false
// let now = new Date();
if (solarType === 'all') {
solarEvents = PERMITTED_SOLAR_EVENTS.join(',')
}
if (expressionType === 'solar') {
const opt = {
locationType: opts.locationType || opts.defaultLocationType,
defaultLocationType: opts.defaultLocationType,
defaultLocation: opts.defaultLocation,
expressionType,
location: expression,
offset: offset || 0,
name: 'dummy',
solarType,
solarEvents,
payloadType: 'default',
payload: ''
}
if (validateOpt(opt)) {
const pos = coordParser(opt.location)
const offset = isNumber(opt.offset) ? parseInt(opt.offset) : 0
const nowOffset = new Date(now.getTime() - offset * 60000)
result = getSolarTimes(pos.lat, pos.lon, 0, solarEvents, now, offset)
// eslint-disable-next-line eqeqeq
if (opts.includeSolarStateOffset && offset != 0) {
const ssOffset = getSolarTimes(pos.lat, pos.lon, 0, solarEvents, nowOffset, 0)
result.solarStateOffset = ssOffset.solarState
}
result.offset = offset
result.now = now
result.nowOffset = nowOffset
ds = parseDateSequence(result.eventTimes.map((event) => event.timeOffset))
dsOk = ds && ds.isDateSequence
}
} else {
if (expressionType === 'cron' || expressionType === '') {
exOk = cronosjs.validate(expression)
} else {
ds = parseDateSequence(expression)
dsOk = ds.isDateSequence
}
if (!exOk && !dsOk) {
result.description = 'Invalid expression'
return result
}
}
if (dsOk) {
const task = ds.task
const dates = ds.dates
const dsFutureDates = dates.filter(d => d >= now)
const count = dsFutureDates ? dsFutureDates.length : 0
result.description = 'Date sequence with fixed dates'
if (task && task._sequence && count) {
result.nextDate = dsFutureDates[0]
const ms = result.nextDate.valueOf() - now.valueOf()
result.prettyNext = (result.nextEvent ? result.nextEvent + ' ' : '') + `in ${prettyMs(ms, { secondsDecimalDigits: 0, verbose: true })}`
if (expressionType === 'solar') {
if (solarType === 'all') {
result.description = 'All Solar Events'
} else {
result.description = "Solar Events: '" + solarEvents.split(',').join(', ') + "'"
}
} else {
if (count === 1) {
result.description = 'One time at ' + formatShortDateTimeWithTZ(result.nextDate, timeZone)
} else {
result.description = count + ' Date Sequences starting at ' + formatShortDateTimeWithTZ(result.nextDate, timeZone)
}
result.nextDates = dsFutureDates.slice(0, 5)
}
}
}
if (exOk) {
const ex = cronosjs.CronosExpression.parse(expression, cronOpts)
const next = ex.nextDate()
if (next) {
const ms = next.valueOf() - now.valueOf()
result.prettyNext = `in ${prettyMs(ms, { secondsDecimalDigits: 0, verbose: true })}`
try {
result.nextDates = ex.nextNDates(now, 5)
} catch (error) {
console.debug(error)
}
}
result.description = humanizeCron(expression)
result.nextDate = next
}
return result
}
/**
* Returns a formatted string based on the provided tz.
* If tz is not specified, then Date.toString() is used
* @param {Date | string | number} date The date to format
* @param {string} [tz] Timezone to use (exclude to use system)
* @returns {string}
* The formatted date or empty string if `date` is null|undefined
*/
function formatShortDateTimeWithTZ (date, tz) {
if (!date) {
return ''
}
let dateString
const o = {
timeZone: tz || undefined,
timeZoneName: 'short',
hourCycle: 'h23',
year: 'numeric',
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}
try {
dateString = new Intl.DateTimeFormat('default', o).format(new Date(date))
} catch (error) {
dateString = 'Error. Check timezone setting'
}
return dateString
}
/**
* Determine if a variable is a number
* @param {string|number} n The string or number to test
* @returns {boolean}
*/
function isNumber (n) {
return !isNaN(parseFloat(n)) && isFinite(n)
}
/**
* Determine if a variable is a valid object
* NOTE: Arrays are also objects - be sure to use Array.isArray if you need to know the difference
* @param {*} o The variable to test
* @returns {boolean}
*/
function isObject (o) {
return (typeof o === 'object' && o !== null)
}
/**
* Determine if a variable is a valid date
* @param {*} d The variable to test
* @returns {boolean}
*/
function isValidDateObject (d) {
return d instanceof Date && !isNaN(d)
}
/**
* Determine if a variable is a cron like string
* @param {string} expression The variable to test
* @returns {boolean}
*/
function isCronLike (expression) {
if (typeof expression !== 'string') return false
if (expression.includes('*')) return true
const cleaned = expression.replace(/\s\s+/g, ' ')
const spaces = cleaned.split(' ')
return spaces.length >= 4 && spaces.length <= 6
}
/**
* Apply defaults to the cron schedule object
* @param {integer} optionIndex An index number to use for defaults
* @param {object} option The option object to update
*/
function applyOptionDefaults (node, option, optionIndex) {
if (isObject(option) === false) {
return// no point in continuing
}
optionIndex = optionIndex == null ? 0 : optionIndex
// eslint-disable-next-line eqeqeq
if (option.expressionType == '') {
if (isDateSequence(option.expression)) {
option.expressionType = 'dates'
} else {
option.expressionType = 'cron'// if empty, default to cron
}
} else if (['cron', 'dates', 'solar'].indexOf(option.expressionType) < 0) {
// if expressionType is not cron or solar - it might be sunrise or sunset from an older version
if (option.expressionType === 'sunrise') {
option.solarEvents = option.solarEvents || 'sunrise'
option.expressionType = 'solar'
} else if (option.expressionType === 'sunset') {
option.solarEvents = option.solarEvents || 'sunset'
option.expressionType = 'solar'
} else {
option.expressionType = 'cron'
}
}
option.name = option.name || 'schedule' + (optionIndex + 1)
option.topic = option.topic || option.name
option.payloadType = option.payloadType || option.type
if (option.payloadType == null && typeof option.payload === 'string' && option.payload.length) {
option.payloadType = 'str'
}
option.payloadType = option.payloadType || 'default'
delete option.type
if (option.expressionType === 'cron' && !option.expression) option.expression = '0 * * * * * *'
if (option.expressionType === 'solar') {
if (!option.solarType) option.solarType = option.solarEvents ? 'selected' : 'all'
if (!option.solarEvents) option.solarEvents = 'sunrise,sunset'
if (!option.location) option.location = ''
option.locationType = node.defaultLocationType
}
}
function parseDateSequence (expression) {
const result = { isDateSequence: false, expression }
let dates = expression
if (typeof expression === 'string') {
const spl = expression.split(',')
for (let index = 0; index < spl.length; index++) {
spl[index] = spl[index].trim()
if (isCronLike(spl[index])) {
return result// fail
}
}
dates = spl.map(x => {
if (isNumber(x)) {
x = parseInt(x)
}
const d = new Date(x)
return d
})
}
const ds = new cronosjs.CronosTask(dates)
if (ds && ds._sequence) {
result.dates = ds._sequence._dates
result.task = ds
result.isDateSequence = true
}
return result
}
function parseSolarTimes (opt) {
// opt.location = location || ''
const pos = coordParser(opt.location || '0.0,0.0')
const offset = opt.offset ? parseInt(opt.offset) : 0
const date = opt.date ? new Date(opt.date) : new Date()
const events = opt.solarType === 'all' ? PERMITTED_SOLAR_EVENTS : opt.solarEvents
const result = getSolarTimes(pos.lat, pos.lon, 0, events, date, offset)
const task = parseDateSequence(result.eventTimes.map((o) => o.timeOffset))
task.solarEventTimes = result
return task
}
function getSolarTimes (lat, lng, elevation, solarEvents, startDate = null, offset = 0) {
// performance.mark('Start');
const solarEventsPast = [...PERMITTED_SOLAR_EVENTS]
const solarEventsFuture = [...PERMITTED_SOLAR_EVENTS]
const solarEventsArr = []
// get list of usable solar events into solarEventsArr
let solarEventsArrTemp = []
if (typeof solarEvents === 'string') {
solarEventsArrTemp = solarEvents.split(',')
} else if (Array.isArray(solarEvents)) {
solarEventsArrTemp = [...solarEvents]
} else {
throw new Error('solarEvents must be a CSV or Array')
}
for (let index = 0; index < solarEventsArrTemp.length; index++) {
const se = solarEventsArrTemp[index].trim()
if (PERMITTED_SOLAR_EVENTS.includes(se)) {
solarEventsArr.push(se)
}
}
offset = isNumber(offset) ? parseInt(offset) : 0
elevation = isNumber(elevation) ? parseInt(elevation) : 0// not used for now
startDate = startDate ? new Date(startDate) : new Date()
let scanDate = new Date(startDate.toDateString()) // new Date(startDate); //scanDate = new Date(startDate.toDateString())
scanDate.setDate(scanDate.getDate() + 1)// fwd one day to catch times behind of scan day
let loopMonitor = 0
const result = []
// performance.mark('initEnd')
// performance.measure('Start to Now', 'Start', 'initEnd')
// performance.mark('FirstScanStart');
// first scan backwards to get prior solar events
while (loopMonitor < 3 && solarEventsPast.length) {
loopMonitor++
const timesIteration1 = SunCalc.getTimes(scanDate, lat, lng)
// timesIteration1 = new SolarCalc(scanDate,lat,lng);
for (let index = 0; index < solarEventsPast.length; index++) {
const se = solarEventsPast[index]
const seTime = timesIteration1[se]
const seTimeOffset = new Date(seTime.getTime() + offset * 60000)
if (isValidDateObject(seTimeOffset) && seTimeOffset <= startDate) {
result.push({ event: se, time: seTime, timeOffset: seTimeOffset })
solarEventsPast.splice(index, 1)// remove that item
index--
}
}
scanDate.setDate(scanDate.getDate() - 1)
}
scanDate = new Date(startDate.toDateString())
scanDate.setDate(scanDate.getDate() - 1)// back one day to catch times ahead of current day
loopMonitor = 0
// now scan forwards to get future events
while (loopMonitor < 183 && solarEventsFuture.length) {
loopMonitor++
const timesIteration2 = SunCalc.getTimes(scanDate, lat, lng)
// timesIteration2 = new SolarCalc(scanDate,lat,lng);
for (let index = 0; index < solarEventsFuture.length; index++) {
const se = solarEventsFuture[index]
const seTime = timesIteration2[se]
const seTimeOffset = new Date(seTime.getTime() + offset * 60000)
if (isValidDateObject(seTimeOffset) && seTimeOffset > startDate) {
result.push({ event: se, time: seTime, timeOffset: seTimeOffset })
solarEventsFuture.splice(index, 1)// remove that item
index--
}
}
scanDate.setDate(scanDate.getDate() + 1)
}
// performance.mark('SecondScanEnd');
// performance.measure('FirstScanEnd to SecondScanEnd', 'FirstScanEnd', 'SecondScanEnd');
// sort the results to get a timeline
const sorted = result.sort((a, b) => {
if (a.time < b.time) {
return -1
} else if (a.time > b.time) {
return 1
} else {
return 0
}
})
// now scan through sorted solar events to determine day/night/twilight etc
let state = ''; const solarState = {}
for (let index = 0; index < sorted.length; index++) {
const event = sorted[index]
if (event.time < startDate) {
switch (event.event) {
case 'nightEnd':
state = 'Astronomical Twilight'// todo: i18n
updateSolarState(solarState, state, 'rise', false, false, true, false, false, false, false)
break
// case "astronomicalDawn":
// state = "Astronomical Twilight";//todo: i18n
// updateSolarState(solarState,state,"rise",false,false,true,false,false,false,false);
// break;
case 'nauticalDawn':
state = 'Nautical Twilight'
updateSolarState(solarState, state, 'rise', false, false, false, true, false, false, false)
break
case 'civilDawn':
state = 'Civil Twilight'
updateSolarState(solarState, state, 'rise', false, false, false, false, true, true, false)
break
// case "morningGoldenHourStart":
// updateSolarState(solarState,null,"rise",false,false,false,false,true,true,false);
// break;
case 'sunrise':
state = 'Civil Twilight'
updateSolarState(solarState, state, 'rise', false, false, false, false, true, true, false)
break
case 'sunriseEnd':
state = 'Day'
updateSolarState(solarState, state, 'rise', true, false, false, false, false, true, false)
break
case 'morningGoldenHourEnd':
state = 'Day'
updateSolarState(solarState, state, 'rise', true, false, false, false, false, false, false)
break
case 'solarNoon':
updateSolarState(solarState, null, 'fall')
break
case 'eveningGoldenHourStart':
state = 'Day'
updateSolarState(solarState, state, 'fall', true, false, false, false, false, false, true)
break
case 'sunsetStart':
state = 'Day'
updateSolarState(solarState, state, 'fall', true, false, false, false, false, false, true)
break
case 'sunset':
state = 'Civil Twilight'
updateSolarState(solarState, state, 'fall', false, false, false, false, true, false, true)
break
// case "eveningGoldenHourEnd":
// state = "Nautical Twilight";
// updateSolarState(solarState,state,"fall",false,false,false,false,true,false,false);
// break;
case 'civilDusk':
state = 'Nautical Twilight'
updateSolarState(solarState, state, 'fall', false, false, false, true, false, false, false)
break
case 'nauticalDusk':
state = 'Astronomical Twilight'
updateSolarState(solarState, state, 'fall', false, false, true, false, false, false, false)
break
// case "astronomicalDusk":
case 'night':
case 'nightStart':
state = 'Night'
updateSolarState(solarState, state, 'fall', false, true, false, false, false, false, false)
break
case 'nadir':
updateSolarState(solarState, null, 'rise')
break
}
} else {
break
}
}
// update final states
updateSolarState(solarState)// only sending `stateObject` makes updateSolarState() compute dawn/dusk etc
// now filter to only events of interest
const futureEvents = sorted.filter((e) => e && e.timeOffset >= startDate)
const wantedFutureEvents = []
for (let index = 0; index < futureEvents.length; index++) {
const fe = futureEvents[index]
if (solarEventsArr.includes(fe.event)) {
wantedFutureEvents.push(fe)
}
}
const nextType = wantedFutureEvents[0].event
const nextTime = wantedFutureEvents[0].time
const nextTimeOffset = wantedFutureEvents[0].timeOffset
// performance.mark('End')
// performance.measure('SecondScanEnd to End', 'SecondScanEnd', 'End')
// performance.measure('Start to End', 'Start', 'End')
return {
solarState,
nextEvent: nextType,
nextEventTime: nextTime,
nextEventTimeOffset: nextTimeOffset,
eventTimes: wantedFutureEvents
// allTimes: sorted,
// eventTimesByType: resultCategories
}
function updateSolarState (stateObject, state, direction, day, night,
astrologicalTwilight, nauticalTwilight, civilTwilight,
morningGoldenHour, eveningGoldenHour) {
if (arguments.length > 1) {
if (state) stateObject.state = state
stateObject.direction = direction
if (arguments.length > 3) {
stateObject.day = day
stateObject.night = night
stateObject.astrologicalTwilight = astrologicalTwilight
stateObject.nauticalTwilight = nauticalTwilight
stateObject.civilTwilight = civilTwilight
stateObject.goldenHour = morningGoldenHour || eveningGoldenHour
stateObject.twilight = stateObject.astrologicalTwilight || stateObject.nauticalTwilight || stateObject.civilTwilight
}
return
}
stateObject.morningTwilight = stateObject.direction === 'rise' && stateObject.twilight
stateObject.eveningTwilight = stateObject.direction === 'fall' && stateObject.twilight
stateObject.dawn = stateObject.direction === 'rise' && stateObject.civilTwilight
stateObject.dusk = stateObject.direction === 'fall' && stateObject.civilTwilight
stateObject.morningGoldenHour = stateObject.direction === 'rise' && stateObject.goldenHour
stateObject.eveningGoldenHour = stateObject.direction === 'fall' && stateObject.goldenHour
}
}
function exportTask (task, includeStatus) {
const o = {
topic: task.node_topic || task.name,
name: task.name || task.node_topic,
index: task.node_index,
payloadType: task.node_payloadType,
payload: task.node_payload,
limit: task.node_limit || null,
expressionType: task.node_expressionType
}
if (o.expressionType === 'solar') {
o.solarType = task.node_solarType
o.solarEvents = task.node_solarEvents
o.location = task.node_location
o.offset = task.node_offset
} else {
o.expression = task.node_expression
}
if (includeStatus) {
o.isDynamic = task.isDynamic === true
o.modified = task.node_modified === true
o.isRunning = task.isRunning === true
o.count = task.node_count
}
return o
}
function isTaskFinished (_task) {
if (!_task) return true
return _task.node_limit ? _task.node_count >= _task.node_limit : false
}
function getTaskStatus (node, task, opts) {
opts = opts || {}
opts.locationType = node.defaultLocationType
opts.defaultLocation = node.defaultLocation
opts.defaultLocationType = node.defaultLocationType
const sol = task.node_expressionType === 'solar'
const exp = sol ? task.node_location : task.node_expression
const h = _describeExpression(exp, task.node_expressionType, node.timeZone, task.node_offset, task.node_solarType, task.node_solarEvents, null, opts)
let nextDescription = null
let nextDate = null
const running = !isTaskFinished(task)
if (running) {
// nextDescription = h.nextDescription;
nextDescription = h.prettyNext
nextDate = sol ? h.nextEventTimeOffset : h.nextDate
}
let tz = node.timeZone
let localTZ = ''
try {
localTZ = Intl.DateTimeFormat().resolvedOptions().timeZone
if (!tz) tz = localTZ
// eslint-disable-next-line no-empty
} catch (error) { }
const r = {
type: task.isDynamic ? 'dynamic' : 'static',
modified: !!task.modified,
isRunning: running && task.isRunning,
count: task.node_count,
limit: task.node_limit,
nextDescription,
nextDate: running ? nextDate : null,
nextDateTZ: running ? formatShortDateTimeWithTZ(nextDate, tz) : null,
timeZone: tz,
serverTime: new Date(),
serverTimeZone: localTZ,
description: h.description
}
if (sol) {
r.solarState = h.solarState
if (h.offset) r.solarStateOffset = h.solarStateOffset
r.solarTimes = running ? h.eventTimes : null
r.nextDescription = running ? nextDescription : null// r.solarTimes && (r.solarTimes[0].event + " " + r.nextDescription);
}
return r
}
let userDir = ''
let persistPath = ''
let FSAvailable = false
let contextAvailable = false
const cronplusDir = 'cronplusdata'
module.exports = function (RED) {
const STORE_NAMES = getStoreNames()
// when running tests, RED.settings.userDir & RED.settings.settingsFile (amongst others) are undefined
const testMode = typeof RED.settings.userDir === 'undefined' && typeof RED.settings.settingsFile === 'undefined'
if (testMode) {
FSAvailable = false
contextAvailable = false
} else {
userDir = RED.settings.userDir || ''
persistPath = path.join(userDir, cronplusDir)
try {
if (!fs.existsSync(persistPath)) {
fs.mkdirSync(persistPath)
}
FSAvailable = fs.existsSync(persistPath)
} catch (e) {
if (e.code !== 'EEXIST') {
RED.log.error(`cron-plus: Error creating persistence folder '${persistPath}'. ${e.message}`)
FSAvailable = false
}
}
contextAvailable = STORE_NAMES.length > 2 // 1st 2 are 'none' and 'file', any others are context stores
}
function CronPlus (config) {
RED.nodes.createNode(this, config)
const node = this
node.name = config.name
node.payloadType = config.payloadType || config.type || 'default'
delete config.type
node.payload = config.payload
node.crontab = config.crontab
node.outputField = config.outputField || 'payload'
node.timeZone = config.timeZone
node.options = config.options
node.commandResponseMsgOutput = config.commandResponseMsgOutput || 'output1'
node.defaultLocation = config.defaultLocation
node.defaultLocationType = config.defaultLocationType
node.outputs = 1
node.fanOut = false
node.queuedSerialisationRequest = null
node.serialisationRequestBusy = null
setInterval(async function () {
if (node.serialisationRequestBusy) return
if (node.queuedSerialisationRequest) {
node.serialisationRequestBusy = node.queuedSerialisationRequest
await serialise()
node.queuedSerialisationRequest = null
node.serialisationRequestBusy = null
}
}, 2500) // 2.5 seconds
// convert node.persistDynamic to a getter/setter
Object.defineProperty(node, 'persistDynamic', {
get: function () {
return !!node.storeName
}
})
// inherit/upgrade deprecated properties from config
const hasStoreNameProperty = Object.prototype.hasOwnProperty.call(config, 'storeName') && typeof config.storeName === 'string'
const hasDeprecatedPersistDynamic = Object.prototype.hasOwnProperty.call(config, 'persistDynamic') && typeof config.persistDynamic === 'boolean'
if (hasStoreNameProperty) {
// not an upgrade - let use this property
node.storeName = config.storeName
} else if (hasDeprecatedPersistDynamic) {
// upgrade from older version
node.storeName = config.persistDynamic ? 'file' : ''
} else {
// default
node.storeName = ''
}
if (node.storeName && node.storeName !== 'file' && STORE_NAMES.indexOf(node.storeName) < 0) {
node.warn(`Invalid store name specified '${node.storeName}' - state will not be persisted for this node`)
contextAvailable = false
}
if (config.commandResponseMsgOutput === 'output2') {
node.outputs = 2 // 1 output pins (all messages), 2 outputs (schedules out of pin1, command responses out of pin2)
} else if (config.commandResponseMsgOutput === 'fanOut') {
node.outputs = 2 + (node.options ? node.options.length : 0)
node.fanOut = true
} else {
config.commandResponseMsgOutput = 'output1'
}
node.statusUpdatePending = false
const MAX_CLOCK_DIFF = Number(RED.settings.CRONPLUS_MAX_CLOCK_DIFF || process.env.CRONPLUS_MAX_CLOCK_DIFF || 5000)
const clockMonitor = setInterval(async function timeChecker () {
const oldTime = timeChecker.oldTime || new Date()
const newTime = new Date()
const timeDiff = newTime - oldTime
timeChecker.oldTime = newTime
if (Math.abs(timeDiff) >= MAX_CLOCK_DIFF) {
node.log('System Time Change Detected - refreshing schedules! If the system time was not changed then this typically occurs due to blocking code elsewhere in your application')
await refreshTasks(node)
}
}, 1000)
const setProperty = function (msg, field, value) {
const set = (obj, path, val) => {
const keys = path.split('.')
const lastKey = keys.pop()
// eslint-disable-next-line no-return-assign
const lastObj = keys.reduce((obj, key) =>
obj[key] = obj[key] || {},
obj)
lastObj[lastKey] = val
}
set(msg, field, value)
}
const updateNodeNextInfo = (node, now) => {
const t = getNextTask(node.tasks)
if (t) {
const indicator = t.isDynamic ? 'ring' : 'dot'
const nx = (t._expression || t._sequence)
node.nextDate = nx.nextDate(now)
node.nextEvent = t.name
node.nextIndicator = indicator
if (t.node_solarEventTimes && t.node_solarEventTimes.nextEvent) {
node.nextEvent = t.node_solarEventTimes.nextEvent
}
} else {
node.nextDate = null
node.nextEvent = ''
node.nextIndicator = ''
}
}
const updateDoneStatus = (node, task) => {
let indicator = 'dot'
if (task) {
indicator = node.nextIndicator || 'dot'
}
node.status({ fill: 'green', shape: indicator, text: 'Done: ' + formatShortDateTimeWithTZ(Date.now(), node.timeZone) })
// node.nextDate = getNextTask(node.tasks);
const now = new Date()
updateNodeNextInfo(node, now)
const next = node.nextDate ? new Date(node.nextDate).valueOf() : (Date.now() + 5001)
const msTillNext = next - now
if (msTillNext > 5000) {
node.statusUpdatePending = true
setTimeout(function () {
node.statusUpdatePending = false
updateNextStatus(node, true)
}, 4000)
}
}
const sendMsg = async (node, task, cronTimestamp, manualTrigger) => {
const msg = { cronplus: {} }
msg.topic = task.node_topic
msg.cronplus.triggerTimestamp = cronTimestamp
const se = task.node_expressionType === 'solar' ? node.nextEvent : ''
msg.cronplus.status = getTaskStatus(node, task, { includeSolarStateOffset: true })
if (se) msg.cronplus.status.solarEvent = se
msg.cronplus.config = exportTask(task)
if (manualTrigger) msg.manualTrigger = true
msg.scheduledEvent = !msg.manualTrigger
const indicator = node.nextIndicator || 'dot'
const taskType = task.isDynamic ? 'dynamic' : 'static'
const index = task.node_index || 0
node.status({ fill: 'green', shape: indicator, text: 'Schedule Started' })
try {
if (task.node_payloadType !== 'flow' && task.node_payloadType !== 'global') {
let pl
if ((task.node_payloadType == null && task.node_payload === '') || task.node_payloadType === 'date') {
pl = Date.now()
} else if (task.node_payloadType == null) {
pl = task.node_payload
} else if (task.node_payloadType === 'none') {
pl = ''
} else if (task.node_payloadType === 'json' && isObject(task.node_payload)) {
pl = task.node_payload
} else if (task.node_payloadType === 'bin' && Array.isArray(task.node_payload)) {
pl = Buffer.from(task.node_payload)
} else if (task.node_payloadType === 'default') {
pl = msg.cronplus
delete msg.cronplus // To delete or not?
} else {
pl = await evaluateNodeProperty(task.node_payload, task.node_payloadType, node, msg)
}
setProperty(msg, node.outputField, pl)
node.send(generateSendMsg(node, msg, taskType, index))
updateDoneStatus(node, task)
} else {
const res = await evaluateNodeProperty(task.node_payload, task.node_payloadType, node, msg)
setProperty(msg, node.outputField, res)
node.send(generateSendMsg(node, msg, taskType, index))
updateDoneStatus(node, task)
}
} catch (err) {
node.error(err, msg)
}
}
(async function () {
try {
node.status({})
node.nextDate = null
if (!node.options) {
node.status({ fill: 'grey', shape: 'dot', text: 'Nothing set' })
return
}
node.tasks = []
for (let iOpt = 0; iOpt < node.options.length; iOpt++) {
const opt = node.options[iOpt]
opt.name = opt.name || opt.topic
node.statusUpdatePending = true// prevent unnecessary status updates while loading
await createTask(node, opt, iOpt, true)
}
// now load dynamic schedules from file
await deserialise()
setTimeout(() => {
updateNextStatus(node, true)
}, 200)
} catch (err) {
if (node.tasks) {
node.tasks.forEach(task => task.stop())
}
node.status({ fill: 'red', shape: 'dot', text: 'Error creating schedule' })
node.error(err)
}
})()
node.on('close', async function (done) {
try {
await serialise()
} catch (error) {
node.error(error)
}
deleteAllTasks(this)
if (clockMonitor) clearInterval(clockMonitor)
if (done && typeof done === 'function') done()
})
this.on('input', async function (msg, send, done) {
send = send || function () { node.send.apply(node, arguments) }
done = done || function (err) {
if (err) {
node.error(err, msg)
}
}
// is this an button press?...
if (!msg.payload && !msg.topic) { // TODO: better method of differentiating between bad input and button press
await sendMsg(node, node.tasks[0], Date.now(), true)
done()
return
}
const controlTopic = controlTopics.find(ct => ct.command === msg.topic)
let payload = msg.payload
if (controlTopic) {
if (controlTopic.payloadIsName) {
if (!payload || typeof payload !== 'string') {
node.error(`Invalid payload! Control topic '${msg.topic}' expects the name of the schedule to be in msg.payload`, msg)
return
}
// emulate the cmd object
payload = {
command: controlTopic.command,
name: payload
}
} else {
payload = {
command: controlTopic.command
}
}
}
if (typeof payload !== 'object') {
return
}
try {
let input = payload
if (Array.isArray(payload) === false) {
input = [input]
}
const sendCommandResponse = function (msg) {
send(generateSendMsg(node, msg, 'command-response'))
}
for (let i = 0; i < input.length; i++) {
const cmd = input[i]
const action = cmd.command || ''
// let newMsg = {topic: msg.topic, payload:{command:cmd, result:{}}};
const newMsg = RED.util.cloneMessage(msg)
newMsg.payload = { command: cmd, result: {} }
const cmdAll = action.endsWith('-all')
const cmdAllStatic = action.endsWith('-all-static')
const cmdAllDynamic = action.endsWith('-all-dynamic')
const cmdActive = action.endsWith('-active')
const cmdInactive = action.endsWith('-inactive')
const cmdActiveDynamic = action.includes('-active-dynamic')
const cmdActiveStatic = action.includes('-active-static')
const cmdInactiveDynamic = action.includes('-inactive-dynamic')
const cmdInactiveStatic = action.includes('-inactive-static')
let cmdFilter = null
const actionParts = action.split('-')
let mainAction = actionParts[0]
if (actionParts.length > 1) mainAction += '-'
if (cmdAllDynamic) {
cmdFilter = 'dynamic'
} else if (cmdAllStatic) {
cmdFilter = 'static'
} else if (cmdActive) {
cmdFilter = 'active'
} else if (cmdInactive) {
cmdFilter = 'inactive'
} else if (cmdActiveDynamic) {
cmdFilter = 'active-dynamic'
} else if (cmdActiveStatic) {
cmdFilter = 'active-static'
} else if (cmdInactiveDynamic) {
cmdFilter = 'inactive-dynamic'
} else if (cmdInactiveStatic) {
cmdFilter = 'inactive-static'
}
switch (mainAction) {
case 'trigger': // single
{
const tt = getTask(node, cmd.name)
if (!tt) throw new Error(`Manual Trigger failed. Cannot find schedule named '${cmd.name}'`)
await sendMsg(node, tt, Date.now(), true)
}
break
case 'trigger-': // multiple
if (node.tasks) {
for (let index = 0; index < node.tasks.length; index++) {
const task = node.tasks[index]
if (task && (cmdAll || taskFilterMatch(task, cmdFilter))) {
await sendMsg(node, task, Date.now(), true)
}
}
}
break
case 'describe': // single
{
const exp = (cmd.expressionType === 'solar') ? cmd.location : cmd.expression
applyOptionDefaults(node, cmd)
newMsg.payload.result = _describeExpression(exp, cmd.expressionType, cmd.timeZone || node.timeZone, cmd.offset, cmd.solarType, cmd.solarEvents, cmd.time, { includeSolarStateOffset: true, locationType: node.node_locationType })
sendCommandResponse(newMsg)
}
break
case 'list': // single
case 'status': // single
{
const task = getTask(node, cmd.name)
if (task) {
newMsg.payload.result.config = exportTask(task, true)
newMsg.payload.result.status = getTaskStatus(node, task, { includeSolarStateOffset: true })
} else {
newMsg.error = `${cmd.name} not found`
}
sendCommandResponse(newMsg)
}
updateNextStatus(node, true)
break
case 'export': // single
{
const task = getTask(node, cmd.name)
if (task) {
newMsg.payload.result = exportTask(task, false)
} else {
newMsg.error = `${cmd.name} not found`
}
sendCommandResponse(newMsg)
}
break
case 'list-': // multiple
case 'status-': // multiple
{
const results = []
if (node.tasks) {
for (let index = 0; index < node.tasks.length; index++) {
const task = node.tasks[index]
if (task && (cmdAll || taskFilterMatch(task, cmdFilter))) {
const result = {}
result.config = exportTask(task, true)
result.status = getTaskStatus(node, task, { includeSolarStateOffset: true })
results.push(result)
}
}
}
newMsg.payload.result = results
sendCommandResponse(newMsg)
}
break
case 'export-': // multiple
{
const results = []
if (node.tasks) {
for (let index = 0; index < node.tasks.length; index++) {
const task = node.tasks[index]
if (cmdAll || taskFilterMatch(task, cmdFilter)) {
results.push(exportTask(task, false))
}
}
}
newMsg.payload.result = results
sendCommandResponse(newMsg)
}
break
case 'add': // single
case 'update': // single
await updateTask(node, cmd, msg)
updateNextStatus(node, true)
requestSerialisation()// update persistence
break
case 'clear':
case 'remove-': // multiple
case 'delete-': // multiple
deleteAllTasks(node, cmdFilter)
updateNextStatus(node, true)
requestSerialisation()// update persistence
break
case 'remove': // single
case 'delete': // single
deleteTask(node, cmd.name)
updateNextStatus(node, true)
requestSerialisation()// update persistence
break
case 'start': // single
await startTaskByName(node, cmd.name)
updateNextStatus(node, true)
requestSerialisation()// update persistence
break
case 'start-': // multiple
await startAllTasks(node, cmdFilter)
updateNextStatus(node, true)
requestSerialisation()// update persistence
break
case 'stop': // single
case 'pause': // single
stopTask(node, cmd.name, cmd.command === 'stop')
updateNextStatus(node, true)
requestSerialisation()// update persistence
break
case 'stop-': // multiple
case 'pause-': {
const resetCounter = cmd.command.startsWith('stop-')
stopAllTasks(node, resetCounter, cmdFilter)
updateNextStatus(node, true)
requestSerialisation()// update persistence
}
break
case 'next':
if (node.tasks && node.tasks.length) {
// gather statuses
const statuses = []
for (let index = 0; index < node.tasks.length; index++) {
const task = node.tasks[index]
const result = {}
result.config = exportTask(task, true)
result.status = getTaskStatus(node, task, { includeSolarStateOffset: true })
statuses.push(result)
}
const next = statuses.length && statuses.reduce((a, b) => a.status.nextDate < b.status.nextDate ? a : b)
if (next) {
newMsg.payload = {
name: next.config.name,
topic: next.config.topic,
next: next.status.nextDate,
nextLocal: next.status.nextDateTZ,
timeZone: next.status.serverTimeZone,
when: next.status.description,
msUntil: next.status.nextDate.valueOf() - next.status.serverTime.valueOf(),
description: next.status.nextDescription
}
} else {
newMsg.payload = {}
}
} else {
newMsg.payload = {}
}
sendCommandResponse(newMsg)
break
case 'debug': {
const task = getTask(node, cmd.name)
const thisDebug = getTaskStatus(node, task, { includeSolarStateOffset: true })
thisDebug.name = task.name
thisDebug.topic = task.node_topic
thisDebug.expressionType = task.node_expressionType
thisDebug.expression = task.node_expression
thisDebug.location = task.node_location
thisDebug.offset = task.node_offset
thisDebug.solarType = task.node_solarType
thisDebug.solarEvents = task.node_solarEvents
newMsg.payload = thisDebug
sendCommandResponse(newMsg)
}
break
case 'debug-': { // multiple
const results = []
if (node.tasks) {
for (let index = 0; index < node.tasks.length; index++) {
const task = node.tasks[index]
if (cmdAll || taskFilterMatch(task, cmdFilter)) {
const thisDebug = getTaskStatus(node, task, { includeSolarStateOffset: true })
thisDebug.name = task.name
thisDebug.topic = task.node_topic
thisDebug.expressionType = task.node_expressionType
thisDebug.expression = task.node_expression
thisDebug.location = task.node_location
thisDebug.offset = task.node_offset
thisDebug.solarType = task.node_solarType
thisDebug.solarEvents = task.node_solarEvents
results.push(thisDebug)
}
}
}
newMsg.payload = results
sendCommandResponse(newMsg)
}
break
}
}
done()
} catch (error) {
done(error)
// node.error(error,msg);
}
})
/**
* Get a task by name
* @param {Object} node - the cronplus node
* @param {*} name - the name of the task
* @returns {cronosjs.CronosTask} the found task or null
*/
function getTask (node, name) {
const task = node.tasks.find(function (task) {
return task.name === name
})
return task
}
async function refreshTasks (node, tasks) {
const _tasks = tasks || node.tasks
if (_tasks && _tasks.length) {
try {
const tasksToRefresh = _tasks.filter(function (task) {
return task._sequence || (task.isRunning && task._expression && !isTaskFinished(task))
})
if (!tasksToRefresh || !tasksToRefresh.length) {
return null
}
for (let index = 0; index < _tasks.length; index++) {
const task = _tasks[index]
if (task.node_expressionType === 'cron') {
task.stop()
task.start()
} else {
await updateTask(node, task.node_opt, null)
}
}
} catch (e) { }
updateNextStatus(node)
}
}
function taskFilterMatch (task, filter) {
if (!task) return false
// eslint-disable-next-line eqeqeq
const isActive = function (task) { return isTaskFinished(task) == false && task.isRunning == true }
// eslint-disable-next-line eqeqeq
const isInactive = function (task) { return isTaskFinished(task) || task.isRunning == false }
// eslint-disable-next-line eqeqeq
const isStatic = function (task) { return (task.isStatic == true || task.isDynamic == false) }
// eslint-disable-next-line eqeqeq
const isDynamic = function (task) { return (task.isDynamic == true || task.isStatic == false) }
switch (filter) {
case 'all':
return true
case 'static':
return isStatic(task)
case 'dynamic':
return isDynamic(task)
case 'active':
return isActive(task)
case 'inactive':
return isInactive(task)
case 'active-dynamic':
return isActive(task) && isDynamic(task)
case 'active-static':
return isActive(task) && isStatic(task)
case 'inactive-dynamic':
return isInactive(task) && isDynamic(task)
case 'inactive-static':
return isInactive(task) && isStatic(task)
}
return false
}
function stopTask (node, name, resetCounter) {
const task = getTask(node, name)
if (task) {
task.stop()
if (resetCounter) { task.node_count = 0 }
}
return task
}
function stopAllTasks (node, resetCounter, filter) {
if (node.tasks) {
for (let index = 0; index < node.tasks.length; index++) {
const task = node.tasks[index]
if (task) {
let skip = false
if (filter) skip = (taskFilterMatch(task, filter) === false)
if (!skip) {
task.stop()
if (resetCounter) { task.node_count = 0 }
}
}
}
}
}
async function startTaskByName (node, name) {
const task = getTask(node, name)
return await startTask(node, task)
}
/**
* Start a cron task
* @param {Object} node - the cronplus node
* @param {cronosjs.CronosTask} task - the task to start
* @returns {Promise<cronosjs.CronosTask>} the started task
*/
async function startTask (node, task) {
if (task) {
try {
if (isTaskFinished(task)) {
task.node_count = 0
}
task.stop()// prevent bug where calling start without first calling stop causes events to bunch up
task.start()
// lets see if this is a solar task that failed to start...
if (!task.isRunning && task.node_opt && task.node_opt.expressionType === 'solar') {
// solar tasks that are paused when its _time_ passes miss the call to `update`
// which is responsible for generating the next occurrence
// lets try updating it now
await updateTask(node, task.node_opt, null)
}
} catch (e) {
node.debug(`startTask error: ${e.message}`)
}
}
return task
}
async function startAllTasks (node, filter) {
const promises = []
if (node.tasks) {
const taskNames = node.tasks.map(t => t.name)
for (const name of taskNames) {
const task = getTask(node, name)
let skip = false
if (filter) skip = (taskFilterMatch(task, filter) === false)
if (!skip && task) {
promises.push(startTaskByName(node, name))
}
}
}
return Promise.all(promises)
}
function deleteAllTasks (node, filter) {
if (node.tasks) {
for (let index = 0; index < node.tasks.length; index++) {
try {
const task = node.tasks[index]
if (task) {
let skip = false
if (filter) skip = (taskFilterMatch(task, filter) === false)
if (!skip) {
_deleteTask(task)
node.tasks[index] = null
node.tasks.splice(index, 1)
index--
}
}
// eslint-disable-next-line no-empty
} catch (error) { }
}
}
}
function deleteTask (node, name) {
let task = getTask(node, name)
if (task) {
_deleteTask(task)
node.tasks = node.tasks.filter(t => t && t.name !== name)
task = null
}
}
/**
* Delete a task
* @param {cronosjs.CronosTask} task - the task to delete
*/
function _deleteTask (task) {
try {
task.off('run')
task.off('ended')
task.off('started')
task.off('stopped')
task.stop()
task = null
// eslint-disable-next-line no-empty
} catch (error) {}
}
async function updateTask (node, options, msg) {
if (!options || typeof options !== 'object') {
node.warn('schedule settings are not valid', msg)
return null
}
// eslint-disable-next-line eqeqeq
if (Array.isArray(options) == false) {
options = [options]
}
for (let index = 0; index < options.length; index++) {
const opt = options[index]
opt.payloadType = opt.payloadType || opt.type
if (opt.payloadType == null && typeof opt.payload === 'string' && opt.payload.length) {
opt.payloadType = 'str'
}
opt.payloadType = opt.payloadType || 'default'
delete opt.type
try {
validateOpt(opt)
} catch (error) {
node.warn(error, msg)
return null
}
}
for (let index = 0; index < options.length; index++) {
const opt = options[index]
const task = getTask(node, opt.name)
const isDynamic = !task || task.isDynamic
// let isStatic = task && task.isStatic;
let opCount = 0; let modified = false
if (task) {
modified = true
opCount = (task.node_count || 0)
if (Object.prototype.hasOwnProperty.call(opt, 'count') && typeof opt.count === 'number' && opt.count >= 0) {
opCount = opt.count
}
opt.count = opCount
deleteTask(node, opt.name)
}
const taskCount = node.tasks ? node.tasks.length : 0
const taskIndex = task && node.fanOut ? (task.node_index || 0) : taskCount
const t = await createTask(node, opt, taskIndex, !isDynamic)
if (t) {
if (modified) t.node_modified = true
t.node_count = opCount
t.isDynamic = isDynamic
}
}
requestSerialisation()// request persistent state be written
}
async function createTask (node, opt, index, _static) {
opt = opt || {}
try {
node.debug(`createTask - index: ${index}, static: ${_static}, opt: ${JSON.stringify(opt)}`)
} catch (error) {
node.error(error)
}
applyOptionDefaults(node, opt, index)
try {
validateOpt(opt)
} catch (error) {
node.warn(error)
const indicator = _static ? 'dot' : 'ring'
node.status({ fill: 'red', shape: indicator, text: error.message })
return null
}
const cronOpts = node.timeZone ? { timezone: node.timeZone } : undefined
let task
if (opt.expressionType === 'cron') {
const expression = cronosjs.CronosExpression.parse(opt.expression, cronOpts)
task = new cronosjs.CronosTask(expression)
} else if (opt.expressionType === 'solar') {
if (node.defaultLocationType === 'env' || node.defaultLocationType === 'fixed') {
opt.locationType = node.defaultLocationType
opt.location = await evaluateNodeProperty(node.defaultLocation, node.defaultLocationType, node)
} else { // per schedule
opt.location = await evaluateNodeProperty(opt.location, 'str', node)
}
const ds = parseSolarTimes(opt)
task = ds.task
task.node_solarEventTimes = ds.solarEventTimes
} else {
const ds = parseDateSequence(opt.expression)
task = ds.task
}
task.isDynamic = !_static
task.isStatic = _static
task.name = '' + opt.name
task.node_topic = opt.topic
task.node_expressionType = opt.expressionType
task.node_expression = opt.expression
task.node_payloadType = opt.payloadType
task.node_payload = opt.payload
task.node_locationType = opt.locationType
task.node_location = opt.location
task.node_solarType = opt.solarType
task.node_solarEvents = opt.solarEvents
task.node_offset = opt.offset
task.node_index = index
task.node_opt = opt
task.node_limit = typeof opt.limit === 'number' ? opt.limit : 0
task.node_count = typeof opt.count === 'number' ? opt.count : 0
if (task.node_limit > 0 && task.node_count > task.node_limit) {
task.node_count = task.node_limit
}
task.stop()
task.on('run', (timestamp) => {
node.debug(`running '${task.name}' ~ '${task.node_topic}'\n now time ${new Date()}\n crontime ${new Date(timestamp)}`)
const indicator = task.isDynamic ? 'ring' : 'dot'
node.status({ fill: 'green', shape: indicator, text: 'Running ' + formatShortDateTimeWithTZ(timestamp, node.timeZone) })
if (isTaskFinished(task)) {
process.nextTick(function () {
// using nextTick is a work around for an issue (#3) in cronosjs where the job restarts itself after this event handler has exited
task.stop()
updateNextStatus(node)
})
return
}
task.node_count = task.node_count + 1// ++ stops at 2147483647
sendMsg(node, task, timestamp)
process.nextTick(async function () {
if (task.node_expressionType === 'solar') {
await updateTask(node, task.node_opt, null)
}
requestSerialisation()// request persistent state be written
})
})
.on('ended', () => {
node.debug(`ended '${task.name}' ~ '${task.node_topic}'`)
updateNextStatus(node)
requestSerialisation()// request persistent state be written
})
.on('started', () => {
node.debug(`started '${task.name}' ~ '${task.node_topic}'`)
process.nextTick(function () {
updateNextStatus(node)
})
requestSerialisation()// request persistent state be written
})
.on('stopped', () => {
node.debug(`stopped '${task.name}' ~ '${task.node_topic}'`)
updateNextStatus(node)
requestSerialisation()// request persistent state be written
})
task.stop()// prevent bug where calling start without first calling stop causes events to bunch up
if (opt.dontStartTheTask === true || isTaskFinished(task)) {
// do not start the task
} else {
task.start()
}
node.tasks.push(task)
return task
}
function requestSerialisation () {
if (node.serialisationRequestBusy) {
return
}
node.queuedSerialisationRequest = Date.now()
}
async function serialise () {
let filePath = ''
try {
if (!node.persistDynamic) {
return
}
if (!FSAvailable && node.storeName === 'file') {
return
}
const dynNodes = node.tasks.filter((e) => e && e.isDynamic)
const statNodes = node.tasks.filter((e) => e && e.isDynamic !== true)
const exp = (task) => exportTask(task, true)
const dynNodesExp = dynNodes.map(exp)
const statNodesExp = statNodes.map(exp)
const state = {
version: 2,
dynamicSchedules: dynNodesExp,
staticSchedules: statNodesExp
}
if (node.storeName === 'file') {
try {
filePath = getPersistFilePath()
const fileData = JSON.stringify(state)
fs.writeFileSync(filePath, fileData)
} catch (err) {
node.error(`An error occurred while writing state to file '${filePath}' - (${err.message})`)
// return
}
} else {
const contextKey = 'state'
const storeName = node.storeName || 'default'
if (!contextAvailable || STORE_NAMES.indexOf(storeName) === -1) {
return
}
await contextSet(node.context(), contextKey, state, storeName)
}
/* if(!dynNodesExp || !dynNodesExp.length){
//FUTURE TODO: Sanity check before deletion
//and only if someone asks for it :)
//other wise, file clean up is a manual task
fs.unlinkSync(filePath);
return;
} */
} catch (e) {
node.error(`Error saving persistence data. ${e.message}`)
} finally {
node.queuedSerialisationRequest = null
}
}
async function deserialise () {
let filePath = ''
try {
if (!node.persistDynamic) {
return
}
if (!FSAvailable && node.storeName === 'file') {
return
}
const restoreState = async (state) => {
if (!state) {
return // nothing to add
}
if (state.version !== 1 && state.version !== 2) {
throw new Error('Invalid version - cannot load dynamic schedules')
}
if (state.version === 2 && state.staticSchedules && state.staticSchedules.length) {
for (let iOpt = 0; iOpt < state.staticSchedules.length; iOpt++) {
const opt = state.staticSchedules[iOpt]
const task = node.tasks.find(e => e.name === opt.name)
if (task) {
task.node_count = opt.count
}
if (opt.isRunning === false) {
stopTask(node, opt.name)
} else if (opt.isRunning === true) {
startTaskByName(node, opt.name)
}
}
updateNodeNextInfo(node)
}
if (state.version === 1) {
state.dynamicSchedules = state.schedules
}
if (state.dynamicSchedules && state.dynamicSchedules.length) {
for (let iOpt = 0; iOpt < state.dynamicSchedules.length; iOpt++) {
const opt = state.dynamicSchedules[iOpt]
let task
opt.name = opt.name || opt.topic
if (state.version === 1) {
task = await createTask(node, opt, iOpt, false)
} else if (state.version === 2) {
opt.dontStartTheTask = !opt.isRunning
task = await createTask(node, opt, iOpt, false)
if (task) {
task.node_count = opt.count
task.isRunning = opt.isRunning
task.isDynamic = true
}
if (opt.isRunning === false) {
stopTask(node, opt.name)
} else if (opt.isRunning === true) {
startTaskByName(node, opt.name)
}
}
}
updateNodeNextInfo(node)
}
}
if (node.storeName === 'file') {
filePath = getPersistFilePath()
if (fs.existsSync(filePath)) {
const fileData = fs.readFileSync(filePath)
const state = JSON.parse(fileData)
await restoreState(state)
} else {
RED.log.debug(`cron-plus: no persistence data found for node '${node.id}'.`)
}
} else {
// use context
const storeName = node.storeName || 'default'
const contextKey = 'state'
if (!contextAvailable || !STORE_NAMES.indexOf(storeName)) {
return
}
const state = await contextGet(node.context(), contextKey, storeName)
await restoreState(state)
}
} catch (error) {
node.error(`cron-plus: Error loading persistence data '${filePath}'. ${error.message}`)
}
}
function getPersistFilePath () {
const fileName = `node-${node.id}.json`
return path.join(persistPath, fileName)
}
function updateNextStatus (node, force) {
const now = new Date()
updateNodeNextInfo(node, now)
if (node.statusUpdatePending === true) {
if (force) {
node.statusUpdatePending = false
} else {
return
}
}
if (node.tasks) {
const indicator = node.nextIndicator || 'dot'
if (node.nextDate) {
const d = formatShortDateTimeWithTZ(node.nextDate, node.timeZone) || 'Never'
node.status({ fill: 'blue', shape: indicator, text: (node.nextEvent || 'Next') + ': ' + d })
} else if (node.tasks && node.tasks.length) {
node.status({ fill: 'grey', shape: indicator, text: 'All stopped' })
} else {
node.status({ }) // no tasks
}
} else {
node.status({})
}
}
function getNextTask (tasks) {
try {
const now = new Date()
if (!tasks || !tasks.length) { return null }
const runningTasks = tasks.filter(function (task) {
const finished = isTaskFinished(task)
return task.isRunning && (task._expression || task._sequence) && !finished
})
if (!runningTasks || !runningTasks.length) {
return null
}
let nextToRunTask
if (runningTasks.length === 1) {
// let x = (runningTasks[0]._expression || runningTasks[0]._sequence)
nextToRunTask = runningTasks[0]
// d = x.nextDate(now);
} else {
nextToRunTask = runningTasks.reduce(function (prev, current) {
// let p, c;
if (!prev) return current
if (!current) return prev
const px = (prev._expression || prev._sequence)
const cx = (current._expression || current._sequence)
return (px.nextDate(now) < cx.nextDate(now)) ? prev : current
})
}
return nextToRunTask
} catch (error) {
node.debug(error)
}
return null
}
function generateSendMsg (node, msg, type, index) {
const outputCount = node.outputs
const fanOut = node.fanOut
const hasCommandOutputPin = !!((node.commandResponseMsgOutput === 'output2' || fanOut))
const optionCount = node.options ? node.options.length : 0
let staticOutputPinIndex = 0
let dynOutputPinIndex = 0
let cmdOutputPin = 0
if (fanOut) {
dynOutputPinIndex = optionCount
cmdOutputPin = optionCount + 1
staticOutputPinIndex = index || 0
}
if (!fanOut && hasCommandOutputPin) {
cmdOutputPin = 1
}
let idx = 0
switch (type) {
case 'static':
idx = staticOutputPinIndex
break
case 'dynamic':
idx = dynOutputPinIndex
break
case 'command-response':
idx = cmdOutputPin
break
}
const arr = Array(outputCount || (idx + 1))
arr.fill(null)
arr[idx] = msg
return arr
}
}
function evaluateNodeProperty (value, type, node, msg) {
return new Promise(function (resolve, reject) {
RED.util.evaluateNodeProperty(value, type, node, msg, function (e, r) {
if (e) {
reject(e)
} else {
resolve(r)
}
})
})
}
function contextGet (context, contextKey, storeName) {
return new Promise(function (resolve, reject) {
context.get(contextKey, storeName, function (e, r) {
if (e) {
reject(e)
} else {
resolve(r)
}
})
})
}
function contextSet (context, contextKey, value, storeName) {
return new Promise(function (resolve, reject) {
context.set(contextKey, value, storeName, function (e, r) {
if (e) {
reject(e)
} else {
resolve()
}
})
})
}
function getStoreNames () {
const stores = ['', 'file']
if (!RED.settings.contextStorage) {
return stores
}
if (typeof RED.settings.contextStorage !== 'object') {
return stores
}
return [...stores, ...Object.keys(RED.settings.contextStorage)]
}
RED.nodes.registerType('cronplus', CronPlus)
RED.httpAdmin.post('/cronplusinject/:id', RED.auth.needsPermission('cronplus.write'), function (req, res) {
const node = RED.nodes.getNode(req.params.id)
if (node != null) {
try {
node.receive()
res.sendStatus(200)
} catch (err) {
res.sendStatus(500)
node.error(RED._('inject.failed', { error: err.toString() }))
}
} else {
res.sendStatus(404)
}
})
RED.httpAdmin.post('/cronplus/:id/:operation', RED.auth.needsPermission('cronplus.read'), async function (req, res) {
// console.log("/cronplus", req.body);
try {
const operation = req.params.operation
const node = RED.nodes.getNode(req.params.id)
if (operation === 'expressionTip') {
const timeZone = req.body.timeZone ? req.body.timeZone : undefined
const expressionType = req.body.expressionType ? req.body.expressionType : undefined
const opts = { expression: req.body.expression }
if (timeZone) opts.timezone = timeZone
if (expressionType) {
opts.expressionType = expressionType
if (opts.expressionType === 'solar') {
opts.solarType = req.body.solarType || ''
opts.solarEvents = req.body.solarEvents || ''
let pos = ''
const fakeNode = () => {
const n = {
id: req.body.nodeId,
_flow: { } // something - get the flow object based on req.body.flowId fc65972c8d3b1d68
}
return n
}
if (req.body.defaultLocationType === 'env') {
if (req.body.env) {
for (let index = 0; index < req.body.env.length; index++) {
const envVar = req.body.env[index]
if (envVar.name === req.body.defaultLocation) {
pos = await evaluateNodeProperty(envVar.value, envVar.type, fakeNode())
break
}
}
}
if (!pos && node) {
pos = await evaluateNodeProperty(node.defaultLocation, node.defaultLocationType, node)
}
} else if (req.body.defaultLocationType === 'fixed') {
if (node) {
pos = await evaluateNodeProperty(req.body.defaultLocation, req.body.defaultLocationType, node)
} else {
pos = await evaluateNodeProperty(req.body.defaultLocation, req.body.defaultLocationType, fakeNode())
}
} else { // per schedule
let loc = (req.body.location + '').trim()
let locType = 'str'
if (/\$\{(.+)\}/.test(loc)) {
locType = 'env'
loc = /\$\{(.+)\}/.exec(loc)[1]
}
if (locType === 'env') {
for (let index = 0; index < req.body.env.length; index++) {
const envVar = req.body.env[index]
if (envVar.name === loc) {
pos = await evaluateNodeProperty(envVar.value, envVar.type, node || fakeNode())
break
}
}
} else {
pos = await evaluateNodeProperty(loc, 'str', node || fakeNode())
}
}
opts.location = pos || ''
opts.offset = req.body.offset || 0
}
}
const exp = (opts.expressionType === 'solar') ? opts.location : opts.expression
const h = _describeExpression(exp, opts.expressionType, opts.timezone, opts.offset, opts.solarType, opts.solarEvents, null, { locationType: opts.locationType || opts.defaultLocationType, defaultLocationType: opts.defaultLocationType, defaultLocation: opts.defaultLocation })
let r = null
if (opts.expressionType === 'solar') {
const times = h.eventTimes && h.eventTimes.slice(1)
r = {
...opts,
// description: desc,
description: h.description,
// next: next,
next: h.nextEventTimeOffset,
// nextEventDesc: nextEventDesc,
nextEventDesc: h.nextEvent,
// prettyNext: prettyNext,
prettyNext: h.prettyNext,
// nextDates: nextDates
nextDates: times
}
} else {
const times = h.nextDates && h.nextDates.slice(1)
r = {
...opts,
description: h.description,
// next: next,
next: h.nextDate,
// nextEventDesc: nextEventDesc,
nextEventDesc: h.nextDescription,
// prettyNext: prettyNext,
prettyNext: h.prettyNext,
// nextDates: nextDates
nextDates: times
}
}
res.json(r)
} else if (operation === 'getDynamic') {
if (!node) {
res.json([])
return
}
const dynNodes = node.tasks.filter((e) => e && e.isDynamic)
const exp = (t) => {
return {
config: exportTask(t, false),
status: getTaskStatus(node, t, { includeSolarStateOffset: true })
}
}
const dynNodesExp = dynNodes.map(exp)
res.json(dynNodesExp)
} else if (operation === 'tz') {
res.json(timeZones)
}
} catch (err) {
res.sendStatus(500)
console.debug(err)
}
})
}
/**
* Array of timezones
*/
const timeZones = [
{ code: 'CI', latLon: '+0519-00402', tz: 'Africa/Abidjan', UTCOffset: '+00:00', UTCDSTOffset: '+00:00' },
{ code: 'GH', latLon: '+0533-00013', tz: 'Africa/Accra', UTCOffset: '+00:00', UTCDSTOffset: '+00:00' },
{ code: 'DZ', latLon: '3950', tz: 'Africa/Algiers', UTCOffset: '+01:00', UTCDSTOffset: '+01:00' },
{ code: 'GW', latLon: '+1151-01535', tz: 'Africa/Bissau', UTCOffset: '+00:00', UTCDSTOffset: '+00:00' },
{ code: 'EG', latLon: '6118', tz: 'Africa/Cairo', UTCOffset: '+02:00', UTCDSTOffset: '+02:00' },
{ code: 'MA', latLon: '+3339-00735', tz: 'Africa/Casablanca', UTCOffset: '+01:00', UTCDSTOffset: '+01:00' },
{ code: 'ES', latLon: '+3553-00519', tz: 'Africa/Ceuta', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'EH', latLon: '+2709-01312', tz: 'Africa/El_Aaiun', UTCOffset: '+00:00', UTCDSTOffset: '+01:00' },
{ code: 'ZA', latLon: '-2615+02800', tz: 'Africa/Johannesburg', UTCOffset: '+02:00', UTCDSTOffset: '+02:00' },
{ code: 'SS', latLon: '3587', tz: 'Africa/Juba', UTCOffset: '+03:00', UTCDSTOffset: '+03:00' },
{ code: 'SD', latLon: '4768', tz: 'Africa/Khartoum', UTCOffset: '+02:00', UTCDSTOffset: '+02:00' },
{ code: 'NG', latLon: '951', tz: 'Africa/Lagos', UTCOffset: '+01:00', UTCDSTOffset: '+01:00' },
{ code: 'MZ', latLon: '-2558+03235', tz: 'Africa/Maputo', UTCOffset: '+02:00', UTCDSTOffset: '+02:00' },
{ code: 'LR', latLon: '+0618-01047', tz: 'Africa/Monrovia', UTCOffset: '+00:00', UTCDSTOffset: '+00:00' },
{ code: 'KE', latLon: '-0117+03649', tz: 'Africa/Nairobi', UTCOffset: '+03:00', UTCDSTOffset: '+03:00' },
{ code: 'TD', latLon: '2710', tz: 'Africa/Ndjamena', UTCOffset: '+01:00', UTCDSTOffset: '+01:00' },
{ code: 'LY', latLon: '4565', tz: 'Africa/Tripoli', UTCOffset: '+02:00', UTCDSTOffset: '+02:00' },
{ code: 'TN', latLon: '4659', tz: 'Africa/Tunis', UTCOffset: '+01:00', UTCDSTOffset: '+01:00' },
{ code: 'NA', latLon: '-2234+01706', tz: 'Africa/Windhoek', UTCOffset: '+02:00', UTCDSTOffset: '+02:00' },
{ code: 'US', latLon: '+515248-1763929', tz: 'America/Adak', UTCOffset: '-10:00', UTCDSTOffset: '-09:00' },
{ code: 'US', latLon: '+611305-1495401', tz: 'America/Anchorage', UTCOffset: '-09:00', UTCDSTOffset: '-08:00' },
{ code: 'BR', latLon: '-0712-04812', tz: 'America/Araguaina', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'AR', latLon: '-3436-05827', tz: 'America/Argentina/Buenos_Aires', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'AR', latLon: '-2828-06547', tz: 'America/Argentina/Catamarca', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'AR', latLon: '-3124-06411', tz: 'America/Argentina/Cordoba', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'AR', latLon: '-2411-06518', tz: 'America/Argentina/Jujuy', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'AR', latLon: '-2926-06651', tz: 'America/Argentina/La_Rioja', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'AR', latLon: '-3253-06849', tz: 'America/Argentina/Mendoza', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'AR', latLon: '-5138-06913', tz: 'America/Argentina/Rio_Gallegos', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'AR', latLon: '-2447-06525', tz: 'America/Argentina/Salta', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'AR', latLon: '-3132-06831', tz: 'America/Argentina/San_Juan', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'AR', latLon: '-3319-06621', tz: 'America/Argentina/San_Luis', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'AR', latLon: '-2649-06513', tz: 'America/Argentina/Tucuman', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'AR', latLon: '-5448-06818', tz: 'America/Argentina/Ushuaia', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'PY', latLon: '-2516-05740', tz: 'America/Asuncion', UTCOffset: '-04:00', UTCDSTOffset: '-03:00' },
{ code: 'CA', latLon: '+484531-0913718', tz: 'America/Atikokan', UTCOffset: '-05:00', UTCDSTOffset: '-05:00' },
{ code: 'BR', latLon: '-1259-03831', tz: 'America/Bahia', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'MX', latLon: '+2048-10515', tz: 'America/Bahia_Banderas', UTCOffset: '-06:00', UTCDSTOffset: '-05:00' },
{ code: 'BB', latLon: '+1306-05937', tz: 'America/Barbados', UTCOffset: '-04:00', UTCDSTOffset: '-04:00' },
{ code: 'BR', latLon: '-0127-04829', tz: 'America/Belem', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'BZ', latLon: '+1730-08812', tz: 'America/Belize', UTCOffset: '-06:00', UTCDSTOffset: '-06:00' },
{ code: 'CA', latLon: '+5125-05707', tz: 'America/Blanc-Sablon', UTCOffset: '-04:00', UTCDSTOffset: '-04:00' },
{ code: 'BR', latLon: '+0249-06040', tz: 'America/Boa_Vista', UTCOffset: '-04:00', UTCDSTOffset: '-04:00' },
{ code: 'CO', latLon: '+0436-07405', tz: 'America/Bogota', UTCOffset: '-05:00', UTCDSTOffset: '-05:00' },
{ code: 'US', latLon: '+433649-1161209', tz: 'America/Boise', UTCOffset: '-07:00', UTCDSTOffset: '-06:00' },
{ code: 'CA', latLon: '+690650-1050310', tz: 'America/Cambridge_Bay', UTCOffset: '-07:00', UTCDSTOffset: '-06:00' },
{ code: 'BR', latLon: '-2027-05437', tz: 'America/Campo_Grande', UTCOffset: '-04:00', UTCDSTOffset: '-04:00' },
{ code: 'MX', latLon: '+2105-08646', tz: 'America/Cancun', UTCOffset: '-05:00', UTCDSTOffset: '-05:00' },
{ code: 'VE', latLon: '+1030-06656', tz: 'America/Caracas', UTCOffset: '-04:00', UTCDSTOffset: '-04:00' },
{ code: 'GF', latLon: '+0456-05220', tz: 'America/Cayenne', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'US', latLon: '+4151-08739', tz: 'America/Chicago', UTCOffset: '-06:00', UTCDSTOffset: '-05:00' },
{ code: 'MX', latLon: '+2838-10605', tz: 'America/Chihuahua', UTCOffset: '-07:00', UTCDSTOffset: '-06:00' },
{ code: 'CR', latLon: '+0956-08405', tz: 'America/Costa_Rica', UTCOffset: '-06:00', UTCDSTOffset: '-06:00' },
{ code: 'CA', latLon: '+4906-11631', tz: 'America/Creston', UTCOffset: '-07:00', UTCDSTOffset: '-07:00' },
{ code: 'BR', latLon: '-1535-05605', tz: 'America/Cuiaba', UTCOffset: '-04:00', UTCDSTOffset: '-04:00' },
{ code: 'CW', latLon: '+1211-06900', tz: 'America/Curacao', UTCOffset: '-04:00', UTCDSTOffset: '-04:00' },
{ code: 'GL', latLon: '+7646-01840', tz: 'America/Danmarkshavn', UTCOffset: '+00:00', UTCDSTOffset: '+00:00' },
{ code: 'CA', latLon: '+6404-13925', tz: 'America/Dawson', UTCOffset: '-08:00', UTCDSTOffset: '-07:00' },
{ code: 'CA', latLon: '+5946-12014', tz: 'America/Dawson_Creek', UTCOffset: '-07:00', UTCDSTOffset: '-07:00' },
{ code: 'US', latLon: '+394421-1045903', tz: 'America/Denver', UTCOffset: '-07:00', UTCDSTOffset: '-06:00' },
{ code: 'US', latLon: '+421953-0830245', tz: 'America/Detroit', UTCOffset: '-05:00', UTCDSTOffset: '-04:00' },
{ code: 'CA', latLon: '+5333-11328', tz: 'America/Edmonton', UTCOffset: '-07:00', UTCDSTOffset: '-06:00' },
{ code: 'BR', latLon: '-0640-06952', tz: 'America/Eirunepe', UTCOffset: '-05:00', UTCDSTOffset: '-05:00' },
{ code: 'SV', latLon: '+1342-08912', tz: 'America/El_Salvador', UTCOffset: '-06:00', UTCDSTOffset: '-06:00' },
{ code: 'CA', latLon: '+5848-12242', tz: 'America/Fort_Nelson', UTCOffset: '-07:00', UTCDSTOffset: '-07:00' },
{ code: 'BR', latLon: '-0343-03830', tz: 'America/Fortaleza', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'CA', latLon: '+4612-05957', tz: 'America/Glace_Bay', UTCOffset: '-04:00', UTCDSTOffset: '-03:00' },
{ code: 'GL', latLon: '+6411-05144', tz: 'America/Godthab', UTCOffset: '-03:00', UTCDSTOffset: '-02:00' },
{ code: 'CA', latLon: '+5320-06025', tz: 'America/Goose_Bay', UTCOffset: '-04:00', UTCDSTOffset: '-03:00' },
{ code: 'TC', latLon: '+2128-07108', tz: 'America/Grand_Turk', UTCOffset: '-05:00', UTCDSTOffset: '-04:00' },
{ code: 'GT', latLon: '+1438-09031', tz: 'America/Guatemala', UTCOffset: '-06:00', UTCDSTOffset: '-06:00' },
{ code: 'EC', latLon: '-0210-07950', tz: 'America/Guayaquil', UTCOffset: '-05:00', UTCDSTOffset: '-05:00' },
{ code: 'GY', latLon: '+0648-05810', tz: 'America/Guyana', UTCOffset: '-04:00', UTCDSTOffset: '-04:00' },
{ code: 'CA', latLon: '+4439-06336', tz: 'America/Halifax', UTCOffset: '-04:00', UTCDSTOffset: '-03:00' },
{ code: 'CU', latLon: '+2308-08222', tz: 'America/Havana', UTCOffset: '-05:00', UTCDSTOffset: '-04:00' },
{ code: 'MX', latLon: '+2904-11058', tz: 'America/Hermosillo', UTCOffset: '-07:00', UTCDSTOffset: '-07:00' },
{ code: 'US', latLon: '+394606-0860929', tz: 'America/Indiana/Indianapolis', UTCOffset: '-05:00', UTCDSTOffset: '-04:00' },
{ code: 'US', latLon: '+411745-0863730', tz: 'America/Indiana/Knox', UTCOffset: '-06:00', UTCDSTOffset: '-05:00' },
{ code: 'US', latLon: '+382232-0862041', tz: 'America/Indiana/Marengo', UTCOffset: '-05:00', UTCDSTOffset: '-04:00' },
{ code: 'US', latLon: '+382931-0871643', tz: 'America/Indiana/Petersburg', UTCOffset: '-05:00', UTCDSTOffset: '-04:00' },
{ code: 'US', latLon: '+375711-0864541', tz: 'America/Indiana/Tell_City', UTCOffset: '-06:00', UTCDSTOffset: '-05:00' },
{ code: 'US', latLon: '+384452-0850402', tz: 'America/Indiana/Vevay', UTCOffset: '-05:00', UTCDSTOffset: '-04:00' },
{ code: 'US', latLon: '+384038-0873143', tz: 'America/Indiana/Vincennes', UTCOffset: '-05:00', UTCDSTOffset: '-04:00' },
{ code: 'US', latLon: '+410305-0863611', tz: 'America/Indiana/Winamac', UTCOffset: '-05:00', UTCDSTOffset: '-04:00' },
{ code: 'CA', latLon: '+682059-13343', tz: 'America/Inuvik', UTCOffset: '-07:00', UTCDSTOffset: '-06:00' },
{ code: 'CA', latLon: '+6344-06828', tz: 'America/Iqaluit', UTCOffset: '-05:00', UTCDSTOffset: '-04:00' },
{ code: 'JM', latLon: '+175805-0764736', tz: 'America/Jamaica', UTCOffset: '-05:00', UTCDSTOffset: '-05:00' },
{ code: 'US', latLon: '+581807-1342511', tz: 'America/Juneau', UTCOffset: '-09:00', UTCDSTOffset: '-08:00' },
{ code: 'US', latLon: '+381515-0854534', tz: 'America/Kentucky/Louisville', UTCOffset: '-05:00', UTCDSTOffset: '-04:00' },
{ code: 'US', latLon: '+364947-0845057', tz: 'America/Kentucky/Monticello', UTCOffset: '-05:00', UTCDSTOffset: '-04:00' },
{ code: 'BO', latLon: '-1630-06809', tz: 'America/La_Paz', UTCOffset: '-04:00', UTCDSTOffset: '-04:00' },
{ code: 'PE', latLon: '-1203-07703', tz: 'America/Lima', UTCOffset: '-05:00', UTCDSTOffset: '-05:00' },
{ code: 'US', latLon: '+340308-1181434', tz: 'America/Los_Angeles', UTCOffset: '-08:00', UTCDSTOffset: '-07:00' },
{ code: 'BR', latLon: '-0940-03543', tz: 'America/Maceio', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'NI', latLon: '+1209-08617', tz: 'America/Managua', UTCOffset: '-06:00', UTCDSTOffset: '-06:00' },
{ code: 'BR', latLon: '-0308-06001', tz: 'America/Manaus', UTCOffset: '-04:00', UTCDSTOffset: '-04:00' },
{ code: 'MQ', latLon: '+1436-06105', tz: 'America/Martinique', UTCOffset: '-04:00', UTCDSTOffset: '-04:00' },
{ code: 'MX', latLon: '+2550-09730', tz: 'America/Matamoros', UTCOffset: '-06:00', UTCDSTOffset: '-05:00' },
{ code: 'MX', latLon: '+2313-10625', tz: 'America/Mazatlan', UTCOffset: '-07:00', UTCDSTOffset: '-06:00' },
{ code: 'US', latLon: '+450628-0873651', tz: 'America/Menominee', UTCOffset: '-06:00', UTCDSTOffset: '-05:00' },
{ code: 'MX', latLon: '+2058-08937', tz: 'America/Merida', UTCOffset: '-06:00', UTCDSTOffset: '-05:00' },
{ code: 'US', latLon: '+550737-1313435', tz: 'America/Metlakatla', UTCOffset: '-09:00', UTCDSTOffset: '-08:00' },
{ code: 'MX', latLon: '+1924-09909', tz: 'America/Mexico_City', UTCOffset: '-06:00', UTCDSTOffset: '-05:00' },
{ code: 'PM', latLon: '+4703-05620', tz: 'America/Miquelon', UTCOffset: '-03:00', UTCDSTOffset: '-02:00' },
{ code: 'CA', latLon: '+4606-06447', tz: 'America/Moncton', UTCOffset: '-04:00', UTCDSTOffset: '-03:00' },
{ code: 'MX', latLon: '+2540-10019', tz: 'America/Monterrey', UTCOffset: '-06:00', UTCDSTOffset: '-05:00' },
{ code: 'UY', latLon: '-3453-05611', tz: 'America/Montevideo', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'BS', latLon: '+2505-07721', tz: 'America/Nassau', UTCOffset: '-05:00', UTCDSTOffset: '-04:00' },
{ code: 'US', latLon: '+404251-0740023', tz: 'America/New_York', UTCOffset: '-05:00', UTCDSTOffset: '-04:00' },
{ code: 'CA', latLon: '+4901-08816', tz: 'America/Nipigon', UTCOffset: '-05:00', UTCDSTOffset: '-04:00' },
{ code: 'US', latLon: '+643004-1652423', tz: 'America/Nome', UTCOffset: '-09:00', UTCDSTOffset: '-08:00' },
{ code: 'BR', latLon: '-0351-03225', tz: 'America/Noronha', UTCOffset: '-02:00', UTCDSTOffset: '-02:00' },
{ code: 'US', latLon: '+471551-1014640', tz: 'America/North_Dakota/Beulah', UTCOffset: '-06:00', UTCDSTOffset: '-05:00' },
{ code: 'US', latLon: '+470659-1011757', tz: 'America/North_Dakota/Center', UTCOffset: '-06:00', UTCDSTOffset: '-05:00' },
{ code: 'US', latLon: '+465042-1012439', tz: 'America/North_Dakota/New_Salem', UTCOffset: '-06:00', UTCDSTOffset: '-05:00' },
{ code: 'MX', latLon: '+2934-10425', tz: 'America/Ojinaga', UTCOffset: '-07:00', UTCDSTOffset: '-06:00' },
{ code: 'PA', latLon: '+0858-07932', tz: 'America/Panama', UTCOffset: '-05:00', UTCDSTOffset: '-05:00' },
{ code: 'CA', latLon: '+6608-06544', tz: 'America/Pangnirtung', UTCOffset: '-05:00', UTCDSTOffset: '-04:00' },
{ code: 'SR', latLon: '+0550-05510', tz: 'America/Paramaribo', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'US', latLon: '+332654-1120424', tz: 'America/Phoenix', UTCOffset: '-07:00', UTCDSTOffset: '-07:00' },
{ code: 'TT', latLon: '+1039-06131', tz: 'America/Port_of_Spain', UTCOffset: '-04:00', UTCDSTOffset: '-04:00' },
{ code: 'HT', latLon: '+1832-07220', tz: 'America/Port-au-Prince', UTCOffset: '-05:00', UTCDSTOffset: '-04:00' },
{ code: 'BR', latLon: '-0846-06354', tz: 'America/Porto_Velho', UTCOffset: '-04:00', UTCDSTOffset: '-04:00' },
{ code: 'PR', latLon: '+182806-0660622', tz: 'America/Puerto_Rico', UTCOffset: '-04:00', UTCDSTOffset: '-04:00' },
{ code: 'CL', latLon: '-5309-07055', tz: 'America/Punta_Arenas', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'CA', latLon: '+4843-09434', tz: 'America/Rainy_River', UTCOffset: '-06:00', UTCDSTOffset: '-05:00' },
{ code: 'CA', latLon: '+6249-0920459', tz: 'America/Rankin_Inlet', UTCOffset: '-06:00', UTCDSTOffset: '-05:00' },
{ code: 'BR', latLon: '-0803-03454', tz: 'America/Recife', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'CA', latLon: '+5024-10439', tz: 'America/Regina', UTCOffset: '-06:00', UTCDSTOffset: '-06:00' },
{ code: 'CA', latLon: '+744144-0944945', tz: 'America/Resolute', UTCOffset: '-06:00', UTCDSTOffset: '-05:00' },
{ code: 'BR', latLon: '-0958-06748', tz: 'America/Rio_Branco', UTCOffset: '-05:00', UTCDSTOffset: '-05:00' },
{ code: 'BR', latLon: '-0226-05452', tz: 'America/Santarem', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'CL', latLon: '-3327-07040', tz: 'America/Santiago', UTCOffset: '-04:00', UTCDSTOffset: '-03:00' },
{ code: 'DO', latLon: '+1828-06954', tz: 'America/Santo_Domingo', UTCOffset: '-04:00', UTCDSTOffset: '-04:00' },
{ code: 'BR', latLon: '-2332-04637', tz: 'America/Sao_Paulo', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'GL', latLon: '+7029-02158', tz: 'America/Scoresbysund', UTCOffset: '-01:00', UTCDSTOffset: '+00:00' },
{ code: 'US', latLon: '+571035-1351807', tz: 'America/Sitka', UTCOffset: '-09:00', UTCDSTOffset: '-08:00' },
{ code: 'CA', latLon: '+4734-05243', tz: 'America/St_Johns', UTCOffset: '-03:30', UTCDSTOffset: '-02:30' },
{ code: 'CA', latLon: '+5017-10750', tz: 'America/Swift_Current', UTCOffset: '-06:00', UTCDSTOffset: '-06:00' },
{ code: 'HN', latLon: '+1406-08713', tz: 'America/Tegucigalpa', UTCOffset: '-06:00', UTCDSTOffset: '-06:00' },
{ code: 'GL', latLon: '+7634-06847', tz: 'America/Thule', UTCOffset: '-04:00', UTCDSTOffset: '-03:00' },
{ code: 'CA', latLon: '+4823-08915', tz: 'America/Thunder_Bay', UTCOffset: '-05:00', UTCDSTOffset: '-04:00' },
{ code: 'MX', latLon: '+3232-11701', tz: 'America/Tijuana', UTCOffset: '-08:00', UTCDSTOffset: '-07:00' },
{ code: 'CA', latLon: '+4339-07923', tz: 'America/Toronto', UTCOffset: '-05:00', UTCDSTOffset: '-04:00' },
{ code: 'CA', latLon: '+4916-12307', tz: 'America/Vancouver', UTCOffset: '-08:00', UTCDSTOffset: '-07:00' },
{ code: 'CA', latLon: '+6043-13503', tz: 'America/Whitehorse', UTCOffset: '-08:00', UTCDSTOffset: '-07:00' },
{ code: 'CA', latLon: '+4953-09709', tz: 'America/Winnipeg', UTCOffset: '-06:00', UTCDSTOffset: '-05:00' },
{ code: 'US', latLon: '+593249-1394338', tz: 'America/Yakutat', UTCOffset: '-09:00', UTCDSTOffset: '-08:00' },
{ code: 'CA', latLon: '+6227-11421', tz: 'America/Yellowknife', UTCOffset: '-07:00', UTCDSTOffset: '-06:00' },
{ code: 'AQ', latLon: '-6617+11031', tz: 'Antarctica/Casey', UTCOffset: '+11:00', UTCDSTOffset: '+11:00' },
{ code: 'AQ', latLon: '-6835+07758', tz: 'Antarctica/Davis', UTCOffset: '+07:00', UTCDSTOffset: '+07:00' },
{ code: 'AQ', latLon: '-6640+14001', tz: 'Antarctica/DumontDUrville', UTCOffset: '+10:00', UTCDSTOffset: '+10:00' },
{ code: 'AU', latLon: '-5430+15857', tz: 'Antarctica/Macquarie', UTCOffset: '+11:00', UTCDSTOffset: '+11:00' },
{ code: 'AQ', latLon: '-6736+06253', tz: 'Antarctica/Mawson', UTCOffset: '+05:00', UTCDSTOffset: '+05:00' },
{ code: 'AQ', latLon: '-6448-06406', tz: 'Antarctica/Palmer', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'AQ', latLon: '-6734-06808', tz: 'Antarctica/Rothera', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'AQ', latLon: '-690022+0393524', tz: 'Antarctica/Syowa', UTCOffset: '+03:00', UTCDSTOffset: '+03:00' },
{ code: 'AQ', latLon: '-720041+0023206', tz: 'Antarctica/Troll', UTCOffset: '+00:00', UTCDSTOffset: '+02:00' },
{ code: 'AQ', latLon: '-7824+10654', tz: 'Antarctica/Vostok', UTCOffset: '+06:00', UTCDSTOffset: '+06:00' },
{ code: 'KZ', latLon: '11972', tz: 'Asia/Almaty', UTCOffset: '+06:00', UTCDSTOffset: '+06:00' },
{ code: 'JO', latLon: '6713', tz: 'Asia/Amman', UTCOffset: '+02:00', UTCDSTOffset: '+03:00' },
{ code: 'RU', latLon: '24174', tz: 'Asia/Anadyr', UTCOffset: '+12:00', UTCDSTOffset: '+12:00' },
{ code: 'KZ', latLon: '9447', tz: 'Asia/Aqtau', UTCOffset: '+05:00', UTCDSTOffset: '+05:00' },
{ code: 'KZ', latLon: '10727', tz: 'Asia/Aqtobe', UTCOffset: '+05:00', UTCDSTOffset: '+05:00' },
{ code: 'TM', latLon: '9580', tz: 'Asia/Ashgabat', UTCOffset: '+05:00', UTCDSTOffset: '+05:00' },
{ code: 'KZ', latLon: '9863', tz: 'Asia/Atyrau', UTCOffset: '+05:00', UTCDSTOffset: '+05:00' },
{ code: 'IQ', latLon: '7746', tz: 'Asia/Baghdad', UTCOffset: '+03:00', UTCDSTOffset: '+03:00' },
{ code: 'AZ', latLon: '8974', tz: 'Asia/Baku', UTCOffset: '+04:00', UTCDSTOffset: '+04:00' },
{ code: 'TH', latLon: '11376', tz: 'Asia/Bangkok', UTCOffset: '+07:00', UTCDSTOffset: '+07:00' },
{ code: 'RU', latLon: '13667', tz: 'Asia/Barnaul', UTCOffset: '+07:00', UTCDSTOffset: '+07:00' },
{ code: 'LB', latLon: '6883', tz: 'Asia/Beirut', UTCOffset: '+02:00', UTCDSTOffset: '+03:00' },
{ code: 'KG', latLon: '11690', tz: 'Asia/Bishkek', UTCOffset: '+06:00', UTCDSTOffset: '+06:00' },
{ code: 'BN', latLon: '11911', tz: 'Asia/Brunei', UTCOffset: '+08:00', UTCDSTOffset: '+08:00' },
{ code: 'RU', latLon: '16531', tz: 'Asia/Chita', UTCOffset: '+09:00', UTCDSTOffset: '+09:00' },
{ code: 'MN', latLon: '16234', tz: 'Asia/Choibalsan', UTCOffset: '+08:00', UTCDSTOffset: '+08:00' },
{ code: 'LK', latLon: '8607', tz: 'Asia/Colombo', UTCOffset: '+05:30', UTCDSTOffset: '+05:30' },
{ code: 'SY', latLon: '6948', tz: 'Asia/Damascus', UTCOffset: '+02:00', UTCDSTOffset: '+03:00' },
{ code: 'BD', latLon: '11368', tz: 'Asia/Dhaka', UTCOffset: '+06:00', UTCDSTOffset: '+06:00' },
{ code: 'TL', latLon: '-0833+12535', tz: 'Asia/Dili', UTCOffset: '+09:00', UTCDSTOffset: '+09:00' },
{ code: 'AE', latLon: '8036', tz: 'Asia/Dubai', UTCOffset: '+04:00', UTCDSTOffset: '+04:00' },
{ code: 'TJ', latLon: '10683', tz: 'Asia/Dushanbe', UTCOffset: '+05:00', UTCDSTOffset: '+05:00' },
{ code: 'CY', latLon: '6864', tz: 'Asia/Famagusta', UTCOffset: '+02:00', UTCDSTOffset: '+02:00' },
{ code: 'PS', latLon: '6558', tz: 'Asia/Gaza', UTCOffset: '+02:00', UTCDSTOffset: '+03:00' },
{ code: 'PS', latLon: '353674', tz: 'Asia/Hebron', UTCOffset: '+02:00', UTCDSTOffset: '+03:00' },
{ code: 'VN', latLon: '11685', tz: 'Asia/Ho_Chi_Minh', UTCOffset: '+07:00', UTCDSTOffset: '+07:00' },
{ code: 'HK', latLon: '13626', tz: 'Asia/Hong_Kong', UTCOffset: '+08:00', UTCDSTOffset: '+08:00' },
{ code: 'MN', latLon: '13940', tz: 'Asia/Hovd', UTCOffset: '+07:00', UTCDSTOffset: '+07:00' },
{ code: 'RU', latLon: '15636', tz: 'Asia/Irkutsk', UTCOffset: '+08:00', UTCDSTOffset: '+08:00' },
{ code: 'ID', latLon: '-0610+10648', tz: 'Asia/Jakarta', UTCOffset: '+07:00', UTCDSTOffset: '+07:00' },
{ code: 'ID', latLon: '-0232+14042', tz: 'Asia/Jayapura', UTCOffset: '+09:00', UTCDSTOffset: '+09:00' },
{ code: 'IL', latLon: '665976', tz: 'Asia/Jerusalem', UTCOffset: '+02:00', UTCDSTOffset: '+03:00' },
{ code: 'AF', latLon: '10343', tz: 'Asia/Kabul', UTCOffset: '+04:30', UTCDSTOffset: '+04:30' },
{ code: 'RU', latLon: '21140', tz: 'Asia/Kamchatka', UTCOffset: '+12:00', UTCDSTOffset: '+12:00' },
{ code: 'PK', latLon: '9155', tz: 'Asia/Karachi', UTCOffset: '+05:00', UTCDSTOffset: '+05:00' },
{ code: 'NP', latLon: '11262', tz: 'Asia/Kathmandu', UTCOffset: '+05:45', UTCDSTOffset: '+05:45' },
{ code: 'RU', latLon: '1977237', tz: 'Asia/Khandyga', UTCOffset: '+09:00', UTCDSTOffset: '+09:00' },
{ code: 'IN', latLon: '11054', tz: 'Asia/Kolkata', UTCOffset: '+05:30', UTCDSTOffset: '+05:30' },
{ code: 'RU', latLon: '14851', tz: 'Asia/Krasnoyarsk', UTCOffset: '+07:00', UTCDSTOffset: '+07:00' },
{ code: 'MY', latLon: '10452', tz: 'Asia/Kuala_Lumpur', UTCOffset: '+08:00', UTCDSTOffset: '+08:00' },
{ code: 'MY', latLon: '11153', tz: 'Asia/Kuching', UTCOffset: '+08:00', UTCDSTOffset: '+08:00' },
{ code: 'MO', latLon: '13549', tz: 'Asia/Macau', UTCOffset: '+08:00', UTCDSTOffset: '+08:00' },
{ code: 'RU', latLon: '20982', tz: 'Asia/Magadan', UTCOffset: '+11:00', UTCDSTOffset: '+11:00' },
{ code: 'ID', latLon: '-0507+11924', tz: 'Asia/Makassar', UTCOffset: '+08:00', UTCDSTOffset: '+08:00' },
{ code: 'PH', latLon: '13535', tz: 'Asia/Manila', UTCOffset: '+08:00', UTCDSTOffset: '+08:00' },
{ code: 'RU', latLon: '14052', tz: 'Asia/Novokuznetsk', UTCOffset: '+07:00', UTCDSTOffset: '+07:00' },
{ code: 'RU', latLon: '13757', tz: 'Asia/Novosibirsk', UTCOffset: '+07:00', UTCDSTOffset: '+07:00' },
{ code: 'RU', latLon: '12824', tz: 'Asia/Omsk', UTCOffset: '+06:00', UTCDSTOffset: '+06:00' },
{ code: 'KZ', latLon: '10234', tz: 'Asia/Oral', UTCOffset: '+05:00', UTCDSTOffset: '+05:00' },
{ code: 'ID', latLon: '-0002+10920', tz: 'Asia/Pontianak', UTCOffset: '+07:00', UTCDSTOffset: '+07:00' },
{ code: 'KP', latLon: '16446', tz: 'Asia/Pyongyang', UTCOffset: '+09:00', UTCDSTOffset: '+09:00' },
{ code: 'QA', latLon: '7649', tz: 'Asia/Qatar', UTCOffset: '+03:00', UTCDSTOffset: '+03:00' },
{ code: 'KZ', latLon: '10976', tz: 'Asia/Qyzylorda', UTCOffset: '+05:00', UTCDSTOffset: '+05:00' },
{ code: 'SA', latLon: '7081', tz: 'Asia/Riyadh', UTCOffset: '+03:00', UTCDSTOffset: '+03:00' },
{ code: 'RU', latLon: '18900', tz: 'Asia/Sakhalin', UTCOffset: '+11:00', UTCDSTOffset: '+11:00' },
{ code: 'UZ', latLon: '10588', tz: 'Asia/Samarkand', UTCOffset: '+05:00', UTCDSTOffset: '+05:00' },
{ code: 'KR', latLon: '16391', tz: 'Asia/Seoul', UTCOffset: '+09:00', UTCDSTOffset: '+09:00' },
{ code: 'CN', latLon: '15242', tz: 'Asia/Shanghai', UTCOffset: '+08:00', UTCDSTOffset: '+08:00' },
{ code: 'SG', latLon: '10468', tz: 'Asia/Singapore', UTCOffset: '+08:00', UTCDSTOffset: '+08:00' },
{ code: 'RU', latLon: '22071', tz: 'Asia/Srednekolymsk', UTCOffset: '+11:00', UTCDSTOffset: '+11:00' },
{ code: 'TW', latLon: '14633', tz: 'Asia/Taipei', UTCOffset: '+08:00', UTCDSTOffset: '+08:00' },
{ code: 'UZ', latLon: '11038', tz: 'Asia/Tashkent', UTCOffset: '+05:00', UTCDSTOffset: '+05:00' },
{ code: 'GE', latLon: '8592', tz: 'Asia/Tbilisi', UTCOffset: '+04:00', UTCDSTOffset: '+04:00' },
{ code: 'IR', latLon: '8666', tz: 'Asia/Tehran', UTCOffset: '+03:30', UTCDSTOffset: '+04:30' },
{ code: 'BT', latLon: '11667', tz: 'Asia/Thimphu', UTCOffset: '+06:00', UTCDSTOffset: '+06:00' },
{ code: 'JP', latLon: '1748357', tz: 'Asia/Tokyo', UTCOffset: '+09:00', UTCDSTOffset: '+09:00' },
{ code: 'RU', latLon: '14088', tz: 'Asia/Tomsk', UTCOffset: '+07:00', UTCDSTOffset: '+07:00' },
{ code: 'MN', latLon: '15408', tz: 'Asia/Ulaanbaatar', UTCOffset: '+08:00', UTCDSTOffset: '+08:00' },
{ code: 'CN', latLon: '13083', tz: 'Asia/Urumqi', UTCOffset: '+06:00', UTCDSTOffset: '+06:00' },
{ code: 'RU', latLon: '2074673', tz: 'Asia/Ust-Nera', UTCOffset: '+10:00', UTCDSTOffset: '+10:00' },
{ code: 'RU', latLon: '17466', tz: 'Asia/Vladivostok', UTCOffset: '+10:00', UTCDSTOffset: '+10:00' },
{ code: 'RU', latLon: '19140', tz: 'Asia/Yakutsk', UTCOffset: '+09:00', UTCDSTOffset: '+09:00' },
{ code: 'MM', latLon: '11257', tz: 'Asia/Yangon', UTCOffset: '+06:30', UTCDSTOffset: '+06:30' },
{ code: 'RU', latLon: '11687', tz: 'Asia/Yekaterinburg', UTCOffset: '+05:00', UTCDSTOffset: '+05:00' },
{ code: 'AM', latLon: '8441', tz: 'Asia/Yerevan', UTCOffset: '+04:00', UTCDSTOffset: '+04:00' },
{ code: 'PT', latLon: '+3744-02540', tz: 'Atlantic/Azores', UTCOffset: '-01:00', UTCDSTOffset: '+00:00' },
{ code: 'BM', latLon: '+3217-06446', tz: 'Atlantic/Bermuda', UTCOffset: '-04:00', UTCDSTOffset: '-03:00' },
{ code: 'ES', latLon: '+2806-01524', tz: 'Atlantic/Canary', UTCOffset: '+00:00', UTCDSTOffset: '+01:00' },
{ code: 'CV', latLon: '+1455-02331', tz: 'Atlantic/Cape_Verde', UTCOffset: '-01:00', UTCDSTOffset: '-01:00' },
{ code: 'FO', latLon: '+6201-00646', tz: 'Atlantic/Faroe', UTCOffset: '+00:00', UTCDSTOffset: '+01:00' },
{ code: 'PT', latLon: '+3238-01654', tz: 'Atlantic/Madeira', UTCOffset: '+00:00', UTCDSTOffset: '+01:00' },
{ code: 'IS', latLon: '+6409-02151', tz: 'Atlantic/Reykjavik', UTCOffset: '+00:00', UTCDSTOffset: '+00:00' },
{ code: 'GS', latLon: '-5416-03632', tz: 'Atlantic/South_Georgia', UTCOffset: '-02:00', UTCDSTOffset: '-02:00' },
{ code: 'FK', latLon: '-5142-05751', tz: 'Atlantic/Stanley', UTCOffset: '-03:00', UTCDSTOffset: '-03:00' },
{ code: 'AU', latLon: '-3455+13835', tz: 'Australia/Adelaide', UTCOffset: '+09:30', UTCDSTOffset: '+10:30' },
{ code: 'AU', latLon: '-2728+15302', tz: 'Australia/Brisbane', UTCOffset: '+10:00', UTCDSTOffset: '+10:00' },
{ code: 'AU', latLon: '-3157+14127', tz: 'Australia/Broken_Hill', UTCOffset: '+09:30', UTCDSTOffset: '+10:30' },
{ code: 'AU', latLon: '-3956+14352', tz: 'Australia/Currie', UTCOffset: '+10:00', UTCDSTOffset: '+11:00' },
{ code: 'AU', latLon: '-1228+13050', tz: 'Australia/Darwin', UTCOffset: '+09:30', UTCDSTOffset: '+09:30' },
{ code: 'AU', latLon: '-3143+12852', tz: 'Australia/Eucla', UTCOffset: '+08:45', UTCDSTOffset: '+08:45' },
{ code: 'AU', latLon: '-4253+14719', tz: 'Australia/Hobart', UTCOffset: '+10:00', UTCDSTOffset: '+11:00' },
{ code: 'AU', latLon: '-2016+14900', tz: 'Australia/Lindeman', UTCOffset: '+10:00', UTCDSTOffset: '+10:00' },
{ code: 'AU', latLon: '-3133+15905', tz: 'Australia/Lord_Howe', UTCOffset: '+10:30', UTCDSTOffset: '+11:00' },
{ code: 'AU', latLon: '-3749+14458', tz: 'Australia/Melbourne', UTCOffset: '+10:00', UTCDSTOffset: '+11:00' },
{ code: 'AU', latLon: '-3157+11551', tz: 'Australia/Perth', UTCOffset: '+08:00', UTCDSTOffset: '+08:00' },
{ code: 'AU', latLon: '-3352+15113', tz: 'Australia/Sydney', UTCOffset: '+10:00', UTCDSTOffset: '+11:00' },
{ code: 'NL', latLon: '5676', tz: 'Europe/Amsterdam', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'AD', latLon: '4361', tz: 'Europe/Andorra', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'RU', latLon: '9424', tz: 'Europe/Astrakhan', UTCOffset: '+04:00', UTCDSTOffset: '+04:00' },
{ code: 'GR', latLon: '6101', tz: 'Europe/Athens', UTCOffset: '+02:00', UTCDSTOffset: '+03:00' },
{ code: 'RS', latLon: '6480', tz: 'Europe/Belgrade', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'DE', latLon: '6552', tz: 'Europe/Berlin', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'BE', latLon: '5470', tz: 'Europe/Brussels', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'RO', latLon: '7032', tz: 'Europe/Bucharest', UTCOffset: '+02:00', UTCDSTOffset: '+03:00' },
{ code: 'HU', latLon: '6635', tz: 'Europe/Budapest', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'MD', latLon: '7550', tz: 'Europe/Chisinau', UTCOffset: '+02:00', UTCDSTOffset: '+03:00' },
{ code: 'DK', latLon: '6775', tz: 'Europe/Copenhagen', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'IE', latLon: '+5320-00615', tz: 'Europe/Dublin', UTCOffset: '+00:00', UTCDSTOffset: '+01:00' },
{ code: 'GI', latLon: '+3608-00521', tz: 'Europe/Gibraltar', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'FI', latLon: '8468', tz: 'Europe/Helsinki', UTCOffset: '+02:00', UTCDSTOffset: '+03:00' },
{ code: 'TR', latLon: '6959', tz: 'Europe/Istanbul', UTCOffset: '+03:00', UTCDSTOffset: '+03:00' },
{ code: 'RU', latLon: '7473', tz: 'Europe/Kaliningrad', UTCOffset: '+02:00', UTCDSTOffset: '+02:00' },
{ code: 'UA', latLon: '8057', tz: 'Europe/Kiev', UTCOffset: '+02:00', UTCDSTOffset: '+03:00' },
{ code: 'RU', latLon: '10775', tz: 'Europe/Kirov', UTCOffset: '+03:00', UTCDSTOffset: '+03:00' },
{ code: 'PT', latLon: '+3843-00908', tz: 'Europe/Lisbon', UTCOffset: '+00:00', UTCDSTOffset: '+01:00' },
{ code: 'GB', latLon: '+513030-0000731', tz: 'Europe/London', UTCOffset: '+00:00', UTCDSTOffset: '+01:00' },
{ code: 'LU', latLon: '5545', tz: 'Europe/Luxembourg', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'ES', latLon: '+4024-00341', tz: 'Europe/Madrid', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'MT', latLon: '4985', tz: 'Europe/Malta', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'BY', latLon: '8088', tz: 'Europe/Minsk', UTCOffset: '+03:00', UTCDSTOffset: '+03:00' },
{ code: 'MC', latLon: '5065', tz: 'Europe/Monaco', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'RU', latLon: '928225', tz: 'Europe/Moscow', UTCOffset: '+03:00', UTCDSTOffset: '+03:00' },
{ code: 'CY', latLon: '6832', tz: 'Asia/Nicosia', UTCOffset: '+02:00', UTCDSTOffset: '+03:00' },
{ code: 'NO', latLon: '7000', tz: 'Europe/Oslo', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'FR', latLon: '5072', tz: 'Europe/Paris', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'CZ', latLon: '6431', tz: 'Europe/Prague', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'LV', latLon: '8063', tz: 'Europe/Riga', UTCOffset: '+02:00', UTCDSTOffset: '+03:00' },
{ code: 'IT', latLon: '5383', tz: 'Europe/Rome', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'RU', latLon: '10321', tz: 'Europe/Samara', UTCOffset: '+04:00', UTCDSTOffset: '+04:00' },
{ code: 'RU', latLon: '9736', tz: 'Europe/Saratov', UTCOffset: '+04:00', UTCDSTOffset: '+04:00' },
{ code: 'UA', latLon: '7863', tz: 'Europe/Simferopol', UTCOffset: '+03:00', UTCDSTOffset: '+03:00' },
{ code: 'BG', latLon: '6560', tz: 'Europe/Sofia', UTCOffset: '+02:00', UTCDSTOffset: '+03:00' },
{ code: 'SE', latLon: '7723', tz: 'Europe/Stockholm', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'EE', latLon: '8370', tz: 'Europe/Tallinn', UTCOffset: '+02:00', UTCDSTOffset: '+03:00' },
{ code: 'AL', latLon: '6070', tz: 'Europe/Tirane', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'RU', latLon: '10244', tz: 'Europe/Ulyanovsk', UTCOffset: '+04:00', UTCDSTOffset: '+04:00' },
{ code: 'UA', latLon: '7055', tz: 'Europe/Uzhgorod', UTCOffset: '+02:00', UTCDSTOffset: '+03:00' },
{ code: 'AT', latLon: '6433', tz: 'Europe/Vienna', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'LT', latLon: '7960', tz: 'Europe/Vilnius', UTCOffset: '+02:00', UTCDSTOffset: '+03:00' },
{ code: 'RU', latLon: '9269', tz: 'Europe/Volgograd', UTCOffset: '+04:00', UTCDSTOffset: '+04:00' },
{ code: 'PL', latLon: '7315', tz: 'Europe/Warsaw', UTCOffset: '+02:00', UTCDSTOffset: '+03:00' },
{ code: 'UA', latLon: '8260', tz: 'Europe/Zaporozhye', UTCOffset: '+02:00', UTCDSTOffset: '+03:00' },
{ code: 'CH', latLon: '5555', tz: 'Europe/Zurich', UTCOffset: '+01:00', UTCDSTOffset: '+02:00' },
{ code: 'IO', latLon: '-0720+07225', tz: 'Indian/Chagos', UTCOffset: '+06:00', UTCDSTOffset: '+06:00' },
{ code: 'CX', latLon: '-1025+10543', tz: 'Indian/Christmas', UTCOffset: '+07:00', UTCDSTOffset: '+07:00' },
{ code: 'CC', latLon: '-1210+09655', tz: 'Indian/Cocos', UTCOffset: '+06:30', UTCDSTOffset: '+06:30' },
{ code: 'TF', latLon: '-492110+0701303', tz: 'Indian/Kerguelen', UTCOffset: '+05:00', UTCDSTOffset: '+05:00' },
{ code: 'SC', latLon: '-0440+05528', tz: 'Indian/Mahe', UTCOffset: '+04:00', UTCDSTOffset: '+04:00' },
{ code: 'MV', latLon: '7740', tz: 'Indian/Maldives', UTCOffset: '+05:00', UTCDSTOffset: '+05:00' },
{ code: 'MU', latLon: '-2010+05730', tz: 'Indian/Mauritius', UTCOffset: '+04:00', UTCDSTOffset: '+04:00' },
{ code: 'RE', latLon: '-2052+05528', tz: 'Indian/Reunion', UTCOffset: '+04:00', UTCDSTOffset: '+04:00' },
{ code: 'WS', latLon: '-1350-17144', tz: 'Pacific/Apia', UTCOffset: '+13:00', UTCDSTOffset: '+14:00' },
{ code: 'NZ', latLon: '-3652+17446', tz: 'Pacific/Auckland', UTCOffset: '+12:00', UTCDSTOffset: '+13:00' },
{ code: 'PG', latLon: '-0613+15534', tz: 'Pacific/Bougainville', UTCOffset: '+11:00', UTCDSTOffset: '+11:00' },
{ code: 'NZ', latLon: '-4357-17633', tz: 'Pacific/Chatham', UTCOffset: '+12:45', UTCDSTOffset: '+13:45' },
{ code: 'FM', latLon: '15872', tz: 'Pacific/Chuuk', UTCOffset: '+10:00', UTCDSTOffset: '+10:00' },
{ code: 'CL', latLon: '-2709-10926', tz: 'Pacific/Easter', UTCOffset: '-06:00', UTCDSTOffset: '-05:00' },
{ code: 'VU', latLon: '-1740+16825', tz: 'Pacific/Efate', UTCOffset: '+11:00', UTCDSTOffset: '+11:00' },
{ code: 'KI', latLon: '-0308-17105', tz: 'Pacific/Enderbury', UTCOffset: '+13:00', UTCDSTOffset: '+13:00' },
{ code: 'TK', latLon: '-0922-17114', tz: 'Pacific/Fakaofo', UTCOffset: '+13:00', UTCDSTOffset: '+13:00' },
{ code: 'FJ', latLon: '-1808+17825', tz: 'Pacific/Fiji', UTCOffset: '+12:00', UTCDSTOffset: '+13:00' },
{ code: 'TV', latLon: '-0831+17913', tz: 'Pacific/Funafuti', UTCOffset: '+12:00', UTCDSTOffset: '+12:00' },
{ code: 'EC', latLon: '-0054-08936', tz: 'Pacific/Galapagos', UTCOffset: '-06:00', UTCDSTOffset: '-06:00' },
{ code: 'PF', latLon: '-2308-13457', tz: 'Pacific/Gambier', UTCOffset: '-09:00', UTCDSTOffset: '-09:00' },
{ code: 'SB', latLon: '-0932+16012', tz: 'Pacific/Guadalcanal', UTCOffset: '+11:00', UTCDSTOffset: '+11:00' },
{ code: 'GU', latLon: '15773', tz: 'Pacific/Guam', UTCOffset: '+10:00', UTCDSTOffset: '+10:00' },
{ code: 'US', latLon: '+211825-1575130', tz: 'Pacific/Honolulu', UTCOffset: '-10:00', UTCDSTOffset: '-10:00' },
{ code: 'KI', latLon: '+0152-15720', tz: 'Pacific/Kiritimati', UTCOffset: '+14:00', UTCDSTOffset: '+14:00' },
{ code: 'FM', latLon: '16778', tz: 'Pacific/Kosrae', UTCOffset: '+11:00', UTCDSTOffset: '+11:00' },
{ code: 'MH', latLon: '17625', tz: 'Pacific/Kwajalein', UTCOffset: '+12:00', UTCDSTOffset: '+12:00' },
{ code: 'MH', latLon: '17821', tz: 'Pacific/Majuro', UTCOffset: '+12:00', UTCDSTOffset: '+12:00' },
{ code: 'PF', latLon: '-0900-13930', tz: 'Pacific/Marquesas', UTCOffset: '-09:30', UTCDSTOffset: '-09:30' },
{ code: 'NR', latLon: '-0031+16655', tz: 'Pacific/Nauru', UTCOffset: '+12:00', UTCDSTOffset: '+12:00' },
{ code: 'NU', latLon: '-1901-16955', tz: 'Pacific/Niue', UTCOffset: '-11:00', UTCDSTOffset: '-11:00' },
{ code: 'NF', latLon: '-2903+16758', tz: 'Pacific/Norfolk', UTCOffset: '+11:00', UTCDSTOffset: '+11:00' },
{ code: 'NC', latLon: '-2216+16627', tz: 'Pacific/Noumea', UTCOffset: '+11:00', UTCDSTOffset: '+11:00' },
{ code: 'AS', latLon: '-1416-17042', tz: 'Pacific/Pago_Pago', UTCOffset: '-11:00', UTCDSTOffset: '-11:00' },
{ code: 'PW', latLon: '14149', tz: 'Pacific/Palau', UTCOffset: '+09:00', UTCDSTOffset: '+09:00' },
{ code: 'PN', latLon: '-2504-13005', tz: 'Pacific/Pitcairn', UTCOffset: '-08:00', UTCDSTOffset: '-08:00' },
{ code: 'FM', latLon: '16471', tz: 'Pacific/Pohnpei', UTCOffset: '+11:00', UTCDSTOffset: '+11:00' },
{ code: 'PG', latLon: '-0930+14710', tz: 'Pacific/Port_Moresby', UTCOffset: '+10:00', UTCDSTOffset: '+10:00' },
{ code: 'CK', latLon: '-2114-15946', tz: 'Pacific/Rarotonga', UTCOffset: '-10:00', UTCDSTOffset: '-10:00' },
{ code: 'PF', latLon: '-1732-14934', tz: 'Pacific/Tahiti', UTCOffset: '-10:00', UTCDSTOffset: '-10:00' },
{ code: 'KI', latLon: '17425', tz: 'Pacific/Tarawa', UTCOffset: '+12:00', UTCDSTOffset: '+12:00' },
{ code: 'TO', latLon: '-2110-17510', tz: 'Pacific/Tongatapu', UTCOffset: '+13:00', UTCDSTOffset: '+14:00' },
{ code: 'UM', latLon: '18554', tz: 'Pacific/Wake', UTCOffset: '+12:00', UTCDSTOffset: '+12:00' },
{ code: 'WF', latLon: '-1318-17610', tz: 'Pacific/Wallis', UTCOffset: '+12:00', UTCDSTOffset: '+12:00' }
]