From dd5ba8bd8ea374a9ffa6a7956132dd630c5bc73d Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 28 Sep 2026 09:04:04 -0500 Subject: [PATCH] update monitored hardware after a runtime config swap `swap_runtime_config` rebound the stats emitter to the new config but not its `HardwareStats`, which kept polling hardware for the old config and applied camera updates to the discarded object. It now follows the swap along with its camera update subscriber. --- frigate/api/config_util.py | 1 + frigate/stats/hardware.py | 11 +++++++++++ frigate/test/test_config_util.py | 1 + frigate/test/test_hardware_stats.py | 11 +++++++++++ 4 files changed, 24 insertions(+) diff --git a/frigate/api/config_util.py b/frigate/api/config_util.py index 0cb954af10..c69c242aed 100644 --- a/frigate/api/config_util.py +++ b/frigate/api/config_util.py @@ -51,6 +51,7 @@ def swap_runtime_config(app: FastAPI, config: FrigateConfig) -> None: if app.stats_emitter is not None: app.stats_emitter.config = config + app.stats_emitter.hardware_stats.set_config(config) if app.dispatcher is not None: app.dispatcher.config = config diff --git a/frigate/stats/hardware.py b/frigate/stats/hardware.py index b8a877761c..dbe18cc548 100644 --- a/frigate/stats/hardware.py +++ b/frigate/stats/hardware.py @@ -256,6 +256,17 @@ class HardwareStats: ) self.update_config() + def set_config(self, config: FrigateConfig) -> None: + """Follow a runtime config swap and recalculate the monitored hardware. + + The camera update subscriber has to follow too, or later camera updates + would land on the discarded config. + """ + self.config = config + self._config_subscriber.config = config + self._config_subscriber.camera_configs = config.cameras + self.update_config() + def update_config(self) -> None: """Recalculate all hardware that needs to be monitored from the config.""" names = self._scan_ffmpeg() | self._scan_detectors() | self._scan_enrichments() diff --git a/frigate/test/test_config_util.py b/frigate/test/test_config_util.py index 5888f26188..71d1eccdfd 100644 --- a/frigate/test/test_config_util.py +++ b/frigate/test/test_config_util.py @@ -26,6 +26,7 @@ class TestSwapRuntimeConfig(unittest.TestCase): app.genai_manager.update_config.assert_called_once_with(config) app.profile_manager.update_config.assert_called_once_with(config) self.assertIs(app.stats_emitter.config, config) + app.stats_emitter.hardware_stats.set_config.assert_called_once_with(config) self.assertIs(app.dispatcher.config, config) for comm in app.dispatcher.comms: self.assertIs(comm.config, config) diff --git a/frigate/test/test_hardware_stats.py b/frigate/test/test_hardware_stats.py index 2bb1e9cc9b..e75218d5d3 100644 --- a/frigate/test/test_hardware_stats.py +++ b/frigate/test/test_hardware_stats.py @@ -288,6 +288,17 @@ class TestUpdateConfig(HardwareStatsTestCase): self.assertEqual(set(stats._monitored), {"rockchip"}) + def test_follows_a_runtime_config_swap(self): + stats = self.make_stats(self.make_config()) + self.assertEqual(set(stats._monitored), set()) + + swapped = self.make_config("preset-rk-h264") + stats.set_config(swapped) + + self.assertEqual(set(stats._monitored), {"rockchip"}) + self.assertIs(self.subscriber.return_value.config, swapped) + self.assertIs(self.subscriber.return_value.camera_configs, swapped.cameras) + class TestUpdateStats(HardwareStatsTestCase): def run_stats(self, stats: HardwareStats) -> dict: