Merge pull request #530 from dermotduffy/state-in-overrides

Allow use of HA state in Frigate Card Conditions (e.g. for use in configuration overrides)
This commit is contained in:
Dermot Duffy
2022-05-01 12:56:49 -07:00
committed by GitHub
6 changed files with 126 additions and 65 deletions
+38
View File
@@ -579,9 +579,30 @@ All variables listed are under a `conditions:` section.
| `view` | A list of [views](#views) in which this condition is satified (e.g. `clips`) | | `view` | A list of [views](#views) in which this condition is satified (e.g. `clips`) |
| `camera` | A list of camera ids in which this condition is satisfied. See [camera IDs](#camera-ids).| | `camera` | A list of camera ids in which this condition is satisfied. See [camera IDs](#camera-ids).|
| `fullscreen` | If `true` the condition is satisfied if the card is in fullscreen mode. If `false` the condition is satisfied if the card is **NOT** in fullscreen mode.| | `fullscreen` | If `true` the condition is satisfied if the card is in fullscreen mode. If `false` the condition is satisfied if the card is **NOT** in fullscreen mode.|
| `state` | A list of state conditions to compare with Home Assistant state. See below. |
See the [PTZ example below](#frigate-card-conditional-example) for a real-world example of how these conditions can be used. See the [PTZ example below](#frigate-card-conditional-example) for a real-world example of how these conditions can be used.
### State Conditions
```yaml
- conditions:
state:
- [entries]
```
The Frigate Card Condition can compare HA state against fixed string values. This is the same as the Home Assistant [Conditional Element](https://www.home-assistant.io/dashboards/picture-elements/#conditional-element) condition, but can be used outside of a Picture Element context (e.g. card configuration overrides).
If multiple entries are provided, the results are `AND`ed.
| Parameter | Description |
| - | - |
| `entity` | The entity ID to check the state for |
| `state` | Condition will be met if state is equal to this optional string. |
| `state_not` | Condition will be met if state is unequal to this optional string. |
See the [Menu override example below](#frigate-card-menu-override-example) for an illustration.
<a name="frigate-card-elements"></a> <a name="frigate-card-elements"></a>
## Picture Elements / Menu Customizations ## Picture Elements / Menu Customizations
@@ -1230,6 +1251,23 @@ overrides:
</details> </details>
<a name="frigate-card-menu-override-example"></a>
<details>
<summary>Expand: Change the menu position based on HA state</summary>
```yaml
overrides:
- conditions:
state:
- entity: light.office_lights
state: 'on'
overrides:
menu:
position: bottom
```
</details>
### Refreshing a static image ### Refreshing a static image
<details> <details>
+17 -3
View File
@@ -3,12 +3,14 @@ import type {
OverrideConfigurationKey, OverrideConfigurationKey,
RawFrigateCardConfig, RawFrigateCardConfig,
} from './types'; } from './types';
import { merge, cloneDeep } from 'lodash-es'; import { HassEntities } from 'home-assistant-js-websocket';
import { cloneDeep, merge } from 'lodash-es';
export interface ConditionState { export interface ConditionState {
view?: string; view?: string;
fullscreen?: boolean; fullscreen?: boolean;
camera?: string; camera?: string;
state?: HassEntities;
} }
class ConditionStateRequestEvent extends Event { class ConditionStateRequestEvent extends Event {
@@ -34,6 +36,18 @@ export function evaluateCondition(
if (condition?.camera?.length) { if (condition?.camera?.length) {
result &&= !!state.camera && condition.camera.includes(state.camera); 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; return result;
} }
@@ -42,7 +56,7 @@ export function evaluateCondition(
* @returns A boolean indicating whether the condition is met. * @returns A boolean indicating whether the condition is met.
*/ */
export function fetchStateAndEvaluateCondition( export function fetchStateAndEvaluateCondition(
node: HTMLElement, element: HTMLElement,
condition?: FrigateCardCondition, condition?: FrigateCardCondition,
): boolean { ): boolean {
if (!condition) { if (!condition) {
@@ -69,7 +83,7 @@ export function fetchStateAndEvaluateCondition(
* synchronously, the state will be added to the event before the flow * synchronously, the state will be added to the event before the flow
* proceeds. * proceeds.
*/ */
node.dispatchEvent(stateEvent); element.dispatchEvent(stateEvent);
return evaluateCondition(condition, stateEvent.conditionState); return evaluateCondition(condition, stateEvent.conditionState);
} }
+4 -1
View File
@@ -204,6 +204,9 @@ export class FrigateCard extends LitElement {
} }
} }
// HA entity state is part of the condition state.
this._generateConditionState();
// Dark mode may depend on HASS. // Dark mode may depend on HASS.
this._setLightOrDarkMode(); this._setLightOrDarkMode();
} }
@@ -247,6 +250,7 @@ export class FrigateCard extends LitElement {
view: this._view?.view, view: this._view?.view,
fullscreen: screenfull.isEnabled && screenfull.isFullscreen, fullscreen: screenfull.isEnabled && screenfull.isFullscreen,
camera: this._view?.camera, camera: this._view?.camera,
state: this._hass?.states,
}; };
const overriddenConfig = getOverriddenConfig( const overriddenConfig = getOverriddenConfig(
@@ -1075,7 +1079,6 @@ export class FrigateCard extends LitElement {
.hass=${this._hass} .hass=${this._hass}
.menuConfig=${this._getConfig().menu} .menuConfig=${this._getConfig().menu}
.buttons=${this._getMenuButtons()} .buttons=${this._getMenuButtons()}
.conditionState=${this._conditionState}
></frigate-card-menu> ></frigate-card-menu>
`; `;
} }
+51 -50
View File
@@ -1,6 +1,5 @@
import { LitElement, TemplateResult, html, CSSResultGroup, unsafeCSS } from 'lit'; import { LitElement, TemplateResult, html, CSSResultGroup, unsafeCSS } from 'lit';
import { HomeAssistant } from 'custom-card-helpers'; import { HomeAssistant } from 'custom-card-helpers';
import { createRef, ref, Ref } from 'lit/directives/ref.js';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import { import {
@@ -11,10 +10,7 @@ import {
PictureElements, PictureElements,
MenuSubmenu, MenuSubmenu,
} from '../types.js'; } from '../types.js';
import { import { dispatchErrorMessageEvent, dispatchFrigateCardEvent } from '../common.js';
dispatchErrorMessageEvent,
dispatchFrigateCardEvent,
} from '../common.js';
import elementsStyle from '../scss/elements.scss'; import elementsStyle from '../scss/elements.scss';
import { localize } from '../localize/localize.js'; import { localize } from '../localize/localize.js';
@@ -53,10 +49,15 @@ import { ConditionState, fetchStateAndEvaluateCondition } from '../card-conditio
* upper layers to handle correctly. * 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 // A small wrapper around a HA conditional element used to render a set of
// picture elements. // picture elements.
@customElement('frigate-card-elements-core') @customElement('frigate-card-elements-core')
class FrigateCardElementsCore extends LitElement { export class FrigateCardElementsCore extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
protected elements: PictureElements; protected elements: PictureElements;
@@ -67,19 +68,10 @@ class FrigateCardElementsCore extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
protected conditionState?: ConditionState; protected conditionState?: ConditionState;
protected _root: HTMLElement | null = null; protected _root: HuiConditionalElement | null = null;
protected _hass?: HomeAssistant;
/** @property({ attribute: false })
* Set Home Assistant object. protected hass?: HomeAssistant;
*/
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;
}
/** /**
* Create a transparent render root. * Create a transparent render root.
@@ -90,17 +82,16 @@ class FrigateCardElementsCore extends LitElement {
/** /**
* Create the root node for our picture elements. * Create the root node for our picture elements.
* @returns * @returns The newly created root.
*/ */
protected _createRoot(): HTMLElement { protected _createRoot(): HuiConditionalElement {
// eslint-disable-next-line @typescript-eslint/no-explicit-any const elementConstructor = customElements.get('hui-conditional-element');
const elementConstructor = customElements.get('hui-conditional-element') as any; if (!elementConstructor || !this.hass) {
if (!elementConstructor || !this._hass) {
throw new Error(localize('error.could_not_render_elements')); throw new Error(localize('error.could_not_render_elements'));
} }
const element = new elementConstructor(); const element = new elementConstructor() as HuiConditionalElement;
element.hass = this._hass; element.hass = this.hass;
const config = { const config = {
type: 'conditional', type: 'conditional',
conditions: [], conditions: [],
@@ -115,20 +106,37 @@ class FrigateCardElementsCore extends LitElement {
return element; 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. * Render the elements.
* @returns A rendered template or void. * @returns A rendered template or void.
*/ */
protected render(): TemplateResult | 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 || ''}`; 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 }) @property({ attribute: false })
protected conditionState?: ConditionState; protected conditionState?: ConditionState;
protected _boundMenuRemoveHandler = this._menuRemoveHandler.bind(this);
/** /**
* Handle a picture element to be removed from the menu. * Handle a picture element to be removed from the menu.
* @param ev The event. * @param ev The event.
@@ -175,13 +185,10 @@ export class FrigateCardElements extends LitElement {
// Ensure listener is only attached 1 time by removing it first. // Ensure listener is only attached 1 time by removing it first.
path[0].removeEventListener( path[0].removeEventListener(
'frigate-card:menu-remove', 'frigate-card:menu-remove',
this._menuRemoveHandler.bind(this), this._boundMenuRemoveHandler,
); );
path[0].addEventListener( path[0].addEventListener('frigate-card:menu-remove', this._boundMenuRemoveHandler);
'frigate-card:menu-remove',
this._menuRemoveHandler.bind(this),
);
} }
/** /**
@@ -232,18 +239,13 @@ export class FrigateCardElements extends LitElement {
@customElement('frigate-card-conditional') @customElement('frigate-card-conditional')
export class FrigateCardElementsConditional extends LitElement { export class FrigateCardElementsConditional extends LitElement {
protected _config?: FrigateConditional; protected _config?: FrigateConditional;
protected _hass?: HomeAssistant;
protected _refCore: Ref<FrigateCardElementsCore> = createRef() ;
/** // Every set of hass is treated as a reason to re-evaluate. Given that this
* Set the Home Assistant object. // 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
set hass(hass: HomeAssistant) { // re-fetch even if hass itself has not changed.
if (this._refCore.value) { @property({ attribute: false, hasChanged: () => true })
this._refCore.value.hass = hass; protected hass?: HomeAssistant;
}
this._hass = hass;
}
/** /**
* Set the card configuration. * Set the card configuration.
@@ -279,8 +281,7 @@ export class FrigateCardElementsConditional extends LitElement {
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
if (fetchStateAndEvaluateCondition(this, this._config.conditions)) { if (fetchStateAndEvaluateCondition(this, this._config.conditions)) {
return html` <frigate-card-elements-core return html` <frigate-card-elements-core
${ref(this._refCore)} .hass=${this.hass}
.hass=${this._hass}
.elements=${this._config.elements} .elements=${this._config.elements}
> >
</frigate-card-elements-core>`; </frigate-card-elements-core>`;
+2 -2
View File
@@ -395,10 +395,10 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
// The conditionState object contains the currently live camera, which (in // The conditionState object contains the currently live camera, which (in
// the carousel for example) is not necessarily the live camera this // the carousel for example) is not necessarily the live camera this
// <frigate-card-live-provider> is rendering right now. // <frigate-card-live-provider> is rendering right now.
const conditionState = Object.assign({ const conditionState = {
...this.conditionState, ...this.conditionState,
camera: camera, camera: camera,
}); };
const config = getOverriddenConfig( const config = getOverriddenConfig(
this.liveConfig, this.liveConfig,
+12 -7
View File
@@ -295,16 +295,20 @@ const imageSchema = elementsBaseSchema.extend({
aspect_ratio: z.string().optional(), 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 // https://www.home-assistant.io/lovelace/picture-elements/#image-element
const conditionalSchema = z.object({ const conditionalSchema = z.object({
type: z.literal('conditional'), type: z.literal('conditional'),
conditions: z conditions: stateConditions,
.object({
entity: z.string(),
state: z.string().optional(),
state_not: z.string().optional(),
})
.array(),
elements: z.lazy(() => pictureElementsSchema), elements: z.lazy(() => pictureElementsSchema),
}); });
@@ -411,6 +415,7 @@ const frigateCardConditionSchema = z.object({
view: z.string().array().optional(), view: z.string().array().optional(),
fullscreen: z.boolean().optional(), fullscreen: z.boolean().optional(),
camera: z.string().array().optional(), camera: z.string().array().optional(),
state: stateConditions.optional(),
}); });
export type FrigateCardCondition = z.infer<typeof frigateCardConditionSchema>; export type FrigateCardCondition = z.infer<typeof frigateCardConditionSchema>;