@blueharford/scrypted-spatial-awareness 0.1.8 → 0.1.10

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.10",
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
@@ -380,20 +380,23 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
380
380
  </div>
381
381
  <script>
382
382
  window.openSATopologyEditor = function() {
383
- // Remove existing modal if any
384
- const existing = document.getElementById('sa-topology-modal');
383
+ var existing = document.getElementById('sa-topology-modal');
385
384
  if (existing) existing.remove();
386
-
387
- // Create modal and append to body
388
- const modal = document.createElement('div');
385
+ var modal = document.createElement('div');
389
386
  modal.id = 'sa-topology-modal';
390
387
  modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.9);z-index:2147483647;display:flex;align-items:center;justify-content:center;';
391
- modal.innerHTML = \`
392
- <div style="width:95vw;height:92vh;max-width:1800px;background:#1a1a2e;border-radius:12px;overflow:hidden;position:relative;box-shadow:0 25px 50px rgba(0,0,0,0.5);">
393
- <button onclick="document.getElementById('sa-topology-modal').remove()" style="position:absolute;top:15px;right:15px;z-index:2147483647;background:#e94560;color:white;border:none;width:40px;height:40px;border-radius:50%;font-size:24px;cursor:pointer;display:flex;align-items:center;justify-content:center;">&times;</button>
394
- <iframe src="/endpoint/@blueharford/scrypted-spatial-awareness/ui/editor" style="width:100%;height:100%;border:none;"></iframe>
395
- </div>
396
- \`;
388
+ var container = document.createElement('div');
389
+ container.style.cssText = 'width:95vw;height:92vh;max-width:1800px;background:#1a1a2e;border-radius:12px;overflow:hidden;position:relative;box-shadow:0 25px 50px rgba(0,0,0,0.5);';
390
+ var closeBtn = document.createElement('button');
391
+ closeBtn.innerHTML = '&times;';
392
+ closeBtn.style.cssText = 'position:absolute;top:15px;right:15px;z-index:2147483647;background:#e94560;color:white;border:none;width:40px;height:40px;border-radius:50%;font-size:24px;cursor:pointer;';
393
+ closeBtn.onclick = function() { modal.remove(); };
394
+ var iframe = document.createElement('iframe');
395
+ iframe.src = '/endpoint/@blueharford/scrypted-spatial-awareness/ui/editor';
396
+ iframe.style.cssText = 'width:100%;height:100%;border:none;';
397
+ container.appendChild(closeBtn);
398
+ container.appendChild(iframe);
399
+ modal.appendChild(container);
397
400
  modal.onclick = function(e) { if (e.target === modal) modal.remove(); };
398
401
  document.body.appendChild(modal);
399
402
  };
@@ -491,6 +494,10 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
491
494
  return this.handleAlertsRequest(request, response);
492
495
  }
493
496
 
497
+ if (path.endsWith('/api/cameras')) {
498
+ return this.handleCamerasRequest(response);
499
+ }
500
+
494
501
  if (path.endsWith('/api/floor-plan')) {
495
502
  return this.handleFloorPlanRequest(request, response);
496
503
  }
@@ -597,6 +604,36 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
597
604
  });
598
605
  }
599
606
 
607
+ private handleCamerasRequest(response: HttpResponse): void {
608
+ try {
609
+ // Get all devices with ObjectDetector interface
610
+ const cameras: { id: string; name: string }[] = [];
611
+
612
+ for (const id of Object.keys(systemManager.getSystemState())) {
613
+ try {
614
+ const device = systemManager.getDeviceById(id);
615
+ if (device && device.interfaces?.includes(ScryptedInterface.ObjectDetector)) {
616
+ cameras.push({
617
+ id: id,
618
+ name: device.name || `Camera ${id}`,
619
+ });
620
+ }
621
+ } catch (e) {
622
+ // Skip devices that can't be accessed
623
+ }
624
+ }
625
+
626
+ response.send(JSON.stringify(cameras), {
627
+ headers: { 'Content-Type': 'application/json' },
628
+ });
629
+ } catch (e) {
630
+ this.console.error('Error getting cameras:', e);
631
+ response.send(JSON.stringify([]), {
632
+ headers: { 'Content-Type': 'application/json' },
633
+ });
634
+ }
635
+ }
636
+
600
637
  private handleFloorPlanRequest(request: HttpRequest, response: HttpResponse): void {
601
638
  if (request.method === 'GET') {
602
639
  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;