mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-01-08 22:07:06 +00:00
Pull up emeter handling to tapodevice base class (#601)
* Pull has_emeter property up to tapodevice base class This will also use the existence of energy_monitoring in the component_nego query to decide if the device has the service. * Move emeter related functions to tapodevice * Remove supported_modules override for now This should be done in a separate PR, if we want to expose the available components to cli and downstreams * Dedent extra reqs * Move extra_reqs initialization * Fix tests
This commit is contained in:
parent
864ea92ece
commit
10fc2c3c54
@ -17,17 +17,6 @@ class TapoBulb(TapoDevice, SmartBulb):
|
|||||||
Documentation TBD. See :class:`~kasa.smartbulb.SmartBulb` for now.
|
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
|
@property
|
||||||
def is_color(self) -> bool:
|
def is_color(self) -> bool:
|
||||||
"""Whether the bulb supports color changes."""
|
"""Whether the bulb supports color changes."""
|
||||||
|
@ -6,7 +6,9 @@ from typing import Any, Dict, Optional, Set, cast
|
|||||||
|
|
||||||
from ..aestransport import AesTransport
|
from ..aestransport import AesTransport
|
||||||
from ..deviceconfig import DeviceConfig
|
from ..deviceconfig import DeviceConfig
|
||||||
|
from ..emeterstatus import EmeterStatus
|
||||||
from ..exceptions import AuthenticationException
|
from ..exceptions import AuthenticationException
|
||||||
|
from ..modules import Emeter
|
||||||
from ..protocol import TPLinkProtocol
|
from ..protocol import TPLinkProtocol
|
||||||
from ..smartdevice import SmartDevice
|
from ..smartdevice import SmartDevice
|
||||||
from ..smartprotocol import SmartProtocol
|
from ..smartprotocol import SmartProtocol
|
||||||
@ -28,38 +30,67 @@ class TapoDevice(SmartDevice):
|
|||||||
transport=AesTransport(config=config or DeviceConfig(host=host)),
|
transport=AesTransport(config=config or DeviceConfig(host=host)),
|
||||||
)
|
)
|
||||||
super().__init__(host=host, config=config, protocol=_protocol)
|
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._state_information: Dict[str, Any] = {}
|
||||||
self._discovery_info: Optional[Dict[str, Any]] = None
|
self._discovery_info: Optional[Dict[str, Any]] = None
|
||||||
|
self.modules: Dict[str, Any] = {}
|
||||||
|
|
||||||
async def update(self, update_children: bool = True):
|
async def update(self, update_children: bool = True):
|
||||||
"""Update the device."""
|
"""Update the device."""
|
||||||
if self.credentials is None or self.credentials.username is None:
|
if self.credentials is None or self.credentials.username is None:
|
||||||
raise AuthenticationException("Tapo plug requires authentication.")
|
raise AuthenticationException("Tapo plug requires authentication.")
|
||||||
|
|
||||||
if self._components is None:
|
if self._components_raw is None:
|
||||||
resp = await self.protocol.query("component_nego")
|
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 = {
|
req = {
|
||||||
"get_device_info": None,
|
"get_device_info": None,
|
||||||
"get_device_usage": None,
|
"get_device_usage": None,
|
||||||
"get_device_time": None,
|
"get_device_time": None,
|
||||||
|
**extra_reqs,
|
||||||
}
|
}
|
||||||
|
|
||||||
resp = await self.protocol.query(req)
|
resp = await self.protocol.query(req)
|
||||||
|
|
||||||
self._info = resp["get_device_info"]
|
self._info = resp["get_device_info"]
|
||||||
self._usage = resp["get_device_usage"]
|
self._usage = resp["get_device_usage"]
|
||||||
self._time = resp["get_device_time"]
|
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 = {
|
self._last_update = self._data = {
|
||||||
"components": self._components,
|
"components": self._components_raw,
|
||||||
"info": self._info,
|
"info": self._info,
|
||||||
"usage": self._usage,
|
"usage": self._usage,
|
||||||
"time": self._time,
|
"time": self._time,
|
||||||
|
"energy": self._energy,
|
||||||
|
"emeter": self._emeter,
|
||||||
}
|
}
|
||||||
|
|
||||||
_LOGGER.debug("Got an update: %s", self._data)
|
_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
|
@property
|
||||||
def sys_info(self) -> Dict[str, Any]:
|
def sys_info(self) -> Dict[str, Any]:
|
||||||
"""Returns the device info."""
|
"""Returns the device info."""
|
||||||
@ -161,6 +192,11 @@ class TapoDevice(SmartDevice):
|
|||||||
# TODO:
|
# TODO:
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_emeter(self) -> bool:
|
||||||
|
"""Return if the device has emeter."""
|
||||||
|
return "energy_monitoring" in self._components
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return true if the device is on."""
|
"""Return true if the device is on."""
|
||||||
@ -178,3 +214,36 @@ class TapoDevice(SmartDevice):
|
|||||||
"""Update state from info from the discover call."""
|
"""Update state from info from the discover call."""
|
||||||
self._discovery_info = info
|
self._discovery_info = info
|
||||||
self._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)
|
||||||
|
@ -4,10 +4,8 @@ from datetime import datetime, timedelta
|
|||||||
from typing import Any, Dict, Optional, cast
|
from typing import Any, Dict, Optional, cast
|
||||||
|
|
||||||
from ..deviceconfig import DeviceConfig
|
from ..deviceconfig import DeviceConfig
|
||||||
from ..emeterstatus import EmeterStatus
|
|
||||||
from ..modules import Emeter
|
|
||||||
from ..protocol import TPLinkProtocol
|
from ..protocol import TPLinkProtocol
|
||||||
from ..smartdevice import DeviceType, requires_update
|
from ..smartdevice import DeviceType
|
||||||
from .tapodevice import TapoDevice
|
from .tapodevice import TapoDevice
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -25,32 +23,6 @@ class TapoPlug(TapoDevice):
|
|||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(host=host, config=config, protocol=protocol)
|
super().__init__(host=host, config=config, protocol=protocol)
|
||||||
self._device_type = DeviceType.Plug
|
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
|
@property
|
||||||
def state_information(self) -> Dict[str, Any]:
|
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
|
@property
|
||||||
def on_since(self) -> Optional[datetime]:
|
def on_since(self) -> Optional[datetime]:
|
||||||
"""Return the time that the device was turned on or None if turned off."""
|
"""Return the time that the device was turned on or None if turned off."""
|
||||||
@ -100,7 +43,3 @@ class TapoPlug(TapoDevice):
|
|||||||
return None
|
return None
|
||||||
on_time = cast(float, self._info.get("on_time"))
|
on_time = cast(float, self._info.get("on_time"))
|
||||||
return datetime.now().replace(microsecond=0) - timedelta(seconds=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
|
|
||||||
|
@ -7,7 +7,7 @@ import pytest # type: ignore # https://github.com/pytest-dev/pytest/issues/3342
|
|||||||
import kasa
|
import kasa
|
||||||
from kasa import Credentials, DeviceConfig, SmartDevice, SmartDeviceException
|
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
|
from .newfakes import PLUG_SCHEMA, TZ_SCHEMA, FakeTransportProtocol
|
||||||
|
|
||||||
# List of all SmartXXX classes including the SmartDevice base class
|
# List of all SmartXXX classes including the SmartDevice base class
|
||||||
@ -35,7 +35,7 @@ async def test_invalid_connection(dev):
|
|||||||
await dev.update()
|
await dev.update()
|
||||||
|
|
||||||
|
|
||||||
@has_emeter
|
@has_emeter_iot
|
||||||
async def test_initial_update_emeter(dev, mocker):
|
async def test_initial_update_emeter(dev, mocker):
|
||||||
"""Test that the initial update performs second query if emeter is available."""
|
"""Test that the initial update performs second query if emeter is available."""
|
||||||
dev._last_update = None
|
dev._last_update = None
|
||||||
|
Loading…
Reference in New Issue
Block a user