homesec 1.1.0__py3-none-any.whl → 1.1.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.
Files changed (45) hide show
  1. homesec/__init__.py +1 -1
  2. homesec/app.py +38 -84
  3. homesec/cli.py +9 -10
  4. homesec/config/validation.py +38 -12
  5. homesec/interfaces.py +50 -2
  6. homesec/maintenance/cleanup_clips.py +4 -4
  7. homesec/models/__init__.py +6 -5
  8. homesec/models/alert.py +3 -2
  9. homesec/models/clip.py +4 -2
  10. homesec/models/config.py +62 -17
  11. homesec/models/enums.py +114 -0
  12. homesec/models/events.py +19 -18
  13. homesec/models/filter.py +13 -3
  14. homesec/models/source.py +3 -0
  15. homesec/models/vlm.py +18 -7
  16. homesec/plugins/__init__.py +7 -33
  17. homesec/plugins/alert_policies/__init__.py +34 -59
  18. homesec/plugins/alert_policies/default.py +20 -45
  19. homesec/plugins/alert_policies/noop.py +14 -29
  20. homesec/plugins/analyzers/__init__.py +20 -105
  21. homesec/plugins/analyzers/openai.py +70 -53
  22. homesec/plugins/filters/__init__.py +18 -102
  23. homesec/plugins/filters/yolo.py +103 -66
  24. homesec/plugins/notifiers/__init__.py +20 -56
  25. homesec/plugins/notifiers/mqtt.py +22 -30
  26. homesec/plugins/notifiers/sendgrid_email.py +34 -32
  27. homesec/plugins/registry.py +160 -0
  28. homesec/plugins/sources/__init__.py +45 -0
  29. homesec/plugins/sources/ftp.py +25 -0
  30. homesec/plugins/sources/local_folder.py +30 -0
  31. homesec/plugins/sources/rtsp.py +27 -0
  32. homesec/plugins/storage/__init__.py +18 -88
  33. homesec/plugins/storage/dropbox.py +36 -37
  34. homesec/plugins/storage/local.py +8 -29
  35. homesec/plugins/utils.py +8 -4
  36. homesec/repository/clip_repository.py +20 -14
  37. homesec/sources/base.py +24 -2
  38. homesec/sources/local_folder.py +57 -78
  39. homesec/state/postgres.py +46 -17
  40. {homesec-1.1.0.dist-info → homesec-1.1.2.dist-info}/METADATA +1 -1
  41. homesec-1.1.2.dist-info/RECORD +68 -0
  42. homesec-1.1.0.dist-info/RECORD +0 -62
  43. {homesec-1.1.0.dist-info → homesec-1.1.2.dist-info}/WHEEL +0 -0
  44. {homesec-1.1.0.dist-info → homesec-1.1.2.dist-info}/entry_points.txt +0 -0
  45. {homesec-1.1.0.dist-info → homesec-1.1.2.dist-info}/licenses/LICENSE +0 -0
homesec/__init__.py CHANGED
@@ -10,11 +10,11 @@ from homesec.models.filter import FilterResult
10
10
  from homesec.models.vlm import AnalysisResult
11
11
 
12
12
  __all__ = [
13
- "__version__",
14
13
  "Alert",
15
14
  "AnalysisResult",
16
15
  "Clip",
17
16
  "ClipStateData",
18
17
  "FilterResult",
19
18
  "PipelineError",
19
+ "__version__",
20
20
  ]
homesec/app.py CHANGED
@@ -5,11 +5,8 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import logging
7
7
  import signal
8
- from collections.abc import Callable
9
8
  from pathlib import Path
10
- from typing import TYPE_CHECKING, cast
11
-
12
- from pydantic import BaseModel
9
+ from typing import TYPE_CHECKING
13
10
 
14
11
  from homesec.config import (
15
12
  load_config,
@@ -19,21 +16,19 @@ from homesec.config import (
19
16
  )
20
17
  from homesec.health import HealthServer
21
18
  from homesec.interfaces import EventStore
22
- from homesec.models.config import NotifierConfig
23
- from homesec.models.source import FtpSourceConfig, LocalFolderSourceConfig, RTSPSourceConfig
24
19
  from homesec.pipeline import ClipPipeline
25
- from homesec.plugins.alert_policies import ALERT_POLICY_REGISTRY
26
- from homesec.plugins.analyzers import VLM_REGISTRY, load_vlm_plugin
27
- from homesec.plugins.filters import FILTER_REGISTRY, load_filter_plugin
20
+ from homesec.plugins.alert_policies import load_alert_policy
21
+ from homesec.plugins.analyzers import load_analyzer
22
+ from homesec.plugins.filters import load_filter
28
23
  from homesec.plugins.notifiers import (
29
- NOTIFIER_REGISTRY,
30
24
  MultiplexNotifier,
31
25
  NotifierEntry,
32
- NotifierPlugin,
26
+ load_notifier_plugin,
33
27
  )
34
- from homesec.plugins.storage import STORAGE_REGISTRY, create_storage
28
+ from homesec.plugins.registry import PluginType, get_plugin_names
29
+ from homesec.plugins.sources import load_source_plugin
30
+ from homesec.plugins.storage import load_storage_plugin
35
31
  from homesec.repository import ClipRepository
36
- from homesec.sources import FtpSource, LocalFolderSource, RTSPSource
37
32
  from homesec.state import NoopEventStore, NoopStateStore, PostgresStateStore
38
33
 
39
34
  if TYPE_CHECKING:
@@ -148,8 +143,8 @@ class Application:
148
143
  await self._log_notifier_health()
149
144
 
150
145
  # Create filter, VLM, and alert policy plugins
151
- filter_plugin = load_filter_plugin(config.filter)
152
- vlm_plugin = load_vlm_plugin(config.vlm)
146
+ filter_plugin = load_filter(config.filter)
147
+ vlm_plugin = load_analyzer(config.vlm)
153
148
  self._filter = filter_plugin
154
149
  self._vlm = vlm_plugin
155
150
  alert_policy = self._create_alert_policy(config)
@@ -191,7 +186,7 @@ class Application:
191
186
 
192
187
  def _create_storage(self, config: Config) -> StorageBackend:
193
188
  """Create storage backend based on config."""
194
- return create_storage(config.storage)
189
+ return load_storage_plugin(config.storage)
195
190
 
196
191
  async def _create_state_store(self, config: Config) -> StateStore:
197
192
  """Create state store based on config."""
@@ -207,23 +202,20 @@ class Application:
207
202
 
208
203
  def _create_event_store(self, state_store: StateStore) -> EventStore:
209
204
  """Create event store based on state store backend."""
210
- create_event_store = getattr(state_store, "create_event_store", None)
211
- if callable(create_event_store):
212
- event_store = cast(Callable[[], EventStore], create_event_store)()
213
- if isinstance(event_store, NoopEventStore):
214
- logger.warning("Event store unavailable; events will be dropped")
215
- return event_store
216
- logger.warning("Unsupported state store for events; events will be dropped")
217
- return NoopEventStore()
205
+ event_store = state_store.create_event_store()
206
+ if isinstance(event_store, NoopEventStore):
207
+ logger.warning(
208
+ "Event store unavailable (NoopEventStore returned); events will be dropped"
209
+ )
210
+ return event_store
218
211
 
219
212
  def _create_notifier(self, config: Config) -> Notifier:
220
- """Create notifier(s) based on config."""
213
+ """Create notifier(s) based on config using plugin registry."""
221
214
  entries: list[NotifierEntry] = []
222
215
  for index, notifier_cfg in enumerate(config.notifiers):
223
- plugin, validated_cfg = self._validate_notifier_config(notifier_cfg)
224
216
  if not notifier_cfg.enabled:
225
217
  continue
226
- notifier = plugin.factory(validated_cfg)
218
+ notifier = load_notifier_plugin(notifier_cfg.backend, notifier_cfg.config)
227
219
  name = f"{notifier_cfg.backend}[{index}]"
228
220
  entries.append(NotifierEntry(name=name, notifier=notifier))
229
221
 
@@ -256,65 +248,26 @@ class Application:
256
248
  exc_info=err,
257
249
  )
258
250
 
259
- def _validate_notifier_config(
260
- self, notifier_cfg: NotifierConfig
261
- ) -> tuple[NotifierPlugin, BaseModel]:
262
- """Validate notifier config and return the plugin and validated config."""
263
- plugin = NOTIFIER_REGISTRY.get(notifier_cfg.backend)
264
- if plugin is None:
265
- raise RuntimeError(f"Unsupported notifier backend: {notifier_cfg.backend}")
266
- validated_cfg = plugin.config_model.model_validate(notifier_cfg.config)
267
- return plugin, validated_cfg
268
-
269
251
  def _create_alert_policy(self, config: Config) -> AlertPolicy:
270
- policy_cfg = config.alert_policy
271
-
272
- # Use noop backend when alert policy is disabled
273
- backend = "noop" if not policy_cfg.enabled else policy_cfg.backend
274
-
275
- plugin = ALERT_POLICY_REGISTRY.get(backend)
276
- if plugin is None:
277
- raise RuntimeError(f"Unsupported alert policy backend: {backend}")
278
-
279
- # Always validate to ensure proper BaseModel contract
280
- if policy_cfg.enabled:
281
- settings = plugin.config_model.model_validate(policy_cfg.config)
282
- else:
283
- # Noop uses empty BaseModel, validate empty dict to get BaseModel instance
284
- settings = plugin.config_model.model_validate({})
285
-
286
- return plugin.factory(settings, config.per_camera_alert, config.vlm.trigger_classes)
252
+ """Create alert policy using the plugin registry."""
253
+ return load_alert_policy(
254
+ config.alert_policy,
255
+ per_camera_overrides=config.per_camera_alert,
256
+ trigger_classes=config.vlm.trigger_classes,
257
+ )
287
258
 
288
259
  def _create_sources(self, config: Config) -> list[ClipSource]:
289
- """Create clip sources based on config."""
260
+ """Create clip sources based on config using plugin registry."""
290
261
  sources: list[ClipSource] = []
291
262
 
292
263
  for camera in config.cameras:
293
264
  source_cfg = camera.source
294
- match source_cfg.type:
295
- case "local_folder":
296
- local_cfg = source_cfg.config
297
- assert isinstance(local_cfg, LocalFolderSourceConfig)
298
- sources.append(
299
- LocalFolderSource(
300
- local_cfg,
301
- camera_name=camera.name,
302
- state_store=self._state_store,
303
- )
304
- )
305
-
306
- case "rtsp":
307
- rtsp_cfg = source_cfg.config
308
- assert isinstance(rtsp_cfg, RTSPSourceConfig)
309
- sources.append(RTSPSource(rtsp_cfg, camera_name=camera.name))
310
-
311
- case "ftp":
312
- ftp_cfg = source_cfg.config
313
- assert isinstance(ftp_cfg, FtpSourceConfig)
314
- sources.append(FtpSource(ftp_cfg, camera_name=camera.name))
315
-
316
- case _:
317
- raise RuntimeError(f"Unsupported source type: {source_cfg.type}")
265
+ source = load_source_plugin(
266
+ source_type=source_cfg.type,
267
+ config=source_cfg.config,
268
+ camera_name=camera.name,
269
+ )
270
+ sources.append(source)
318
271
 
319
272
  return sources
320
273
 
@@ -327,11 +280,12 @@ class Application:
327
280
  validate_camera_references(config)
328
281
  validate_plugin_names(
329
282
  config,
330
- valid_filters=sorted(FILTER_REGISTRY.keys()),
331
- valid_vlms=sorted(VLM_REGISTRY.keys()),
332
- valid_storage=sorted(STORAGE_REGISTRY.keys()),
333
- valid_notifiers=sorted(NOTIFIER_REGISTRY.keys()),
334
- valid_alert_policies=sorted(ALERT_POLICY_REGISTRY.keys()),
283
+ valid_filters=get_plugin_names(PluginType.FILTER),
284
+ valid_vlms=get_plugin_names(PluginType.ANALYZER),
285
+ valid_storage=get_plugin_names(PluginType.STORAGE),
286
+ valid_notifiers=get_plugin_names(PluginType.NOTIFIER),
287
+ valid_alert_policies=get_plugin_names(PluginType.ALERT_POLICY),
288
+ valid_sources=get_plugin_names(PluginType.SOURCE),
335
289
  )
336
290
 
337
291
  def _setup_signal_handlers(self) -> None:
homesec/cli.py CHANGED
@@ -17,11 +17,7 @@ from homesec.config import ConfigError, load_config
17
17
  from homesec.config.validation import validate_camera_references, validate_plugin_names
18
18
  from homesec.logging_setup import configure_logging
19
19
  from homesec.maintenance.cleanup_clips import CleanupOptions, run_cleanup
20
- from homesec.plugins.alert_policies import ALERT_POLICY_REGISTRY
21
- from homesec.plugins.analyzers import VLM_REGISTRY
22
- from homesec.plugins.filters import FILTER_REGISTRY
23
- from homesec.plugins.notifiers import NOTIFIER_REGISTRY
24
- from homesec.plugins.storage import STORAGE_REGISTRY
20
+ from homesec.plugins.registry import PluginType, get_plugin_names
25
21
 
26
22
 
27
23
  def setup_logging(level: str = "INFO") -> None:
@@ -73,11 +69,11 @@ class HomeSec:
73
69
  validate_camera_references(cfg)
74
70
  validate_plugin_names(
75
71
  cfg,
76
- sorted(FILTER_REGISTRY.keys()),
77
- sorted(VLM_REGISTRY.keys()),
78
- valid_storage=sorted(STORAGE_REGISTRY.keys()),
79
- valid_notifiers=sorted(NOTIFIER_REGISTRY.keys()),
80
- valid_alert_policies=sorted(ALERT_POLICY_REGISTRY.keys()),
72
+ sorted(get_plugin_names(PluginType.FILTER)),
73
+ sorted(get_plugin_names(PluginType.ANALYZER)),
74
+ valid_storage=sorted(get_plugin_names(PluginType.STORAGE)),
75
+ valid_notifiers=sorted(get_plugin_names(PluginType.NOTIFIER)),
76
+ valid_alert_policies=sorted(get_plugin_names(PluginType.ALERT_POLICY)),
81
77
  )
82
78
 
83
79
  print(f"✓ Config valid: {config_path}")
@@ -155,6 +151,9 @@ class HomeSec:
155
151
 
156
152
  def main() -> None:
157
153
  """Main CLI entrypoint."""
154
+ # Strip --help/-h when it's the only arg so Fire shows its commands list
155
+ if len(sys.argv) == 2 and sys.argv[1] in ("--help", "-h"):
156
+ sys.argv.pop()
158
157
  fire.Fire(HomeSec)
159
158
 
160
159
 
@@ -37,6 +37,7 @@ def validate_plugin_names(
37
37
  valid_storage: list[str] | None = None,
38
38
  valid_notifiers: list[str] | None = None,
39
39
  valid_alert_policies: list[str] | None = None,
40
+ valid_sources: list[str] | None = None,
40
41
  ) -> None:
41
42
  """Validate that plugin names are recognized.
42
43
 
@@ -46,34 +47,59 @@ def validate_plugin_names(
46
47
  valid_vlms: List of valid VLM plugin names
47
48
  valid_storage: Optional list of valid storage backends
48
49
  valid_notifiers: Optional list of valid notifier backends
50
+ valid_alert_policies: Optional list of valid alert policy backends
51
+ valid_sources: Optional list of valid source types
49
52
 
50
53
  Raises:
51
54
  ConfigError: If plugin names are not recognized
52
55
  """
53
56
  errors = []
54
57
 
55
- if config.filter.plugin not in valid_filters:
56
- errors.append(f"Unknown filter plugin: {config.filter.plugin} (valid: {valid_filters})")
57
-
58
- if config.vlm.backend not in valid_vlms:
59
- errors.append(f"Unknown VLM plugin: {config.vlm.backend} (valid: {valid_vlms})")
60
-
61
- if valid_storage is not None and config.storage.backend not in valid_storage:
62
- errors.append(f"Unknown storage backend: {config.storage.backend} (valid: {valid_storage})")
58
+ valid_filters_lower = {name.lower() for name in valid_filters}
59
+ if config.filter.plugin.lower() not in valid_filters_lower:
60
+ errors.append(
61
+ f"Unknown filter plugin: {config.filter.plugin} (valid: {sorted(valid_filters_lower)})"
62
+ )
63
+
64
+ valid_vlms_lower = {name.lower() for name in valid_vlms}
65
+ if config.vlm.backend.lower() not in valid_vlms_lower:
66
+ errors.append(
67
+ f"Unknown VLM plugin: {config.vlm.backend} (valid: {sorted(valid_vlms_lower)})"
68
+ )
69
+
70
+ if valid_storage is not None:
71
+ valid_storage_lower = {name.lower() for name in valid_storage}
72
+ if config.storage.backend.lower() not in valid_storage_lower:
73
+ errors.append(
74
+ f"Unknown storage backend: {config.storage.backend} "
75
+ f"(valid: {sorted(valid_storage_lower)})"
76
+ )
63
77
 
64
78
  if valid_notifiers is not None:
79
+ valid_notifiers_lower = {name.lower() for name in valid_notifiers}
65
80
  for notifier in config.notifiers:
66
- if notifier.backend not in valid_notifiers:
81
+ if notifier.backend.lower() not in valid_notifiers_lower:
67
82
  errors.append(
68
- f"Unknown notifier backend: {notifier.backend} (valid: {valid_notifiers})"
83
+ f"Unknown notifier backend: {notifier.backend} "
84
+ f"(valid: {sorted(valid_notifiers_lower)})"
69
85
  )
70
86
 
71
87
  if valid_alert_policies is not None:
72
- if config.alert_policy.backend not in valid_alert_policies:
88
+ valid_alert_policies_lower = {name.lower() for name in valid_alert_policies}
89
+ if config.alert_policy.backend.lower() not in valid_alert_policies_lower:
73
90
  errors.append(
74
91
  "Unknown alert policy backend: "
75
- f"{config.alert_policy.backend} (valid: {valid_alert_policies})"
92
+ f"{config.alert_policy.backend} (valid: {sorted(valid_alert_policies_lower)})"
76
93
  )
77
94
 
95
+ if valid_sources is not None:
96
+ valid_sources_lower = {name.lower() for name in valid_sources}
97
+ for camera in config.cameras:
98
+ if camera.source.type.lower() not in valid_sources_lower:
99
+ errors.append(
100
+ f"Unknown source type for camera '{camera.name}': "
101
+ f"{camera.source.type} (valid: {sorted(valid_sources_lower)})"
102
+ )
103
+
78
104
  if errors:
79
105
  raise ConfigError("Invalid plugin configuration:\n " + "\n ".join(errors))
homesec/interfaces.py CHANGED
@@ -69,6 +69,19 @@ class ClipSource(Shutdownable, ABC):
69
69
  """
70
70
  raise NotImplementedError
71
71
 
72
+ @abstractmethod
73
+ async def ping(self) -> bool:
74
+ """Health check for the clip source.
75
+
76
+ Returns True if the source is operational:
77
+ - Connection/watcher is alive
78
+ - Can receive new clips
79
+
80
+ This is similar to is_healthy() but async and follows the standard
81
+ ping() pattern used by other interfaces.
82
+ """
83
+ raise NotImplementedError
84
+
72
85
 
73
86
  class StorageBackend(Shutdownable, ABC):
74
87
  """Stores raw clips and derived artifacts."""
@@ -137,8 +150,18 @@ class StateStore(Shutdownable, ABC):
137
150
  """Health check. Returns True if database is reachable."""
138
151
  raise NotImplementedError
139
152
 
153
+ @abstractmethod
154
+ def create_event_store(self) -> EventStore:
155
+ """Create an event store associated with this state store.
156
+
157
+ Returns NoopEventStore if not supported.
158
+ """
159
+ from homesec.state import NoopEventStore
160
+
161
+ return NoopEventStore()
140
162
 
141
- class EventStore(ABC):
163
+
164
+ class EventStore(Shutdownable, ABC):
142
165
  """Manages clip lifecycle events in Postgres."""
143
166
 
144
167
  @abstractmethod
@@ -162,6 +185,11 @@ class EventStore(ABC):
162
185
  """
163
186
  raise NotImplementedError
164
187
 
188
+ @abstractmethod
189
+ async def ping(self) -> bool:
190
+ """Health check. Returns True if event store is reachable."""
191
+ raise NotImplementedError
192
+
165
193
 
166
194
  class Notifier(Shutdownable, ABC):
167
195
  """Sends notifications (e.g., MQTT, email, SMS)."""
@@ -229,6 +257,17 @@ class ObjectFilter(Shutdownable, ABC):
229
257
  """
230
258
  raise NotImplementedError
231
259
 
260
+ @abstractmethod
261
+ async def ping(self) -> bool:
262
+ """Health check for the filter.
263
+
264
+ Returns True if the filter is operational:
265
+ - Model is loaded
266
+ - Executor pool is alive (if applicable)
267
+ - Ready to process detection requests
268
+ """
269
+ raise NotImplementedError
270
+
232
271
 
233
272
  class VLMAnalyzer(Shutdownable, ABC):
234
273
  """Plugin interface for VLM-based clip analysis."""
@@ -251,4 +290,13 @@ class VLMAnalyzer(Shutdownable, ABC):
251
290
  """
252
291
  raise NotImplementedError
253
292
 
254
- # shutdown() defined in Shutdownable
293
+ @abstractmethod
294
+ async def ping(self) -> bool:
295
+ """Health check for the analyzer.
296
+
297
+ Returns True if the analyzer is operational:
298
+ - API endpoint reachable (for API-based analyzers)
299
+ - Model loaded (for local analyzers)
300
+ - HTTP session alive
301
+ """
302
+ raise NotImplementedError
@@ -21,8 +21,8 @@ from homesec.interfaces import ObjectFilter, StorageBackend
21
21
  from homesec.models.clip import ClipStateData
22
22
  from homesec.models.filter import FilterConfig, YoloFilterSettings
23
23
  from homesec.plugins import discover_all_plugins
24
- from homesec.plugins.filters import load_filter_plugin
25
- from homesec.plugins.storage import create_storage
24
+ from homesec.plugins.filters import load_filter
25
+ from homesec.plugins.storage import load_storage_plugin
26
26
  from homesec.repository.clip_repository import ClipRepository
27
27
  from homesec.state.postgres import PostgresStateStore
28
28
 
@@ -168,7 +168,7 @@ async def run_cleanup(opts: CleanupOptions) -> None:
168
168
  if not dsn:
169
169
  raise RuntimeError("Postgres DSN is required for cleanup")
170
170
 
171
- storage = create_storage(cfg.storage)
171
+ storage = load_storage_plugin(cfg.storage)
172
172
  state_store = PostgresStateStore(dsn)
173
173
  ok = await state_store.initialize()
174
174
  if not ok:
@@ -178,7 +178,7 @@ async def run_cleanup(opts: CleanupOptions) -> None:
178
178
  repo = ClipRepository(state_store, event_store, retry=cfg.retry)
179
179
 
180
180
  recheck_cfg = _build_recheck_filter_config(cfg.filter, opts)
181
- filter_plugin = load_filter_plugin(recheck_cfg)
181
+ filter_plugin = load_filter(recheck_cfg)
182
182
 
183
183
  sem = asyncio.Semaphore(int(opts.workers))
184
184
 
@@ -23,13 +23,13 @@ from homesec.models.config import (
23
23
  StorageConfig,
24
24
  StoragePathsConfig,
25
25
  )
26
+ from homesec.models.enums import RiskLevel, RiskLevelField
26
27
  from homesec.models.filter import FilterConfig, FilterOverrides, FilterResult, YoloFilterSettings
27
28
  from homesec.models.source import FtpSourceConfig, LocalFolderSourceConfig, RTSPSourceConfig
28
29
  from homesec.models.vlm import (
29
30
  AnalysisResult,
30
31
  EntityTimeline,
31
32
  OpenAILLMConfig,
32
- RiskLevel,
33
33
  SequenceAnalysis,
34
34
  VLMConfig,
35
35
  VLMPreprocessConfig,
@@ -43,7 +43,6 @@ __all__ = [
43
43
  "AlertDecision",
44
44
  "AlertPolicyConfig",
45
45
  "AlertPolicyOverrides",
46
- "DefaultAlertPolicySettings",
47
46
  "AnalysisResult",
48
47
  "CameraConfig",
49
48
  "CameraSourceConfig",
@@ -51,24 +50,26 @@ __all__ = [
51
50
  "ClipStateData",
52
51
  "ConcurrencyConfig",
53
52
  "Config",
53
+ "DefaultAlertPolicySettings",
54
54
  "DropboxStorageConfig",
55
55
  "EntityTimeline",
56
56
  "FilterConfig",
57
57
  "FilterOverrides",
58
- "FtpSourceConfig",
59
58
  "FilterResult",
59
+ "FtpSourceConfig",
60
60
  "HealthConfig",
61
- "LocalStorageConfig",
62
61
  "LocalFolderSourceConfig",
62
+ "LocalStorageConfig",
63
63
  "MQTTAuthConfig",
64
64
  "MQTTConfig",
65
65
  "NotifierConfig",
66
66
  "OpenAILLMConfig",
67
67
  "RTSPSourceConfig",
68
- "SendGridEmailConfig",
69
68
  "RetentionConfig",
70
69
  "RetryConfig",
71
70
  "RiskLevel",
71
+ "RiskLevelField",
72
+ "SendGridEmailConfig",
72
73
  "SequenceAnalysis",
73
74
  "StateStoreConfig",
74
75
  "StorageConfig",
homesec/models/alert.py CHANGED
@@ -6,7 +6,8 @@ from datetime import datetime
6
6
 
7
7
  from pydantic import BaseModel
8
8
 
9
- from homesec.models.vlm import RiskLevel, SequenceAnalysis
9
+ from homesec.models.enums import RiskLevelField
10
+ from homesec.models.vlm import SequenceAnalysis
10
11
 
11
12
 
12
13
  class AlertDecision(BaseModel):
@@ -23,7 +24,7 @@ class Alert(BaseModel):
23
24
  camera_name: str
24
25
  storage_uri: str | None
25
26
  view_url: str | None
26
- risk_level: RiskLevel | None # None if VLM skipped
27
+ risk_level: RiskLevelField | None # None if VLM skipped
27
28
  activity_type: str | None
28
29
  notify_reason: str
29
30
  summary: str | None
homesec/models/clip.py CHANGED
@@ -4,10 +4,12 @@ from __future__ import annotations
4
4
 
5
5
  from datetime import datetime
6
6
  from pathlib import Path
7
- from typing import TYPE_CHECKING, Literal
7
+ from typing import TYPE_CHECKING
8
8
 
9
9
  from pydantic import BaseModel
10
10
 
11
+ from homesec.models.enums import ClipStatus
12
+
11
13
  if TYPE_CHECKING:
12
14
  from homesec.models.alert import AlertDecision
13
15
  from homesec.models.filter import FilterResult
@@ -33,7 +35,7 @@ class ClipStateData(BaseModel):
33
35
  camera_name: str
34
36
 
35
37
  # High-level status for queries
36
- status: Literal["queued_local", "uploaded", "analyzed", "done", "error", "deleted"]
38
+ status: ClipStatus
37
39
 
38
40
  # Pointers
39
41
  local_path: str