Add native select entity support.

This commit is contained in:
Dermot Duffy
2022-05-08 15:58:56 -07:00
parent c7462e7f4f
commit 28292483fc
6 changed files with 200 additions and 11 deletions
+64
View File
@@ -655,6 +655,8 @@ Parameters for the `custom:frigate-card-menu-submenu` element are identical to t
| `type` | Must be `custom:frigate-card-menu-submenu`. | | `type` | Must be `custom:frigate-card-menu-submenu`. |
| `items` | A list of menu items, as described below. | | `items` | A list of menu items, as described below. |
<a name="frigate-card-submenu-items"></a>
##### Submenu Items ##### Submenu Items
| Parameter | Default | Description | | Parameter | Default | Description |
@@ -669,6 +671,19 @@ Parameters for the `custom:frigate-card-menu-submenu` element are identical to t
See the [Configuring a Submenu example](#configuring-a-submenu-example). See the [Configuring a Submenu example](#configuring-a-submenu-example).
#### `custom:frigate-card-menu-submenu-select`
This element allows you to easily convert a [Home Assistant Select Entity](https://www.home-assistant.io/integrations/select/) or [Home Assistant Input Select Entity](https://www.home-assistant.io/integrations/input_select/) (an entity either starting with `select` or `input_select`) into an overridable submenu. This *could* be done by hand using a regular submenu (above) -- this element is a convenience.
Parameters for the `custom:frigate-card-menu-submenu-select` element are identical to the parameters of the [stock Home Assistant State Icon Element](https://www.home-assistant.io/dashboards/picture-elements/#state-icon) with the exception of these parameters which differ:
| Parameter | Description |
| - | - |
| `type` | Must be `custom:frigate-card-menu-submenu-select`. |
| `options` | An optional dictionary of overrides keyed by the option name that the given select entity supports. These options can be used to set or override submenu item parameters on a per-option basis. The format is as described in [Submenu Items](#frigate-card-submenu-items) above.|
See the [Configuring a Select Submenu example](#configuring-a-select-submenu-example).
<a name="frigate-card-conditional"></a> <a name="frigate-card-conditional"></a>
#### `custom:frigate-card-conditional` #### `custom:frigate-card-conditional`
@@ -1210,6 +1225,55 @@ elements:
</details> </details>
<a name="configuring-a-select-submenu-example"></a>
### Configuring a `select` submenu
You can easily add a submenu to the menu based on a `select` or `input_select` entity.
<details>
<summary>Expand: Adding a select submenu</summary>
This example imagines the user has an `input_select` entity configured in their Home Assistant configuration like so:
```yaml
input_select:
kitchen_scene:
name: Kitchen Scene Select
options:
- scene.kitchen_cooking_scene
- scene.kitchen_tv_scene
icon: mdi:lightbulb
```
The following will convert this entity into a submenu:
```yaml
[...]
elements:
- type: custom:frigate-card-menu-submenu-select
icon: mdi:lamps
entity: input_select.kitchen_scene
```
To override 1 or more individual options (e.g. to set custom icons and titles)
```yaml
[...]
elements:
- type: custom:frigate-card-menu-submenu-select
icon: mdi:lamps
entity: input_select.kitchen_scene
options:
scene.kitchen_cooking_scene:
icon: mdi:chef-hat
title: Cooking time!
scene.kitchen_dining_scene:
icon: mdi:television
title: TV!
```
</details>
### Overriding card behavior ### Overriding card behavior
+3 -2
View File
@@ -9,7 +9,8 @@ import styles from 'rollup-plugin-styles';
import image from '@rollup/plugin-image'; import image from '@rollup/plugin-image';
import replace from '@rollup/plugin-replace'; import replace from '@rollup/plugin-replace';
const dev = process.env.ROLLUP_WATCH === 'true' || process.env.ROLLUP_WATCH === '1'; const watch = process.env.ROLLUP_WATCH === 'true' || process.env.ROLLUP_WATCH === '1';
const dev = watch || process.env.DEV === 'true' || process.env.DEV === '1';
const serveopts = { const serveopts = {
contentBase: ['./dist'], contentBase: ['./dist'],
@@ -50,7 +51,7 @@ const plugins = [
'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'), 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'),
} }
}), }),
dev && serve(serveopts), watch && serve(serveopts),
!dev && terser(), !dev && terser(),
]; ];
+4
View File
@@ -9,6 +9,7 @@ import {
MenuStateIcon, MenuStateIcon,
PictureElements, PictureElements,
MenuSubmenu, MenuSubmenu,
MenuSubmenuSelect,
} from '../types.js'; } from '../types.js';
import { dispatchErrorMessageEvent, dispatchFrigateCardEvent } from '../common.js'; import { dispatchErrorMessageEvent, dispatchFrigateCardEvent } from '../common.js';
@@ -331,3 +332,6 @@ export class FrigateCardElementsMenuStateIcon extends FrigateCardElementsBaseMen
@customElement('frigate-card-menu-submenu') @customElement('frigate-card-menu-submenu')
export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuIcon<MenuSubmenu> {} export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuIcon<MenuSubmenu> {}
@customElement('frigate-card-menu-submenu-select')
export class FrigateCardElementsMenuSubmenuSelect extends FrigateCardElementsBaseMenuIcon<MenuSubmenuSelect> {}
+8 -1
View File
@@ -252,13 +252,20 @@ export class FrigateCardMenu extends LitElement {
if (button.enabled === false) { if (button.enabled === false) {
return; return;
} }
if (button.type == 'custom:frigate-card-menu-submenu') { if (button.type === 'custom:frigate-card-menu-submenu') {
return html` <frigate-card-submenu return html` <frigate-card-submenu
.hass=${this.hass} .hass=${this.hass}
.submenu=${button} .submenu=${button}
@action=${this._actionHandler.bind(this)} @action=${this._actionHandler.bind(this)}
> >
</frigate-card-submenu>`; </frigate-card-submenu>`;
} else if (button.type === 'custom:frigate-card-menu-submenu-select') {
return html` <frigate-card-submenu-select
.hass=${this.hass}
.submenuSelect=${button}
@action=${this._actionHandler.bind(this)}
>
</frigate-card-submenu-select>`;
} }
let stateParameters: StateParameters = { ...button }; let stateParameters: StateParameters = { ...button };
+108 -6
View File
@@ -1,17 +1,27 @@
import type { Corner } from '@material/mwc-menu'; import type { Corner } from '@material/mwc-menu';
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit'; import {
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
unsafeCSS,
} from 'lit';
import { HomeAssistant } from 'custom-card-helpers'; import { HomeAssistant } from 'custom-card-helpers';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import { import {
frigateCardHasAction, frigateCardHasAction,
refreshDynamicStateParameters, refreshDynamicStateParameters,
shouldUpdateBasedOnHass,
stopEventFromActivatingCardWideActions, stopEventFromActivatingCardWideActions,
} from '../common.js'; } from '../common.js';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { MenuSubmenu, MenuSubmenuItem } from '../types.js'; import { MenuSubmenu, MenuSubmenuItem, MenuSubmenuSelect } from '../types.js';
import { actionHandler } from '../action-handler-directive.js'; import { actionHandler } from '../action-handler-directive.js';
import { domainIcon } from '../icons/domain-icon.js';
import submenuStyle from '../scss/submenu.scss'; import submenuStyle from '../scss/submenu.scss';
@@ -35,7 +45,7 @@ export class FrigateCardSubmenu extends LitElement {
return html` return html`
<mwc-list-item <mwc-list-item
style="${styleMap(stateParameters.style || {})}" style="${styleMap(stateParameters.style || {})}"
graphic="icon" graphic=${ifDefined(stateParameters.icon ? 'icon' : undefined)}
?twoline=${!!item.subtitle} ?twoline=${!!item.subtitle}
?selected=${item.selected} ?selected=${item.selected}
?activated=${item.selected} ?activated=${item.selected}
@@ -51,9 +61,7 @@ export class FrigateCardSubmenu extends LitElement {
})} })}
> >
<span>${stateParameters.title || ''}</span> <span>${stateParameters.title || ''}</span>
${item.subtitle ${item.subtitle ? html`<span slot="secondary">${item.subtitle}</span>` : ''}
? html`<span slot="secondary">${item.subtitle}</span>`
: ''}
${stateParameters.icon ${stateParameters.icon
? html` <ha-icon ? html` <ha-icon
data-domain=${ifDefined(stateParameters.data_domain)} data-domain=${ifDefined(stateParameters.data_domain)}
@@ -109,3 +117,97 @@ export class FrigateCardSubmenu extends LitElement {
return unsafeCSS(submenuStyle); return unsafeCSS(submenuStyle);
} }
} }
@customElement('frigate-card-submenu-select')
export class FrigateCardSubmenuSelect extends LitElement {
@property({ attribute: false })
public hass?: HomeAssistant;
@property({ attribute: false })
public submenuSelect?: MenuSubmenuSelect;
@property({ attribute: false })
public corner?: Corner;
protected _generatedSubmenu?: MenuSubmenu;
protected shouldUpdate(changedProps: PropertyValues): boolean {
// No need to update the submenu unless the select entity has changed.
const oldHass = changedProps.get('hass') as HomeAssistant | undefined;
return (
changedProps.size != 1 ||
!this.submenuSelect ||
(!!oldHass &&
shouldUpdateBasedOnHass(this.hass, oldHass, [this.submenuSelect.entity]))
);
}
protected willUpdate(): void {
if (!this.submenuSelect || !this.hass) {
return;
}
const entity = this.submenuSelect.entity;
const stateObj = this.hass.states[entity];
const options = stateObj?.attributes?.options;
if (!stateObj || !options) {
return;
}
const submenu: MenuSubmenu = {
// Default icon. It should be impossible for this to be used, since
// this.submenuSelect will always have an entity, which means
// refreshDynamicStateParameters will always return an icon.
icon: domainIcon('select'),
// Pull out the dynamic properties (like icon, and title) from the state.
...refreshDynamicStateParameters(this.hass, this.submenuSelect),
// Override it with anything explicitly set in the submenuSelect.
...this.submenuSelect,
type: 'custom:frigate-card-menu-submenu',
items: [],
}
// Remove the options parameter which is unused/unsupported in submenu.
delete submenu['options'];
for (const option of options) {
// If there's a device_class there may be a localized translation of the
// select title available via HASS.
const title = stateObj.attributes.device_class
? this.hass.localize(
`component.select.state.${stateObj.attributes.device_class}.${option}`,
)
: option;
submenu.items.push({
selected: stateObj.state === option,
title: title || option,
...((entity.startsWith('select.') || entity.startsWith('input_select.')) && {
tap_action: {
action: 'call-service',
service: entity.startsWith('select.')
? 'select.select_option'
: 'input_select.select_option',
service_data: {
entity_id: entity,
option: option,
},
},
}),
// Apply overrides the user may have specified for a given option.
...(this.submenuSelect.options && this.submenuSelect.options[option]),
});
}
this._generatedSubmenu = submenu;
}
protected render(): TemplateResult {
return html` <frigate-card-submenu
.hass=${this.hass}
.corner=${this.corner}
.submenu=${this._generatedSubmenu}
></frigate-card-submenu>`;
}
}
+13 -2
View File
@@ -436,12 +436,21 @@ const menuSubmenuItemSchema = elementsBaseSchema.extend({
}); });
export type MenuSubmenuItem = z.infer<typeof menuSubmenuItemSchema>; export type MenuSubmenuItem = z.infer<typeof menuSubmenuItemSchema>;
export const menuSubmenuSchema = menuBaseSchema.merge(iconSchema).extend({ const menuSubmenuSchema = menuBaseSchema.merge(iconSchema).extend({
type: z.literal('custom:frigate-card-menu-submenu'), type: z.literal('custom:frigate-card-menu-submenu'),
items: menuSubmenuItemSchema.array(), items: menuSubmenuItemSchema.array(),
}); });
export type MenuSubmenu = z.infer<typeof menuSubmenuSchema>; export type MenuSubmenu = z.infer<typeof menuSubmenuSchema>;
export type MenuItem = MenuIcon | MenuStateIcon | MenuSubmenu;
const menuSubmenuSelectSchema = menuBaseSchema.merge(stateIconSchema).extend({
type: z.literal('custom:frigate-card-menu-submenu-select'),
// Please ensure additions to this type are filtered out appropriately in
// `submenu.ts` when this is converted to a MenuSubmenu.
options: z.record(menuSubmenuItemSchema).optional(),
});
export type MenuSubmenuSelect = z.infer<typeof menuSubmenuSelectSchema>;
export type MenuItem = MenuIcon | MenuStateIcon | MenuSubmenu | MenuSubmenuSelect;
const frigateCardConditionSchema = z.object({ const frigateCardConditionSchema = z.object({
view: z.string().array().optional(), view: z.string().array().optional(),
@@ -464,6 +473,7 @@ const pictureElementSchema = z.union([
menuStateIconSchema, menuStateIconSchema,
menuIconSchema, menuIconSchema,
menuSubmenuSchema, menuSubmenuSchema,
menuSubmenuSelectSchema,
frigateConditionalSchema, frigateConditionalSchema,
stateBadgeIconSchema, stateBadgeIconSchema,
stateIconSchema, stateIconSchema,
@@ -1072,6 +1082,7 @@ const menuButtonSchema = z.discriminatedUnion('type', [
menuIconSchema, menuIconSchema,
menuStateIconSchema, menuStateIconSchema,
menuSubmenuSchema, menuSubmenuSchema,
menuSubmenuSelectSchema,
]); ]);
export type MenuButton = z.infer<typeof menuButtonSchema>; export type MenuButton = z.infer<typeof menuButtonSchema>;
export interface ExtendedHomeAssistant extends HomeAssistant { export interface ExtendedHomeAssistant extends HomeAssistant {