From b25b8a72947603591470485a3a188d30ebff98f1 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 30 Oct 2026 21:19:38 -0500 Subject: [PATCH] fixes --- docs/static/frigate-analytics-schema.json | 4 +-- frigate/analytics/collectors/cameras.py | 17 +++++++---- frigate/analytics/collectors/features.py | 3 +- frigate/analytics/schema.py | 8 ++++-- frigate/object_detection/base.py | 3 +- .../test/test_analytics_collectors_cameras.py | 28 ++++++++++++++++--- .../test_analytics_collectors_detection.py | 7 +++++ .../test_analytics_collectors_features.py | 1 + 8 files changed, 56 insertions(+), 15 deletions(-) diff --git a/docs/static/frigate-analytics-schema.json b/docs/static/frigate-analytics-schema.json index b11b63409f..10c12fb697 100644 --- a/docs/static/frigate-analytics-schema.json +++ b/docs/static/frigate-analytics-schema.json @@ -72,7 +72,7 @@ "x-public": true }, "audio_transcription": { - "description": "Cameras with audio transcription on", + "description": "Cameras with audio detection and transcription on", "minimum": 0, "title": "Audio Transcription", "type": "integer", @@ -143,7 +143,7 @@ "x-public": true }, "enabled": { - "description": "Enabled cameras", + "description": "Cameras enabled in config; every count below covers only these", "minimum": 0, "title": "Enabled", "type": "integer", diff --git a/frigate/analytics/collectors/cameras.py b/frigate/analytics/collectors/cameras.py index 19e710497c..10dff999bb 100644 --- a/frigate/analytics/collectors/cameras.py +++ b/frigate/analytics/collectors/cameras.py @@ -108,6 +108,11 @@ def collect(ctx: ReportContext) -> CamerasSection: } camera_stats = ctx.stats.get("cameras", {}) + # cameras disabled in config are parked, so they'd skew feature adoption + active = [ + (name, camera) for name, camera in cameras.items() if camera.enabled_in_config + ] + flags: Counter[str] = Counter() types: Counter[CameraTypeEnum] = Counter() heights: Counter[HeightBucket] = Counter() @@ -119,7 +124,7 @@ def collect(ctx: ReportContext) -> CamerasSection: period: Counter() for period in ("continuous", "motion", "alerts", "detections") } - for name, camera in cameras.items(): + for name, camera in active: detect_input = role_input(camera, CameraRoleEnum.detect) record_input = role_input(camera, CameraRoleEnum.record) @@ -145,7 +150,8 @@ def collect(ctx: ReportContext) -> CamerasSection: state = camera_stats.get(name, {}).get("connection_quality") - if state in QUALITIES: + # a camera turned off at runtime reports unusable + if camera.enabled and state in QUALITIES: quality[ConnectionQuality(state)] += 1 if camera.record.enabled: @@ -157,7 +163,6 @@ def collect(ctx: ReportContext) -> CamerasSection: ] += 1 zones = len(camera.zones) - flags["enabled"] += camera.enabled flags["go2rtc_restream"] += any( is_restream(i.path) for i in camera.ffmpeg.inputs ) @@ -169,7 +174,9 @@ def collect(ctx: ReportContext) -> CamerasSection: flags["sub_stream_record"] += camera.record.sub.enabled flags["snapshots"] += camera.snapshots.enabled flags["audio"] += camera.audio.enabled - flags["audio_transcription"] += camera.audio_transcription.enabled + flags["audio_transcription"] += ( + camera.audio.enabled and camera.audio_transcription.enabled + ) flags["birdseye"] += camera.birdseye.enabled flags["onvif"] += bool(camera.onvif.host) flags["autotracking"] += camera.onvif.autotracking.enabled @@ -187,7 +194,7 @@ def collect(ctx: ReportContext) -> CamerasSection: return CamerasSection( total=len(cameras), - enabled=flags["enabled"], + enabled=len(active), types=histogram(types), detect_height=histogram(heights), detect_fps=histogram(fps), diff --git a/frigate/analytics/collectors/features.py b/frigate/analytics/collectors/features.py index a0247c98c3..04164d3dcd 100644 --- a/frigate/analytics/collectors/features.py +++ b/frigate/analytics/collectors/features.py @@ -42,7 +42,8 @@ def enrichment_device(label: object) -> EnrichmentDevice | None: runtime, _, target = label.partition(" ") if runtime == "OpenVINO": - first = target.split(",")[0].split(".")[0].strip().lower() + # AUTO reports "(CPU)" while it warms up on the CPU + first = target.split(",")[0].split(".")[0].strip("() ").lower() return OPENVINO_DEVICES.get(first, EnrichmentDevice.other) return RUNTIME_DEVICES.get(label.lower(), EnrichmentDevice.other) diff --git a/frigate/analytics/schema.py b/frigate/analytics/schema.py index bf38b2cd1b..7f6053be16 100644 --- a/frigate/analytics/schema.py +++ b/frigate/analytics/schema.py @@ -267,7 +267,9 @@ class RetainDays(AnalyticsModel): class CamerasSection(AnalyticsModel): total: int = count("Configured cameras") - enabled: int = count("Enabled cameras") + enabled: int = count( + "Cameras enabled in config; every count below covers only these" + ) types: dict[CameraTypeEnum, int] = metric("Cameras by type") detect_height: dict[HeightBucket, int] = metric( "Cameras by detect resolution height" @@ -288,7 +290,9 @@ class CamerasSection(AnalyticsModel): sub_stream_record: int = count("Cameras with sub stream recording on") snapshots: int = count("Cameras with snapshots on") audio: int = count("Cameras with audio detection on") - audio_transcription: int = count("Cameras with audio transcription on") + audio_transcription: int = count( + "Cameras with audio detection and transcription on" + ) birdseye: int = count("Cameras in birdseye") onvif: int = count("Cameras with an ONVIF host") autotracking: int = count("Cameras with PTZ autotracking on") diff --git a/frigate/object_detection/base.py b/frigate/object_detection/base.py index 49cfc9b3d0..0b0e6c0125 100644 --- a/frigate/object_detection/base.py +++ b/frigate/object_detection/base.py @@ -328,7 +328,8 @@ class ObjectDetectProcess: self.name = name self.cameras = cameras self.detection_queue = detection_queue - self.avg_inference_speed = Value("d", 0.01) + # stays 0 until the first inference, so stats can tell an idle detector + self.avg_inference_speed = Value("d", 0.0) self.detection_start = Value("d", 0.0) self.detect_process: FrigateProcess | None = None self.config = config diff --git a/frigate/test/test_analytics_collectors_cameras.py b/frigate/test/test_analytics_collectors_cameras.py index 8e7b380c2c..41b5a5fda8 100644 --- a/frigate/test/test_analytics_collectors_cameras.py +++ b/frigate/test/test_analytics_collectors_cameras.py @@ -9,6 +9,7 @@ from frigate.test.analytics_helpers import make_config, make_context MASK = {"coordinates": "0,0,1,0,1,1"} CONFIG = { + "audio_transcription": {"enabled": True}, "record": { "enabled": True, "continuous": {"days": 3}, @@ -38,17 +39,27 @@ CONFIG = { "onvif": {"host": "10.0.0.9"}, }, "garage": { - "enabled": False, "type": "lpr", "ffmpeg": { "hwaccel_args": ["-hwaccel", "vaapi"], "inputs": [ - {"path": "rtsp://10.0.0.5/stream", "roles": ["detect", "record"]} + { + "path": "rtsp://10.0.0.5/stream", + "roles": ["detect", "record", "audio"], + } ], }, + "audio": {"enabled": True}, "detect": {"height": 2160, "width": 3840, "fps": 15}, "record": {"enabled": False}, }, + "attic": { + "enabled": False, + "ffmpeg": { + "inputs": [{"path": "rtsp://10.0.0.6/stream", "roles": ["detect"]}] + }, + "detect": {"enabled": True, "height": 480, "width": 640, "fps": 5}, + }, "_replay_front": { "ffmpeg": { "inputs": [{"path": "rtsp://10.0.0.1/replay", "roles": ["detect"]}] @@ -62,16 +73,17 @@ STATS = { "cameras": { "front": {"connection_quality": "excellent"}, "garage": {"connection_quality": "offline"}, + "attic": {"connection_quality": "unusable"}, "_replay_front": {"connection_quality": "fair"}, } } class TestCamerasCollector(unittest.TestCase): - def test_aggregates_cameras_and_skips_replays(self): + def test_aggregates_enabled_cameras_and_skips_replays(self): section = cameras.collect(make_context(make_config(CONFIG), STATS)) - self.assertEqual((section.total, section.enabled), (2, 1)) + self.assertEqual((section.total, section.enabled), (3, 2)) self.assertEqual(section.types, {"generic": 1, "lpr": 1}) self.assertEqual(section.detect_height, {"720": 1, "ge_2160": 1}) self.assertEqual(section.detect_fps, {"le_5": 1, "gt_10": 1}) @@ -82,7 +94,15 @@ class TestCamerasCollector(unittest.TestCase): self.assertEqual((section.detect, section.record, section.onvif), (1, 1, 1)) self.assertEqual((section.zones, section.cameras_with_zones), (2, 1)) self.assertEqual((section.motion_masks, section.object_masks), (1, 2)) + self.assertEqual((section.audio, section.audio_transcription), (1, 1)) self.assertEqual(section.connection_quality, {"excellent": 1}) + + def test_runtime_disabled_camera_skips_connection_quality(self): + config = make_config(CONFIG) + config.cameras["front"].enabled = False + section = cameras.collect(make_context(config, STATS)) + + self.assertEqual(section.connection_quality, {}) self.assertEqual(section.retain_days.continuous, {"1_7": 1}) self.assertEqual(section.retain_days.motion, {"0": 1}) self.assertEqual(section.retain_days.alerts, {"8_30": 1}) diff --git a/frigate/test/test_analytics_collectors_detection.py b/frigate/test/test_analytics_collectors_detection.py index 0d9198ca8d..7061eb8d09 100644 --- a/frigate/test/test_analytics_collectors_detection.py +++ b/frigate/test/test_analytics_collectors_detection.py @@ -35,6 +35,13 @@ class TestDetectionCollector(unittest.TestCase): self.assertEqual(section.detection_fps, 12.35) self.assertEqual(section.skipped_fps, 0.0) + def test_idle_detector_reports_no_speed(self): + config = make_config({"models": [{"devices": ["cpu"]}]}) + stats = {"detectors": {"cpu": {"inference_speed": 0.0}}} + + [model] = detection.collect(make_context(config, stats)).models + self.assertIsNone(model.inference_ms) + def test_named_scene_reports_no_name(self): config = make_config( { diff --git a/frigate/test/test_analytics_collectors_features.py b/frigate/test/test_analytics_collectors_features.py index 6eb111fbf0..147f70a0ab 100644 --- a/frigate/test/test_analytics_collectors_features.py +++ b/frigate/test/test_analytics_collectors_features.py @@ -109,6 +109,7 @@ class TestFeatureMappings(unittest.TestCase): "MIGraphX": EnrichmentDevice.migraphx, "OpenVINO NPU": EnrichmentDevice.openvino_npu, "OpenVINO GPU.0,CPU": EnrichmentDevice.openvino_gpu, + "OpenVINO (CPU)": EnrichmentDevice.openvino_cpu, "OpenVINO": EnrichmentDevice.other, "CoreML": EnrichmentDevice.other, }