homesec 1.2.1__py3-none-any.whl → 1.2.2__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/models/__init__.py +3 -1
- homesec/models/config.py +3 -1
- homesec/models/source/__init__.py +3 -0
- homesec/models/source/ftp.py +97 -0
- homesec/models/source/local_folder.py +30 -0
- homesec/models/source/rtsp.py +165 -0
- homesec/plugins/sources/ftp.py +1 -1
- homesec/plugins/sources/local_folder.py +1 -1
- homesec/plugins/sources/rtsp.py +2 -2
- homesec/sources/__init__.py +4 -2
- homesec/sources/ftp.py +1 -1
- homesec/sources/local_folder.py +1 -1
- homesec/sources/rtsp/__init__.py +5 -0
- homesec/sources/rtsp/clock.py +18 -0
- homesec/sources/rtsp/core.py +1264 -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.2.dist-info}/METADATA +7 -5
- {homesec-1.2.1.dist-info → homesec-1.2.2.dist-info}/RECORD +25 -15
- homesec/models/source.py +0 -81
- homesec/sources/rtsp.py +0 -1304
- {homesec-1.2.1.dist-info → homesec-1.2.2.dist-info}/WHEEL +0 -0
- {homesec-1.2.1.dist-info → homesec-1.2.2.dist-info}/entry_points.txt +0 -0
- {homesec-1.2.1.dist-info → homesec-1.2.2.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.2
|
|
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
|
|
@@ -404,9 +404,11 @@ cameras:
|
|
|
404
404
|
config:
|
|
405
405
|
rtsp_url_env: DRIVEWAY_RTSP_URL
|
|
406
406
|
output_dir: "./recordings"
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
407
|
+
stream:
|
|
408
|
+
# Critical for camera compatibility:
|
|
409
|
+
ffmpeg_flags: ["-rtsp_transport", "tcp", "-vsync", "0"]
|
|
410
|
+
reconnect:
|
|
411
|
+
backoff_s: 5
|
|
410
412
|
|
|
411
413
|
filter:
|
|
412
414
|
plugin: yolo
|
|
@@ -488,7 +490,7 @@ HomeSec uses a plugin architecture where every component is discovered at runtim
|
|
|
488
490
|
|
|
489
491
|
| Type | Plugins |
|
|
490
492
|
|------|---------|
|
|
491
|
-
| Sources | [`rtsp`](src/homesec/sources/rtsp.py), [`ftp`](src/homesec/sources/ftp.py), [`local_folder`](src/homesec/sources/local_folder.py) |
|
|
493
|
+
| Sources | [`rtsp`](src/homesec/sources/rtsp/core.py), [`ftp`](src/homesec/sources/ftp.py), [`local_folder`](src/homesec/sources/local_folder.py) |
|
|
492
494
|
| Filters | [`yolo`](src/homesec/plugins/filters/yolo.py) |
|
|
493
495
|
| Storage | [`dropbox`](src/homesec/plugins/storage/dropbox.py), [`local`](src/homesec/plugins/storage/local.py) |
|
|
494
496
|
| VLM analyzers | [`openai`](src/homesec/plugins/analyzers/openai.py) |
|
|
@@ -13,16 +13,19 @@ homesec/health/__init__.py,sha256=fbndfsLOR9aA7d_5I1mEZN0oM5IYMmcJNjOt0iaXKZc,10
|
|
|
13
13
|
homesec/health/server.py,sha256=VP-4XmZ0K3ooFyd000AFOZohZ5R7QcnZC4n7oj0RMqI,7014
|
|
14
14
|
homesec/maintenance/__init__.py,sha256=6a5W2x8oUgnoWaK374-Wq_nrOD5UDAUqUtSANaEck2M,60
|
|
15
15
|
homesec/maintenance/cleanup_clips.py,sha256=sqeWLE9GrJfe29ntCasgvT2M-FWTymydHDQG-fZ3khg,23451
|
|
16
|
-
homesec/models/__init__.py,sha256=
|
|
16
|
+
homesec/models/__init__.py,sha256=0gcTdSg0CW77Hy3D7wlCyyYvvBJMdD51BL5uIiz4Vfc,2043
|
|
17
17
|
homesec/models/alert.py,sha256=cMFr4NUGygq0-m2ep0jhjzVrFKM7kN1wJON1J0XwGY4,1010
|
|
18
18
|
homesec/models/clip.py,sha256=QaftnY7q20fQX4BTX6j-FBZLaoVSgk0DbpNf8kdnT60,2225
|
|
19
|
-
homesec/models/config.py,sha256=
|
|
19
|
+
homesec/models/config.py,sha256=5V0_RNIh-hqKA8OCU5EdBwQVRbZ3nmkqoay54V6vgVE,14272
|
|
20
20
|
homesec/models/enums.py,sha256=WQk1PvYnJd5iaz51P5GM3XayqCf0VpQnLYmSuw1hrZk,3293
|
|
21
21
|
homesec/models/events.py,sha256=sgPDCSp9w60VUKKREYxNBZaxrFWW_bYyLwMFGHBawjk,4942
|
|
22
22
|
homesec/models/filter.py,sha256=6NS1rBI2zmrswK9NtMn0vZlbwbeMGCPuLCDKP9XWN0I,2259
|
|
23
|
-
homesec/models/source.py,sha256=F8ksGrOa09bxx5IEgQmxPWJNWOm8zvFLsRJxihJTlM4,2367
|
|
24
23
|
homesec/models/storage.py,sha256=63wyHdDt3QrfdsP0SmhrxtOeWRllZ1O2GPrA4jI7XmU,235
|
|
25
24
|
homesec/models/vlm.py,sha256=Uk6TPwqbKxzyAsOlBSzZru74nKjp2-LLyzIp5b3wM_c,3293
|
|
25
|
+
homesec/models/source/__init__.py,sha256=vZFpll5ftShIGe_VOoqQ6XiFZKKAKqgZ3bOW9feB_VM,60
|
|
26
|
+
homesec/models/source/ftp.py,sha256=ZRU0YxNa99qTlCS2pIXKl8kmM1KQyIHOHyaFox6XPKk,2871
|
|
27
|
+
homesec/models/source/local_folder.py,sha256=ymneTjcCzvLzFmen3GF8kYfGIlxWgb10BDvgdXnWFcQ,803
|
|
28
|
+
homesec/models/source/rtsp.py,sha256=h_XzIM22JKHA0Mz4IUZMTPlzKkU_U7sSF6p5PPIVoWw,4852
|
|
26
29
|
homesec/pipeline/__init__.py,sha256=kiQLECc6JIPmeIdBJrVpTApPs0GBAgWoZ1kU4XZyJVY,214
|
|
27
30
|
homesec/pipeline/alert_policy.py,sha256=gFl5SJ96fgEfEUnhSL51YA6O2GPGXTXmxaDC-q3h1rs,152
|
|
28
31
|
homesec/pipeline/core.py,sha256=D552e-xpIpom5C-Y_TkWB9Ufbm8xClGmCdo_4tseXuU,23840
|
|
@@ -41,19 +44,26 @@ homesec/plugins/notifiers/mqtt.py,sha256=1zUKUHFvT65ysawFXEwWHvY8rg310fRsSSKhIFe
|
|
|
41
44
|
homesec/plugins/notifiers/multiplex.py,sha256=LlnwozjkMDQwz7__v7mT4AohZbiWZK39CZunamRp7FM,3676
|
|
42
45
|
homesec/plugins/notifiers/sendgrid_email.py,sha256=gZSv3FRaN8qCMO6D-MX8b6XVz-gSgrFhkFV6j1ILdi4,8682
|
|
43
46
|
homesec/plugins/sources/__init__.py,sha256=weLYuCLrmWIUvRTYmfgqVcOFHonZgTngDKFSks4yg8s,1025
|
|
44
|
-
homesec/plugins/sources/ftp.py,sha256=
|
|
45
|
-
homesec/plugins/sources/local_folder.py,sha256=
|
|
46
|
-
homesec/plugins/sources/rtsp.py,sha256=
|
|
47
|
+
homesec/plugins/sources/ftp.py,sha256=c2HktqWlzQyRDMVMVEb6R9zIK-Q1AyJMHPUp92jHhyM,853
|
|
48
|
+
homesec/plugins/sources/local_folder.py,sha256=zHyMT-PKEJCaf4mw29TBjj-jK_oQmO3zPyOAt4n5HWI,1227
|
|
49
|
+
homesec/plugins/sources/rtsp.py,sha256=rqWtOOKgxGqC2lwdgkEdpH_xMatH2-3xn_Kt_biAA20,824
|
|
47
50
|
homesec/plugins/storage/__init__.py,sha256=oJPrjgpke7VK3MY8L-GGmDVB4eWx7IgXcmVQXMcMw7g,1235
|
|
48
51
|
homesec/plugins/storage/dropbox.py,sha256=AQkEFV4lXqS1pbVazNuskEaEpr5CvLkrYdUK8EE7eAM,9992
|
|
49
52
|
homesec/plugins/storage/local.py,sha256=CbuSaWEi9ft1zxOURcmOKzwyq0UXIyHqyHoQD6sxivI,3231
|
|
50
53
|
homesec/repository/__init__.py,sha256=6cye2uQIA2v6jeLk5D2S9y3rlkfzJH5GceqdOroF3hU,160
|
|
51
54
|
homesec/repository/clip_repository.py,sha256=nRcswsIX--Z9p7J33FsqNlDtMAmt438VUbfvZHX0FlY,17090
|
|
52
|
-
homesec/sources/__init__.py,sha256=
|
|
55
|
+
homesec/sources/__init__.py,sha256=zjjoGbpha4Aqx2Y3fca743gs6gEyYDtRjUbCCwJ0NG0,598
|
|
53
56
|
homesec/sources/base.py,sha256=dKTxJxcDwJtykWDN3WYzkW5mtkRqlOJxJLWcLy82_Zo,7582
|
|
54
|
-
homesec/sources/ftp.py,sha256=
|
|
55
|
-
homesec/sources/local_folder.py,sha256=
|
|
56
|
-
homesec/sources/rtsp.py,sha256=
|
|
57
|
+
homesec/sources/ftp.py,sha256=t1XFo7aN31mN4wFfSVJuJhA2o_nFi1zQQZCl4VZGQlA,7312
|
|
58
|
+
homesec/sources/local_folder.py,sha256=UduSyDKesT5DBoBx8Qjp7tAZatUR6bvXTEHzC7VoLWQ,8474
|
|
59
|
+
homesec/sources/rtsp/__init__.py,sha256=wtBzdwzL7Cg0HyIGIpS3lBagekCAi_EYJOyCZFbT7K0,103
|
|
60
|
+
homesec/sources/rtsp/clock.py,sha256=Gf-CLBfgUmxfajmZim89vmWRG14hnc7iUUBVNNiUz6w,338
|
|
61
|
+
homesec/sources/rtsp/core.py,sha256=tX6FH_ete2Xop-6O7CzH-a0A42OaCa2-jUQXjl5oQ0E,48235
|
|
62
|
+
homesec/sources/rtsp/frame_pipeline.py,sha256=cIGrH5z4WE8swoAThTl7Wu3SViNq-00YyCcKMLBrCso,11620
|
|
63
|
+
homesec/sources/rtsp/hardware.py,sha256=sKCJhoVdmkDfCk2s5RL0lDPX_CuvcwlgVUESgRJK3us,4892
|
|
64
|
+
homesec/sources/rtsp/motion.py,sha256=FKDa1hvD_H3uxgnQ-Z0c7MhVd6u_35cO6XKZRVrXpaY,2870
|
|
65
|
+
homesec/sources/rtsp/recorder.py,sha256=fqL-zr-vtnFrDQc87zZFYeM-6JSUfMwQsc0zfR-mgy4,7009
|
|
66
|
+
homesec/sources/rtsp/utils.py,sha256=aufPAP6oc39kyFAiN0HTDhdXf3bQqWSVeVWNIHx5MmI,1021
|
|
57
67
|
homesec/state/__init__.py,sha256=Evt1jqTebmpJD1NUzNh3vwt5pbjDlLjQ0DgMCSAZOuM,255
|
|
58
68
|
homesec/state/postgres.py,sha256=I-cXqW5cgz-hpaHc0JIv3DnIBTmGxE28P8ZxBAGabSw,17765
|
|
59
69
|
homesec/telemetry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -61,8 +71,8 @@ homesec/telemetry/db_log_handler.py,sha256=KM8g4kcOyPzFJbpGxpSzecx_hrEWY0YfpoIKy
|
|
|
61
71
|
homesec/telemetry/postgres_settings.py,sha256=EVD2_oi_KReFJvQmXxW026aurl_YD-KexT7rkbGQPHc,1198
|
|
62
72
|
homesec/telemetry/db/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
63
73
|
homesec/telemetry/db/log_table.py,sha256=wcZLwRht7FMa0z2gf37f_RxdVTNIdDiK4i_N3c_ibwg,473
|
|
64
|
-
homesec-1.2.
|
|
65
|
-
homesec-1.2.
|
|
66
|
-
homesec-1.2.
|
|
67
|
-
homesec-1.2.
|
|
68
|
-
homesec-1.2.
|
|
74
|
+
homesec-1.2.2.dist-info/METADATA,sha256=dYxpcVPEWLuX_TLZIEZZywjcgYgJO--J1IGgDu8aUyQ,25157
|
|
75
|
+
homesec-1.2.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
76
|
+
homesec-1.2.2.dist-info/entry_points.txt,sha256=8ocCj_fP1qxIuL-DVDAUiaUbEdTMX_kg_BzVrJsbQYg,45
|
|
77
|
+
homesec-1.2.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
78
|
+
homesec-1.2.2.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
|