From 1589f005c4be6d91da6b81b0e3a1b85e7c6b1977 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Nov 2023 08:19:39 -0600 Subject: [PATCH] Fix tplink overloading power strips --- homeassistant/components/tplink/__init__.py | 19 +++++++++-- .../components/tplink/coordinator.py | 15 ++------- .../components/tplink/diagnostics.py | 5 +-- homeassistant/components/tplink/entity.py | 2 +- homeassistant/components/tplink/light.py | 17 +++++----- homeassistant/components/tplink/models.py | 14 ++++++++ homeassistant/components/tplink/sensor.py | 33 ++++++++++++------- homeassistant/components/tplink/switch.py | 12 ++++--- 8 files changed, 73 insertions(+), 44 deletions(-) create mode 100644 homeassistant/components/tplink/models.py diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index f2a1e6823043..4efd7ffdf0be 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -29,6 +29,7 @@ from .const import DOMAIN, PLATFORMS from .coordinator import TPLinkDataUpdateCoordinator +from .models import TPLinkData DISCOVERY_INTERVAL = timedelta(minutes=15) CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) @@ -102,7 +103,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"Unexpected device found at {host}; expected {entry.unique_id}, found {found_mac}" ) - hass.data[DOMAIN][entry.entry_id] = TPLinkDataUpdateCoordinator(hass, device) + parent_coordinator = TPLinkDataUpdateCoordinator(hass, device, timedelta(seconds=5)) + child_coordinators: list[TPLinkDataUpdateCoordinator] = [] + + if device.is_strip: + 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)) + for child in device.children + ] + + hass.data[DOMAIN][entry.entry_id] = TPLinkData( + parent_coordinator, child_coordinators + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -111,7 +125,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" hass_data: dict[str, Any] = hass.data[DOMAIN] - device: SmartDevice = hass_data[entry.entry_id].device + data: TPLinkData = hass_data[entry.entry_id] + device = data.parent_coordinator.device if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass_data.pop(entry.entry_id) await device.protocol.close() diff --git a/homeassistant/components/tplink/coordinator.py b/homeassistant/components/tplink/coordinator.py index 97c8397831df..582c49638e7c 100644 --- a/homeassistant/components/tplink/coordinator.py +++ b/homeassistant/components/tplink/coordinator.py @@ -22,11 +22,10 @@ def __init__( self, hass: HomeAssistant, device: SmartDevice, + update_interval: timedelta, ) -> None: """Initialize DataUpdateCoordinator to gather data for specific SmartPlug.""" self.device = device - self.update_children = True - update_interval = timedelta(seconds=5) super().__init__( hass, _LOGGER, @@ -39,19 +38,9 @@ def __init__( ), ) - async def async_request_refresh_without_children(self) -> None: - """Request a refresh without the children.""" - # If the children do get updated this is ok as this is an - # optimization to reduce the number of requests on the device - # when we do not need it. - self.update_children = False - await self.async_request_refresh() - async def _async_update_data(self) -> None: """Fetch all device and sensor data from api.""" try: - await self.device.update(update_children=self.update_children) + await self.device.update(update_children=False) except SmartDeviceException as ex: raise UpdateFailed(ex) from ex - finally: - self.update_children = True diff --git a/homeassistant/components/tplink/diagnostics.py b/homeassistant/components/tplink/diagnostics.py index c81356ee658c..65646e8b8581 100644 --- a/homeassistant/components/tplink/diagnostics.py +++ b/homeassistant/components/tplink/diagnostics.py @@ -9,7 +9,7 @@ from homeassistant.helpers.device_registry import format_mac from .const import DOMAIN -from .coordinator import TPLinkDataUpdateCoordinator +from .models import TPLinkData TO_REDACT = { # Entry fields @@ -36,7 +36,8 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + data: TPLinkData = hass.data[DOMAIN][entry.entry_id] + coordinator = data.parent_coordinator oui = format_mac(coordinator.device.mac)[:8].upper() return async_redact_data( {"device_last_response": coordinator.device.internal_state, "oui": oui}, diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index afb341b47ede..df082cfb8bd9 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -24,7 +24,7 @@ def async_refresh_after( async def _async_wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: await func(self, *args, **kwargs) - await self.coordinator.async_request_refresh_without_children() + await self.coordinator.async_request_refresh() return _async_wrap diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index db7e6ff355e8..b0151278c936 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -28,6 +28,7 @@ from .const import DOMAIN from .coordinator import TPLinkDataUpdateCoordinator from .entity import CoordinatedTPLinkEntity, async_refresh_after +from .models import TPLinkData _LOGGER = logging.getLogger(__name__) @@ -132,14 +133,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up switches.""" - coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - if coordinator.device.is_light_strip: + data: TPLinkData = hass.data[DOMAIN][config_entry.entry_id] + parent_coordinator = data.parent_coordinator + device = parent_coordinator.device + if device.is_light_strip: async_add_entities( - [ - TPLinkSmartLightStrip( - cast(SmartLightStrip, coordinator.device), coordinator - ) - ] + [TPLinkSmartLightStrip(cast(SmartLightStrip, device), parent_coordinator)] ) platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( @@ -152,9 +151,9 @@ async def async_setup_entry( SEQUENCE_EFFECT_DICT, "async_set_sequence_effect", ) - elif coordinator.device.is_bulb or coordinator.device.is_dimmer: + elif device.is_bulb or device.is_dimmer: async_add_entities( - [TPLinkSmartBulb(cast(SmartBulb, coordinator.device), coordinator)] + [TPLinkSmartBulb(cast(SmartBulb, device), parent_coordinator)] ) diff --git a/homeassistant/components/tplink/models.py b/homeassistant/components/tplink/models.py new file mode 100644 index 000000000000..4367f46711dc --- /dev/null +++ b/homeassistant/components/tplink/models.py @@ -0,0 +1,14 @@ +"""The tplink integration models.""" +from __future__ import annotations + +from dataclasses import dataclass + +from .coordinator import TPLinkDataUpdateCoordinator + + +@dataclass(slots=True) +class TPLinkData: + """Data for the tplink integration.""" + + parent_coordinator: TPLinkDataUpdateCoordinator + children_coordinators: list[TPLinkDataUpdateCoordinator] diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index 46909f39dfec..e2b333d76465 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -33,6 +33,7 @@ ) from .coordinator import TPLinkDataUpdateCoordinator from .entity import CoordinatedTPLinkEntity +from .models import TPLinkData @dataclass @@ -106,31 +107,39 @@ def async_emeter_from_device( return None if device.is_bulb else 0.0 +def _async_sensors_for_device( + device: SmartDevice, coordinator: TPLinkDataUpdateCoordinator +) -> list[SmartPlugSensor]: + """Generate the sensors for the device.""" + return [ + SmartPlugSensor(device, coordinator, description) + for description in ENERGY_SENSORS + if async_emeter_from_device(device, description) is not None + ] + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensors.""" - coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + data: TPLinkData = hass.data[DOMAIN][config_entry.entry_id] + parent_coordinator = data.parent_coordinator + children_coordinators = data.children_coordinators entities: list[SmartPlugSensor] = [] - parent = coordinator.device + parent = parent_coordinator.device if not parent.has_emeter: return - def _async_sensors_for_device(device: SmartDevice) -> list[SmartPlugSensor]: - return [ - SmartPlugSensor(device, coordinator, description) - for description in ENERGY_SENSORS - if async_emeter_from_device(device, description) is not None - ] - if parent.is_strip: # Historically we only add the children if the device is a strip - for child in parent.children: - entities.extend(_async_sensors_for_device(child)) + for idx, child in enumerate(parent.children): + entities.extend( + _async_sensors_for_device(child, children_coordinators[idx]) + ) else: - entities.extend(_async_sensors_for_device(parent)) + entities.extend(_async_sensors_for_device(parent, parent_coordinator)) async_add_entities(entities) diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index fb812abc2933..b1ca848260f0 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -16,6 +16,7 @@ from .const import DOMAIN from .coordinator import TPLinkDataUpdateCoordinator from .entity import CoordinatedTPLinkEntity, async_refresh_after +from .models import TPLinkData _LOGGER = logging.getLogger(__name__) @@ -26,8 +27,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up switches.""" - coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - device = cast(SmartPlug, coordinator.device) + data: TPLinkData = hass.data[DOMAIN][config_entry.entry_id] + parent_coordinator = data.parent_coordinator + device = cast(SmartPlug, parent_coordinator.device) if not device.is_plug and not device.is_strip and not device.is_dimmer: return entities: list = [] @@ -35,11 +37,11 @@ async def async_setup_entry( # Historically we only add the children if the device is a strip _LOGGER.debug("Initializing strip with %s sockets", len(device.children)) for child in device.children: - entities.append(SmartPlugSwitchChild(device, coordinator, child)) + entities.append(SmartPlugSwitchChild(device, parent_coordinator, child)) elif device.is_plug: - entities.append(SmartPlugSwitch(device, coordinator)) + entities.append(SmartPlugSwitch(device, parent_coordinator)) - entities.append(SmartPlugLedSwitch(device, coordinator)) + entities.append(SmartPlugLedSwitch(device, parent_coordinator)) async_add_entities(entities)