python-kasa/kasa/interfaces/energy.py
Teemu R. 5da41fcc11
Use stacklevel=2 for warnings to report on callsites (#1219)
Use stacklevel=2 for warnings, as this will correctly show the callsite
instead of the line where the warning is reported.

Currently:
```
kasa/__init__.py:110
  /home/tpr/code/python-kasa/kasa/__init__.py:110: DeprecationWarning: SmartDevice is deprecated, use IotDevice from package kasa.iot instead or use Discover.discover_single() and Device.connect() to support new protocols
    warn(
```

After:
```
kasa/tests/smart/modules/test_contact.py:3
  /home/tpr/code/python-kasa/kasa/tests/smart/modules/test_contact.py:3: DeprecationWarning: SmartDevice is deprecated, use IotDevice from package kasa.iot instead or use Discover.discover_single() and Device.connect() to support new protocols
    from kasa import Module, SmartDevice

``` 

Currently:
```
kasa/tests/test_lightstrip.py: 56 warnings
  /home/tpr/code/python-kasa/kasa/device.py:559: DeprecationWarning: effect is deprecated, use: Module.LightEffect in device.modules instead
    warn(msg, DeprecationWarning, stacklevel=1)
```

After:
```
kasa/tests/test_lightstrip.py::test_effects_lightstrip_set_effect_transition[500-KL430(US)_2.0_1.0.9.json]
  /home/tpr/code/python-kasa/kasa/tests/test_lightstrip.py:62: DeprecationWarning: set_effect is deprecated, use: Module.LightEffect in device.modules instead
    await dev.set_effect("Candy Cane")
```
2024-10-31 14:12:17 +01:00

188 lines
5.9 KiB
Python

"""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_getter=lambda: "W",
id="current_consumption",
precision_hint=1,
category=Feature.Category.Primary,
type=Feature.Type.Sensor,
)
)
self._add_feature(
Feature(
device,
name="Today's consumption",
attribute_getter="consumption_today",
container=self,
unit_getter=lambda: "kWh",
id="consumption_today",
precision_hint=3,
category=Feature.Category.Info,
type=Feature.Type.Sensor,
)
)
self._add_feature(
Feature(
device,
id="consumption_this_month",
name="This month's consumption",
attribute_getter="consumption_this_month",
container=self,
unit_getter=lambda: "kWh",
precision_hint=3,
category=Feature.Category.Info,
type=Feature.Type.Sensor,
)
)
if self.supports(self.ModuleFeature.CONSUMPTION_TOTAL):
self._add_feature(
Feature(
device,
name="Total consumption since reboot",
attribute_getter="consumption_total",
container=self,
unit_getter=lambda: "kWh",
id="consumption_total",
precision_hint=3,
category=Feature.Category.Info,
type=Feature.Type.Sensor,
)
)
if self.supports(self.ModuleFeature.VOLTAGE_CURRENT):
self._add_feature(
Feature(
device,
name="Voltage",
attribute_getter="voltage",
container=self,
unit_getter=lambda: "V",
id="voltage",
precision_hint=1,
category=Feature.Category.Primary,
type=Feature.Type.Sensor,
)
)
self._add_feature(
Feature(
device,
name="Current",
attribute_getter="current",
container=self,
unit_getter=lambda: "A",
id="current",
precision_hint=2,
category=Feature.Category.Primary,
type=Feature.Type.Sensor,
)
)
@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=2)
return getattr(self, attr)
raise AttributeError(f"Energy module has no attribute {name!r}")