Basic configuration upgrade support.

This commit is contained in:
Dermot Duffy
2021-11-06 10:11:12 -07:00
parent 80218957b6
commit be76d5bb24
8 changed files with 462 additions and 147 deletions
+2
View File
@@ -19,6 +19,8 @@
"@material/image-list": "^12.0.0", "@material/image-list": "^12.0.0",
"custom-card-helpers": "^1.8.0", "custom-card-helpers": "^1.8.0",
"dayjs": "^1.10.7", "dayjs": "^1.10.7",
"dlv": "github:developit/dlv",
"dset": "^3.1.1",
"embla-carousel": "^5.0.1", "embla-carousel": "^5.0.1",
"home-assistant-js-websocket": "^5.11.1", "home-assistant-js-websocket": "^5.11.1",
"lit": "^2.0.2", "lit": "^2.0.2",
+11 -4
View File
@@ -20,7 +20,7 @@ import {
import screenfull from 'screenfull'; import screenfull from 'screenfull';
import { z } from 'zod'; import { z } from 'zod';
import { entitySchema, frigateCardConfigSchema, MenuInteraction } from './types.js'; import { entitySchema, frigateCardConfigSchema, MenuInteraction, RawFrigateCardConfig } from './types.js';
import type { import type {
BrowseMediaQueryParameters, BrowseMediaQueryParameters,
Entity, Entity,
@@ -58,6 +58,7 @@ import './patches/ha-hls-player.js';
import cardStyle from './scss/card.scss'; import cardStyle from './scss/card.scss';
import { ResolvedMediaCache } from './resolved-media.js'; import { ResolvedMediaCache } from './resolved-media.js';
import { BrowseMediaUtil } from './browse-media-util.js'; import { BrowseMediaUtil } from './browse-media-util.js';
import { isConfigUpgradeable } from './config-mgmt.js';
/** A note on media callbacks: /** A note on media callbacks:
* *
@@ -401,19 +402,25 @@ export class FrigateCard extends LitElement {
* Set the card configuration. * Set the card configuration.
* @param inputConfig The card configuration. * @param inputConfig The card configuration.
*/ */
public setConfig(inputConfig: FrigateCardConfig): void { public setConfig(inputConfig: RawFrigateCardConfig): void {
if (!inputConfig) { if (!inputConfig) {
throw new Error(localize('error.invalid_configuration:')); throw new Error(localize('error.invalid_configuration'));
} }
const configUpgradeable = isConfigUpgradeable(inputConfig);
const parseResult = frigateCardConfigSchema.safeParse(inputConfig); const parseResult = frigateCardConfigSchema.safeParse(inputConfig);
if (!parseResult.success) { if (!parseResult.success) {
const hint = this._getParseErrorPaths(parseResult.error); const hint = this._getParseErrorPaths(parseResult.error);
let upgradeMessage = '';
if (configUpgradeable && getLovelace().mode !== 'yaml') {
upgradeMessage = `${localize('editor.upgrade_available_in_editor')}. `;
}
throw new Error( throw new Error(
upgradeMessage +
`${localize('error.invalid_configuration')}: ` + `${localize('error.invalid_configuration')}: ` +
(hint.length (hint.length
? JSON.stringify(hint, null, ' ') ? JSON.stringify(hint, null, ' ')
: localize('error.invalid_configuration_no_hint')), : localize('error.invalid_configuration_no_hint'))
); );
} }
const config = parseResult.data; const config = parseResult.data;
+152
View File
@@ -0,0 +1,152 @@
import delve from 'dlv';
import { dset } from 'dset';
import { RawFrigateCardConfig } from './types';
/**
* Set a configuration value.
* @param obj The configuration.
* @param key The key to the property to set.
* @param value The value to set.
*/
export const setConfigValue = (
obj: RawFrigateCardConfig,
key: string,
value: unknown,
): void => {
dset(obj, key, value);
};
/**
* Get a configuration value.
* @param obj The configuration.
* @param key The key to the property to retrieve.
* @returns The property or undefined if not found.
*/
export const getConfigValue = (obj: RawFrigateCardConfig, key: string, def?: unknown): unknown => {
return delve(obj, key, def);
};
/**
* Delete a configuration value.
* @param obj The configuration.
* @param key The key to the property to delete.
*/
export const deleteConfigValue = (obj: RawFrigateCardConfig, key: string): void => {
let id = key;
let targetObj: unknown = obj;
if (key && key.split && key.includes('.')) {
const keys = key.split('.');
id = keys[keys.length - 1];
targetObj = getConfigValue(obj, keys.slice(0, -1).join('.'));
}
if (targetObj && typeof targetObj === 'object') {
delete targetObj[id];
}
};
/**
* Upgrade a configuration.
* @param obj The configuration to upgrade.
* @returns `true` if the configuration is modified.
*/
export const upgradeConfig = function (obj: RawFrigateCardConfig): boolean {
let upgraded = false;
for (let i = 0; i < UPGRADES.length; i++) {
upgraded = UPGRADES[i](obj) || upgraded;
}
trimConfig(obj);
return upgraded;
};
/**
* Determine if a configuration is automatically upgradeable.
* @param obj The configuration. It is not modified.
* @returns `true` if the configuration is upgradeable.
*/
export const isConfigUpgradeable = function (obj: RawFrigateCardConfig): boolean {
const newObj = JSON.parse(JSON.stringify(obj));
return upgradeConfig(newObj);
};
/**
* Remove empty sections from a configuration.
* @param obj Configuration object.
*/
export const trimConfig = function (obj: RawFrigateCardConfig): void {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (typeof obj[key] === 'object' && obj[key] != null) {
trimConfig(obj[key] as RawFrigateCardConfig);
if (!Object.keys(obj[key] as RawFrigateCardConfig).length) {
delete obj[key];
}
}
}
};
/**
* Copy a configuration.
* @param obj Configuration to copy.
* @returns A new deeply-copied configuration.
*/
export const copyConfig = function (obj: RawFrigateCardConfig): RawFrigateCardConfig {
return JSON.parse(JSON.stringify(obj));
}
/**
* Determines if a property is not an object.
* @param value The property.
* @returns `true` is the value is not an object.
*/
const isNotObject = function (value: unknown) {
return typeof value !== 'object' ? value : undefined;
};
/**
* Move a property from one location to another.
* @param oldPath The old property path.
* @param newPath The new property path.
* @param transform An optional transform for the value.
* @returns `true` if the configuration was modified.
*/
const upgradeMoveTo = function (
oldPath: string,
newPath: string,
transform?: (valueIn: unknown) => unknown,
): (obj: RawFrigateCardConfig) => boolean {
return function (obj: RawFrigateCardConfig): boolean {
let value = getConfigValue(obj, oldPath);
if (transform) {
value = transform(value);
}
if (typeof value !== 'undefined') {
deleteConfigValue(obj, oldPath);
setConfigValue(obj, newPath, value);
return true;
}
return false;
};
};
const UPGRADES = [
// v1.2.1 -> v2.0.0
upgradeMoveTo('frigate_url', 'frigate.url'),
upgradeMoveTo('frigate_client_id', 'frigate.client_id'),
upgradeMoveTo('frigate_camera_name', 'frigate.camera_name'),
upgradeMoveTo('label', 'frigate.label'),
upgradeMoveTo('zone', 'frigate.zone'),
upgradeMoveTo('view_default', 'view.default'),
upgradeMoveTo('view_timeout', 'view.timeout'),
upgradeMoveTo('live_provider', 'live.provider'),
upgradeMoveTo('live_preload', 'live.preload'),
upgradeMoveTo('webrtc', 'live.webrtc'),
upgradeMoveTo('autoplay_clip', 'event_viewer.autoplay_clip'),
upgradeMoveTo('controls.nextprev', 'event_viewer.controls.next_previous.style'),
upgradeMoveTo('controls.nextprev_size', 'event_viewer.controls.next_previous.size'),
upgradeMoveTo('menu_mode', 'menu.mode'),
upgradeMoveTo('menu_buttons', 'menu.buttons'),
upgradeMoveTo('menu_button_size', 'menu.button_size'),
upgradeMoveTo('image', 'image.src', isNotObject),
];
+270 -136
View File
@@ -4,15 +4,40 @@ import { customElement, property, state } from 'lit/decorators.js';
import { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helpers'; import { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helpers';
import { localize } from './localize/localize.js'; import { localize } from './localize/localize.js';
import { import { frigateCardConfigDefaults, RawFrigateCardConfig } from './types.js';
FrigateCardConfig,
frigateCardConfigDefaults,
frigateCardConfigSchema,
} from './types.js';
import frigate_card_editor_style from './scss/editor.scss'; import frigate_card_editor_style from './scss/editor.scss';
import {
copyConfig,
deleteConfigValue,
getConfigValue,
isConfigUpgradeable,
setConfigValue,
trimConfig,
upgradeConfig,
} from './config-mgmt.js';
const options = { interface EditorOptionsSet {
icon: string;
name: string;
secondary: string;
show: boolean;
}
interface EditorOptions {
[setName: string]: EditorOptionsSet;
}
interface EditorOptionTarget {
configValue: string;
checked?: boolean;
value?: string;
}
interface EditorOptionSetTarget {
optionSetName: string;
}
const options: EditorOptions = {
basic: { basic: {
icon: 'cog', icon: 'cog',
name: localize('editor.basic'), name: localize('editor.basic'),
@@ -58,7 +83,7 @@ const options = {
dimensions: { dimensions: {
icon: 'aspect-ratio', icon: 'aspect-ratio',
name: localize('editor.dimensions'), name: localize('editor.dimensions'),
secondary: localize('editor.dimensions'), secondary: localize('editor.dimensions_secondary'),
show: false, show: false,
}, },
}; };
@@ -66,16 +91,18 @@ const options = {
@customElement('frigate-card-editor') @customElement('frigate-card-editor')
export class FrigateCardEditor extends LitElement implements LovelaceCardEditor { export class FrigateCardEditor extends LitElement implements LovelaceCardEditor {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: FrigateCardConfig; @state() protected _config?: RawFrigateCardConfig;
@state() private _toggle?: boolean; @state() protected _helpers?: any;
@state() private _helpers?: any; protected _initialized = false;
private _initialized = false; protected _configUpgradeable = false;
public setConfig(config: FrigateCardConfig): void { public setConfig(config: RawFrigateCardConfig): void {
// Note: This does not use Zod to parse the configuration, so it may be // Note: This does not use Zod to parse the configuration, so it may be
// partially or completely invalid. It's more useful to have a partially // partially or completely invalid. It's more useful to have a partially
// valid configuration here, to allow the user to fix the broken parts. // valid configuration here, to allow the user to fix the broken parts. As
// such, RawFrigateCardConfig is used as the type.
this._config = config; this._config = config;
this._configUpgradeable = isConfigUpgradeable(config);
this.loadCardHelpers(); this.loadCardHelpers();
} }
@@ -102,7 +129,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
} }
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
if (!this.hass || !this._helpers) { if (!this.hass || !this._helpers || !this._config) {
return html``; return html``;
} }
@@ -111,11 +138,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
this._helpers.importMoreInfoControl('climate'); this._helpers.importMoreInfoControl('climate');
const cameraEntities = this._getEntities('camera'); const cameraEntities = this._getEntities('camera');
const webrtcCameraEntity = String(
const webrtcCameraEntity = getConfigValue(this._config, 'live.webrtc.entity', ''),
this._config?.live?.webrtc && (this._config?.live.webrtc as any).entity );
? (this._config?.live.webrtc as any).entity
: '';
const viewModes = { const viewModes = {
'': '', '': '',
@@ -172,8 +197,35 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
const defaults = frigateCardConfigDefaults; const defaults = frigateCardConfigDefaults;
return html` return html`
${this._configUpgradeable
? html` <div class="upgrade">
<span>${localize('editor.upgrade_available')}</span>
<span>
<mwc-button
raised
label="${localize('editor.upgrade')}"
@click=${() => {
if (this._config) {
const upgradedConfig = copyConfig(this._config);
upgradeConfig(upgradedConfig);
this._config = upgradedConfig;
fireEvent(this, 'config-changed', { config: this._config });
this.requestUpdate();
}
}}
>
</mwc-button>
</span>
</div>
<br />`
: html``}
<div class="card-config"> <div class="card-config">
<div class="option" @click=${this._toggleOption} .option=${'basic'}> <div
class="option"
@click=${this._toggleOptionHandler}
.optionSetName=${'basic'}
>
<div class="row"> <div class="row">
<ha-icon .icon=${`mdi:${options.basic.icon}`}></ha-icon> <ha-icon .icon=${`mdi:${options.basic.icon}`}></ha-icon>
<div class="title">${options.basic.name}</div> <div class="title">${options.basic.name}</div>
@@ -185,13 +237,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<div class="values"> <div class="values">
<paper-dropdown-menu <paper-dropdown-menu
label=${localize('config.camera_entity')} label=${localize('config.camera_entity')}
@value-changed=${this._valueChanged} @value-changed=${this._valueChangedHandler}
.configValue=${'camera_entity'} .configValue=${'camera_entity'}
> >
<paper-listbox <paper-listbox
slot="dropdown-content" slot="dropdown-content"
.selected=${cameraEntities.indexOf( .selected=${cameraEntities.indexOf(
this._config?.camera_entity || '', String(getConfigValue(this._config, 'camera_entity', '')),
)} )}
> >
${cameraEntities.map((entity) => { ${cameraEntities.map((entity) => {
@@ -202,7 +254,11 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
</div> </div>
` `
: ''} : ''}
<div class="option" @click=${this._toggleOption} .option=${'frigate'}> <div
class="option"
@click=${this._toggleOptionHandler}
.optionSetName=${'frigate'}
>
<div class="row"> <div class="row">
<ha-icon .icon=${`mdi:${options.frigate.icon}`}></ha-icon> <ha-icon .icon=${`mdi:${options.frigate.icon}`}></ha-icon>
<div class="title">${options.frigate.name}</div> <div class="title">${options.frigate.name}</div>
@@ -214,38 +270,38 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<div class="values"> <div class="values">
<paper-input <paper-input
label=${localize('config.frigate.camera_name')} label=${localize('config.frigate.camera_name')}
.value=${this._config?.frigate?.camera_name || ''} .value=${getConfigValue(this._config, 'frigate.camera_name', '')}
.configValue=${'frigate.camera_name'} .configValue=${'frigate.camera_name'}
@value-changed=${this._valueChanged} @value-changed=${this._valueChangedHandler}
></paper-input> ></paper-input>
<paper-input <paper-input
label=${localize('config.frigate.url')} label=${localize('config.frigate.url')}
.value=${this._config?.frigate?.url || ''} .value=${getConfigValue(this._config, 'frigate.url', '')}
.configValue=${'frigate.url'} .configValue=${'frigate.url'}
@value-changed=${this._valueChanged} @value-changed=${this._valueChangedHandler}
></paper-input> ></paper-input>
<paper-input <paper-input
.label=${localize('config.frigate.label')} .label=${localize('config.frigate.label')}
.value=${this._config?.frigate?.label || ''} .value=${getConfigValue(this._config, 'frigate.label', '')}
.configValue=${'frigate.label'} .configValue=${'frigate.label'}
@value-changed=${this._valueChanged} @value-changed=${this._valueChangedHandler}
></paper-input> ></paper-input>
<paper-input <paper-input
.label=${localize('config.frigate.zone')} .label=${localize('config.frigate.zone')}
.value=${this._config?.frigate?.zone || ''} .value=${getConfigValue(this._config, 'frigate.zone', '')}
.configValue=${'frigate.zone'} .configValue=${'frigate.zone'}
@value-changed=${this._valueChanged} @value-changed=${this._valueChangedHandler}
></paper-input> ></paper-input>
<paper-input <paper-input
label=${localize('config.frigate.client_id')} label=${localize('config.frigate.client_id')}
.value=${this._config?.frigate?.client_id || ''} .value=${getConfigValue(this._config, 'frigate.client_id', '')}
.configValue=${'frigate.client_id'} .configValue=${'frigate.client_id'}
@value-changed=${this._valueChanged} @value-changed=${this._valueChangedHandler}
></paper-input> ></paper-input>
</div> </div>
` `
: ''} : ''}
<div class="option" @click=${this._toggleOption} .option=${'view'}> <div class="option" @click=${this._toggleOptionHandler} .optionSetName=${'view'}>
<div class="row"> <div class="row">
<ha-icon .icon=${`mdi:${options.view.icon}`}></ha-icon> <ha-icon .icon=${`mdi:${options.view.icon}`}></ha-icon>
<div class="title">${options.view.name}</div> <div class="title">${options.view.name}</div>
@@ -257,13 +313,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<div class="values"> <div class="values">
<paper-dropdown-menu <paper-dropdown-menu
label=${localize('config.view.default')} label=${localize('config.view.default')}
@value-changed=${this._valueChanged} @value-changed=${this._valueChangedHandler}
.configValue=${'view.default'} .configValue=${'view.default'}
> >
<paper-listbox <paper-listbox
slot="dropdown-content" slot="dropdown-content"
.selected=${Object.keys(viewModes).indexOf( .selected=${Object.keys(viewModes).indexOf(
this._config?.view?.default || '', String(getConfigValue(this._config, 'view.default', '')),
)} )}
> >
${Object.keys(viewModes).map((key) => { ${Object.keys(viewModes).map((key) => {
@@ -277,16 +333,14 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
label=${localize('config.view.timeout')} label=${localize('config.view.timeout')}
prevent-invalid-input prevent-invalid-input
allowed-pattern="[0-9]" allowed-pattern="[0-9]"
.value=${this._config?.view?.timeout .value=${getConfigValue(this._config, 'view.timeout', '')}
? String(this._config?.view?.timeout)
: ''}
.configValue=${'view.timeout'} .configValue=${'view.timeout'}
@value-changed=${this._valueChanged} @value-changed=${this._valueChangedHandler}
></paper-input> ></paper-input>
</div> </div>
` `
: ''} : ''}
<div class="option" @click=${this._toggleOption} .option=${'menu'}> <div class="option" @click=${this._toggleOptionHandler} .optionSetName=${'menu'}>
<div class="row"> <div class="row">
<ha-icon .icon=${`mdi:${options.menu.icon}`}></ha-icon> <ha-icon .icon=${`mdi:${options.menu.icon}`}></ha-icon>
<div class="title">${options.menu.name}</div> <div class="title">${options.menu.name}</div>
@@ -298,13 +352,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<div class="values"> <div class="values">
<paper-dropdown-menu <paper-dropdown-menu
.label=${localize('config.menu.mode')} .label=${localize('config.menu.mode')}
@value-changed=${this._valueChanged} @value-changed=${this._valueChangedHandler}
.configValue=${'menu.mode'} .configValue=${'menu.mode'}
> >
<paper-listbox <paper-listbox
slot="dropdown-content" slot="dropdown-content"
.selected=${Object.keys(menuModes).indexOf( .selected=${Object.keys(menuModes).indexOf(
this._config?.menu?.mode || '', String(getConfigValue(this._config, 'menu.mode', '')),
)} )}
> >
${Object.keys(menuModes).map((key) => { ${Object.keys(menuModes).map((key) => {
@@ -316,9 +370,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
</paper-dropdown-menu> </paper-dropdown-menu>
<paper-input <paper-input
label=${localize('config.menu.button_size')} label=${localize('config.menu.button_size')}
.value=${this._config?.menu?.button_size || ''} .value=${getConfigValue(this._config, 'menu.button_size', '')}
.configValue=${'menu.button_size'} .configValue=${'menu.button_size'}
@value-changed=${this._valueChanged} @value-changed=${this._valueChangedHandler}
></paper-input> ></paper-input>
<ha-formfield <ha-formfield
.label=${localize('editor.show_button') + .label=${localize('editor.show_button') +
@@ -326,10 +380,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
localize('config.menu.buttons.frigate')} localize('config.menu.buttons.frigate')}
> >
<ha-switch <ha-switch
.checked=${this._config?.menu?.buttons?.frigate ?? .checked="${getConfigValue(
defaults.menu.buttons.frigate} this._config,
'menu.buttons.frigate',
defaults.menu.buttons.frigate,
)},"
.configValue=${'menu.buttons.frigate'} .configValue=${'menu.buttons.frigate'}
@change=${this._valueChanged} @change=${this._valueChangedHandler}
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>
<ha-formfield <ha-formfield
@@ -338,10 +395,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
localize('config.view.views.live')} localize('config.view.views.live')}
> >
<ha-switch <ha-switch
.checked=${this._config?.menu?.buttons?.live ?? .checked=${getConfigValue(
defaults.menu.buttons.live} this._config,
'menu.buttons.live',
defaults.menu.buttons.live,
)}
.configValue=${'menu.buttons.live'} .configValue=${'menu.buttons.live'}
@change=${this._valueChanged} @change=${this._valueChangedHandler}
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>
<ha-formfield <ha-formfield
@@ -350,10 +410,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
localize('config.view.views.clips')} localize('config.view.views.clips')}
> >
<ha-switch <ha-switch
.checked=${this._config?.menu?.buttons?.clips ?? .checked=${getConfigValue(
defaults.menu.buttons.clips} this._config,
'menu.buttons.clips',
defaults.menu.buttons.clips,
)}
.configValue=${'menu.buttons.clips'} .configValue=${'menu.buttons.clips'}
@change=${this._valueChanged} @change=${this._valueChangedHandler}
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>
<ha-formfield <ha-formfield
@@ -362,10 +425,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
localize('config.view.views.snapshots')} localize('config.view.views.snapshots')}
> >
<ha-switch <ha-switch
.checked=${this._config?.menu?.buttons?.snapshots ?? .checked=${getConfigValue(
defaults.menu.buttons.snapshots} this._config,
'menu.buttons.snapshots',
defaults.menu.buttons.snapshots,
)}
.configValue=${'menu.buttons.snapshots'} .configValue=${'menu.buttons.snapshots'}
@change=${this._valueChanged} @change=${this._valueChangedHandler}
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>
<ha-formfield <ha-formfield
@@ -374,10 +440,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
localize('config.view.views.image')} localize('config.view.views.image')}
> >
<ha-switch <ha-switch
.checked=${this._config?.menu?.buttons?.image ?? .checked=${getConfigValue(
defaults.menu.buttons.image} this._config,
'menu.buttons.image',
defaults.menu.buttons.image,
)}
.configValue=${'menu.buttons.image'} .configValue=${'menu.buttons.image'}
@change=${this._valueChanged} @change=${this._valueChangedHandler}
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>
<ha-formfield <ha-formfield
@@ -386,10 +455,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
localize('config.menu.buttons.download')} localize('config.menu.buttons.download')}
> >
<ha-switch <ha-switch
.checked=${this._config?.menu?.buttons?.download ?? .checked=${getConfigValue(
defaults.menu.buttons.download} this._config,
'menu.buttons.download',
defaults.menu.buttons.download,
)}
.configValue=${'menu.buttons.download'} .configValue=${'menu.buttons.download'}
@change=${this._valueChanged} @change=${this._valueChangedHandler}
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>
<ha-formfield <ha-formfield
@@ -398,10 +470,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
localize('config.menu.buttons.frigate_ui')} localize('config.menu.buttons.frigate_ui')}
> >
<ha-switch <ha-switch
.checked=${this._config?.menu?.buttons?.frigate_ui ?? .checked=${getConfigValue(
defaults.menu.buttons.frigate_ui} this._config,
'menu.buttons.frigate_ui',
defaults.menu.buttons.frigate_ui,
)}
.configValue=${'menu.buttons.frigate_ui'} .configValue=${'menu.buttons.frigate_ui'}
@change=${this._valueChanged} @change=${this._valueChangedHandler}
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>
<ha-formfield <ha-formfield
@@ -410,16 +485,19 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
localize('config.menu.buttons.fullscreen')} localize('config.menu.buttons.fullscreen')}
> >
<ha-switch <ha-switch
.checked=${this._config?.menu?.buttons?.fullscreen ?? .checked=${getConfigValue(
defaults.menu.buttons.fullscreen} this._config,
'menu.buttons.fullscreen',
defaults.menu.buttons.fullscreen,
)}
.configValue=${'menu.buttons.fullscreen'} .configValue=${'menu.buttons.fullscreen'}
@change=${this._valueChanged} @change=${this._valueChangedHandler}
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>
</div> </div>
` `
: ''} : ''}
<div class="option" @click=${this._toggleOption} .option=${'live'}> <div class="option" @click=${this._toggleOptionHandler} .optionSetName=${'live'}>
<div class="row"> <div class="row">
<ha-icon .icon=${`mdi:${options.live.icon}`}></ha-icon> <ha-icon .icon=${`mdi:${options.live.icon}`}></ha-icon>
<div class="title">${options.live.name}</div> <div class="title">${options.live.name}</div>
@@ -432,20 +510,24 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<br /> <br />
<ha-formfield .label=${localize('config.live.preload')}> <ha-formfield .label=${localize('config.live.preload')}>
<ha-switch <ha-switch
.checked=${this._config?.live?.preload ?? defaults.live.preload} .checked=${getConfigValue(
this._config,
'live.preload',
defaults.live.preload,
)}
.configValue=${'live.preload'} .configValue=${'live.preload'}
@change=${this._valueChanged} @change=${this._valueChangedHandler}
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>
<paper-dropdown-menu <paper-dropdown-menu
.label=${localize('config.live.provider')} .label=${localize('config.live.provider')}
@value-changed=${this._valueChanged} @value-changed=${this._valueChangedHandler}
.configValue=${'live.provider'} .configValue=${'live.provider'}
> >
<paper-listbox <paper-listbox
slot="dropdown-content" slot="dropdown-content"
.selected=${Object.keys(liveProviders).indexOf( .selected=${Object.keys(liveProviders).indexOf(
this._config?.live?.provider || '', String(getConfigValue(this._config, 'live.provider', '')),
)} )}
> >
${Object.keys(liveProviders).map((key) => { ${Object.keys(liveProviders).map((key) => {
@@ -457,7 +539,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
</paper-dropdown-menu> </paper-dropdown-menu>
<paper-dropdown-menu <paper-dropdown-menu
.label=${localize('config.live.webrtc.entity')} .label=${localize('config.live.webrtc.entity')}
@value-changed=${this._valueChanged} @value-changed=${this._valueChangedHandler}
.configValue=${'live.webrtc.entity'} .configValue=${'live.webrtc.entity'}
> >
<paper-listbox <paper-listbox
@@ -471,14 +553,18 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
</paper-dropdown-menu> </paper-dropdown-menu>
<paper-input <paper-input
label=${localize('config.live.webrtc.url')} label=${localize('config.live.webrtc.url')}
.value=${this._config?.live?.webrtc?.url || ''} .value=${getConfigValue(this._config, 'live.webrtc.url', '')}
.configValue=${'live.webrtc.url'} .configValue=${'live.webrtc.url'}
@value-changed=${this._valueChanged} @value-changed=${this._valueChangedHandler}
></paper-input> ></paper-input>
</div> </div>
` `
: ''} : ''}
<div class="option" @click=${this._toggleOption} .option=${'event_viewer'}> <div
class="option"
@click=${this._toggleOptionHandler}
.optionSetName=${'event_viewer'}
>
<div class="row"> <div class="row">
<ha-icon .icon=${`mdi:${options.event_viewer.icon}`}></ha-icon> <ha-icon .icon=${`mdi:${options.event_viewer.icon}`}></ha-icon>
<div class="title">${options.event_viewer.name}</div> <div class="title">${options.event_viewer.name}</div>
@@ -490,29 +576,41 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<br /> <br />
<ha-formfield .label=${localize('config.event_viewer.autoplay_clip')}> <ha-formfield .label=${localize('config.event_viewer.autoplay_clip')}>
<ha-switch <ha-switch
.checked=${this._config?.event_viewer?.autoplay_clip ?? .checked=${getConfigValue(
defaults.event_viewer.autoplay_clip} this._config,
'event_viewer.autoplay_clip',
defaults.event_viewer.autoplay_clip,
)}
.configValue=${'event_viewer.autoplay_clip'} .configValue=${'event_viewer.autoplay_clip'}
@change=${this._valueChanged} @change=${this._valueChangedHandler}
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>
<ha-formfield .label=${localize('config.event_viewer.lazy_load')}> <ha-formfield .label=${localize('config.event_viewer.lazy_load')}>
<ha-switch <ha-switch
.checked=${this._config?.event_viewer?.lazy_load ?? .checked=${getConfigValue(
defaults.event_viewer.lazy_load} this._config,
'event_viewer.lazy_load',
defaults.event_viewer.lazy_load,
)}
.configValue=${'event_viewer.lazy_load'} .configValue=${'event_viewer.lazy_load'}
@change=${this._valueChanged} @change=${this._valueChangedHandler}
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>
<paper-dropdown-menu <paper-dropdown-menu
.label=${localize('config.event_viewer.controls.next_previous.style')} .label=${localize('config.event_viewer.controls.next_previous.style')}
@value-changed=${this._valueChanged} @value-changed=${this._valueChangedHandler}
.configValue=${'event_viewer.controls.next_previous.style'} .configValue=${'event_viewer.controls.next_previous.style'}
> >
<paper-listbox <paper-listbox
slot="dropdown-content" slot="dropdown-content"
.selected=${Object.keys(eventViewerNextPreviousControlStyles).indexOf( .selected=${Object.keys(eventViewerNextPreviousControlStyles).indexOf(
this._config?.event_viewer?.controls?.next_previous?.style || '', String(
getConfigValue(
this._config,
'event_viewer.controls.next_previous.style',
'',
),
),
)} )}
> >
${Object.keys(eventViewerNextPreviousControlStyles).map((key) => { ${Object.keys(eventViewerNextPreviousControlStyles).map((key) => {
@@ -526,13 +624,21 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
</paper-dropdown-menu> </paper-dropdown-menu>
<paper-input <paper-input
label=${localize('config.event_viewer.controls.next_previous.size')} label=${localize('config.event_viewer.controls.next_previous.size')}
.value=${this._config?.event_viewer?.controls?.next_previous?.size || ''} .value=${getConfigValue(
this._config,
'event_viewer.controls.next_previous.size',
'',
)}
.configValue=${'event_viewer.controls.next_previous.size'} .configValue=${'event_viewer.controls.next_previous.size'}
@value-changed=${this._valueChanged} @value-changed=${this._valueChangedHandler}
></paper-input> ></paper-input>
</div>` </div>`
: ''} : ''}
<div class="option" @click=${this._toggleOption} .option=${'image'}> <div
class="option"
@click=${this._toggleOptionHandler}
.optionSetName=${'image'}
>
<div class="row"> <div class="row">
<ha-icon .icon=${`mdi:${options.image.icon}`}></ha-icon> <ha-icon .icon=${`mdi:${options.image.icon}`}></ha-icon>
<div class="title">${options.image.name}</div> <div class="title">${options.image.name}</div>
@@ -544,15 +650,17 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<paper-input <paper-input
label=${localize('config.image.src')} label=${localize('config.image.src')}
prevent-invalid-input prevent-invalid-input
.value=${this._config?.image?.src .value=${getConfigValue(this._config, 'image.src', '')}
? String(this._config?.image?.src)
: ''}
.configValue=${'image.src'} .configValue=${'image.src'}
@value-changed=${this._valueChanged} @value-changed=${this._valueChangedHandler}
></paper-input> ></paper-input>
</div>` </div>`
: ''} : ''}
<div class="option" @click=${this._toggleOption} .option=${'dimensions'}> <div
class="option"
@click=${this._toggleOptionHandler}
.optionSetName=${'dimensions'}
>
<div class="row"> <div class="row">
<ha-icon .icon=${`mdi:${options.dimensions.icon}`}></ha-icon> <ha-icon .icon=${`mdi:${options.dimensions.icon}`}></ha-icon>
<div class="title">${options.dimensions.name}</div> <div class="title">${options.dimensions.name}</div>
@@ -563,13 +671,15 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
? html` <div class="values"> ? html` <div class="values">
<paper-dropdown-menu <paper-dropdown-menu
.label=${localize('config.dimensions.aspect_ratio_mode')} .label=${localize('config.dimensions.aspect_ratio_mode')}
@value-changed=${this._valueChanged} @value-changed=${this._valueChangedHandler}
.configValue=${'dimensions.aspect_ratio_mode'} .configValue=${'dimensions.aspect_ratio_mode'}
> >
<paper-listbox <paper-listbox
slot="dropdown-content" slot="dropdown-content"
.selected=${Object.keys(aspectRatioModes).indexOf( .selected=${Object.keys(aspectRatioModes).indexOf(
this._config?.dimensions?.aspect_ratio_mode || '', String(
getConfigValue(this._config, 'dimensions.aspect_ratio_mode', ''),
),
)} )}
> >
${Object.keys(aspectRatioModes).map((key) => { ${Object.keys(aspectRatioModes).map((key) => {
@@ -582,11 +692,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<paper-input <paper-input
label=${localize('config.dimensions.aspect_ratio')} label=${localize('config.dimensions.aspect_ratio')}
prevent-invalid-input prevent-invalid-input
.value=${this._config?.dimensions?.aspect_ratio .value=${getConfigValue(this._config, 'dimensions.aspect_ratio', '')}
? String(this._config?.dimensions?.aspect_ratio)
: ''}
.configValue=${'dimensions.aspect_ratio'} .configValue=${'dimensions.aspect_ratio'}
@value-changed=${this._valueChanged} @value-changed=${this._valueChangedHandler}
></paper-input> ></paper-input>
</div>` </div>`
: ''} : ''}
@@ -594,69 +702,95 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
`; `;
} }
private _initialize(): void { /**
* Verify editor is initialized.
*/
protected _initialize(): void {
if (this.hass === undefined) return; if (this.hass === undefined) return;
if (this._config === undefined) return; if (this._config === undefined) return;
if (this._helpers === undefined) return; if (this._helpers === undefined) return;
this._initialized = true; this._initialized = true;
} }
private async loadCardHelpers(): Promise<void> { /**
* Load card helpers.
*/
protected async loadCardHelpers(): Promise<void> {
this._helpers = await (window as any).loadCardHelpers(); this._helpers = await (window as any).loadCardHelpers();
} }
private _toggleOption(ev): void { /**
this._toggleThing(ev, options); * Handle a toggled set of options.
* @param ev The event triggering the change.
*/
protected _toggleOptionHandler(ev: { target: EditorOptionSetTarget | null }): void {
this._toggleOptionSet(ev, options);
} }
private _toggleThing(ev, optionList): void { /**
const show = !optionList[ev.target.option].show; * Toggle display of a set of options (e.g. 'Live')
for (const [key] of Object.entries(optionList)) { * @param ev The event triggering the change.
optionList[key].show = false; * @param options The EditorOptions object.
*/
protected _toggleOptionSet(
ev: { target: EditorOptionSetTarget | null },
options: EditorOptions,
): void {
if (ev && ev.target) {
const show = !options[ev.target.optionSetName].show;
for (const [key] of Object.entries(options)) {
options[key].show = false;
}
options[ev.target.optionSetName].show = show;
this.requestUpdate();
} }
optionList[ev.target.option].show = show;
this._toggle = !this._toggle;
} }
private _valueChanged(ev): void { /**
if (!this._config || !this.hass) { * Handle a changed option value.
* @param ev Event triggering the change.
* @returns
*/
protected _valueChangedHandler(ev: {
target: (EditorOptionTarget & HTMLElement) | null;
}): void {
const target = ev.target;
if (!this._config || !this.hass || !target) {
return; return;
} }
const target = ev.target;
const value = target.value?.trim();
let key: string = target.configValue;
let value;
if ('checked' in target) {
value = target.checked;
} else {
value = target.value?.trim();
}
const key: string = target.configValue;
if (!key) { if (!key) {
return; return;
} }
// Need to deep copy the config so cannot use Object.assign. const newConfig = copyConfig(this._config);
const newConfig = JSON.parse(JSON.stringify(this._config)); if (value === '' || typeof value === 'undefined') {
let objectTarget = newConfig; // Don't delete empty properties that are from a dropdown menu. An empty
// property in that context may just be a user-entered value that is not
if (key.includes('.')) { // in the valid choices in the dropdown. This probably won't end well for
const parts = key.split('.'); // the user anyway, but having the whole property deleted the moment they
objectTarget = parts.slice(0, -1).reduce((obj, key) => { // press a key is very jarring.
if (!(key in obj)) { if (target.tagName != 'PAPER-DROPDOWN-MENU') {
obj[key] = {}; deleteConfigValue(newConfig, key);
} }
return obj[key];
}, newConfig);
key = parts[parts.length - 1];
}
if (value !== undefined && objectTarget[key] === value) {
return;
} else if (value === '') {
delete objectTarget[key];
} else { } else {
objectTarget[key] = target.checked !== undefined ? target.checked : value; setConfigValue(newConfig, key, value);
} }
trimConfig(newConfig);
this._config = newConfig; this._config = newConfig;
fireEvent(this, 'config-changed', { config: this._config }); fireEvent(this, 'config-changed', { config: this._config });
} }
// Return compiled CSS styles (thus safe to use with unsafeCSS). /**
* Return compiled CSS styles.
*/
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return unsafeCSS(frigate_card_editor_style); return unsafeCSS(frigate_card_editor_style);
} }
+10 -7
View File
@@ -101,22 +101,25 @@
}, },
"editor": { "editor": {
"basic": "Basic", "basic": "Basic",
"basic_secondary": "Basic options for most users", "basic_secondary": "Options for most users",
"frigate": "Frigate", "frigate": "Frigate",
"frigate_secondary": "Frigate server options", "frigate_secondary": "Frigate server options",
"view": "View", "view": "View",
"view_secondary": "Card view options", "view_secondary": "What the card should show and how to show it",
"menu": "Menu", "menu": "Menu",
"menu_secondary": "Menu options", "menu_secondary": "Menu look & feel options",
"live": "Live", "live": "Live",
"live_secondary": "Live view options", "live_secondary": "Live camera view options",
"event_viewer": "Event viewer", "event_viewer": "Event viewer",
"event_viewer_secondary": "Event viewer options", "event_viewer_secondary": "Snapshots & clips gallery options",
"image": "Image", "image": "Image",
"image_secondary": "Static image view options", "image_secondary": "Static image view options",
"dimensions": "Dimensions", "dimensions": "Dimensions",
"dimensions_secondary": "Card dimensions options", "dimensions_secondary": "Dimensions & shape options",
"show_button": "Show button" "show_button": "Show button",
"upgrade": "Upgrade",
"upgrade_available": "An automatic card configuration upgrade is available",
"upgrade_available_in_editor": "An automatic card configuration upgrade is available in the editor"
}, },
"error": { "error": {
"empty_response": "Received empty response from Home Assistant for request", "empty_response": "Received empty response from Home Assistant for request",
+12
View File
@@ -25,3 +25,15 @@
ha-formfield { ha-formfield {
padding-bottom: 8px; padding-bottom: 8px;
} }
div.upgrade {
width: auto;
border: 1px dotted var(--primary-color);
margin: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
div.upgrade span {
padding: 10px;
}
+4
View File
@@ -1,3 +1,7 @@
:host {
--video-max-height: none;
}
canvas { canvas {
width: 100%; width: 100%;
display: block; display: block;
+1
View File
@@ -466,6 +466,7 @@ export const frigateCardConfigSchema = z.object({
test_gui: z.boolean().optional(), test_gui: z.boolean().optional(),
}); });
export type FrigateCardConfig = z.infer<typeof frigateCardConfigSchema>; export type FrigateCardConfig = z.infer<typeof frigateCardConfigSchema>;
export type RawFrigateCardConfig = Record<string, unknown>;
export const frigateCardConfigDefaults = { export const frigateCardConfigDefaults = {
frigate: frigateConfigDefault, frigate: frigateConfigDefault,