From 7fd81171f0b35bfe83086a5c28cc4e24db865034 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sat, 14 Nov 2026 08:34:17 -0600 Subject: [PATCH] Miscellaneous fixes (#24970) * analytics notifications flag and device grant warnings - report notifications as on when any camera enables them, not only the global setting - don't warn about device nodes the runtime users can already use, such as world-rw nodes in an unprivileged LXC * clarify which stream the All Cameras dashboard uses * add exception catchall for autotracking calibration * only default review api after to 24 hours when no bounds are given The review endpoint replaced a missing or zero `after` with 24 hours ago, so a `before`-only query older than 24 hours returned nothing and `after=0` couldn't mean no lower bound. The default now applies only when neither `before` nor `after` is sent, a `before`-only query has no lower bound, and both bounds use `is not None` checks. The Live dashboard alerts bar sends no bounds and keeps the 24 hour window. --- .../etc/s6-overlay/s6-rc.d/init-devices/run | 16 ++++++++++ docs/docs/configuration/live.md | 2 +- frigate/analytics/collectors/features.py | 3 +- frigate/api/review.py | 15 ++++++--- frigate/ptz/autotrack.py | 8 ++++- frigate/test/http_api/test_http_review.py | 31 +++++++++++++++++++ .../test_analytics_collectors_features.py | 11 +++++++ frigate/test/test_ptz_autotrack.py | 29 +++++++++++++++++ 8 files changed, 108 insertions(+), 7 deletions(-) diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/init-devices/run b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/init-devices/run index 81e47f973a..44f3b400ed 100755 --- a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/init-devices/run +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/init-devices/run @@ -57,6 +57,17 @@ done granted=0 failed=0 +reachable() { + local node="$1" user + local access=(-r "$node" -a -w "$node") + if [[ -d "$node" ]]; then + access=(-r "$node" -a -x "$node") + fi + for user in frigate go2rtc; do + s6-setuidgid "$user" test "${access[@]}" || return 1 + done +} + grant() { local node="$1" # nullglob only drops patterns that hold a metacharacter, so a literal @@ -72,6 +83,11 @@ grant() { fi if setfacl -m "$spec" "$node" 2>/dev/null; then granted=$((granted + 1)) + + # an unprivileged LXC can't set ACLs on its idmapped nodes, which are often + # already world-rw + elif reachable "$node"; then + return 0 else failed=$((failed + 1)) echo "[WARN] could not grant device access on ${node}; see EXTRA_GROUPS in the non-root docs for the fallback" diff --git a/docs/docs/configuration/live.md b/docs/docs/configuration/live.md index cf8bc42f5e..074e5a81f0 100644 --- a/docs/docs/configuration/live.md +++ b/docs/docs/configuration/live.md @@ -98,7 +98,7 @@ Additionally, when creating and editing camera groups in the UI, you can choose :::note -Frigate's default dashboard ("All Cameras") will always use the first entry you've defined in streams when playing live streams from your cameras. +Frigate's default dashboard ("All Cameras") always uses the first entry in `streams`. That should be your highest quality stream, because Auto starts on the first stream and only steps down from there. To show a lower quality stream on a dashboard, create a camera group and choose the stream in each camera's [streaming settings](#streaming-options-on-camera-group-dashboards) for that group. ::: diff --git a/frigate/analytics/collectors/features.py b/frigate/analytics/collectors/features.py index 1eca92af6f..058e6a320d 100644 --- a/frigate/analytics/collectors/features.py +++ b/frigate/analytics/collectors/features.py @@ -139,7 +139,8 @@ def collect(ctx: ReportContext) -> FeaturesSection: restream=config.birdseye.restream, ), mqtt=config.mqtt.enabled, - notifications=config.notifications.enabled, + notifications=config.notifications.enabled + or any(camera.notifications.enabled for camera in cameras), auth=config.auth.enabled, proxy_auth=config.proxy.header_map.user is not None, tls=config.tls.enabled, diff --git a/frigate/api/review.py b/frigate/api/review.py index 0a0c758436..25b9070a62 100644 --- a/frigate/api/review.py +++ b/frigate/api/review.py @@ -82,12 +82,19 @@ async def review( reviewed = params.reviewed limit = params.limit severity = params.severity - before = params.before or datetime.datetime.now().timestamp() - after = ( - params.after - or (datetime.datetime.now() - datetime.timedelta(hours=24)).timestamp() + before = ( + params.before + if params.before is not None + else datetime.datetime.now().timestamp() ) + if params.after is not None: + after = params.after + elif params.before is not None: + after = 0 + else: + after = (datetime.datetime.now() - datetime.timedelta(hours=24)).timestamp() + clauses = [ (ReviewSegment.start_time < before) & ((ReviewSegment.end_time.is_null(True)) | (ReviewSegment.end_time > after)) diff --git a/frigate/ptz/autotrack.py b/frigate/ptz/autotrack.py index cd0edec684..94f3860bb7 100644 --- a/frigate/ptz/autotrack.py +++ b/frigate/ptz/autotrack.py @@ -318,7 +318,13 @@ class PtzAutoTracker(threading.Thread): ) = map(float, camera_config.onvif.autotracking.movement_weights) if startup and camera_config.onvif.autotracking.calibrate_on_startup: - await self._calibrate_camera(camera) + try: + await self._calibrate_camera(camera) + except Exception: + logger.exception(f"{camera}: Autotracker calibration failed") + self.calibrating[camera] = False + self._disable(camera, "calibration failed") + return self.dispatcher.publish(f"{camera}/ptz_autotracker/active", "OFF", retain=False) self.autotracker_init[camera] = True diff --git a/frigate/test/http_api/test_http_review.py b/frigate/test/http_api/test_http_review.py index 54bcee8172..8f24ba24d4 100644 --- a/frigate/test/http_api/test_http_review.py +++ b/frigate/test/http_api/test_http_review.py @@ -89,6 +89,37 @@ class TestHttpReview(BaseTestHttp): assert response_json[0]["id"] == id assert response_json[0]["has_been_reviewed"] == False + def test_get_review_before_only_has_no_lower_bound(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id, now - 72 * 3600, now - 71 * 3600) + response = client.get("/review", params={"before": now - 48 * 3600}) + assert response.status_code == 200 + assert [r["id"] for r in response.json()] == [id] + + def test_get_review_after_zero_is_not_defaulted(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id, now - 72 * 3600, now - 71 * 3600) + response = client.get("/review", params={"after": 0}) + assert response.status_code == 200 + assert [r["id"] for r in response.json()] == [id] + + def test_get_review_no_bounds_defaults_to_last_24_hours(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + super().insert_mock_review_segment( + "123456.random", now - 72 * 3600, now - 71 * 3600 + ) + response = client.get("/review", params={"limit": 10}) + assert response.status_code == 200 + assert response.json() == [] + def test_get_review_with_time_filter_no_matches(self): """Test that review items outside the range are not returned.""" now = datetime.now().timestamp() diff --git a/frigate/test/test_analytics_collectors_features.py b/frigate/test/test_analytics_collectors_features.py index 147f70a0ab..49ebadfb6f 100644 --- a/frigate/test/test_analytics_collectors_features.py +++ b/frigate/test/test_analytics_collectors_features.py @@ -100,6 +100,17 @@ class TestFeaturesCollector(unittest.TestCase): self.assertEqual(section.users, {"admin": 1, "viewer": 1, "custom": 1}) self.assertEqual((section.camera_groups, section.profiles), (1, 0)) self.assertFalse(section.plus_api_key) + self.assertFalse(section.notifications) + + def test_camera_notifications_count_as_on(self): + config = { + **CONFIG, + "cameras": {"front": {**FRONT_CAMERA, "notifications": {"enabled": True}}}, + } + + section = features.collect(make_context(make_config(config), {})) + + self.assertTrue(section.notifications) class TestFeatureMappings(unittest.TestCase): diff --git a/frigate/test/test_ptz_autotrack.py b/frigate/test/test_ptz_autotrack.py index fa84f0a672..a20e7c2f64 100644 --- a/frigate/test/test_ptz_autotrack.py +++ b/frigate/test/test_ptz_autotrack.py @@ -193,6 +193,35 @@ class TestAutotrackerPrediction(unittest.TestCase): self.assertEqual(self._target_box([-1, -2, 1, 2], 0), 0.2) +class TestAutotrackerCalibrationFailure(unittest.IsolatedAsyncioTestCase): + async def test_calibration_failure_disables_autotracking(self) -> None: + tracker = _make_tracker() + camera_config = tracker.config.cameras[CAMERA] + camera_config.onvif.autotracking.calibrate_on_startup = True + tracker.tracked_object_history = {} + tracker.tracked_object_metrics = {} + tracker.move_metrics = {} + tracker.intercept = {} + tracker.move_coefficients = {} + tracker.zoom_time = {} + tracker.move_queues = {} + tracker.move_queue_locks = {} + tracker.onvif.loop = asyncio.get_running_loop() + tracker.onvif.cams = {CAMERA: {"init": True, "features": ["pt-r-fov"]}} + tracker.onvif.get_service_capabilities = AsyncMock(return_value=True) + tracker.onvif.get_camera_status = AsyncMock() + tracker._process_move_queue = AsyncMock() + tracker._calibrate_camera = AsyncMock(side_effect=RuntimeError("disconnected")) + tracker.autotracker_init[CAMERA] = False + + with self.assertLogs("frigate.ptz.autotrack", level="ERROR"): + await tracker._autotracker_setup(camera_config, CAMERA, startup=True) + + self.assertFalse(camera_config.onvif.autotracking.enabled) + self.assertFalse(tracker.autotracker_init[CAMERA]) + self.assertFalse(tracker.calibrating[CAMERA]) + + class TestAutotrackerMoveQueue(unittest.IsolatedAsyncioTestCase): async def test_absolute_zoom_does_not_overwrite_the_pan_tilt_sample(self) -> None: tracker = _make_tracker()