From 4f03480afd7647645f7878ed5cd4cd394950280d Mon Sep 17 00:00:00 2001 From: Gleb Nikiforov <72576128+NikiforovG@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:44:48 +0100 Subject: [PATCH] Fix audio worker startup after enabling a disabled camera (#24806) * Fix audio worker startup after enabling a disabled camera * Document audio lifecycle test helpers * Focus audio startup coverage on the enabled update regression --- frigate/events/audio.py | 1 + frigate/test/test_audio_spawn.py | 38 +++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/frigate/events/audio.py b/frigate/events/audio.py index 3d1bfbee06..e7b2f70477 100644 --- a/frigate/events/audio.py +++ b/frigate/events/audio.py @@ -178,6 +178,7 @@ class AudioProcessor(FrigateProcess): [ CameraConfigUpdateEnum.add, CameraConfigUpdateEnum.audio, + CameraConfigUpdateEnum.enabled, CameraConfigUpdateEnum.ffmpeg, CameraConfigUpdateEnum.remove, ], diff --git a/frigate/test/test_audio_spawn.py b/frigate/test/test_audio_spawn.py index 6c1c31a6a5..8b3f7daa4a 100644 --- a/frigate/test/test_audio_spawn.py +++ b/frigate/test/test_audio_spawn.py @@ -1,4 +1,4 @@ -"""Tests for audio maintainer spawning on runtime camera adds.""" +"""Tests for audio maintainer spawning on runtime camera updates.""" import threading import unittest @@ -42,6 +42,42 @@ class TestAudioSpawnIfNeeded(unittest.TestCase): processor.logger = MagicMock() return processor + def test_initially_disabled_camera_spawns_after_enabled_update(self): + """Consume an enabled update through the manager's real subscriber.""" + config = _build_config() + config.cameras["front_door"].enabled = False + stop = MagicMock() + stop.wait.side_effect = [False, True] + processor = AudioProcessor( + config, {"front_door": MagicMock()}, MagicMock(), stop + ) + processor.logger = MagicMock() + updates = iter([("config/cameras/front_door/enabled", True), (None, None)]) + self.addCleanup( + setattr, threading.current_thread(), "name", threading.current_thread().name + ) + + with ( + patch.object(processor, "pre_run_setup"), + patch("frigate.config.camera.updater.ConfigSubscriber") as transport, + patch("frigate.events.audio.AudioEventMaintainer") as maintainer, + ): + + def receive_update(): + self.assertEqual(processor.audio_threads, {}) + maintainer.assert_not_called() + return next(updates) + + transport.return_value.check_for_update.side_effect = receive_update + maintainer.return_value.is_alive.return_value = False + processor.run() + + self.assertEqual( + processor.audio_threads, {"front_door": maintainer.return_value} + ) + maintainer.assert_called_once() + maintainer.return_value.start.assert_called_once() + def test_skips_camera_whose_metrics_have_not_been_created_yet(self): """The camera maintainer creates metrics on its own poll of the add update, so the audio processor can see the camera first."""