Fix tplink deprecated entity cleanup (#136160)

This commit is contained in:
Steven B. 2025-01-23 18:51:56 +00:00 committed by GitHub
parent b2624e6274
commit 2466df2b78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 210 additions and 60 deletions

View File

@ -17,7 +17,6 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TPLinkConfigEntry
from .deprecate import async_cleanup_deprecated
from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription
@ -88,12 +87,10 @@ async def async_setup_entry(
feature_type=Feature.Type.BinarySensor,
entity_class=TPLinkBinarySensorEntity,
descriptions=BINARYSENSOR_DESCRIPTIONS_MAP,
platform_domain=BINARY_SENSOR_DOMAIN,
known_child_device_ids=known_child_device_ids,
first_check=first_check,
)
async_cleanup_deprecated(
hass, BINARY_SENSOR_DOMAIN, config_entry.entry_id, entities
)
async_add_entities(entities)
_check_device()

View File

@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TPLinkConfigEntry
from .deprecate import DeprecatedInfo, async_cleanup_deprecated
from .deprecate import DeprecatedInfo
from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription
@ -95,10 +95,10 @@ async def async_setup_entry(
feature_type=Feature.Type.Action,
entity_class=TPLinkButtonEntity,
descriptions=BUTTON_DESCRIPTIONS_MAP,
platform_domain=BUTTON_DOMAIN,
known_child_device_ids=known_child_device_ids,
first_check=first_check,
)
async_cleanup_deprecated(hass, BUTTON_DOMAIN, config_entry.entry_id, entities)
async_add_entities(entities)
_check_device()

View File

@ -11,6 +11,7 @@ from kasa import Device, Module, StreamResolution
from homeassistant.components import ffmpeg, stream
from homeassistant.components.camera import (
DOMAIN as CAMERA_DOMAIN,
Camera,
CameraEntityDescription,
CameraEntityFeature,
@ -20,7 +21,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TPLinkConfigEntry, legacy_device_id
from . import TPLinkConfigEntry
from .const import CONF_CAMERA_CREDENTIALS
from .coordinator import TPLinkDataUpdateCoordinator
from .entity import CoordinatedTPLinkModuleEntity, TPLinkModuleEntityDescription
@ -75,6 +76,7 @@ async def async_setup_entry(
coordinator=parent_coordinator,
entity_class=TPLinkCameraEntity,
descriptions=CAMERA_DESCRIPTIONS,
platform_domain=CAMERA_DOMAIN,
known_child_device_ids=known_child_device_ids,
first_check=first_check,
)
@ -121,10 +123,6 @@ class TPLinkCameraEntity(CoordinatedTPLinkModuleEntity, Camera):
self._can_stream = True
self._http_mpeg_stream_running = False
def _get_unique_id(self) -> str:
"""Return unique ID for the entity."""
return f"{legacy_device_id(self._device)}-{self.entity_description.key}"
async def async_added_to_hass(self) -> None:
"""Call update attributes after the device is added to the platform."""
await super().async_added_to_hass()

View File

@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Any, cast
@ -11,6 +12,7 @@ from kasa.smart.modules.temperaturecontrol import ThermostatState
from homeassistant.components.climate import (
ATTR_TEMPERATURE,
DOMAIN as CLIMATE_DOMAIN,
ClimateEntity,
ClimateEntityDescription,
ClimateEntityFeature,
@ -22,7 +24,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TPLinkConfigEntry
from . import TPLinkConfigEntry, legacy_device_id
from .const import DOMAIN, UNIT_MAPPING
from .coordinator import TPLinkDataUpdateCoordinator
from .entity import (
@ -52,6 +54,10 @@ class TPLinkClimateEntityDescription(
):
"""Base class for climate entity description."""
unique_id_fn: Callable[[Device, TPLinkModuleEntityDescription], str] = (
lambda device, desc: f"{legacy_device_id(device)}_{desc.key}"
)
CLIMATE_DESCRIPTIONS: tuple[TPLinkClimateEntityDescription, ...] = (
TPLinkClimateEntityDescription(
@ -81,6 +87,7 @@ async def async_setup_entry(
coordinator=parent_coordinator,
entity_class=TPLinkClimateEntity,
descriptions=CLIMATE_DESCRIPTIONS,
platform_domain=CLIMATE_DOMAIN,
known_child_device_ids=known_child_device_ids,
first_check=first_check,
)
@ -182,7 +189,3 @@ class TPLinkClimateEntity(CoordinatedTPLinkModuleEntity, ClimateEntity):
cast(ThermostatState, self._mode_feature.value)
]
return True
def _get_unique_id(self) -> str:
"""Return unique id."""
return f"{self._device.device_id}_climate"

View File

@ -6,16 +6,20 @@ from collections.abc import Sequence
from dataclasses import dataclass
from typing import TYPE_CHECKING
from kasa import Device
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.script import scripts_with_entity
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from . import legacy_device_id
from .const import DOMAIN
if TYPE_CHECKING:
from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription
from .entity import CoordinatedTPLinkEntity, TPLinkEntityDescription
@dataclass(slots=True)
@ -30,7 +34,7 @@ class DeprecatedInfo:
def async_check_create_deprecated(
hass: HomeAssistant,
unique_id: str,
entity_description: TPLinkFeatureEntityDescription,
entity_description: TPLinkEntityDescription,
) -> bool:
"""Return true if the entity should be created based on the deprecated_info.
@ -58,13 +62,21 @@ def async_check_create_deprecated(
return not entity_entry.disabled
def async_cleanup_deprecated(
def async_process_deprecated(
hass: HomeAssistant,
platform: str,
platform_domain: str,
entry_id: str,
entities: Sequence[CoordinatedTPLinkFeatureEntity],
entities: Sequence[CoordinatedTPLinkEntity],
device: Device,
) -> None:
"""Remove disabled deprecated entities or create issues if necessary."""
"""Process deprecated entities for a device.
Create issues for deprececated entities that appear in automations.
Delete entities that are no longer provided by the integration either
because they have been removed at the end of the deprecation period, or
they are disabled by the user so the async_check_create_deprecated
returned false.
"""
ent_reg = er.async_get(hass)
for entity in entities:
if not (deprecated_info := entity.entity_description.deprecated_info):
@ -72,7 +84,7 @@ def async_cleanup_deprecated(
assert entity.unique_id
entity_id = ent_reg.async_get_entity_id(
platform,
platform_domain,
DOMAIN,
entity.unique_id,
)
@ -94,17 +106,27 @@ def async_cleanup_deprecated(
translation_placeholders={
"entity": entity_id,
"info": item,
"platform": platform,
"platform": platform_domain,
"new_platform": deprecated_info.new_platform,
},
)
# The light platform does not currently support cleaning up disabled
# deprecated entities because it uses two entity classes so a completeness
# check is not possible. It also uses the mac address as device id in some
# instances instead of device_id.
if platform_domain == LIGHT_DOMAIN:
return
# Remove entities that are no longer provided and have been disabled.
device_id = legacy_device_id(device)
unique_ids = {entity.unique_id for entity in entities}
for entity_entry in er.async_entries_for_config_entry(ent_reg, entry_id):
if (
entity_entry.domain == platform
entity_entry.domain == platform_domain
and entity_entry.disabled
and entity_entry.unique_id.startswith(device_id)
and entity_entry.unique_id not in unique_ids
):
ent_reg.async_remove(entity_entry.entity_id)

View File

@ -36,7 +36,11 @@ from .const import (
PRIMARY_STATE_ID,
)
from .coordinator import TPLinkConfigEntry, TPLinkDataUpdateCoordinator
from .deprecate import DeprecatedInfo, async_check_create_deprecated
from .deprecate import (
DeprecatedInfo,
async_check_create_deprecated,
async_process_deprecated,
)
_LOGGER = logging.getLogger(__name__)
@ -102,6 +106,9 @@ class TPLinkModuleEntityDescription(TPLinkEntityDescription):
"""Base class for a TPLink module based entity description."""
exists_fn: Callable[[Device, TPLinkConfigEntry], bool]
unique_id_fn: Callable[[Device, TPLinkModuleEntityDescription], str] = (
lambda device, desc: f"{legacy_device_id(device)}-{desc.key}"
)
def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P](
@ -151,6 +158,8 @@ class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator], AB
_attr_has_entity_name = True
_device: Device
entity_description: TPLinkEntityDescription
def __init__(
self,
device: Device,
@ -235,7 +244,7 @@ class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator], AB
def _get_unique_id(self) -> str:
"""Return unique ID for the entity."""
return legacy_device_id(self._device)
raise NotImplementedError
async def async_added_to_hass(self) -> None:
"""Call update attributes after the device is added to the platform."""
@ -405,6 +414,7 @@ class CoordinatedTPLinkFeatureEntity(CoordinatedTPLinkEntity, ABC):
feature_type: Feature.Type,
entity_class: type[_E],
descriptions: Mapping[str, _D],
platform_domain: str,
parent: Device | None = None,
) -> list[_E]:
"""Return a list of entities to add.
@ -439,6 +449,9 @@ class CoordinatedTPLinkFeatureEntity(CoordinatedTPLinkEntity, ABC):
desc,
)
]
async_process_deprecated(
hass, platform_domain, coordinator.config_entry.entry_id, entities, device
)
return entities
@classmethod
@ -454,6 +467,7 @@ class CoordinatedTPLinkFeatureEntity(CoordinatedTPLinkEntity, ABC):
feature_type: Feature.Type,
entity_class: type[_E],
descriptions: Mapping[str, _D],
platform_domain: str,
known_child_device_ids: set[str],
first_check: bool,
) -> list[_E]:
@ -473,6 +487,7 @@ class CoordinatedTPLinkFeatureEntity(CoordinatedTPLinkEntity, ABC):
feature_type=feature_type,
entity_class=entity_class,
descriptions=descriptions,
platform_domain=platform_domain,
)
)
@ -498,6 +513,7 @@ class CoordinatedTPLinkFeatureEntity(CoordinatedTPLinkEntity, ABC):
feature_type=feature_type,
entity_class=entity_class,
descriptions=descriptions,
platform_domain=platform_domain,
parent=device,
)
_LOGGER.debug(
@ -539,6 +555,11 @@ class CoordinatedTPLinkModuleEntity(CoordinatedTPLinkEntity, ABC):
else:
self._attr_name = get_device_name(device)
def _get_unique_id(self) -> str:
"""Return unique ID for the entity."""
desc = self.entity_description
return desc.unique_id_fn(self._device, desc)
@classmethod
def _entities_for_device[
_E: CoordinatedTPLinkModuleEntity,
@ -551,6 +572,7 @@ class CoordinatedTPLinkModuleEntity(CoordinatedTPLinkEntity, ABC):
*,
entity_class: type[_E],
descriptions: Iterable[_D],
platform_domain: str,
parent: Device | None = None,
) -> list[_E]:
"""Return a list of entities to add."""
@ -563,7 +585,15 @@ class CoordinatedTPLinkModuleEntity(CoordinatedTPLinkEntity, ABC):
)
for description in descriptions
if description.exists_fn(device, coordinator.config_entry)
and async_check_create_deprecated(
hass,
description.unique_id_fn(device, description),
description,
)
]
async_process_deprecated(
hass, platform_domain, coordinator.config_entry.entry_id, entities, device
)
return entities
@classmethod
@ -578,6 +608,7 @@ class CoordinatedTPLinkModuleEntity(CoordinatedTPLinkEntity, ABC):
*,
entity_class: type[_E],
descriptions: Iterable[_D],
platform_domain: str,
known_child_device_ids: set[str],
first_check: bool,
) -> list[_E]:
@ -597,6 +628,7 @@ class CoordinatedTPLinkModuleEntity(CoordinatedTPLinkEntity, ABC):
coordinator=coordinator,
entity_class=entity_class,
descriptions=descriptions,
platform_domain=platform_domain,
)
)
has_parent_entities = bool(entities)
@ -621,6 +653,7 @@ class CoordinatedTPLinkModuleEntity(CoordinatedTPLinkEntity, ABC):
coordinator=child_coordinator,
entity_class=entity_class,
descriptions=descriptions,
platform_domain=platform_domain,
parent=device,
)
_LOGGER.debug(

View File

@ -1,5 +1,6 @@
"""Support for TPLink Fan devices."""
from collections.abc import Callable
from dataclasses import dataclass
import logging
import math
@ -8,6 +9,7 @@ from typing import Any
from kasa import Device, Module
from homeassistant.components.fan import (
DOMAIN as FAN_DOMAIN,
FanEntity,
FanEntityDescription,
FanEntityFeature,
@ -20,7 +22,7 @@ from homeassistant.util.percentage import (
)
from homeassistant.util.scaling import int_states_in_range
from . import TPLinkConfigEntry
from . import TPLinkConfigEntry, legacy_device_id
from .coordinator import TPLinkDataUpdateCoordinator
from .entity import (
CoordinatedTPLinkModuleEntity,
@ -39,6 +41,12 @@ _LOGGER = logging.getLogger(__name__)
class TPLinkFanEntityDescription(FanEntityDescription, TPLinkModuleEntityDescription):
"""Base class for fan entity description."""
unique_id_fn: Callable[[Device, TPLinkModuleEntityDescription], str] = (
lambda device, desc: legacy_device_id(device)
if desc.key == "fan"
else f"{legacy_device_id(device)}-{desc.key}"
)
FAN_DESCRIPTIONS: tuple[TPLinkFanEntityDescription, ...] = (
TPLinkFanEntityDescription(
@ -68,6 +76,7 @@ async def async_setup_entry(
coordinator=parent_coordinator,
entity_class=TPLinkFanEntity,
descriptions=FAN_DESCRIPTIONS,
platform_domain=FAN_DOMAIN,
known_child_device_ids=known_child_device_ids,
first_check=first_check,
)

View File

@ -18,6 +18,7 @@ from homeassistant.components.light import (
ATTR_EFFECT,
ATTR_HS_COLOR,
ATTR_TRANSITION,
DOMAIN as LIGHT_DOMAIN,
EFFECT_OFF,
ColorMode,
LightEntity,
@ -141,12 +142,39 @@ def _async_build_base_effect(
}
def _get_backwards_compatible_light_unique_id(
device: Device, entity_description: TPLinkModuleEntityDescription
) -> str:
"""Return unique ID for the entity."""
# For historical reasons the light platform uses the mac address as
# the unique id whereas all other platforms use device_id.
# For backwards compat with pyHS100
if device.device_type is DeviceType.Dimmer and isinstance(device, IotDevice):
# Dimmers used to use the switch format since
# pyHS100 treated them as SmartPlug but the old code
# created them as lights
# https://github.com/home-assistant/core/blob/2021.9.7/ \
# homeassistant/components/tplink/common.py#L86
return legacy_device_id(device)
# Newer devices can have child lights. While there isn't currently
# an example of a device with more than one light we use the device_id
# for consistency and future proofing
if device.parent or device.children:
return legacy_device_id(device)
return device.mac.replace(":", "").upper()
@dataclass(frozen=True, kw_only=True)
class TPLinkLightEntityDescription(
LightEntityDescription, TPLinkModuleEntityDescription
):
"""Base class for tplink light entity description."""
unique_id_fn = _get_backwards_compatible_light_unique_id
LIGHT_DESCRIPTIONS: tuple[TPLinkLightEntityDescription, ...] = (
TPLinkLightEntityDescription(
@ -186,6 +214,7 @@ async def async_setup_entry(
coordinator=parent_coordinator,
entity_class=TPLinkLightEntity,
descriptions=LIGHT_DESCRIPTIONS,
platform_domain=LIGHT_DOMAIN,
known_child_device_ids=known_child_device_ids_light,
first_check=first_check,
)
@ -196,6 +225,7 @@ async def async_setup_entry(
coordinator=parent_coordinator,
entity_class=TPLinkLightEffectEntity,
descriptions=LIGHT_EFFECT_DESCRIPTIONS,
platform_domain=LIGHT_DOMAIN,
known_child_device_ids=known_child_device_ids_light_effect,
first_check=first_check,
)
@ -242,29 +272,6 @@ class TPLinkLightEntity(CoordinatedTPLinkModuleEntity, LightEntity):
# If the light supports only a single color mode, set it now
self._fixed_color_mode = next(iter(self._attr_supported_color_modes))
def _get_unique_id(self) -> str:
"""Return unique ID for the entity."""
# For historical reasons the light platform uses the mac address as
# the unique id whereas all other platforms use device_id.
device = self._device
# For backwards compat with pyHS100
if device.device_type is DeviceType.Dimmer and isinstance(device, IotDevice):
# Dimmers used to use the switch format since
# pyHS100 treated them as SmartPlug but the old code
# created them as lights
# https://github.com/home-assistant/core/blob/2021.9.7/ \
# homeassistant/components/tplink/common.py#L86
return legacy_device_id(device)
# Newer devices can have child lights. While there isn't currently
# an example of a device with more than one light we use the device_id
# for consistency and future proofing
if self._parent or device.children:
return legacy_device_id(device)
return device.mac.replace(":", "").upper()
@callback
def _async_extract_brightness_transition(
self, **kwargs: Any

View File

@ -18,7 +18,6 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TPLinkConfigEntry
from .deprecate import async_cleanup_deprecated
from .entity import (
CoordinatedTPLinkFeatureEntity,
TPLinkDataUpdateCoordinator,
@ -91,10 +90,10 @@ async def async_setup_entry(
feature_type=Feature.Type.Number,
entity_class=TPLinkNumberEntity,
descriptions=NUMBER_DESCRIPTIONS_MAP,
platform_domain=NUMBER_DOMAIN,
known_child_device_ids=known_child_device_ids,
first_check=first_check,
)
async_cleanup_deprecated(hass, NUMBER_DOMAIN, config_entry.entry_id, entities)
async_add_entities(entities)
_check_device()

View File

@ -16,7 +16,6 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TPLinkConfigEntry
from .deprecate import async_cleanup_deprecated
from .entity import (
CoordinatedTPLinkFeatureEntity,
TPLinkDataUpdateCoordinator,
@ -71,10 +70,10 @@ async def async_setup_entry(
feature_type=Feature.Type.Choice,
entity_class=TPLinkSelectEntity,
descriptions=SELECT_DESCRIPTIONS_MAP,
platform_domain=SELECT_DOMAIN,
known_child_device_ids=known_child_device_ids,
first_check=first_check,
)
async_cleanup_deprecated(hass, SELECT_DOMAIN, config_entry.entry_id, entities)
async_add_entities(entities)
_check_device()

View File

@ -19,7 +19,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TPLinkConfigEntry
from .const import UNIT_MAPPING
from .deprecate import async_cleanup_deprecated
from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription
@ -141,10 +140,10 @@ async def async_setup_entry(
feature_type=Feature.Type.Sensor,
entity_class=TPLinkSensorEntity,
descriptions=SENSOR_DESCRIPTIONS_MAP,
platform_domain=SENSOR_DOMAIN,
known_child_device_ids=known_child_device_ids,
first_check=first_check,
)
async_cleanup_deprecated(hass, SENSOR_DOMAIN, config_entry.entry_id, entities)
async_add_entities(entities)
_check_device()

View File

@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
@ -9,6 +10,7 @@ from kasa import Device, Module
from kasa.smart.modules.alarm import Alarm
from homeassistant.components.siren import (
DOMAIN as SIREN_DOMAIN,
SirenEntity,
SirenEntityDescription,
SirenEntityFeature,
@ -16,7 +18,7 @@ from homeassistant.components.siren import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TPLinkConfigEntry
from . import TPLinkConfigEntry, legacy_device_id
from .coordinator import TPLinkDataUpdateCoordinator
from .entity import (
CoordinatedTPLinkModuleEntity,
@ -35,6 +37,12 @@ class TPLinkSirenEntityDescription(
):
"""Base class for siren entity description."""
unique_id_fn: Callable[[Device, TPLinkModuleEntityDescription], str] = (
lambda device, desc: legacy_device_id(device)
if desc.key == "siren"
else f"{legacy_device_id(device)}-{desc.key}"
)
SIREN_DESCRIPTIONS: tuple[TPLinkSirenEntityDescription, ...] = (
TPLinkSirenEntityDescription(
@ -64,6 +72,7 @@ async def async_setup_entry(
coordinator=parent_coordinator,
entity_class=TPLinkSirenEntity,
descriptions=SIREN_DESCRIPTIONS,
platform_domain=SIREN_DOMAIN,
known_child_device_ids=known_child_device_ids,
first_check=first_check,
)

View File

@ -17,7 +17,6 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TPLinkConfigEntry
from .deprecate import async_cleanup_deprecated
from .entity import (
CoordinatedTPLinkFeatureEntity,
TPLinkFeatureEntityDescription,
@ -100,10 +99,10 @@ async def async_setup_entry(
feature_type=Feature.Switch,
entity_class=TPLinkSwitch,
descriptions=SWITCH_DESCRIPTIONS_MAP,
platform_domain=SWITCH_DOMAIN,
known_child_device_ids=known_child_device_ids,
first_check=first_check,
)
async_cleanup_deprecated(hass, SWITCH_DOMAIN, config_entry.entry_id, entities)
async_add_entities(entities)
_check_device()

View File

@ -197,10 +197,12 @@ def _mocked_device(
mod.get_feature.side_effect = device_features.get
mod.has_feature.side_effect = lambda id: id in device_features
device.parent = None
device.children = []
if children:
for child in children:
child.mac = mac
child.parent = device
device.children = children
device.device_type = device_type if device_type else DeviceType.Unknown
if (

View File

@ -1052,6 +1052,10 @@ async def test_automatic_module_device_addition_and_removal(
ip_address=IP_ADDRESS3,
mac=MAC_ADDRESS3,
)
# Set the parent property for the dynamic children as mock_device only sets
# it on initialization
for child in children.values():
child.parent = mock_device
with override_side_effect(mock_connect["connect"], lambda *_, **__: mock_device):
mock_camera_config_entry.add_to_hass(hass)
@ -1150,3 +1154,73 @@ async def test_automatic_module_device_addition_and_removal(
)
assert device_entry
assert device_entry.via_device_id == parent_device.id
async def test_automatic_device_addition_does_not_remove_disabled_default(
hass: HomeAssistant,
mock_camera_config_entry: MockConfigEntry,
mock_connect: AsyncMock,
mock_discovery: AsyncMock,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test for automatic device addition does not remove disabled default entities."""
features = ["ssid", "signal_level"]
children = {
f"child{index}": _mocked_device(
alias=f"child {index}",
features=features,
device_id=f"child{index}",
)
for index in range(1, 5)
}
mock_device = _mocked_device(
alias="hub",
children=[children["child1"], children["child2"]],
features=features,
device_type=DeviceType.Hub,
device_id="hub_parent",
ip_address=IP_ADDRESS3,
mac=MAC_ADDRESS3,
)
# Set the parent property for the dynamic children as mock_device only sets
# it on initialization
for child in children.values():
child.parent = mock_device
with override_side_effect(mock_connect["connect"], lambda *_, **__: mock_device):
mock_camera_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_camera_config_entry.entry_id)
await hass.async_block_till_done()
def check_entities(entity_id_device):
entity_id = f"sensor.{entity_id_device}_signal_level"
state = hass.states.get(entity_id)
assert state
reg_ent = entity_registry.async_get(entity_id)
assert reg_ent
assert reg_ent.disabled is False
entity_id = f"sensor.{entity_id_device}_ssid"
state = hass.states.get(entity_id)
assert state is None
reg_ent = entity_registry.async_get(entity_id)
assert reg_ent
assert reg_ent.disabled is True
assert reg_ent.disabled_by is er.RegistryEntryDisabler.INTEGRATION
check_entities("hub")
for child_id in (1, 2):
check_entities(f"child_{child_id}")
# Add child devices
mock_device.children = [children["child1"], children["child2"], children["child3"]]
freezer.tick(5)
async_fire_time_changed(hass)
check_entities("hub")
for child_id in (1, 2, 3):
check_entities(f"child_{child_id}")