pumaguard 21.post13__py3-none-any.whl → 21.post16__py3-none-any.whl

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.
@@ -4,7 +4,13 @@ from __future__ import (
4
4
  annotations,
5
5
  )
6
6
 
7
+ import json
7
8
  import logging
9
+ import queue
10
+ import threading
11
+ from collections.abc import (
12
+ Callable,
13
+ )
8
14
  from datetime import (
9
15
  datetime,
10
16
  timezone,
@@ -14,8 +20,10 @@ from typing import (
14
20
  )
15
21
 
16
22
  from flask import (
23
+ Response,
17
24
  jsonify,
18
25
  request,
26
+ stream_with_context,
19
27
  )
20
28
 
21
29
  if TYPE_CHECKING:
@@ -30,8 +38,44 @@ if TYPE_CHECKING:
30
38
  logger = logging.getLogger(__name__)
31
39
 
32
40
 
33
- def register_dhcp_routes(app: "Flask", webui: "WebUI") -> None:
34
- """Register DHCP event endpoints for camera detection."""
41
+ def register_dhcp_routes(
42
+ app: "Flask", webui: "WebUI"
43
+ ) -> Callable[[str, dict], None]:
44
+ """
45
+ Register DHCP event endpoints for camera detection.
46
+
47
+ Returns:
48
+ Callback function to notify SSE clients of camera changes
49
+ """
50
+
51
+ # Queue for SSE notifications
52
+ sse_clients: list[queue.Queue] = []
53
+ sse_clients_lock = threading.Lock()
54
+
55
+ def notify_camera_change(event_type: str, camera_data: dict) -> None:
56
+ """Notify all SSE clients about a camera status change."""
57
+ message = {
58
+ "type": event_type,
59
+ "data": camera_data,
60
+ "timestamp": datetime.now(timezone.utc).strftime(
61
+ "%Y-%m-%dT%H:%M:%SZ"
62
+ ),
63
+ }
64
+
65
+ with sse_clients_lock:
66
+ disconnected_clients = []
67
+ for client_queue in sse_clients:
68
+ try:
69
+ # Non-blocking put with timeout
70
+ client_queue.put(message, block=False)
71
+ except queue.Full:
72
+ # Client queue is full, mark for removal
73
+ disconnected_clients.append(client_queue)
74
+
75
+ # Remove disconnected clients
76
+ for client_queue in disconnected_clients:
77
+ sse_clients.remove(client_queue)
78
+ logger.debug("Removed disconnected SSE client")
35
79
 
36
80
  @app.route("/api/dhcp/event", methods=["POST"])
37
81
  def dhcp_event():
@@ -85,6 +129,11 @@ def register_dhcp_routes(app: "Flask", webui: "WebUI") -> None:
85
129
  "status": "connected",
86
130
  }
87
131
 
132
+ # Notify SSE clients
133
+ notify_camera_change(
134
+ "camera_connected", dict(webui.cameras[mac_address])
135
+ )
136
+
88
137
  # Update settings with camera list
89
138
  # Convert cameras dict to list for settings persistence
90
139
  camera_list = []
@@ -118,6 +167,11 @@ def register_dhcp_routes(app: "Flask", webui: "WebUI") -> None:
118
167
  webui.cameras[mac_address]["status"] = "disconnected"
119
168
  webui.cameras[mac_address]["last_seen"] = timestamp
120
169
 
170
+ # Notify SSE clients
171
+ notify_camera_change(
172
+ "camera_disconnected", dict(webui.cameras[mac_address])
173
+ )
174
+
121
175
  # Update settings with updated camera list
122
176
  camera_list = []
123
177
  for _, cam_info in webui.cameras.items():
@@ -287,6 +341,11 @@ def register_dhcp_routes(app: "Flask", webui: "WebUI") -> None:
287
341
  "Failed to save camera list to settings: %s", str(e)
288
342
  )
289
343
 
344
+ # Notify SSE clients
345
+ notify_camera_change(
346
+ "camera_added", dict(webui.cameras[mac_address])
347
+ )
348
+
290
349
  return (
291
350
  jsonify(
292
351
  {
@@ -386,3 +445,79 @@ def register_dhcp_routes(app: "Flask", webui: "WebUI") -> None:
386
445
  ),
387
446
  500,
388
447
  )
448
+
449
+ @app.route("/api/dhcp/cameras/events", methods=["GET"])
450
+ def camera_events():
451
+ """
452
+ Server-Sent Events (SSE) endpoint for real-time camera status updates.
453
+
454
+ This endpoint provides a stream of camera status changes including:
455
+ - camera_connected: A camera has connected
456
+ - camera_disconnected: A camera has disconnected
457
+ - camera_added: A camera was manually added
458
+ - camera_status_changed: Camera status changed (from heartbeat)
459
+
460
+ The stream sends JSON-formatted events with the following structure:
461
+ {
462
+ "type": "event_type",
463
+ "data": {...camera data...},
464
+ "timestamp": "ISO8601 timestamp"
465
+ }
466
+ """
467
+
468
+ def event_stream():
469
+ # Create a queue for this client
470
+ client_queue: queue.Queue = queue.Queue(maxsize=10)
471
+
472
+ with sse_clients_lock:
473
+ sse_clients.append(client_queue)
474
+
475
+ logger.info(
476
+ "SSE client connected, total clients: %d", len(sse_clients)
477
+ )
478
+
479
+ try:
480
+ # Send initial connection message
481
+ # Send initial connection message
482
+ initial_msg = {
483
+ "type": "connected",
484
+ "timestamp": datetime.now(timezone.utc).strftime(
485
+ "%Y-%m-%dT%H:%M:%SZ"
486
+ ),
487
+ }
488
+ yield f"data: {json.dumps(initial_msg)}\n\n"
489
+
490
+ # Stream events to client
491
+ while True:
492
+ try:
493
+ # Wait for messages with timeout for periodic keepalive
494
+ message = client_queue.get(timeout=30)
495
+ yield f"data: {json.dumps(message)}\n\n"
496
+ except queue.Empty:
497
+ # Send keepalive comment to prevent connection timeout
498
+ yield ": keepalive\n\n"
499
+ except GeneratorExit:
500
+ # Client disconnected
501
+ logger.info("SSE client disconnected")
502
+ finally:
503
+ # Remove this client from the list
504
+ with sse_clients_lock:
505
+ if client_queue in sse_clients:
506
+ sse_clients.remove(client_queue)
507
+ logger.info(
508
+ "SSE client removed, remaining clients: %d",
509
+ len(sse_clients),
510
+ )
511
+
512
+ return Response(
513
+ stream_with_context(event_stream()),
514
+ mimetype="text/event-stream",
515
+ headers={
516
+ "Cache-Control": "no-cache",
517
+ "X-Accel-Buffering": "no",
518
+ "Connection": "keep-alive",
519
+ },
520
+ )
521
+
522
+ # Return the notification callback so it can be wired to heartbeat
523
+ return notify_camera_change
pumaguard/web_ui.py CHANGED
@@ -164,7 +164,7 @@ class WebUI:
164
164
  # Format: {mac_address: CameraInfo}
165
165
  self.cameras: dict[str, CameraInfo] = {}
166
166
 
167
- # Camera heartbeat monitoring
167
+ # Camera heartbeat monitoring (callback set after routes registered)
168
168
  self.heartbeat: CameraHeartbeat = CameraHeartbeat(
169
169
  webui=self,
170
170
  interval=presets.camera_heartbeat_interval,
@@ -173,6 +173,8 @@ class WebUI:
173
173
  tcp_port=presets.camera_heartbeat_tcp_port,
174
174
  tcp_timeout=presets.camera_heartbeat_tcp_timeout,
175
175
  icmp_timeout=presets.camera_heartbeat_icmp_timeout,
176
+ # Will be set after routes are registered
177
+ status_change_callback=None,
176
178
  )
177
179
 
178
180
  # Load cameras from persisted settings
@@ -284,7 +286,11 @@ class WebUI:
284
286
 
285
287
  register_diagnostics_routes(self.app, self)
286
288
 
287
- register_dhcp_routes(self.app, self)
289
+ # Register DHCP routes and get the SSE notification callback
290
+ camera_notification_callback = register_dhcp_routes(self.app, self)
291
+
292
+ # Wire the callback to the heartbeat monitor
293
+ self.heartbeat.status_change_callback = camera_notification_callback
288
294
 
289
295
  @self.app.route("/<path:path>")
290
296
  def serve_static(path: str):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pumaguard
3
- Version: 21.post13
3
+ Version: 21.post16
4
4
  Author-email: Nicolas Bock <nicolasbock@gmail.com>
5
5
  Project-URL: Homepage, http://pumaguard.rtfd.io/
6
6
  Project-URL: Repository, https://github.com/PEEC-Nature-Youth-Group/pumaguard
@@ -1,5 +1,5 @@
1
1
  pumaguard/__init__.py,sha256=QUsYaH1hG9RKygFZfaS5tpe9pgN_1Kf7EMroQCVlQfE,421
2
- pumaguard/camera_heartbeat.py,sha256=vfdprxBnVDF-_iA9MUaI2tVh4AbeqP1j09xKqT2N25Y,10291
2
+ pumaguard/camera_heartbeat.py,sha256=-prAAzR29Cqhp2AL3xEgB4o6RmXEON2ezgkcG_1MkN8,11343
3
3
  pumaguard/classify.py,sha256=QnHfnIlkzSxHGy0dgqT6IeTbRBboRT3c3B_YC3YpqfI,1057
4
4
  pumaguard/lock_manager.py,sha256=sYLl8Z7miOr_9R7ehoSFlKf8tToHNNUT6aH9Mlw2M9g,1608
5
5
  pumaguard/main.py,sha256=1Wazv1wjwb46yNlqgWt88HQwKSxGmY24X5OsUv8gYyE,7029
@@ -12,7 +12,7 @@ pumaguard/sound.py,sha256=-ceyO9xjfuVSal-CvaM_o2l45gYWUUV3bJN-Eni2XQQ,4732
12
12
  pumaguard/stats.py,sha256=ZwocfnFCQ-ky7me-YTTrEoJqsIHOWAgSzeoJHItsIU4,927
13
13
  pumaguard/utils.py,sha256=w1EgOLSZGyjq_b49hvVZhBESy-lVP0yRtNHe-sXBoIU,19735
14
14
  pumaguard/verify.py,sha256=vfw3PRzDt1uuH5FKV9F5vb1PH7KQ6AEgVNhJ6jck_hQ,5513
15
- pumaguard/web_ui.py,sha256=N51XEKBuCJfBWUsm4EqvzpS1EG5ey6Z2kBlKnCitRqQ,17332
15
+ pumaguard/web_ui.py,sha256=HPecXKkWCA-HXiCRGrSwaP79HKYDvh07Ymk3xJwDQXc,17697
16
16
  pumaguard/completions/pumaguard-classify-completions.sh,sha256=5QySg-2Jdinj15qpUYa5UzHbTgYzi2gmPVYYyyXny4c,1353
17
17
  pumaguard/completions/pumaguard-completions.sh,sha256=bRx3Q3_gM__3w0PyfQSCVdxylhhr3QlzaLCav24dfNc,1196
18
18
  pumaguard/completions/pumaguard-server-completions.sh,sha256=33c6GjbTImBOHn0SSNUOJoxqJ2mMHuDv3P3GQJGGHhA,1161
@@ -23,7 +23,7 @@ pumaguard/pumaguard-ui/flutter.js,sha256=7V1ZIKmGiouT15CpquQWWmKWJyjUq77FoU9gDXP
23
23
  pumaguard/pumaguard-ui/flutter_bootstrap.js,sha256=Nyt0e7m8U4hq4CmcHhK1ij4hNgUqANM1o4YieKUx4rA,9692
24
24
  pumaguard/pumaguard-ui/flutter_service_worker.js,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  pumaguard/pumaguard-ui/index.html,sha256=901-ZY0WysVAZWPwj2xGatoezwm9TX9IV_jpMrlsaXg,1205
26
- pumaguard/pumaguard-ui/main.dart.js,sha256=WO_7npcxA3OOln28y_R0Dz1wJSMmwY7vf1u-upDtcDk,2729035
26
+ pumaguard/pumaguard-ui/main.dart.js,sha256=5OLnww5PiKlfAjDnHjWtjacrZ6aiJy7GgerfAjOER78,2743113
27
27
  pumaguard/pumaguard-ui/manifest.json,sha256=Hhnw_eLUivdrOlL7O9KGBsGXCKKt3lix17Fh3GB0g-s,920
28
28
  pumaguard/pumaguard-ui/version.json,sha256=uXZ6musTJUZaO0N2bEbr3cy9rpx2aesAS2YFMcu2WF8,94
29
29
  pumaguard/pumaguard-ui/assets/AssetManifest.bin,sha256=Qzp1G9iPlHSW-PnHyszTxZO31_NjmTlvSBWY_REPH_8,562
@@ -59,14 +59,14 @@ pumaguard/pumaguard-ui/icons/Icon-maskable-192.png,sha256=0shC4iqfTsnZlrIzc6kFyI
59
59
  pumaguard/pumaguard-ui/icons/Icon-maskable-512.png,sha256=au4Gzcq2sq73Sxc0xHePRCHS2hALD_nlKyG1UkAgKSk,20998
60
60
  pumaguard/web_routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
61
  pumaguard/web_routes/artifacts.py,sha256=IpnMLdbgAYkwU3TuYJE-JHGnC_x5_XNCrc-1M_n2YKk,3879
62
- pumaguard/web_routes/dhcp.py,sha256=YoF-987Ro1DqVBu1szloIkW-XQx81-WMKgqk83dNoRc,12607
62
+ pumaguard/web_routes/dhcp.py,sha256=Fr3Zx8vA_6q1v3F37v4TlDlDM-wjBwo9FpfxOZ72zuo,17265
63
63
  pumaguard/web_routes/diagnostics.py,sha256=EIIbjuixJyGXdnVQf8RQ6xQxJar0UHZO8dF-9zQLY9g,3294
64
64
  pumaguard/web_routes/directories.py,sha256=yy5TghCEyB4reRGAcVHIEfr2vlHnuiDChIXl9ZFquRM,2410
65
65
  pumaguard/web_routes/folders.py,sha256=Z63ap6dRi6NWye70HYurpCnsSXmFgzTbTsFKYdZ1Bjk,6305
66
66
  pumaguard/web_routes/photos.py,sha256=Tac_CbaZSeZzOfaJ73vlp3iyZbvfD7ei1YM3tsb0nTY,5106
67
67
  pumaguard/web_routes/settings.py,sha256=GA7MERNRRnR2lfrG-aSRx8bJO5OTqVzyQTYSqPYP8wc,11754
68
68
  pumaguard/web_routes/sync.py,sha256=Zvv6VARGE5xP29C5gWH3ul81PISRxoF8n472DITItE0,6378
69
- pumaguard-21.post13.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
69
+ pumaguard-21.post16.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
70
70
  pumaguard-sounds/cougar_call.mp3,sha256=jdPzi7Qneect3ez2G6XAeHWtetU5vSOSB6pceuB26Wc,129048
71
71
  pumaguard-sounds/cougarsounds.wav,sha256=hwVmmQ75dkOP3qd07YAvVOSm1neYtxLSzxw3Ulvs2cM,96346
72
72
  pumaguard-sounds/dark-engine-logo-141942.mp3,sha256=Vw-qyLTMPJZvsgQcZtH0DpGcP1dd7nJq-9BnHuNPGug,372819
@@ -84,8 +84,8 @@ pumaguard-sounds/mixkit-vintage-telephone-ringtone-1356.wav,sha256=zWWY2uFF0-l7P
84
84
  pumaguard-sounds/pumaguard-warning.mp3,sha256=wcCfHsulPo5P5s8MjpQAG2NYHQDsRpjqoMig1-o_MDI,232249
85
85
  pumaguard-sounds/short-round-110940.mp3,sha256=vdskGD94SeH1UJyJyR0Ek_7xGXPIZfnPdoBvxGnUt98,450816
86
86
  pumaguard-ui/ios/Flutter/ephemeral/flutter_lldb_helper.py,sha256=Bc_jl3_e5ZPvrSBJpPYtN05VxpztyKq-7lVms3rLg4Q,1276
87
- pumaguard-21.post13.dist-info/METADATA,sha256=i0rqW0PfthiI_pNCAfyRC0n1Cra2OCGbiSrBaNBd3bI,8617
88
- pumaguard-21.post13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
89
- pumaguard-21.post13.dist-info/entry_points.txt,sha256=rmCdBTPWrbJQvPPwABSVobXE9D7hrKsITGZ6nvCrko8,127
90
- pumaguard-21.post13.dist-info/top_level.txt,sha256=B-PzS4agkQNhOYbLLIrMVOyMD_pl5F-yujPBm5zYYjY,40
91
- pumaguard-21.post13.dist-info/RECORD,,
87
+ pumaguard-21.post16.dist-info/METADATA,sha256=yAdAikLIAgVmeNJV0rP6PLnPLt3Z4va3C54H25oaExI,8617
88
+ pumaguard-21.post16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
89
+ pumaguard-21.post16.dist-info/entry_points.txt,sha256=rmCdBTPWrbJQvPPwABSVobXE9D7hrKsITGZ6nvCrko8,127
90
+ pumaguard-21.post16.dist-info/top_level.txt,sha256=B-PzS4agkQNhOYbLLIrMVOyMD_pl5F-yujPBm5zYYjY,40
91
+ pumaguard-21.post16.dist-info/RECORD,,