@blueharford/scrypted-spatial-awareness 0.1.14 → 0.1.16

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/plugin.zip CHANGED
Binary file
@@ -34525,6 +34525,25 @@ class AlertManager {
34525
34525
  */
34526
34526
  getDefaultNotifiers() {
34527
34527
  try {
34528
+ // Try new multiple notifiers setting first
34529
+ const notifiers = this.storage.getItem('defaultNotifiers');
34530
+ if (notifiers) {
34531
+ // Could be JSON array or comma-separated string
34532
+ try {
34533
+ const parsed = JSON.parse(notifiers);
34534
+ if (Array.isArray(parsed)) {
34535
+ return parsed;
34536
+ }
34537
+ }
34538
+ catch {
34539
+ // Not JSON, might be comma-separated or single value
34540
+ if (notifiers.includes(',')) {
34541
+ return notifiers.split(',').map(s => s.trim()).filter(Boolean);
34542
+ }
34543
+ return [notifiers];
34544
+ }
34545
+ }
34546
+ // Fallback to old single notifier setting
34528
34547
  const defaultNotifier = this.storage.getItem('defaultNotifier');
34529
34548
  return defaultNotifier ? [defaultNotifier] : [];
34530
34549
  }
@@ -35051,6 +35070,10 @@ class TrackingEngine {
35051
35070
  listeners = new Map();
35052
35071
  pendingTimers = new Map();
35053
35072
  lostCheckInterval = null;
35073
+ /** Track last alert time per object to enforce cooldown */
35074
+ objectLastAlertTime = new Map();
35075
+ /** Cache for LLM device reference */
35076
+ llmDevice = null;
35054
35077
  constructor(topology, state, alertManager, config, console) {
35055
35078
  this.topology = topology;
35056
35079
  this.state = state;
@@ -35153,6 +35176,68 @@ class TrackingEngine {
35153
35176
  await this.processSighting(sighting, camera.isEntryPoint, camera.isExitPoint);
35154
35177
  }
35155
35178
  }
35179
+ /** Check if object passes loitering threshold */
35180
+ passesLoiteringThreshold(tracked) {
35181
+ const visibleDuration = tracked.lastSeen - tracked.firstSeen;
35182
+ return visibleDuration >= this.config.loiteringThreshold;
35183
+ }
35184
+ /** Check if object is in alert cooldown */
35185
+ isInAlertCooldown(globalId) {
35186
+ const lastAlertTime = this.objectLastAlertTime.get(globalId);
35187
+ if (!lastAlertTime)
35188
+ return false;
35189
+ return (Date.now() - lastAlertTime) < this.config.objectAlertCooldown;
35190
+ }
35191
+ /** Record that we alerted for this object */
35192
+ recordAlertTime(globalId) {
35193
+ this.objectLastAlertTime.set(globalId, Date.now());
35194
+ }
35195
+ /** Try to get LLM-enhanced description for movement */
35196
+ async getLlmDescription(tracked, fromCamera, toCamera, cameraId) {
35197
+ if (!this.config.useLlmDescriptions)
35198
+ return null;
35199
+ try {
35200
+ // Find LLM plugin device if not cached
35201
+ if (!this.llmDevice) {
35202
+ for (const id of Object.keys(systemManager.getSystemState())) {
35203
+ const device = systemManager.getDeviceById(id);
35204
+ if (device?.interfaces?.includes(sdk_1.ScryptedInterface.ObjectDetection) &&
35205
+ device.name?.toLowerCase().includes('llm')) {
35206
+ this.llmDevice = device;
35207
+ this.console.log(`Found LLM device: ${device.name}`);
35208
+ break;
35209
+ }
35210
+ }
35211
+ }
35212
+ if (!this.llmDevice)
35213
+ return null;
35214
+ // Get snapshot from camera for LLM analysis
35215
+ const camera = systemManager.getDeviceById(cameraId);
35216
+ if (!camera?.interfaces?.includes(sdk_1.ScryptedInterface.Camera))
35217
+ return null;
35218
+ const picture = await camera.takePicture();
35219
+ if (!picture)
35220
+ return null;
35221
+ // Ask LLM to describe the movement
35222
+ const prompt = `Describe this ${tracked.className} in one short sentence. ` +
35223
+ `They are moving from the ${fromCamera} area towards the ${toCamera}. ` +
35224
+ `Include details like: gender (man/woman), clothing color, vehicle color/type if applicable. ` +
35225
+ `Example: "Man in blue jacket walking from garage towards front door" or ` +
35226
+ `"Black SUV driving from driveway towards street"`;
35227
+ const result = await this.llmDevice.detectObjects(picture, {
35228
+ settings: { prompt }
35229
+ });
35230
+ // Extract description from LLM response
35231
+ if (result.detections?.[0]?.label) {
35232
+ return result.detections[0].label;
35233
+ }
35234
+ return null;
35235
+ }
35236
+ catch (e) {
35237
+ this.console.warn('LLM description failed:', e);
35238
+ return null;
35239
+ }
35240
+ }
35156
35241
  /** Process a single sighting */
35157
35242
  async processSighting(sighting, isEntryPoint, isExitPoint) {
35158
35243
  // Try to correlate with existing tracked objects
@@ -35178,17 +35263,23 @@ class TrackingEngine {
35178
35263
  this.console.log(`Object ${tracked.globalId.slice(0, 8)} transited: ` +
35179
35264
  `${lastSighting.cameraName} → ${sighting.cameraName} ` +
35180
35265
  `(confidence: ${(correlation.confidence * 100).toFixed(0)}%)`);
35181
- // Generate movement alert for cross-camera transition
35182
- await this.alertManager.checkAndAlert('movement', tracked, {
35183
- fromCameraId: lastSighting.cameraId,
35184
- fromCameraName: lastSighting.cameraName,
35185
- toCameraId: sighting.cameraId,
35186
- toCameraName: sighting.cameraName,
35187
- transitTime: transitDuration,
35188
- objectClass: sighting.detection.className,
35189
- objectLabel: sighting.detection.label,
35190
- detectionId: sighting.detectionId,
35191
- });
35266
+ // Check loitering threshold and per-object cooldown before alerting
35267
+ if (this.passesLoiteringThreshold(tracked) && !this.isInAlertCooldown(tracked.globalId)) {
35268
+ // Try to get LLM-enhanced description
35269
+ const llmDescription = await this.getLlmDescription(tracked, lastSighting.cameraName, sighting.cameraName, sighting.cameraId);
35270
+ // Generate movement alert for cross-camera transition
35271
+ await this.alertManager.checkAndAlert('movement', tracked, {
35272
+ fromCameraId: lastSighting.cameraId,
35273
+ fromCameraName: lastSighting.cameraName,
35274
+ toCameraId: sighting.cameraId,
35275
+ toCameraName: sighting.cameraName,
35276
+ transitTime: transitDuration,
35277
+ objectClass: sighting.detection.className,
35278
+ objectLabel: llmDescription || sighting.detection.label,
35279
+ detectionId: sighting.detectionId,
35280
+ });
35281
+ this.recordAlertTime(tracked.globalId);
35282
+ }
35192
35283
  }
35193
35284
  // Add sighting to tracked object
35194
35285
  this.state.addSighting(tracked.globalId, sighting);
@@ -35212,14 +35303,17 @@ class TrackingEngine {
35212
35303
  this.console.log(`New ${sighting.detection.className} detected on ${sighting.cameraName} ` +
35213
35304
  `(ID: ${globalId.slice(0, 8)})`);
35214
35305
  // Generate entry alert if this is an entry point
35215
- if (isEntryPoint) {
35306
+ // Entry alerts also respect loitering threshold and cooldown
35307
+ if (isEntryPoint && this.passesLoiteringThreshold(tracked) && !this.isInAlertCooldown(globalId)) {
35308
+ const llmDescription = await this.getLlmDescription(tracked, 'outside', sighting.cameraName, sighting.cameraId);
35216
35309
  await this.alertManager.checkAndAlert('property_entry', tracked, {
35217
35310
  cameraId: sighting.cameraId,
35218
35311
  cameraName: sighting.cameraName,
35219
35312
  objectClass: sighting.detection.className,
35220
- objectLabel: sighting.detection.label,
35313
+ objectLabel: llmDescription || sighting.detection.label,
35221
35314
  detectionId: sighting.detectionId,
35222
35315
  });
35316
+ this.recordAlertTime(globalId);
35223
35317
  }
35224
35318
  }
35225
35319
  }
@@ -36072,6 +36166,28 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
36072
36166
  description: 'Use visual embeddings for object correlation (requires compatible detectors)',
36073
36167
  group: 'Tracking',
36074
36168
  },
36169
+ loiteringThreshold: {
36170
+ title: 'Loitering Threshold (seconds)',
36171
+ type: 'number',
36172
+ defaultValue: 3,
36173
+ description: 'Object must be visible for this duration before triggering movement alerts',
36174
+ group: 'Tracking',
36175
+ },
36176
+ objectAlertCooldown: {
36177
+ title: 'Per-Object Alert Cooldown (seconds)',
36178
+ type: 'number',
36179
+ defaultValue: 30,
36180
+ description: 'Minimum time between alerts for the same tracked object',
36181
+ group: 'Tracking',
36182
+ },
36183
+ // LLM Integration
36184
+ useLlmDescriptions: {
36185
+ title: 'Use LLM for Rich Descriptions',
36186
+ type: 'boolean',
36187
+ defaultValue: true,
36188
+ description: 'Use LLM plugin (if installed) to generate descriptive alerts like "Man walking from garage towards front door"',
36189
+ group: 'Tracking',
36190
+ },
36075
36191
  // MQTT Settings
36076
36192
  enableMqtt: {
36077
36193
  title: 'Enable MQTT',
@@ -36108,10 +36224,12 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
36108
36224
  defaultValue: true,
36109
36225
  group: 'Alerts',
36110
36226
  },
36111
- defaultNotifier: {
36112
- title: 'Default Notifier',
36227
+ defaultNotifiers: {
36228
+ title: 'Notifiers',
36113
36229
  type: 'device',
36230
+ multiple: true,
36114
36231
  deviceFilter: `interfaces.includes('${sdk_1.ScryptedInterface.Notifier}')`,
36232
+ description: 'Select one or more notifiers to receive alerts',
36115
36233
  group: 'Alerts',
36116
36234
  },
36117
36235
  // Tracked Cameras
@@ -36212,6 +36330,9 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
36212
36330
  correlationThreshold: this.storageSettings.values.correlationThreshold || 0.6,
36213
36331
  lostTimeout: (this.storageSettings.values.lostTimeout || 300) * 1000,
36214
36332
  useVisualMatching: this.storageSettings.values.useVisualMatching ?? true,
36333
+ loiteringThreshold: (this.storageSettings.values.loiteringThreshold || 3) * 1000,
36334
+ objectAlertCooldown: (this.storageSettings.values.objectAlertCooldown || 30) * 1000,
36335
+ useLlmDescriptions: this.storageSettings.values.useLlmDescriptions ?? true,
36215
36336
  };
36216
36337
  this.trackingEngine = new tracking_engine_1.TrackingEngine(topology, this.trackingState, this.alertManager, config, this.console);
36217
36338
  await this.trackingEngine.startTracking();
@@ -36450,7 +36571,10 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
36450
36571
  key === 'correlationWindow' ||
36451
36572
  key === 'correlationThreshold' ||
36452
36573
  key === 'lostTimeout' ||
36453
- key === 'useVisualMatching') {
36574
+ key === 'useVisualMatching' ||
36575
+ key === 'loiteringThreshold' ||
36576
+ key === 'objectAlertCooldown' ||
36577
+ key === 'useLlmDescriptions') {
36454
36578
  const topologyJson = this.storage.getItem('topology');
36455
36579
  if (topologyJson) {
36456
36580
  try {