Initial support for tapos with child devices (#720)

* Add ChildDevice and ChildProtocolWrapper

* Initialize & update children

* Fix circular imports

* Add dummy_protocol fixture and tests for unwrapping responseData

* Use dummy_protocol for existing smartprotocol tests

* Move _ChildProtocolWrapper to smartprotocol.py

* Use dummy_protocol for test multiple requests

* Use device_id instead of position for selecting the child

* Fix wrapping for regular requests

* Remove unused imports

* tweak

* rename child_device to childdevice

* Fix import
This commit is contained in:
Teemu R
2024-01-29 17:11:29 +01:00
committed by GitHub
parent b479b6d84d
commit 1ad2a05b65
6 changed files with 280 additions and 43 deletions

44
kasa/tapo/childdevice.py Normal file
View File

@@ -0,0 +1,44 @@
"""Child device implementation."""
from typing import Dict, Optional
from ..deviceconfig import DeviceConfig
from ..exceptions import SmartDeviceException
from ..smartprotocol import SmartProtocol, _ChildProtocolWrapper
from .tapodevice import TapoDevice
class ChildDevice(TapoDevice):
"""Presentation of a child device.
This wraps the protocol communications and sets internal data for the child.
"""
def __init__(
self,
parent: TapoDevice,
child_id: str,
config: Optional[DeviceConfig] = None,
protocol: Optional[SmartProtocol] = None,
) -> None:
super().__init__(parent.host, config=parent.config, protocol=parent.protocol)
self._parent = parent
self._id = child_id
self.protocol = _ChildProtocolWrapper(child_id, parent.protocol)
async def update(self, update_children: bool = True):
"""We just set the info here accordingly."""
def _get_child_info() -> Dict:
"""Return the subdevice information for this device."""
for child in self._parent._last_update["child_info"]["child_device_list"]:
if child["device_id"] == self._id:
return child
raise SmartDeviceException(
f"Unable to find child device with position {self._id}"
)
self._last_update = self._sys_info = self._info = _get_child_info()
def __repr__(self):
return f"<ChildDevice {self.alias} of {self._parent}>"

View File

@@ -5,11 +5,11 @@ from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Set, cast
from ..aestransport import AesTransport
from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig
from ..emeterstatus import EmeterStatus
from ..exceptions import AuthenticationException, SmartDeviceException
from ..modules import Emeter
from ..protocol import BaseProtocol
from ..smartdevice import SmartDevice, WifiNetwork
from ..smartprotocol import SmartProtocol
@@ -24,17 +24,27 @@ class TapoDevice(SmartDevice):
host: str,
*,
config: Optional[DeviceConfig] = None,
protocol: Optional[BaseProtocol] = None,
protocol: Optional[SmartProtocol] = None,
) -> None:
_protocol = protocol or SmartProtocol(
transport=AesTransport(config=config or DeviceConfig(host=host)),
)
super().__init__(host=host, config=config, protocol=_protocol)
self.protocol: SmartProtocol
self._components_raw: Optional[Dict[str, Any]] = None
self._components: Dict[str, int]
self._state_information: Dict[str, Any] = {}
self._discovery_info: Optional[Dict[str, Any]] = None
self.modules: Dict[str, Any] = {}
async def _initialize_children(self):
children = self._last_update["child_info"]["child_device_list"]
# TODO: Use the type information to construct children,
# as hubs can also have them.
from .childdevice import ChildDevice
self.children = [
ChildDevice(parent=self, child_id=child["device_id"]) for child in children
]
self._device_type = DeviceType.Strip
async def update(self, update_children: bool = True):
"""Update the device."""
@@ -51,6 +61,10 @@ class TapoDevice(SmartDevice):
await self._initialize_modules()
extra_reqs: Dict[str, Any] = {}
if "child_device" in self._components:
extra_reqs = {**extra_reqs, "get_child_device_list": None}
if "energy_monitoring" in self._components:
extra_reqs = {
**extra_reqs,
@@ -81,8 +95,15 @@ class TapoDevice(SmartDevice):
"time": self._time,
"energy": self._energy,
"emeter": self._emeter,
"child_info": resp.get("get_child_device_list", {}),
}
if self._last_update["child_info"]:
if not self.children:
await self._initialize_children()
for child in self.children:
await child.update()
_LOGGER.debug("Got an update: %s", self._data)
async def _initialize_modules(self):

View File

@@ -4,8 +4,8 @@ from datetime import datetime, timedelta
from typing import Any, Dict, Optional, cast
from ..deviceconfig import DeviceConfig
from ..protocol import BaseProtocol
from ..smartdevice import DeviceType
from ..smartprotocol import SmartProtocol
from .tapodevice import TapoDevice
_LOGGER = logging.getLogger(__name__)
@@ -19,7 +19,7 @@ class TapoPlug(TapoDevice):
host: str,
*,
config: Optional[DeviceConfig] = None,
protocol: Optional[BaseProtocol] = None,
protocol: Optional[SmartProtocol] = None,
) -> None:
super().__init__(host=host, config=config, protocol=protocol)
self._device_type = DeviceType.Plug