Merge pull request #1 from ep1cman/main

Merge Main
This commit is contained in:
jimmydoh
2024-12-05 15:44:49 +10:00
committed by GitHub
6 changed files with 1127 additions and 966 deletions

2001
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,7 +30,7 @@ apprise = "^1.5.0"
expiring-dict = "^1.1.0"
async-lru = "^2.0.4"
aiolimiter = "^1.1.0"
uiprotect = "^5.4.0"
uiprotect = "^6.3.1"
[tool.poetry.group.dev]
optional = true

View File

@@ -10,7 +10,7 @@ from unifi_protect_backup import __version__
from unifi_protect_backup.unifi_protect_backup_core import UnifiProtectBackup
from unifi_protect_backup.utils import human_readable_to_float
DETECTION_TYPES = ["motion", "person", "vehicle", "ring"]
DETECTION_TYPES = ["motion", "person", "vehicle", "ring", "line"]
def _parse_detection_types(ctx, param, value):

View File

@@ -6,6 +6,7 @@ from time import sleep
from typing import List
from uiprotect.api import ProtectApiClient
from uiprotect.websocket import WebsocketState
from uiprotect.data.nvr import Event
from uiprotect.data.types import EventType
from uiprotect.data.websocket import WSAction, WSSubscriptionMessage
@@ -34,18 +35,16 @@ class EventListener:
self._event_queue: asyncio.Queue = event_queue
self._protect: ProtectApiClient = protect
self._unsub = None
self._unsub_websocketstate = None
self.detection_types: List[str] = detection_types
self.ignore_cameras: List[str] = ignore_cameras
async def start(self):
"""Main Loop."""
logger.debug("Subscribed to websocket")
self._unsub_websocket_state = self._protect.subscribe_websocket_state(self._websocket_state_callback)
self._unsub = self._protect.subscribe_websocket(self._websocket_callback)
while True:
await asyncio.sleep(60)
await self._check_websocket_and_reconnect()
def _websocket_callback(self, msg: WSSubscriptionMessage) -> None:
"""Callback for "EVENT" websocket messages.
@@ -63,7 +62,12 @@ class EventListener:
return
if "end" not in msg.changed_data:
return
if msg.new_obj.type not in [EventType.MOTION, EventType.SMART_DETECT, EventType.RING]:
if msg.new_obj.type not in [
EventType.MOTION,
EventType.SMART_DETECT,
EventType.RING,
EventType.SMART_DETECT_LINE,
]:
return
if msg.new_obj.type is EventType.MOTION and "motion" not in self.detection_types:
logger.extra_debug(f"Skipping unwanted motion detection event: {msg.new_obj.id}") # type: ignore
@@ -71,6 +75,9 @@ class EventListener:
if msg.new_obj.type is EventType.RING and "ring" not in self.detection_types:
logger.extra_debug(f"Skipping unwanted ring event: {msg.new_obj.id}") # type: ignore
return
if msg.new_obj.type is EventType.SMART_DETECT_LINE and "line" not in self.detection_types:
logger.extra_debug(f"Skipping unwanted line event: {msg.new_obj.id}") # type: ignore
return
elif msg.new_obj.type is EventType.SMART_DETECT:
for event_smart_detection_type in msg.new_obj.smart_detect_types:
if event_smart_detection_type not in self.detection_types:
@@ -94,37 +101,15 @@ class EventListener:
logger.debug(f"Adding event {msg.new_obj.id} to queue (Current download queue={self._event_queue.qsize()})")
async def _check_websocket_and_reconnect(self):
"""Checks for websocket disconnect and triggers a reconnect."""
logger.extra_debug("Checking the status of the websocket...")
if self._protect.check_ws():
logger.extra_debug("Websocket is connected.")
else:
self._protect.connect_event.clear()
logger.warning("Lost connection to Unifi Protect.")
def _websocket_state_callback(self, state: WebsocketState) -> None:
"""Callback for websocket state messages.
# Unsubscribe, close the session.
self._unsub()
await self._protect.close_session()
Flags the websocket for reconnection
while True:
logger.warning("Attempting reconnect...")
try:
# Start the uiprotect connection by calling `update`
await self._protect.close_session()
self._protect._bootstrap = None
await self._protect.update(force=True)
if self._protect.check_ws():
self._unsub = self._protect.subscribe_websocket(self._websocket_callback)
break
else:
logger.error("Unable to establish connection to Unifi Protect")
except Exception as e:
logger.error("Unexpected exception occurred while trying to reconnect:", exc_info=e)
# Back off for a little while
await asyncio.sleep(10)
self._protect.connect_event.set()
logger.info("Re-established connection to Unifi Protect and to the websocket.")
Args:
msg (WebsocketState): new state of the websocket
"""
if state == WebsocketState.DISCONNECTED:
logger.error("Unifi Protect Websocket lost connection. Reconnecting...")
elif state == WebsocketState.CONNECTED:
logger.info("Unifi Protect Websocket connection restored")

View File

@@ -65,16 +65,23 @@ class MissingEventChecker:
events_chunk = await self._protect.get_events(
start=start_time,
end=end_time,
types=[EventType.MOTION, EventType.SMART_DETECT, EventType.RING],
types=[
EventType.MOTION,
EventType.SMART_DETECT,
EventType.RING,
EventType.SMART_DETECT_LINE,
],
limit=chunk_size,
)
if not events_chunk:
break # There were no events to backup
assert events_chunk[-1].end is not None
start_time = events_chunk[-1].end
unifi_events = {event.id: event for event in events_chunk}
# Filter out on-going events
unifi_events = {event.id: event for event in events_chunk if event.end is not None}
# Next chunks start time should be the end of the oldest complete event in the current chunk
start_time = max([event.end for event in unifi_events.values()])
# Get list of events that have been backed up from the database
@@ -107,6 +114,8 @@ class MissingEventChecker:
return False
if event.type is EventType.RING and "ring" not in self.detection_types:
return False
if event.type is EventType.SMART_DETECT_LINE and "line" not in self.detection_types:
return False
elif event.type is EventType.SMART_DETECT:
for event_smart_detection_type in event.smart_detect_types:
if event_smart_detection_type not in self.detection_types:

View File

@@ -197,18 +197,22 @@ class UnifiProtectBackup:
# Start the uiprotect connection by calling `update`
logger.info("Connecting to Unifi Protect...")
for attempts in range(10):
delay = 5 # Start with a 5 second delay
max_delay = 3600 # 1 hour in seconds
for attempts in range(20):
try:
await self._protect.update()
break
except Exception as e:
logger.warning(
f"Failed to connect to UniFi Protect, retrying in {attempts}s...",
f"Failed to connect to UniFi Protect, retrying in {delay}s...",
exc_info=e,
)
await asyncio.sleep(attempts)
await asyncio.sleep(delay)
delay = min(max_delay, delay * 2) # Double the delay but do not exceed max_delay
else:
raise ConnectionError("Failed to connect to UniFi Protect after 10 attempts")
raise ConnectionError("Failed to connect to UniFi Protect after 20 attempts")
# Add a lock to the protect client that can be used to prevent code accessing the client when it has
# lost connection