Support overriding live configuration per camera.
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user