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
+ );
+};