Add frigate-card-conditional element to select views.

This commit is contained in:
Dermot Duffy
2021-10-09 23:33:54 -07:00
parent 9a915af390
commit 7f76543bbd
7 changed files with 367 additions and 115 deletions
+145 -19
View File
@@ -207,39 +207,55 @@ entities:
show: false show: false
``` ```
### Picture Elements / Menu customizations ## Picture Elements / Menu customizations
This card supports the [Picture Elements configuration This card supports the [Picture Elements configuration
syntax](https://www.home-assistant.io/lovelace/picture-elements/) to seamlessly syntax](https://www.home-assistant.io/lovelace/picture-elements/) to seamlessly
allow the user to add custom elements to the card, which may be configured to allow the user to add custom elements to the card, which may be configured to
perform a variety of actions on `tap`, `double_tap` and `hold`. perform a variety of actions on `tap`, `double_tap` and `hold`.
In the card YAML configuration, elements may be manually added under an `elements` key. In the card YAML configuration, elements may be manually added under an
`elements` key.
#### Special Elements See the [action
documentation](https://www.home-assistant.io/lovelace/actions/#hold-action) for
more information on the action options available.
This card supports all [Picture Elements](https://www.home-assistant.io/lovelace/picture-elements/#icon-element) using the same syntax. The card also supports two special elements to add plain icons and state-based icons to the Frigate card menu. ### Special Elements
This card supports all [Picture Elements](https://www.home-assistant.io/lovelace/picture-elements/#icon-element) using the same syntax. The card also supports a handful of custom special elements to add special Frigate card functionality.
| Element name | Description | | Element name | Description |
| ------------- | --------------------------------------------- | | ------------- | --------------------------------------------- |
| `custom:frigate-card-menu-icon` | Add an arbitrary icon to the Frigate Card menu. Configuration is ~identical to that of the [Picture elements icon](https://www.home-assistant.io/lovelace/picture-elements/#icon-element).| | `custom:frigate-card-menu-icon` | Add an arbitrary icon to the Frigate Card menu. Configuration is ~identical to that of the [Picture Elements Icon](https://www.home-assistant.io/lovelace/picture-elements/#icon-element) except with a type name of `custom:frigate-card-menu-icon`.|
| `custom:frigate-card-menu-state-icon` | Add a state icon to the Frigate Card menu that represents the state of a Home Assistant entity. Configuration is ~identical to that of the [Picture elements state icon](https://www.home-assistant.io/lovelace/picture-elements/#state-icon).| | `custom:frigate-card-menu-state-icon` | Add a state icon to the Frigate Card menu that represents the state of a Home Assistant entity. Configuration is ~identical to that of the [Picture Elements State Icon](https://www.home-assistant.io/lovelace/picture-elements/#state-icon) except with a type name of `custom:frigate-card-menu-state-icon`.|
| `custom:frigate-card-conditional` | Restrict a set of elements to only render when the card is showing particular a particular [view](#views). See [configuration below](#frigate-card-conditional).|
See the [action documentation](https://www.home-assistant.io/lovelace/actions/#hold-action) for more information on the action options available. <a name="frigate-card-conditional"></a>
#### Elements Examples ### `custom:frigate-card-conditional`
Add an icon that represents the state of the `light.office_main_lights` entity, that shows more information on single click (the default action) and toggles the light on double click. Parameters for the `custom:frigate-card-conditional` element:
```yaml | Parameter | Description |
elements: | ------------- | --------------------------------------------- |
- type: custom:frigate-card-menu-state-icon | `type` | Must be `custom:frigate-card-conditional`. |
entity: light.office_main_lights | `conditions` | A set of conditions that must evaluate to true in order for the elements to be rendered. |
double_tap_action: | `conditions.view` | A list of [views](#views) in which these elements should be rendered. |
action: toggle | `elements` | The elements to render. Can be any supported element, include additional condition or custom elements. |
```
Add an icon that navigates the brower to the releases page for this card: See the [PTZ example below](#frigate-card-conditional-example) for a real-world example.
### Elements Examples
#### Menu icons
You can add custom icons to the menu with arbitrary actions.
<details>
<summary>Expand: Custom menu icon</summary>
This example adds an icon that navigates the brower to the releases page for this
card:
```yaml ```yaml
- type: custom:frigate-card-menu-icon - type: custom:frigate-card-menu-icon
@@ -248,8 +264,35 @@ Add an icon that navigates the brower to the releases page for this card:
action: url action: url
url_path: https://github.com/dermotduffy/frigate-hass-card/releases url_path: https://github.com/dermotduffy/frigate-hass-card/releases
``` ```
</details>
Add a state badge showing the temperature but hide the label text: #### Menu state icons
You can add custom state icons to the menu to show the state of an entity and complete arbitrary actions.
<details>
<summary>Expand: Custom menu state icon</summary>
This example adds an icon that represents the state of the
`light.office_main_lights` entity, that toggles the light on double click.
```yaml
elements:
- type: custom:frigate-card-menu-state-icon
entity: light.office_main_lights
tap_action:
action: toggle
```
</details>
#### State badges
You can adds a state badge to the card showing arbitrary entity states.
<details>
<summary>Expand: State badge</summary>
This example adds a state badge showing the temperature and hides the label text:
```yaml ```yaml
- type: state-badge - type: state-badge
@@ -262,8 +305,16 @@ Add a state badge showing the temperature but hide the label text:
``` ```
<img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/picture_elements_temperature.png" alt="Picture elements temperature example" width="400px"> <img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/picture_elements_temperature.png" alt="Picture elements temperature example" width="400px">
</details>
You can also have icons conditionally added to the menu, such as only showing a menu icon if a light is on: #### Conditional menu icons
You can have icons conditionally added to the menu based on entity state.
<details>
<summary>Expand: Conditional menu icons</summary>
This example only adds the light entity to the menu if a light is on.
```yaml ```yaml
- type: conditional - type: conditional
@@ -276,6 +327,81 @@ You can also have icons conditionally added to the menu, such as only showing a
tap_action: tap_action:
action: toggle action: toggle
``` ```
</details>
<a name="frigate-card-conditional-example"></a>
#### Restricting icons to certain views
You can restrict icons to only show for certain [views](#views) using a
`custom:frigate-card-conditional` element (e.g. PTZ controls)
<details>
<summary>Expand: View-based conditions (e.g. PTZ controls)</summary>
This example shows PTZ icons that call a PTZ service, but only in the `live` view.
```yaml
elements:
- type: custom:frigate-card-conditional
conditions:
view:
- live
elements:
- type: icon
icon: mdi:arrow-up
style:
background: rgba(255, 255, 255, 0.25)
border-radius: 5px
right: 25px
bottom: 50px
tap_action:
action: call-service
service: amcrest.ptz_control
service_data:
entity_id: camera.kitchen
movement: up
- type: icon
icon: mdi:arrow-down
style:
background: rgba(255, 255, 255, 0.25)
border-radius: 5px
right: 25px
bottom: 0px
tap_action:
action: call-service
service: amcrest.ptz_control
service_data:
entity_id: camera.kitchen
movement: down
- type: icon
icon: mdi:arrow-left
style:
background: rgba(255, 255, 255, 0.25)
border-radius: 5px
right: 50px
bottom: 25px
tap_action:
action: call-service
service: amcrest.ptz_control
service_data:
entity_id: camera.kitchen
movement: left
- type: icon
icon: mdi:arrow-right
style:
background: rgba(255, 255, 255, 0.25)
border-radius: 5px
right: 0px
bottom: 25px
tap_action:
action: call-service
service: amcrest.ptz_control
service_data:
entity_id: camera.kitchen
movement: right
```
</details>
<a name="views"></a> <a name="views"></a>
+10 -18
View File
@@ -19,11 +19,7 @@ import {
} from 'custom-card-helpers'; } from 'custom-card-helpers';
import screenfull from 'screenfull'; import screenfull from 'screenfull';
import { import { entitySchema, frigateCardConfigSchema, Message } from './types';
entitySchema,
frigateCardConfigSchema,
Message,
} from './types';
import type { import type {
BrowseMediaQueryParameters, BrowseMediaQueryParameters,
Entity, Entity,
@@ -280,6 +276,8 @@ export class FrigateCard extends LitElement {
} }
protected _changeView(view?: View | undefined): void { protected _changeView(view?: View | undefined): void {
this._message = null;
if (view === undefined) { if (view === undefined) {
this._view = new View({ view: this.config.view_default }); this._view = new View({ view: this.config.view_default });
} else { } else {
@@ -412,8 +410,8 @@ export class FrigateCard extends LitElement {
} }
protected _setMessageAndUpdate(message: Message): void { protected _setMessageAndUpdate(message: Message): void {
// Only register the first message. // Register the first message, or prioritize errors if there's pre-render competition.
if (!this._message) { if (!this._message || (message.type == 'error' && this._message.type != 'error')) {
this._message = message; this._message = message;
this.requestUpdate(); this.requestUpdate();
} }
@@ -423,15 +421,6 @@ export class FrigateCard extends LitElement {
return this._setMessageAndUpdate(e.detail); return this._setMessageAndUpdate(e.detail);
} }
protected _renderAndResetMessage(): TemplateResult | void {
if (this._message) {
const message = this._message;
this._message = null;
return renderMessage(message);
}
return html``;
}
protected _mediaLoadHandler(e: CustomEvent<MediaLoadInfo>): void { protected _mediaLoadHandler(e: CustomEvent<MediaLoadInfo>): void {
const mediaInfo = e.detail; const mediaInfo = e.detail;
// In Safari, with WebRTC, 0x0 is occasionally returned during loading, // In Safari, with WebRTC, 0x0 is occasionally returned during loading,
@@ -556,7 +545,7 @@ export class FrigateCard extends LitElement {
<div class="container outer" style="${styleMap(outerStyle)}"> <div class="container outer" style="${styleMap(outerStyle)}">
<div class="${classMap(contentClasses)}" style="${styleMap(innerStyle)}"> <div class="${classMap(contentClasses)}" style="${styleMap(innerStyle)}">
${this._message ${this._message
? this._renderAndResetMessage() ? renderMessage(this._message)
: until(this._render(), renderProgressIndicator())} : until(this._render(), renderProgressIndicator())}
</div> </div>
</div> </div>
@@ -624,7 +613,7 @@ export class FrigateCard extends LitElement {
: ``} : ``}
<frigate-card-elements <frigate-card-elements
.hass=${this._hass} .hass=${this._hass}
.pictureElements=${this.config.elements} .elements=${this.config.elements}
@frigate-card:message=${this._messageHandler} @frigate-card:message=${this._messageHandler}
@frigate-card:menu-add=${(e) => { @frigate-card:menu-add=${(e) => {
this._menu.addButton(e.detail); this._menu.addButton(e.detail);
@@ -632,6 +621,9 @@ export class FrigateCard extends LitElement {
@frigate-card:menu-remove=${(e) => { @frigate-card:menu-remove=${(e) => {
this._menu.removeButton(e.detail); this._menu.removeButton(e.detail);
}} }}
@frigate-card:state-request=${(e) => {
e.view = this._view;
}}
> >
</frigate-card-elements> </frigate-card-elements>
</div> </div>
+170 -53
View File
@@ -1,14 +1,20 @@
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 { customElement, property } from 'lit/decorators'; import { customElement, property, query } from 'lit/decorators';
import { ExtendedHomeAssistant, MenuButton, MenuIcon, MenuStateIcon, menuStateIconSchema, PictureElements } from '../types'; import {
import { menuIconSchema } from '../types' ExtendedHomeAssistant,
import { dispatchErrorMessageEvent, dispatchEvent, getParseErrorKeys } from '../common'; FrigateConditional,
MenuButton,
MenuIcon,
MenuStateIcon,
PictureElements,
} from '../types';
import { dispatchErrorMessageEvent, dispatchEvent } from '../common';
import elementsStyle from '../scss/elements.scss'; import elementsStyle from '../scss/elements.scss';
import { localize } from '../localize/localize'; import { localize } from '../localize/localize';
import { z } from 'zod'; import { View } from '../view';
/* A note on picture element rendering: /* A note on picture element rendering:
* *
@@ -43,22 +49,82 @@ import { z } from 'zod';
* upper layers to handle correctly. * upper layers to handle correctly.
*/ */
// A small wrapper around a HA conditional element used to render a set of
@customElement('frigate-card-elements') // picture elements.
export class FrigateCardElements extends LitElement { @customElement('frigate-card-elements-core')
class FrigateCardElementsCore extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
protected pictureElements: PictureElements; protected elements: PictureElements;
protected _hass!: HomeAssistant & ExtendedHomeAssistant;
protected _root: HTMLElement | null = null; protected _root: HTMLElement | null = null;
protected _hass!: HomeAssistant & ExtendedHomeAssistant;
set hass(hass: HomeAssistant & ExtendedHomeAssistant) { set hass(hass: HomeAssistant & ExtendedHomeAssistant) {
if (this._root) { if (this._root) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this._root as any).hass = hass; (this._root as any).hass = hass;
} }
this._hass = hass; this._hass = hass;
} }
// Transparent to elements.
createRenderRoot(): LitElement {
return this;
}
protected _createRoot(): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const elementConstructor = customElements.get('hui-conditional-element') as any;
if (!elementConstructor) {
throw new Error(localize('error.could_not_render_elements'));
}
const element = new elementConstructor();
element.hass = this._hass;
const config = {
type: 'conditional',
conditions: [],
elements: this.elements,
};
try {
element.setConfig(config);
} catch (e) {
console.error(e, (e as Error).stack);
throw new Error(localize('error.invalid_elements_config'));
}
this._root = element;
}
protected render(): TemplateResult | void {
if (!this._root) {
try {
this._createRoot();
} catch (e) {
return dispatchErrorMessageEvent(this, (e as Error).message);
}
}
return html`${this._root || ''}`;
}
}
// THe master <frigate-card-elements> class, handles event listeners and styles.
@customElement('frigate-card-elements')
export class FrigateCardElements extends LitElement {
@property({ attribute: false })
protected elements: PictureElements;
protected _hass!: HomeAssistant & ExtendedHomeAssistant;
@query('frigate-card-elements-core')
_core!: FrigateCardElementsCore;
set hass(hass: HomeAssistant & ExtendedHomeAssistant) {
if (this._core) {
this._core.hass = hass;
}
this._hass = hass;
}
protected _menuRemoveHandler(ev: Event): void { protected _menuRemoveHandler(ev: Event): void {
// Re-dispatch event from this element (instead of the disconnected one, as // Re-dispatch event from this element (instead of the disconnected one, as
// there is no parent of the disconnected element). // there is no parent of the disconnected element).
@@ -67,7 +133,7 @@ export class FrigateCardElements extends LitElement {
protected _menuAddHandler(ev: Event): void { protected _menuAddHandler(ev: Event): void {
ev = ev as CustomEvent<MenuButton>; ev = ev as CustomEvent<MenuButton>;
const path = ev.composedPath() const path = ev.composedPath();
if (!path.length) { if (!path.length) {
return; return;
} }
@@ -77,11 +143,13 @@ 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._menuRemoveHandler.bind(this),
);
path[0].addEventListener( path[0].addEventListener(
'frigate-card:menu-remove', 'frigate-card:menu-remove',
this._menuRemoveHandler.bind(this)); this._menuRemoveHandler.bind(this),
);
} }
connectedCallback(): void { connectedCallback(): void {
@@ -89,49 +157,20 @@ export class FrigateCardElements extends LitElement {
// Catch icons being added to the menu (so their removal can be subsequently // Catch icons being added to the menu (so their removal can be subsequently
// handled). // handled).
this.addEventListener( this.addEventListener('frigate-card:menu-add', this._menuAddHandler);
'frigate-card:menu-add',
this._menuAddHandler,
);
if (!this._root) {
this._createRoot();
}
} }
disconnectedCallback(): void { disconnectedCallback(): void {
this.removeEventListener( this.removeEventListener('frigate-card:menu-add', this._menuAddHandler);
'frigate-card:menu-add',
this._menuAddHandler,
);
super.disconnectedCallback(); super.disconnectedCallback();
} }
protected _createRoot(): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const elementConstructor = customElements.get('hui-conditional-element') as any;
if (!elementConstructor) {
return;
}
const element = new elementConstructor();
element.hass = this._hass;
const config = {
type: 'conditional',
conditions: [],
elements: this.pictureElements,
}
try {
element.setConfig(config);
} catch (e) {
console.error(e, (e as Error).stack);
return;
}
this._root = element;
}
protected render(): TemplateResult { protected render(): TemplateResult {
return html`${this._root || ''}`; return html` <frigate-card-elements-core
.hass=${this._hass}
.elements=${this.elements}
>
</frigate-card-elements-core>`;
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
@@ -139,6 +178,84 @@ export class FrigateCardElements extends LitElement {
} }
} }
class StateRequestEvent extends Event {
public view: View | undefined;
}
// An element that can render others based on Frigate state (e.g. only show
// overlays in particular views). This is the Frigate Card equivalent to the HA
// conditional card.
@customElement('frigate-card-conditional')
export class FrigateCardElementsConditional extends LitElement {
protected _config: FrigateConditional | null = null;
protected _hass!: HomeAssistant & ExtendedHomeAssistant;
@query('frigate-card-elements-core')
_core!: FrigateCardElementsCore;
set hass(hass: HomeAssistant & ExtendedHomeAssistant) {
if (this._core) {
this._core.hass = hass;
}
this._hass = hass;
}
public setConfig(config: FrigateConditional): void {
this._config = config;
}
// Transparent to elements.
createRenderRoot(): LitElement {
return this;
}
protected evaluate(stateEvent: StateRequestEvent): boolean {
if (stateEvent.view && this._config.conditions.view) {
return this._config.conditions.view.includes(stateEvent.view.view);
}
return true;
}
connectedCallback(): void {
super.connectedCallback();
// HA will automatically attach the 'element' class to picture elements. As
// this is a transparent 'conditional' element (just like the stock HA
// 'conditional' element), it should not have positioning.
this.className = '';
}
protected render(): TemplateResult | void {
const stateEvent = new StateRequestEvent(`frigate-card:state-request`, {
bubbles: true,
composed: true,
});
/* Special note on what's going on here:
*
* Picture elements all are descendents of <frigate-card-elements>, but
* there may be arbitrary complexity and layers (that this card doesn't
* control) between that master element and this custom conditional element.
* This element needs Frigate card state to function (e.g. view), but
* there's no clean way to pass state from the rest of card down through
* these layers. Instead, we dispatch a "request for state"
* (StateRequestEvent) event upwards which is caught by the outer card and
* state added to the event object. Because event propagation is handled
* synchronously, the state will be added to the event before the flow
* proceeds.
*/
this.dispatchEvent(stateEvent);
if (this.evaluate(stateEvent)) {
return html` <frigate-card-elements-core
.hass=${this._hass}
.elements=${this._config.elements}
>
</frigate-card-elements-core>`;
}
}
}
// A base class for rendering menu icons / menu state icons.
export class FrigateCardElementsBaseMenuIcon<T> extends LitElement { export class FrigateCardElementsBaseMenuIcon<T> extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
protected _config: T | null = null; protected _config: T | null = null;
@@ -148,17 +265,17 @@ export class FrigateCardElementsBaseMenuIcon<T> extends LitElement {
} }
connectedCallback(): void { connectedCallback(): void {
super.connectedCallback() super.connectedCallback();
if (this._config) { if (this._config) {
dispatchEvent<T>(this, 'menu-add', this._config); dispatchEvent<T>(this, 'menu-add', this._config);
} }
} }
disconnectedCallback(): void { disconnectedCallback(): void {
if (this._config) { if (this._config) {
dispatchEvent<T>(this, 'menu-remove', this._config); dispatchEvent<T>(this, 'menu-remove', this._config);
} }
super.disconnectedCallback() super.disconnectedCallback();
} }
} }
+4 -2
View File
@@ -24,7 +24,9 @@ export class FrigateCardMessage extends LitElement {
return html` <div class="message"> return html` <div class="message">
<span> <span>
<ha-icon icon="${icon}"> </ha-icon> <ha-icon icon="${icon}"> </ha-icon>
${this.message ? html`&nbsp;${this.message}` : ''} </span>
<span>
${this.message ? html`${this.message}` : ''}
</span> </span>
</div>`; </div>`;
} }
@@ -42,7 +44,7 @@ export class FrigateCardErrorMessage extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` <frigate-card-message return html` <frigate-card-message
.message=${html` ${this.error}. .message=${html` ${this.error}.
<a href="${URL_TROUBLESHOOTING}"> ${localize('error.troubleshooting')} </a>.`} <a href="${URL_TROUBLESHOOTING}"> ${localize('error.troubleshooting')}</a>.`}
.icon=${'mdi:alert-circle'} .icon=${'mdi:alert-circle'}
> >
</frigate-card-message>`; </frigate-card-message>`;
+3 -1
View File
@@ -95,6 +95,8 @@
"no_live_camera": "No live camera", "no_live_camera": "No live camera",
"invalid_configuration": "Invalid configuration", "invalid_configuration": "Invalid configuration",
"missing_webrtc": "WebRTC component not found", "missing_webrtc": "WebRTC component not found",
"no_frigate_camera_name": "Cannot derive frigate_camera_name, you may need to set it manually" "no_frigate_camera_name": "Cannot derive frigate_camera_name, you may need to set it manually",
"could_not_render_elements": "Could not render picture elements",
"invalid_elements_config": "Invalid picture elements configuration"
} }
} }
+4
View File
@@ -10,3 +10,7 @@
.message a { .message a {
color: var(--primary-text-color, white); color: var(--primary-text-color, white);
} }
span {
padding: 10px;
}
+28 -19
View File
@@ -181,8 +181,7 @@ const imageSchema = elementsBaseSchema.merge(
})); }));
// https://www.home-assistant.io/lovelace/picture-elements/#image-element // https://www.home-assistant.io/lovelace/picture-elements/#image-element
const conditionalSchema = elementsBaseSchema.merge( const conditionalSchema = z.object({
z.object({
type: z.literal('conditional'), type: z.literal('conditional'),
conditions: z.object({ conditions: z.object({
entity: z.string(), entity: z.string(),
@@ -190,7 +189,7 @@ const conditionalSchema = elementsBaseSchema.merge(
state_not: z.string().optional(), state_not: z.string().optional(),
}).array(), }).array(),
elements: z.lazy(() => pictureElementsSchema), elements: z.lazy(() => pictureElementsSchema),
})); });
// https://www.home-assistant.io/lovelace/picture-elements/#custom-elements // https://www.home-assistant.io/lovelace/picture-elements/#custom-elements
const customSchema = z.object({ const customSchema = z.object({
@@ -199,7 +198,7 @@ const customSchema = z.object({
}).passthrough(); }).passthrough();
/** /**
* Menu Element Types * Custom Element Types
*/ */
export const menuIconSchema = iconSchema.merge( export const menuIconSchema = iconSchema.merge(
@@ -214,28 +213,22 @@ export const menuStateIconSchema = stateIconSchema.merge(
})); }));
export type MenuStateIcon = z.infer<typeof menuStateIconSchema>; export type MenuStateIcon = z.infer<typeof menuStateIconSchema>;
// Schema for card (non-user configured) menu icons. const frigateConditionalSchema = z.object({
const internalMenuIconSchema = z type: z.literal('custom:frigate-card-conditional'),
.object({ conditions: z.object({
type: z.literal('internal-menu-icon'), view: z.string().array().optional(),
title: z.string(), }),
icon: z.string().optional(), elements: z.lazy(() => pictureElementsSchema),
emphasize: z.boolean().default(false).optional(), });
card_action: z.string(), export type FrigateConditional = z.infer<typeof frigateConditionalSchema>;
});
const menuButtonSchema = z.union([
menuIconSchema,
menuStateIconSchema,
internalMenuIconSchema,
]);
export type MenuButton = z.infer<typeof menuButtonSchema>;
// 'internalMenuIconSchema' is excluded to disallow the user from manually // 'internalMenuIconSchema' is excluded to disallow the user from manually
// changing the internal menu buttons. // changing the internal menu buttons.
const pictureElementSchema = z.union([ const pictureElementSchema = z.union([
menuStateIconSchema, menuStateIconSchema,
menuIconSchema, menuIconSchema,
frigateConditionalSchema,
stateBadgeIconSchema, stateBadgeIconSchema,
stateIconSchema, stateIconSchema,
stateLabelSchema, stateLabelSchema,
@@ -323,6 +316,22 @@ export const frigateCardConfigSchema = z.object({
}); });
export type FrigateCardConfig = z.infer<typeof frigateCardConfigSchema>; export type FrigateCardConfig = z.infer<typeof frigateCardConfigSchema>;
// Schema for card (non-user configured) menu icons.
const internalMenuIconSchema = z
.object({
type: z.literal('internal-menu-icon'),
title: z.string(),
icon: z.string().optional(),
emphasize: z.boolean().default(false).optional(),
card_action: z.string(),
});
const menuButtonSchema = z.union([
menuIconSchema,
menuStateIconSchema,
internalMenuIconSchema,
]);
export type MenuButton = z.infer<typeof menuButtonSchema>;
export interface ExtendedHomeAssistant { export interface ExtendedHomeAssistant {
hassUrl(path?): string; hassUrl(path?): string;
} }