pumaguard 21.post9__py3-none-any.whl → 21.post12__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.
@@ -0,0 +1,310 @@
1
+ """Camera heartbeat monitoring for PumaGuard.
2
+
3
+ This module provides background monitoring of camera availability using
4
+ ICMP ping and TCP connection checks.
5
+ """
6
+
7
+ from __future__ import (
8
+ annotations,
9
+ )
10
+
11
+ import logging
12
+ import socket
13
+ import subprocess
14
+ import threading
15
+ from datetime import (
16
+ datetime,
17
+ timezone,
18
+ )
19
+ from typing import (
20
+ TYPE_CHECKING,
21
+ )
22
+
23
+ if TYPE_CHECKING:
24
+ from pumaguard.web_ui import (
25
+ WebUI,
26
+ )
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class CameraHeartbeat:
32
+ """
33
+ Background service to monitor camera availability via ICMP ping
34
+ and TCP checks.
35
+
36
+ The heartbeat monitor runs in a separate thread and periodically
37
+ checks if cameras are reachable. It updates the camera status and
38
+ last_seen timestamp based on the results.
39
+ """
40
+
41
+ # pylint: disable=too-many-arguments,too-many-positional-arguments
42
+ def __init__(
43
+ self,
44
+ webui: "WebUI",
45
+ interval: int = 60,
46
+ enabled: bool = True,
47
+ check_method: str = "tcp",
48
+ tcp_port: int = 80,
49
+ tcp_timeout: int = 3,
50
+ icmp_timeout: int = 2,
51
+ ):
52
+ """
53
+ Initialize the camera heartbeat monitor.
54
+
55
+ Args:
56
+ webui: WebUI instance containing camera information
57
+ interval: Check interval in seconds (default: 60)
58
+ enabled: Enable heartbeat monitoring (default: True)
59
+ check_method: Health check method - "icmp", "tcp", or
60
+ "both" (default: "tcp")
61
+ tcp_port: TCP port to check (default: 80 for HTTP)
62
+ tcp_timeout: TCP connection timeout in seconds (default: 3)
63
+ icmp_timeout: ICMP ping timeout in seconds (default: 2)
64
+ """
65
+ self.webui = webui
66
+ self.interval = interval
67
+ self.enabled = enabled
68
+ self.check_method = check_method.lower()
69
+ self.tcp_port = tcp_port
70
+ self.tcp_timeout = tcp_timeout
71
+ self.icmp_timeout = icmp_timeout
72
+
73
+ self._running = False
74
+ self._thread: threading.Thread | None = None
75
+ self._stop_event = threading.Event()
76
+
77
+ # Validate check method
78
+ if self.check_method not in ["icmp", "tcp", "both"]:
79
+ logger.warning(
80
+ "Invalid check_method '%s', defaulting to 'tcp'",
81
+ self.check_method,
82
+ )
83
+ self.check_method = "tcp"
84
+
85
+ def _check_icmp(self, ip_address: str) -> bool:
86
+ """
87
+ Check camera availability using ICMP ping.
88
+
89
+ Args:
90
+ ip_address: IP address to ping
91
+
92
+ Returns:
93
+ True if ping successful, False otherwise
94
+ """
95
+ try:
96
+ # Use ping command with 1 packet and timeout
97
+ # -c 1: send 1 packet
98
+ # -W timeout: wait timeout seconds for response
99
+ # -q: quiet output (only summary)
100
+ result = subprocess.run(
101
+ ["ping", "-c", "1", "-W", str(self.icmp_timeout), ip_address],
102
+ capture_output=True,
103
+ timeout=self.icmp_timeout + 1,
104
+ check=False,
105
+ )
106
+ return result.returncode == 0
107
+ except (subprocess.TimeoutExpired, FileNotFoundError) as e:
108
+ logger.debug("ICMP ping failed for %s: %s", ip_address, str(e))
109
+ return False
110
+
111
+ def _check_tcp(self, ip_address: str, port: int) -> bool:
112
+ """
113
+ Check camera availability using TCP connection test.
114
+
115
+ Args:
116
+ ip_address: IP address to connect to
117
+ port: TCP port to connect to
118
+
119
+ Returns:
120
+ True if connection successful, False otherwise
121
+ """
122
+ try:
123
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
124
+ sock.settimeout(self.tcp_timeout)
125
+ result = sock.connect_ex((ip_address, port))
126
+ sock.close()
127
+ return result == 0
128
+ except (socket.error, OSError) as e:
129
+ logger.debug(
130
+ "TCP connection failed for %s:%d: %s",
131
+ ip_address,
132
+ port,
133
+ str(e),
134
+ )
135
+ return False
136
+
137
+ def check_camera(self, ip_address: str) -> bool:
138
+ """
139
+ Check if a camera is reachable using the configured method.
140
+
141
+ Args:
142
+ ip_address: IP address of the camera
143
+
144
+ Returns:
145
+ True if camera is reachable, False otherwise
146
+ """
147
+ if self.check_method == "icmp":
148
+ return self._check_icmp(ip_address)
149
+ if self.check_method == "tcp":
150
+ return self._check_tcp(ip_address, self.tcp_port)
151
+ if self.check_method == "both":
152
+ # Try ICMP first (faster), fall back to TCP
153
+ if self._check_icmp(ip_address):
154
+ return True
155
+ return self._check_tcp(ip_address, self.tcp_port)
156
+ return False
157
+
158
+ def _update_camera_status(
159
+ self, mac_address: str, is_reachable: bool
160
+ ) -> None:
161
+ """
162
+ Update camera status and last_seen timestamp.
163
+
164
+ Args:
165
+ mac_address: MAC address of the camera
166
+ is_reachable: Whether the camera is currently reachable
167
+ """
168
+ if mac_address not in self.webui.cameras:
169
+ return
170
+
171
+ camera = self.webui.cameras[mac_address]
172
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
173
+
174
+ if is_reachable:
175
+ # Camera is reachable - update status to connected
176
+ if camera["status"] != "connected":
177
+ logger.info(
178
+ "Camera '%s' is now reachable at %s",
179
+ camera["hostname"],
180
+ camera["ip_address"],
181
+ )
182
+ camera["status"] = "connected"
183
+ camera["last_seen"] = timestamp
184
+ else:
185
+ # Camera is not reachable - update status to disconnected
186
+ if camera["status"] == "connected":
187
+ logger.warning(
188
+ "Camera '%s' is no longer reachable at %s",
189
+ camera["hostname"],
190
+ camera["ip_address"],
191
+ )
192
+ camera["status"] = "disconnected"
193
+ # Don't update last_seen on failure - keep the last successful time
194
+
195
+ # Persist changes to settings
196
+ self._save_camera_list()
197
+
198
+ def _save_camera_list(self) -> None:
199
+ """Save the camera list to settings file."""
200
+ try:
201
+ camera_list = []
202
+ for _, cam_info in self.webui.cameras.items():
203
+ camera_list.append(
204
+ {
205
+ "hostname": cam_info["hostname"],
206
+ "ip_address": cam_info["ip_address"],
207
+ "mac_address": cam_info["mac_address"],
208
+ "last_seen": cam_info["last_seen"],
209
+ "status": cam_info["status"],
210
+ }
211
+ )
212
+ self.webui.presets.cameras = camera_list
213
+ self.webui.presets.save()
214
+ except Exception as e: # pylint: disable=broad-except
215
+ logger.error("Failed to save camera list: %s", str(e))
216
+
217
+ def _monitor_loop(self) -> None:
218
+ """Main monitoring loop that runs in a background thread."""
219
+ logger.info(
220
+ "Camera heartbeat monitor started "
221
+ "(method=%s, interval=%ds, port=%d)",
222
+ self.check_method,
223
+ self.interval,
224
+ self.tcp_port,
225
+ )
226
+
227
+ while not self._stop_event.is_set():
228
+ try:
229
+ # Check each camera
230
+ for mac_address, camera in list(self.webui.cameras.items()):
231
+ if self._stop_event.is_set():
232
+ break
233
+
234
+ ip_address = camera["ip_address"]
235
+ if not ip_address:
236
+ continue
237
+
238
+ logger.debug(
239
+ "Checking camera '%s' at %s",
240
+ camera["hostname"],
241
+ ip_address,
242
+ )
243
+
244
+ is_reachable = self.check_camera(ip_address)
245
+ self._update_camera_status(mac_address, is_reachable)
246
+
247
+ except Exception as e: # pylint: disable=broad-except
248
+ logger.error("Error in heartbeat monitor loop: %s", str(e))
249
+
250
+ # Wait for the next check interval or stop event
251
+ self._stop_event.wait(self.interval)
252
+
253
+ logger.info("Camera heartbeat monitor stopped")
254
+
255
+ def start(self) -> None:
256
+ """Start the heartbeat monitoring thread."""
257
+ if not self.enabled:
258
+ logger.info("Camera heartbeat monitoring is disabled")
259
+ return
260
+
261
+ if self._running:
262
+ logger.warning("Heartbeat monitor is already running")
263
+ return
264
+
265
+ self._running = True
266
+ self._stop_event.clear()
267
+ self._thread = threading.Thread(
268
+ target=self._monitor_loop, daemon=True, name="CameraHeartbeat"
269
+ )
270
+ self._thread.start()
271
+ logger.info("Camera heartbeat monitoring started")
272
+
273
+ def stop(self) -> None:
274
+ """Stop the heartbeat monitoring thread."""
275
+ if not self._running:
276
+ logger.warning("Heartbeat monitor is not running")
277
+ return
278
+
279
+ self._running = False
280
+ self._stop_event.set()
281
+
282
+ if self._thread and self._thread.is_alive():
283
+ self._thread.join(timeout=5)
284
+ if self._thread.is_alive():
285
+ logger.warning("Heartbeat monitor thread did not stop cleanly")
286
+ else:
287
+ logger.info("Camera heartbeat monitoring stopped")
288
+
289
+ def check_now(self) -> dict[str, bool]:
290
+ """
291
+ Immediately check all cameras and return results.
292
+
293
+ This can be called manually to force a check outside the
294
+ regular interval.
295
+
296
+ Returns:
297
+ Dictionary mapping MAC addresses to reachability status
298
+ """
299
+ results = {}
300
+ for mac_address, camera in self.webui.cameras.items():
301
+ ip_address = camera["ip_address"]
302
+ if not ip_address:
303
+ results[mac_address] = False
304
+ continue
305
+
306
+ is_reachable = self.check_camera(ip_address)
307
+ self._update_camera_status(mac_address, is_reachable)
308
+ results[mac_address] = is_reachable
309
+
310
+ return results
pumaguard/presets.py CHANGED
@@ -151,6 +151,14 @@ class Preset:
151
151
  self.no_lion_directories: list[str] = []
152
152
  self.validation_no_lion_directories: list[str] = []
153
153
  self.with_augmentation = False
154
+
155
+ # Camera heartbeat monitoring settings
156
+ self.camera_heartbeat_enabled = True
157
+ self.camera_heartbeat_interval = 60 # Check interval in seconds
158
+ self.camera_heartbeat_method = "tcp" # "icmp", "tcp", or "both"
159
+ self.camera_heartbeat_tcp_port = 80 # TCP port to check
160
+ self.camera_heartbeat_tcp_timeout = 3 # TCP timeout in seconds
161
+ self.camera_heartbeat_icmp_timeout = 2 # ICMP timeout in seconds
154
162
  if version.parse(tf.__version__) < version.parse("2.17"):
155
163
  self.tf_compat = "2.15"
156
164
  else:
@@ -88927,7 +88927,7 @@ A.MQ.prototype={
88927
88927
  K(a){var s,r=null,q=A.awv(B.ae,r,r,B.nE)
88928
88928
  q=A.qz(B.mC,r,new A.md(r,r,r,r,2,r,new A.cM(A.hG(12),B.u)),q,"Roboto",!0)
88929
88929
  s=A.awv(B.al,r,r,B.nE)
88930
- return new A.Ay(new A.vb(new A.abK(),r,r,r,r,t.Gj),r,r,new A.zI(B.Hh,"PumaGuard v21-9-g1370251",q,A.qz(B.mC,r,new A.md(r,r,r,r,2,r,new A.cM(A.hG(12),B.u)),s,"Roboto",!0),B.AN,!1,r),r,t.Bp)}}
88930
+ return new A.Ay(new A.vb(new A.abK(),r,r,r,r,t.Gj),r,r,new A.zI(B.Hh,"PumaGuard v21-12-g234e8a3",q,A.qz(B.mC,r,new A.md(r,r,r,r,2,r,new A.cM(A.hG(12),B.u)),s,"Roboto",!0),B.AN,!1,r),r,t.Bp)}}
88931
88931
  A.abK.prototype={
88932
88932
  $1(a){return new A.m6()},
88933
88933
  $S:541}
@@ -89248,7 +89248,7 @@ q=A.aC("System Information",h,h,h,A.p(q).ok.r,h,h)
89248
89248
  s=i.d
89249
89249
  s=s==null?h:s.b
89250
89250
  s=i.tF("Backend Version",s==null?"Unknown":s)
89251
- n=i.tF("UI Version","v21-9-g1370251")
89251
+ n=i.tF("UI Version","v21-12-g234e8a3")
89252
89252
  l=i.d
89253
89253
  k=l==null
89254
89254
  j=k?h:l.d
@@ -337,3 +337,52 @@ def register_dhcp_routes(app: "Flask", webui: "WebUI") -> None:
337
337
  ),
338
338
  200,
339
339
  )
340
+
341
+ @app.route("/api/dhcp/cameras/heartbeat", methods=["POST"])
342
+ def check_heartbeat():
343
+ """
344
+ Manually trigger a heartbeat check for all cameras.
345
+
346
+ This immediately checks all cameras and returns their
347
+ reachability status.
348
+
349
+ Returns:
350
+ JSON with camera reachability results
351
+ """
352
+ try:
353
+ results = webui.heartbeat.check_now()
354
+
355
+ # Convert results to human-readable format
356
+ camera_status = {}
357
+ for mac_address, is_reachable in results.items():
358
+ camera = webui.cameras.get(mac_address)
359
+ if camera:
360
+ camera_status[mac_address] = {
361
+ "hostname": camera["hostname"],
362
+ "ip_address": camera["ip_address"],
363
+ "reachable": is_reachable,
364
+ "status": camera["status"],
365
+ "last_seen": camera["last_seen"],
366
+ }
367
+
368
+ return (
369
+ jsonify(
370
+ {
371
+ "status": "success",
372
+ "message": "Heartbeat check completed",
373
+ "cameras": camera_status,
374
+ }
375
+ ),
376
+ 200,
377
+ )
378
+
379
+ except Exception as e: # pylint: disable=broad-except
380
+ logger.error("Error performing heartbeat check: %s", str(e))
381
+ return (
382
+ jsonify(
383
+ {
384
+ "error": "Failed to perform heartbeat check",
385
+ }
386
+ ),
387
+ 500,
388
+ )
pumaguard/web_ui.py CHANGED
@@ -33,6 +33,9 @@ from zeroconf import (
33
33
  Zeroconf,
34
34
  )
35
35
 
36
+ from pumaguard.camera_heartbeat import (
37
+ CameraHeartbeat,
38
+ )
36
39
  from pumaguard.presets import (
37
40
  Preset,
38
41
  )
@@ -161,6 +164,17 @@ class WebUI:
161
164
  # Format: {mac_address: CameraInfo}
162
165
  self.cameras: dict[str, CameraInfo] = {}
163
166
 
167
+ # Camera heartbeat monitoring
168
+ self.heartbeat: CameraHeartbeat = CameraHeartbeat(
169
+ webui=self,
170
+ interval=presets.camera_heartbeat_interval,
171
+ enabled=presets.camera_heartbeat_enabled,
172
+ check_method=presets.camera_heartbeat_method,
173
+ tcp_port=presets.camera_heartbeat_tcp_port,
174
+ tcp_timeout=presets.camera_heartbeat_tcp_timeout,
175
+ icmp_timeout=presets.camera_heartbeat_icmp_timeout,
176
+ )
177
+
164
178
  # Load cameras from persisted settings
165
179
  for camera in presets.cameras:
166
180
  mac = camera.get("mac_address")
@@ -454,6 +468,9 @@ class WebUI:
454
468
  # Start mDNS service
455
469
  self._start_mdns()
456
470
 
471
+ # Start camera heartbeat monitoring
472
+ self.heartbeat.start()
473
+
457
474
  if self.debug:
458
475
  self._run_server()
459
476
  else:
@@ -473,6 +490,9 @@ class WebUI:
473
490
 
474
491
  self._running = False
475
492
 
493
+ # Stop camera heartbeat monitoring
494
+ self.heartbeat.stop()
495
+
476
496
  # Stop mDNS service
477
497
  self._stop_mdns()
478
498
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pumaguard
3
- Version: 21.post9
3
+ Version: 21.post12
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,17 +1,18 @@
1
1
  pumaguard/__init__.py,sha256=QUsYaH1hG9RKygFZfaS5tpe9pgN_1Kf7EMroQCVlQfE,421
2
+ pumaguard/camera_heartbeat.py,sha256=vfdprxBnVDF-_iA9MUaI2tVh4AbeqP1j09xKqT2N25Y,10291
2
3
  pumaguard/classify.py,sha256=QnHfnIlkzSxHGy0dgqT6IeTbRBboRT3c3B_YC3YpqfI,1057
3
4
  pumaguard/lock_manager.py,sha256=sYLl8Z7miOr_9R7ehoSFlKf8tToHNNUT6aH9Mlw2M9g,1608
4
5
  pumaguard/main.py,sha256=1Wazv1wjwb46yNlqgWt88HQwKSxGmY24X5OsUv8gYyE,7029
5
6
  pumaguard/model-registry.yaml,sha256=V-pTaqJrk_jJo568okBDaVhs51qTHttSqKe6PqdX6Bc,10318
6
7
  pumaguard/model_cli.py,sha256=nzDv0lXSvRKpLxs579tiHInJPPV-AFO4jzeLk5t2GaA,1394
7
8
  pumaguard/model_downloader.py,sha256=zJQgCMOF2AfhB30VsfOMYtgRxcxVxkZBAdtG8KznPyY,12895
8
- pumaguard/presets.py,sha256=1VIfe7nTdLHHuJsncbgAzz7ywgZ9Eoi1nsK1KFbSaKM,27714
9
+ pumaguard/presets.py,sha256=RziRfHRcRAA4ykNm9m2dfk7ko3xdpoS6natlAlzLJH4,28164
9
10
  pumaguard/server.py,sha256=zmzSXabt6K26u8kwBPdm1gI6aMAwJo3gCaSaX5Sh0vk,14705
10
11
  pumaguard/sound.py,sha256=-ceyO9xjfuVSal-CvaM_o2l45gYWUUV3bJN-Eni2XQQ,4732
11
12
  pumaguard/stats.py,sha256=ZwocfnFCQ-ky7me-YTTrEoJqsIHOWAgSzeoJHItsIU4,927
12
13
  pumaguard/utils.py,sha256=w1EgOLSZGyjq_b49hvVZhBESy-lVP0yRtNHe-sXBoIU,19735
13
14
  pumaguard/verify.py,sha256=vfw3PRzDt1uuH5FKV9F5vb1PH7KQ6AEgVNhJ6jck_hQ,5513
14
- pumaguard/web_ui.py,sha256=2NtiW35rlLXhWXPAIShsK-pH8HUvog6rDQykeTD8Dy0,16636
15
+ pumaguard/web_ui.py,sha256=N51XEKBuCJfBWUsm4EqvzpS1EG5ey6Z2kBlKnCitRqQ,17332
15
16
  pumaguard/completions/pumaguard-classify-completions.sh,sha256=5QySg-2Jdinj15qpUYa5UzHbTgYzi2gmPVYYyyXny4c,1353
16
17
  pumaguard/completions/pumaguard-completions.sh,sha256=bRx3Q3_gM__3w0PyfQSCVdxylhhr3QlzaLCav24dfNc,1196
17
18
  pumaguard/completions/pumaguard-server-completions.sh,sha256=33c6GjbTImBOHn0SSNUOJoxqJ2mMHuDv3P3GQJGGHhA,1161
@@ -22,7 +23,7 @@ pumaguard/pumaguard-ui/flutter.js,sha256=7V1ZIKmGiouT15CpquQWWmKWJyjUq77FoU9gDXP
22
23
  pumaguard/pumaguard-ui/flutter_bootstrap.js,sha256=Nyt0e7m8U4hq4CmcHhK1ij4hNgUqANM1o4YieKUx4rA,9692
23
24
  pumaguard/pumaguard-ui/flutter_service_worker.js,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
25
  pumaguard/pumaguard-ui/index.html,sha256=901-ZY0WysVAZWPwj2xGatoezwm9TX9IV_jpMrlsaXg,1205
25
- pumaguard/pumaguard-ui/main.dart.js,sha256=4qmj5ofhXBxECarNMbxu0k4lUTQOMZosmOgWupcmYa8,2729033
26
+ pumaguard/pumaguard-ui/main.dart.js,sha256=x084j40pcRNTCBLkRa8waf0hqNWcfy-jeKkV7g19zKE,2729035
26
27
  pumaguard/pumaguard-ui/manifest.json,sha256=Hhnw_eLUivdrOlL7O9KGBsGXCKKt3lix17Fh3GB0g-s,920
27
28
  pumaguard/pumaguard-ui/version.json,sha256=uXZ6musTJUZaO0N2bEbr3cy9rpx2aesAS2YFMcu2WF8,94
28
29
  pumaguard/pumaguard-ui/assets/AssetManifest.bin,sha256=Qzp1G9iPlHSW-PnHyszTxZO31_NjmTlvSBWY_REPH_8,562
@@ -58,14 +59,14 @@ pumaguard/pumaguard-ui/icons/Icon-maskable-192.png,sha256=0shC4iqfTsnZlrIzc6kFyI
58
59
  pumaguard/pumaguard-ui/icons/Icon-maskable-512.png,sha256=au4Gzcq2sq73Sxc0xHePRCHS2hALD_nlKyG1UkAgKSk,20998
59
60
  pumaguard/web_routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
61
  pumaguard/web_routes/artifacts.py,sha256=IpnMLdbgAYkwU3TuYJE-JHGnC_x5_XNCrc-1M_n2YKk,3879
61
- pumaguard/web_routes/dhcp.py,sha256=hQfYLiDmJeDvGzPmx74LovzbhNN2ywPZfP7avXTJ-FA,10971
62
+ pumaguard/web_routes/dhcp.py,sha256=YoF-987Ro1DqVBu1szloIkW-XQx81-WMKgqk83dNoRc,12607
62
63
  pumaguard/web_routes/diagnostics.py,sha256=EIIbjuixJyGXdnVQf8RQ6xQxJar0UHZO8dF-9zQLY9g,3294
63
64
  pumaguard/web_routes/directories.py,sha256=yy5TghCEyB4reRGAcVHIEfr2vlHnuiDChIXl9ZFquRM,2410
64
65
  pumaguard/web_routes/folders.py,sha256=Z63ap6dRi6NWye70HYurpCnsSXmFgzTbTsFKYdZ1Bjk,6305
65
66
  pumaguard/web_routes/photos.py,sha256=Tac_CbaZSeZzOfaJ73vlp3iyZbvfD7ei1YM3tsb0nTY,5106
66
67
  pumaguard/web_routes/settings.py,sha256=GA7MERNRRnR2lfrG-aSRx8bJO5OTqVzyQTYSqPYP8wc,11754
67
68
  pumaguard/web_routes/sync.py,sha256=Zvv6VARGE5xP29C5gWH3ul81PISRxoF8n472DITItE0,6378
68
- pumaguard-21.post9.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
69
+ pumaguard-21.post12.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
69
70
  pumaguard-sounds/cougar_call.mp3,sha256=jdPzi7Qneect3ez2G6XAeHWtetU5vSOSB6pceuB26Wc,129048
70
71
  pumaguard-sounds/cougarsounds.wav,sha256=hwVmmQ75dkOP3qd07YAvVOSm1neYtxLSzxw3Ulvs2cM,96346
71
72
  pumaguard-sounds/dark-engine-logo-141942.mp3,sha256=Vw-qyLTMPJZvsgQcZtH0DpGcP1dd7nJq-9BnHuNPGug,372819
@@ -83,8 +84,8 @@ pumaguard-sounds/mixkit-vintage-telephone-ringtone-1356.wav,sha256=zWWY2uFF0-l7P
83
84
  pumaguard-sounds/pumaguard-warning.mp3,sha256=wcCfHsulPo5P5s8MjpQAG2NYHQDsRpjqoMig1-o_MDI,232249
84
85
  pumaguard-sounds/short-round-110940.mp3,sha256=vdskGD94SeH1UJyJyR0Ek_7xGXPIZfnPdoBvxGnUt98,450816
85
86
  pumaguard-ui/ios/Flutter/ephemeral/flutter_lldb_helper.py,sha256=Bc_jl3_e5ZPvrSBJpPYtN05VxpztyKq-7lVms3rLg4Q,1276
86
- pumaguard-21.post9.dist-info/METADATA,sha256=eCOq3MlPct2Y7ZtifG971t32l509VQhWlzsF0-Qdccw,8616
87
- pumaguard-21.post9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
88
- pumaguard-21.post9.dist-info/entry_points.txt,sha256=rmCdBTPWrbJQvPPwABSVobXE9D7hrKsITGZ6nvCrko8,127
89
- pumaguard-21.post9.dist-info/top_level.txt,sha256=B-PzS4agkQNhOYbLLIrMVOyMD_pl5F-yujPBm5zYYjY,40
90
- pumaguard-21.post9.dist-info/RECORD,,
87
+ pumaguard-21.post12.dist-info/METADATA,sha256=dVEczrWr1qlnND546UgF_cpmQS_yW-9gNNBJY4GcjKI,8617
88
+ pumaguard-21.post12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
89
+ pumaguard-21.post12.dist-info/entry_points.txt,sha256=rmCdBTPWrbJQvPPwABSVobXE9D7hrKsITGZ6nvCrko8,127
90
+ pumaguard-21.post12.dist-info/top_level.txt,sha256=B-PzS4agkQNhOYbLLIrMVOyMD_pl5F-yujPBm5zYYjY,40
91
+ pumaguard-21.post12.dist-info/RECORD,,