Miscellaneous fixes (#23651)

This commit is contained in:
Josh Hawkins
2026-07-08 08:27:38 -05:00
committed by GitHub
parent c99d6b0dcf
commit e6cac50250
43 changed files with 448 additions and 318 deletions
+2
View File
@@ -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."""
+26
View File
@@ -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)
+64
View File
@@ -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
)