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.
- homesec/__init__.py +1 -1
- homesec/app.py +38 -84
- homesec/cli.py +9 -10
- homesec/config/validation.py +38 -12
- homesec/interfaces.py +50 -2
- homesec/maintenance/cleanup_clips.py +4 -4
- homesec/models/__init__.py +6 -5
- homesec/models/alert.py +3 -2
- homesec/models/clip.py +4 -2
- homesec/models/config.py +62 -17
- homesec/models/enums.py +114 -0
- homesec/models/events.py +19 -18
- homesec/models/filter.py +13 -3
- homesec/models/source.py +3 -0
- homesec/models/vlm.py +18 -7
- homesec/plugins/__init__.py +7 -33
- homesec/plugins/alert_policies/__init__.py +34 -59
- homesec/plugins/alert_policies/default.py +20 -45
- homesec/plugins/alert_policies/noop.py +14 -29
- homesec/plugins/analyzers/__init__.py +20 -105
- homesec/plugins/analyzers/openai.py +70 -53
- homesec/plugins/filters/__init__.py +18 -102
- homesec/plugins/filters/yolo.py +103 -66
- homesec/plugins/notifiers/__init__.py +20 -56
- homesec/plugins/notifiers/mqtt.py +22 -30
- homesec/plugins/notifiers/sendgrid_email.py +34 -32
- homesec/plugins/registry.py +160 -0
- homesec/plugins/sources/__init__.py +45 -0
- homesec/plugins/sources/ftp.py +25 -0
- homesec/plugins/sources/local_folder.py +30 -0
- homesec/plugins/sources/rtsp.py +27 -0
- homesec/plugins/storage/__init__.py +18 -88
- homesec/plugins/storage/dropbox.py +36 -37
- homesec/plugins/storage/local.py +8 -29
- homesec/plugins/utils.py +8 -4
- homesec/repository/clip_repository.py +20 -14
- homesec/sources/base.py +24 -2
- homesec/sources/local_folder.py +57 -78
- homesec/state/postgres.py +46 -17
- {homesec-1.1.0.dist-info → homesec-1.1.2.dist-info}/METADATA +1 -1
- homesec-1.1.2.dist-info/RECORD +68 -0
- homesec-1.1.0.dist-info/RECORD +0 -62
- {homesec-1.1.0.dist-info → homesec-1.1.2.dist-info}/WHEEL +0 -0
- {homesec-1.1.0.dist-info → homesec-1.1.2.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
|
26
|
-
from homesec.plugins.analyzers import
|
|
27
|
-
from homesec.plugins.filters import
|
|
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
|
-
|
|
26
|
+
load_notifier_plugin,
|
|
33
27
|
)
|
|
34
|
-
from homesec.plugins.
|
|
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 =
|
|
152
|
-
vlm_plugin =
|
|
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
|
|
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
|
-
|
|
211
|
-
if
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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 =
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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=
|
|
331
|
-
valid_vlms=
|
|
332
|
-
valid_storage=
|
|
333
|
-
valid_notifiers=
|
|
334
|
-
valid_alert_policies=
|
|
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.
|
|
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(
|
|
77
|
-
sorted(
|
|
78
|
-
valid_storage=sorted(
|
|
79
|
-
valid_notifiers=sorted(
|
|
80
|
-
valid_alert_policies=sorted(
|
|
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
|
|
homesec/config/validation.py
CHANGED
|
@@ -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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
81
|
+
if notifier.backend.lower() not in valid_notifiers_lower:
|
|
67
82
|
errors.append(
|
|
68
|
-
f"Unknown notifier backend: {notifier.backend}
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
25
|
-
from homesec.plugins.storage import
|
|
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 =
|
|
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 =
|
|
181
|
+
filter_plugin = load_filter(recheck_cfg)
|
|
182
182
|
|
|
183
183
|
sem = asyncio.Semaphore(int(opts.workers))
|
|
184
184
|
|
homesec/models/__init__.py
CHANGED
|
@@ -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.
|
|
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:
|
|
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
|
|
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:
|
|
38
|
+
status: ClipStatus
|
|
37
39
|
|
|
38
40
|
# Pointers
|
|
39
41
|
local_path: str
|