homesec 1.2.2__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 (50) 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 -25
  9. homesec/models/clip.py +1 -1
  10. homesec/models/config.py +10 -261
  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 +1 -1
  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 -6
  36. homesec/sources/ftp.py +95 -2
  37. homesec/sources/local_folder.py +27 -2
  38. homesec/sources/rtsp/core.py +162 -2
  39. {homesec-1.2.2.dist-info → homesec-1.2.3.dist-info}/METADATA +7 -12
  40. homesec-1.2.3.dist-info/RECORD +73 -0
  41. homesec/models/source/__init__.py +0 -3
  42. homesec/models/source/ftp.py +0 -97
  43. homesec/models/source/local_folder.py +0 -30
  44. homesec/models/source/rtsp.py +0 -165
  45. homesec/pipeline/alert_policy.py +0 -5
  46. homesec-1.2.2.dist-info/RECORD +0 -78
  47. /homesec/{plugins/notifiers → notifiers}/multiplex.py +0 -0
  48. {homesec-1.2.2.dist-info → homesec-1.2.3.dist-info}/WHEEL +0 -0
  49. {homesec-1.2.2.dist-info → homesec-1.2.3.dist-info}/entry_points.txt +0 -0
  50. {homesec-1.2.2.dist-info → homesec-1.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -17,9 +17,9 @@ from pathlib import Path
17
17
 
18
18
  import numpy as np
19
19
  import numpy.typing as npt
20
+ from pydantic import BaseModel, Field, model_validator
20
21
 
21
22
  from homesec.models.clip import Clip
22
- from homesec.models.source.rtsp import RTSPSourceConfig
23
23
  from homesec.sources.base import ThreadedClipSource
24
24
  from homesec.sources.rtsp.clock import Clock, SystemClock
25
25
  from homesec.sources.rtsp.frame_pipeline import FfmpegFramePipeline, FramePipeline
@@ -36,6 +36,166 @@ from homesec.sources.rtsp.utils import (
36
36
  logger = logging.getLogger(__name__)
37
37
 
38
38
 
39
+ class RTSPMotionConfig(BaseModel):
40
+ """Motion detection configuration."""
41
+
42
+ model_config = {"extra": "forbid"}
43
+
44
+ pixel_threshold: int = Field(
45
+ default=45,
46
+ ge=0,
47
+ description="Pixel intensity delta required to count a pixel as changed.",
48
+ )
49
+ min_changed_pct: float = Field(
50
+ default=1.0,
51
+ ge=0.0,
52
+ description="Percent of pixels that must change to trigger motion (idle state).",
53
+ )
54
+ recording_sensitivity_factor: float = Field(
55
+ default=2.0,
56
+ ge=1.0,
57
+ description="Factor to reduce the threshold while recording (>=1.0).",
58
+ )
59
+ blur_kernel: int = Field(
60
+ default=5,
61
+ ge=0,
62
+ description="Gaussian blur kernel size (odd or zero; even values are normalized).",
63
+ )
64
+
65
+
66
+ class RTSPRecordingConfig(BaseModel):
67
+ """Recording lifecycle configuration."""
68
+
69
+ model_config = {"extra": "forbid"}
70
+
71
+ stop_delay: float = Field(
72
+ default=10.0,
73
+ ge=0.0,
74
+ description="Seconds to keep recording after motion stops.",
75
+ )
76
+ max_recording_s: float = Field(
77
+ default=60.0,
78
+ gt=0.0,
79
+ description="Maximum seconds per recording before rotating.",
80
+ )
81
+
82
+
83
+ class RTSPStreamConfig(BaseModel):
84
+ """RTSP/ffmpeg transport configuration."""
85
+
86
+ model_config = {"extra": "forbid"}
87
+
88
+ connect_timeout_s: float = Field(
89
+ default=2.0,
90
+ ge=0.0,
91
+ description="RTSP connect timeout (seconds) passed to ffmpeg/ffprobe when supported.",
92
+ )
93
+ io_timeout_s: float = Field(
94
+ default=2.0,
95
+ ge=0.0,
96
+ description="RTSP I/O timeout (seconds) passed to ffmpeg/ffprobe when supported.",
97
+ )
98
+ ffmpeg_flags: list[str] = Field(
99
+ default_factory=list,
100
+ description="Additional ffmpeg flags appended to the command.",
101
+ )
102
+ disable_hwaccel: bool = Field(
103
+ default=False,
104
+ description="Disable hardware-accelerated decoding.",
105
+ )
106
+
107
+
108
+ class RTSPReconnectConfig(BaseModel):
109
+ """Reconnect and fallback policy."""
110
+
111
+ model_config = {"extra": "forbid"}
112
+
113
+ max_attempts: int = Field(
114
+ default=0,
115
+ ge=0,
116
+ description="Max reconnect attempts (0 = retry forever).",
117
+ )
118
+ backoff_s: float = Field(
119
+ default=1.0,
120
+ ge=0.0,
121
+ description="Base backoff (seconds) between reconnect attempts.",
122
+ )
123
+ detect_fallback_attempts: int = Field(
124
+ default=3,
125
+ ge=0,
126
+ description="Failures before falling back from detect stream to main stream.",
127
+ )
128
+
129
+
130
+ class RTSPRuntimeConfig(BaseModel):
131
+ """Runtime loop configuration."""
132
+
133
+ model_config = {"extra": "forbid"}
134
+
135
+ frame_timeout_s: float = Field(
136
+ default=2.0,
137
+ ge=0.0,
138
+ description="Seconds without frames before considering the pipeline stalled.",
139
+ )
140
+ frame_queue_size: int = Field(
141
+ default=20,
142
+ ge=1,
143
+ description="Frame queue size used by the frame reader thread.",
144
+ )
145
+ heartbeat_s: float = Field(
146
+ default=30.0,
147
+ ge=0.0,
148
+ description="Seconds between heartbeat logs.",
149
+ )
150
+ debug_motion: bool = Field(
151
+ default=False,
152
+ description="Enable verbose motion detection logging.",
153
+ )
154
+
155
+
156
+ class RTSPSourceConfig(BaseModel):
157
+ """RTSP source configuration."""
158
+
159
+ model_config = {"extra": "forbid"}
160
+
161
+ camera_name: str | None = Field(
162
+ default=None,
163
+ description="Optional human-friendly camera name.",
164
+ )
165
+ rtsp_url_env: str | None = Field(
166
+ default=None,
167
+ description="Environment variable containing the RTSP URL.",
168
+ )
169
+ rtsp_url: str | None = Field(
170
+ default=None,
171
+ description="RTSP URL for the main stream.",
172
+ )
173
+ detect_rtsp_url_env: str | None = Field(
174
+ default=None,
175
+ description="Environment variable containing the detect stream RTSP URL.",
176
+ )
177
+ detect_rtsp_url: str | None = Field(
178
+ default=None,
179
+ description="RTSP URL for the detect stream.",
180
+ )
181
+ output_dir: str = Field(
182
+ default="./recordings",
183
+ description="Directory to store recordings and logs.",
184
+ )
185
+
186
+ motion: RTSPMotionConfig = Field(default_factory=RTSPMotionConfig)
187
+ recording: RTSPRecordingConfig = Field(default_factory=RTSPRecordingConfig)
188
+ stream: RTSPStreamConfig = Field(default_factory=RTSPStreamConfig)
189
+ reconnect: RTSPReconnectConfig = Field(default_factory=RTSPReconnectConfig)
190
+ runtime: RTSPRuntimeConfig = Field(default_factory=RTSPRuntimeConfig)
191
+
192
+ @model_validator(mode="after")
193
+ def _require_rtsp_url(self) -> RTSPSourceConfig:
194
+ if not (self.rtsp_url or self.rtsp_url_env):
195
+ raise ValueError("rtsp_url_env or rtsp_url required for RTSP source")
196
+ return self
197
+
198
+
39
199
  class RTSPRunState(str, Enum):
40
200
  IDLE = "idle"
41
201
  RECORDING = "recording"
@@ -970,7 +1130,7 @@ class RTSPSource(ThreadedClipSource):
970
1130
  start_ts=start_ts,
971
1131
  end_ts=end_ts,
972
1132
  duration_s=duration_s,
973
- source_type="rtsp",
1133
+ source_backend="rtsp",
974
1134
  )
975
1135
 
976
1136
  self._emit_clip(clip)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: homesec
3
- Version: 1.2.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
- ### 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,7 +395,7 @@ 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"
@@ -411,7 +406,7 @@ cameras:
411
406
  backoff_s: 5
412
407
 
413
408
  filter:
414
- plugin: yolo
409
+ backend: yolo
415
410
  config:
416
411
  classes: ["person", "car"]
417
412
  min_confidence: 0.6
@@ -428,7 +423,7 @@ Uploads to Cloud but keeps analysis local.
428
423
  ```yaml
429
424
  storage:
430
425
  backend: dropbox
431
- dropbox:
426
+ config:
432
427
  token_env: DROPBOX_TOKEN
433
428
  root: "/SecurityCam"
434
429
 
@@ -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,,
@@ -1,3 +0,0 @@
1
- """Source configuration models."""
2
-
3
- __all__: list[str] = []
@@ -1,97 +0,0 @@
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
@@ -1,30 +0,0 @@
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
- )
@@ -1,165 +0,0 @@
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
@@ -1,5 +0,0 @@
1
- """Alert policy implementation (re-export)."""
2
-
3
- from homesec.plugins.alert_policies.default import DefaultAlertPolicy
4
-
5
- __all__ = ["DefaultAlertPolicy"]