2024-01-05 01:01:00 +00:00
|
|
|
import datetime
|
|
|
|
from unittest.mock import Mock
|
|
|
|
|
2020-05-27 14:55:18 +00:00
|
|
|
import pytest
|
2024-01-29 19:26:39 +00:00
|
|
|
from voluptuous import (
|
|
|
|
All,
|
|
|
|
Any,
|
2024-06-19 13:07:59 +00:00
|
|
|
Coerce,
|
2024-01-29 19:26:39 +00:00
|
|
|
Range,
|
|
|
|
Schema,
|
|
|
|
)
|
2020-05-27 14:55:18 +00:00
|
|
|
|
2024-11-13 16:10:06 +00:00
|
|
|
from kasa import Device, DeviceType, EmeterStatus, Module
|
2024-06-17 10:22:05 +00:00
|
|
|
from kasa.interfaces.energy import Energy
|
|
|
|
from kasa.iot import IotDevice, IotStrip
|
2024-02-04 15:20:08 +00:00
|
|
|
from kasa.iot.modules.emeter import Emeter
|
2024-10-27 12:08:02 +00:00
|
|
|
from kasa.smart import SmartDevice
|
|
|
|
from kasa.smart.modules import Energy as SmartEnergyModule
|
2024-11-21 18:40:13 +00:00
|
|
|
from kasa.smart.smartmodule import SmartModule
|
2020-05-27 14:55:18 +00:00
|
|
|
|
2023-12-20 17:08:04 +00:00
|
|
|
from .conftest import has_emeter, has_emeter_iot, no_emeter
|
2024-01-29 19:26:39 +00:00
|
|
|
|
|
|
|
CURRENT_CONSUMPTION_SCHEMA = Schema(
|
|
|
|
Any(
|
|
|
|
{
|
|
|
|
"voltage": Any(All(float, Range(min=0, max=300)), None),
|
2024-06-19 13:07:59 +00:00
|
|
|
"power": Any(Coerce(float), None),
|
|
|
|
"total": Any(Coerce(float), None),
|
|
|
|
"current": Any(All(float), None),
|
2024-01-29 19:26:39 +00:00
|
|
|
"voltage_mv": Any(All(float, Range(min=0, max=300000)), int, None),
|
2024-06-19 13:07:59 +00:00
|
|
|
"power_mw": Any(Coerce(float), None),
|
|
|
|
"total_wh": Any(Coerce(float), None),
|
|
|
|
"current_ma": Any(All(float), int, None),
|
|
|
|
"slot_id": Any(Coerce(int), None),
|
2024-01-29 19:26:39 +00:00
|
|
|
},
|
|
|
|
None,
|
|
|
|
)
|
|
|
|
)
|
2020-05-27 14:55:18 +00:00
|
|
|
|
|
|
|
|
|
|
|
@no_emeter
|
|
|
|
async def test_no_emeter(dev):
|
|
|
|
assert not dev.has_emeter
|
|
|
|
|
2024-06-17 10:22:05 +00:00
|
|
|
with pytest.raises(AttributeError):
|
2020-05-27 14:55:18 +00:00
|
|
|
await dev.get_emeter_realtime()
|
2024-02-04 15:20:08 +00:00
|
|
|
# Only iot devices support the historical stats so other
|
|
|
|
# devices will not implement the methods below
|
|
|
|
if isinstance(dev, IotDevice):
|
2024-06-17 10:22:05 +00:00
|
|
|
with pytest.raises(AttributeError):
|
2024-02-04 15:20:08 +00:00
|
|
|
await dev.get_emeter_daily()
|
2024-06-17 10:22:05 +00:00
|
|
|
with pytest.raises(AttributeError):
|
2024-02-04 15:20:08 +00:00
|
|
|
await dev.get_emeter_monthly()
|
2024-06-17 10:22:05 +00:00
|
|
|
with pytest.raises(AttributeError):
|
2024-02-04 15:20:08 +00:00
|
|
|
await dev.erase_emeter_stats()
|
2020-05-27 14:55:18 +00:00
|
|
|
|
|
|
|
|
2024-01-10 19:37:43 +00:00
|
|
|
@has_emeter
|
2020-05-27 14:55:18 +00:00
|
|
|
async def test_get_emeter_realtime(dev):
|
2024-10-27 12:08:02 +00:00
|
|
|
if isinstance(dev, SmartDevice):
|
|
|
|
mod = SmartEnergyModule(dev, str(Module.Energy))
|
|
|
|
if not await mod._check_supported():
|
|
|
|
pytest.skip(f"Energy module not supported for {dev}.")
|
|
|
|
|
2024-11-13 16:10:06 +00:00
|
|
|
emeter = dev.modules[Module.Energy]
|
2020-05-27 14:55:18 +00:00
|
|
|
|
2024-11-13 16:10:06 +00:00
|
|
|
current_emeter = await emeter.get_status()
|
2020-05-27 14:55:18 +00:00
|
|
|
CURRENT_CONSUMPTION_SCHEMA(current_emeter)
|
|
|
|
|
|
|
|
|
2023-12-20 17:08:04 +00:00
|
|
|
@has_emeter_iot
|
2024-11-18 18:46:36 +00:00
|
|
|
@pytest.mark.requires_dummy
|
2020-05-27 14:55:18 +00:00
|
|
|
async def test_get_emeter_daily(dev):
|
2024-11-13 16:10:06 +00:00
|
|
|
emeter = dev.modules[Module.Energy]
|
2020-05-27 14:55:18 +00:00
|
|
|
|
2024-11-13 16:10:06 +00:00
|
|
|
assert await emeter.get_daily_stats(year=1900, month=1) == {}
|
2020-05-27 14:55:18 +00:00
|
|
|
|
2024-11-13 16:10:06 +00:00
|
|
|
d = await emeter.get_daily_stats()
|
2020-05-27 14:55:18 +00:00
|
|
|
assert len(d) > 0
|
|
|
|
|
|
|
|
k, v = d.popitem()
|
|
|
|
assert isinstance(k, int)
|
|
|
|
assert isinstance(v, float)
|
|
|
|
|
|
|
|
# Test kwh (energy, energy_wh)
|
2024-11-13 16:10:06 +00:00
|
|
|
d = await emeter.get_daily_stats(kwh=False)
|
2020-05-27 14:55:18 +00:00
|
|
|
k2, v2 = d.popitem()
|
|
|
|
assert v * 1000 == v2
|
|
|
|
|
|
|
|
|
2023-12-20 17:08:04 +00:00
|
|
|
@has_emeter_iot
|
2024-11-18 18:46:36 +00:00
|
|
|
@pytest.mark.requires_dummy
|
2020-05-27 14:55:18 +00:00
|
|
|
async def test_get_emeter_monthly(dev):
|
2024-11-13 16:10:06 +00:00
|
|
|
emeter = dev.modules[Module.Energy]
|
2020-05-27 14:55:18 +00:00
|
|
|
|
2024-11-13 16:10:06 +00:00
|
|
|
assert await emeter.get_monthly_stats(year=1900) == {}
|
2020-05-27 14:55:18 +00:00
|
|
|
|
2024-11-13 16:10:06 +00:00
|
|
|
d = await emeter.get_monthly_stats()
|
2020-05-27 14:55:18 +00:00
|
|
|
assert len(d) > 0
|
|
|
|
|
|
|
|
k, v = d.popitem()
|
|
|
|
assert isinstance(k, int)
|
|
|
|
assert isinstance(v, float)
|
|
|
|
|
|
|
|
# Test kwh (energy, energy_wh)
|
2024-11-13 16:10:06 +00:00
|
|
|
d = await emeter.get_monthly_stats(kwh=False)
|
2020-05-27 14:55:18 +00:00
|
|
|
k2, v2 = d.popitem()
|
|
|
|
assert v * 1000 == v2
|
|
|
|
|
|
|
|
|
2023-12-20 17:08:04 +00:00
|
|
|
@has_emeter_iot
|
2020-05-27 14:55:18 +00:00
|
|
|
async def test_emeter_status(dev):
|
2024-11-13 16:10:06 +00:00
|
|
|
emeter = dev.modules[Module.Energy]
|
2020-05-27 14:55:18 +00:00
|
|
|
|
2024-11-13 16:10:06 +00:00
|
|
|
d = await emeter.get_status()
|
2020-05-27 14:55:18 +00:00
|
|
|
|
|
|
|
with pytest.raises(KeyError):
|
|
|
|
assert d["foo"]
|
|
|
|
|
|
|
|
assert d["power_mw"] == d["power"] * 1000
|
|
|
|
# bulbs have only power according to tplink simulator.
|
2024-11-13 16:10:06 +00:00
|
|
|
if (
|
|
|
|
dev.device_type is not DeviceType.Bulb
|
|
|
|
and dev.device_type is not DeviceType.LightStrip
|
|
|
|
):
|
2020-05-27 14:55:18 +00:00
|
|
|
assert d["voltage_mv"] == d["voltage"] * 1000
|
|
|
|
|
|
|
|
assert d["current_ma"] == d["current"] * 1000
|
|
|
|
assert d["total_wh"] == d["total"] * 1000
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skip("not clearing your stats..")
|
|
|
|
@has_emeter
|
|
|
|
async def test_erase_emeter_stats(dev):
|
2024-11-13 16:10:06 +00:00
|
|
|
emeter = dev.modules[Module.Energy]
|
2020-05-27 14:55:18 +00:00
|
|
|
|
2024-11-13 16:10:06 +00:00
|
|
|
await emeter.erase_emeter()
|
2020-05-27 14:55:18 +00:00
|
|
|
|
|
|
|
|
2024-02-04 15:20:08 +00:00
|
|
|
@has_emeter_iot
|
2020-05-27 14:55:18 +00:00
|
|
|
async def test_current_consumption(dev):
|
2024-11-13 16:10:06 +00:00
|
|
|
emeter = dev.modules[Module.Energy]
|
|
|
|
x = emeter.current_consumption
|
|
|
|
assert isinstance(x, float)
|
|
|
|
assert x >= 0.0
|
2021-03-18 18:22:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
async def test_emeterstatus_missing_current():
|
|
|
|
"""KL125 does not report 'current' for emeter."""
|
|
|
|
regular = EmeterStatus(
|
|
|
|
{"err_code": 0, "power_mw": 0, "total_wh": 13, "current_ma": 123}
|
|
|
|
)
|
|
|
|
assert regular["current"] == 0.123
|
|
|
|
|
|
|
|
with pytest.raises(KeyError):
|
|
|
|
regular["invalid_key"]
|
|
|
|
|
|
|
|
missing_current = EmeterStatus({"err_code": 0, "power_mw": 0, "total_wh": 13})
|
|
|
|
assert missing_current["current"] is None
|
2024-01-05 01:01:00 +00:00
|
|
|
|
|
|
|
|
|
|
|
async def test_emeter_daily():
|
|
|
|
"""Test fetching the emeter for today.
|
|
|
|
|
|
|
|
This test uses inline data since the fixtures
|
|
|
|
will not have data for the current day.
|
|
|
|
"""
|
|
|
|
emeter_data = {
|
|
|
|
"get_daystat": {
|
|
|
|
"day_list": [{"day": 1, "energy_wh": 8, "month": 1, "year": 2023}],
|
|
|
|
"err_code": 0,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class MockEmeter(Emeter):
|
|
|
|
@property
|
|
|
|
def data(self):
|
|
|
|
return emeter_data
|
|
|
|
|
|
|
|
emeter = MockEmeter(Mock(), "emeter")
|
|
|
|
now = datetime.datetime.now()
|
|
|
|
emeter_data["get_daystat"]["day_list"].append(
|
|
|
|
{"day": now.day, "energy_wh": 500, "month": now.month, "year": now.year}
|
|
|
|
)
|
2024-11-13 16:10:06 +00:00
|
|
|
assert emeter.consumption_today == 0.500
|
2024-06-17 10:22:05 +00:00
|
|
|
|
|
|
|
|
|
|
|
@has_emeter
|
|
|
|
async def test_supported(dev: Device):
|
2024-10-27 12:08:02 +00:00
|
|
|
if isinstance(dev, SmartDevice):
|
|
|
|
mod = SmartEnergyModule(dev, str(Module.Energy))
|
|
|
|
if not await mod._check_supported():
|
|
|
|
pytest.skip(f"Energy module not supported for {dev}.")
|
2024-06-17 10:22:05 +00:00
|
|
|
energy_module = dev.modules.get(Module.Energy)
|
|
|
|
assert energy_module
|
2024-11-21 18:40:13 +00:00
|
|
|
|
2024-06-17 10:22:05 +00:00
|
|
|
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:
|
2024-11-21 18:40:13 +00:00
|
|
|
assert isinstance(energy_module, SmartModule)
|
2024-06-17 10:22:05 +00:00
|
|
|
assert energy_module.supports(Energy.ModuleFeature.CONSUMPTION_TOTAL) is False
|
|
|
|
assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is False
|
2024-11-20 18:18:30 +00:00
|
|
|
if energy_module.supported_version < 2:
|
|
|
|
assert energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT) is False
|
|
|
|
else:
|
|
|
|
assert energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT) is True
|