@camstack/system 1.1.50 → 1.1.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon-runner.js +2 -2
- package/dist/addon-runner.mjs +2 -2
- package/dist/addon-utils.js +1 -1
- package/dist/addon-utils.mjs +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/device-manager/device-linked-devices.d.ts +47 -0
- package/dist/builtins/device-manager/device-manager.addon.js +1840 -1712
- package/dist/builtins/device-manager/device-manager.addon.mjs +1840 -1712
- package/dist/builtins/doorbell/index.d.ts +2 -0
- package/dist/builtins/doorbell/index.js +8 -0
- package/dist/builtins/doorbell/index.mjs +2 -0
- package/dist/builtins/doorbell/trigger-engine.d.ts +69 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +87 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +489 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +483 -0
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +2 -2
- package/dist/builtins/local-auth/local-auth.addon.mjs +2 -2
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +162 -12
- package/dist/builtins/snapshot/index.mjs +162 -12
- package/dist/builtins/snapshot/snapshot-media-handler.d.ts +38 -0
- package/dist/builtins/snapshot/snapshot.addon.d.ts +26 -0
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +4 -4
- package/dist/builtins/system-config/system-config.addon.mjs +4 -4
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-CC3E-_3h.mjs → dist-B_PSvr-m.mjs} +237 -13
- package/dist/{dist-CQYo6Zp8.js → dist-BduBhv6N.js} +242 -12
- package/dist/http/file-data-plane.d.ts +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +35 -23
- package/dist/index.mjs +35 -24
- package/dist/kernel/addon-installer.d.ts +8 -0
- package/dist/kernel/deps/manifest-native-deps.d.ts +1 -1
- package/dist/kernel/deps/npm-command.d.ts +38 -0
- package/dist/{manifest-python-deps-DqATCsK0.mjs → manifest-python-deps-CIyvuHAB.mjs} +268 -124
- package/dist/{manifest-python-deps-Wy8dNJpr.js → manifest-python-deps-CPAMH5gV.js} +279 -123
- package/dist/{model-download-service-C-IHWnXx.mjs → model-download-service-Cp9f4dk6.mjs} +1 -1
- package/dist/{model-download-service-D-Umz4-L.js → model-download-service-hf0ookyy.js} +1 -1
- 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-B_PSvr-m.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-
|
|
6
|
+
const require_dist = require("../../dist-BduBhv6N.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 {
|
|
1
|
+
import { L as logDestinationCapability, it as BaseAddon } from "../../dist-B_PSvr-m.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-
|
|
6
|
+
const require_dist = require("../../dist-BduBhv6N.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() : "
|
|
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,
|
|
1
|
+
import { _ as authProviderCapability, d as UserRecordSchema, it as BaseAddon, l as ScopedTokenSchema, r as ApiKeyRecordSchema, tt as userManagementCapability } from "../../dist-B_PSvr-m.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() : "
|
|
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-
|
|
3
|
+
const require_dist = require("../../dist-BduBhv6N.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 {
|
|
1
|
+
import { I as localNetworkCapability, it as BaseAddon, kt as EventCategory } from "../../dist-B_PSvr-m.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-
|
|
6
|
+
const require_dist = require("../../dist-BduBhv6N.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 {
|
|
1
|
+
import { _t as createEvent, it as BaseAddon, kt as EventCategory, z as metricsProviderCapability } from "../../dist-B_PSvr-m.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-
|
|
6
|
+
const require_dist = require("../../dist-BduBhv6N.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 {
|
|
1
|
+
import { W as platformProbeCapability, it as BaseAddon, kt as EventCategory, q as scoreRuntimes, rt as errMsg, yt as emitReadiness } from "../../dist-B_PSvr-m.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-
|
|
6
|
+
const require_dist = require("../../dist-BduBhv6N.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 {
|
|
1
|
+
import { it as BaseAddon, kt as EventCategory } from "../../dist-B_PSvr-m.mjs";
|
|
2
2
|
//#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
|
|
3
3
|
/**
|
|
4
4
|
* Reconcile the durable `enabledProviders` set against authoritative
|