diff --git a/kasa/tapo/tapobulb.py b/kasa/tapo/tapobulb.py index 3640074f..d1e953d7 100644 --- a/kasa/tapo/tapobulb.py +++ b/kasa/tapo/tapobulb.py @@ -17,17 +17,6 @@ class TapoBulb(TapoDevice, SmartBulb): Documentation TBD. See :class:`~kasa.smartbulb.SmartBulb` for now. """ - @property - def has_emeter(self) -> bool: - """Bulbs have only historical emeter. - - {'usage': - 'power_usage': {'today': 6, 'past7': 106, 'past30': 106}, - 'saved_power': {'today': 35, 'past7': 529, 'past30': 529}, - } - """ - return False - @property def is_color(self) -> bool: """Whether the bulb supports color changes.""" diff --git a/kasa/tapo/tapodevice.py b/kasa/tapo/tapodevice.py index 717de7ef..24848843 100644 --- a/kasa/tapo/tapodevice.py +++ b/kasa/tapo/tapodevice.py @@ -6,7 +6,9 @@ from typing import Any, Dict, Optional, Set, cast from ..aestransport import AesTransport from ..deviceconfig import DeviceConfig +from ..emeterstatus import EmeterStatus from ..exceptions import AuthenticationException +from ..modules import Emeter from ..protocol import TPLinkProtocol from ..smartdevice import SmartDevice from ..smartprotocol import SmartProtocol @@ -28,38 +30,67 @@ class TapoDevice(SmartDevice): transport=AesTransport(config=config or DeviceConfig(host=host)), ) super().__init__(host=host, config=config, protocol=_protocol) - self._components: Optional[Dict[str, Any]] = None + 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 update(self, update_children: bool = True): """Update the device.""" if self.credentials is None or self.credentials.username is None: raise AuthenticationException("Tapo plug requires authentication.") - if self._components is None: + if self._components_raw is None: resp = await self.protocol.query("component_nego") - self._components = resp["component_nego"] + self._components_raw = resp["component_nego"] + self._components = { + comp["id"]: comp["ver_code"] + for comp in self._components_raw["component_list"] + } + await self._initialize_modules() + + extra_reqs: Dict[str, Any] = {} + if "energy_monitoring" in self._components: + extra_reqs = { + **extra_reqs, + "get_energy_usage": None, + "get_current_power": None, + } req = { "get_device_info": None, "get_device_usage": None, "get_device_time": None, + **extra_reqs, } + resp = await self.protocol.query(req) + self._info = resp["get_device_info"] self._usage = resp["get_device_usage"] self._time = resp["get_device_time"] + # Emeter is not always available, but we set them still for now. + self._energy = resp.get("get_energy_usage", {}) + self._emeter = resp.get("get_current_power", {}) self._last_update = self._data = { - "components": self._components, + "components": self._components_raw, "info": self._info, "usage": self._usage, "time": self._time, + "energy": self._energy, + "emeter": self._emeter, } _LOGGER.debug("Got an update: %s", self._data) + async def _initialize_modules(self): + """Initialize modules based on component negotiation response.""" + if "energy_monitoring" in self._components: + self.emeter_type = "emeter" + self.modules["emeter"] = Emeter(self, self.emeter_type) + @property def sys_info(self) -> Dict[str, Any]: """Returns the device info.""" @@ -161,6 +192,11 @@ class TapoDevice(SmartDevice): # TODO: return set() + @property + def has_emeter(self) -> bool: + """Return if the device has emeter.""" + return "energy_monitoring" in self._components + @property def is_on(self) -> bool: """Return true if the device is on.""" @@ -178,3 +214,36 @@ class TapoDevice(SmartDevice): """Update state from info from the discover call.""" self._discovery_info = info self._info = info + + async def get_emeter_realtime(self) -> EmeterStatus: + """Retrieve current energy readings.""" + self._verify_emeter() + resp = await self.protocol.query("get_energy_usage") + self._energy = resp["get_energy_usage"] + return self.emeter_realtime + + def _convert_energy_data(self, data, scale) -> Optional[float]: + """Return adjusted emeter information.""" + return data if not data else data * scale + + @property + def emeter_realtime(self) -> EmeterStatus: + """Get the emeter status.""" + return EmeterStatus( + { + "power_mw": self._energy.get("current_power"), + "total": self._convert_energy_data( + self._energy.get("today_energy"), 1 / 1000 + ), + } + ) + + @property + def emeter_this_month(self) -> Optional[float]: + """Get the emeter value for this month.""" + return self._convert_energy_data(self._energy.get("month_energy"), 1 / 1000) + + @property + def emeter_today(self) -> Optional[float]: + """Get the emeter value for today.""" + return self._convert_energy_data(self._energy.get("today_energy"), 1 / 1000) diff --git a/kasa/tapo/tapoplug.py b/kasa/tapo/tapoplug.py index 67aed565..bb20f5cc 100644 --- a/kasa/tapo/tapoplug.py +++ b/kasa/tapo/tapoplug.py @@ -4,10 +4,8 @@ from datetime import datetime, timedelta from typing import Any, Dict, Optional, cast from ..deviceconfig import DeviceConfig -from ..emeterstatus import EmeterStatus -from ..modules import Emeter from ..protocol import TPLinkProtocol -from ..smartdevice import DeviceType, requires_update +from ..smartdevice import DeviceType from .tapodevice import TapoDevice _LOGGER = logging.getLogger(__name__) @@ -25,32 +23,6 @@ class TapoPlug(TapoDevice): ) -> None: super().__init__(host=host, config=config, protocol=protocol) self._device_type = DeviceType.Plug - self.modules: Dict[str, Any] = {} - self.emeter_type = "emeter" - self.modules["emeter"] = Emeter(self, self.emeter_type) - - @property # type: ignore - @requires_update - def has_emeter(self) -> bool: - """Return that the plug has an emeter.""" - return True - - async def update(self, update_children: bool = True): - """Call the device endpoint and update the device data.""" - await super().update(update_children) - - req = { - "get_energy_usage": None, - "get_current_power": None, - } - resp = await self.protocol.query(req) - self._energy = resp["get_energy_usage"] - self._emeter = resp["get_current_power"] - - self._data["energy"] = self._energy - self._data["emeter"] = self._emeter - - _LOGGER.debug("Got an update: %s %s", self._energy, self._emeter) @property def state_information(self) -> Dict[str, Any]: @@ -64,35 +36,6 @@ class TapoPlug(TapoDevice): }, } - @property - def emeter_realtime(self) -> EmeterStatus: - """Get the emeter status.""" - return EmeterStatus( - { - "power_mw": self._energy.get("current_power"), - "total": self._convert_energy_data( - self._energy.get("today_energy"), 1 / 1000 - ), - } - ) - - async def get_emeter_realtime(self) -> EmeterStatus: - """Retrieve current energy readings.""" - self._verify_emeter() - resp = await self.protocol.query("get_energy_usage") - self._energy = resp["get_energy_usage"] - return self.emeter_realtime - - @property - def emeter_today(self) -> Optional[float]: - """Get the emeter value for today.""" - return self._convert_energy_data(self._energy.get("today_energy"), 1 / 1000) - - @property - def emeter_this_month(self) -> Optional[float]: - """Get the emeter value for this month.""" - return self._convert_energy_data(self._energy.get("month_energy"), 1 / 1000) - @property def on_since(self) -> Optional[datetime]: """Return the time that the device was turned on or None if turned off.""" @@ -100,7 +43,3 @@ class TapoPlug(TapoDevice): return None on_time = cast(float, self._info.get("on_time")) return datetime.now().replace(microsecond=0) - timedelta(seconds=on_time) - - def _convert_energy_data(self, data, scale) -> Optional[float]: - """Return adjusted emeter information.""" - return data if not data else data * scale diff --git a/kasa/tests/test_smartdevice.py b/kasa/tests/test_smartdevice.py index a3019bff..b2ae9c33 100644 --- a/kasa/tests/test_smartdevice.py +++ b/kasa/tests/test_smartdevice.py @@ -7,7 +7,7 @@ import pytest # type: ignore # https://github.com/pytest-dev/pytest/issues/3342 import kasa from kasa import Credentials, DeviceConfig, SmartDevice, SmartDeviceException -from .conftest import device_iot, handle_turn_on, has_emeter, no_emeter_iot, turn_on +from .conftest import device_iot, handle_turn_on, has_emeter_iot, no_emeter_iot, turn_on from .newfakes import PLUG_SCHEMA, TZ_SCHEMA, FakeTransportProtocol # List of all SmartXXX classes including the SmartDevice base class @@ -35,7 +35,7 @@ async def test_invalid_connection(dev): await dev.update() -@has_emeter +@has_emeter_iot async def test_initial_update_emeter(dev, mocker): """Test that the initial update performs second query if emeter is available.""" dev._last_update = None