@camera.ui/sdk 0.0.1

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 (43) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/CONTRIBUTING.md +1 -0
  3. package/LICENSE.md +22 -0
  4. package/README.md +5 -0
  5. package/dist/camera/config.js +1 -0
  6. package/dist/camera/detection.js +1 -0
  7. package/dist/camera/device.js +1 -0
  8. package/dist/camera/index.js +5 -0
  9. package/dist/camera/streaming.js +1 -0
  10. package/dist/camera/types.js +1 -0
  11. package/dist/external.js +1 -0
  12. package/dist/index.d.ts +5042 -0
  13. package/dist/index.js +16 -0
  14. package/dist/manager/index.js +1 -0
  15. package/dist/manager/types.js +1 -0
  16. package/dist/plugin/contract.js +120 -0
  17. package/dist/plugin/index.js +3 -0
  18. package/dist/plugin/interfaces.js +1 -0
  19. package/dist/plugin/types.js +1 -0
  20. package/dist/sensor/audio.js +118 -0
  21. package/dist/sensor/base.js +460 -0
  22. package/dist/sensor/battery.js +101 -0
  23. package/dist/sensor/contact.js +33 -0
  24. package/dist/sensor/doorbell.js +63 -0
  25. package/dist/sensor/face.js +124 -0
  26. package/dist/sensor/guards.js +133 -0
  27. package/dist/sensor/index.js +19 -0
  28. package/dist/sensor/licensePlate.js +116 -0
  29. package/dist/sensor/light.js +66 -0
  30. package/dist/sensor/motion.js +103 -0
  31. package/dist/sensor/object.js +137 -0
  32. package/dist/sensor/ptz.js +159 -0
  33. package/dist/sensor/siren.js +73 -0
  34. package/dist/sensor/types.js +46 -0
  35. package/dist/service/base.js +96 -0
  36. package/dist/service/index.js +3 -0
  37. package/dist/service/services.js +1 -0
  38. package/dist/service/types.js +1 -0
  39. package/dist/storage/index.js +2 -0
  40. package/dist/storage/schema.js +1 -0
  41. package/dist/storage/storages.js +1 -0
  42. package/package.json +61 -0
  43. package/tsconfig.node.json +30 -0
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Abstract Sensor Base Class
3
+ *
4
+ * Properties are intercepted via Proxy and automatically synchronized via RPC.
5
+ * Plugin developers can simply write `sensor.detected = true` and the update
6
+ * is automatically propagated.
7
+ *
8
+ * @template TProperties - The properties interface for this sensor
9
+ * @template TStorage - The storage values interface for this sensor
10
+ * @template TCapability - The capability enum type for this sensor (default: string)
11
+ */
12
+ export class Sensor {
13
+ /** Unique sensor ID */
14
+ id;
15
+ /** Camera ID this sensor belongs to (set by addSensor) */
16
+ _cameraId;
17
+ /** Plugin ID that provides this sensor (set by addSensor) */
18
+ _pluginId;
19
+ /** Whether this sensor is online/available */
20
+ _online = true;
21
+ /** Internal properties storage */
22
+ _propertiesStore;
23
+ /** Proxied properties for intercepting writes */
24
+ _propertiesProxy;
25
+ /** Update function for RPC propagation */
26
+ _updateFn;
27
+ /** Property change listeners */
28
+ _listeners = new Set();
29
+ /** Simple property change listeners (for SensorLike interface) */
30
+ _simpleListeners = new Set();
31
+ /** Capability change listeners (for SensorLike interface) */
32
+ _capabilityListeners = new Set();
33
+ /** Storage instance for sensor-type-specific configuration (set by system) */
34
+ _storage;
35
+ /** Sensor capabilities */
36
+ _capabilities = [];
37
+ /** Capability update function for RPC propagation */
38
+ _capabilitiesUpdateFn;
39
+ /** Whether this sensor is assigned to be the active provider for its type */
40
+ _isAssigned = false;
41
+ /** Assignment change listeners */
42
+ _assignmentListeners = new Set();
43
+ /**
44
+ * Get schemas for sensor-type-specific configuration
45
+ *
46
+ * @returns An array of JsonSchema objects for configuration
47
+ */
48
+ schema;
49
+ /**
50
+ * Display name for UI, HomeKit, logs etc.
51
+ * Initially equals name, can be changed by user via UI.
52
+ * Persisted in sensor storage under '_displayName'.
53
+ */
54
+ displayName = '';
55
+ /**
56
+ * Whether this sensor requires video frames for detection.
57
+ * - true: Frame-based detection (e.g., rust-motion, ML object detection)
58
+ * - false: External detection (e.g., Ring, ONVIF events, SMTP)
59
+ *
60
+ * Override in subclass to specify behavior.
61
+ * DetectorSensor subclasses set this to true.
62
+ */
63
+ _requiresFrames;
64
+ /**
65
+ * Create a new sensor instance.
66
+ *
67
+ * Note: The id is a random UUID used for RPC namespaces.
68
+ * The storage key is based on the stable 'name' property.
69
+ */
70
+ constructor() {
71
+ this.id = crypto.randomUUID();
72
+ // Initialize empty storage
73
+ this._propertiesStore = {};
74
+ // Create Proxy for property interception
75
+ this._propertiesProxy = new Proxy(this._propertiesStore, {
76
+ set: (target, prop, value) => {
77
+ const key = prop;
78
+ const previousValue = target[key];
79
+ // Only update if value changed
80
+ if (previousValue !== value) {
81
+ target[key] = value;
82
+ // Fire-and-forget RPC update
83
+ this._updateFn?.(prop, value);
84
+ // Notify local listeners (prop is always a valid property enum value)
85
+ this._notifyListeners(prop, value, previousValue);
86
+ }
87
+ return true;
88
+ },
89
+ get: (target, prop) => target[prop],
90
+ });
91
+ }
92
+ /**
93
+ * Initialize sensor with update function (called by backend)
94
+ *
95
+ * @param updateFn - The property update function
96
+ *
97
+ * @internal
98
+ */
99
+ _init(updateFn) {
100
+ this._updateFn = updateFn;
101
+ }
102
+ /**
103
+ * Set property value from external source (e.g., RPC)
104
+ * This bypasses the proxy to avoid re-broadcasting
105
+ *
106
+ * @param property - The property name
107
+ *
108
+ * @param value - The new property value
109
+ *
110
+ * @internal
111
+ */
112
+ _setPropertyInternal(property, value) {
113
+ const previousValue = this._propertiesStore[property];
114
+ if (previousValue !== value) {
115
+ this._propertiesStore[property] = value;
116
+ // property is always a valid property enum value (K extends keyof TProperties)
117
+ this._notifyListeners(property, value, previousValue);
118
+ }
119
+ }
120
+ /**
121
+ * Get all current property values
122
+ *
123
+ * @internal
124
+ *
125
+ * @returns The current properties
126
+ */
127
+ _getProperties() {
128
+ return { ...this._propertiesStore };
129
+ }
130
+ /**
131
+ * Access to proxied properties for derived classes
132
+ */
133
+ get props() {
134
+ return this._propertiesProxy;
135
+ }
136
+ /**
137
+ * Read-only access to raw properties (no proxy)
138
+ */
139
+ get rawProps() {
140
+ return this._propertiesStore;
141
+ }
142
+ /**
143
+ * Whether this sensor is online/available
144
+ */
145
+ get online() {
146
+ return this._online;
147
+ }
148
+ /**
149
+ * Set online state
150
+ *
151
+ * @param online - The new online state
152
+ *
153
+ * @internal
154
+ */
155
+ _setOnline(online) {
156
+ this._online = online;
157
+ }
158
+ /**
159
+ * Camera ID this sensor belongs to
160
+ * Set automatically by CameraDevice.addSensor()
161
+ */
162
+ get cameraId() {
163
+ if (!this._cameraId) {
164
+ throw new Error('Sensor not attached to camera. Call camera.addSensor() first.');
165
+ }
166
+ return this._cameraId;
167
+ }
168
+ /**
169
+ * Set camera ID (called by CameraDeviceProxy.addSensor)
170
+ *
171
+ * @param cameraId - The camera ID
172
+ *
173
+ * @internal
174
+ */
175
+ _setCameraId(cameraId) {
176
+ this._cameraId = cameraId;
177
+ }
178
+ /**
179
+ * Plugin ID that provides this sensor
180
+ * Set automatically by CameraDevice.addSensor()
181
+ */
182
+ get pluginId() {
183
+ return this._pluginId;
184
+ }
185
+ /**
186
+ * Set plugin ID (called by CameraDeviceProxy.addSensor)
187
+ *
188
+ * @param pluginId - The plugin ID
189
+ *
190
+ * @internal
191
+ */
192
+ _setPluginId(pluginId) {
193
+ this._pluginId = pluginId;
194
+ }
195
+ /**
196
+ * Storage for sensor-type-specific configuration
197
+ * Available after sensor is added via CameraDevice.addSensor()
198
+ * Only available if schemas returns non-empty array
199
+ */
200
+ get storage() {
201
+ return this._storage;
202
+ }
203
+ /**
204
+ * Set storage instance (called by CameraDeviceProxy after addSensor)
205
+ *
206
+ * @param storage - The storage instance
207
+ *
208
+ * @internal
209
+ */
210
+ _setStorage(storage) {
211
+ this._storage = storage;
212
+ }
213
+ /**
214
+ * Get sensor capabilities
215
+ * Returns a type-safe array of capabilities for this sensor type.
216
+ */
217
+ get capabilities() {
218
+ return this._capabilities;
219
+ }
220
+ /**
221
+ * Set sensor capabilities
222
+ * Updates the capabilities and broadcasts to consumers via RPC.
223
+ * Capabilities are automatically deduplicated.
224
+ *
225
+ * @param value - The new capabilities array
226
+ */
227
+ set capabilities(value) {
228
+ // Deduplicate capabilities
229
+ this._capabilities = [...new Set(value)];
230
+ // Broadcast to SensorController (for RPC propagation)
231
+ this._capabilitiesUpdateFn?.(this._capabilities);
232
+ // Notify local listeners
233
+ this._notifyCapabilityListeners();
234
+ }
235
+ /**
236
+ * Check if sensor has a specific capability
237
+ *
238
+ * @param capability - The capability to check
239
+ *
240
+ * @returns True if the sensor has the capability
241
+ */
242
+ hasCapability(capability) {
243
+ return this._capabilities.includes(capability);
244
+ }
245
+ /**
246
+ * Initialize capabilities update function (called by backend)
247
+ *
248
+ * @param updateFn - The capability update function
249
+ *
250
+ * @internal
251
+ */
252
+ _initCapabilities(updateFn) {
253
+ this._capabilitiesUpdateFn = updateFn;
254
+ }
255
+ // ========== Assignment Status ==========
256
+ /**
257
+ * Whether this sensor is assigned to be the active provider
258
+ * for its sensor type on this camera.
259
+ *
260
+ * A sensor is "assigned" when the user has selected this plugin
261
+ * to provide this sensor type for the camera. Unassigned sensors
262
+ * are still registered but their events are filtered out.
263
+ *
264
+ * Use this to conditionally initialize resources:
265
+ * - Only start ONVIF event subscriptions if assigned
266
+ * - Only connect to external services if needed
267
+ */
268
+ get isAssigned() {
269
+ return this._isAssigned;
270
+ }
271
+ /**
272
+ * Subscribe to assignment changes.
273
+ *
274
+ * Called when the user changes which plugin provides this sensor type.
275
+ * Use this to react dynamically:
276
+ * - Start/stop event loops
277
+ * - Connect/disconnect from external services
278
+ *
279
+ * @param callback - Called with true when assigned, false when unassigned
280
+ *
281
+ * @returns A function to unsubscribe
282
+ *
283
+ * @example
284
+ * ```typescript
285
+ * sensor.onAssignmentChanged((assigned) => {
286
+ * if (assigned) {
287
+ * this.startEventLoop();
288
+ * } else {
289
+ * this.stopEventLoop();
290
+ * }
291
+ * });
292
+ * ```
293
+ */
294
+ onAssignmentChanged(callback) {
295
+ this._assignmentListeners.add(callback);
296
+ return () => this._assignmentListeners.delete(callback);
297
+ }
298
+ /**
299
+ * Set assignment status (called by system)
300
+ *
301
+ * @param assigned - Whether this sensor is now assigned
302
+ *
303
+ * @internal
304
+ */
305
+ _setAssigned(assigned) {
306
+ if (this._isAssigned !== assigned) {
307
+ this._isAssigned = assigned;
308
+ for (const listener of this._assignmentListeners) {
309
+ try {
310
+ listener(assigned);
311
+ }
312
+ catch {
313
+ // Ignore listener errors
314
+ }
315
+ }
316
+ }
317
+ }
318
+ // ========== SensorLike Interface Methods ==========
319
+ /**
320
+ * Get a property value by name (SensorLike interface)
321
+ *
322
+ * @param property - The property name
323
+ *
324
+ * @returns The property value or undefined if not found
325
+ */
326
+ getPropertyValue(property) {
327
+ return this._propertiesStore[property];
328
+ }
329
+ /**
330
+ * Get all property values (SensorLike interface)
331
+ *
332
+ * @returns All property values as a record
333
+ */
334
+ getAllPropertyValues() {
335
+ return { ...this._propertiesStore };
336
+ }
337
+ /**
338
+ * Set a property value (SensorLike interface)
339
+ * For Control sensors, this triggers the property setter through the proxy.
340
+ *
341
+ * @param property - The property name
342
+ *
343
+ * @param value - The new property value
344
+ */
345
+ async setPropertyValue(property, value) {
346
+ this._propertiesProxy[property] = value;
347
+ }
348
+ /**
349
+ * Add a simple property change listener (SensorLike interface)
350
+ * This is for consumers who just want property/value pairs.
351
+ *
352
+ * @param callback - The callback function to invoke on property changes
353
+ *
354
+ * @returns A function to remove the listener
355
+ */
356
+ onPropertyChanged(callback) {
357
+ this._simpleListeners.add(callback);
358
+ return () => this._simpleListeners.delete(callback);
359
+ }
360
+ /**
361
+ * Add a detailed property change listener
362
+ * This includes full event details like previous value, timestamps, etc.
363
+ *
364
+ * @param callback - The callback function to invoke on detailed property changes
365
+ *
366
+ * @returns A function to remove the listener
367
+ */
368
+ onPropertyChangedDetailed(callback) {
369
+ this._listeners.add(callback);
370
+ return () => this._listeners.delete(callback);
371
+ }
372
+ /**
373
+ * Add a capability change listener (SensorLike interface)
374
+ * Called when the sensor's capabilities are updated at runtime.
375
+ *
376
+ * @param callback - The callback function to invoke on capability changes
377
+ *
378
+ * @returns A function to remove the listener
379
+ */
380
+ onCapabilitiesChanged(callback) {
381
+ this._capabilityListeners.add(callback);
382
+ return () => this._capabilityListeners.delete(callback);
383
+ }
384
+ /**
385
+ * Notify all capability listeners of a capability change
386
+ */
387
+ _notifyCapabilityListeners() {
388
+ for (const listener of this._capabilityListeners) {
389
+ try {
390
+ listener(this._capabilities);
391
+ }
392
+ catch {
393
+ // Ignore listener errors
394
+ }
395
+ }
396
+ }
397
+ /**
398
+ * Notify all listeners of a property change
399
+ *
400
+ * @param property - The property name (must be a valid SensorPropertyType)
401
+ *
402
+ * @param value - The new property value
403
+ *
404
+ * @param previousValue - The previous property value
405
+ */
406
+ _notifyListeners(property, value, previousValue) {
407
+ // Skip notification if sensor not yet attached to camera
408
+ // (e.g., during constructor initialization)
409
+ if (!this._cameraId) {
410
+ return;
411
+ }
412
+ const event = {
413
+ cameraId: this._cameraId,
414
+ sensorId: this.id,
415
+ sensorType: this.type,
416
+ property,
417
+ value,
418
+ previousValue,
419
+ timestamp: Date.now(),
420
+ };
421
+ // Notify detailed listeners
422
+ for (const listener of this._listeners) {
423
+ try {
424
+ listener(event);
425
+ }
426
+ catch {
427
+ // Ignore listener errors
428
+ }
429
+ }
430
+ // Notify simple listeners
431
+ for (const listener of this._simpleListeners) {
432
+ try {
433
+ listener(property, value);
434
+ }
435
+ catch {
436
+ // Ignore listener errors
437
+ }
438
+ }
439
+ }
440
+ /**
441
+ * Serialize sensor for transport
442
+ *
443
+ * @returns The serialized sensor representation
444
+ */
445
+ toJSON() {
446
+ return {
447
+ id: this.id,
448
+ type: this.type,
449
+ name: this.name,
450
+ displayName: this.displayName || this.name,
451
+ category: this.category,
452
+ cameraId: this.cameraId,
453
+ pluginId: this.pluginId,
454
+ online: this._online,
455
+ properties: this._getProperties(),
456
+ capabilities: this._capabilities,
457
+ requiresFrames: this._requiresFrames,
458
+ };
459
+ }
460
+ }
@@ -0,0 +1,101 @@
1
+ import { Sensor } from './base.js';
2
+ import { ChargingState, SensorCategory, SensorType } from './types.js';
3
+ /**
4
+ * Battery Capability enum
5
+ * Describes what Battery features this sensor supports
6
+ * Note: "level" property is always available (standard)
7
+ */
8
+ export var BatteryCapability;
9
+ (function (BatteryCapability) {
10
+ /** Supports low battery detection */
11
+ BatteryCapability["LowBattery"] = "lowBattery";
12
+ /** Supports charging state detection */
13
+ BatteryCapability["Charging"] = "charging";
14
+ })(BatteryCapability || (BatteryCapability = {}));
15
+ /**
16
+ * Battery info property keys
17
+ */
18
+ export var BatteryProperty;
19
+ (function (BatteryProperty) {
20
+ BatteryProperty["Level"] = "level";
21
+ BatteryProperty["Charging"] = "charging";
22
+ BatteryProperty["Low"] = "low";
23
+ })(BatteryProperty || (BatteryProperty = {}));
24
+ /**
25
+ * Battery Info
26
+ *
27
+ * Read-only hardware status for battery-powered cameras.
28
+ */
29
+ export class BatteryInfo extends Sensor {
30
+ type = SensorType.Battery;
31
+ category = SensorCategory.Info;
32
+ name;
33
+ /** Threshold for low battery warning (default: 20%) */
34
+ lowBatteryThreshold = 20;
35
+ constructor(name = 'Battery') {
36
+ super();
37
+ this.name = name;
38
+ // Initialize defaults
39
+ this.props.level = 100;
40
+ this.props.charging = ChargingState.NotCharging;
41
+ this.props.low = false;
42
+ }
43
+ // ========== Properties (Getter/Setter Pairs) ==========
44
+ /** Battery level (0-100) */
45
+ get level() {
46
+ return this.rawProps.level;
47
+ }
48
+ /** Set battery level (0-100) */
49
+ set level(value) {
50
+ const clampedValue = Math.max(0, Math.min(100, value));
51
+ this.props.level = clampedValue;
52
+ // Auto-update low battery state
53
+ this.props.low = clampedValue <= this.lowBatteryThreshold;
54
+ }
55
+ /** Charging state */
56
+ get charging() {
57
+ return this.rawProps.charging;
58
+ }
59
+ /** Set charging state */
60
+ set charging(value) {
61
+ this.props.charging = value;
62
+ }
63
+ /** Whether battery is low */
64
+ get low() {
65
+ return this.rawProps.low;
66
+ }
67
+ /** Set low battery state */
68
+ set low(value) {
69
+ this.props.low = value;
70
+ }
71
+ // ========== Convenience Methods ==========
72
+ /**
73
+ * Update battery state
74
+ *
75
+ * @param level - Battery level (0-100)
76
+ *
77
+ * @param charging - Charging state
78
+ */
79
+ setBattery(level, charging) {
80
+ this.level = level;
81
+ if (charging !== undefined) {
82
+ this.props.charging = charging;
83
+ }
84
+ }
85
+ /**
86
+ * Check if battery is charging
87
+ *
88
+ * @returns True if battery is charging, false otherwise
89
+ */
90
+ isCharging() {
91
+ return this.charging === ChargingState.Charging;
92
+ }
93
+ /**
94
+ * Check if battery is fully charged
95
+ *
96
+ * @returns True if battery is fully charged, false otherwise
97
+ */
98
+ isFullyCharged() {
99
+ return this.charging === ChargingState.Full || this.level === 100;
100
+ }
101
+ }
@@ -0,0 +1,33 @@
1
+ import { Sensor } from './base.js';
2
+ import { SensorCategory, SensorType } from './types.js';
3
+ /**
4
+ * Contact sensor property keys
5
+ */
6
+ export var ContactProperty;
7
+ (function (ContactProperty) {
8
+ ContactProperty["Detected"] = "detected";
9
+ })(ContactProperty || (ContactProperty = {}));
10
+ /**
11
+ * Contact Sensor
12
+ *
13
+ * Detects contact state (open/closed) for doors, windows, etc.
14
+ */
15
+ export class ContactSensor extends Sensor {
16
+ type = SensorType.Contact;
17
+ category = SensorCategory.Sensor;
18
+ name;
19
+ constructor(name = 'Contact Sensor') {
20
+ super();
21
+ this.name = name;
22
+ // Initialize defaults
23
+ this.props.detected = false;
24
+ }
25
+ /** Whether contact is detected (closed) */
26
+ get detected() {
27
+ return this.rawProps.detected;
28
+ }
29
+ /** Set contact detected state */
30
+ set detected(value) {
31
+ this.props.detected = value;
32
+ }
33
+ }
@@ -0,0 +1,63 @@
1
+ import { Sensor } from './base.js';
2
+ import { SensorCategory, SensorType } from './types.js';
3
+ /**
4
+ * Doorbell trigger property keys
5
+ */
6
+ export var DoorbellProperty;
7
+ (function (DoorbellProperty) {
8
+ DoorbellProperty["Ring"] = "ring";
9
+ })(DoorbellProperty || (DoorbellProperty = {}));
10
+ /**
11
+ * Doorbell Trigger
12
+ *
13
+ * Event-based trigger for doorbell rings.
14
+ * Ring property auto-resets after a short delay.
15
+ */
16
+ export class DoorbellTrigger extends Sensor {
17
+ type = SensorType.Doorbell;
18
+ category = SensorCategory.Trigger;
19
+ name;
20
+ _resetTimeout;
21
+ constructor(name = 'Doorbell') {
22
+ super();
23
+ this.name = name;
24
+ // Initialize defaults
25
+ this.props.ring = false;
26
+ }
27
+ // ========== Properties (Getter/Setter Pairs) ==========
28
+ /** Whether doorbell is currently ringing */
29
+ get ring() {
30
+ return this.rawProps.ring;
31
+ }
32
+ /** Set ring state */
33
+ set ring(value) {
34
+ this.props.ring = value;
35
+ }
36
+ // ========== Convenience Methods ==========
37
+ /**
38
+ * Trigger doorbell ring
39
+ * Auto-resets after duration (default: 1000ms)
40
+ *
41
+ * @param resetAfterMs - Duration in milliseconds to auto-reset ring state
42
+ */
43
+ triggerRing(resetAfterMs = 1000) {
44
+ // Clear any existing reset timeout
45
+ if (this._resetTimeout) {
46
+ clearTimeout(this._resetTimeout);
47
+ }
48
+ this.props.ring = true;
49
+ // Auto-reset after duration
50
+ this._resetTimeout = setTimeout(() => {
51
+ this.props.ring = false;
52
+ this._resetTimeout = undefined;
53
+ }, resetAfterMs);
54
+ }
55
+ /** Reset ring state immediately */
56
+ resetRing() {
57
+ if (this._resetTimeout) {
58
+ clearTimeout(this._resetTimeout);
59
+ this._resetTimeout = undefined;
60
+ }
61
+ this.props.ring = false;
62
+ }
63
+ }