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
@@ -23,6 +23,7 @@ import {
QueryReturnType,
QueryType,
} from '../../../src/camera-manager/types';
import { QuerySource } from '../../../src/query-source';
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
import { BrowseMedia, browseMediaSchema } from '../../../src/ha/browse-media/types';
import { BrowseMediaWalker } from '../../../src/ha/browse-media/walker';
@@ -340,6 +341,7 @@ describe('ReolinkCameraManagerEngine', () => {
expect(
await engine.getEvents(createHASS(), createStore(), {
...query,
source: QuerySource.Camera,
cameraIDs: new Set(['office']),
type: QueryType.Event,
}),
@@ -360,6 +362,7 @@ describe('ReolinkCameraManagerEngine', () => {
createHASS(),
store,
{
source: QuerySource.Camera,
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
@@ -377,6 +380,7 @@ describe('ReolinkCameraManagerEngine', () => {
cameraIDs: new Set(['office']),
end: new Date('2024-11-04T22:00:00'),
start: new Date('2024-11-04T21:00:00'),
source: 'camera',
type: 'event-query',
},
{
@@ -473,6 +477,7 @@ describe('ReolinkCameraManagerEngine', () => {
createHASS(),
store,
{
source: QuerySource.Camera,
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
@@ -510,6 +515,7 @@ describe('ReolinkCameraManagerEngine', () => {
const hass = createHASS();
await engine.getEvents(hass, store, {
source: QuerySource.Camera,
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
@@ -561,6 +567,7 @@ describe('ReolinkCameraManagerEngine', () => {
});
const events = await engine.getEvents(createHASS(), store, {
source: QuerySource.Camera,
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
@@ -574,6 +581,7 @@ describe('ReolinkCameraManagerEngine', () => {
cameraIDs: new Set(['office']),
end: new Date('2024-11-04T22:00:00'),
start: new Date('2024-11-04T21:00:00'),
source: 'camera',
type: 'event-query',
},
{
@@ -603,6 +611,7 @@ describe('ReolinkCameraManagerEngine', () => {
const store = await createStoreWithReolinkCamera(engine);
const events = await engine.getEvents(createHASS(), store, {
source: QuerySource.Camera,
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
@@ -616,6 +625,7 @@ describe('ReolinkCameraManagerEngine', () => {
cameraIDs: new Set(['office']),
end: new Date('2024-11-04T22:00:00'),
start: new Date('2024-11-04T21:00:00'),
source: 'camera',
type: 'event-query',
},
{
@@ -638,6 +648,7 @@ describe('ReolinkCameraManagerEngine', () => {
const hass = createHASS();
const events = await engine.getEvents(hass, store, {
source: QuerySource.Camera,
type: QueryType.Event,
cameraIDs: new Set(['office']),
});
@@ -647,6 +658,7 @@ describe('ReolinkCameraManagerEngine', () => {
[
{
cameraIDs: new Set(['office']),
source: 'camera',
type: 'event-query',
},
{
@@ -678,6 +690,7 @@ describe('ReolinkCameraManagerEngine', () => {
});
const events = await engine.getEvents(createHASS(), store, {
source: QuerySource.Camera,
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
@@ -691,6 +704,7 @@ describe('ReolinkCameraManagerEngine', () => {
cameraIDs: new Set(['office']),
end: new Date('2024-11-04T22:00:00'),
start: new Date('2024-11-04T21:00:00'),
source: 'camera',
type: 'event-query',
},
{
@@ -731,6 +745,7 @@ describe('ReolinkCameraManagerEngine', () => {
});
const events = await engine.getEvents(createHASS(), store, {
source: QuerySource.Camera,
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
@@ -744,6 +759,7 @@ describe('ReolinkCameraManagerEngine', () => {
cameraIDs: new Set(['office']),
end: new Date('2024-11-04T22:00:00'),
start: new Date('2024-11-04T21:00:00'),
source: 'camera',
type: 'event-query',
},
{
@@ -792,6 +808,7 @@ describe('ReolinkCameraManagerEngine', () => {
});
const events = await engine.getEvents(createHASS(), store, {
source: QuerySource.Camera,
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
@@ -805,6 +822,7 @@ describe('ReolinkCameraManagerEngine', () => {
cameraIDs: new Set(['office']),
end: new Date('2024-11-04T22:00:00'),
start: new Date('2024-11-04T21:00:00'),
source: 'camera',
type: 'event-query',
},
{
@@ -851,6 +869,7 @@ describe('ReolinkCameraManagerEngine', () => {
});
const events = await engine.getEvents(createHASS(), store, {
source: QuerySource.Camera,
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
@@ -864,6 +883,7 @@ describe('ReolinkCameraManagerEngine', () => {
cameraIDs: new Set(['office']),
end: new Date('2024-11-04T22:00:00'),
start: new Date('2024-11-04T21:00:00'),
source: 'camera',
type: 'event-query',
},
{
@@ -910,6 +930,7 @@ describe('ReolinkCameraManagerEngine', () => {
});
const events = await engine.getEvents(createHASS(), store, {
source: QuerySource.Camera,
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
@@ -923,6 +944,7 @@ describe('ReolinkCameraManagerEngine', () => {
cameraIDs: new Set(['office']),
end: new Date('2024-11-04T22:00:00'),
start: new Date('2024-11-04T21:00:00'),
source: 'camera',
type: 'event-query',
},
{
@@ -958,6 +980,7 @@ describe('ReolinkCameraManagerEngine', () => {
describe('should generate media from events', () => {
it('should generate media successfully', () => {
const query: EventQuery = {
source: QuerySource.Camera,
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
@@ -1004,6 +1027,7 @@ describe('ReolinkCameraManagerEngine', () => {
it('should reject non-reolink results', () => {
const query: EventQuery = {
source: QuerySource.Camera,
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),