feat: Add severity media filter (#2323)

This commit is contained in:
Dermot Duffy
2026-01-24 21:25:10 -08:00
committed by GitHub
parent f4e905a7fb
commit c03ddd9a13
22 changed files with 404 additions and 69 deletions
@@ -1,19 +1,33 @@
import { describe, expect, it } from 'vitest';
import { afterEach, assert, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { RecordingSegmentsCache } from '../../../src/camera-manager/cache';
import { FrigateCameraManagerEngine } from '../../../src/camera-manager/frigate/engine-frigate';
import {
FrigateCameraManagerEngine,
FrigateQueryResultsClassifier,
} from '../../../src/camera-manager/frigate/engine-frigate';
import {
FrigateEventViewMedia,
FrigateRecordingViewMedia,
} from '../../../src/camera-manager/frigate/media';
import { FrigateEvent, eventSchema } from '../../../src/camera-manager/frigate/types.js';
import { CameraManagerRequestCache } from '../../../src/camera-manager/types';
import { getReviews } from '../../../src/camera-manager/frigate/requests';
import {
FrigateEvent,
FrigateReview,
eventSchema,
} from '../../../src/camera-manager/frigate/types.js';
import { CameraManagerStore } from '../../../src/camera-manager/store';
import { CameraManagerRequestCache, QueryType } from '../../../src/camera-manager/types';
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
import { CameraConfig } from '../../../src/config/schema/cameras';
import { RawAdvancedCameraCardConfig } from '../../../src/config/types';
import { QuerySource } from '../../../src/query-source';
import { Severity } from '../../../src/severity';
import { ViewMedia, ViewMediaType } from '../../../src/view/item';
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
import { createCameraConfig, createHASS } from '../../test-utils';
vi.mock('../../../src/camera-manager/frigate/requests');
const createEngine = (): FrigateCameraManagerEngine => {
return new FrigateCameraManagerEngine(
new EntityRegistryManagerMock(),
@@ -56,6 +70,17 @@ const createEvent = (): FrigateEvent => {
});
};
const createFrigateReview = (id: string): FrigateReview => ({
id,
camera: 'camera-1',
start_time: 100,
end_time: 200,
severity: 'alert',
thumb_path: 'thumb.jpg',
data: { objects: [], zones: [] },
has_been_reviewed: false,
});
const createClipMedia = (): FrigateEventViewMedia => {
return new FrigateEventViewMedia(
ViewMediaType.Clip,
@@ -140,3 +165,169 @@ describe('getMediaDownloadPath', () => {
expect(endpoint).toBeNull();
});
});
describe('getReviews', () => {
afterEach(() => {
vi.resetAllMocks();
});
it('should fan out requests for multiple severities', async () => {
const engine = createEngine();
const hass = createHASS();
const cameraConfig = createCameraConfig({
frigate: { camera_name: 'camera-1', client_id: 'client-1' },
});
const store = mock<CameraManagerStore>();
store.getCameraIDs.mockReturnValue(new Set(['camera-1']));
store.getCameraIDsWithCapability.mockReturnValue(new Set(['camera-1']));
store.getCameraConfig.mockReturnValue(cameraConfig);
store.getCameraConfigs.mockImplementation(function* () {
yield cameraConfig;
});
store.getCameraConfigEntries.mockImplementation(function* () {
yield ['camera-1', cameraConfig];
});
store.hasCameraID.mockReturnValue(true);
const reviewHigh = createFrigateReview('review-high');
const reviewMedium = createFrigateReview('review-medium');
vi.mocked(getReviews)
.mockResolvedValueOnce([reviewHigh])
.mockResolvedValueOnce([reviewMedium]);
const resultsMap = await engine.getReviews(hass, store, {
type: QueryType.Review,
source: QuerySource.Camera,
cameraIDs: new Set(['camera-1']),
severity: new Set(['high', 'medium']),
});
expect(getReviews).toHaveBeenCalledTimes(2);
expect(getReviews).toHaveBeenCalledWith(
hass,
expect.objectContaining({
severity: 'alert',
}),
);
expect(getReviews).toHaveBeenCalledWith(
hass,
expect.objectContaining({
severity: 'detection',
}),
);
const result = resultsMap?.values().next().value;
assert(result);
assert(FrigateQueryResultsClassifier.isFrigateReviewQueryResults(result));
expect(result.reviews).toHaveLength(2);
expect(result.reviews).toContainEqual(reviewHigh);
expect(result.reviews).toContainEqual(reviewMedium);
});
it('should handle single severity', async () => {
const engine = createEngine();
const hass = createHASS();
const cameraConfig = createCameraConfig({
frigate: { camera_name: 'camera-1', client_id: 'client-1' },
});
const store = mock<CameraManagerStore>();
store.getCameraIDs.mockReturnValue(new Set(['camera-1']));
store.getCameraIDsWithCapability.mockReturnValue(new Set(['camera-1']));
store.getCameraConfig.mockReturnValue(cameraConfig);
store.getCameraConfigs.mockImplementation(function* () {
yield cameraConfig;
});
store.getCameraConfigEntries.mockImplementation(function* () {
yield ['camera-1', cameraConfig];
});
store.hasCameraID.mockReturnValue(true);
const review = createFrigateReview('review-low');
vi.mocked(getReviews).mockResolvedValue([review]);
await engine.getReviews(hass, store, {
type: QueryType.Review,
source: QuerySource.Camera,
cameraIDs: new Set(['camera-1']),
severity: new Set<Severity>(['high']),
});
expect(getReviews).toHaveBeenCalledTimes(1);
expect(getReviews).toHaveBeenCalledWith(
hass,
expect.objectContaining({
severity: 'alert',
}),
);
});
it('should ignore low severity', async () => {
const engine = createEngine();
const hass = createHASS();
const cameraConfig = createCameraConfig({
frigate: { camera_name: 'camera-1', client_id: 'client-1' },
});
const store = mock<CameraManagerStore>();
store.getCameraIDs.mockReturnValue(new Set(['camera-1']));
store.getCameraIDsWithCapability.mockReturnValue(new Set(['camera-1']));
store.getCameraConfig.mockReturnValue(cameraConfig);
store.getCameraConfigs.mockImplementation(function* () {
yield cameraConfig;
});
store.getCameraConfigEntries.mockImplementation(function* () {
yield ['camera-1', cameraConfig];
});
store.hasCameraID.mockReturnValue(true);
vi.mocked(getReviews).mockResolvedValue([]);
await engine.getReviews(hass, store, {
type: QueryType.Review,
source: QuerySource.Camera,
cameraIDs: new Set(['camera-1']),
severity: new Set<Severity>(['low']),
});
expect(getReviews).not.toHaveBeenCalled();
});
it('should query all valid severities when severity is undefined', async () => {
const engine = createEngine();
const hass = createHASS();
const cameraConfig = createCameraConfig({
frigate: { camera_name: 'camera-1', client_id: 'client-1' },
});
const store = mock<CameraManagerStore>();
store.getCameraIDs.mockReturnValue(new Set(['camera-1']));
store.getCameraIDsWithCapability.mockReturnValue(new Set(['camera-1']));
store.getCameraConfig.mockReturnValue(cameraConfig);
store.getCameraConfigs.mockImplementation(function* () {
yield cameraConfig;
});
store.getCameraConfigEntries.mockImplementation(function* () {
yield ['camera-1', cameraConfig];
});
store.hasCameraID.mockReturnValue(true);
vi.mocked(getReviews).mockResolvedValue([]);
await engine.getReviews(hass, store, {
type: QueryType.Review,
source: QuerySource.Camera,
cameraIDs: new Set(['camera-1']),
severity: undefined,
});
expect(getReviews).toHaveBeenCalledTimes(1);
expect(getReviews).toHaveBeenCalledWith(
hass,
expect.objectContaining({
severity: undefined,
}),
);
});
});