8000 Add common energy module and deprecate device emeter attributes (#976) · python-kasa/python-kasa@b4a6df2 · GitHub
[go: up one dir, main page]

Skip to content

Commit b4a6df2

Browse files
authored
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.
1 parent 51a9725 commit b4a6df2

File tree

14 files changed

+486
-381
lines changed

14 files changed

+486
-381
lines changed

kasa/device.py

Lines changed: 21 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
DeviceEncryptionType,
2020
DeviceFamily,
2121
)
22-
from .emeterstatus import EmeterStatus
2322
from .exceptions import KasaException
2423
from .feature import Feature
2524
from .iotprotocol import IotProtocol
@@ -323,27 +322,6 @@ def has_emeter(self) -> bool:
323322
def on_since(self) -> datetime | None:
324323
"""Return the time that the device was turned on or None if turned off."""
325324

326-
@abstractmethod
327-
async def get_emeter_realtime(self) -> EmeterStatus:
328-
"""Retrieve current energy readings."""
329-
330-
@property
331-
@abstractmethod
332-
def emeter_realtime(self) -> EmeterStatus:
333-
"""Get the emeter status."""
334-
335-
@property
336-
@abstractmethod
337-
def emeter_this_month(self) -> float | None:
338-
"""Get the emeter value for this month."""
339-
340-
@property
341-
@abstractmethod
342-
def emeter_today(self) -> float | None | Any:
343-
"""Get the emeter value for today."""
344-
# Return type of Any ensures consumers being shielded from the return
345-
# type by @update_required are not affected.
346-
347325
@abstractmethod
348326
async def wifi_scan(self) -> list[WifiNetwork]:
349327
"""Scan for available wifi networks."""
@@ -373,12 +351,15 @@ def __repr__(self):
373351
}
374352

375353
def _get_replacing_attr(self, module_name: ModuleName, *attrs):
376-
if module_name not in self.modules:
354+
# If module name is None check self
355+
if not module_name:
356+
check = self
357+
elif (check := self.modules.get(module_name)) is None:
377358
return None
378359

379360
for attr in attrs:
380-
if hasattr(self.modules[module_name], attr):
381-
return getattr(self.modules[module_name], attr)
361+
if hasattr(check, attr):
362+
return attr
382363

383364
return None
384365

@@ -411,6 +392,16 @@ def _get_replacing_attr(self, module_name: ModuleName, *attrs):
411392
# light preset attributes
412393
"presets": (Module.LightPreset, ["_deprecated_presets", "preset_states_list"]),
413394
"save_preset": (Module.LightPreset, ["_deprecated_save_preset"]),
395+
# Emeter attribues
396+
"get_emeter_realtime": (Module.Energy, ["get_status"]),
397+
"emeter_realtime": (Module.Energy, ["status"]),
398+
"emeter_today": (Module.Energy, ["consumption_today"]),
399+
"emeter_this_month": (Module.Energy, ["consumption_this_month"]),
400+
"current_consumption": (Module.Energy, ["current_consumption"]),
401+
"get_emeter_daily": (Module.Energy, ["get_daily_stats"]),
402+
"get_emeter_monthly": (Module.Energy, ["get_monthly_stats"]),
403+
# Other attributes
404+
"supported_modules": (None, ["modules"]),
414405
}
415406

416407
def __getattr__(self, name):
@@ -427,11 +418,10 @@ def __getattr__(self, name):
427418
(replacing_attr := self._get_replacing_attr(dep_attr[0], *dep_attr[1]))
428419
is not None
429420
):
430-
module_name = dep_attr[0]
431-
msg = (
432-
f"{name} is deprecated, use: "
433-
+ f"Module.{module_name} in device.modules instead"
434-
)
421+
mod = dep_attr[0]
422+
dev_or_mod = self.modules[mod] if mod else self
423+
replacing = f"Module.{mod} in device.modules" if mod else replacing_attr
424+
msg = f"{name} is deprecated, use: {replacing} instead"
435425
warn(msg, DeprecationWarning, stacklevel=1)
436-
return replacing_attr
426+
return getattr(dev_or_mod, replacing_attr)
437427
raise AttributeError(f"Device has no attribute {name!r}")

kasa/interfaces/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Package for interfaces."""
22

3+
from .energy import Energy
34
from .fan import Fan
45
from .led import Led
56
from .light import Light, LightState
@@ -8,6 +9,7 @@
89

910
__all__ = [
1011
"Fan",
12+
"Energy",
1113
"Led",
1214
"Light",
1315
"LightEffect",

kasa/interfaces/energy.py

Lines changed: 181 additions & 0 deletions
< 10000 /tr>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
"""Module for base energy module."""
2+
3+
from __future__ import annotations
4+
5+
from abc import ABC, abstractmethod
6+
from enum import IntFlag, auto
7+
from warnings import warn
8+
9+
from ..emeterstatus import EmeterStatus
10+
from ..feature import Feature
11+
from ..module import Module
12+
13+
14+
class Energy(Module, ABC):
15+
"""Base interface to represent an Energy module."""
16+
17+
class ModuleFeature(IntFlag):
18+
"""Features supported by the device."""
19+
20+
#: Device reports :attr:`voltage` and :attr:`current`
21+
VOLTAGE_CURRENT = auto()
22+
#: Device reports :attr:`consumption_total`
23+
CONSUMPTION_TOTAL = auto()
24+
#: Device reports periodic stats via :meth:`get_daily_stats`
25+
#: and :meth:`get_monthly_stats`
26+
PERIODIC_STATS = auto()
27+
28+
_supported: ModuleFeature = ModuleFeature(0)
29+
30+
def supports(self, module_feature: ModuleFeature) -> bool:
31+
"""Return True if module supports the feature."""
32+
return module_feature in self._supported
33+
34+
def _initialize_features(self):
35+
"""Initialize features."""
36+
device = self._device
37+
self._add_feature(
38+
Feature(
39+
device,
40+
name="Current consumption",
41+
attribute_getter="current_consumption",
42+
container=self,
43+
unit="W",
44+
id="current_consumption",
45+
precision_hint=1,
46+
category=Feature.Category.Primary,
47+
)
48+
)
49+
self._add_feature(
50+
Feature(
51+
device,
52+
name="Today's consumption",
53+
attribute_getter="consumption_today",
54+
container=self,
55+
unit="kWh",
56+
id="consumption_today",
57+
precision_hint=3,
58+
category=Feature.Category.Info,
59+
)
60+
)
61+
self._add_feature(
62+
Feature(
63+
device,
64+
id="consumption_this_month",
65+
name="This month's consumption",
66+
attribute_getter="consumption_this_month",
67+
container=self,
68+
unit="kWh",
69+
precision_hint=3,
70+
category=Feature.Category.Info,
71+
)
72+
)
73+
if self.supports(self.ModuleFeature.CONSUMPTION_TOTAL):
74+
self._add_feature(
75+
Feature(
76+
device,
77+
name="Total consumption since reboot",
78+
attribute_getter="consumption_total",
79+
container=self,
80+
unit="kWh",
81+
id="consumption_total",
82+
precision_hint=3,
83+
category=Feature.Category.Info,
84+
)
85+
)
86+
if self.supports(self.ModuleFeature.VOLTAGE_CURRENT):
87+
self._add_feature(
88+
Feature(
89+
device,
90+
name="Voltage",
91+
attribute_getter="voltage",
92+
container=self,
93+
unit="V",
94+
id="voltage",
95+
precision_hint=1,
96+
category=Feature.Category.Primary,
97+
)
98+
)
99+
self._add_feature(
100+
Feature(
101+
device,
102+
name="Current",
103+
attribute_getter="current",
104+
container=self,
105+
unit="A",
106+
id="current",
107+
precision_hint=2,
108+
category=Feature.Category.Primary,
109+
)
110+
)
111+
112+
@property
113+
@abstractmethod
114+
def status(self) -> EmeterStatus:
115+
"""Return current energy readings."""
116+
117+
@property
118+
@abstractmethod
119+
def current_consumption(self) -> float | None:
120+
"""Get the current power consumption in Watt."""
121+
122+
@property
123+
@abstractmethod
124+
def consumption_today(self) -> float | None:
125+
"""Return today's energy consumption in kWh."""
126+
127+
@property
128+
@abstractmethod
129+
def consumption_this_month(self) -> float | None:
130+
"""Return this month's energy consumption in kWh."""
131+
132+
@property
133+
@abstractmethod
134+
def consumption_total(self) -> float | None:
135+
"""Return total consumption since last reboot in kWh."""
136+
137+
@property
138+
@abstractmethod
139+
def current(self) -> float | None:
140+
"""Return the current in A."""
141+
142+
@property
143+
@abstractmethod
144+
def voltage(self) -> float | None:
145+
"""Get the current voltage in V."""
146+
147+
@abstractmethod
148+
async def get_status(self):
149+
"""Return real-time statistics."""
150+
151+
@abstractmethod
152+
async def erase_stats(self):
153+
"""Erase all stats."""
154+
155+
@abstractmethod
156+
async def get_daily_stats(self, *, year=None, month=None, kwh=True) -> dict:
157+
"""Return daily stats for the given year & month.
158+
159+
The return value is a dictionary of {day: energy, ...}.
160+
"""
161+
162+
@abstractmethod
163+
async def get_monthly_stats(self, *, year=None, kwh=True) -> dict:
164+
"""Return monthly stats for the given year."""
165+
166+
_deprecated_attributes = {
167+
"emeter_today": "consumption_today",
168+
"emeter_this_month": "consumption_this_month",
169+
"realtime": "status",
170+
"get_realtime": "get_status",
171+
"erase_emeter_stats": "erase_stats",
172+
"get_daystat": "get_daily_stats",
173+
"get_monthstat": "get_monthly_stats",
174+
}
175+
176+
def __getattr__(self, name):
177+
if attr := self._deprecated_attributes.get(name):
178+
msg = f"{name} is deprecated, use {attr} instead"
179+
warn(msg, DeprecationWarning, stacklevel=1)
180+
return getattr(self, attr)
181+
raise AttributeError(f"Energy module has no attribute {name!r}")

kasa/iot/iotbulb.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ async def _initialize_modules(self):
220220
Module.IotAntitheft, Antitheft(self, "smartlife.iot.common.anti_theft")
221221
)
222222
self.add_module(Module.IotTime, Time(self, "smartlife.iot.common.timesetting"))
223-
self.add_module(Module.IotEmeter, Emeter(self, self.emeter_type))
223+
self.add_module(Module.Energy, Emeter(self, self.emeter_type))
224224
self.add_module(Module.IotCountdown, Countdown(self, "countdown"))
225225
self.add_module(Module.IotCloud, Cloud(self, "smartlife.iot.common.cloud"))
226226
self.add_module(Module.Light, Light(self, self.LIGHT_SERVICE))

0 commit comments

Comments
 (0)
0