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.
- homesec/app.py +5 -14
- homesec/cli.py +5 -4
- homesec/config/__init__.py +8 -1
- homesec/config/loader.py +17 -2
- homesec/config/validation.py +99 -6
- homesec/interfaces.py +2 -2
- homesec/maintenance/cleanup_clips.py +17 -4
- homesec/models/__init__.py +3 -23
- homesec/models/clip.py +1 -1
- homesec/models/config.py +10 -259
- homesec/models/enums.py +8 -0
- homesec/models/events.py +1 -1
- homesec/models/filter.py +3 -21
- homesec/models/vlm.py +11 -20
- homesec/pipeline/__init__.py +1 -2
- homesec/pipeline/core.py +9 -10
- homesec/plugins/alert_policies/__init__.py +5 -5
- homesec/plugins/alert_policies/default.py +21 -2
- homesec/plugins/analyzers/__init__.py +1 -3
- homesec/plugins/analyzers/openai.py +20 -13
- homesec/plugins/filters/__init__.py +1 -2
- homesec/plugins/filters/yolo.py +25 -5
- homesec/plugins/notifiers/__init__.py +1 -6
- homesec/plugins/notifiers/mqtt.py +21 -1
- homesec/plugins/notifiers/sendgrid_email.py +52 -1
- homesec/plugins/registry.py +27 -0
- homesec/plugins/sources/__init__.py +4 -4
- homesec/plugins/sources/ftp.py +1 -1
- homesec/plugins/sources/local_folder.py +1 -1
- homesec/plugins/sources/rtsp.py +2 -2
- homesec/plugins/storage/__init__.py +1 -9
- homesec/plugins/storage/dropbox.py +13 -1
- homesec/plugins/storage/local.py +8 -1
- homesec/repository/clip_repository.py +1 -1
- homesec/sources/__init__.py +3 -4
- homesec/sources/ftp.py +95 -2
- homesec/sources/local_folder.py +27 -2
- homesec/sources/rtsp/__init__.py +5 -0
- homesec/sources/rtsp/clock.py +18 -0
- homesec/sources/rtsp/core.py +1424 -0
- homesec/sources/rtsp/frame_pipeline.py +325 -0
- homesec/sources/rtsp/hardware.py +143 -0
- homesec/sources/rtsp/motion.py +94 -0
- homesec/sources/rtsp/recorder.py +180 -0
- homesec/sources/rtsp/utils.py +35 -0
- {homesec-1.2.1.dist-info → homesec-1.2.3.dist-info}/METADATA +13 -16
- homesec-1.2.3.dist-info/RECORD +73 -0
- homesec/models/source.py +0 -81
- homesec/pipeline/alert_policy.py +0 -5
- homesec/sources/rtsp.py +0 -1304
- homesec-1.2.1.dist-info/RECORD +0 -68
- /homesec/{plugins/notifiers → notifiers}/multiplex.py +0 -0
- {homesec-1.2.1.dist-info → homesec-1.2.3.dist-info}/WHEEL +0 -0
- {homesec-1.2.1.dist-info → homesec-1.2.3.dist-info}/entry_points.txt +0 -0
- {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.
|
|
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
|
-
###
|
|
330
|
-
|
|
329
|
+
### Docker
|
|
330
|
+
Use the included [docker-compose.yml](docker-compose.yml) (HomeSec + Postgres, pulls `leva/homesec:latest`).
|
|
331
331
|
|
|
332
|
-
|
|
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
|
-
|
|
398
|
+
backend: rtsp
|
|
404
399
|
config:
|
|
405
400
|
rtsp_url_env: DRIVEWAY_RTSP_URL
|
|
406
401
|
output_dir: "./recordings"
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|