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:
Teemu R
2020-05-27 16:55:18 +02:00
committed by GitHub
parent 836f1701b9
commit 644a10a0d1
19 changed files with 762 additions and 969 deletions

View File

@@ -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:

View File

@@ -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
View 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)

View File

@@ -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
View 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

View File

@@ -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
View 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)

View 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
View 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]