mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-24 15:35:46 +03:00
Miscellaneous fixes (#23651)
This commit is contained in:
@@ -105,6 +105,8 @@ class Dispatcher:
|
||||
self.web_push_client = next(
|
||||
(comm for comm in communicators if isinstance(comm, WebPushClient)), None
|
||||
)
|
||||
if self.web_push_client is not None:
|
||||
self.web_push_client.set_suspension_broadcaster(self.publish)
|
||||
|
||||
def _receive(self, topic: str, payload: Any) -> Any | None:
|
||||
"""Handle receiving of payload from communicators."""
|
||||
|
||||
@@ -55,6 +55,7 @@ class WebPushClient(Communicator):
|
||||
c.name: 0 # type: ignore[misc]
|
||||
for c in self.config.cameras.values()
|
||||
}
|
||||
self.suspension_broadcaster: Callable[[str, Any, bool], None] | None = None
|
||||
self.last_camera_notification_time: dict[str, float] = {
|
||||
c.name: 0 # type: ignore[misc]
|
||||
for c in self.config.cameras.values()
|
||||
@@ -66,6 +67,10 @@ class WebPushClient(Communicator):
|
||||
target=self._process_notifications, daemon=True
|
||||
)
|
||||
self.notification_thread.start()
|
||||
self.suspension_thread = threading.Thread(
|
||||
target=self._process_suspensions, daemon=True
|
||||
)
|
||||
self.suspension_thread.start()
|
||||
|
||||
if not self.config.notifications.email:
|
||||
logger.warning("Email must be provided for push notifications to be sent.")
|
||||
@@ -164,6 +169,27 @@ class WebPushClient(Communicator):
|
||||
def is_camera_suspended(self, camera: str) -> bool:
|
||||
return datetime.datetime.now().timestamp() <= self.suspended_cameras[camera]
|
||||
|
||||
def set_suspension_broadcaster(
|
||||
self, broadcaster: Callable[[str, Any, bool], None]
|
||||
) -> None:
|
||||
"""Register the callback used to broadcast suspension state changes."""
|
||||
self.suspension_broadcaster = broadcaster
|
||||
|
||||
def _process_suspensions(self) -> None:
|
||||
while not self.stop_event.wait(1):
|
||||
self._clear_expired_suspensions()
|
||||
|
||||
def _clear_expired_suspensions(self) -> None:
|
||||
"""Reset and broadcast cameras whose suspension window has elapsed."""
|
||||
now = datetime.datetime.now().timestamp()
|
||||
for camera, suspended_until in list(self.suspended_cameras.items()):
|
||||
if suspended_until and now > suspended_until:
|
||||
self.unsuspend_notifications(camera)
|
||||
if self.suspension_broadcaster is not None:
|
||||
self.suspension_broadcaster(
|
||||
f"{camera}/notifications/suspended", "0", True
|
||||
)
|
||||
|
||||
def publish(self, topic: str, payload: Any, retain: bool = False) -> None:
|
||||
"""Wrapper for publishing when client is in valid state."""
|
||||
# check for updated global config (notifications, auth)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Tests for notification suspension expiry handling."""
|
||||
|
||||
import datetime
|
||||
import unittest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from frigate.comms.webpush import WebPushClient
|
||||
|
||||
|
||||
class TestSuspensionExpiry(unittest.TestCase):
|
||||
def _make_client(self, suspended_cameras: dict[str, int]) -> WebPushClient:
|
||||
client = WebPushClient.__new__(WebPushClient)
|
||||
client.suspended_cameras = suspended_cameras
|
||||
client.suspension_broadcaster = MagicMock()
|
||||
return client
|
||||
|
||||
def test_clears_and_broadcasts_expired_suspension(self):
|
||||
now = datetime.datetime.now().timestamp()
|
||||
client = self._make_client({"front_door": int(now - 60)})
|
||||
|
||||
client._clear_expired_suspensions()
|
||||
|
||||
self.assertEqual(client.suspended_cameras["front_door"], 0)
|
||||
client.suspension_broadcaster.assert_called_once_with(
|
||||
"front_door/notifications/suspended", "0", True
|
||||
)
|
||||
|
||||
def test_leaves_active_suspension_untouched(self):
|
||||
now = datetime.datetime.now().timestamp()
|
||||
suspend_until = int(now + 3600)
|
||||
client = self._make_client({"front_door": suspend_until})
|
||||
|
||||
client._clear_expired_suspensions()
|
||||
|
||||
self.assertEqual(client.suspended_cameras["front_door"], suspend_until)
|
||||
client.suspension_broadcaster.assert_not_called()
|
||||
|
||||
def test_ignores_unsuspended_camera(self):
|
||||
client = self._make_client({"front_door": 0})
|
||||
|
||||
client._clear_expired_suspensions()
|
||||
|
||||
self.assertEqual(client.suspended_cameras["front_door"], 0)
|
||||
client.suspension_broadcaster.assert_not_called()
|
||||
|
||||
def test_only_expired_cameras_are_cleared(self):
|
||||
now = datetime.datetime.now().timestamp()
|
||||
active_until = int(now + 3600)
|
||||
client = self._make_client(
|
||||
{
|
||||
"expired": int(now - 5),
|
||||
"active": active_until,
|
||||
"idle": 0,
|
||||
}
|
||||
)
|
||||
|
||||
client._clear_expired_suspensions()
|
||||
|
||||
self.assertEqual(client.suspended_cameras["expired"], 0)
|
||||
self.assertEqual(client.suspended_cameras["active"], active_until)
|
||||
self.assertEqual(client.suspended_cameras["idle"], 0)
|
||||
client.suspension_broadcaster.assert_called_once_with(
|
||||
"expired/notifications/suspended", "0", True
|
||||
)
|
||||
Reference in New Issue
Block a user