homesec 0.1.0__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 (62) hide show
  1. homesec/__init__.py +20 -0
  2. homesec/app.py +393 -0
  3. homesec/cli.py +159 -0
  4. homesec/config/__init__.py +18 -0
  5. homesec/config/loader.py +109 -0
  6. homesec/config/validation.py +82 -0
  7. homesec/errors.py +71 -0
  8. homesec/health/__init__.py +5 -0
  9. homesec/health/server.py +226 -0
  10. homesec/interfaces.py +249 -0
  11. homesec/logging_setup.py +176 -0
  12. homesec/maintenance/__init__.py +1 -0
  13. homesec/maintenance/cleanup_clips.py +632 -0
  14. homesec/models/__init__.py +79 -0
  15. homesec/models/alert.py +32 -0
  16. homesec/models/clip.py +71 -0
  17. homesec/models/config.py +362 -0
  18. homesec/models/events.py +184 -0
  19. homesec/models/filter.py +62 -0
  20. homesec/models/source.py +77 -0
  21. homesec/models/storage.py +12 -0
  22. homesec/models/vlm.py +99 -0
  23. homesec/pipeline/__init__.py +6 -0
  24. homesec/pipeline/alert_policy.py +5 -0
  25. homesec/pipeline/core.py +639 -0
  26. homesec/plugins/__init__.py +62 -0
  27. homesec/plugins/alert_policies/__init__.py +80 -0
  28. homesec/plugins/alert_policies/default.py +111 -0
  29. homesec/plugins/alert_policies/noop.py +60 -0
  30. homesec/plugins/analyzers/__init__.py +126 -0
  31. homesec/plugins/analyzers/openai.py +446 -0
  32. homesec/plugins/filters/__init__.py +124 -0
  33. homesec/plugins/filters/yolo.py +317 -0
  34. homesec/plugins/notifiers/__init__.py +80 -0
  35. homesec/plugins/notifiers/mqtt.py +189 -0
  36. homesec/plugins/notifiers/multiplex.py +106 -0
  37. homesec/plugins/notifiers/sendgrid_email.py +228 -0
  38. homesec/plugins/storage/__init__.py +116 -0
  39. homesec/plugins/storage/dropbox.py +272 -0
  40. homesec/plugins/storage/local.py +108 -0
  41. homesec/plugins/utils.py +63 -0
  42. homesec/py.typed +0 -0
  43. homesec/repository/__init__.py +5 -0
  44. homesec/repository/clip_repository.py +552 -0
  45. homesec/sources/__init__.py +17 -0
  46. homesec/sources/base.py +224 -0
  47. homesec/sources/ftp.py +209 -0
  48. homesec/sources/local_folder.py +238 -0
  49. homesec/sources/rtsp.py +1251 -0
  50. homesec/state/__init__.py +10 -0
  51. homesec/state/postgres.py +501 -0
  52. homesec/storage_paths.py +46 -0
  53. homesec/telemetry/__init__.py +0 -0
  54. homesec/telemetry/db/__init__.py +1 -0
  55. homesec/telemetry/db/log_table.py +16 -0
  56. homesec/telemetry/db_log_handler.py +246 -0
  57. homesec/telemetry/postgres_settings.py +42 -0
  58. homesec-0.1.0.dist-info/METADATA +446 -0
  59. homesec-0.1.0.dist-info/RECORD +62 -0
  60. homesec-0.1.0.dist-info/WHEEL +4 -0
  61. homesec-0.1.0.dist-info/entry_points.txt +2 -0
  62. homesec-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,80 @@
1
+ """Alert policy plugins and registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass
7
+ from typing import Callable, TypeVar
8
+
9
+ from pydantic import BaseModel
10
+
11
+ from homesec.interfaces import AlertPolicy
12
+ from homesec.models.config import AlertPolicyOverrides
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ AlertPolicyFactory = Callable[
18
+ [BaseModel, dict[str, AlertPolicyOverrides], list[str]], AlertPolicy
19
+ ]
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class AlertPolicyPlugin:
24
+ name: str
25
+ config_model: type[BaseModel]
26
+ factory: AlertPolicyFactory
27
+
28
+
29
+ ALERT_POLICY_REGISTRY: dict[str, AlertPolicyPlugin] = {}
30
+
31
+
32
+ def register_alert_policy(plugin: AlertPolicyPlugin) -> None:
33
+ """Register an alert policy plugin with collision detection.
34
+
35
+ Args:
36
+ plugin: Alert policy plugin to register
37
+
38
+ Raises:
39
+ ValueError: If a plugin with the same name is already registered
40
+ """
41
+ if plugin.name in ALERT_POLICY_REGISTRY:
42
+ raise ValueError(
43
+ f"Alert policy plugin '{plugin.name}' is already registered. "
44
+ f"Plugin names must be unique across all alert policy plugins."
45
+ )
46
+ ALERT_POLICY_REGISTRY[plugin.name] = plugin
47
+
48
+
49
+ T = TypeVar("T", bound=Callable[[], AlertPolicyPlugin])
50
+
51
+
52
+ def alert_policy_plugin(name: str) -> Callable[[T], T]:
53
+ """Decorator to register an alert policy plugin.
54
+
55
+ Usage:
56
+ @alert_policy_plugin(name="my_policy")
57
+ def my_policy_plugin() -> AlertPolicyPlugin:
58
+ return AlertPolicyPlugin(...)
59
+
60
+ Args:
61
+ name: Plugin name (for validation only - must match plugin.name)
62
+
63
+ Returns:
64
+ Decorator function that registers the plugin
65
+ """
66
+
67
+ def decorator(factory_fn: T) -> T:
68
+ plugin = factory_fn()
69
+ register_alert_policy(plugin)
70
+ return factory_fn
71
+
72
+ return decorator
73
+
74
+
75
+ __all__ = [
76
+ "AlertPolicyPlugin",
77
+ "ALERT_POLICY_REGISTRY",
78
+ "register_alert_policy",
79
+ "alert_policy_plugin",
80
+ ]
@@ -0,0 +1,111 @@
1
+ """Default alert policy plugin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from homesec.interfaces import AlertPolicy
6
+ from homesec.models.alert import AlertDecision
7
+ from homesec.models.config import AlertPolicyOverrides, DefaultAlertPolicySettings
8
+ from homesec.models.filter import FilterResult
9
+ from homesec.models.vlm import AnalysisResult
10
+
11
+ # Risk level ordering for comparison
12
+ RISK_LEVELS = {"low": 0, "medium": 1, "high": 2, "critical": 3}
13
+
14
+
15
+ class DefaultAlertPolicy(AlertPolicy):
16
+ """Default alert policy implementation."""
17
+
18
+ def __init__(
19
+ self,
20
+ settings: DefaultAlertPolicySettings,
21
+ overrides: dict[str, AlertPolicyOverrides],
22
+ trigger_classes: list[str],
23
+ ) -> None:
24
+ self._settings = settings
25
+ self._overrides = overrides
26
+ self._trigger_classes = list(trigger_classes)
27
+
28
+ def should_notify(
29
+ self,
30
+ camera_name: str,
31
+ filter_result: FilterResult | None,
32
+ analysis: AnalysisResult | None,
33
+ ) -> tuple[bool, str]:
34
+ policy = self._policy_for(camera_name)
35
+
36
+ # Check notify_on_motion first (bypasses VLM)
37
+ if policy.notify_on_motion:
38
+ return True, "notify_on_motion=true"
39
+
40
+ # If no analysis, check if filter detected trigger classes (VLM failure fallback)
41
+ if analysis is None:
42
+ if filter_result and self._filter_detected_trigger_classes(filter_result):
43
+ return True, "filter_detected_trigger_vlm_failed"
44
+ return False, "no_analysis"
45
+
46
+ # Check risk level threshold
47
+ if self._risk_meets_threshold(analysis.risk_level, policy.min_risk_level):
48
+ return True, f"risk_level={analysis.risk_level}"
49
+
50
+ # Check activity type list
51
+ if analysis.activity_type in policy.notify_on_activity_types:
52
+ return True, f"activity_type={analysis.activity_type} (per-camera)"
53
+
54
+ return False, "below_threshold"
55
+
56
+ def make_decision(
57
+ self,
58
+ camera_name: str,
59
+ filter_result: FilterResult | None,
60
+ analysis: AnalysisResult | None,
61
+ ) -> AlertDecision:
62
+ notify, reason = self.should_notify(camera_name, filter_result, analysis)
63
+ return AlertDecision(notify=notify, notify_reason=reason)
64
+
65
+ def _policy_for(self, camera_name: str) -> DefaultAlertPolicySettings:
66
+ overrides = self._overrides.get(camera_name)
67
+ if overrides is None:
68
+ return self._settings
69
+ merged = {
70
+ **self._settings.model_dump(),
71
+ **overrides.model_dump(exclude_none=True),
72
+ }
73
+ return DefaultAlertPolicySettings.model_validate(merged)
74
+
75
+ def _risk_meets_threshold(self, actual: str, threshold: str) -> bool:
76
+ return RISK_LEVELS.get(actual, 0) >= RISK_LEVELS.get(threshold, 0)
77
+
78
+ def _filter_detected_trigger_classes(self, filter_result: FilterResult) -> bool:
79
+ detected = set(filter_result.detected_classes)
80
+ trigger = set(self._trigger_classes)
81
+ return bool(detected & trigger)
82
+
83
+
84
+ # Plugin registration
85
+ from pydantic import BaseModel
86
+ from homesec.plugins.alert_policies import AlertPolicyPlugin, alert_policy_plugin
87
+ from homesec.interfaces import AlertPolicy
88
+
89
+
90
+ @alert_policy_plugin(name="default")
91
+ def default_alert_policy_plugin() -> AlertPolicyPlugin:
92
+ """Default alert policy plugin factory.
93
+
94
+ Returns:
95
+ AlertPolicyPlugin for default risk-based alert policy
96
+ """
97
+ from homesec.models.config import AlertPolicyOverrides, DefaultAlertPolicySettings
98
+
99
+ def factory(
100
+ cfg: BaseModel,
101
+ overrides: dict[str, AlertPolicyOverrides],
102
+ trigger_classes: list[str],
103
+ ) -> AlertPolicy:
104
+ settings = DefaultAlertPolicySettings.model_validate(cfg)
105
+ return DefaultAlertPolicy(settings, overrides, trigger_classes)
106
+
107
+ return AlertPolicyPlugin(
108
+ name="default",
109
+ config_model=DefaultAlertPolicySettings,
110
+ factory=factory,
111
+ )
@@ -0,0 +1,60 @@
1
+ """No-op alert policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from homesec.interfaces import AlertPolicy
8
+ from homesec.models.alert import AlertDecision
9
+ from homesec.models.config import AlertPolicyOverrides
10
+ from homesec.models.filter import FilterResult
11
+ from homesec.models.vlm import AnalysisResult
12
+
13
+
14
+ class NoopAlertPolicy(AlertPolicy):
15
+ """Alert policy that never notifies."""
16
+
17
+ def should_notify(
18
+ self,
19
+ camera_name: str,
20
+ filter_result: FilterResult | None,
21
+ analysis: AnalysisResult | None,
22
+ ) -> tuple[bool, str]:
23
+ return False, "alert_policy_disabled"
24
+
25
+ def make_decision(
26
+ self,
27
+ camera_name: str,
28
+ filter_result: FilterResult | None,
29
+ analysis: AnalysisResult | None,
30
+ ) -> AlertDecision:
31
+ return AlertDecision(notify=False, notify_reason="alert_policy_disabled")
32
+
33
+
34
+ # Plugin registration
35
+ from pydantic import BaseModel
36
+ from homesec.plugins.alert_policies import AlertPolicyPlugin, alert_policy_plugin
37
+ from homesec.interfaces import AlertPolicy
38
+
39
+
40
+ @alert_policy_plugin(name="noop")
41
+ def noop_alert_policy_plugin() -> AlertPolicyPlugin:
42
+ """Noop alert policy plugin that never sends alerts.
43
+
44
+ Returns:
45
+ AlertPolicyPlugin for no-op alert policy
46
+ """
47
+
48
+ def factory(
49
+ cfg: BaseModel,
50
+ overrides: dict[str, AlertPolicyOverrides],
51
+ trigger_classes: list[str],
52
+ ) -> AlertPolicy:
53
+ # NoopAlertPolicy doesn't use any config
54
+ return NoopAlertPolicy()
55
+
56
+ return AlertPolicyPlugin(
57
+ name="noop",
58
+ config_model=BaseModel, # No config needed
59
+ factory=factory,
60
+ )
@@ -0,0 +1,126 @@
1
+ """VLM analyzer plugins and registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass
7
+ from typing import Callable, TYPE_CHECKING, TypeVar
8
+
9
+ from pydantic import BaseModel
10
+
11
+ from homesec.interfaces import VLMAnalyzer
12
+
13
+ if TYPE_CHECKING:
14
+ from homesec.models.vlm import VLMConfig
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Type alias for clarity
19
+ VLMFactory = Callable[["VLMConfig"], VLMAnalyzer]
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class VLMPlugin:
24
+ """Metadata for a VLM analyzer plugin."""
25
+
26
+ name: str
27
+ config_model: type[BaseModel]
28
+ factory: VLMFactory
29
+
30
+
31
+ VLM_REGISTRY: dict[str, VLMPlugin] = {}
32
+
33
+
34
+ def register_vlm(plugin: VLMPlugin) -> None:
35
+ """Register a VLM plugin with collision detection.
36
+
37
+ Args:
38
+ plugin: VLM plugin to register
39
+
40
+ Raises:
41
+ ValueError: If a plugin with the same name is already registered
42
+ """
43
+ if plugin.name in VLM_REGISTRY:
44
+ raise ValueError(
45
+ f"VLM plugin '{plugin.name}' is already registered. "
46
+ f"Plugin names must be unique across all VLM plugins."
47
+ )
48
+ VLM_REGISTRY[plugin.name] = plugin
49
+
50
+
51
+ T = TypeVar("T", bound=Callable[[], VLMPlugin])
52
+
53
+
54
+ def vlm_plugin(name: str) -> Callable[[T], T]:
55
+ """Decorator to register a VLM analyzer plugin.
56
+
57
+ Usage:
58
+ @vlm_plugin(name="my_vlm")
59
+ def my_vlm_plugin() -> VLMPlugin:
60
+ return VLMPlugin(...)
61
+
62
+ Args:
63
+ name: Plugin name (for validation only - must match plugin.name)
64
+
65
+ Returns:
66
+ Decorator function that registers the plugin
67
+ """
68
+
69
+ def decorator(factory_fn: T) -> T:
70
+ plugin = factory_fn()
71
+ register_vlm(plugin)
72
+ return factory_fn
73
+
74
+ return decorator
75
+
76
+
77
+ def load_vlm_plugin(config: VLMConfig) -> VLMAnalyzer:
78
+ """Load VLM plugin by name from config.
79
+
80
+ Validates the llm dict against the plugin's config_model and creates
81
+ a VLMConfig with the validated settings object.
82
+
83
+ Args:
84
+ config: VLM configuration with backend name and raw llm dict
85
+
86
+ Returns:
87
+ Instantiated VLM plugin
88
+
89
+ Raises:
90
+ ValueError: If plugin name is unknown or config validation fails
91
+ """
92
+ plugin_name = config.backend.lower()
93
+
94
+ if plugin_name not in VLM_REGISTRY:
95
+ available = ", ".join(sorted(VLM_REGISTRY.keys()))
96
+ raise ValueError(
97
+ f"Unknown VLM plugin: '{plugin_name}'. Available: {available}"
98
+ )
99
+
100
+ plugin = VLM_REGISTRY[plugin_name]
101
+
102
+ # Validate config.llm dict against plugin's config_model
103
+ validated_llm_settings = plugin.config_model.model_validate(config.llm)
104
+
105
+ # Create new VLMConfig with validated llm settings object
106
+ from homesec.models.vlm import VLMConfig as VLMConfigModel
107
+
108
+ validated_config = VLMConfigModel(
109
+ backend=config.backend,
110
+ trigger_classes=config.trigger_classes,
111
+ max_workers=config.max_workers,
112
+ llm=validated_llm_settings,
113
+ preprocessing=config.preprocessing,
114
+ )
115
+
116
+ return plugin.factory(validated_config)
117
+
118
+
119
+ __all__ = [
120
+ "VLMPlugin",
121
+ "VLMFactory",
122
+ "VLM_REGISTRY",
123
+ "register_vlm",
124
+ "vlm_plugin",
125
+ "load_vlm_plugin",
126
+ ]