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:
@@ -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`) |
|
||||
| `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.|
|
||||
| `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.
|
||||
|
||||
### 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>
|
||||
|
||||
## Picture Elements / Menu Customizations
|
||||
@@ -1230,6 +1251,23 @@ overrides:
|
||||
|
||||
</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
|
||||
|
||||
<details>
|
||||
|
||||
+17
-3
@@ -3,12 +3,14 @@ import type {
|
||||
OverrideConfigurationKey,
|
||||
RawFrigateCardConfig,
|
||||
} from './types';
|
||||
import { merge, cloneDeep } from 'lodash-es';
|
||||
import { HassEntities } from 'home-assistant-js-websocket';
|
||||
import { cloneDeep, merge } from 'lodash-es';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -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.
|
||||
this._setLightOrDarkMode();
|
||||
}
|
||||
@@ -247,6 +250,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 +1079,6 @@ export class FrigateCard extends LitElement {
|
||||
.hass=${this._hass}
|
||||
.menuConfig=${this._getConfig().menu}
|
||||
.buttons=${this._getMenuButtons()}
|
||||
.conditionState=${this._conditionState}
|
||||
></frigate-card-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
+53
-52
@@ -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,10 +49,15 @@ 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')
|
||||
class FrigateCardElementsCore extends LitElement {
|
||||
export class FrigateCardElementsCore extends LitElement {
|
||||
@property({ attribute: false })
|
||||
protected elements: PictureElements;
|
||||
|
||||
@@ -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<FrigateCardElementsCore> = 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` <frigate-card-elements-core
|
||||
${ref(this._refCore)}
|
||||
.hass=${this._hass}
|
||||
.hass=${this.hass}
|
||||
.elements=${this._config.elements}
|
||||
>
|
||||
</frigate-card-elements-core>`;
|
||||
@@ -329,4 +330,4 @@ export class FrigateCardElementsMenuIcon extends FrigateCardElementsBaseMenuIcon
|
||||
export class FrigateCardElementsMenuStateIcon extends FrigateCardElementsBaseMenuIcon<MenuStateIcon> {}
|
||||
|
||||
@customElement('frigate-card-menu-submenu')
|
||||
export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuIcon<MenuSubmenu> {}
|
||||
export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuIcon<MenuSubmenu> {}
|
||||
|
||||
@@ -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
|
||||
// <frigate-card-live-provider> is rendering right now.
|
||||
const conditionState = Object.assign({
|
||||
const conditionState = {
|
||||
...this.conditionState,
|
||||
camera: camera,
|
||||
});
|
||||
};
|
||||
|
||||
const config = getOverriddenConfig(
|
||||
this.liveConfig,
|
||||
|
||||
+12
-7
@@ -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<typeof frigateCardConditionSchema>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user