feat: Add support for Frigate reviews / detections [initial PR] (#2315)
- Add support for Frigate reviews / detections. - Add support for GenAI metadata. - Significant internal refactor to more flexible "UnifiedQuery" to allow mixing cameras with simple metadata and review metadata (e.g. a timeline view of a Frigate camera with reviews, and a Reolink camera with simple metadata). - Add support for folder media as camera media. There are a few more PRs to commit prior to this going live, but commiting this for now due to the scale of the change. BREAKING CHANGE: `media_type` and `events_type` are retired under `live`, `viewer` and `timeline` configuration sections, instead media type is associated (once) with the camera under `media`.
This commit is contained in:
@@ -11,8 +11,6 @@ import {
|
||||
QueryResultClassifier,
|
||||
} from '../../src/camera-manager/manager.js';
|
||||
import {
|
||||
CameraEndpoints,
|
||||
CameraEndpointsContext,
|
||||
CameraEvent,
|
||||
CameraManagerCameraMetadata,
|
||||
Engine,
|
||||
@@ -27,23 +25,24 @@ import { CardController } from '../../src/card-controller/controller.js';
|
||||
import { sortItems } from '../../src/card-controller/view/sort.js';
|
||||
import { CameraConfig } from '../../src/config/schema/cameras.js';
|
||||
import { HomeAssistant } from '../../src/ha/types.js';
|
||||
import { QuerySource } from '../../src/query-source.js';
|
||||
import { Endpoint, PTZMovementType } from '../../src/types.js';
|
||||
import { ViewFolder, ViewItem, ViewMedia } from '../../src/view/item.js';
|
||||
import { ViewItemCapabilities } from '../../src/view/types.js';
|
||||
import {
|
||||
TestViewMedia,
|
||||
createInitializedCamera,
|
||||
createCameraConfig,
|
||||
createCapabilities,
|
||||
createCardAPI,
|
||||
createConfig,
|
||||
createFolder,
|
||||
createHASS,
|
||||
createInitializedCamera,
|
||||
generateViewMediaArray,
|
||||
} from '../test-utils.js';
|
||||
|
||||
describe('QueryClassifier', async () => {
|
||||
it('should classify event query', async () => {
|
||||
describe('QueryClassifier', () => {
|
||||
it('should classify event query', () => {
|
||||
expect(CameraQueryClassifier.isEventQuery({ type: QueryType.Event })).toBeTruthy();
|
||||
expect(
|
||||
CameraQueryClassifier.isEventQuery({ type: QueryType.Recording }),
|
||||
@@ -55,7 +54,7 @@ describe('QueryClassifier', async () => {
|
||||
CameraQueryClassifier.isEventQuery({ type: QueryType.MediaMetadata }),
|
||||
).toBeFalsy();
|
||||
});
|
||||
it('should classify recording query', async () => {
|
||||
it('should classify recording query', () => {
|
||||
expect(
|
||||
CameraQueryClassifier.isRecordingQuery({ type: QueryType.Event }),
|
||||
).toBeFalsy();
|
||||
@@ -69,7 +68,7 @@ describe('QueryClassifier', async () => {
|
||||
CameraQueryClassifier.isRecordingQuery({ type: QueryType.MediaMetadata }),
|
||||
).toBeFalsy();
|
||||
});
|
||||
it('should classify recording segments query', async () => {
|
||||
it('should classify recording segments query', () => {
|
||||
expect(
|
||||
CameraQueryClassifier.isRecordingSegmentsQuery({ type: QueryType.Event }),
|
||||
).toBeFalsy();
|
||||
@@ -85,7 +84,7 @@ describe('QueryClassifier', async () => {
|
||||
CameraQueryClassifier.isRecordingSegmentsQuery({ type: QueryType.MediaMetadata }),
|
||||
).toBeFalsy();
|
||||
});
|
||||
it('should classify media metadata query', async () => {
|
||||
it('should classify media metadata query', () => {
|
||||
expect(
|
||||
CameraQueryClassifier.isMediaMetadataQuery({ type: QueryType.Event }),
|
||||
).toBeFalsy();
|
||||
@@ -99,6 +98,19 @@ describe('QueryClassifier', async () => {
|
||||
CameraQueryClassifier.isMediaMetadataQuery({ type: QueryType.MediaMetadata }),
|
||||
).toBeTruthy();
|
||||
});
|
||||
it('should classify review query', () => {
|
||||
expect(CameraQueryClassifier.isReviewQuery({ type: QueryType.Event })).toBeFalsy();
|
||||
expect(
|
||||
CameraQueryClassifier.isReviewQuery({ type: QueryType.Recording }),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
CameraQueryClassifier.isReviewQuery({ type: QueryType.RecordingSegments }),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
CameraQueryClassifier.isReviewQuery({ type: QueryType.MediaMetadata }),
|
||||
).toBeFalsy();
|
||||
expect(CameraQueryClassifier.isReviewQuery({ type: QueryType.Review })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('QueryResultClassifier', async () => {
|
||||
@@ -195,9 +207,32 @@ describe('QueryResultClassifier', async () => {
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
it('should classify review query result', async () => {
|
||||
expect(
|
||||
QueryResultClassifier.isReviewQueryResult(createResults(QueryResultsType.Event)),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
QueryResultClassifier.isReviewQueryResult(
|
||||
createResults(QueryResultsType.Recording),
|
||||
),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
QueryResultClassifier.isReviewQueryResult(
|
||||
createResults(QueryResultsType.RecordingSegments),
|
||||
),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
QueryResultClassifier.isReviewQueryResult(
|
||||
createResults(QueryResultsType.MediaMetadata),
|
||||
),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
QueryResultClassifier.isReviewQueryResult(createResults(QueryResultsType.Review)),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CameraManager', async () => {
|
||||
describe('CameraManager', () => {
|
||||
const baseCameraConfig = {
|
||||
id: 'id',
|
||||
camera_entity: 'camera.foo',
|
||||
@@ -205,6 +240,7 @@ describe('CameraManager', async () => {
|
||||
};
|
||||
|
||||
const baseEventQuery: EventQuery = {
|
||||
source: QuerySource.Camera,
|
||||
type: QueryType.Event as const,
|
||||
cameraIDs: new Set(['id']),
|
||||
};
|
||||
@@ -215,6 +251,7 @@ describe('CameraManager', async () => {
|
||||
};
|
||||
|
||||
const baseRecordingQuery = {
|
||||
source: QuerySource.Camera as const,
|
||||
type: QueryType.Recording as const,
|
||||
cameraIDs: new Set(['id']),
|
||||
};
|
||||
@@ -266,7 +303,7 @@ describe('CameraManager', async () => {
|
||||
return new CameraManager(api, { factory: mockFactory });
|
||||
};
|
||||
|
||||
it('should construct', async () => {
|
||||
it('should construct', () => {
|
||||
const manager = new CameraManager(createCardAPI());
|
||||
expect(manager.getStore()).toBeTruthy();
|
||||
});
|
||||
@@ -440,6 +477,11 @@ describe('CameraManager', async () => {
|
||||
'generateDefaultRecordingSegmentsQuery',
|
||||
'generateDefaultRecordingSegmentsQueries',
|
||||
],
|
||||
[
|
||||
QueryType.Review as const,
|
||||
'generateDefaultReviewQuery',
|
||||
'generateDefaultReviewQueries',
|
||||
],
|
||||
])(
|
||||
'basic %s',
|
||||
async (
|
||||
@@ -482,6 +524,33 @@ describe('CameraManager', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultQueryParameters', () => {
|
||||
it('should return empty object for non-existent camera', async () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||
|
||||
const manager = createCameraManager(api, mock<CameraManagerEngine>());
|
||||
|
||||
expect(
|
||||
manager.getDefaultQueryParameters('not_a_camera', QueryType.Event),
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
it('should return parameters from engine for existing camera', async () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||
|
||||
const engine = mock<CameraManagerEngine>();
|
||||
const manager = createCameraManager(api, engine);
|
||||
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
|
||||
|
||||
engine.getDefaultQueryParameters.mockReturnValue({ what: new Set(['person']) });
|
||||
expect(manager.getDefaultQueryParameters('id', QueryType.Event)).toEqual({
|
||||
what: new Set(['person']),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge defaults correctly', async () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||
@@ -590,6 +659,34 @@ describe('CameraManager', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('should review media', () => {
|
||||
it('without camera', async () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||
const manager = createCameraManager(api);
|
||||
const media = new TestViewMedia();
|
||||
|
||||
await manager.reviewMedia(media, true);
|
||||
});
|
||||
|
||||
it('successfully', async () => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const engine = mock<CameraManagerEngine>();
|
||||
vi.mocked(engine.getEngineType).mockReturnValue(Engine.Generic);
|
||||
|
||||
const manager = createCameraManager(api, engine);
|
||||
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
|
||||
const media = new TestViewMedia({ cameraID: 'id' });
|
||||
|
||||
await manager.reviewMedia(media, true);
|
||||
|
||||
expect(engine.reviewMedia).toBeCalledWith(hass, expect.anything(), media, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should get recordings', () => {
|
||||
it('successfully', async () => {
|
||||
const api = createCardAPI();
|
||||
@@ -740,7 +837,7 @@ describe('CameraManager', async () => {
|
||||
new TestViewMedia({
|
||||
startTime: add(dateBase, { days: 2 }),
|
||||
}),
|
||||
new ViewFolder(createFolder()),
|
||||
new ViewFolder(createFolder(), []),
|
||||
];
|
||||
|
||||
it('without hass', async () => {
|
||||
|
||||
Reference in New Issue
Block a user