Skip to content

Commit

Permalink
Improve emeterstatus API, move into own module (#205)
Browse files Browse the repository at this point in the history
Adds the following properties to EmeterStatus for saner API:
* voltage (in V)
* power (in W)
* current (in A)
* total (in kWh)
  • Loading branch information
rytilahti committed Sep 23, 2021
1 parent 47a1405 commit 36c412a
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 53 deletions.
3 changes: 2 additions & 1 deletion kasa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
from importlib_metadata import version # type: ignore

from kasa.discover import Discover
from kasa.emeterstatus import EmeterStatus
from kasa.exceptions import SmartDeviceException
from kasa.protocol import TPLinkSmartHomeProtocol
from kasa.smartbulb import SmartBulb
from kasa.smartdevice import DeviceType, EmeterStatus, SmartDevice
from kasa.smartdevice import DeviceType, SmartDevice
from kasa.smartdimmer import SmartDimmer
from kasa.smartlightstrip import SmartLightStrip
from kasa.smartplug import SmartPlug
Expand Down
82 changes: 82 additions & 0 deletions kasa/emeterstatus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Module for emeter container."""
import logging
from typing import Optional

_LOGGER = logging.getLogger(__name__)


class EmeterStatus(dict):
"""Container for converting different representations of emeter data.
Newer FW/HW versions postfix the variable names with the used units,
where-as the olders do not have this feature.
This class automatically converts between these two to allow
backwards and forwards compatibility.
"""

@property
def voltage(self) -> Optional[float]:
"""Return voltage in V."""
try:
return self["voltage"]
except ValueError:
return None

@property
def power(self) -> Optional[float]:
"""Return power in W."""
try:
return self["power"]
except ValueError:
return None

@property
def current(self) -> Optional[float]:
"""Return current in A."""
try:
return self["current"]
except ValueError:
return None

@property
def total(self) -> Optional[float]:
"""Return total in kWh."""
try:
return self["total"]
except ValueError:
return None

def __repr__(self):
return f"<EmeterStatus power={self.power} voltage={self.voltage} current={self.current} total={self.total}>"

def __getitem__(self, item):
valid_keys = [
"voltage_mv",
"power_mw",
"current_ma",
"energy_wh",
"total_wh",
"voltage",
"power",
"current",
"total",
"energy",
]

# 1. if requested data is available, return it
if item in super().keys():
return super().__getitem__(item)
# otherwise decide how to convert it
else:
if item not in valid_keys:
raise KeyError(item)
if "_" in item: # upscale
return super().__getitem__(item[: item.find("_")]) * 1000
else: # downscale
for i in super().keys():
if i.startswith(item):
return self.__getitem__(i) / 1000

_LOGGER.debug(f"Unable to find value for '{item}'")
return None
7 changes: 1 addition & 6 deletions kasa/smartbulb.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,7 @@
import re
from typing import Any, Dict, NamedTuple, cast

from kasa.smartdevice import (
DeviceType,
SmartDevice,
SmartDeviceException,
requires_update,
)
from .smartdevice import DeviceType, SmartDevice, SmartDeviceException, requires_update


class ColorTempRange(NamedTuple):
Expand Down
45 changes: 2 additions & 43 deletions kasa/smartdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from enum import Enum, auto
from typing import Any, Dict, List, Optional

from .emeterstatus import EmeterStatus
from .exceptions import SmartDeviceException
from .protocol import TPLinkSmartHomeProtocol

Expand Down Expand Up @@ -50,48 +51,6 @@ class WifiNetwork:
rssi: Optional[int] = None


class EmeterStatus(dict):
"""Container for converting different representations of emeter data.
Newer FW/HW versions postfix the variable names with the used units,
where-as the olders do not have this feature.
This class automatically converts between these two to allow
backwards and forwards compatibility.
"""

def __getitem__(self, item):
valid_keys = [
"voltage_mv",
"power_mw",
"current_ma",
"energy_wh",
"total_wh",
"voltage",
"power",
"current",
"total",
"energy",
]

# 1. if requested data is available, return it
if item in super().keys():
return super().__getitem__(item)
# otherwise decide how to convert it
else:
if item not in valid_keys:
raise KeyError(item)
if "_" in item: # upscale
return super().__getitem__(item[: item.find("_")]) * 1000
else: # downscale
for i in super().keys():
if i.startswith(item):
return self.__getitem__(i) / 1000

_LOGGER.debug(f"Unable to find value for '{item}'")
return None


def requires_update(f):
"""Indicate that `update` should be called before accessing this method.""" # noqa: D202
if inspect.iscoroutinefunction(f):
Expand Down Expand Up @@ -202,7 +161,7 @@ class SmartDevice:
>>> dev.has_emeter
True
>>> dev.emeter_realtime
{'current': 0.015342, 'err_code': 0, 'power': 0.983971, 'total': 32.448, 'voltage': 235.595234}
<EmeterStatus power=0.983971 voltage=235.595234 current=0.015342 total=32.448>
>>> dev.emeter_today
>>> dev.emeter_this_month
Expand Down
4 changes: 1 addition & 3 deletions kasa/tests/test_emeter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from kasa import SmartDeviceException
from kasa import EmeterStatus, SmartDeviceException

from .conftest import has_emeter, no_emeter, pytestmark
from .newfakes import CURRENT_CONSUMPTION_SCHEMA
Expand Down Expand Up @@ -121,8 +121,6 @@ async def test_current_consumption(dev):

async def test_emeterstatus_missing_current():
"""KL125 does not report 'current' for emeter."""
from kasa import EmeterStatus

regular = EmeterStatus(
{"err_code": 0, "power_mw": 0, "total_wh": 13, "current_ma": 123}
)
Expand Down

0 comments on commit 36c412a

Please sign in to comment.