feat: Add severity media filter (#2323)
This commit is contained in:
@@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
MediaFilterMediaType,
|
||||
} from '../../src/components-lib/media-filter-controller';
|
||||
import { QuerySource } from '../../src/query-source';
|
||||
import { Severity } from '../../src/severity';
|
||||
import { UnifiedQuery } from '../../src/view/unified-query';
|
||||
import {
|
||||
createCameraConfig,
|
||||
@@ -317,6 +318,17 @@ const queryDefaultTestCases: Array<[string, UnifiedQuery, MediaFilterCoreDefault
|
||||
mediaTypes: [MediaFilterMediaType.Reviews],
|
||||
},
|
||||
],
|
||||
[
|
||||
'severity',
|
||||
createQueryWithNodes([
|
||||
{ type: QueryType.Review, severity: new Set<Severity>(['high']) },
|
||||
]),
|
||||
{
|
||||
cameraIDs: ['camera.kitchen'],
|
||||
mediaTypes: [MediaFilterMediaType.Reviews],
|
||||
severity: ['high'],
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
// @vitest-environment jsdom
|
||||
@@ -382,6 +394,15 @@ describe('MediaFilterController', () => {
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
expect(controller.getTagsOptions()).toEqual([]);
|
||||
});
|
||||
|
||||
it('severity', () => {
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
expect(controller.getSeverityOptions()).toEqual([
|
||||
{ value: 'high', label: 'High' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'low', label: 'Low' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should calculate correct dynamic options', () => {
|
||||
@@ -675,6 +696,36 @@ describe('MediaFilterController', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('with severity', async () => {
|
||||
const host = createLitElement();
|
||||
const cameraManager = createCameraManager(createCameraStore());
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(createView());
|
||||
|
||||
const controller = new MediaFilterController(host);
|
||||
controller.setViewManager(viewManager);
|
||||
|
||||
await controller.valueChangeHandler(
|
||||
cameraManager,
|
||||
mock<FoldersManager>(),
|
||||
{},
|
||||
{
|
||||
mediaTypes: [MediaFilterMediaType.Reviews],
|
||||
when: {},
|
||||
severity: ['high'],
|
||||
},
|
||||
);
|
||||
|
||||
expect(viewManager.setViewByParametersWithExistingQuery).toBeCalled();
|
||||
|
||||
const nodes = getQueryNodes(viewManager);
|
||||
expect(nodes).toHaveLength(1);
|
||||
expect(nodes?.[0]).toMatchObject({
|
||||
type: QueryType.Review,
|
||||
severity: new Set(['high']),
|
||||
});
|
||||
});
|
||||
|
||||
it('with multiple media types', async () => {
|
||||
const host = createLitElement();
|
||||
const cameraManager = createCameraManager(createCameraStore());
|
||||
|
||||
+1
-1
@@ -70,12 +70,12 @@ import { Device } from '../src/ha/registry/device/types';
|
||||
import { Entity, EntityRegistryManager } from '../src/ha/registry/entity/types';
|
||||
import { CurrentUser, HassStateDifference, HomeAssistant } from '../src/ha/types';
|
||||
import { QuerySource } from '../src/query-source';
|
||||
import { Severity } from '../src/severity';
|
||||
import {
|
||||
CapabilitiesRaw,
|
||||
EffectsControllerAPI,
|
||||
Interaction,
|
||||
MediaLoadedInfo,
|
||||
Severity,
|
||||
} from '../src/types';
|
||||
import {
|
||||
EventViewMedia,
|
||||
|
||||
@@ -462,6 +462,24 @@ describe('UnifiedQueryBuilder', () => {
|
||||
expect(nodes[0].what).toEqual(new Set(['person']));
|
||||
expect(nodes[0].where).toEqual(new Set(['zone']));
|
||||
});
|
||||
|
||||
it('should apply severity filter', () => {
|
||||
const { cameraManager, foldersManager } = createMocks();
|
||||
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
|
||||
|
||||
const query = builder.buildFilterQuery(
|
||||
new Set(['camera.office']),
|
||||
new Set(['clips']),
|
||||
{
|
||||
severity: new Set(['high']),
|
||||
},
|
||||
);
|
||||
|
||||
assert(query);
|
||||
const nodes = query.getNodes();
|
||||
expect(nodes).toHaveLength(1);
|
||||
expect(nodes[0].severity).toEqual(new Set(['high']));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user