Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Improve feature setter robustness #870

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 2, 2024
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
7 changes: 6 additions & 1 deletion kasa/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,14 @@ async def set_value(self, value):
return await getattr(container, self.attribute_setter)(value)

def __repr__(self):
value = self.value
try:
value = self.value
except Exception as ex:
return f"Unable to read value ({self.id}): {ex}"

if self.precision_hint is not None and value is not None:
value = round(self.value, self.precision_hint)

s = f"{self.name} ({self.id}): {value}"
if self.unit is not None:
s += f" {self.unit}"
Expand Down
1 change: 1 addition & 0 deletions kasa/iot/iotbulb.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ async def _initialize_features(self):
attribute_setter="set_color_temp",
range_getter="valid_temperature_range",
category=Feature.Category.Primary,
type=Feature.Type.Number,
)
)

Expand Down
8 changes: 4 additions & 4 deletions kasa/smart/modules/autooffmodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ def enabled(self) -> bool:
"""Return True if enabled."""
return self.data["enable"]

def set_enabled(self, enable: bool):
async def set_enabled(self, enable: bool):
"""Enable/disable auto off."""
return self.call(
return await self.call(
"set_auto_off_config",
{"enable": enable, "delay_min": self.data["delay_min"]},
)
Expand All @@ -67,9 +67,9 @@ def delay(self) -> int:
"""Return time until auto off."""
return self.data["delay_min"]

def set_delay(self, delay: int):
async def set_delay(self, delay: int):
"""Set time until auto off."""
return self.call(
return await self.call(
"set_auto_off_config", {"delay_min": delay, "enable": self.data["enable"]}
)

Expand Down
1 change: 1 addition & 0 deletions kasa/smart/modules/colortemp.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def __init__(self, device: SmartDevice, module: str):
attribute_setter="set_color_temp",
range_getter="valid_temperature_range",
category=Feature.Category.Primary,
type=Feature.Type.Number,
)
)

Expand Down
4 changes: 2 additions & 2 deletions kasa/smart/modules/lighttransitionmodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ def _turn_off(self):

return self.data["off_state"]

def set_enabled_v1(self, enable: bool):
async def set_enabled_v1(self, enable: bool):
"""Enable gradual on/off."""
return self.call("set_on_off_gradually_info", {"enable": enable})
return await self.call("set_on_off_gradually_info", {"enable": enable})

@property
def enabled_v1(self) -> bool:
Expand Down
1 change: 1 addition & 0 deletions kasa/smart/modules/temperature.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def __init__(self, device: SmartDevice, module: str):
attribute_getter="temperature_unit",
attribute_setter="set_temperature_unit",
type=Feature.Type.Choice,
choices=["celsius", "fahrenheit"],
)
)
# TODO: use temperature_unit for feature creation
Expand Down
18 changes: 10 additions & 8 deletions kasa/tests/device_fixtures.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from typing import AsyncGenerator

import pytest

from kasa import (
Expand Down Expand Up @@ -346,13 +348,13 @@ def device_for_fixture_name(model, protocol):
raise Exception("Unable to find type for %s", model)


async def _update_and_close(d):
async def _update_and_close(d) -> Device:
await d.update()
await d.protocol.close()
return d


async def _discover_update_and_close(ip, username, password):
async def _discover_update_and_close(ip, username, password) -> Device:
if username and password:
credentials = Credentials(username=username, password=password)
else:
Expand All @@ -361,7 +363,7 @@ async def _discover_update_and_close(ip, username, password):
return await _update_and_close(d)


async def get_device_for_fixture(fixture_data: FixtureInfo):
async def get_device_for_fixture(fixture_data: FixtureInfo) -> Device:
# if the wanted file is not an absolute path, prepend the fixtures directory

d = device_for_fixture_name(fixture_data.name, fixture_data.protocol)(
Expand Down Expand Up @@ -395,13 +397,14 @@ async def get_device_for_fixture_protocol(fixture, protocol):


@pytest.fixture(params=filter_fixtures("main devices"), ids=idgenerator)
async def dev(request):
async def dev(request) -> AsyncGenerator[Device, None]:
"""Device fixture.

Provides a device (given --ip) or parametrized fixture for the supported devices.
The initial update is called automatically before returning the device.
"""
fixture_data: FixtureInfo = request.param
dev: Device

ip = request.config.getoption("--ip")
username = request.config.getoption("--username")
Expand All @@ -412,13 +415,12 @@ async def dev(request):
if not model:
d = await _discover_update_and_close(ip, username, password)
IP_MODEL_CACHE[ip] = model = d.model

if model not in fixture_data.name:
pytest.skip(f"skipping file {fixture_data.name}")
dev: Device = (
d if d else await _discover_update_and_close(ip, username, password)
)
dev = d if d else await _discover_update_and_close(ip, username, password)
else:
dev: Device = await get_device_for_fixture(fixture_data)
dev = await get_device_for_fixture(fixture_data)

yield dev

Expand Down
68 changes: 65 additions & 3 deletions kasa/tests/test_feature.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import logging
import sys

import pytest
from pytest_mock import MockFixture
from pytest_mock import MockerFixture

from kasa import Device, Feature, KasaException

from kasa import Feature
_LOGGER = logging.getLogger(__name__)


class DummyDevice:
Expand Down Expand Up @@ -111,7 +116,7 @@ async def test_feature_action(mocker):
mock_call_action.assert_called()


async def test_feature_choice_list(dummy_feature, caplog, mocker: MockFixture):
async def test_feature_choice_list(dummy_feature, caplog, mocker: MockerFixture):
"""Test the choice feature type."""
dummy_feature.type = Feature.Type.Choice
dummy_feature.choices = ["first", "second"]
Expand All @@ -138,3 +143,60 @@ async def test_precision_hint(dummy_feature, precision_hint):
dummy_feature.attribute_getter = lambda x: dummy_value
assert dummy_feature.value == dummy_value
assert f"{round(dummy_value, precision_hint)} dummyunit" in repr(dummy_feature)


@pytest.mark.skipif(
sys.version_info < (3, 11),
reason="exceptiongroup requires python3.11+",
)
async def test_feature_setters(dev: Device, mocker: MockerFixture):
"""Test that all feature setters query something."""

async def _test_feature(feat, query_mock):
if feat.attribute_setter is None:
return

expecting_call = True

if feat.type == Feature.Type.Number:
await feat.set_value(feat.minimum_value)
elif feat.type == Feature.Type.Switch:
await feat.set_value(True)
elif feat.type == Feature.Type.Action:
await feat.set_value("dummyvalue")
elif feat.type == Feature.Type.Choice:
await feat.set_value(feat.choices[0])
elif feat.type == Feature.Type.Unknown:
_LOGGER.warning("Feature '%s' has no type, cannot test the setter", feat)
expecting_call = False
else:
raise NotImplementedError(f"set_value not implemented for {feat.type}")

if expecting_call:
query_mock.assert_called()

async def _test_features(dev):
exceptions = []
query = mocker.patch.object(dev.protocol, "query")
for feat in dev.features.values():
query.reset_mock()
try:
await _test_feature(feat, query)
# we allow our own exceptions to avoid mocking valid responses
except KasaException:
pass
except Exception as ex:
ex.add_note(f"Exception when trying to set {feat} on {dev}")
exceptions.append(ex)

return exceptions

exceptions = await _test_features(dev)

for child in dev.children:
exceptions.extend(await _test_features(child))

if exceptions:
raise ExceptionGroup(
"Got exceptions while testing attribute_setters", exceptions
)