Merge pull request #985 from dermotduffy/select-labels
Fix broken select entity translation labels
This commit is contained in:
@@ -1699,6 +1699,7 @@ class FrigateCard extends LitElement {
|
|||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.menuConfig=${this._getConfig().menu}
|
.menuConfig=${this._getConfig().menu}
|
||||||
.buttons=${this._getMenuButtons()}
|
.buttons=${this._getMenuButtons()}
|
||||||
|
.entityRegistryManager=${this._entityRegistryManager}
|
||||||
></frigate-card-menu>
|
></frigate-card-menu>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
import { FRIGATE_ICON_SVG_PATH } from '../camera-manager/frigate/icon.js';
|
import { FRIGATE_ICON_SVG_PATH } from '../camera-manager/frigate/icon.js';
|
||||||
import { refreshDynamicStateParameters } from '../utils/ha';
|
import { refreshDynamicStateParameters } from '../utils/ha';
|
||||||
import './submenu.js';
|
import './submenu.js';
|
||||||
|
import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js';
|
||||||
|
|
||||||
export const FRIGATE_BUTTON_MENU_ICON = 'frigate';
|
export const FRIGATE_BUTTON_MENU_ICON = 'frigate';
|
||||||
|
|
||||||
@@ -64,6 +65,9 @@ export class FrigateCardMenu extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public buttons: MenuButton[] = [];
|
public buttons: MenuButton[] = [];
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public entityRegistryManager?: EntityRegistryManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if a given menu configuration is a hiding menu.
|
* Determine if a given menu configuration is a hiding menu.
|
||||||
* @param menuConfig The menu configuration.
|
* @param menuConfig The menu configuration.
|
||||||
@@ -237,6 +241,7 @@ export class FrigateCardMenu extends LitElement {
|
|||||||
return html` <frigate-card-submenu-select
|
return html` <frigate-card-submenu-select
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.submenuSelect=${button}
|
.submenuSelect=${button}
|
||||||
|
.entityRegistryManager=${this.entityRegistryManager}
|
||||||
@action=${this._actionHandler.bind(this)}
|
@action=${this._actionHandler.bind(this)}
|
||||||
>
|
>
|
||||||
</frigate-card-submenu-select>`;
|
</frigate-card-submenu-select>`;
|
||||||
|
|||||||
+49
-15
@@ -7,7 +7,7 @@ import {
|
|||||||
TemplateResult,
|
TemplateResult,
|
||||||
unsafeCSS,
|
unsafeCSS,
|
||||||
} from 'lit';
|
} 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 { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
import { actionHandler } from '../action-handler-directive.js';
|
import { actionHandler } from '../action-handler-directive.js';
|
||||||
@@ -23,6 +23,8 @@ import {
|
|||||||
stopEventFromActivatingCardWideActions,
|
stopEventFromActivatingCardWideActions,
|
||||||
} from '../utils/action.js';
|
} from '../utils/action.js';
|
||||||
import { isHassDifferent, refreshDynamicStateParameters } from '../utils/ha';
|
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';
|
import { domainIcon } from '../utils/icons/domain-icon.js';
|
||||||
|
|
||||||
@customElement('frigate-card-submenu')
|
@customElement('frigate-card-submenu')
|
||||||
@@ -127,6 +129,12 @@ export class FrigateCardSubmenuSelect extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public submenuSelect?: MenuSubmenuSelect;
|
public submenuSelect?: MenuSubmenuSelect;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public entityRegistryManager?: EntityRegistryManager;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
protected _optionTitles?: Record<string, string>;
|
||||||
|
|
||||||
protected _generatedSubmenu?: MenuSubmenu;
|
protected _generatedSubmenu?: MenuSubmenu;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,12 +146,39 @@ export class FrigateCardSubmenuSelect extends LitElement {
|
|||||||
// No need to update the submenu unless the select entity has changed.
|
// No need to update the submenu unless the select entity has changed.
|
||||||
const oldHass = changedProps.get('hass') as HomeAssistant | undefined;
|
const oldHass = changedProps.get('hass') as HomeAssistant | undefined;
|
||||||
return (
|
return (
|
||||||
changedProps.size != 1 ||
|
!changedProps.has('hass') ||
|
||||||
|
!oldHass ||
|
||||||
!this.submenuSelect ||
|
!this.submenuSelect ||
|
||||||
(!!oldHass && isHassDifferent(this.hass, oldHass, [this.submenuSelect.entity]))
|
isHassDifferent(this.hass, oldHass, [this.submenuSelect.entity])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async _refreshOptionTitles(): Promise<void> {
|
||||||
|
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.
|
* Called when the render function will be called.
|
||||||
*/
|
*/
|
||||||
@@ -151,8 +186,13 @@ export class FrigateCardSubmenuSelect extends LitElement {
|
|||||||
if (!this.submenuSelect || !this.hass) {
|
if (!this.submenuSelect || !this.hass) {
|
||||||
return;
|
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;
|
const options = stateObj?.attributes?.options;
|
||||||
if (!stateObj || !options) {
|
if (!stateObj || !options) {
|
||||||
return;
|
return;
|
||||||
@@ -180,26 +220,20 @@ export class FrigateCardSubmenuSelect extends LitElement {
|
|||||||
delete submenu['options'];
|
delete submenu['options'];
|
||||||
|
|
||||||
for (const option of options) {
|
for (const option of options) {
|
||||||
// If there's a device_class there may be a localized translation of the
|
const title = this._optionTitles?.[option] ?? option;
|
||||||
// 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({
|
submenu.items.push({
|
||||||
state_color: true,
|
state_color: true,
|
||||||
selected: stateObj.state === option,
|
selected: stateObj.state === option,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
title: title || option,
|
title: title || option,
|
||||||
...((entity.startsWith('select.') || entity.startsWith('input_select.')) && {
|
...((entityID.startsWith('select.') || entityID.startsWith('input_select.')) && {
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: 'call-service',
|
action: 'call-service',
|
||||||
service: entity.startsWith('select.')
|
service: entityID.startsWith('select.')
|
||||||
? 'select.select_option'
|
? 'select.select_option'
|
||||||
: 'input_select.select_option',
|
: 'input_select.select_option',
|
||||||
service_data: {
|
service_data: {
|
||||||
entity_id: entity,
|
entity_id: entityID,
|
||||||
option: option,
|
option: option,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const entitySchema = z.object({
|
|||||||
entity_id: z.string(),
|
entity_id: z.string(),
|
||||||
hidden_by: z.string().nullable(),
|
hidden_by: z.string().nullable(),
|
||||||
platform: z.string(),
|
platform: z.string(),
|
||||||
|
translation_key: z.string().nullable(),
|
||||||
unique_id: z.string().optional(),
|
unique_id: z.string().optional(),
|
||||||
});
|
});
|
||||||
export type Entity = z.infer<typeof entitySchema>;
|
export type Entity = z.infer<typeof entitySchema>;
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user