Miscellaneous fixes (#23373)

* republish MQTT switch states when a profile is activated or deactivated

* fix object mask default name when created from Explore tracking details

* tweak annotation offset max in UI

* optimize recordings/unavailable gap detection and drop empty motion activity buckets

* add tests
This commit is contained in:
Josh Hawkins
2026-06-01 13:55:52 -06:00
committed by GitHub
parent 8073174c20
commit 570e21340a
8 changed files with 185 additions and 16 deletions
+72
View File
@@ -403,3 +403,75 @@ class TestHttpMedia(BaseTestHttp):
assert len(summary) == 1
assert "2024-03-10" in summary
assert summary["2024-03-10"] is True
def test_recordings_unavailable_reports_gap_between_recordings(self):
"""A gap between two recordings is reported as an unavailable segment."""
with AuthTestClient(self.app) as client:
# Two recordings with a 20s gap (1010-1030) between them.
Recordings.insert(
id="rec_a",
path="/media/recordings/a.mp4",
camera="front_door",
start_time=1000,
end_time=1010,
duration=10,
motion=0,
).execute()
Recordings.insert(
id="rec_b",
path="/media/recordings/b.mp4",
camera="front_door",
start_time=1030,
end_time=1040,
duration=10,
motion=0,
).execute()
response = client.get(
"/recordings/unavailable",
params={
"after": 1000,
"before": 1040,
"scale": 5,
"cameras": "front_door",
},
)
assert response.status_code == 200
assert response.json() == [{"start_time": 1010, "end_time": 1030}]
def test_recordings_unavailable_merges_overlapping_recordings(self):
"""Overlapping recordings are merged so no false gap is reported."""
with AuthTestClient(self.app) as client:
# Overlapping recordings spanning the whole requested range.
Recordings.insert(
id="rec_a",
path="/media/recordings/a.mp4",
camera="front_door",
start_time=1000,
end_time=1020,
duration=20,
motion=0,
).execute()
Recordings.insert(
id="rec_b",
path="/media/recordings/b.mp4",
camera="front_door",
start_time=1010,
end_time=1030,
duration=20,
motion=0,
).execute()
response = client.get(
"/recordings/unavailable",
params={
"after": 1000,
"before": 1030,
"scale": 5,
"cameras": "front_door",
},
)
assert response.status_code == 200
assert response.json() == []
+4 -7
View File
@@ -610,19 +610,16 @@ class TestHttpReview(BaseTestHttp):
response = client.get("/review/activity/motion", params=params)
assert response.status_code == 200
response_json = response.json()
assert len(response_json) == 61
# Only buckets with an actual recording are returned. Empty
# gap-fill buckets between the two recordings are dropped.
assert len(response_json) == 2
self.assertDictEqual(
{"motion": 50.5, "camera": "front_door", "start_time": now + 1},
response_json[0],
)
for item in response_json[1:-1]:
self.assertDictEqual(
{"motion": 0.0, "camera": "", "start_time": item["start_time"]},
item,
)
self.assertDictEqual(
{"motion": 100.0, "camera": "front_door", "start_time": one_m + 1},
response_json[len(response_json) - 1],
response_json[1],
)
####################################################################################################################
+31
View File
@@ -1,5 +1,6 @@
"""Tests for the profiles system."""
import copy
import json
import os
import unittest
@@ -746,6 +747,36 @@ class TestProfileManager(unittest.TestCase):
manager.activate_profile(None)
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")
def test_startup_replay_does_not_clear_runtime_state(self, mock_persist):
"""Startup callers pass clear_runtime_overrides=False to preserve state."""