From 3cfebabd1b08d67cef744b52ed018644a362b929 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 4 Mar 2023 12:16:38 -0800 Subject: [PATCH] Fix broken select entity translation labels. --- src/card.ts | 1 + src/components/menu.ts | 5 ++ src/components/submenu.ts | 64 ++++++++++++++++++------ src/utils/ha/entity-registry/types.ts | 1 + src/utils/ha/entity-state-translation.ts | 49 ++++++++++++++++++ 5 files changed, 105 insertions(+), 15 deletions(-) create mode 100644 src/utils/ha/entity-state-translation.ts diff --git a/src/card.ts b/src/card.ts index 9abdd740..aeddda8c 100644 --- a/src/card.ts +++ b/src/card.ts @@ -1699,6 +1699,7 @@ class FrigateCard extends LitElement { .hass=${this._hass} .menuConfig=${this._getConfig().menu} .buttons=${this._getMenuButtons()} + .entityRegistryManager=${this._entityRegistryManager} > `; } diff --git a/src/components/menu.ts b/src/components/menu.ts index 1a3cd342..010861a1 100644 --- a/src/components/menu.ts +++ b/src/components/menu.ts @@ -30,6 +30,7 @@ import { import { FRIGATE_ICON_SVG_PATH } from '../camera-manager/frigate/icon.js'; import { refreshDynamicStateParameters } from '../utils/ha'; import './submenu.js'; +import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js'; export const FRIGATE_BUTTON_MENU_ICON = 'frigate'; @@ -64,6 +65,9 @@ export class FrigateCardMenu extends LitElement { @property({ attribute: false }) public buttons: MenuButton[] = []; + @property({ attribute: false }) + public entityRegistryManager?: EntityRegistryManager; + /** * Determine if a given menu configuration is a hiding menu. * @param menuConfig The menu configuration. @@ -237,6 +241,7 @@ export class FrigateCardMenu extends LitElement { return html` `; diff --git a/src/components/submenu.ts b/src/components/submenu.ts index a1bd6247..4e9af581 100644 --- a/src/components/submenu.ts +++ b/src/components/submenu.ts @@ -7,7 +7,7 @@ import { TemplateResult, unsafeCSS, } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; +import { customElement, property, state } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { styleMap } from 'lit/directives/style-map.js'; import { actionHandler } from '../action-handler-directive.js'; @@ -23,6 +23,8 @@ import { stopEventFromActivatingCardWideActions, } from '../utils/action.js'; import { isHassDifferent, refreshDynamicStateParameters } from '../utils/ha'; +import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js'; +import { getEntityStateTranslation } from '../utils/ha/entity-state-translation.js'; import { domainIcon } from '../utils/icons/domain-icon.js'; @customElement('frigate-card-submenu') @@ -127,6 +129,12 @@ export class FrigateCardSubmenuSelect extends LitElement { @property({ attribute: false }) public submenuSelect?: MenuSubmenuSelect; + @property({ attribute: false }) + public entityRegistryManager?: EntityRegistryManager; + + @state() + protected _optionTitles?: Record; + protected _generatedSubmenu?: MenuSubmenu; /** @@ -138,12 +146,39 @@ export class FrigateCardSubmenuSelect extends LitElement { // No need to update the submenu unless the select entity has changed. const oldHass = changedProps.get('hass') as HomeAssistant | undefined; return ( - changedProps.size != 1 || + !changedProps.has('hass') || + !oldHass || !this.submenuSelect || - (!!oldHass && isHassDifferent(this.hass, oldHass, [this.submenuSelect.entity])) + isHassDifferent(this.hass, oldHass, [this.submenuSelect.entity]) ); } + protected async _refreshOptionTitles(): Promise { + if (!this.hass || !this.submenuSelect) { + return; + } + const entityID = this.submenuSelect.entity; + const stateObj = this.hass.states[entityID]; + const options = stateObj?.attributes?.options; + const entity = + (await this.entityRegistryManager?.getEntity(this.hass, entityID)) ?? null; + + const optionTitles = {}; + for (const option of options) { + const title = getEntityStateTranslation(this.hass, entityID, { + ...(entity && { entity: entity }), + state: option, + }); + if (title) { + optionTitles[option] = title; + } + } + + // This will cause a re-render with the updated title if it is + // different. + this._optionTitles = optionTitles; + } + /** * Called when the render function will be called. */ @@ -151,8 +186,13 @@ export class FrigateCardSubmenuSelect extends LitElement { if (!this.submenuSelect || !this.hass) { return; } - const entity = this.submenuSelect.entity; - const stateObj = this.hass.states[entity]; + + if (!this._optionTitles) { + this._refreshOptionTitles(); + } + + const entityID = this.submenuSelect.entity; + const stateObj = this.hass.states[entityID]; const options = stateObj?.attributes?.options; if (!stateObj || !options) { return; @@ -180,26 +220,20 @@ export class FrigateCardSubmenuSelect extends LitElement { 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; + const title = this._optionTitles?.[option] ?? option; submenu.items.push({ state_color: true, selected: stateObj.state === option, enabled: true, title: title || option, - ...((entity.startsWith('select.') || entity.startsWith('input_select.')) && { + ...((entityID.startsWith('select.') || entityID.startsWith('input_select.')) && { tap_action: { action: 'call-service', - service: entity.startsWith('select.') + service: entityID.startsWith('select.') ? 'select.select_option' : 'input_select.select_option', service_data: { - entity_id: entity, + entity_id: entityID, option: option, }, }, diff --git a/src/utils/ha/entity-registry/types.ts b/src/utils/ha/entity-registry/types.ts index ddf0b9dc..0d042483 100644 --- a/src/utils/ha/entity-registry/types.ts +++ b/src/utils/ha/entity-registry/types.ts @@ -6,6 +6,7 @@ export const entitySchema = z.object({ entity_id: z.string(), hidden_by: z.string().nullable(), platform: z.string(), + translation_key: z.string().nullable(), unique_id: z.string().optional(), }); export type Entity = z.infer; diff --git a/src/utils/ha/entity-state-translation.ts b/src/utils/ha/entity-state-translation.ts new file mode 100644 index 00000000..2a4e26fe --- /dev/null +++ b/src/utils/ha/entity-state-translation.ts @@ -0,0 +1,49 @@ +import { computeDomain, HomeAssistant } from 'custom-card-helpers'; +import { HassEntity } from 'home-assistant-js-websocket'; +import { Entity } from './entity-registry/types'; + +/** + * Get the translation of an entity state. Inspired by: + * https://github.com/home-assistant/frontend/blob/dev/src/common/entity/compute_state_display.ts#L204-L218 + * + * This may no longer be necessary to custom implement if `custom-card-helpers` + * is updated to reflect how the Home Assistant frontend now [as of 2023-03-04] + * computes state display (e.g. supports usage of `translation_key`). + * + * https://github.com/custom-cards/custom-card-helpers/blob/master/src/compute-state-display.ts + * + */ +export const getEntityStateTranslation = ( + hass: HomeAssistant, + entityID: string, + options?: { + entity?: Entity; + state?: string; + }, +): string | null => { + const stateObj: HassEntity | undefined = hass.states[entityID]; + const state = options?.state ? options.state : stateObj ? stateObj.state : null; + + if (!state) { + return null; + } + + const domain = computeDomain(entityID); + const attributes = stateObj ? stateObj.attributes : null; + + return ( + // Return the translation_key translation. + (options?.entity?.translation_key && + hass.localize( + `component.${options.entity.platform}.entity.${domain}` + + `.${options.entity.translation_key}.state.${state}`, + )) || + // Return device class translation + (attributes?.device_class && + hass.localize(`component.${domain}.state.${attributes.device_class}.${state}`)) || + // Return default translation + hass.localize(`component.${domain}.state._.${state}`) || + // We don't know! Return the raw state. + state + ); +};