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:
Dermot Duffy
2026-01-19 13:32:50 -08:00
committed by GitHub
parent 6eb0a87ca8
commit fc32727860
215 changed files with 14491 additions and 6160 deletions
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { GenericCameraManagerEngine } from '../../../src/camera-manager/generic/engine-generic';
import { Engine, QueryResultsType, QueryType } from '../../../src/camera-manager/types';
import { QuerySource } from '../../../src/query-source';
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
import { CameraConfig } from '../../../src/config/schema/cameras';
import { RawAdvancedCameraCardConfig } from '../../../src/config/types';
@@ -43,6 +44,14 @@ describe('GenericCameraManagerEngine', () => {
expect(camera.getCapabilities()?.has('trigger')).toBeTruthy();
});
it('should get default query parameters', async () => {
const config = createGenericCameraConfig();
const camera = await createEngine().createCamera(createHASS(), config);
expect(createEngine().getDefaultQueryParameters(camera, QueryType.Event)).toEqual(
{},
);
});
it('should generate default event query', () => {
const engine = createEngine();
expect(
@@ -82,7 +91,11 @@ describe('GenericCameraManagerEngine', () => {
await engine.getEvents(
createHASS(),
createStore([{ cameraID: 'camera-1', engine: engine }]),
{ type: QueryType.Event, cameraIDs: new Set(['camera-1']) },
{
source: QuerySource.Camera,
type: QueryType.Event,
cameraIDs: new Set(['camera-1']),
},
),
).toBeNull();
});
@@ -93,7 +106,11 @@ describe('GenericCameraManagerEngine', () => {
await engine.getRecordings(
createHASS(),
createStore([{ cameraID: 'camera-1', engine: engine }]),
{ type: QueryType.Recording, cameraIDs: new Set(['camera-1']) },
{
source: QuerySource.Camera,
type: QueryType.Recording,
cameraIDs: new Set(['camera-1']),
},
),
).toBeNull();
});
@@ -114,6 +131,32 @@ describe('GenericCameraManagerEngine', () => {
).toBeNull();
});
it('should generate default review query', () => {
const engine = createEngine();
expect(
engine.generateDefaultReviewQuery(
createStore([{ cameraID: 'camera-1', engine: engine }]),
new Set(['camera-1']),
{},
),
).toBeNull();
});
it('should get reviews', async () => {
const engine = createEngine();
expect(
await engine.getReviews(
createHASS(),
createStore([{ cameraID: 'camera-1', engine: engine }]),
{
source: QuerySource.Camera,
type: QueryType.Review,
cameraIDs: new Set(['camera-1']),
},
),
).toBeNull();
});
it('should generate media from events', async () => {
const engine = createEngine();
expect(
@@ -121,6 +164,7 @@ describe('GenericCameraManagerEngine', () => {
createHASS(),
createStore([{ cameraID: 'camera-1', engine: engine }]),
{
source: QuerySource.Camera,
type: QueryType.Event,
cameraIDs: new Set(['camera-1']),
},
@@ -139,6 +183,7 @@ describe('GenericCameraManagerEngine', () => {
createHASS(),
createStore([{ cameraID: 'camera-1', engine: engine }]),
{
source: QuerySource.Camera,
type: QueryType.Recording,
cameraIDs: new Set(['camera-1']),
start: new Date(),
@@ -152,6 +197,25 @@ describe('GenericCameraManagerEngine', () => {
).toBeNull();
});
it('should generate media from reviews', async () => {
const engine = createEngine();
expect(
engine.generateMediaFromReviews(
createHASS(),
createStore([{ cameraID: 'camera-1', engine: engine }]),
{
source: QuerySource.Camera,
type: QueryType.Review,
cameraIDs: new Set(['camera-1']),
},
{
type: QueryResultsType.Review,
engine: Engine.Generic,
},
),
).toBeNull();
});
it('should get media download path', async () => {
expect(
await createEngine().getMediaDownloadPath(
@@ -173,6 +237,17 @@ describe('GenericCameraManagerEngine', () => {
).toBeUndefined();
});
it('should review media', async () => {
expect(
await createEngine().reviewMedia(
createHASS(),
createGenericCameraConfig(),
new TestViewMedia(),
true,
),
).toBeUndefined();
});
it('should get query result max age', () => {
expect(
createEngine().getQueryResultMaxAge({