@blueharford/scrypted-spatial-awareness 0.1.8 → 0.1.9

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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blueharford/scrypted-spatial-awareness",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Cross-camera object tracking for Scrypted NVR with spatial awareness",
5
5
  "author": "Joshua Seidel <blueharford>",
6
6
  "license": "Apache-2.0",
package/src/main.ts CHANGED
@@ -491,6 +491,10 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
491
491
  return this.handleAlertsRequest(request, response);
492
492
  }
493
493
 
494
+ if (path.endsWith('/api/cameras')) {
495
+ return this.handleCamerasRequest(response);
496
+ }
497
+
494
498
  if (path.endsWith('/api/floor-plan')) {
495
499
  return this.handleFloorPlanRequest(request, response);
496
500
  }
@@ -597,6 +601,36 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
597
601
  });
598
602
  }
599
603
 
604
+ private handleCamerasRequest(response: HttpResponse): void {
605
+ try {
606
+ // Get all devices with ObjectDetector interface
607
+ const cameras: { id: string; name: string }[] = [];
608
+
609
+ for (const id of Object.keys(systemManager.getSystemState())) {
610
+ try {
611
+ const device = systemManager.getDeviceById(id);
612
+ if (device && device.interfaces?.includes(ScryptedInterface.ObjectDetector)) {
613
+ cameras.push({
614
+ id: id,
615
+ name: device.name || `Camera ${id}`,
616
+ });
617
+ }
618
+ } catch (e) {
619
+ // Skip devices that can't be accessed
620
+ }
621
+ }
622
+
623
+ response.send(JSON.stringify(cameras), {
624
+ headers: { 'Content-Type': 'application/json' },
625
+ });
626
+ } catch (e) {
627
+ this.console.error('Error getting cameras:', e);
628
+ response.send(JSON.stringify([]), {
629
+ headers: { 'Content-Type': 'application/json' },
630
+ });
631
+ }
632
+ }
633
+
600
634
  private handleFloorPlanRequest(request: HttpRequest, response: HttpResponse): void {
601
635
  if (request.method === 'GET') {
602
636
  const imageData = this.storage.getItem('floorPlanImage');
@@ -260,7 +260,17 @@ export const EDITOR_HTML = `<!DOCTYPE html>
260
260
  }
261
261
 
262
262
  async function loadAvailableCameras() {
263
- availableCameras = topology.cameras.map(c => ({ id: c.deviceId, name: c.name }));
263
+ try {
264
+ const response = await fetch('../api/cameras');
265
+ if (response.ok) {
266
+ availableCameras = await response.json();
267
+ } else {
268
+ availableCameras = [];
269
+ }
270
+ } catch (e) {
271
+ console.error('Failed to load cameras:', e);
272
+ availableCameras = [];
273
+ }
264
274
  updateCameraSelects();
265
275
  }
266
276
 
@@ -431,14 +441,20 @@ export const EDITOR_HTML = `<!DOCTYPE html>
431
441
 
432
442
  function addCamera() {
433
443
  const deviceId = document.getElementById('camera-device-select').value;
434
- const name = document.getElementById('camera-name-input').value || 'New Camera';
444
+ if (!deviceId) {
445
+ alert('Please select a camera');
446
+ return;
447
+ }
448
+ const selectedCam = availableCameras.find(c => c.id === deviceId);
449
+ const customName = document.getElementById('camera-name-input').value;
450
+ const name = customName || (selectedCam ? selectedCam.name : 'New Camera');
435
451
  const isEntry = document.getElementById('camera-entry-checkbox').checked;
436
452
  const isExit = document.getElementById('camera-exit-checkbox').checked;
437
453
  // Use pending position from click, or default to center
438
454
  const pos = topology._pendingCameraPos || { x: canvas.width / 2 + Math.random() * 100 - 50, y: canvas.height / 2 + Math.random() * 100 - 50 };
439
455
  delete topology._pendingCameraPos;
440
456
  const camera = {
441
- deviceId: deviceId || 'camera-' + Date.now(),
457
+ deviceId: deviceId,
442
458
  nativeId: 'cam-' + Date.now(),
443
459
  name,
444
460
  isEntryPoint: isEntry,
@@ -448,6 +464,11 @@ export const EDITOR_HTML = `<!DOCTYPE html>
448
464
  };
449
465
  topology.cameras.push(camera);
450
466
  closeModal('add-camera-modal');
467
+ // Clear form
468
+ document.getElementById('camera-name-input').value = '';
469
+ document.getElementById('camera-entry-checkbox').checked = false;
470
+ document.getElementById('camera-exit-checkbox').checked = false;
471
+ updateCameraSelects();
451
472
  updateUI();
452
473
  render();
453
474
  }
@@ -459,6 +480,22 @@ export const EDITOR_HTML = `<!DOCTYPE html>
459
480
  }
460
481
 
461
482
  function updateCameraSelects() {
483
+ // Update camera device select (for adding new cameras)
484
+ const cameraDeviceSelect = document.getElementById('camera-device-select');
485
+ if (availableCameras.length > 0) {
486
+ const existingIds = topology.cameras.map(c => c.deviceId);
487
+ const available = availableCameras.filter(c => !existingIds.includes(c.id));
488
+ if (available.length > 0) {
489
+ cameraDeviceSelect.innerHTML = '<option value="">Select a camera...</option>' +
490
+ available.map(c => '<option value="' + c.id + '">' + c.name + '</option>').join('');
491
+ } else {
492
+ cameraDeviceSelect.innerHTML = '<option value="">All cameras already added</option>';
493
+ }
494
+ } else {
495
+ cameraDeviceSelect.innerHTML = '<option value="">No cameras with object detection found</option>';
496
+ }
497
+
498
+ // Update connection selects (for existing topology cameras)
462
499
  const options = topology.cameras.map(c => '<option value="' + c.deviceId + '">' + c.name + '</option>').join('');
463
500
  document.getElementById('connection-from-select').innerHTML = options;
464
501
  document.getElementById('connection-to-select').innerHTML = options;