This commit is contained in:
Josh Hawkins
2026-10-30 21:19:38 -05:00
parent d3bc434e2d
commit b25b8a7294
8 changed files with 56 additions and 15 deletions
+2 -2
View File
@@ -72,7 +72,7 @@
"x-public": true "x-public": true
}, },
"audio_transcription": { "audio_transcription": {
"description": "Cameras with audio transcription on", "description": "Cameras with audio detection and transcription on",
"minimum": 0, "minimum": 0,
"title": "Audio Transcription", "title": "Audio Transcription",
"type": "integer", "type": "integer",
@@ -143,7 +143,7 @@
"x-public": true "x-public": true
}, },
"enabled": { "enabled": {
"description": "Enabled cameras", "description": "Cameras enabled in config; every count below covers only these",
"minimum": 0, "minimum": 0,
"title": "Enabled", "title": "Enabled",
"type": "integer", "type": "integer",
+12 -5
View File
@@ -108,6 +108,11 @@ def collect(ctx: ReportContext) -> CamerasSection:
} }
camera_stats = ctx.stats.get("cameras", {}) 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() flags: Counter[str] = Counter()
types: Counter[CameraTypeEnum] = Counter() types: Counter[CameraTypeEnum] = Counter()
heights: Counter[HeightBucket] = Counter() heights: Counter[HeightBucket] = Counter()
@@ -119,7 +124,7 @@ def collect(ctx: ReportContext) -> CamerasSection:
period: Counter() for period in ("continuous", "motion", "alerts", "detections") 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) detect_input = role_input(camera, CameraRoleEnum.detect)
record_input = role_input(camera, CameraRoleEnum.record) record_input = role_input(camera, CameraRoleEnum.record)
@@ -145,7 +150,8 @@ def collect(ctx: ReportContext) -> CamerasSection:
state = camera_stats.get(name, {}).get("connection_quality") 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 quality[ConnectionQuality(state)] += 1
if camera.record.enabled: if camera.record.enabled:
@@ -157,7 +163,6 @@ def collect(ctx: ReportContext) -> CamerasSection:
] += 1 ] += 1
zones = len(camera.zones) zones = len(camera.zones)
flags["enabled"] += camera.enabled
flags["go2rtc_restream"] += any( flags["go2rtc_restream"] += any(
is_restream(i.path) for i in camera.ffmpeg.inputs 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["sub_stream_record"] += camera.record.sub.enabled
flags["snapshots"] += camera.snapshots.enabled flags["snapshots"] += camera.snapshots.enabled
flags["audio"] += camera.audio.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["birdseye"] += camera.birdseye.enabled
flags["onvif"] += bool(camera.onvif.host) flags["onvif"] += bool(camera.onvif.host)
flags["autotracking"] += camera.onvif.autotracking.enabled flags["autotracking"] += camera.onvif.autotracking.enabled
@@ -187,7 +194,7 @@ def collect(ctx: ReportContext) -> CamerasSection:
return CamerasSection( return CamerasSection(
total=len(cameras), total=len(cameras),
enabled=flags["enabled"], enabled=len(active),
types=histogram(types), types=histogram(types),
detect_height=histogram(heights), detect_height=histogram(heights),
detect_fps=histogram(fps), detect_fps=histogram(fps),
+2 -1
View File
@@ -42,7 +42,8 @@ def enrichment_device(label: object) -> EnrichmentDevice | None:
runtime, _, target = label.partition(" ") runtime, _, target = label.partition(" ")
if runtime == "OpenVINO": 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 OPENVINO_DEVICES.get(first, EnrichmentDevice.other)
return RUNTIME_DEVICES.get(label.lower(), EnrichmentDevice.other) return RUNTIME_DEVICES.get(label.lower(), EnrichmentDevice.other)
+6 -2
View File
@@ -267,7 +267,9 @@ class RetainDays(AnalyticsModel):
class CamerasSection(AnalyticsModel): class CamerasSection(AnalyticsModel):
total: int = count("Configured cameras") 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") types: dict[CameraTypeEnum, int] = metric("Cameras by type")
detect_height: dict[HeightBucket, int] = metric( detect_height: dict[HeightBucket, int] = metric(
"Cameras by detect resolution height" "Cameras by detect resolution height"
@@ -288,7 +290,9 @@ class CamerasSection(AnalyticsModel):
sub_stream_record: int = count("Cameras with sub stream recording on") sub_stream_record: int = count("Cameras with sub stream recording on")
snapshots: int = count("Cameras with snapshots on") snapshots: int = count("Cameras with snapshots on")
audio: int = count("Cameras with audio detection 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") birdseye: int = count("Cameras in birdseye")
onvif: int = count("Cameras with an ONVIF host") onvif: int = count("Cameras with an ONVIF host")
autotracking: int = count("Cameras with PTZ autotracking on") autotracking: int = count("Cameras with PTZ autotracking on")
+2 -1
View File
@@ -328,7 +328,8 @@ class ObjectDetectProcess:
self.name = name self.name = name
self.cameras = cameras self.cameras = cameras
self.detection_queue = detection_queue 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.detection_start = Value("d", 0.0)
self.detect_process: FrigateProcess | None = None self.detect_process: FrigateProcess | None = None
self.config = config self.config = config
@@ -9,6 +9,7 @@ from frigate.test.analytics_helpers import make_config, make_context
MASK = {"coordinates": "0,0,1,0,1,1"} MASK = {"coordinates": "0,0,1,0,1,1"}
CONFIG = { CONFIG = {
"audio_transcription": {"enabled": True},
"record": { "record": {
"enabled": True, "enabled": True,
"continuous": {"days": 3}, "continuous": {"days": 3},
@@ -38,17 +39,27 @@ CONFIG = {
"onvif": {"host": "10.0.0.9"}, "onvif": {"host": "10.0.0.9"},
}, },
"garage": { "garage": {
"enabled": False,
"type": "lpr", "type": "lpr",
"ffmpeg": { "ffmpeg": {
"hwaccel_args": ["-hwaccel", "vaapi"], "hwaccel_args": ["-hwaccel", "vaapi"],
"inputs": [ "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}, "detect": {"height": 2160, "width": 3840, "fps": 15},
"record": {"enabled": False}, "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": { "_replay_front": {
"ffmpeg": { "ffmpeg": {
"inputs": [{"path": "rtsp://10.0.0.1/replay", "roles": ["detect"]}] "inputs": [{"path": "rtsp://10.0.0.1/replay", "roles": ["detect"]}]
@@ -62,16 +73,17 @@ STATS = {
"cameras": { "cameras": {
"front": {"connection_quality": "excellent"}, "front": {"connection_quality": "excellent"},
"garage": {"connection_quality": "offline"}, "garage": {"connection_quality": "offline"},
"attic": {"connection_quality": "unusable"},
"_replay_front": {"connection_quality": "fair"}, "_replay_front": {"connection_quality": "fair"},
} }
} }
class TestCamerasCollector(unittest.TestCase): 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)) 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.types, {"generic": 1, "lpr": 1})
self.assertEqual(section.detect_height, {"720": 1, "ge_2160": 1}) self.assertEqual(section.detect_height, {"720": 1, "ge_2160": 1})
self.assertEqual(section.detect_fps, {"le_5": 1, "gt_10": 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.detect, section.record, section.onvif), (1, 1, 1))
self.assertEqual((section.zones, section.cameras_with_zones), (2, 1)) self.assertEqual((section.zones, section.cameras_with_zones), (2, 1))
self.assertEqual((section.motion_masks, section.object_masks), (1, 2)) 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}) 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.continuous, {"1_7": 1})
self.assertEqual(section.retain_days.motion, {"0": 1}) self.assertEqual(section.retain_days.motion, {"0": 1})
self.assertEqual(section.retain_days.alerts, {"8_30": 1}) self.assertEqual(section.retain_days.alerts, {"8_30": 1})
@@ -35,6 +35,13 @@ class TestDetectionCollector(unittest.TestCase):
self.assertEqual(section.detection_fps, 12.35) self.assertEqual(section.detection_fps, 12.35)
self.assertEqual(section.skipped_fps, 0.0) 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): def test_named_scene_reports_no_name(self):
config = make_config( config = make_config(
{ {
@@ -109,6 +109,7 @@ class TestFeatureMappings(unittest.TestCase):
"MIGraphX": EnrichmentDevice.migraphx, "MIGraphX": EnrichmentDevice.migraphx,
"OpenVINO NPU": EnrichmentDevice.openvino_npu, "OpenVINO NPU": EnrichmentDevice.openvino_npu,
"OpenVINO GPU.0,CPU": EnrichmentDevice.openvino_gpu, "OpenVINO GPU.0,CPU": EnrichmentDevice.openvino_gpu,
"OpenVINO (CPU)": EnrichmentDevice.openvino_cpu,
"OpenVINO": EnrichmentDevice.other, "OpenVINO": EnrichmentDevice.other,
"CoreML": EnrichmentDevice.other, "CoreML": EnrichmentDevice.other,
} }