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.
@@ -25,7 +25,9 @@ from homesec.models.config import (
25
25
  )
26
26
  from homesec.models.enums import RiskLevel, RiskLevelField
27
27
  from homesec.models.filter import FilterConfig, FilterOverrides, FilterResult, YoloFilterSettings
28
- from homesec.models.source import FtpSourceConfig, LocalFolderSourceConfig, RTSPSourceConfig
28
+ from homesec.models.source.ftp import FtpSourceConfig
29
+ from homesec.models.source.local_folder import LocalFolderSourceConfig
30
+ from homesec.models.source.rtsp import RTSPSourceConfig
29
31
  from homesec.models.vlm import (
30
32
  AnalysisResult,
31
33
  EntityTimeline,
homesec/models/config.py CHANGED
@@ -8,7 +8,9 @@ from pydantic import BaseModel, Field, field_validator, model_validator
8
8
 
9
9
  from homesec.models.enums import RiskLevel, RiskLevelField
10
10
  from homesec.models.filter import FilterConfig
11
- from homesec.models.source import FtpSourceConfig, LocalFolderSourceConfig, RTSPSourceConfig
11
+ from homesec.models.source.ftp import FtpSourceConfig
12
+ from homesec.models.source.local_folder import LocalFolderSourceConfig
13
+ from homesec.models.source.rtsp import RTSPSourceConfig
12
14
  from homesec.models.vlm import VLMConfig
13
15
 
14
16
 
@@ -0,0 +1,3 @@
1
+ """Source configuration models."""
2
+
3
+ __all__: list[str] = []
@@ -0,0 +1,97 @@
1
+ """FTP source configuration model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, Field, field_validator
6
+
7
+
8
+ class FtpSourceConfig(BaseModel):
9
+ """FTP source configuration."""
10
+
11
+ model_config = {"extra": "forbid"}
12
+
13
+ camera_name: str | None = Field(
14
+ default=None,
15
+ description="Optional human-friendly camera name.",
16
+ )
17
+ host: str = Field(
18
+ default="0.0.0.0",
19
+ description="FTP bind address.",
20
+ )
21
+ port: int = Field(
22
+ default=2121,
23
+ ge=0,
24
+ le=65535,
25
+ description="FTP listen port (0 lets the OS choose an ephemeral port).",
26
+ )
27
+ root_dir: str = Field(
28
+ default="./ftp_incoming",
29
+ description="FTP root directory for uploads.",
30
+ )
31
+ ftp_subdir: str | None = Field(
32
+ default=None,
33
+ description="Optional subdirectory under root_dir.",
34
+ )
35
+ anonymous: bool = Field(
36
+ default=True,
37
+ description="Allow anonymous FTP uploads.",
38
+ )
39
+ username_env: str | None = Field(
40
+ default=None,
41
+ description="Environment variable containing FTP username.",
42
+ )
43
+ password_env: str | None = Field(
44
+ default=None,
45
+ description="Environment variable containing FTP password.",
46
+ )
47
+ perms: str = Field(
48
+ default="elw",
49
+ description="pyftpdlib permissions string.",
50
+ )
51
+ passive_ports: str | None = Field(
52
+ default=None,
53
+ description="Passive ports range (e.g., '60000-60100' or '60000,60010').",
54
+ )
55
+ masquerade_address: str | None = Field(
56
+ default=None,
57
+ description="Optional masquerade address for passive mode.",
58
+ )
59
+ heartbeat_s: float = Field(
60
+ default=30.0,
61
+ ge=0.0,
62
+ description="Seconds between FTP health checks.",
63
+ )
64
+ allowed_extensions: list[str] = Field(
65
+ default_factory=lambda: [".mp4"],
66
+ description="Allowed file extensions for uploaded clips.",
67
+ )
68
+ delete_non_matching: bool = Field(
69
+ default=True,
70
+ description="Delete files with disallowed extensions.",
71
+ )
72
+ delete_incomplete: bool = Field(
73
+ default=True,
74
+ description="Delete incomplete uploads when enabled.",
75
+ )
76
+ default_duration_s: float = Field(
77
+ default=10.0,
78
+ ge=0.0,
79
+ description="Fallback clip duration when timestamps are missing.",
80
+ )
81
+ log_level: str = Field(
82
+ default="INFO",
83
+ description="FTP server log level.",
84
+ )
85
+
86
+ @field_validator("allowed_extensions")
87
+ @classmethod
88
+ def _normalize_extensions(cls, value: list[str]) -> list[str]:
89
+ cleaned: list[str] = []
90
+ for item in value:
91
+ ext = str(item).strip().lower()
92
+ if not ext:
93
+ continue
94
+ if not ext.startswith("."):
95
+ ext = f".{ext}"
96
+ cleaned.append(ext)
97
+ return cleaned
@@ -0,0 +1,30 @@
1
+ """Local folder source configuration model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class LocalFolderSourceConfig(BaseModel):
9
+ """Local folder source configuration."""
10
+
11
+ model_config = {"extra": "forbid"}
12
+
13
+ camera_name: str | None = Field(
14
+ default=None,
15
+ description="Optional human-friendly camera name.",
16
+ )
17
+ watch_dir: str = Field(
18
+ default="recordings",
19
+ description="Directory to watch for new clips.",
20
+ )
21
+ poll_interval: float = Field(
22
+ default=1.0,
23
+ ge=0.0,
24
+ description="Polling interval in seconds.",
25
+ )
26
+ stability_threshold_s: float = Field(
27
+ default=3.0,
28
+ ge=0.0,
29
+ description="Seconds to wait for file size to stabilize before accepting a clip.",
30
+ )
@@ -0,0 +1,165 @@
1
+ """RTSP source configuration models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, Field, model_validator
6
+
7
+
8
+ class RTSPMotionConfig(BaseModel):
9
+ """Motion detection configuration."""
10
+
11
+ model_config = {"extra": "forbid"}
12
+
13
+ pixel_threshold: int = Field(
14
+ default=45,
15
+ ge=0,
16
+ description="Pixel intensity delta required to count a pixel as changed.",
17
+ )
18
+ min_changed_pct: float = Field(
19
+ default=1.0,
20
+ ge=0.0,
21
+ description="Percent of pixels that must change to trigger motion (idle state).",
22
+ )
23
+ recording_sensitivity_factor: float = Field(
24
+ default=2.0,
25
+ ge=1.0,
26
+ description="Factor to reduce the threshold while recording (>=1.0).",
27
+ )
28
+ blur_kernel: int = Field(
29
+ default=5,
30
+ ge=0,
31
+ description="Gaussian blur kernel size (odd or zero; even values are normalized).",
32
+ )
33
+
34
+
35
+ class RTSPRecordingConfig(BaseModel):
36
+ """Recording lifecycle configuration."""
37
+
38
+ model_config = {"extra": "forbid"}
39
+
40
+ stop_delay: float = Field(
41
+ default=10.0,
42
+ ge=0.0,
43
+ description="Seconds to keep recording after motion stops.",
44
+ )
45
+ max_recording_s: float = Field(
46
+ default=60.0,
47
+ gt=0.0,
48
+ description="Maximum seconds per recording before rotating.",
49
+ )
50
+
51
+
52
+ class RTSPStreamConfig(BaseModel):
53
+ """RTSP/ffmpeg transport configuration."""
54
+
55
+ model_config = {"extra": "forbid"}
56
+
57
+ connect_timeout_s: float = Field(
58
+ default=2.0,
59
+ ge=0.0,
60
+ description="RTSP connect timeout (seconds) passed to ffmpeg/ffprobe when supported.",
61
+ )
62
+ io_timeout_s: float = Field(
63
+ default=2.0,
64
+ ge=0.0,
65
+ description="RTSP I/O timeout (seconds) passed to ffmpeg/ffprobe when supported.",
66
+ )
67
+ ffmpeg_flags: list[str] = Field(
68
+ default_factory=list,
69
+ description="Additional ffmpeg flags appended to the command.",
70
+ )
71
+ disable_hwaccel: bool = Field(
72
+ default=False,
73
+ description="Disable hardware-accelerated decoding.",
74
+ )
75
+
76
+
77
+ class RTSPReconnectConfig(BaseModel):
78
+ """Reconnect and fallback policy."""
79
+
80
+ model_config = {"extra": "forbid"}
81
+
82
+ max_attempts: int = Field(
83
+ default=0,
84
+ ge=0,
85
+ description="Max reconnect attempts (0 = retry forever).",
86
+ )
87
+ backoff_s: float = Field(
88
+ default=1.0,
89
+ ge=0.0,
90
+ description="Base backoff (seconds) between reconnect attempts.",
91
+ )
92
+ detect_fallback_attempts: int = Field(
93
+ default=3,
94
+ ge=0,
95
+ description="Failures before falling back from detect stream to main stream.",
96
+ )
97
+
98
+
99
+ class RTSPRuntimeConfig(BaseModel):
100
+ """Runtime loop configuration."""
101
+
102
+ model_config = {"extra": "forbid"}
103
+
104
+ frame_timeout_s: float = Field(
105
+ default=2.0,
106
+ ge=0.0,
107
+ description="Seconds without frames before considering the pipeline stalled.",
108
+ )
109
+ frame_queue_size: int = Field(
110
+ default=20,
111
+ ge=1,
112
+ description="Frame queue size used by the frame reader thread.",
113
+ )
114
+ heartbeat_s: float = Field(
115
+ default=30.0,
116
+ ge=0.0,
117
+ description="Seconds between heartbeat logs.",
118
+ )
119
+ debug_motion: bool = Field(
120
+ default=False,
121
+ description="Enable verbose motion detection logging.",
122
+ )
123
+
124
+
125
+ class RTSPSourceConfig(BaseModel):
126
+ """RTSP source configuration."""
127
+
128
+ model_config = {"extra": "forbid"}
129
+
130
+ camera_name: str | None = Field(
131
+ default=None,
132
+ description="Optional human-friendly camera name.",
133
+ )
134
+ rtsp_url_env: str | None = Field(
135
+ default=None,
136
+ description="Environment variable containing the RTSP URL.",
137
+ )
138
+ rtsp_url: str | None = Field(
139
+ default=None,
140
+ description="RTSP URL for the main stream.",
141
+ )
142
+ detect_rtsp_url_env: str | None = Field(
143
+ default=None,
144
+ description="Environment variable containing the detect stream RTSP URL.",
145
+ )
146
+ detect_rtsp_url: str | None = Field(
147
+ default=None,
148
+ description="RTSP URL for the detect stream.",
149
+ )
150
+ output_dir: str = Field(
151
+ default="./recordings",
152
+ description="Directory to store recordings and logs.",
153
+ )
154
+
155
+ motion: RTSPMotionConfig = Field(default_factory=RTSPMotionConfig)
156
+ recording: RTSPRecordingConfig = Field(default_factory=RTSPRecordingConfig)
157
+ stream: RTSPStreamConfig = Field(default_factory=RTSPStreamConfig)
158
+ reconnect: RTSPReconnectConfig = Field(default_factory=RTSPReconnectConfig)
159
+ runtime: RTSPRuntimeConfig = Field(default_factory=RTSPRuntimeConfig)
160
+
161
+ @model_validator(mode="after")
162
+ def _require_rtsp_url(self) -> RTSPSourceConfig:
163
+ if not (self.rtsp_url or self.rtsp_url_env):
164
+ raise ValueError("rtsp_url_env or rtsp_url required for RTSP source")
165
+ return self
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from homesec.interfaces import ClipSource
6
- from homesec.models.source import FtpSourceConfig
6
+ from homesec.models.source.ftp import FtpSourceConfig
7
7
  from homesec.plugins.registry import PluginType, plugin
8
8
 
9
9
  # Import the actual implementation from sources module
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from homesec.interfaces import ClipSource
6
- from homesec.models.source import LocalFolderSourceConfig
6
+ from homesec.models.source.local_folder import LocalFolderSourceConfig
7
7
  from homesec.plugins.registry import PluginType, plugin
8
8
  from homesec.sources.local_folder import LocalFolderSource as LocalFolderSourceImpl
9
9
 
@@ -5,9 +5,9 @@ from __future__ import annotations
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  from homesec.interfaces import ClipSource
8
- from homesec.models.source import RTSPSourceConfig
8
+ from homesec.models.source.rtsp import RTSPSourceConfig
9
9
  from homesec.plugins.registry import PluginType, plugin
10
- from homesec.sources.rtsp import RTSPSource as RTSPSourceImpl
10
+ from homesec.sources.rtsp.core import RTSPSource as RTSPSourceImpl
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  pass
@@ -1,10 +1,12 @@
1
1
  """Clip source implementations."""
2
2
 
3
- from homesec.models.source import FtpSourceConfig, LocalFolderSourceConfig, RTSPSourceConfig
3
+ from homesec.models.source.ftp import FtpSourceConfig
4
+ from homesec.models.source.local_folder import LocalFolderSourceConfig
5
+ from homesec.models.source.rtsp import RTSPSourceConfig
4
6
  from homesec.sources.base import ThreadedClipSource
5
7
  from homesec.sources.ftp import FtpSource
6
8
  from homesec.sources.local_folder import LocalFolderSource
7
- from homesec.sources.rtsp import RTSPSource
9
+ from homesec.sources.rtsp.core import RTSPSource
8
10
 
9
11
  __all__ = [
10
12
  "FtpSource",
homesec/sources/ftp.py CHANGED
@@ -10,7 +10,7 @@ from threading import Thread
10
10
  from typing import Any
11
11
 
12
12
  from homesec.models.clip import Clip
13
- from homesec.models.source import FtpSourceConfig
13
+ from homesec.models.source.ftp import FtpSourceConfig
14
14
  from homesec.sources.base import ThreadedClipSource
15
15
 
16
16
  logger = logging.getLogger(__name__)
@@ -14,7 +14,7 @@ from pathlib import Path
14
14
  from anyio import Path as AsyncPath
15
15
 
16
16
  from homesec.models.clip import Clip
17
- from homesec.models.source import LocalFolderSourceConfig
17
+ from homesec.models.source.local_folder import LocalFolderSourceConfig
18
18
  from homesec.sources.base import AsyncClipSource
19
19
 
20
20
  logger = logging.getLogger(__name__)
@@ -0,0 +1,5 @@
1
+ """RTSP source package."""
2
+
3
+ from homesec.sources.rtsp.core import RTSPSource
4
+
5
+ __all__ = ["RTSPSource"]
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Protocol
5
+
6
+
7
+ class Clock(Protocol):
8
+ def now(self) -> float: ...
9
+
10
+ def sleep(self, seconds: float) -> None: ...
11
+
12
+
13
+ class SystemClock:
14
+ def now(self) -> float:
15
+ return time.monotonic()
16
+
17
+ def sleep(self, seconds: float) -> None:
18
+ time.sleep(seconds)