mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-06-21 11:51:53 +03:00
republish MQTT switch states when a profile is activated or deactivated
This commit is contained in:
parent
47a06c8b30
commit
bc5fbe5eba
@ -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:
|
||||||
|
|||||||
@ -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."""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user