iobroker.assistant 0.1.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 (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +176 -0
  3. package/admin/assistant.svg +19 -0
  4. package/admin/custom/.vite/manifest.json +169 -0
  5. package/admin/custom/assets/Components-DcnuVFM6.js +1 -0
  6. package/admin/custom/assets/_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-DogEjv3U.js +1 -0
  7. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-Dw5TkU1t.js +1 -0
  8. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-BEFjTs9S.js +1 -0
  9. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-C4r3udVz.js +1243 -0
  10. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_json_mf_2_config__loadShare__.js-DlOBHPL0.js +1494 -0
  11. package/admin/custom/assets/hostInit-DKMI-sLp.js +1 -0
  12. package/admin/custom/assets/index-B8wW0kQx.js +1 -0
  13. package/admin/custom/assets/lib-DpE8sc4h.js +3 -0
  14. package/admin/custom/assets/mf-entry-bootstrap-0-e7fa582d.js +36 -0
  15. package/admin/custom/assets/rolldown-runtime-C0FnF6B9.js +1 -0
  16. package/admin/custom/assets/virtualExposes-DOTO22UK.js +1 -0
  17. package/admin/custom/assets/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__ConfigCustomAssistant__customComponents_js-CQa59yVi.js +1 -0
  18. package/admin/custom/assets/virtual_mf-exposes-ssr___mfe_internal__ConfigCustomAssistant__customComponents_js-9g2Fj8tk.js +2 -0
  19. package/admin/custom/assets/virtual_mf-exposes___mfe_internal__ConfigCustomAssistant__customComponents_js-BTUQTbYo.js +2 -0
  20. package/admin/custom/assets/vite-preload-helper-B7qeedMF.js +1 -0
  21. package/admin/custom/customComponents.js +1 -0
  22. package/admin/custom/customComponents.ssr.js +1 -0
  23. package/admin/custom/i18n/de.json +58 -0
  24. package/admin/custom/i18n/en.json +58 -0
  25. package/admin/custom/i18n/es.json +34 -0
  26. package/admin/custom/i18n/fr.json +34 -0
  27. package/admin/custom/i18n/it.json +34 -0
  28. package/admin/custom/i18n/nl.json +34 -0
  29. package/admin/custom/i18n/pl.json +34 -0
  30. package/admin/custom/i18n/pt.json +34 -0
  31. package/admin/custom/i18n/ru.json +34 -0
  32. package/admin/custom/i18n/uk.json +34 -0
  33. package/admin/custom/i18n/zh-cn.json +34 -0
  34. package/admin/custom/index.html +32 -0
  35. package/admin/custom/mf-manifest.json +1 -0
  36. package/admin/custom/mf-stats.json +1 -0
  37. package/admin/i18n/de.json +130 -0
  38. package/admin/i18n/en.json +130 -0
  39. package/admin/i18n/es.json +106 -0
  40. package/admin/i18n/fr.json +106 -0
  41. package/admin/i18n/it.json +106 -0
  42. package/admin/i18n/nl.json +106 -0
  43. package/admin/i18n/pl.json +106 -0
  44. package/admin/i18n/pt.json +106 -0
  45. package/admin/i18n/ru.json +106 -0
  46. package/admin/i18n/uk.json +106 -0
  47. package/admin/i18n/zh-cn.json +106 -0
  48. package/admin/jsonConfig.json +743 -0
  49. package/admin/sounds/alarm.wav +0 -0
  50. package/admin/sounds/timer.wav +0 -0
  51. package/build/lib/alarms.js +281 -0
  52. package/build/lib/alarms.js.map +1 -0
  53. package/build/lib/context.js +52 -0
  54. package/build/lib/context.js.map +1 -0
  55. package/build/lib/credentials.js +121 -0
  56. package/build/lib/credentials.js.map +1 -0
  57. package/build/lib/llm.js +303 -0
  58. package/build/lib/llm.js.map +1 -0
  59. package/build/lib/localLlm.js +221 -0
  60. package/build/lib/localLlm.js.map +1 -0
  61. package/build/lib/memory.js +190 -0
  62. package/build/lib/memory.js.map +1 -0
  63. package/build/lib/nlu.js +929 -0
  64. package/build/lib/nlu.js.map +1 -0
  65. package/build/lib/timers.js +245 -0
  66. package/build/lib/timers.js.map +1 -0
  67. package/build/lib/tools.js +299 -0
  68. package/build/lib/tools.js.map +1 -0
  69. package/build/lib/voice/aws.js +102 -0
  70. package/build/lib/voice/aws.js.map +1 -0
  71. package/build/lib/voice/azure.js +144 -0
  72. package/build/lib/voice/azure.js.map +1 -0
  73. package/build/lib/voice/download.js +76 -0
  74. package/build/lib/voice/download.js.map +1 -0
  75. package/build/lib/voice/engines.js +89 -0
  76. package/build/lib/voice/engines.js.map +1 -0
  77. package/build/lib/voice/lang.js +33 -0
  78. package/build/lib/voice/lang.js.map +1 -0
  79. package/build/lib/voice/piper.js +191 -0
  80. package/build/lib/voice/piper.js.map +1 -0
  81. package/build/lib/voice/protocol.js +35 -0
  82. package/build/lib/voice/protocol.js.map +1 -0
  83. package/build/lib/voice/stt.js +116 -0
  84. package/build/lib/voice/stt.js.map +1 -0
  85. package/build/lib/voice/tts.js +36 -0
  86. package/build/lib/voice/tts.js.map +1 -0
  87. package/build/lib/voice/voiceServer.js +249 -0
  88. package/build/lib/voice/voiceServer.js.map +1 -0
  89. package/build/lib/voice/vosk.js +260 -0
  90. package/build/lib/voice/vosk.js.map +1 -0
  91. package/build/lib/voice/wyoming.js +320 -0
  92. package/build/lib/voice/wyoming.js.map +1 -0
  93. package/build/lib/weather.js +423 -0
  94. package/build/lib/weather.js.map +1 -0
  95. package/build/main.js +3152 -0
  96. package/build/main.js.map +1 -0
  97. package/io-package.json +528 -0
  98. package/package.json +68 -0
Binary file
Binary file
@@ -0,0 +1,281 @@
1
+ "use strict";
2
+ /**
3
+ * Alarms — reminders at a fixed wall-clock time ("weck mich um 7 Uhr"), optionally recurring on chosen
4
+ * weekdays. Sibling of {@link TimerManager} (countdown timers): same ioBroker-first philosophy, but the
5
+ * fire time is an absolute clock time instead of a relative duration, and an alarm can repeat.
6
+ *
7
+ * Like the timer engine this never writes any state on a periodic schedule: each alarm fires from its own
8
+ * `setTimeout` at `nextFireAt`, and the states carry only that absolute timestamp — a vis/script derives
9
+ * the countdown itself. One-shot alarms (no weekdays) are removed after firing; recurring ones are
10
+ * rescheduled to their next matching weekday.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.AlarmManager = void 0;
14
+ exports.computeNextFire = computeNextFire;
15
+ exports.formatClock = formatClock;
16
+ exports.formatWeekdays = formatWeekdays;
17
+ /** Largest delay `setTimeout` accepts (~24.8 days); a farther alarm is re-armed in chunks. */
18
+ const MAX_TIMEOUT = 2 ** 31 - 1;
19
+ /**
20
+ * Compute the next epoch-ms an alarm at `hour:minute` fires strictly after `fromMs`, honouring `weekdays`
21
+ * (empty = any day). Uses the host's local time. Returns 0 if nothing matches within a week (never happens
22
+ * for valid input).
23
+ */
24
+ function computeNextFire(hour, minute, weekdays, fromMs) {
25
+ const base = new Date(fromMs);
26
+ for (let addDays = 0; addDays <= 7; addDays++) {
27
+ const d = new Date(base.getFullYear(), base.getMonth(), base.getDate() + addDays, hour, minute, 0, 0);
28
+ if (d.getTime() <= fromMs) {
29
+ continue; // must be strictly in the future
30
+ }
31
+ if (weekdays.length && !weekdays.includes(d.getDay())) {
32
+ continue;
33
+ }
34
+ return d.getTime();
35
+ }
36
+ return 0;
37
+ }
38
+ /** In-memory alarm scheduler; each alarm fires from its own `setTimeout` (no polling, no periodic writes). */
39
+ class AlarmManager {
40
+ opts;
41
+ alarms = new Map();
42
+ handles = new Map();
43
+ seq = 0;
44
+ now;
45
+ constructor(opts) {
46
+ this.opts = opts;
47
+ this.now = opts.now || Date.now;
48
+ }
49
+ /** Add an alarm, schedule it and notify. Returns the created {@link AlarmInfo}. */
50
+ add(o) {
51
+ const now = this.now();
52
+ const weekdays = [...new Set((o.weekdays || []).filter(d => d >= 0 && d <= 6))].sort();
53
+ const hour = Math.max(0, Math.min(23, Math.round(o.hour)));
54
+ const minute = Math.max(0, Math.min(59, Math.round(o.minute)));
55
+ const info = {
56
+ id: this.genId(now),
57
+ label: (o.label || '').trim(),
58
+ room: (o.room || '').trim(),
59
+ source: o.source || '',
60
+ hour,
61
+ minute,
62
+ weekdays,
63
+ enabled: true,
64
+ createdAt: now,
65
+ nextFireAt: computeNextFire(hour, minute, weekdays, now),
66
+ };
67
+ this.alarms.set(info.id, info);
68
+ this.opts.log?.debug(`alarm added: ${info.id} "${info.label}" at ${hour}:${String(minute).padStart(2, '0')} [${weekdays.join(',') || 'once'}]`);
69
+ this.arm(info);
70
+ this.emitChange();
71
+ return info;
72
+ }
73
+ /** Enable/disable an alarm without deleting it. Returns true if it existed. */
74
+ setEnabled(id, enabled) {
75
+ const a = this.alarms.get(id);
76
+ if (!a) {
77
+ return false;
78
+ }
79
+ a.enabled = enabled;
80
+ this.clearHandle(id);
81
+ if (enabled) {
82
+ a.nextFireAt = computeNextFire(a.hour, a.minute, a.weekdays, this.now());
83
+ this.arm(a);
84
+ }
85
+ else {
86
+ a.nextFireAt = 0;
87
+ }
88
+ this.emitChange();
89
+ return true;
90
+ }
91
+ /** Cancel/delete one alarm by id. Returns true if it existed. */
92
+ cancel(id) {
93
+ this.clearHandle(id);
94
+ if (!this.alarms.delete(id)) {
95
+ return false;
96
+ }
97
+ this.emitChange();
98
+ return true;
99
+ }
100
+ /** Cancel all alarms (optionally only those in a room). Returns the number cancelled. */
101
+ cancelAll(room) {
102
+ const victims = this.list().filter(a => !room || a.room === room);
103
+ for (const a of victims) {
104
+ this.clearHandle(a.id);
105
+ this.alarms.delete(a.id);
106
+ }
107
+ if (victims.length) {
108
+ this.emitChange();
109
+ }
110
+ return victims.length;
111
+ }
112
+ /** The alarms, soonest-to-fire first (disabled ones — nextFireAt 0 — sort last). */
113
+ list() {
114
+ return [...this.alarms.values()].sort((a, b) => {
115
+ const an = a.nextFireAt || Infinity;
116
+ const bn = b.nextFireAt || Infinity;
117
+ return an - bn;
118
+ });
119
+ }
120
+ get(id) {
121
+ return this.alarms.get(id);
122
+ }
123
+ /**
124
+ * Restore persisted alarms after a restart. Disabled alarms are kept as-is; enabled ones recompute
125
+ * their next fire from now. A one-shot whose time passed while the adapter was down is dropped.
126
+ * Returns how many were restored / dropped.
127
+ */
128
+ restore(list) {
129
+ const now = this.now();
130
+ let restored = 0;
131
+ let dropped = 0;
132
+ for (const a of list) {
133
+ if (!a || typeof a.id !== 'string' || typeof a.hour !== 'number' || typeof a.minute !== 'number') {
134
+ continue;
135
+ }
136
+ const weekdays = Array.isArray(a.weekdays) ? a.weekdays : [];
137
+ const info = {
138
+ id: a.id,
139
+ label: a.label || '',
140
+ room: a.room || '',
141
+ source: a.source || '',
142
+ hour: a.hour,
143
+ minute: a.minute,
144
+ weekdays,
145
+ enabled: a.enabled !== false,
146
+ createdAt: a.createdAt || now,
147
+ nextFireAt: 0,
148
+ };
149
+ if (!info.enabled) {
150
+ this.alarms.set(info.id, info); // keep, but don't arm
151
+ restored++;
152
+ continue;
153
+ }
154
+ if (!weekdays.length && typeof a.nextFireAt === 'number' && a.nextFireAt <= now) {
155
+ dropped++; // a one-shot we missed during downtime
156
+ continue;
157
+ }
158
+ info.nextFireAt = computeNextFire(info.hour, info.minute, weekdays, now);
159
+ this.alarms.set(info.id, info);
160
+ this.arm(info);
161
+ restored++;
162
+ }
163
+ this.emitChange();
164
+ return { restored, dropped };
165
+ }
166
+ /**
167
+ * Fire every enabled alarm whose `nextFireAt` has passed — public so tests can drive it with a mocked
168
+ * clock (production fires each alarm from its own `setTimeout`, so this is a no-op there).
169
+ */
170
+ tick() {
171
+ const now = this.now();
172
+ const due = this.list().filter(a => a.enabled && a.nextFireAt && a.nextFireAt <= now);
173
+ for (const a of due) {
174
+ this.fire(a.id, false);
175
+ }
176
+ if (due.length) {
177
+ this.emitChange();
178
+ }
179
+ }
180
+ /** Clear all pending timeouts and drop the in-memory alarms (no callbacks). Call on unload. */
181
+ dispose() {
182
+ for (const h of this.handles.values()) {
183
+ clearTimeout(h);
184
+ }
185
+ this.handles.clear();
186
+ this.alarms.clear();
187
+ }
188
+ /** Fire one alarm: one-shots are removed, recurring ones are rescheduled to their next weekday. */
189
+ fire(id, emit) {
190
+ const a = this.alarms.get(id);
191
+ if (!a) {
192
+ return;
193
+ }
194
+ this.clearHandle(id);
195
+ const snapshot = { ...a, weekdays: [...a.weekdays] };
196
+ if (a.weekdays.length) {
197
+ // Recompute from just after this fire so we don't immediately re-fire the same minute.
198
+ a.nextFireAt = computeNextFire(a.hour, a.minute, a.weekdays, this.now() + 1000);
199
+ this.arm(a);
200
+ }
201
+ else {
202
+ this.alarms.delete(id);
203
+ }
204
+ try {
205
+ this.opts.onFire(snapshot);
206
+ }
207
+ catch (e) {
208
+ this.opts.log?.warn(`alarm onFire failed: ${e.message}`);
209
+ }
210
+ if (emit) {
211
+ this.emitChange();
212
+ }
213
+ }
214
+ /** Arm the `setTimeout` for an alarm (chunked for fire times beyond the setTimeout limit). */
215
+ arm(info) {
216
+ if (this.opts.manualTick || !info.enabled || !info.nextFireAt) {
217
+ return;
218
+ }
219
+ this.clearHandle(info.id);
220
+ const delay = Math.max(0, info.nextFireAt - this.now());
221
+ const handle = setTimeout(() => {
222
+ const a = this.alarms.get(info.id);
223
+ if (a && a.nextFireAt - this.now() > 0) {
224
+ this.arm(a); // was only a chunk of a long wait
225
+ }
226
+ else {
227
+ this.fire(info.id, true);
228
+ }
229
+ }, Math.min(delay, MAX_TIMEOUT));
230
+ handle.unref?.();
231
+ this.handles.set(info.id, handle);
232
+ }
233
+ clearHandle(id) {
234
+ const h = this.handles.get(id);
235
+ if (h) {
236
+ clearTimeout(h);
237
+ this.handles.delete(id);
238
+ }
239
+ }
240
+ genId(now) {
241
+ return `a${now.toString(36)}${(this.seq++).toString(36)}`;
242
+ }
243
+ emitChange() {
244
+ try {
245
+ this.opts.onChange(this.list());
246
+ }
247
+ catch (e) {
248
+ this.opts.log?.warn(`alarm onChange failed: ${e.message}`);
249
+ }
250
+ }
251
+ }
252
+ exports.AlarmManager = AlarmManager;
253
+ /** Weekday names → short localized labels for the spoken reply / states (0 = Sunday). */
254
+ const WEEKDAY_NAMES = {
255
+ de: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
256
+ en: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
257
+ ru: ['воскресенье', 'понедельник', 'вторник', 'среда', 'четверг', 'пятница', 'суббота'],
258
+ };
259
+ /** "07:30" from hour/minute. */
260
+ function formatClock(hour, minute) {
261
+ return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
262
+ }
263
+ /** Human, spoken-friendly recurrence text: "" (once), "täglich", "wochentags", or a weekday list. */
264
+ function formatWeekdays(weekdays, lang) {
265
+ const l = lang === 'ru' ? 'ru' : lang === 'de' ? 'de' : 'en';
266
+ const days = [...new Set(weekdays)].sort();
267
+ if (!days.length) {
268
+ return '';
269
+ }
270
+ if (days.length === 7) {
271
+ return l === 'ru' ? 'ежедневно' : l === 'de' ? 'täglich' : 'daily';
272
+ }
273
+ if (days.length === 5 && [1, 2, 3, 4, 5].every(d => days.includes(d))) {
274
+ return l === 'ru' ? 'по будням' : l === 'de' ? 'wochentags' : 'on weekdays';
275
+ }
276
+ if (days.length === 2 && days.includes(0) && days.includes(6)) {
277
+ return l === 'ru' ? 'по выходным' : l === 'de' ? 'am Wochenende' : 'on weekends';
278
+ }
279
+ return days.map(d => WEEKDAY_NAMES[l][d]).join(', ');
280
+ }
281
+ //# sourceMappingURL=alarms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alarms.js","sourceRoot":"","sources":["../../src/lib/alarms.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;;AAoDH,0CAaC;AA8OD,kCAEC;AAGD,wCAgBC;AAxRD,8FAA8F;AAC9F,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AAEhC;;;;GAIG;AACH,SAAgB,eAAe,CAAC,IAAY,EAAE,MAAc,EAAE,QAAkB,EAAE,MAAc;IAC5F,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC;IAC9B,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC;QAC5C,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE,IAAI,CAAC,OAAO,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QACtG,IAAI,CAAC,CAAC,OAAO,EAAE,IAAI,MAAM,EAAE,CAAC;YACxB,SAAS,CAAC,iCAAiC;QAC/C,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;YACpD,SAAS;QACb,CAAC;QACD,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC;IACvB,CAAC;IACD,OAAO,CAAC,CAAC;AACb,CAAC;AAED,8GAA8G;AAC9G,MAAa,YAAY;IAMQ;IALZ,MAAM,GAAG,IAAI,GAAG,EAAqB,CAAC;IACtC,OAAO,GAAG,IAAI,GAAG,EAAyC,CAAC;IACpE,GAAG,GAAG,CAAC,CAAC;IACC,GAAG,CAAe;IAEnC,YAA6B,IAAyB;QAAzB,SAAI,GAAJ,IAAI,CAAqB;QAClD,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACpC,CAAC;IAED,mFAAmF;IACnF,GAAG,CAAC,CAAkB;QAClB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACvF,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3D,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC/D,MAAM,IAAI,GAAc;YACpB,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC;YACnB,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;YAC7B,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;YAC3B,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE;YACtB,IAAI;YACJ,MAAM;YACN,QAAQ;YACR,OAAO,EAAE,IAAI;YACb,SAAS,EAAE,GAAG;YACd,UAAU,EAAE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC;SAC3D,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QAC/B,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAChB,gBAAgB,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC,KAAK,QAAQ,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,MAAM,GAAG,CAC5H,CAAC;QACF,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACf,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,+EAA+E;IAC/E,UAAU,CAAC,EAAU,EAAE,OAAgB;QACnC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC9B,IAAI,CAAC,CAAC,EAAE,CAAC;YACL,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,CAAC,CAAC,OAAO,GAAG,OAAO,CAAC;QACpB,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACrB,IAAI,OAAO,EAAE,CAAC;YACV,CAAC,CAAC,UAAU,GAAG,eAAe,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;YACzE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAChB,CAAC;aAAM,CAAC;YACJ,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC;QACrB,CAAC;QACD,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,iEAAiE;IACjE,MAAM,CAAC,EAAU;QACb,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACrB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;YAC1B,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,yFAAyF;IACzF,SAAS,CAAC,IAAa;QACnB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;QAClE,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACtB,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACvB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC7B,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACjB,IAAI,CAAC,UAAU,EAAE,CAAC;QACtB,CAAC;QACD,OAAO,OAAO,CAAC,MAAM,CAAC;IAC1B,CAAC;IAED,oFAAoF;IACpF,IAAI;QACA,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;YAC3C,MAAM,EAAE,GAAG,CAAC,CAAC,UAAU,IAAI,QAAQ,CAAC;YACpC,MAAM,EAAE,GAAG,CAAC,CAAC,UAAU,IAAI,QAAQ,CAAC;YACpC,OAAO,EAAE,GAAG,EAAE,CAAC;QACnB,CAAC,CAAC,CAAC;IACP,CAAC;IAED,GAAG,CAAC,EAAU;QACV,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC/B,CAAC;IAED;;;;OAIG;IACH,OAAO,CAAC,IAAiB;QACrB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,IAAI,OAAO,GAAG,CAAC,CAAC;QAChB,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;YACnB,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;gBAC/F,SAAS;YACb,CAAC;YACD,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7D,MAAM,IAAI,GAAc;gBACpB,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;gBACpB,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE;gBAClB,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE;gBACtB,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,QAAQ;gBACR,OAAO,EAAE,CAAC,CAAC,OAAO,KAAK,KAAK;gBAC5B,SAAS,EAAE,CAAC,CAAC,SAAS,IAAI,GAAG;gBAC7B,UAAU,EAAE,CAAC;aAChB,CAAC;YACF,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;gBAChB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,sBAAsB;gBACtD,QAAQ,EAAE,CAAC;gBACX,SAAS;YACb,CAAC;YACD,IAAI,CAAC,QAAQ,CAAC,MAAM,IAAI,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ,IAAI,CAAC,CAAC,UAAU,IAAI,GAAG,EAAE,CAAC;gBAC9E,OAAO,EAAE,CAAC,CAAC,uCAAuC;gBAClD,SAAS;YACb,CAAC;YACD,IAAI,CAAC,UAAU,GAAG,eAAe,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC;YACzE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YAC/B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACf,QAAQ,EAAE,CAAC;QACf,CAAC;QACD,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IACjC,CAAC;IAED;;;OAGG;IACH,IAAI;QACA,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,UAAU,IAAI,GAAG,CAAC,CAAC;QACtF,KAAK,MAAM,CAAC,IAAI,GAAG,EAAE,CAAC;YAClB,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QAC3B,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;YACb,IAAI,CAAC,UAAU,EAAE,CAAC;QACtB,CAAC;IACL,CAAC;IAED,+FAA+F;IAC/F,OAAO;QACH,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;YACpC,YAAY,CAAC,CAAC,CAAC,CAAC;QACpB,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAED,mGAAmG;IAC3F,IAAI,CAAC,EAAU,EAAE,IAAa;QAClC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC9B,IAAI,CAAC,CAAC,EAAE,CAAC;YACL,OAAO;QACX,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACrB,MAAM,QAAQ,GAAc,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;QAChE,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YACpB,uFAAuF;YACvF,CAAC,CAAC,UAAU,GAAG,eAAe,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;YAChF,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAChB,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC3B,CAAC;QACD,IAAI,CAAC;YACD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC/B,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,wBAAyB,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QACxE,CAAC;QACD,IAAI,IAAI,EAAE,CAAC;YACP,IAAI,CAAC,UAAU,EAAE,CAAC;QACtB,CAAC;IACL,CAAC;IAED,8FAA8F;IACtF,GAAG,CAAC,IAAe;QACvB,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YAC5D,OAAO;QACX,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QACxD,MAAM,MAAM,GAAG,UAAU,CACrB,GAAG,EAAE;YACD,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACnC,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC;gBACrC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,kCAAkC;YACnD,CAAC;iBAAM,CAAC;gBACJ,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YAC7B,CAAC;QACL,CAAC,EACD,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,WAAW,CAAC,CAC/B,CAAC;QACD,MAAiC,CAAC,KAAK,EAAE,EAAE,CAAC;QAC7C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IACtC,CAAC;IAEO,WAAW,CAAC,EAAU;QAC1B,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC/B,IAAI,CAAC,EAAE,CAAC;YACJ,YAAY,CAAC,CAAC,CAAC,CAAC;YAChB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC5B,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,GAAW;QACrB,OAAO,IAAI,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;IAC9D,CAAC;IAEO,UAAU;QACd,IAAI,CAAC;YACD,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACpC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,0BAA2B,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QAC1E,CAAC;IACL,CAAC;CACJ;AAjOD,oCAiOC;AAED,yFAAyF;AACzF,MAAM,aAAa,GAAyC;IACxD,EAAE,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,CAAC;IACrF,EAAE,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,CAAC;IAClF,EAAE,EAAE,CAAC,aAAa,EAAE,aAAa,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,CAAC;CAC1F,CAAC;AAEF,gCAAgC;AAChC,SAAgB,WAAW,CAAC,IAAY,EAAE,MAAc;IACpD,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;AACjF,CAAC;AAED,qGAAqG;AACrG,SAAgB,cAAc,CAAC,QAAkB,EAAE,IAAY;IAC3D,MAAM,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7D,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC3C,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;QACf,OAAO,EAAE,CAAC;IACd,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpB,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;IACvE,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACpE,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,aAAa,CAAC;IAChF,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,aAAa,CAAC;IACrF,CAAC;IACD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACzD,CAAC","sourcesContent":["/**\n * Alarms — reminders at a fixed wall-clock time (\"weck mich um 7 Uhr\"), optionally recurring on chosen\n * weekdays. Sibling of {@link TimerManager} (countdown timers): same ioBroker-first philosophy, but the\n * fire time is an absolute clock time instead of a relative duration, and an alarm can repeat.\n *\n * Like the timer engine this never writes any state on a periodic schedule: each alarm fires from its own\n * `setTimeout` at `nextFireAt`, and the states carry only that absolute timestamp — a vis/script derives\n * the countdown itself. One-shot alarms (no weekdays) are removed after firing; recurring ones are\n * rescheduled to their next matching weekday.\n */\n\n/** One alarm. Plain data so it round-trips through JSON for persistence. */\nexport interface AlarmInfo {\n /** Short unique id (also the `alarms.items.<id>` state-object segment). */\n id: string;\n label: string;\n room: string;\n /** Origin of the request (satellite device name / 'chat' / '') — used to target the announcement. */\n source: string;\n /** Hour 0–23. */\n hour: number;\n /** Minute 0–59. */\n minute: number;\n /** Weekdays it repeats on (0 = Sunday … 6 = Saturday). Empty = one-shot at the next occurrence. */\n weekdays: number[];\n /** When disabled, the alarm is kept but does not fire (nextFireAt = 0). */\n enabled: boolean;\n createdAt: number;\n /** Epoch ms of the next fire (0 when disabled / none). */\n nextFireAt: number;\n}\n\nexport interface AddAlarmOptions {\n label?: string;\n room?: string;\n source?: string;\n hour: number;\n minute: number;\n weekdays?: number[];\n}\n\nexport interface AlarmManagerOptions {\n /** Called when an alarm fires (with a snapshot; recurring alarms stay active, one-shots are removed). */\n onFire: (alarm: AlarmInfo) => void;\n /** Called whenever the list changes (add/cancel/enable/fire/restore) with the new sorted list. */\n onChange: (alarms: AlarmInfo[]) => void;\n log?: Pick<ioBroker.Log, 'debug' | 'info' | 'warn'>;\n /** Injectable clock (tests). Defaults to `Date.now`. */\n now?: () => number;\n /** Don't arm real `setTimeout`s — the caller drives {@link AlarmManager.tick} (tests). */\n manualTick?: boolean;\n}\n\n/** Largest delay `setTimeout` accepts (~24.8 days); a farther alarm is re-armed in chunks. */\nconst MAX_TIMEOUT = 2 ** 31 - 1;\n\n/**\n * Compute the next epoch-ms an alarm at `hour:minute` fires strictly after `fromMs`, honouring `weekdays`\n * (empty = any day). Uses the host's local time. Returns 0 if nothing matches within a week (never happens\n * for valid input).\n */\nexport function computeNextFire(hour: number, minute: number, weekdays: number[], fromMs: number): number {\n const base = new Date(fromMs);\n for (let addDays = 0; addDays <= 7; addDays++) {\n const d = new Date(base.getFullYear(), base.getMonth(), base.getDate() + addDays, hour, minute, 0, 0);\n if (d.getTime() <= fromMs) {\n continue; // must be strictly in the future\n }\n if (weekdays.length && !weekdays.includes(d.getDay())) {\n continue;\n }\n return d.getTime();\n }\n return 0;\n}\n\n/** In-memory alarm scheduler; each alarm fires from its own `setTimeout` (no polling, no periodic writes). */\nexport class AlarmManager {\n private readonly alarms = new Map<string, AlarmInfo>();\n private readonly handles = new Map<string, ReturnType<typeof setTimeout>>();\n private seq = 0;\n private readonly now: () => number;\n\n constructor(private readonly opts: AlarmManagerOptions) {\n this.now = opts.now || Date.now;\n }\n\n /** Add an alarm, schedule it and notify. Returns the created {@link AlarmInfo}. */\n add(o: AddAlarmOptions): AlarmInfo {\n const now = this.now();\n const weekdays = [...new Set((o.weekdays || []).filter(d => d >= 0 && d <= 6))].sort();\n const hour = Math.max(0, Math.min(23, Math.round(o.hour)));\n const minute = Math.max(0, Math.min(59, Math.round(o.minute)));\n const info: AlarmInfo = {\n id: this.genId(now),\n label: (o.label || '').trim(),\n room: (o.room || '').trim(),\n source: o.source || '',\n hour,\n minute,\n weekdays,\n enabled: true,\n createdAt: now,\n nextFireAt: computeNextFire(hour, minute, weekdays, now),\n };\n this.alarms.set(info.id, info);\n this.opts.log?.debug(\n `alarm added: ${info.id} \"${info.label}\" at ${hour}:${String(minute).padStart(2, '0')} [${weekdays.join(',') || 'once'}]`,\n );\n this.arm(info);\n this.emitChange();\n return info;\n }\n\n /** Enable/disable an alarm without deleting it. Returns true if it existed. */\n setEnabled(id: string, enabled: boolean): boolean {\n const a = this.alarms.get(id);\n if (!a) {\n return false;\n }\n a.enabled = enabled;\n this.clearHandle(id);\n if (enabled) {\n a.nextFireAt = computeNextFire(a.hour, a.minute, a.weekdays, this.now());\n this.arm(a);\n } else {\n a.nextFireAt = 0;\n }\n this.emitChange();\n return true;\n }\n\n /** Cancel/delete one alarm by id. Returns true if it existed. */\n cancel(id: string): boolean {\n this.clearHandle(id);\n if (!this.alarms.delete(id)) {\n return false;\n }\n this.emitChange();\n return true;\n }\n\n /** Cancel all alarms (optionally only those in a room). Returns the number cancelled. */\n cancelAll(room?: string): number {\n const victims = this.list().filter(a => !room || a.room === room);\n for (const a of victims) {\n this.clearHandle(a.id);\n this.alarms.delete(a.id);\n }\n if (victims.length) {\n this.emitChange();\n }\n return victims.length;\n }\n\n /** The alarms, soonest-to-fire first (disabled ones — nextFireAt 0 — sort last). */\n list(): AlarmInfo[] {\n return [...this.alarms.values()].sort((a, b) => {\n const an = a.nextFireAt || Infinity;\n const bn = b.nextFireAt || Infinity;\n return an - bn;\n });\n }\n\n get(id: string): AlarmInfo | undefined {\n return this.alarms.get(id);\n }\n\n /**\n * Restore persisted alarms after a restart. Disabled alarms are kept as-is; enabled ones recompute\n * their next fire from now. A one-shot whose time passed while the adapter was down is dropped.\n * Returns how many were restored / dropped.\n */\n restore(list: AlarmInfo[]): { restored: number; dropped: number } {\n const now = this.now();\n let restored = 0;\n let dropped = 0;\n for (const a of list) {\n if (!a || typeof a.id !== 'string' || typeof a.hour !== 'number' || typeof a.minute !== 'number') {\n continue;\n }\n const weekdays = Array.isArray(a.weekdays) ? a.weekdays : [];\n const info: AlarmInfo = {\n id: a.id,\n label: a.label || '',\n room: a.room || '',\n source: a.source || '',\n hour: a.hour,\n minute: a.minute,\n weekdays,\n enabled: a.enabled !== false,\n createdAt: a.createdAt || now,\n nextFireAt: 0,\n };\n if (!info.enabled) {\n this.alarms.set(info.id, info); // keep, but don't arm\n restored++;\n continue;\n }\n if (!weekdays.length && typeof a.nextFireAt === 'number' && a.nextFireAt <= now) {\n dropped++; // a one-shot we missed during downtime\n continue;\n }\n info.nextFireAt = computeNextFire(info.hour, info.minute, weekdays, now);\n this.alarms.set(info.id, info);\n this.arm(info);\n restored++;\n }\n this.emitChange();\n return { restored, dropped };\n }\n\n /**\n * Fire every enabled alarm whose `nextFireAt` has passed — public so tests can drive it with a mocked\n * clock (production fires each alarm from its own `setTimeout`, so this is a no-op there).\n */\n tick(): void {\n const now = this.now();\n const due = this.list().filter(a => a.enabled && a.nextFireAt && a.nextFireAt <= now);\n for (const a of due) {\n this.fire(a.id, false);\n }\n if (due.length) {\n this.emitChange();\n }\n }\n\n /** Clear all pending timeouts and drop the in-memory alarms (no callbacks). Call on unload. */\n dispose(): void {\n for (const h of this.handles.values()) {\n clearTimeout(h);\n }\n this.handles.clear();\n this.alarms.clear();\n }\n\n /** Fire one alarm: one-shots are removed, recurring ones are rescheduled to their next weekday. */\n private fire(id: string, emit: boolean): void {\n const a = this.alarms.get(id);\n if (!a) {\n return;\n }\n this.clearHandle(id);\n const snapshot: AlarmInfo = { ...a, weekdays: [...a.weekdays] };\n if (a.weekdays.length) {\n // Recompute from just after this fire so we don't immediately re-fire the same minute.\n a.nextFireAt = computeNextFire(a.hour, a.minute, a.weekdays, this.now() + 1000);\n this.arm(a);\n } else {\n this.alarms.delete(id);\n }\n try {\n this.opts.onFire(snapshot);\n } catch (e) {\n this.opts.log?.warn(`alarm onFire failed: ${(e as Error).message}`);\n }\n if (emit) {\n this.emitChange();\n }\n }\n\n /** Arm the `setTimeout` for an alarm (chunked for fire times beyond the setTimeout limit). */\n private arm(info: AlarmInfo): void {\n if (this.opts.manualTick || !info.enabled || !info.nextFireAt) {\n return;\n }\n this.clearHandle(info.id);\n const delay = Math.max(0, info.nextFireAt - this.now());\n const handle = setTimeout(\n () => {\n const a = this.alarms.get(info.id);\n if (a && a.nextFireAt - this.now() > 0) {\n this.arm(a); // was only a chunk of a long wait\n } else {\n this.fire(info.id, true);\n }\n },\n Math.min(delay, MAX_TIMEOUT),\n );\n (handle as { unref?: () => void }).unref?.();\n this.handles.set(info.id, handle);\n }\n\n private clearHandle(id: string): void {\n const h = this.handles.get(id);\n if (h) {\n clearTimeout(h);\n this.handles.delete(id);\n }\n }\n\n private genId(now: number): string {\n return `a${now.toString(36)}${(this.seq++).toString(36)}`;\n }\n\n private emitChange(): void {\n try {\n this.opts.onChange(this.list());\n } catch (e) {\n this.opts.log?.warn(`alarm onChange failed: ${(e as Error).message}`);\n }\n }\n}\n\n/** Weekday names → short localized labels for the spoken reply / states (0 = Sunday). */\nconst WEEKDAY_NAMES: Record<'de' | 'en' | 'ru', string[]> = {\n de: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],\n en: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],\n ru: ['воскресенье', 'понедельник', 'вторник', 'среда', 'четверг', 'пятница', 'суббота'],\n};\n\n/** \"07:30\" from hour/minute. */\nexport function formatClock(hour: number, minute: number): string {\n return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;\n}\n\n/** Human, spoken-friendly recurrence text: \"\" (once), \"täglich\", \"wochentags\", or a weekday list. */\nexport function formatWeekdays(weekdays: number[], lang: string): string {\n const l = lang === 'ru' ? 'ru' : lang === 'de' ? 'de' : 'en';\n const days = [...new Set(weekdays)].sort();\n if (!days.length) {\n return '';\n }\n if (days.length === 7) {\n return l === 'ru' ? 'ежедневно' : l === 'de' ? 'täglich' : 'daily';\n }\n if (days.length === 5 && [1, 2, 3, 4, 5].every(d => days.includes(d))) {\n return l === 'ru' ? 'по будням' : l === 'de' ? 'wochentags' : 'on weekdays';\n }\n if (days.length === 2 && days.includes(0) && days.includes(6)) {\n return l === 'ru' ? 'по выходным' : l === 'de' ? 'am Wochenende' : 'on weekends';\n }\n return days.map(d => WEEKDAY_NAMES[l][d]).join(', ');\n}\n"]}
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConversationStore = void 0;
4
+ class ConversationStore {
5
+ maxTurns;
6
+ ttlMs;
7
+ map = new Map();
8
+ constructor(
9
+ /** Keep at most this many turns (user+assistant messages) per source. */
10
+ maxTurns = 6,
11
+ /** Drop a thread after this idle time (ms). */
12
+ ttlMs = 5 * 60 * 1000) {
13
+ this.maxTurns = maxTurns;
14
+ this.ttlMs = ttlMs;
15
+ }
16
+ /** Recent turns for a source (empty if none or expired). */
17
+ get(source) {
18
+ const e = this.map.get(source);
19
+ if (!e) {
20
+ return [];
21
+ }
22
+ if (Date.now() - e.ts > this.ttlMs) {
23
+ this.map.delete(source);
24
+ return [];
25
+ }
26
+ return e.turns;
27
+ }
28
+ /** Append a completed exchange (question + answer) and cap/refresh the buffer. */
29
+ add(source, user, assistant) {
30
+ if (!user || !assistant) {
31
+ return;
32
+ }
33
+ const e = this.map.get(source) ?? { turns: [], ts: 0 };
34
+ e.turns.push({ role: 'user', content: user }, { role: 'assistant', content: assistant });
35
+ while (e.turns.length > this.maxTurns) {
36
+ e.turns.shift();
37
+ }
38
+ e.ts = Date.now();
39
+ this.map.set(source, e);
40
+ }
41
+ /** Reset one source (or all if omitted). */
42
+ clear(source) {
43
+ if (source === undefined) {
44
+ this.map.clear();
45
+ }
46
+ else {
47
+ this.map.delete(source);
48
+ }
49
+ }
50
+ }
51
+ exports.ConversationStore = ConversationStore;
52
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/lib/context.ts"],"names":[],"mappings":";;;AAUA,MAAa,iBAAiB;IAKL;IAEA;IANJ,GAAG,GAAG,IAAI,GAAG,EAAqD,CAAC;IAEpF;IACI,yEAAyE;IACxD,WAAW,CAAC;IAC7B,+CAA+C;IAC9B,QAAQ,CAAC,GAAG,EAAE,GAAG,IAAI;QAFrB,aAAQ,GAAR,QAAQ,CAAI;QAEZ,UAAK,GAAL,KAAK,CAAgB;IACvC,CAAC;IAEJ,4DAA4D;IAC5D,GAAG,CAAC,MAAc;QACd,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC/B,IAAI,CAAC,CAAC,EAAE,CAAC;YACL,OAAO,EAAE,CAAC;QACd,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;YACjC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YACxB,OAAO,EAAE,CAAC;QACd,CAAC;QACD,OAAO,CAAC,CAAC,KAAK,CAAC;IACnB,CAAC;IAED,kFAAkF;IAClF,GAAG,CAAC,MAAc,EAAE,IAAY,EAAE,SAAiB;QAC/C,IAAI,CAAC,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACtB,OAAO;QACX,CAAC;QACD,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;QACvD,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;QACzF,OAAO,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YACpC,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACpB,CAAC;QACD,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAClB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAC5B,CAAC;IAED,4CAA4C;IAC5C,KAAK,CAAC,MAAe;QACjB,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACvB,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;QACrB,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC;IACL,CAAC;CACJ;AA7CD,8CA6CC","sourcesContent":["/**\n * Short-term conversation memory, keyed by request source (e.g. 'chat', 'telegram:Max', a satellite\n * name). Lets the LLM resolve follow-ups like \"…and the kitchen light too\" or \"turn it back off\".\n * Each source is an independent thread; entries expire after a TTL so a new conversation starts fresh.\n */\nexport interface ConversationTurn {\n role: 'user' | 'assistant';\n content: string;\n}\n\nexport class ConversationStore {\n private readonly map = new Map<string, { turns: ConversationTurn[]; ts: number }>();\n\n constructor(\n /** Keep at most this many turns (user+assistant messages) per source. */\n private readonly maxTurns = 6,\n /** Drop a thread after this idle time (ms). */\n private readonly ttlMs = 5 * 60 * 1000,\n ) {}\n\n /** Recent turns for a source (empty if none or expired). */\n get(source: string): ConversationTurn[] {\n const e = this.map.get(source);\n if (!e) {\n return [];\n }\n if (Date.now() - e.ts > this.ttlMs) {\n this.map.delete(source);\n return [];\n }\n return e.turns;\n }\n\n /** Append a completed exchange (question + answer) and cap/refresh the buffer. */\n add(source: string, user: string, assistant: string): void {\n if (!user || !assistant) {\n return;\n }\n const e = this.map.get(source) ?? { turns: [], ts: 0 };\n e.turns.push({ role: 'user', content: user }, { role: 'assistant', content: assistant });\n while (e.turns.length > this.maxTurns) {\n e.turns.shift();\n }\n e.ts = Date.now();\n this.map.set(source, e);\n }\n\n /** Reset one source (or all if omitted). */\n clear(source?: string): void {\n if (source === undefined) {\n this.map.clear();\n } else {\n this.map.delete(source);\n }\n }\n}\n"]}
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readCentralKey = readCentralKey;
7
+ exports.resolveApiKey = resolveApiKey;
8
+ exports.readCentralValues = readCentralValues;
9
+ exports.resolveVoiceCredentials = resolveVoiceCredentials;
10
+ /**
11
+ * Resolve the LLM API key from the adapter config.
12
+ *
13
+ * Two modes (`config.credentialType`):
14
+ * - 'manual': the key is stored directly in the (encrypted) adapter config (`config.apiKey`).
15
+ * - 'manager': the config only stores the ID of a credential in the central ioBroker
16
+ * credential store (`system.credentials.*`); the real key is resolved at runtime
17
+ * via the Credentials API.
18
+ *
19
+ * The central Credentials API (`Credentials.getCredentials`) is only available with
20
+ * js-controller >= 7.2 / a recent \@iobroker/adapter-core. We access it defensively so the
21
+ * adapter still compiles and runs (in `manual` mode) on older controllers.
22
+ */
23
+ const credentials_1 = __importDefault(require("@iobroker/adapter-core/credentials"));
24
+ /** Read and decrypt a single credential's key from the central store; '' (and a warning) on error. */
25
+ async function readCentralKey(adapter, id) {
26
+ if (!id) {
27
+ return '';
28
+ }
29
+ if (!credentials_1.default?.getCredentials) {
30
+ adapter.log.warn(`Cannot read credential "${id}": the central Credentials API needs js-controller >= 7.2`);
31
+ return '';
32
+ }
33
+ try {
34
+ const cred = await credentials_1.default.getCredentials(adapter, id);
35
+ return (cred?.values?.key || '').trim();
36
+ }
37
+ catch (e) {
38
+ adapter.log.warn(`Cannot read credential "${id}": ${e.message}`);
39
+ return '';
40
+ }
41
+ }
42
+ /**
43
+ * Resolve the effective API key for the configured provider.
44
+ * `overrideKey`/`overrideCredentialId` win over the stored config (used by the settings-dialog
45
+ * Test button, where unsaved form values should be used).
46
+ */
47
+ async function resolveApiKey(adapter, config, override) {
48
+ const mode = override?.credentialType || config.credentialType || 'manual';
49
+ if (mode === 'manager') {
50
+ const id = (override?.credentialId || config.credentialIdApiKey || '').trim();
51
+ return readCentralKey(adapter, id);
52
+ }
53
+ return (override?.apiKey || config.apiKey || '').trim();
54
+ }
55
+ /** Read and decrypt all fields of a credential from the central store; {} (and a warning) on error. */
56
+ async function readCentralValues(adapter, id) {
57
+ if (!id) {
58
+ return {};
59
+ }
60
+ if (!credentials_1.default?.getCredentials) {
61
+ adapter.log.warn(`Cannot read credential "${id}": the central Credentials API needs js-controller >= 7.2`);
62
+ return {};
63
+ }
64
+ try {
65
+ const cred = await credentials_1.default.getCredentials(adapter, id);
66
+ return cred?.values || {};
67
+ }
68
+ catch (e) {
69
+ adapter.log.warn(`Cannot read credential "${id}": ${e.message}`);
70
+ return {};
71
+ }
72
+ }
73
+ /** Safe stringify of a credential/config field (values are strings/numbers/booleans). */
74
+ function str(v) {
75
+ if (typeof v === 'string') {
76
+ return v.trim();
77
+ }
78
+ if (typeof v === 'number' || typeof v === 'boolean') {
79
+ return String(v);
80
+ }
81
+ return '';
82
+ }
83
+ /**
84
+ * Resolve the speech (STT/TTS) credentials for OpenAI, Azure and AWS.
85
+ * - 'manual': the encrypted key/region config fields.
86
+ * - 'manager': the central credential store, keyed by the *CredentialId fields
87
+ * (azure values: { key, region }; aws values: { accessKeyId, secretAccessKey, region }).
88
+ * `mainApiKey` is reused for OpenAI speech when no dedicated OpenAI speech credential is set and the
89
+ * LLM provider is already OpenAI.
90
+ */
91
+ async function resolveVoiceCredentials(adapter, config, mainApiKey) {
92
+ const openaiFallback = config.provider === 'openai' ? mainApiKey : '';
93
+ if (config.voiceCredentialType === 'manager') {
94
+ const openaiKey = config.voiceCredentialId
95
+ ? await readCentralKey(adapter, config.voiceCredentialId)
96
+ : openaiFallback;
97
+ const azure = config.azureCredentialId ? await readCentralValues(adapter, config.azureCredentialId) : {};
98
+ const aws = config.awsCredentialId ? await readCentralValues(adapter, config.awsCredentialId) : {};
99
+ return {
100
+ openaiKey,
101
+ azureKey: str(azure.key),
102
+ azureRegion: str(azure.region),
103
+ aws: {
104
+ accessKeyId: str(aws.accessKeyId),
105
+ secretAccessKey: str(aws.secretAccessKey),
106
+ region: str(aws.region),
107
+ },
108
+ };
109
+ }
110
+ return {
111
+ openaiKey: str(config.voiceApiKey) || openaiFallback,
112
+ azureKey: str(config.azureSpeechKey),
113
+ azureRegion: str(config.azureSpeechRegion),
114
+ aws: {
115
+ accessKeyId: str(config.awsAccessKeyId),
116
+ secretAccessKey: str(config.awsSecretAccessKey),
117
+ region: str(config.awsRegion),
118
+ },
119
+ };
120
+ }
121
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/lib/credentials.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Resolve the LLM API key from the adapter config.\n *\n * Two modes (`config.credentialType`):\n * - 'manual': the key is stored directly in the (encrypted) adapter config (`config.apiKey`).\n * - 'manager': the config only stores the ID of a credential in the central ioBroker\n * credential store (`system.credentials.*`); the real key is resolved at runtime\n * via the Credentials API.\n *\n * The central Credentials API (`Credentials.getCredentials`) is only available with\n * js-controller >= 7.2 / a recent \\@iobroker/adapter-core. We access it defensively so the\n * adapter still compiles and runs (in `manual` mode) on older controllers.\n */\nimport Credentials from '@iobroker/adapter-core/credentials';\nimport type { AdapterConfig } from '../types';\nimport type { VoiceCredentials } from './voice/engines';\n\n/** Read and decrypt a single credential's key from the central store; '' (and a warning) on error. */\nexport async function readCentralKey(adapter: ioBroker.Adapter, id: string): Promise<string> {\n if (!id) {\n return '';\n }\n if (!Credentials?.getCredentials) {\n adapter.log.warn(`Cannot read credential \"${id}\": the central Credentials API needs js-controller >= 7.2`);\n return '';\n }\n try {\n const cred = await Credentials.getCredentials(adapter, id);\n return ((cred?.values?.key as string) || '').trim();\n } catch (e) {\n adapter.log.warn(`Cannot read credential \"${id}\": ${(e as Error).message}`);\n return '';\n }\n}\n\n/**\n * Resolve the effective API key for the configured provider.\n * `overrideKey`/`overrideCredentialId` win over the stored config (used by the settings-dialog\n * Test button, where unsaved form values should be used).\n */\nexport async function resolveApiKey(\n adapter: ioBroker.Adapter,\n config: AdapterConfig,\n override?: { credentialType?: 'manual' | 'manager'; apiKey?: string; credentialId?: string },\n): Promise<string> {\n const mode = override?.credentialType || config.credentialType || 'manual';\n if (mode === 'manager') {\n const id = (override?.credentialId || config.credentialIdApiKey || '').trim();\n return readCentralKey(adapter, id);\n }\n return (override?.apiKey || config.apiKey || '').trim();\n}\n\n/** Read and decrypt all fields of a credential from the central store; {} (and a warning) on error. */\nexport async function readCentralValues(adapter: ioBroker.Adapter, id: string): Promise<Record<string, unknown>> {\n if (!id) {\n return {};\n }\n if (!Credentials?.getCredentials) {\n adapter.log.warn(`Cannot read credential \"${id}\": the central Credentials API needs js-controller >= 7.2`);\n return {};\n }\n try {\n const cred = await Credentials.getCredentials(adapter, id);\n return cred?.values || {};\n } catch (e) {\n adapter.log.warn(`Cannot read credential \"${id}\": ${(e as Error).message}`);\n return {};\n }\n}\n\n/** Safe stringify of a credential/config field (values are strings/numbers/booleans). */\nfunction str(v: unknown): string {\n if (typeof v === 'string') {\n return v.trim();\n }\n if (typeof v === 'number' || typeof v === 'boolean') {\n return String(v);\n }\n return '';\n}\n\n/**\n * Resolve the speech (STT/TTS) credentials for OpenAI, Azure and AWS.\n * - 'manual': the encrypted key/region config fields.\n * - 'manager': the central credential store, keyed by the *CredentialId fields\n * (azure values: { key, region }; aws values: { accessKeyId, secretAccessKey, region }).\n * `mainApiKey` is reused for OpenAI speech when no dedicated OpenAI speech credential is set and the\n * LLM provider is already OpenAI.\n */\nexport async function resolveVoiceCredentials(\n adapter: ioBroker.Adapter,\n config: AdapterConfig,\n mainApiKey: string,\n): Promise<VoiceCredentials> {\n const openaiFallback = config.provider === 'openai' ? mainApiKey : '';\n\n if (config.voiceCredentialType === 'manager') {\n const openaiKey = config.voiceCredentialId\n ? await readCentralKey(adapter, config.voiceCredentialId)\n : openaiFallback;\n const azure = config.azureCredentialId ? await readCentralValues(adapter, config.azureCredentialId) : {};\n const aws = config.awsCredentialId ? await readCentralValues(adapter, config.awsCredentialId) : {};\n return {\n openaiKey,\n azureKey: str(azure.key),\n azureRegion: str(azure.region),\n aws: {\n accessKeyId: str(aws.accessKeyId),\n secretAccessKey: str(aws.secretAccessKey),\n region: str(aws.region),\n },\n };\n }\n\n return {\n openaiKey: str(config.voiceApiKey) || openaiFallback,\n azureKey: str(config.azureSpeechKey),\n azureRegion: str(config.azureSpeechRegion),\n aws: {\n accessKeyId: str(config.awsAccessKeyId),\n secretAccessKey: str(config.awsSecretAccessKey),\n region: str(config.awsRegion),\n },\n };\n}\n"]}