@blueharford/scrypted-spatial-awareness 0.6.31 → 0.6.33
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/CHANGELOG.md +18 -0
- package/README.md +5 -2
- package/dist/main.nodejs.js +1 -1
- package/dist/main.nodejs.js.map +1 -1
- package/dist/plugin.zip +0 -0
- package/out/main.nodejs.js +381 -113
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +4 -2
- package/session-manager-plugin.pkg +0 -0
- package/src/alerts/alert-manager.ts +149 -21
- package/src/alerts/alert-utils.ts +32 -0
- package/src/core/tracking-engine.ts +161 -46
- package/src/main.ts +55 -13
- package/src/models/tracked-object.ts +1 -1
- package/src/state/tracking-state.ts +18 -5
- package/tests/run-tests.ts +50 -0
package/src/main.ts
CHANGED
|
@@ -111,6 +111,13 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
111
111
|
description: 'Object must be visible for this duration before triggering movement alerts',
|
|
112
112
|
group: 'Tracking',
|
|
113
113
|
},
|
|
114
|
+
minDetectionScore: {
|
|
115
|
+
title: 'Minimum Detection Confidence',
|
|
116
|
+
type: 'number',
|
|
117
|
+
defaultValue: 0.5,
|
|
118
|
+
description: 'Minimum detection score (0-1) to consider for tracking',
|
|
119
|
+
group: 'Tracking',
|
|
120
|
+
},
|
|
114
121
|
objectAlertCooldown: {
|
|
115
122
|
title: 'Per-Object Alert Cooldown (seconds)',
|
|
116
123
|
type: 'number',
|
|
@@ -118,6 +125,20 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
118
125
|
description: 'Minimum time between alerts for the same tracked object',
|
|
119
126
|
group: 'Tracking',
|
|
120
127
|
},
|
|
128
|
+
notifyOnAlertUpdates: {
|
|
129
|
+
title: 'Notify on Alert Updates',
|
|
130
|
+
type: 'boolean',
|
|
131
|
+
defaultValue: false,
|
|
132
|
+
description: 'Send notifications when an existing alert is updated (camera transitions, context changes)',
|
|
133
|
+
group: 'Tracking',
|
|
134
|
+
},
|
|
135
|
+
alertUpdateCooldown: {
|
|
136
|
+
title: 'Alert Update Cooldown (seconds)',
|
|
137
|
+
type: 'number',
|
|
138
|
+
defaultValue: 60,
|
|
139
|
+
description: 'Minimum time between update notifications for the same tracked object (0 = no limit)',
|
|
140
|
+
group: 'Tracking',
|
|
141
|
+
},
|
|
121
142
|
|
|
122
143
|
// LLM Integration
|
|
123
144
|
useLlmDescriptions: {
|
|
@@ -130,7 +151,7 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
130
151
|
llmDebounceInterval: {
|
|
131
152
|
title: 'LLM Rate Limit (seconds)',
|
|
132
153
|
type: 'number',
|
|
133
|
-
defaultValue:
|
|
154
|
+
defaultValue: 5,
|
|
134
155
|
description: 'Minimum time between LLM calls to prevent API rate limiting. Increase if you get rate limit errors. (0 = no limit)',
|
|
135
156
|
group: 'AI & Spatial Reasoning',
|
|
136
157
|
},
|
|
@@ -321,6 +342,8 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
321
342
|
await this.initializeMqtt();
|
|
322
343
|
}
|
|
323
344
|
|
|
345
|
+
this.applyAlertUpdateSettings();
|
|
346
|
+
|
|
324
347
|
this.console.log('Spatial Awareness Plugin initialized');
|
|
325
348
|
}
|
|
326
349
|
|
|
@@ -356,21 +379,22 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
356
379
|
}
|
|
357
380
|
|
|
358
381
|
const config: TrackingEngineConfig = {
|
|
359
|
-
correlationWindow: (this.storageSettings.values.correlationWindow
|
|
360
|
-
correlationThreshold: this.storageSettings.values.correlationThreshold
|
|
361
|
-
lostTimeout: (this.storageSettings.values.lostTimeout
|
|
382
|
+
correlationWindow: this.getNumberSetting(this.storageSettings.values.correlationWindow, 30) * 1000,
|
|
383
|
+
correlationThreshold: this.getNumberSetting(this.storageSettings.values.correlationThreshold, 0.35),
|
|
384
|
+
lostTimeout: this.getNumberSetting(this.storageSettings.values.lostTimeout, 300) * 1000,
|
|
362
385
|
useVisualMatching: this.storageSettings.values.useVisualMatching as boolean ?? true,
|
|
363
|
-
loiteringThreshold: (this.storageSettings.values.loiteringThreshold
|
|
364
|
-
|
|
386
|
+
loiteringThreshold: this.getNumberSetting(this.storageSettings.values.loiteringThreshold, 3) * 1000,
|
|
387
|
+
minDetectionScore: this.getNumberSetting(this.storageSettings.values.minDetectionScore, 0.5),
|
|
388
|
+
objectAlertCooldown: this.getNumberSetting(this.storageSettings.values.objectAlertCooldown, 30) * 1000,
|
|
365
389
|
useLlmDescriptions: this.storageSettings.values.useLlmDescriptions as boolean ?? true,
|
|
366
390
|
llmDeviceIds: this.parseLlmProviders(),
|
|
367
|
-
llmDebounceInterval: (this.storageSettings.values.llmDebounceInterval
|
|
391
|
+
llmDebounceInterval: this.getNumberSetting(this.storageSettings.values.llmDebounceInterval, 5) * 1000,
|
|
368
392
|
llmFallbackEnabled: this.storageSettings.values.llmFallbackEnabled as boolean ?? true,
|
|
369
|
-
llmFallbackTimeout: (this.storageSettings.values.llmFallbackTimeout
|
|
393
|
+
llmFallbackTimeout: this.getNumberSetting(this.storageSettings.values.llmFallbackTimeout, 3) * 1000,
|
|
370
394
|
enableTransitTimeLearning: this.storageSettings.values.enableTransitTimeLearning as boolean ?? true,
|
|
371
395
|
enableConnectionSuggestions: this.storageSettings.values.enableConnectionSuggestions as boolean ?? true,
|
|
372
396
|
enableLandmarkLearning: this.storageSettings.values.enableLandmarkLearning as boolean ?? true,
|
|
373
|
-
landmarkConfidenceThreshold: this.storageSettings.values.landmarkConfidenceThreshold
|
|
397
|
+
landmarkConfidenceThreshold: this.getNumberSetting(this.storageSettings.values.landmarkConfidenceThreshold, 0.7),
|
|
374
398
|
};
|
|
375
399
|
|
|
376
400
|
this.trackingEngine = new TrackingEngine(
|
|
@@ -381,6 +405,8 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
381
405
|
this.console
|
|
382
406
|
);
|
|
383
407
|
|
|
408
|
+
this.applyAlertUpdateSettings();
|
|
409
|
+
|
|
384
410
|
// Set up callback to save topology changes (e.g., from accepted landmark suggestions)
|
|
385
411
|
this.trackingEngine.setTopologyChangeCallback((updatedTopology) => {
|
|
386
412
|
this.storage.setItem('topology', JSON.stringify(updatedTopology));
|
|
@@ -396,10 +422,10 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
396
422
|
|
|
397
423
|
private async initializeDiscoveryEngine(topology: CameraTopology): Promise<void> {
|
|
398
424
|
const discoveryConfig: DiscoveryConfig = {
|
|
399
|
-
discoveryIntervalHours: this.storageSettings.values.discoveryIntervalHours
|
|
400
|
-
autoAcceptThreshold: this.storageSettings.values.autoAcceptThreshold
|
|
401
|
-
minLandmarkConfidence: this.storageSettings.values.minLandmarkConfidence
|
|
402
|
-
minConnectionConfidence: this.storageSettings.values.minConnectionConfidence
|
|
425
|
+
discoveryIntervalHours: this.getNumberSetting(this.storageSettings.values.discoveryIntervalHours, 0),
|
|
426
|
+
autoAcceptThreshold: this.getNumberSetting(this.storageSettings.values.autoAcceptThreshold, 0.85),
|
|
427
|
+
minLandmarkConfidence: this.getNumberSetting(this.storageSettings.values.minLandmarkConfidence, 0.6),
|
|
428
|
+
minConnectionConfidence: this.getNumberSetting(this.storageSettings.values.minConnectionConfidence, 0.5),
|
|
403
429
|
};
|
|
404
430
|
|
|
405
431
|
if (this.discoveryEngine) {
|
|
@@ -448,6 +474,12 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
448
474
|
return undefined;
|
|
449
475
|
}
|
|
450
476
|
|
|
477
|
+
private getNumberSetting(value: unknown, fallback: number): number {
|
|
478
|
+
if (value === undefined || value === null) return fallback;
|
|
479
|
+
const num = typeof value === 'string' ? Number(value) : (value as number);
|
|
480
|
+
return Number.isFinite(num) ? num : fallback;
|
|
481
|
+
}
|
|
482
|
+
|
|
451
483
|
// ==================== DeviceProvider Implementation ====================
|
|
452
484
|
|
|
453
485
|
async getDevice(nativeId: string): Promise<any> {
|
|
@@ -771,6 +803,10 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
771
803
|
}
|
|
772
804
|
}
|
|
773
805
|
|
|
806
|
+
if (key === 'notifyOnAlertUpdates' || key === 'alertUpdateCooldown') {
|
|
807
|
+
this.applyAlertUpdateSettings();
|
|
808
|
+
}
|
|
809
|
+
|
|
774
810
|
// Handle MQTT setting changes
|
|
775
811
|
if (key === 'enableMqtt' || key === 'mqttBroker' || key === 'mqttUsername' ||
|
|
776
812
|
key === 'mqttPassword' || key === 'mqttBaseTopic') {
|
|
@@ -784,6 +820,12 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
784
820
|
}
|
|
785
821
|
}
|
|
786
822
|
|
|
823
|
+
private applyAlertUpdateSettings(): void {
|
|
824
|
+
const enabled = this.storageSettings.values.notifyOnAlertUpdates as boolean ?? false;
|
|
825
|
+
const cooldownSeconds = this.getNumberSetting(this.storageSettings.values.alertUpdateCooldown, 60);
|
|
826
|
+
this.alertManager.setUpdateNotificationOptions(enabled, cooldownSeconds * 1000);
|
|
827
|
+
}
|
|
828
|
+
|
|
787
829
|
// ==================== HttpRequestHandler Implementation ====================
|
|
788
830
|
|
|
789
831
|
async onRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Defines objects being tracked across multiple cameras
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { ObjectDetectionResult } from '@scrypted/sdk';
|
|
6
|
+
import type { ObjectDetectionResult } from '@scrypted/sdk';
|
|
7
7
|
|
|
8
8
|
/** Unique identifier for a globally tracked object */
|
|
9
9
|
export type GlobalTrackingId = string;
|
|
@@ -21,6 +21,8 @@ export class TrackingState {
|
|
|
21
21
|
private changeCallbacks: StateChangeCallback[] = [];
|
|
22
22
|
private storage: Storage;
|
|
23
23
|
private console: Console;
|
|
24
|
+
private persistTimer: NodeJS.Timeout | null = null;
|
|
25
|
+
private readonly persistDebounceMs: number = 2000;
|
|
24
26
|
|
|
25
27
|
constructor(storage: Storage, console: Console) {
|
|
26
28
|
this.storage = storage;
|
|
@@ -60,7 +62,7 @@ export class TrackingState {
|
|
|
60
62
|
|
|
61
63
|
this.objects.set(object.globalId, object);
|
|
62
64
|
this.notifyChange();
|
|
63
|
-
this.
|
|
65
|
+
this.schedulePersist();
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
/** Add a new sighting to an existing tracked object */
|
|
@@ -77,7 +79,7 @@ export class TrackingState {
|
|
|
77
79
|
this.objectsByCamera.get(sighting.cameraId)!.add(globalId);
|
|
78
80
|
|
|
79
81
|
this.notifyChange();
|
|
80
|
-
this.
|
|
82
|
+
this.schedulePersist();
|
|
81
83
|
return true;
|
|
82
84
|
}
|
|
83
85
|
|
|
@@ -96,7 +98,7 @@ export class TrackingState {
|
|
|
96
98
|
this.objectsByCamera.get(segment.toCameraId)!.add(globalId);
|
|
97
99
|
|
|
98
100
|
this.notifyChange();
|
|
99
|
-
this.
|
|
101
|
+
this.schedulePersist();
|
|
100
102
|
return true;
|
|
101
103
|
}
|
|
102
104
|
|
|
@@ -150,7 +152,7 @@ export class TrackingState {
|
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
this.notifyChange();
|
|
153
|
-
this.
|
|
155
|
+
this.schedulePersist();
|
|
154
156
|
}
|
|
155
157
|
}
|
|
156
158
|
|
|
@@ -167,7 +169,7 @@ export class TrackingState {
|
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
this.notifyChange();
|
|
170
|
-
this.
|
|
172
|
+
this.schedulePersist();
|
|
171
173
|
}
|
|
172
174
|
}
|
|
173
175
|
|
|
@@ -229,6 +231,17 @@ export class TrackingState {
|
|
|
229
231
|
}
|
|
230
232
|
}
|
|
231
233
|
|
|
234
|
+
private schedulePersist(): void {
|
|
235
|
+
if (this.persistTimer) {
|
|
236
|
+
clearTimeout(this.persistTimer);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this.persistTimer = setTimeout(() => {
|
|
240
|
+
this.persistTimer = null;
|
|
241
|
+
this.persistState();
|
|
242
|
+
}, this.persistDebounceMs);
|
|
243
|
+
}
|
|
244
|
+
|
|
232
245
|
private loadPersistedState(): void {
|
|
233
246
|
try {
|
|
234
247
|
const json = this.storage.getItem('tracked-objects');
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from 'assert/strict';
|
|
2
|
+
import {
|
|
3
|
+
getActiveAlertKey,
|
|
4
|
+
hasMeaningfulAlertChange,
|
|
5
|
+
shouldSendUpdateNotification,
|
|
6
|
+
} from '../src/alerts/alert-utils';
|
|
7
|
+
|
|
8
|
+
async function testAlertUpdateHelpers(): Promise<void> {
|
|
9
|
+
const prev = {
|
|
10
|
+
cameraId: 'cam-1',
|
|
11
|
+
objectLabel: 'Person at front',
|
|
12
|
+
involvedLandmarks: ['Front Door'],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const same = {
|
|
16
|
+
cameraId: 'cam-1',
|
|
17
|
+
objectLabel: 'Person at front',
|
|
18
|
+
involvedLandmarks: ['Front Door'],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const changed = {
|
|
22
|
+
cameraId: 'cam-2',
|
|
23
|
+
objectLabel: 'Person moving to driveway',
|
|
24
|
+
involvedLandmarks: ['Driveway'],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
assert.equal(hasMeaningfulAlertChange(prev, same), false);
|
|
28
|
+
assert.equal(hasMeaningfulAlertChange(prev, changed), true);
|
|
29
|
+
|
|
30
|
+
const key = getActiveAlertKey('movement', 'movement', 'obj-1');
|
|
31
|
+
assert.equal(key, 'movement:movement:obj-1');
|
|
32
|
+
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
assert.equal(shouldSendUpdateNotification(false, now - 100000, now, 60000), false);
|
|
35
|
+
assert.equal(shouldSendUpdateNotification(true, now - 100000, now, 60000), true);
|
|
36
|
+
assert.equal(shouldSendUpdateNotification(true, now - 1000, now, 60000), false);
|
|
37
|
+
assert.equal(shouldSendUpdateNotification(true, now - 1000, now, 0), true);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function run(): Promise<void> {
|
|
41
|
+
await testAlertUpdateHelpers();
|
|
42
|
+
// eslint-disable-next-line no-console
|
|
43
|
+
console.log('All tests passed');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
run().catch(err => {
|
|
47
|
+
// eslint-disable-next-line no-console
|
|
48
|
+
console.error(err);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
});
|