pumaguard 20.post235__py3-none-any.whl → 20.post241__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/server.py CHANGED
@@ -272,7 +272,7 @@ class FolderObserver:
272
272
  sound_file_path = os.path.join(
273
273
  self.presets.sound_path, self.presets.deterrent_sound_file
274
274
  )
275
- playsound(sound_file_path)
275
+ playsound(sound_file_path, self.presets.volume)
276
276
  # Move original file into classification folder
277
277
  try:
278
278
  dest_root = (
pumaguard/sound.py CHANGED
@@ -2,22 +2,155 @@
2
2
  Sounds
3
3
  """
4
4
 
5
+ import logging
5
6
  import subprocess
6
7
  import sys
8
+ import threading
9
+ from typing import Optional
7
10
 
11
+ logger = logging.getLogger(__name__)
8
12
 
9
- def playsound(soundfile: str):
13
+ # Global variable to track the current playing process
14
+ _current_process: Optional[subprocess.Popen] = None
15
+ _process_lock = threading.Lock()
16
+
17
+
18
+ def playsound(soundfile: str, volume: int = 80, blocking: bool = True):
10
19
  """
11
- Play a sound file.
20
+ Play a sound file with specified volume.
21
+
22
+ Args:
23
+ soundfile: Path to the sound file to play
24
+ volume: Volume level from 0-100 (default: 80)
25
+ blocking: If True, wait for sound to finish. If False, return
26
+ immediately (default: True)
12
27
  """
28
+ global _current_process # pylint: disable=global-statement
29
+
30
+ logger.info(
31
+ "playsound called: file=%s, volume=%d, blocking=%s",
32
+ soundfile,
33
+ volume,
34
+ blocking,
35
+ )
36
+
37
+ # mpg123 -f flag scales output samples (soft gain)
38
+ # Default/normal is 32768 (100%)
39
+ # Valid range: 0 to much higher than 32768
40
+ # Convert 0-100 percentage to mpg123 scale:
41
+ # 0% = 0 (muted), 100% = 32768 (normal), 200% = 65536 (double)
42
+ # Linear scaling: mpg123_volume = (volume / 100) * 32768
43
+ mpg123_volume = int((volume / 100.0) * 32768)
44
+
45
+ logger.debug(
46
+ "Volume conversion: %d%% -> mpg123 scale %d", volume, mpg123_volume
47
+ )
48
+
13
49
  try:
14
- subprocess.run(["mpg123", "-o", "alsa", soundfile], check=True)
15
- except subprocess.CalledProcessError as e:
50
+ with _process_lock:
51
+ # Stop any currently playing sound
52
+ if _current_process is not None:
53
+ try:
54
+ _current_process.terminate()
55
+ _current_process.wait(timeout=1)
56
+ except (subprocess.TimeoutExpired, ProcessLookupError):
57
+ pass
58
+ _current_process = None
59
+
60
+ # Start new process
61
+ # pylint: disable=consider-using-with
62
+ cmd = ["mpg123", "-o", "alsa", "-f", str(mpg123_volume), soundfile]
63
+ logger.info("Executing command: %s", " ".join(cmd))
64
+
65
+ _current_process = subprocess.Popen(
66
+ cmd,
67
+ stdout=subprocess.DEVNULL,
68
+ stderr=subprocess.DEVNULL,
69
+ )
70
+
71
+ logger.info(
72
+ "Sound playback started, PID: %d", _current_process.pid
73
+ )
74
+
75
+ if blocking:
76
+ # Wait for completion
77
+ _current_process.wait()
78
+ _current_process = None
79
+
80
+ except subprocess.SubprocessError as e:
81
+ logger.error("Error playing soundfile %s: %s", soundfile, e)
16
82
  print(f"Error playing soundfile {soundfile}: {e}")
83
+ with _process_lock:
84
+ _current_process = None
85
+
86
+
87
+ def stop_sound():
88
+ """
89
+ Stop any currently playing sound.
90
+
91
+ Returns:
92
+ bool: True if a sound was stopped, False if nothing was playing
93
+ """
94
+ global _current_process # pylint: disable=global-statement
95
+
96
+ with _process_lock:
97
+ if _current_process is not None:
98
+ try:
99
+ logger.info(
100
+ "Stopping sound playback, PID: %d", _current_process.pid
101
+ )
102
+ _current_process.terminate()
103
+ _current_process.wait(timeout=1)
104
+ logger.info("Sound playback stopped successfully")
105
+ return True
106
+ except (subprocess.TimeoutExpired, ProcessLookupError):
107
+ try:
108
+ _current_process.kill()
109
+ _current_process.wait(timeout=1)
110
+ except (subprocess.TimeoutExpired, ProcessLookupError):
111
+ pass
112
+ return True
113
+ finally:
114
+ _current_process = None
115
+ return False
116
+
117
+
118
+ def is_playing():
119
+ """
120
+ Check if a sound is currently playing.
121
+
122
+ Returns:
123
+ bool: True if a sound is currently playing, False otherwise
124
+ """
125
+ global _current_process # pylint: disable=global-statement
126
+
127
+ with _process_lock:
128
+ if _current_process is not None:
129
+ # Check if process is still running
130
+ if _current_process.poll() is None:
131
+ return True
132
+ # Process finished, clean up
133
+ _current_process = None
134
+ return False
17
135
 
18
136
 
19
137
  def main():
20
138
  """
21
139
  Main entry point.
22
140
  """
23
- playsound(sys.argv[1])
141
+ if len(sys.argv) < 2:
142
+ print("Usage: pumaguard-sound <soundfile> [volume]")
143
+ sys.exit(1)
144
+
145
+ volume = 80
146
+ if len(sys.argv) >= 3:
147
+ try:
148
+ volume = int(sys.argv[2])
149
+ if volume < 0 or volume > 100:
150
+ print("Volume must be between 0 and 100")
151
+ sys.exit(1)
152
+ except ValueError:
153
+ print("Volume must be an integer")
154
+ sys.exit(1)
155
+
156
+ playsound(sys.argv[1], volume)
@@ -26,7 +26,9 @@ from pumaguard.model_downloader import (
26
26
  verify_file_checksum,
27
27
  )
28
28
  from pumaguard.sound import (
29
+ is_playing,
29
30
  playsound,
31
+ stop_sound,
30
32
  )
31
33
 
32
34
  if TYPE_CHECKING:
@@ -64,6 +66,8 @@ def register_settings_routes(app: "Flask", webui: "WebUI") -> None:
64
66
  "deterrent-sound-file",
65
67
  "file-stabilization-extra-wait",
66
68
  "play-sound",
69
+ "volume",
70
+ "camera-url",
67
71
  ]
68
72
 
69
73
  if len(data) == 0:
@@ -71,9 +75,18 @@ def register_settings_routes(app: "Flask", webui: "WebUI") -> None:
71
75
 
72
76
  for key, value in data.items():
73
77
  if key in allowed_settings:
74
- logger.debug("Updating %s with %s", key, value)
78
+ logger.info(
79
+ "Updating setting %s with value %s", key, value
80
+ )
75
81
  attr_name = key.replace("-", "_").replace("YOLO_", "yolo_")
76
82
  setattr(webui.presets, attr_name, value)
83
+ # Log verification of volume setting
84
+ if key == "volume":
85
+ logger.info(
86
+ "Volume setting updated to %d, verified: %d",
87
+ value,
88
+ webui.presets.volume,
89
+ )
77
90
  else:
78
91
  logger.debug("Skipping unknown/read-only setting: %s", key)
79
92
 
@@ -82,7 +95,11 @@ def register_settings_routes(app: "Flask", webui: "WebUI") -> None:
82
95
  settings_dict = dict(webui.presets)
83
96
  with open(filepath, "w", encoding="utf-8") as f:
84
97
  yaml.dump(settings_dict, f, default_flow_style=False)
85
- logger.info("Settings updated and saved to %s", filepath)
98
+ logger.info(
99
+ "Settings updated and saved to %s (volume: %d)",
100
+ filepath,
101
+ webui.presets.volume,
102
+ )
86
103
  except YAMLError:
87
104
  logger.exception("Error saving settings")
88
105
  return (
@@ -151,19 +168,65 @@ def register_settings_routes(app: "Flask", webui: "WebUI") -> None:
151
168
  404,
152
169
  )
153
170
 
154
- # Play the sound
155
- logger.info("Testing sound playback: %s", sound_file_path)
156
- playsound(sound_file_path)
171
+ # Play the sound with configured volume (non-blocking)
172
+ volume = webui.presets.volume
173
+ logger.info(
174
+ "Testing sound playback: file=%s, volume=%d",
175
+ sound_file_path,
176
+ volume,
177
+ )
178
+ logger.debug(
179
+ "Current presets.volume value before playsound: %d",
180
+ webui.presets.volume,
181
+ )
182
+ playsound(sound_file_path, volume, blocking=False)
157
183
  return jsonify(
158
184
  {
159
185
  "success": True,
160
- "message": f"Sound played: {sound_file}",
186
+ "message": f"Sound started: {sound_file}",
161
187
  }
162
188
  )
163
189
  except Exception as e: # pylint: disable=broad-except
164
190
  logger.exception("Error testing sound")
165
191
  return jsonify({"error": str(e)}), 500
166
192
 
193
+ @app.route("/api/settings/stop-sound", methods=["POST"])
194
+ def stop_test_sound():
195
+ """Stop the currently playing test sound."""
196
+ try:
197
+ stopped = stop_sound()
198
+ if stopped:
199
+ logger.info("Sound playback stopped")
200
+ return jsonify(
201
+ {
202
+ "success": True,
203
+ "message": "Sound stopped",
204
+ }
205
+ )
206
+ return jsonify(
207
+ {
208
+ "success": True,
209
+ "message": "No sound was playing",
210
+ }
211
+ )
212
+ except Exception as e: # pylint: disable=broad-except
213
+ logger.exception("Error stopping sound")
214
+ return jsonify({"error": str(e)}), 500
215
+
216
+ @app.route("/api/settings/sound-status", methods=["GET"])
217
+ def get_sound_status():
218
+ """Check if a sound is currently playing."""
219
+ try:
220
+ playing = is_playing()
221
+ return jsonify(
222
+ {
223
+ "playing": playing,
224
+ }
225
+ )
226
+ except Exception as e: # pylint: disable=broad-except
227
+ logger.exception("Error checking sound status")
228
+ return jsonify({"error": str(e)}), 500
229
+
167
230
  @app.route("/api/models/available", methods=["GET"])
168
231
  def get_available_models():
169
232
  """Get list of available models with cache status.
@@ -261,3 +324,14 @@ def register_settings_routes(app: "Flask", webui: "WebUI") -> None:
261
324
  except Exception as e: # pylint: disable=broad-except
262
325
  logger.exception("Error getting available sounds")
263
326
  return jsonify({"error": str(e)}), 500
327
+
328
+ @app.route("/api/camera/url", methods=["GET"])
329
+ def get_camera_url():
330
+ """Get the configured camera URL."""
331
+ try:
332
+ camera_url = webui.presets.camera_url
333
+ logger.info("Camera URL requested: '%s'", camera_url)
334
+ return jsonify({"camera_url": camera_url})
335
+ except Exception as e: # pylint: disable=broad-except
336
+ logger.exception("Error getting camera URL")
337
+ return jsonify({"error": str(e)}), 500
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pumaguard
3
- Version: 20.post235
3
+ Version: 20.post241
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
@@ -5,9 +5,9 @@ pumaguard/main.py,sha256=1Wazv1wjwb46yNlqgWt88HQwKSxGmY24X5OsUv8gYyE,7029
5
5
  pumaguard/model-registry.yaml,sha256=V-pTaqJrk_jJo568okBDaVhs51qTHttSqKe6PqdX6Bc,10318
6
6
  pumaguard/model_cli.py,sha256=nzDv0lXSvRKpLxs579tiHInJPPV-AFO4jzeLk5t2GaA,1394
7
7
  pumaguard/model_downloader.py,sha256=zJQgCMOF2AfhB30VsfOMYtgRxcxVxkZBAdtG8KznPyY,12895
8
- pumaguard/presets.py,sha256=fr3zK_c--x10rMJs8IyAxsp2dx3Qqm5tLMifjWJCGas,26915
9
- pumaguard/server.py,sha256=gpn1Lco61OO0IPYzG5eWQ4BaxfIpMFN-AwpBip5csSQ,14684
10
- pumaguard/sound.py,sha256=VKkU1jepPxSps9uAVbLSyy4blMBNlIaDGP8jMc5xHp4,383
8
+ pumaguard/presets.py,sha256=2owFBEqpeAjrFLSYtF9o3wQCc8aDjpwnU5j_YXSyIZ0,27757
9
+ pumaguard/server.py,sha256=zmzSXabt6K26u8kwBPdm1gI6aMAwJo3gCaSaX5Sh0vk,14705
10
+ pumaguard/sound.py,sha256=1qIV4NFOtyRfiIpFTnjbm_hYdNw54bGjMnopeHj_bhM,4633
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
@@ -22,14 +22,14 @@ pumaguard/pumaguard-ui/flutter.js,sha256=7V1ZIKmGiouT15CpquQWWmKWJyjUq77FoU9gDXP
22
22
  pumaguard/pumaguard-ui/flutter_bootstrap.js,sha256=RYi4PhelQMs063sa3jCr0BLWDMAzY7lKs-ksCptkaQM,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=84-Hk8TaSE_Ryab3pKBqZ5PmA2VJgM34cN8rkQJenQU,2673763
25
+ pumaguard/pumaguard-ui/main.dart.js,sha256=bBi_d1lU0e1KUqxMHhoIPxfk61C_ldiDNbhswY0oDtU,2727495
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
29
29
  pumaguard/pumaguard-ui/assets/AssetManifest.bin.json,sha256=_6pfLT_4Bcd6SkcHE6GNc8Uoh6UyL4dxCaK7bypu5lc,754
30
30
  pumaguard/pumaguard-ui/assets/FontManifest.json,sha256=TbzXC0njmfIhJ8D_sqF4P6NGmuI9BBSOl8AzaY-zNMo,598
31
- pumaguard/pumaguard-ui/assets/NOTICES,sha256=36KDhv5JjUTHLHSv3M-zMkJyjezJjCQB-20DGbpx7JM,1372380
32
- pumaguard/pumaguard-ui/assets/fonts/MaterialIcons-Regular.otf,sha256=R8-aO9K-VgimZR7FNPwS9yffsSwG_MlvUwqMEcH0Syg,10768
31
+ pumaguard/pumaguard-ui/assets/NOTICES,sha256=pXK1o4s9viUW6snqVEqfZsFJa-43d8tCPzyBAanqn0I,1383980
32
+ pumaguard/pumaguard-ui/assets/fonts/MaterialIcons-Regular.otf,sha256=_jHTXqz97mq6qdprZetBY0OYMj2DjhQS8g7hSNZS9X4,11092
33
33
  pumaguard/pumaguard-ui/assets/fonts/Roboto-Bold.ttf,sha256=YfifjbSSYcL2EG6NzMNd97L37ZCQINtAo_yQXpX5kzQ,514260
34
34
  pumaguard/pumaguard-ui/assets/fonts/Roboto-Light.ttf,sha256=Ao-EOxmQukbiocTvG4JynE2pqUaw2djb9Z5iPRCV5FQ,518580
35
35
  pumaguard/pumaguard-ui/assets/fonts/Roboto-Medium.ttf,sha256=KHml7Lf7-hOn_D4s3X_sv3OqRekbVB39-ixELu0KrCE,511592
@@ -62,9 +62,9 @@ pumaguard/web_routes/diagnostics.py,sha256=EIIbjuixJyGXdnVQf8RQ6xQxJar0UHZO8dF-9
62
62
  pumaguard/web_routes/directories.py,sha256=yy5TghCEyB4reRGAcVHIEfr2vlHnuiDChIXl9ZFquRM,2410
63
63
  pumaguard/web_routes/folders.py,sha256=Z63ap6dRi6NWye70HYurpCnsSXmFgzTbTsFKYdZ1Bjk,6305
64
64
  pumaguard/web_routes/photos.py,sha256=Tac_CbaZSeZzOfaJ73vlp3iyZbvfD7ei1YM3tsb0nTY,5106
65
- pumaguard/web_routes/settings.py,sha256=kVkKdaBLitKCGJiePlv34H_-E0yu_HUC3Wnxxynw5QU,8982
65
+ pumaguard/web_routes/settings.py,sha256=kjSqoX6E38UJ_J_YZRQr7QmACR7bDwFcUarqnowjHP4,11655
66
66
  pumaguard/web_routes/sync.py,sha256=Zvv6VARGE5xP29C5gWH3ul81PISRxoF8n472DITItE0,6378
67
- pumaguard-20.post235.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
67
+ pumaguard-20.post241.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
68
68
  pumaguard-sounds/cougar_call.mp3,sha256=jdPzi7Qneect3ez2G6XAeHWtetU5vSOSB6pceuB26Wc,129048
69
69
  pumaguard-sounds/cougarsounds.wav,sha256=hwVmmQ75dkOP3qd07YAvVOSm1neYtxLSzxw3Ulvs2cM,96346
70
70
  pumaguard-sounds/dark-engine-logo-141942.mp3,sha256=Vw-qyLTMPJZvsgQcZtH0DpGcP1dd7nJq-9BnHuNPGug,372819
@@ -82,8 +82,8 @@ pumaguard-sounds/mixkit-vintage-telephone-ringtone-1356.wav,sha256=zWWY2uFF0-l7P
82
82
  pumaguard-sounds/pumaguard-warning.mp3,sha256=wcCfHsulPo5P5s8MjpQAG2NYHQDsRpjqoMig1-o_MDI,232249
83
83
  pumaguard-sounds/short-round-110940.mp3,sha256=vdskGD94SeH1UJyJyR0Ek_7xGXPIZfnPdoBvxGnUt98,450816
84
84
  pumaguard-ui/ios/Flutter/ephemeral/flutter_lldb_helper.py,sha256=Bc_jl3_e5ZPvrSBJpPYtN05VxpztyKq-7lVms3rLg4Q,1276
85
- pumaguard-20.post235.dist-info/METADATA,sha256=7wGqQJ4BRlKsixUzRZXjE-EXc2vDDyrAQQDCrsJjaig,8618
86
- pumaguard-20.post235.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
87
- pumaguard-20.post235.dist-info/entry_points.txt,sha256=rmCdBTPWrbJQvPPwABSVobXE9D7hrKsITGZ6nvCrko8,127
88
- pumaguard-20.post235.dist-info/top_level.txt,sha256=B-PzS4agkQNhOYbLLIrMVOyMD_pl5F-yujPBm5zYYjY,40
89
- pumaguard-20.post235.dist-info/RECORD,,
85
+ pumaguard-20.post241.dist-info/METADATA,sha256=em3o9el8uRwvvg1VIsOwJb83VHgNnf7IxQKjURJnB2I,8618
86
+ pumaguard-20.post241.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
87
+ pumaguard-20.post241.dist-info/entry_points.txt,sha256=rmCdBTPWrbJQvPPwABSVobXE9D7hrKsITGZ6nvCrko8,127
88
+ pumaguard-20.post241.dist-info/top_level.txt,sha256=B-PzS4agkQNhOYbLLIrMVOyMD_pl5F-yujPBm5zYYjY,40
89
+ pumaguard-20.post241.dist-info/RECORD,,