@blueharford/scrypted-spatial-awareness 0.6.32 → 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/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: {
@@ -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 as number || 30) * 1000,
360
- correlationThreshold: this.storageSettings.values.correlationThreshold as number || 0.35,
361
- lostTimeout: (this.storageSettings.values.lostTimeout as number || 300) * 1000,
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 as number || 3) * 1000,
364
- objectAlertCooldown: (this.storageSettings.values.objectAlertCooldown as number || 30) * 1000,
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 as number || 30) * 1000,
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 as number || 3) * 1000,
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 as number ?? 0.7,
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 as number ?? 0,
400
- autoAcceptThreshold: this.storageSettings.values.autoAcceptThreshold as number ?? 0.85,
401
- minLandmarkConfidence: this.storageSettings.values.minLandmarkConfidence as number ?? 0.6,
402
- minConnectionConfidence: this.storageSettings.values.minConnectionConfidence as number ?? 0.5,
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.persistState();
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.persistState();
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.persistState();
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.persistState();
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.persistState();
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
+ });