iobroker.assistant 0.1.6 → 1.0.2

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.
Files changed (108) hide show
  1. package/README.md +61 -4
  2. package/admin/custom/.vite/manifest.json +122 -77
  3. package/admin/custom/assets/Components-7BDDNrGP.js +1 -0
  4. package/admin/custom/assets/_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-DcAGnE2z.js +1 -0
  5. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__198531368674860__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-CzBMxGFr.js +1 -0
  6. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__198531368674860__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-1XsLCK2M.js +1 -0
  7. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__198531368674860__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-CMv8bcyA.js +1217 -0
  8. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__198531368674860__loadShare__shared-3mzaJWDF.js +2 -0
  9. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__198531368674860__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-DS8oaQQt.js +122 -0
  10. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__198531368674860__prebuild__react_mf_1_jsx_mf_2_runtime__prebuild__-B_v57Qfr.js +1 -0
  11. package/admin/custom/assets/dist-DiU1y0R9.js +1 -0
  12. package/admin/custom/assets/helpers-Dot7vgQQ.js +1 -0
  13. package/admin/custom/assets/hostInit-MshDicd1.js +1 -0
  14. package/admin/custom/assets/iconLibraryData-B5fiyl1C.js +1 -0
  15. package/admin/custom/assets/index-P2Xu9kJm.js +1 -0
  16. package/admin/custom/assets/lib-DjwHKtRi.js +3 -0
  17. package/admin/custom/assets/{mf-entry-bootstrap-0-87f15505.js → mf-entry-bootstrap-0-7c1ddf47.js} +14 -2
  18. package/admin/custom/assets/pendingShares-CaJDCKLB.js +2 -0
  19. package/admin/custom/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
  20. package/admin/custom/assets/utils-towTPKqG.js +4 -0
  21. package/admin/custom/assets/vite-preload-helper-BbbETgeG.js +1 -0
  22. package/admin/custom/customComponents.js +2 -2
  23. package/admin/custom/i18n/de.json +64 -57
  24. package/admin/custom/i18n/en.json +8 -1
  25. package/admin/custom/i18n/es.json +64 -57
  26. package/admin/custom/i18n/fr.json +64 -57
  27. package/admin/custom/i18n/it.json +64 -57
  28. package/admin/custom/i18n/nl.json +64 -57
  29. package/admin/custom/i18n/pl.json +64 -57
  30. package/admin/custom/i18n/pt.json +64 -57
  31. package/admin/custom/i18n/ru.json +64 -57
  32. package/admin/custom/i18n/uk.json +64 -57
  33. package/admin/custom/i18n/zh-cn.json +64 -57
  34. package/admin/custom/index.html +2 -2
  35. package/admin/custom/mf-manifest.json +1 -1
  36. package/admin/custom/mf-stats.json +1 -1
  37. package/admin/i18n/de.json +59 -3
  38. package/admin/i18n/en.json +59 -3
  39. package/admin/i18n/es.json +59 -3
  40. package/admin/i18n/fr.json +59 -3
  41. package/admin/i18n/it.json +59 -3
  42. package/admin/i18n/nl.json +59 -3
  43. package/admin/i18n/pl.json +59 -3
  44. package/admin/i18n/pt.json +59 -3
  45. package/admin/i18n/ru.json +59 -3
  46. package/admin/i18n/uk.json +59 -3
  47. package/admin/i18n/zh-cn.json +59 -3
  48. package/admin/jsonConfig.json +589 -75
  49. package/build/lib/ask.js +130 -0
  50. package/build/lib/ask.js.map +1 -0
  51. package/build/lib/llm.js +135 -13
  52. package/build/lib/llm.js.map +1 -1
  53. package/build/lib/nlu.js +74 -1
  54. package/build/lib/nlu.js.map +1 -1
  55. package/build/lib/notifications.js +112 -0
  56. package/build/lib/notifications.js.map +1 -0
  57. package/build/lib/presence.js +188 -0
  58. package/build/lib/presence.js.map +1 -0
  59. package/build/lib/routines.js +115 -0
  60. package/build/lib/routines.js.map +1 -0
  61. package/build/lib/targets.js +96 -0
  62. package/build/lib/targets.js.map +1 -0
  63. package/build/lib/triggers.js +632 -0
  64. package/build/lib/triggers.js.map +1 -0
  65. package/build/lib/voice/aws.js +9 -1
  66. package/build/lib/voice/aws.js.map +1 -1
  67. package/build/lib/voice/azure.js +9 -2
  68. package/build/lib/voice/azure.js.map +1 -1
  69. package/build/lib/voice/engines.js +82 -0
  70. package/build/lib/voice/engines.js.map +1 -1
  71. package/build/lib/voice/esphome.js +732 -0
  72. package/build/lib/voice/esphome.js.map +1 -0
  73. package/build/lib/voice/esphomeEntities.js +303 -0
  74. package/build/lib/voice/esphomeEntities.js.map +1 -0
  75. package/build/lib/voice/esphomeProto.js +181 -0
  76. package/build/lib/voice/esphomeProto.js.map +1 -0
  77. package/build/lib/voice/fallback.js +85 -0
  78. package/build/lib/voice/fallback.js.map +1 -0
  79. package/build/lib/voice/mediaServer.js +151 -0
  80. package/build/lib/voice/mediaServer.js.map +1 -0
  81. package/build/lib/voice/protocol.js.map +1 -1
  82. package/build/lib/voice/tone.js +32 -0
  83. package/build/lib/voice/tone.js.map +1 -0
  84. package/build/lib/voice/tts.js.map +1 -1
  85. package/build/lib/voice/ttsCache.js +274 -0
  86. package/build/lib/voice/ttsCache.js.map +1 -0
  87. package/build/lib/voice/vad.js +173 -0
  88. package/build/lib/voice/vad.js.map +1 -0
  89. package/build/lib/voice/voiceServer.js +13 -0
  90. package/build/lib/voice/voiceServer.js.map +1 -1
  91. package/build/main.js +1721 -67
  92. package/build/main.js.map +1 -1
  93. package/io-package.json +241 -3
  94. package/package.json +13 -12
  95. package/admin/custom/assets/Components-xWdtqvTH.js +0 -1
  96. package/admin/custom/assets/_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-dybYZgsu.js +0 -1
  97. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-BnSUsPHa.js +0 -1
  98. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-DlSVT49A.js +0 -1
  99. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-Cfsgderv.js +0 -1243
  100. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_json_mf_2_config__loadShare__.js-ByJf4zv_.js +0 -1494
  101. package/admin/custom/assets/hostInit-CZawMH9B.js +0 -1
  102. package/admin/custom/assets/index-JB7g1jgx.js +0 -1
  103. package/admin/custom/assets/lib-bB-LHRhZ.js +0 -3
  104. package/admin/custom/assets/rolldown-runtime-C0FnF6B9.js +0 -1
  105. package/admin/custom/assets/virtualExposes-brKKZvi0.js +0 -1
  106. package/admin/custom/assets/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__ConfigCustomAssistant__customComponents_js-CRhwj12f.js +0 -1
  107. package/admin/custom/assets/virtual_mf-exposes___mfe_internal__ConfigCustomAssistant__customComponents_js-C5XHUc1J.js +0 -2
  108. package/admin/custom/assets/vite-preload-helper-B7qeedMF.js +0 -1
@@ -0,0 +1,632 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TriggerEngine = exports.DEFAULT_COOLDOWN_SEC = void 0;
4
+ exports.parseDelay = parseDelay;
5
+ exports.parseTimeOfDay = parseTimeOfDay;
6
+ exports.effectiveActions = effectiveActions;
7
+ exports.referencedStates = referencedStates;
8
+ exports.valueMatches = valueMatches;
9
+ exports.parseTriggerRows = parseTriggerRows;
10
+ exports.validate = validate;
11
+ /**
12
+ * Proactive triggers — the assistant speaking first instead of answering.
13
+ *
14
+ * A trigger watches ioBroker states and/or the clock and, when its condition holds, says something, writes
15
+ * a state, or **asks a question and acts on the answer** ("The fryer has been on for 5 hours. Shall I
16
+ * switch it off?" → `askUser` → classify the reply → switch it off). The question is what sets this apart
17
+ * from a script or a scene: those can switch, but they cannot have a conversation about it.
18
+ *
19
+ * Semantics are ported from the Python original (`C:\iot\Hannah`, `core/hannah/trigger_engine.py`), whose
20
+ * corner cases were earned the hard way and are kept deliberately:
21
+ * - a state trigger fires on the **transition into** the condition, never on a repeated identical value;
22
+ * - `also` with an unknown state blocks (we cannot confirm it), `unless` with an unknown state does not
23
+ * (we cannot confirm the block either, and a lock that fires by accident is worse than one that waits);
24
+ * - the cooldown is consumed when the trigger *starts*, so a delayed trigger cannot re-arm while waiting.
25
+ *
26
+ * Unlike the original there is **no poll loop**: like {@link AlarmManager}, every time trigger fires from
27
+ * its own `setTimeout` at an absolute timestamp, so an idle system does no work at all. Execution itself is
28
+ * the host's job ({@link TriggerEngineOptions.execute}) — this module owns only *whether* and *when*.
29
+ */
30
+ const alarms_1 = require("./alarms");
31
+ /** Default minimum gap between two firings of the same trigger. */
32
+ exports.DEFAULT_COOLDOWN_SEC = 3600;
33
+ /** Largest delay `setTimeout` accepts (~24.8 days); a longer wait is re-armed in chunks. */
34
+ const MAX_TIMEOUT = 2 ** 31 - 1;
35
+ /** Normalise a condition field that may hold one condition or several. */
36
+ function asList(c) {
37
+ if (!c) {
38
+ return [];
39
+ }
40
+ return Array.isArray(c) ? c.filter(Boolean) : [c];
41
+ }
42
+ /** True when the value is a condition group (`{op, conditions}`) rather than a plain condition. */
43
+ function isGroup(c) {
44
+ return Array.isArray(c.conditions);
45
+ }
46
+ /**
47
+ * Parse a delay: `"5h"`, `"30m"`, `"90s"`, `"2d"`, or a plain number of seconds. Returns 0 when there is
48
+ * nothing to wait for and -1 when the text makes no sense (so the caller can complain instead of firing).
49
+ */
50
+ function parseDelay(delay) {
51
+ if (delay === undefined || delay === null || delay === '') {
52
+ return 0;
53
+ }
54
+ if (typeof delay === 'number') {
55
+ return Number.isFinite(delay) && delay > 0 ? Math.round(delay) : 0;
56
+ }
57
+ const m = /^\s*(\d+(?:[.,]\d+)?)\s*([smhd]?)\s*$/i.exec(delay);
58
+ if (!m) {
59
+ return -1;
60
+ }
61
+ const n = parseFloat(m[1].replace(',', '.'));
62
+ const unit = (m[2] || 's').toLowerCase();
63
+ const factor = unit === 'd' ? 86400 : unit === 'h' ? 3600 : unit === 'm' ? 60 : 1;
64
+ return Math.round(n * factor);
65
+ }
66
+ /** `HH:MM` → `{hour, minute}`, or null if it isn't a time. */
67
+ function parseTimeOfDay(time) {
68
+ const m = /^\s*(\d{1,2}):(\d{2})\s*$/.exec(time || '');
69
+ if (!m) {
70
+ return null;
71
+ }
72
+ const hour = Number(m[1]);
73
+ const minute = Number(m[2]);
74
+ return hour <= 23 && minute <= 59 ? { hour, minute } : null;
75
+ }
76
+ /** The actions a trigger runs: its own list, or the `say`/`room` shorthand folded into one. */
77
+ function effectiveActions(def) {
78
+ const list = (def.actions || []).filter(a => a && (a.say || a.setState));
79
+ if (list.length) {
80
+ return list.map(a => ({ ...a, room: a.room ?? def.room }));
81
+ }
82
+ return def.say ? [{ say: def.say, room: def.room }] : [];
83
+ }
84
+ /** Every state id a trigger references, across `when`, `also`, `unless` and `cancelWhen`. */
85
+ function referencedStates(def) {
86
+ const ids = new Set();
87
+ const walk = (c) => {
88
+ if (!c) {
89
+ return;
90
+ }
91
+ if (Array.isArray(c)) {
92
+ c.forEach(walk);
93
+ return;
94
+ }
95
+ if (isGroup(c)) {
96
+ c.conditions?.forEach(walk);
97
+ return;
98
+ }
99
+ if (c.state) {
100
+ ids.add(c.state);
101
+ }
102
+ walk(c.also);
103
+ walk(c.unless);
104
+ };
105
+ walk(asList(def.when));
106
+ walk(asList(def.cancelWhen));
107
+ return [...ids];
108
+ }
109
+ /**
110
+ * Does `value` satisfy the condition's value filter? `value` wins over `above`/`below`; without any filter
111
+ * every value matches (the caller has already established that it *changed*).
112
+ */
113
+ function valueMatches(cond, value) {
114
+ if (value === undefined) {
115
+ return false;
116
+ }
117
+ if (cond.value !== undefined) {
118
+ // Compare loosely on purpose: a state can hold 'true'/1 where the config says true.
119
+ return normalize(value) === normalize(cond.value);
120
+ }
121
+ if (cond.above !== undefined) {
122
+ const n = Number(value);
123
+ return Number.isFinite(n) && n > Number(cond.above);
124
+ }
125
+ if (cond.below !== undefined) {
126
+ const n = Number(value);
127
+ return Number.isFinite(n) && n < Number(cond.below);
128
+ }
129
+ return true;
130
+ }
131
+ /** Fold a value to a comparable primitive: booleans and numeric strings compare across types. */
132
+ function normalize(v) {
133
+ if (typeof v === 'string') {
134
+ const s = v.trim();
135
+ if (/^(true|false)$/i.test(s)) {
136
+ return s.toLowerCase() === 'true';
137
+ }
138
+ if (s !== '' && Number.isFinite(Number(s))) {
139
+ return Number(s);
140
+ }
141
+ return s;
142
+ }
143
+ return v;
144
+ }
145
+ /** Trigger scheduler/evaluator. Owns *whether* and *when*; the host owns *what happens*. */
146
+ class TriggerEngine {
147
+ opts;
148
+ triggers = new Map();
149
+ /** Last seen value per state, for transition detection. */
150
+ prev = new Map();
151
+ timeHandles = new Map();
152
+ delays = new Map();
153
+ now;
154
+ constructor(opts) {
155
+ this.opts = opts;
156
+ this.now = opts.now || Date.now;
157
+ }
158
+ /**
159
+ * Replace the trigger set (on start and whenever the configuration changes). Runtime state of a
160
+ * trigger that is still there survives — its cooldown and its live enabled flag — because a config
161
+ * reload must not turn into a free pass to fire again immediately.
162
+ */
163
+ load(defs) {
164
+ const keep = new Map(this.triggers);
165
+ for (const id of this.triggers.keys()) {
166
+ this.clearTime(id);
167
+ }
168
+ this.triggers.clear();
169
+ for (const def of defs || []) {
170
+ const problem = validate(def);
171
+ if (problem) {
172
+ this.opts.log?.warn(`trigger ${def?.id ? `'${def.id}'` : '(no id)'} ignored: ${problem}`);
173
+ continue;
174
+ }
175
+ if (this.triggers.has(def.id)) {
176
+ this.opts.log?.warn(`trigger '${def.id}' ignored: duplicate id`);
177
+ continue;
178
+ }
179
+ const before = keep.get(def.id);
180
+ this.triggers.set(def.id, {
181
+ def,
182
+ enabled: before ? before.enabled : def.enabled !== false,
183
+ lastFired: before?.lastFired || 0,
184
+ nextFireAt: 0,
185
+ nextCond: null,
186
+ });
187
+ }
188
+ // Drop pending delays of triggers that no longer exist.
189
+ for (const id of [...this.delays.keys()]) {
190
+ if (!this.triggers.has(id)) {
191
+ this.clearDelay(id);
192
+ }
193
+ }
194
+ for (const t of this.triggers.values()) {
195
+ this.armTime(t);
196
+ }
197
+ this.opts.log?.debug(`triggers loaded: ${this.triggers.size}`);
198
+ this.emitChange();
199
+ }
200
+ /** Restore live enabled flags (and last-fired times) persisted across a restart. */
201
+ restore(status) {
202
+ for (const s of status || []) {
203
+ const t = this.triggers.get(s?.id);
204
+ if (!t) {
205
+ continue;
206
+ }
207
+ t.lastFired = Number(s.lastFired) || 0;
208
+ if (s.enabled === false) {
209
+ t.enabled = false;
210
+ this.clearTime(s.id);
211
+ t.nextFireAt = 0;
212
+ t.nextCond = null;
213
+ }
214
+ }
215
+ this.emitChange();
216
+ }
217
+ /**
218
+ * Read every watched state once and remember its value, so the **first** report after a restart is not
219
+ * mistaken for a transition. Without this, a device that re-sends its unchanged value periodically
220
+ * would fire its trigger shortly after every adapter start. A state that cannot be read stays unknown,
221
+ * so its first real value still counts.
222
+ */
223
+ async prime() {
224
+ for (const id of this.stateIds()) {
225
+ await this.read(id);
226
+ }
227
+ }
228
+ /** All state ids the current triggers reference — exactly what the host has to subscribe to. */
229
+ stateIds() {
230
+ const ids = new Set();
231
+ for (const t of this.triggers.values()) {
232
+ referencedStates(t.def).forEach(id => ids.add(id));
233
+ }
234
+ return [...ids];
235
+ }
236
+ list() {
237
+ return [...this.triggers.values()]
238
+ .map(t => ({
239
+ id: t.def.id,
240
+ name: t.def.name || '',
241
+ enabled: t.enabled,
242
+ lastFired: t.lastFired,
243
+ nextFireAt: t.nextFireAt,
244
+ pendingUntil: this.delays.get(t.def.id)?.until || 0,
245
+ }))
246
+ .sort((a, b) => a.id.localeCompare(b.id));
247
+ }
248
+ get(id) {
249
+ return this.triggers.get(id)?.def;
250
+ }
251
+ /** Enable/disable a trigger at runtime (keeps it loaded). Returns true if it existed. */
252
+ setEnabled(id, enabled) {
253
+ const t = this.triggers.get(id);
254
+ if (!t) {
255
+ return false;
256
+ }
257
+ t.enabled = enabled;
258
+ if (enabled) {
259
+ this.armTime(t);
260
+ }
261
+ else {
262
+ this.clearTime(id);
263
+ this.clearDelay(id);
264
+ t.nextFireAt = 0;
265
+ t.nextCond = null;
266
+ }
267
+ this.emitChange();
268
+ return true;
269
+ }
270
+ /**
271
+ * Feed a state change in. Checks `cancelWhen` first (a pending delay may have to be aborted) and then
272
+ * every `when` alternative that watches this state. Values identical to the last one are ignored — a
273
+ * trigger reacts to a transition, not to a device repeating itself.
274
+ */
275
+ async onStateChange(id, value) {
276
+ const prev = this.prev.get(id);
277
+ const known = this.prev.has(id);
278
+ this.prev.set(id, value);
279
+ if (known && normalize(prev) === normalize(value)) {
280
+ return;
281
+ }
282
+ for (const t of [...this.triggers.values()]) {
283
+ if (!t.enabled) {
284
+ continue;
285
+ }
286
+ for (const cond of asList(t.def.cancelWhen)) {
287
+ if (cond.state === id && valueMatches(cond, value) && this.clearDelay(t.def.id)) {
288
+ this.opts.log?.info(`trigger '${t.def.id}': pending delay cancelled (cancelWhen matched)`);
289
+ this.emitChange();
290
+ }
291
+ }
292
+ for (const cond of asList(t.def.when)) {
293
+ if (cond.state !== id || !valueMatches(cond, value)) {
294
+ continue;
295
+ }
296
+ if (!(await this.extraConditionsHold(cond, t.def.id))) {
297
+ break;
298
+ }
299
+ await this.start(t, 'state');
300
+ break;
301
+ }
302
+ }
303
+ }
304
+ /**
305
+ * Fire a trigger by hand (the `fire` button / sendTo). Skips the cooldown and the delay: someone asked
306
+ * for it to happen now. Returns false if the id is unknown.
307
+ */
308
+ async fireNow(id) {
309
+ const t = this.triggers.get(id);
310
+ if (!t) {
311
+ return false;
312
+ }
313
+ t.lastFired = this.now();
314
+ this.emitChange();
315
+ await this.run(t, 'manual');
316
+ return true;
317
+ }
318
+ /**
319
+ * Fire every time trigger that is due — public so tests can drive it with a mocked clock; in
320
+ * production each trigger fires from its own `setTimeout`, so this does nothing there.
321
+ */
322
+ async tick() {
323
+ const now = this.now();
324
+ for (const t of [...this.triggers.values()]) {
325
+ if (t.enabled && t.nextFireAt && t.nextFireAt <= now) {
326
+ await this.fireTime(t);
327
+ }
328
+ }
329
+ for (const [id, delay] of [...this.delays.entries()]) {
330
+ if (delay.until && delay.until <= now) {
331
+ const t = this.triggers.get(id);
332
+ this.clearDelay(id);
333
+ if (t) {
334
+ await this.run(t, 'delay');
335
+ }
336
+ }
337
+ }
338
+ }
339
+ /** Clear all timers and drop everything (no callbacks). Call on unload. */
340
+ dispose() {
341
+ for (const id of [...this.triggers.keys()]) {
342
+ this.clearTime(id);
343
+ }
344
+ for (const id of [...this.delays.keys()]) {
345
+ this.clearDelay(id);
346
+ }
347
+ this.triggers.clear();
348
+ this.prev.clear();
349
+ }
350
+ /** `also` holds and `unless` does not — the half of a condition that needs other states. */
351
+ async extraConditionsHold(cond, id) {
352
+ if (!(await this.alsoHolds(cond.also))) {
353
+ this.opts.log?.debug(`trigger '${id}': 'also' condition not met`);
354
+ return false;
355
+ }
356
+ if (await this.unlessBlocks(cond.unless)) {
357
+ this.opts.log?.debug(`trigger '${id}': blocked by 'unless'`);
358
+ return false;
359
+ }
360
+ return true;
361
+ }
362
+ async alsoHolds(also) {
363
+ if (!also) {
364
+ return true;
365
+ }
366
+ if (Array.isArray(also)) {
367
+ for (const c of also) {
368
+ if (!(await this.alsoHolds(c))) {
369
+ return false;
370
+ }
371
+ }
372
+ return true;
373
+ }
374
+ if (isGroup(also)) {
375
+ const list = also.conditions || [];
376
+ if (also.op === 'or') {
377
+ for (const c of list) {
378
+ if (await this.alsoHolds(c)) {
379
+ return true;
380
+ }
381
+ }
382
+ return false;
383
+ }
384
+ for (const c of list) {
385
+ if (!(await this.alsoHolds(c))) {
386
+ return false;
387
+ }
388
+ }
389
+ return true;
390
+ }
391
+ if (!also.state) {
392
+ return true;
393
+ }
394
+ // Unknown state blocks: we must not claim a condition holds when we cannot read it.
395
+ return valueMatches(also, await this.read(also.state));
396
+ }
397
+ async unlessBlocks(unless) {
398
+ for (const c of asList(unless)) {
399
+ if (!c.state) {
400
+ continue;
401
+ }
402
+ const value = await this.read(c.state);
403
+ // Unknown state does NOT block — see the module header.
404
+ if (value !== undefined && valueMatches(c, value)) {
405
+ return true;
406
+ }
407
+ }
408
+ return false;
409
+ }
410
+ async read(id) {
411
+ try {
412
+ const value = await this.opts.getState(id);
413
+ if (value !== undefined && value !== null) {
414
+ this.prev.set(id, value);
415
+ }
416
+ return value === null ? undefined : value;
417
+ }
418
+ catch (e) {
419
+ this.opts.log?.debug(`trigger: cannot read '${id}': ${e.message}`);
420
+ return undefined;
421
+ }
422
+ }
423
+ /**
424
+ * A condition held: take the cooldown, then either run now or start the delay. The cooldown is taken
425
+ * here rather than at execution time so a condition flapping during a long delay cannot queue up.
426
+ */
427
+ async start(t, origin) {
428
+ const now = this.now();
429
+ const cooldown = (t.def.cooldownSec ?? exports.DEFAULT_COOLDOWN_SEC) * 1000;
430
+ if (cooldown > 0 && t.lastFired && now - t.lastFired < cooldown) {
431
+ const left = Math.round((cooldown - (now - t.lastFired)) / 1000);
432
+ this.opts.log?.debug(`trigger '${t.def.id}' in cooldown (${left}s left) — skipped`);
433
+ return;
434
+ }
435
+ t.lastFired = now;
436
+ const delaySec = parseDelay(t.def.delay);
437
+ if (delaySec < 0) {
438
+ this.opts.log?.warn(`trigger '${t.def.id}': invalid delay '${String(t.def.delay)}' — firing now`);
439
+ }
440
+ if (delaySec > 0) {
441
+ if (this.delays.has(t.def.id)) {
442
+ this.opts.log?.debug(`trigger '${t.def.id}': delay already running — skipped`);
443
+ return;
444
+ }
445
+ const until = now + delaySec * 1000;
446
+ this.opts.log?.info(`trigger '${t.def.id}' armed by ${origin}: waiting ${delaySec}s`);
447
+ const handle = this.opts.manualTick
448
+ ? null
449
+ : setTimeout(() => {
450
+ this.clearDelay(t.def.id);
451
+ void this.run(t, 'delay');
452
+ }, Math.min(delaySec * 1000, MAX_TIMEOUT));
453
+ handle?.unref?.();
454
+ this.delays.set(t.def.id, { handle, until });
455
+ this.emitChange();
456
+ return;
457
+ }
458
+ this.emitChange();
459
+ await this.run(t, origin);
460
+ }
461
+ /** Hand a trigger to the host and keep going if it throws — one bad trigger must not stop the rest. */
462
+ async run(t, origin) {
463
+ this.opts.log?.info(`trigger '${t.def.id}' fired (${origin})`);
464
+ try {
465
+ await this.opts.execute(t.def);
466
+ }
467
+ catch (e) {
468
+ this.opts.log?.error(`trigger '${t.def.id}' failed: ${e.message}`);
469
+ }
470
+ this.emitChange();
471
+ }
472
+ /** A time trigger came due: re-check its extra conditions, fire, and schedule the next occurrence. */
473
+ async fireTime(t) {
474
+ const cond = t.nextCond;
475
+ this.clearTime(t.def.id);
476
+ t.nextFireAt = 0;
477
+ t.nextCond = null;
478
+ const allowed = !cond || (await this.extraConditionsHold(cond, t.def.id));
479
+ this.armTime(t); // re-arm first: an execution error must not stop tomorrow's run
480
+ if (allowed) {
481
+ await this.start(t, 'time');
482
+ }
483
+ }
484
+ /** Schedule the earliest upcoming time condition of a trigger (no-op if it has none). */
485
+ armTime(t) {
486
+ this.clearTime(t.def.id);
487
+ t.nextFireAt = 0;
488
+ t.nextCond = null;
489
+ if (!t.enabled) {
490
+ return;
491
+ }
492
+ const now = this.now();
493
+ for (const cond of asList(t.def.when)) {
494
+ const clock = parseTimeOfDay(cond.time);
495
+ if (!clock) {
496
+ continue;
497
+ }
498
+ const days = (cond.days || []).filter(d => d >= 0 && d <= 6);
499
+ const at = (0, alarms_1.computeNextFire)(clock.hour, clock.minute, days, now);
500
+ if (at && (!t.nextFireAt || at < t.nextFireAt)) {
501
+ t.nextFireAt = at;
502
+ t.nextCond = cond;
503
+ }
504
+ }
505
+ if (!t.nextFireAt || this.opts.manualTick) {
506
+ return;
507
+ }
508
+ const handle = setTimeout(() => {
509
+ const current = this.triggers.get(t.def.id);
510
+ if (!current) {
511
+ return;
512
+ }
513
+ if (current.nextFireAt - this.now() > 0) {
514
+ this.armTime(current); // was only a chunk of a long wait
515
+ }
516
+ else {
517
+ void this.fireTime(current);
518
+ }
519
+ }, Math.min(Math.max(0, t.nextFireAt - now), MAX_TIMEOUT));
520
+ handle.unref?.();
521
+ this.timeHandles.set(t.def.id, handle);
522
+ }
523
+ clearTime(id) {
524
+ const h = this.timeHandles.get(id);
525
+ if (h) {
526
+ clearTimeout(h);
527
+ this.timeHandles.delete(id);
528
+ }
529
+ }
530
+ clearDelay(id) {
531
+ const d = this.delays.get(id);
532
+ if (!d) {
533
+ return false;
534
+ }
535
+ if (d.handle) {
536
+ clearTimeout(d.handle);
537
+ }
538
+ this.delays.delete(id);
539
+ return true;
540
+ }
541
+ emitChange() {
542
+ try {
543
+ this.opts.onChange?.(this.list());
544
+ }
545
+ catch (e) {
546
+ this.opts.log?.warn(`trigger onChange failed: ${e.message}`);
547
+ }
548
+ }
549
+ }
550
+ exports.TriggerEngine = TriggerEngine;
551
+ /**
552
+ * Turn settings-table rows into definitions: the JSON-typed columns are parsed here so a typo surfaces as
553
+ * one clear warning per field instead of breaking the trigger later, and empty cells fall back to the
554
+ * defaults rather than to zero. A row written by a script may pass the structured form straight through.
555
+ */
556
+ function parseTriggerRows(rows, opts = {}) {
557
+ const out = [];
558
+ for (const row of rows || []) {
559
+ const id = String(row?.id ?? '').trim();
560
+ if (!id) {
561
+ continue; // an empty row in the table is not an error, it is just empty
562
+ }
563
+ const parse = (field, raw) => {
564
+ if (raw === undefined || raw === null || raw === '') {
565
+ return undefined;
566
+ }
567
+ if (typeof raw !== 'string') {
568
+ return raw;
569
+ }
570
+ try {
571
+ return JSON.parse(raw);
572
+ }
573
+ catch (e) {
574
+ opts.warn?.(`trigger '${id}': ${field} is not valid JSON (${e.message}) — ignored`);
575
+ return undefined;
576
+ }
577
+ };
578
+ const cooldown = Number(row.cooldownSec);
579
+ out.push({
580
+ id,
581
+ name: String(row.name ?? '').trim(),
582
+ enabled: row.enabled,
583
+ when: parse('when', row.when),
584
+ room: String(row.room ?? '').trim(),
585
+ say: String(row.say ?? '').trim(),
586
+ ask: String(row.ask ?? '').trim(),
587
+ onResponse: parse('onResponse', row.onResponse),
588
+ actions: parse('actions', row.actions),
589
+ delay: row.delay,
590
+ cooldownSec: row.cooldownSec === undefined ||
591
+ row.cooldownSec === null ||
592
+ row.cooldownSec === '' ||
593
+ !Number.isFinite(cooldown)
594
+ ? undefined
595
+ : cooldown,
596
+ rephrase: row.rephrase ?? opts.rephraseDefault === true,
597
+ });
598
+ }
599
+ return out;
600
+ }
601
+ /** Why a definition cannot be used, or '' when it is fine. */
602
+ function validate(def) {
603
+ if (!def || typeof def !== 'object') {
604
+ return 'not an object';
605
+ }
606
+ if (!def.id || typeof def.id !== 'string') {
607
+ return 'missing id';
608
+ }
609
+ const when = asList(def.when);
610
+ if (!when.length) {
611
+ return 'missing "when" condition';
612
+ }
613
+ for (const cond of when) {
614
+ if (cond.time !== undefined && !parseTimeOfDay(cond.time)) {
615
+ return `invalid time '${String(cond.time)}' (expected HH:MM)`;
616
+ }
617
+ if (!cond.state && cond.time === undefined) {
618
+ return 'a "when" condition needs either a state or a time';
619
+ }
620
+ }
621
+ if (parseDelay(def.delay) < 0) {
622
+ return `invalid delay '${String(def.delay)}' (expected 90s, 30m, 5h, 2d or seconds)`;
623
+ }
624
+ if (!def.ask && !effectiveActions(def).length) {
625
+ return 'nothing to do (no say, actions or ask)';
626
+ }
627
+ if (def.ask && !(def.onResponse || []).length) {
628
+ return 'an "ask" trigger needs onResponse rules';
629
+ }
630
+ return '';
631
+ }
632
+ //# sourceMappingURL=triggers.js.map