@camstack/system 1.1.51 → 1.1.53

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 (61) hide show
  1. package/dist/addon-runner.js +2 -2
  2. package/dist/addon-runner.mjs +2 -2
  3. package/dist/addon-utils.js +1 -1
  4. package/dist/addon-utils.mjs +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  9. package/dist/builtins/alerts/alerts.addon.js +1 -1
  10. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  13. package/dist/builtins/console-logging/index.js +1 -1
  14. package/dist/builtins/console-logging/index.mjs +1 -1
  15. package/dist/builtins/device-manager/device-linked-devices.d.ts +47 -0
  16. package/dist/builtins/device-manager/device-manager.addon.js +2821 -2683
  17. package/dist/builtins/device-manager/device-manager.addon.mjs +2821 -2683
  18. package/dist/builtins/doorbell/index.d.ts +2 -0
  19. package/dist/builtins/doorbell/index.js +8 -0
  20. package/dist/builtins/doorbell/index.mjs +2 -0
  21. package/dist/builtins/doorbell/trigger-engine.d.ts +69 -0
  22. package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +87 -0
  23. package/dist/builtins/doorbell/virtual-doorbell.addon.js +489 -0
  24. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +483 -0
  25. package/dist/builtins/hub-forwarder/index.js +1 -1
  26. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  27. package/dist/builtins/local-auth/local-auth.addon.js +2 -2
  28. package/dist/builtins/local-auth/local-auth.addon.mjs +2 -2
  29. package/dist/builtins/local-network/local-network.addon.js +1 -1
  30. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  31. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  32. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  33. package/dist/builtins/platform-probe/index.js +1 -1
  34. package/dist/builtins/platform-probe/index.mjs +1 -1
  35. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  36. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  37. package/dist/builtins/snapshot/index.js +1 -1
  38. package/dist/builtins/snapshot/index.mjs +1 -1
  39. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  40. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  41. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +11 -3
  42. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +11 -3
  43. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  44. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  45. package/dist/builtins/system-config/system-config.addon.js +4 -4
  46. package/dist/builtins/system-config/system-config.addon.mjs +4 -4
  47. package/dist/builtins/winston-logging/index.js +1 -1
  48. package/dist/builtins/winston-logging/index.mjs +1 -1
  49. package/dist/{dist-C3uHEBtP.js → dist-BNro5CRu.js} +325 -27
  50. package/dist/{dist-BWQX9yUj.mjs → dist-D7_69kDt.mjs} +320 -28
  51. package/dist/http/file-data-plane.d.ts +8 -0
  52. package/dist/index.js +23 -6
  53. package/dist/index.mjs +23 -7
  54. package/dist/kernel/index.d.ts +1 -1
  55. package/dist/kernel/moleculer/readiness-context.d.ts +12 -8
  56. package/dist/kernel/moleculer/readiness-service.d.ts +16 -1
  57. package/dist/{manifest-python-deps-CVcJ9hDX.js → manifest-python-deps-DszE7bYn.js} +35 -9
  58. package/dist/{manifest-python-deps-BA6KA9If.mjs → manifest-python-deps-EM2H-jjS.mjs} +35 -9
  59. package/dist/{model-download-service-C-IHWnXx.mjs → model-download-service-Cp9f4dk6.mjs} +1 -1
  60. package/dist/{model-download-service-D-Umz4-L.js → model-download-service-hf0ookyy.js} +1 -1
  61. package/package.json +13 -1
@@ -0,0 +1,483 @@
1
+ import { C as doorbellCapability, _t as createEvent, it as BaseAddon, kt as EventCategory, rt as errMsg, ut as DeviceType } from "../../dist-D7_69kDt.mjs";
2
+ //#region src/builtins/doorbell/trigger-engine.ts
3
+ var DOORBELL_TRIGGERS = [
4
+ "contact-open",
5
+ "contact-close",
6
+ "switch-on",
7
+ "event"
8
+ ];
9
+ /** Type guard for {@link DoorbellTrigger}. */
10
+ function isDoorbellTrigger(value) {
11
+ return typeof value === "string" && DOORBELL_TRIGGERS.includes(value);
12
+ }
13
+ /** Cap slices the engine understands as binary trigger sources. */
14
+ var CONTACT_CAP = "contact";
15
+ var SWITCH_CAP = "switch";
16
+ /**
17
+ * Map an observed boolean transition on a source cap to the trigger it
18
+ * satisfies. `null` when the transition has no configured meaning
19
+ * (e.g. switch turning OFF).
20
+ */
21
+ function edgeToTrigger(capName, value) {
22
+ if (capName === CONTACT_CAP) return value ? "contact-open" : "contact-close";
23
+ if (capName === SWITCH_CAP) return value ? "switch-on" : null;
24
+ return null;
25
+ }
26
+ /** Extract the binary state carried by a contact / switch slice. */
27
+ function sliceBinaryValue(capName, slice) {
28
+ const raw = capName === CONTACT_CAP ? slice["entryOpen"] : slice["on"];
29
+ return typeof raw === "boolean" ? raw : null;
30
+ }
31
+ /**
32
+ * Stateful (but side-effect-free towards the outside) trigger engine.
33
+ *
34
+ * Tracks the last observed binary value per (sourceDevice, cap) so a
35
+ * `DeviceStateChanged` re-emission (the slice's `lastChangedAt` moves
36
+ * while the binary value does not) never double-fires, and the FIRST
37
+ * observation after boot only seeds the baseline — a state hydration
38
+ * at startup must not ring anybody's doorbell.
39
+ */
40
+ var DoorbellTriggerEngine = class {
41
+ assignments = [];
42
+ /** Last observed binary value keyed by `${deviceId}:${capName}`. */
43
+ lastValues = /* @__PURE__ */ new Map();
44
+ /** Last fire timestamp per camera — debounce bookkeeping. */
45
+ lastFiredAt = /* @__PURE__ */ new Map();
46
+ debounceMs;
47
+ now;
48
+ constructor(options) {
49
+ this.debounceMs = options?.debounceMs ?? 2e3;
50
+ this.now = options?.now ?? Date.now;
51
+ }
52
+ /** Replace the full assignment set (rebuilt whenever settings change). */
53
+ setAssignments(assignments) {
54
+ this.assignments = [...assignments];
55
+ }
56
+ getAssignments() {
57
+ return this.assignments;
58
+ }
59
+ /** True when any assignment references `deviceId` as its source. */
60
+ hasSource(deviceId) {
61
+ return this.assignments.some((a) => a.sourceDeviceId === deviceId);
62
+ }
63
+ /**
64
+ * Feed one `DeviceStateChanged` slice. Returns the camera ids whose
65
+ * doorbell should ring (already debounced). Baseline-only sightings
66
+ * and non-transitions return an empty array.
67
+ */
68
+ onStateSlice(deviceId, capName, slice) {
69
+ if (capName !== CONTACT_CAP && capName !== SWITCH_CAP) return [];
70
+ const value = sliceBinaryValue(capName, slice);
71
+ if (value === null) return [];
72
+ const key = `${deviceId}:${capName}`;
73
+ const prior = this.lastValues.get(key);
74
+ this.lastValues.set(key, value);
75
+ if (prior === void 0 || prior === value) return [];
76
+ const trigger = edgeToTrigger(capName, value);
77
+ if (!trigger) return [];
78
+ return this.matchAndDebounce((a) => a.sourceDeviceId === deviceId && a.trigger === trigger);
79
+ }
80
+ /**
81
+ * Feed one event-emitter `onEvent` firing. Returns the camera ids to
82
+ * ring — assignments with `trigger: 'event'`, a matching source and a
83
+ * verbatim `eventType` match. An assignment with no configured
84
+ * eventType never matches (misconfiguration must not ring on every
85
+ * event the device emits).
86
+ */
87
+ onEmitterEvent(deviceId, eventType) {
88
+ return this.matchAndDebounce((a) => a.trigger === "event" && a.sourceDeviceId === deviceId && a.eventType !== null && a.eventType.length > 0 && a.eventType === eventType);
89
+ }
90
+ matchAndDebounce(predicate) {
91
+ const now = this.now();
92
+ const fired = [];
93
+ for (const assignment of this.assignments) {
94
+ if (!predicate(assignment)) continue;
95
+ const last = this.lastFiredAt.get(assignment.cameraId);
96
+ if (last !== void 0 && now - last < this.debounceMs) continue;
97
+ this.lastFiredAt.set(assignment.cameraId, now);
98
+ fired.push(assignment.cameraId);
99
+ }
100
+ return fired;
101
+ }
102
+ };
103
+ //#endregion
104
+ //#region src/builtins/doorbell/virtual-doorbell.addon.ts
105
+ /** Device-store keys (namespaced to avoid collisions in merged stores). */
106
+ var KEY_SOURCE = "doorbellSourceDeviceId";
107
+ var KEY_TRIGGER = "doorbellTrigger";
108
+ var KEY_EVENT_TYPE = "doorbellEventType";
109
+ var DEFAULT_TRIGGER = "contact-open";
110
+ /**
111
+ * Caps whose presence in a device's bindings marks it as a usable
112
+ * doorbell SOURCE — anything with a binary-ish surface. Deliberately
113
+ * not gated on device linking: which device rings which camera is an
114
+ * operator decision.
115
+ */
116
+ var SOURCE_CAPS = [
117
+ "contact",
118
+ "flood",
119
+ "gas",
120
+ "carbon-monoxide",
121
+ "enum-sensor",
122
+ "event-emitter",
123
+ "switch"
124
+ ];
125
+ /** Reload the camera→source assignment map at most this often (lazily,
126
+ * on trigger traffic). Covers devices/settings appearing after boot
127
+ * without a settings save. */
128
+ var ASSIGNMENTS_TTL_MS = 6e4;
129
+ var TRIGGER_LABELS = {
130
+ "contact-open": "Contact opens",
131
+ "contact-close": "Contact closes",
132
+ "switch-on": "Switch turns on",
133
+ event: "Device event"
134
+ };
135
+ /** Structural narrowing helper for untrusted bus payloads. */
136
+ function isRecord(value) {
137
+ return typeof value === "object" && value !== null && !Array.isArray(value);
138
+ }
139
+ /**
140
+ * Cast-free narrowing of the untyped `device.state-changed` payload
141
+ * (the category is not part of the typed EventCatalog — subscribers
142
+ * validate at the boundary).
143
+ */
144
+ function parseDeviceStateChanged(data) {
145
+ if (!isRecord(data)) return null;
146
+ const deviceId = data["deviceId"];
147
+ const capName = data["capName"];
148
+ const slice = data["slice"];
149
+ if (typeof deviceId !== "number" || typeof capName !== "string") return null;
150
+ if (!isRecord(slice)) return null;
151
+ return {
152
+ deviceId,
153
+ capName,
154
+ slice
155
+ };
156
+ }
157
+ /** Narrow a raw `doorbell` runtime-state slice to counters (defaults on absence). */
158
+ function parseDoorbellSlice(slice) {
159
+ const lastPressedAtRaw = slice?.["lastPressedAt"];
160
+ const countRaw = slice?.["pressCountSinceStart"];
161
+ return {
162
+ lastPressedAt: typeof lastPressedAtRaw === "number" ? lastPressedAtRaw : null,
163
+ pressCountSinceStart: typeof countRaw === "number" && Number.isFinite(countRaw) && countRaw >= 0 ? countRaw : 0
164
+ };
165
+ }
166
+ /**
167
+ * VirtualDoorbellAddon — WRAPPER over the `doorbell` capability.
168
+ *
169
+ * Turns any binary-ish device (contact sensor, switch, event emitter,
170
+ * flood/gas/CO sensor) into a doorbell for a camera:
171
+ *
172
+ * 1. The operator binds the wrapper to a camera in the bindings UI
173
+ * (`defaultActive: false` — never auto-bound).
174
+ * 2. Per-camera settings pick the SOURCE device + trigger
175
+ * (`contact-open` / `contact-close` / `switch-on` / `event`
176
+ * with an event-type match).
177
+ * 3. At runtime the addon watches `EventCategory.DeviceStateChanged`
178
+ * (contact/switch slice transitions — EDGE detected per trigger)
179
+ * and `EventCategory.EventEmitted` (event-emitter firings). On a
180
+ * matching, debounced trigger it emits
181
+ * `EventCategory.DoorbellOnPressed` with the CAMERA as source
182
+ * (`{deviceId: cameraId, timestamp}` — the same shape Reolink's
183
+ * native `emitDoorbellPressed` produces) and bumps the camera's
184
+ * `doorbell` runtime-state counters via the standard
185
+ * `deviceState.setCapSlice` plumbing.
186
+ *
187
+ * Fires ONLY for cameras where the wrapper is BOUND (checked against
188
+ * `deviceManager.getBindings`) and a source device is configured.
189
+ */
190
+ var VirtualDoorbellAddon = class extends BaseAddon {
191
+ engine;
192
+ /** Monotonic guard: assignments are reloaded lazily on trigger traffic. */
193
+ assignmentsLoadedAt = 0;
194
+ assignmentsLoading = null;
195
+ constructor() {
196
+ super({ debounceMs: 2e3 });
197
+ this.engine = new DoorbellTriggerEngine({ debounceMs: 2e3 });
198
+ }
199
+ async onInitialize() {
200
+ this.ctx.logger.info("Virtual doorbell wrapper initialized");
201
+ this.subscribe({ category: EventCategory.DeviceStateChanged }, (event) => {
202
+ const data = parseDeviceStateChanged(event.data);
203
+ if (!data) return;
204
+ this.handleStateChanged(data);
205
+ });
206
+ this.subscribe({ category: EventCategory.EventEmitted }, (event) => {
207
+ this.handleEmitterEvent(event.data.deviceId, event.data.eventType);
208
+ });
209
+ return [{
210
+ capability: doorbellCapability,
211
+ provider: {
212
+ getStatus: (input) => this.getStatus(input.deviceId),
213
+ getDeviceSettingsContribution: (input) => this.buildDeviceSettingsContribution(input.deviceId),
214
+ getDeviceLiveContribution: async () => null,
215
+ applyDeviceSettingsPatch: (input) => this.saveDeviceSettingsPatch(input.deviceId, input.patch)
216
+ }
217
+ }];
218
+ }
219
+ async handleStateChanged(data) {
220
+ if (data.capName !== "contact" && data.capName !== "switch") return;
221
+ await this.ensureAssignments();
222
+ const cameraIds = this.engine.onStateSlice(data.deviceId, data.capName, data.slice);
223
+ for (const cameraId of cameraIds) await this.fireDoorbell(cameraId, Date.now());
224
+ }
225
+ async handleEmitterEvent(sourceDeviceId, eventType) {
226
+ await this.ensureAssignments();
227
+ const cameraIds = this.engine.onEmitterEvent(sourceDeviceId, eventType);
228
+ for (const cameraId of cameraIds) await this.fireDoorbell(cameraId, Date.now());
229
+ }
230
+ /**
231
+ * Ring the camera's virtual doorbell: verify the wrapper is BOUND to
232
+ * the camera, bump the `doorbell` runtime-state counters through the
233
+ * canonical `deviceState.setCapSlice` write, and emit the typed
234
+ * `DoorbellOnPressed` event with the CAMERA as source — byte-shaped
235
+ * like a native firmware ring so every downstream consumer (UI
236
+ * toast, notifier rules, exporters) is agnostic to the origin.
237
+ */
238
+ async fireDoorbell(cameraId, timestamp) {
239
+ if (!await this.isWrapperBound(cameraId)) {
240
+ this.ctx.logger.debug("virtual-doorbell: trigger matched but wrapper not bound — skipping", { tags: { deviceId: cameraId } });
241
+ return;
242
+ }
243
+ try {
244
+ const next = {
245
+ lastPressedAt: timestamp,
246
+ pressCountSinceStart: parseDoorbellSlice(await this.ctx.api.deviceState.getCapSlice.query({
247
+ deviceId: cameraId,
248
+ capName: doorbellCapability.name
249
+ })).pressCountSinceStart + 1
250
+ };
251
+ await this.ctx.api.deviceState.setCapSlice.mutate({
252
+ deviceId: cameraId,
253
+ capName: doorbellCapability.name,
254
+ slice: next
255
+ });
256
+ } catch (err) {
257
+ this.ctx.logger.warn("virtual-doorbell: runtime-state update failed", {
258
+ tags: { deviceId: cameraId },
259
+ meta: { error: errMsg(err) }
260
+ });
261
+ }
262
+ this.ctx.eventBus.emit(createEvent(EventCategory.DoorbellOnPressed, {
263
+ type: "device",
264
+ id: cameraId,
265
+ deviceId: cameraId,
266
+ addonId: this.ctx.id
267
+ }, {
268
+ deviceId: cameraId,
269
+ timestamp
270
+ }));
271
+ this.ctx.logger.info("virtual-doorbell: pressed", {
272
+ tags: { deviceId: cameraId },
273
+ meta: { timestamp }
274
+ });
275
+ }
276
+ /** True when THIS wrapper is the active `doorbell` binding for the camera. */
277
+ async isWrapperBound(cameraId) {
278
+ try {
279
+ return (await this.ctx.api.deviceManager.getBindings.query({ deviceId: cameraId })).entries.some((entry) => entry.capName === doorbellCapability.name && entry.kind === "wrapped" && entry.providerAddonId === this.ctx.id);
280
+ } catch (err) {
281
+ this.ctx.logger.debug("virtual-doorbell: getBindings failed — treating as unbound", {
282
+ tags: { deviceId: cameraId },
283
+ meta: { error: errMsg(err) }
284
+ });
285
+ return false;
286
+ }
287
+ }
288
+ /**
289
+ * Lazily (re)build the camera→source assignment set from every
290
+ * camera's device store. TTL-bounded so devices/settings appearing
291
+ * after boot are picked up without a settings save; invalidated
292
+ * eagerly by `applyDeviceSettingsPatch`.
293
+ */
294
+ async ensureAssignments() {
295
+ if (Date.now() - this.assignmentsLoadedAt < ASSIGNMENTS_TTL_MS) return;
296
+ if (this.assignmentsLoading) return this.assignmentsLoading;
297
+ this.assignmentsLoading = this.loadAssignments().then(() => {
298
+ this.assignmentsLoadedAt = Date.now();
299
+ }).catch((err) => {
300
+ this.ctx.logger.warn("virtual-doorbell: assignment reload failed", { meta: { error: errMsg(err) } });
301
+ }).finally(() => {
302
+ this.assignmentsLoading = null;
303
+ });
304
+ return this.assignmentsLoading;
305
+ }
306
+ async loadAssignments() {
307
+ const cameras = (await this.ctx.api.deviceManager.listAll.query({})).filter((d) => d.type === DeviceType.Camera);
308
+ const assignments = [];
309
+ for (const camera of cameras) {
310
+ const settings = await this.readDeviceSettings(camera.id);
311
+ if (settings.sourceDeviceId === null) continue;
312
+ assignments.push({
313
+ cameraId: camera.id,
314
+ sourceDeviceId: settings.sourceDeviceId,
315
+ trigger: settings.trigger,
316
+ eventType: settings.eventType.length > 0 ? settings.eventType : null
317
+ });
318
+ }
319
+ this.engine.setAssignments(assignments);
320
+ this.ctx.logger.debug("virtual-doorbell: assignments loaded", { meta: { count: assignments.length } });
321
+ }
322
+ invalidateAssignments() {
323
+ this.assignmentsLoadedAt = 0;
324
+ }
325
+ /**
326
+ * Status from the kernel-managed `doorbell` runtime-state slice —
327
+ * the wrapper keeps NO parallel counter fields (the slice is the
328
+ * single source of truth, persisted across restarts).
329
+ */
330
+ async getStatus(deviceId) {
331
+ try {
332
+ return parseDoorbellSlice(await this.ctx.api.deviceState.getCapSlice.query({
333
+ deviceId,
334
+ capName: doorbellCapability.name
335
+ }));
336
+ } catch (err) {
337
+ this.ctx.logger.debug("virtual-doorbell: getCapSlice failed during getStatus", {
338
+ tags: { deviceId },
339
+ meta: { error: errMsg(err) }
340
+ });
341
+ return null;
342
+ }
343
+ }
344
+ async readDeviceSettings(deviceId) {
345
+ if (!this.ctx.settings) return {
346
+ sourceDeviceId: null,
347
+ trigger: DEFAULT_TRIGGER,
348
+ eventType: ""
349
+ };
350
+ const raw = await this.ctx.settings.readDeviceStore(deviceId);
351
+ const sourceRaw = raw[KEY_SOURCE];
352
+ const triggerRaw = raw[KEY_TRIGGER];
353
+ const eventTypeRaw = raw[KEY_EVENT_TYPE];
354
+ return {
355
+ sourceDeviceId: typeof sourceRaw === "number" && Number.isFinite(sourceRaw) && sourceRaw > 0 ? sourceRaw : null,
356
+ trigger: isDoorbellTrigger(triggerRaw) ? triggerRaw : DEFAULT_TRIGGER,
357
+ eventType: typeof eventTypeRaw === "string" ? eventTypeRaw : ""
358
+ };
359
+ }
360
+ /**
361
+ * Single-trip, non-throwing device lookup (mirrors snapshot's
362
+ * `lookupDeviceMeta`). Null on any failure — callers fall back to
363
+ * rendering the section (better a spurious section than a thrown
364
+ * aggregator entry).
365
+ */
366
+ async lookupDevice(deviceId) {
367
+ try {
368
+ const found = await this.ctx.api.deviceManager.getDevice.query({ deviceId });
369
+ return found ? { type: found.type } : null;
370
+ } catch (err) {
371
+ this.ctx.logger.debug("virtual-doorbell: getDevice failed during contribution", {
372
+ tags: { deviceId },
373
+ meta: { error: errMsg(err) }
374
+ });
375
+ return null;
376
+ }
377
+ }
378
+ /**
379
+ * Settings section for the device-detail page. Camera-only — the
380
+ * doorbell cap also applies to Button accessories (native providers),
381
+ * where a virtual-source picker is meaningless.
382
+ */
383
+ async buildDeviceSettingsContribution(deviceId) {
384
+ const device = await this.lookupDevice(deviceId);
385
+ if (device && device.type !== DeviceType.Camera) return null;
386
+ const current = await this.readDeviceSettings(deviceId);
387
+ return { sections: [{
388
+ id: "virtual-doorbell",
389
+ title: "Virtual Doorbell",
390
+ tab: "doorbell",
391
+ order: 70,
392
+ fields: [
393
+ {
394
+ type: "select",
395
+ key: KEY_SOURCE,
396
+ label: "Source device",
397
+ description: "Device whose state change rings this camera’s doorbell (contact, switch, sensor, event emitter).",
398
+ options: await this.listSourceDeviceOptions(),
399
+ required: true,
400
+ value: current.sourceDeviceId === null ? "none" : String(current.sourceDeviceId)
401
+ },
402
+ {
403
+ type: "select",
404
+ key: KEY_TRIGGER,
405
+ label: "Trigger",
406
+ description: "Which transition of the source device counts as a ring.",
407
+ options: DOORBELL_TRIGGERS.map((t) => ({
408
+ value: t,
409
+ label: TRIGGER_LABELS[t]
410
+ })),
411
+ required: true,
412
+ value: current.trigger
413
+ },
414
+ {
415
+ type: "text",
416
+ key: KEY_EVENT_TYPE,
417
+ label: "Event type",
418
+ description: "Only used when Trigger is 'Device event' — matched verbatim against the source device's emitted event type (e.g. press_short).",
419
+ value: current.eventType
420
+ }
421
+ ]
422
+ }] };
423
+ }
424
+ /**
425
+ * Cluster-wide picker options: every device whose BINDINGS expose one
426
+ * of the binary-ish source caps. Binding-sourced (not feature-flag
427
+ * sourced) because cap names are exactly what the runtime consumes.
428
+ */
429
+ async listSourceDeviceOptions() {
430
+ const none = {
431
+ value: "none",
432
+ label: "None"
433
+ };
434
+ try {
435
+ const [devices, allBindings] = await Promise.all([this.ctx.api.deviceManager.listAll.query({}), this.ctx.api.deviceManager.getAllBindings.query({})]);
436
+ const eligibleIds = /* @__PURE__ */ new Set();
437
+ for (const deviceBindings of allBindings) if (deviceBindings.entries.some((e) => SOURCE_CAPS.includes(e.capName))) eligibleIds.add(deviceBindings.deviceId);
438
+ return [none, ...devices.filter((d) => eligibleIds.has(d.id)).map((d) => ({
439
+ value: String(d.id),
440
+ label: `${d.name} (#${d.id})`
441
+ })).toSorted((a, b) => a.label.localeCompare(b.label))];
442
+ } catch (err) {
443
+ this.ctx.logger.warn("virtual-doorbell: source-device enumeration failed", { meta: { error: errMsg(err) } });
444
+ return [none];
445
+ }
446
+ }
447
+ async saveDeviceSettingsPatch(deviceId, patch) {
448
+ if (!this.ctx.settings) throw new Error("[virtual-doorbell] settings store unavailable — cannot persist per-device settings");
449
+ const next = { ...await this.ctx.settings.readDeviceStore(deviceId) };
450
+ if (KEY_SOURCE in patch) {
451
+ const parsed = parseSourceDeviceId(patch[KEY_SOURCE]);
452
+ if (parsed === null) delete next[KEY_SOURCE];
453
+ else next[KEY_SOURCE] = parsed;
454
+ }
455
+ if (KEY_TRIGGER in patch) {
456
+ const v = patch[KEY_TRIGGER];
457
+ if (isDoorbellTrigger(v)) next[KEY_TRIGGER] = v;
458
+ else delete next[KEY_TRIGGER];
459
+ }
460
+ if (KEY_EVENT_TYPE in patch) {
461
+ const v = patch[KEY_EVENT_TYPE];
462
+ next[KEY_EVENT_TYPE] = typeof v === "string" ? v.trim() : "";
463
+ }
464
+ await this.ctx.settings.writeDeviceStore(deviceId, next);
465
+ this.invalidateAssignments();
466
+ return { success: true };
467
+ }
468
+ };
469
+ /**
470
+ * Coerce the select value ('none' | numeric string | number) back to a
471
+ * device id. The select field surfaces string values; a stored value
472
+ * round-trips as a number.
473
+ */
474
+ function parseSourceDeviceId(value) {
475
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
476
+ if (typeof value === "string" && value !== "none" && value.trim().length > 0) {
477
+ const parsed = Number(value);
478
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
479
+ }
480
+ return null;
481
+ }
482
+ //#endregion
483
+ export { VirtualDoorbellAddon, VirtualDoorbellAddon as default };
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C3uHEBtP.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
9
9
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,4 +1,4 @@
1
- import { I as logDestinationCapability, rt as BaseAddon } from "../../dist-BWQX9yUj.mjs";
1
+ import { L as logDestinationCapability, it as BaseAddon } from "../../dist-D7_69kDt.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
4
4
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C3uHEBtP.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  node_crypto = require_chunk.__toESM(node_crypto);
9
9
  let crypto$1 = require("crypto");
@@ -5957,7 +5957,7 @@ var AuthManager = class {
5957
5957
  }
5958
5958
  signToken(payload) {
5959
5959
  const configured = this.config.get("auth.tokenExpiry");
5960
- const expiresIn = typeof configured === "string" && configured.trim() !== "" ? configured.trim() : "7d";
5960
+ const expiresIn = typeof configured === "string" && configured.trim() !== "" ? configured.trim() : "30d";
5961
5961
  return import_jsonwebtoken.sign({ ...payload }, this.jwtSecret, { expiresIn });
5962
5962
  }
5963
5963
  verifyToken(token) {
@@ -1,4 +1,4 @@
1
- import { _ as authProviderCapability, d as UserRecordSchema, et as userManagementCapability, l as ScopedTokenSchema, r as ApiKeyRecordSchema, rt as BaseAddon } from "../../dist-BWQX9yUj.mjs";
1
+ import { _ as authProviderCapability, d as UserRecordSchema, it as BaseAddon, l as ScopedTokenSchema, r as ApiKeyRecordSchema, tt as userManagementCapability } from "../../dist-D7_69kDt.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import * as crypto$1 from "node:crypto";
4
4
  import { randomUUID } from "node:crypto";
@@ -5984,7 +5984,7 @@ var AuthManager = class {
5984
5984
  }
5985
5985
  signToken(payload) {
5986
5986
  const configured = this.config.get("auth.tokenExpiry");
5987
- const expiresIn = typeof configured === "string" && configured.trim() !== "" ? configured.trim() : "7d";
5987
+ const expiresIn = typeof configured === "string" && configured.trim() !== "" ? configured.trim() : "30d";
5988
5988
  return import_jsonwebtoken.sign({ ...payload }, this.jwtSecret, { expiresIn });
5989
5989
  }
5990
5990
  verifyToken(token) {
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-C3uHEBtP.js");
3
+ const require_dist = require("../../dist-BNro5CRu.js");
4
4
  let node_os = require("node:os");
5
5
  node_os = require_chunk.__toESM(node_os);
6
6
  //#region src/builtins/local-network/local-network.addon.ts
@@ -1,4 +1,4 @@
1
- import { F as localNetworkCapability, Ot as EventCategory, rt as BaseAddon } from "../../dist-BWQX9yUj.mjs";
1
+ import { I as localNetworkCapability, it as BaseAddon, kt as EventCategory } from "../../dist-D7_69kDt.mjs";
2
2
  import * as os from "node:os";
3
3
  //#region src/builtins/local-network/local-network.addon.ts
4
4
  /**
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C3uHEBtP.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_chunk.__toESM(node_fs);
9
9
  let node_child_process = require("node:child_process");
@@ -1,4 +1,4 @@
1
- import { Ot as EventCategory, R as metricsProviderCapability, gt as createEvent, rt as BaseAddon } from "../../dist-BWQX9yUj.mjs";
1
+ import { _t as createEvent, it as BaseAddon, kt as EventCategory, z as metricsProviderCapability } from "../../dist-D7_69kDt.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import { execFile, execFileSync } from "node:child_process";
4
4
  import { promisify } from "node:util";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C3uHEBtP.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_chunk.__toESM(node_fs);
9
9
  let node_child_process = require("node:child_process");
@@ -1,4 +1,4 @@
1
- import { K as scoreRuntimes, Ot as EventCategory, U as platformProbeCapability, nt as errMsg, rt as BaseAddon, vt as emitReadiness } from "../../dist-BWQX9yUj.mjs";
1
+ import { W as platformProbeCapability, it as BaseAddon, kt as EventCategory, q as scoreRuntimes, rt as errMsg, yt as emitReadiness } from "../../dist-D7_69kDt.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import { execFile } from "node:child_process";
4
4
  import { promisify } from "node:util";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C3uHEBtP.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
8
8
  /**
9
9
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -1,4 +1,4 @@
1
- import { Ot as EventCategory, rt as BaseAddon } from "../../dist-BWQX9yUj.mjs";
1
+ import { it as BaseAddon, kt as EventCategory } from "../../dist-D7_69kDt.mjs";
2
2
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
3
3
  /**
4
4
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C3uHEBtP.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  let node_child_process = require("node:child_process");
8
8
  //#region src/builtins/snapshot/snapshot-coalescing.ts
9
9
  /**
@@ -1,4 +1,4 @@
1
- import { Q as streamQualityLabel, Y as snapshotCapability, lt as DeviceType, nt as errMsg, rt as BaseAddon, st as DeviceFeature, z as nodePin } from "../../dist-BWQX9yUj.mjs";
1
+ import { $ as streamQualityLabel, B as nodePin, X as snapshotCapability, ct as DeviceFeature, it as BaseAddon, rt as errMsg, ut as DeviceType } from "../../dist-D7_69kDt.mjs";
2
2
  import { execFile } from "node:child_process";
3
3
  //#region src/builtins/snapshot/snapshot-coalescing.ts
4
4
  /**
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C3uHEBtP.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_chunk.__toESM(node_fs);
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { D as filesystemBrowseCapability, Z as storageProviderCapability, rt as BaseAddon } from "../../dist-BWQX9yUj.mjs";
1
+ import { O as filesystemBrowseCapability, Q as storageProviderCapability, it as BaseAddon } from "../../dist-D7_69kDt.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import * as path$1 from "node:path";
4
4
  import { basename, dirname, join, resolve, sep } from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C3uHEBtP.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let better_sqlite3 = require("better-sqlite3");
9
9
  better_sqlite3 = require_chunk.__toESM(better_sqlite3);
@@ -510,10 +510,18 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
510
510
  if (existingCols.length > 0) {
511
511
  const existingNames = new Set(existingCols.map((c) => c.name));
512
512
  const declaredNames = new Set(schema.columns.map((c) => c.name));
513
- const missingDeclared = schema.columns.some((c) => !existingNames.has(c.name));
514
- if (existingCols.length <= 2 && existingNames.has("data") && !declaredNames.has("data") || missingDeclared) try {
513
+ const missingCols = schema.columns.filter((c) => !existingNames.has(c.name));
514
+ const isLegacyKv = existingCols.length <= 2 && existingNames.has("data") && !declaredNames.has("data");
515
+ const addableInPlace = (col) => !col.primaryKey && !col.unique && (!col.notNull || col.defaultValue !== void 0);
516
+ if (isLegacyKv || missingCols.some((c) => !addableInPlace(c))) try {
515
517
  this.getDb().exec(`DROP TABLE "${table}"`);
516
518
  } catch {}
519
+ else if (missingCols.length > 0) for (const col of missingCols) {
520
+ const parts = [`"${col.name}" ${col.type}`];
521
+ if (col.notNull) parts.push("NOT NULL");
522
+ if (col.defaultValue !== void 0) parts.push(`DEFAULT ${typeof col.defaultValue === "string" ? `'${col.defaultValue}'` : col.defaultValue === null ? "NULL" : String(col.defaultValue)}`);
523
+ this.getDb().exec(`ALTER TABLE "${table}" ADD COLUMN ${parts.join(" ")}`);
524
+ }
517
525
  }
518
526
  this.getDb().exec(`CREATE TABLE IF NOT EXISTS "${table}" (${colDefs.join(", ")})`);
519
527
  if (schema.indexes) for (const idx of schema.indexes) {