@camera.ui/sdk 0.0.2 → 0.0.4
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/LICENSE.md +1 -1
- package/README.md +3 -3
- package/dist/camera/events.js +2 -0
- package/dist/camera/index.js +3 -1
- package/dist/external.js +7 -0
- package/dist/index.d.ts +7132 -4248
- package/dist/index.js +3 -11
- package/dist/internal/contract-validators.js +21 -0
- package/dist/internal/index.d.ts +915 -0
- package/dist/internal/index.js +9 -0
- package/dist/internal/sensor-triggers.js +2 -0
- package/dist/internal/shared-utils.js +86 -0
- package/dist/internal/streaming-internal.js +1 -0
- package/dist/manager/index.js +1 -1
- package/dist/observable/index.js +419 -0
- package/dist/plugin/api.js +21 -0
- package/dist/plugin/contract.js +101 -114
- package/dist/plugin/helper.js +277 -0
- package/dist/plugin/index.js +4 -1
- package/dist/plugin/interfaces.js +51 -1
- package/dist/plugin/notifier.js +23 -0
- package/dist/plugin/oauth.js +1 -0
- package/dist/sensor/audio.js +103 -81
- package/dist/sensor/base.js +350 -318
- package/dist/sensor/battery.js +73 -59
- package/dist/sensor/classifier.js +117 -0
- package/dist/sensor/clip.js +30 -0
- package/dist/sensor/contact.js +37 -18
- package/dist/sensor/detection.js +4 -0
- package/dist/sensor/doorbell.js +52 -38
- package/dist/sensor/face.js +71 -86
- package/dist/sensor/garage.js +121 -0
- package/dist/sensor/humidity.js +52 -0
- package/dist/sensor/index.js +17 -11
- package/dist/sensor/leak.js +52 -0
- package/dist/sensor/licensePlate.js +70 -79
- package/dist/sensor/light.js +82 -38
- package/dist/sensor/lock.js +99 -0
- package/dist/sensor/motion.js +85 -70
- package/dist/sensor/object.js +73 -94
- package/dist/sensor/occupancy.js +52 -0
- package/dist/sensor/ptz.js +114 -100
- package/dist/sensor/securitySystem.js +98 -0
- package/dist/sensor/siren.js +75 -43
- package/dist/sensor/smoke.js +52 -0
- package/dist/sensor/spec.js +1 -0
- package/dist/sensor/switch.js +72 -0
- package/dist/sensor/temperature.js +52 -0
- package/dist/storage/index.js +1 -2
- package/dist/types.js +1 -0
- package/docs/.vitepress/config.ts +77 -0
- package/docs/.vitepress/theme/index.ts +5 -0
- package/docs/.vitepress/theme/style.css +117 -0
- package/docs/index.md +16 -0
- package/docs/logo.png +0 -0
- package/docs/public/apple-touch-icon.png +0 -0
- package/docs/public/favicon-16.ico +0 -0
- package/docs/public/favicon.ico +0 -0
- package/docs/public/logo.svg +1 -0
- package/examples/README.md +7 -0
- package/examples/getting-started.md +535 -0
- package/package.json +36 -23
- package/scripts/build-example-docs.mjs +62 -0
- package/tsconfig.node.json +3 -2
- package/typedoc.json +42 -0
- package/dist/sensor/guards.js +0 -133
- package/dist/sensor/types.js +0 -46
- package/dist/service/base.js +0 -96
- package/dist/service/index.js +0 -3
- /package/dist/camera/{types.js → enums.js} +0 -0
- /package/dist/{manager/types.js → camera/frames.js} +0 -0
- /package/dist/{plugin/types.js → internal/camera-config-internal.js} +0 -0
- /package/dist/{service/services.js → internal/camera-enums.js} +0 -0
- /package/dist/{service/types.js → internal/camera-wire.js} +0 -0
- /package/dist/{storage/schema.js → internal/manager-rpc.js} +0 -0
- /package/dist/{storage/storages.js → internal/sensor-rpc.js} +0 -0
package/dist/sensor/base.js
CHANGED
|
@@ -1,408 +1,467 @@
|
|
|
1
|
+
import { Subject } from '../observable/index.js';
|
|
2
|
+
import { isEqual } from '../internal/shared-utils.js';
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
4
|
+
* Type of sensor. Each maps to a smart-home concept (HomeKit service).
|
|
5
|
+
* Plugins create sensors of these types and attach them to cameras.
|
|
6
|
+
*/
|
|
7
|
+
export var SensorType;
|
|
8
|
+
(function (SensorType) {
|
|
9
|
+
// Detection Sensors — analyze frames and report detections
|
|
10
|
+
/** Video-based motion detection */
|
|
11
|
+
SensorType["Motion"] = "motion";
|
|
12
|
+
/** Object detection (person, vehicle, animal, etc.) */
|
|
13
|
+
SensorType["Object"] = "object";
|
|
14
|
+
/** Audio event detection (glass break, scream, etc.) */
|
|
15
|
+
SensorType["Audio"] = "audio";
|
|
16
|
+
/** Face detection and recognition */
|
|
17
|
+
SensorType["Face"] = "face";
|
|
18
|
+
/** License plate detection and OCR */
|
|
19
|
+
SensorType["LicensePlate"] = "licensePlate";
|
|
20
|
+
/** General-purpose image classifier */
|
|
21
|
+
SensorType["Classifier"] = "classifier";
|
|
22
|
+
/** CLIP embedding generation for semantic search */
|
|
23
|
+
SensorType["Clip"] = "clip";
|
|
24
|
+
// Sensors — read-only state/environment sensors
|
|
25
|
+
/** Contact/open-close sensor (door, window) */
|
|
26
|
+
SensorType["Contact"] = "contact";
|
|
27
|
+
/** Temperature sensor (°C) */
|
|
28
|
+
SensorType["Temperature"] = "temperature";
|
|
29
|
+
/** Humidity sensor (0–100%) */
|
|
30
|
+
SensorType["Humidity"] = "humidity";
|
|
31
|
+
/** Occupancy/presence sensor */
|
|
32
|
+
SensorType["Occupancy"] = "occupancy";
|
|
33
|
+
/** Smoke detector */
|
|
34
|
+
SensorType["Smoke"] = "smoke";
|
|
35
|
+
/** Water leak detector */
|
|
36
|
+
SensorType["Leak"] = "leak";
|
|
37
|
+
// Controls — writable sensors the user can toggle from UI
|
|
38
|
+
/** Light on/off and brightness control */
|
|
39
|
+
SensorType["Light"] = "light";
|
|
40
|
+
/** Siren on/off and volume control */
|
|
41
|
+
SensorType["Siren"] = "siren";
|
|
42
|
+
/** Generic on/off switch */
|
|
43
|
+
SensorType["Switch"] = "switch";
|
|
44
|
+
/** Lock/unlock control */
|
|
45
|
+
SensorType["Lock"] = "lock";
|
|
46
|
+
/** Pan-tilt-zoom camera control */
|
|
47
|
+
SensorType["PTZ"] = "ptz";
|
|
48
|
+
/** Security system arm/disarm control */
|
|
49
|
+
SensorType["SecuritySystem"] = "securitySystem";
|
|
50
|
+
/** Garage door opener */
|
|
51
|
+
SensorType["Garage"] = "garage";
|
|
52
|
+
// Triggers
|
|
53
|
+
/** Doorbell ring trigger */
|
|
54
|
+
SensorType["Doorbell"] = "doorbell";
|
|
55
|
+
// Info
|
|
56
|
+
/** Battery level and charging state */
|
|
57
|
+
SensorType["Battery"] = "battery";
|
|
58
|
+
})(SensorType || (SensorType = {}));
|
|
59
|
+
/**
|
|
60
|
+
* Categorizes a sensor's role in the system.
|
|
61
|
+
* Determines how the backend treats the sensor (read-only vs. controllable).
|
|
62
|
+
*/
|
|
63
|
+
export var SensorCategory;
|
|
64
|
+
(function (SensorCategory) {
|
|
65
|
+
/** Read-only detection sensor (motion, object, audio, etc.) */
|
|
66
|
+
SensorCategory["Sensor"] = "sensor";
|
|
67
|
+
/** Controllable sensor with set methods (light, siren, PTZ, etc.) */
|
|
68
|
+
SensorCategory["Control"] = "control";
|
|
69
|
+
/** Event trigger (doorbell ring) */
|
|
70
|
+
SensorCategory["Trigger"] = "trigger";
|
|
71
|
+
/** Informational read-only state (battery level) */
|
|
72
|
+
SensorCategory["Info"] = "info";
|
|
73
|
+
})(SensorCategory || (SensorCategory = {}));
|
|
74
|
+
/**
|
|
75
|
+
* Abstract base class for all sensors. Plugins extend this (or use specialized
|
|
76
|
+
* subclasses like `MotionSensor`, `LightControl`, etc.) to implement sensor logic.
|
|
3
77
|
*
|
|
4
|
-
* Properties are
|
|
5
|
-
*
|
|
6
|
-
* is automatically propagated.
|
|
78
|
+
* Properties are managed through a reactive proxy — setting a property via `this.props`
|
|
79
|
+
* automatically notifies the backend and local listeners if the value changed.
|
|
7
80
|
*
|
|
8
|
-
* @template TProperties -
|
|
9
|
-
* @template TStorage -
|
|
10
|
-
* @template TCapability -
|
|
81
|
+
* @template TProperties - Sensor-specific property interface (e.g., MotionSensorProperties)
|
|
82
|
+
* @template TStorage - Persistent storage schema for per-sensor config
|
|
83
|
+
* @template TCapability - Capability enum type (e.g., PTZCapability)
|
|
11
84
|
*/
|
|
12
85
|
export class Sensor {
|
|
13
|
-
|
|
86
|
+
name;
|
|
14
87
|
id;
|
|
15
|
-
/** Camera ID this sensor belongs to (set by addSensor) */
|
|
16
88
|
_cameraId;
|
|
17
|
-
/** Plugin ID that provides this sensor (set by addSensor) */
|
|
18
89
|
_pluginId;
|
|
19
|
-
/** Whether this sensor is online/available */
|
|
20
|
-
_online = true;
|
|
21
|
-
/** Internal properties storage */
|
|
22
90
|
_propertiesStore;
|
|
23
|
-
/** Proxied properties for intercepting writes */
|
|
24
|
-
_propertiesProxy;
|
|
25
|
-
/** Update function for RPC propagation */
|
|
26
|
-
_updateFn;
|
|
27
|
-
/** Property change listeners */
|
|
28
91
|
_listeners = new Set();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
/**
|
|
92
|
+
#propertyChangedSubject = new Subject();
|
|
93
|
+
onPropertyChanged = this.#propertyChangedSubject.asObservable();
|
|
94
|
+
#capabilitiesChangedSubject = new Subject();
|
|
95
|
+
onCapabilitiesChanged = this.#capabilitiesChangedSubject.asObservable();
|
|
96
|
+
/** Per-sensor persistent storage (available after sensor is added to a camera) */
|
|
34
97
|
_storage;
|
|
35
|
-
/** Sensor capabilities */
|
|
36
98
|
_capabilities = [];
|
|
37
|
-
/** Capability update function for RPC propagation */
|
|
38
99
|
_capabilitiesUpdateFn;
|
|
39
|
-
/** Whether this sensor is assigned to be the active provider for its type */
|
|
40
100
|
_isAssigned = false;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
*
|
|
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.
|
|
101
|
+
#assignmentChangedSubject = new Subject();
|
|
102
|
+
onAssignmentChanged = this.#assignmentChangedSubject.asObservable();
|
|
103
|
+
_updateFn;
|
|
104
|
+
_displayName = '';
|
|
105
|
+
/** Override to provide a JSON schema for per-sensor storage settings UI */
|
|
106
|
+
get storageSchema() {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
/*
|
|
110
|
+
* @internal
|
|
62
111
|
*/
|
|
63
112
|
_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() {
|
|
113
|
+
constructor(name) {
|
|
71
114
|
this.id = crypto.randomUUID();
|
|
115
|
+
this.name = name;
|
|
72
116
|
// Initialize empty storage
|
|
73
117
|
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
118
|
}
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
get displayName() {
|
|
120
|
+
return this._displayName || this.name;
|
|
119
121
|
}
|
|
120
122
|
/**
|
|
121
|
-
*
|
|
123
|
+
* Set the display name (the only mutable identifier on a sensor).
|
|
122
124
|
*
|
|
123
|
-
* @
|
|
125
|
+
* @param value - Human-readable label shown in the UI.
|
|
124
126
|
*
|
|
125
|
-
* @
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* sensor.setDisplayName('Front Door Motion');
|
|
130
|
+
* ```
|
|
144
131
|
*/
|
|
145
|
-
|
|
146
|
-
|
|
132
|
+
setDisplayName(value) {
|
|
133
|
+
this._displayName = value;
|
|
147
134
|
}
|
|
148
|
-
/**
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
* @param online - The new online state
|
|
152
|
-
*
|
|
153
|
-
* @internal
|
|
154
|
-
*/
|
|
155
|
-
_setOnline(online) {
|
|
156
|
-
this._online = online;
|
|
135
|
+
/** Whether this sensor has been assigned to a camera in the backend */
|
|
136
|
+
get isAssigned() {
|
|
137
|
+
return this._isAssigned;
|
|
157
138
|
}
|
|
158
|
-
/**
|
|
159
|
-
* Camera ID this sensor belongs to
|
|
160
|
-
* Set automatically by CameraDevice.addSensor()
|
|
161
|
-
*/
|
|
139
|
+
/** Camera ID this sensor belongs to. Throws if sensor not yet added to a camera. */
|
|
162
140
|
get cameraId() {
|
|
163
141
|
if (!this._cameraId) {
|
|
164
142
|
throw new Error('Sensor not attached to camera. Call camera.addSensor() first.');
|
|
165
143
|
}
|
|
166
144
|
return this._cameraId;
|
|
167
145
|
}
|
|
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
146
|
get pluginId() {
|
|
183
147
|
return this._pluginId;
|
|
184
148
|
}
|
|
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
|
-
*/
|
|
149
|
+
/** Per-sensor persistent storage. Throws if sensor not yet added to a camera. */
|
|
200
150
|
get storage() {
|
|
151
|
+
if (!this._storage) {
|
|
152
|
+
throw new Error('Storage not initialized - sensor not added to camera yet');
|
|
153
|
+
}
|
|
201
154
|
return this._storage;
|
|
202
155
|
}
|
|
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
|
-
*/
|
|
156
|
+
/** Optional feature flags advertised by this sensor (e.g., PTZ pan/tilt/zoom) */
|
|
217
157
|
get capabilities() {
|
|
218
158
|
return this._capabilities;
|
|
219
159
|
}
|
|
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
|
-
*/
|
|
160
|
+
/** Set capabilities and notify the backend. Automatically deduplicates. */
|
|
227
161
|
set capabilities(value) {
|
|
228
162
|
// Deduplicate capabilities
|
|
229
163
|
this._capabilities = [...new Set(value)];
|
|
230
164
|
// Broadcast to SensorController (for RPC propagation)
|
|
231
165
|
this._capabilitiesUpdateFn?.(this._capabilities);
|
|
232
166
|
// Notify local listeners
|
|
233
|
-
this.
|
|
167
|
+
this.#capabilitiesChangedSubject.next(this._capabilities);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Read-only access to the internal property store. Subclasses use this to
|
|
171
|
+
* read current state when implementing semantic methods (e.g., `if (this.blocked) return`).
|
|
172
|
+
*/
|
|
173
|
+
get props() {
|
|
174
|
+
return this._propertiesStore;
|
|
175
|
+
}
|
|
176
|
+
getValue(property) {
|
|
177
|
+
return this._propertiesStore[property];
|
|
234
178
|
}
|
|
235
179
|
/**
|
|
236
|
-
*
|
|
180
|
+
* Get a read-only snapshot of all property values.
|
|
237
181
|
*
|
|
238
|
-
* @
|
|
182
|
+
* @returns Frozen view of every property currently held by the sensor.
|
|
239
183
|
*
|
|
240
|
-
* @
|
|
184
|
+
* @example
|
|
185
|
+
* ```ts
|
|
186
|
+
* const snapshot = sensor.getValues();
|
|
187
|
+
* console.log(snapshot);
|
|
188
|
+
* ```
|
|
241
189
|
*/
|
|
242
|
-
|
|
243
|
-
return this.
|
|
190
|
+
getValues() {
|
|
191
|
+
return { ...this._propertiesStore };
|
|
244
192
|
}
|
|
245
193
|
/**
|
|
246
|
-
*
|
|
194
|
+
* Iterates over the partial, writes changed properties to the store, fires a **single
|
|
195
|
+
* batched** RPC update with the delta, and notifies local listeners per-property.
|
|
196
|
+
*
|
|
197
|
+
* Used by the semantic helper methods on each sensor type (`setOn`, `setLow`,
|
|
198
|
+
* `reportDetections`, etc.) — **not for plugin authors**. Plugin code should
|
|
199
|
+
* call the semantic helpers, not write state directly.
|
|
247
200
|
*
|
|
248
|
-
*
|
|
201
|
+
* One `_writeState` call → one `_updateFn` invocation. The receiver sees an
|
|
202
|
+
* atomic state transition for this sensor.
|
|
203
|
+
*
|
|
204
|
+
* @param partial - Partial property delta to apply to the sensor's state store.
|
|
249
205
|
*
|
|
250
206
|
* @internal
|
|
251
207
|
*/
|
|
252
|
-
|
|
253
|
-
|
|
208
|
+
_writeState(partial) {
|
|
209
|
+
const delta = {};
|
|
210
|
+
const changes = [];
|
|
211
|
+
for (const key of Object.keys(partial)) {
|
|
212
|
+
const value = partial[key];
|
|
213
|
+
if (value === undefined)
|
|
214
|
+
continue;
|
|
215
|
+
const previousValue = this._propertiesStore[key];
|
|
216
|
+
// Only update if value changed (deep compare for objects/arrays)
|
|
217
|
+
if (isEqual(previousValue, value, true))
|
|
218
|
+
continue;
|
|
219
|
+
this._propertiesStore[key] = value;
|
|
220
|
+
delta[key] = value;
|
|
221
|
+
changes.push({ property: key, value, previousValue });
|
|
222
|
+
}
|
|
223
|
+
if (Object.keys(delta).length === 0)
|
|
224
|
+
return;
|
|
225
|
+
// Fire-and-forget batched RPC update — one callback for the whole delta
|
|
226
|
+
this._updateFn?.(delta);
|
|
227
|
+
// Notify local listeners per-property (existing observable contract)
|
|
228
|
+
for (const change of changes) {
|
|
229
|
+
this._notifyListeners(change.property, change.value, change.previousValue);
|
|
230
|
+
}
|
|
254
231
|
}
|
|
255
|
-
// ========== Assignment Status ==========
|
|
256
232
|
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
233
|
+
* Helper for `reportDetections(detected, detections?)` flows.
|
|
234
|
+
*
|
|
235
|
+
* - If `detected === false` → returns `[]` (clear).
|
|
236
|
+
* - If `detected === true` and `detections` has items → returns them as-is.
|
|
237
|
+
* - If `detected === true` and `detections` is missing/empty → returns a single
|
|
238
|
+
* synthesized full-frame detection with the given `fallbackLabel` and any
|
|
239
|
+
* `fallbackExtra` fields (used for type-specific properties like `attribute`,
|
|
240
|
+
* `plateText`, etc.).
|
|
241
|
+
*
|
|
242
|
+
* Generic over `T extends Detection` so each sensor's `reportDetections` can
|
|
243
|
+
* use its specific Detection subtype.
|
|
244
|
+
*
|
|
245
|
+
* @param detected - Whether the caller is reporting an active detection.
|
|
246
|
+
*
|
|
247
|
+
* @param detections - Caller-provided detections (may be empty/undefined).
|
|
248
|
+
*
|
|
249
|
+
* @param fallbackLabel - Label used when synthesizing a fallback detection.
|
|
259
250
|
*
|
|
260
|
-
*
|
|
261
|
-
* to provide this sensor type for the camera. Unassigned sensors
|
|
262
|
-
* are still registered but their events are filtered out.
|
|
251
|
+
* @param fallbackExtra - Additional fields merged into the synthesized fallback.
|
|
263
252
|
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
253
|
+
* @returns Normalized detection list ready to write into the sensor's state.
|
|
254
|
+
*
|
|
255
|
+
* @internal
|
|
267
256
|
*/
|
|
268
|
-
|
|
269
|
-
|
|
257
|
+
_normalizeReportedDetections(detected, detections, fallbackLabel, fallbackExtra) {
|
|
258
|
+
if (!detected)
|
|
259
|
+
return [];
|
|
260
|
+
if (detections && detections.length > 0)
|
|
261
|
+
return detections;
|
|
262
|
+
return [
|
|
263
|
+
{
|
|
264
|
+
label: fallbackLabel,
|
|
265
|
+
confidence: 1,
|
|
266
|
+
box: { x: 0, y: 0, width: 1, height: 1 },
|
|
267
|
+
...(fallbackExtra ?? {}),
|
|
268
|
+
},
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
hasCapability(capability) {
|
|
272
|
+
return this._capabilities.includes(capability);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Serialize this sensor to a JSON-safe object for RPC transport.
|
|
276
|
+
*
|
|
277
|
+
* @returns The wire representation used to mirror the sensor across processes.
|
|
278
|
+
*
|
|
279
|
+
* @internal
|
|
280
|
+
*/
|
|
281
|
+
toJSON() {
|
|
282
|
+
return {
|
|
283
|
+
id: this.id,
|
|
284
|
+
type: this.type,
|
|
285
|
+
name: this.name,
|
|
286
|
+
displayName: this.displayName,
|
|
287
|
+
category: this.category,
|
|
288
|
+
cameraId: this.cameraId,
|
|
289
|
+
pluginId: this.pluginId,
|
|
290
|
+
properties: this._getProperties(),
|
|
291
|
+
capabilities: this._capabilities,
|
|
292
|
+
requiresFrames: this._requiresFrames,
|
|
293
|
+
};
|
|
270
294
|
}
|
|
271
295
|
/**
|
|
272
|
-
*
|
|
296
|
+
* Lifecycle hook: the sensor just became assigned to a camera. Override to
|
|
297
|
+
* start background work that should only run while this sensor is live —
|
|
298
|
+
* polling loops, event subscriptions, timers, external connections.
|
|
299
|
+
*
|
|
300
|
+
* Called AFTER `cameraId`, `storage`, and RPC channels are wired up, so the
|
|
301
|
+
* override can safely access `this.cameraId`, `this.storage`, and publish
|
|
302
|
+
* properties via the semantic helper methods.
|
|
273
303
|
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
* - Start/stop event loops
|
|
277
|
-
* - Connect/disconnect from external services
|
|
304
|
+
* Errors thrown here are caught and logged — they will NOT break assignment
|
|
305
|
+
* bookkeeping. If your work can fail, handle it inside the override.
|
|
278
306
|
*
|
|
279
|
-
*
|
|
307
|
+
* Paired 1:1 with `onDeassigned` — for every `onAssigned` call there is
|
|
308
|
+
* exactly one matching `onDeassigned` later (on deassignment or cleanup).
|
|
280
309
|
*
|
|
281
|
-
*
|
|
310
|
+
* Default: no-op. Most sensors don't need lifecycle hooks.
|
|
282
311
|
*
|
|
283
312
|
* @example
|
|
284
|
-
* ```
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
* } else {
|
|
289
|
-
* this.stopEventLoop();
|
|
290
|
-
* }
|
|
291
|
-
* });
|
|
313
|
+
* ```ts
|
|
314
|
+
* protected override async onAssigned(): Promise<void> {
|
|
315
|
+
* this._timer = setInterval(() => this.poll(), 5_000);
|
|
316
|
+
* }
|
|
292
317
|
* ```
|
|
293
318
|
*/
|
|
294
|
-
|
|
295
|
-
this._assignmentListeners.add(callback);
|
|
296
|
-
return () => this._assignmentListeners.delete(callback);
|
|
297
|
-
}
|
|
319
|
+
onAssigned() { }
|
|
298
320
|
/**
|
|
299
|
-
*
|
|
321
|
+
* Lifecycle hook: the sensor is being deassigned. Override to tear down
|
|
322
|
+
* whatever was started in `onAssigned` — clear timers, close subscriptions,
|
|
323
|
+
* release external resources.
|
|
324
|
+
*
|
|
325
|
+
* Always called exactly once for each prior `onAssigned`. Also called from
|
|
326
|
+
* `_cleanup` if the sensor is being removed while still assigned, so you
|
|
327
|
+
* can rely on this as the single teardown point.
|
|
300
328
|
*
|
|
301
|
-
*
|
|
329
|
+
* Default: no-op.
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ```ts
|
|
333
|
+
* protected override onDeassigned(): void {
|
|
334
|
+
* if (this._timer) clearInterval(this._timer);
|
|
335
|
+
* }
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
onDeassigned() { }
|
|
339
|
+
/**
|
|
340
|
+
* @param updateFn - Receiver invoked with each batched property delta.
|
|
302
341
|
*
|
|
303
342
|
* @internal
|
|
304
343
|
*/
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
}
|
|
344
|
+
_init(updateFn) {
|
|
345
|
+
this._updateFn = updateFn;
|
|
317
346
|
}
|
|
318
|
-
// ========== SensorLike Interface Methods ==========
|
|
319
347
|
/**
|
|
320
|
-
*
|
|
348
|
+
* @param property - Property key to update on the internal store.
|
|
321
349
|
*
|
|
322
|
-
* @param
|
|
350
|
+
* @param value - New value to assign to the property.
|
|
323
351
|
*
|
|
324
|
-
* @
|
|
352
|
+
* @internal
|
|
325
353
|
*/
|
|
326
|
-
|
|
327
|
-
|
|
354
|
+
_setPropertyInternal(property, value) {
|
|
355
|
+
const previousValue = this._propertiesStore[property];
|
|
356
|
+
// Deep compare for objects/arrays
|
|
357
|
+
if (!isEqual(previousValue, value)) {
|
|
358
|
+
this._propertiesStore[property] = value;
|
|
359
|
+
// property is always a valid property enum value (K extends keyof TProperties)
|
|
360
|
+
this._notifyListeners(property, value, previousValue);
|
|
361
|
+
}
|
|
328
362
|
}
|
|
329
363
|
/**
|
|
330
|
-
*
|
|
364
|
+
* @returns A shallow copy of the sensor's current property store.
|
|
331
365
|
*
|
|
332
|
-
* @
|
|
366
|
+
* @internal
|
|
333
367
|
*/
|
|
334
|
-
|
|
368
|
+
_getProperties() {
|
|
335
369
|
return { ...this._propertiesStore };
|
|
336
370
|
}
|
|
337
371
|
/**
|
|
338
|
-
*
|
|
339
|
-
* For Control sensors, this triggers the property setter through the proxy.
|
|
340
|
-
*
|
|
341
|
-
* @param property - The property name
|
|
372
|
+
* @param cameraId - ID of the camera the sensor has been attached to.
|
|
342
373
|
*
|
|
343
|
-
* @
|
|
374
|
+
* @internal
|
|
344
375
|
*/
|
|
345
|
-
|
|
346
|
-
this.
|
|
376
|
+
_setCameraId(cameraId) {
|
|
377
|
+
this._cameraId = cameraId;
|
|
347
378
|
}
|
|
348
379
|
/**
|
|
349
|
-
*
|
|
350
|
-
* This is for consumers who just want property/value pairs.
|
|
380
|
+
* @param pluginId - Plugin ID owning this sensor.
|
|
351
381
|
*
|
|
352
|
-
* @
|
|
382
|
+
* @internal
|
|
383
|
+
*/
|
|
384
|
+
_setPluginId(pluginId) {
|
|
385
|
+
this._pluginId = pluginId;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* @param storage - Per-sensor persistent storage handle.
|
|
353
389
|
*
|
|
354
|
-
* @
|
|
390
|
+
* @internal
|
|
355
391
|
*/
|
|
356
|
-
|
|
357
|
-
this.
|
|
358
|
-
return () => this._simpleListeners.delete(callback);
|
|
392
|
+
_setStorage(storage) {
|
|
393
|
+
this._storage = storage;
|
|
359
394
|
}
|
|
360
395
|
/**
|
|
361
|
-
*
|
|
362
|
-
* This includes full event details like previous value, timestamps, etc.
|
|
396
|
+
* @param updateFn - Receiver invoked with the deduplicated capability list when capabilities change.
|
|
363
397
|
*
|
|
364
|
-
* @
|
|
398
|
+
* @internal
|
|
399
|
+
*/
|
|
400
|
+
_initCapabilities(updateFn) {
|
|
401
|
+
this._capabilitiesUpdateFn = updateFn;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* @param assigned - True when the sensor is assigned to a camera, false when deassigned.
|
|
365
405
|
*
|
|
366
|
-
* @
|
|
406
|
+
* @internal
|
|
367
407
|
*/
|
|
368
|
-
|
|
369
|
-
this.
|
|
370
|
-
|
|
408
|
+
_setAssigned(assigned) {
|
|
409
|
+
if (this._isAssigned === assigned)
|
|
410
|
+
return;
|
|
411
|
+
this._isAssigned = assigned;
|
|
412
|
+
this.#assignmentChangedSubject.next(assigned);
|
|
413
|
+
// Fire-and-forget the lifecycle hook. Plugin authors who need error
|
|
414
|
+
// handling can wrap their onAssigned/onDeassigned body in try/catch.
|
|
415
|
+
try {
|
|
416
|
+
const result = assigned ? this.onAssigned() : this.onDeassigned();
|
|
417
|
+
if (result && typeof result.catch === 'function') {
|
|
418
|
+
result.catch(() => {
|
|
419
|
+
// swallow — lifecycle errors must not break assignment bookkeeping
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
// swallow synchronous errors for the same reason
|
|
425
|
+
}
|
|
371
426
|
}
|
|
372
427
|
/**
|
|
373
|
-
*
|
|
374
|
-
* Called when the sensor's capabilities are updated at runtime.
|
|
428
|
+
* @param property - Property name pushed by the backend.
|
|
375
429
|
*
|
|
376
|
-
* @param
|
|
430
|
+
* @param value - New value for the property.
|
|
377
431
|
*
|
|
378
|
-
* @
|
|
432
|
+
* @internal
|
|
379
433
|
*/
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
434
|
+
_onBackendPropertyChanged(property, value) {
|
|
435
|
+
// Update internal state (bypasses proxy to avoid re-broadcast)
|
|
436
|
+
this._setPropertyInternal(property, value);
|
|
383
437
|
}
|
|
384
438
|
/**
|
|
385
|
-
*
|
|
439
|
+
* @internal
|
|
386
440
|
*/
|
|
387
|
-
|
|
388
|
-
|
|
441
|
+
_cleanup() {
|
|
442
|
+
// Trigger onDeassigned if we're still assigned — guarantees the hook is
|
|
443
|
+
// paired 1:1 even when the sensor is force-removed without going through
|
|
444
|
+
// a proper deassignment path.
|
|
445
|
+
if (this._isAssigned) {
|
|
446
|
+
this._isAssigned = false;
|
|
389
447
|
try {
|
|
390
|
-
|
|
448
|
+
const result = this.onDeassigned();
|
|
449
|
+
if (result && typeof result.catch === 'function') {
|
|
450
|
+
result.catch(() => { });
|
|
451
|
+
}
|
|
391
452
|
}
|
|
392
453
|
catch {
|
|
393
|
-
//
|
|
454
|
+
// swallow
|
|
394
455
|
}
|
|
395
456
|
}
|
|
457
|
+
this._updateFn = undefined;
|
|
458
|
+
this._capabilitiesUpdateFn = undefined;
|
|
459
|
+
this._storage = undefined;
|
|
460
|
+
this._listeners.clear();
|
|
461
|
+
this.#propertyChangedSubject.complete();
|
|
462
|
+
this.#capabilitiesChangedSubject.complete();
|
|
463
|
+
this.#assignmentChangedSubject.complete();
|
|
396
464
|
}
|
|
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
465
|
_notifyListeners(property, value, previousValue) {
|
|
407
466
|
// Skip notification if sensor not yet attached to camera
|
|
408
467
|
// (e.g., during constructor initialization)
|
|
@@ -427,34 +486,7 @@ export class Sensor {
|
|
|
427
486
|
// Ignore listener errors
|
|
428
487
|
}
|
|
429
488
|
}
|
|
430
|
-
// Notify
|
|
431
|
-
|
|
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
|
-
};
|
|
489
|
+
// Notify via Observable subject
|
|
490
|
+
this.#propertyChangedSubject.next({ property, value, timestamp: event.timestamp });
|
|
459
491
|
}
|
|
460
492
|
}
|