Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ This will make sure that the checks are passing when you do a commit.

You can also execute the checks by running either `tox -e lint` to only do the linting checks, or `tox` to also execute the tests.

### Running tests

You can run tests on the library by executing `pytest` in the source directory.
This will run the tests against contributed example responses, but you can also execute the tests against a real device:
```
pytest --ip <address>
```
Note that this will perform state changes on the device.

### Analyzing network captures

The simplest way to add support for a new device or to improve existing ones is to capture traffic between the mobile app and the device.
Expand Down
1 change: 1 addition & 0 deletions kasa/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ async def alias(dev, new_alias, index):
if new_alias is not None:
click.echo(f"Setting alias to {new_alias}")
click.echo(await dev.set_alias(new_alias))
await dev.update()

click.echo(f"Alias: {dev.alias}")
if dev.is_strip:
Expand Down
5 changes: 3 additions & 2 deletions kasa/smartdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ async def current_consumption(self) -> float:
raise SmartDeviceException("Device has no emeter")

response = EmeterStatus(await self.get_emeter_realtime())
return response["power"]
return float(response["power"])

async def reboot(self, delay: int = 1) -> None:
"""Reboot the device.
Expand Down Expand Up @@ -643,7 +643,8 @@ def state_information(self) -> Dict[str, Any]:
def device_id(self) -> str:
"""Return unique ID for the device.

This is the MAC address of the device.
If not overridden, this is the MAC address of the device.
Individual sockets on strips will override this.
"""
return self.mac

Expand Down
3 changes: 0 additions & 3 deletions kasa/smartstrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,10 @@ async def update(self):
async def turn_on(self, **kwargs):
"""Turn the strip on."""
await self._query_helper("system", "set_relay_state", {"state": 1})
await self.update()

async def turn_off(self, **kwargs):
"""Turn the strip off."""
await self._query_helper("system", "set_relay_state", {"state": 0})
await self.update()

@property # type: ignore
@requires_update
Expand All @@ -126,7 +124,6 @@ def led(self) -> bool:
async def set_led(self, state: bool):
"""Set the state of the led (night mode)."""
await self._query_helper("system", "set_led_off", {"off": int(not state)})
await self.update()

@property # type: ignore
@requires_update
Expand Down
16 changes: 12 additions & 4 deletions kasa/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def get_device_for_file(file):
with open(p) as f:
sysinfo = json.load(f)
model = basename(file)
p = device_for_file(model)(host="123.123.123.123")
p = device_for_file(model)(host="127.0.0.123")
p.protocol = FakeTransportProtocol(sysinfo)
asyncio.run(p.update())
return p
Expand All @@ -168,21 +168,29 @@ def dev(request):
asyncio.run(d.update())
if d.model in file:
return d
raise Exception("Unable to find type for %s" % ip)
else:
pytest.skip(f"skipping file {file}")

return get_device_for_file(file)


def pytest_addoption(parser):
parser.addoption("--ip", action="store", default=None, help="run against device")
parser.addoption(
"--ip", action="store", default=None, help="run against device on given ip"
)


def pytest_collection_modifyitems(config, items):
if not config.getoption("--ip"):
print("Testing against fixtures.")
return
else:
print("Running against ip %s" % config.getoption("--ip"))
requires_dummy = pytest.mark.skip(
reason="test requires to be run against dummy data"
)
for item in items:
if "requires_dummy" in item.keywords:
item.add_marker(requires_dummy)


# allow mocks to be awaited
Expand Down
2 changes: 1 addition & 1 deletion kasa/tests/newfakes.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def lb_dev_state(x):
"dft_on_state": Optional(
{
"brightness": All(int, Range(min=0, max=100)),
"color_temp": All(int, Range(min=2000, max=9000)),
"color_temp": All(int, Range(min=0, max=9000)),
"hue": All(int, Range(min=0, max=255)),
"mode": str,
"saturation": All(int, Range(min=0, max=255)),
Expand Down
4 changes: 4 additions & 0 deletions kasa/tests/test_bulb.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ async def test_hsv(dev, turn_on):

await dev.set_hsv(hue=1, saturation=1, value=1)

await dev.update()
hue, saturation, brightness = dev.hsv
assert hue == 1
assert saturation == 1
Expand Down Expand Up @@ -134,6 +135,7 @@ async def test_variable_temp_state_information(dev):
async def test_try_set_colortemp(dev, turn_on):
await handle_turn_on(dev, turn_on)
await dev.set_color_temp(2700)
await dev.update()
assert dev.color_temp == 2700


Expand Down Expand Up @@ -179,9 +181,11 @@ async def test_dimmable_brightness(dev, turn_on):
assert dev.is_dimmable

await dev.set_brightness(50)
await dev.update()
assert dev.brightness == 50

await dev.set_brightness(10)
await dev.update()
assert dev.brightness == 10

with pytest.raises(ValueError):
Expand Down
8 changes: 7 additions & 1 deletion kasa/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ async def test_state(dev, turn_on):
await handle_turn_on(dev, turn_on)
runner = CliRunner()
res = await runner.invoke(state, obj=dev)
print(res.output)
await dev.update()

if dev.is_on:
assert "Device state: ON" in res.output
Expand All @@ -32,13 +32,17 @@ async def test_alias(dev):
res = await runner.invoke(alias, obj=dev)
assert f"Alias: {dev.alias}" in res.output

old_alias = dev.alias

new_alias = "new alias"
res = await runner.invoke(alias, [new_alias], obj=dev)
assert f"Setting alias to {new_alias}" in res.output

res = await runner.invoke(alias, obj=dev)
assert f"Alias: {new_alias}" in res.output

await dev.set_alias(old_alias)


async def test_raw_command(dev):
runner = CliRunner()
Expand All @@ -63,11 +67,13 @@ async def test_emeter(dev: SmartDevice, mocker):
assert "== Emeter ==" in res.output

monthly = mocker.patch.object(dev, "get_emeter_monthly")
monthly.return_value = []
res = await runner.invoke(emeter, ["--year", "1900"], obj=dev)
assert "For year" in res.output
monthly.assert_called()

daily = mocker.patch.object(dev, "get_emeter_daily")
daily.return_value = []
res = await runner.invoke(emeter, ["--month", "1900-12"], obj=dev)
assert "For month" in res.output
daily.assert_called()
Expand Down
10 changes: 5 additions & 5 deletions kasa/tests/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@

@plug
async def test_type_detection_plug(dev: SmartDevice):
d = Discover._get_device_class(dev.protocol.discovery_data)("localhost")
d = Discover._get_device_class(dev._last_update)("localhost")
assert d.is_plug
assert d.device_type == DeviceType.Plug


@bulb
async def test_type_detection_bulb(dev: SmartDevice):
d = Discover._get_device_class(dev.protocol.discovery_data)("localhost")
d = Discover._get_device_class(dev._last_update)("localhost")
# TODO: light_strip is a special case for now to force bulb tests on it
if not d.is_light_strip:
assert d.is_bulb
Expand All @@ -24,21 +24,21 @@ async def test_type_detection_bulb(dev: SmartDevice):

@strip
async def test_type_detection_strip(dev: SmartDevice):
d = Discover._get_device_class(dev.protocol.discovery_data)("localhost")
d = Discover._get_device_class(dev._last_update)("localhost")
assert d.is_strip
assert d.device_type == DeviceType.Strip


@dimmer
async def test_type_detection_dimmer(dev: SmartDevice):
d = Discover._get_device_class(dev.protocol.discovery_data)("localhost")
d = Discover._get_device_class(dev._last_update)("localhost")
assert d.is_dimmer
assert d.device_type == DeviceType.Dimmer


@lightstrip
async def test_type_detection_lightstrip(dev: SmartDevice):
d = Discover._get_device_class(dev.protocol.discovery_data)("localhost")
d = Discover._get_device_class(dev._last_update)("localhost")
assert d.is_light_strip
assert d.device_type == DeviceType.LightStrip

Expand Down
2 changes: 2 additions & 0 deletions kasa/tests/test_emeter.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ async def test_get_emeter_realtime(dev):


@has_emeter
@pytest.mark.requires_dummy
async def test_get_emeter_daily(dev):
if dev.is_strip:
pytest.skip("Disabled for strips temporarily")
Expand All @@ -54,6 +55,7 @@ async def test_get_emeter_daily(dev):


@has_emeter
@pytest.mark.requires_dummy
async def test_get_emeter_monthly(dev):
if dev.is_strip:
pytest.skip("Disabled for strips temporarily")
Expand Down
2 changes: 2 additions & 0 deletions kasa/tests/test_plug.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ async def test_led(dev):
original = dev.led

await dev.set_led(False)
await dev.update()
assert not dev.led

await dev.set_led(True)
await dev.update()
assert dev.led

await dev.set_led(original)
7 changes: 7 additions & 0 deletions kasa/tests/test_smartdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ async def test_state_info(dev):
assert isinstance(dev.state_information, dict)


@pytest.mark.requires_dummy
async def test_invalid_connection(dev):
with patch.object(FakeTransportProtocol, "query", side_effect=SmartDeviceException):
with pytest.raises(SmartDeviceException):
Expand All @@ -32,18 +33,22 @@ async def test_state(dev, turn_on):
orig_state = dev.is_on
if orig_state:
await dev.turn_off()
await dev.update()
assert not dev.is_on
assert dev.is_off

await dev.turn_on()
await dev.update()
assert dev.is_on
assert not dev.is_off
else:
await dev.turn_on()
await dev.update()
assert dev.is_on
assert not dev.is_off

await dev.turn_off()
await dev.update()
assert not dev.is_on
assert dev.is_off

Expand All @@ -54,9 +59,11 @@ async def test_alias(dev):

assert isinstance(original, str)
await dev.set_alias(test_alias)
await dev.update()
assert dev.alias == test_alias

await dev.set_alias(original)
await dev.update()
assert dev.alias == original


Expand Down
20 changes: 12 additions & 8 deletions kasa/tests/test_strip.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,24 @@ async def test_children_change_state(dev, turn_on):
orig_state = plug.is_on
if orig_state:
await plug.turn_off()
assert not plug.is_on
assert plug.is_off
await dev.update()
assert plug.is_on is False
assert plug.is_off is True

await plug.turn_on()
assert plug.is_on
assert not plug.is_off
await dev.update()
assert plug.is_on is True
assert plug.is_off is False
else:
await plug.turn_on()
assert plug.is_on
assert not plug.is_off
await dev.update()
assert plug.is_on is True
assert plug.is_off is False

await plug.turn_off()
assert not plug.is_on
assert plug.is_off
await dev.update()
assert plug.is_on is False
assert plug.is_off is True


@strip
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ fail-under = 100
exclude = ['kasa/tests/*']
verbose = 2

[tool.pytest.ini_options]
markers = [
"requires_dummy: test requires dummy data to pass, skipped on real devices",
]

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"