Files
advanced-camera-card/src/components-lib/editor/schema/cameras.ts
T
flan 88e1fba401 feat: Add cameras[].media.severities to filter review media by severity
Review queries were issued without a severity filter, so the thumbnail
bar, gallery and timeline showed every Frigate review item -- alerts and
detections alike -- regardless of what a camera is configured to alert
on. Severity could only be narrowed interactively, in the media filter
drawer, and that choice did not persist.

`media.severities` resolves into the default query the same way
`media.reviewed` does, and is overridden by an explicit severity filter
so the media filter drawer keeps working. Unset means all severities, so
existing behaviour is unchanged.

    cameras:
      - camera_entity: camera.office
        media:
          severities:
            - high
2026-09-04 17:56:20 +00:00

524 lines
17 KiB
TypeScript

import type {
HAFormExpandableSchema,
HAFormSchema,
HASelectSelectorOption,
} from '../../../ha/types';
import { localize } from '../../../localize/localize';
import { capabilityKeys } from '../../../types';
import { createGrid } from './common/grid';
import { getImageFieldsSchema } from './common/image';
import { getProxySchema } from './common/proxy';
import { createNumberSelector, createSelectSelector } from './common/selectors';
const percentageSelector = createNumberSelector({ min: 0, max: 100 });
const getMediaLayoutSchema = (): HAFormExpandableSchema => ({
name: 'layout',
type: 'expandable',
title: localize('config.cameras.dimensions.layout.editor_label'),
icon: 'mdi:page-layout-body',
schema: [
{
name: 'fit',
selector: createSelectSelector([
{
value: 'contain',
label: localize('config.cameras.dimensions.layout.fits.contain'),
},
{
value: 'cover',
label: localize('config.cameras.dimensions.layout.fits.cover'),
},
{
value: 'fill',
label: localize('config.cameras.dimensions.layout.fits.fill'),
},
]),
},
{
name: 'position',
type: 'expandable',
title: localize('config.cameras.dimensions.layout.position.editor_label'),
schema: [
createGrid([
{ name: 'x', selector: percentageSelector },
{ name: 'y', selector: percentageSelector },
]),
],
},
{
name: 'view_box',
type: 'expandable',
title: localize('config.cameras.dimensions.layout.view_box.editor_label'),
schema: [
createGrid([
{ name: 'top', selector: percentageSelector },
{ name: 'bottom', selector: percentageSelector },
{ name: 'left', selector: percentageSelector },
{ name: 'right', selector: percentageSelector },
]),
],
},
{
name: 'pan',
type: 'expandable',
title: localize('config.cameras.dimensions.layout.pan.editor_label'),
schema: [
createGrid([
{ name: 'x', selector: percentageSelector },
{ name: 'y', selector: percentageSelector },
]),
],
},
{
name: 'zoom',
selector: createNumberSelector({ min: 1, max: 10 }),
},
],
});
/**
* Get the camera fields the simple editor shows: the entity, how it is
* streamed, and how it is presented.
* @returns The camera fields.
*/
export const getCameraSimpleFields = (): HAFormSchema[] => [
{ name: 'camera_entity', selector: { entity: { domain: 'camera' } } },
{
name: 'live_provider',
selector: createSelectSelector([
{ value: 'auto', label: localize('config.cameras.live_providers.auto') },
{ value: 'ha', label: localize('config.cameras.live_providers.ha') },
{ value: 'image', label: localize('config.cameras.live_providers.image') },
{ value: 'jsmpeg', label: localize('config.cameras.live_providers.jsmpeg') },
{ value: 'go2rtc', label: localize('config.cameras.live_providers.go2rtc') },
{
value: 'go2rtc-experimental',
label: localize('config.cameras.live_providers.go2rtc-experimental'),
},
{
value: 'webrtc-card',
label: localize('config.cameras.live_providers.webrtc-card'),
},
]),
},
createGrid([
{ name: 'title', selector: { text: {} } },
{ name: 'icon', selector: { icon: {} } },
]),
];
interface CameraSchemaOptions {
otherCameras: HASelectSelectorOption[];
folders: HASelectSelectorOption[];
}
/**
* Get the schema for a single camera (everything except the triggers group,
* which is rendered by a hand-built panel so it can host the events array
* list).
* @param options The camera and folder lists the dropdowns choose from.
* @returns The camera form schema.
*/
export const getCameraSchema = (options: CameraSchemaOptions): HAFormSchema[] => {
const capabilityOptions: HASelectSelectorOption[] = capabilityKeys.map((key) => ({
value: key,
label: localize(`config.cameras.capabilities.capabilities.${key}`),
}));
return [
...getCameraSimpleFields(),
{ name: 'id', selector: { text: {} } },
{ name: 'always_error_if_entity_unavailable', selector: { boolean: {} } },
{
name: 'capabilities',
type: 'expandable',
title: localize('config.cameras.capabilities.editor_label'),
icon: 'mdi:cog-stop',
schema: [
{
name: 'disable',
selector: createSelectSelector(capabilityOptions, { multiple: true }),
},
{
name: 'disable_except',
selector: createSelectSelector(capabilityOptions, { multiple: true }),
},
{
name: 'force',
selector: createSelectSelector(
[
{
value: '2-way-audio',
label: localize('config.cameras.capabilities.capabilities.2-way-audio'),
},
],
{ multiple: true },
),
},
],
},
{
name: 'cast',
type: 'expandable',
title: localize('config.cameras.cast.editor_label'),
icon: 'mdi:cast',
schema: [
{
name: 'method',
selector: createSelectSelector([
{
value: 'standard',
label: localize('config.cameras.cast.methods.standard'),
},
{
value: 'dashboard',
label: localize('config.cameras.cast.methods.dashboard'),
},
]),
},
{
name: 'dashboard',
type: 'expandable',
title: localize('config.cameras.cast.dashboard.editor_label'),
schema: [
createGrid([
{ name: 'dashboard_path', selector: { text: {} } },
{ name: 'view_path', selector: { text: {} } },
]),
],
},
],
},
{
name: 'dependencies',
type: 'expandable',
title: localize('config.cameras.dependencies.editor_label'),
icon: 'mdi:graph',
schema: [
{ name: 'all_cameras', selector: { boolean: {} } },
{
name: 'cameras',
selector: createSelectSelector(options.otherCameras, { multiple: true }),
},
],
},
{
name: 'dimensions',
type: 'expandable',
title: localize('config.cameras.dimensions.editor_label'),
icon: 'mdi:aspect-ratio',
schema: [
createGrid([
{ name: 'aspect_ratio', selector: { text: {} } },
{
name: 'rotation',
selector: createSelectSelector([
{ value: 0, label: localize('config.cameras.dimensions.rotations.0') },
{ value: 90, label: localize('config.cameras.dimensions.rotations.90') },
{
value: 180,
label: localize('config.cameras.dimensions.rotations.180'),
},
{
value: 270,
label: localize('config.cameras.dimensions.rotations.270'),
},
]),
},
]),
getMediaLayoutSchema(),
],
},
{
// Nameless: a visual grouping only:
// `engine`/`frigate`/`motioneye`/`reolink` are direct camera keys, not
// nested under an `engine` object.
type: 'expandable',
title: localize('config.cameras.engines.editor_label'),
icon: 'mdi:engine',
docPath: ['cameras', 'engine'],
schema: [
{
name: 'engine',
label: localize('config.cameras.engine'),
selector: createSelectSelector([
{ value: 'auto', label: localize('config.cameras.engines.auto') },
{ value: 'frigate', label: localize('config.cameras.engines.frigate') },
{ value: 'generic', label: localize('config.cameras.engines.generic') },
{ value: 'motioneye', label: localize('config.cameras.engines.motioneye') },
{ value: 'reolink', label: localize('config.cameras.engines.reolink') },
{ value: 'tplink', label: localize('config.cameras.engines.tplink') },
]),
},
{
name: 'frigate',
type: 'expandable',
title: localize('config.cameras.frigate.editor_label'),
icon: 'advanced-camera-card:frigate',
schema: [
{ name: 'camera_name', selector: { text: {} } },
{ name: 'url', selector: { text: {} } },
{ name: 'labels', selector: createSelectSelector([], { multiple: true }) },
{ name: 'zones', selector: createSelectSelector([], { multiple: true }) },
{ name: 'client_id', selector: { text: {} } },
],
},
{
name: 'motioneye',
type: 'expandable',
title: localize('config.cameras.motioneye.editor_label'),
icon: 'advanced-camera-card:motioneye',
schema: [
{ name: 'url', selector: { text: {} } },
{
name: 'images',
type: 'expandable',
title: localize('config.cameras.motioneye.images.editor_label'),
schema: [
createGrid([
{ name: 'directory_pattern', selector: { text: {} } },
{ name: 'file_pattern', selector: { text: {} } },
]),
],
},
{
name: 'movies',
type: 'expandable',
title: localize('config.cameras.motioneye.movies.editor_label'),
schema: [
createGrid([
{ name: 'directory_pattern', selector: { text: {} } },
{ name: 'file_pattern', selector: { text: {} } },
]),
],
},
],
},
{
name: 'reolink',
type: 'expandable',
title: localize('config.cameras.reolink.editor_label'),
icon: 'advanced-camera-card:reolink',
schema: [
{ name: 'url', selector: { text: {} } },
{
name: 'media_resolution',
label: localize('config.cameras.reolink.media_resolution.editor_label'),
selector: createSelectSelector([
{
value: 'high',
label: localize('config.cameras.reolink.media_resolution.high'),
},
{
value: 'low',
label: localize('config.cameras.reolink.media_resolution.low'),
},
]),
},
],
},
],
},
{
// Nameless: a visual grouping only: `go2rtc`/`image`/`webrtc_card` are
// direct camera keys.
type: 'expandable',
title: localize('config.cameras.live_provider_options.editor_label'),
icon: 'mdi:cctv',
docPath: ['cameras', 'live_provider'],
schema: [
{
name: 'go2rtc',
type: 'expandable',
title: localize('config.cameras.go2rtc.editor_label'),
icon: 'mdi:eye',
schema: [
{
name: 'modes',
label: localize('config.cameras.go2rtc.modes.editor_label'),
selector: createSelectSelector(
[
{
value: 'webrtc',
label: localize('config.cameras.go2rtc.modes.webrtc'),
},
{ value: 'mse', label: localize('config.cameras.go2rtc.modes.mse') },
{ value: 'mp4', label: localize('config.cameras.go2rtc.modes.mp4') },
{
value: 'mjpeg',
label: localize('config.cameras.go2rtc.modes.mjpeg'),
},
],
{ multiple: true },
),
},
createGrid([
{ name: 'stream', selector: { text: {} } },
{ name: 'url', selector: { text: {} } },
]),
{ name: 'metadata_fetch_timeout_seconds', selector: createNumberSelector() },
],
},
{
name: 'image',
type: 'expandable',
title: localize('config.cameras.image.editor_label'),
icon: 'mdi:image',
schema: getImageFieldsSchema(),
},
{
name: 'webrtc_card',
type: 'expandable',
title: localize('config.cameras.webrtc_card.editor_label'),
icon: 'mdi:webrtc',
schema: [
{ name: 'entity', selector: { entity: { domain: 'camera' } } },
{ name: 'url', selector: { text: {} } },
],
},
],
},
{
name: 'media',
type: 'expandable',
title: localize('config.cameras.media.editor_label'),
icon: 'mdi:play-box-outline',
schema: [
createGrid([
{
name: 'type',
selector: createSelectSelector([
{ value: 'auto', label: localize('config.common.media_types.auto') },
{
value: 'reviews',
label: localize('config.common.media_types.reviews'),
},
{ value: 'events', label: localize('config.common.media_types.events') },
{
value: 'recordings',
label: localize('config.common.media_types.recordings'),
},
{ value: 'folder', label: localize('config.common.media_types.folder') },
]),
},
{
name: 'events_type',
selector: createSelectSelector([
{ value: 'all', label: localize('config.common.events_media_types.all') },
{
value: 'clips',
label: localize('config.common.events_media_types.clips'),
},
{
value: 'snapshots',
label: localize('config.common.events_media_types.snapshots'),
},
]),
},
]),
{
name: 'reviewed',
selector: createSelectSelector([
{
value: 'unreviewed',
label: localize('config.cameras.media.revieweds.unreviewed'),
},
{
value: 'reviewed',
label: localize('config.cameras.media.revieweds.reviewed'),
},
{ value: 'all', label: localize('config.cameras.media.revieweds.all') },
]),
},
{
name: 'severities',
label: localize('config.cameras.media.severities'),
selector: createSelectSelector(
[
{ value: 'high', label: localize('common.severities.high') },
{ value: 'medium', label: localize('common.severities.medium') },
{ value: 'low', label: localize('common.severities.low') },
],
{ multiple: true },
),
},
{
name: 'folders',
selector: createSelectSelector(options.folders, { multiple: true }),
},
],
},
getProxySchema({
title: localize('config.cameras.proxy.editor_label'),
includeLive: true,
includeMedia: true,
}),
];
};
/**
* Get the schema for a camera's trigger scalars (the events list is rendered
* separately by the array editor).
* @param entities The entity IDs offered for the trigger-entities field.
* @returns The triggers form schema.
*/
export const getCameraTriggersSchema = (): HAFormSchema[] => [
{ name: 'occupancy', selector: { boolean: {} } },
{ name: 'motion', selector: { boolean: {} } },
{ name: 'doorbell', selector: { boolean: {} } },
{ name: 'entities', selector: { entity: { multiple: true } } },
{
name: 'media_events',
label: localize('config.cameras.triggers.media_events.editor_label'),
selector: createSelectSelector(
[
{
value: 'events',
label: localize('config.cameras.triggers.media_events.events'),
},
{
value: 'clips',
label: localize('config.cameras.triggers.media_events.clips'),
},
{
value: 'snapshots',
label: localize('config.cameras.triggers.media_events.snapshots'),
},
],
{ multiple: true },
),
},
{
name: 'reviews',
type: 'expandable',
title: localize('config.cameras.triggers.reviews.editor_label'),
icon: 'mdi:check-circle',
schema: [
{
name: 'severities',
label: localize('common.severity'),
selector: createSelectSelector(
[
{ value: 'high', label: localize('common.severities.high') },
{ value: 'medium', label: localize('common.severities.medium') },
{ value: 'low', label: localize('common.severities.low') },
],
{ multiple: true },
),
},
{ name: 'description', selector: { boolean: {} } },
],
},
];
/**
* Get the schema for a single Home Assistant event trigger.
* @returns The event form schema.
*/
export const getTriggerEventSchema = (): HAFormSchema[] => [
{ name: 'event_type', selector: { text: {} } },
{ name: 'event_data', selector: { object: {} } },
];