@blueharford/scrypted-spatial-awareness 0.6.2 → 0.6.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/dist/plugin.zip CHANGED
Binary file
@@ -36081,13 +36081,15 @@ Identify:
36081
36081
  2. Camera connections - How someone could move between camera views and estimated walking time
36082
36082
  3. Overall layout - Describe the property layout based on what you see
36083
36083
 
36084
+ IMPORTANT: For camera references, use the EXACT device ID shown in parentheses (e.g., "device_123"), NOT the camera name.
36085
+
36084
36086
  Respond with ONLY valid JSON:
36085
36087
  {
36086
36088
  "sharedLandmarks": [
36087
- {"name": "Driveway", "type": "access", "seenByCameras": ["camera1", "camera2"], "confidence": 0.8, "description": "Concrete driveway"}
36089
+ {"name": "Driveway", "type": "access", "seenByCameras": ["device_123", "device_456"], "confidence": 0.8, "description": "Concrete driveway"}
36088
36090
  ],
36089
36091
  "connections": [
36090
- {"from": "camera1", "to": "camera2", "transitSeconds": 10, "via": "driveway", "confidence": 0.7, "bidirectional": true}
36092
+ {"from": "device_123", "to": "device_456", "transitSeconds": 10, "via": "driveway", "confidence": 0.7, "bidirectional": true}
36091
36093
  ],
36092
36094
  "layoutDescription": "Single-story house with front yard facing street, driveway on the left side, backyard accessible through side gate"
36093
36095
  }`;
@@ -36410,6 +36412,36 @@ class TopologyDiscoveryEngine {
36410
36412
  return 'west';
36411
36413
  return 'unknown';
36412
36414
  }
36415
+ /** Resolve a camera reference (name or deviceId) to its deviceId */
36416
+ resolveCameraRef(ref) {
36417
+ if (!this.topology?.cameras || !ref)
36418
+ return null;
36419
+ // Try exact deviceId match first
36420
+ const byId = this.topology.cameras.find(c => c.deviceId === ref);
36421
+ if (byId)
36422
+ return byId.deviceId;
36423
+ // Try exact name match
36424
+ const byName = this.topology.cameras.find(c => c.name === ref);
36425
+ if (byName)
36426
+ return byName.deviceId;
36427
+ // Try case-insensitive name match
36428
+ const refLower = ref.toLowerCase();
36429
+ const byNameLower = this.topology.cameras.find(c => c.name.toLowerCase() === refLower);
36430
+ if (byNameLower)
36431
+ return byNameLower.deviceId;
36432
+ // Try partial name match (LLM might truncate or abbreviate)
36433
+ const byPartial = this.topology.cameras.find(c => c.name.toLowerCase().includes(refLower) || refLower.includes(c.name.toLowerCase()));
36434
+ if (byPartial)
36435
+ return byPartial.deviceId;
36436
+ this.console.warn(`[Discovery] Could not resolve camera reference: "${ref}"`);
36437
+ return null;
36438
+ }
36439
+ /** Normalize camera references in an array to deviceIds */
36440
+ normalizeCameraRefs(refs) {
36441
+ return refs
36442
+ .map(ref => this.resolveCameraRef(ref))
36443
+ .filter((id) => id !== null);
36444
+ }
36413
36445
  /** Analyze all cameras and correlate findings */
36414
36446
  async runFullDiscovery() {
36415
36447
  if (!this.topology?.cameras?.length) {
@@ -36502,23 +36534,41 @@ class TopologyDiscoveryEngine {
36502
36534
  timestamp: Date.now(),
36503
36535
  };
36504
36536
  if (Array.isArray(parsed.sharedLandmarks)) {
36505
- correlation.sharedLandmarks = parsed.sharedLandmarks.map((l) => ({
36506
- name: l.name || 'Unknown',
36507
- type: this.mapLandmarkType(l.type),
36508
- seenByCameras: Array.isArray(l.seenByCameras) ? l.seenByCameras : [],
36509
- confidence: typeof l.confidence === 'number' ? l.confidence : 0.7,
36510
- description: l.description,
36511
- }));
36537
+ correlation.sharedLandmarks = parsed.sharedLandmarks.map((l) => {
36538
+ // Normalize camera references to deviceIds
36539
+ const rawRefs = Array.isArray(l.seenByCameras) ? l.seenByCameras : [];
36540
+ const normalizedRefs = this.normalizeCameraRefs(rawRefs);
36541
+ if (rawRefs.length > 0 && normalizedRefs.length === 0) {
36542
+ this.console.warn(`[Discovery] Landmark "${l.name}" has no resolvable camera refs: ${JSON.stringify(rawRefs)}`);
36543
+ }
36544
+ return {
36545
+ name: l.name || 'Unknown',
36546
+ type: this.mapLandmarkType(l.type),
36547
+ seenByCameras: normalizedRefs,
36548
+ confidence: typeof l.confidence === 'number' ? l.confidence : 0.7,
36549
+ description: l.description,
36550
+ };
36551
+ });
36512
36552
  }
36513
36553
  if (Array.isArray(parsed.connections)) {
36514
- correlation.suggestedConnections = parsed.connections.map((c) => ({
36515
- fromCameraId: c.from || c.fromCameraId || '',
36516
- toCameraId: c.to || c.toCameraId || '',
36517
- transitSeconds: typeof c.transitSeconds === 'number' ? c.transitSeconds : 15,
36518
- via: c.via || '',
36519
- confidence: typeof c.confidence === 'number' ? c.confidence : 0.6,
36520
- bidirectional: c.bidirectional !== false,
36521
- }));
36554
+ correlation.suggestedConnections = parsed.connections.map((c) => {
36555
+ // Normalize camera references to deviceIds
36556
+ const fromRef = c.from || c.fromCameraId || '';
36557
+ const toRef = c.to || c.toCameraId || '';
36558
+ const fromId = this.resolveCameraRef(fromRef);
36559
+ const toId = this.resolveCameraRef(toRef);
36560
+ if (!fromId || !toId) {
36561
+ this.console.warn(`[Discovery] Connection has unresolvable camera refs: from="${fromRef}" to="${toRef}"`);
36562
+ }
36563
+ return {
36564
+ fromCameraId: fromId || fromRef,
36565
+ toCameraId: toId || toRef,
36566
+ transitSeconds: typeof c.transitSeconds === 'number' ? c.transitSeconds : 15,
36567
+ via: c.via || '',
36568
+ confidence: typeof c.confidence === 'number' ? c.confidence : 0.6,
36569
+ bidirectional: c.bidirectional !== false,
36570
+ };
36571
+ });
36522
36572
  }
36523
36573
  this.console.log(`[Discovery] Correlation found ${correlation.sharedLandmarks.length} shared landmarks, ${correlation.suggestedConnections.length} connections`);
36524
36574
  return correlation;
@@ -40227,9 +40277,28 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40227
40277
  // Calculate position for the landmark WITHIN the camera's field of view
40228
40278
  let position = suggestion.landmark.position;
40229
40279
  if (!position || (position.x === 0 && position.y === 0)) {
40280
+ // Debug logging
40281
+ this.console.log(`[Discovery] Processing landmark "${suggestion.landmark.name}"`);
40282
+ this.console.log(`[Discovery] sourceCameras: ${JSON.stringify(suggestion.sourceCameras)}`);
40283
+ this.console.log(`[Discovery] visibleFromCameras: ${JSON.stringify(suggestion.landmark.visibleFromCameras)}`);
40284
+ this.console.log(`[Discovery] Available cameras: ${topology.cameras.map(c => `${c.name}(${c.deviceId})`).join(', ')}`);
40230
40285
  // Find a camera that can see this landmark
40231
- const visibleCameraId = suggestion.landmark.visibleFromCameras?.[0];
40232
- const camera = visibleCameraId ? topology.cameras.find(c => c.deviceId === visibleCameraId) : null;
40286
+ // PREFER sourceCameras (set from analysis.cameraId) over visibleFromCameras (from LLM parsing)
40287
+ const sourceCameraRef = suggestion.sourceCameras?.[0];
40288
+ const visibleCameraRef = suggestion.landmark.visibleFromCameras?.[0];
40289
+ const cameraRef = sourceCameraRef || visibleCameraRef;
40290
+ // Use flexible matching (deviceId, name, or case-insensitive)
40291
+ const camera = cameraRef ? topology.cameras.find(c => c.deviceId === cameraRef ||
40292
+ c.name === cameraRef ||
40293
+ c.name.toLowerCase() === cameraRef.toLowerCase()) : null;
40294
+ if (camera) {
40295
+ this.console.log(`[Discovery] Matched camera: ${camera.name} (${camera.deviceId})`);
40296
+ this.console.log(`[Discovery] Camera position: ${JSON.stringify(camera.floorPlanPosition)}`);
40297
+ this.console.log(`[Discovery] Camera FOV: ${JSON.stringify(camera.fov)}`);
40298
+ }
40299
+ else {
40300
+ this.console.warn(`[Discovery] No camera matched for ref="${cameraRef}" (source="${sourceCameraRef}", visible="${visibleCameraRef}")`);
40301
+ }
40233
40302
  if (camera?.floorPlanPosition) {
40234
40303
  // Get camera's FOV direction and range (cast to any for flexible access)
40235
40304
  const fov = (camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 });
@@ -40237,7 +40306,9 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40237
40306
  const range = fov.range || 80;
40238
40307
  const fovAngle = fov.angle || 90;
40239
40308
  // Count existing landmarks from this camera to spread them out
40240
- const existingFromCamera = (topology.landmarks || []).filter(l => l.visibleFromCameras?.includes(visibleCameraId)).length;
40309
+ const cameraDeviceId = camera.deviceId;
40310
+ const existingFromCamera = (topology.landmarks || []).filter(l => l.visibleFromCameras?.includes(cameraDeviceId) ||
40311
+ l.visibleFromCameras?.includes(camera.name)).length;
40241
40312
  // Calculate position in front of camera within its FOV
40242
40313
  // Convert direction to radians (0 = up/north, 90 = right/east)
40243
40314
  const dirRad = (direction - 90) * Math.PI / 180;
@@ -40287,9 +40358,19 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40287
40358
  const zone = suggestion.zone;
40288
40359
  // Find cameras that see this zone
40289
40360
  const sourceCameras = suggestion.sourceCameras || [];
40290
- const camera = sourceCameras[0]
40291
- ? topology.cameras.find(c => c.deviceId === sourceCameras[0] || c.name === sourceCameras[0])
40361
+ const cameraRef = sourceCameras[0];
40362
+ const camera = cameraRef
40363
+ ? topology.cameras.find(c => c.deviceId === cameraRef ||
40364
+ c.name === cameraRef ||
40365
+ c.name.toLowerCase() === cameraRef.toLowerCase())
40292
40366
  : null;
40367
+ this.console.log(`[Discovery] Processing zone "${zone.name}" from camera ref="${cameraRef}"`);
40368
+ if (camera) {
40369
+ this.console.log(`[Discovery] Matched camera: ${camera.name} (${camera.deviceId})`);
40370
+ }
40371
+ else if (cameraRef) {
40372
+ this.console.warn(`[Discovery] No camera matched for zone source ref="${cameraRef}"`);
40373
+ }
40293
40374
  // Create zone polygon WITHIN the camera's field of view
40294
40375
  let polygon = [];
40295
40376
  const timestamp = Date.now();
@@ -40361,9 +40442,13 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40361
40442
  const globalZoneType = this.mapZoneTypeToGlobalType(zone.type);
40362
40443
  const cameraZones = sourceCameras
40363
40444
  .map(camRef => {
40364
- const cam = topology.cameras.find(c => c.deviceId === camRef || c.name === camRef);
40365
- if (!cam)
40445
+ const cam = topology.cameras.find(c => c.deviceId === camRef ||
40446
+ c.name === camRef ||
40447
+ c.name.toLowerCase() === camRef.toLowerCase());
40448
+ if (!cam) {
40449
+ this.console.warn(`[Discovery] GlobalZone: No camera matched for ref="${camRef}"`);
40366
40450
  return null;
40451
+ }
40367
40452
  return {
40368
40453
  cameraId: cam.deviceId,
40369
40454
  zone: [[0, 0], [100, 0], [100, 100], [0, 100]], // Full frame default