diff --git a/src/common/entity/delete_entity.ts b/src/common/entity/delete_entity.ts new file mode 100644 index 000000000000..4f104716692c --- /dev/null +++ b/src/common/entity/delete_entity.ts @@ -0,0 +1,91 @@ +import type { HomeAssistant } from "../../types"; +import type { IntegrationManifest } from "../../data/integration"; +import { computeDomain } from "./compute_domain"; +import { HELPERS_CRUD } from "../../data/helpers_crud"; +import type { Helper } from "../../panels/config/helpers/const"; +import { isHelperDomain } from "../../panels/config/helpers/const"; +import { isComponentLoaded } from "../config/is_component_loaded"; +import type { EntityRegistryEntry } from "../../data/entity_registry"; +import { removeEntityRegistryEntry } from "../../data/entity_registry"; +import type { ConfigEntry } from "../../data/config_entries"; +import { deleteConfigEntry } from "../../data/config_entries"; + +export const isDeletableEntity = ( + hass: HomeAssistant, + entity_id: string, + manifests: IntegrationManifest[], + entityRegistry: EntityRegistryEntry[], + configEntries: ConfigEntry[], + fetchedHelpers: Helper[] +): boolean => { + const restored = !!hass.states[entity_id]?.attributes.restored; + if (restored) { + return true; + } + + const domain = computeDomain(entity_id); + const entityRegEntry = entityRegistry.find((e) => e.entity_id === entity_id); + if (isHelperDomain(domain)) { + return !!( + isComponentLoaded(hass, domain) && + entityRegEntry && + fetchedHelpers.some((e) => e.id === entityRegEntry.unique_id) + ); + } + + const configEntryId = entityRegEntry?.config_entry_id; + if (!configEntryId) { + return false; + } + const configEntry = configEntries.find((e) => e.entry_id === configEntryId); + return ( + manifests.find((m) => m.domain === configEntry?.domain) + ?.integration_type === "helper" + ); +}; + +export const deleteEntity = ( + hass: HomeAssistant, + entity_id: string, + manifests: IntegrationManifest[], + entityRegistry: EntityRegistryEntry[], + configEntries: ConfigEntry[], + fetchedHelpers: Helper[] +) => { + // This function assumes the entity_id already was validated by isDeletableEntity and does not repeat all those checks. + const domain = computeDomain(entity_id); + const entityRegEntry = entityRegistry.find((e) => e.entity_id === entity_id); + if (isHelperDomain(domain)) { + if (isComponentLoaded(hass, domain)) { + if ( + entityRegEntry && + fetchedHelpers.some((e) => e.id === entityRegEntry.unique_id) + ) { + HELPERS_CRUD[domain].delete(hass, entityRegEntry.unique_id); + return; + } + } + const stateObj = hass.states[entity_id]; + if (!stateObj?.attributes.restored) { + return; + } + removeEntityRegistryEntry(hass, entity_id); + return; + } + + const configEntryId = entityRegEntry?.config_entry_id; + const configEntry = configEntryId + ? configEntries.find((e) => e.entry_id === configEntryId) + : undefined; + const isHelperEntryType = configEntry + ? manifests.find((m) => m.domain === configEntry.domain) + ?.integration_type === "helper" + : false; + + if (isHelperEntryType) { + deleteConfigEntry(hass, configEntryId!); + return; + } + + removeEntityRegistryEntry(hass, entity_id); +}; diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 3e175b0558d7..55fb4d4312d3 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -27,6 +27,13 @@ import { formatShortDateTime } from "../../../common/datetime/format_date_time"; import { storage } from "../../../common/decorators/storage"; import type { HASSDomEvent } from "../../../common/dom/fire_event"; import { computeDomain } from "../../../common/entity/compute_domain"; +import { + isDeletableEntity, + deleteEntity, +} from "../../../common/entity/delete_entity"; +import type { Helper } from "../helpers/const"; +import { isHelperDomain } from "../helpers/const"; +import { HELPERS_CRUD } from "../../../data/helpers_crud"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { PROTOCOL_INTEGRATIONS, @@ -73,12 +80,15 @@ import type { } from "../../../data/entity_registry"; import { computeEntityRegistryName, - removeEntityRegistryEntry, updateEntityRegistryEntry, } from "../../../data/entity_registry"; +import type { IntegrationManifest } from "../../../data/integration"; +import { + fetchIntegrationManifests, + domainToName, +} from "../../../data/integration"; import type { EntitySources } from "../../../data/entity_sources"; import { fetchEntitySourcesWithCache } from "../../../data/entity_sources"; -import { domainToName } from "../../../data/integration"; import type { LabelRegistryEntry } from "../../../data/label_registry"; import { createLabelRegistryEntry, @@ -136,6 +146,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { @state() private _entries?: ConfigEntry[]; + @state() private _manifests?: IntegrationManifest[]; + @state() @consume({ context: fullEntitiesContext, subscribe: true }) _entities!: EntityRegistryEntry[]; @@ -1280,11 +1292,46 @@ ${rejected }); } - private _removeSelected() { - const removeableEntities = this._selected.filter((entity) => { - const stateObj = this.hass.states[entity]; - return stateObj?.attributes.restored; + private async _removeSelected() { + if (!this._entities || !this.hass) { + return; + } + + const manifestsProm = this._manifests + ? undefined + : fetchIntegrationManifests(this.hass); + const helperDomains = [ + ...new Set(this._selected.map((s) => computeDomain(s))), + ].filter((d) => isHelperDomain(d)); + + const configEntriesProm = this._entries + ? undefined + : this._loadConfigEntries(); + const domainProms = helperDomains.map((d) => + HELPERS_CRUD[d].fetch(this.hass) + ); + const helpersResult = await Promise.all(domainProms); + let fetchedHelpers: Helper[] = []; + helpersResult.forEach((r) => { + fetchedHelpers = fetchedHelpers.concat(r); }); + if (manifestsProm) { + this._manifests = await manifestsProm; + } + if (configEntriesProm) { + await configEntriesProm; + } + + const removeableEntities = this._selected.filter((entity_id) => + isDeletableEntity( + this.hass, + entity_id, + this._manifests!, + this._entities, + this._entries!, + fetchedHelpers + ) + ); showConfirmationDialog(this, { title: this.hass.localize( `ui.panel.config.entities.picker.delete_selected.confirm_title` @@ -1305,8 +1352,15 @@ ${rejected dismissText: this.hass.localize("ui.common.cancel"), destructive: true, confirm: () => { - removeableEntities.forEach((entity) => - removeEntityRegistryEntry(this.hass, entity) + removeableEntities.forEach((entity_id) => + deleteEntity( + this.hass, + entity_id, + this._manifests!, + this._entities, + this._entries!, + fetchedHelpers + ) ); this._clearSelection(); },