mirror of
https://github.com/home-assistant/core.git
synced 2025-07-09 06:17:07 +00:00
Add dynamic child device handling to tplink integration (#135229)
Add dynamic child device handling to tplink integration. For child devices that could be added/removed to hubs.
This commit is contained in:
parent
c6cab3259c
commit
51e3bf42f2
@ -18,7 +18,6 @@ from kasa import (
|
|||||||
KasaException,
|
KasaException,
|
||||||
)
|
)
|
||||||
from kasa.httpclient import get_cookie_jar
|
from kasa.httpclient import get_cookie_jar
|
||||||
from kasa.iot import IotStrip
|
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components import network
|
from homeassistant.components import network
|
||||||
@ -235,17 +234,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo
|
|||||||
parent_coordinator = TPLinkDataUpdateCoordinator(
|
parent_coordinator = TPLinkDataUpdateCoordinator(
|
||||||
hass, device, timedelta(seconds=5), entry
|
hass, device, timedelta(seconds=5), entry
|
||||||
)
|
)
|
||||||
child_coordinators: list[TPLinkDataUpdateCoordinator] = []
|
|
||||||
|
|
||||||
# The iot HS300 allows a limited number of concurrent requests and fetching the
|
|
||||||
# emeter information requires separate ones so create child coordinators here.
|
|
||||||
if isinstance(device, IotStrip):
|
|
||||||
child_coordinators = [
|
|
||||||
# The child coordinators only update energy data so we can
|
|
||||||
# set a longer update interval to avoid flooding the device
|
|
||||||
TPLinkDataUpdateCoordinator(hass, child, timedelta(seconds=60), entry)
|
|
||||||
for child in device.children
|
|
||||||
]
|
|
||||||
|
|
||||||
camera_creds: Credentials | None = None
|
camera_creds: Credentials | None = None
|
||||||
if camera_creds_dict := entry.data.get(CONF_CAMERA_CREDENTIALS):
|
if camera_creds_dict := entry.data.get(CONF_CAMERA_CREDENTIALS):
|
||||||
@ -254,9 +242,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo
|
|||||||
)
|
)
|
||||||
live_view = entry.data.get(CONF_LIVE_VIEW)
|
live_view = entry.data.get(CONF_LIVE_VIEW)
|
||||||
|
|
||||||
entry.runtime_data = TPLinkData(
|
entry.runtime_data = TPLinkData(parent_coordinator, camera_creds, live_view)
|
||||||
parent_coordinator, child_coordinators, camera_creds, live_view
|
|
||||||
)
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -8,6 +8,7 @@ from typing import Final, cast
|
|||||||
from kasa import Feature
|
from kasa import Feature
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
|
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||||
BinarySensorDeviceClass,
|
BinarySensorDeviceClass,
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
BinarySensorEntityDescription,
|
BinarySensorEntityDescription,
|
||||||
@ -16,6 +17,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import TPLinkConfigEntry
|
from . import TPLinkConfigEntry
|
||||||
|
from .deprecate import async_cleanup_deprecated
|
||||||
from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription
|
from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription
|
||||||
|
|
||||||
|
|
||||||
@ -73,19 +75,30 @@ async def async_setup_entry(
|
|||||||
"""Set up sensors."""
|
"""Set up sensors."""
|
||||||
data = config_entry.runtime_data
|
data = config_entry.runtime_data
|
||||||
parent_coordinator = data.parent_coordinator
|
parent_coordinator = data.parent_coordinator
|
||||||
children_coordinators = data.children_coordinators
|
|
||||||
device = parent_coordinator.device
|
device = parent_coordinator.device
|
||||||
|
|
||||||
entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
|
known_child_device_ids: set[str] = set()
|
||||||
hass=hass,
|
first_check = True
|
||||||
device=device,
|
|
||||||
coordinator=parent_coordinator,
|
def _check_device() -> None:
|
||||||
feature_type=Feature.Type.BinarySensor,
|
entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
|
||||||
entity_class=TPLinkBinarySensorEntity,
|
hass=hass,
|
||||||
descriptions=BINARYSENSOR_DESCRIPTIONS_MAP,
|
device=device,
|
||||||
child_coordinators=children_coordinators,
|
coordinator=parent_coordinator,
|
||||||
)
|
feature_type=Feature.Type.BinarySensor,
|
||||||
async_add_entities(entities)
|
entity_class=TPLinkBinarySensorEntity,
|
||||||
|
descriptions=BINARYSENSOR_DESCRIPTIONS_MAP,
|
||||||
|
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()
|
||||||
|
first_check = False
|
||||||
|
config_entry.async_on_unload(parent_coordinator.async_add_listener(_check_device))
|
||||||
|
|
||||||
|
|
||||||
class TPLinkBinarySensorEntity(CoordinatedTPLinkFeatureEntity, BinarySensorEntity):
|
class TPLinkBinarySensorEntity(CoordinatedTPLinkFeatureEntity, BinarySensorEntity):
|
||||||
|
@ -83,20 +83,27 @@ async def async_setup_entry(
|
|||||||
"""Set up buttons."""
|
"""Set up buttons."""
|
||||||
data = config_entry.runtime_data
|
data = config_entry.runtime_data
|
||||||
parent_coordinator = data.parent_coordinator
|
parent_coordinator = data.parent_coordinator
|
||||||
children_coordinators = data.children_coordinators
|
|
||||||
device = parent_coordinator.device
|
device = parent_coordinator.device
|
||||||
|
known_child_device_ids: set[str] = set()
|
||||||
|
first_check = True
|
||||||
|
|
||||||
entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
|
def _check_device() -> None:
|
||||||
hass=hass,
|
entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
|
||||||
device=device,
|
hass=hass,
|
||||||
coordinator=parent_coordinator,
|
device=device,
|
||||||
feature_type=Feature.Type.Action,
|
coordinator=parent_coordinator,
|
||||||
entity_class=TPLinkButtonEntity,
|
feature_type=Feature.Type.Action,
|
||||||
descriptions=BUTTON_DESCRIPTIONS_MAP,
|
entity_class=TPLinkButtonEntity,
|
||||||
child_coordinators=children_coordinators,
|
descriptions=BUTTON_DESCRIPTIONS_MAP,
|
||||||
)
|
known_child_device_ids=known_child_device_ids,
|
||||||
async_cleanup_deprecated(hass, BUTTON_DOMAIN, config_entry.entry_id, entities)
|
first_check=first_check,
|
||||||
async_add_entities(entities)
|
)
|
||||||
|
async_cleanup_deprecated(hass, BUTTON_DOMAIN, config_entry.entry_id, entities)
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
_check_device()
|
||||||
|
first_check = False
|
||||||
|
config_entry.async_on_unload(parent_coordinator.async_add_listener(_check_device))
|
||||||
|
|
||||||
|
|
||||||
class TPLinkButtonEntity(CoordinatedTPLinkFeatureEntity, ButtonEntity):
|
class TPLinkButtonEntity(CoordinatedTPLinkFeatureEntity, ButtonEntity):
|
||||||
|
@ -7,10 +7,12 @@ from datetime import timedelta
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from kasa import AuthenticationError, Credentials, Device, KasaException
|
from kasa import AuthenticationError, Credentials, Device, KasaException
|
||||||
|
from kasa.iot import IotStrip
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.debounce import Debouncer
|
from homeassistant.helpers.debounce import Debouncer
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
@ -24,7 +26,6 @@ class TPLinkData:
|
|||||||
"""Data for the tplink integration."""
|
"""Data for the tplink integration."""
|
||||||
|
|
||||||
parent_coordinator: TPLinkDataUpdateCoordinator
|
parent_coordinator: TPLinkDataUpdateCoordinator
|
||||||
children_coordinators: list[TPLinkDataUpdateCoordinator]
|
|
||||||
camera_credentials: Credentials | None
|
camera_credentials: Credentials | None
|
||||||
live_view: bool | None
|
live_view: bool | None
|
||||||
|
|
||||||
@ -60,6 +61,9 @@ class TPLinkDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
|||||||
hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False
|
hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
self._previous_child_device_ids = {child.device_id for child in device.children}
|
||||||
|
self.removed_child_device_ids: set[str] = set()
|
||||||
|
self._child_coordinators: dict[str, TPLinkDataUpdateCoordinator] = {}
|
||||||
|
|
||||||
async def _async_update_data(self) -> None:
|
async def _async_update_data(self) -> None:
|
||||||
"""Fetch all device and sensor data from api."""
|
"""Fetch all device and sensor data from api."""
|
||||||
@ -83,3 +87,48 @@ class TPLinkDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
|||||||
"exc": str(ex),
|
"exc": str(ex),
|
||||||
},
|
},
|
||||||
) from ex
|
) from ex
|
||||||
|
|
||||||
|
await self._process_child_devices()
|
||||||
|
|
||||||
|
async def _process_child_devices(self) -> None:
|
||||||
|
"""Process child devices and remove stale devices."""
|
||||||
|
current_child_device_ids = {child.device_id for child in self.device.children}
|
||||||
|
if (
|
||||||
|
stale_device_ids := self._previous_child_device_ids
|
||||||
|
- current_child_device_ids
|
||||||
|
):
|
||||||
|
device_registry = dr.async_get(self.hass)
|
||||||
|
for device_id in stale_device_ids:
|
||||||
|
device = device_registry.async_get_device(
|
||||||
|
identifiers={(DOMAIN, device_id)}
|
||||||
|
)
|
||||||
|
if device:
|
||||||
|
device_registry.async_update_device(
|
||||||
|
device_id=device.id,
|
||||||
|
remove_config_entry_id=self.config_entry.entry_id,
|
||||||
|
)
|
||||||
|
child_coordinator = self._child_coordinators.pop(device_id, None)
|
||||||
|
if child_coordinator:
|
||||||
|
await child_coordinator.async_shutdown()
|
||||||
|
|
||||||
|
self._previous_child_device_ids = current_child_device_ids
|
||||||
|
self.removed_child_device_ids = stale_device_ids
|
||||||
|
|
||||||
|
def get_child_coordinator(
|
||||||
|
self,
|
||||||
|
child: Device,
|
||||||
|
) -> TPLinkDataUpdateCoordinator:
|
||||||
|
"""Get separate child coordinator for a device or self if not needed."""
|
||||||
|
# The iot HS300 allows a limited number of concurrent requests and fetching the
|
||||||
|
# emeter information requires separate ones so create child coordinators here.
|
||||||
|
if isinstance(self.device, IotStrip):
|
||||||
|
if not (child_coordinator := self._child_coordinators.get(child.device_id)):
|
||||||
|
# The child coordinators only update energy data so we can
|
||||||
|
# set a longer update interval to avoid flooding the device
|
||||||
|
child_coordinator = TPLinkDataUpdateCoordinator(
|
||||||
|
self.hass, child, timedelta(seconds=60), self.config_entry
|
||||||
|
)
|
||||||
|
self._child_coordinators[child.device_id] = child_coordinator
|
||||||
|
return child_coordinator
|
||||||
|
|
||||||
|
return self
|
||||||
|
@ -434,7 +434,8 @@ class CoordinatedTPLinkFeatureEntity(CoordinatedTPLinkEntity, ABC):
|
|||||||
feature_type: Feature.Type,
|
feature_type: Feature.Type,
|
||||||
entity_class: type[_E],
|
entity_class: type[_E],
|
||||||
descriptions: Mapping[str, _D],
|
descriptions: Mapping[str, _D],
|
||||||
child_coordinators: list[TPLinkDataUpdateCoordinator] | None = None,
|
known_child_device_ids: set[str],
|
||||||
|
first_check: bool,
|
||||||
) -> list[_E]:
|
) -> list[_E]:
|
||||||
"""Create entities for device and its children.
|
"""Create entities for device and its children.
|
||||||
|
|
||||||
@ -442,36 +443,69 @@ class CoordinatedTPLinkFeatureEntity(CoordinatedTPLinkEntity, ABC):
|
|||||||
"""
|
"""
|
||||||
entities: list[_E] = []
|
entities: list[_E] = []
|
||||||
# Add parent entities before children so via_device id works.
|
# Add parent entities before children so via_device id works.
|
||||||
entities.extend(
|
# Only add the parent entities the first time
|
||||||
cls._entities_for_device(
|
if first_check:
|
||||||
|
entities.extend(
|
||||||
|
cls._entities_for_device(
|
||||||
|
hass,
|
||||||
|
device,
|
||||||
|
coordinator=coordinator,
|
||||||
|
feature_type=feature_type,
|
||||||
|
entity_class=entity_class,
|
||||||
|
descriptions=descriptions,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove any device ids removed via the coordinator so they can be re-added
|
||||||
|
for removed_child_id in coordinator.removed_child_device_ids:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Removing %s from known %s child ids for device %s"
|
||||||
|
"as it has been removed by the coordinator",
|
||||||
|
removed_child_id,
|
||||||
|
entity_class.__name__,
|
||||||
|
device.host,
|
||||||
|
)
|
||||||
|
known_child_device_ids.discard(removed_child_id)
|
||||||
|
|
||||||
|
current_child_devices = {child.device_id: child for child in device.children}
|
||||||
|
current_child_device_ids = set(current_child_devices.keys())
|
||||||
|
new_child_device_ids = current_child_device_ids - known_child_device_ids
|
||||||
|
children = []
|
||||||
|
|
||||||
|
if new_child_device_ids:
|
||||||
|
children = [
|
||||||
|
child
|
||||||
|
for child_id, child in current_child_devices.items()
|
||||||
|
if child_id in new_child_device_ids
|
||||||
|
]
|
||||||
|
known_child_device_ids.update(new_child_device_ids)
|
||||||
|
|
||||||
|
if children:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Getting %s entities for %s child devices on device %s",
|
||||||
|
entity_class.__name__,
|
||||||
|
len(children),
|
||||||
|
device.host,
|
||||||
|
)
|
||||||
|
for child in children:
|
||||||
|
child_coordinator = coordinator.get_child_coordinator(child)
|
||||||
|
|
||||||
|
child_entities = cls._entities_for_device(
|
||||||
hass,
|
hass,
|
||||||
device,
|
child,
|
||||||
coordinator=coordinator,
|
coordinator=child_coordinator,
|
||||||
feature_type=feature_type,
|
feature_type=feature_type,
|
||||||
entity_class=entity_class,
|
entity_class=entity_class,
|
||||||
descriptions=descriptions,
|
descriptions=descriptions,
|
||||||
|
parent=device,
|
||||||
)
|
)
|
||||||
)
|
_LOGGER.debug(
|
||||||
if device.children:
|
"Device %s, found %s child %s entities for child id %s",
|
||||||
_LOGGER.debug("Initializing device with %s children", len(device.children))
|
device.host,
|
||||||
for idx, child in enumerate(device.children):
|
len(entities),
|
||||||
# HS300 does not like too many concurrent requests and its
|
entity_class.__name__,
|
||||||
# emeter data requires a request for each socket, so we receive
|
child.device_id,
|
||||||
# separate coordinators.
|
)
|
||||||
if child_coordinators:
|
entities.extend(child_entities)
|
||||||
child_coordinator = child_coordinators[idx]
|
|
||||||
else:
|
|
||||||
child_coordinator = coordinator
|
|
||||||
entities.extend(
|
|
||||||
cls._entities_for_device(
|
|
||||||
hass,
|
|
||||||
child,
|
|
||||||
coordinator=child_coordinator,
|
|
||||||
feature_type=feature_type,
|
|
||||||
entity_class=entity_class,
|
|
||||||
descriptions=descriptions,
|
|
||||||
parent=device,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return entities
|
return entities
|
||||||
|
@ -9,6 +9,7 @@ from typing import Final, cast
|
|||||||
from kasa import Device, Feature
|
from kasa import Device, Feature
|
||||||
|
|
||||||
from homeassistant.components.number import (
|
from homeassistant.components.number import (
|
||||||
|
DOMAIN as NUMBER_DOMAIN,
|
||||||
NumberEntity,
|
NumberEntity,
|
||||||
NumberEntityDescription,
|
NumberEntityDescription,
|
||||||
NumberMode,
|
NumberMode,
|
||||||
@ -17,6 +18,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import TPLinkConfigEntry
|
from . import TPLinkConfigEntry
|
||||||
|
from .deprecate import async_cleanup_deprecated
|
||||||
from .entity import (
|
from .entity import (
|
||||||
CoordinatedTPLinkFeatureEntity,
|
CoordinatedTPLinkFeatureEntity,
|
||||||
TPLinkDataUpdateCoordinator,
|
TPLinkDataUpdateCoordinator,
|
||||||
@ -77,19 +79,27 @@ async def async_setup_entry(
|
|||||||
"""Set up number entities."""
|
"""Set up number entities."""
|
||||||
data = config_entry.runtime_data
|
data = config_entry.runtime_data
|
||||||
parent_coordinator = data.parent_coordinator
|
parent_coordinator = data.parent_coordinator
|
||||||
children_coordinators = data.children_coordinators
|
|
||||||
device = parent_coordinator.device
|
device = parent_coordinator.device
|
||||||
entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
|
known_child_device_ids: set[str] = set()
|
||||||
hass=hass,
|
first_check = True
|
||||||
device=device,
|
|
||||||
coordinator=parent_coordinator,
|
|
||||||
feature_type=Feature.Type.Number,
|
|
||||||
entity_class=TPLinkNumberEntity,
|
|
||||||
descriptions=NUMBER_DESCRIPTIONS_MAP,
|
|
||||||
child_coordinators=children_coordinators,
|
|
||||||
)
|
|
||||||
|
|
||||||
async_add_entities(entities)
|
def _check_device() -> None:
|
||||||
|
entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
|
||||||
|
hass=hass,
|
||||||
|
device=device,
|
||||||
|
coordinator=parent_coordinator,
|
||||||
|
feature_type=Feature.Type.Number,
|
||||||
|
entity_class=TPLinkNumberEntity,
|
||||||
|
descriptions=NUMBER_DESCRIPTIONS_MAP,
|
||||||
|
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()
|
||||||
|
first_check = False
|
||||||
|
config_entry.async_on_unload(parent_coordinator.async_add_listener(_check_device))
|
||||||
|
|
||||||
|
|
||||||
class TPLinkNumberEntity(CoordinatedTPLinkFeatureEntity, NumberEntity):
|
class TPLinkNumberEntity(CoordinatedTPLinkFeatureEntity, NumberEntity):
|
||||||
|
@ -7,11 +7,16 @@ from typing import Final, cast
|
|||||||
|
|
||||||
from kasa import Device, Feature
|
from kasa import Device, Feature
|
||||||
|
|
||||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
from homeassistant.components.select import (
|
||||||
|
DOMAIN as SELECT_DOMAIN,
|
||||||
|
SelectEntity,
|
||||||
|
SelectEntityDescription,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import TPLinkConfigEntry
|
from . import TPLinkConfigEntry
|
||||||
|
from .deprecate import async_cleanup_deprecated
|
||||||
from .entity import (
|
from .entity import (
|
||||||
CoordinatedTPLinkFeatureEntity,
|
CoordinatedTPLinkFeatureEntity,
|
||||||
TPLinkDataUpdateCoordinator,
|
TPLinkDataUpdateCoordinator,
|
||||||
@ -54,19 +59,27 @@ async def async_setup_entry(
|
|||||||
"""Set up select entities."""
|
"""Set up select entities."""
|
||||||
data = config_entry.runtime_data
|
data = config_entry.runtime_data
|
||||||
parent_coordinator = data.parent_coordinator
|
parent_coordinator = data.parent_coordinator
|
||||||
children_coordinators = data.children_coordinators
|
|
||||||
device = parent_coordinator.device
|
device = parent_coordinator.device
|
||||||
|
known_child_device_ids: set[str] = set()
|
||||||
|
first_check = True
|
||||||
|
|
||||||
entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
|
def _check_device() -> None:
|
||||||
hass=hass,
|
entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
|
||||||
device=device,
|
hass=hass,
|
||||||
coordinator=parent_coordinator,
|
device=device,
|
||||||
feature_type=Feature.Type.Choice,
|
coordinator=parent_coordinator,
|
||||||
entity_class=TPLinkSelectEntity,
|
feature_type=Feature.Type.Choice,
|
||||||
descriptions=SELECT_DESCRIPTIONS_MAP,
|
entity_class=TPLinkSelectEntity,
|
||||||
child_coordinators=children_coordinators,
|
descriptions=SELECT_DESCRIPTIONS_MAP,
|
||||||
)
|
known_child_device_ids=known_child_device_ids,
|
||||||
async_add_entities(entities)
|
first_check=first_check,
|
||||||
|
)
|
||||||
|
async_cleanup_deprecated(hass, SELECT_DOMAIN, config_entry.entry_id, entities)
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
_check_device()
|
||||||
|
first_check = False
|
||||||
|
config_entry.async_on_unload(parent_coordinator.async_add_listener(_check_device))
|
||||||
|
|
||||||
|
|
||||||
class TPLinkSelectEntity(CoordinatedTPLinkFeatureEntity, SelectEntity):
|
class TPLinkSelectEntity(CoordinatedTPLinkFeatureEntity, SelectEntity):
|
||||||
|
@ -129,20 +129,27 @@ async def async_setup_entry(
|
|||||||
"""Set up sensors."""
|
"""Set up sensors."""
|
||||||
data = config_entry.runtime_data
|
data = config_entry.runtime_data
|
||||||
parent_coordinator = data.parent_coordinator
|
parent_coordinator = data.parent_coordinator
|
||||||
children_coordinators = data.children_coordinators
|
|
||||||
device = parent_coordinator.device
|
device = parent_coordinator.device
|
||||||
|
known_child_device_ids: set[str] = set()
|
||||||
|
first_check = True
|
||||||
|
|
||||||
entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
|
def _check_device() -> None:
|
||||||
hass=hass,
|
entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
|
||||||
device=device,
|
hass=hass,
|
||||||
coordinator=parent_coordinator,
|
device=device,
|
||||||
feature_type=Feature.Type.Sensor,
|
coordinator=parent_coordinator,
|
||||||
entity_class=TPLinkSensorEntity,
|
feature_type=Feature.Type.Sensor,
|
||||||
descriptions=SENSOR_DESCRIPTIONS_MAP,
|
entity_class=TPLinkSensorEntity,
|
||||||
child_coordinators=children_coordinators,
|
descriptions=SENSOR_DESCRIPTIONS_MAP,
|
||||||
)
|
known_child_device_ids=known_child_device_ids,
|
||||||
async_cleanup_deprecated(hass, SENSOR_DOMAIN, config_entry.entry_id, entities)
|
first_check=first_check,
|
||||||
async_add_entities(entities)
|
)
|
||||||
|
async_cleanup_deprecated(hass, SENSOR_DOMAIN, config_entry.entry_id, entities)
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
_check_device()
|
||||||
|
first_check = False
|
||||||
|
config_entry.async_on_unload(parent_coordinator.async_add_listener(_check_device))
|
||||||
|
|
||||||
|
|
||||||
class TPLinkSensorEntity(CoordinatedTPLinkFeatureEntity, SensorEntity):
|
class TPLinkSensorEntity(CoordinatedTPLinkFeatureEntity, SensorEntity):
|
||||||
|
@ -8,11 +8,16 @@ from typing import Any, cast
|
|||||||
|
|
||||||
from kasa import Feature
|
from kasa import Feature
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
from homeassistant.components.switch import (
|
||||||
|
DOMAIN as SWITCH_DOMAIN,
|
||||||
|
SwitchEntity,
|
||||||
|
SwitchEntityDescription,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import TPLinkConfigEntry
|
from . import TPLinkConfigEntry
|
||||||
|
from .deprecate import async_cleanup_deprecated
|
||||||
from .entity import (
|
from .entity import (
|
||||||
CoordinatedTPLinkFeatureEntity,
|
CoordinatedTPLinkFeatureEntity,
|
||||||
TPLinkFeatureEntityDescription,
|
TPLinkFeatureEntityDescription,
|
||||||
@ -84,17 +89,26 @@ async def async_setup_entry(
|
|||||||
data = config_entry.runtime_data
|
data = config_entry.runtime_data
|
||||||
parent_coordinator = data.parent_coordinator
|
parent_coordinator = data.parent_coordinator
|
||||||
device = parent_coordinator.device
|
device = parent_coordinator.device
|
||||||
|
known_child_device_ids: set[str] = set()
|
||||||
|
first_check = True
|
||||||
|
|
||||||
entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
|
def _check_device() -> None:
|
||||||
hass=hass,
|
entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
|
||||||
device=device,
|
hass=hass,
|
||||||
coordinator=parent_coordinator,
|
device=device,
|
||||||
feature_type=Feature.Switch,
|
coordinator=parent_coordinator,
|
||||||
entity_class=TPLinkSwitch,
|
feature_type=Feature.Switch,
|
||||||
descriptions=SWITCH_DESCRIPTIONS_MAP,
|
entity_class=TPLinkSwitch,
|
||||||
)
|
descriptions=SWITCH_DESCRIPTIONS_MAP,
|
||||||
|
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)
|
||||||
|
|
||||||
async_add_entities(entities)
|
_check_device()
|
||||||
|
first_check = False
|
||||||
|
config_entry.async_on_unload(parent_coordinator.async_add_listener(_check_device))
|
||||||
|
|
||||||
|
|
||||||
class TPLinkSwitch(CoordinatedTPLinkFeatureEntity, SwitchEntity):
|
class TPLinkSwitch(CoordinatedTPLinkFeatureEntity, SwitchEntity):
|
||||||
|
@ -8,7 +8,16 @@ from typing import Any
|
|||||||
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
|
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
|
||||||
|
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
from kasa import AuthenticationError, DeviceConfig, Feature, KasaException, Module
|
from kasa import (
|
||||||
|
AuthenticationError,
|
||||||
|
Device,
|
||||||
|
DeviceConfig,
|
||||||
|
DeviceType,
|
||||||
|
Feature,
|
||||||
|
KasaException,
|
||||||
|
Module,
|
||||||
|
)
|
||||||
|
from kasa.iot import IotStrip
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant import setup
|
from homeassistant import setup
|
||||||
@ -827,3 +836,152 @@ async def test_migrate_remove_device_config(
|
|||||||
assert entry.data == expected_entry_data
|
assert entry.data == expected_entry_data
|
||||||
|
|
||||||
assert "Migration to version 1.5 complete" in caplog.text
|
assert "Migration to version 1.5 complete" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("device_type"),
|
||||||
|
[
|
||||||
|
(Device),
|
||||||
|
(IotStrip),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("platform", "feature_id", "translated_name"),
|
||||||
|
[
|
||||||
|
pytest.param("switch", "led", "led", id="switch"),
|
||||||
|
pytest.param(
|
||||||
|
"sensor", "current_consumption", "current_consumption", id="sensor"
|
||||||
|
),
|
||||||
|
pytest.param("binary_sensor", "overheated", "overheated", id="binary_sensor"),
|
||||||
|
pytest.param("number", "smooth_transition_on", "smooth_on", id="number"),
|
||||||
|
pytest.param("select", "light_preset", "light_preset", id="select"),
|
||||||
|
pytest.param("button", "reboot", "restart", id="button"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_automatic_device_addition_and_removal(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_connect: AsyncMock,
|
||||||
|
mock_discovery: AsyncMock,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
platform: str,
|
||||||
|
feature_id: str,
|
||||||
|
translated_name: str,
|
||||||
|
device_type: type,
|
||||||
|
) -> None:
|
||||||
|
"""Test for automatic device addition and removal."""
|
||||||
|
|
||||||
|
children = {
|
||||||
|
f"child{index}": _mocked_device(
|
||||||
|
alias=f"child {index}",
|
||||||
|
features=[feature_id],
|
||||||
|
device_type=DeviceType.StripSocket,
|
||||||
|
device_id=f"child{index}",
|
||||||
|
)
|
||||||
|
for index in range(1, 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_device = _mocked_device(
|
||||||
|
alias="hub",
|
||||||
|
children=[children["child1"], children["child2"]],
|
||||||
|
features=[feature_id],
|
||||||
|
device_type=DeviceType.Hub,
|
||||||
|
spec=device_type,
|
||||||
|
device_id="hub_parent",
|
||||||
|
)
|
||||||
|
|
||||||
|
with override_side_effect(mock_connect["connect"], lambda *_, **__: mock_device):
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
for child_id in (1, 2):
|
||||||
|
entity_id = f"{platform}.child_{child_id}_{translated_name}"
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert entity_registry.async_get(entity_id)
|
||||||
|
|
||||||
|
parent_device = device_registry.async_get_device(
|
||||||
|
identifiers={(DOMAIN, "hub_parent")}
|
||||||
|
)
|
||||||
|
assert parent_device
|
||||||
|
|
||||||
|
for device_id in ("child1", "child2"):
|
||||||
|
device_entry = device_registry.async_get_device(
|
||||||
|
identifiers={(DOMAIN, device_id)}
|
||||||
|
)
|
||||||
|
assert device_entry
|
||||||
|
assert device_entry.via_device_id == parent_device.id
|
||||||
|
|
||||||
|
# Remove one of the devices
|
||||||
|
mock_device.children = [children["child1"]]
|
||||||
|
freezer.tick(5)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
|
||||||
|
entity_id = f"{platform}.child_2_{translated_name}"
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state is None
|
||||||
|
assert entity_registry.async_get(entity_id) is None
|
||||||
|
|
||||||
|
assert device_registry.async_get_device(identifiers={(DOMAIN, "child2")}) is None
|
||||||
|
|
||||||
|
# Re-dd the previously removed child device
|
||||||
|
mock_device.children = [
|
||||||
|
children["child1"],
|
||||||
|
children["child2"],
|
||||||
|
]
|
||||||
|
freezer.tick(5)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
|
||||||
|
for child_id in (1, 2):
|
||||||
|
entity_id = f"{platform}.child_{child_id}_{translated_name}"
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert entity_registry.async_get(entity_id)
|
||||||
|
|
||||||
|
for device_id in ("child1", "child2"):
|
||||||
|
device_entry = device_registry.async_get_device(
|
||||||
|
identifiers={(DOMAIN, device_id)}
|
||||||
|
)
|
||||||
|
assert device_entry
|
||||||
|
assert device_entry.via_device_id == parent_device.id
|
||||||
|
|
||||||
|
# Add child devices
|
||||||
|
mock_device.children = [children["child1"], children["child3"], children["child4"]]
|
||||||
|
freezer.tick(5)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
|
||||||
|
for child_id in (1, 3, 4):
|
||||||
|
entity_id = f"{platform}.child_{child_id}_{translated_name}"
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert entity_registry.async_get(entity_id)
|
||||||
|
|
||||||
|
for device_id in ("child1", "child3", "child4"):
|
||||||
|
assert device_registry.async_get_device(identifiers={(DOMAIN, device_id)})
|
||||||
|
|
||||||
|
# Add the previously removed child device
|
||||||
|
mock_device.children = [
|
||||||
|
children["child1"],
|
||||||
|
children["child2"],
|
||||||
|
children["child3"],
|
||||||
|
children["child4"],
|
||||||
|
]
|
||||||
|
freezer.tick(5)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
|
||||||
|
for child_id in (1, 2, 3, 4):
|
||||||
|
entity_id = f"{platform}.child_{child_id}_{translated_name}"
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert entity_registry.async_get(entity_id)
|
||||||
|
|
||||||
|
for device_id in ("child1", "child2", "child3", "child4"):
|
||||||
|
device_entry = device_registry.async_get_device(
|
||||||
|
identifiers={(DOMAIN, device_id)}
|
||||||
|
)
|
||||||
|
assert device_entry
|
||||||
|
assert device_entry.via_device_id == parent_device.id
|
||||||
|
Loading…
x
Reference in New Issue
Block a user