mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-08-06 10:44:04 +00:00
General cleanups all around (janitoring) (#63)
* Move tests to device-type specific test files to make improvements more approachable * protocol: remove the port parameter from query, as there are no other known ports, fix docstrings * Revise docstrings, remove superfluous information and remove unused methods ({set,get_icon} and set_time) * cli: indent device output to make it more easily readable when having multiple devices * remove adjust flake8 ignores (we have no setup.py anymore) * pyproject: include cli tool to coverage, add config for interrogate (docstring coverage) * bulb: raise exception on color_temp error cases instead of returning zero values * improve bulb tests, simplify conftest * strip: rename plugs property to children and move it to smartdevice
This commit is contained in:
@@ -17,12 +17,14 @@ SUPPORTED_DEVICES = glob.glob(
|
||||
|
||||
BULBS = {"KL60", "LB100", "LB120", "LB130", "KL120", "KL130"}
|
||||
VARIABLE_TEMP = {"LB120", "LB130", "KL120", "KL130"}
|
||||
COLOR_BULBS = {"LB130", "KL130"}
|
||||
|
||||
PLUGS = {"HS100", "HS103", "HS105", "HS110", "HS200", "HS210"}
|
||||
STRIPS = {"HS107", "HS300", "KP303", "KP400"}
|
||||
DIMMERS = {"HS220"}
|
||||
COLOR_BULBS = {"LB130", "KL130"}
|
||||
DIMMABLE = {*BULBS, "HS220"}
|
||||
EMETER = {"HS110", "HS300", *BULBS, *STRIPS}
|
||||
|
||||
DIMMABLE = {*BULBS, *DIMMERS}
|
||||
WITH_EMETER = {"HS110", "HS300", *BULBS, *STRIPS}
|
||||
|
||||
ALL_DEVICES = BULBS.union(PLUGS).union(STRIPS).union(DIMMERS)
|
||||
|
||||
@@ -39,17 +41,28 @@ def filter_model(desc, filter):
|
||||
return filtered
|
||||
|
||||
|
||||
has_emeter = pytest.mark.parametrize(
|
||||
"dev", filter_model("has emeter", EMETER), indirect=True
|
||||
)
|
||||
no_emeter = pytest.mark.parametrize(
|
||||
"dev", filter_model("no emeter", ALL_DEVICES - EMETER), indirect=True
|
||||
)
|
||||
def parametrize(desc, devices, ids=None):
|
||||
# if ids is None:
|
||||
# ids = ["on", "off"]
|
||||
return pytest.mark.parametrize(
|
||||
"dev", filter_model(desc, devices), indirect=True, ids=ids
|
||||
)
|
||||
|
||||
bulb = pytest.mark.parametrize("dev", filter_model("bulbs", BULBS), indirect=True)
|
||||
plug = pytest.mark.parametrize("dev", filter_model("plugs", PLUGS), indirect=True)
|
||||
strip = pytest.mark.parametrize("dev", filter_model("strips", STRIPS), indirect=True)
|
||||
dimmer = pytest.mark.parametrize("dev", filter_model("dimmers", DIMMERS), indirect=True)
|
||||
|
||||
has_emeter = parametrize("has emeter", WITH_EMETER)
|
||||
no_emeter = parametrize("no emeter", ALL_DEVICES - WITH_EMETER)
|
||||
|
||||
|
||||
def name_for_filename(x):
|
||||
from os.path import basename
|
||||
|
||||
return basename(x)
|
||||
|
||||
|
||||
bulb = parametrize("bulbs", BULBS, ids=name_for_filename)
|
||||
plug = parametrize("plugs", PLUGS, ids=name_for_filename)
|
||||
strip = parametrize("strips", STRIPS, ids=name_for_filename)
|
||||
dimmer = parametrize("dimmers", DIMMERS, ids=name_for_filename)
|
||||
|
||||
# This ensures that every single file inside fixtures/ is being placed in some category
|
||||
categorized_fixtures = set(dimmer.args[1] + strip.args[1] + plug.args[1] + bulb.args[1])
|
||||
@@ -62,29 +75,14 @@ if diff:
|
||||
)
|
||||
raise Exception("Missing category for %s" % diff)
|
||||
|
||||
dimmable = pytest.mark.parametrize(
|
||||
"dev", filter_model("dimmable", DIMMABLE), indirect=True
|
||||
)
|
||||
non_dimmable = pytest.mark.parametrize(
|
||||
"dev",
|
||||
filter_model("non-dimmable", ALL_DEVICES - DIMMABLE - STRIPS - PLUGS),
|
||||
indirect=True,
|
||||
)
|
||||
|
||||
variable_temp = pytest.mark.parametrize(
|
||||
"dev", filter_model("variable color temp", VARIABLE_TEMP), indirect=True
|
||||
)
|
||||
non_variable_temp = pytest.mark.parametrize(
|
||||
"dev", filter_model("non-variable color temp", BULBS - VARIABLE_TEMP), indirect=True
|
||||
)
|
||||
|
||||
color_bulb = pytest.mark.parametrize(
|
||||
"dev", filter_model("color bulbs", COLOR_BULBS), indirect=True
|
||||
)
|
||||
non_color_bulb = pytest.mark.parametrize(
|
||||
"dev", filter_model("non-color bulbs", BULBS - COLOR_BULBS), indirect=True
|
||||
)
|
||||
|
||||
# bulb types
|
||||
dimmable = parametrize("dimmable", DIMMABLE)
|
||||
non_dimmable = parametrize("non-dimmable", BULBS - DIMMABLE)
|
||||
variable_temp = parametrize("variable color temp", VARIABLE_TEMP)
|
||||
non_variable_temp = parametrize("non-variable color temp", BULBS - VARIABLE_TEMP)
|
||||
color_bulb = parametrize("color bulbs", COLOR_BULBS)
|
||||
non_color_bulb = parametrize("non-color bulbs", BULBS - COLOR_BULBS)
|
||||
|
||||
# Parametrize tests to run with device both on and off
|
||||
turn_on = pytest.mark.parametrize("turn_on", [True, False])
|
||||
@@ -97,6 +95,10 @@ async def handle_turn_on(dev, turn_on):
|
||||
await dev.turn_off()
|
||||
|
||||
|
||||
# to avoid adding this for each async function separately
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
@pytest.fixture(params=SUPPORTED_DEVICES)
|
||||
def dev(request):
|
||||
"""Device fixture.
|
||||
@@ -112,7 +114,7 @@ def dev(request):
|
||||
asyncio.run(d.update())
|
||||
if d.model in file:
|
||||
return d
|
||||
return
|
||||
raise Exception("Unable to find type for %s" % ip)
|
||||
|
||||
def device_for_file(model):
|
||||
for d in STRIPS:
|
||||
|
@@ -113,6 +113,27 @@ PLUG_SCHEMA = Schema(
|
||||
extra=REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
LIGHT_STATE_SCHEMA = Schema(
|
||||
{
|
||||
"brightness": All(int, Range(min=0, max=100)),
|
||||
"color_temp": int,
|
||||
"hue": All(int, Range(min=0, max=255)),
|
||||
"mode": str,
|
||||
"on_off": check_int_bool,
|
||||
"saturation": All(int, Range(min=0, max=255)),
|
||||
"dft_on_state": Optional(
|
||||
{
|
||||
"brightness": All(int, Range(min=0, max=100)),
|
||||
"color_temp": All(int, Range(min=2000, max=9000)),
|
||||
"hue": All(int, Range(min=0, max=255)),
|
||||
"mode": str,
|
||||
"saturation": All(int, Range(min=0, max=255)),
|
||||
}
|
||||
),
|
||||
"err_code": int,
|
||||
}
|
||||
)
|
||||
|
||||
BULB_SCHEMA = PLUG_SCHEMA.extend(
|
||||
{
|
||||
"ctrl_protocols": Optional(dict),
|
||||
@@ -124,24 +145,7 @@ BULB_SCHEMA = PLUG_SCHEMA.extend(
|
||||
"is_dimmable": check_int_bool,
|
||||
"is_factory": bool,
|
||||
"is_variable_color_temp": check_int_bool,
|
||||
"light_state": {
|
||||
"brightness": All(int, Range(min=0, max=100)),
|
||||
"color_temp": int,
|
||||
"hue": All(int, Range(min=0, max=255)),
|
||||
"mode": str,
|
||||
"on_off": check_int_bool,
|
||||
"saturation": All(int, Range(min=0, max=255)),
|
||||
"dft_on_state": Optional(
|
||||
{
|
||||
"brightness": All(int, Range(min=0, max=100)),
|
||||
"color_temp": All(int, Range(min=2000, max=9000)),
|
||||
"hue": All(int, Range(min=0, max=255)),
|
||||
"mode": str,
|
||||
"saturation": All(int, Range(min=0, max=255)),
|
||||
}
|
||||
),
|
||||
"err_code": int,
|
||||
},
|
||||
"light_state": LIGHT_STATE_SCHEMA,
|
||||
"preferred_state": [
|
||||
{
|
||||
"brightness": All(int, Range(min=0, max=100)),
|
||||
|
187
kasa/tests/test_bulb.py
Normal file
187
kasa/tests/test_bulb.py
Normal file
@@ -0,0 +1,187 @@
|
||||
import pytest
|
||||
|
||||
from kasa import DeviceType, SmartDeviceException
|
||||
|
||||
from .conftest import (
|
||||
bulb,
|
||||
color_bulb,
|
||||
dimmable,
|
||||
handle_turn_on,
|
||||
non_color_bulb,
|
||||
non_dimmable,
|
||||
non_variable_temp,
|
||||
turn_on,
|
||||
variable_temp,
|
||||
)
|
||||
from .newfakes import BULB_SCHEMA, LIGHT_STATE_SCHEMA
|
||||
|
||||
|
||||
@bulb
|
||||
async def test_bulb_sysinfo(dev):
|
||||
assert dev.sys_info is not None
|
||||
BULB_SCHEMA(dev.sys_info)
|
||||
|
||||
assert dev.model is not None
|
||||
|
||||
assert dev.device_type == DeviceType.Bulb
|
||||
assert dev.is_bulb
|
||||
|
||||
|
||||
@bulb
|
||||
async def test_state_attributes(dev):
|
||||
assert "Brightness" in dev.state_information
|
||||
assert dev.state_information["Brightness"] == dev.brightness
|
||||
|
||||
assert "Is dimmable" in dev.state_information
|
||||
assert dev.state_information["Is dimmable"] == dev.is_dimmable
|
||||
|
||||
|
||||
@bulb
|
||||
async def test_light_state_without_update(dev, monkeypatch):
|
||||
with pytest.raises(SmartDeviceException):
|
||||
monkeypatch.setitem(
|
||||
dev._last_update["system"]["get_sysinfo"], "light_state", None
|
||||
)
|
||||
print(dev.light_state)
|
||||
|
||||
|
||||
@bulb
|
||||
async def test_get_light_state(dev):
|
||||
LIGHT_STATE_SCHEMA(await dev.get_light_state())
|
||||
|
||||
|
||||
@color_bulb
|
||||
@turn_on
|
||||
async def test_hsv(dev, turn_on):
|
||||
await handle_turn_on(dev, turn_on)
|
||||
assert dev.is_color
|
||||
|
||||
hue, saturation, brightness = dev.hsv
|
||||
assert 0 <= hue <= 255
|
||||
assert 0 <= saturation <= 100
|
||||
assert 0 <= brightness <= 100
|
||||
|
||||
await dev.set_hsv(hue=1, saturation=1, value=1)
|
||||
|
||||
hue, saturation, brightness = dev.hsv
|
||||
assert hue == 1
|
||||
assert saturation == 1
|
||||
assert brightness == 1
|
||||
|
||||
|
||||
@color_bulb
|
||||
@turn_on
|
||||
async def test_invalid_hsv(dev, turn_on):
|
||||
await handle_turn_on(dev, turn_on)
|
||||
assert dev.is_color
|
||||
|
||||
for invalid_hue in [-1, 361, 0.5]:
|
||||
with pytest.raises(ValueError):
|
||||
await dev.set_hsv(invalid_hue, 0, 0)
|
||||
|
||||
for invalid_saturation in [-1, 101, 0.5]:
|
||||
with pytest.raises(ValueError):
|
||||
await dev.set_hsv(0, invalid_saturation, 0)
|
||||
|
||||
for invalid_brightness in [-1, 101, 0.5]:
|
||||
with pytest.raises(ValueError):
|
||||
await dev.set_hsv(0, 0, invalid_brightness)
|
||||
|
||||
|
||||
@color_bulb
|
||||
async def test_color_state_information(dev):
|
||||
assert "HSV" in dev.state_information
|
||||
assert dev.state_information["HSV"] == dev.hsv
|
||||
|
||||
|
||||
@non_color_bulb
|
||||
async def test_hsv_on_non_color(dev):
|
||||
assert not dev.is_color
|
||||
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev.set_hsv(0, 0, 0)
|
||||
with pytest.raises(SmartDeviceException):
|
||||
print(dev.hsv)
|
||||
|
||||
|
||||
@variable_temp
|
||||
async def test_variable_temp_state_information(dev):
|
||||
assert "Color temperature" in dev.state_information
|
||||
assert dev.state_information["Color temperature"] == dev.color_temp
|
||||
|
||||
assert "Valid temperature range" in dev.state_information
|
||||
assert (
|
||||
dev.state_information["Valid temperature range"] == dev.valid_temperature_range
|
||||
)
|
||||
|
||||
|
||||
@variable_temp
|
||||
@turn_on
|
||||
async def test_try_set_colortemp(dev, turn_on):
|
||||
await handle_turn_on(dev, turn_on)
|
||||
await dev.set_color_temp(2700)
|
||||
assert dev.color_temp == 2700
|
||||
|
||||
|
||||
@variable_temp
|
||||
async def test_unknown_temp_range(dev, monkeypatch):
|
||||
with pytest.raises(SmartDeviceException):
|
||||
monkeypatch.setitem(dev._sys_info, "model", "unknown bulb")
|
||||
dev.valid_temperature_range()
|
||||
|
||||
|
||||
@variable_temp
|
||||
async def test_out_of_range_temperature(dev):
|
||||
with pytest.raises(ValueError):
|
||||
await dev.set_color_temp(1000)
|
||||
with pytest.raises(ValueError):
|
||||
await dev.set_color_temp(10000)
|
||||
|
||||
|
||||
@non_variable_temp
|
||||
async def test_non_variable_temp(dev):
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev.set_color_temp(2700)
|
||||
|
||||
with pytest.raises(SmartDeviceException):
|
||||
dev.valid_temperature_range()
|
||||
|
||||
with pytest.raises(SmartDeviceException):
|
||||
print(dev.color_temp)
|
||||
|
||||
|
||||
@dimmable
|
||||
@turn_on
|
||||
async def test_dimmable_brightness(dev, turn_on):
|
||||
await handle_turn_on(dev, turn_on)
|
||||
assert dev.is_dimmable
|
||||
|
||||
await dev.set_brightness(50)
|
||||
assert dev.brightness == 50
|
||||
|
||||
await dev.set_brightness(10)
|
||||
assert dev.brightness == 10
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await dev.set_brightness("foo")
|
||||
|
||||
|
||||
@dimmable
|
||||
async def test_invalid_brightness(dev):
|
||||
assert dev.is_dimmable
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await dev.set_brightness(110)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await dev.set_brightness(-100)
|
||||
|
||||
|
||||
@non_dimmable
|
||||
async def test_non_dimmable(dev):
|
||||
assert not dev.is_dimmable
|
||||
|
||||
with pytest.raises(SmartDeviceException):
|
||||
assert dev.brightness == 0
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev.set_brightness(100)
|
@@ -28,9 +28,6 @@ async def test_state(dev, turn_on):
|
||||
else:
|
||||
assert "Device state: OFF" in res.output
|
||||
|
||||
if not dev.has_emeter:
|
||||
assert "Device has no emeter" in res.output
|
||||
|
||||
|
||||
async def test_alias(dev):
|
||||
runner = CliRunner()
|
||||
|
117
kasa/tests/test_emeter.py
Normal file
117
kasa/tests/test_emeter.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import pytest
|
||||
|
||||
from kasa import SmartDeviceException
|
||||
|
||||
from .conftest import has_emeter, no_emeter
|
||||
from .newfakes import CURRENT_CONSUMPTION_SCHEMA
|
||||
|
||||
|
||||
@no_emeter
|
||||
async def test_no_emeter(dev):
|
||||
assert not dev.has_emeter
|
||||
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev.get_emeter_realtime()
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev.get_emeter_daily()
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev.get_emeter_monthly()
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev.erase_emeter_stats()
|
||||
|
||||
|
||||
@has_emeter
|
||||
async def test_get_emeter_realtime(dev):
|
||||
if dev.is_strip:
|
||||
pytest.skip("Disabled for strips temporarily")
|
||||
|
||||
assert dev.has_emeter
|
||||
|
||||
current_emeter = await dev.get_emeter_realtime()
|
||||
CURRENT_CONSUMPTION_SCHEMA(current_emeter)
|
||||
|
||||
|
||||
@has_emeter
|
||||
async def test_get_emeter_daily(dev):
|
||||
if dev.is_strip:
|
||||
pytest.skip("Disabled for strips temporarily")
|
||||
|
||||
assert dev.has_emeter
|
||||
|
||||
assert await dev.get_emeter_daily(year=1900, month=1) == {}
|
||||
|
||||
d = await dev.get_emeter_daily()
|
||||
assert len(d) > 0
|
||||
|
||||
k, v = d.popitem()
|
||||
assert isinstance(k, int)
|
||||
assert isinstance(v, float)
|
||||
|
||||
# Test kwh (energy, energy_wh)
|
||||
d = await dev.get_emeter_daily(kwh=False)
|
||||
k2, v2 = d.popitem()
|
||||
assert v * 1000 == v2
|
||||
|
||||
|
||||
@has_emeter
|
||||
async def test_get_emeter_monthly(dev):
|
||||
if dev.is_strip:
|
||||
pytest.skip("Disabled for strips temporarily")
|
||||
|
||||
assert dev.has_emeter
|
||||
|
||||
assert await dev.get_emeter_monthly(year=1900) == {}
|
||||
|
||||
d = await dev.get_emeter_monthly()
|
||||
assert len(d) > 0
|
||||
|
||||
k, v = d.popitem()
|
||||
assert isinstance(k, int)
|
||||
assert isinstance(v, float)
|
||||
|
||||
# Test kwh (energy, energy_wh)
|
||||
d = await dev.get_emeter_monthly(kwh=False)
|
||||
k2, v2 = d.popitem()
|
||||
assert v * 1000 == v2
|
||||
|
||||
|
||||
@has_emeter
|
||||
async def test_emeter_status(dev):
|
||||
if dev.is_strip:
|
||||
pytest.skip("Disabled for strips temporarily")
|
||||
|
||||
assert dev.has_emeter
|
||||
|
||||
d = await dev.get_emeter_realtime()
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
assert d["foo"]
|
||||
|
||||
assert d["power_mw"] == d["power"] * 1000
|
||||
# bulbs have only power according to tplink simulator.
|
||||
if not dev.is_bulb:
|
||||
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):
|
||||
assert dev.has_emeter
|
||||
|
||||
await dev.erase_emeter()
|
||||
|
||||
|
||||
@has_emeter
|
||||
async def test_current_consumption(dev):
|
||||
if dev.is_strip:
|
||||
pytest.skip("Disabled for strips temporarily")
|
||||
|
||||
if dev.has_emeter:
|
||||
x = await dev.current_consumption()
|
||||
assert isinstance(x, float)
|
||||
assert x >= 0.0
|
||||
else:
|
||||
assert await dev.current_consumption() is None
|
@@ -1,518 +0,0 @@
|
||||
from datetime import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest # type: ignore # https://github.com/pytest-dev/pytest/issues/3342
|
||||
|
||||
from kasa import DeviceType, SmartDeviceException, SmartStrip
|
||||
|
||||
from .conftest import (
|
||||
bulb,
|
||||
color_bulb,
|
||||
dimmable,
|
||||
handle_turn_on,
|
||||
has_emeter,
|
||||
no_emeter,
|
||||
non_color_bulb,
|
||||
non_dimmable,
|
||||
non_variable_temp,
|
||||
plug,
|
||||
strip,
|
||||
turn_on,
|
||||
variable_temp,
|
||||
)
|
||||
from .newfakes import (
|
||||
BULB_SCHEMA,
|
||||
CURRENT_CONSUMPTION_SCHEMA,
|
||||
PLUG_SCHEMA,
|
||||
TZ_SCHEMA,
|
||||
FakeTransportProtocol,
|
||||
)
|
||||
|
||||
# to avoid adding this for each async function separately
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
@plug
|
||||
async def test_plug_sysinfo(dev):
|
||||
assert dev.sys_info is not None
|
||||
PLUG_SCHEMA(dev.sys_info)
|
||||
|
||||
assert dev.model is not None
|
||||
|
||||
assert dev.device_type == DeviceType.Plug or dev.device_type == DeviceType.Strip
|
||||
assert dev.is_plug or dev.is_strip
|
||||
|
||||
|
||||
@bulb
|
||||
async def test_bulb_sysinfo(dev):
|
||||
assert dev.sys_info is not None
|
||||
BULB_SCHEMA(dev.sys_info)
|
||||
|
||||
assert dev.model is not None
|
||||
|
||||
assert dev.device_type == DeviceType.Bulb
|
||||
assert dev.is_bulb
|
||||
|
||||
|
||||
async def test_state_info(dev):
|
||||
assert isinstance(dev.state_information, dict)
|
||||
|
||||
|
||||
async def test_invalid_connection(dev):
|
||||
with patch.object(FakeTransportProtocol, "query", side_effect=SmartDeviceException):
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev.update()
|
||||
dev.is_on
|
||||
|
||||
|
||||
async def test_query_helper(dev):
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev._query_helper("test", "testcmd", {})
|
||||
# TODO check for unwrapping?
|
||||
|
||||
|
||||
@turn_on
|
||||
async def test_state(dev, turn_on):
|
||||
await handle_turn_on(dev, turn_on)
|
||||
orig_state = dev.is_on
|
||||
if orig_state:
|
||||
await dev.turn_off()
|
||||
assert not dev.is_on
|
||||
assert dev.is_off
|
||||
|
||||
await dev.turn_on()
|
||||
assert dev.is_on
|
||||
assert not dev.is_off
|
||||
else:
|
||||
await dev.turn_on()
|
||||
assert dev.is_on
|
||||
assert not dev.is_off
|
||||
|
||||
await dev.turn_off()
|
||||
assert not dev.is_on
|
||||
assert dev.is_off
|
||||
|
||||
|
||||
@no_emeter
|
||||
async def test_no_emeter(dev):
|
||||
assert not dev.has_emeter
|
||||
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev.get_emeter_realtime()
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev.get_emeter_daily()
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev.get_emeter_monthly()
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev.erase_emeter_stats()
|
||||
|
||||
|
||||
@has_emeter
|
||||
async def test_get_emeter_realtime(dev):
|
||||
if dev.is_strip:
|
||||
pytest.skip("Disabled for strips temporarily")
|
||||
|
||||
assert dev.has_emeter
|
||||
|
||||
current_emeter = await dev.get_emeter_realtime()
|
||||
CURRENT_CONSUMPTION_SCHEMA(current_emeter)
|
||||
|
||||
|
||||
@has_emeter
|
||||
async def test_get_emeter_daily(dev):
|
||||
if dev.is_strip:
|
||||
pytest.skip("Disabled for strips temporarily")
|
||||
|
||||
assert dev.has_emeter
|
||||
|
||||
assert await dev.get_emeter_daily(year=1900, month=1) == {}
|
||||
|
||||
d = await dev.get_emeter_daily()
|
||||
assert len(d) > 0
|
||||
|
||||
k, v = d.popitem()
|
||||
assert isinstance(k, int)
|
||||
assert isinstance(v, float)
|
||||
|
||||
# Test kwh (energy, energy_wh)
|
||||
d = await dev.get_emeter_daily(kwh=False)
|
||||
k2, v2 = d.popitem()
|
||||
assert v * 1000 == v2
|
||||
|
||||
|
||||
@has_emeter
|
||||
async def test_get_emeter_monthly(dev):
|
||||
if dev.is_strip:
|
||||
pytest.skip("Disabled for strips temporarily")
|
||||
|
||||
assert dev.has_emeter
|
||||
|
||||
assert await dev.get_emeter_monthly(year=1900) == {}
|
||||
|
||||
d = await dev.get_emeter_monthly()
|
||||
assert len(d) > 0
|
||||
|
||||
k, v = d.popitem()
|
||||
assert isinstance(k, int)
|
||||
assert isinstance(v, float)
|
||||
|
||||
# Test kwh (energy, energy_wh)
|
||||
d = await dev.get_emeter_monthly(kwh=False)
|
||||
k2, v2 = d.popitem()
|
||||
assert v * 1000 == v2
|
||||
|
||||
|
||||
@has_emeter
|
||||
async def test_emeter_status(dev):
|
||||
if dev.is_strip:
|
||||
pytest.skip("Disabled for strips temporarily")
|
||||
|
||||
assert dev.has_emeter
|
||||
|
||||
d = await dev.get_emeter_realtime()
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
assert d["foo"]
|
||||
|
||||
assert d["power_mw"] == d["power"] * 1000
|
||||
# bulbs have only power according to tplink simulator.
|
||||
if not dev.is_bulb:
|
||||
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):
|
||||
assert dev.has_emeter
|
||||
|
||||
await dev.erase_emeter()
|
||||
|
||||
|
||||
@has_emeter
|
||||
async def test_current_consumption(dev):
|
||||
if dev.is_strip:
|
||||
pytest.skip("Disabled for strips temporarily")
|
||||
|
||||
if dev.has_emeter:
|
||||
x = await dev.current_consumption()
|
||||
assert isinstance(x, float)
|
||||
assert x >= 0.0
|
||||
else:
|
||||
assert await dev.current_consumption() is None
|
||||
|
||||
|
||||
async def test_alias(dev):
|
||||
test_alias = "TEST1234"
|
||||
original = dev.alias
|
||||
|
||||
assert isinstance(original, str)
|
||||
await dev.set_alias(test_alias)
|
||||
assert dev.alias == test_alias
|
||||
|
||||
await dev.set_alias(original)
|
||||
assert dev.alias == original
|
||||
|
||||
|
||||
@plug
|
||||
async def test_led(dev):
|
||||
original = dev.led
|
||||
|
||||
await dev.set_led(False)
|
||||
assert not dev.led
|
||||
|
||||
await dev.set_led(True)
|
||||
assert dev.led
|
||||
|
||||
await dev.set_led(original)
|
||||
|
||||
|
||||
@turn_on
|
||||
async def test_on_since(dev, turn_on):
|
||||
await handle_turn_on(dev, turn_on)
|
||||
orig_state = dev.is_on
|
||||
if "on_time" not in dev.sys_info and not dev.is_strip:
|
||||
assert dev.on_since is None
|
||||
elif orig_state:
|
||||
assert isinstance(dev.on_since, datetime)
|
||||
else:
|
||||
assert dev.on_since is None
|
||||
|
||||
|
||||
async def test_icon(dev):
|
||||
assert set((await dev.get_icon()).keys()), {"icon", "hash"}
|
||||
|
||||
|
||||
async def test_time(dev):
|
||||
assert isinstance(await dev.get_time(), datetime)
|
||||
# TODO check setting?
|
||||
|
||||
|
||||
async def test_timezone(dev):
|
||||
TZ_SCHEMA(await dev.get_timezone())
|
||||
|
||||
|
||||
async def test_hw_info(dev):
|
||||
PLUG_SCHEMA(dev.hw_info)
|
||||
|
||||
|
||||
async def test_location(dev):
|
||||
PLUG_SCHEMA(dev.location)
|
||||
|
||||
|
||||
async def test_rssi(dev):
|
||||
PLUG_SCHEMA({"rssi": dev.rssi}) # wrapping for vol
|
||||
|
||||
|
||||
async def test_mac(dev):
|
||||
PLUG_SCHEMA({"mac": dev.mac}) # wrapping for val
|
||||
# TODO check setting?
|
||||
|
||||
|
||||
@non_variable_temp
|
||||
async def test_temperature_on_nonsupporting(dev):
|
||||
assert dev.valid_temperature_range == (0, 0)
|
||||
|
||||
# TODO test when device does not support temperature range
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev.set_color_temp(2700)
|
||||
with pytest.raises(SmartDeviceException):
|
||||
print(dev.color_temp)
|
||||
|
||||
|
||||
@variable_temp
|
||||
async def test_out_of_range_temperature(dev):
|
||||
with pytest.raises(ValueError):
|
||||
await dev.set_color_temp(1000)
|
||||
with pytest.raises(ValueError):
|
||||
await dev.set_color_temp(10000)
|
||||
|
||||
|
||||
@non_dimmable
|
||||
async def test_non_dimmable(dev):
|
||||
assert not dev.is_dimmable
|
||||
|
||||
with pytest.raises(SmartDeviceException):
|
||||
assert dev.brightness == 0
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev.set_brightness(100)
|
||||
|
||||
|
||||
@dimmable
|
||||
@turn_on
|
||||
async def test_dimmable_brightness(dev, turn_on):
|
||||
await handle_turn_on(dev, turn_on)
|
||||
assert dev.is_dimmable
|
||||
|
||||
await dev.set_brightness(50)
|
||||
assert dev.brightness == 50
|
||||
|
||||
await dev.set_brightness(10)
|
||||
assert dev.brightness == 10
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await dev.set_brightness("foo")
|
||||
|
||||
|
||||
@dimmable
|
||||
async def test_invalid_brightness(dev):
|
||||
assert dev.is_dimmable
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await dev.set_brightness(110)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await dev.set_brightness(-100)
|
||||
|
||||
|
||||
@color_bulb
|
||||
@turn_on
|
||||
async def test_hsv(dev, turn_on):
|
||||
await handle_turn_on(dev, turn_on)
|
||||
assert dev.is_color
|
||||
|
||||
hue, saturation, brightness = dev.hsv
|
||||
assert 0 <= hue <= 255
|
||||
assert 0 <= saturation <= 100
|
||||
assert 0 <= brightness <= 100
|
||||
|
||||
await dev.set_hsv(hue=1, saturation=1, value=1)
|
||||
|
||||
hue, saturation, brightness = dev.hsv
|
||||
assert hue == 1
|
||||
assert saturation == 1
|
||||
assert brightness == 1
|
||||
|
||||
|
||||
@color_bulb
|
||||
@turn_on
|
||||
async def test_invalid_hsv(dev, turn_on):
|
||||
await handle_turn_on(dev, turn_on)
|
||||
assert dev.is_color
|
||||
|
||||
for invalid_hue in [-1, 361, 0.5]:
|
||||
with pytest.raises(ValueError):
|
||||
await dev.set_hsv(invalid_hue, 0, 0)
|
||||
|
||||
for invalid_saturation in [-1, 101, 0.5]:
|
||||
with pytest.raises(ValueError):
|
||||
await dev.set_hsv(0, invalid_saturation, 0)
|
||||
|
||||
for invalid_brightness in [-1, 101, 0.5]:
|
||||
with pytest.raises(ValueError):
|
||||
await dev.set_hsv(0, 0, invalid_brightness)
|
||||
|
||||
|
||||
@non_color_bulb
|
||||
async def test_hsv_on_non_color(dev):
|
||||
assert not dev.is_color
|
||||
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev.set_hsv(0, 0, 0)
|
||||
with pytest.raises(SmartDeviceException):
|
||||
print(dev.hsv)
|
||||
|
||||
|
||||
@variable_temp
|
||||
@turn_on
|
||||
async def test_try_set_colortemp(dev, turn_on):
|
||||
await handle_turn_on(dev, turn_on)
|
||||
await dev.set_color_temp(2700)
|
||||
assert dev.color_temp == 2700
|
||||
|
||||
|
||||
@non_variable_temp
|
||||
async def test_non_variable_temp(dev):
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev.set_color_temp(2700)
|
||||
|
||||
|
||||
@strip
|
||||
@turn_on
|
||||
async def test_children_change_state(dev, turn_on):
|
||||
await handle_turn_on(dev, turn_on)
|
||||
for plug in dev.plugs:
|
||||
orig_state = plug.is_on
|
||||
if orig_state:
|
||||
await plug.turn_off()
|
||||
assert not plug.is_on
|
||||
assert plug.is_off
|
||||
|
||||
await plug.turn_on()
|
||||
assert plug.is_on
|
||||
assert not plug.is_off
|
||||
else:
|
||||
await plug.turn_on()
|
||||
assert plug.is_on
|
||||
assert not plug.is_off
|
||||
|
||||
await plug.turn_off()
|
||||
assert not plug.is_on
|
||||
assert plug.is_off
|
||||
|
||||
|
||||
@strip
|
||||
async def test_children_alias(dev):
|
||||
test_alias = "TEST1234"
|
||||
for plug in dev.plugs:
|
||||
original = plug.alias
|
||||
await plug.set_alias(alias=test_alias)
|
||||
await dev.update() # TODO: set_alias does not call parent's update()..
|
||||
assert plug.alias == test_alias
|
||||
|
||||
await plug.set_alias(alias=original)
|
||||
await dev.update() # TODO: set_alias does not call parent's update()..
|
||||
assert plug.alias == original
|
||||
|
||||
|
||||
@strip
|
||||
async def test_children_on_since(dev):
|
||||
on_sinces = []
|
||||
for plug in dev.plugs:
|
||||
if plug.is_on:
|
||||
on_sinces.append(plug.on_since)
|
||||
assert isinstance(plug.on_since, datetime)
|
||||
else:
|
||||
assert plug.on_since is None
|
||||
|
||||
if dev.is_off:
|
||||
assert dev.on_since is None
|
||||
# TODO: testing this would require some mocking utcnow which is not
|
||||
# very straightforward.
|
||||
# else:
|
||||
# assert dev.on_since == max(on_sinces)
|
||||
|
||||
|
||||
@strip
|
||||
async def test_get_plug_by_name(dev: SmartStrip):
|
||||
name = dev.plugs[0].alias
|
||||
assert dev.get_plug_by_name(name) == dev.plugs[0]
|
||||
|
||||
with pytest.raises(SmartDeviceException):
|
||||
dev.get_plug_by_name("NONEXISTING NAME")
|
||||
|
||||
|
||||
@strip
|
||||
async def test_get_plug_by_index(dev: SmartStrip):
|
||||
assert dev.get_plug_by_index(0) == dev.plugs[0]
|
||||
|
||||
with pytest.raises(SmartDeviceException):
|
||||
dev.get_plug_by_index(-1)
|
||||
|
||||
with pytest.raises(SmartDeviceException):
|
||||
dev.get_plug_by_index(len(dev.plugs))
|
||||
|
||||
|
||||
@pytest.mark.skip("this test will wear out your relays")
|
||||
async def test_all_binary_states(dev):
|
||||
# test every binary state
|
||||
# TODO: this needs to be fixed, dev.plugs is not available for each device..
|
||||
for state in range(2 ** len(dev.plugs)):
|
||||
# create binary state map
|
||||
state_map = {}
|
||||
for plug_index in range(len(dev.plugs)):
|
||||
state_map[plug_index] = bool((state >> plug_index) & 1)
|
||||
|
||||
if state_map[plug_index]:
|
||||
await dev.turn_on(index=plug_index)
|
||||
else:
|
||||
await dev.turn_off(index=plug_index)
|
||||
|
||||
# check state map applied
|
||||
for index, state in dev.is_on.items():
|
||||
assert state_map[index] == state
|
||||
|
||||
# toggle each outlet with state map applied
|
||||
for plug_index in range(len(dev.plugs)):
|
||||
|
||||
# toggle state
|
||||
if state_map[plug_index]:
|
||||
await dev.turn_off(index=plug_index)
|
||||
else:
|
||||
await dev.turn_on(index=plug_index)
|
||||
|
||||
# only target outlet should have state changed
|
||||
for index, state in dev.is_on.items():
|
||||
if index == plug_index:
|
||||
assert state != state_map[index]
|
||||
else:
|
||||
assert state == state_map[index]
|
||||
|
||||
# reset state
|
||||
if state_map[plug_index]:
|
||||
await dev.turn_on(index=plug_index)
|
||||
else:
|
||||
await dev.turn_off(index=plug_index)
|
||||
|
||||
# original state map should be restored
|
||||
for index, state in dev.is_on.items():
|
||||
assert state == state_map[index]
|
||||
|
||||
|
||||
async def test_representation(dev):
|
||||
import re
|
||||
|
||||
pattern = re.compile("<.* model .* at .* (.*), is_on: .* - dev specific: .*>")
|
||||
assert pattern.match(str(dev))
|
28
kasa/tests/test_plug.py
Normal file
28
kasa/tests/test_plug.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from kasa import DeviceType
|
||||
|
||||
from .conftest import plug
|
||||
from .newfakes import PLUG_SCHEMA
|
||||
|
||||
|
||||
@plug
|
||||
async def test_plug_sysinfo(dev):
|
||||
assert dev.sys_info is not None
|
||||
PLUG_SCHEMA(dev.sys_info)
|
||||
|
||||
assert dev.model is not None
|
||||
|
||||
assert dev.device_type == DeviceType.Plug or dev.device_type == DeviceType.Strip
|
||||
assert dev.is_plug or dev.is_strip
|
||||
|
||||
|
||||
@plug
|
||||
async def test_led(dev):
|
||||
original = dev.led
|
||||
|
||||
await dev.set_led(False)
|
||||
assert not dev.led
|
||||
|
||||
await dev.set_led(True)
|
||||
assert dev.led
|
||||
|
||||
await dev.set_led(original)
|
111
kasa/tests/test_smartdevice.py
Normal file
111
kasa/tests/test_smartdevice.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from datetime import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest # type: ignore # https://github.com/pytest-dev/pytest/issues/3342
|
||||
|
||||
from kasa import SmartDeviceException
|
||||
|
||||
from .conftest import handle_turn_on, turn_on
|
||||
from .newfakes import PLUG_SCHEMA, TZ_SCHEMA, FakeTransportProtocol
|
||||
|
||||
|
||||
async def test_state_info(dev):
|
||||
assert isinstance(dev.state_information, dict)
|
||||
|
||||
|
||||
async def test_invalid_connection(dev):
|
||||
with patch.object(FakeTransportProtocol, "query", side_effect=SmartDeviceException):
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev.update()
|
||||
dev.is_on
|
||||
|
||||
|
||||
async def test_query_helper(dev):
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev._query_helper("test", "testcmd", {})
|
||||
# TODO check for unwrapping?
|
||||
|
||||
|
||||
@turn_on
|
||||
async def test_state(dev, turn_on):
|
||||
await handle_turn_on(dev, turn_on)
|
||||
orig_state = dev.is_on
|
||||
if orig_state:
|
||||
await dev.turn_off()
|
||||
assert not dev.is_on
|
||||
assert dev.is_off
|
||||
|
||||
await dev.turn_on()
|
||||
assert dev.is_on
|
||||
assert not dev.is_off
|
||||
else:
|
||||
await dev.turn_on()
|
||||
assert dev.is_on
|
||||
assert not dev.is_off
|
||||
|
||||
await dev.turn_off()
|
||||
assert not dev.is_on
|
||||
assert dev.is_off
|
||||
|
||||
|
||||
async def test_alias(dev):
|
||||
test_alias = "TEST1234"
|
||||
original = dev.alias
|
||||
|
||||
assert isinstance(original, str)
|
||||
await dev.set_alias(test_alias)
|
||||
assert dev.alias == test_alias
|
||||
|
||||
await dev.set_alias(original)
|
||||
assert dev.alias == original
|
||||
|
||||
|
||||
@turn_on
|
||||
async def test_on_since(dev, turn_on):
|
||||
await handle_turn_on(dev, turn_on)
|
||||
orig_state = dev.is_on
|
||||
if "on_time" not in dev.sys_info and not dev.is_strip:
|
||||
assert dev.on_since is None
|
||||
elif orig_state:
|
||||
assert isinstance(dev.on_since, datetime)
|
||||
else:
|
||||
assert dev.on_since is None
|
||||
|
||||
|
||||
async def test_time(dev):
|
||||
assert isinstance(await dev.get_time(), datetime)
|
||||
|
||||
|
||||
async def test_timezone(dev):
|
||||
TZ_SCHEMA(await dev.get_timezone())
|
||||
|
||||
|
||||
async def test_hw_info(dev):
|
||||
PLUG_SCHEMA(dev.hw_info)
|
||||
|
||||
|
||||
async def test_location(dev):
|
||||
PLUG_SCHEMA(dev.location)
|
||||
|
||||
|
||||
async def test_rssi(dev):
|
||||
PLUG_SCHEMA({"rssi": dev.rssi}) # wrapping for vol
|
||||
|
||||
|
||||
async def test_mac(dev):
|
||||
PLUG_SCHEMA({"mac": dev.mac}) # wrapping for val
|
||||
|
||||
|
||||
async def test_representation(dev):
|
||||
import re
|
||||
|
||||
pattern = re.compile("<.* model .* at .* (.*), is_on: .* - dev specific: .*>")
|
||||
assert pattern.match(str(dev))
|
||||
|
||||
|
||||
async def test_childrens(dev):
|
||||
"""Make sure that children property is exposed by every device."""
|
||||
if dev.is_strip:
|
||||
assert len(dev.children) > 0
|
||||
else:
|
||||
assert len(dev.children) == 0
|
129
kasa/tests/test_strip.py
Normal file
129
kasa/tests/test_strip.py
Normal file
@@ -0,0 +1,129 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from kasa import SmartDeviceException, SmartStrip
|
||||
|
||||
from .conftest import handle_turn_on, strip, turn_on
|
||||
|
||||
|
||||
@strip
|
||||
@turn_on
|
||||
async def test_children_change_state(dev, turn_on):
|
||||
await handle_turn_on(dev, turn_on)
|
||||
for plug in dev.children:
|
||||
orig_state = plug.is_on
|
||||
if orig_state:
|
||||
await plug.turn_off()
|
||||
assert not plug.is_on
|
||||
assert plug.is_off
|
||||
|
||||
await plug.turn_on()
|
||||
assert plug.is_on
|
||||
assert not plug.is_off
|
||||
else:
|
||||
await plug.turn_on()
|
||||
assert plug.is_on
|
||||
assert not plug.is_off
|
||||
|
||||
await plug.turn_off()
|
||||
assert not plug.is_on
|
||||
assert plug.is_off
|
||||
|
||||
|
||||
@strip
|
||||
async def test_children_alias(dev):
|
||||
test_alias = "TEST1234"
|
||||
for plug in dev.children:
|
||||
original = plug.alias
|
||||
await plug.set_alias(alias=test_alias)
|
||||
await dev.update() # TODO: set_alias does not call parent's update()..
|
||||
assert plug.alias == test_alias
|
||||
|
||||
await plug.set_alias(alias=original)
|
||||
await dev.update() # TODO: set_alias does not call parent's update()..
|
||||
assert plug.alias == original
|
||||
|
||||
|
||||
@strip
|
||||
async def test_children_on_since(dev):
|
||||
on_sinces = []
|
||||
for plug in dev.children:
|
||||
if plug.is_on:
|
||||
on_sinces.append(plug.on_since)
|
||||
assert isinstance(plug.on_since, datetime)
|
||||
else:
|
||||
assert plug.on_since is None
|
||||
|
||||
if dev.is_off:
|
||||
assert dev.on_since is None
|
||||
# TODO: testing this would require some mocking utcnow which is not
|
||||
# very straightforward.
|
||||
# else:
|
||||
# assert dev.on_since == max(on_sinces)
|
||||
|
||||
|
||||
@strip
|
||||
async def test_get_plug_by_name(dev: SmartStrip):
|
||||
name = dev.children[0].alias
|
||||
assert dev.get_plug_by_name(name) == dev.children[0]
|
||||
|
||||
with pytest.raises(SmartDeviceException):
|
||||
dev.get_plug_by_name("NONEXISTING NAME")
|
||||
|
||||
|
||||
@strip
|
||||
async def test_get_plug_by_index(dev: SmartStrip):
|
||||
assert dev.get_plug_by_index(0) == dev.children[0]
|
||||
|
||||
with pytest.raises(SmartDeviceException):
|
||||
dev.get_plug_by_index(-1)
|
||||
|
||||
with pytest.raises(SmartDeviceException):
|
||||
dev.get_plug_by_index(len(dev.children))
|
||||
|
||||
|
||||
@pytest.mark.skip("this test will wear out your relays")
|
||||
async def test_all_binary_states(dev):
|
||||
# test every binary state
|
||||
# TODO: this needs to be fixed, dev.plugs is not available for each device..
|
||||
for state in range(2 ** len(dev.children)):
|
||||
# create binary state map
|
||||
state_map = {}
|
||||
for plug_index in range(len(dev.children)):
|
||||
state_map[plug_index] = bool((state >> plug_index) & 1)
|
||||
|
||||
if state_map[plug_index]:
|
||||
await dev.turn_on(index=plug_index)
|
||||
else:
|
||||
await dev.turn_off(index=plug_index)
|
||||
|
||||
# check state map applied
|
||||
for index, state in dev.is_on.items():
|
||||
assert state_map[index] == state
|
||||
|
||||
# toggle each outlet with state map applied
|
||||
for plug_index in range(len(dev.children)):
|
||||
|
||||
# toggle state
|
||||
if state_map[plug_index]:
|
||||
await dev.turn_off(index=plug_index)
|
||||
else:
|
||||
await dev.turn_on(index=plug_index)
|
||||
|
||||
# only target outlet should have state changed
|
||||
for index, state in dev.is_on.items():
|
||||
if index == plug_index:
|
||||
assert state != state_map[index]
|
||||
else:
|
||||
assert state == state_map[index]
|
||||
|
||||
# reset state
|
||||
if state_map[plug_index]:
|
||||
await dev.turn_on(index=plug_index)
|
||||
else:
|
||||
await dev.turn_off(index=plug_index)
|
||||
|
||||
# original state map should be restored
|
||||
for index, state in dev.is_on.items():
|
||||
assert state == state_map[index]
|
Reference in New Issue
Block a user