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
|
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, TypeVar
|
|
|
10
10
|
|
|
11
11
|
from homesec.models.clip import Clip, ClipStateData
|
|
12
12
|
from homesec.models.config import RetryConfig
|
|
13
|
+
from homesec.models.enums import ClipStatus, RiskLevelField
|
|
13
14
|
from homesec.models.events import (
|
|
14
15
|
AlertDecisionMadeEvent,
|
|
15
16
|
ClipDeletedEvent,
|
|
@@ -63,7 +64,7 @@ class ClipRepository:
|
|
|
63
64
|
"""Create initial state + record clip received event."""
|
|
64
65
|
state = ClipStateData(
|
|
65
66
|
camera_name=clip.camera_name,
|
|
66
|
-
status=
|
|
67
|
+
status=ClipStatus.QUEUED_LOCAL,
|
|
67
68
|
local_path=str(clip.local_path),
|
|
68
69
|
)
|
|
69
70
|
|
|
@@ -105,8 +106,13 @@ class ClipRepository:
|
|
|
105
106
|
|
|
106
107
|
state.storage_uri = storage_uri
|
|
107
108
|
state.view_url = view_url
|
|
108
|
-
if state.status not in (
|
|
109
|
-
|
|
109
|
+
if state.status not in (
|
|
110
|
+
ClipStatus.ANALYZED,
|
|
111
|
+
ClipStatus.DONE,
|
|
112
|
+
ClipStatus.ERROR,
|
|
113
|
+
ClipStatus.DELETED,
|
|
114
|
+
):
|
|
115
|
+
state.status = ClipStatus.UPLOADED
|
|
110
116
|
|
|
111
117
|
event = UploadCompletedEvent(
|
|
112
118
|
clip_id=clip_id,
|
|
@@ -208,8 +214,8 @@ class ClipRepository:
|
|
|
208
214
|
if state is None:
|
|
209
215
|
return None
|
|
210
216
|
|
|
211
|
-
if state.status !=
|
|
212
|
-
state.status =
|
|
217
|
+
if state.status != ClipStatus.DELETED:
|
|
218
|
+
state.status = ClipStatus.ERROR
|
|
213
219
|
await self._safe_upsert(clip_id, state)
|
|
214
220
|
return state
|
|
215
221
|
|
|
@@ -238,8 +244,8 @@ class ClipRepository:
|
|
|
238
244
|
return None
|
|
239
245
|
|
|
240
246
|
state.analysis_result = result
|
|
241
|
-
if state.status !=
|
|
242
|
-
state.status =
|
|
247
|
+
if state.status != ClipStatus.DELETED:
|
|
248
|
+
state.status = ClipStatus.ANALYZED
|
|
243
249
|
|
|
244
250
|
event = VLMCompletedEvent(
|
|
245
251
|
clip_id=clip_id,
|
|
@@ -294,7 +300,7 @@ class ClipRepository:
|
|
|
294
300
|
clip_id: str,
|
|
295
301
|
decision: AlertDecision,
|
|
296
302
|
detected_classes: list[str] | None,
|
|
297
|
-
vlm_risk:
|
|
303
|
+
vlm_risk: RiskLevelField | None,
|
|
298
304
|
) -> ClipStateData | None:
|
|
299
305
|
"""Record alert decision + update state."""
|
|
300
306
|
state = await self._load_state(clip_id, action="alert decision")
|
|
@@ -328,8 +334,8 @@ class ClipRepository:
|
|
|
328
334
|
if state is None:
|
|
329
335
|
return None
|
|
330
336
|
|
|
331
|
-
if state.status !=
|
|
332
|
-
state.status =
|
|
337
|
+
if state.status != ClipStatus.DELETED:
|
|
338
|
+
state.status = ClipStatus.DONE
|
|
333
339
|
|
|
334
340
|
event = NotificationSentEvent(
|
|
335
341
|
clip_id=clip_id,
|
|
@@ -380,7 +386,7 @@ class ClipRepository:
|
|
|
380
386
|
if state is None:
|
|
381
387
|
return None
|
|
382
388
|
|
|
383
|
-
state.status =
|
|
389
|
+
state.status = ClipStatus.DELETED
|
|
384
390
|
|
|
385
391
|
event = ClipDeletedEvent(
|
|
386
392
|
clip_id=clip_id,
|
|
@@ -411,7 +417,7 @@ class ClipRepository:
|
|
|
411
417
|
state = await self._load_state(clip_id, action="clip recheck")
|
|
412
418
|
if state is None:
|
|
413
419
|
return None
|
|
414
|
-
if state.status ==
|
|
420
|
+
if state.status == ClipStatus.DELETED:
|
|
415
421
|
return state
|
|
416
422
|
|
|
417
423
|
state.filter_result = result
|
|
@@ -464,10 +470,10 @@ class ClipRepository:
|
|
|
464
470
|
if state is None:
|
|
465
471
|
return None
|
|
466
472
|
|
|
467
|
-
if state.status in (
|
|
473
|
+
if state.status in (ClipStatus.DONE, ClipStatus.DELETED):
|
|
468
474
|
return state
|
|
469
475
|
|
|
470
|
-
state.status =
|
|
476
|
+
state.status = ClipStatus.DONE
|
|
471
477
|
await self._safe_upsert(clip_id, state)
|
|
472
478
|
return state
|
|
473
479
|
|
homesec/sources/base.py
CHANGED
|
@@ -23,6 +23,7 @@ class ThreadedClipSource(ClipSource, ABC):
|
|
|
23
23
|
self._thread: Thread | None = None
|
|
24
24
|
self._stop_event = Event()
|
|
25
25
|
self._last_heartbeat = time.monotonic()
|
|
26
|
+
self._started = False
|
|
26
27
|
|
|
27
28
|
def register_callback(self, callback: Callable[[Clip], None]) -> None:
|
|
28
29
|
"""Register callback to be invoked when a new clip is ready."""
|
|
@@ -34,6 +35,7 @@ class ThreadedClipSource(ClipSource, ABC):
|
|
|
34
35
|
logger.warning("%s already started", self.__class__.__name__)
|
|
35
36
|
return
|
|
36
37
|
|
|
38
|
+
self._started = True
|
|
37
39
|
self._stop_event.clear()
|
|
38
40
|
self._on_start()
|
|
39
41
|
self._thread = Thread(target=self._run_wrapper, daemon=True)
|
|
@@ -68,12 +70,21 @@ class ThreadedClipSource(ClipSource, ABC):
|
|
|
68
70
|
"""Return monotonic timestamp of last heartbeat update."""
|
|
69
71
|
return self._last_heartbeat
|
|
70
72
|
|
|
73
|
+
async def ping(self) -> bool:
|
|
74
|
+
"""Health check - verify source is operational.
|
|
75
|
+
|
|
76
|
+
Returns True if:
|
|
77
|
+
- Source not started yet (ready to start)
|
|
78
|
+
- Background thread is alive
|
|
79
|
+
"""
|
|
80
|
+
return self._thread_is_healthy()
|
|
81
|
+
|
|
71
82
|
def _touch_heartbeat(self) -> None:
|
|
72
83
|
self._last_heartbeat = time.monotonic()
|
|
73
84
|
|
|
74
85
|
def _thread_is_healthy(self) -> bool:
|
|
75
86
|
if self._thread is None:
|
|
76
|
-
return
|
|
87
|
+
return not self._started
|
|
77
88
|
return self._thread.is_alive()
|
|
78
89
|
|
|
79
90
|
def _emit_clip(self, clip: Clip) -> None:
|
|
@@ -126,6 +137,7 @@ class AsyncClipSource(ClipSource, ABC):
|
|
|
126
137
|
self._task: asyncio.Task[None] | None = None
|
|
127
138
|
self._stop_event = asyncio.Event()
|
|
128
139
|
self._last_heartbeat = time.monotonic()
|
|
140
|
+
self._started = False
|
|
129
141
|
|
|
130
142
|
def register_callback(self, callback: Callable[[Clip], None]) -> None:
|
|
131
143
|
"""Register callback to be invoked when a new clip is ready."""
|
|
@@ -137,6 +149,7 @@ class AsyncClipSource(ClipSource, ABC):
|
|
|
137
149
|
logger.warning("%s already started", self.__class__.__name__)
|
|
138
150
|
return
|
|
139
151
|
|
|
152
|
+
self._started = True
|
|
140
153
|
self._stop_event.clear()
|
|
141
154
|
self._on_start()
|
|
142
155
|
self._task = asyncio.create_task(self._run_wrapper())
|
|
@@ -174,12 +187,21 @@ class AsyncClipSource(ClipSource, ABC):
|
|
|
174
187
|
"""Return timestamp (monotonic) of last successful operation."""
|
|
175
188
|
return self._last_heartbeat
|
|
176
189
|
|
|
190
|
+
async def ping(self) -> bool:
|
|
191
|
+
"""Health check - verify source is operational.
|
|
192
|
+
|
|
193
|
+
Returns True if:
|
|
194
|
+
- Source not started yet (ready to start)
|
|
195
|
+
- Background task is running
|
|
196
|
+
"""
|
|
197
|
+
return self._task_is_healthy()
|
|
198
|
+
|
|
177
199
|
def _touch_heartbeat(self) -> None:
|
|
178
200
|
self._last_heartbeat = time.monotonic()
|
|
179
201
|
|
|
180
202
|
def _task_is_healthy(self) -> bool:
|
|
181
203
|
if self._task is None:
|
|
182
|
-
return
|
|
204
|
+
return not self._started
|
|
183
205
|
return not self._task.done()
|
|
184
206
|
|
|
185
207
|
def _emit_clip(self, clip: Clip) -> None:
|
homesec/sources/local_folder.py
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import json
|
|
6
7
|
import logging
|
|
7
8
|
import time
|
|
8
9
|
from collections import OrderedDict
|
|
9
10
|
from collections.abc import Callable
|
|
10
11
|
from datetime import datetime, timedelta
|
|
11
12
|
from pathlib import Path
|
|
12
|
-
from typing import TYPE_CHECKING
|
|
13
13
|
|
|
14
14
|
from anyio import Path as AsyncPath
|
|
15
15
|
|
|
@@ -17,9 +17,6 @@ from homesec.models.clip import Clip
|
|
|
17
17
|
from homesec.models.source import LocalFolderSourceConfig
|
|
18
18
|
from homesec.sources.base import AsyncClipSource
|
|
19
19
|
|
|
20
|
-
if TYPE_CHECKING:
|
|
21
|
-
from homesec.interfaces import StateStore
|
|
22
|
-
|
|
23
20
|
logger = logging.getLogger(__name__)
|
|
24
21
|
|
|
25
22
|
|
|
@@ -34,38 +31,38 @@ class LocalFolderSource(AsyncClipSource):
|
|
|
34
31
|
def __init__(
|
|
35
32
|
self,
|
|
36
33
|
config: LocalFolderSourceConfig,
|
|
37
|
-
camera_name: str =
|
|
38
|
-
state_store: StateStore | None = None,
|
|
34
|
+
camera_name: str | None = None,
|
|
39
35
|
) -> None:
|
|
40
36
|
"""Initialize folder watcher.
|
|
41
37
|
|
|
42
38
|
Args:
|
|
43
39
|
config: LocalFolder source configuration
|
|
44
|
-
camera_name: Name of the camera (
|
|
45
|
-
state_store: Optional state store for deduplication via clip_states table.
|
|
46
|
-
If None, falls back to in-memory cache only (may reprocess files after restart).
|
|
40
|
+
camera_name: Name of the camera (overrides config.camera_name).
|
|
47
41
|
"""
|
|
48
42
|
super().__init__()
|
|
49
43
|
self.watch_dir = Path(config.watch_dir)
|
|
50
|
-
|
|
44
|
+
# Use config's camera_name if not explicitly passed, else default to "local"
|
|
45
|
+
self.camera_name = camera_name or config.camera_name or "local"
|
|
51
46
|
self.poll_interval = float(config.poll_interval)
|
|
52
47
|
self.stability_threshold_s = float(config.stability_threshold_s)
|
|
53
|
-
self._state_store = state_store
|
|
54
48
|
|
|
55
49
|
# Ensure watch dir exists
|
|
56
50
|
self.watch_dir.mkdir(parents=True, exist_ok=True)
|
|
57
51
|
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
# Local State Manifest (replaces StateStore dependency)
|
|
53
|
+
# This file tracks which clips have been processed to avoid re-emitting on restart.
|
|
54
|
+
self._state_file = self.watch_dir / ".homesec_state.json"
|
|
55
|
+
self._processed_files: set[str] = set()
|
|
56
|
+
self._load_local_state()
|
|
57
|
+
|
|
58
|
+
# Bounded in-memory cache for performance (avoid checking set on every scan)
|
|
62
59
|
self._seen_files: OrderedDict[str, None] = OrderedDict()
|
|
63
60
|
self._max_seen_files = 10000
|
|
64
61
|
|
|
65
62
|
logger.info(
|
|
66
|
-
"LocalFolderSource initialized: watch_dir=%s,
|
|
63
|
+
"LocalFolderSource initialized: watch_dir=%s, camera_name=%s",
|
|
67
64
|
self.watch_dir,
|
|
68
|
-
|
|
65
|
+
self.camera_name,
|
|
69
66
|
)
|
|
70
67
|
|
|
71
68
|
def register_callback(self, callback: Callable[[Clip], None]) -> None:
|
|
@@ -74,12 +71,7 @@ class LocalFolderSource(AsyncClipSource):
|
|
|
74
71
|
logger.debug("Callback registered for %s", self.camera_name)
|
|
75
72
|
|
|
76
73
|
def is_healthy(self) -> bool:
|
|
77
|
-
"""Check if source is healthy.
|
|
78
|
-
|
|
79
|
-
Returns True if:
|
|
80
|
-
- Watch directory exists and is readable
|
|
81
|
-
- Watch task is alive (if started)
|
|
82
|
-
"""
|
|
74
|
+
"""Check if source is healthy."""
|
|
83
75
|
if not self.watch_dir.exists():
|
|
84
76
|
return False
|
|
85
77
|
|
|
@@ -101,12 +93,34 @@ class LocalFolderSource(AsyncClipSource):
|
|
|
101
93
|
def _on_stopped(self) -> None:
|
|
102
94
|
logger.info("LocalFolderSource stopped")
|
|
103
95
|
|
|
104
|
-
|
|
105
|
-
"""
|
|
96
|
+
def _load_local_state(self) -> None:
|
|
97
|
+
"""Load processed files from local JSON manifest."""
|
|
98
|
+
if not self._state_file.exists():
|
|
99
|
+
return
|
|
100
|
+
try:
|
|
101
|
+
with open(self._state_file) as f:
|
|
102
|
+
data = json.load(f)
|
|
103
|
+
if isinstance(data, list):
|
|
104
|
+
self._processed_files = set(data)
|
|
105
|
+
elif isinstance(data, dict) and "processed_files" in data:
|
|
106
|
+
self._processed_files = set(data["processed_files"])
|
|
107
|
+
logger.info("Loaded %d processed files from local state", len(self._processed_files))
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.warning("Failed to load local state file: %s", e)
|
|
106
110
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
def _save_local_state(self) -> None:
|
|
112
|
+
"""Save processed files to local JSON manifest."""
|
|
113
|
+
# Simple atomic write
|
|
114
|
+
try:
|
|
115
|
+
temp_file = self._state_file.with_suffix(".tmp")
|
|
116
|
+
with open(temp_file, "w") as f:
|
|
117
|
+
json.dump({"processed_files": list(self._processed_files)}, f)
|
|
118
|
+
temp_file.replace(self._state_file)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.warning("Failed to save local state file: %s", e)
|
|
121
|
+
|
|
122
|
+
async def _run(self) -> None:
|
|
123
|
+
"""Background task that polls for new files."""
|
|
110
124
|
logger.info("Watch loop started")
|
|
111
125
|
|
|
112
126
|
# Create async path wrapper for watch directory
|
|
@@ -117,13 +131,18 @@ class LocalFolderSource(AsyncClipSource):
|
|
|
117
131
|
# Update heartbeat
|
|
118
132
|
self._touch_heartbeat()
|
|
119
133
|
|
|
134
|
+
new_files_processed = False
|
|
135
|
+
|
|
120
136
|
# Scan for new .mp4 files (async to avoid blocking event loop)
|
|
121
137
|
async for async_file_path in async_watch_dir.glob("*.mp4"):
|
|
122
138
|
file_path = Path(async_file_path) # Convert to regular Path for Clip
|
|
123
139
|
file_id = str(file_path)
|
|
124
140
|
clip_id = file_path.stem
|
|
125
141
|
|
|
126
|
-
# Check
|
|
142
|
+
# Check if already processed (Session cache OR Persistent Manifest)
|
|
143
|
+
if clip_id in self._processed_files:
|
|
144
|
+
continue
|
|
145
|
+
|
|
127
146
|
if file_id in self._seen_files:
|
|
128
147
|
continue
|
|
129
148
|
|
|
@@ -144,16 +163,10 @@ class LocalFolderSource(AsyncClipSource):
|
|
|
144
163
|
logger.warning("Failed to stat file %s: %s", file_path, e, exc_info=True)
|
|
145
164
|
continue
|
|
146
165
|
|
|
147
|
-
#
|
|
148
|
-
|
|
149
|
-
if await self._has_clip_state(clip_id):
|
|
150
|
-
# File was already processed - add to cache and skip
|
|
151
|
-
self._add_to_cache(file_id)
|
|
152
|
-
logger.debug("Skipping already-processed file: %s", file_path.name)
|
|
153
|
-
continue
|
|
154
|
-
|
|
155
|
-
# Mark as seen in cache
|
|
166
|
+
# Mark as seen
|
|
167
|
+
self._processed_files.add(clip_id)
|
|
156
168
|
self._add_to_cache(file_id)
|
|
169
|
+
new_files_processed = True
|
|
157
170
|
|
|
158
171
|
# Create Clip object (reuse mtime from async stat to avoid blocking)
|
|
159
172
|
clip = self._create_clip(file_path, mtime=mtime)
|
|
@@ -162,6 +175,11 @@ class LocalFolderSource(AsyncClipSource):
|
|
|
162
175
|
logger.info("New clip detected: %s", file_path.name)
|
|
163
176
|
self._emit_clip(clip)
|
|
164
177
|
|
|
178
|
+
# Check for removed files to clean up manifest (OPTIONAL, maybe too expensive?)
|
|
179
|
+
# For now, let's just save if we added anything.
|
|
180
|
+
if new_files_processed:
|
|
181
|
+
await asyncio.to_thread(self._save_local_state)
|
|
182
|
+
|
|
165
183
|
except Exception as e:
|
|
166
184
|
logger.error("Error in watch loop: %s", e, exc_info=True)
|
|
167
185
|
|
|
@@ -183,50 +201,11 @@ class LocalFolderSource(AsyncClipSource):
|
|
|
183
201
|
self._seen_files.popitem(last=False)
|
|
184
202
|
logger.debug("Evicted %d old entries from seen files cache", evict_count)
|
|
185
203
|
|
|
186
|
-
async def _has_clip_state(self, clip_id: str) -> bool:
|
|
187
|
-
"""Check if clip_id exists in clip_states table.
|
|
188
|
-
|
|
189
|
-
Returns:
|
|
190
|
-
True if clip_state exists (any status including 'deleted'), False otherwise
|
|
191
|
-
"""
|
|
192
|
-
if self._state_store is None:
|
|
193
|
-
return False
|
|
194
|
-
|
|
195
|
-
try:
|
|
196
|
-
# Direct async DB access - no threading complexity!
|
|
197
|
-
state = await asyncio.wait_for(
|
|
198
|
-
self._state_store.get(clip_id),
|
|
199
|
-
timeout=5.0,
|
|
200
|
-
)
|
|
201
|
-
return state is not None
|
|
202
|
-
except asyncio.TimeoutError:
|
|
203
|
-
logger.warning(
|
|
204
|
-
"DB query timeout for clip_states check: %s (assuming not seen)",
|
|
205
|
-
clip_id,
|
|
206
|
-
)
|
|
207
|
-
return False
|
|
208
|
-
except Exception as e:
|
|
209
|
-
logger.warning(
|
|
210
|
-
"Error checking clip_states for %s: %s (assuming not seen)",
|
|
211
|
-
clip_id,
|
|
212
|
-
e,
|
|
213
|
-
exc_info=True,
|
|
214
|
-
)
|
|
215
|
-
return False
|
|
216
|
-
|
|
217
204
|
def _create_clip(self, file_path: Path, mtime: float) -> Clip:
|
|
218
|
-
"""Create Clip object from file path.
|
|
219
|
-
|
|
220
|
-
Args:
|
|
221
|
-
file_path: Path to the video file
|
|
222
|
-
mtime: File modification timestamp (from async stat)
|
|
223
|
-
|
|
224
|
-
Estimates timestamps based on file modification time.
|
|
225
|
-
"""
|
|
205
|
+
"""Create Clip object from file path."""
|
|
226
206
|
mtime_dt = datetime.fromtimestamp(mtime)
|
|
227
207
|
|
|
228
208
|
# Estimate clip duration (assume 10s if we can't determine)
|
|
229
|
-
# In production, would parse from filename or video metadata
|
|
230
209
|
duration_s = 10.0
|
|
231
210
|
|
|
232
211
|
return Clip(
|
homesec/state/postgres.py
CHANGED
|
@@ -18,6 +18,7 @@ from sqlalchemy import (
|
|
|
18
18
|
func,
|
|
19
19
|
or_,
|
|
20
20
|
select,
|
|
21
|
+
text,
|
|
21
22
|
)
|
|
22
23
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
23
24
|
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
|
@@ -27,6 +28,7 @@ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
|
27
28
|
|
|
28
29
|
from homesec.interfaces import EventStore, StateStore
|
|
29
30
|
from homesec.models.clip import ClipStateData
|
|
31
|
+
from homesec.models.enums import EventType
|
|
30
32
|
from homesec.models.events import (
|
|
31
33
|
AlertDecisionMadeEvent,
|
|
32
34
|
ClipDeletedEvent,
|
|
@@ -52,23 +54,25 @@ from homesec.models.events import (
|
|
|
52
54
|
|
|
53
55
|
logger = logging.getLogger(__name__)
|
|
54
56
|
|
|
57
|
+
# Map EventType enum to event model classes
|
|
58
|
+
# Using enum values ensures consistency with event models
|
|
55
59
|
_EVENT_TYPE_MAP: dict[str, type[ClipEventModel]] = {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
60
|
+
EventType.CLIP_RECORDED: ClipRecordedEvent,
|
|
61
|
+
EventType.CLIP_DELETED: ClipDeletedEvent,
|
|
62
|
+
EventType.CLIP_RECHECKED: ClipRecheckedEvent,
|
|
63
|
+
EventType.UPLOAD_STARTED: UploadStartedEvent,
|
|
64
|
+
EventType.UPLOAD_COMPLETED: UploadCompletedEvent,
|
|
65
|
+
EventType.UPLOAD_FAILED: UploadFailedEvent,
|
|
66
|
+
EventType.FILTER_STARTED: FilterStartedEvent,
|
|
67
|
+
EventType.FILTER_COMPLETED: FilterCompletedEvent,
|
|
68
|
+
EventType.FILTER_FAILED: FilterFailedEvent,
|
|
69
|
+
EventType.VLM_STARTED: VLMStartedEvent,
|
|
70
|
+
EventType.VLM_COMPLETED: VLMCompletedEvent,
|
|
71
|
+
EventType.VLM_FAILED: VLMFailedEvent,
|
|
72
|
+
EventType.VLM_SKIPPED: VLMSkippedEvent,
|
|
73
|
+
EventType.ALERT_DECISION_MADE: AlertDecisionMadeEvent,
|
|
74
|
+
EventType.NOTIFICATION_SENT: NotificationSentEvent,
|
|
75
|
+
EventType.NOTIFICATION_FAILED: NotificationFailedEvent,
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
|
|
@@ -338,7 +342,7 @@ class PostgresStateStore(StateStore):
|
|
|
338
342
|
"""Parse JSONB payload from SQLAlchemy into a dict."""
|
|
339
343
|
return _parse_jsonb_payload(raw)
|
|
340
344
|
|
|
341
|
-
def create_event_store(self) ->
|
|
345
|
+
def create_event_store(self) -> EventStore:
|
|
342
346
|
"""Create a Postgres-backed event store or a no-op fallback."""
|
|
343
347
|
if self._engine is None:
|
|
344
348
|
return NoopEventStore()
|
|
@@ -455,6 +459,19 @@ class PostgresEventStore(EventStore):
|
|
|
455
459
|
logger.error("Failed to get events for %s: %s", clip_id, e, exc_info=e)
|
|
456
460
|
return []
|
|
457
461
|
|
|
462
|
+
async def shutdown(self, timeout: float | None = None) -> None:
|
|
463
|
+
"""Shutdown is handled by PostgresStateStore which owns the engine."""
|
|
464
|
+
_ = timeout
|
|
465
|
+
|
|
466
|
+
async def ping(self) -> bool:
|
|
467
|
+
"""Health check - verify database is reachable."""
|
|
468
|
+
try:
|
|
469
|
+
async with self._engine.connect() as conn:
|
|
470
|
+
await conn.execute(text("SELECT 1"))
|
|
471
|
+
return True
|
|
472
|
+
except Exception:
|
|
473
|
+
return False
|
|
474
|
+
|
|
458
475
|
|
|
459
476
|
class NoopEventStore(EventStore):
|
|
460
477
|
"""Event store that drops events (used when Postgres is unavailable)."""
|
|
@@ -469,6 +486,14 @@ class NoopEventStore(EventStore):
|
|
|
469
486
|
) -> list[ClipLifecycleEvent]:
|
|
470
487
|
return []
|
|
471
488
|
|
|
489
|
+
async def shutdown(self, timeout: float | None = None) -> None:
|
|
490
|
+
"""No resources to clean up."""
|
|
491
|
+
_ = timeout
|
|
492
|
+
|
|
493
|
+
async def ping(self) -> bool:
|
|
494
|
+
"""Noop store is always 'unhealthy' - indicates no real backend."""
|
|
495
|
+
return False
|
|
496
|
+
|
|
472
497
|
|
|
473
498
|
class NoopStateStore(StateStore):
|
|
474
499
|
"""State store that drops writes and returns no data."""
|
|
@@ -498,3 +523,7 @@ class NoopStateStore(StateStore):
|
|
|
498
523
|
|
|
499
524
|
async def ping(self) -> bool:
|
|
500
525
|
return False
|
|
526
|
+
|
|
527
|
+
def create_event_store(self) -> EventStore:
|
|
528
|
+
"""Return NoopEventStore."""
|
|
529
|
+
return NoopEventStore()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: homesec
|
|
3
|
-
Version: 1.1.
|
|
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
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
homesec/__init__.py,sha256=i4wRlg0CV3bxbxAWL_uSqW39r_F2sTRBNbelmoYOPuA,452
|
|
2
|
+
homesec/app.py,sha256=-zGi1kwKUImu6e8Z6K_-SHyfiFBCwwpSaSBpCKvN_qU,11933
|
|
3
|
+
homesec/cli.py,sha256=rW5WR9dXqVONsUAB39ziDCaktZlDwOjKiGKPDno4gwU,5630
|
|
4
|
+
homesec/errors.py,sha256=fBW_OdnYgqtb6u6t0YZh4tHgsiv0Pb1DiF9G6b4Vcbk,2105
|
|
5
|
+
homesec/interfaces.py,sha256=b6BsYUQUelZv4yWHqm1XpUBzVQNn8tPbUFFCFLC4qoQ,9836
|
|
6
|
+
homesec/logging_setup.py,sha256=Z12nzCPK04cqFHfIQgasioGfXb4TLvXJT9stzD8Dh4c,5546
|
|
7
|
+
homesec/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
homesec/storage_paths.py,sha256=MGQNT_7mJS6wuTWUHZNl0hvRmSVRvNksLu_imfZOEpo,1696
|
|
9
|
+
homesec/config/__init__.py,sha256=CjvulJwzj4Rdivrxz7tuDlyfw0c2IhqnSeUcHeu7ldk,421
|
|
10
|
+
homesec/config/loader.py,sha256=7BDw4plVQUCo8SHexdNiwAPd9mR8eVQEKnwzlvQthYQ,3030
|
|
11
|
+
homesec/config/validation.py,sha256=K1jgaVb-gi_99rb-gobEJBmF2WSiZJQfVMGfjqjm3iw,4017
|
|
12
|
+
homesec/health/__init__.py,sha256=fbndfsLOR9aA7d_5I1mEZN0oM5IYMmcJNjOt0iaXKZc,107
|
|
13
|
+
homesec/health/server.py,sha256=VP-4XmZ0K3ooFyd000AFOZohZ5R7QcnZC4n7oj0RMqI,7014
|
|
14
|
+
homesec/maintenance/__init__.py,sha256=6a5W2x8oUgnoWaK374-Wq_nrOD5UDAUqUtSANaEck2M,60
|
|
15
|
+
homesec/maintenance/cleanup_clips.py,sha256=sqeWLE9GrJfe29ntCasgvT2M-FWTymydHDQG-fZ3khg,23451
|
|
16
|
+
homesec/models/__init__.py,sha256=IVmkIEgTe1SvOzXilWw6BwSRQezAeBbqAt5y6EdZV5o,1955
|
|
17
|
+
homesec/models/alert.py,sha256=cMFr4NUGygq0-m2ep0jhjzVrFKM7kN1wJON1J0XwGY4,1010
|
|
18
|
+
homesec/models/clip.py,sha256=QaftnY7q20fQX4BTX6j-FBZLaoVSgk0DbpNf8kdnT60,2225
|
|
19
|
+
homesec/models/config.py,sha256=Z70xKrbDHi96j8IoRXPBw7DYAI7sjHR4M6nqReHTNds,14184
|
|
20
|
+
homesec/models/enums.py,sha256=WQk1PvYnJd5iaz51P5GM3XayqCf0VpQnLYmSuw1hrZk,3293
|
|
21
|
+
homesec/models/events.py,sha256=sgPDCSp9w60VUKKREYxNBZaxrFWW_bYyLwMFGHBawjk,4942
|
|
22
|
+
homesec/models/filter.py,sha256=6NS1rBI2zmrswK9NtMn0vZlbwbeMGCPuLCDKP9XWN0I,2259
|
|
23
|
+
homesec/models/source.py,sha256=PDtANsPaVzLz_VvR3cFVkQcfvema-7TrcuTY8a-gVq0,2309
|
|
24
|
+
homesec/models/storage.py,sha256=63wyHdDt3QrfdsP0SmhrxtOeWRllZ1O2GPrA4jI7XmU,235
|
|
25
|
+
homesec/models/vlm.py,sha256=Uk6TPwqbKxzyAsOlBSzZru74nKjp2-LLyzIp5b3wM_c,3293
|
|
26
|
+
homesec/pipeline/__init__.py,sha256=kiQLECc6JIPmeIdBJrVpTApPs0GBAgWoZ1kU4XZyJVY,214
|
|
27
|
+
homesec/pipeline/alert_policy.py,sha256=gFl5SJ96fgEfEUnhSL51YA6O2GPGXTXmxaDC-q3h1rs,152
|
|
28
|
+
homesec/pipeline/core.py,sha256=D552e-xpIpom5C-Y_TkWB9Ufbm8xClGmCdo_4tseXuU,23840
|
|
29
|
+
homesec/plugins/__init__.py,sha256=ex1AY_pJa9PZkr6yaneX3KgImZ-GkenTwExH2cMcF3A,1269
|
|
30
|
+
homesec/plugins/registry.py,sha256=zzpWlFO2PMmtAyVx0xwcQEVkTUoQ8K6fzL_nkMeN1xY,5551
|
|
31
|
+
homesec/plugins/utils.py,sha256=a87kRYBZPnaUuvn6kRdf6N38b8aAbcPW9r-30T2rlBI,1980
|
|
32
|
+
homesec/plugins/alert_policies/__init__.py,sha256=ApjWte2c8lG_ZFx6TxHTRUp_41Q1-e1mwPO3yY6rO1M,1502
|
|
33
|
+
homesec/plugins/alert_policies/default.py,sha256=bB6DseIEXaiOlV5Y9VlTn18GTG9yaTEwPY83gHykvto,3266
|
|
34
|
+
homesec/plugins/alert_policies/noop.py,sha256=xcdyZAlQf6X_AqQWUPkRMHLK8J6d7PTXWoPbwpnSlQE,1262
|
|
35
|
+
homesec/plugins/analyzers/__init__.py,sha256=GLC01lACmqZjKjMbkUnB_yYnOTxW_XEOwOfHK-I9SX0,915
|
|
36
|
+
homesec/plugins/analyzers/openai.py,sha256=qLN9Qhj-DvqEE6nHXIfW0QPp2EhSVwIVVXzq-QSqdzQ,15979
|
|
37
|
+
homesec/plugins/filters/__init__.py,sha256=JgPJTcW8yKa6JRIED4w8o5j_n_k5J7xwQbhAqDYofA4,866
|
|
38
|
+
homesec/plugins/filters/yolo.py,sha256=gCuFHtqZsJVimaX1p7XvP9MTP9vliLiwniZWioAH5RA,11130
|
|
39
|
+
homesec/plugins/notifiers/__init__.py,sha256=6T3OX4Vg1XKZeSG6AlZzw0o2KlaRr_mePBDIZEbr5Rw,1066
|
|
40
|
+
homesec/plugins/notifiers/mqtt.py,sha256=1zUKUHFvT65ysawFXEwWHvY8rg310fRsSSKhIFeaUL4,5946
|
|
41
|
+
homesec/plugins/notifiers/multiplex.py,sha256=LlnwozjkMDQwz7__v7mT4AohZbiWZK39CZunamRp7FM,3676
|
|
42
|
+
homesec/plugins/notifiers/sendgrid_email.py,sha256=gZSv3FRaN8qCMO6D-MX8b6XVz-gSgrFhkFV6j1ILdi4,8682
|
|
43
|
+
homesec/plugins/sources/__init__.py,sha256=weLYuCLrmWIUvRTYmfgqVcOFHonZgTngDKFSks4yg8s,1025
|
|
44
|
+
homesec/plugins/sources/ftp.py,sha256=hdMcjC3lhCAdn9ZnuYNZbKbUWsXUOcql1r_OJXoCPso,849
|
|
45
|
+
homesec/plugins/sources/local_folder.py,sha256=nCAJr0l8FpUpI3amkOPDN69XkAYtxgez3utq1jNRdPQ,1214
|
|
46
|
+
homesec/plugins/sources/rtsp.py,sha256=PbSrDEcvbTPq6xnA6i87DLCEtxHWdMVBjd6QQm1DAsc,814
|
|
47
|
+
homesec/plugins/storage/__init__.py,sha256=oJPrjgpke7VK3MY8L-GGmDVB4eWx7IgXcmVQXMcMw7g,1235
|
|
48
|
+
homesec/plugins/storage/dropbox.py,sha256=AQkEFV4lXqS1pbVazNuskEaEpr5CvLkrYdUK8EE7eAM,9992
|
|
49
|
+
homesec/plugins/storage/local.py,sha256=CbuSaWEi9ft1zxOURcmOKzwyq0UXIyHqyHoQD6sxivI,3231
|
|
50
|
+
homesec/repository/__init__.py,sha256=6cye2uQIA2v6jeLk5D2S9y3rlkfzJH5GceqdOroF3hU,160
|
|
51
|
+
homesec/repository/clip_repository.py,sha256=nRcswsIX--Z9p7J33FsqNlDtMAmt438VUbfvZHX0FlY,17090
|
|
52
|
+
homesec/sources/__init__.py,sha256=wuCtiF44ceo7n3wJN51VHHcDavko3ubUDICtFbWmaRI,505
|
|
53
|
+
homesec/sources/base.py,sha256=dKTxJxcDwJtykWDN3WYzkW5mtkRqlOJxJLWcLy82_Zo,7582
|
|
54
|
+
homesec/sources/ftp.py,sha256=ynIPbgcbIi1jub8yr4H1259Y1HbNM42RFDBBivXD4mg,7308
|
|
55
|
+
homesec/sources/local_folder.py,sha256=eW7ghgRsqTnZ5ZMPbsXh9ntqfue1UeM29ZpvRvLthPA,8461
|
|
56
|
+
homesec/sources/rtsp.py,sha256=3TOFDfIqadH3-DepB1xgv6mtOd0h-FYppZXBXSTBBi0,47304
|
|
57
|
+
homesec/state/__init__.py,sha256=Evt1jqTebmpJD1NUzNh3vwt5pbjDlLjQ0DgMCSAZOuM,255
|
|
58
|
+
homesec/state/postgres.py,sha256=I-cXqW5cgz-hpaHc0JIv3DnIBTmGxE28P8ZxBAGabSw,17765
|
|
59
|
+
homesec/telemetry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
60
|
+
homesec/telemetry/db_log_handler.py,sha256=KM8g4kcOyPzFJbpGxpSzecx_hrEWY0YfpoIKygETy5k,7539
|
|
61
|
+
homesec/telemetry/postgres_settings.py,sha256=EVD2_oi_KReFJvQmXxW026aurl_YD-KexT7rkbGQPHc,1198
|
|
62
|
+
homesec/telemetry/db/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
63
|
+
homesec/telemetry/db/log_table.py,sha256=wcZLwRht7FMa0z2gf37f_RxdVTNIdDiK4i_N3c_ibwg,473
|
|
64
|
+
homesec-1.1.2.dist-info/METADATA,sha256=hdgJS5FnVtvipMsjSCaG_f9T2s_QLEdePmTDrcQF9yI,23274
|
|
65
|
+
homesec-1.1.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
66
|
+
homesec-1.1.2.dist-info/entry_points.txt,sha256=8ocCj_fP1qxIuL-DVDAUiaUbEdTMX_kg_BzVrJsbQYg,45
|
|
67
|
+
homesec-1.1.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
68
|
+
homesec-1.1.2.dist-info/RECORD,,
|
homesec-1.1.0.dist-info/RECORD
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
homesec/__init__.py,sha256=tO_V5DJgS44qHCIySDUFIK5-Rbgjh3MpUBHa2HhZiB4,452
|
|
2
|
-
homesec/app.py,sha256=ma1_wClIHAFyVB-dOzSlKvoWAWUhHvAPjZVARtqHTK0,14124
|
|
3
|
-
homesec/cli.py,sha256=S7-K7aAIkus4mWvhjPqcGURabNhDlL4237iDeLvPxBI,5608
|
|
4
|
-
homesec/errors.py,sha256=fBW_OdnYgqtb6u6t0YZh4tHgsiv0Pb1DiF9G6b4Vcbk,2105
|
|
5
|
-
homesec/interfaces.py,sha256=z_bL6zrow5dxXBQLyeCK4ED7HzDrGBHe83Mm5W_4_pk,8388
|
|
6
|
-
homesec/logging_setup.py,sha256=Z12nzCPK04cqFHfIQgasioGfXb4TLvXJT9stzD8Dh4c,5546
|
|
7
|
-
homesec/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
homesec/storage_paths.py,sha256=MGQNT_7mJS6wuTWUHZNl0hvRmSVRvNksLu_imfZOEpo,1696
|
|
9
|
-
homesec/config/__init__.py,sha256=CjvulJwzj4Rdivrxz7tuDlyfw0c2IhqnSeUcHeu7ldk,421
|
|
10
|
-
homesec/config/loader.py,sha256=7BDw4plVQUCo8SHexdNiwAPd9mR8eVQEKnwzlvQthYQ,3030
|
|
11
|
-
homesec/config/validation.py,sha256=eDMM8upz_ATVWPDBtXkhNQOJEvh1gPU1LzY0UlH---M,2792
|
|
12
|
-
homesec/health/__init__.py,sha256=fbndfsLOR9aA7d_5I1mEZN0oM5IYMmcJNjOt0iaXKZc,107
|
|
13
|
-
homesec/health/server.py,sha256=VP-4XmZ0K3ooFyd000AFOZohZ5R7QcnZC4n7oj0RMqI,7014
|
|
14
|
-
homesec/maintenance/__init__.py,sha256=6a5W2x8oUgnoWaK374-Wq_nrOD5UDAUqUtSANaEck2M,60
|
|
15
|
-
homesec/maintenance/cleanup_clips.py,sha256=KmpadZjcYjsq3q0xjgN307w6X5dxdvqa1ukUMn_8llg,23455
|
|
16
|
-
homesec/models/__init__.py,sha256=zcSyQR0Mq_YlIrUFG304pQ72the1vtNbkWhjNJsso50,1889
|
|
17
|
-
homesec/models/alert.py,sha256=lBPP7qQK97JN2RI5Wlpqk6JSeGQxRiiwxzEloNfyKOc,968
|
|
18
|
-
homesec/models/clip.py,sha256=srqSLT1YXSG9CSZscOQssMSyg40SGaoEjABg9FzOxFQ,2254
|
|
19
|
-
homesec/models/config.py,sha256=BHHxfWQ_RtcTkAyHFVTBybiwZSCHgZJ9cmR5veOt3BA,12234
|
|
20
|
-
homesec/models/events.py,sha256=WdfkQrVPpEbjvPEhPsMnQQe9TgVglMfo9Ctz3xUJmFc,4605
|
|
21
|
-
homesec/models/filter.py,sha256=3f0hhXA1qPw1IOCxUIq5tGEQpeUTwJrrPL7DowQhMRg,1974
|
|
22
|
-
homesec/models/source.py,sha256=eHaJV8gk559kWNy0mt4KMr_zpdIccMvF7x4t3PkotGs,2204
|
|
23
|
-
homesec/models/storage.py,sha256=63wyHdDt3QrfdsP0SmhrxtOeWRllZ1O2GPrA4jI7XmU,235
|
|
24
|
-
homesec/models/vlm.py,sha256=Wd_hxWyIAETLtOth0FJoRt-sY9NdCS89T-BV-8ZDcP4,2883
|
|
25
|
-
homesec/pipeline/__init__.py,sha256=kiQLECc6JIPmeIdBJrVpTApPs0GBAgWoZ1kU4XZyJVY,214
|
|
26
|
-
homesec/pipeline/alert_policy.py,sha256=gFl5SJ96fgEfEUnhSL51YA6O2GPGXTXmxaDC-q3h1rs,152
|
|
27
|
-
homesec/pipeline/core.py,sha256=D552e-xpIpom5C-Y_TkWB9Ufbm8xClGmCdo_4tseXuU,23840
|
|
28
|
-
homesec/plugins/__init__.py,sha256=RhsQXLV1TJHoPVmk4Ql_X3IPQhFam5WXJMXjGUoXLuE,2117
|
|
29
|
-
homesec/plugins/utils.py,sha256=Cj9bnL4ldlD8ryvqCwAhc-HBgYby3eugN49Ltycemy4,1778
|
|
30
|
-
homesec/plugins/alert_policies/__init__.py,sha256=hPM7bK1DW6jown-IMNm4TkW471TBqkd56kMHpliJjg0,1978
|
|
31
|
-
homesec/plugins/alert_policies/default.py,sha256=uWoxGGFDE-t6ykddFbkOAX_2Tizg30alrUYJ9muuA_E,3895
|
|
32
|
-
homesec/plugins/alert_policies/noop.py,sha256=s-pJMpq6ekC6dVlg_lY7pizT_RYqCrOOHjJU7qAt5A0,1627
|
|
33
|
-
homesec/plugins/analyzers/__init__.py,sha256=K2LqO9Isa9xbL_duwi59TGvRTD9i5L-yYcg1pEBiJ_o,3138
|
|
34
|
-
homesec/plugins/analyzers/openai.py,sha256=18UfBiki0Qq7UcyGwxBY1lBz541mYYy-itxBamz6rqU,15392
|
|
35
|
-
homesec/plugins/filters/__init__.py,sha256=mpn7l51fCU7r3y5GkB5krVf6dX66gJIFftvUWg0Zf4I,3165
|
|
36
|
-
homesec/plugins/filters/yolo.py,sha256=bcE6S9QFBF7ZIbF2G_vhKQuAcXpPBJrmmxQPC3r4YYY,9781
|
|
37
|
-
homesec/plugins/notifiers/__init__.py,sha256=PaKnY6Yvzk_ovK8mmJ_iqUtK_B-DeUpgtN480rBbxH0,1911
|
|
38
|
-
homesec/plugins/notifiers/mqtt.py,sha256=p-Q4dTvQFqjazX141_FHKjGtIWsuvUQsfq_DnwBF-pM,6131
|
|
39
|
-
homesec/plugins/notifiers/multiplex.py,sha256=LlnwozjkMDQwz7__v7mT4AohZbiWZK39CZunamRp7FM,3676
|
|
40
|
-
homesec/plugins/notifiers/sendgrid_email.py,sha256=UZ_e-K31yHebUsXRUsAqdllytZUp1vv594RhdI0jwhE,8462
|
|
41
|
-
homesec/plugins/storage/__init__.py,sha256=UzYg0TeQwSBwm-5EAZ6lRn3-fI-BYOBQWqTu2cUHVks,3026
|
|
42
|
-
homesec/plugins/storage/dropbox.py,sha256=Rafi4NTTJBvm82BId5CYSFXkIbvV4IDDFAJiLbQUlL0,9793
|
|
43
|
-
homesec/plugins/storage/local.py,sha256=lXaVQRUpiZMqtSGDLKb2IZUIela_ysqc3QbRx3lPBBI,3703
|
|
44
|
-
homesec/repository/__init__.py,sha256=6cye2uQIA2v6jeLk5D2S9y3rlkfzJH5GceqdOroF3hU,160
|
|
45
|
-
homesec/repository/clip_repository.py,sha256=qSkfVk2MIx9wOKskUWUNnYwO-w0showR4WPsxUdCijs,16807
|
|
46
|
-
homesec/sources/__init__.py,sha256=wuCtiF44ceo7n3wJN51VHHcDavko3ubUDICtFbWmaRI,505
|
|
47
|
-
homesec/sources/base.py,sha256=HZkGJkyUjRRkELZnpEoslUuV0du7S-uWPvNQhToxDWU,6926
|
|
48
|
-
homesec/sources/ftp.py,sha256=ynIPbgcbIi1jub8yr4H1259Y1HbNM42RFDBBivXD4mg,7308
|
|
49
|
-
homesec/sources/local_folder.py,sha256=-6CbnuLNOhVZx7wDoQUtGc_t6JtegLkIKbusvtI85NQ,8955
|
|
50
|
-
homesec/sources/rtsp.py,sha256=3TOFDfIqadH3-DepB1xgv6mtOd0h-FYppZXBXSTBBi0,47304
|
|
51
|
-
homesec/state/__init__.py,sha256=Evt1jqTebmpJD1NUzNh3vwt5pbjDlLjQ0DgMCSAZOuM,255
|
|
52
|
-
homesec/state/postgres.py,sha256=Yis8ip2Y9dNYyTr2WEBcZ51m0ECoHuOe_XWT83cYQaI,16670
|
|
53
|
-
homesec/telemetry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
54
|
-
homesec/telemetry/db_log_handler.py,sha256=KM8g4kcOyPzFJbpGxpSzecx_hrEWY0YfpoIKygETy5k,7539
|
|
55
|
-
homesec/telemetry/postgres_settings.py,sha256=EVD2_oi_KReFJvQmXxW026aurl_YD-KexT7rkbGQPHc,1198
|
|
56
|
-
homesec/telemetry/db/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
57
|
-
homesec/telemetry/db/log_table.py,sha256=wcZLwRht7FMa0z2gf37f_RxdVTNIdDiK4i_N3c_ibwg,473
|
|
58
|
-
homesec-1.1.0.dist-info/METADATA,sha256=6FTsc92YcuDR06LBKMelYBot24Ii4EAPdA5ddrcsJUA,23274
|
|
59
|
-
homesec-1.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
60
|
-
homesec-1.1.0.dist-info/entry_points.txt,sha256=8ocCj_fP1qxIuL-DVDAUiaUbEdTMX_kg_BzVrJsbQYg,45
|
|
61
|
-
homesec-1.1.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
62
|
-
homesec-1.1.0.dist-info/RECORD,,
|
|
File without changes
|