pumaguard 21.post12__py3-none-any.whl → 21.post15__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.
- pumaguard/camera_heartbeat.py +26 -0
- pumaguard/pumaguard-ui/main.dart.js +47091 -46599
- pumaguard/server.py +5 -0
- pumaguard/web_routes/dhcp.py +137 -2
- pumaguard/web_ui.py +8 -2
- {pumaguard-21.post12.dist-info → pumaguard-21.post15.dist-info}/METADATA +1 -1
- {pumaguard-21.post12.dist-info → pumaguard-21.post15.dist-info}/RECORD +11 -11
- {pumaguard-21.post12.dist-info → pumaguard-21.post15.dist-info}/WHEEL +0 -0
- {pumaguard-21.post12.dist-info → pumaguard-21.post15.dist-info}/entry_points.txt +0 -0
- {pumaguard-21.post12.dist-info → pumaguard-21.post15.dist-info}/licenses/LICENSE +0 -0
- {pumaguard-21.post12.dist-info → pumaguard-21.post15.dist-info}/top_level.txt +0 -0
pumaguard/server.py
CHANGED
|
@@ -20,6 +20,9 @@ from PIL import (
|
|
|
20
20
|
Image,
|
|
21
21
|
)
|
|
22
22
|
|
|
23
|
+
from pumaguard import (
|
|
24
|
+
__version__,
|
|
25
|
+
)
|
|
23
26
|
from pumaguard.lock_manager import (
|
|
24
27
|
acquire_lock,
|
|
25
28
|
)
|
|
@@ -430,6 +433,8 @@ def main(options: argparse.Namespace, presets: Preset):
|
|
|
430
433
|
signal.signal(signal.SIGTERM, handle_termination)
|
|
431
434
|
signal.signal(signal.SIGINT, handle_termination)
|
|
432
435
|
|
|
436
|
+
logger.info("Pumaguard version %s started", __version__)
|
|
437
|
+
|
|
433
438
|
try:
|
|
434
439
|
while True:
|
|
435
440
|
time.sleep(1)
|
pumaguard/web_routes/dhcp.py
CHANGED
|
@@ -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(
|
|
34
|
-
""
|
|
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
|
-
|
|
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,5 +1,5 @@
|
|
|
1
1
|
pumaguard/__init__.py,sha256=QUsYaH1hG9RKygFZfaS5tpe9pgN_1Kf7EMroQCVlQfE,421
|
|
2
|
-
pumaguard/camera_heartbeat.py,sha256
|
|
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
|
|
@@ -7,12 +7,12 @@ pumaguard/model-registry.yaml,sha256=V-pTaqJrk_jJo568okBDaVhs51qTHttSqKe6PqdX6Bc
|
|
|
7
7
|
pumaguard/model_cli.py,sha256=nzDv0lXSvRKpLxs579tiHInJPPV-AFO4jzeLk5t2GaA,1394
|
|
8
8
|
pumaguard/model_downloader.py,sha256=zJQgCMOF2AfhB30VsfOMYtgRxcxVxkZBAdtG8KznPyY,12895
|
|
9
9
|
pumaguard/presets.py,sha256=RziRfHRcRAA4ykNm9m2dfk7ko3xdpoS6natlAlzLJH4,28164
|
|
10
|
-
pumaguard/server.py,sha256=
|
|
10
|
+
pumaguard/server.py,sha256=MhZhQxGBTNiCSegxmVCfcGbIs3XX5BVZvTBxRrvsLL4,14810
|
|
11
11
|
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=
|
|
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=
|
|
26
|
+
pumaguard/pumaguard-ui/main.dart.js,sha256=o1zAaTzulSMEVOFIRcvGflrS4F0MiKGCyTgn0w-iZAM,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=
|
|
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.
|
|
69
|
+
pumaguard-21.post15.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.
|
|
88
|
-
pumaguard-21.
|
|
89
|
-
pumaguard-21.
|
|
90
|
-
pumaguard-21.
|
|
91
|
-
pumaguard-21.
|
|
87
|
+
pumaguard-21.post15.dist-info/METADATA,sha256=B-FrBx-CPyu_8heSlsv7pJY2wBPuiv6C80HQhWorBFg,8617
|
|
88
|
+
pumaguard-21.post15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
89
|
+
pumaguard-21.post15.dist-info/entry_points.txt,sha256=rmCdBTPWrbJQvPPwABSVobXE9D7hrKsITGZ6nvCrko8,127
|
|
90
|
+
pumaguard-21.post15.dist-info/top_level.txt,sha256=B-PzS4agkQNhOYbLLIrMVOyMD_pl5F-yujPBm5zYYjY,40
|
|
91
|
+
pumaguard-21.post15.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|