UNPKG

iobroker.javascript

Version:
448 lines (416 loc) 17.2 kB
'use strict'; // const DEFAULT = { // time: { // exactTime: false, // // start: '00:00', // end: '23:59', // // mode: 'hours', // interval: 1, // }, // period: { // once: '', // days: 1, // dows: '', // dates: '', // weeks: 0, // months: '', // // years: 0, // yearMonth: 0, // yearDate: 0, // }, // valid: { // from: '', // to: '' // } // }; class Scheduler { constructor(log, DateTest, suncalc, latitude, longitude) { this.list = {}; this.Date = DateTest || Date; this.suncalc = suncalc; this.latitude = latitude; this.longitude = longitude; this.log = log || { debug: function (text) {console.log(text);}, info: function (text) {console.log(text);}, log: function (text) {console.log(text);}, warn: function (text) {console.warn(text);}, error: function (text) {console.error(text);}, silly: function (text) {console.log(text);} }; this._setAstroVars(); } _getId() { return Math.round(Math.random() * 1000000) + '.' + this.Date.now(); } recalculate() { const count = Object.keys(this.list).length; if (count && !this.timer) { const d = new this.Date(); d.setMilliseconds(2); // 2 ms to be sure that the next second is reached, they do not hurt anyone d.setSeconds(0); d.setMinutes(d.getMinutes() + 1); this.timer = setTimeout((notBefore) => this.checkSchedules(notBefore), d.getTime() - this.Date.now(), d.getTime()); } else if (!count && this.timer) { clearTimeout(this.timer); this.timer = null; } } getContext() { const now = new this.Date(); return { now: now.getTime(), minutesOfDay: now.getHours() * 60 + now.getMinutes(), y: now.getFullYear(), M: now.getMonth(), d: now.getDate(), h: now.getHours(), m: now.getMinutes(), dow: now.getDay() }; } checkSchedules(notBeforeTime) { const context = this.getContext(); // Work around for not that precise RTCs in some system if (notBeforeTime !== undefined && context.now < notBeforeTime) { this.timer = setTimeout((notBefore) => this.checkSchedules(notBefore), notBeforeTime - this.Date.now(), notBeforeTime); return; } for (const id in this.list) { if (!Object.prototype.hasOwnProperty.call(this.list, id)) { continue; } if (this.checkSchedule(context, this.list[id])) { setImmediate(id => this.list[id] && typeof this.list[id].cb === 'function' && this.list[id].cb(id), id); } } const d = new this.Date(); d.setMilliseconds(2); // 2 ms to be sure that the next second is reached, they do not hurt anyone d.setSeconds(0); d.setMinutes(d.getMinutes() + 1); this.timer = setTimeout((notBefore) => this.checkSchedules(notBefore), d.getTime() - this.Date.now(), d.getTime()); } monthDiff(d1, d2) { let months; months = (d2.getFullYear() - d1.getFullYear()) * 12; months -= d1.getMonth() + 1; months += d2.getMonth(); return months <= 0 ? 0 : months; } checkSchedule(context, schedule) { if (schedule.valid) { if (schedule.valid.from && !this.isPast(context, schedule.valid.from) && !this.isToday(context, schedule.valid.from)) { return; } // "to" this.Date is in the past => delete it from list if (schedule.valid.to && this.isPast(context, schedule.valid.to)) { delete this.list[schedule.id]; return; } } if (schedule.period) { if (schedule.period.once && !this.isToday(context, schedule.period.once)) { if (this.isPast(context, schedule.period.once)) { delete this.list[schedule.id]; } return; } else if (schedule.period.days) { if (schedule.period.dows && !schedule.period.dows.includes(context.dow)) { return; } else if (schedule.period.days > 1) { const diff = Math.round((context.now - schedule.valid.fromDate) / (60000 * 24) + 0.5); if (diff % schedule.period.days) { return; } } } else if (schedule.period.weeks) { if (schedule.period.dows && !schedule.period.dows.includes(context.dow)) { return; } if (schedule.period.weeks > 1) { const diff = Math.round((context.now - schedule.valid.fromDate) / (60000 * 24 * 7) + 0.5); if (diff % schedule.period.weeks) { return; } } } else if (schedule.period.months) { if (Array.isArray(schedule.period.months) && !schedule.period.months.includes(context.M)) { return; } if (schedule.period.fromDate && typeof schedule.period.months === 'number' && schedule.period.months > 1) { const diff = this.monthDiff(schedule.period.fromDate, new this.Date(context.now)); if (diff % schedule.period.months) { return; } } if (schedule.period.dates && !schedule.period.dates.includes(context.d)) { return; } } else if (schedule.period.years) { if (schedule.period.yearMonth !== undefined && schedule.period.yearMonth !== context.M) { return; } if (schedule.period.yearDate && schedule.period.yearDate !== context.d) { return; } if (schedule.period.fromDate && typeof schedule.period.years === 'number' && schedule.period.years > 1) { const diff = Math.floor(this.monthDiff(schedule.period.fromDate, new this.Date(context.now)) / 12); if (diff % schedule.period.years) { return; } } } } if (schedule.time) { let start = schedule.time.start; let end = schedule.time.end; const now = new this.Date(context.now); const astroNameStart = this._getAstroName(start); const astroNameEnd = this._getAstroName(end); if (now.getDate() !== this.todaysAstroTimes['sunrise'].getDate()) { this._setAstroVars(); } if (astroNameStart) { let times = this.todaysAstroTimes; if (times[astroNameStart].getDate() !== now.getDate()) { times = this.yesterdaysAstroTimes; } start = times[astroNameStart]; start = start.getHours() * 60 + start.getMinutes(); } if (astroNameEnd) { let times = this.todaysAstroTimes; if (times[astroNameEnd].getDate() !== now.getDate()) { times = this.yesterdaysAstroTimes; } end = times[astroNameEnd]; end = end.getHours() * 60 + end.getMinutes(); } start = start || 0; end = end || (60 * 24); if (schedule.time.exactTime) { if (context.minutesOfDay !== start) { return; } } else { if (start >= context.minutesOfDay || (end && end < context.minutesOfDay)) { return; } if (schedule.time.mode === 60) { if (schedule.time.interval > 1 && ((context.minutesOfDay - start) % schedule.time.interval)) { return; } } else if (schedule.time.mode === 3600) { if ((context.minutesOfDay - start) % (schedule.time.interval * 60)) { return; } } } } return true; } string2date(date) { let parts = date.split('.'); let d; if (parts.length !== 3) { parts = date.split('-'); d = new this.Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10)); } else { d = new this.Date(parseInt(parts[2], 10), parseInt(parts[1], 10) - 1, parseInt(parts[0], 10)); } return {y: d.getFullYear(), M: d.getMonth(), d: d.getDate()}; } isPast(context, date) { if (date) { if (date.y < context.y) { return true; } else if (date.y === context.y) { if (date.M < context.M) { return true; } else if (date.M === context.M) { if (date.d < context.d) { return true; } } } } } isToday(context, date) { return date && date.y === context.y && date.M === context.M && date.d === context.d; } add(schedule, scriptName, cb) { if (typeof schedule === 'string') { try { schedule = JSON.parse(schedule); } catch (e) { this.log.error('Cannot parse schedule: ' + schedule); return; } } const id = this._getId(); if (typeof schedule !== 'object' || !schedule.period) { return this.log.error('Invalid schedule structure: ' + JSON.stringify(schedule)); } const context = this.getContext(); const sch = JSON.parse(JSON.stringify(schedule)); sch.scriptName = scriptName; sch.original = JSON.stringify(schedule); sch.id = id; sch.cb = cb; if (sch.time && sch.time.start) { const astroNameStart = this._getAstroName(sch.time.start); if (sch.time.start.includes(':')) { const parts = sch.time.start.split(':'); sch.time.start = parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10); } else if (astroNameStart) { sch.time.start = astroNameStart; } else { this.log.error('unknown astro event "' + sch.time.start + '"'); return null; } } if (sch.time && sch.time.end) { const astroNameEnd = this._getAstroName(sch.time.end); if (sch.time.end.includes(':')) { const parts = sch.time.end.split(':'); sch.time.end = parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10); } else if (astroNameEnd) { sch.time.end = astroNameEnd; } else { this.log.error('unknown astro event "' + sch.time.end + '"'); return null; } } if (sch.time.mode === 'minutes') { sch.time.mode = 60; } else if (sch.time.mode === 'hours') { sch.time.mode = 3600; } sch.period.once = sch.period.once && this.string2date(sch.period.once); if (sch.valid) { sch.valid.from = sch.valid.from && this.string2date(sch.valid.from); sch.valid.to = sch.valid.to && this.string2date(sch.valid.to); if (this.isPast(context, sch.valid.to)) { this.log.warn('End of schedule is in the past'); return; } if ((typeof sch.period.days === 'number' && sch.period.days > 1)) { // fromDate must be unix time sch.valid.fromDate = new this.Date(sch.valid.from.y, sch.valid.from.M, sch.valid.from.d).getTime(); } else if (typeof sch.period.weeks === 'number' && sch.period.weeks > 1) { sch.valid.fromDate = sch.valid.from ? new this.Date(sch.valid.from.y, sch.valid.from.M, sch.valid.from.d) : new this.Date(); //sch.valid.fromDate.setDate(-sch.valid.fromDate.getDate() - sch.valid.fromDate.getDay()); // fromDate must be unix time sch.valid.fromDate = sch.valid.fromDate.getTime(); } else if (typeof sch.period.months === 'number' && sch.period.months > 1) { // fromDate must be object sch.valid.fromDate = new this.Date(sch.valid.from.y, sch.valid.from.M, sch.valid.from.d); } else if (typeof sch.period.years === 'number' && sch.period.years > 1) { // fromDate must be object sch.valid.fromDate = new this.Date(sch.valid.from.y, sch.valid.from.M, sch.valid.from.d); } } if (sch.period && ( (typeof sch.period.days === 'number' && sch.period.days > 1) || (typeof sch.period.weeks === 'number' && sch.period.weeks > 1) || (typeof sch.period.months === 'number' && sch.period.months > 1) || (typeof sch.period.years === 'number' && sch.period.years > 1) ) && ( !sch.valid || !sch.valid.fromDate )) { this.log.warn('Invalid Schedule definition: Period day/weeks/months/years only allowed with a valid.from date!'); return; } if (sch.period.dows) { try { sch.period.dows = JSON.parse(sch.period.dows); } catch (e) { this.log.error('Cannot parse day of weeks: ' + sch.period.dows); return; } if (!Array.isArray(sch.period.dows)) { this.log.error('day of weeks is no array: ' + JSON.stringify(sch.period.dows)); return; } } if (sch.period.months && typeof sch.period.months !== 'number') { // can be number or array-string try { sch.period.months = JSON.parse(sch.period.months); } catch (e) { this.log.error('Cannot parse day of months: ' + sch.period.months); return; } sch.period.months = sch.period.months.map(m => m - 1); } if (sch.period.dates) { try { sch.period.dates = JSON.parse(sch.period.dates); } catch (e) { this.log.error('Cannot parse day of dates: ' + sch.period.dates); return; } } sch.period.yearMonth = sch.period.yearMonth && (sch.period.yearMonth - 1); this.list[id] = sch; this.recalculate(); return id; } remove(id) { if (typeof id === 'object' && id.type === 'schedule' && typeof id.id === 'string' && id.id.startsWith('schedule_')) { if (this.list[id.id.substring('schedule_'.length)]) { delete this.list[id.id.substring('schedule_'.length)]; this.recalculate(); return true; } else { return false; } } else if (typeof id === 'string' && this.list[id]) { delete this.list[id]; this.recalculate(); return true; } else { return false; } } getList() { return Object.keys(this.list) .map(id => ({id: 'schedule_' + id, type: 'schedule', schedule: this.list[id].original, scriptId: this.list[id].scriptId})); } get(id) { if (id && typeof id === 'object' && id.type === 'schedule' && typeof id.id === 'string' && id.id.startsWith('schedule_')) { return this.list[id.id.substring('schedule_'.length)]; } else if (typeof id === 'string') { return this.list[id]; } else { return null; } } _setAstroVars() { const todayNoon = new this.Date(); const yesterdayNoon = new this.Date(); todayNoon.setHours(12, 0, 0, 0); yesterdayNoon.setHours(-12, 0, 0, 0); this.todaysAstroTimes = this.suncalc.getTimes(todayNoon, this.latitude, this.longitude); this.yesterdaysAstroTimes = this.suncalc.getTimes(yesterdayNoon, this.latitude, this.longitude); this.astroList = this.astroList || Object.keys(this.todaysAstroTimes); this.astroListLow = this.astroListLow || this.astroList.map(key => key.toLowerCase()); } _getAstroName(evt) { if (typeof evt === 'string') { const pos = this.astroListLow.indexOf(evt.toLowerCase()); if (pos > -1) { return this.astroList[pos]; } } return null; } } module.exports = Scheduler;