Support overriding live configuration per camera.

This commit is contained in:
Dermot Duffy
2022-01-14 21:31:16 -08:00
parent 795ef323bb
commit 7d4f32ce73
8 changed files with 358 additions and 151 deletions
+2
View File
@@ -27,6 +27,7 @@
"embla-carousel": "^5.0.1",
"home-assistant-js-websocket": "^5.11.1",
"lit": "^2.0.2",
"lodash-es": "^4.17.21",
"quick-lru": "github:sindresorhus/quick-lru",
"screenfull": "^5.1.0",
"zod": "^3.11.6"
@@ -37,6 +38,7 @@
"@babel/plugin-proposal-decorators": "^7.15.8",
"@rollup/plugin-image": "^2.1.1",
"@rollup/plugin-json": "^4.1.0",
"@types/lodash-es": "^4.17.5",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"eslint": "^7.32.0",
+77 -10
View File
@@ -1,10 +1,16 @@
import type { FrigateCardCondition } from './types';
import type {
CameraConfig,
FrigateCardCondition,
RawFrigateCardConfig,
} from './types';
import { merge, cloneDeep } from 'lodash-es';
import { View } from './view';
export interface ConditionState {
view?: Readonly<View>;
fullscreen?: boolean;
camera?: string;
camera?: CameraConfig;
}
class ConditionStateRequestEvent extends Event {
@@ -12,18 +18,49 @@ class ConditionStateRequestEvent extends Event {
}
export function evaluateCondition(
condition?: FrigateCardCondition,
state?: ConditionState,
condition?: Readonly<FrigateCardCondition>,
state?: Readonly<ConditionState>,
): boolean {
if (!state) {
return false;
}
let result = true;
if (condition?.view?.length && state?.view) {
result &&= condition?.view.includes(state?.view.view);
if (condition?.view?.length && state.view) {
result &&= condition?.view.includes(state.view.view);
}
if (condition?.fullscreen !== undefined && state?.fullscreen !== undefined) {
result &&= condition?.fullscreen == state?.fullscreen;
if (condition?.fullscreen !== undefined && state.fullscreen !== undefined) {
result &&= condition?.fullscreen == state.fullscreen;
}
if (condition?.camera?.length && state?.camera) {
result &&= condition?.camera.includes(state?.camera);
const evaluateNested = (
input: Readonly<RawFrigateCardConfig>,
condition: Readonly<RawFrigateCardConfig>,
): boolean => {
let result = true;
for (const key of Object.keys(condition)) {
if (typeof condition[key] === 'string') {
// If the test is a literal, it must exactly match.
result &&= input[key] === condition[key];
} else if (Array.isArray(condition[key])) {
// If the test is an array, it's a list of acceptable values.
result &&= (condition[key] as unknown[]).includes(input[key]);
} else if (typeof condition[key] === 'object' && typeof input[key] === 'object') {
// If the test is an object, recursively navigate downwards.
result &&= evaluateNested(
input[key] as RawFrigateCardConfig,
condition[key] as RawFrigateCardConfig,
);
} else if (input[key] === undefined) {
return false;
}
}
return result;
};
if (condition?.camera) {
result &&= state.camera ? evaluateNested(state.camera, condition.camera) : false;
}
return result;
}
@@ -70,3 +107,33 @@ export function conditionStateRequestHandler(
): void {
ev.conditionState = conditionState;
}
type Overrides = {
conditions: FrigateCardCondition;
overrides: RawFrigateCardConfig;
}[];
export function getOverriddenConfig(
config: Readonly<RawFrigateCardConfig>,
conditionState?: Readonly<ConditionState>,
overrides?: Readonly<Overrides>,
): RawFrigateCardConfig {
const overridesSource =
overrides || (config['overrides'] as Readonly<Overrides> | undefined);
if (!overridesSource) {
return config;
}
const output = cloneDeep(config);
let overridden = false;
for (const override of overridesSource) {
if (evaluateCondition(override.conditions, conditionState)) {
merge(output, override.overrides);
overridden = true;
}
}
// Attempt to return the same configuration object if it has not been
// overridden (to reduce re-renders for a configuration that has not changed).
return overridden ? output : config;
}
+16 -5
View File
@@ -22,13 +22,14 @@ import screenfull from 'screenfull';
import { z } from 'zod';
import {
Actions,
ActionType,
CameraConfig,
GetFrigateCardMenuButtonParameters,
LiveConfig,
RawFrigateCardConfig,
entitySchema,
frigateCardConfigSchema,
Actions,
CameraConfig,
} from './types.js';
import type {
Entity,
@@ -74,7 +75,11 @@ import { ResolvedMediaCache } from './resolved-media.js';
import { BrowseMediaUtil } from './browse-media-util.js';
import { isConfigUpgradeable } from './config-mgmt.js';
import { actionHandler } from './action-handler-directive.js';
import { ConditionState, conditionStateRequestHandler } from './card-condition.js';
import {
ConditionState,
conditionStateRequestHandler,
getOverriddenConfig,
} from './card-condition.js';
/** A note on media callbacks:
*
@@ -219,7 +224,8 @@ export class FrigateCard extends LitElement {
this._conditionState = {
view: this._view,
fullscreen: screenfull.isEnabled && screenfull.isFullscreen,
camera: this._view?.camera,
camera:
this._cameras && this._view ? this._cameras.get(this._view.camera) : undefined,
};
}
@@ -1035,7 +1041,11 @@ export class FrigateCard extends LitElement {
let specificActions: Actions | undefined = undefined;
if (this._view?.is('live')) {
specificActions = this.config.live.actions;
const config = getOverriddenConfig(
this.config.live,
this._conditionState,
) as LiveConfig;
specificActions = config.actions;
} else if (this._view?.isGalleryView()) {
specificActions = this.config.event_gallery?.actions;
} else if (this._view?.isViewerView()) {
@@ -1198,6 +1208,7 @@ export class FrigateCard extends LitElement {
.hass=${this._hass}
.view=${this._view}
.liveConfig=${this.config.live}
.conditionState=${this._conditionState}
.cameras=${this._cameras}
.preload=${this.config.live.preload && !this._view.is('live')}
class="${classMap(liveClasses)}"
+73 -18
View File
@@ -1,5 +1,9 @@
// TODO update_entities should reload view rather than be involved in rendering
// TODO different live configs per camera
// TODO replace dset/dlv with lodash
// TODO evaluate single override section instead of per-subcomponent
// TODO fix adaptive height in fullscreen
// TODO Remove id as a concept for cameras and use an array instead?
// TODO Convert menu condition to use overrides
// TODO Remove media load event console message
// TODO Remove view change console message
// TODO readme
@@ -26,10 +30,12 @@ import {
import { EmblaOptionsType } from 'embla-carousel';
import { HomeAssistant } from 'custom-card-helpers';
import { customElement, property, state } from 'lit/decorators.js';
import { isEqual } from 'lodash-es';
import { ref } from 'lit/directives/ref';
import { until } from 'lit/directives/until.js';
import { BrowseMediaUtil } from '../browse-media-util.js';
import { ConditionState, getOverriddenConfig } from '../card-condition.js';
import { FrigateCardMediaCarousel } from './media-carousel.js';
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
@@ -75,6 +81,9 @@ export class FrigateCardLive extends LitElement {
@property({ attribute: false })
protected liveConfig?: LiveConfig;
@property({ attribute: false })
protected conditionState?: ConditionState;
set preload(preload: boolean) {
this._preload = preload;
@@ -108,17 +117,17 @@ export class FrigateCardLive extends LitElement {
* Render thumbnails carousel.
* @returns A rendered template or void.
*/
protected renderThumbnails(): TemplateResult | void {
protected renderThumbnails(config: LiveConfig): TemplateResult | void {
if (!this.liveConfig || !this.view) {
return;
}
const fetchThumbnailsThenRender = async (): Promise<TemplateResult | void> => {
if (!this.hass || !this.cameras || !this.view || !this.liveConfig) {
if (!this.hass || !this.cameras || !this.view) {
return;
}
const browseMediaParams = BrowseMediaUtil.getBrowseMediaQueryParameters(
this.liveConfig.controls.thumbnails.media,
config.controls.thumbnails.media,
this.cameras.get(this.view.camera),
);
if (!browseMediaParams) {
@@ -135,7 +144,7 @@ export class FrigateCardLive extends LitElement {
return html` <frigate-card-thumbnail-carousel
.target=${parent}
.view=${this.view}
.config=${this.liveConfig?.controls.thumbnails}
.config=${config.controls.thumbnails}
.highlightSelected=${false}
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
const mediaType = browseMediaParams.mediaType;
@@ -165,16 +174,23 @@ export class FrigateCardLive extends LitElement {
return;
}
const config = getOverriddenConfig(
this.liveConfig,
this.conditionState,
) as LiveConfig;
// Note use of liveConfig and not config below -- the carousel will
// independently override the liveconfig to reflect the camera in the
// carousel (not necessarily the selected camera).
return html`
${this.liveConfig.controls.thumbnails.mode === 'above'
? this.renderThumbnails()
: ''}
${config.controls.thumbnails.mode === 'above' ? this.renderThumbnails(config) : ''}
<frigate-card-live-carousel
.hass=${this.hass}
.view=${this.view}
.cameras=${this.cameras}
.liveConfig=${this.liveConfig}
.preload=${this._preload}
.conditionState=${this.conditionState}
@frigate-card:media-show=${this._mediaShowHandler}
@frigate-card:carousel:select=${() => {
// Re-rendering the component will cause the thumbnails to be
@@ -183,9 +199,7 @@ export class FrigateCardLive extends LitElement {
}}
>
</frigate-card-live-carousel>
${this.liveConfig.controls.thumbnails.mode === 'below'
? this.renderThumbnails()
: ''}
${config.controls.thumbnails.mode === 'below' ? this.renderThumbnails(config) : ''}
`;
}
@@ -214,6 +228,9 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
@property({ attribute: false })
protected preload?: boolean;
@property({ attribute: false })
protected conditionState?: ConditionState;
// Index between camera name and slide number.
protected _cameraToSlide: Record<string, number> = {};
@@ -332,13 +349,29 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
}
}
protected _renderLive(cameraConfig: CameraConfig, slideIndex: number): TemplateResult {
protected _renderLive(
cameraConfig: CameraConfig,
slideIndex: number,
): TemplateResult | void {
if (!this.liveConfig) {
return;
}
// The conditionState object contains the currently live camera, which (in
// the carousel for example) is not necessarily the live camera this
// <frigate-card-live-provider> is rendering right now.
const conditionState = Object.assign({
...this.conditionState,
camera: cameraConfig,
});
const config = getOverriddenConfig(this.liveConfig, conditionState) as LiveConfig;
return html` <div class="embla__slide">
<frigate-card-live-provider
.title=${getCameraTitle(this.hass, cameraConfig)}
.hass=${this.hass}
.cameraConfig=${cameraConfig}
.liveConfig=${this.liveConfig}
.liveConfig=${config}
?disabled=${this._isLazyLoading()}
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) =>
this._mediaShowEventHandler(slideIndex, e)}
@@ -400,17 +433,22 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
protected render(): TemplateResult | void {
const [slides, cameraToSlide] = this._getSlides();
this._cameraToSlide = cameraToSlide;
if (!slides) {
if (!slides || !this.liveConfig) {
return;
}
const config = getOverriddenConfig(
this.liveConfig,
this.conditionState,
) as LiveConfig;
const [prev, next] = this._getCameraNeighbors();
return html`
<div class="embla">
<frigate-card-next-previous-control
${ref(this._previousControlRef)}
.direction=${'previous'}
.controlConfig=${this.liveConfig?.controls.next_previous}
.controlConfig=${config.controls.next_previous}
.title=${getCameraTitle(this.hass, prev)}
.icon=${getCameraIcon(this.hass, prev)}
?disabled=${prev == null}
@@ -425,7 +463,7 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
<frigate-card-next-previous-control
${ref(this._nextControlRef)}
.direction=${'next'}
.controlConfig=${this.liveConfig?.controls.next_previous}
.controlConfig=${config.controls.next_previous}
.title=${getCameraTitle(this.hass, next)}
.icon=${getCameraIcon(this.hass, next)}
?disabled=${next == null}
@@ -533,7 +571,18 @@ export class FrigateCardLiveFrigate extends LitElement {
// - https://github.com/AlexxIT/WebRTC
@customElement('frigate-card-live-webrtc')
export class FrigateCardLiveWebRTC extends LitElement {
@property({ attribute: false })
@property({
attribute: false,
// Resetting the WebRTC/JSMPEG configuration is expensive as the connections
// need to be re-established. These configurations may be overridden which
// creates semantically equal configurations at different addresses --
// ensure LIT only considers the property as having changed if it's actually
// different.
hasChanged(n: WebRTCConfig, o: WebRTCConfig): boolean {
return !isEqual(n, o);
},
})
protected webRTCConfig?: WebRTCConfig;
@property({ attribute: false })
@@ -638,7 +687,13 @@ export class FrigateCardLiveJSMPEG extends LitElement {
@property({ attribute: false })
protected cameraConfig?: CameraConfig;
@property({ attribute: false })
@property({
attribute: false,
// See note under FrigateCardLiveWebRTC.
hasChanged(n: JSMPEGConfig, o: JSMPEGConfig): boolean {
return !isEqual(n, o);
},
})
protected jsmpegConfig?: JSMPEGConfig;
protected hass?: HomeAssistant & ExtendedHomeAssistant;
+3 -2
View File
@@ -1,5 +1,5 @@
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { NextPreviousControlConfig } from '../types.js';
@@ -11,13 +11,14 @@ export class FrigateCardNextPreviousControl extends LitElement {
@property({ attribute: false })
public direction?: 'next' | 'previous';
@property({ attribute: false })
set controlConfig(controlConfig: NextPreviousControlConfig | undefined) {
if (controlConfig?.size) {
this.style.setProperty('--frigate-card-next-prev-size', controlConfig.size);
}
this._controlConfig = controlConfig;
}
@state()
protected _controlConfig?: NextPreviousControlConfig;
@property({ attribute: false })
+1 -1
View File
@@ -193,7 +193,7 @@ const upgradeMoveTo = function (
};
/**
* Sanitize a potentially-unsafe key segment.
* Upgrade from a singular camera model to multiple.
* @param key A string key.
* @returns A safe key.
*/
+68 -50
View File
@@ -1,62 +1,80 @@
export const CARD_VERSION = '2.1.0';
export const REPO_URL = 'https://github.com/dermotduffy/frigate-hass-card';
export const TROUBLESHOOTING_URL = `${REPO_URL}#troubleshooting`;
export const CARD_VERSION = '2.1.0' as const;
export const REPO_URL = 'https://github.com/dermotduffy/frigate-hass-card' as const;
export const TROUBLESHOOTING_URL = `${REPO_URL}#troubleshooting` as const;
export const CONF_CAMERAS = 'cameras';
export const CONF_CAMERAS_ARRAY_CAMERA_ENTITY = 'cameras.#.camera_entity';
export const CONF_CAMERAS_ARRAY_CAMERA_NAME = 'cameras.#.camera_name';
export const CONF_CAMERAS_ARRAY_CLIENT_ID = 'cameras.#.client_id';
export const CONF_CAMERAS_ARRAY_LABEL = 'cameras.#.label';
export const CONF_CAMERAS_ARRAY_URL = 'cameras.#.frigate_url';
export const CONF_CAMERAS_ARRAY_ZONE = 'cameras.#.zone';
export const CONF_CAMERAS_ARRAY_ID = 'cameras.#.id';
export const CONF_CAMERAS_ARRAY_TITLE = 'cameras.#.title';
export const CONF_CAMERAS_ARRAY_ICON = 'cameras.#.icon';
export const CONF_CAMERAS_ARRAY_WEBRTC_ENTITY = 'cameras.#.webrtc.entity';
export const CONF_CAMERAS_ARRAY_WEBRTC_URL = 'cameras.#.webrtc.url';
export const CONF_CAMERAS = 'cameras' as const;
export const CONF_CAMERAS_ARRAY_CAMERA_ENTITY =
`${CONF_CAMERAS}.#.camera_entity` as const;
export const CONF_CAMERAS_ARRAY_CAMERA_NAME = `${CONF_CAMERAS}.#.camera_name` as const;
export const CONF_CAMERAS_ARRAY_CLIENT_ID = `${CONF_CAMERAS}.#.client_id` as const;
export const CONF_CAMERAS_ARRAY_LABEL = `${CONF_CAMERAS}.#.label` as const;
export const CONF_CAMERAS_ARRAY_URL = `${CONF_CAMERAS}.#.frigate_url` as const;
export const CONF_CAMERAS_ARRAY_ZONE = `${CONF_CAMERAS}.#.zone` as const;
export const CONF_CAMERAS_ARRAY_ID = `${CONF_CAMERAS}.#.id` as const;
export const CONF_CAMERAS_ARRAY_TITLE = `${CONF_CAMERAS}.#.title` as const;
export const CONF_CAMERAS_ARRAY_ICON = `${CONF_CAMERAS}.#.icon` as const;
export const CONF_CAMERAS_ARRAY_WEBRTC_ENTITY =
`${CONF_CAMERAS}.#.webrtc.entity` as const;
export const CONF_CAMERAS_ARRAY_WEBRTC_URL = `${CONF_CAMERAS}.#.webrtc.url` as const;
export const CONF_VIEW_DEFAULT = 'view.default';
export const CONF_VIEW_TIMEOUT = 'view.timeout';
export const CONF_VIEW_UPDATE_FORCE = 'view.update_force';
export const CONF_VIEW_UPDATE_ENTITIES = 'view.update_entities';
export const CONF_VIEW = 'view' as const;
export const CONF_VIEW_DEFAULT = `${CONF_VIEW}.default` as const;
export const CONF_VIEW_TIMEOUT = `${CONF_VIEW}.timeout` as const;
export const CONF_VIEW_UPDATE_FORCE = `${CONF_VIEW}.update_force` as const;
export const CONF_VIEW_UPDATE_ENTITIES = `${CONF_VIEW}.update_entities` as const;
export const CONF_EVENT_VIEWER_AUTOPLAY_CLIP = 'event_viewer.autoplay_clip';
export const CONF_EVENT_VIEWER_DRAGGABLE = 'event_viewer.draggable';
export const CONF_EVENT_VIEWER_LAZY_LOAD = 'event_viewer.lazy_load';
export const CONF_EVENT_VIEWER = 'event_viewer' as const;
export const CONF_EVENT_VIEWER_AUTOPLAY_CLIP =
`${CONF_EVENT_VIEWER}.autoplay_clip` as const;
export const CONF_EVENT_VIEWER_DRAGGABLE = `${CONF_EVENT_VIEWER}.draggable` as const;
export const CONF_EVENT_VIEWER_LAZY_LOAD = `${CONF_EVENT_VIEWER}.lazy_load` as const;
export const CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE =
'event_viewer.controls.next_previous.style';
`${CONF_EVENT_VIEWER}.controls.next_previous.style` as const;
export const CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE =
'event_viewer.controls.next_previous.size';
`${CONF_EVENT_VIEWER}.controls.next_previous.size` as const;
export const CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE =
'event_viewer.controls.thumbnails.mode';
`${CONF_EVENT_VIEWER}.controls.thumbnails.mode` as const;
export const CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE =
'event_viewer.controls.thumbnails.size';
`${CONF_EVENT_VIEWER}.controls.thumbnails.size` as const;
export const CONF_LIVE = 'live' as const;
export const CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE =
'live.controls.next_previous.style';
export const CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE = 'live.controls.next_previous.size';
export const CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA = 'live.controls.thumbnails.media';
export const CONF_LIVE_CONTROLS_THUMBNAILS_MODE = 'live.controls.thumbnails.mode';
export const CONF_LIVE_CONTROLS_THUMBNAILS_SIZE = 'live.controls.thumbnails.size';
export const CONF_LIVE_DRAGGABLE = 'live.draggable';
export const CONF_LIVE_LAZY_LOAD = 'live.lazy_load';
export const CONF_LIVE_PRELOAD = 'live.preload';
export const CONF_LIVE_PROVIDER = 'live.provider';
export const CONF_LIVE_WEBRTC_ENTITY = 'live.webrtc.entity';
export const CONF_LIVE_WEBRTC_URL = 'live.webrtc.url';
`${CONF_LIVE}.controls.next_previous.style` as const;
export const CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE =
`${CONF_LIVE}.controls.next_previous.size` as const;
export const CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA =
`${CONF_LIVE}.controls.thumbnails.media` as const;
export const CONF_LIVE_CONTROLS_THUMBNAILS_MODE =
`${CONF_LIVE}.controls.thumbnails.mode` as const;
export const CONF_LIVE_CONTROLS_THUMBNAILS_SIZE =
`${CONF_LIVE}.controls.thumbnails.size` as const;
export const CONF_LIVE_DRAGGABLE = `${CONF_LIVE}.draggable` as const;
export const CONF_LIVE_JSMPEG = `${CONF_LIVE}.jsmpeg` as const;
export const CONF_LIVE_LAZY_LOAD = `${CONF_LIVE}.lazy_load` as const;
export const CONF_LIVE_PRELOAD = `${CONF_LIVE}.preload` as const;
export const CONF_LIVE_PROVIDER = `${CONF_LIVE}.provider` as const;
export const CONF_LIVE_WEBRTC = `${CONF_LIVE}.webrtc` as const;
export const CONF_LIVE_WEBRTC_ENTITY = `${CONF_LIVE_WEBRTC}.entity` as const;
export const CONF_LIVE_WEBRTC_URL = `${CONF_LIVE_WEBRTC}.url` as const;
export const CONF_IMAGE_SRC = 'image.src';
export const CONF_IMAGE = 'image' as const;
export const CONF_IMAGE_SRC = `${CONF_IMAGE}.src` as const;
export const CONF_MENU_BUTTONS_FRIGATE = 'menu.buttons.frigate';
export const CONF_MENU_BUTTONS_FRIGATE_UI = 'menu.buttons.frigate_ui';
export const CONF_MENU_BUTTONS_FRIGATE_FULLSCREEN = 'menu.buttons.fullscreen';
export const CONF_MENU_BUTTONS_FRIGATE_DOWNLOAD = 'menu.buttons.download';
export const CONF_MENU_BUTTONS_LIVE = 'menu.buttons.live';
export const CONF_MENU_BUTTONS_CLIPS = 'menu.buttons.clips';
export const CONF_MENU_BUTTONS_SNAPSHOTS = 'menu.buttons.snapshots';
export const CONF_MENU_BUTTONS_IMAGE = 'menu.buttons.image';
export const CONF_MENU_BUTTON_SIZE = 'menu.button_size';
export const CONF_MENU_MODE = 'menu.mode';
export const CONF_MENU = 'menu' as const;
export const CONF_MENU_BUTTONS_FRIGATE = `${CONF_MENU}.buttons.frigate` as const;
export const CONF_MENU_BUTTONS_FRIGATE_UI = `${CONF_MENU}.buttons.frigate_ui` as const;
export const CONF_MENU_BUTTONS_FRIGATE_FULLSCREEN =
`${CONF_MENU}.buttons.fullscreen` as const;
export const CONF_MENU_BUTTONS_FRIGATE_DOWNLOAD =
`${CONF_MENU}.buttons.download` as const;
export const CONF_MENU_BUTTONS_LIVE = `${CONF_MENU}.buttons.live` as const;
export const CONF_MENU_BUTTONS_CLIPS = `${CONF_MENU}.buttons.clips` as const;
export const CONF_MENU_BUTTONS_SNAPSHOTS = `${CONF_MENU}.buttons.snapshots` as const;
export const CONF_MENU_BUTTONS_IMAGE = `${CONF_MENU}.buttons.image` as const;
export const CONF_MENU_BUTTON_SIZE = `${CONF_MENU}.button_size` as const;
export const CONF_MENU_MODE = `${CONF_MENU}.mode` as const;
export const CONF_DIMENSIONS_ASPECT_RATIO = 'dimensions.aspect_ratio';
export const CONF_DIMENSIONS_ASPECT_RATIO_MODE = 'dimensions.aspect_ratio_mode';
export const CONF_DIMENSIONS = 'dimensions' as const;
export const CONF_DIMENSIONS_ASPECT_RATIO = `${CONF_DIMENSIONS}.aspect_ratio` as const;
export const CONF_DIMENSIONS_ASPECT_RATIO_MODE =
`${CONF_DIMENSIONS}.aspect_ratio_mode` as const;
+118 -65
View File
@@ -276,6 +276,41 @@ const customSchema = z
})
.passthrough();
/**
* Camera configuration section
*/
export const cameraConfigDefault = {
client_id: 'frigate' as const,
};
const webrtcCameraConfigSchema = z.object({
entity: z.string().optional(),
url: z.string().optional(),
});
const cameraConfigSchema = z
.object({
// No URL validation to allow relative URLs within HA (e.g. Frigate addon).
frigate_url: z.string().optional(),
client_id: z.string().optional().default(cameraConfigDefault.client_id),
camera_name: z.string().optional(),
label: z.string().optional(),
zone: z.string().optional(),
camera_entity: z.string().optional(),
// Used for presentation in the UI (autodetected from the entity if
// specified).
icon: z.string().optional(),
title: z.string().optional(),
// Optional identifier to separate different camera configurations used in
// this card.
id: z.string().optional(),
// Camera identifiers for WebRTC.
webrtc: webrtcCameraConfigSchema.optional(),
})
.default(cameraConfigDefault);
export type CameraConfig = z.infer<typeof cameraConfigSchema>;
/**
* Custom Element Types.
*/
@@ -314,7 +349,9 @@ export type MenuSubmenu = z.infer<typeof menuSubmenuSchema>;
const frigateCardConditionSchema = z.object({
view: z.string().array().optional(),
fullscreen: z.boolean().optional(),
camera: z.string().array().optional(),
// Allow matching any field of cameraConfig.
camera: z.record(z.any()),
});
export type FrigateCardCondition = z.infer<typeof frigateCardConditionSchema>;
@@ -345,39 +382,16 @@ const pictureElementsSchema = pictureElementSchema.array().optional();
export type PictureElements = z.infer<typeof pictureElementsSchema>;
/**
* Camera configuration section
* Configuration overrides
*/
export const cameraConfigDefault = {
client_id: 'frigate' as const,
};
const webrtcCameraConfigSchema = z.object({
entity: z.string().optional(),
url: z.string().optional(),
});
const cameraConfigDefaultSchema = z
.object({
// No URL validation to allow relative URLs within HA (e.g. Frigate addon).
frigate_url: z.string().optional(),
client_id: z.string().optional().default(cameraConfigDefault.client_id),
camera_name: z.string().optional(),
label: z.string().optional(),
zone: z.string().optional(),
camera_entity: z.string().optional(),
// Used for presentation in the UI (autodetected from the entity if
// specified).
icon: z.string().optional(),
title: z.string().optional(),
// Optional identifier to separate different camera configurations used in
// this card.
id: z.string().optional(),
// Camera identifiers for WebRTC.
webrtc: webrtcCameraConfigSchema.optional(),
})
.default(cameraConfigDefault);
export type CameraConfig = z.infer<typeof cameraConfigDefaultSchema>;
const overridesSchema = z
.object({
conditions: frigateCardConditionSchema,
overrides: z.record(z.unknown()),
})
.array()
.optional();
export type Overrides = z.infer<typeof overridesSchema>;
/**
* View configuration section.
@@ -424,12 +438,8 @@ export type ImageViewConfig = z.infer<typeof imageConfigSchema>;
* Thumbnail controls configuration section.
*/
const thumbnailsControlDefault = {
mode: 'none' as const,
};
const thumbnailsControlSchema = z.object({
mode: z.enum(['none', 'above', 'below']).default(thumbnailsControlDefault.mode),
mode: z.enum(['none', 'above', 'below']),
size: z.string().optional(),
});
export type ThumbnailsControlConfig = z.infer<typeof thumbnailsControlSchema>;
@@ -459,6 +469,8 @@ const liveConfigDefault = {
},
thumbnails: {
media: 'clips' as const,
size: '100px',
mode: 'none' as const,
},
},
};
@@ -489,44 +501,77 @@ const jsmpegConfigSchema = z
.optional();
export type JSMPEGConfig = z.infer<typeof jsmpegConfigSchema>;
const liveNextPreviousControlConfigSchema = nextPreviousControlConfigSchema.merge(
z.object({
style: z
.enum(['none', 'chevrons', 'icons'])
.default(liveConfigDefault.controls.next_previous.style),
size: z.string().default(liveConfigDefault.controls.next_previous.size),
}),
);
export type LiveNextPreviousControlConfig = z.infer<
typeof liveNextPreviousControlConfigSchema
>;
const liveNextPreviousControlConfigSchema = nextPreviousControlConfigSchema.extend({
// Live cannot show thumbnails, remove that option.
style: z.enum(['none', 'chevrons', 'icons']),
});
const liveConfigSchema = z
const liveOverridableConfigSchema = z
.object({
provider: z.enum(LIVE_PROVIDERS).default(liveConfigDefault.provider),
preload: z.boolean().default(liveConfigDefault.preload),
provider: z.enum(LIVE_PROVIDERS).optional(),
webrtc: webrtcConfigSchema,
jsmpeg: jsmpegConfigSchema,
lazy_load: z.boolean().default(liveConfigDefault.lazy_load),
draggable: z.boolean().default(liveConfigDefault.draggable),
controls: z
.object({
next_previous: liveNextPreviousControlConfigSchema.default(
liveConfigDefault.controls.next_previous,
),
next_previous: liveNextPreviousControlConfigSchema.optional(),
thumbnails: thumbnailsControlSchema
.merge(
z.object({
media: z
.enum(['clips', 'snapshots'])
.default(liveConfigDefault.controls.thumbnails.media),
media: z.enum(['clips', 'snapshots']),
}),
)
.optional(),
})
.optional(),
})
.merge(actionsSchema);
const liveConfigSchema = liveOverridableConfigSchema
.extend({
// Replace attributes from the overridable schema with those with defaults.
provider: liveOverridableConfigSchema.shape.provider.default(
liveConfigDefault.provider,
),
controls: z
.object({
next_previous: liveNextPreviousControlConfigSchema
.extend({
size: liveNextPreviousControlConfigSchema.shape.size.default(
liveConfigDefault.controls.next_previous.size,
),
style: liveNextPreviousControlConfigSchema.shape.style.default(
liveConfigDefault.controls.next_previous.style,
),
})
.default(liveConfigDefault.controls.next_previous),
thumbnails: thumbnailsControlSchema
.extend({
mode: thumbnailsControlSchema.shape.mode.default(
liveConfigDefault.controls.thumbnails.mode,
),
size: thumbnailsControlSchema.shape.size.default(
liveConfigDefault.controls.thumbnails.size,
),
media: z
.enum(['clips', 'snapshots'])
.default(liveConfigDefault.controls.thumbnails.media),
})
.default(liveConfigDefault.controls.thumbnails),
})
.default(liveConfigDefault.controls),
// Non-overrideable parameters.
preload: z.boolean().default(liveConfigDefault.preload),
lazy_load: z.boolean().default(liveConfigDefault.lazy_load),
draggable: z.boolean().default(liveConfigDefault.draggable),
overrides: z
.object({
conditions: frigateCardConditionSchema,
overrides: liveOverridableConfigSchema,
})
.array()
.optional(),
})
.merge(actionsSchema)
.default(liveConfigDefault);
export type LiveConfig = z.infer<typeof liveConfigSchema>;
@@ -583,6 +628,7 @@ const viewerConfigDefault = {
style: 'thumbnails' as const,
},
thumbnails: {
size: '100px',
mode: 'none' as const,
},
},
@@ -609,9 +655,16 @@ const viewerConfigSchema = z
next_previous: viewerNextPreviousControlConfigSchema.default(
viewerConfigDefault.controls.next_previous,
),
thumbnails: thumbnailsControlSchema.default(
viewerConfigDefault.controls.thumbnails,
),
thumbnails: thumbnailsControlSchema
.extend({
mode: thumbnailsControlSchema.shape.mode.default(
viewerConfigDefault.controls.thumbnails.mode,
),
size: thumbnailsControlSchema.shape.size.default(
viewerConfigDefault.controls.thumbnails.size,
),
})
.default(viewerConfigDefault.controls.thumbnails),
})
.default(viewerConfigDefault.controls),
})
@@ -656,7 +709,7 @@ const dimensionsConfigSchema = z
*/
export const frigateCardConfigSchema = z.object({
// Main configuration sections.
cameras: cameraConfigDefaultSchema.array().nonempty(),
cameras: cameraConfigSchema.array().nonempty(),
view: viewConfigSchema,
menu: menuConfigSchema,
live: liveConfigSchema,