From 33eb1448d3847eecea2dccf79bd5bb9241ebe9fe Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 1 Mar 2024 19:41:10 +0100 Subject: [PATCH 1/5] Change state_information to return feature values --- kasa/device.py | 4 ++-- kasa/iot/iotbulb.py | 17 ----------------- kasa/iot/iotdevice.py | 6 ------ kasa/iot/iotdimmer.py | 9 --------- kasa/iot/iotlightstrip.py | 12 ------------ kasa/iot/iotplug.py | 6 ------ kasa/iot/iotstrip.py | 13 ------------- kasa/smart/smartbulb.py | 19 ------------------- kasa/smart/smartdevice.py | 9 --------- 9 files changed, 2 insertions(+), 93 deletions(-) diff --git a/kasa/device.py b/kasa/device.py index cebec582c..309e0a368 100644 --- a/kasa/device.py +++ b/kasa/device.py @@ -304,9 +304,9 @@ def internal_state(self) -> Any: """Return all the internal state data.""" @property - @abstractmethod def state_information(self) -> Dict[str, Any]: - """Return the key state information.""" + """Return available features and their values.""" + return {feat.name: feat.value for feat in self._features.values()} @property def features(self) -> Dict[str, Feature]: diff --git a/kasa/iot/iotbulb.py b/kasa/iot/iotbulb.py index 6b8d37b06..0d8d038e5 100644 --- a/kasa/iot/iotbulb.py +++ b/kasa/iot/iotbulb.py @@ -445,23 +445,6 @@ async def set_brightness( light_state = {"brightness": brightness} return await self.set_light_state(light_state, transition=transition) - @property # type: ignore - @requires_update - def state_information(self) -> Dict[str, Any]: - """Return bulb-specific state information.""" - info: Dict[str, Any] = { - "Brightness": self.brightness, - "Is dimmable": self.is_dimmable, - } - if self.is_variable_color_temp: - info["Color temperature"] = self.color_temp - info["Valid temperature range"] = self.valid_temperature_range - if self.is_color: - info["HSV"] = self.hsv - info["Presets"] = self.presets - - return info - @property # type: ignore @requires_update def is_on(self) -> bool: diff --git a/kasa/iot/iotdevice.py b/kasa/iot/iotdevice.py index 5bbb95058..0f34d8fb9 100755 --- a/kasa/iot/iotdevice.py +++ b/kasa/iot/iotdevice.py @@ -615,12 +615,6 @@ def on_since(self) -> Optional[datetime]: return datetime.now().replace(microsecond=0) - timedelta(seconds=on_time) - @property # type: ignore - @requires_update - def state_information(self) -> Dict[str, Any]: - """Return device-type specific, end-user friendly state information.""" - raise NotImplementedError("Device subclass needs to implement this.") - @property # type: ignore @requires_update def device_id(self) -> str: diff --git a/kasa/iot/iotdimmer.py b/kasa/iot/iotdimmer.py index 721a2c4b3..70f161453 100644 --- a/kasa/iot/iotdimmer.py +++ b/kasa/iot/iotdimmer.py @@ -215,12 +215,3 @@ def is_dimmable(self) -> bool: """Whether the switch supports brightness changes.""" sys_info = self.sys_info return "brightness" in sys_info - - @property # type: ignore - @requires_update - def state_information(self) -> Dict[str, Any]: - """Return switch-specific state information.""" - info = super().state_information - info["Brightness"] = self.brightness - - return info diff --git a/kasa/iot/iotlightstrip.py b/kasa/iot/iotlightstrip.py index fa341a2c5..8fb2118e3 100644 --- a/kasa/iot/iotlightstrip.py +++ b/kasa/iot/iotlightstrip.py @@ -84,18 +84,6 @@ def effect_list(self) -> Optional[List[str]]: """ return EFFECT_NAMES_V1 if self.has_effects else None - @property # type: ignore - @requires_update - def state_information(self) -> Dict[str, Any]: - """Return strip specific state information.""" - info = super().state_information - - info["Length"] = self.length - if self.has_effects: - info["Effect"] = self.effect["name"] - - return info - @requires_update async def set_effect( self, diff --git a/kasa/iot/iotplug.py b/kasa/iot/iotplug.py index 3f776b985..70e96c71b 100644 --- a/kasa/iot/iotplug.py +++ b/kasa/iot/iotplug.py @@ -96,12 +96,6 @@ async def set_led(self, state: bool): "system", "set_led_off", {"off": int(not state)} ) - @property # type: ignore - @requires_update - def state_information(self) -> Dict[str, Any]: - """Return switch-specific state information.""" - return {} - class IotWallSwitch(IotPlug): """Representation of a TP-Link Smart Wall Switch.""" diff --git a/kasa/iot/iotstrip.py b/kasa/iot/iotstrip.py index 4bf31cc76..2e5af0d08 100755 --- a/kasa/iot/iotstrip.py +++ b/kasa/iot/iotstrip.py @@ -154,19 +154,6 @@ 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)}) - @property # type: ignore - @requires_update - def state_information(self) -> Dict[str, Any]: - """Return strip-specific state information. - - :return: Strip information dict, keys in user-presentable form. - """ - return { - "LED state": self.led, - "Childs count": len(self.children), - "On since": self.on_since, - } - async def current_consumption(self) -> float: """Get the current power consumption in watts.""" return sum([await plug.current_consumption() for plug in self.children]) diff --git a/kasa/smart/smartbulb.py b/kasa/smart/smartbulb.py index eb3310e81..2d74a4f12 100644 --- a/kasa/smart/smartbulb.py +++ b/kasa/smart/smartbulb.py @@ -238,25 +238,6 @@ async def set_effect( } ) - @property # type: ignore - def state_information(self) -> Dict[str, Any]: - """Return bulb-specific state information.""" - info: Dict[str, Any] = { - # TODO: re-enable after we don't inherit from smartbulb - # **super().state_information - "Is dimmable": self.is_dimmable, - } - if self.is_dimmable: - info["Brightness"] = self.brightness - if self.is_variable_color_temp: - info["Color temperature"] = self.color_temp - info["Valid temperature range"] = self.valid_temperature_range - if self.is_color: - info["HSV"] = self.hsv - info["Presets"] = self.presets - - return info - @property def presets(self) -> List[BulbPreset]: """Return a list of available bulb setting presets.""" diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index 8b0236c37..bf4641148 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -319,15 +319,6 @@ def ssid(self) -> str: ssid = base64.b64decode(ssid).decode() if ssid else "No SSID" return ssid - @property - def state_information(self) -> Dict[str, Any]: - """Return the key state information.""" - return { - "overheated": self._info.get("overheated"), - "signal_level": self._info.get("signal_level"), - "SSID": self.ssid, - } - @property def has_emeter(self) -> bool: """Return if the device has emeter.""" From 5b9d55fac88da09dd2386b337bd72627567ac4fc Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 1 Mar 2024 19:56:12 +0100 Subject: [PATCH 2/5] Rename features to device-specific information, remove dupe printout --- kasa/cli.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/kasa/cli.py b/kasa/cli.py index 78553ebf2..20372be4e 100755 --- a/kasa/cli.py +++ b/kasa/cli.py @@ -609,16 +609,7 @@ async def state(ctx, dev: Device): echo(f"\tMAC (rssi): {dev.mac} ({dev.rssi})") echo(f"\tLocation: {dev.location}") - echo("\n\t[bold]== Device specific information ==[/bold]") - for info_name, info_data in dev.state_information.items(): - if isinstance(info_data, list): - echo(f"\t{info_name}:") - for item in info_data: - echo(f"\t\t{item}") - else: - echo(f"\t{info_name}: {info_data}") - - echo("\n\t[bold]== Features == [/bold]") + echo("\n\t[bold]== Device-specific information == [/bold]") for id_, feature in dev.features.items(): echo(f"\t{feature.name} ({id_}): {feature.value}") From e02499df3b3e3cd80d5241046983a3037c7f8a30 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Mon, 4 Mar 2024 19:56:43 +0100 Subject: [PATCH 3/5] Fix linting --- kasa/device.py | 2 +- kasa/iot/iotbulb.py | 2 +- kasa/iot/iotlightstrip.py | 2 +- kasa/iot/iotplug.py | 2 +- kasa/smart/smartbulb.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/kasa/device.py b/kasa/device.py index 309e0a368..3a1668e77 100644 --- a/kasa/device.py +++ b/kasa/device.py @@ -306,7 +306,7 @@ def internal_state(self) -> Any: @property def state_information(self) -> Dict[str, Any]: """Return available features and their values.""" - return {feat.name: feat.value for feat in self._features.values()} + return {feat.name: feat.value for feat in self._features.values()} @property def features(self) -> Dict[str, Feature]: diff --git a/kasa/iot/iotbulb.py b/kasa/iot/iotbulb.py index 0d8d038e5..938f655df 100644 --- a/kasa/iot/iotbulb.py +++ b/kasa/iot/iotbulb.py @@ -2,7 +2,7 @@ import logging import re from enum import Enum -from typing import Any, Dict, List, Optional, cast +from typing import Dict, List, Optional, cast try: from pydantic.v1 import BaseModel, Field, root_validator diff --git a/kasa/iot/iotlightstrip.py b/kasa/iot/iotlightstrip.py index 8fb2118e3..1e657a987 100644 --- a/kasa/iot/iotlightstrip.py +++ b/kasa/iot/iotlightstrip.py @@ -1,5 +1,5 @@ """Module for light strips (KL430).""" -from typing import Any, Dict, List, Optional +from typing import Dict, List, Optional from ..device_type import DeviceType from ..deviceconfig import DeviceConfig diff --git a/kasa/iot/iotplug.py b/kasa/iot/iotplug.py index 70e96c71b..bcb8b7cc7 100644 --- a/kasa/iot/iotplug.py +++ b/kasa/iot/iotplug.py @@ -1,6 +1,6 @@ """Module for smart plugs (HS100, HS110, ..).""" import logging -from typing import Any, Dict, Optional +from typing import Optional from ..device_type import DeviceType from ..deviceconfig import DeviceConfig diff --git a/kasa/smart/smartbulb.py b/kasa/smart/smartbulb.py index 2d74a4f12..b92edecd2 100644 --- a/kasa/smart/smartbulb.py +++ b/kasa/smart/smartbulb.py @@ -1,5 +1,5 @@ """Module for tapo-branded smart bulbs (L5**).""" -from typing import Any, Dict, List, Optional +from typing import Dict, List, Optional from ..bulb import Bulb from ..exceptions import KasaException From 1da657951373515571cd84937d5175ba920b984b Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 22 Mar 2024 15:32:28 +0100 Subject: [PATCH 4/5] Fix tests --- kasa/tests/fakeprotocol_iot.py | 17 +++++++++++++++++ kasa/tests/test_bulb.py | 9 ++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/kasa/tests/fakeprotocol_iot.py b/kasa/tests/fakeprotocol_iot.py index 864576541..bd7ee2b3c 100644 --- a/kasa/tests/fakeprotocol_iot.py +++ b/kasa/tests/fakeprotocol_iot.py @@ -121,6 +121,21 @@ def success(res): "set_timezone": None, } +CLOUD_MODULE = { + "get_info": { + "username": "", + "server": "devs.tplinkcloud.com", + "binded": 0, + "cld_connection": 0, + "illegalType": -1, + "stopConnect": -1, + "tcspStatus": -1, + "fwDlPage": "", + "tcspInfo": "", + "fwNotifyType": 0, + } +} + class FakeIotProtocol(IotProtocol): def __init__(self, info): @@ -308,6 +323,8 @@ def light_state(self, x, *args): }, "smartlife.iot.LAS": {}, "smartlife.iot.PIR": {}, + "cnCloud": CLOUD_MODULE, + "smartlife.iot.common.cloud": CLOUD_MODULE, } async def query(self, request, port=9999): diff --git a/kasa/tests/test_bulb.py b/kasa/tests/test_bulb.py index e8c95dbd8..1813c068d 100644 --- a/kasa/tests/test_bulb.py +++ b/kasa/tests/test_bulb.py @@ -42,11 +42,8 @@ async def test_bulb_sysinfo(dev: Bulb): @bulb async def test_state_attributes(dev: Bulb): - 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 + assert "Cloud connection" in dev.state_information + assert isinstance(dev.state_information["Cloud connection"], bool) @bulb_iot @@ -114,6 +111,7 @@ async def test_invalid_hsv(dev: Bulb, turn_on): @color_bulb +@pytest.mark.skip("requires color feature") async def test_color_state_information(dev: Bulb): assert "HSV" in dev.state_information assert dev.state_information["HSV"] == dev.hsv @@ -130,6 +128,7 @@ async def test_hsv_on_non_color(dev: Bulb): @variable_temp +@pytest.mark.skip("requires colortemp module") async def test_variable_temp_state_information(dev: Bulb): assert "Color temperature" in dev.state_information assert dev.state_information["Color temperature"] == dev.color_temp From b1921fb89d19709a0a597f5b94839e7fa07a09fb Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 22 Mar 2024 15:41:59 +0100 Subject: [PATCH 5/5] Add dummy smartlife.iot.las and smartlife.iot.pir, fix state_information test for lightstrip effect --- kasa/tests/fakeprotocol_iot.py | 44 ++++++++++++++++++++++++++++++++-- kasa/tests/test_lightstrip.py | 1 - 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/kasa/tests/fakeprotocol_iot.py b/kasa/tests/fakeprotocol_iot.py index bd7ee2b3c..6b22db0bd 100644 --- a/kasa/tests/fakeprotocol_iot.py +++ b/kasa/tests/fakeprotocol_iot.py @@ -137,6 +137,46 @@ def success(res): } +AMBIENT_MODULE = { + "get_current_brt": {"value": 26, "err_code": 0}, + "get_config": { + "devs": [ + { + "hw_id": 0, + "enable": 0, + "dark_index": 1, + "min_adc": 0, + "max_adc": 2450, + "level_array": [ + {"name": "cloudy", "adc": 490, "value": 20}, + {"name": "overcast", "adc": 294, "value": 12}, + {"name": "dawn", "adc": 222, "value": 9}, + {"name": "twilight", "adc": 222, "value": 9}, + {"name": "total darkness", "adc": 111, "value": 4}, + {"name": "custom", "adc": 2400, "value": 97}, + ], + } + ], + "ver": "1.0", + "err_code": 0, + }, +} + + +MOTION_MODULE = { + "get_config": { + "enable": 0, + "version": "1.0", + "trigger_index": 2, + "cold_time": 60000, + "min_adc": 0, + "max_adc": 4095, + "array": [80, 50, 20, 0], + "err_code": 0, + } +} + + class FakeIotProtocol(IotProtocol): def __init__(self, info): super().__init__( @@ -321,8 +361,8 @@ def light_state(self, x, *args): "set_brightness": set_hs220_brightness, "set_dimmer_transition": set_hs220_dimmer_transition, }, - "smartlife.iot.LAS": {}, - "smartlife.iot.PIR": {}, + "smartlife.iot.LAS": AMBIENT_MODULE, + "smartlife.iot.PIR": MOTION_MODULE, "cnCloud": CLOUD_MODULE, "smartlife.iot.common.cloud": CLOUD_MODULE, } diff --git a/kasa/tests/test_lightstrip.py b/kasa/tests/test_lightstrip.py index 123360a4e..438cd0805 100644 --- a/kasa/tests/test_lightstrip.py +++ b/kasa/tests/test_lightstrip.py @@ -28,7 +28,6 @@ async def test_effects_lightstrip_set_effect(dev: IotLightStrip): await dev.set_effect("Candy Cane") assert dev.effect["name"] == "Candy Cane" - assert dev.state_information["Effect"] == "Candy Cane" @lightstrip