homesec 1.1.0__tar.gz → 1.1.2__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.
Files changed (134) hide show
  1. {homesec-1.1.0 → homesec-1.1.2}/.github/workflows/release.yaml +3 -2
  2. {homesec-1.1.0 → homesec-1.1.2}/.gitignore +1 -0
  3. {homesec-1.1.0 → homesec-1.1.2}/AGENTS.md +71 -21
  4. {homesec-1.1.0 → homesec-1.1.2}/CHANGELOG.md +32 -0
  5. {homesec-1.1.0 → homesec-1.1.2}/DESIGN.md +101 -30
  6. {homesec-1.1.0 → homesec-1.1.2}/PKG-INFO +1 -1
  7. homesec-1.1.2/PLUGIN_DEVELOPMENT.md +308 -0
  8. {homesec-1.1.0 → homesec-1.1.2}/pyproject.toml +1 -1
  9. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/app.py +38 -84
  10. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/cli.py +9 -10
  11. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/config/validation.py +38 -12
  12. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/interfaces.py +50 -2
  13. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/maintenance/cleanup_clips.py +4 -4
  14. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/models/__init__.py +6 -5
  15. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/models/alert.py +3 -2
  16. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/models/clip.py +4 -2
  17. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/models/config.py +62 -17
  18. homesec-1.1.2/src/homesec/models/enums.py +114 -0
  19. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/models/events.py +19 -18
  20. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/models/filter.py +13 -3
  21. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/models/source.py +3 -0
  22. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/models/vlm.py +18 -7
  23. homesec-1.1.2/src/homesec/plugins/__init__.py +36 -0
  24. homesec-1.1.2/src/homesec/plugins/alert_policies/__init__.py +54 -0
  25. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/plugins/alert_policies/default.py +20 -45
  26. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/plugins/alert_policies/noop.py +14 -29
  27. homesec-1.1.2/src/homesec/plugins/analyzers/__init__.py +40 -0
  28. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/plugins/analyzers/openai.py +70 -53
  29. homesec-1.1.2/src/homesec/plugins/filters/__init__.py +39 -0
  30. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/plugins/filters/yolo.py +103 -66
  31. homesec-1.1.2/src/homesec/plugins/notifiers/__init__.py +45 -0
  32. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/plugins/notifiers/mqtt.py +22 -30
  33. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/plugins/notifiers/sendgrid_email.py +34 -32
  34. homesec-1.1.2/src/homesec/plugins/registry.py +160 -0
  35. homesec-1.1.2/src/homesec/plugins/sources/__init__.py +45 -0
  36. homesec-1.1.2/src/homesec/plugins/sources/ftp.py +25 -0
  37. homesec-1.1.2/src/homesec/plugins/sources/local_folder.py +30 -0
  38. homesec-1.1.2/src/homesec/plugins/sources/rtsp.py +27 -0
  39. homesec-1.1.2/src/homesec/plugins/storage/__init__.py +45 -0
  40. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/plugins/storage/dropbox.py +36 -37
  41. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/plugins/storage/local.py +8 -29
  42. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/plugins/utils.py +8 -4
  43. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/repository/clip_repository.py +20 -14
  44. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/sources/base.py +24 -2
  45. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/sources/local_folder.py +57 -78
  46. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/state/postgres.py +46 -17
  47. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/mocks/event_store.py +8 -0
  48. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_alert_policy.py +24 -33
  49. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_app.py +55 -34
  50. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_cleanup_clips.py +6 -2
  51. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_cli.py +66 -8
  52. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_clip_repository.py +2 -1
  53. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_config.py +37 -4
  54. homesec-1.1.2/tests/homesec/test_enums.py +243 -0
  55. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_integration.py +4 -5
  56. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_openai_vlm.py +26 -27
  57. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_pipeline.py +6 -6
  58. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_pipeline_events.py +4 -5
  59. homesec-1.1.2/tests/homesec/test_plugin_registration.py +89 -0
  60. homesec-1.1.2/tests/homesec/test_source_health.py +71 -0
  61. {homesec-1.1.0 → homesec-1.1.2}/uv.lock +1 -1
  62. homesec-1.1.0/src/homesec/plugins/__init__.py +0 -62
  63. homesec-1.1.0/src/homesec/plugins/alert_policies/__init__.py +0 -79
  64. homesec-1.1.0/src/homesec/plugins/analyzers/__init__.py +0 -125
  65. homesec-1.1.0/src/homesec/plugins/filters/__init__.py +0 -123
  66. homesec-1.1.0/src/homesec/plugins/notifiers/__init__.py +0 -81
  67. homesec-1.1.0/src/homesec/plugins/storage/__init__.py +0 -115
  68. homesec-1.1.0/tests/homesec/test_local_folder_deduplication.py +0 -432
  69. homesec-1.1.0/tests/homesec/test_plugin_registration.py +0 -406
  70. {homesec-1.1.0 → homesec-1.1.2}/.dockerignore +0 -0
  71. {homesec-1.1.0 → homesec-1.1.2}/.env.example +0 -0
  72. {homesec-1.1.0 → homesec-1.1.2}/.github/workflows/ci.yml +0 -0
  73. {homesec-1.1.0 → homesec-1.1.2}/.github/workflows/validate-pr-title.yaml +0 -0
  74. {homesec-1.1.0 → homesec-1.1.2}/Dockerfile +0 -0
  75. {homesec-1.1.0 → homesec-1.1.2}/LICENSE +0 -0
  76. {homesec-1.1.0 → homesec-1.1.2}/Makefile +0 -0
  77. {homesec-1.1.0 → homesec-1.1.2}/README.md +0 -0
  78. {homesec-1.1.0 → homesec-1.1.2}/TESTING.md +0 -0
  79. {homesec-1.1.0 → homesec-1.1.2}/alembic/env.py +0 -0
  80. {homesec-1.1.0 → homesec-1.1.2}/alembic/script.py.mako +0 -0
  81. {homesec-1.1.0 → homesec-1.1.2}/alembic/versions/e6f25df0df90_initial.py +0 -0
  82. {homesec-1.1.0 → homesec-1.1.2}/alembic.ini +0 -0
  83. {homesec-1.1.0 → homesec-1.1.2}/config/example.yaml +0 -0
  84. {homesec-1.1.0 → homesec-1.1.2}/docker-compose.yml +0 -0
  85. {homesec-1.1.0 → homesec-1.1.2}/docker-entrypoint.sh +0 -0
  86. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/__init__.py +1 -1
  87. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/config/__init__.py +0 -0
  88. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/config/loader.py +0 -0
  89. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/errors.py +0 -0
  90. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/health/__init__.py +0 -0
  91. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/health/server.py +0 -0
  92. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/logging_setup.py +0 -0
  93. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/maintenance/__init__.py +0 -0
  94. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/models/storage.py +0 -0
  95. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/pipeline/__init__.py +0 -0
  96. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/pipeline/alert_policy.py +0 -0
  97. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/pipeline/core.py +0 -0
  98. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/plugins/notifiers/multiplex.py +0 -0
  99. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/py.typed +0 -0
  100. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/repository/__init__.py +0 -0
  101. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/sources/__init__.py +0 -0
  102. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/sources/ftp.py +0 -0
  103. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/sources/rtsp.py +0 -0
  104. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/state/__init__.py +0 -0
  105. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/storage_paths.py +0 -0
  106. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/telemetry/__init__.py +0 -0
  107. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/telemetry/db/__init__.py +0 -0
  108. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/telemetry/db/log_table.py +0 -0
  109. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/telemetry/db_log_handler.py +0 -0
  110. {homesec-1.1.0 → homesec-1.1.2}/src/homesec/telemetry/postgres_settings.py +0 -0
  111. {homesec-1.1.0 → homesec-1.1.2}/tests/__init__.py +0 -0
  112. {homesec-1.1.0 → homesec-1.1.2}/tests/conftest.py +0 -0
  113. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/__init__.py +0 -0
  114. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/conftest.py +0 -0
  115. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/mocks/__init__.py +0 -0
  116. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/mocks/filter.py +0 -0
  117. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/mocks/notifier.py +0 -0
  118. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/mocks/state_store.py +0 -0
  119. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/mocks/storage.py +0 -0
  120. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/mocks/vlm.py +0 -0
  121. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_clip_sources.py +0 -0
  122. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_dropbox_storage.py +0 -0
  123. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_event_store.py +0 -0
  124. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_ftp_source.py +0 -0
  125. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_health.py +0 -0
  126. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_local_storage.py +0 -0
  127. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_logging_setup.py +0 -0
  128. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_mqtt_notifier.py +0 -0
  129. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_notifiers.py +0 -0
  130. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_plugin_utils.py +0 -0
  131. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_rtsp_helpers.py +0 -0
  132. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_sendgrid_notifier.py +0 -0
  133. {homesec-1.1.0 → homesec-1.1.2}/tests/homesec/test_state_store.py +0 -0
  134. {homesec-1.1.0 → homesec-1.1.2}/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.GITHUB_TOKEN }}
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.GITHUB_TOKEN }}
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
@@ -9,3 +9,4 @@ video_cache
9
9
  yolo_cache
10
10
  ftp_incoming
11
11
  config/config.yaml
12
+ .coverage
@@ -1,6 +1,6 @@
1
1
  # HomeSec Development Guidelines
2
2
 
3
- **Last reviewed:** 2025-01-11
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 entry point discovery for all plugin types. Allows users to add custom plugins without modifying core code.
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: Entry Point Discovery**
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(name: str, config_model: type, factory: Callable):
254
- NOTIFIER_REGISTRY[name] = NotifierPlugin(name, config_model, factory)
255
-
256
- def discover_notifiers(import_paths: list[str]):
257
- # Discover from pyproject.toml entry points
258
- for ep in entry_points(group="homesec.notifiers"):
259
- register_fn = ep.load()
260
- register_fn()
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
- # In custom plugin package:
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
- # In user's pyproject.toml:
268
- # [project.entry-points."homesec.notifiers"]
269
- # mqtt = "homesec_mqtt:register"
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
- **Note:** All plugin types (filters, VLMs, storage, notifiers, alert policies) use this unified pattern. For shared utilities, see `src/homesec/plugins/utils.py` which provides `iter_entry_points()` and `load_plugin_from_entry_point()` helpers.
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
- **Reference:** See any plugin `__init__.py` file for complete implementations: `src/homesec/plugins/filters/__init__.py`, `src/homesec/plugins/analyzers/__init__.py`, `src/homesec/plugins/storage/__init__.py`, `src/homesec/plugins/notifiers/__init__.py`, or `src/homesec/plugins/alert_policies/__init__.py`.
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,38 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.1.2 (2026-01-19)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Use PAT token for release workflow to bypass branch protection
10
+ ([#13](https://github.com/lan17/homesec/pull/13),
11
+ [`511ac6d`](https://github.com/lan17/homesec/commit/511ac6d8899365d324ede41aefdbe8fab910f1cb))
12
+
13
+ ### Refactoring
14
+
15
+ - Complete plugin architecture standardization ([#10](https://github.com/lan17/homesec/pull/10),
16
+ [`4e5b85f`](https://github.com/lan17/homesec/commit/4e5b85fd9a32d4f71c4365222a735c2e01eba583))
17
+
18
+
19
+ ## v1.1.1 (2026-01-16)
20
+
21
+ ### Bug Fixes
22
+
23
+ - Improve CLI help output to show available commands ([#8](https://github.com/lan17/homesec/pull/8),
24
+ [`4c8342f`](https://github.com/lan17/homesec/commit/4c8342f71274f2110231f112b77d68c8bb119c17))
25
+
26
+ ### Chores
27
+
28
+ - Sync uv.lock version ([#8](https://github.com/lan17/homesec/pull/8),
29
+ [`4c8342f`](https://github.com/lan17/homesec/commit/4c8342f71274f2110231f112b77d68c8bb119c17))
30
+
31
+ ### Refactoring
32
+
33
+ - Use Fire's native help output for CLI commands ([#8](https://github.com/lan17/homesec/pull/8),
34
+ [`4c8342f`](https://github.com/lan17/homesec/commit/4c8342f71274f2110231f112b77d68c8bb119c17))
35
+
36
+
5
37
  ## v1.1.0 (2026-01-12)
6
38
 
7
39
  ### 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/motion_recorder.py` (OpenCV motion detection + ffmpeg recording; optional Dropbox upload via `src/dropbox_uploader.py`).
51
- - Object detection plugin (reference implementation): `src/human_filter/human_filter.py` (YOLOv8 sampling to detect people/animals).
52
- - VLM plugin (reference implementation): `src/vlm.py` (structured output with `risk_level`, `activity_type`, timeline, and `alert_threshold`).
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.max_workers if managing worker pool
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 (Proposed)
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.1.0
3
+ Version: 1.1.2
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