From 3c94da995566f25c18b5cbf1a24be279bb8661af Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Thu, 23 May 2024 15:37:48 +0200 Subject: [PATCH 1/7] Initialize autooff features only when data is available --- kasa/smart/modules/autooff.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/kasa/smart/modules/autooff.py b/kasa/smart/modules/autooff.py index 385364fa6..684a2c510 100644 --- a/kasa/smart/modules/autooff.py +++ b/kasa/smart/modules/autooff.py @@ -2,14 +2,13 @@ from __future__ import annotations +import logging from datetime import datetime, timedelta -from typing import TYPE_CHECKING from ...feature import Feature from ..smartmodule import SmartModule -if TYPE_CHECKING: - from ..smartdevice import SmartDevice +_LOGGER = logging.getLogger(__name__) class AutoOff(SmartModule): @@ -18,11 +17,17 @@ class AutoOff(SmartModule): REQUIRED_COMPONENT = "auto_off" QUERY_GETTER_NAME = "get_auto_off_config" - def __init__(self, device: SmartDevice, module: str): - super().__init__(device, module) + def _initialize_features(self): + """Initialize features after the initial update.""" + if not isinstance(self.data, dict): + _LOGGER.warning( + "No data available for module, skipping %s: %s", self, self.data + ) + return + self._add_feature( Feature( - device, + self._device, id="auto_off_enabled", name="Auto off enabled", container=self, @@ -33,7 +38,7 @@ def __init__(self, device: SmartDevice, module: str): ) self._add_feature( Feature( - device, + self._device, id="auto_off_minutes", name="Auto off minutes", container=self, @@ -44,7 +49,7 @@ def __init__(self, device: SmartDevice, module: str): ) self._add_feature( Feature( - device, + self._device, id="auto_off_at", name="Auto off at", container=self, From 54308ba7cbb8cafee3b607895893514727de3edd Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Thu, 23 May 2024 17:06:24 +0200 Subject: [PATCH 2/7] Add tests --- kasa/tests/smart/modules/test_autooff.py | 97 ++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 kasa/tests/smart/modules/test_autooff.py diff --git a/kasa/tests/smart/modules/test_autooff.py b/kasa/tests/smart/modules/test_autooff.py new file mode 100644 index 000000000..6ad8b67a6 --- /dev/null +++ b/kasa/tests/smart/modules/test_autooff.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from datetime import datetime + +import pytest +from pytest_mock import MockerFixture + +from kasa import Module +from kasa.smart import SmartDevice +from kasa.tests.device_fixtures import parametrize + +autooff = parametrize( + "has autooff", component_filter="auto_off", protocol_filter={"SMART"} +) + + +@autooff +@pytest.mark.parametrize( + "feature, prop_name, type", + [ + ("auto_off_enabled", "enabled", bool), + ("auto_off_minutes", "delay", int), + ("auto_off_at", "auto_off_at", datetime | None), + ], +) +async def test_autooff_features( + dev: SmartDevice, feature: str, prop_name: str, type: type +): + """Test that features are registered and work as expected.""" + autooff = dev.modules.get(Module.AutoOff) + assert autooff is not None + + prop = getattr(autooff, prop_name) + assert isinstance(prop, type) + + feat = dev.features[feature] + assert feat.value == prop + assert isinstance(feat.value, type) + + +@autooff +async def test_settings(dev: SmartDevice, mocker: MockerFixture): + """Test autooff settings.""" + autooff = dev.modules.get(Module.AutoOff) + assert autooff + + enabled = dev.features["auto_off_enabled"] + assert autooff.enabled == enabled.value + + delay = dev.features["auto_off_minutes"] + assert autooff.delay == delay.value + + call = mocker.spy(autooff, "call") + new_state = True + + await autooff.set_enabled(new_state) + call.assert_called_with( + "set_auto_off_config", {"enable": new_state, "delay_min": delay.value} + ) + call.reset_mock() + await dev.update() + + new_delay = 123 + + await autooff.set_delay(new_delay) + + call.assert_called_with( + "set_auto_off_config", {"enable": new_state, "delay_min": new_delay} + ) + + await dev.update() + + assert autooff.enabled == new_state + assert autooff.delay == new_delay + + +@autooff +@pytest.mark.parametrize("is_timer_active", [True, False]) +async def test_auto_off_at( + dev: SmartDevice, mocker: MockerFixture, is_timer_active: bool +): + """Test auto-off at sensor.""" + autooff = dev.modules.get(Module.AutoOff) + assert autooff + + autooff_at = dev.features["auto_off_at"] + + mocker.patch.object( + type(autooff), + "is_timer_active", + new_callable=mocker.PropertyMock, + return_value=is_timer_active, + ) + if is_timer_active: + assert isinstance(autooff_at.value, datetime) + else: + assert autooff_at.value is None From ca91afbb3c948fbe66c32f711c1528f5b1486ef3 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Thu, 23 May 2024 17:20:11 +0200 Subject: [PATCH 3/7] skip the feature tests for py<3.10 --- kasa/tests/smart/modules/test_autooff.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/kasa/tests/smart/modules/test_autooff.py b/kasa/tests/smart/modules/test_autooff.py index 6ad8b67a6..df8a891f1 100644 --- a/kasa/tests/smart/modules/test_autooff.py +++ b/kasa/tests/smart/modules/test_autooff.py @@ -1,5 +1,6 @@ from __future__ import annotations +import sys from datetime import datetime import pytest @@ -23,6 +24,10 @@ ("auto_off_at", "auto_off_at", datetime | None), ], ) +@pytest.mark.skipif( + sys.version_info < (3, 10), + reason="union type shortcut requires python3.10", +) async def test_autooff_features( dev: SmartDevice, feature: str, prop_name: str, type: type ): From e309fdea8d0db9c17762a385138b2aeb60bf06d6 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Thu, 23 May 2024 17:26:26 +0200 Subject: [PATCH 4/7] Allow None for all feature values.. --- kasa/tests/smart/modules/test_autooff.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/kasa/tests/smart/modules/test_autooff.py b/kasa/tests/smart/modules/test_autooff.py index df8a891f1..d923a7e3f 100644 --- a/kasa/tests/smart/modules/test_autooff.py +++ b/kasa/tests/smart/modules/test_autooff.py @@ -1,6 +1,5 @@ from __future__ import annotations -import sys from datetime import datetime import pytest @@ -21,13 +20,9 @@ [ ("auto_off_enabled", "enabled", bool), ("auto_off_minutes", "delay", int), - ("auto_off_at", "auto_off_at", datetime | None), + ("auto_off_at", "auto_off_at", datetime), ], ) -@pytest.mark.skipif( - sys.version_info < (3, 10), - reason="union type shortcut requires python3.10", -) async def test_autooff_features( dev: SmartDevice, feature: str, prop_name: str, type: type ): @@ -36,11 +31,12 @@ async def test_autooff_features( assert autooff is not None prop = getattr(autooff, prop_name) - assert isinstance(prop, type) + # Note, special handling for None as this is allowed for auto_off_at + assert isinstance(prop, type) or prop is None feat = dev.features[feature] assert feat.value == prop - assert isinstance(feat.value, type) + assert isinstance(feat.value, type) or feat.value is None @autooff From bc9e8ca3217b1f6eba6b25805261eb876900f8f6 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Thu, 23 May 2024 17:36:21 +0200 Subject: [PATCH 5/7] One more try, use typing.Optional --- kasa/tests/smart/modules/test_autooff.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kasa/tests/smart/modules/test_autooff.py b/kasa/tests/smart/modules/test_autooff.py index d923a7e3f..0209bca8c 100644 --- a/kasa/tests/smart/modules/test_autooff.py +++ b/kasa/tests/smart/modules/test_autooff.py @@ -1,6 +1,7 @@ from __future__ import annotations from datetime import datetime +from typing import Optional import pytest from pytest_mock import MockerFixture @@ -20,7 +21,7 @@ [ ("auto_off_enabled", "enabled", bool), ("auto_off_minutes", "delay", int), - ("auto_off_at", "auto_off_at", datetime), + ("auto_off_at", "auto_off_at", Optional[datetime]), ], ) async def test_autooff_features( @@ -31,12 +32,11 @@ async def test_autooff_features( assert autooff is not None prop = getattr(autooff, prop_name) - # Note, special handling for None as this is allowed for auto_off_at - assert isinstance(prop, type) or prop is None + assert isinstance(prop, type) feat = dev.features[feature] assert feat.value == prop - assert isinstance(feat.value, type) or feat.value is None + assert isinstance(feat.value, type) @autooff From 76f1962ec75a669dbd8c60f7ac95090be478d530 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Thu, 23 May 2024 17:56:49 +0200 Subject: [PATCH 6/7] Skip the tests again for py<3.10 --- kasa/cli.py | 2 +- kasa/tests/smart/modules/test_autooff.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/kasa/cli.py b/kasa/cli.py index 235387bc1..e1fea6124 100755 --- a/kasa/cli.py +++ b/kasa/cli.py @@ -111,7 +111,7 @@ def CatchAllExceptions(cls): def _handle_exception(debug, exc): if isinstance(exc, click.ClickException): raise - echo(f"Raised error: {exc}") + echo(f"Raised error: {exc!r} {type(exc)=}") if debug: raise echo("Run with --debug enabled to see stacktrace") diff --git a/kasa/tests/smart/modules/test_autooff.py b/kasa/tests/smart/modules/test_autooff.py index 0209bca8c..c44617a76 100644 --- a/kasa/tests/smart/modules/test_autooff.py +++ b/kasa/tests/smart/modules/test_autooff.py @@ -1,5 +1,6 @@ from __future__ import annotations +import sys from datetime import datetime from typing import Optional @@ -24,6 +25,10 @@ ("auto_off_at", "auto_off_at", Optional[datetime]), ], ) +@pytest.mark.skipif( + sys.version_info < (3, 10), + reason="Subscripted generics cannot be used with class and instance checks", +) async def test_autooff_features( dev: SmartDevice, feature: str, prop_name: str, type: type ): From 8e765f3b5cf1d3516e3a2dd995cbaa1ff19a28b7 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Thu, 23 May 2024 18:06:33 +0200 Subject: [PATCH 7/7] Don't print out exception details on error --- kasa/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kasa/cli.py b/kasa/cli.py index e1fea6124..235387bc1 100755 --- a/kasa/cli.py +++ b/kasa/cli.py @@ -111,7 +111,7 @@ def CatchAllExceptions(cls): def _handle_exception(debug, exc): if isinstance(exc, click.ClickException): raise - echo(f"Raised error: {exc!r} {type(exc)=}") + echo(f"Raised error: {exc}") if debug: raise echo("Run with --debug enabled to see stacktrace")