Miscellaneous fixes (#24970)
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / AMD64 Smoke Test (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions

* 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.
This commit is contained in:
Josh Hawkins
2026-11-14 07:34:17 -07:00
committed by GitHub
parent 0e3dca5915
commit 7fd81171f0
8 changed files with 108 additions and 7 deletions
@@ -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"
+1 -1
View File
@@ -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.
:::
+2 -1
View File
@@ -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,
+11 -4
View File
@@ -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))
+7 -1
View File
@@ -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
+31
View File
@@ -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()
@@ -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):
+29
View File
@@ -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()