pumaguard 20.post244__py3-none-any.whl → 20.post247__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.
@@ -1 +1 @@
1
- 0ae99f9305ff2c86009e63f124cd92f1
1
+ 01c252faf73780a4cebf82557c853a5d
@@ -34,6 +34,6 @@ addEventListener("message", eventListener);
34
34
  if (!window._flutter) {
35
35
  window._flutter = {};
36
36
  }
37
- _flutter.buildConfig = {"engineRevision":"a5cb96369ef86c7e85abf5d662a1ca5d89775053","builds":[{"compileTarget":"dart2js","renderer":"canvaskit","mainJsPath":"main.dart.js"},{}],"useLocalCanvasKit":true};
37
+ _flutter.buildConfig = {"engineRevision":"1527ae0ec577a4ef50e65f6fefcfc1326707d9bf","builds":[{"compileTarget":"dart2js","renderer":"canvaskit","mainJsPath":"main.dart.js"},{}],"useLocalCanvasKit":true};
38
38
 
39
39
  _flutter.loader.load();
@@ -88888,7 +88888,7 @@ A.MN.prototype={
88888
88888
  K(a){var s,r=null,q=A.awp(B.ae,r,r,B.nE)
88889
88889
  q=A.qy(B.mB,r,new A.mb(r,r,r,r,2,r,new A.cM(A.hF(12),B.u)),q,"Roboto",!0)
88890
88890
  s=A.awp(B.al,r,r,B.nE)
88891
- return new A.Av(new A.va(new A.abI(),r,r,r,r,t.Gj),r,r,new A.zF(B.Hi,"PumaGuard v20-244-gf7678e7",q,A.qy(B.mB,r,new A.mb(r,r,r,r,2,r,new A.cM(A.hF(12),B.u)),s,"Roboto",!0),B.AQ,!1,r),r,t.Bp)}}
88891
+ return new A.Av(new A.va(new A.abI(),r,r,r,r,t.Gj),r,r,new A.zF(B.Hi,"PumaGuard v20-247-gf025667",q,A.qy(B.mB,r,new A.mb(r,r,r,r,2,r,new A.cM(A.hF(12),B.u)),s,"Roboto",!0),B.AQ,!1,r),r,t.Bp)}}
88892
88892
  A.abI.prototype={
88893
88893
  $1(a){return new A.m5()},
88894
88894
  $S:541}
@@ -89168,7 +89168,7 @@ q=A.aL("System Information",h,h,h,A.r(q).ok.r,h,h)
89168
89168
  s=i.d
89169
89169
  s=s==null?h:s.b
89170
89170
  s=i.tE("Backend Version",s==null?"Unknown":s)
89171
- n=i.tE("UI Version","v20-244-gf7678e7")
89171
+ n=i.tE("UI Version","v20-247-gf025667")
89172
89172
  l=i.d
89173
89173
  k=l==null
89174
89174
  j=k?h:l.d
@@ -0,0 +1,178 @@
1
+ """DHCP event routes for camera detection notifications."""
2
+
3
+ from __future__ import (
4
+ annotations,
5
+ )
6
+
7
+ import logging
8
+ from typing import (
9
+ TYPE_CHECKING,
10
+ )
11
+
12
+ from flask import (
13
+ jsonify,
14
+ request,
15
+ )
16
+
17
+ if TYPE_CHECKING:
18
+ from flask import (
19
+ Flask,
20
+ )
21
+
22
+ from pumaguard.web_ui import (
23
+ WebUI,
24
+ )
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ def register_dhcp_routes(app: "Flask", webui: "WebUI") -> None:
30
+ """Register DHCP event endpoints for camera detection."""
31
+
32
+ @app.route("/api/dhcp/event", methods=["POST"])
33
+ def dhcp_event():
34
+ """
35
+ Receive DHCP event notifications from dnsmasq script.
36
+
37
+ Expected JSON payload:
38
+ {
39
+ "action": "add|old|del",
40
+ "mac_address": "xx:xx:xx:xx:xx:xx",
41
+ "ip_address": "192.168.52.xxx",
42
+ "hostname": "device-hostname",
43
+ "timestamp": "ISO8601 timestamp"
44
+ }
45
+ """
46
+ try:
47
+ data = request.get_json()
48
+
49
+ if not data:
50
+ return jsonify({"error": "No JSON data provided"}), 400
51
+
52
+ action = data.get("action")
53
+ mac_address = data.get("mac_address")
54
+ ip_address = data.get("ip_address")
55
+ hostname = data.get("hostname")
56
+ timestamp = data.get("timestamp")
57
+
58
+ # Log the DHCP event (MAC address redacted)
59
+ logger.info(
60
+ "DHCP event received: action=%s, hostname=%s, "
61
+ "mac=%s, ip=%s, timestamp=%s",
62
+ action,
63
+ hostname,
64
+ "***",
65
+ ip_address,
66
+ timestamp,
67
+ )
68
+
69
+ # Store camera information in webui.cameras dictionary
70
+ if action in ["add", "old"]:
71
+ # Camera connected or renewed lease
72
+ logger.info(
73
+ "Camera '%s' connected at IP %s", hostname, ip_address
74
+ )
75
+ # Store camera info indexed by MAC address
76
+ webui.cameras[mac_address] = {
77
+ "hostname": hostname,
78
+ "ip_address": ip_address,
79
+ "mac_address": mac_address,
80
+ "last_seen": timestamp,
81
+ "status": "connected",
82
+ }
83
+ elif action == "del":
84
+ # Camera disconnected
85
+ logger.info("Camera '%s' disconnected", hostname)
86
+ # Update camera status to disconnected (keep history)
87
+ if mac_address in webui.cameras:
88
+ webui.cameras[mac_address]["status"] = "disconnected"
89
+ webui.cameras[mac_address]["last_seen"] = timestamp
90
+
91
+ return (
92
+ jsonify(
93
+ {
94
+ "status": "success",
95
+ "message": "DHCP event processed",
96
+ "data": {
97
+ "action": action,
98
+ "hostname": hostname,
99
+ "ip_address": ip_address,
100
+ },
101
+ }
102
+ ),
103
+ 200,
104
+ )
105
+
106
+ except Exception as e: # pylint: disable=broad-except
107
+ logger.error("Error processing DHCP event: %s", str(e))
108
+ return (
109
+ jsonify(
110
+ {
111
+ "error": "Failed to process DHCP event",
112
+ }
113
+ ),
114
+ 500,
115
+ )
116
+
117
+ @app.route("/api/dhcp/cameras", methods=["GET"])
118
+ def get_cameras():
119
+ """
120
+ Get list of known cameras.
121
+
122
+ Returns all detected cameras with their connection status.
123
+ """
124
+ cameras_list = list(webui.cameras.values())
125
+
126
+ return (
127
+ jsonify(
128
+ {
129
+ "cameras": cameras_list,
130
+ "count": len(cameras_list),
131
+ }
132
+ ),
133
+ 200,
134
+ )
135
+
136
+ @app.route("/api/dhcp/cameras/<mac_address>", methods=["GET"])
137
+ def get_camera(mac_address: str):
138
+ """
139
+ Get specific camera by MAC address.
140
+
141
+ Args:
142
+ mac_address: MAC address of the camera (e.g., aa:bb:cc:dd:ee:ff)
143
+ """
144
+ # Normalize MAC address format (lowercase, colons)
145
+ mac_address = mac_address.lower()
146
+
147
+ if mac_address in webui.cameras:
148
+ return jsonify(webui.cameras[mac_address]), 200
149
+ return (
150
+ jsonify(
151
+ {
152
+ "error": "Camera not found",
153
+ "mac_address": mac_address,
154
+ }
155
+ ),
156
+ 404,
157
+ )
158
+
159
+ @app.route("/api/dhcp/cameras", methods=["DELETE"])
160
+ def clear_cameras():
161
+ """
162
+ Clear all camera records.
163
+
164
+ This removes all stored camera information from memory.
165
+ """
166
+ count = len(webui.cameras)
167
+ webui.cameras.clear()
168
+ logger.info("Cleared %d camera records", count)
169
+
170
+ return (
171
+ jsonify(
172
+ {
173
+ "status": "success",
174
+ "message": f"Cleared {count} camera record(s)",
175
+ }
176
+ ),
177
+ 200,
178
+ )
pumaguard/web_ui.py CHANGED
@@ -39,6 +39,9 @@ from pumaguard.presets import (
39
39
  from pumaguard.web_routes.artifacts import (
40
40
  register_artifacts_routes,
41
41
  )
42
+ from pumaguard.web_routes.dhcp import (
43
+ register_dhcp_routes,
44
+ )
42
45
  from pumaguard.web_routes.diagnostics import (
43
46
  register_diagnostics_routes,
44
47
  )
@@ -66,6 +69,16 @@ if TYPE_CHECKING:
66
69
  logger = logging.getLogger(__name__)
67
70
 
68
71
 
72
+ class CameraInfo(TypedDict):
73
+ """Type definition for camera information stored in webui.cameras."""
74
+
75
+ hostname: str
76
+ ip_address: str
77
+ mac_address: str
78
+ last_seen: str
79
+ status: str
80
+
81
+
69
82
  class PhotoDict(TypedDict):
70
83
  """Type definition for photo metadata dictionary."""
71
84
 
@@ -144,6 +157,10 @@ class WebUI:
144
157
  self.image_directories: list[str] = []
145
158
  self.classification_directories: list[str] = []
146
159
 
160
+ # Camera tracking - stores detected cameras by MAC address
161
+ # Format: {mac_address: CameraInfo}
162
+ self.cameras: dict[str, CameraInfo] = {}
163
+
147
164
  # mDNS/Zeroconf support
148
165
  self.zeroconf: Zeroconf | None = None
149
166
  self.service_info: ServiceInfo | None = None
@@ -241,6 +258,8 @@ class WebUI:
241
258
 
242
259
  register_diagnostics_routes(self.app, self)
243
260
 
261
+ register_dhcp_routes(self.app, self)
262
+
244
263
  @self.app.route("/<path:path>")
245
264
  def serve_static(path: str):
246
265
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pumaguard
3
- Version: 20.post244
3
+ Version: 20.post247
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
@@ -11,18 +11,18 @@ pumaguard/sound.py,sha256=fUp0FWmuj99ZMDacZAayYZOurFfvp68aFxqj0VvbqGw,4723
11
11
  pumaguard/stats.py,sha256=ZwocfnFCQ-ky7me-YTTrEoJqsIHOWAgSzeoJHItsIU4,927
12
12
  pumaguard/utils.py,sha256=w1EgOLSZGyjq_b49hvVZhBESy-lVP0yRtNHe-sXBoIU,19735
13
13
  pumaguard/verify.py,sha256=vfw3PRzDt1uuH5FKV9F5vb1PH7KQ6AEgVNhJ6jck_hQ,5513
14
- pumaguard/web_ui.py,sha256=2TuJs3Wcn9HHr9FQBl2HHtS9Ki-qg0ufXc35Up3TQiY,15665
14
+ pumaguard/web_ui.py,sha256=_DiEELlJwuL4DjlUoxGFnHecRmIc21J6zvQwEcXpFdA,16140
15
15
  pumaguard/completions/pumaguard-classify-completions.sh,sha256=5QySg-2Jdinj15qpUYa5UzHbTgYzi2gmPVYYyyXny4c,1353
16
16
  pumaguard/completions/pumaguard-completions.sh,sha256=bRx3Q3_gM__3w0PyfQSCVdxylhhr3QlzaLCav24dfNc,1196
17
17
  pumaguard/completions/pumaguard-server-completions.sh,sha256=33c6GjbTImBOHn0SSNUOJoxqJ2mMHuDv3P3GQJGGHhA,1161
18
18
  pumaguard/completions/pumaguard-train-completions.sh,sha256=lI8LG-QrncvhUqCeKtfrSU1MSRBn52KnDsiJJQm37W4,1184
19
- pumaguard/pumaguard-ui/.last_build_id,sha256=44nL03JzUFDZwdvMJrofy7jk3ZWVr-aW8se0TOV0weg,32
19
+ pumaguard/pumaguard-ui/.last_build_id,sha256=MqeXJXz-uOBq3M0XNvGxIcX4cKB19A_LfS3HU_820DM,32
20
20
  pumaguard/pumaguard-ui/favicon.png,sha256=erJSX0uGtl0-THA1ihfloar29Df5nLzARtrXPVm7kBU,917
21
21
  pumaguard/pumaguard-ui/flutter.js,sha256=7V1ZIKmGiouT15CpquQWWmKWJyjUq77FoU9gDXPFO9M,9412
22
- pumaguard/pumaguard-ui/flutter_bootstrap.js,sha256=RYi4PhelQMs063sa3jCr0BLWDMAzY7lKs-ksCptkaQM,9692
22
+ pumaguard/pumaguard-ui/flutter_bootstrap.js,sha256=Nyt0e7m8U4hq4CmcHhK1ij4hNgUqANM1o4YieKUx4rA,9692
23
23
  pumaguard/pumaguard-ui/flutter_service_worker.js,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  pumaguard/pumaguard-ui/index.html,sha256=901-ZY0WysVAZWPwj2xGatoezwm9TX9IV_jpMrlsaXg,1205
25
- pumaguard/pumaguard-ui/main.dart.js,sha256=Iuyy0NCeH642Ig0EqaARkXzSwo4Kp-05uuU3bUk_GjE,2727567
25
+ pumaguard/pumaguard-ui/main.dart.js,sha256=uoSU-v_5ZgHKWW3pWQoEZg2hQXQoTKymXVCSXmJ61hQ,2727567
26
26
  pumaguard/pumaguard-ui/manifest.json,sha256=Hhnw_eLUivdrOlL7O9KGBsGXCKKt3lix17Fh3GB0g-s,920
27
27
  pumaguard/pumaguard-ui/version.json,sha256=uXZ6musTJUZaO0N2bEbr3cy9rpx2aesAS2YFMcu2WF8,94
28
28
  pumaguard/pumaguard-ui/assets/AssetManifest.bin,sha256=Qzp1G9iPlHSW-PnHyszTxZO31_NjmTlvSBWY_REPH_8,562
@@ -58,13 +58,14 @@ pumaguard/pumaguard-ui/icons/Icon-maskable-192.png,sha256=0shC4iqfTsnZlrIzc6kFyI
58
58
  pumaguard/pumaguard-ui/icons/Icon-maskable-512.png,sha256=au4Gzcq2sq73Sxc0xHePRCHS2hALD_nlKyG1UkAgKSk,20998
59
59
  pumaguard/web_routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
60
  pumaguard/web_routes/artifacts.py,sha256=IpnMLdbgAYkwU3TuYJE-JHGnC_x5_XNCrc-1M_n2YKk,3879
61
+ pumaguard/web_routes/dhcp.py,sha256=wgRwN-pMix5FxJ-UWG38Hn5FiZpMvUxW0rpff_hYjUA,5176
61
62
  pumaguard/web_routes/diagnostics.py,sha256=EIIbjuixJyGXdnVQf8RQ6xQxJar0UHZO8dF-9zQLY9g,3294
62
63
  pumaguard/web_routes/directories.py,sha256=yy5TghCEyB4reRGAcVHIEfr2vlHnuiDChIXl9ZFquRM,2410
63
64
  pumaguard/web_routes/folders.py,sha256=Z63ap6dRi6NWye70HYurpCnsSXmFgzTbTsFKYdZ1Bjk,6305
64
65
  pumaguard/web_routes/photos.py,sha256=Tac_CbaZSeZzOfaJ73vlp3iyZbvfD7ei1YM3tsb0nTY,5106
65
66
  pumaguard/web_routes/settings.py,sha256=kjSqoX6E38UJ_J_YZRQr7QmACR7bDwFcUarqnowjHP4,11655
66
67
  pumaguard/web_routes/sync.py,sha256=Zvv6VARGE5xP29C5gWH3ul81PISRxoF8n472DITItE0,6378
67
- pumaguard-20.post244.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
68
+ pumaguard-20.post247.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
68
69
  pumaguard-sounds/cougar_call.mp3,sha256=jdPzi7Qneect3ez2G6XAeHWtetU5vSOSB6pceuB26Wc,129048
69
70
  pumaguard-sounds/cougarsounds.wav,sha256=hwVmmQ75dkOP3qd07YAvVOSm1neYtxLSzxw3Ulvs2cM,96346
70
71
  pumaguard-sounds/dark-engine-logo-141942.mp3,sha256=Vw-qyLTMPJZvsgQcZtH0DpGcP1dd7nJq-9BnHuNPGug,372819
@@ -82,8 +83,8 @@ pumaguard-sounds/mixkit-vintage-telephone-ringtone-1356.wav,sha256=zWWY2uFF0-l7P
82
83
  pumaguard-sounds/pumaguard-warning.mp3,sha256=wcCfHsulPo5P5s8MjpQAG2NYHQDsRpjqoMig1-o_MDI,232249
83
84
  pumaguard-sounds/short-round-110940.mp3,sha256=vdskGD94SeH1UJyJyR0Ek_7xGXPIZfnPdoBvxGnUt98,450816
84
85
  pumaguard-ui/ios/Flutter/ephemeral/flutter_lldb_helper.py,sha256=Bc_jl3_e5ZPvrSBJpPYtN05VxpztyKq-7lVms3rLg4Q,1276
85
- pumaguard-20.post244.dist-info/METADATA,sha256=E5fBahqRasHj0DrWgqy9M-sArwNjzVkw823cpyCuX0k,8618
86
- pumaguard-20.post244.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
87
- pumaguard-20.post244.dist-info/entry_points.txt,sha256=rmCdBTPWrbJQvPPwABSVobXE9D7hrKsITGZ6nvCrko8,127
88
- pumaguard-20.post244.dist-info/top_level.txt,sha256=B-PzS4agkQNhOYbLLIrMVOyMD_pl5F-yujPBm5zYYjY,40
89
- pumaguard-20.post244.dist-info/RECORD,,
86
+ pumaguard-20.post247.dist-info/METADATA,sha256=c7Jd4SrpH9_78yrulTC-8HsivfMx5as13QBC-0uqWeE,8618
87
+ pumaguard-20.post247.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
88
+ pumaguard-20.post247.dist-info/entry_points.txt,sha256=rmCdBTPWrbJQvPPwABSVobXE9D7hrKsITGZ6nvCrko8,127
89
+ pumaguard-20.post247.dist-info/top_level.txt,sha256=B-PzS4agkQNhOYbLLIrMVOyMD_pl5F-yujPBm5zYYjY,40
90
+ pumaguard-20.post247.dist-info/RECORD,,