UNPKG

hw-server

Version:
717 lines (647 loc) 20.4 kB
/*global module,setTimeout,clearTimeout,setInterval,clearInterval,require*/ const nodeschedule = require("node-schedule"); const cronparser = require("cron-parser"); const suncalc = require("suncalc"); var homework = undefined; var utils = undefined; var location = { latitude: undefined, longitude: undefined }; function caseify(s) { if (typeof s !== "string" || s.length < 2) return s; return s[0].toUpperCase() + s.substr(1).toLowerCase(); } function clone(obj) { var ret = Object.create(null); for (var k in obj) { ret[k] = obj[k]; } return ret; } function createDate(d, adj, set) { const adjust = function(date, adj) { if (!(date instanceof Date)) return undefined; const keys = { time: { set: "setTime", get: "getTime" }, day: { set: "setDate", get: "getDate" }, hour: { set: "setHours", get: "getHours" }, minute: { set: "setMinutes", get: "getMinutes" }, year: { set: "setFullYear", get: "getFullYear" }, month: { set: "setMonth", get: "getMonth" }, second: { set: "setSeconds", get: "getSeconds" }, ms: { set: "setMilliseconds", get: "getMilliseconds" } }; for (var k in keys) { if (k in adj && keys[k].set in date && keys[k].get in date) { if (set) date[keys[k].set](adj[k]); else date[keys[k].set](date[keys[k].get]() + adj[k]); } } return date; }; var dt; if (typeof d === "string") { switch (d) { case "sunset": case "sunrise": if (location.latitude === undefined || location.longitude === undefined) { throw "Need location.latitude and location.longitude set in config"; } dt = suncalc.getTimes(new Date(), location.latitude, location.longitude); if (adj) { adjust(dt[d], adj); if (dt[d].getTime() === dt[d].getTime()) { return dt[d]; } return undefined; } return dt[d]; default: dt = new Date(d); if (adj) adjust(dt, adj); if (dt.getTime() === dt.getTime()) return dt; var ce; var cd = new Date(); cd.setHours(0); cd.setMinutes(0); cd.setSeconds(0); cd.setMilliseconds(0); try { ce = cronparser.parseExpression(d, { currentDate: cd } ); } catch (e) { return undefined; } dt = ce.next().toDate(); if (adj) adjust(dt, adj); if (dt.getTime() === dt.getTime()) return dt; } } else if (typeof d === "object") { if (d instanceof Date) { if (adj) adjust(d, adj); if (d.getTime() === d.getTime()) return d; return undefined; } dt = adjust(new Date(), d); if (dt.getTime() === dt.getTime()) return dt; } return undefined; } function Timer(type, name) { var f = this._funcs[type]; if (f === undefined) { throw "Invalid timer " + type; } this._type = type; this._id = undefined; this._name = name; this._start = f.start; this._stop = f.stop; this._single = f.single; this._state = "idle"; this._started = undefined; this._timeout = undefined; this._initOns(); } Timer.prototype = { _funcs: { timeout: { start: setTimeout, stop: clearTimeout, single: true }, interval: { start: setInterval, stop: clearInterval, single: false } }, _id: undefined, _type: undefined, _start: undefined, _stop: undefined, _single: undefined, _state: undefined, _started: undefined, _timeout: undefined, start: function(ms) { if (this._id !== undefined) this._stop(this._id); this._state = "running"; this._started = Date.now(); this._timeout = ms; homework.timerUpdated(this._type, this._name); this._id = this._start(() => { this._emit("fired"); if (this._single) { this._id = undefined; this._state = "idle"; this._started = undefined; } else { this._started = Date.now(); } homework.timerUpdated(this._type, this._name); }, ms); }, stop: function() { if (this._id !== undefined) { this._stop(this._id); this._state = "idle"; this._id = undefined; homework.timerUpdated(this._type, this._name); } }, destroy: function() { this.stop(); this._emit("destroyed"); }, get state() { return this._state; }, get started() { return this._started; }, get single() { return this._single; }, get timeout() { return this._timeout; } }; function Schedule(val, name) { if (arguments.length !== 2) { throw "Schedule needs two argument, spec and name"; } this._name = name; this._init(val); this._initOns(); } Schedule.prototype = { _job: undefined, _date: undefined, get date() { return this._date; }, set date(spec) { if (spec == this._date) return; this.stop(); this._init(spec); }, stop: function() { if (this._job) { this._job.cancel(); this._job = undefined; homework.timerUpdated("schedule", this._name); } }, destroy: function() { this.stop(); this._emit("destroyed"); }, _fire: function() { this._emit("fired"); homework.timerUpdated("schedule", this._name); }, _init: function(val) { this._job = nodeschedule.scheduleJob(val, () => { this._fire(); }); if (!this._job) { if (typeof val === "string") { var date = new Date(val); // Fun fact, NaN === NaN returns false if (date.getTime() === date.getTime()) { this._job = nodeschedule.scheduleJob(date, () => { this._fire(); }); } else { // might be a sunset/sunrise thing this._createSpecial(val); } } if (!this._job) throw "Couldn't schedule job " + JSON.stringify(val); } this._date = val; // we don't want this to happen as part of construction so delay it slightly setTimeout(() => { homework.timerUpdated("schedule", this._name); }, 50); }, _createSpecial: function(val) { this.stop(); var now = new Date(); var date = createDate(val); if (date) { if (now.getTime() + 10000 > date.getTime()) { date = createDate(val, { day: 1 }, false); if (date && now < date) { this._job = nodeschedule.scheduleJob(date, () => { this._fire(); this._createSpecial(val); }); } } else { this._job = nodeschedule.scheduleJob(date, () => { this._fire(); this._createSpecial(val); }); } } } }; function Event() { if (arguments.length < 1) { throw "Timer event needs one arguments, name"; } if (this._type !== "timeout" && this._type !== "interval" && this._type !== "schedule") { throw "Timer event type mismatch " + this._type; } const obj = timers[this._type]; if (!(arguments[0] in obj)) { throw "Timer event given a timer name that doesn't exists " + arguments[0]; } this._initOns(); this._fired = false; this._name = arguments[0]; this._keep = false; const tt = obj[arguments[0]]; if (this._type === "schedule") { this._date = tt.date; } else { this._date = undefined; } tt.on("fired", () => { homework.Console.log("triggering timer event", this._name); this._fired = true; this._emit("triggered", this); if (!this._keep) { setTimeout(() => { this._fired = false; }, 1000); } }); tt.on("destroyed", () => { this._emit("cancel"); }); } Event.prototype = { _date: undefined, check: function() { return this._fired; }, serialize: function() { return { type: "TimerEvent" + this._type, name: this._name, date: this._date }; }, format: function() { return [caseify(this._type), this._name, "fires"]; } }; function TimeoutEvent() { Event.apply(this, arguments); this._keep = true; } TimeoutEvent.prototype = clone(Event.prototype); TimeoutEvent.prototype._type = "timeout"; function IntervalEvent() { Event.apply(this, arguments); } IntervalEvent.prototype = clone(Event.prototype); IntervalEvent.prototype._type = "interval"; function ScheduleEvent() { Event.apply(this, arguments); } ScheduleEvent.prototype = clone(Event.prototype); ScheduleEvent.prototype._type = "schedule"; function RangeEvent(start, end) { if (arguments.length !== 2) { throw "RangeEvent needs two arguments, start and end"; } const fromJSON = function(a) { if (typeof a === "object") { return a; } else if (typeof a === "string") { var ret; try { ret = JSON.parse(a); } catch (e) { return a; } return ret; } return undefined; }; const allowed = function(a) { if (typeof a === "object") return true; switch (a) { case "sunrise": case "sunset": return true; } // might be a cron expression try { cronparser.parseExpression(a); } catch (e) { return false; } return true; }; start = fromJSON(start); end = fromJSON(end); if (!allowed(start)) { throw "RangeEvent start is not allowed: " + JSON.stringify(start); } if (!allowed(end)) { throw "RangeEvent end is not allowed: " + JSON.stringify(end); } var d1 = createDate(start); var d2 = createDate(end); if (d1 && d1 instanceof Date && d2 && d2 instanceof Date) { this._start = start; this._end = end; if (d1 > d2) { this._endAdjust = { day: 1 }; } else { this._endAdjust = undefined; } this._initOns(); } else { throw "RangeEvent needs two date arguments"; } } RangeEvent.prototype = { _start: undefined, _end: undefined, _endAdjust: undefined, check: function() { var d1 = createDate(this._start); var d2 = createDate(this._end, this._endAdjust, false); var now = new Date(); // console.log("checking range1", now); // console.log("checking range2", d1, this._start); // console.log("checking range3", d2, this._end, this._endAdjust); if (now < d1 && now < d2) { // try to adjust -1 but only if the end date is one day past and current is the same day as start if (now.getDate() == d1.getDate() && now.getDate() + 1 == d2.getDate()) { d1 = createDate(this._start, { day: -1 }, false); var adj = this._endAdjust ? clone(this._endAdjust) : Object.create(null); if (!("day" in adj)) { adj.day = -1; } else { adj.day -= 1; } d1 = createDate(this._start, { day: -1 }, false); d2 = createDate(this._end, adj, false); // console.log("checking again range1", now); // console.log("checking again range2", d1, this._start); // console.log("checking again range3", d2, this._end, this._endAdjust); } } return d1 <= now && d2 >= now; }, serialize: function() { return { type: "TimeRangeEvent", start: this._start, end: this._end }; }, format: function() { return ["TimeRange", this._start, this._end]; } }; function Action() { if (arguments.length < 2) { throw "Timer action needs at least two arguments, name and action"; } if (this._type !== "timeout" && this._type !== "interval") { throw "Timer action argument mismatch " + this._type; } if (arguments[1] !== "start" && arguments[1] !== "stop") { throw "Timer action needs to be start or stop"; } if (arguments[1] === "start" && arguments.length !== 3) { throw "Timer action start needs three arguments, name, action and timeout"; } const obj = timers[this._type]; if (!(arguments[0] in obj)) { throw "Timer action given a timer name that doesn't exists " + arguments[0]; } this._initOns(); obj[arguments[0]].on("destroyed", () => { this._emit("cancel"); }); this._name = arguments[0]; this._action = arguments[1]; if (arguments.length === 3) this._timeout = arguments[2]; this._timer = obj[arguments[0]]; } Action.prototype = { trigger: function(pri) { if (pri === homework.rulePriorities.Medium) { homework.Console.log("trigger timer action", this._action, this._timeout); switch (this._action) { case "stop": this._timer.stop(); break; case "start": this._timer.start(parseInt(this._timeout)); break; } } }, serialize: function() { return { type: "TimerAction" + this._type, name: this._name, action: this._action, arg: this._timeout }; }, format: function() { var ret = [caseify(this._type), this._name, this._action]; if (this._timeout !== undefined) ret.push(this._timeout); return ret; } }; function TimeoutAction() { Action.apply(this, arguments); } TimeoutAction.prototype = clone(Action.prototype); TimeoutAction.prototype._type = "timeout"; function IntervalAction() { Action.apply(this, arguments); } IntervalAction.prototype = clone(Action.prototype); IntervalAction.prototype._type = "interval"; function rangeEventCompleter() { // ### complete on dates? if (arguments.length < 2) return { type: "string", values: ["sunrise", "sunset"] }; return { values: [] }; } function rangeEventDeserializer(e) { if (!(typeof e === "object")) return null; if (e.type !== "TimeRangeEvent") return null; var event; try { event = new RangeEvent(e.start, e.end); } catch (e) { return null; } return event; } function eventCompleter(items) { if (!items) return { values: [] }; if (arguments.length === 1) { return { type: "array", values: Object.keys(items) }; } // see if this timer exists if (!(arguments[1] in items)) { return { values: [] }; } var args = utils.strip(arguments).slice(1); if (args.length === 1) { return { type: "array", values: ["fires"] }; } return { values: [] }; } function actionCompleter(items) { if (!items) return { values: [] }; if (arguments.length === 1) { return { type: "array", values: Object.keys(items) }; } // see if this timer exists if (!(arguments[1] in items)) { return { values: [] }; } var args = utils.strip(arguments).slice(1); if (args.length === 1) { return { type: "array", values: ["start", "stop"] }; } if (args.length === 2 && args[1] === "start") { return { type: "number", values: [] }; } return { values: [] }; } function eventDeserializer(type, e) { if (!(typeof e === "object")) return null; if (e.type !== "TimerEvent" + type) return null; var event; try { // create the timer now if it doesn't exist timers.create(type, e.name, e.date); if (type === "timeout") event = new TimeoutEvent(e.name); else if (type === "interval") event = new IntervalEvent(e.name); else if (type === "schedule") event = new ScheduleEvent(e.name); } catch (e) { return null; } return event; } function actionDeserializer(type, e) { if (!(typeof e === "object")) return null; if (e.type !== "TimerAction" + type) return null; var event; try { // create the timer now if it doesn't exist timers.create(type, e.name); if (type === "timeout") event = new TimeoutAction(e.name, e.action, e.arg); else if (type === "interval") event = new IntervalAction(e.name, e.action, e.arg); } catch (e) { return null; } return event; } const timers = { timeout: Object.create(null), interval: Object.create(null), schedule: Object.create(null), init: function(hw) { homework = hw; utils = hw.utils; const cfg = hw.config; if ("location" in cfg) { if ("latitude" in cfg.location && "longitude" in cfg.location) { location.latitude = cfg.location.latitude; location.longitude = cfg.location.longitude; } else { throw "need latitude and longitude in config"; } } utils.onify(Timer.prototype); utils.onify(Schedule.prototype); utils.onify(TimeoutEvent.prototype); utils.onify(TimeoutAction.prototype); utils.onify(IntervalEvent.prototype); utils.onify(IntervalAction.prototype); utils.onify(ScheduleEvent.prototype); utils.onify(RangeEvent.prototype); homework.registerEvent("Timeout", TimeoutEvent, eventCompleter.bind(null, timers.timeout), eventDeserializer.bind(null, "timeout")); homework.registerAction("Timeout", TimeoutAction, actionCompleter.bind(null, timers.timeout), actionDeserializer.bind(null, "timeout")); homework.registerEvent("Interval", IntervalEvent, eventCompleter.bind(null, timers.interval), eventDeserializer.bind(null, "interval")); homework.registerAction("Interval", IntervalAction, actionCompleter.bind(null, timers.interval), actionDeserializer.bind(null, "interval")); homework.registerEvent("Schedule", ScheduleEvent, eventCompleter.bind(null, timers.schedule), eventDeserializer.bind(null, "schedule")); homework.registerEvent("TimeRange", RangeEvent, rangeEventCompleter, rangeEventDeserializer); homework.loadRules(); }, names: function(type) { if (type !== "timeout" && type !== "interval" && type !== "schedule") return []; return Object.keys(this[type]); }, create: function(type, name) { if (type !== "timeout" && type !== "interval" && type !== "schedule") return null; if (name in this[type]) return null; var t; if (type === "schedule") { if (arguments.length < 3) return null; t = new Schedule([].slice.apply(arguments).slice(2).join(" "), name); this.schedule[name] = t; } else { t = new Timer(type, name); this[type][name] = t; } if (type !== "schedule") homework.timerUpdated(type, name); return t; }, destroy: function(type, name) { if (type !== "timeout" && type !== "interval" && type !== "schedule") return false; if (name in this[type]) { this[type][name].destroy(); delete this[type][name]; return true; } return false; } }; module.exports = timers;