From 28292483fce0d27576f329fb051196b9480d5184 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sun, 8 May 2022 15:58:56 -0700 Subject: [PATCH 1/2] Add native select entity support. --- README.md | 64 +++++++++++++++++++++ rollup.config.js | 5 +- src/components/elements.ts | 4 ++ src/components/menu.ts | 9 ++- src/components/submenu.ts | 114 +++++++++++++++++++++++++++++++++++-- src/types.ts | 15 ++++- 6 files changed, 200 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index c2da62c6..87d60d34 100644 --- a/README.md +++ b/README.md @@ -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`. | | `items` | A list of menu items, as described below. | + + ##### Submenu Items | 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). +#### `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). + #### `custom:frigate-card-conditional` @@ -1210,6 +1225,55 @@ elements: + + +### Configuring a `select` submenu + +You can easily add a submenu to the menu based on a `select` or `input_select` entity. + +
+ Expand: Adding a select submenu + +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! +``` + +
### Overriding card behavior diff --git a/rollup.config.js b/rollup.config.js index d6d1f407..daf92929 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -9,7 +9,8 @@ import styles from 'rollup-plugin-styles'; import image from '@rollup/plugin-image'; 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 = { contentBase: ['./dist'], @@ -50,7 +51,7 @@ const plugins = [ 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'), } }), - dev && serve(serveopts), + watch && serve(serveopts), !dev && terser(), ]; diff --git a/src/components/elements.ts b/src/components/elements.ts index 11e6648e..71578269 100644 --- a/src/components/elements.ts +++ b/src/components/elements.ts @@ -9,6 +9,7 @@ import { MenuStateIcon, PictureElements, MenuSubmenu, + MenuSubmenuSelect, } from '../types.js'; import { dispatchErrorMessageEvent, dispatchFrigateCardEvent } from '../common.js'; @@ -331,3 +332,6 @@ export class FrigateCardElementsMenuStateIcon extends FrigateCardElementsBaseMen @customElement('frigate-card-menu-submenu') export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuIcon {} + +@customElement('frigate-card-menu-submenu-select') +export class FrigateCardElementsMenuSubmenuSelect extends FrigateCardElementsBaseMenuIcon {} diff --git a/src/components/menu.ts b/src/components/menu.ts index 3336289b..07888542 100644 --- a/src/components/menu.ts +++ b/src/components/menu.ts @@ -252,13 +252,20 @@ export class FrigateCardMenu extends LitElement { if (button.enabled === false) { return; } - if (button.type == 'custom:frigate-card-menu-submenu') { + if (button.type === 'custom:frigate-card-menu-submenu') { return html` `; + } else if (button.type === 'custom:frigate-card-menu-submenu-select') { + return html` + `; } let stateParameters: StateParameters = { ...button }; diff --git a/src/components/submenu.ts b/src/components/submenu.ts index 2e25a398..41b6e3a4 100644 --- a/src/components/submenu.ts +++ b/src/components/submenu.ts @@ -1,17 +1,27 @@ 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 { customElement, property } from 'lit/decorators.js'; + import { frigateCardHasAction, refreshDynamicStateParameters, + shouldUpdateBasedOnHass, stopEventFromActivatingCardWideActions, } from '../common.js'; import { ifDefined } from 'lit/directives/if-defined.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 { domainIcon } from '../icons/domain-icon.js'; import submenuStyle from '../scss/submenu.scss'; @@ -35,7 +45,7 @@ export class FrigateCardSubmenu extends LitElement { return html` ${stateParameters.title || ''} - ${item.subtitle - ? html`${item.subtitle}` - : ''} + ${item.subtitle ? html`${item.subtitle}` : ''} ${stateParameters.icon ? html` `; + } +} diff --git a/src/types.ts b/src/types.ts index f66ea1f6..b9c2fa27 100644 --- a/src/types.ts +++ b/src/types.ts @@ -436,12 +436,21 @@ const menuSubmenuItemSchema = elementsBaseSchema.extend({ }); export type MenuSubmenuItem = z.infer; -export const menuSubmenuSchema = menuBaseSchema.merge(iconSchema).extend({ +const menuSubmenuSchema = menuBaseSchema.merge(iconSchema).extend({ type: z.literal('custom:frigate-card-menu-submenu'), items: menuSubmenuItemSchema.array(), }); export type MenuSubmenu = z.infer; -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; + +export type MenuItem = MenuIcon | MenuStateIcon | MenuSubmenu | MenuSubmenuSelect; const frigateCardConditionSchema = z.object({ view: z.string().array().optional(), @@ -464,6 +473,7 @@ const pictureElementSchema = z.union([ menuStateIconSchema, menuIconSchema, menuSubmenuSchema, + menuSubmenuSelectSchema, frigateConditionalSchema, stateBadgeIconSchema, stateIconSchema, @@ -1072,6 +1082,7 @@ const menuButtonSchema = z.discriminatedUnion('type', [ menuIconSchema, menuStateIconSchema, menuSubmenuSchema, + menuSubmenuSelectSchema, ]); export type MenuButton = z.infer; export interface ExtendedHomeAssistant extends HomeAssistant { From a417df55212d6a067bbce4e05a92e57e5b5e767a Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sun, 8 May 2022 16:12:28 -0700 Subject: [PATCH 2/2] Codereview fixes. --- README.md | 4 +--- src/components/submenu.ts | 16 +++++++++++++--- src/types.ts | 2 -- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 87d60d34..e87481b0 100644 --- a/README.md +++ b/README.md @@ -1252,7 +1252,6 @@ The following will convert this entity into a submenu: [...] elements: - type: custom:frigate-card-menu-submenu-select - icon: mdi:lamps entity: input_select.kitchen_scene ``` @@ -1268,11 +1267,10 @@ elements: scene.kitchen_cooking_scene: icon: mdi:chef-hat title: Cooking time! - scene.kitchen_dining_scene: + scene.kitchen_tv_scene: icon: mdi:television title: TV! ``` - ### Overriding card behavior diff --git a/src/components/submenu.ts b/src/components/submenu.ts index 41b6e3a4..38be9689 100644 --- a/src/components/submenu.ts +++ b/src/components/submenu.ts @@ -131,6 +131,11 @@ export class FrigateCardSubmenuSelect extends LitElement { protected _generatedSubmenu?: MenuSubmenu; + /** + * Called to determine if the update should proceed. + * @param changedProps + * @returns `true` if the update should proceed, `false` otherwise. + */ 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; @@ -142,6 +147,9 @@ export class FrigateCardSubmenuSelect extends LitElement { ); } + /** + * Called when the render function will be called. + */ protected willUpdate(): void { if (!this.submenuSelect || !this.hass) { return; @@ -161,15 +169,17 @@ export class FrigateCardSubmenuSelect extends LitElement { // 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. + // For cleanliness remove the options parameter which is unused by the + // submenu rendering itself (above). It is only in this method to populate + // the items correctly (below). delete submenu['options']; for (const option of options) { diff --git a/src/types.ts b/src/types.ts index b9c2fa27..a78cdcd5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -444,8 +444,6 @@ export type MenuSubmenu = z.infer; 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;