homesec 1.1.1__tar.gz → 1.2.0__tar.gz
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-1.1.1 → homesec-1.2.0}/.github/workflows/release.yaml +3 -2
- {homesec-1.1.1 → homesec-1.2.0}/.gitignore +1 -0
- {homesec-1.1.1 → homesec-1.2.0}/AGENTS.md +71 -21
- {homesec-1.1.1 → homesec-1.2.0}/CHANGELOG.md +35 -0
- {homesec-1.1.1 → homesec-1.2.0}/DESIGN.md +101 -30
- {homesec-1.1.1 → homesec-1.2.0}/PKG-INFO +1 -1
- homesec-1.2.0/PLUGIN_DEVELOPMENT.md +308 -0
- {homesec-1.1.1 → homesec-1.2.0}/pyproject.toml +1 -1
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/app.py +38 -84
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/cli.py +6 -10
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/config/validation.py +38 -12
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/interfaces.py +50 -2
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/maintenance/cleanup_clips.py +4 -4
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/models/__init__.py +6 -5
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/models/alert.py +3 -2
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/models/clip.py +4 -2
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/models/config.py +62 -17
- homesec-1.2.0/src/homesec/models/enums.py +114 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/models/events.py +19 -18
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/models/filter.py +13 -3
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/models/source.py +4 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/models/vlm.py +18 -7
- homesec-1.2.0/src/homesec/plugins/__init__.py +36 -0
- homesec-1.2.0/src/homesec/plugins/alert_policies/__init__.py +54 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/plugins/alert_policies/default.py +20 -45
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/plugins/alert_policies/noop.py +14 -29
- homesec-1.2.0/src/homesec/plugins/analyzers/__init__.py +40 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/plugins/analyzers/openai.py +70 -53
- homesec-1.2.0/src/homesec/plugins/filters/__init__.py +39 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/plugins/filters/yolo.py +103 -66
- homesec-1.2.0/src/homesec/plugins/notifiers/__init__.py +45 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/plugins/notifiers/mqtt.py +22 -30
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/plugins/notifiers/sendgrid_email.py +34 -32
- homesec-1.2.0/src/homesec/plugins/registry.py +160 -0
- homesec-1.2.0/src/homesec/plugins/sources/__init__.py +45 -0
- homesec-1.2.0/src/homesec/plugins/sources/ftp.py +25 -0
- homesec-1.2.0/src/homesec/plugins/sources/local_folder.py +30 -0
- homesec-1.2.0/src/homesec/plugins/sources/rtsp.py +27 -0
- homesec-1.2.0/src/homesec/plugins/storage/__init__.py +45 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/plugins/storage/dropbox.py +36 -37
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/plugins/storage/local.py +8 -29
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/plugins/utils.py +8 -4
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/repository/clip_repository.py +20 -14
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/sources/base.py +24 -2
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/sources/local_folder.py +57 -78
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/sources/rtsp.py +45 -4
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/state/postgres.py +46 -17
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/mocks/event_store.py +8 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_alert_policy.py +24 -33
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_app.py +55 -34
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_cleanup_clips.py +6 -2
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_clip_repository.py +2 -1
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_config.py +37 -4
- homesec-1.2.0/tests/homesec/test_enums.py +243 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_integration.py +4 -5
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_openai_vlm.py +26 -27
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_pipeline.py +6 -6
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_pipeline_events.py +4 -5
- homesec-1.2.0/tests/homesec/test_plugin_registration.py +89 -0
- homesec-1.2.0/tests/homesec/test_source_health.py +71 -0
- {homesec-1.1.1 → homesec-1.2.0}/uv.lock +1 -1
- homesec-1.1.1/src/homesec/plugins/__init__.py +0 -62
- homesec-1.1.1/src/homesec/plugins/alert_policies/__init__.py +0 -79
- homesec-1.1.1/src/homesec/plugins/analyzers/__init__.py +0 -125
- homesec-1.1.1/src/homesec/plugins/filters/__init__.py +0 -123
- homesec-1.1.1/src/homesec/plugins/notifiers/__init__.py +0 -81
- homesec-1.1.1/src/homesec/plugins/storage/__init__.py +0 -115
- homesec-1.1.1/tests/homesec/test_local_folder_deduplication.py +0 -432
- homesec-1.1.1/tests/homesec/test_plugin_registration.py +0 -406
- {homesec-1.1.1 → homesec-1.2.0}/.dockerignore +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/.env.example +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/.github/workflows/ci.yml +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/.github/workflows/validate-pr-title.yaml +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/Dockerfile +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/LICENSE +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/Makefile +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/README.md +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/TESTING.md +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/alembic/env.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/alembic/script.py.mako +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/alembic/versions/e6f25df0df90_initial.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/alembic.ini +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/config/example.yaml +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/docker-compose.yml +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/docker-entrypoint.sh +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/__init__.py +1 -1
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/config/__init__.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/config/loader.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/errors.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/health/__init__.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/health/server.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/logging_setup.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/maintenance/__init__.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/models/storage.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/pipeline/__init__.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/pipeline/alert_policy.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/pipeline/core.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/plugins/notifiers/multiplex.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/py.typed +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/repository/__init__.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/sources/__init__.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/sources/ftp.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/state/__init__.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/storage_paths.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/telemetry/__init__.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/telemetry/db/__init__.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/telemetry/db/log_table.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/telemetry/db_log_handler.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/src/homesec/telemetry/postgres_settings.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/__init__.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/conftest.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/__init__.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/conftest.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/mocks/__init__.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/mocks/filter.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/mocks/notifier.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/mocks/state_store.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/mocks/storage.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/mocks/vlm.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_cli.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_clip_sources.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_dropbox_storage.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_event_store.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_ftp_source.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_health.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_local_storage.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_logging_setup.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_mqtt_notifier.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_notifiers.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_plugin_utils.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_rtsp_helpers.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_sendgrid_notifier.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_state_store.py +0 -0
- {homesec-1.1.1 → homesec-1.2.0}/tests/homesec/test_yolo_filter.py +0 -0
|
@@ -21,6 +21,7 @@ jobs:
|
|
|
21
21
|
uses: actions/checkout@v4
|
|
22
22
|
with:
|
|
23
23
|
fetch-depth: 0
|
|
24
|
+
token: ${{ secrets.GH_TOKEN }}
|
|
24
25
|
|
|
25
26
|
- name: Setup Python
|
|
26
27
|
uses: actions/setup-python@v5
|
|
@@ -43,7 +44,7 @@ jobs:
|
|
|
43
44
|
if: ${{ !inputs.dry_run }}
|
|
44
45
|
id: semrel
|
|
45
46
|
env:
|
|
46
|
-
GH_TOKEN: ${{ secrets.
|
|
47
|
+
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
47
48
|
run: |
|
|
48
49
|
semantic-release version
|
|
49
50
|
echo "version=$(grep 'version = ' pyproject.toml | head -1 | cut -d'\"' -f2)" >> $GITHUB_OUTPUT
|
|
@@ -63,7 +64,7 @@ jobs:
|
|
|
63
64
|
- name: Upload to GitHub Release
|
|
64
65
|
if: ${{ !inputs.dry_run }}
|
|
65
66
|
env:
|
|
66
|
-
GH_TOKEN: ${{ secrets.
|
|
67
|
+
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
67
68
|
run: |
|
|
68
69
|
VERSION=${{ steps.semrel.outputs.version }}
|
|
69
70
|
# Upload dist files to the release that semantic-release created
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# HomeSec Development Guidelines
|
|
2
2
|
|
|
3
|
-
**Last reviewed:**
|
|
3
|
+
**Last reviewed:** 2026-01-18
|
|
4
4
|
**Purpose:** Critical patterns to prevent runtime bugs when extending HomeSec. For architecture overview, see `DESIGN.md`.
|
|
5
5
|
|
|
6
6
|
---
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
- **Repository pattern**: Use `ClipRepository` for all state/event writes. Never touch `StateStore`/`EventStore` directly.
|
|
13
13
|
- **Preserve stack traces**: Custom errors must set `self.__cause__ = cause` to preserve original exception.
|
|
14
14
|
- **Tests must use Given/When/Then comments**: Every test must include these comments and follow behavioral testing principles (see `TESTING.md`).
|
|
15
|
+
- **Import from canonical source**: Import types from their defining module, not re-exports. For example, import `RiskLevel` from `models.enums`, not from `models.vlm`. Avoid creating re-exports in `__all__`.
|
|
15
16
|
- **Postgres for state**: Use `clip_states` table with `clip_id` (primary key) + `data` (jsonb) for evolvable schema.
|
|
16
17
|
- **Pydantic everywhere**: Validate config, DB payloads, VLM outputs, and MQTT payloads with Pydantic models.
|
|
17
18
|
- **Clarify before complexity**: Ask user for clarification when simpler design may exist. Don't proceed with complex workarounds.
|
|
@@ -232,14 +233,13 @@ await self._event_store.append(event) # What if this fails? State already updat
|
|
|
232
233
|
|
|
233
234
|
### 4. Plugin Registration
|
|
234
235
|
|
|
235
|
-
**Rule:** Use
|
|
236
|
+
**Rule:** Use decorator-based registration with typed factories. All plugins follow the same pattern: `@<type>_plugin` decorator + `load_<type>_plugin()` loader.
|
|
236
237
|
|
|
237
|
-
**✅ Template:
|
|
238
|
+
**✅ Template: Decorator-Based Registration**
|
|
238
239
|
|
|
239
240
|
```python
|
|
240
241
|
# In src/homesec/plugins/notifiers/__init__.py
|
|
241
242
|
from dataclasses import dataclass
|
|
242
|
-
from importlib.metadata import entry_points
|
|
243
243
|
from pydantic import BaseModel
|
|
244
244
|
|
|
245
245
|
@dataclass(frozen=True)
|
|
@@ -250,28 +250,71 @@ class NotifierPlugin:
|
|
|
250
250
|
|
|
251
251
|
NOTIFIER_REGISTRY: dict[str, NotifierPlugin] = {}
|
|
252
252
|
|
|
253
|
-
def register_notifier(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
253
|
+
def register_notifier(plugin: NotifierPlugin) -> None:
|
|
254
|
+
"""Register with collision detection."""
|
|
255
|
+
if plugin.name in NOTIFIER_REGISTRY:
|
|
256
|
+
raise ValueError(f"Plugin '{plugin.name}' already registered")
|
|
257
|
+
NOTIFIER_REGISTRY[plugin.name] = plugin
|
|
258
|
+
|
|
259
|
+
def notifier_plugin(name: str) -> Callable[[T], T]:
|
|
260
|
+
"""Decorator to register a notifier plugin."""
|
|
261
|
+
def decorator(factory_fn: T) -> T:
|
|
262
|
+
plugin = factory_fn()
|
|
263
|
+
register_notifier(plugin)
|
|
264
|
+
return factory_fn
|
|
265
|
+
return decorator
|
|
266
|
+
|
|
267
|
+
def load_notifier_plugin(backend: str, config: dict | BaseModel) -> Notifier:
|
|
268
|
+
"""Load and instantiate a notifier plugin with config validation."""
|
|
269
|
+
plugin = NOTIFIER_REGISTRY[backend.lower()]
|
|
270
|
+
validated = plugin.config_model.model_validate(config) if isinstance(config, dict) else config
|
|
271
|
+
return plugin.factory(validated)
|
|
272
|
+
```
|
|
261
273
|
|
|
262
|
-
|
|
263
|
-
def register():
|
|
264
|
-
from homesec.plugins.notifiers import register_notifier
|
|
265
|
-
register_notifier("mqtt", MQTTConfig, lambda cfg: MQTTNotifier(cfg))
|
|
274
|
+
**✅ Template: Plugin Implementation with Type Safety**
|
|
266
275
|
|
|
267
|
-
|
|
268
|
-
#
|
|
269
|
-
|
|
276
|
+
```python
|
|
277
|
+
# In src/homesec/plugins/notifiers/mqtt.py
|
|
278
|
+
from pydantic import BaseModel
|
|
279
|
+
from homesec.plugins.notifiers import NotifierPlugin, notifier_plugin
|
|
280
|
+
|
|
281
|
+
class MQTTNotifierConfig(BaseModel):
|
|
282
|
+
broker_url: str
|
|
283
|
+
topic_prefix: str = "homesec"
|
|
284
|
+
|
|
285
|
+
@notifier_plugin(name="mqtt")
|
|
286
|
+
def mqtt_notifier_plugin() -> NotifierPlugin:
|
|
287
|
+
def factory(cfg: BaseModel) -> Notifier:
|
|
288
|
+
# Type-safe: validate config type at runtime
|
|
289
|
+
if not isinstance(cfg, MQTTNotifierConfig):
|
|
290
|
+
raise TypeError(f"Expected MQTTNotifierConfig, got {type(cfg).__name__}")
|
|
291
|
+
return MQTTNotifier(cfg)
|
|
292
|
+
|
|
293
|
+
return NotifierPlugin(
|
|
294
|
+
name="mqtt",
|
|
295
|
+
config_model=MQTTNotifierConfig,
|
|
296
|
+
factory=factory,
|
|
297
|
+
)
|
|
270
298
|
```
|
|
271
299
|
|
|
272
|
-
**
|
|
300
|
+
**Plugin Types and Context Objects:**
|
|
301
|
+
|
|
302
|
+
| Plugin Type | Decorator | Context | Factory Signature |
|
|
303
|
+
|-------------|-----------|---------|-------------------|
|
|
304
|
+
| Filters | `@filter_plugin` | `FilterContext` | `(BaseModel, FilterContext) -> ObjectFilter` |
|
|
305
|
+
| Analyzers | `@vlm_plugin` | `VLMContext` | `(BaseModel, VLMContext) -> VLMAnalyzer` |
|
|
306
|
+
| Sources | `@source_plugin` | `SourceContext` | `(BaseModel, SourceContext) -> ClipSource` |
|
|
307
|
+
| Alert Policies | `@alert_policy_plugin` | `AlertPolicyContext` | `(BaseModel, AlertPolicyContext) -> AlertPolicy` |
|
|
308
|
+
| Notifiers | `@notifier_plugin` | None | `(BaseModel) -> Notifier` |
|
|
309
|
+
| Storage | `@storage_plugin` | None | `(BaseModel) -> StorageBackend` |
|
|
273
310
|
|
|
274
|
-
**
|
|
311
|
+
**Why some plugins have Context and others don't:**
|
|
312
|
+
- **With Context** (Filters, Analyzers, Sources, AlertPolicies): These plugins need runtime information beyond their config—camera names, executor pools, file paths, or pipeline state. Context objects provide this without polluting config models.
|
|
313
|
+
- **Without Context** (Notifiers, Storage): These are stateless services that only need their own config. They don't depend on which camera or pipeline invokes them.
|
|
314
|
+
|
|
315
|
+
**Note:** Always use `isinstance()` checks in factories instead of `cast()` to ensure type safety at runtime.
|
|
316
|
+
|
|
317
|
+
**Reference:** See `PLUGIN_DEVELOPMENT.md` for complete guide. Example implementations: `src/homesec/plugins/filters/yolo.py`, `src/homesec/plugins/notifiers/mqtt.py`.
|
|
275
318
|
|
|
276
319
|
---
|
|
277
320
|
|
|
@@ -381,6 +424,13 @@ make db-migrate # Run Alembic migrations
|
|
|
381
424
|
- `UPPER_SNAKE_CASE` for constants
|
|
382
425
|
- 4-space indentation
|
|
383
426
|
|
|
427
|
+
### Import Organization
|
|
428
|
+
|
|
429
|
+
- **All imports at top of file** - Never use local/inline imports except when absolutely necessary to avoid circular imports
|
|
430
|
+
- **Standard ordering**: stdlib → third-party → homesec (ruff handles this with `ruff check --fix`)
|
|
431
|
+
- **No re-imports at end of file** - Plugin registration decorators should use imports from the top
|
|
432
|
+
- **Circular import avoidance**: Use `TYPE_CHECKING` blocks for type-only imports when needed
|
|
433
|
+
|
|
384
434
|
### Security
|
|
385
435
|
|
|
386
436
|
```python
|
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.2.0 (2026-01-19)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- **rtsp**: Remove incompatible -rw_timeout flag and unused variables
|
|
10
|
+
([`40339a2`](https://github.com/lan17/homesec/commit/40339a27ff3f45117ef918c6485da8dbcc0be724))
|
|
11
|
+
|
|
12
|
+
- **rtsp**: Use -vsync 0 instead of -fps_mode for older ffmpeg compat
|
|
13
|
+
([`22fc7e3`](https://github.com/lan17/homesec/commit/22fc7e3fab57aefa869d4246b036d5d0b47494dd))
|
|
14
|
+
|
|
15
|
+
### Chores
|
|
16
|
+
|
|
17
|
+
- Sync uv.lock with project version 1.1.2 ([#14](https://github.com/lan17/homesec/pull/14),
|
|
18
|
+
[`64f50d8`](https://github.com/lan17/homesec/commit/64f50d86d22f0a52a8298c9ba38146cc17a1f0be))
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
- **rtsp**: Add configurable ffmpeg_flags and robust defaults
|
|
23
|
+
([`e460637`](https://github.com/lan17/homesec/commit/e460637bbdeee7f3947ddf2778a69c980edcf24b))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## v1.1.2 (2026-01-19)
|
|
27
|
+
|
|
28
|
+
### Bug Fixes
|
|
29
|
+
|
|
30
|
+
- Use PAT token for release workflow to bypass branch protection
|
|
31
|
+
([#13](https://github.com/lan17/homesec/pull/13),
|
|
32
|
+
[`511ac6d`](https://github.com/lan17/homesec/commit/511ac6d8899365d324ede41aefdbe8fab910f1cb))
|
|
33
|
+
|
|
34
|
+
### Refactoring
|
|
35
|
+
|
|
36
|
+
- Complete plugin architecture standardization ([#10](https://github.com/lan17/homesec/pull/10),
|
|
37
|
+
[`4e5b85f`](https://github.com/lan17/homesec/commit/4e5b85fd9a32d4f71c4365222a735c2e01eba583))
|
|
38
|
+
|
|
39
|
+
|
|
5
40
|
## v1.1.1 (2026-01-16)
|
|
6
41
|
|
|
7
42
|
### Bug Fixes
|
|
@@ -8,6 +8,95 @@
|
|
|
8
8
|
4. **Single async flow per clip.** In-process handoff with bounded concurrency (global + per-stage `asyncio.Semaphore`). Callback-based ClipSource interface for extensibility.
|
|
9
9
|
5. **Pluggable object detection and VLM analysis.** Specific models (YOLO, GPT-4, etc.) are abstracted behind async plugin interfaces. Pipeline is model-agnostic; plugins manage their own resources (GPU, process pools, API clients).
|
|
10
10
|
6. **Errors as values.** Stage methods return `Result | StageError` instead of raising exceptions. This enables partial failures (e.g., upload fails but filter succeeds → still send alert). Stack traces are preserved in error objects. **Strict type checking required** (mypy --strict or pyright) to prevent runtime errors from missing `isinstance()` checks.
|
|
11
|
+
7. **Type-safe enums for domain values.** Use `StrEnum`/`IntEnum` from `models/enums.py` for type safety, IDE support, and maintainability. See [Type Safety & Enums](#type-safety--enums) below.
|
|
12
|
+
|
|
13
|
+
## Type Safety & Enums
|
|
14
|
+
|
|
15
|
+
Domain values that appear in multiple places use centralized enums in `models/enums.py`:
|
|
16
|
+
|
|
17
|
+
| Enum | Type | Purpose | Example |
|
|
18
|
+
|------|------|---------|---------|
|
|
19
|
+
| `EventType` | `StrEnum` | Clip lifecycle event types | `EventType.CLIP_RECORDED` |
|
|
20
|
+
| `ClipStatus` | `StrEnum` | Clip processing status | `ClipStatus.UPLOADED` |
|
|
21
|
+
| `RiskLevel` | `IntEnum` | VLM risk assessment (ordered) | `RiskLevel.HIGH > RiskLevel.LOW` |
|
|
22
|
+
|
|
23
|
+
### Benefits
|
|
24
|
+
|
|
25
|
+
- **Type safety**: Catch typos at compile time with mypy/pyright
|
|
26
|
+
- **IDE support**: Autocomplete and refactoring
|
|
27
|
+
- **Single source of truth**: No duplicate string literals
|
|
28
|
+
- **Natural comparison**: `RiskLevel` uses `IntEnum` for `>=` comparisons
|
|
29
|
+
|
|
30
|
+
### Usage Examples
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from homesec.models.enums import EventType, ClipStatus, RiskLevel
|
|
34
|
+
|
|
35
|
+
# Event types in event models
|
|
36
|
+
class ClipRecordedEvent(ClipEvent):
|
|
37
|
+
event_type: Literal[EventType.CLIP_RECORDED] = EventType.CLIP_RECORDED
|
|
38
|
+
|
|
39
|
+
# Status comparisons
|
|
40
|
+
if state.status == ClipStatus.UPLOADED:
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
# Risk level comparison (IntEnum)
|
|
44
|
+
if analysis.risk_level >= RiskLevel.MEDIUM:
|
|
45
|
+
send_alert()
|
|
46
|
+
|
|
47
|
+
# Parse from string (for config)
|
|
48
|
+
level = RiskLevel.from_string("high") # Returns RiskLevel.HIGH
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Serialization
|
|
52
|
+
|
|
53
|
+
- `StrEnum` values serialize to their string value automatically
|
|
54
|
+
- `RiskLevel` (IntEnum) uses custom Pydantic serialization to maintain string representation in configs (`"medium"` not `1`)
|
|
55
|
+
|
|
56
|
+
## Plugin Architecture & Philosophy
|
|
57
|
+
|
|
58
|
+
For detailed implementation guides and how-tos, see **[PLUGIN_DEVELOPMENT.md](PLUGIN_DEVELOPMENT.md)**.
|
|
59
|
+
|
|
60
|
+
HomeSec employs a **Class-Based Plugin Architecture (V2)** designed for strict type safety, runtime validation, and clear separation of concerns. This section details the design patterns and philosophies driving the system.
|
|
61
|
+
|
|
62
|
+
### 1. Core Philosophy: "Configuration is the Contract"
|
|
63
|
+
|
|
64
|
+
In V1, plugins were factories accepting raw dictionaries and loose context objects. In V2, the **Configuration Model** (Pydantic) defines the entire contract for a plugin.
|
|
65
|
+
|
|
66
|
+
- **Type Safety**: Every plugin defines a `config_cls` (Pydantic model). The registry validates raw JSON/YAML against this model *before* the plugin is instantiated.
|
|
67
|
+
- **Dependency Injection**: Runtime dependencies (like `camera_name` for sources, or `trigger_classes` for analyzers) are injected into the configuration dictionary *before* validation.
|
|
68
|
+
- *Benefit*: The plugin implementation doesn't need a separate `context` argument. It just declares `camera_name: str` in its config model, and the system ensures it's present.
|
|
69
|
+
- **Fail Fast**: Invalid configs cause a `ValidationError` at loading time, preventing partial start-ups.
|
|
70
|
+
|
|
71
|
+
### 2. The Unified Registry Pattern
|
|
72
|
+
|
|
73
|
+
Instead of maintaining separate registries for each plugin type (`SOURCE_REGISTRY`, `FILTER_REGISTRY`), we use a single, generic `PluginRegistry[ConfigT, PluginInterfaceT]`.
|
|
74
|
+
|
|
75
|
+
- **Generics for Safety**: The registry is generic over the configuration type and the plugin interface.
|
|
76
|
+
```python
|
|
77
|
+
# strict typing ensures that load_plugin(PluginType.SOURCE, ...) returns a ClipSource
|
|
78
|
+
source = registry.load_plugin(PluginType.SOURCE, "my_source", config_dict)
|
|
79
|
+
```
|
|
80
|
+
- **Declarative Registration**: Plugins register themselves using the `@plugin` decorator, which captures metadata (name, type) and creates the association without manual mapping code.
|
|
81
|
+
|
|
82
|
+
### 3. Factory Pattern vs. Class Creation
|
|
83
|
+
|
|
84
|
+
We transitioned from functional factories (`make_source()`) to class-based factories (`Class.create()`).
|
|
85
|
+
|
|
86
|
+
- **Encapsulation**: The class handles its own construction logic in `create()`, keeping `__init__` clean or free for dependency injection.
|
|
87
|
+
- **State Management**: Plugins often hold state (database connections, ML model handles). Classes naturally encapsulate this state and provide lifecycle methods (`shutdown()`) to clean it up.
|
|
88
|
+
|
|
89
|
+
### 4. Decoupling & Local State
|
|
90
|
+
|
|
91
|
+
A key design validation was separating `LocalFolderSource` from the global `StateStore`.
|
|
92
|
+
|
|
93
|
+
- **Philosophy**: Components should own their local truth.
|
|
94
|
+
- **Implementation**: `LocalFolderSource` uses a "Local State Manifest" (`.homesec_state.json`) to track processed files. This means the source can function even if the central database is down, fulfilling the "P0: Never miss new clips" requirement.
|
|
95
|
+
|
|
96
|
+
### 5. Error Handling Philosophy
|
|
97
|
+
|
|
98
|
+
- **Boundaries must never crash**: Top-level loops wrap plugin calls in broad exception handlers (specifically catching `PipelineError`).
|
|
99
|
+
- **Errors as Values**: Where possible, return error objects/states rather than raising exceptions, allowing the pipeline to degrade gracefully (e.g., skip analysis but still upload).
|
|
11
100
|
|
|
12
101
|
## Goals
|
|
13
102
|
|
|
@@ -47,9 +136,9 @@ Build a reliable, pluggable pipeline to:
|
|
|
47
136
|
|
|
48
137
|
## Current Building Blocks (Repo)
|
|
49
138
|
|
|
50
|
-
- Motion + recording: `src/
|
|
51
|
-
- Object detection plugin (reference
|
|
52
|
-
- VLM plugin (reference
|
|
139
|
+
- Motion + recording: `src/homesec/sources/rtsp.py` (OpenCV motion detection + ffmpeg recording).
|
|
140
|
+
- Object detection plugin (reference): `src/homesec/plugins/filters/yolo.py` (YOLOv8 sampling to detect people/animals).
|
|
141
|
+
- VLM plugin (reference): `src/homesec/plugins/analyzers/openai.py` (structured output with `risk_level`, `activity_type`, timeline).
|
|
53
142
|
- Postgres (workflow state when available): Alembic migrations (`alembic/`) + telemetry logging (`src/db_log_handler.py`) + workflow state table `clip_states` (best-effort; DB outages may drop state updates).
|
|
54
143
|
|
|
55
144
|
## Proposed Architecture (Event-Driven Pipeline)
|
|
@@ -277,13 +366,11 @@ class AlertPolicy(Protocol):
|
|
|
277
366
|
|
|
278
367
|
# === Plugin Interfaces ===
|
|
279
368
|
|
|
280
|
-
class ObjectFilter(Protocol):
|
|
369
|
+
class ObjectFilter(Shutdownable, Protocol):
|
|
281
370
|
"""Plugin interface for object detection in video clips."""
|
|
282
371
|
|
|
283
372
|
async def detect(
|
|
284
|
-
self,
|
|
285
|
-
video_path: Path,
|
|
286
|
-
config: FilterConfig
|
|
373
|
+
self, video_path: Path, overrides: FilterOverrides | None = None
|
|
287
374
|
) -> FilterResult:
|
|
288
375
|
"""
|
|
289
376
|
Detect objects in video clip.
|
|
@@ -292,8 +379,9 @@ class ObjectFilter(Protocol):
|
|
|
292
379
|
- MUST be async (use asyncio.to_thread or run_in_executor for blocking code)
|
|
293
380
|
- CPU/GPU-bound plugins should manage their own ProcessPoolExecutor internally
|
|
294
381
|
- I/O-bound plugins can use async HTTP clients directly
|
|
295
|
-
- Should respect config
|
|
382
|
+
- Should respect the instance config max_workers if managing worker pool
|
|
296
383
|
- Should support early exit on first detection for efficiency
|
|
384
|
+
- overrides apply per-call (model path cannot be overridden)
|
|
297
385
|
|
|
298
386
|
Returns:
|
|
299
387
|
FilterResult with detected_classes, confidence, sampled_frames, model name
|
|
@@ -303,24 +391,14 @@ class ObjectFilter(Protocol):
|
|
|
303
391
|
async def shutdown(self) -> None:
|
|
304
392
|
"""
|
|
305
393
|
Cleanup resources (process pools, GPU memory, file handles).
|
|
306
|
-
|
|
307
|
-
Called once during application shutdown. Implementation should:
|
|
308
|
-
- Shutdown ProcessPoolExecutor if used (wait=True to finish in-flight work)
|
|
309
|
-
- Release GPU memory
|
|
310
|
-
- Close any open file handles or connections
|
|
311
|
-
|
|
312
|
-
Example:
|
|
313
|
-
self._executor.shutdown(wait=True, cancel_futures=False)
|
|
314
394
|
"""
|
|
395
|
+
...
|
|
315
396
|
|
|
316
|
-
class VLMAnalyzer(Protocol):
|
|
397
|
+
class VLMAnalyzer(Shutdownable, Protocol):
|
|
317
398
|
"""Plugin interface for VLM-based clip analysis."""
|
|
318
399
|
|
|
319
400
|
async def analyze(
|
|
320
|
-
self,
|
|
321
|
-
video_path: Path,
|
|
322
|
-
filter_result: FilterResult,
|
|
323
|
-
config: VLMConfig
|
|
401
|
+
self, video_path: Path, filter_result: FilterResult, config: VLMConfig
|
|
324
402
|
) -> AnalysisResult:
|
|
325
403
|
"""
|
|
326
404
|
Analyze clip and produce structured assessment.
|
|
@@ -340,15 +418,8 @@ class VLMAnalyzer(Protocol):
|
|
|
340
418
|
async def shutdown(self) -> None:
|
|
341
419
|
"""
|
|
342
420
|
Cleanup resources (HTTP sessions, process pools, model memory).
|
|
343
|
-
|
|
344
|
-
Called once during application shutdown. Implementation should:
|
|
345
|
-
- Close HTTP sessions (aiohttp.ClientSession.close())
|
|
346
|
-
- Shutdown ProcessPoolExecutor if using local models
|
|
347
|
-
- Unload models from GPU/RAM
|
|
348
|
-
|
|
349
|
-
Example:
|
|
350
|
-
await self._session.close()
|
|
351
421
|
"""
|
|
422
|
+
...
|
|
352
423
|
```
|
|
353
424
|
|
|
354
425
|
**Reference Implementations (MVP):**
|
|
@@ -361,7 +432,7 @@ class VLMAnalyzer(Protocol):
|
|
|
361
432
|
- `MockFilter` / `MockVLM`: For testing (instant responses, no actual inference)
|
|
362
433
|
- `shutdown()` implementation: no-op (no resources to clean up)
|
|
363
434
|
|
|
364
|
-
## Configuration
|
|
435
|
+
## Configuration
|
|
365
436
|
|
|
366
437
|
Prefer a single YAML file for non-secret configuration (cameras, sources, policies, MQTT). Keep secrets in `.env` and reference them by env var name from YAML.
|
|
367
438
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: homesec
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
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
|