From dafaae7fe4c04e3895666fc1ef83f24d7e22591d Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 30 Apr 2022 21:39:29 -0700 Subject: [PATCH] First attempt to add state to frigate card conditions. --- src/card-condition.ts | 18 ++++++- src/card.ts | 4 +- src/components/elements.ts | 103 +++++++++++++++++++------------------ src/components/live.ts | 4 +- src/types.ts | 19 ++++--- 5 files changed, 85 insertions(+), 63 deletions(-) diff --git a/src/card-condition.ts b/src/card-condition.ts index fb21ae0d..f5dc66b5 100644 --- a/src/card-condition.ts +++ b/src/card-condition.ts @@ -4,11 +4,13 @@ import type { RawFrigateCardConfig, } from './types'; import { merge, cloneDeep } from 'lodash-es'; +import { HassEntities } from 'home-assistant-js-websocket'; export interface ConditionState { view?: string; fullscreen?: boolean; camera?: string; + state?: HassEntities; } class ConditionStateRequestEvent extends Event { @@ -34,6 +36,18 @@ export function evaluateCondition( if (condition?.camera?.length) { result &&= !!state.camera && condition.camera.includes(state.camera); } + if (condition?.state?.length) { + for (const stateTest of condition?.state) { + result &&= + !!state.state && + ((!stateTest.state && !stateTest.state_not) || + (stateTest.entity in state.state && + (!stateTest.state || + state.state[stateTest.entity].state === stateTest.state) && + (!stateTest.state_not || + state.state[stateTest.entity].state !== stateTest.state_not))); + } + } return result; } @@ -42,7 +56,7 @@ export function evaluateCondition( * @returns A boolean indicating whether the condition is met. */ export function fetchStateAndEvaluateCondition( - node: HTMLElement, + element: HTMLElement, condition?: FrigateCardCondition, ): boolean { if (!condition) { @@ -69,7 +83,7 @@ export function fetchStateAndEvaluateCondition( * synchronously, the state will be added to the event before the flow * proceeds. */ - node.dispatchEvent(stateEvent); + element.dispatchEvent(stateEvent); return evaluateCondition(condition, stateEvent.conditionState); } diff --git a/src/card.ts b/src/card.ts index 37295275..1a299954 100644 --- a/src/card.ts +++ b/src/card.ts @@ -204,6 +204,8 @@ export class FrigateCard extends LitElement { } } + this._generateConditionState(); + // Dark mode may depend on HASS. this._setLightOrDarkMode(); } @@ -247,6 +249,7 @@ export class FrigateCard extends LitElement { view: this._view?.view, fullscreen: screenfull.isEnabled && screenfull.isFullscreen, camera: this._view?.camera, + state: this._hass?.states, }; const overriddenConfig = getOverriddenConfig( @@ -1075,7 +1078,6 @@ export class FrigateCard extends LitElement { .hass=${this._hass} .menuConfig=${this._getConfig().menu} .buttons=${this._getMenuButtons()} - .conditionState=${this._conditionState} > `; } diff --git a/src/components/elements.ts b/src/components/elements.ts index bc2253ab..0396882a 100644 --- a/src/components/elements.ts +++ b/src/components/elements.ts @@ -1,6 +1,5 @@ import { LitElement, TemplateResult, html, CSSResultGroup, unsafeCSS } from 'lit'; import { HomeAssistant } from 'custom-card-helpers'; -import { createRef, ref, Ref } from 'lit/directives/ref.js'; import { customElement, property } from 'lit/decorators.js'; import { @@ -11,10 +10,7 @@ import { PictureElements, MenuSubmenu, } from '../types.js'; -import { - dispatchErrorMessageEvent, - dispatchFrigateCardEvent, -} from '../common.js'; +import { dispatchErrorMessageEvent, dispatchFrigateCardEvent } from '../common.js'; import elementsStyle from '../scss/elements.scss'; import { localize } from '../localize/localize.js'; @@ -53,6 +49,11 @@ import { ConditionState, fetchStateAndEvaluateCondition } from '../card-conditio * upper layers to handle correctly. */ +interface HuiConditionalElement extends HTMLElement { + hass: HomeAssistant; + setConfig(config: unknown): void; +} + // A small wrapper around a HA conditional element used to render a set of // picture elements. @customElement('frigate-card-elements-core') @@ -67,19 +68,10 @@ class FrigateCardElementsCore extends LitElement { @property({ attribute: false }) protected conditionState?: ConditionState; - protected _root: HTMLElement | null = null; - protected _hass?: HomeAssistant; + protected _root: HuiConditionalElement | null = null; - /** - * Set Home Assistant object. - */ - set hass(hass: HomeAssistant) { - if (this._root) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this._root as any).hass = hass; - } - this._hass = hass; - } + @property({ attribute: false }) + protected hass?: HomeAssistant; /** * Create a transparent render root. @@ -90,17 +82,16 @@ class FrigateCardElementsCore extends LitElement { /** * Create the root node for our picture elements. - * @returns + * @returns The newly created root. */ - protected _createRoot(): HTMLElement { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const elementConstructor = customElements.get('hui-conditional-element') as any; - if (!elementConstructor || !this._hass) { + protected _createRoot(): HuiConditionalElement { + const elementConstructor = customElements.get('hui-conditional-element'); + if (!elementConstructor || !this.hass) { throw new Error(localize('error.could_not_render_elements')); } - const element = new elementConstructor(); - element.hass = this._hass; + const element = new elementConstructor() as HuiConditionalElement; + element.hass = this.hass; const config = { type: 'conditional', conditions: [], @@ -115,20 +106,37 @@ class FrigateCardElementsCore extends LitElement { return element; } + /** + * Create the root as necessary prior to rendering. + */ + protected willUpdate(): void { + try { + // The root is only created once, to avoid the elements being continually + // re-created & destroyed (for some elements, e.g. image, this would + // otherwise cause serious flickering). + if (!this._root) { + this._root = this._createRoot(); + } + } catch (e) { + return dispatchErrorMessageEvent(this, (e as Error).message); + } + } + /** * Render the elements. * @returns A rendered template or void. */ protected render(): TemplateResult | void { - try { - // Recreate the root on each render to ensure conditional ancestors - // re-fire events as necessary. - this._root = this._createRoot(); - } catch (e) { - return dispatchErrorMessageEvent(this, (e as Error).message); - } return html`${this._root || ''}`; } + + protected updated(): void { + if (this.hass && this._root) { + // Always update hass. It is used as a trigger to re-evaluate conditions + // down the chain, see the note on FrigateCardElementsConditional. + this._root.hass = this.hass; + } + } } /** @@ -145,6 +153,8 @@ export class FrigateCardElements extends LitElement { @property({ attribute: false }) protected conditionState?: ConditionState; + protected _boundMenuRemoveHandler = this._menuRemoveHandler.bind(this); + /** * Handle a picture element to be removed from the menu. * @param ev The event. @@ -175,13 +185,10 @@ export class FrigateCardElements extends LitElement { // Ensure listener is only attached 1 time by removing it first. path[0].removeEventListener( 'frigate-card:menu-remove', - this._menuRemoveHandler.bind(this), + this._boundMenuRemoveHandler, ); - path[0].addEventListener( - 'frigate-card:menu-remove', - this._menuRemoveHandler.bind(this), - ); + path[0].addEventListener('frigate-card:menu-remove', this._boundMenuRemoveHandler); } /** @@ -232,18 +239,13 @@ export class FrigateCardElements extends LitElement { @customElement('frigate-card-conditional') export class FrigateCardElementsConditional extends LitElement { protected _config?: FrigateConditional; - protected _hass?: HomeAssistant; - protected _refCore: Ref = createRef() ; - /** - * Set the Home Assistant object. - */ - set hass(hass: HomeAssistant) { - if (this._refCore.value) { - this._refCore.value.hass = hass; - } - this._hass = hass; - } + // Every set of hass is treated as a reason to re-evaluate. Given that this + // node may be buried down the DOM (as a descendent of non-Frigate card + // elements), the hass object is used as the (only) trigger for condition + // re-fetch even if hass itself has not changed. + @property({ attribute: false, hasChanged: () => true }) + protected hass?: HomeAssistant; /** * Set the card configuration. @@ -255,7 +257,7 @@ export class FrigateCardElementsConditional extends LitElement { /** * Create a root into which to render. This card is "transparent". - * @returns + * @returns */ createRenderRoot(): LitElement { return this; @@ -279,8 +281,7 @@ export class FrigateCardElementsConditional extends LitElement { protected render(): TemplateResult | void { if (fetchStateAndEvaluateCondition(this, this._config.conditions)) { return html` `; @@ -329,4 +330,4 @@ export class FrigateCardElementsMenuIcon extends FrigateCardElementsBaseMenuIcon export class FrigateCardElementsMenuStateIcon extends FrigateCardElementsBaseMenuIcon {} @customElement('frigate-card-menu-submenu') -export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuIcon {} \ No newline at end of file +export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuIcon {} diff --git a/src/components/live.ts b/src/components/live.ts index 42f649be..7dc20f0e 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -395,10 +395,10 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { // The conditionState object contains the currently live camera, which (in // the carousel for example) is not necessarily the live camera this // is rendering right now. - const conditionState = Object.assign({ + const conditionState = { ...this.conditionState, camera: camera, - }); + }; const config = getOverriddenConfig( this.liveConfig, diff --git a/src/types.ts b/src/types.ts index 513c2da3..66e9da9d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -295,16 +295,20 @@ const imageSchema = elementsBaseSchema.extend({ aspect_ratio: z.string().optional(), }); +// This state condition is used both for the Picture elements conditional +// schema, and also in frigateCardConditionSchema. +const stateConditions = z + .object({ + entity: z.string(), + state: z.string().optional(), + state_not: z.string().optional(), + }) + .array(); + // https://www.home-assistant.io/lovelace/picture-elements/#image-element const conditionalSchema = z.object({ type: z.literal('conditional'), - conditions: z - .object({ - entity: z.string(), - state: z.string().optional(), - state_not: z.string().optional(), - }) - .array(), + conditions: stateConditions, elements: z.lazy(() => pictureElementsSchema), }); @@ -411,6 +415,7 @@ const frigateCardConditionSchema = z.object({ view: z.string().array().optional(), fullscreen: z.boolean().optional(), camera: z.string().array().optional(), + state: stateConditions.optional(), }); export type FrigateCardCondition = z.infer;