Skip to content

Commit

Permalink
Enable dynamic child devices for tplink module entities (home-assista…
Browse files Browse the repository at this point in the history
…nt#135822)

Add dynamic child device handling to tplink integration for module based entities. For child devices that could be added/removed to hubs.

This address the module based platforms. home-assistant#135229 addressed feature based platforms.
  • Loading branch information
sdb9696 authored Jan 20, 2025
1 parent cf33671 commit a84335a
Show file tree
Hide file tree
Showing 9 changed files with 663 additions and 173 deletions.
84 changes: 49 additions & 35 deletions homeassistant/components/tplink/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@

from aiohttp import web
from haffmpeg.camera import CameraMjpeg
from kasa import Credentials, Device, Module, StreamResolution
from kasa.smartcam.modules import Camera as CameraModule
from kasa import Device, Module, StreamResolution

from homeassistant.components import ffmpeg, stream
from homeassistant.components.camera import (
Expand All @@ -24,10 +23,14 @@
from . import TPLinkConfigEntry, legacy_device_id
from .const import CONF_CAMERA_CREDENTIALS
from .coordinator import TPLinkDataUpdateCoordinator
from .entity import CoordinatedTPLinkEntity, TPLinkModuleEntityDescription
from .entity import CoordinatedTPLinkModuleEntity, TPLinkModuleEntityDescription

_LOGGER = logging.getLogger(__name__)

# Coordinator is used to centralize the data updates
# For actions the integration handles locking of concurrent device request
PARALLEL_UPDATES = 0


@dataclass(frozen=True, kw_only=True)
class TPLinkCameraEntityDescription(
Expand All @@ -36,15 +39,18 @@ class TPLinkCameraEntityDescription(
"""Base class for camera entity description."""


# Coordinator is used to centralize the data updates
# For actions the integration handles locking of concurrent device request
PARALLEL_UPDATES = 0

CAMERA_DESCRIPTIONS: tuple[TPLinkCameraEntityDescription, ...] = (
TPLinkCameraEntityDescription(
key="live_view",
translation_key="live_view",
available_fn=lambda dev: dev.is_on,
exists_fn=lambda dev, entry: (
(rtd := entry.runtime_data) is not None
and rtd.live_view is True
and (cam_creds := rtd.camera_credentials) is not None
and (cm := dev.modules.get(Module.Camera)) is not None
and cm.stream_rtsp_url(cam_creds) is not None
),
),
)

Expand All @@ -58,26 +64,28 @@ async def async_setup_entry(
data = config_entry.runtime_data
parent_coordinator = data.parent_coordinator
device = parent_coordinator.device
camera_credentials = data.camera_credentials
live_view = data.live_view
ffmpeg_manager = ffmpeg.get_ffmpeg_manager(hass)

async_add_entities(
TPLinkCameraEntity(
device,
parent_coordinator,
description,
camera_module=camera_module,
parent=None,
ffmpeg_manager=ffmpeg_manager,
camera_credentials=camera_credentials,

known_child_device_ids: set[str] = set()
first_check = True

def _check_device() -> None:
entities = CoordinatedTPLinkModuleEntity.entities_for_device_and_its_children(
hass=hass,
device=device,
coordinator=parent_coordinator,
entity_class=TPLinkCameraEntity,
descriptions=CAMERA_DESCRIPTIONS,
known_child_device_ids=known_child_device_ids,
first_check=first_check,
)
for description in CAMERA_DESCRIPTIONS
if (camera_module := device.modules.get(Module.Camera)) and live_view
)
async_add_entities(entities)

_check_device()
first_check = False
config_entry.async_on_unload(parent_coordinator.async_add_listener(_check_device))

class TPLinkCameraEntity(CoordinatedTPLinkEntity, Camera):

class TPLinkCameraEntity(CoordinatedTPLinkModuleEntity, Camera):
"""Representation of a TPLink camera."""

IMAGE_INTERVAL = 5 * 60
Expand All @@ -86,37 +94,43 @@ class TPLinkCameraEntity(CoordinatedTPLinkEntity, Camera):

entity_description: TPLinkCameraEntityDescription

_ffmpeg_manager: ffmpeg.FFmpegManager

def __init__(
self,
device: Device,
coordinator: TPLinkDataUpdateCoordinator,
description: TPLinkCameraEntityDescription,
*,
camera_module: CameraModule,
parent: Device | None = None,
ffmpeg_manager: ffmpeg.FFmpegManager,
camera_credentials: Credentials | None,
) -> None:
"""Initialize a TPlink camera."""
self.entity_description = description
self._camera_module = camera_module
self._video_url = camera_module.stream_rtsp_url(
camera_credentials, stream_resolution=StreamResolution.SD
super().__init__(device, coordinator, description=description, parent=parent)
Camera.__init__(self)

self._camera_module = device.modules[Module.Camera]
self._camera_credentials = (
coordinator.config_entry.runtime_data.camera_credentials
)
self._video_url = self._camera_module.stream_rtsp_url(
self._camera_credentials, stream_resolution=StreamResolution.SD
)
self._image: bytes | None = None
super().__init__(device, coordinator, parent=parent)
Camera.__init__(self)
self._ffmpeg_manager = ffmpeg_manager
self._image_lock = asyncio.Lock()
self._last_update: float = 0
self._camera_credentials = camera_credentials
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()

self._ffmpeg_manager = ffmpeg.get_ffmpeg_manager(self.hass)

@callback
def _async_update_attrs(self) -> bool:
"""Update the entity's attributes."""
Expand Down
57 changes: 46 additions & 11 deletions homeassistant/components/tplink/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@

from __future__ import annotations

from dataclasses import dataclass
import logging
from typing import Any, cast

from kasa import Device, DeviceType
from kasa import Device
from kasa.smart.modules.temperaturecontrol import ThermostatState

from homeassistant.components.climate import (
ATTR_TEMPERATURE,
ClimateEntity,
ClimateEntityDescription,
ClimateEntityFeature,
HVACAction,
HVACMode,
Expand All @@ -23,7 +25,11 @@
from . import TPLinkConfigEntry
from .const import DOMAIN, UNIT_MAPPING
from .coordinator import TPLinkDataUpdateCoordinator
from .entity import CoordinatedTPLinkEntity, async_refresh_after
from .entity import (
CoordinatedTPLinkModuleEntity,
TPLinkModuleEntityDescription,
async_refresh_after,
)

# Coordinator is used to centralize the data updates
# For actions the integration handles locking of concurrent device request
Expand All @@ -40,6 +46,21 @@
_LOGGER = logging.getLogger(__name__)


@dataclass(frozen=True, kw_only=True)
class TPLinkClimateEntityDescription(
ClimateEntityDescription, TPLinkModuleEntityDescription
):
"""Base class for climate entity description."""


CLIMATE_DESCRIPTIONS: tuple[TPLinkClimateEntityDescription, ...] = (
TPLinkClimateEntityDescription(
key="climate",
exists_fn=lambda dev, _: dev.device_type is Device.Type.Thermostat,
),
)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: TPLinkConfigEntry,
Expand All @@ -50,15 +71,27 @@ async def async_setup_entry(
parent_coordinator = data.parent_coordinator
device = parent_coordinator.device

# As there are no standalone thermostats, we just iterate over the children.
async_add_entities(
TPLinkClimateEntity(child, parent_coordinator, parent=device)
for child in device.children
if child.device_type is DeviceType.Thermostat
)
known_child_device_ids: set[str] = set()
first_check = True

def _check_device() -> None:
entities = CoordinatedTPLinkModuleEntity.entities_for_device_and_its_children(
hass=hass,
device=device,
coordinator=parent_coordinator,
entity_class=TPLinkClimateEntity,
descriptions=CLIMATE_DESCRIPTIONS,
known_child_device_ids=known_child_device_ids,
first_check=first_check,
)
async_add_entities(entities)

_check_device()
first_check = False
config_entry.async_on_unload(parent_coordinator.async_add_listener(_check_device))


class TPLinkClimateEntity(CoordinatedTPLinkEntity, ClimateEntity):
class TPLinkClimateEntity(CoordinatedTPLinkModuleEntity, ClimateEntity):
"""Representation of a TPLink thermostat."""

_attr_name = None
Expand All @@ -70,16 +103,20 @@ class TPLinkClimateEntity(CoordinatedTPLinkEntity, ClimateEntity):
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
_attr_precision = PRECISION_TENTHS

entity_description: TPLinkClimateEntityDescription

# This disables the warning for async_turn_{on,off}, can be removed later.

def __init__(
self,
device: Device,
coordinator: TPLinkDataUpdateCoordinator,
description: TPLinkClimateEntityDescription,
*,
parent: Device,
) -> None:
"""Initialize the climate entity."""
super().__init__(device, coordinator, description, parent=parent)
self._state_feature = device.features["state"]
self._mode_feature = device.features["thermostat_mode"]
self._temp_feature = device.features["temperature"]
Expand All @@ -89,8 +126,6 @@ def __init__(
self._attr_max_temp = self._target_feature.maximum_value
self._attr_temperature_unit = UNIT_MAPPING[cast(str, self._temp_feature.unit)]

super().__init__(device, coordinator, parent=parent)

@async_refresh_after
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set target temperature."""
Expand Down
Loading

0 comments on commit a84335a

Please sign in to comment.