diff --git a/package.json b/package.json index 34dc814b..00f06483 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "@material/image-list": "^12.0.0", "custom-card-helpers": "^1.8.0", "dayjs": "^1.10.7", + "dlv": "github:developit/dlv", + "dset": "^3.1.1", "embla-carousel": "^5.0.1", "home-assistant-js-websocket": "^5.11.1", "lit": "^2.0.2", diff --git a/src/card.ts b/src/card.ts index c15199cc..46ff25a2 100644 --- a/src/card.ts +++ b/src/card.ts @@ -20,7 +20,7 @@ import { import screenfull from 'screenfull'; import { z } from 'zod'; -import { entitySchema, frigateCardConfigSchema, MenuInteraction } from './types.js'; +import { entitySchema, frigateCardConfigSchema, MenuInteraction, RawFrigateCardConfig } from './types.js'; import type { BrowseMediaQueryParameters, Entity, @@ -58,6 +58,7 @@ import './patches/ha-hls-player.js'; import cardStyle from './scss/card.scss'; import { ResolvedMediaCache } from './resolved-media.js'; import { BrowseMediaUtil } from './browse-media-util.js'; +import { isConfigUpgradeable } from './config-mgmt.js'; /** A note on media callbacks: * @@ -401,19 +402,25 @@ export class FrigateCard extends LitElement { * Set the card configuration. * @param inputConfig The card configuration. */ - public setConfig(inputConfig: FrigateCardConfig): void { + public setConfig(inputConfig: RawFrigateCardConfig): void { 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); if (!parseResult.success) { const hint = this._getParseErrorPaths(parseResult.error); + let upgradeMessage = ''; + if (configUpgradeable && getLovelace().mode !== 'yaml') { + upgradeMessage = `${localize('editor.upgrade_available_in_editor')}. `; + } throw new Error( + upgradeMessage + `${localize('error.invalid_configuration')}: ` + (hint.length ? JSON.stringify(hint, null, ' ') - : localize('error.invalid_configuration_no_hint')), + : localize('error.invalid_configuration_no_hint')) ); } const config = parseResult.data; diff --git a/src/config-mgmt.ts b/src/config-mgmt.ts new file mode 100644 index 00000000..88055dfc --- /dev/null +++ b/src/config-mgmt.ts @@ -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), +]; diff --git a/src/editor.ts b/src/editor.ts index f5235978..50c720a4 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -4,15 +4,40 @@ import { customElement, property, state } from 'lit/decorators.js'; import { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helpers'; import { localize } from './localize/localize.js'; -import { - FrigateCardConfig, - frigateCardConfigDefaults, - frigateCardConfigSchema, -} from './types.js'; +import { frigateCardConfigDefaults, RawFrigateCardConfig } from './types.js'; 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: { icon: 'cog', name: localize('editor.basic'), @@ -58,7 +83,7 @@ const options = { dimensions: { icon: 'aspect-ratio', name: localize('editor.dimensions'), - secondary: localize('editor.dimensions'), + secondary: localize('editor.dimensions_secondary'), show: false, }, }; @@ -66,16 +91,18 @@ const options = { @customElement('frigate-card-editor') export class FrigateCardEditor extends LitElement implements LovelaceCardEditor { @property({ attribute: false }) public hass?: HomeAssistant; - @state() private _config?: FrigateCardConfig; - @state() private _toggle?: boolean; - @state() private _helpers?: any; - private _initialized = false; + @state() protected _config?: RawFrigateCardConfig; + @state() protected _helpers?: any; + protected _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 // 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._configUpgradeable = isConfigUpgradeable(config); this.loadCardHelpers(); } @@ -102,7 +129,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor } protected render(): TemplateResult | void { - if (!this.hass || !this._helpers) { + if (!this.hass || !this._helpers || !this._config) { return html``; } @@ -111,11 +138,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor this._helpers.importMoreInfoControl('climate'); const cameraEntities = this._getEntities('camera'); - - const webrtcCameraEntity = - this._config?.live?.webrtc && (this._config?.live.webrtc as any).entity - ? (this._config?.live.webrtc as any).entity - : ''; + const webrtcCameraEntity = String( + getConfigValue(this._config, 'live.webrtc.entity', ''), + ); const viewModes = { '': '', @@ -172,8 +197,35 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor const defaults = frigateCardConfigDefaults; return html` + ${this._configUpgradeable + ? html`