@blueharford/scrypted-spatial-awareness 0.1.7 → 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.7",
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
@@ -335,63 +335,13 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
335
335
  async getSettings(): Promise<Setting[]> {
336
336
  const settings = await this.storageSettings.getSettings();
337
337
 
338
- // Topology editor button that opens modal overlay
338
+ // Topology editor button that opens modal overlay (appended to body for proper z-index)
339
339
  settings.push({
340
340
  key: 'topologyEditor',
341
341
  title: 'Topology Editor',
342
342
  type: 'html' as any,
343
343
  value: `
344
344
  <style>
345
- .sa-modal-overlay {
346
- display: none;
347
- position: fixed;
348
- top: 0;
349
- left: 0;
350
- right: 0;
351
- bottom: 0;
352
- background: rgba(0, 0, 0, 0.85);
353
- z-index: 999999;
354
- align-items: center;
355
- justify-content: center;
356
- }
357
- .sa-modal-overlay.active {
358
- display: flex;
359
- }
360
- .sa-modal-container {
361
- width: 95vw;
362
- height: 90vh;
363
- max-width: 1600px;
364
- background: #1a1a2e;
365
- border-radius: 12px;
366
- overflow: hidden;
367
- position: relative;
368
- box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
369
- }
370
- .sa-modal-close {
371
- position: absolute;
372
- top: 10px;
373
- right: 10px;
374
- z-index: 1000000;
375
- background: #e94560;
376
- color: white;
377
- border: none;
378
- width: 36px;
379
- height: 36px;
380
- border-radius: 50%;
381
- font-size: 20px;
382
- cursor: pointer;
383
- display: flex;
384
- align-items: center;
385
- justify-content: center;
386
- }
387
- .sa-modal-close:hover {
388
- background: #ff6b6b;
389
- }
390
- .sa-modal-iframe {
391
- width: 100%;
392
- height: 100%;
393
- border: none;
394
- }
395
345
  .sa-open-btn {
396
346
  background: linear-gradient(135deg, #e94560 0%, #0f3460 100%);
397
347
  color: white;
@@ -424,16 +374,30 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
424
374
  </style>
425
375
  <div class="sa-btn-container">
426
376
  <p class="sa-btn-desc">Configure camera positions, connections, and transit times</p>
427
- <button class="sa-open-btn" onclick="document.getElementById('sa-topology-modal').classList.add('active')">
377
+ <button class="sa-open-btn" onclick="window.openSATopologyEditor()">
428
378
  <span>&#9881;</span> Open Topology Editor
429
379
  </button>
430
380
  </div>
431
- <div id="sa-topology-modal" class="sa-modal-overlay" onclick="if(event.target===this)this.classList.remove('active')">
432
- <div class="sa-modal-container">
433
- <button class="sa-modal-close" onclick="document.getElementById('sa-topology-modal').classList.remove('active')">&times;</button>
434
- <iframe class="sa-modal-iframe" src="/endpoint/@blueharford/scrypted-spatial-awareness/ui/editor"></iframe>
435
- </div>
436
- </div>
381
+ <script>
382
+ window.openSATopologyEditor = function() {
383
+ // Remove existing modal if any
384
+ const existing = document.getElementById('sa-topology-modal');
385
+ if (existing) existing.remove();
386
+
387
+ // Create modal and append to body
388
+ const modal = document.createElement('div');
389
+ modal.id = 'sa-topology-modal';
390
+ 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
+ \`;
397
+ modal.onclick = function(e) { if (e.target === modal) modal.remove(); };
398
+ document.body.appendChild(modal);
399
+ };
400
+ </script>
437
401
  `,
438
402
  group: 'Topology',
439
403
  });
@@ -527,6 +491,10 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
527
491
  return this.handleAlertsRequest(request, response);
528
492
  }
529
493
 
494
+ if (path.endsWith('/api/cameras')) {
495
+ return this.handleCamerasRequest(response);
496
+ }
497
+
530
498
  if (path.endsWith('/api/floor-plan')) {
531
499
  return this.handleFloorPlanRequest(request, response);
532
500
  }
@@ -633,6 +601,36 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
633
601
  });
634
602
  }
635
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
+
636
634
  private handleFloorPlanRequest(request: HttpRequest, response: HttpResponse): void {
637
635
  if (request.method === 'GET') {
638
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;