diff --git a/demo/src/stubs/frontend.ts b/demo/src/stubs/frontend.ts index ae4ac073fd..70a4d5a0d2 100644 --- a/demo/src/stubs/frontend.ts +++ b/demo/src/stubs/frontend.ts @@ -1,7 +1,30 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; +let changeFunction; + export const mockFrontend = (hass: MockHomeAssistant) => { hass.mockWS("frontend/get_user_data", () => ({ value: null, })); + hass.mockWS("frontend/set_user_data", ({ key, value }) => { + if (key === "sidebar") { + changeFunction?.({ + value: { + panelOrder: value.panelOrder || [], + hiddenPanels: value.hiddenPanels || [], + }, + }); + } + }); + hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => { + changeFunction = onChange; + onChange?.({ + value: { + panelOrder: [], + hiddenPanels: [], + }, + }); + // eslint-disable-next-line @typescript-eslint/no-empty-function + return () => {}; + }); }; diff --git a/src/components/ha-items-display-editor.ts b/src/components/ha-items-display-editor.ts index 77e1dfff35..e87ecfaef0 100644 --- a/src/components/ha-items-display-editor.ts +++ b/src/components/ha-items-display-editor.ts @@ -262,7 +262,7 @@ export class HaItemDisplayEditor extends LitElement { ]; } - return items.sort((a, b) => + return visibleItems.sort((a, b) => a.disableSorting && !b.disableSorting ? -1 : compare(a.value, b.value) ); } diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index d2f77a9770..ea303ab4c8 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -368,7 +368,7 @@ class HaSidebar extends SubscribeMixin(LitElement) { if (!this._panelOrder || !this._hiddenPanels) { return html` `; } diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 4376c7bd45..0b04097dae 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -2,12 +2,12 @@ import type { Connection } from "home-assistant-js-websocket"; import { createCollection } from "home-assistant-js-websocket"; import type { Store } from "home-assistant-js-websocket/dist/store"; import memoizeOne from "memoize-one"; +import { computeDomain } from "../common/entity/compute_domain"; import { computeStateName } from "../common/entity/compute_state_name"; import { caseInsensitiveStringCompare } from "../common/string/compare"; import { debounce } from "../common/util/debounce"; import type { HomeAssistant } from "../types"; import type { LightColor } from "./light"; -import { computeDomain } from "../common/entity/compute_domain"; import type { RegistryEntry } from "./registry"; type EntityCategory = "config" | "diagnostic"; @@ -315,3 +315,12 @@ export const getEntityPlatformLookup = ( } return entityLookup; }; + +export const getAutomaticEntityIds = ( + hass: HomeAssistant, + entity_ids: string[] +) => + hass.callWS>({ + type: "config/entity_registry/get_automatic_entity_ids", + entity_ids, + }); diff --git a/src/data/history.ts b/src/data/history.ts index fb784b54f4..553421d404 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -640,6 +640,12 @@ export const mergeHistoryResults = ( } for (const item of ltsResult.line) { + if (item.unit === BLANK_UNIT) { + // disabled entities have no unit, so we need to find the unit from the history result + item.unit = + historyResult.line.find((line) => line.identifier === item.identifier) + ?.unit ?? BLANK_UNIT; + } const key = computeGroupKey( item.unit, item.device_class, diff --git a/src/data/regenerate_entity_ids.ts b/src/data/regenerate_entity_ids.ts new file mode 100644 index 0000000000..4488ffb342 --- /dev/null +++ b/src/data/regenerate_entity_ids.ts @@ -0,0 +1,129 @@ +import { html, nothing } from "lit"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../dialogs/generic/show-dialog-box"; +import type { HomeAssistant } from "../types"; +import { + getAutomaticEntityIds, + updateEntityRegistryEntry, +} from "./entity_registry"; +import "../components/ha-expansion-panel"; + +export const regenerateEntityIds = async ( + element: HTMLElement, + hass: HomeAssistant, + entities: string[] +): Promise => { + const entityIdsMapping = await getAutomaticEntityIds(hass, entities); + + const entityIdsEntries = Object.entries(entityIdsMapping); + + const dialogRename = entityIdsEntries + .filter(([oldId, newId]) => newId && oldId !== newId) + .map( + ([oldId, newId]) => + html` + ${oldId} + ${newId} + ` + ); + const dialogCantRename = entityIdsEntries + .filter(([_oldId, newId]) => newId === null) + .map(([oldId]) => html`
  • ${oldId}
  • `); + const dialogNoRename = entityIdsEntries + .filter(([oldId, newId]) => oldId === newId) + .map(([oldId]) => html`
  • ${oldId}
  • `); + if (dialogRename.length) { + showConfirmationDialog(element, { + title: hass.localize( + "ui.dialogs.recreate_entity_ids.confirm_rename_title" + ), + text: html`${hass.localize( + "ui.dialogs.recreate_entity_ids.confirm_rename_warning" + )}

    + + ${hass.localize("ui.dialogs.recreate_entity_ids.will_rename", { + count: dialogRename.length, + })} +
    + + + + + + ${dialogRename} +
    ${hass.localize("ui.dialogs.recreate_entity_ids.old")}${hass.localize("ui.dialogs.recreate_entity_ids.new")}
    +
    +
    + ${dialogCantRename.length + ? html` + ${hass.localize("ui.dialogs.recreate_entity_ids.cant_rename", { + count: dialogCantRename.length, + })} + ${dialogCantRename} + ` + : nothing} + ${dialogNoRename.length + ? html` + ${hass.localize("ui.dialogs.recreate_entity_ids.wont_change", { + count: dialogNoRename.length, + })} + ${dialogNoRename} + ` + : nothing}`, + confirmText: hass.localize("ui.common.update"), + dismissText: hass.localize("ui.common.cancel"), + destructive: true, + confirm: () => { + entityIdsEntries + .filter(([oldId, newId]) => newId && oldId !== newId) + .forEach(([oldEntityId, newEntityId]) => + updateEntityRegistryEntry(hass, oldEntityId, { + new_entity_id: newEntityId!, + }).catch((err: any) => { + showAlertDialog(element, { + title: hass.localize( + "ui.dialogs.recreate_entity_ids.update_entity_error", + { entityId: oldEntityId } + ), + text: err.message, + }); + }) + ); + }, + }); + } else { + showAlertDialog(element, { + title: hass.localize( + "ui.dialogs.recreate_entity_ids.confirm_no_renamable_entity_ids" + ), + text: html`${dialogCantRename.length + ? html` + ${hass.localize("ui.dialogs.recreate_entity_ids.cant_rename", { + count: dialogCantRename.length, + })} + ${dialogCantRename} + ` + : nothing} + ${dialogNoRename.length + ? html` + ${hass.localize("ui.dialogs.recreate_entity_ids.wont_change", { + count: dialogNoRename.length, + })} + ${dialogNoRename} + ` + : nothing}`, + }); + } +}; diff --git a/src/dialogs/config-flow/step-flow-create-entry.ts b/src/dialogs/config-flow/step-flow-create-entry.ts index 37849587bd..e9a212b13f 100644 --- a/src/dialogs/config-flow/step-flow-create-entry.ts +++ b/src/dialogs/config-flow/step-flow-create-entry.ts @@ -9,7 +9,6 @@ import { } from "../../common/entity/compute_device_name"; import { computeDomain } from "../../common/entity/compute_domain"; import { navigate } from "../../common/navigate"; -import { slugify } from "../../common/string/slugify"; import "../../components/ha-area-picker"; import "../../components/ha-button"; import { assistSatelliteSupportsSetupFlow } from "../../data/assist_satellite"; @@ -17,6 +16,7 @@ import type { DataEntryFlowStepCreateEntry } from "../../data/data_entry_flow"; import type { DeviceRegistryEntry } from "../../data/device_registry"; import { updateDeviceRegistryEntry } from "../../data/device_registry"; import { + getAutomaticEntityIds, updateEntityRegistryEntry, type EntityRegistryDisplayEntry, } from "../../data/entity_registry"; @@ -182,19 +182,11 @@ class StepFlowCreateEntry extends LitElement { private async _flowDone(): Promise { if (Object.keys(this._deviceUpdate).length) { - const renamedDevices: { - deviceId: string; - oldDeviceName: string | null | undefined; - newDeviceName: string; - }[] = []; + const renamedDevices: string[] = []; const deviceUpdates = Object.entries(this._deviceUpdate).map( ([deviceId, update]) => { if (update.name) { - renamedDevices.push({ - deviceId, - oldDeviceName: computeDeviceName(this.hass.devices[deviceId]), - newDeviceName: update.name, - }); + renamedDevices.push(deviceId); } return updateDeviceRegistryEntry(this.hass, deviceId, { name_by_user: update.name, @@ -209,38 +201,36 @@ class StepFlowCreateEntry extends LitElement { }); } ); + await Promise.allSettled(deviceUpdates); const entityUpdates: Promise[] = []; - renamedDevices.forEach(({ deviceId, oldDeviceName, newDeviceName }) => { - if (!oldDeviceName) { - return; - } + const entityIds: string[] = []; + renamedDevices.forEach((deviceId) => { const entities = this._deviceEntities( deviceId, Object.values(this.hass.entities) ); - const oldDeviceSlug = slugify(oldDeviceName); - const newDeviceSlug = slugify(newDeviceName); - entities.forEach((entity) => { - const oldId = entity.entity_id; - - if (oldId.includes(oldDeviceSlug)) { - const newEntityId = oldId.replace(oldDeviceSlug, newDeviceSlug); - entityUpdates.push( - updateEntityRegistryEntry(this.hass, entity.entity_id, { - new_entity_id: newEntityId, - }).catch((err) => - showAlertDialog(this, { - text: this.hass.localize( - "ui.panel.config.integrations.config_flow.error_saving_entity", - { error: err.message } - ), - }) - ) - ); - } - }); + entityIds.push(...entities.map((entity) => entity.entity_id)); }); - await Promise.allSettled([...deviceUpdates, ...entityUpdates]); + + const entityIdsMapping = getAutomaticEntityIds(this.hass, entityIds); + + Object.entries(entityIdsMapping).forEach(([oldEntityId, newEntityId]) => { + if (newEntityId) { + entityUpdates.push( + updateEntityRegistryEntry(this.hass, oldEntityId, { + new_entity_id: newEntityId, + }).catch((err) => + showAlertDialog(this, { + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.error_saving_entity", + { error: err.message } + ), + }) + ) + ); + } + }); + await Promise.allSettled(entityUpdates); } fireEvent(this, "flow-update", { step: undefined }); diff --git a/src/dialogs/enter-code/dialog-enter-code.ts b/src/dialogs/enter-code/dialog-enter-code.ts index baf4ef350b..0eb95992a1 100644 --- a/src/dialogs/enter-code/dialog-enter-code.ts +++ b/src/dialogs/enter-code/dialog-enter-code.ts @@ -222,7 +222,6 @@ export class DialogEnterCode grid-column-start: 6; } } - ha-control-button { width: 56px; height: 56px; @@ -238,12 +237,6 @@ export class DialogEnterCode --control-button-background-color: var(--red-color); --control-button-icon-color: var(--red-color); } - .hidden { - display: none; - } - .buttons { - margin-top: 12px; - } `; } diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 4cfea54ae7..66b19abb7e 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -8,6 +8,7 @@ import { mdiOpenInNew, mdiPencil, mdiPlusCircle, + mdiRestore, } from "@mdi/js"; import type { CSSResultGroup, TemplateResult } from "lit"; import { LitElement, css, html, nothing } from "lit"; @@ -22,13 +23,11 @@ import { computeEntityEntryName } from "../../../common/entity/compute_entity_na import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { stringCompare } from "../../../common/string/compare"; -import { slugify } from "../../../common/string/slugify"; import { groupBy } from "../../../common/util/group-by"; import "../../../components/entity/ha-battery-icon"; import "../../../components/ha-alert"; -import "../../../components/ha-button-menu"; import "../../../components/ha-button"; -import "../../../components/ha-expansion-panel"; +import "../../../components/ha-button-menu"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-next"; import "../../../components/ha-list-item"; @@ -64,6 +63,7 @@ import { } from "../../../data/entity_registry"; import type { IntegrationManifest } from "../../../data/integration"; import { domainToName } from "../../../data/integration"; +import { regenerateEntityIds } from "../../../data/regenerate_entity_ids"; import type { SceneEntities } from "../../../data/scene"; import { showSceneEditor } from "../../../data/scene"; import type { RelatedResult } from "../../../data/search"; @@ -406,7 +406,11 @@ export class HaConfigDevicePage extends LitElement { ${device.disabled_by === "user" ? html`
    - + ${this.hass.localize("ui.common.enable")}
    @@ -664,7 +668,7 @@ export class HaConfigDevicePage extends LitElement { ` : ""; - return html` + + + + + +
    + ${this.hass.localize("ui.panel.config.devices.restore_entity_ids")} +
    +
    +
    +
    ${area @@ -744,39 +763,35 @@ export class HaConfigDevicePage extends LitElement { ? html`
    - - - ${firstDeviceAction!.label} - ${firstDeviceAction!.icon - ? html` - - ` - : ""} - ${firstDeviceAction!.trailingIcon - ? html` - - ` - : ""} - - + ${firstDeviceAction!.label} + ${firstDeviceAction!.icon + ? html` + + ` + : nothing} + ${firstDeviceAction!.trailingIcon + ? html` + + ` + : nothing} +
    ${actions.length @@ -1256,7 +1271,14 @@ export class HaConfigDevicePage extends LitElement { } } - private async _showSettings() { + private _resetEntityIds = () => { + const entities = this._entities(this.deviceId, this._entityReg).map( + (e) => e.entity_id + ); + regenerateEntityIds(this, this.hass, entities); + }; + + private _showSettings = async () => { const device = this.hass.devices[this.deviceId]; showDeviceRegistryDetailDialog(this, { device, @@ -1342,153 +1364,34 @@ export class HaConfigDevicePage extends LitElement { } const entities = this._entities(this.deviceId, this._entityReg); - let renameEntityid = false; - let entityIdRenames: { oldId: string; newId?: string }[] = []; - - if (this.showAdvanced) { - const oldDeviceSlug = slugify(oldDeviceName); - const newDeviceSlug = slugify(newDeviceName); - entityIdRenames = entities.map((entity) => { - const oldId = entity.entity_id; - if (oldId.includes(oldDeviceSlug)) { - const newId = oldId.replace(oldDeviceSlug, newDeviceSlug); - return { oldId, newId }; - } - return { oldId }; - }); - - const dialogRenames = entityIdRenames - .filter((entity) => entity.newId) - .map( - (entity) => - html` - ${entity.oldId} - ${entity.newId} - ` - ); - const dialogNoRenames = entityIdRenames - .filter((entity) => !entity.newId) - .map((entity) => html`
  • ${entity.oldId}
  • `); - - if (dialogRenames.length) { - renameEntityid = await showConfirmationDialog(this, { - title: this.hass.localize( - "ui.panel.config.devices.confirm_rename_entity_ids" - ), - text: html`${this.hass.localize( - "ui.panel.config.devices.confirm_rename_entity_ids_warning" - )}

    - - ${this.hass.localize( - "ui.panel.config.devices.confirm_rename_entity_will_rename", - { count: dialogRenames.length } - )} -
    - - - - - - ${dialogRenames} -
    - ${this.hass.localize( - "ui.panel.config.devices.confirm_rename_old" - )} - - ${this.hass.localize( - "ui.panel.config.devices.confirm_rename_new" - )} -
    -
    -
    - ${dialogNoRenames.length - ? html` - ${this.hass.localize( - "ui.panel.config.devices.confirm_rename_entity_wont_rename", - { - count: dialogNoRenames.length, - deviceSlug: oldDeviceSlug, - } - )} - ${dialogNoRenames}` - : nothing} `, - confirmText: this.hass.localize("ui.common.rename"), - dismissText: this.hass.localize("ui.common.no"), - warning: true, - }); - } else if (dialogNoRenames.length) { - await showAlertDialog(this, { - title: this.hass.localize( - "ui.panel.config.devices.confirm_rename_entity_no_renamable_entity_ids" - ), - text: html` - ${this.hass.localize( - "ui.panel.config.devices.confirm_rename_entity_wont_rename", - { - deviceSlug: oldDeviceSlug, - count: dialogNoRenames.length, - } - )} - ${dialogNoRenames} - `, - }); - } - } - const updateProms = entities.map((entity) => { const name = entity.name || entity.stateName; - let newEntityId: string | undefined; let newName: string | null | undefined; - let shouldUpdateName: boolean; - let shouldUpdateEntityId = false; - if (entity.has_entity_name && !entity.name) { - shouldUpdateName = false; - } else if ( + return undefined; + } + + if ( entity.has_entity_name && (entity.name === oldDeviceName || entity.name === newDeviceName) ) { - shouldUpdateName = true; // clear name if it matches the device name and it uses the device name (entity naming) newName = null; } else if (name && name.includes(oldDeviceName)) { - shouldUpdateName = true; newName = name.replace(oldDeviceName, newDeviceName); } else { - shouldUpdateName = false; - } - - if (renameEntityid) { - const entityRename = entityIdRenames?.find( - (item) => item.oldId === entity.entity_id - ); - if (entityRename?.newId) { - shouldUpdateEntityId = true; - newEntityId = entityRename.newId; - } - } - - if (newName === undefined && newEntityId === undefined) { return undefined; } return updateEntityRegistryEntry(this.hass!, entity.entity_id, { - name: shouldUpdateName ? newName : undefined, - new_entity_id: shouldUpdateEntityId ? newEntityId : undefined, + name: newName, }); }); await Promise.all(updateProms); }, }); - } + }; private async _enableDevice(): Promise { await updateDeviceRegistryEntry(this.hass, this.deviceId, { diff --git a/src/panels/config/entities/entity-registry-settings-editor.ts b/src/panels/config/entities/entity-registry-settings-editor.ts index 0250d6d5dd..34e4140819 100644 --- a/src/panels/config/entities/entity-registry-settings-editor.ts +++ b/src/panels/config/entities/entity-registry-settings-editor.ts @@ -1,4 +1,4 @@ -import { mdiContentCopy } from "@mdi/js"; +import { mdiContentCopy, mdiRestore } from "@mdi/js"; import type { HassEntity } from "home-assistant-js-websocket"; import type { CSSResultGroup, PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; @@ -13,6 +13,7 @@ import { computeObjectId } from "../../../common/entity/compute_object_id"; import { supportsFeature } from "../../../common/entity/supports-feature"; import { formatNumber } from "../../../common/number/format_number"; import { stringCompare } from "../../../common/string/compare"; +import { autoCaseNoun } from "../../../common/translations/auto_case_noun"; import type { LocalizeFunc, LocalizeKeys, @@ -23,13 +24,13 @@ import "../../../components/ha-area-picker"; import "../../../components/ha-icon"; import "../../../components/ha-icon-button-next"; import "../../../components/ha-icon-picker"; +import "../../../components/ha-labels-picker"; import "../../../components/ha-list-item"; import "../../../components/ha-radio"; import "../../../components/ha-select"; import "../../../components/ha-settings-row"; import "../../../components/ha-state-icon"; import "../../../components/ha-switch"; -import "../../../components/ha-labels-picker"; import type { HaSwitch } from "../../../components/ha-switch"; import "../../../components/ha-textfield"; import { @@ -59,6 +60,7 @@ import type { SensorEntityOptions, } from "../../../data/entity_registry"; import { + getAutomaticEntityIds, subscribeEntityRegistry, updateEntityRegistryEntry, } from "../../../data/entity_registry"; @@ -89,7 +91,6 @@ import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { showToast } from "../../../util/toast"; import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail"; -import { autoCaseNoun } from "../../../common/translations/auto_case_noun"; const OVERRIDE_DEVICE_CLASSES = { cover: [ @@ -757,11 +758,16 @@ export class EntityRegistrySettingsEditor extends LitElement { autocorrect="off" input-spellcheck="false" > - +
    + + +
    ${!this.entry.device_id ? html` { + const entityIds = await getAutomaticEntityIds(this.hass, [ + this._origEntityId, + ]); + this._entityId = entityIds[this._origEntityId] || this._origEntityId; + } + private async _copyEntityId(): Promise { await copyToClipboard(this._entityId); showToast(this, { @@ -1507,7 +1520,7 @@ export class EntityRegistrySettingsEditor extends LitElement { --text-field-prefix-padding-right: 0; --textfield-icon-trailing-padding: 0; } - ha-textfield.entityId > ha-icon-button { + ha-textfield.entityId ha-icon-button { position: relative; right: -8px; --mdc-icon-button-size: 36px; diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index ddbe51dc7a..a716bee2c6 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -10,6 +10,7 @@ import { mdiMenuDown, mdiPencilOff, mdiPlus, + mdiRestore, mdiRestoreAlert, mdiToggleSwitch, mdiToggleSwitchOffOutline, @@ -95,6 +96,7 @@ import { createLabelRegistryEntry, subscribeLabelRegistry, } from "../../../data/label_registry"; +import { regenerateEntityIds } from "../../../data/regenerate_entity_ids"; import { showAlertDialog, showConfirmationDialog, @@ -952,6 +954,21 @@ ${ )}
    + + + + + +
    + ${this.hass.localize( + "ui.panel.config.entities.picker.restore_entity_id_selected.button" + )} +
    +
    + @@ -1371,6 +1388,12 @@ ${rejected }); }; + private _restoreEntityIdSelected = () => { + regenerateEntityIds(this, this.hass, this._selected); + + this._clearSelection(); + }; + private _removeSelected = async () => { if (!this._entities || !this.hass) { return; diff --git a/src/panels/config/integrations/integration-panels/zha/zha-device-card.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-card.ts index e40bd08aad..4e0908e746 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-device-card.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-device-card.ts @@ -6,7 +6,6 @@ import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../../common/dom/fire_event"; import { computeStateName } from "../../../../../common/entity/compute_state_name"; import { stringCompare } from "../../../../../common/string/compare"; -import { slugify } from "../../../../../common/string/slugify"; import "../../../../../components/entity/state-badge"; import "../../../../../components/ha-area-picker"; import "../../../../../components/ha-card"; @@ -14,6 +13,7 @@ import "../../../../../components/ha-textfield"; import { updateDeviceRegistryEntry } from "../../../../../data/device_registry"; import type { EntityRegistryEntry } from "../../../../../data/entity_registry"; import { + getAutomaticEntityIds, subscribeEntityRegistry, updateEntityRegistryEntry, } from "../../../../../data/entity_registry"; @@ -23,7 +23,6 @@ import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant } from "../../../../../types"; import type { EntityRegistryStateEntry } from "../../../devices/ha-config-device-page"; -import { getIeeeTail } from "./functions"; @customElement("zha-device-card") class ZHADeviceCard extends SubscribeMixin(LitElement) { @@ -135,30 +134,35 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) { } const entities = this._deviceEntities(device.device_reg_id, this._entities); - const oldDeviceEntityId = slugify(oldDeviceName); - const newDeviceEntityId = slugify(newDeviceName); - const ieeeTail = getIeeeTail(device.ieee); + const entityIdsMapping = getAutomaticEntityIds( + this.hass, + entities.map((entity) => entity.entity_id) + ); const updateProms = entities.map((entity) => { - const name = entity.name || entity.stateName; - let newEntityId: string | null = null; - let newName: string | null = null; + const name = entity.name; + const newEntityId = entityIdsMapping[entity.entity_id]; + let newName: string | null | undefined; - if (name && name.includes(oldDeviceName)) { - newName = name.replace(` ${ieeeTail}`, ""); - newName = newName.replace(oldDeviceName, newDeviceName); - newEntityId = entity.entity_id.replace(`_${ieeeTail}`, ""); - newEntityId = newEntityId.replace(oldDeviceEntityId, newDeviceEntityId); + if (entity.has_entity_name && !entity.name) { + newName = undefined; + } else if ( + entity.has_entity_name && + (entity.name === oldDeviceName || entity.name === newDeviceName) + ) { + // clear name if it matches the device name and it uses the device name (entity naming) + newName = null; + } else if (name && name.includes(oldDeviceName)) { + newName = name.replace(oldDeviceName, newDeviceName); } - if (!newName && !newEntityId) { + if (newName !== undefined && !newEntityId) { return undefined; } return updateEntityRegistryEntry(this.hass!, entity.entity_id, { - name: newName || name, - disabled_by: entity.disabled_by, - new_entity_id: newEntityId || entity.entity_id, + name: newName, + new_entity_id: newEntityId || undefined, }); }); await Promise.all(updateProms); diff --git a/src/panels/config/integrations/integration-panels/zwave_js/add-node/dialog-zwave_js-add-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/add-node/dialog-zwave_js-add-node.ts index 7723a7f39a..561cf10848 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/add-node/dialog-zwave_js-add-node.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/add-node/dialog-zwave_js-add-node.ts @@ -49,11 +49,10 @@ import "../../../../../../components/ha-fade-in"; import "../../../../../../components/ha-icon-button"; import "../../../../../../components/ha-qr-scanner"; -import { computeStateName } from "../../../../../../common/entity/compute_state_name"; import { navigate } from "../../../../../../common/navigate"; -import { slugify } from "../../../../../../common/string/slugify"; import type { EntityRegistryEntry } from "../../../../../../data/entity_registry"; import { + getAutomaticEntityIds, subscribeEntityRegistry, updateEntityRegistryEntry, } from "../../../../../../data/entity_registry"; @@ -874,42 +873,47 @@ class DialogZWaveJSAddNode extends SubscribeMixin(LitElement) { if (nameChanged) { // rename entities - const oldDeviceEntityId = slugify(oldDeviceName); - const newDeviceEntityId = slugify(newDeviceName); + + const entities = this._entities.filter( + (entity) => entity.device_id === this._device!.id + ); + + const entityIdsMapping = getAutomaticEntityIds( + this.hass, + entities.map((entity) => entity.entity_id) + ); await Promise.all( - this._entities - .filter((entity) => entity.device_id === this._device!.id) - .map((entity) => { - const entityState = this.hass.states[entity.entity_id]; - const name = - entity.name || - (entityState && computeStateName(entityState)); - let newEntityId: string | null = null; - let newName: string | null = null; + entities.map((entity) => { + const name = entity.name; + let newName: string | null | undefined; + const newEntityId = entityIdsMapping[entity.entity_id]; - if (name && name.includes(oldDeviceName)) { - newName = name.replace(oldDeviceName, newDeviceName); - } + if (entity.has_entity_name && !entity.name) { + newName = undefined; + } else if ( + entity.has_entity_name && + (entity.name === oldDeviceName || + entity.name === newDeviceName) + ) { + // clear name if it matches the device name and it uses the device name (entity naming) + newName = null; + } else if (name && name.includes(oldDeviceName)) { + newName = name.replace(oldDeviceName, newDeviceName); + } - newEntityId = entity.entity_id.replace( - oldDeviceEntityId, - newDeviceEntityId - ); + if ( + (newName === undefined && !newEntityId) || + newEntityId === entity.entity_id + ) { + return undefined; + } - if (!newName && newEntityId === entity.entity_id) { - return undefined; - } - - return updateEntityRegistryEntry( - this.hass!, - entity.entity_id, - { - name: newName || name, - new_entity_id: newEntityId || entity.entity_id, - } - ); - }) + return updateEntityRegistryEntry(this.hass!, entity.entity_id, { + name: newName || name, + new_entity_id: newEntityId || undefined, + }); + }) ); } } catch (_err: any) { diff --git a/src/panels/lovelace/cards/hui-alarm-panel-card.ts b/src/panels/lovelace/cards/hui-alarm-panel-card.ts index c7ceccd8da..d85cda1630 100644 --- a/src/panels/lovelace/cards/hui-alarm-panel-card.ts +++ b/src/panels/lovelace/cards/hui-alarm-panel-card.ts @@ -1,7 +1,8 @@ +import { mdiClose } from "@mdi/js"; import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import type { PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { styleMap } from "lit/directives/style-map"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; @@ -9,8 +10,8 @@ import { fireEvent } from "../../../common/dom/fire_event"; import { stateColorCss } from "../../../common/entity/state_color"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/chips/ha-assist-chip"; -import "../../../components/ha-card"; import "../../../components/ha-button"; +import "../../../components/ha-card"; import "../../../components/ha-state-icon"; import "../../../components/ha-textfield"; import type { HaTextField } from "../../../components/ha-textfield"; @@ -21,18 +22,18 @@ import { callAlarmAction, } from "../../../data/alarm_control_panel"; import { UNAVAILABLE } from "../../../data/entity"; -import type { HomeAssistant } from "../../../types"; -import { findEntities } from "../common/find-entities"; -import { createEntityNotFoundWarning } from "../components/hui-warning"; -import type { LovelaceCard } from "../types"; import type { ExtEntityRegistryEntry } from "../../../data/entity_registry"; import { getExtendedEntityRegistryEntry, subscribeEntityRegistry, } from "../../../data/entity_registry"; +import type { HomeAssistant } from "../../../types"; +import { findEntities } from "../common/find-entities"; +import { createEntityNotFoundWarning } from "../components/hui-warning"; +import type { LovelaceCard } from "../types"; import type { AlarmPanelCardConfig, AlarmPanelCardConfigState } from "./types"; -const BUTTONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "", "0", "clear"]; +const BUTTONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "clear", "0", ""]; export const DEFAULT_STATES = [ "arm_home", @@ -101,7 +102,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard { @state() private _entry?: ExtEntityRegistryEntry | null; - @query("#alarmCode") private _input?: HaTextField; + @state() private _value?: string; private _unsubEntityRegistry?: UnsubscribeFunc; @@ -117,14 +118,14 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard { public async getCardSize(): Promise { if (!this._config || !this.hass) { - return 9; + return 10; } const stateObj = this.hass.states[this._config.entity]; return !stateObj || stateObj.attributes.code_format !== FORMAT_NUMBER ? 4 - : 9; + : 10; } public setConfig(config: AlarmPanelCardConfig): void { @@ -275,7 +276,8 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard { ? nothing : html` +
    ${BUTTONS.map((value) => value === "" - ? html` ` - : html` - - ${value === "clear" - ? this.hass!.localize( - `ui.card.alarm_control_panel.clear_code` - ) - : value} - - ` + ? html`` + : value === "clear" + ? html` + + + + ` + : html` + + ${value} + + ` )}
    `} @@ -327,22 +334,23 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard { ) || entityState; } + private _handleInput(e: Event): void { + this._value = (e.currentTarget as HaTextField).value; + } + private _handlePadClick(e: MouseEvent): void { const val = (e.currentTarget! as any).value; - this._input!.value = val === "clear" ? "" : this._input!.value + val; + this._value = val === "clear" ? "" : (this._value || "") + val; } private _handleActionClick(e: MouseEvent): void { - const input = this._input; callAlarmAction( this.hass!, this._config!.entity, (e.currentTarget! as any).action, - input?.value || undefined + this._value || undefined ); - if (input) { - input.value = ""; - } + this._value = undefined; } private _handleMoreInfo() { @@ -411,20 +419,28 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard { animation: none; } - #keypad { - display: flex; - justify-content: center; - flex-wrap: wrap; - margin: auto; - width: 100%; - max-width: 300px; - direction: ltr; + .keypad { + --keypad-columns: 3; + margin-top: 12px; + padding: 12px; + display: grid; + grid-template-columns: repeat(var(--keypad-columns), auto); + grid-auto-rows: auto; + grid-gap: 24px; + justify-items: center; + align-items: center; } - #keypad ha-button { - padding: 8px; - width: 30%; - box-sizing: border-box; + ha-control-button { + width: 56px; + height: 56px; + --control-button-border-radius: 28px; + --mdc-icon-size: 24px; + font-size: var(--ha-font-size-2xl); + } + .clear { + --control-button-background-color: var(--red-color); + --control-button-icon-color: var(--red-color); } .actions { @@ -437,10 +453,6 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard { .actions ha-button { margin: 0 4px 4px; } - - ha-button.numberkey { - --ha-button-font-size: var(--keypad-font-size, var(--ha-font-size-s)); - } `; } diff --git a/src/panels/lovelace/cards/hui-button-card.ts b/src/panels/lovelace/cards/hui-button-card.ts index f6cc9b0bcd..957c8a868b 100644 --- a/src/panels/lovelace/cards/hui-button-card.ts +++ b/src/panels/lovelace/cards/hui-button-card.ts @@ -224,19 +224,19 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { filter: colored ? stateColorBrightness(stateObj) : undefined, height: this._config.icon_height ? this._config.icon_height - : "", + : undefined, })} > ` - : ""} + : nothing} ${this._config.show_name ? html`${name}` - : ""} + : nothing} ${this._config.show_state && stateObj ? html` ${this.hass.formatEntityState(stateObj)} ` - : ""} + : nothing} `; } @@ -282,7 +282,8 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { align-items: center; text-align: center; padding: 4% 0; - font-size: 16.8px; + font-size: var(--ha-font-size-l); + line-height: var(--ha-line-height-condensed); height: 100%; box-sizing: border-box; justify-content: center; diff --git a/src/translations/en.json b/src/translations/en.json index 9283951559..ba4f3cced5 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -339,6 +339,7 @@ "remove": "Remove", "enable": "Enable", "disable": "Disable", + "update": "Update", "hide": "Hide", "close": "Close", "clear": "Clear", @@ -1535,6 +1536,17 @@ "no_aliases": "Configure aliases and expose settings for voice assistants" } }, + "recreate_entity_ids": { + "confirm_rename_title": "Are you sure you want to recreate your entity IDs?", + "confirm_rename_warning": "This will not change any configuration (like automations, scripts, scenes, dashboards) that is currently using these entities! You will have to manually edit them yourself to use the new entity IDs!", + "will_rename": "{count} {count, plural,\n one {entity ID}\n other {entity IDs}\n} will be renamed", + "new": "New", + "old": "Old", + "cant_rename": "{count} {count, plural,\n one {entity ID}\n other {entity IDs}\n} can not be regenerated because they are not available", + "wont_change": "{count} {count, plural,\n one {entity ID}\n other {entity IDs}\n} will not change", + "update_entity_error": "Updating the entity ID of {entityId} failed", + "confirm_no_renamable_entity_ids": "No renamable entity IDs" + }, "device-registry-detail": { "name": "[%key:ui::panel::config::devices::name%]", "enabled_label": "[%key:ui::panel::config::devices::enabled_label%]", @@ -4941,6 +4953,7 @@ "filtering_by_config_entry": "[%key:ui::panel::config::entities::picker::filtering_by_config_entry%]", "device_info": "{type} info", "edit_settings": "Edit settings", + "restore_entity_ids": "Recreate entity IDs", "unnamed_device": "Unnamed {type}", "unknown_error": "Unknown error", "name": "Name", @@ -5040,13 +5053,6 @@ "disabled_entities": "+{count} disabled {count, plural,\n one {entity}\n other {entities}\n}", "hidden": "Hidden" }, - "confirm_rename_entity_ids": "Do you also want to rename the entity IDs of your entities?", - "confirm_rename_entity_ids_warning": "This will not change any configuration (like automations, scripts, scenes, dashboards) that is currently using these entities! You will have to manually edit them yourself to use the new entity IDs!", - "confirm_rename_entity_will_rename": "{count} {count, plural,\n one {entity ID}\n other {entity IDs}\n} will be renamed", - "confirm_rename_new": "New", - "confirm_rename_old": "Old", - "confirm_rename_entity_wont_rename": "{count} {count, plural,\n one {entity ID}\n other {entity IDs}\n} will not be renamed as they do not contain the current device name ({deviceSlug})", - "confirm_rename_entity_no_renamable_entity_ids": "No renamable entity IDs", "confirm_disable_config_entry": "There are no more devices for the config entry {entry_name}, do you want to instead disable the config entry?", "update_device_error": "Updating the device failed", "disabled": "Disabled", @@ -5122,6 +5128,12 @@ "confirm_title": "Do you want to disable {number} {number, plural,\n one {entity}\n other {entities}\n}?", "confirm_text": "Disabled entities will not be added to Home Assistant." }, + "restore_entity_id_selected": { + "button": "Recreate entity IDs of selected", + "confirm_title": "Recreate entity IDs?", + "confirm_text": "Are you sure you want to change the entity IDs of these entities? You will have to change you dashboards, automations and scripts to use the new entity IDs.", + "changes": "The following entity IDs will be updated:" + }, "delete_selected": { "button": "Delete selected", "confirm_title": "Delete selected entities?", @@ -9081,7 +9093,6 @@ "yes": "[%key:ui::common::yes%]", "no": "[%key:ui::common::no%]", "add": "[%key:supervisor::dialog::repositories::add%]", - "description": "Description", "failed_to_restart_name": "Failed to restart {name}", "failed_to_update_name": "Failed to update {name}", "learn_more": "Learn more",