diff --git a/README.md b/README.md
index fe8649f3..6239c9ec 100644
--- a/README.md
+++ b/README.md
@@ -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.
+
## Picture Elements / Menu Customizations
@@ -1230,6 +1251,23 @@ overrides:
+
+
+
+ Expand: Change the menu position based on HA state
+
+```yaml
+overrides:
+ - conditions:
+ state:
+ - entity: light.office_lights
+ state: 'on'
+ overrides:
+ menu:
+ position: bottom
+```
+
+
### Refreshing a static image
diff --git a/src/card-condition.ts b/src/card-condition.ts
index fb21ae0d..086e9ebb 100644
--- a/src/card-condition.ts
+++ b/src/card-condition.ts
@@ -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);
}
diff --git a/src/card.ts b/src/card.ts
index 37295275..978bfc4b 100644
--- a/src/card.ts
+++ b/src/card.ts
@@ -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}
>
`;
}
diff --git a/src/components/elements.ts b/src/components/elements.ts
index bc2253ab..11e6648e 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,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 = 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;