homesec 1.2.1__py3-none-any.whl → 1.2.3__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.
Files changed (55) hide show
  1. homesec/app.py +5 -14
  2. homesec/cli.py +5 -4
  3. homesec/config/__init__.py +8 -1
  4. homesec/config/loader.py +17 -2
  5. homesec/config/validation.py +99 -6
  6. homesec/interfaces.py +2 -2
  7. homesec/maintenance/cleanup_clips.py +17 -4
  8. homesec/models/__init__.py +3 -23
  9. homesec/models/clip.py +1 -1
  10. homesec/models/config.py +10 -259
  11. homesec/models/enums.py +8 -0
  12. homesec/models/events.py +1 -1
  13. homesec/models/filter.py +3 -21
  14. homesec/models/vlm.py +11 -20
  15. homesec/pipeline/__init__.py +1 -2
  16. homesec/pipeline/core.py +9 -10
  17. homesec/plugins/alert_policies/__init__.py +5 -5
  18. homesec/plugins/alert_policies/default.py +21 -2
  19. homesec/plugins/analyzers/__init__.py +1 -3
  20. homesec/plugins/analyzers/openai.py +20 -13
  21. homesec/plugins/filters/__init__.py +1 -2
  22. homesec/plugins/filters/yolo.py +25 -5
  23. homesec/plugins/notifiers/__init__.py +1 -6
  24. homesec/plugins/notifiers/mqtt.py +21 -1
  25. homesec/plugins/notifiers/sendgrid_email.py +52 -1
  26. homesec/plugins/registry.py +27 -0
  27. homesec/plugins/sources/__init__.py +4 -4
  28. homesec/plugins/sources/ftp.py +1 -1
  29. homesec/plugins/sources/local_folder.py +1 -1
  30. homesec/plugins/sources/rtsp.py +2 -2
  31. homesec/plugins/storage/__init__.py +1 -9
  32. homesec/plugins/storage/dropbox.py +13 -1
  33. homesec/plugins/storage/local.py +8 -1
  34. homesec/repository/clip_repository.py +1 -1
  35. homesec/sources/__init__.py +3 -4
  36. homesec/sources/ftp.py +95 -2
  37. homesec/sources/local_folder.py +27 -2
  38. homesec/sources/rtsp/__init__.py +5 -0
  39. homesec/sources/rtsp/clock.py +18 -0
  40. homesec/sources/rtsp/core.py +1424 -0
  41. homesec/sources/rtsp/frame_pipeline.py +325 -0
  42. homesec/sources/rtsp/hardware.py +143 -0
  43. homesec/sources/rtsp/motion.py +94 -0
  44. homesec/sources/rtsp/recorder.py +180 -0
  45. homesec/sources/rtsp/utils.py +35 -0
  46. {homesec-1.2.1.dist-info → homesec-1.2.3.dist-info}/METADATA +13 -16
  47. homesec-1.2.3.dist-info/RECORD +73 -0
  48. homesec/models/source.py +0 -81
  49. homesec/pipeline/alert_policy.py +0 -5
  50. homesec/sources/rtsp.py +0 -1304
  51. homesec-1.2.1.dist-info/RECORD +0 -68
  52. /homesec/{plugins/notifiers → notifiers}/multiplex.py +0 -0
  53. {homesec-1.2.1.dist-info → homesec-1.2.3.dist-info}/WHEEL +0 -0
  54. {homesec-1.2.1.dist-info → homesec-1.2.3.dist-info}/entry_points.txt +0 -0
  55. {homesec-1.2.1.dist-info → homesec-1.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,180 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import subprocess
5
+ from pathlib import Path
6
+ from typing import Protocol
7
+
8
+ from homesec.sources.rtsp.clock import Clock
9
+ from homesec.sources.rtsp.utils import _format_cmd, _is_timeout_option_error, _redact_rtsp_url
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class Recorder(Protocol):
15
+ def start(self, output_file: Path, stderr_log: Path) -> subprocess.Popen[bytes] | None: ...
16
+
17
+ def stop(self, proc: subprocess.Popen[bytes], output_file: Path | None) -> None: ...
18
+
19
+ def is_alive(self, proc: subprocess.Popen[bytes]) -> bool: ...
20
+
21
+
22
+ class FfmpegRecorder:
23
+ def __init__(
24
+ self,
25
+ *,
26
+ rtsp_url: str,
27
+ ffmpeg_flags: list[str],
28
+ rtsp_connect_timeout_s: float,
29
+ rtsp_io_timeout_s: float,
30
+ clock: Clock,
31
+ ) -> None:
32
+ self._rtsp_url = rtsp_url
33
+ self._ffmpeg_flags = ffmpeg_flags
34
+ self._rtsp_connect_timeout_s = rtsp_connect_timeout_s
35
+ self._rtsp_io_timeout_s = rtsp_io_timeout_s
36
+ self._clock = clock
37
+
38
+ def start(self, output_file: Path, stderr_log: Path) -> subprocess.Popen[bytes] | None:
39
+ def _read_tail(path: Path, max_bytes: int = 4000) -> str:
40
+ try:
41
+ data = path.read_bytes()
42
+ except Exception as exc:
43
+ logger.warning("Failed to read recording stderr tail: %s", exc, exc_info=True)
44
+ return ""
45
+ if len(data) <= max_bytes:
46
+ return data.decode(errors="replace")
47
+ return data[-max_bytes:].decode(errors="replace")
48
+
49
+ cmd_base = [
50
+ "ffmpeg",
51
+ "-rtsp_transport",
52
+ "tcp",
53
+ "-rtsp_flags",
54
+ "prefer_tcp",
55
+ "-user_agent",
56
+ "Lavf",
57
+ ]
58
+
59
+ user_flags = self._ffmpeg_flags
60
+ has_stimeout = any(x == "-stimeout" for x in user_flags)
61
+ has_rw_timeout = any(x == "-rw_timeout" for x in user_flags)
62
+ timeout_us_connect = str(int(max(0.1, self._rtsp_connect_timeout_s) * 1_000_000))
63
+ timeout_us_io = str(int(max(0.1, self._rtsp_io_timeout_s) * 1_000_000))
64
+
65
+ timeout_args: list[str] = []
66
+ if not has_stimeout and self._rtsp_connect_timeout_s > 0:
67
+ timeout_args.extend(["-stimeout", timeout_us_connect])
68
+ if not has_rw_timeout and self._rtsp_io_timeout_s > 0:
69
+ timeout_args.extend(["-rw_timeout", timeout_us_io])
70
+
71
+ cmd_tail = ["-i", self._rtsp_url, "-c", "copy", "-f", "mp4", "-y"]
72
+
73
+ # Naive check to see if user overrode defaults
74
+ # If user supplies ANY -loglevel, we don't add ours.
75
+ # If user supplies ANY -fflags, we don't add ours (to avoid concatenation complexity).
76
+ # This allows full user control.
77
+ has_loglevel = any(x == "-loglevel" for x in user_flags)
78
+ if not has_loglevel:
79
+ cmd_tail.extend(["-loglevel", "warning"])
80
+
81
+ has_fflags = any(x == "-fflags" for x in user_flags)
82
+ if not has_fflags:
83
+ cmd_tail.extend(["-fflags", "+genpts+igndts"])
84
+
85
+ has_fps_mode = any(x == "-fps_mode" or x == "-vsync" for x in user_flags)
86
+ if not has_fps_mode:
87
+ cmd_tail.extend(["-vsync", "0"])
88
+
89
+ # Add user flags last so they can potentially override or add to the above
90
+ cmd_tail.extend(user_flags)
91
+ cmd_tail.extend([str(output_file)])
92
+
93
+ attempts: list[tuple[str, list[str]]] = []
94
+ if timeout_args:
95
+ attempts.append(("timeouts", timeout_args))
96
+ attempts.append(("no_timeouts" if timeout_args else "default", []))
97
+
98
+ for label, extra_args in attempts:
99
+ cmd = list(cmd_base) + list(extra_args) + cmd_tail
100
+
101
+ safe_cmd = list(cmd)
102
+ try:
103
+ idx = safe_cmd.index("-i")
104
+ safe_cmd[idx + 1] = _redact_rtsp_url(str(safe_cmd[idx + 1]))
105
+ except Exception as exc:
106
+ logger.warning("Failed to redact recording RTSP URL: %s", exc, exc_info=True)
107
+ logger.debug("Recording ffmpeg (%s): %s", label, _format_cmd(safe_cmd))
108
+
109
+ try:
110
+ with open(stderr_log, "w") as stderr_file:
111
+ proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=stderr_file)
112
+
113
+ self._clock.sleep(0.5)
114
+ if proc.poll() is None:
115
+ return proc
116
+
117
+ stderr_tail = _read_tail(stderr_log)
118
+ timeout_option_error = (
119
+ label == "timeouts"
120
+ and bool(stderr_tail)
121
+ and _is_timeout_option_error(stderr_tail)
122
+ )
123
+
124
+ if timeout_option_error:
125
+ logger.warning(
126
+ "Recording process died immediately (%s, exit code: %s); timeout options unsupported",
127
+ label,
128
+ proc.returncode,
129
+ )
130
+ logger.warning("Check logs at: %s", stderr_log)
131
+ else:
132
+ logger.error(
133
+ "Recording process died immediately (%s, exit code: %s)",
134
+ label,
135
+ proc.returncode,
136
+ )
137
+ logger.error("Check logs at: %s", stderr_log)
138
+
139
+ if stderr_tail:
140
+ redacted_tail = stderr_tail.replace(
141
+ self._rtsp_url, _redact_rtsp_url(self._rtsp_url)
142
+ )
143
+ if timeout_option_error:
144
+ logger.warning("Recording stderr tail (%s):\n%s", label, redacted_tail)
145
+ logger.warning(
146
+ "Recording ffmpeg missing timeout options; retrying without timeouts"
147
+ )
148
+ continue
149
+ logger.error("Recording stderr tail (%s):\n%s", label, redacted_tail)
150
+ if label == "timeouts":
151
+ return None
152
+ except Exception:
153
+ logger.exception("Failed to start recording")
154
+ return None
155
+
156
+ return None
157
+
158
+ def stop(self, proc: subprocess.Popen[bytes], output_file: Path | None) -> None:
159
+ try:
160
+ if proc.poll() is None:
161
+ proc.terminate()
162
+ proc.wait(timeout=5)
163
+ except subprocess.TimeoutExpired:
164
+ logger.warning("Recording process did not terminate, killing (PID: %s)", proc.pid)
165
+ try:
166
+ proc.kill()
167
+ proc.wait(timeout=2)
168
+ except Exception:
169
+ logger.exception("Failed to kill recording process (PID: %s)", proc.pid)
170
+ except Exception:
171
+ logger.exception("Failed while stopping recording process (PID: %s)", proc.pid)
172
+
173
+ logger.debug(
174
+ "Stopped recording: %s",
175
+ output_file,
176
+ extra={"recording_id": output_file.name if output_file else None},
177
+ )
178
+
179
+ def is_alive(self, proc: subprocess.Popen[bytes]) -> bool:
180
+ return proc.poll() is None
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import shlex
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def _redact_rtsp_url(url: str) -> str:
10
+ if "://" not in url:
11
+ return url
12
+ scheme, rest = url.split("://", 1)
13
+ if "@" not in rest:
14
+ return url
15
+ _creds, host = rest.split("@", 1)
16
+ return f"{scheme}://***:***@{host}"
17
+
18
+
19
+ def _format_cmd(cmd: list[str]) -> str:
20
+ try:
21
+ return shlex.join([str(x) for x in cmd])
22
+ except Exception as exc:
23
+ logger.warning("Failed to format command with shlex.join: %s", exc, exc_info=True)
24
+ return " ".join([str(x) for x in cmd])
25
+
26
+
27
+ def _is_timeout_option_error(stderr_text: str) -> bool:
28
+ text = stderr_text.lower()
29
+ return ("rw_timeout" in text and ("not found" in text or "unrecognized option" in text)) or (
30
+ "stimeout" in text and ("not found" in text or "unrecognized option" in text)
31
+ )
32
+
33
+
34
+ def _next_backoff(backoff_s: float, cap_s: float, *, factor: float = 1.6) -> float:
35
+ return min(backoff_s * factor, cap_s)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: homesec
3
- Version: 1.2.1
3
+ Version: 1.2.3
4
4
  Summary: Pluggable async home security camera pipeline with detection, VLM analysis, and alerts.
5
5
  Project-URL: Homepage, https://github.com/lan17/homesec
6
6
  Project-URL: Source, https://github.com/lan17/homesec
@@ -326,15 +326,10 @@ graph TD
326
326
 
327
327
  ## Quickstart
328
328
 
329
- ### 30-Second Start (Docker)
330
- The fastest way to see it in action. Includes a pre-configured Postgres and a dummy local source.
329
+ ### Docker
330
+ Use the included [docker-compose.yml](docker-compose.yml) (HomeSec + Postgres, pulls `leva/homesec:latest`).
331
331
 
332
- ```bash
333
- git clone https://github.com/lan17/homesec.git
334
- cd homesec
335
- make up
336
- ```
337
- *Modify `config/config.yaml` to add your real cameras, then restart.*
332
+ Configure your own config.yaml and .env files as described in Manual Setup.
338
333
 
339
334
  ### Manual Setup
340
335
  For standard production usage without Docker Compose:
@@ -400,16 +395,18 @@ Best for real-world setups with flaky cameras.
400
395
  cameras:
401
396
  - name: driveway
402
397
  source:
403
- type: rtsp
398
+ backend: rtsp
404
399
  config:
405
400
  rtsp_url_env: DRIVEWAY_RTSP_URL
406
401
  output_dir: "./recordings"
407
- # Critical for camera compatibility:
408
- ffmpeg_flags: ["-rtsp_transport", "tcp", "-vsync", "0"]
409
- reconnect_backoff_s: 5
402
+ stream:
403
+ # Critical for camera compatibility:
404
+ ffmpeg_flags: ["-rtsp_transport", "tcp", "-vsync", "0"]
405
+ reconnect:
406
+ backoff_s: 5
410
407
 
411
408
  filter:
412
- plugin: yolo
409
+ backend: yolo
413
410
  config:
414
411
  classes: ["person", "car"]
415
412
  min_confidence: 0.6
@@ -426,7 +423,7 @@ Uploads to Cloud but keeps analysis local.
426
423
  ```yaml
427
424
  storage:
428
425
  backend: dropbox
429
- dropbox:
426
+ config:
430
427
  token_env: DROPBOX_TOKEN
431
428
  root: "/SecurityCam"
432
429
 
@@ -488,7 +485,7 @@ HomeSec uses a plugin architecture where every component is discovered at runtim
488
485
 
489
486
  | Type | Plugins |
490
487
  |------|---------|
491
- | Sources | [`rtsp`](src/homesec/sources/rtsp.py), [`ftp`](src/homesec/sources/ftp.py), [`local_folder`](src/homesec/sources/local_folder.py) |
488
+ | Sources | [`rtsp`](src/homesec/sources/rtsp/core.py), [`ftp`](src/homesec/sources/ftp.py), [`local_folder`](src/homesec/sources/local_folder.py) |
492
489
  | Filters | [`yolo`](src/homesec/plugins/filters/yolo.py) |
493
490
  | Storage | [`dropbox`](src/homesec/plugins/storage/dropbox.py), [`local`](src/homesec/plugins/storage/local.py) |
494
491
  | VLM analyzers | [`openai`](src/homesec/plugins/analyzers/openai.py) |
@@ -0,0 +1,73 @@
1
+ homesec/__init__.py,sha256=i4wRlg0CV3bxbxAWL_uSqW39r_F2sTRBNbelmoYOPuA,452
2
+ homesec/app.py,sha256=QkO_cs10_ARXS4H7fVe1scTfMDWjJPXU2hLVuCcRheA,11860
3
+ homesec/cli.py,sha256=8I8odsgjsZpzpV2AKZNqktx7xcw7n6i_Vzr6mlpNwAM,5685
4
+ homesec/errors.py,sha256=fBW_OdnYgqtb6u6t0YZh4tHgsiv0Pb1DiF9G6b4Vcbk,2105
5
+ homesec/interfaces.py,sha256=O059JmSVcRVWnUspd257ReEjX1zshjoiz86lYa52nsk,9873
6
+ homesec/logging_setup.py,sha256=Z12nzCPK04cqFHfIQgasioGfXb4TLvXJT9stzD8Dh4c,5546
7
+ homesec/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ homesec/storage_paths.py,sha256=MGQNT_7mJS6wuTWUHZNl0hvRmSVRvNksLu_imfZOEpo,1696
9
+ homesec/config/__init__.py,sha256=luKM0A_xT-Ky5-xmwPNA3f1EAYn9oBGCra2Inf6ATFU,538
10
+ homesec/config/loader.py,sha256=lUpry5Ha9Kbd9Rx8SkGMSQ2xt1_-yEfBHldvstuU310,3465
11
+ homesec/config/validation.py,sha256=z_8AN0ytFNme3cApUDwpLZYTKbmOXMsOH5y9Ca-dSJA,7056
12
+ homesec/health/__init__.py,sha256=fbndfsLOR9aA7d_5I1mEZN0oM5IYMmcJNjOt0iaXKZc,107
13
+ homesec/health/server.py,sha256=VP-4XmZ0K3ooFyd000AFOZohZ5R7QcnZC4n7oj0RMqI,7014
14
+ homesec/maintenance/__init__.py,sha256=6a5W2x8oUgnoWaK374-Wq_nrOD5UDAUqUtSANaEck2M,60
15
+ homesec/maintenance/cleanup_clips.py,sha256=H-12mY9yJcmJjh7es0vZLpif0ttj1uTNQ7aWTwEYr4I,24047
16
+ homesec/models/__init__.py,sha256=AZZxyqdNyLqWz8Lg-1IwwEIBsFO1WNsEXNGQinMFRys,1372
17
+ homesec/models/alert.py,sha256=cMFr4NUGygq0-m2ep0jhjzVrFKM7kN1wJON1J0XwGY4,1010
18
+ homesec/models/clip.py,sha256=FNun9yH8gN-Z66zbrFM4FA2gbAwp8iLyOifWLL41qDY,2228
19
+ homesec/models/config.py,sha256=jGsi65mNgfBg1qqh2Urrrt5C9bBmEL0hPX24mqwx1pU,4319
20
+ homesec/models/enums.py,sha256=puyfL8_JQNy3RbML5vkO2vTFMjXTeRgGp7GQhok1SVQ,3446
21
+ homesec/models/events.py,sha256=m3jQM6GrgUCiXj4pXgtaM-EBtL56qE5nqYaXrz3mMuA,4945
22
+ homesec/models/filter.py,sha256=Jqd0nk6hygf-1rB_yjQcabcvVsavk5TY6v3K_v_Xjac,1647
23
+ homesec/models/storage.py,sha256=63wyHdDt3QrfdsP0SmhrxtOeWRllZ1O2GPrA4jI7XmU,235
24
+ homesec/models/vlm.py,sha256=QpwItxAm4k7DM-4GWm-L_ObdBRq4j9hOxoUJI2cTTY0,2966
25
+ homesec/notifiers/multiplex.py,sha256=LlnwozjkMDQwz7__v7mT4AohZbiWZK39CZunamRp7FM,3676
26
+ homesec/pipeline/__init__.py,sha256=7iil1gPFagQIM79hTtmY4gtr8Q-g4HOBI_3BA2CRVHo,131
27
+ homesec/pipeline/core.py,sha256=IhyFNjBL-OMfpj100xv9ddH12-TQxKvi6r3E8nSE3yw,23596
28
+ homesec/plugins/__init__.py,sha256=ex1AY_pJa9PZkr6yaneX3KgImZ-GkenTwExH2cMcF3A,1269
29
+ homesec/plugins/registry.py,sha256=crLQ2Fidni1GNNwuaGGSg66K-e1-nvjiY-KdLTrfjlA,6564
30
+ homesec/plugins/utils.py,sha256=a87kRYBZPnaUuvn6kRdf6N38b8aAbcPW9r-30T2rlBI,1980
31
+ homesec/plugins/alert_policies/__init__.py,sha256=WllHQAOLF85H6DdeiJAzgNJNceaWnR5gw7ZUnZH_aDA,1405
32
+ homesec/plugins/alert_policies/default.py,sha256=Rhm__YBYLFq-Q6whpuJMWl0ccl2mw8NMEBZ9J4DwGsA,3914
33
+ homesec/plugins/alert_policies/noop.py,sha256=xcdyZAlQf6X_AqQWUPkRMHLK8J6d7PTXWoPbwpnSlQE,1262
34
+ homesec/plugins/analyzers/__init__.py,sha256=Lg1DcOx4zGCDmZ4p09oDH1ZX-rxTUL_k0DLU0vvRW08,822
35
+ homesec/plugins/analyzers/openai.py,sha256=LBXAm2MULwSxgto-y5qhVi1sEXLtFusUNLUcXz7cy5w,16280
36
+ homesec/plugins/filters/__init__.py,sha256=IO7kSKKXPgBYNa3_a9w0wQv41orOIuUhfQ8dxOdIIBM,823
37
+ homesec/plugins/filters/yolo.py,sha256=H1m-ZHd796EJUjwofw0WH6MBu0r_S5VprZwjulsAdW0,11705
38
+ homesec/plugins/notifiers/__init__.py,sha256=gr9TFzVayun5DFIr92S3Iy5DtmKqVvefSKfH2-Nj-MM,932
39
+ homesec/plugins/notifiers/mqtt.py,sha256=nMlaynpgdLYmhjg8bo2c3lw0pJCCgdBQykFbnctJ7vc,6377
40
+ homesec/plugins/notifiers/sendgrid_email.py,sha256=lMOg7BCThbVNnlXkT8yqqw-Xb-lWbiMHT8oiww2c83Y,10614
41
+ homesec/plugins/sources/__init__.py,sha256=DrseH7lAcRFl70sKUG9GQXWWBbPVuoE9cAsz61fZV9o,1037
42
+ homesec/plugins/sources/ftp.py,sha256=PTz9IdjPUjRnqqYqkjYF3SRFJPkFR2lJlzMHray-u5g,847
43
+ homesec/plugins/sources/local_folder.py,sha256=5CalSDRCijGKC_1Lk6EaZt9uZ3QuQiukrLQeupEnzlU,1221
44
+ homesec/plugins/sources/rtsp.py,sha256=-JdbDrYo924tKetU423HWQRlHelVHXs9EPRkx94sa18,823
45
+ homesec/plugins/storage/__init__.py,sha256=_mjmAEX7-0uDRY5v50thv6NmXyG-jpqo_Av5bCw959A,887
46
+ homesec/plugins/storage/dropbox.py,sha256=HLkblKL4_qod8m4q7ZUiwEMYJ1reaEgZ2cw5gPOhsz8,10301
47
+ homesec/plugins/storage/local.py,sha256=QJgd0lRX3KUSzJFAtOII7SiaIVgGKE59QSo6e7YYXG4,3317
48
+ homesec/repository/__init__.py,sha256=6cye2uQIA2v6jeLk5D2S9y3rlkfzJH5GceqdOroF3hU,160
49
+ homesec/repository/clip_repository.py,sha256=NrJXa4-HedO9xMnlZsckH7mi6EqeOpUb53LlE0qb2TA,17096
50
+ homesec/sources/__init__.py,sha256=GEhd68w1BNhF09SK2rfH93RImOoyYzsPG7OW1Lrk2yE,477
51
+ homesec/sources/base.py,sha256=dKTxJxcDwJtykWDN3WYzkW5mtkRqlOJxJLWcLy82_Zo,7582
52
+ homesec/sources/ftp.py,sha256=PRg3oFqlUrxSbwyoayF1Fp076zJILNylLdJVUZAU_Eg,10058
53
+ homesec/sources/local_folder.py,sha256=C6g0FGZqp-O31gEhLphLmfCtLhOlJsymY0lgMMc8dh4,9125
54
+ homesec/sources/rtsp/__init__.py,sha256=wtBzdwzL7Cg0HyIGIpS3lBagekCAi_EYJOyCZFbT7K0,103
55
+ homesec/sources/rtsp/clock.py,sha256=Gf-CLBfgUmxfajmZim89vmWRG14hnc7iUUBVNNiUz6w,338
56
+ homesec/sources/rtsp/core.py,sha256=bzO5RBqQQx6rEAxLwesdZY5-Tm05MM_n9Gd-HA-cLDQ,52957
57
+ homesec/sources/rtsp/frame_pipeline.py,sha256=cIGrH5z4WE8swoAThTl7Wu3SViNq-00YyCcKMLBrCso,11620
58
+ homesec/sources/rtsp/hardware.py,sha256=sKCJhoVdmkDfCk2s5RL0lDPX_CuvcwlgVUESgRJK3us,4892
59
+ homesec/sources/rtsp/motion.py,sha256=FKDa1hvD_H3uxgnQ-Z0c7MhVd6u_35cO6XKZRVrXpaY,2870
60
+ homesec/sources/rtsp/recorder.py,sha256=fqL-zr-vtnFrDQc87zZFYeM-6JSUfMwQsc0zfR-mgy4,7009
61
+ homesec/sources/rtsp/utils.py,sha256=aufPAP6oc39kyFAiN0HTDhdXf3bQqWSVeVWNIHx5MmI,1021
62
+ homesec/state/__init__.py,sha256=Evt1jqTebmpJD1NUzNh3vwt5pbjDlLjQ0DgMCSAZOuM,255
63
+ homesec/state/postgres.py,sha256=I-cXqW5cgz-hpaHc0JIv3DnIBTmGxE28P8ZxBAGabSw,17765
64
+ homesec/telemetry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
+ homesec/telemetry/db_log_handler.py,sha256=KM8g4kcOyPzFJbpGxpSzecx_hrEWY0YfpoIKygETy5k,7539
66
+ homesec/telemetry/postgres_settings.py,sha256=EVD2_oi_KReFJvQmXxW026aurl_YD-KexT7rkbGQPHc,1198
67
+ homesec/telemetry/db/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
68
+ homesec/telemetry/db/log_table.py,sha256=wcZLwRht7FMa0z2gf37f_RxdVTNIdDiK4i_N3c_ibwg,473
69
+ homesec-1.2.3.dist-info/METADATA,sha256=j8zvaP62Dws7sbTz6eUB3TCYcOAX-tlHjiz0q2lWpQk,25081
70
+ homesec-1.2.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
71
+ homesec-1.2.3.dist-info/entry_points.txt,sha256=8ocCj_fP1qxIuL-DVDAUiaUbEdTMX_kg_BzVrJsbQYg,45
72
+ homesec-1.2.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
73
+ homesec-1.2.3.dist-info/RECORD,,
homesec/models/source.py DELETED
@@ -1,81 +0,0 @@
1
- """Source configuration models."""
2
-
3
- from __future__ import annotations
4
-
5
- from pydantic import BaseModel, Field, field_validator
6
-
7
-
8
- class RTSPSourceConfig(BaseModel):
9
- """RTSP source configuration."""
10
-
11
- model_config = {"extra": "forbid"}
12
-
13
- camera_name: str | None = None
14
- rtsp_url_env: str | None = None
15
- rtsp_url: str | None = None
16
- detect_rtsp_url_env: str | None = None
17
- detect_rtsp_url: str | None = None
18
- output_dir: str = "./recordings"
19
- pixel_threshold: int = 45
20
- min_changed_pct: float = 1.0
21
- blur_kernel: int = 5
22
- stop_delay: float = 10.0
23
- max_recording_s: float = 60.0
24
- max_reconnect_attempts: int = 20
25
- disable_hwaccel: bool = False
26
- frame_timeout_s: float = 2.0
27
- frame_queue_size: int = 20
28
- reconnect_backoff_s: float = 1.0
29
- debug_motion: bool = False
30
- heartbeat_s: float = 30.0
31
- rtsp_connect_timeout_s: float = 2.0
32
- rtsp_io_timeout_s: float = 2.0
33
- ffmpeg_flags: list[str] = Field(default_factory=list)
34
-
35
-
36
- class LocalFolderSourceConfig(BaseModel):
37
- """Local folder source configuration."""
38
-
39
- model_config = {"extra": "forbid"}
40
-
41
- camera_name: str | None = None
42
- watch_dir: str = "recordings"
43
- poll_interval: float = 1.0
44
- stability_threshold_s: float = 3.0
45
-
46
-
47
- class FtpSourceConfig(BaseModel):
48
- """FTP source configuration."""
49
-
50
- model_config = {"extra": "forbid"}
51
-
52
- camera_name: str | None = None
53
- host: str = "0.0.0.0"
54
- port: int = 2121
55
- root_dir: str = "./ftp_incoming"
56
- ftp_subdir: str | None = None
57
- anonymous: bool = True
58
- username_env: str | None = None
59
- password_env: str | None = None
60
- perms: str = "elw"
61
- passive_ports: str | None = None
62
- masquerade_address: str | None = None
63
- heartbeat_s: float = 30.0
64
- allowed_extensions: list[str] = Field(default_factory=lambda: [".mp4"])
65
- delete_non_matching: bool = True
66
- delete_incomplete: bool = True
67
- default_duration_s: float = 10.0
68
- log_level: str = "INFO"
69
-
70
- @field_validator("allowed_extensions")
71
- @classmethod
72
- def _normalize_extensions(cls, value: list[str]) -> list[str]:
73
- cleaned: list[str] = []
74
- for item in value:
75
- ext = str(item).strip().lower()
76
- if not ext:
77
- continue
78
- if not ext.startswith("."):
79
- ext = f".{ext}"
80
- cleaned.append(ext)
81
- return cleaned
@@ -1,5 +0,0 @@
1
- """Alert policy implementation (re-export)."""
2
-
3
- from homesec.plugins.alert_policies.default import DefaultAlertPolicy
4
-
5
- __all__ = ["DefaultAlertPolicy"]