diff --git a/demo/src/ha-demo.ts b/demo/src/ha-demo.ts index 070fea8b44..37a707952a 100644 --- a/demo/src/ha-demo.ts +++ b/demo/src/ha-demo.ts @@ -71,6 +71,7 @@ class HaDemo extends HomeAssistantAppEl { entity_category: null, has_entity_name: false, unique_id: "co2_intensity", + aliases: [], }, { config_entry_id: "co2signal", @@ -86,6 +87,7 @@ class HaDemo extends HomeAssistantAppEl { entity_category: null, has_entity_name: false, unique_id: "grid_fossil_fuel_percentage", + aliases: [], }, ]); diff --git a/gallery/src/pages/misc/integration-card.ts b/gallery/src/pages/misc/integration-card.ts index 56de4308e8..8bd6acf6c8 100644 --- a/gallery/src/pages/misc/integration-card.ts +++ b/gallery/src/pages/misc/integration-card.ts @@ -197,6 +197,7 @@ const createEntityRegistryEntries = ( platform: "updater", has_entity_name: false, unique_id: "updater", + aliases: [], }, ]; diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index a80562cb41..44e1b3679c 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -22,6 +22,7 @@ export interface EntityRegistryEntry { original_name?: string; unique_id: string; translation_key?: string; + aliases: string[]; } export interface ExtEntityRegistryEntry extends EntityRegistryEntry { @@ -63,6 +64,7 @@ export interface EntityRegistryEntryUpdateParams { new_entity_id?: string; options_domain?: string; options?: SensorEntityOptions | NumberEntityOptions | WeatherEntityOptions; + aliases?: string[]; } export const findBatteryEntity = ( diff --git a/src/panels/config/entities/entity-aliases/dialog-entity-aliases.ts b/src/panels/config/entities/entity-aliases/dialog-entity-aliases.ts new file mode 100644 index 0000000000..29e3e012cc --- /dev/null +++ b/src/panels/config/entities/entity-aliases/dialog-entity-aliases.ts @@ -0,0 +1,200 @@ +import "@material/mwc-button/mwc-button"; +import { mdiDeleteOutline, mdiPlus } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { computeStateName } from "../../../../common/entity/compute_state_name"; +import "../../../../components/ha-alert"; +import "../../../../components/ha-area-picker"; +import "../../../../components/ha-dialog"; +import "../../../../components/ha-textfield"; +import type { HaTextField } from "../../../../components/ha-textfield"; +import { haStyle, haStyleDialog } from "../../../../resources/styles"; +import { HomeAssistant } from "../../../../types"; +import { EntityAliasesDialogParams } from "./show-dialog-entity-aliases"; + +@customElement("dialog-entity-aliases") +class DialogEntityAliases extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _error?: string; + + @state() private _params?: EntityAliasesDialogParams; + + @state() private _aliases!: string[]; + + @state() private _submitting = false; + + public async showDialog(params: EntityAliasesDialogParams): Promise { + this._params = params; + this._error = undefined; + this._aliases = + this._params.entity.aliases?.length > 0 + ? this._params.entity.aliases + : [""]; + await this.updateComplete; + } + + public closeDialog(): void { + this._error = ""; + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._params) { + return html``; + } + + const entityId = this._params.entity.entity_id; + const stateObj = entityId ? this.hass.states[entityId] : undefined; + + const name = (stateObj && computeStateName(stateObj)) || entityId; + + return html` + +
+ ${this._error + ? html`${this._error} ` + : ""} +
+ ${this._aliases.map( + (alias, index) => html` +
+ + +
+ ` + )} +
+ + ${this.hass!.localize( + "ui.dialogs.entity_registry.editor.aliases.add_alias" + )} + + +
+
+
+ + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.aliases.save" + )} + +
+ `; + } + + private async _addAlias() { + this._aliases = [...this._aliases, ""]; + await this.updateComplete; + const field = this.shadowRoot?.querySelector(`ha-textfield[data-last]`) as + | HaTextField + | undefined; + field?.focus(); + } + + private async _editAlias(ev: Event) { + const index = (ev.target as any).index; + this._aliases[index] = (ev.target as any).value; + } + + private async _removeAlias(ev: Event) { + const index = (ev.target as any).index; + const aliases = [...this._aliases]; + aliases.splice(index, 1); + this._aliases = aliases; + } + + private async _updateEntry(): Promise { + this._submitting = true; + const noEmptyAliases = this._aliases + .map((alias) => alias.trim()) + .filter((alias) => alias); + + try { + await this._params!.updateEntry({ + aliases: noEmptyAliases, + }); + this.closeDialog(); + } catch (err: any) { + this._error = + err.message || + this.hass.localize( + "ui.dialogs.entity_registry.editor.aliases.unknown_error" + ); + } finally { + this._submitting = false; + } + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + haStyleDialog, + css` + .row { + margin-bottom: 8px; + } + ha-textfield { + display: block; + } + ha-icon-button { + display: block; + } + mwc-button { + margin-left: 8px; + } + #alias_input { + margin-top: 8px; + } + .alias { + border: 1px solid var(--divider-color); + border-radius: 4px; + margin-top: 4px; + --mdc-icon-button-size: 24px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-entity-aliases": DialogEntityAliases; + } +} diff --git a/src/panels/config/entities/entity-aliases/show-dialog-entity-aliases.ts b/src/panels/config/entities/entity-aliases/show-dialog-entity-aliases.ts new file mode 100644 index 0000000000..d3fec599ec --- /dev/null +++ b/src/panels/config/entities/entity-aliases/show-dialog-entity-aliases.ts @@ -0,0 +1,25 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; +import { + EntityRegistryEntry, + EntityRegistryEntryUpdateParams, +} from "../../../../data/entity_registry"; + +export interface EntityAliasesDialogParams { + entity: EntityRegistryEntry; + updateEntry: ( + updates: Partial + ) => Promise; +} + +export const loadEntityAliasesDialog = () => import("./dialog-entity-aliases"); + +export const showEntityAliasesDialog = ( + element: HTMLElement, + entityAliasesParams: EntityAliasesDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-entity-aliases", + dialogImport: loadEntityAliasesDialog, + dialogParams: entityAliasesParams, + }); +}; diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 21f1fbd1cd..615485d399 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -1,6 +1,7 @@ import "@material/mwc-button/mwc-button"; import "@material/mwc-formfield/mwc-formfield"; import "@material/mwc-list/mwc-list-item"; +import { mdiPencil } from "@mdi/js"; import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, @@ -26,6 +27,7 @@ import { import "../../../components/ha-alert"; import "../../../components/ha-area-picker"; import "../../../components/ha-expansion-panel"; +import "../../../components/ha-icon"; import "../../../components/ha-icon-picker"; import "../../../components/ha-radio"; import "../../../components/ha-select"; @@ -75,6 +77,7 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail"; +import { showEntityAliasesDialog } from "./entity-aliases/show-dialog-entity-aliases"; const OVERRIDE_DEVICE_CLASSES = { cover: [ @@ -673,7 +676,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
${this.hass.localize( "ui.dialogs.entity_registry.editor.entity_status" - )}: + )}
${this._disabledBy && @@ -760,12 +763,43 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
` : ""} + +
+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.aliases_section" + )} +
+ + 0} + hasMeta + @click=${this._openAliasesSettings} + > + + ${this.entry.aliases.length > 0 + ? this.hass.localize( + "ui.dialogs.entity_registry.editor.configured_aliases", + { count: this.entry.aliases.length } + ) + : this.hass.localize( + "ui.dialogs.entity_registry.editor.no_aliases" + )} + + ${this.entry.aliases.join(", ")} + + + +
+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.aliases.description" + )} +
${this.entry.device_id ? html`
${this.hass.localize( "ui.dialogs.entity_registry.editor.change_area" - )}: + )}
{ + const result = await updateEntityRegistryEntry( + this.hass, + this.entry.entity_id, + updates + ); + fireEvent(this, "entity-entry-updated", result.entity_entry); + }, + }); + } + private async _enableEntry() { this._error = undefined; this._submitting = true; @@ -1212,7 +1260,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { } .secondary { margin: 8px 0; - width: 340px; } li[divider] { border-bottom-color: var(--divider-color); @@ -1220,6 +1267,13 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { ha-alert mwc-button { width: max-content; } + .aliases { + border-radius: 4px; + margin-top: 4px; + margin-bottom: 4px; + --mdc-icon-button-size: 24px; + overflow: hidden; + } `, ]; } diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 5fb9d6bf9c..cafff74d14 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -728,6 +728,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { selectable: false, entity_category: null, has_entity_name: false, + aliases: [], }); } if (changed) { diff --git a/src/translations/en.json b/src/translations/en.json index f8415ad180..c828d219e0 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -961,6 +961,19 @@ "stream_orientation_6": "Rotate left", "stream_orientation_7": "Rotate right and flip", "stream_orientation_8": "Rotate right" + }, + "aliases_section": "Aliases", + "no_aliases": "No configured aliases", + "configured_aliases": "{count} configured {count, plural,\n one {alias}\n other {aliases}\n}", + "aliases": { + "heading": "{name} aliases", + "description": "Aliases are alternative names used in voice assistants to refer to this entity.", + "remove_alias": "Remove alias", + "save": "Save", + "add_alias": "Add alias", + "no_aliases": "No aliases have been added yet", + "update": "Update", + "unknown_error": "Unknown error" } } },