mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 11:13:34 +00:00
Add common energy module and deprecate device emeter attributes (#976)
Consolidates logic for energy monitoring across smart and iot devices. Deprecates emeter attributes in favour of common names.
This commit is contained in:
parent
51a972542f
commit
b4a6df2b5c
@ -19,7 +19,6 @@ from .deviceconfig import (
|
||||
DeviceEncryptionType,
|
||||
DeviceFamily,
|
||||
)
|
||||
from .emeterstatus import EmeterStatus
|
||||
from .exceptions import KasaException
|
||||
from .feature import Feature
|
||||
from .iotprotocol import IotProtocol
|
||||
@ -323,27 +322,6 @@ class Device(ABC):
|
||||
def on_since(self) -> datetime | None:
|
||||
"""Return the time that the device was turned on or None if turned off."""
|
||||
|
||||
@abstractmethod
|
||||
async def get_emeter_realtime(self) -> EmeterStatus:
|
||||
"""Retrieve current energy readings."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def emeter_realtime(self) -> EmeterStatus:
|
||||
"""Get the emeter status."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def emeter_this_month(self) -> float | None:
|
||||
"""Get the emeter value for this month."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def emeter_today(self) -> float | None | Any:
|
||||
"""Get the emeter value for today."""
|
||||
# Return type of Any ensures consumers being shielded from the return
|
||||
# type by @update_required are not affected.
|
||||
|
||||
@abstractmethod
|
||||
async def wifi_scan(self) -> list[WifiNetwork]:
|
||||
"""Scan for available wifi networks."""
|
||||
@ -373,12 +351,15 @@ class Device(ABC):
|
||||
}
|
||||
|
||||
def _get_replacing_attr(self, module_name: ModuleName, *attrs):
|
||||
if module_name not in self.modules:
|
||||
# If module name is None check self
|
||||
if not module_name:
|
||||
check = self
|
||||
elif (check := self.modules.get(module_name)) is None:
|
||||
return None
|
||||
|
||||
for attr in attrs:
|
||||
if hasattr(self.modules[module_name], attr):
|
||||
return getattr(self.modules[module_name], attr)
|
||||
if hasattr(check, attr):
|
||||
return attr
|
||||
|
||||
return None
|
||||
|
||||
@ -411,6 +392,16 @@ class Device(ABC):
|
||||
# light preset attributes
|
||||
"presets": (Module.LightPreset, ["_deprecated_presets", "preset_states_list"]),
|
||||
"save_preset": (Module.LightPreset, ["_deprecated_save_preset"]),
|
||||
# Emeter attribues
|
||||
"get_emeter_realtime": (Module.Energy, ["get_status"]),
|
||||
"emeter_realtime": (Module.Energy, ["status"]),
|
||||
"emeter_today": (Module.Energy, ["consumption_today"]),
|
||||
"emeter_this_month": (Module.Energy, ["consumption_this_month"]),
|
||||
"current_consumption": (Module.Energy, ["current_consumption"]),
|
||||
"get_emeter_daily": (Module.Energy, ["get_daily_stats"]),
|
||||
"get_emeter_monthly": (Module.Energy, ["get_monthly_stats"]),
|
||||
# Other attributes
|
||||
"supported_modules": (None, ["modules"]),
|
||||
}
|
||||
|
||||
def __getattr__(self, name):
|
||||
@ -427,11 +418,10 @@ class Device(ABC):
|
||||
(replacing_attr := self._get_replacing_attr(dep_attr[0], *dep_attr[1]))
|
||||
is not None
|
||||
):
|
||||
module_name = dep_attr[0]
|
||||
msg = (
|
||||
f"{name} is deprecated, use: "
|
||||
+ f"Module.{module_name} in device.modules instead"
|
||||
)
|
||||
mod = dep_attr[0]
|
||||
dev_or_mod = self.modules[mod] if mod else self
|
||||
replacing = f"Module.{mod} in device.modules" if mod else replacing_attr
|
||||
msg = f"{name} is deprecated, use: {replacing} instead"
|
||||
warn(msg, DeprecationWarning, stacklevel=1)
|
||||
return replacing_attr
|
||||
return getattr(dev_or_mod, replacing_attr)
|
||||
raise AttributeError(f"Device has no attribute {name!r}")
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Package for interfaces."""
|
||||
|
||||
from .energy import Energy
|
||||
from .fan import Fan
|
||||
from .led import Led
|
||||
from .light import Light, LightState
|
||||
@ -8,6 +9,7 @@ from .lightpreset import LightPreset
|
||||
|
||||
__all__ = [
|
||||
"Fan",
|
||||
"Energy",
|
||||
"Led",
|
||||
"Light",
|
||||
"LightEffect",
|
||||
|
181
kasa/interfaces/energy.py
Normal file
181
kasa/interfaces/energy.py
Normal file
@ -0,0 +1,181 @@
|
||||
"""Module for base energy module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import IntFlag, auto
|
||||
from warnings import warn
|
||||
|
||||
from ..emeterstatus import EmeterStatus
|
||||
from ..feature import Feature
|
||||
from ..module import Module
|
||||
|
||||
|
||||
class Energy(Module, ABC):
|
||||
"""Base interface to represent an Energy module."""
|
||||
|
||||
class ModuleFeature(IntFlag):
|
||||
"""Features supported by the device."""
|
||||
|
||||
#: Device reports :attr:`voltage` and :attr:`current`
|
||||
VOLTAGE_CURRENT = auto()
|
||||
#: Device reports :attr:`consumption_total`
|
||||
CONSUMPTION_TOTAL = auto()
|
||||
#: Device reports periodic stats via :meth:`get_daily_stats`
|
||||
#: and :meth:`get_monthly_stats`
|
||||
PERIODIC_STATS = auto()
|
||||
|
||||
_supported: ModuleFeature = ModuleFeature(0)
|
||||
|
||||
def supports(self, module_feature: ModuleFeature) -> bool:
|
||||
"""Return True if module supports the feature."""
|
||||
return module_feature in self._supported
|
||||
|
||||
def _initialize_features(self):
|
||||
"""Initialize features."""
|
||||
device = self._device
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
name="Current consumption",
|
||||
attribute_getter="current_consumption",
|
||||
container=self,
|
||||
unit="W",
|
||||
id="current_consumption",
|
||||
precision_hint=1,
|
||||
category=Feature.Category.Primary,
|
||||
)
|
||||
)
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
name="Today's consumption",
|
||||
attribute_getter="consumption_today",
|
||||
container=self,
|
||||
unit="kWh",
|
||||
id="consumption_today",
|
||||
precision_hint=3,
|
||||
category=Feature.Category.Info,
|
||||
)
|
||||
)
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
id="consumption_this_month",
|
||||
name="This month's consumption",
|
||||
attribute_getter="consumption_this_month",
|
||||
container=self,
|
||||
unit="kWh",
|
||||
precision_hint=3,
|
||||
category=Feature.Category.Info,
|
||||
)
|
||||
)
|
||||
if self.supports(self.ModuleFeature.CONSUMPTION_TOTAL):
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
name="Total consumption since reboot",
|
||||
attribute_getter="consumption_total",
|
||||
container=self,
|
||||
unit="kWh",
|
||||
id="consumption_total",
|
||||
precision_hint=3,
|
||||
category=Feature.Category.Info,
|
||||
)
|
||||
)
|
||||
if self.supports(self.ModuleFeature.VOLTAGE_CURRENT):
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
name="Voltage",
|
||||
attribute_getter="voltage",
|
||||
container=self,
|
||||
unit="V",
|
||||
id="voltage",
|
||||
precision_hint=1,
|
||||
category=Feature.Category.Primary,
|
||||
)
|
||||
)
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
name="Current",
|
||||
attribute_getter="current",
|
||||
container=self,
|
||||
unit="A",
|
||||
id="current",
|
||||
precision_hint=2,
|
||||
category=Feature.Category.Primary,
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def status(self) -> EmeterStatus:
|
||||
"""Return current energy readings."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def current_consumption(self) -> float | None:
|
||||
"""Get the current power consumption in Watt."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def consumption_today(self) -> float | None:
|
||||
"""Return today's energy consumption in kWh."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def consumption_this_month(self) -> float | None:
|
||||
"""Return this month's energy consumption in kWh."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def consumption_total(self) -> float | None:
|
||||
"""Return total consumption since last reboot in kWh."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def current(self) -> float | None:
|
||||
"""Return the current in A."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def voltage(self) -> float | None:
|
||||
"""Get the current voltage in V."""
|
||||
|
||||
@abstractmethod
|
||||
async def get_status(self):
|
||||
"""Return real-time statistics."""
|
||||
|
||||
@abstractmethod
|
||||
async def erase_stats(self):
|
||||
"""Erase all stats."""
|
||||
|
||||
@abstractmethod
|
||||
async def get_daily_stats(self, *, year=None, month=None, kwh=True) -> dict:
|
||||
"""Return daily stats for the given year & month.
|
||||
|
||||
The return value is a dictionary of {day: energy, ...}.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def get_monthly_stats(self, *, year=None, kwh=True) -> dict:
|
||||
"""Return monthly stats for the given year."""
|
||||
|
||||
_deprecated_attributes = {
|
||||
"emeter_today": "consumption_today",
|
||||
"emeter_this_month": "consumption_this_month",
|
||||
"realtime": "status",
|
||||
"get_realtime": "get_status",
|
||||
"erase_emeter_stats": "erase_stats",
|
||||
"get_daystat": "get_daily_stats",
|
||||
"get_monthstat": "get_monthly_stats",
|
||||
}
|
||||
|
||||
def __getattr__(self, name):
|
||||
if attr := self._deprecated_attributes.get(name):
|
||||
msg = f"{name} is deprecated, use {attr} instead"
|
||||
warn(msg, DeprecationWarning, stacklevel=1)
|
||||
return getattr(self, attr)
|
||||
raise AttributeError(f"Energy module has no attribute {name!r}")
|
@ -220,7 +220,7 @@ class IotBulb(IotDevice):
|
||||
Module.IotAntitheft, Antitheft(self, "smartlife.iot.common.anti_theft")
|
||||
)
|
||||
self.add_module(Module.IotTime, Time(self, "smartlife.iot.common.timesetting"))
|
||||
self.add_module(Module.IotEmeter, Emeter(self, self.emeter_type))
|
||||
self.add_module(Module.Energy, Emeter(self, self.emeter_type))
|
||||
self.add_module(Module.IotCountdown, Countdown(self, "countdown"))
|
||||
self.add_module(Module.IotCloud, Cloud(self, "smartlife.iot.common.cloud"))
|
||||
self.add_module(Module.Light, Light(self, self.LIGHT_SERVICE))
|
||||
|
@ -23,7 +23,6 @@ from typing import TYPE_CHECKING, Any, Mapping, Sequence, cast
|
||||
|
||||
from ..device import Device, WifiNetwork
|
||||
from ..deviceconfig import DeviceConfig
|
||||
from ..emeterstatus import EmeterStatus
|
||||
from ..exceptions import KasaException
|
||||
from ..feature import Feature
|
||||
from ..module import Module
|
||||
@ -188,7 +187,7 @@ class IotDevice(Device):
|
||||
super().__init__(host=host, config=config, protocol=protocol)
|
||||
|
||||
self._sys_info: Any = None # TODO: this is here to avoid changing tests
|
||||
self._supported_modules: dict[str, IotModule] | None = None
|
||||
self._supported_modules: dict[str | ModuleName[Module], IotModule] | None = None
|
||||
self._legacy_features: set[str] = set()
|
||||
self._children: Mapping[str, IotDevice] = {}
|
||||
self._modules: dict[str | ModuleName[Module], IotModule] = {}
|
||||
@ -199,15 +198,16 @@ class IotDevice(Device):
|
||||
return list(self._children.values())
|
||||
|
||||
@property
|
||||
@requires_update
|
||||
def modules(self) -> ModuleMapping[IotModule]:
|
||||
"""Return the device modules."""
|
||||
if TYPE_CHECKING:
|
||||
return cast(ModuleMapping[IotModule], self._modules)
|
||||
return self._modules
|
||||
return cast(ModuleMapping[IotModule], self._supported_modules)
|
||||
return self._supported_modules
|
||||
|
||||
def add_module(self, name: str | ModuleName[Module], module: IotModule):
|
||||
"""Register a module."""
|
||||
if name in self.modules:
|
||||
if name in self._modules:
|
||||
_LOGGER.debug("Module %s already registered, ignoring..." % name)
|
||||
return
|
||||
|
||||
@ -272,14 +272,6 @@ class IotDevice(Device):
|
||||
"""Return a set of features that the device supports."""
|
||||
return self._features
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
def supported_modules(self) -> list[str | ModuleName[Module]]:
|
||||
"""Return a set of modules supported by the device."""
|
||||
# TODO: this should rather be called `features`, but we don't want to break
|
||||
# the API now. Maybe just deprecate it and point the users to use this?
|
||||
return list(self._modules.keys())
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
def has_emeter(self) -> bool:
|
||||
@ -321,6 +313,11 @@ class IotDevice(Device):
|
||||
|
||||
async def _initialize_modules(self):
|
||||
"""Initialize modules not added in init."""
|
||||
if self.has_emeter:
|
||||
_LOGGER.debug(
|
||||
"The device has emeter, querying its information along sysinfo"
|
||||
)
|
||||
self.add_module(Module.Energy, Emeter(self, self.emeter_type))
|
||||
|
||||
async def _initialize_features(self):
|
||||
"""Initialize common features."""
|
||||
@ -357,29 +354,13 @@ class IotDevice(Device):
|
||||
)
|
||||
)
|
||||
|
||||
for module in self._modules.values():
|
||||
for module in self._supported_modules.values():
|
||||
module._initialize_features()
|
||||
for module_feat in module._module_features.values():
|
||||
self._add_feature(module_feat)
|
||||
|
||||
async def _modular_update(self, req: dict) -> None:
|
||||
"""Execute an update query."""
|
||||
if self.has_emeter:
|
||||
_LOGGER.debug(
|
||||
"The device has emeter, querying its information along sysinfo"
|
||||
)
|
||||
self.add_module(Module.IotEmeter, Emeter(self, self.emeter_type))
|
||||
|
||||
# TODO: perhaps modules should not have unsupported modules,
|
||||
# making separate handling for this unnecessary
|
||||
if self._supported_modules is None:
|
||||
supported = {}
|
||||
for module in self._modules.values():
|
||||
if module.is_supported:
|
||||
supported[module._module] = module
|
||||
|
||||
self._supported_modules = supported
|
||||
|
||||
request_list = []
|
||||
est_response_size = 1024 if "system" in req else 0
|
||||
for module in self._modules.values():
|
||||
@ -411,6 +392,15 @@ class IotDevice(Device):
|
||||
update = {**update, **response}
|
||||
self._last_update = update
|
||||
|
||||
# IOT modules are added as default but could be unsupported post first update
|
||||
if self._supported_modules is None:
|
||||
supported = {}
|
||||
for module_name, module in self._modules.items():
|
||||
if module.is_supported:
|
||||
supported[module_name] = module
|
||||
|
||||
self._supported_modules = supported
|
||||
|
||||
def update_from_discover_info(self, info: dict[str, Any]) -> None:
|
||||
"""Update state from info from the discover call."""
|
||||
self._discovery_info = info
|
||||
@ -557,74 +547,6 @@ class IotDevice(Device):
|
||||
"""
|
||||
return await self._query_helper("system", "set_mac_addr", {"mac": mac})
|
||||
|
||||
@property
|
||||
@requires_update
|
||||
def emeter_realtime(self) -> EmeterStatus:
|
||||
"""Return current energy readings."""
|
||||
self._verify_emeter()
|
||||
return EmeterStatus(self.modules[Module.IotEmeter].realtime)
|
||||
|
||||
async def get_emeter_realtime(self) -> EmeterStatus:
|
||||
"""Retrieve current energy readings."""
|
||||
self._verify_emeter()
|
||||
return EmeterStatus(await self.modules[Module.IotEmeter].get_realtime())
|
||||
|
||||
@property
|
||||
@requires_update
|
||||
def emeter_today(self) -> float | None:
|
||||
"""Return today's energy consumption in kWh."""
|
||||
self._verify_emeter()
|
||||
return self.modules[Module.IotEmeter].emeter_today
|
||||
|
||||
@property
|
||||
@requires_update
|
||||
def emeter_this_month(self) -> float | None:
|
||||
"""Return this month's energy consumption in kWh."""
|
||||
self._verify_emeter()
|
||||
return self.modules[Module.IotEmeter].emeter_this_month
|
||||
|
||||
async def get_emeter_daily(
|
||||
self, year: int | None = None, month: int | None = None, kwh: bool = True
|
||||
) -> dict:
|
||||
"""Retrieve daily statistics for a given month.
|
||||
|
||||
:param year: year for which to retrieve statistics (default: this year)
|
||||
:param month: month for which to retrieve statistics (default: this
|
||||
month)
|
||||
:param kwh: return usage in kWh (default: True)
|
||||
:return: mapping of day of month to value
|
||||
"""
|
||||
self._verify_emeter()
|
||||
return await self.modules[Module.IotEmeter].get_daystat(
|
||||
year=year, month=month, kwh=kwh
|
||||
)
|
||||
|
||||
@requires_update
|
||||
async def get_emeter_monthly(
|
||||
self, year: int | None = None, kwh: bool = True
|
||||
) -> dict:
|
||||
"""Retrieve monthly statistics for a given year.
|
||||
|
||||
:param year: year for which to retrieve statistics (default: this year)
|
||||
:param kwh: return usage in kWh (default: True)
|
||||
:return: dict: mapping of month to value
|
||||
"""
|
||||
self._verify_emeter()
|
||||
return await self.modules[Module.IotEmeter].get_monthstat(year=year, kwh=kwh)
|
||||
|
||||
@requires_update
|
||||
async def erase_emeter_stats(self) -> dict:
|
||||
"""Erase energy meter statistics."""
|
||||
self._verify_emeter()
|
||||
return await self.modules[Module.IotEmeter].erase_stats()
|
||||
|
||||
@requires_update
|
||||
async def current_consumption(self) -> float:
|
||||
"""Get the current power consumption in Watt."""
|
||||
self._verify_emeter()
|
||||
response = self.emeter_realtime
|
||||
return float(response["power"])
|
||||
|
||||
async def reboot(self, delay: int = 1) -> None:
|
||||
"""Reboot the device.
|
||||
|
||||
|
@ -9,16 +9,17 @@ from typing import Any
|
||||
|
||||
from ..device_type import DeviceType
|
||||
from ..deviceconfig import DeviceConfig
|
||||
from ..emeterstatus import EmeterStatus
|
||||
from ..exceptions import KasaException
|
||||
from ..feature import Feature
|
||||
from ..interfaces import Energy
|
||||
from ..module import Module
|
||||
from ..protocol import BaseProtocol
|
||||
from .iotdevice import (
|
||||
EmeterStatus,
|
||||
IotDevice,
|
||||
merge,
|
||||
requires_update,
|
||||
)
|
||||
from .iotmodule import IotModule
|
||||
from .iotplug import IotPlug
|
||||
from .modules import Antitheft, Countdown, Schedule, Time, Usage
|
||||
|
||||
@ -97,11 +98,20 @@ class IotStrip(IotDevice):
|
||||
super().__init__(host=host, config=config, protocol=protocol)
|
||||
self.emeter_type = "emeter"
|
||||
self._device_type = DeviceType.Strip
|
||||
|
||||
async def _initialize_modules(self):
|
||||
"""Initialize modules."""
|
||||
# Strip has different modules to plug so do not call super
|
||||
self.add_module(Module.IotAntitheft, Antitheft(self, "anti_theft"))
|
||||
self.add_module(Module.IotSchedule, Schedule(self, "schedule"))
|
||||
self.add_module(Module.IotUsage, Usage(self, "schedule"))
|
||||
self.add_module(Module.IotTime, Time(self, "time"))
|
||||
self.add_module(Module.IotCountdown, Countdown(self, "countdown"))
|
||||
if self.has_emeter:
|
||||
_LOGGER.debug(
|
||||
"The device has emeter, querying its information along sysinfo"
|
||||
)
|
||||
self.add_module(Module.Energy, StripEmeter(self, self.emeter_type))
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
@ -114,10 +124,12 @@ class IotStrip(IotDevice):
|
||||
|
||||
Needed for methods that are decorated with `requires_update`.
|
||||
"""
|
||||
# Super initializes modules and features
|
||||
await super().update(update_children)
|
||||
|
||||
initialize_children = not self.children
|
||||
# Initialize the child devices during the first update.
|
||||
if not self.children:
|
||||
if initialize_children:
|
||||
children = self.sys_info["children"]
|
||||
_LOGGER.debug("Initializing %s child sockets", len(children))
|
||||
self._children = {
|
||||
@ -127,12 +139,22 @@ class IotStrip(IotDevice):
|
||||
for child in children
|
||||
}
|
||||
for child in self._children.values():
|
||||
await child._initialize_features()
|
||||
await child._initialize_modules()
|
||||
|
||||
if update_children and self.has_emeter:
|
||||
if update_children:
|
||||
for plug in self.children:
|
||||
await plug.update()
|
||||
|
||||
if not self.features:
|
||||
await self._initialize_features()
|
||||
|
||||
async def _initialize_features(self):
|
||||
"""Initialize common features."""
|
||||
# Do not initialize features until children are created
|
||||
if not self.children:
|
||||
return
|
||||
await super()._initialize_features()
|
||||
|
||||
async def turn_on(self, **kwargs):
|
||||
"""Turn the strip on."""
|
||||
await self._query_helper("system", "set_relay_state", {"state": 1})
|
||||
@ -150,21 +172,43 @@ class IotStrip(IotDevice):
|
||||
|
||||
return min(plug.on_since for plug in self.children if plug.on_since is not None)
|
||||
|
||||
async def current_consumption(self) -> float:
|
||||
"""Get the current power consumption in watts."""
|
||||
return sum([await plug.current_consumption() for plug in self.children])
|
||||
|
||||
@requires_update
|
||||
async def get_emeter_realtime(self) -> EmeterStatus:
|
||||
class StripEmeter(IotModule, Energy):
|
||||
"""Energy module implementation to aggregate child modules."""
|
||||
|
||||
_supported = (
|
||||
Energy.ModuleFeature.CONSUMPTION_TOTAL
|
||||
| Energy.ModuleFeature.PERIODIC_STATS
|
||||
| Energy.ModuleFeature.VOLTAGE_CURRENT
|
||||
)
|
||||
|
||||
def supports(self, module_feature: Energy.ModuleFeature) -> bool:
|
||||
"""Return True if module supports the feature."""
|
||||
return module_feature in self._supported
|
||||
|
||||
def query(self):
|
||||
"""Return the base query."""
|
||||
return {}
|
||||
|
||||
@property
|
||||
def current_consumption(self) -> float | None:
|
||||
"""Get the current power consumption in watts."""
|
||||
return sum(
|
||||
v if (v := plug.modules[Module.Energy].current_consumption) else 0.0
|
||||
for plug in self._device.children
|
||||
)
|
||||
|
||||
async def get_status(self) -> EmeterStatus:
|
||||
"""Retrieve current energy readings."""
|
||||
emeter_rt = await self._async_get_emeter_sum("get_emeter_realtime", {})
|
||||
emeter_rt = await self._async_get_emeter_sum("get_status", {})
|
||||
# Voltage is averaged since each read will result
|
||||
# in a slightly different voltage since they are not atomic
|
||||
emeter_rt["voltage_mv"] = int(emeter_rt["voltage_mv"] / len(self.children))
|
||||
emeter_rt["voltage_mv"] = int(
|
||||
emeter_rt["voltage_mv"] / len(self._device.children)
|
||||
)
|
||||
return EmeterStatus(emeter_rt)
|
||||
|
||||
@requires_update
|
||||
async def get_emeter_daily(
|
||||
async def get_daily_stats(
|
||||
self, year: int | None = None, month: int | None = None, kwh: bool = True
|
||||
) -> dict:
|
||||
"""Retrieve daily statistics for a given month.
|
||||
@ -176,11 +220,10 @@ class IotStrip(IotDevice):
|
||||
:return: mapping of day of month to value
|
||||
"""
|
||||
return await self._async_get_emeter_sum(
|
||||
"get_emeter_daily", {"year": year, "month": month, "kwh": kwh}
|
||||
"get_daily_stats", {"year": year, "month": month, "kwh": kwh}
|
||||
)
|
||||
|
||||
@requires_update
|
||||
async def get_emeter_monthly(
|
||||
async def get_monthly_stats(
|
||||
self, year: int | None = None, kwh: bool = True
|
||||
) -> dict:
|
||||
"""Retrieve monthly statistics for a given year.
|
||||
@ -189,44 +232,68 @@ class IotStrip(IotDevice):
|
||||
:param kwh: return usage in kWh (default: True)
|
||||
"""
|
||||
return await self._async_get_emeter_sum(
|
||||
"get_emeter_monthly", {"year": year, "kwh": kwh}
|
||||
"get_monthly_stats", {"year": year, "kwh": kwh}
|
||||
)
|
||||
|
||||
async def _async_get_emeter_sum(self, func: str, kwargs: dict[str, Any]) -> dict:
|
||||
"""Retreive emeter stats for a time period from children."""
|
||||
self._verify_emeter()
|
||||
"""Retrieve emeter stats for a time period from children."""
|
||||
return merge_sums(
|
||||
[await getattr(plug, func)(**kwargs) for plug in self.children]
|
||||
[
|
||||
await getattr(plug.modules[Module.Energy], func)(**kwargs)
|
||||
for plug in self._device.children
|
||||
]
|
||||
)
|
||||
|
||||
@requires_update
|
||||
async def erase_emeter_stats(self):
|
||||
async def erase_stats(self):
|
||||
"""Erase energy meter statistics for all plugs."""
|
||||
for plug in self.children:
|
||||
await plug.erase_emeter_stats()
|
||||
for plug in self._device.children:
|
||||
await plug.modules[Module.Energy].erase_stats()
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
def emeter_this_month(self) -> float | None:
|
||||
def consumption_this_month(self) -> float | None:
|
||||
"""Return this month's energy consumption in kWh."""
|
||||
return sum(v if (v := plug.emeter_this_month) else 0 for plug in self.children)
|
||||
return sum(
|
||||
v if (v := plug.modules[Module.Energy].consumption_this_month) else 0.0
|
||||
for plug in self._device.children
|
||||
)
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
def emeter_today(self) -> float | None:
|
||||
def consumption_today(self) -> float | None:
|
||||
"""Return this month's energy consumption in kWh."""
|
||||
return sum(v if (v := plug.emeter_today) else 0 for plug in self.children)
|
||||
return sum(
|
||||
v if (v := plug.modules[Module.Energy].consumption_today) else 0.0
|
||||
for plug in self._device.children
|
||||
)
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
def emeter_realtime(self) -> EmeterStatus:
|
||||
def consumption_total(self) -> float | None:
|
||||
"""Return total energy consumption since reboot in kWh."""
|
||||
return sum(
|
||||
v if (v := plug.modules[Module.Energy].consumption_total) else 0.0
|
||||
for plug in self._device.children
|
||||
)
|
||||
|
||||
@property # type: ignore
|
||||
def status(self) -> EmeterStatus:
|
||||
"""Return current energy readings."""
|
||||
emeter = merge_sums([plug.emeter_realtime for plug in self.children])
|
||||
emeter = merge_sums(
|
||||
[plug.modules[Module.Energy].status for plug in self._device.children]
|
||||
)
|
||||
# Voltage is averaged since each read will result
|
||||
# in a slightly different voltage since they are not atomic
|
||||
emeter["voltage_mv"] = int(emeter["voltage_mv"] / len(self.children))
|
||||
emeter["voltage_mv"] = int(emeter["voltage_mv"] / len(self._device.children))
|
||||
return EmeterStatus(emeter)
|
||||
|
||||
@property
|
||||
def current(self) -> float | None:
|
||||
"""Return the current in A."""
|
||||
return self.status.current
|
||||
|
||||
@property
|
||||
def voltage(self) -> float | None:
|
||||
"""Get the current voltage in V."""
|
||||
return self.status.voltage
|
||||
|
||||
|
||||
class IotStripPlug(IotPlug):
|
||||
"""Representation of a single socket in a power strip.
|
||||
@ -275,9 +342,10 @@ class IotStripPlug(IotPlug):
|
||||
icon="mdi:clock",
|
||||
)
|
||||
)
|
||||
# If the strip plug has it's own modules we should call initialize
|
||||
# features for the modules here. However the _initialize_modules function
|
||||
# above does not seem to be called.
|
||||
for module in self._supported_modules.values():
|
||||
module._initialize_features()
|
||||
for module_feat in module._module_features.values():
|
||||
self._add_feature(module_feat)
|
||||
|
||||
async def update(self, update_children: bool = True):
|
||||
"""Query the device to update the data.
|
||||
@ -285,26 +353,8 @@ class IotStripPlug(IotPlug):
|
||||
Needed for properties that are decorated with `requires_update`.
|
||||
"""
|
||||
await self._modular_update({})
|
||||
|
||||
def _create_emeter_request(self, year: int | None = None, month: int | None = None):
|
||||
"""Create a request for requesting all emeter statistics at once."""
|
||||
if year is None:
|
||||
year = datetime.now().year
|
||||
if month is None:
|
||||
month = datetime.now().month
|
||||
|
||||
req: dict[str, Any] = {}
|
||||
|
||||
merge(req, self._create_request("emeter", "get_realtime"))
|
||||
merge(req, self._create_request("emeter", "get_monthstat", {"year": year}))
|
||||
merge(
|
||||
req,
|
||||
self._create_request(
|
||||
"emeter", "get_daystat", {"month": month, "year": year}
|
||||
),
|
||||
)
|
||||
|
||||
return req
|
||||
if not self._features:
|
||||
await self._initialize_features()
|
||||
|
||||
def _create_request(
|
||||
self, target: str, cmd: str, arg: dict | None = None, child_ids=None
|
||||
|
@ -4,130 +4,71 @@ from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from ... import Device
|
||||
from ...emeterstatus import EmeterStatus
|
||||
from ...feature import Feature
|
||||
from ...interfaces.energy import Energy as EnergyInterface
|
||||
from .usage import Usage
|
||||
|
||||
|
||||
class Emeter(Usage):
|
||||
class Emeter(Usage, EnergyInterface):
|
||||
"""Emeter module."""
|
||||
|
||||
def __init__(self, device: Device, module: str):
|
||||
super().__init__(device, module)
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
name="Current consumption",
|
||||
attribute_getter="current_consumption",
|
||||
container=self,
|
||||
unit="W",
|
||||
id="current_power_w", # for homeassistant backwards compat
|
||||
precision_hint=1,
|
||||
category=Feature.Category.Primary,
|
||||
def _post_update_hook(self) -> None:
|
||||
self._supported = EnergyInterface.ModuleFeature.PERIODIC_STATS
|
||||
if (
|
||||
"voltage_mv" in self.data["get_realtime"]
|
||||
or "voltage" in self.data["get_realtime"]
|
||||
):
|
||||
self._supported = (
|
||||
self._supported | EnergyInterface.ModuleFeature.VOLTAGE_CURRENT
|
||||
)
|
||||
)
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
name="Today's consumption",
|
||||
attribute_getter="emeter_today",
|
||||
container=self,
|
||||
unit="kWh",
|
||||
id="today_energy_kwh", # for homeassistant backwards compat
|
||||
precision_hint=3,
|
||||
category=Feature.Category.Info,
|
||||
if (
|
||||
"total_wh" in self.data["get_realtime"]
|
||||
or "total" in self.data["get_realtime"]
|
||||
):
|
||||
self._supported = (
|
||||
self._supported | EnergyInterface.ModuleFeature.CONSUMPTION_TOTAL
|
||||
)
|
||||
)
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
id="consumption_this_month",
|
||||
name="This month's consumption",
|
||||
attribute_getter="emeter_this_month",
|
||||
container=self,
|
||||
unit="kWh",
|
||||
precision_hint=3,
|
||||
category=Feature.Category.Info,
|
||||
)
|
||||
)
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
name="Total consumption since reboot",
|
||||
attribute_getter="emeter_total",
|
||||
container=self,
|
||||
unit="kWh",
|
||||
id="total_energy_kwh", # for homeassistant backwards compat
|
||||
precision_hint=3,
|
||||
category=Feature.Category.Info,
|
||||
)
|
||||
)
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
name="Voltage",
|
||||
attribute_getter="voltage",
|
||||
container=self,
|
||||
unit="V",
|
||||
id="voltage", # for homeassistant backwards compat
|
||||
precision_hint=1,
|
||||
category=Feature.Category.Primary,
|
||||
)
|
||||
)
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
name="Current",
|
||||
attribute_getter="current",
|
||||
container=self,
|
||||
unit="A",
|
||||
id="current_a", # for homeassistant backwards compat
|
||||
precision_hint=2,
|
||||
category=Feature.Category.Primary,
|
||||
)
|
||||
)
|
||||
|
||||
@property # type: ignore
|
||||
def realtime(self) -> EmeterStatus:
|
||||
def status(self) -> EmeterStatus:
|
||||
"""Return current energy readings."""
|
||||
return EmeterStatus(self.data["get_realtime"])
|
||||
|
||||
@property
|
||||
def emeter_today(self) -> float | None:
|
||||
def consumption_today(self) -> float | None:
|
||||
"""Return today's energy consumption in kWh."""
|
||||
raw_data = self.daily_data
|
||||
today = datetime.now().day
|
||||
data = self._convert_stat_data(raw_data, entry_key="day", key=today)
|
||||
return data.get(today)
|
||||
return data.get(today, 0.0)
|
||||
|
||||
@property
|
||||
def emeter_this_month(self) -> float | None:
|
||||
def consumption_this_month(self) -> float | None:
|
||||
"""Return this month's energy consumption in kWh."""
|
||||
raw_data = self.monthly_data
|
||||
current_month = datetime.now().month
|
||||
data = self._convert_stat_data(raw_data, entry_key="month", key=current_month)
|
||||
return data.get(current_month)
|
||||
return data.get(current_month, 0.0)
|
||||
|
||||
@property
|
||||
def current_consumption(self) -> float | None:
|
||||
"""Get the current power consumption in Watt."""
|
||||
return self.realtime.power
|
||||
return self.status.power
|
||||
|
||||
@property
|
||||
def emeter_total(self) -> float | None:
|
||||
def consumption_total(self) -> float | None:
|
||||
"""Return total consumption since last reboot in kWh."""
|
||||
return self.realtime.total
|
||||
return self.status.total
|
||||
|
||||
@property
|
||||
def current(self) -> float | None:
|
||||
"""Return the current in A."""
|
||||
return self.realtime.current
|
||||
return self.status.current
|
||||
|
||||
@property
|
||||
def voltage(self) -> float | None:
|
||||
"""Get the current voltage in V."""
|
||||
return self.realtime.voltage
|
||||
return self.status.voltage
|
||||
|
||||
async def erase_stats(self):
|
||||
"""Erase all stats.
|
||||
@ -136,11 +77,11 @@ class Emeter(Usage):
|
||||
"""
|
||||
return await self.call("erase_emeter_stat")
|
||||
|
||||
async def get_realtime(self):
|
||||
async def get_status(self) -> EmeterStatus:
|
||||
"""Return real-time statistics."""
|
||||
return await self.call("get_realtime")
|
||||
return EmeterStatus(await self.call("get_realtime"))
|
||||
|
||||
async def get_daystat(self, *, year=None, month=None, kwh=True) -> dict:
|
||||
async def get_daily_stats(self, *, year=None, month=None, kwh=True) -> dict:
|
||||
"""Return daily stats for the given year & month.
|
||||
|
||||
The return value is a dictionary of {day: energy, ...}.
|
||||
@ -149,7 +90,7 @@ class Emeter(Usage):
|
||||
data = self._convert_stat_data(data["day_list"], entry_key="day", kwh=kwh)
|
||||
return data
|
||||
|
||||
async def get_monthstat(self, *, year=None, kwh=True) -> dict:
|
||||
async def get_monthly_stats(self, *, year=None, kwh=True) -> dict:
|
||||
"""Return monthly stats for the given year.
|
||||
|
||||
The return value is a dictionary of {month: energy, ...}.
|
||||
|
@ -30,3 +30,8 @@ class Led(IotModule, LedInterface):
|
||||
async def set_led(self, state: bool):
|
||||
"""Set the state of the led (night mode)."""
|
||||
return await self.call("set_led_off", {"off": int(not state)})
|
||||
|
||||
@property
|
||||
def is_supported(self) -> bool:
|
||||
"""Return whether the module is supported by the device."""
|
||||
return "led_off" in self.data
|
||||
|
@ -33,6 +33,8 @@ class Module(ABC):
|
||||
"""
|
||||
|
||||
# Common Modules
|
||||
Energy: Final[ModuleName[interfaces.Energy]] = ModuleName("Energy")
|
||||
Fan: Final[ModuleName[interfaces.Fan]] = ModuleName("Fan")
|
||||
LightEffect: Final[ModuleName[interfaces.LightEffect]] = ModuleName("LightEffect")
|
||||
Led: Final[ModuleName[interfaces.Led]] = ModuleName("Led")
|
||||
Light: Final[ModuleName[interfaces.Light]] = ModuleName("Light")
|
||||
@ -42,7 +44,6 @@ class Module(ABC):
|
||||
IotAmbientLight: Final[ModuleName[iot.AmbientLight]] = ModuleName("ambient")
|
||||
IotAntitheft: Final[ModuleName[iot.Antitheft]] = ModuleName("anti_theft")
|
||||
IotCountdown: Final[ModuleName[iot.Countdown]] = ModuleName("countdown")
|
||||
IotEmeter: Final[ModuleName[iot.Emeter]] = ModuleName("emeter")
|
||||
IotMotion: Final[ModuleName[iot.Motion]] = ModuleName("motion")
|
||||
IotSchedule: Final[ModuleName[iot.Schedule]] = ModuleName("schedule")
|
||||
IotUsage: Final[ModuleName[iot.Usage]] = ModuleName("usage")
|
||||
@ -62,8 +63,6 @@ class Module(ABC):
|
||||
)
|
||||
ContactSensor: Final[ModuleName[smart.ContactSensor]] = ModuleName("ContactSensor")
|
||||
DeviceModule: Final[ModuleName[smart.DeviceModule]] = ModuleName("DeviceModule")
|
||||
Energy: Final[ModuleName[smart.Energy]] = ModuleName("Energy")
|
||||
Fan: Final[ModuleName[smart.Fan]] = ModuleName("Fan")
|
||||
Firmware: Final[ModuleName[smart.Firmware]] = ModuleName("Firmware")
|
||||
FrostProtection: Final[ModuleName[smart.FrostProtection]] = ModuleName(
|
||||
"FrostProtection"
|
||||
|
@ -2,60 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ...emeterstatus import EmeterStatus
|
||||
from ...feature import Feature
|
||||
from ...exceptions import KasaException
|
||||
from ...interfaces.energy import Energy as EnergyInterface
|
||||
from ..smartmodule import SmartModule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..smartdevice import SmartDevice
|
||||
|
||||
|
||||
class Energy(SmartModule):
|
||||
class Energy(SmartModule, EnergyInterface):
|
||||
"""Implementation of energy monitoring module."""
|
||||
|
||||
REQUIRED_COMPONENT = "energy_monitoring"
|
||||
|
||||
def __init__(self, device: SmartDevice, module: str):
|
||||
super().__init__(device, module)
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
"consumption_current",
|
||||
name="Current consumption",
|
||||
attribute_getter="current_power",
|
||||
container=self,
|
||||
unit="W",
|
||||
precision_hint=1,
|
||||
category=Feature.Category.Primary,
|
||||
)
|
||||
)
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
"consumption_today",
|
||||
name="Today's consumption",
|
||||
attribute_getter="emeter_today",
|
||||
container=self,
|
||||
unit="Wh",
|
||||
precision_hint=2,
|
||||
category=Feature.Category.Info,
|
||||
)
|
||||
)
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
"consumption_this_month",
|
||||
name="This month's consumption",
|
||||
attribute_getter="emeter_this_month",
|
||||
container=self,
|
||||
unit="Wh",
|
||||
precision_hint=2,
|
||||
category=Feature.Category.Info,
|
||||
)
|
||||
)
|
||||
|
||||
def query(self) -> dict:
|
||||
"""Query to execute during the update cycle."""
|
||||
req = {
|
||||
@ -66,9 +23,9 @@ class Energy(SmartModule):
|
||||
return req
|
||||
|
||||
@property
|
||||
def current_power(self) -> float | None:
|
||||
def current_consumption(self) -> float | None:
|
||||
"""Current power in watts."""
|
||||
if power := self.energy.get("current_power"):
|
||||
if (power := self.energy.get("current_power")) is not None:
|
||||
return power / 1_000
|
||||
return None
|
||||
|
||||
@ -79,23 +36,64 @@ class Energy(SmartModule):
|
||||
return en
|
||||
return self.data
|
||||
|
||||
@property
|
||||
def emeter_realtime(self):
|
||||
"""Get the emeter status."""
|
||||
# TODO: Perhaps we should get rid of emeterstatus altogether for smartdevices
|
||||
def _get_status_from_energy(self, energy) -> EmeterStatus:
|
||||
return EmeterStatus(
|
||||
{
|
||||
"power_mw": self.energy.get("current_power"),
|
||||
"total": self.energy.get("today_energy") / 1_000,
|
||||
"power_mw": energy.get("current_power"),
|
||||
"total": energy.get("today_energy") / 1_000,
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def emeter_this_month(self) -> float | None:
|
||||
"""Get the emeter value for this month."""
|
||||
return self.energy.get("month_energy")
|
||||
def status(self):
|
||||
"""Get the emeter status."""
|
||||
return self._get_status_from_energy(self.energy)
|
||||
|
||||
async def get_status(self):
|
||||
"""Return real-time statistics."""
|
||||
res = await self.call("get_energy_usage")
|
||||
return self._get_status_from_energy(res["get_energy_usage"])
|
||||
|
||||
@property
|
||||
def emeter_today(self) -> float | None:
|
||||
"""Get the emeter value for today."""
|
||||
return self.energy.get("today_energy")
|
||||
def consumption_this_month(self) -> float | None:
|
||||
"""Get the emeter value for this month in kWh."""
|
||||
return self.energy.get("month_energy") / 1_000
|
||||
|
||||
@property
|
||||
def consumption_today(self) -> float | None:
|
||||
"""Get the emeter value for today in kWh."""
|
||||
return self.energy.get("today_energy") / 1_000
|
||||
|
||||
@property
|
||||
def consumption_total(self) -> float | None:
|
||||
"""Return total consumption since last reboot in kWh."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def current(self) -> float | None:
|
||||
"""Return the current in A."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def voltage(self) -> float | None:
|
||||
"""Get the current voltage in V."""
|
||||
return None
|
||||
|
||||
async def _deprecated_get_realtime(self) -> EmeterStatus:
|
||||
"""Retrieve current energy readings."""
|
||||
return self.status
|
||||
|
||||
async def erase_stats(self):
|
||||
"""Erase all stats."""
|
||||
raise KasaException("Device does not support periodic statistics")
|
||||
|
||||
async def get_daily_stats(self, *, year=None, month=None, kwh=True) -> dict:
|
||||
"""Return daily stats for the given year & month.
|
||||
|
||||
The return value is a dictionary of {day: energy, ...}.
|
||||
"""
|
||||
raise KasaException("Device does not support periodic statistics")
|
||||
|
||||
async def get_monthly_stats(self, *, year=None, kwh=True) -> dict:
|
||||
"""Return monthly stats for the given year."""
|
||||
raise KasaException("Device does not support periodic statistics")
|
||||
|
@ -11,7 +11,6 @@ from ..aestransport import AesTransport
|
||||
from ..device import Device, WifiNetwork
|
||||
from ..device_type import DeviceType
|
||||
from ..deviceconfig import DeviceConfig
|
||||
from ..emeterstatus import EmeterStatus
|
||||
from ..exceptions import AuthenticationError, DeviceError, KasaException, SmartErrorCode
|
||||
from ..feature import Feature
|
||||
from ..module import Module
|
||||
@ -477,31 +476,6 @@ class SmartDevice(Device):
|
||||
self._discovery_info = info
|
||||
self._info = info
|
||||
|
||||
async def get_emeter_realtime(self) -> EmeterStatus:
|
||||
"""Retrieve current energy readings."""
|
||||
_LOGGER.warning("Deprecated, use `emeter_realtime`.")
|
||||
if not self.has_emeter:
|
||||
raise KasaException("Device has no emeter")
|
||||
return self.emeter_realtime
|
||||
|
||||
@property
|
||||
def emeter_realtime(self) -> EmeterStatus:
|
||||
"""Get the emeter status."""
|
||||
energy = self.modules[Module.Energy]
|
||||
return energy.emeter_realtime
|
||||
|
||||
@property
|
||||
def emeter_this_month(self) -> float | None:
|
||||
"""Get the emeter value for this month."""
|
||||
energy = self.modules[Module.Energy]
|
||||
return energy.emeter_this_month
|
||||
|
||||
@property
|
||||
def emeter_today(self) -> float | None:
|
||||
"""Get the emeter value for today."""
|
||||
energy = self.modules[Module.Energy]
|
||||
return energy.emeter_today
|
||||
|
||||
async def wifi_scan(self) -> list[WifiNetwork]:
|
||||
"""Scan for available wifi networks."""
|
||||
|
||||
|
@ -163,12 +163,7 @@ async def _test_attribute(
|
||||
if is_expected and will_raise:
|
||||
ctx = pytest.raises(will_raise)
|
||||
elif is_expected:
|
||||
ctx = pytest.deprecated_call(
|
||||
match=(
|
||||
f"{attribute_name} is deprecated, use: Module."
|
||||
+ f"{module_name} in device.modules instead"
|
||||
)
|
||||
)
|
||||
ctx = pytest.deprecated_call(match=(f"{attribute_name} is deprecated, use:"))
|
||||
else:
|
||||
ctx = pytest.raises(
|
||||
AttributeError, match=f"Device has no attribute '{attribute_name}'"
|
||||
@ -239,6 +234,19 @@ async def test_deprecated_other_attributes(dev: Device):
|
||||
|
||||
await _test_attribute(dev, "led", bool(led_module), "Led")
|
||||
await _test_attribute(dev, "set_led", bool(led_module), "Led", True)
|
||||
await _test_attribute(dev, "supported_modules", True, None)
|
||||
|
||||
|
||||
async def test_deprecated_emeter_attributes(dev: Device):
|
||||
energy_module = dev.modules.get(Module.Energy)
|
||||
|
||||
await _test_attribute(dev, "get_emeter_realtime", bool(energy_module), "Energy")
|
||||
await _test_attribute(dev, "emeter_realtime", bool(energy_module), "Energy")
|
||||
await _test_attribute(dev, "emeter_today", bool(energy_module), "Energy")
|
||||
await _test_attribute(dev, "emeter_this_month", bool(energy_module), "Energy")
|
||||
await _test_attribute(dev, "current_consumption", bool(energy_module), "Energy")
|
||||
await _test_attribute(dev, "get_emeter_daily", bool(energy_module), "Energy")
|
||||
await _test_attribute(dev, "get_emeter_monthly", bool(energy_module), "Energy")
|
||||
|
||||
|
||||
async def test_deprecated_light_preset_attributes(dev: Device):
|
||||
|
@ -10,8 +10,9 @@ from voluptuous import (
|
||||
Schema,
|
||||
)
|
||||
|
||||
from kasa import EmeterStatus, KasaException
|
||||
from kasa.iot import IotDevice
|
||||
from kasa import Device, EmeterStatus, Module
|
||||
from kasa.interfaces.energy import Energy
|
||||
from kasa.iot import IotDevice, IotStrip
|
||||
from kasa.iot.modules.emeter import Emeter
|
||||
|
||||
from .conftest import has_emeter, has_emeter_iot, no_emeter
|
||||
@ -38,16 +39,16 @@ CURRENT_CONSUMPTION_SCHEMA = Schema(
|
||||
async def test_no_emeter(dev):
|
||||
assert not dev.has_emeter
|
||||
|
||||
with pytest.raises(KasaException):
|
||||
with pytest.raises(AttributeError):
|
||||
await dev.get_emeter_realtime()
|
||||
# Only iot devices support the historical stats so other
|
||||
# devices will not implement the methods below
|
||||
if isinstance(dev, IotDevice):
|
||||
with pytest.raises(KasaException):
|
||||
with pytest.raises(AttributeError):
|
||||
await dev.get_emeter_daily()
|
||||
with pytest.raises(KasaException):
|
||||
with pytest.raises(AttributeError):
|
||||
await dev.get_emeter_monthly()
|
||||
with pytest.raises(KasaException):
|
||||
with pytest.raises(AttributeError):
|
||||
await dev.erase_emeter_stats()
|
||||
|
||||
|
||||
@ -128,11 +129,11 @@ async def test_erase_emeter_stats(dev):
|
||||
@has_emeter_iot
|
||||
async def test_current_consumption(dev):
|
||||
if dev.has_emeter:
|
||||
x = await dev.current_consumption()
|
||||
x = dev.current_consumption
|
||||
assert isinstance(x, float)
|
||||
assert x >= 0.0
|
||||
else:
|
||||
assert await dev.current_consumption() is None
|
||||
assert dev.current_consumption is None
|
||||
|
||||
|
||||
async def test_emeterstatus_missing_current():
|
||||
@ -173,3 +174,30 @@ async def test_emeter_daily():
|
||||
{"day": now.day, "energy_wh": 500, "month": now.month, "year": now.year}
|
||||
)
|
||||
assert emeter.emeter_today == 0.500
|
||||
|
||||
|
||||
@has_emeter
|
||||
async def test_supported(dev: Device):
|
||||
energy_module = dev.modules.get(Module.Energy)
|
||||
assert energy_module
|
||||
if isinstance(dev, IotDevice):
|
||||
info = (
|
||||
dev._last_update
|
||||
if not isinstance(dev, IotStrip)
|
||||
else dev.children[0].internal_state
|
||||
)
|
||||
emeter = info[energy_module._module]["get_realtime"]
|
||||
has_total = "total" in emeter or "total_wh" in emeter
|
||||
has_voltage_current = "voltage" in emeter or "voltage_mv" in emeter
|
||||
assert (
|
||||
energy_module.supports(Energy.ModuleFeature.CONSUMPTION_TOTAL) is has_total
|
||||
)
|
||||
assert (
|
||||
energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT)
|
||||
is has_voltage_current
|
||||
)
|
||||
assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is True
|
||||
else:
|
||||
assert energy_module.supports(Energy.ModuleFeature.CONSUMPTION_TOTAL) is False
|
||||
assert energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT) is False
|
||||
assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is False
|
||||
|
@ -116,9 +116,16 @@ async def test_initial_update_no_emeter(dev, mocker):
|
||||
dev._legacy_features = set()
|
||||
spy = mocker.spy(dev.protocol, "query")
|
||||
await dev.update()
|
||||
# 2 calls are necessary as some devices crash on unexpected modules
|
||||
# child calls will happen if a child has a module with a query (e.g. schedule)
|
||||
child_calls = 0
|
||||
for child in dev.children:
|
||||
for module in child.modules.values():
|
||||
if module.query():
|
||||
child_calls += 1
|
||||
break
|
||||
# 2 parent are necessary as some devices crash on unexpected modules
|
||||
# See #105, #120, #161
|
||||
assert spy.call_count == 2
|
||||
assert spy.call_count == 2 + child_calls
|
||||
|
||||
|
||||
@device_iot
|
||||
|
Loading…
Reference in New Issue
Block a user