/* eslint-disable @typescript-eslint/no-explicit-any */ import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; 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 frigate_card_editor_style from './scss/editor.scss'; const options = { basic: { icon: 'cog', name: localize('editor.basic'), secondary: localize('editor.basic_secondary'), show: true, }, frigate: { icon: 'alpha-f-box', name: localize('editor.frigate'), secondary: localize('editor.frigate_secondary'), show: false, }, view: { icon: 'eye', name: localize('editor.view'), secondary: localize('editor.view_secondary'), show: false, }, menu: { icon: 'menu', name: localize('editor.menu'), secondary: localize('editor.menu_secondary'), show: false, }, live: { icon: 'cctv', name: localize('editor.live'), secondary: localize('editor.live_secondary'), show: false, }, event_viewer: { icon: 'filmstrip', name: localize('editor.event_viewer'), secondary: localize('editor.event_viewer_secondary'), show: false, }, image: { icon: 'image', name: localize('editor.image'), secondary: localize('editor.image_secondary'), show: false, }, dimensions: { icon: 'aspect-ratio', name: localize('editor.dimensions'), secondary: localize('editor.dimensions'), show: false, }, }; @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; public setConfig(config: FrigateCardConfig): 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. this._config = config; this.loadCardHelpers(); } protected shouldUpdate(): boolean { if (!this._initialized) { this._initialize(); } return true; } protected _getEntities(domain: string): string[] { if (!this.hass) { return []; } const entities = Object.keys(this.hass.states).filter( (eid) => eid.substr(0, eid.indexOf('.')) === domain, ); entities.sort(); // Add a blank entry to unset a selection. entities.unshift(''); return entities; } protected render(): TemplateResult | void { if (!this.hass || !this._helpers) { return html``; } // The climate more-info has ha-switch and paper-dropdown-menu elements that // are lazy loaded unless explicitly loaded via climate here. 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 viewModes = { '': '', live: localize('config.view.views.live'), clips: localize('config.view.views.clips'), snapshots: localize('config.view.views.snapshots'), clip: localize('config.view.views.clip'), snapshot: localize('config.view.views.snapshot'), image: localize('config.view.views.image'), }; const menuModes = { '': '', none: localize('config.menu.modes.none'), 'hidden-top': localize('config.menu.modes.hidden-top'), 'hidden-left': localize('config.menu.modes.hidden-left'), 'hidden-bottom': localize('config.menu.modes.hidden-bottom'), 'hidden-right': localize('config.menu.modes.hidden-right'), 'overlay-top': localize('config.menu.modes.overlay-top'), 'overlay-left': localize('config.menu.modes.overlay-left'), 'overlay-bottom': localize('config.menu.modes.overlay-bottom'), 'overlay-right': localize('config.menu.modes.overlay-right'), 'hover-top': localize('config.menu.modes.hover-top'), 'hover-left': localize('config.menu.modes.hover-left'), 'hover-bottom': localize('config.menu.modes.hover-bottom'), 'hover-right': localize('config.menu.modes.hover-right'), above: localize('config.menu.modes.above'), below: localize('config.menu.modes.below'), }; const liveProviders = { '': '', frigate: localize('config.live.providers.frigate'), 'frigate-jsmpeg': localize('config.live.providers.frigate-jsmpeg'), webrtc: localize('config.live.providers.webrtc'), }; const eventViewerNextPreviousControlStyles = { '': '', thumbnails: localize( 'config.event_viewer.controls.next_previous.styles.thumbnails', ), chevrons: localize('config.event_viewer.controls.next_previous.styles.chevrons'), none: localize('config.event_viewer.controls.next_previous.styles.none'), }; const aspectRatioModes = { '': '', dynamic: localize('config.dimensions.aspect_ratio_modes.dynamic'), static: localize('config.dimensions.aspect_ratio_modes.static'), unconstrained: localize('config.dimensions.aspect_ratio_modes.unconstrained'), }; const defaults = frigateCardConfigDefaults; return html`
${options.basic.name}
${options.basic.secondary}
${options.basic.show ? html`
${cameraEntities.map((entity) => { return html` ${entity} `; })}
` : ''}
${options.frigate.name}
${options.frigate.secondary}
${options.frigate.show ? html`
` : ''}
${options.view.name}
${options.view.secondary}
${options.view.show ? html`
${Object.keys(viewModes).map((key) => { return html` ${viewModes[key]} `; })}
` : ''}
${options.menu.name}
${options.menu.secondary}
${options.menu.show ? html`
${Object.keys(menuModes).map((key) => { return html` ${menuModes[key]} `; })}
` : ''}
${options.live.name}
${options.live.secondary}
${options.live.show ? html`

${Object.keys(liveProviders).map((key) => { return html` ${liveProviders[key]} `; })} ${cameraEntities.map((entity) => { return html` ${entity} `; })}
` : ''}
${options.event_viewer.name}
${options.event_viewer.secondary}
${options.event_viewer.show ? html`

${Object.keys(eventViewerNextPreviousControlStyles).map((key) => { return html` ${eventViewerNextPreviousControlStyles[key]} `; })}
` : ''}
${options.image.name}
${options.image.secondary}
${options.image.show ? html`
` : ''}
${options.dimensions.name}
${options.dimensions.secondary}
${options.dimensions.show ? html`
${Object.keys(aspectRatioModes).map((key) => { return html` ${aspectRatioModes[key]} `; })}
` : ''}
`; } private _initialize(): void { if (this.hass === undefined) return; if (this._config === undefined) return; if (this._helpers === undefined) return; this._initialized = true; } private async loadCardHelpers(): Promise { this._helpers = await (window as any).loadCardHelpers(); } private _toggleOption(ev): void { this._toggleThing(ev, options); } private _toggleThing(ev, optionList): void { const show = !optionList[ev.target.option].show; for (const [key] of Object.entries(optionList)) { optionList[key].show = false; } optionList[ev.target.option].show = show; this._toggle = !this._toggle; } private _valueChanged(ev): void { if (!this._config || !this.hass) { return; } const target = ev.target; const value = target.value?.trim(); let key: string = target.configValue; if (!key) { return; } // Need to deep copy the config so cannot use Object.assign. const newConfig = JSON.parse(JSON.stringify(this._config)); let objectTarget = newConfig; if (key.includes('.')) { const parts = key.split('.'); objectTarget = parts.slice(0, -1).reduce((obj, key) => { if (!(key in obj)) { obj[key] = {}; } return obj[key]; }, newConfig); key = parts[parts.length - 1]; } if (value !== undefined && objectTarget[key] === value) { return; } else if (value === '') { delete objectTarget[key]; } else { objectTarget[key] = target.checked !== undefined ? target.checked : value; } this._config = newConfig; fireEvent(this, 'config-changed', { config: this._config }); } // Return compiled CSS styles (thus safe to use with unsafeCSS). static get styles(): CSSResultGroup { return unsafeCSS(frigate_card_editor_style); } }