mirror of
https://github.com/home-assistant/core.git
synced 2026-08-19 12:48:57 +01:00
308 lines
12 KiB
Python
308 lines
12 KiB
Python
"""Helpers for helper integrations."""
|
|
|
|
from collections.abc import Callable, Collection, Coroutine
|
|
from typing import Any
|
|
|
|
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, valid_entity_id
|
|
|
|
from . import device_registry as dr, entity_registry as er
|
|
from .deprecation import deprecated_function
|
|
from .event import async_track_entity_registry_updated_event
|
|
from .frame import ReportBehavior, report_usage
|
|
|
|
|
|
def async_handle_source_entity_changes(
|
|
hass: HomeAssistant,
|
|
*,
|
|
helper_config_entry_id: str,
|
|
set_source_entity_id_or_uuid: Callable[[str], None],
|
|
source_device_id: str | None,
|
|
source_entity_id_or_uuid: str,
|
|
source_entity_removed: Callable[[], Coroutine[Any, Any, None]] | None = None,
|
|
**kwargs: Any,
|
|
) -> CALLBACK_TYPE:
|
|
"""Handle changes to a helper entity's source entity.
|
|
|
|
The following changes are handled:
|
|
- Entity removal: If the source entity is removed:
|
|
- If source_entity_removed is provided, it is called to handle the removal.
|
|
- If source_entity_removed is not provided, The helper entity is updated to
|
|
not link to any device.
|
|
- Entity ID changed: If the source entity's entity ID changes and the source
|
|
entity is identified by an entity ID, the set_source_entity_id_or_uuid is
|
|
called. If the source entity is identified by a UUID, the helper config entry
|
|
is reloaded.
|
|
- Source entity moved to another device: The helper entity is updated to link
|
|
to the new device. Then the helper config entry is reloaded.
|
|
- Source entity removed from the device: The helper entity is updated to link
|
|
to no device. Then the helper config entry is reloaded.
|
|
|
|
:param set_source_entity_id_or_uuid: A function which updates the source entity
|
|
ID or UUID, e.g., in the helper config entry options.
|
|
:param source_entity_removed: A function which is called when the source entity
|
|
is removed. This can be used to clean up any resources related to the source
|
|
entity or ask the user to select a new source entity.
|
|
"""
|
|
if "add_helper_config_entry_to_device" in kwargs:
|
|
del kwargs["add_helper_config_entry_to_device"]
|
|
# Adding the helper's config entry to the source device is no longer supported
|
|
# now that a device belongs to a single config entry; the helper entities link to
|
|
# the source device via their device_id instead.
|
|
report_usage(
|
|
"calls async_handle_source_entity_changes with "
|
|
"add_helper_config_entry_to_device, which no longer has any effect",
|
|
core_behavior=ReportBehavior.LOG,
|
|
breaks_in_ha_version="2027.8.0",
|
|
)
|
|
if kwargs:
|
|
raise TypeError(
|
|
"async_handle_source_entity_changes() got unexpected keyword arguments "
|
|
f"{', '.join(map(repr, kwargs))}"
|
|
)
|
|
|
|
async def async_registry_updated(
|
|
event: Event[er.EventEntityRegistryUpdatedData],
|
|
) -> None:
|
|
"""Handle entity registry update."""
|
|
nonlocal source_device_id
|
|
|
|
data = event.data
|
|
if data["action"] == "remove":
|
|
if source_entity_removed:
|
|
await source_entity_removed()
|
|
else:
|
|
for (
|
|
helper_entity_entry
|
|
) in entity_registry.entities.get_entries_for_config_entry_id(
|
|
helper_config_entry_id
|
|
):
|
|
# Update the helper entity to link to the new device (or no device)
|
|
entity_registry.async_update_entity(
|
|
helper_entity_entry.entity_id, device_id=None
|
|
)
|
|
|
|
if data["action"] != "update":
|
|
return
|
|
|
|
if "entity_id" in data["changes"]:
|
|
# Entity_id changed, update or reload the config entry
|
|
if valid_entity_id(source_entity_id_or_uuid):
|
|
# If the entity is pointed to by an entity ID, update the entry
|
|
set_source_entity_id_or_uuid(data["entity_id"])
|
|
else:
|
|
await hass.config_entries.async_reload(helper_config_entry_id)
|
|
|
|
if not source_device_id or "device_id" not in data["changes"]:
|
|
return
|
|
|
|
# Handle the source entity being moved to a different device or removed
|
|
# from the device
|
|
if (
|
|
not (source_entity_entry := entity_registry.async_get(data["entity_id"]))
|
|
or not device_registry.async_get(source_device_id)
|
|
or source_entity_entry.device_id == source_device_id
|
|
):
|
|
# No need to do any cleanup
|
|
return
|
|
|
|
# The source entity has been moved to a different device; relink the helper
|
|
# entities to the new device.
|
|
for helper_entity in entity_registry.entities.get_entries_for_config_entry_id(
|
|
helper_config_entry_id
|
|
):
|
|
# Update the helper entity to link to the new device (or no device)
|
|
entity_registry.async_update_entity(
|
|
helper_entity.entity_id, device_id=source_entity_entry.device_id
|
|
)
|
|
|
|
source_device_id = source_entity_entry.device_id
|
|
|
|
# Reload the config entry so the helper entity is recreated with
|
|
# correct device info
|
|
await hass.config_entries.async_reload(helper_config_entry_id)
|
|
|
|
device_registry = dr.async_get(hass)
|
|
entity_registry = er.async_get(hass)
|
|
source_entity_id = er.async_validate_entity_id(
|
|
entity_registry, source_entity_id_or_uuid
|
|
)
|
|
return async_track_entity_registry_updated_event(
|
|
hass, source_entity_id, async_registry_updated
|
|
)
|
|
|
|
|
|
def async_remove_helper_devices(
|
|
hass: HomeAssistant,
|
|
*,
|
|
helper_config_entry_id: str,
|
|
source_device_id: str | None,
|
|
remove_all_devices: bool = False,
|
|
keep_device_ids: Collection[str] = (),
|
|
) -> None:
|
|
"""Migrate a helper which has tried to own a device instead of just linking to it.
|
|
|
|
In the single-config-entry device model a helper can no longer co-own the source
|
|
device. A helper that co-owned it before the single-config-entry device was introduced
|
|
now owns a split of it (linked by the pre-migration composite_device_id); a helper that
|
|
declared the source device's identifiers or connections in its device_info afterwards
|
|
now owns a fork (a separate device that copied that identity). This removes the helper's
|
|
duplicate device(s) and relinks its entities to the source device, or detaches them when
|
|
the source has no concrete device to hold the link.
|
|
|
|
:param helper_config_entry_id: The config entry id of the helper being migrated.
|
|
:param source_device_id: The device the helper should link its entities to. A concrete
|
|
device relinks the entities to it. A pre-migration composite id and None have no
|
|
concrete device to hold the link, so the entities are detached; in targeted mode
|
|
a composite or None source with no matching duplicate is a no-op.
|
|
:param remove_all_devices: By default only the helper's single duplicate of
|
|
source_device_id (a split or fork) is removed. When True, every device the
|
|
helper owns except source_device_id and keep_device_ids is removed instead -
|
|
even when source_device_id no longer exists, in which case the helper's
|
|
entities are left without a device. Use this to clean up devices the helper
|
|
created but never removed itself, such as a fork left behind for each
|
|
previously selected source device.
|
|
:param keep_device_ids: Devices the helper legitimately owns which must not be removed.
|
|
Only consulted when remove_all_devices is True.
|
|
"""
|
|
device_registry = dr.async_get(hass)
|
|
entity_registry = er.async_get(hass)
|
|
|
|
source_device = (
|
|
device_registry.async_get(source_device_id)
|
|
if source_device_id is not None
|
|
else None
|
|
)
|
|
if source_device is None:
|
|
# No source device (gone, or none selected). In remove-all mode the helper's devices
|
|
# are still removed, leaving its entities without a device; targeted mode has no
|
|
# duplicate to match.
|
|
if remove_all_devices:
|
|
_sweep_helper_devices(
|
|
device_registry,
|
|
entity_registry,
|
|
helper_config_entry_id,
|
|
None,
|
|
keep_device_ids,
|
|
)
|
|
return
|
|
|
|
# source_device_id is either the pre-migration composite id (source_device is then the
|
|
# synthesized composite) or a concrete device. Its splits, if any, share this id as
|
|
# their composite_device_id.
|
|
source_is_concrete = source_device_id in device_registry.devices
|
|
composite_device_id = (
|
|
source_device.composite_device_id if source_is_concrete else source_device_id
|
|
)
|
|
target_device_id = source_device_id if source_is_concrete else None
|
|
|
|
if remove_all_devices:
|
|
_sweep_helper_devices(
|
|
device_registry,
|
|
entity_registry,
|
|
helper_config_entry_id,
|
|
target_device_id,
|
|
keep_device_ids,
|
|
)
|
|
else:
|
|
_remove_duplicate_helper_device(
|
|
device_registry,
|
|
entity_registry,
|
|
helper_config_entry_id,
|
|
source_device,
|
|
composite_device_id,
|
|
target_device_id,
|
|
)
|
|
|
|
|
|
def _remove_duplicate_helper_device(
|
|
device_registry: dr.DeviceRegistry,
|
|
entity_registry: er.EntityRegistry,
|
|
helper_config_entry_id: str,
|
|
source_device: dr.DeviceEntry,
|
|
composite_device_id: str | None,
|
|
target_device_id: str | None,
|
|
) -> None:
|
|
"""Remove the helper's single duplicate of the source device.
|
|
|
|
The duplicate is either a split of the same pre-migration composite (linked by
|
|
composite_device_id) or a fork that copied the source device's identifiers or
|
|
connections into its device_info. Match on both, and only among the helper's own
|
|
devices, so unrelated helper-owned devices are left untouched.
|
|
"""
|
|
duplicate = next(
|
|
(
|
|
device
|
|
for device in dr.async_entries_for_config_entry(
|
|
device_registry, helper_config_entry_id
|
|
)
|
|
if (
|
|
composite_device_id is not None
|
|
and device.composite_device_id == composite_device_id
|
|
)
|
|
or device.identifiers & source_device.identifiers
|
|
or device.connections & source_device.connections
|
|
),
|
|
None,
|
|
)
|
|
if duplicate is None:
|
|
return
|
|
for entity in er.async_entries_for_config_entry(
|
|
entity_registry, helper_config_entry_id
|
|
):
|
|
if entity.device_id == duplicate.id:
|
|
entity_registry.async_update_entity(
|
|
entity.entity_id, device_id=target_device_id
|
|
)
|
|
device_registry.async_remove_device(duplicate.id)
|
|
|
|
|
|
def _sweep_helper_devices(
|
|
device_registry: dr.DeviceRegistry,
|
|
entity_registry: er.EntityRegistry,
|
|
helper_config_entry_id: str,
|
|
target_device_id: str | None,
|
|
keep_device_ids: Collection[str],
|
|
) -> None:
|
|
"""Remove every device the helper owns except the target and the allow-list.
|
|
|
|
Sweeps up devices left behind when the user repeatedly changed the source device: the
|
|
helper's entities are relinked to the target device (except those already on a kept
|
|
device) and the other helper-owned devices are removed.
|
|
"""
|
|
kept_device_ids = {*keep_device_ids}
|
|
if target_device_id is not None:
|
|
kept_device_ids.add(target_device_id)
|
|
for entity in er.async_entries_for_config_entry(
|
|
entity_registry, helper_config_entry_id
|
|
):
|
|
if entity.device_id not in kept_device_ids:
|
|
entity_registry.async_update_entity(
|
|
entity.entity_id, device_id=target_device_id
|
|
)
|
|
for device in dr.async_entries_for_config_entry(
|
|
device_registry, helper_config_entry_id
|
|
):
|
|
if device.id not in kept_device_ids:
|
|
device_registry.async_remove_device(device.id)
|
|
|
|
|
|
@deprecated_function(
|
|
"homeassistant.helpers.helper_integration.async_remove_helper_devices",
|
|
breaks_in_ha_version="2027.8.0",
|
|
)
|
|
def async_remove_helper_config_entry_from_source_device(
|
|
hass: HomeAssistant,
|
|
*,
|
|
helper_config_entry_id: str,
|
|
source_device_id: str,
|
|
) -> None:
|
|
"""Migrate a helper which has tried to add its config entry to the source device.
|
|
|
|
Deprecated alias of async_remove_helper_devices, kept for custom integrations.
|
|
"""
|
|
async_remove_helper_devices(
|
|
hass,
|
|
helper_config_entry_id=helper_config_entry_id,
|
|
source_device_id=source_device_id,
|
|
)
|