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:
@@ -6,13 +6,9 @@ import {
|
||||
CONF_CAMERAS_GLOBAL_PTZ,
|
||||
CONF_DIMENSIONS_HEIGHT,
|
||||
CONF_ELEMENTS,
|
||||
CONF_LIVE_CONTROLS_THUMBNAILS_EVENTS_MEDIA_TYPE,
|
||||
CONF_LIVE_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE,
|
||||
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE,
|
||||
CONF_OVERRIDES,
|
||||
CONF_PROFILES,
|
||||
CONF_STATUS_BAR,
|
||||
CONF_TIMELINE_EVENTS_MEDIA_TYPE,
|
||||
CONF_VIEW_DEFAULT_CYCLE_CAMERA,
|
||||
CONF_VIEW_DEFAULT_RESET_ENTITIES,
|
||||
CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS,
|
||||
@@ -825,18 +821,19 @@ const UPGRADES = [
|
||||
upgradeWithOverrides('media_viewer.auto_unmute', (data) =>
|
||||
data === 'all' ? ['selected', 'visible'] : data === 'never' ? null : arrayify(data),
|
||||
),
|
||||
|
||||
upgradeMoveToWithOverrides(
|
||||
'live.controls.thumbnails.media',
|
||||
CONF_LIVE_CONTROLS_THUMBNAILS_EVENTS_MEDIA_TYPE,
|
||||
'live.controls.thumbnails.events_media_type',
|
||||
),
|
||||
upgradeMoveToWithOverrides('timeline.media', CONF_TIMELINE_EVENTS_MEDIA_TYPE),
|
||||
upgradeMoveToWithOverrides('timeline.media', 'timeline.events_media_type'),
|
||||
upgradeMoveToWithOverrides(
|
||||
'live.controls.timeline.media',
|
||||
CONF_LIVE_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE,
|
||||
'live.controls.timeline.events_media_type',
|
||||
),
|
||||
upgradeMoveToWithOverrides(
|
||||
'media_viewer.controls.timeline.media',
|
||||
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE,
|
||||
'media_viewer.controls.timeline.events_media_type',
|
||||
),
|
||||
upgradeMoveToWithOverrides('view.scan', CONF_VIEW_TRIGGERS),
|
||||
upgradeMoveToWithOverrides(
|
||||
@@ -944,4 +941,23 @@ const UPGRADES = [
|
||||
frigateCardToAdvancedCameraCardStyleTransform,
|
||||
),
|
||||
upgradeMoveToWithOverrides('menu.buttons.frigate', 'menu.buttons.iris'),
|
||||
|
||||
// v8.0.0+
|
||||
upgradeMoveToWithOverrides(
|
||||
'live.controls.thumbnails.media_type',
|
||||
'cameras_global.media.type',
|
||||
),
|
||||
upgradeMoveToWithOverrides(
|
||||
'live.controls.thumbnails.events_media_type',
|
||||
'cameras_global.media.events_type',
|
||||
{
|
||||
transform: (val) => {
|
||||
// 'all' is the default, delete it
|
||||
if (val === 'all') {
|
||||
return null;
|
||||
}
|
||||
return val;
|
||||
},
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
@@ -7,6 +7,7 @@ const GENERAL_ACTIONS = [
|
||||
'download',
|
||||
'expand',
|
||||
'fullscreen',
|
||||
'info',
|
||||
'live_substream_off',
|
||||
'live_substream_on',
|
||||
'menu_toggle',
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
import { advancedCameraCardCustomActionsBaseSchema } from './base';
|
||||
|
||||
export const setReviewActionConfigSchema =
|
||||
advancedCameraCardCustomActionsBaseSchema.extend({
|
||||
advanced_camera_card_action: z.literal('set_review'),
|
||||
|
||||
// Mark a review as reviewed or unreviewed.
|
||||
// - true = mark reviewed
|
||||
// - false = mark unreviewed
|
||||
// - undefined = toggle reviewed status
|
||||
reviewed: z.boolean().optional(),
|
||||
});
|
||||
export type SetReviewActionConfig = z.infer<typeof setReviewActionConfigSchema>;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import { statusBarItemBaseSchema } from '../common/status-bar';
|
||||
import { severitySchema } from '../common/severity';
|
||||
import { advancedCameraCardCustomActionsBaseSchema } from './custom/base';
|
||||
import { cameraSelectActionConfigSchema } from './custom/camera-select';
|
||||
import { viewDisplayModeActionConfigSchema } from './custom/display-mode';
|
||||
@@ -12,6 +13,7 @@ import { ptzActionConfigSchema } from './custom/ptz';
|
||||
import { ptzControlsActionConfigSchema } from './custom/ptz-controls';
|
||||
import { ptzDigitalActionConfigSchema } from './custom/ptz-digital';
|
||||
import { ptzMultiActionSchema } from './custom/ptz-multi';
|
||||
import { setReviewActionConfigSchema } from './custom/set-review';
|
||||
import { sleepActionConfigSchema } from './custom/sleep';
|
||||
import { substreamSelectActionConfigSchema } from './custom/substream-select';
|
||||
import { viewActionConfigSchema } from './custom/view';
|
||||
@@ -51,6 +53,7 @@ const advancedCameraCardCustomActionSchema = z.union([
|
||||
ptzControlsActionConfigSchema,
|
||||
ptzDigitalActionConfigSchema,
|
||||
ptzMultiActionSchema,
|
||||
setReviewActionConfigSchema,
|
||||
sleepActionConfigSchema,
|
||||
statusBarActionConfigSchema,
|
||||
substreamSelectActionConfigSchema,
|
||||
@@ -106,6 +109,7 @@ const statusBarItemElementsBaseSchema = statusBarItemBaseSchema.extend({
|
||||
sufficient: z.boolean().default(false).optional(),
|
||||
exclusive: z.boolean().default(false).optional(),
|
||||
expand: z.boolean().default(false).optional(),
|
||||
severity: severitySchema.optional(),
|
||||
actions: actionsBaseSchema.optional(),
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { capabilityKeys } from '../../types';
|
||||
import { mediaLayoutConfigSchema } from './camera/media-layout';
|
||||
import { ptzCameraConfigDefaults, ptzCameraConfigSchema } from './camera/ptz';
|
||||
import { aspectRatioSchema } from './common/aspect-ratio';
|
||||
import { eventsMediaTypeSchema } from './common/events-media';
|
||||
import { severitySchema } from './common/severity';
|
||||
import { imageBaseConfigSchema, imageConfigDefault } from './common/image';
|
||||
|
||||
const CAMERA_TRIGGER_EVENT_TYPES = [
|
||||
@@ -124,8 +126,12 @@ export const cameraConfigDefault = {
|
||||
triggers: {
|
||||
motion: false,
|
||||
occupancy: false,
|
||||
events: [...CAMERA_TRIGGER_EVENT_TYPES],
|
||||
events: [],
|
||||
entities: [],
|
||||
reviews: {
|
||||
severities: ['high' as const],
|
||||
description: true,
|
||||
},
|
||||
},
|
||||
proxy: {
|
||||
dynamic: true,
|
||||
@@ -173,6 +179,26 @@ const cameraDimensionsSchema = z.object({
|
||||
});
|
||||
export type CameraDimensionsConfig = z.infer<typeof cameraDimensionsSchema>;
|
||||
|
||||
// Camera media configuration for default media type in live/timeline views.
|
||||
const CAMERA_MEDIA_TYPES = [
|
||||
'auto',
|
||||
'reviews',
|
||||
'events',
|
||||
'recordings',
|
||||
'folder',
|
||||
] as const;
|
||||
export type CameraMediaType = (typeof CAMERA_MEDIA_TYPES)[number];
|
||||
|
||||
const cameraMediaConfigDefault = {
|
||||
type: 'auto' as CameraMediaType,
|
||||
};
|
||||
|
||||
const cameraMediaConfigSchema = z.object({
|
||||
type: z.enum(CAMERA_MEDIA_TYPES).default(cameraMediaConfigDefault.type),
|
||||
events_type: eventsMediaTypeSchema.optional(),
|
||||
folders: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export const cameraConfigSchema = z
|
||||
.object({
|
||||
camera_entity: z.string().optional(),
|
||||
@@ -209,6 +235,16 @@ export const cameraConfigSchema = z
|
||||
.enum(CAMERA_TRIGGER_EVENT_TYPES)
|
||||
.array()
|
||||
.default(cameraConfigDefault.triggers.events),
|
||||
reviews: z
|
||||
.object({
|
||||
severities: severitySchema
|
||||
.array()
|
||||
.default([...cameraConfigDefault.triggers.reviews.severities]),
|
||||
description: z
|
||||
.boolean()
|
||||
.default(cameraConfigDefault.triggers.reviews.description),
|
||||
})
|
||||
.default(cameraConfigDefault.triggers.reviews),
|
||||
})
|
||||
.default(cameraConfigDefault.triggers),
|
||||
|
||||
@@ -274,6 +310,8 @@ export const cameraConfigSchema = z
|
||||
|
||||
dimensions: cameraDimensionsSchema.optional(),
|
||||
|
||||
media: cameraMediaConfigSchema.optional(),
|
||||
|
||||
proxy: proxyConfigSchema.default(cameraConfigDefault.proxy),
|
||||
|
||||
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1650
|
||||
|
||||
@@ -22,6 +22,8 @@ export const VIEWS_USER_SPECIFIED = [
|
||||
'snapshots',
|
||||
'recording',
|
||||
'recordings',
|
||||
'review',
|
||||
'reviews',
|
||||
'image',
|
||||
'timeline',
|
||||
] as const;
|
||||
|
||||
@@ -9,8 +9,10 @@ const thumbnailControlsBaseDefaults = {
|
||||
size: THUMBNAIL_WIDTH_DEFAULT,
|
||||
show_details: true,
|
||||
show_favorite_control: true,
|
||||
show_timeline_control: true,
|
||||
show_download_control: true,
|
||||
show_timeline_control: false,
|
||||
show_download_control: false,
|
||||
show_review_control: true,
|
||||
show_info_control: true,
|
||||
};
|
||||
|
||||
// Configuration for the actual rendered thumbnail.
|
||||
@@ -30,6 +32,12 @@ export const thumbnailsControlBaseSchema = z.object({
|
||||
show_download_control: z
|
||||
.boolean()
|
||||
.default(thumbnailControlsBaseDefaults.show_download_control),
|
||||
show_review_control: z
|
||||
.boolean()
|
||||
.default(thumbnailControlsBaseDefaults.show_review_control),
|
||||
show_info_control: z
|
||||
.boolean()
|
||||
.default(thumbnailControlsBaseDefaults.show_info_control),
|
||||
});
|
||||
export type ThumbnailsControlBaseConfig = z.infer<typeof thumbnailsControlBaseSchema>;
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
import { eventsMediaTypeSchema } from '../events-media';
|
||||
import { thumbnailControlsDefaults } from './thumbnails';
|
||||
|
||||
const timelineCoreConfigDefault = {
|
||||
export const timelineCoreConfigDefault = {
|
||||
clustering_threshold: 3,
|
||||
events_media_type: 'all' as const,
|
||||
window_seconds: 60 * 60,
|
||||
show_recordings: true,
|
||||
style: 'stack' as const,
|
||||
@@ -26,9 +23,6 @@ export const timelineCoreConfigSchema = z.object({
|
||||
.number()
|
||||
.optional()
|
||||
.default(timelineCoreConfigDefault.clustering_threshold),
|
||||
events_media_type: eventsMediaTypeSchema
|
||||
.optional()
|
||||
.default(timelineCoreConfigDefault.events_media_type),
|
||||
window_seconds: z
|
||||
.number()
|
||||
.min(1 * 60)
|
||||
@@ -58,10 +52,3 @@ export const miniTimelineConfigSchema = timelineCoreConfigSchema.extend({
|
||||
style: timelineCoreConfigSchema.shape.style.default(miniTimelineConfigDefault.style),
|
||||
});
|
||||
export type MiniTimelineControlConfig = z.infer<typeof miniTimelineConfigSchema>;
|
||||
|
||||
export const timelineConfigDefault = {
|
||||
...timelineCoreConfigDefault,
|
||||
controls: {
|
||||
thumbnails: thumbnailControlsDefaults,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import { SEVERITIES } from '../../../types';
|
||||
|
||||
export const severitySchema = z.enum(SEVERITIES);
|
||||
@@ -13,3 +13,4 @@ export const menuBaseSchema = z.object({
|
||||
icon: z.string().optional(),
|
||||
permanent: z.boolean().default(false).optional(),
|
||||
});
|
||||
export type MenuItemBase = z.infer<typeof menuBaseSchema>;
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
miniTimelineConfigSchema,
|
||||
} from './common/controls/timeline';
|
||||
import { viewDisplaySchema } from './common/display';
|
||||
import { eventsMediaTypeSchema } from './common/events-media';
|
||||
import {
|
||||
MEDIA_ACTION_NEGATIVE_CONDITIONS,
|
||||
MEDIA_ACTION_POSITIVE_CONDITIONS,
|
||||
@@ -41,12 +40,6 @@ const microphoneConfigSchema = z
|
||||
.default(microphoneConfigDefault);
|
||||
export type MicrophoneConfig = z.infer<typeof microphoneConfigSchema>;
|
||||
|
||||
const liveThumbnailControlsDefaults = {
|
||||
...thumbnailControlsDefaults,
|
||||
media_type: 'events' as const,
|
||||
events_media_type: 'all' as const,
|
||||
};
|
||||
|
||||
export const liveConfigDefault = {
|
||||
auto_play: [...MEDIA_ACTION_POSITIVE_CONDITIONS],
|
||||
auto_pause: [],
|
||||
@@ -67,7 +60,7 @@ export const liveConfigDefault = {
|
||||
style: 'chevrons' as const,
|
||||
},
|
||||
ptz: ptzControlsDefaults,
|
||||
thumbnails: liveThumbnailControlsDefaults,
|
||||
thumbnails: thumbnailControlsDefaults,
|
||||
timeline: miniTimelineConfigDefault,
|
||||
wheel: true,
|
||||
},
|
||||
@@ -76,15 +69,6 @@ export const liveConfigDefault = {
|
||||
},
|
||||
};
|
||||
|
||||
const livethumbnailsControlSchema = thumbnailsControlSchema.extend({
|
||||
media_type: z
|
||||
.enum(['events', 'recordings'])
|
||||
.default(liveConfigDefault.controls.thumbnails.media_type),
|
||||
events_media_type: eventsMediaTypeSchema.default(
|
||||
liveConfigDefault.controls.thumbnails.events_media_type,
|
||||
),
|
||||
});
|
||||
|
||||
export const liveConfigSchema = z
|
||||
.object({
|
||||
auto_pause: z
|
||||
@@ -118,7 +102,7 @@ export const liveConfigSchema = z
|
||||
})
|
||||
.default(liveConfigDefault.controls.next_previous),
|
||||
ptz: ptzControlsConfigSchema.default(liveConfigDefault.controls.ptz),
|
||||
thumbnails: livethumbnailsControlSchema.default(
|
||||
thumbnails: thumbnailsControlSchema.default(
|
||||
liveConfigDefault.controls.thumbnails,
|
||||
),
|
||||
timeline: miniTimelineConfigSchema.default(liveConfigDefault.controls.timeline),
|
||||
|
||||
@@ -29,7 +29,7 @@ export const menuConfigDefault = {
|
||||
buttons: {
|
||||
camera_ui: visibleButtonDefault,
|
||||
cameras: visibleButtonDefault,
|
||||
clips: visibleButtonDefault,
|
||||
clips: hiddenButtonDefault,
|
||||
ptz_home: hiddenButtonDefault,
|
||||
display_mode: visibleButtonDefault,
|
||||
download: visibleButtonDefault,
|
||||
@@ -38,6 +38,7 @@ export const menuConfigDefault = {
|
||||
iris: visibleButtonDefault,
|
||||
fullscreen: visibleButtonDefault,
|
||||
image: hiddenButtonDefault,
|
||||
info: visibleButtonDefault,
|
||||
live: visibleButtonDefault,
|
||||
media_player: visibleButtonDefault,
|
||||
microphone: {
|
||||
@@ -48,8 +49,10 @@ export const menuConfigDefault = {
|
||||
play: hiddenButtonDefault,
|
||||
ptz_controls: hiddenButtonDefault,
|
||||
recordings: hiddenButtonDefault,
|
||||
reviews: visibleButtonDefault,
|
||||
set_review: visibleButtonDefault,
|
||||
screenshot: hiddenButtonDefault,
|
||||
snapshots: visibleButtonDefault,
|
||||
snapshots: hiddenButtonDefault,
|
||||
substreams: visibleButtonDefault,
|
||||
timeline: visibleButtonDefault,
|
||||
},
|
||||
@@ -76,7 +79,7 @@ export const menuConfigSchema = z
|
||||
.object({
|
||||
camera_ui: visibleButtonSchema.default(menuConfigDefault.buttons.camera_ui),
|
||||
cameras: visibleButtonSchema.default(menuConfigDefault.buttons.cameras),
|
||||
clips: visibleButtonSchema.default(menuConfigDefault.buttons.clips),
|
||||
clips: hiddenButtonSchema.default(menuConfigDefault.buttons.clips),
|
||||
ptz_home: hiddenButtonSchema.default(menuConfigDefault.buttons.ptz_home),
|
||||
display_mode: visibleButtonSchema.default(
|
||||
menuConfigDefault.buttons.display_mode,
|
||||
@@ -87,6 +90,7 @@ export const menuConfigSchema = z
|
||||
iris: visibleButtonSchema.default(menuConfigDefault.buttons.iris),
|
||||
fullscreen: visibleButtonSchema.default(menuConfigDefault.buttons.fullscreen),
|
||||
image: hiddenButtonSchema.default(menuConfigDefault.buttons.image),
|
||||
info: visibleButtonSchema.default(menuConfigDefault.buttons.info),
|
||||
live: visibleButtonSchema.default(menuConfigDefault.buttons.live),
|
||||
media_player: visibleButtonSchema.default(
|
||||
menuConfigDefault.buttons.media_player,
|
||||
@@ -102,8 +106,10 @@ export const menuConfigSchema = z
|
||||
play: hiddenButtonSchema.default(menuConfigDefault.buttons.play),
|
||||
ptz_controls: hiddenButtonSchema.default(menuConfigDefault.buttons.ptz_controls),
|
||||
recordings: hiddenButtonSchema.default(menuConfigDefault.buttons.recordings),
|
||||
reviews: visibleButtonSchema.default(menuConfigDefault.buttons.reviews),
|
||||
set_review: visibleButtonSchema.default(menuConfigDefault.buttons.set_review),
|
||||
screenshot: hiddenButtonSchema.default(menuConfigDefault.buttons.screenshot),
|
||||
snapshots: visibleButtonSchema.default(menuConfigDefault.buttons.snapshots),
|
||||
snapshots: hiddenButtonSchema.default(menuConfigDefault.buttons.snapshots),
|
||||
substreams: visibleButtonSchema.default(menuConfigDefault.buttons.substreams),
|
||||
timeline: visibleButtonSchema.default(menuConfigDefault.buttons.timeline),
|
||||
})
|
||||
|
||||
@@ -23,6 +23,7 @@ export const statusBarConfigDefault = {
|
||||
items: {
|
||||
engine: statusBarItemDefault,
|
||||
resolution: statusBarItemDefault,
|
||||
severity: statusBarItemDefault,
|
||||
technology: statusBarItemDefault,
|
||||
title: statusBarItemDefault,
|
||||
},
|
||||
@@ -50,6 +51,7 @@ export const statusBarConfigSchema = z
|
||||
resolution: statusBarItemBaseSchema.default(
|
||||
statusBarConfigDefault.items.resolution,
|
||||
),
|
||||
severity: statusBarItemBaseSchema.default(statusBarConfigDefault.items.severity),
|
||||
title: statusBarItemBaseSchema.default(statusBarConfigDefault.items.title),
|
||||
})
|
||||
.default(statusBarConfigDefault.items),
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import { z } from 'zod';
|
||||
import { thumbnailsControlSchema } from './common/controls/thumbnails';
|
||||
import {
|
||||
timelineConfigDefault,
|
||||
thumbnailControlsDefaults,
|
||||
thumbnailsControlSchema,
|
||||
} from './common/controls/thumbnails';
|
||||
import {
|
||||
timelineCoreConfigDefault,
|
||||
timelineCoreConfigSchema,
|
||||
} from './common/controls/timeline';
|
||||
|
||||
export const timelineConfigDefault = {
|
||||
...timelineCoreConfigDefault,
|
||||
controls: {
|
||||
thumbnails: thumbnailControlsDefaults,
|
||||
},
|
||||
};
|
||||
|
||||
export const timelineConfigSchema = timelineCoreConfigSchema
|
||||
.extend({
|
||||
controls: z
|
||||
.object({
|
||||
thumbnails: thumbnailsControlSchema.default(
|
||||
timelineConfigDefault.controls.thumbnails,
|
||||
),
|
||||
thumbnails: thumbnailsControlSchema.default(thumbnailControlsDefaults),
|
||||
})
|
||||
.default(timelineConfigDefault.controls),
|
||||
})
|
||||
|
||||
@@ -3,7 +3,6 @@ import { deepRemoveDefaults } from '../../utils/zod';
|
||||
import { automationsSchema } from './automations';
|
||||
import { cameraConfigDefault, cameraConfigSchema, camerasConfigSchema } from './cameras';
|
||||
import { cardIDRegex } from './common/const';
|
||||
import { timelineConfigDefault } from './common/controls/timeline';
|
||||
import { imageConfigDefault } from './common/image';
|
||||
import { DebugConfig, debugConfigDefault, debugConfigSchema } from './debug';
|
||||
import { dimensionsConfigSchema } from './dimensions';
|
||||
@@ -22,7 +21,7 @@ import {
|
||||
import { profilesSchema } from './profiles';
|
||||
import { remoteControlConfigDefault, remoteControlConfigSchema } from './remote-control';
|
||||
import { statusBarConfigDefault, statusBarConfigSchema } from './status-bar';
|
||||
import { timelineConfigSchema } from './timeline';
|
||||
import { timelineConfigDefault, timelineConfigSchema } from './timeline';
|
||||
import { viewConfigDefault, viewConfigSchema } from './view';
|
||||
import { viewerConfigDefault, viewerConfigSchema } from './viewer';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user