Skip to content
Merged
52 changes: 21 additions & 31 deletions kasa/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
DeviceEncryptionType,
DeviceFamily,
)
from .emeterstatus import EmeterStatus
from .exceptions import KasaException
from .feature import Feature
from .iotprotocol import IotProtocol
Expand Down Expand Up @@ -323,27 +322,6 @@ def has_emeter(self) -> bool:
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."""
Expand Down Expand Up @@ -373,12 +351,15 @@ def __repr__(self):
}

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

Expand Down Expand Up @@ -411,6 +392,16 @@ def _get_replacing_attr(self, module_name: ModuleName, *attrs):
# 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):
Expand All @@ -427,11 +418,10 @@ def __getattr__(self, name):
(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}")
2 changes: 2 additions & 0 deletions kasa/interfaces/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Package for interfaces."""

from .energy import Energy
from .fan import Fan
from .led import Led
from .light import Light, LightState
Expand All @@ -8,6 +9,7 @@

__all__ = [
"Fan",
"Energy",
"Led",
"Light",
"LightEffect",
Expand Down
181 changes: 181 additions & 0 deletions kasa/interfaces/energy.py
Original file line number Diff line number Diff line change
@@ -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}")
2 changes: 1 addition & 1 deletion kasa/iot/iotbulb.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ async def _initialize_modules(self):
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))
Expand Down
Loading