mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 05:24:11 +03:00
Bug Fixes (#20825)
* Correctly sort summary responses * Consider JinaV2 as a complex model * Subscribe to record updates in camera watchdog * Cleanup score showing * No need to sort review summary * Add tests for recording summary * Don't break existing format * Sort event summary by day
This commit is contained in:
parent
a510ea9036
commit
8048168814
@ -912,7 +912,7 @@ def events_summary(
|
|||||||
"count": int(g.count or 0),
|
"count": int(g.count or 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSONResponse(content=list(grouped.values()))
|
return JSONResponse(content=sorted(grouped.values(), key=lambda x: x["day"]))
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
|
|||||||
@ -496,7 +496,7 @@ def all_recordings_summary(
|
|||||||
for g in period_query:
|
for g in period_query:
|
||||||
days[g.day] = True
|
days[g.day] = True
|
||||||
|
|
||||||
return JSONResponse(content=days)
|
return JSONResponse(content=dict(sorted(days.items())))
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
|
|||||||
@ -234,7 +234,10 @@ class OpenVINOModelRunner(BaseModelRunner):
|
|||||||
# Import here to avoid circular imports
|
# Import here to avoid circular imports
|
||||||
from frigate.embeddings.types import EnrichmentModelTypeEnum
|
from frigate.embeddings.types import EnrichmentModelTypeEnum
|
||||||
|
|
||||||
return model_type in [EnrichmentModelTypeEnum.paddleocr.value]
|
return model_type in [
|
||||||
|
EnrichmentModelTypeEnum.paddleocr.value,
|
||||||
|
EnrichmentModelTypeEnum.jina_v2.value,
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, model_path: str, device: str, model_type: str, **kwargs):
|
def __init__(self, model_path: str, device: str, model_type: str, **kwargs):
|
||||||
self.model_path = model_path
|
self.model_path = model_path
|
||||||
@ -345,6 +348,16 @@ class OpenVINOModelRunner(BaseModelRunner):
|
|||||||
|
|
||||||
# Create tensor with the correct element type
|
# Create tensor with the correct element type
|
||||||
input_element_type = input_port.get_element_type()
|
input_element_type = input_port.get_element_type()
|
||||||
|
|
||||||
|
# Ensure input data matches the expected dtype to prevent type mismatches
|
||||||
|
# that can occur with models like Jina-CLIP v2 running on OpenVINO
|
||||||
|
expected_dtype = input_element_type.to_dtype()
|
||||||
|
if input_data.dtype != expected_dtype:
|
||||||
|
logger.debug(
|
||||||
|
f"Converting input '{input_name}' from {input_data.dtype} to {expected_dtype}"
|
||||||
|
)
|
||||||
|
input_data = input_data.astype(expected_dtype)
|
||||||
|
|
||||||
input_tensor = ov.Tensor(input_element_type, input_data.shape)
|
input_tensor = ov.Tensor(input_element_type, input_data.shape)
|
||||||
np.copyto(input_tensor.data, input_data)
|
np.copyto(input_tensor.data, input_data)
|
||||||
|
|
||||||
|
|||||||
379
frigate/test/http_api/test_http_media.py
Normal file
379
frigate/test/http_api/test_http_media.py
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
"""Unit tests for recordings/media API endpoints."""
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user
|
||||||
|
from frigate.models import Recordings
|
||||||
|
from frigate.test.http_api.base_http_test import BaseTestHttp
|
||||||
|
|
||||||
|
|
||||||
|
class TestHttpMedia(BaseTestHttp):
|
||||||
|
"""Test media API endpoints, particularly recordings with DST handling."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
super().setUp([Recordings])
|
||||||
|
self.app = super().create_app()
|
||||||
|
|
||||||
|
# Mock auth to bypass camera access for tests
|
||||||
|
async def mock_get_current_user(request: Any):
|
||||||
|
return {"username": "test_user", "role": "admin"}
|
||||||
|
|
||||||
|
self.app.dependency_overrides[get_current_user] = mock_get_current_user
|
||||||
|
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
|
||||||
|
"front_door",
|
||||||
|
"back_door",
|
||||||
|
]
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Clean up after tests."""
|
||||||
|
self.app.dependency_overrides.clear()
|
||||||
|
super().tearDown()
|
||||||
|
|
||||||
|
def test_recordings_summary_across_dst_spring_forward(self):
|
||||||
|
"""
|
||||||
|
Test recordings summary across spring DST transition (spring forward).
|
||||||
|
|
||||||
|
In 2024, DST in America/New_York transitions on March 10, 2024 at 2:00 AM
|
||||||
|
Clocks spring forward from 2:00 AM to 3:00 AM (EST to EDT)
|
||||||
|
"""
|
||||||
|
tz = pytz.timezone("America/New_York")
|
||||||
|
|
||||||
|
# March 9, 2024 at 12:00 PM EST (before DST)
|
||||||
|
march_9_noon = tz.localize(datetime(2024, 3, 9, 12, 0, 0)).timestamp()
|
||||||
|
|
||||||
|
# March 10, 2024 at 12:00 PM EDT (after DST transition)
|
||||||
|
march_10_noon = tz.localize(datetime(2024, 3, 10, 12, 0, 0)).timestamp()
|
||||||
|
|
||||||
|
# March 11, 2024 at 12:00 PM EDT (after DST)
|
||||||
|
march_11_noon = tz.localize(datetime(2024, 3, 11, 12, 0, 0)).timestamp()
|
||||||
|
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
# Insert recordings for each day
|
||||||
|
Recordings.insert(
|
||||||
|
id="recording_march_9",
|
||||||
|
path="/media/recordings/march_9.mp4",
|
||||||
|
camera="front_door",
|
||||||
|
start_time=march_9_noon,
|
||||||
|
end_time=march_9_noon + 3600, # 1 hour recording
|
||||||
|
duration=3600,
|
||||||
|
motion=100,
|
||||||
|
objects=5,
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
Recordings.insert(
|
||||||
|
id="recording_march_10",
|
||||||
|
path="/media/recordings/march_10.mp4",
|
||||||
|
camera="front_door",
|
||||||
|
start_time=march_10_noon,
|
||||||
|
end_time=march_10_noon + 3600,
|
||||||
|
duration=3600,
|
||||||
|
motion=150,
|
||||||
|
objects=8,
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
Recordings.insert(
|
||||||
|
id="recording_march_11",
|
||||||
|
path="/media/recordings/march_11.mp4",
|
||||||
|
camera="front_door",
|
||||||
|
start_time=march_11_noon,
|
||||||
|
end_time=march_11_noon + 3600,
|
||||||
|
duration=3600,
|
||||||
|
motion=200,
|
||||||
|
objects=10,
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
# Test recordings summary with America/New_York timezone
|
||||||
|
response = client.get(
|
||||||
|
"/recordings/summary",
|
||||||
|
params={"timezone": "America/New_York", "cameras": "all"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
summary = response.json()
|
||||||
|
|
||||||
|
# Verify we get exactly 3 days
|
||||||
|
assert len(summary) == 3, f"Expected 3 days, got {len(summary)}"
|
||||||
|
|
||||||
|
# Verify the correct dates are returned (API returns dict with True values)
|
||||||
|
assert "2024-03-09" in summary, f"Expected 2024-03-09 in {summary}"
|
||||||
|
assert "2024-03-10" in summary, f"Expected 2024-03-10 in {summary}"
|
||||||
|
assert "2024-03-11" in summary, f"Expected 2024-03-11 in {summary}"
|
||||||
|
assert summary["2024-03-09"] is True
|
||||||
|
assert summary["2024-03-10"] is True
|
||||||
|
assert summary["2024-03-11"] is True
|
||||||
|
|
||||||
|
def test_recordings_summary_across_dst_fall_back(self):
|
||||||
|
"""
|
||||||
|
Test recordings summary across fall DST transition (fall back).
|
||||||
|
|
||||||
|
In 2024, DST in America/New_York transitions on November 3, 2024 at 2:00 AM
|
||||||
|
Clocks fall back from 2:00 AM to 1:00 AM (EDT to EST)
|
||||||
|
"""
|
||||||
|
tz = pytz.timezone("America/New_York")
|
||||||
|
|
||||||
|
# November 2, 2024 at 12:00 PM EDT (before DST transition)
|
||||||
|
nov_2_noon = tz.localize(datetime(2024, 11, 2, 12, 0, 0)).timestamp()
|
||||||
|
|
||||||
|
# November 3, 2024 at 12:00 PM EST (after DST transition)
|
||||||
|
# Need to specify is_dst=False to get the time after fall back
|
||||||
|
nov_3_noon = tz.localize(
|
||||||
|
datetime(2024, 11, 3, 12, 0, 0), is_dst=False
|
||||||
|
).timestamp()
|
||||||
|
|
||||||
|
# November 4, 2024 at 12:00 PM EST (after DST)
|
||||||
|
nov_4_noon = tz.localize(datetime(2024, 11, 4, 12, 0, 0)).timestamp()
|
||||||
|
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
# Insert recordings for each day
|
||||||
|
Recordings.insert(
|
||||||
|
id="recording_nov_2",
|
||||||
|
path="/media/recordings/nov_2.mp4",
|
||||||
|
camera="front_door",
|
||||||
|
start_time=nov_2_noon,
|
||||||
|
end_time=nov_2_noon + 3600,
|
||||||
|
duration=3600,
|
||||||
|
motion=100,
|
||||||
|
objects=5,
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
Recordings.insert(
|
||||||
|
id="recording_nov_3",
|
||||||
|
path="/media/recordings/nov_3.mp4",
|
||||||
|
camera="front_door",
|
||||||
|
start_time=nov_3_noon,
|
||||||
|
end_time=nov_3_noon + 3600,
|
||||||
|
duration=3600,
|
||||||
|
motion=150,
|
||||||
|
objects=8,
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
Recordings.insert(
|
||||||
|
id="recording_nov_4",
|
||||||
|
path="/media/recordings/nov_4.mp4",
|
||||||
|
camera="front_door",
|
||||||
|
start_time=nov_4_noon,
|
||||||
|
end_time=nov_4_noon + 3600,
|
||||||
|
duration=3600,
|
||||||
|
motion=200,
|
||||||
|
objects=10,
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
# Test recordings summary with America/New_York timezone
|
||||||
|
response = client.get(
|
||||||
|
"/recordings/summary",
|
||||||
|
params={"timezone": "America/New_York", "cameras": "all"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
summary = response.json()
|
||||||
|
|
||||||
|
# Verify we get exactly 3 days
|
||||||
|
assert len(summary) == 3, f"Expected 3 days, got {len(summary)}"
|
||||||
|
|
||||||
|
# Verify the correct dates are returned (API returns dict with True values)
|
||||||
|
assert "2024-11-02" in summary, f"Expected 2024-11-02 in {summary}"
|
||||||
|
assert "2024-11-03" in summary, f"Expected 2024-11-03 in {summary}"
|
||||||
|
assert "2024-11-04" in summary, f"Expected 2024-11-04 in {summary}"
|
||||||
|
assert summary["2024-11-02"] is True
|
||||||
|
assert summary["2024-11-03"] is True
|
||||||
|
assert summary["2024-11-04"] is True
|
||||||
|
|
||||||
|
def test_recordings_summary_multiple_cameras_across_dst(self):
|
||||||
|
"""
|
||||||
|
Test recordings summary with multiple cameras across DST boundary.
|
||||||
|
"""
|
||||||
|
tz = pytz.timezone("America/New_York")
|
||||||
|
|
||||||
|
# March 9, 2024 at 10:00 AM EST (before DST)
|
||||||
|
march_9_morning = tz.localize(datetime(2024, 3, 9, 10, 0, 0)).timestamp()
|
||||||
|
|
||||||
|
# March 10, 2024 at 3:00 PM EDT (after DST transition)
|
||||||
|
march_10_afternoon = tz.localize(datetime(2024, 3, 10, 15, 0, 0)).timestamp()
|
||||||
|
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
# Insert recordings for front_door on March 9
|
||||||
|
Recordings.insert(
|
||||||
|
id="front_march_9",
|
||||||
|
path="/media/recordings/front_march_9.mp4",
|
||||||
|
camera="front_door",
|
||||||
|
start_time=march_9_morning,
|
||||||
|
end_time=march_9_morning + 3600,
|
||||||
|
duration=3600,
|
||||||
|
motion=100,
|
||||||
|
objects=5,
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
# Insert recordings for back_door on March 10
|
||||||
|
Recordings.insert(
|
||||||
|
id="back_march_10",
|
||||||
|
path="/media/recordings/back_march_10.mp4",
|
||||||
|
camera="back_door",
|
||||||
|
start_time=march_10_afternoon,
|
||||||
|
end_time=march_10_afternoon + 3600,
|
||||||
|
duration=3600,
|
||||||
|
motion=150,
|
||||||
|
objects=8,
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
# Test with all cameras
|
||||||
|
response = client.get(
|
||||||
|
"/recordings/summary",
|
||||||
|
params={"timezone": "America/New_York", "cameras": "all"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
summary = response.json()
|
||||||
|
|
||||||
|
# Verify we get both days
|
||||||
|
assert len(summary) == 2, f"Expected 2 days, got {len(summary)}"
|
||||||
|
assert "2024-03-09" in summary
|
||||||
|
assert "2024-03-10" in summary
|
||||||
|
assert summary["2024-03-09"] is True
|
||||||
|
assert summary["2024-03-10"] is True
|
||||||
|
|
||||||
|
def test_recordings_summary_at_dst_transition_time(self):
|
||||||
|
"""
|
||||||
|
Test recordings that span the exact DST transition time.
|
||||||
|
"""
|
||||||
|
tz = pytz.timezone("America/New_York")
|
||||||
|
|
||||||
|
# March 10, 2024 at 1:00 AM EST (1 hour before DST transition)
|
||||||
|
# At 2:00 AM, clocks jump to 3:00 AM
|
||||||
|
before_transition = tz.localize(datetime(2024, 3, 10, 1, 0, 0)).timestamp()
|
||||||
|
|
||||||
|
# Recording that spans the transition (1:00 AM to 3:30 AM EDT)
|
||||||
|
# This is 1.5 hours of actual time but spans the "missing" hour
|
||||||
|
after_transition = tz.localize(datetime(2024, 3, 10, 3, 30, 0)).timestamp()
|
||||||
|
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
Recordings.insert(
|
||||||
|
id="recording_during_transition",
|
||||||
|
path="/media/recordings/transition.mp4",
|
||||||
|
camera="front_door",
|
||||||
|
start_time=before_transition,
|
||||||
|
end_time=after_transition,
|
||||||
|
duration=after_transition - before_transition,
|
||||||
|
motion=100,
|
||||||
|
objects=5,
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/recordings/summary",
|
||||||
|
params={"timezone": "America/New_York", "cameras": "all"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
summary = response.json()
|
||||||
|
|
||||||
|
# The recording should appear on March 10
|
||||||
|
assert len(summary) == 1
|
||||||
|
assert "2024-03-10" in summary
|
||||||
|
assert summary["2024-03-10"] is True
|
||||||
|
|
||||||
|
def test_recordings_summary_utc_timezone(self):
|
||||||
|
"""
|
||||||
|
Test recordings summary with UTC timezone (no DST).
|
||||||
|
"""
|
||||||
|
# Use UTC timestamps directly
|
||||||
|
march_9_utc = datetime(2024, 3, 9, 17, 0, 0, tzinfo=timezone.utc).timestamp()
|
||||||
|
march_10_utc = datetime(2024, 3, 10, 17, 0, 0, tzinfo=timezone.utc).timestamp()
|
||||||
|
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
Recordings.insert(
|
||||||
|
id="recording_march_9_utc",
|
||||||
|
path="/media/recordings/march_9_utc.mp4",
|
||||||
|
camera="front_door",
|
||||||
|
start_time=march_9_utc,
|
||||||
|
end_time=march_9_utc + 3600,
|
||||||
|
duration=3600,
|
||||||
|
motion=100,
|
||||||
|
objects=5,
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
Recordings.insert(
|
||||||
|
id="recording_march_10_utc",
|
||||||
|
path="/media/recordings/march_10_utc.mp4",
|
||||||
|
camera="front_door",
|
||||||
|
start_time=march_10_utc,
|
||||||
|
end_time=march_10_utc + 3600,
|
||||||
|
duration=3600,
|
||||||
|
motion=150,
|
||||||
|
objects=8,
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
# Test with UTC timezone
|
||||||
|
response = client.get(
|
||||||
|
"/recordings/summary", params={"timezone": "utc", "cameras": "all"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
summary = response.json()
|
||||||
|
|
||||||
|
# Verify we get both days
|
||||||
|
assert len(summary) == 2
|
||||||
|
assert "2024-03-09" in summary
|
||||||
|
assert "2024-03-10" in summary
|
||||||
|
assert summary["2024-03-09"] is True
|
||||||
|
assert summary["2024-03-10"] is True
|
||||||
|
|
||||||
|
def test_recordings_summary_no_recordings(self):
|
||||||
|
"""
|
||||||
|
Test recordings summary when no recordings exist.
|
||||||
|
"""
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
response = client.get(
|
||||||
|
"/recordings/summary",
|
||||||
|
params={"timezone": "America/New_York", "cameras": "all"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
summary = response.json()
|
||||||
|
assert len(summary) == 0
|
||||||
|
|
||||||
|
def test_recordings_summary_single_camera_filter(self):
|
||||||
|
"""
|
||||||
|
Test recordings summary filtered to a single camera.
|
||||||
|
"""
|
||||||
|
tz = pytz.timezone("America/New_York")
|
||||||
|
march_10_noon = tz.localize(datetime(2024, 3, 10, 12, 0, 0)).timestamp()
|
||||||
|
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
# Insert recordings for both cameras
|
||||||
|
Recordings.insert(
|
||||||
|
id="front_recording",
|
||||||
|
path="/media/recordings/front.mp4",
|
||||||
|
camera="front_door",
|
||||||
|
start_time=march_10_noon,
|
||||||
|
end_time=march_10_noon + 3600,
|
||||||
|
duration=3600,
|
||||||
|
motion=100,
|
||||||
|
objects=5,
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
Recordings.insert(
|
||||||
|
id="back_recording",
|
||||||
|
path="/media/recordings/back.mp4",
|
||||||
|
camera="back_door",
|
||||||
|
start_time=march_10_noon,
|
||||||
|
end_time=march_10_noon + 3600,
|
||||||
|
duration=3600,
|
||||||
|
motion=150,
|
||||||
|
objects=8,
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
# Test with only front_door camera
|
||||||
|
response = client.get(
|
||||||
|
"/recordings/summary",
|
||||||
|
params={"timezone": "America/New_York", "cameras": "front_door"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
summary = response.json()
|
||||||
|
assert len(summary) == 1
|
||||||
|
assert "2024-03-10" in summary
|
||||||
|
assert summary["2024-03-10"] is True
|
||||||
@ -196,7 +196,9 @@ class CameraWatchdog(threading.Thread):
|
|||||||
self.sleeptime = self.config.ffmpeg.retry_interval
|
self.sleeptime = self.config.ffmpeg.retry_interval
|
||||||
|
|
||||||
self.config_subscriber = CameraConfigUpdateSubscriber(
|
self.config_subscriber = CameraConfigUpdateSubscriber(
|
||||||
None, {config.name: config}, [CameraConfigUpdateEnum.enabled]
|
None,
|
||||||
|
{config.name: config},
|
||||||
|
[CameraConfigUpdateEnum.enabled, CameraConfigUpdateEnum.record],
|
||||||
)
|
)
|
||||||
self.requestor = InterProcessRequestor()
|
self.requestor = InterProcessRequestor()
|
||||||
self.was_enabled = self.config.enabled
|
self.was_enabled = self.config.enabled
|
||||||
|
|||||||
@ -217,9 +217,7 @@ export function GroupedClassificationCard({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!best) {
|
if (!best) {
|
||||||
// select an item from the middle of the time series as this usually correlates
|
return group.at(-1);
|
||||||
// to a more representative image than the first or last
|
|
||||||
return group.at(Math.floor(group.length / 2));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const bestTyped: ClassificationItemData = best;
|
const bestTyped: ClassificationItemData = best;
|
||||||
@ -230,7 +228,7 @@ export function GroupedClassificationCard({
|
|||||||
? event.sub_label
|
? event.sub_label
|
||||||
: t(noClassificationLabel)
|
: t(noClassificationLabel)
|
||||||
: bestTyped.name,
|
: bestTyped.name,
|
||||||
score: event?.data?.sub_label_score || bestTyped.score,
|
score: event?.data?.sub_label_score,
|
||||||
};
|
};
|
||||||
}, [group, event, noClassificationLabel, t]);
|
}, [group, event, noClassificationLabel, t]);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user