republish MQTT switch states when a profile is activated or deactivated

This commit is contained in:
Josh Hawkins 2026-06-01 07:07:30 -05:00
parent 47a06c8b30
commit bc5fbe5eba
2 changed files with 80 additions and 1 deletions

View File

@ -5,7 +5,7 @@ import json
import logging import logging
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Any, Callable, Optional
from frigate.config.camera.updater import ( from frigate.config.camera.updater import (
CameraConfigUpdateEnum, CameraConfigUpdateEnum,
@ -34,6 +34,45 @@ PROFILE_SECTION_UPDATES: dict[str, CameraConfigUpdateEnum] = {
"zones": CameraConfigUpdateEnum.zones, "zones": CameraConfigUpdateEnum.zones,
} }
# Retained MQTT switch topics per profile section, with a payload getter.
# Republished on profile change so MQTT/HA don't show a stale toggle.
SECTION_STATE_TOPICS: dict[str, list[tuple[str, Callable[[Any], Any]]]] = {
"audio": [("audio", lambda c: "ON" if c.audio.enabled else "OFF")],
"birdseye": [
("birdseye", lambda c: "ON" if c.birdseye.enabled else "OFF"),
(
"birdseye_mode",
lambda c: c.birdseye.mode.value.upper() if c.birdseye.enabled else "OFF",
),
],
"detect": [("detect", lambda c: "ON" if c.detect.enabled else "OFF")],
"motion": [
("motion", lambda c: "ON" if c.motion.enabled else "OFF"),
("improve_contrast", lambda c: "ON" if c.motion.improve_contrast else "OFF"),
("motion_threshold", lambda c: c.motion.threshold),
("motion_contour_area", lambda c: c.motion.contour_area),
],
"notifications": [
("notifications", lambda c: "ON" if c.notifications.enabled else "OFF"),
],
"objects": [
("object_descriptions", lambda c: "ON" if c.objects.genai.enabled else "OFF"),
],
"record": [("recordings", lambda c: "ON" if c.record.enabled else "OFF")],
"review": [
("review_alerts", lambda c: "ON" if c.review.alerts.enabled else "OFF"),
(
"review_detections",
lambda c: "ON" if c.review.detections.enabled else "OFF",
),
(
"review_descriptions",
lambda c: "ON" if c.review.genai.enabled else "OFF",
),
],
"snapshots": [("snapshots", lambda c: "ON" if c.snapshots.enabled else "OFF")],
}
PERSISTENCE_FILE = Path(CONFIG_DIR) / ".profiles" PERSISTENCE_FILE = Path(CONFIG_DIR) / ".profiles"
@ -310,6 +349,15 @@ class ProfileManager:
settings, settings,
) )
# republish MQTT switch states
if self.dispatcher is not None:
for suffix, get_payload in SECTION_STATE_TOPICS.get(section, ()):
self.dispatcher.publish(
f"{cam_name}/{suffix}/state",
get_payload(cam_config),
retain=True,
)
def _persist_active_profile(self, profile_name: Optional[str]) -> None: def _persist_active_profile(self, profile_name: Optional[str]) -> None:
"""Persist the active profile state to disk as JSON.""" """Persist the active profile state to disk as JSON."""
try: try:

View File

@ -1,5 +1,6 @@
"""Tests for the profiles system.""" """Tests for the profiles system."""
import copy
import json import json
import os import os
import unittest import unittest
@ -746,6 +747,36 @@ class TestProfileManager(unittest.TestCase):
manager.activate_profile(None) manager.activate_profile(None)
dispatcher.clear_runtime_state.assert_called_once_with() dispatcher.clear_runtime_state.assert_called_once_with()
@patch.object(ProfileManager, "_persist_active_profile")
def test_profile_change_republishes_switch_states(self, mock_persist):
"""Profile changes republish MQTT switch states so HA stays in sync.
Regression: activating/deactivating a profile updated the in-memory
config (and Frigate's behavior) but left the retained MQTT state
topics stale, so external integrations like Home Assistant kept
showing the pre-profile toggle position.
"""
config_data = copy.deepcopy(self.config_data)
config_data["cameras"]["front"]["profiles"]["disarmed"]["review"] = {
"alerts": {"enabled": False},
}
config = FrigateConfig(**config_data)
dispatcher = MagicMock()
manager = ProfileManager(config, self.mock_updater, dispatcher)
# Activating disarmed turns alerts off -> MQTT state must follow
manager.activate_profile("disarmed")
dispatcher.publish.assert_any_call(
"front/review_alerts/state", "OFF", retain=True
)
# Deactivating restores the base (alerts on) -> MQTT state must follow
dispatcher.publish.reset_mock()
manager.activate_profile(None)
dispatcher.publish.assert_any_call(
"front/review_alerts/state", "ON", retain=True
)
@patch.object(ProfileManager, "_persist_active_profile") @patch.object(ProfileManager, "_persist_active_profile")
def test_startup_replay_does_not_clear_runtime_state(self, mock_persist): def test_startup_replay_does_not_clear_runtime_state(self, mock_persist):
"""Startup callers pass clear_runtime_overrides=False to preserve state.""" """Startup callers pass clear_runtime_overrides=False to preserve state."""