smart-home-engine 2.0.1 → 3.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/web/assets/index-BZWF5ORr.js +394 -0
- package/dist/web/assets/{index-Bu5o4KXc.css → index-CdLKmsX4.css} +1 -1
- package/dist/web/assets/{tsMode-20i8zomj.js → tsMode-BquNtbO5.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/config.js +21 -1
- package/src/index.js +532 -86
- package/src/lib/call-site.js +51 -0
- package/src/lib/command-topics.js +162 -0
- package/src/lib/command.js +188 -0
- package/src/lib/holidays.js +188 -0
- package/src/lib/publish-changed.js +45 -0
- package/src/lib/runtime.js +350 -0
- package/src/lib/script-timers.js +49 -7
- package/src/lib/time.js +402 -0
- package/src/lib/timing.js +421 -0
- package/src/lib/update-channel.js +152 -0
- package/src/sandbox/stdlib.js +14 -0
- package/src/web/ai-tools.js +7 -3
- package/src/web/config-api.js +4 -3
- package/src/web/log-ws.js +49 -2
- package/src/web/prompts/api-ref-compact.md +6 -3
- package/src/web/prompts/api-ref.md +45 -4
- package/src/web/runtime-api.js +66 -0
- package/src/web/server.js +51 -19
- package/dist/web/assets/index-CqYTIaIG.js +0 -306
package/src/lib/time.js
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Zone-aware time helpers for scripts: time windows, wall-clock instants, weekend and holidays,
|
|
4
|
+
* formatting, durations — `she.time`.
|
|
5
|
+
*
|
|
6
|
+
* Everything is computed from `Intl.DateTimeFormat` in the configured time zone, so a script
|
|
7
|
+
* gets the same answers whatever zone the process runs in, and the daylight-saving changes are
|
|
8
|
+
* handled once, here, instead of in every script:
|
|
9
|
+
*
|
|
10
|
+
* - A wall-clock time that does not exist (02:30 on the day the clocks go forward) resolves
|
|
11
|
+
* to the instant one hour later on the wall clock (03:30), like Temporal's `compatible`
|
|
12
|
+
* disambiguation. One that exists twice (02:30 on the day the clocks go back) resolves to
|
|
13
|
+
* its first occurrence (the one still in summer time).
|
|
14
|
+
* - A window compares instants, so the window 02:00–03:00 lasts two hours on the autumn day
|
|
15
|
+
* and is empty on the spring day.
|
|
16
|
+
* - Durations are differences of instants: a night that spans the autumn change is an hour
|
|
17
|
+
* longer than its wall-clock times suggest, and `duration` says so.
|
|
18
|
+
*
|
|
19
|
+
* The helpers take a timestamp in ms (what `she.now()`, `obj.ts` and `Date.now()` give), a
|
|
20
|
+
* `Date`, or an ISO string, and default to now. No dependency besides Intl.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const holidayTable = require('./holidays');
|
|
24
|
+
|
|
25
|
+
const DAY = 86400000;
|
|
26
|
+
const WEEKDAYS = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
|
|
27
|
+
|
|
28
|
+
const _formatters = new Map();
|
|
29
|
+
function formatter(locale, options) {
|
|
30
|
+
const key = locale + '|' + JSON.stringify(options);
|
|
31
|
+
let f = _formatters.get(key);
|
|
32
|
+
if (!f) {
|
|
33
|
+
f = new Intl.DateTimeFormat(locale, options);
|
|
34
|
+
if (_formatters.size > 500) _formatters.clear();
|
|
35
|
+
_formatters.set(key, f);
|
|
36
|
+
}
|
|
37
|
+
return f;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The IANA zone the process runs in. */
|
|
41
|
+
function systemZone() {
|
|
42
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isZone(name) {
|
|
46
|
+
if (typeof name !== 'string' || !name) return false;
|
|
47
|
+
try {
|
|
48
|
+
new Intl.DateTimeFormat('en-US', { timeZone: name });
|
|
49
|
+
return true;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isLocale(tag) {
|
|
56
|
+
if (typeof tag !== 'string' || !tag) return false;
|
|
57
|
+
try {
|
|
58
|
+
return Intl.DateTimeFormat.supportedLocalesOf([tag]).length > 0;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The wall-clock fields of an instant in a zone.
|
|
66
|
+
* @returns {{year:number, month:number, day:number, hour:number, minute:number, second:number, millisecond:number, weekday:number, offset:number}}
|
|
67
|
+
* month 1–12, weekday 0 (Sunday) – 6, offset in minutes east of UTC
|
|
68
|
+
*/
|
|
69
|
+
function wallParts(timeZone, ts) {
|
|
70
|
+
const f = formatter('en-US', {
|
|
71
|
+
timeZone,
|
|
72
|
+
hourCycle: 'h23',
|
|
73
|
+
year: 'numeric',
|
|
74
|
+
month: '2-digit',
|
|
75
|
+
day: '2-digit',
|
|
76
|
+
hour: '2-digit',
|
|
77
|
+
minute: '2-digit',
|
|
78
|
+
second: '2-digit',
|
|
79
|
+
weekday: 'short',
|
|
80
|
+
});
|
|
81
|
+
const whole = Math.floor(ts / 1000) * 1000;
|
|
82
|
+
const p = {};
|
|
83
|
+
for (const { type, value } of f.formatToParts(new Date(whole))) p[type] = value;
|
|
84
|
+
const year = Number(p.year);
|
|
85
|
+
const month = Number(p.month);
|
|
86
|
+
const day = Number(p.day);
|
|
87
|
+
const hour = Number(p.hour) % 24;
|
|
88
|
+
const minute = Number(p.minute);
|
|
89
|
+
const second = Number(p.second);
|
|
90
|
+
const offset = Math.round((Date.UTC(year, month - 1, day, hour, minute, second) - whole) / 60000);
|
|
91
|
+
return { year, month, day, hour, minute, second, millisecond: ts - whole, weekday: WEEKDAYS[p.weekday], offset };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function offsetAt(timeZone, ts) {
|
|
95
|
+
return wallParts(timeZone, ts).offset;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The instant of a wall-clock time in a zone. Fields overflow like `Date.UTC` (day 32, hour 24).
|
|
100
|
+
* A time in a gap resolves forward by the gap; an ambiguous time to its first occurrence.
|
|
101
|
+
*/
|
|
102
|
+
function zonedToInstant(timeZone, year, month, day, hour = 0, minute = 0, second = 0, millisecond = 0) {
|
|
103
|
+
const wall = Date.UTC(year, month - 1, day, hour, minute, second, millisecond);
|
|
104
|
+
const before = offsetAt(timeZone, wall - DAY);
|
|
105
|
+
const after = offsetAt(timeZone, wall + DAY);
|
|
106
|
+
let first = NaN;
|
|
107
|
+
for (const off of before === after ? [before] : [before, after]) {
|
|
108
|
+
const instant = wall - off * 60000;
|
|
109
|
+
if (offsetAt(timeZone, instant) === off && !(instant >= first)) first = instant;
|
|
110
|
+
}
|
|
111
|
+
return Number.isNaN(first) ? wall - before * 60000 : first;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const UNIT_MS = { ms: 1, s: 1000, m: 60000, h: 3600000 };
|
|
115
|
+
|
|
116
|
+
function describe(x) {
|
|
117
|
+
if (typeof x === 'string') return JSON.stringify(x);
|
|
118
|
+
if (x && typeof x === 'object') return Object.prototype.toString.call(x);
|
|
119
|
+
return String(x);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {object} opts
|
|
124
|
+
* @param {string} [opts.timeZone] IANA zone; default the process zone
|
|
125
|
+
* @param {string} [opts.locale] BCP 47 tag for names and words; default 'de-DE'
|
|
126
|
+
* @param {string|null} [opts.holidays] region code of the holiday table, e.g. 'DE-BW'; null = not configured
|
|
127
|
+
* @param {Array<{date: string, name: string}>} [opts.holidaysExtra] parsed with holidays.parseExtra
|
|
128
|
+
* @param {Function} [opts.sunTimes] (noonMs) → the sun events of that day as Dates (suncalc.getTimes)
|
|
129
|
+
* @param {Function} [opts.now] → ms, default Date.now
|
|
130
|
+
* @param {Function} [opts.warn] (msg) one-off warnings
|
|
131
|
+
*/
|
|
132
|
+
function createTime(opts = {}) {
|
|
133
|
+
const timeZone = isZone(opts.timeZone) ? opts.timeZone : systemZone();
|
|
134
|
+
const locale = isLocale(opts.locale) ? opts.locale : 'de-DE';
|
|
135
|
+
const holidays = holidayTable.isRegion(opts.holidays) ? holidayTable.normalizeRegion(opts.holidays) : null;
|
|
136
|
+
const extra = Array.isArray(opts.holidaysExtra) ? opts.holidaysExtra : [];
|
|
137
|
+
const sunTimes = typeof opts.sunTimes === 'function' ? opts.sunTimes : null;
|
|
138
|
+
const now = typeof opts.now === 'function' ? opts.now : Date.now;
|
|
139
|
+
const warn = typeof opts.warn === 'function' ? opts.warn : () => {};
|
|
140
|
+
let warnedHolidays = false;
|
|
141
|
+
|
|
142
|
+
function toMs(x, api, what = 'ts') {
|
|
143
|
+
if (x === undefined || x === null) return now();
|
|
144
|
+
if (typeof x === 'number') return x;
|
|
145
|
+
if (x && typeof x === 'object' && typeof x.getTime === 'function') return x.getTime();
|
|
146
|
+
if (typeof x === 'string') {
|
|
147
|
+
const ms = Date.parse(x);
|
|
148
|
+
if (!Number.isNaN(ms)) return ms;
|
|
149
|
+
}
|
|
150
|
+
throw new TypeError(`${api}: ${what} must be a timestamp in ms, a Date or an ISO string (got ${describe(x)})`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const pad = (n, w = 2) => String(n).padStart(w, '0');
|
|
154
|
+
|
|
155
|
+
function offsetString(minutes) {
|
|
156
|
+
const sign = minutes < 0 ? '-' : '+';
|
|
157
|
+
const abs = Math.abs(minutes);
|
|
158
|
+
return `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parts(ts) {
|
|
162
|
+
const p = wallParts(timeZone, toMs(ts, 'she.time.parts([ts])'));
|
|
163
|
+
p.zone = timeZone;
|
|
164
|
+
return p;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function startOfDay(ts) {
|
|
168
|
+
const p = wallParts(timeZone, toMs(ts, 'she.time.startOfDay([ts])'));
|
|
169
|
+
return zonedToInstant(timeZone, p.year, p.month, p.day);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function endOfDay(ts) {
|
|
173
|
+
const p = wallParts(timeZone, toMs(ts, 'she.time.endOfDay([ts])'));
|
|
174
|
+
return zonedToInstant(timeZone, p.year, p.month, p.day + 1) - 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function addDays(ts, days) {
|
|
178
|
+
const api = 'she.time.addDays(ts, days)';
|
|
179
|
+
const p = wallParts(timeZone, toMs(ts, api));
|
|
180
|
+
if (typeof days !== 'number' || !Number.isInteger(days)) throw new TypeError(`${api}: days must be an integer (got ${describe(days)})`);
|
|
181
|
+
return zonedToInstant(timeZone, p.year, p.month, p.day + days, p.hour, p.minute, p.second, p.millisecond);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Resolve 'HH:mm[:ss]' or a sun event with offset ('sunset-30m') on the day of `dayTs`. */
|
|
185
|
+
function resolve(spec, dayTs, api) {
|
|
186
|
+
if (typeof spec === 'number') return spec;
|
|
187
|
+
if (spec && typeof spec === 'object' && typeof spec.getTime === 'function') return spec.getTime();
|
|
188
|
+
if (typeof spec !== 'string') throw new TypeError(`${api}: expected 'HH:mm' or a sun event like 'sunset-30m' (got ${describe(spec)})`);
|
|
189
|
+
const p = wallParts(timeZone, dayTs);
|
|
190
|
+
const clock = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/.exec(spec.trim());
|
|
191
|
+
if (clock) {
|
|
192
|
+
const [, h, mi, s] = clock.map(Number);
|
|
193
|
+
if (h > 24 || mi > 59 || s > 59 || (h === 24 && (mi || s))) throw new TypeError(`${api}: ${describe(spec)} is not a time of day`);
|
|
194
|
+
return zonedToInstant(timeZone, p.year, p.month, p.day, h, mi, s || 0);
|
|
195
|
+
}
|
|
196
|
+
const sun = /^([a-zA-Z]+)(?:\s*([+-])\s*(\d+(?:\.\d+)?)(ms|s|m|h)?)?$/.exec(spec.trim());
|
|
197
|
+
if (!sun) throw new TypeError(`${api}: expected 'HH:mm' or a sun event like 'sunset-30m' (got ${describe(spec)})`);
|
|
198
|
+
const [, name, sign, amount, unit] = sun;
|
|
199
|
+
if (!sunTimes) throw new TypeError(`${api}: sun events are not available here`);
|
|
200
|
+
const times = sunTimes(zonedToInstant(timeZone, p.year, p.month, p.day, 12));
|
|
201
|
+
if (!times || !Object.prototype.hasOwnProperty.call(times, name)) {
|
|
202
|
+
throw new TypeError(`${api}: unknown time or sun event ${describe(spec)} — expected 'HH:mm' or one of ${Object.keys(times || {}).join(', ')}`);
|
|
203
|
+
}
|
|
204
|
+
if (sign && !unit) throw new TypeError(`${api}: the offset of ${describe(spec)} needs a unit: ms, s, m or h (e.g. 'sunset-30m')`);
|
|
205
|
+
const base = times[name] instanceof Date ? times[name].getTime() : toMs(times[name], api);
|
|
206
|
+
return base + (sign ? (sign === '-' ? -1 : 1) * Number(amount) * UNIT_MS[unit] : 0);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function at(spec, ts) {
|
|
210
|
+
const api = 'she.time.at(time, [ts])';
|
|
211
|
+
return resolve(spec, toMs(ts, api), api);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function between(from, to, ts) {
|
|
215
|
+
const api = 'she.time.between(from, to, [ts])';
|
|
216
|
+
const t = toMs(ts, api);
|
|
217
|
+
const f = resolve(from, t, api);
|
|
218
|
+
const e = resolve(to, t, api);
|
|
219
|
+
if (Number.isNaN(f) || Number.isNaN(e)) return false;
|
|
220
|
+
return f <= e ? t >= f && t < e : t >= f || t < e;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isWeekend(ts) {
|
|
224
|
+
const d = wallParts(timeZone, toMs(ts, 'she.time.isWeekend([ts])')).weekday;
|
|
225
|
+
return d === 0 || d === 6;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function holiday(ts) {
|
|
229
|
+
const p = wallParts(timeZone, toMs(ts, 'she.time.holiday([ts])'));
|
|
230
|
+
const md = `${pad(p.month)}-${pad(p.day)}`;
|
|
231
|
+
const ymd = `${p.year}-${md}`;
|
|
232
|
+
for (const x of extra) if (x.date === md || x.date === ymd) return x.name;
|
|
233
|
+
if (!holidays) {
|
|
234
|
+
if (!warnedHolidays && !extra.length) {
|
|
235
|
+
warnedHolidays = true;
|
|
236
|
+
warn('she.time: no holidays configured — set "holidays" (a region such as DE-BW) or "holidaysExtra" in the config; isHoliday() is always false');
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
return holidayTable.holidaysOfYear(holidays, p.year).get(md) || null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const isHoliday = (ts) => holiday(ts) !== null;
|
|
244
|
+
const isWorkday = (ts) => !isWeekend(ts) && !isHoliday(ts);
|
|
245
|
+
|
|
246
|
+
function names(kind, style, ts) {
|
|
247
|
+
return formatter(locale, { timeZone, [kind]: style }).format(new Date(ts));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const TOKEN = /\[([^\]]*)\]|YYYY|YY|MMMM|MMM|MM|M|DD|D|HH|H|mm|m|ss|s|SSS|ddd|dd|Z|./gs;
|
|
251
|
+
|
|
252
|
+
function format(ts, pattern = 'YYYY-MM-DD HH:mm:ss') {
|
|
253
|
+
const api = 'she.time.format(ts, pattern)';
|
|
254
|
+
const ms = toMs(ts, api);
|
|
255
|
+
if (typeof pattern !== 'string') throw new TypeError(`${api}: pattern must be a string (got ${describe(pattern)})`);
|
|
256
|
+
const p = wallParts(timeZone, ms);
|
|
257
|
+
return pattern.replace(TOKEN, (tok, literal) => {
|
|
258
|
+
if (literal !== undefined) return literal;
|
|
259
|
+
switch (tok) {
|
|
260
|
+
case 'YYYY':
|
|
261
|
+
return pad(p.year, 4);
|
|
262
|
+
case 'YY':
|
|
263
|
+
return pad(p.year % 100);
|
|
264
|
+
case 'MMMM':
|
|
265
|
+
return names('month', 'long', ms);
|
|
266
|
+
case 'MMM':
|
|
267
|
+
return names('month', 'short', ms);
|
|
268
|
+
case 'MM':
|
|
269
|
+
return pad(p.month);
|
|
270
|
+
case 'M':
|
|
271
|
+
return String(p.month);
|
|
272
|
+
case 'DD':
|
|
273
|
+
return pad(p.day);
|
|
274
|
+
case 'D':
|
|
275
|
+
return String(p.day);
|
|
276
|
+
case 'HH':
|
|
277
|
+
return pad(p.hour);
|
|
278
|
+
case 'H':
|
|
279
|
+
return String(p.hour);
|
|
280
|
+
case 'mm':
|
|
281
|
+
return pad(p.minute);
|
|
282
|
+
case 'm':
|
|
283
|
+
return String(p.minute);
|
|
284
|
+
case 'ss':
|
|
285
|
+
return pad(p.second);
|
|
286
|
+
case 's':
|
|
287
|
+
return String(p.second);
|
|
288
|
+
case 'SSS':
|
|
289
|
+
return pad(p.millisecond, 3);
|
|
290
|
+
case 'ddd':
|
|
291
|
+
return names('weekday', 'long', ms);
|
|
292
|
+
case 'dd':
|
|
293
|
+
return names('weekday', 'short', ms);
|
|
294
|
+
case 'Z':
|
|
295
|
+
return offsetString(p.offset);
|
|
296
|
+
default:
|
|
297
|
+
return tok;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function iso(ts) {
|
|
303
|
+
const ms = toMs(ts, 'she.time.iso([ts])');
|
|
304
|
+
const p = wallParts(timeZone, ms);
|
|
305
|
+
return `${pad(p.year, 4)}-${pad(p.month)}-${pad(p.day)}T${pad(p.hour)}:${pad(p.minute)}:${pad(p.second)}${offsetString(p.offset)}`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function duration(a, b) {
|
|
309
|
+
const api = 'she.time.duration(a, b)';
|
|
310
|
+
return toMs(b, api, 'b') - toMs(a, api, 'a');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const UNITS = [
|
|
314
|
+
['day', 'd', DAY],
|
|
315
|
+
['hour', 'h', 3600000],
|
|
316
|
+
['minute', 'min', 60000],
|
|
317
|
+
['second', 's', 1000],
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* '2 h 40 min' — the two largest non-zero units with the SI-style symbols d, h, min, s;
|
|
322
|
+
* `{ long: true }` spells them out in the locale ('2 Stunden 40 Minuten'); `{ parts: n }`
|
|
323
|
+
* shows up to n units.
|
|
324
|
+
*/
|
|
325
|
+
function humanize(ms, options = {}) {
|
|
326
|
+
const api = 'she.time.humanize(ms, [opts])';
|
|
327
|
+
if (typeof ms !== 'number' || Number.isNaN(ms)) throw new TypeError(`${api}: ms must be a number (got ${describe(ms)})`);
|
|
328
|
+
const { long = false, parts: maxParts = 2 } = options;
|
|
329
|
+
const word = (n, unit, symbol) => (long ? new Intl.NumberFormat(locale, { style: 'unit', unit, unitDisplay: 'long' }).format(n) : `${n} ${symbol}`);
|
|
330
|
+
const sign = ms < 0 ? '-' : '';
|
|
331
|
+
let rest = Math.abs(Math.round(ms));
|
|
332
|
+
if (rest < 1000) return sign + word(rest, 'millisecond', 'ms');
|
|
333
|
+
const out = [];
|
|
334
|
+
let used = 0; // units from the largest non-zero one on, zero ones included: 2 h 0 min 30 s is '2 h'
|
|
335
|
+
for (const [unit, symbol, size] of UNITS) {
|
|
336
|
+
const n = Math.floor(rest / size);
|
|
337
|
+
rest -= n * size;
|
|
338
|
+
if (n > 0 || used) {
|
|
339
|
+
used++;
|
|
340
|
+
if (n > 0) out.push(word(n, unit, symbol));
|
|
341
|
+
if (used >= maxParts) break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return sign + out.join(' ');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return Object.freeze({
|
|
348
|
+
zone: timeZone,
|
|
349
|
+
locale,
|
|
350
|
+
holidays,
|
|
351
|
+
now: () => now(),
|
|
352
|
+
parts,
|
|
353
|
+
at,
|
|
354
|
+
between,
|
|
355
|
+
startOfDay,
|
|
356
|
+
endOfDay,
|
|
357
|
+
addDays,
|
|
358
|
+
isWeekend,
|
|
359
|
+
isHoliday,
|
|
360
|
+
holiday,
|
|
361
|
+
isWorkday,
|
|
362
|
+
format,
|
|
363
|
+
iso,
|
|
364
|
+
duration,
|
|
365
|
+
humanize,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* The validated time settings of a daemon from its config, with one warning per problem.
|
|
371
|
+
* @param {object} config `timezone`, `locale`, `holidays`, `holidaysExtra`
|
|
372
|
+
* @param {Function} [warn] (msg)
|
|
373
|
+
* @returns {{ timeZone: string, locale: string, holidays: string|null, holidaysExtra: Array<{date: string, name: string}> }}
|
|
374
|
+
*/
|
|
375
|
+
function settingsFromConfig(config = {}, warn = () => {}) {
|
|
376
|
+
let timeZone = systemZone();
|
|
377
|
+
if (config.timezone) {
|
|
378
|
+
if (isZone(config.timezone)) timeZone = config.timezone;
|
|
379
|
+
else warn(`config: timezone "${config.timezone}" is not an IANA time zone — using ${timeZone}`);
|
|
380
|
+
}
|
|
381
|
+
let locale = 'de-DE';
|
|
382
|
+
if (config.locale) {
|
|
383
|
+
if (isLocale(config.locale)) locale = config.locale;
|
|
384
|
+
else warn(`config: locale "${config.locale}" is not a known locale — using ${locale}`);
|
|
385
|
+
}
|
|
386
|
+
let holidays = null;
|
|
387
|
+
if (config.holidays) {
|
|
388
|
+
if (holidayTable.isRegion(config.holidays)) holidays = holidayTable.normalizeRegion(config.holidays);
|
|
389
|
+
else
|
|
390
|
+
warn(
|
|
391
|
+
`config: holidays "${config.holidays}" is not a known region — known: ${holidayTable
|
|
392
|
+
.listRegions()
|
|
393
|
+
.map((r) => r.code)
|
|
394
|
+
.join(', ')}`,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
const parsed = holidayTable.parseExtra(config.holidaysExtra);
|
|
398
|
+
for (const e of parsed.errors) warn(`config: ${e}`);
|
|
399
|
+
return { timeZone, locale, holidays, holidaysExtra: parsed.entries };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
module.exports = { createTime, settingsFromConfig, wallParts, zonedToInstant, offsetAt, systemZone, isZone, isLocale };
|