@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/main.nodejs.js +1 -1
- package/dist/main.nodejs.js.map +1 -1
- package/dist/plugin.zip +0 -0
- package/out/main.nodejs.js +109 -24
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/core/topology-discovery.ts +71 -17
- package/src/main.ts +51 -7
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/out/main.nodejs.js
CHANGED
|
@@ -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": ["
|
|
36089
|
+
{"name": "Driveway", "type": "access", "seenByCameras": ["device_123", "device_456"], "confidence": 0.8, "description": "Concrete driveway"}
|
|
36088
36090
|
],
|
|
36089
36091
|
"connections": [
|
|
36090
|
-
{"from": "
|
|
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
|
-
|
|
36507
|
-
|
|
36508
|
-
|
|
36509
|
-
|
|
36510
|
-
|
|
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
|
-
|
|
36516
|
-
|
|
36517
|
-
|
|
36518
|
-
|
|
36519
|
-
|
|
36520
|
-
|
|
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
|
-
|
|
40232
|
-
const
|
|
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
|
|
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
|
|
40291
|
-
|
|
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 ||
|
|
40365
|
-
|
|
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
|