From fe0bbf1b98621f72229277c0ca0c64047a128c92 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Mon, 10 Jun 2024 05:59:37 +0100 Subject: [PATCH 1/6] Do not expose child modules on parent devices (#964) Removes the logic to expose child modules on parent devices, which could cause complications with downstream consumers unknowingly duplicating things. --- kasa/smart/smartdevice.py | 17 +---------------- kasa/tests/device_fixtures.py | 13 +++++++++++++ kasa/tests/smart/features/test_brightness.py | 6 +++--- kasa/tests/smart/modules/test_fan.py | 13 ++++++------- kasa/tests/test_common_modules.py | 15 ++++++++------- kasa/tests/test_smartdevice.py | 8 +++----- 6 files changed, 34 insertions(+), 38 deletions(-) diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index 3250c98e0..0c56dba80 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -57,7 +57,6 @@ def __init__( self._components: dict[str, int] = {} self._state_information: dict[str, Any] = {} self._modules: dict[str | ModuleName[Module], SmartModule] = {} - self._exposes_child_modules = False self._parent: SmartDevice | None = None self._children: Mapping[str, SmartDevice] = {} self._last_update = {} @@ -99,16 +98,6 @@ def children(self) -> Sequence[SmartDevice]: @property def modules(self) -> ModuleMapping[SmartModule]: """Return the device modules.""" - if self._exposes_child_modules: - modules = {k: v for k, v in self._modules.items()} - for child in self._children.values(): - for k, v in child._modules.items(): - if k not in modules: - modules[k] = v - if TYPE_CHECKING: - return cast(ModuleMapping[SmartModule], modules) - return modules - if TYPE_CHECKING: # Needed for python 3.8 return cast(ModuleMapping[SmartModule], self._modules) return self._modules @@ -213,7 +202,6 @@ async def _initialize_modules(self): skip_parent_only_modules = True elif self._children and self.device_type == DeviceType.WallSwitch: # _initialize_modules is called on the parent after the children - self._exposes_child_modules = True for child in self._children.values(): child_modules_to_skip.update(**child.modules) @@ -332,10 +320,7 @@ async def _initialize_features(self): ) for module in self.modules.values(): - # Check if module features have already been initialized. - # i.e. when _exposes_child_modules is true - if not module._module_features: - module._initialize_features() + module._initialize_features() for feat in module._module_features.values(): self._add_feature(feat) for child in self._children.values(): diff --git a/kasa/tests/device_fixtures.py b/kasa/tests/device_fixtures.py index 04b6d3917..184eedaab 100644 --- a/kasa/tests/device_fixtures.py +++ b/kasa/tests/device_fixtures.py @@ -434,3 +434,16 @@ async def dev(request) -> AsyncGenerator[Device, None]: yield dev await dev.disconnect() + + +def get_parent_and_child_modules(device: Device, module_name): + """Return iterator of module if exists on parent and children. + + Useful for testing devices that have components listed on the parent that are only + supported on the children, i.e. ks240. + """ + if module_name in device.modules: + yield device.modules[module_name] + for child in device.children: + if module_name in child.modules: + yield child.modules[module_name] diff --git a/kasa/tests/smart/features/test_brightness.py b/kasa/tests/smart/features/test_brightness.py index e3c3c5303..bbf4d6dfa 100644 --- a/kasa/tests/smart/features/test_brightness.py +++ b/kasa/tests/smart/features/test_brightness.py @@ -2,7 +2,7 @@ from kasa.iot import IotDevice from kasa.smart import SmartDevice -from kasa.tests.conftest import dimmable_iot, parametrize +from kasa.tests.conftest import dimmable_iot, get_parent_and_child_modules, parametrize brightness = parametrize("brightness smart", component_filter="brightness") @@ -10,13 +10,13 @@ @brightness async def test_brightness_component(dev: SmartDevice): """Test brightness feature.""" - brightness = dev.modules.get("Brightness") + brightness = next(get_parent_and_child_modules(dev, "Brightness")) assert brightness assert isinstance(dev, SmartDevice) assert "brightness" in dev._components # Test getting the value - feature = dev.features["brightness"] + feature = brightness._device.features["brightness"] assert isinstance(feature.value, int) assert feature.value > 1 and feature.value <= 100 diff --git a/kasa/tests/smart/modules/test_fan.py b/kasa/tests/smart/modules/test_fan.py index 6d5a0dd1d..ee04015fa 100644 --- a/kasa/tests/smart/modules/test_fan.py +++ b/kasa/tests/smart/modules/test_fan.py @@ -3,7 +3,7 @@ from kasa import Module from kasa.smart import SmartDevice -from kasa.tests.device_fixtures import parametrize +from kasa.tests.device_fixtures import get_parent_and_child_modules, parametrize fan = parametrize("has fan", component_filter="fan_control", protocol_filter={"SMART"}) @@ -11,10 +11,9 @@ @fan async def test_fan_speed(dev: SmartDevice, mocker: MockerFixture): """Test fan speed feature.""" - fan = dev.modules.get(Module.Fan) + fan = next(get_parent_and_child_modules(dev, Module.Fan)) assert fan - - level_feature = dev.features["fan_speed_level"] + level_feature = fan._module_features["fan_speed_level"] assert ( level_feature.minimum_value <= level_feature.value @@ -36,9 +35,9 @@ async def test_fan_speed(dev: SmartDevice, mocker: MockerFixture): @fan async def test_sleep_mode(dev: SmartDevice, mocker: MockerFixture): """Test sleep mode feature.""" - fan = dev.modules.get(Module.Fan) + fan = next(get_parent_and_child_modules(dev, Module.Fan)) assert fan - sleep_feature = dev.features["fan_sleep_mode"] + sleep_feature = fan._module_features["fan_sleep_mode"] assert isinstance(sleep_feature.value, bool) call = mocker.spy(fan, "call") @@ -55,7 +54,7 @@ async def test_sleep_mode(dev: SmartDevice, mocker: MockerFixture): async def test_fan_module(dev: SmartDevice, mocker: MockerFixture): """Test fan speed on device interface.""" assert isinstance(dev, SmartDevice) - fan = dev.modules.get(Module.Fan) + fan = next(get_parent_and_child_modules(dev, Module.Fan)) assert fan device = fan._device diff --git a/kasa/tests/test_common_modules.py b/kasa/tests/test_common_modules.py index eaff5c07c..c0d905789 100644 --- a/kasa/tests/test_common_modules.py +++ b/kasa/tests/test_common_modules.py @@ -7,6 +7,7 @@ bulb_smart, dimmable_iot, dimmer_iot, + get_parent_and_child_modules, lightstrip_iot, parametrize, parametrize_combine, @@ -123,11 +124,11 @@ async def test_light_effect_module(dev: Device, mocker: MockerFixture): async def test_light_brightness(dev: Device): """Test brightness setter and getter.""" assert isinstance(dev, Device) - light = dev.modules.get(Module.Light) + light = next(get_parent_and_child_modules(dev, Module.Light)) assert light # Test getting the value - feature = dev.features["brightness"] + feature = light._device.features["brightness"] assert feature.minimum_value == 0 assert feature.maximum_value == 100 @@ -146,7 +147,7 @@ async def test_light_brightness(dev: Device): async def test_light_set_state(dev: Device): """Test brightness setter and getter.""" assert isinstance(dev, Device) - light = dev.modules.get(Module.Light) + light = next(get_parent_and_child_modules(dev, Module.Light)) assert light await light.set_state(LightState(light_on=False)) @@ -169,11 +170,11 @@ async def test_light_set_state(dev: Device): @light_preset async def test_light_preset_module(dev: Device, mocker: MockerFixture): """Test light preset module.""" - preset_mod = dev.modules[Module.LightPreset] + preset_mod = next(get_parent_and_child_modules(dev, Module.LightPreset)) assert preset_mod - light_mod = dev.modules[Module.Light] + light_mod = next(get_parent_and_child_modules(dev, Module.Light)) assert light_mod - feat = dev.features["light_preset"] + feat = preset_mod._device.features["light_preset"] preset_list = preset_mod.preset_list assert "Not set" in preset_list @@ -220,7 +221,7 @@ async def test_light_preset_module(dev: Device, mocker: MockerFixture): @light_preset async def test_light_preset_save(dev: Device, mocker: MockerFixture): """Test saving a new preset value.""" - preset_mod = dev.modules[Module.LightPreset] + preset_mod = next(get_parent_and_child_modules(dev, Module.LightPreset)) assert preset_mod preset_list = preset_mod.preset_list if len(preset_list) == 1: diff --git a/kasa/tests/test_smartdevice.py b/kasa/tests/test_smartdevice.py index 88880e103..2ffc40ba1 100644 --- a/kasa/tests/test_smartdevice.py +++ b/kasa/tests/test_smartdevice.py @@ -16,6 +16,7 @@ from .conftest import ( device_smart, get_device_for_fixture_protocol, + get_parent_and_child_modules, ) @@ -144,11 +145,8 @@ async def test_get_modules(): # Modules on child module = dummy_device.modules.get("Fan") - assert module - assert module._device != dummy_device - assert module._device._parent == dummy_device - - module = dummy_device.modules.get(Module.Fan) + assert module is None + module = next(get_parent_and_child_modules(dummy_device, "Fan")) assert module assert module._device != dummy_device assert module._device._parent == dummy_device From 9e74e1bd40f53c30f5ff0af3d41d79d6ffabc89c Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Mon, 10 Jun 2024 06:21:21 +0100 Subject: [PATCH 2/6] Do not add parent only modules to strip sockets (#963) Excludes modules that child devices report as supported but do not make sense on a child device like firmware, cloud, time etc. --- kasa/smart/smartdevice.py | 10 ++++++---- kasa/tests/device_fixtures.py | 18 ++++++++++++++++++ kasa/tests/test_childdevice.py | 20 +++++++++++++++++++- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index 0c56dba80..9013fc934 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -29,11 +29,11 @@ _LOGGER = logging.getLogger(__name__) -# List of modules that wall switches with children, i.e. ks240 report on +# List of modules that non hub devices with children, i.e. ks240/P300, report on # the child but only work on the parent. See longer note below in _initialize_modules. # This list should be updated when creating new modules that could have the # same issue, homekit perhaps? -WALL_SWITCH_PARENT_ONLY_MODULES = [DeviceModule, Time, Firmware, Cloud] +NON_HUB_PARENT_ONLY_MODULES = [DeviceModule, Time, Firmware, Cloud] # Device must go last as the other interfaces also inherit Device @@ -196,9 +196,11 @@ async def _initialize_modules(self): # when they need to be accessed through the children. # The logic below ensures that such devices add all but whitelisted, only on # the child device. + # It also ensures that devices like power strips do not add modules such as + # firmware to the child devices. skip_parent_only_modules = False child_modules_to_skip = {} - if self._parent and self._parent.device_type == DeviceType.WallSwitch: + if self._parent and self._parent.device_type != DeviceType.Hub: skip_parent_only_modules = True elif self._children and self.device_type == DeviceType.WallSwitch: # _initialize_modules is called on the parent after the children @@ -209,7 +211,7 @@ async def _initialize_modules(self): _LOGGER.debug("%s requires %s", mod, mod.REQUIRED_COMPONENT) if ( - skip_parent_only_modules and mod in WALL_SWITCH_PARENT_ONLY_MODULES + skip_parent_only_modules and mod in NON_HUB_PARENT_ONLY_MODULES ) or mod.__name__ in child_modules_to_skip: continue if ( diff --git a/kasa/tests/device_fixtures.py b/kasa/tests/device_fixtures.py index 184eedaab..844314bef 100644 --- a/kasa/tests/device_fixtures.py +++ b/kasa/tests/device_fixtures.py @@ -152,6 +152,24 @@ def parametrize_combine(parametrized: list[pytest.MarkDecorator]): ) +def parametrize_subtract(params: pytest.MarkDecorator, subtract: pytest.MarkDecorator): + """Combine multiple pytest parametrize dev marks into one set of fixtures.""" + if params.args[0] != "dev" or subtract.args[0] != "dev": + raise Exception( + f"Supplied mark is not for dev fixture: {params.args[0]} {subtract.args[0]}" + ) + fixtures = [] + for param in params.args[1]: + if param not in subtract.args[1]: + fixtures.append(param) + return pytest.mark.parametrize( + "dev", + sorted(fixtures), + indirect=True, + ids=idgenerator, + ) + + def parametrize( desc, *, diff --git a/kasa/tests/test_childdevice.py b/kasa/tests/test_childdevice.py index 9e4b6fdb6..26568c24a 100644 --- a/kasa/tests/test_childdevice.py +++ b/kasa/tests/test_childdevice.py @@ -3,10 +3,20 @@ import pytest +from kasa.device_type import DeviceType from kasa.smart.smartchilddevice import SmartChildDevice +from kasa.smart.smartdevice import NON_HUB_PARENT_ONLY_MODULES from kasa.smartprotocol import _ChildProtocolWrapper -from .conftest import strip_smart +from .conftest import parametrize, parametrize_subtract, strip_smart + +has_children_smart = parametrize( + "has children", component_filter="control_child", protocol_filter={"SMART"} +) +hub_smart = parametrize( + "smart hub", device_type_filter=[DeviceType.Hub], protocol_filter={"SMART"} +) +non_hub_parent_smart = parametrize_subtract(has_children_smart, hub_smart) @strip_smart @@ -82,3 +92,11 @@ def _test_property_getters(): exceptions = list(_test_property_getters()) if exceptions: raise ExceptionGroup("Accessing child properties caused exceptions", exceptions) + + +@non_hub_parent_smart +async def test_parent_only_modules(dev, dummy_protocol, mocker): + """Test that parent only modules are not available on children.""" + for child in dev.children: + for module in NON_HUB_PARENT_ONLY_MODULES: + assert module not in [type(module) for module in child.modules.values()] From 927fe648ac60a354f5f9606defe687c6593917d2 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Mon, 10 Jun 2024 14:13:46 +0100 Subject: [PATCH 3/6] Better checking of child modules not supported by parent device (#966) Replaces the logic to skip adding child modules to parent devices based on whether a device is a wall switch and instead relies on the `_check_supported` method. Is more future proof and will fix issue with the P300 with child `auto_off` modules https://github.com/python-kasa/python-kasa/pull/915 not supported on the parent. --- kasa/smart/modules/lightpreset.py | 10 ++++++++++ kasa/smart/smartdevice.py | 4 ---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/kasa/smart/modules/lightpreset.py b/kasa/smart/modules/lightpreset.py index e0a775aff..0fb57952f 100644 --- a/kasa/smart/modules/lightpreset.py +++ b/kasa/smart/modules/lightpreset.py @@ -140,3 +140,13 @@ def query(self) -> dict: if self._state_in_sysinfo: # Child lights can have states in the child info return {} return {self.QUERY_GETTER_NAME: None} + + async def _check_supported(self): + """Additional check to see if the module is supported by the device. + + Parent devices that report components of children such as ks240 will not have + the brightness value is sysinfo. + """ + # Look in _device.sys_info here because self.data is either sys_info or + # get_preset_rules depending on whether it's a child device or not. + return "brightness" in self._device.sys_info diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index 9013fc934..6f02fad0d 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -202,10 +202,6 @@ async def _initialize_modules(self): child_modules_to_skip = {} if self._parent and self._parent.device_type != DeviceType.Hub: skip_parent_only_modules = True - elif self._children and self.device_type == DeviceType.WallSwitch: - # _initialize_modules is called on the parent after the children - for child in self._children.values(): - child_modules_to_skip.update(**child.modules) for mod in SmartModule.REGISTERED_MODULES.values(): _LOGGER.debug("%s requires %s", mod, mod.REQUIRED_COMPONENT) From db6276d3fd305bc8d70323e175f40d8c16d83696 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:47:00 +0100 Subject: [PATCH 4/6] Support smart child modules queries (#967) Required for the P300 firmware update with `auto_off` module on child devices. Will query child modules for parent devices that are not hubs. Coverage will be fixed when the P300 fixture is added https://github.com/python-kasa/python-kasa/pull/915 --- kasa/smart/modules/autooff.py | 8 ++++++++ kasa/smart/smartchilddevice.py | 13 ++++++++++++- kasa/smart/smartdevice.py | 14 +++++++++----- kasa/tests/fakeprotocol_smart.py | 20 +++++++++++++++++++- kasa/tests/smart/modules/test_autooff.py | 16 ++++++++-------- kasa/tests/test_smartdevice.py | 10 ++++++++-- 6 files changed, 64 insertions(+), 17 deletions(-) diff --git a/kasa/smart/modules/autooff.py b/kasa/smart/modules/autooff.py index 684a2c510..afb822c56 100644 --- a/kasa/smart/modules/autooff.py +++ b/kasa/smart/modules/autooff.py @@ -99,3 +99,11 @@ def auto_off_at(self) -> datetime | None: sysinfo = self._device.sys_info return self._device.time + timedelta(seconds=sysinfo["auto_off_remain_time"]) + + async def _check_supported(self): + """Additional check to see if the module is supported by the device. + + Parent devices that report components of children such as P300 will not have + the auto_off_status is sysinfo. + """ + return "auto_off_status" in self._device.sys_info diff --git a/kasa/smart/smartchilddevice.py b/kasa/smart/smartchilddevice.py index 3c3b0f292..c6596b969 100644 --- a/kasa/smart/smartchilddevice.py +++ b/kasa/smart/smartchilddevice.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from typing import Any from ..device_type import DeviceType from ..deviceconfig import DeviceConfig @@ -34,7 +35,17 @@ def __init__( self.protocol = _ChildProtocolWrapper(self._id, parent.protocol) async def update(self, update_children: bool = True): - """Noop update. The parent updates our internals.""" + """Update child module info. + + The parent updates our internal info so just update modules with + their own queries. + """ + req: dict[str, Any] = {} + for module in self.modules.values(): + if mod_query := module.query(): + req.update(mod_query) + if req: + self._last_update = await self.protocol.query(req) @classmethod async def create(cls, parent: SmartDevice, child_info, child_components): diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index 6f02fad0d..26bf1396d 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -149,7 +149,7 @@ async def _negotiate(self): if "child_device" in self._components and not self.children: await self._initialize_children() - async def update(self, update_children: bool = True): + async def update(self, update_children: bool = False): """Update the device.""" if self.credentials is None and self.credentials_hash is None: raise AuthenticationError("Tapo plug requires authentication.") @@ -167,9 +167,14 @@ async def update(self, update_children: bool = True): self._last_update = resp = await self.protocol.query(req) self._info = self._try_get_response(resp, "get_device_info") + + # Call child update which will only update module calls, info is updated + # from get_child_device_list. update_children only affects hub devices, other + # devices will always update children to prevent errors on module access. + if update_children or self.device_type != DeviceType.Hub: + for child in self._children.values(): + await child.update() if child_info := self._try_get_response(resp, "get_child_device_list", {}): - # TODO: we don't currently perform queries on children based on modules, - # but just update the information that is returned in the main query. for info in child_info["child_device_list"]: self._children[info["device_id"]]._update_internal_state(info) @@ -352,8 +357,7 @@ def alias(self) -> str | None: @property def time(self) -> datetime: """Return the time.""" - # TODO: Default to parent's time module for child devices - if self._parent and Module.Time in self.modules: + if self._parent and Module.Time in self._parent.modules: _timemod = self._parent.modules[Module.Time] else: _timemod = self.modules[Module.Time] diff --git a/kasa/tests/fakeprotocol_smart.py b/kasa/tests/fakeprotocol_smart.py index b36c254de..533cd6486 100644 --- a/kasa/tests/fakeprotocol_smart.py +++ b/kasa/tests/fakeprotocol_smart.py @@ -149,6 +149,11 @@ def _handle_control_child(self, params: dict): if child["device_id"] == device_id: info = child break + # Create the child_devices fixture section for fixtures generated before it was added + if "child_devices" not in self.info: + self.info["child_devices"] = {} + # Get the method calls made directly on the child devices + child_device_calls = self.info["child_devices"].setdefault(device_id, {}) # We only support get & set device info for now. if child_method == "get_device_info": @@ -159,14 +164,27 @@ def _handle_control_child(self, params: dict): return {"error_code": 0} elif child_method == "set_preset_rules": return self._set_child_preset_rules(info, child_params) + elif child_method in child_device_calls: + result = copy.deepcopy(child_device_calls[child_method]) + return {"result": result, "error_code": 0} elif ( # FIXTURE_MISSING is for service calls not in place when # SMART fixtures started to be generated missing_result := self.FIXTURE_MISSING_MAP.get(child_method) ) and missing_result[0] in self.components: - result = copy.deepcopy(missing_result[1]) + # Copy to info so it will work with update methods + child_device_calls[child_method] = copy.deepcopy(missing_result[1]) + result = copy.deepcopy(info[child_method]) retval = {"result": result, "error_code": 0} return retval + elif child_method[:4] == "set_": + target_method = f"get_{child_method[4:]}" + if target_method not in child_device_calls: + raise RuntimeError( + f"No {target_method} in child info, calling set before get not supported." + ) + child_device_calls[target_method].update(child_params) + return {"error_code": 0} else: # PARAMS error returned for KS240 when get_device_usage called # on parent device. Could be any error code though. diff --git a/kasa/tests/smart/modules/test_autooff.py b/kasa/tests/smart/modules/test_autooff.py index c44617a76..50a1c9921 100644 --- a/kasa/tests/smart/modules/test_autooff.py +++ b/kasa/tests/smart/modules/test_autooff.py @@ -9,7 +9,7 @@ from kasa import Module from kasa.smart import SmartDevice -from kasa.tests.device_fixtures import parametrize +from kasa.tests.device_fixtures import get_parent_and_child_modules, parametrize autooff = parametrize( "has autooff", component_filter="auto_off", protocol_filter={"SMART"} @@ -33,13 +33,13 @@ 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) + autooff = next(get_parent_and_child_modules(dev, Module.AutoOff)) assert autooff is not None prop = getattr(autooff, prop_name) assert isinstance(prop, type) - feat = dev.features[feature] + feat = autooff._device.features[feature] assert feat.value == prop assert isinstance(feat.value, type) @@ -47,13 +47,13 @@ async def test_autooff_features( @autooff async def test_settings(dev: SmartDevice, mocker: MockerFixture): """Test autooff settings.""" - autooff = dev.modules.get(Module.AutoOff) + autooff = next(get_parent_and_child_modules(dev, Module.AutoOff)) assert autooff - enabled = dev.features["auto_off_enabled"] + enabled = autooff._device.features["auto_off_enabled"] assert autooff.enabled == enabled.value - delay = dev.features["auto_off_minutes"] + delay = autooff._device.features["auto_off_minutes"] assert autooff.delay == delay.value call = mocker.spy(autooff, "call") @@ -86,10 +86,10 @@ 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) + autooff = next(get_parent_and_child_modules(dev, Module.AutoOff)) assert autooff - autooff_at = dev.features["auto_off_at"] + autooff_at = autooff._device.features["auto_off_at"] mocker.patch.object( type(autooff), diff --git a/kasa/tests/test_smartdevice.py b/kasa/tests/test_smartdevice.py index 2ffc40ba1..4a260003b 100644 --- a/kasa/tests/test_smartdevice.py +++ b/kasa/tests/test_smartdevice.py @@ -9,7 +9,7 @@ import pytest from pytest_mock import MockerFixture -from kasa import KasaException, Module +from kasa import Device, KasaException, Module from kasa.exceptions import SmartErrorCode from kasa.smart import SmartDevice @@ -112,6 +112,11 @@ async def test_update_module_queries(dev: SmartDevice, mocker: MockerFixture): device_queries: dict[SmartDevice, dict[str, Any]] = {} for mod in dev._modules.values(): device_queries.setdefault(mod._device, {}).update(mod.query()) + # Hubs do not query child modules by default. + if dev.device_type != Device.Type.Hub: + for child in dev.children: + for mod in child.modules.values(): + device_queries.setdefault(mod._device, {}).update(mod.query()) spies = {} for device in device_queries: @@ -120,7 +125,8 @@ async def test_update_module_queries(dev: SmartDevice, mocker: MockerFixture): await dev.update() for device in device_queries: if device_queries[device]: - spies[device].assert_called_with(device_queries[device]) + # Need assert any here because the child device updates use the parent's protocol + spies[device].assert_any_call(device_queries[device]) else: spies[device].assert_not_called() From 447d829abe1f7b5bd27d8deed0c276809b0ecdba Mon Sep 17 00:00:00 2001 From: Teemu R Date: Mon, 10 Jun 2024 17:00:31 +0200 Subject: [PATCH 5/6] Add fixture for p300 1.0.15 (#915) This version adds auto-off for individual strip sockets. --- SUPPORTED.md | 1 + .../fixtures/smart/P300(EU)_1.0_1.0.15.json | 966 ++++++++++++++++++ 2 files changed, 967 insertions(+) create mode 100644 kasa/tests/fixtures/smart/P300(EU)_1.0_1.0.15.json diff --git a/SUPPORTED.md b/SUPPORTED.md index dd63dbc9e..9bc5b6b77 100644 --- a/SUPPORTED.md +++ b/SUPPORTED.md @@ -172,6 +172,7 @@ All Tapo devices require authentication.
Hub-Connected Devices may work acros - **P300** - Hardware: 1.0 (EU) / Firmware: 1.0.13 + - Hardware: 1.0 (EU) / Firmware: 1.0.15 - Hardware: 1.0 (EU) / Firmware: 1.0.7 - **TP25** - Hardware: 1.0 (US) / Firmware: 1.0.2 diff --git a/kasa/tests/fixtures/smart/P300(EU)_1.0_1.0.15.json b/kasa/tests/fixtures/smart/P300(EU)_1.0_1.0.15.json new file mode 100644 index 000000000..dd40708e2 --- /dev/null +++ b/kasa/tests/fixtures/smart/P300(EU)_1.0_1.0.15.json @@ -0,0 +1,966 @@ +{ + "child_devices": { + "SCRUBBED_CHILD_DEVICE_ID_1": { + "component_nego": { + "component_list": [ + { + "id": "device", + "ver_code": 2 + }, + { + "id": "quick_setup", + "ver_code": 3 + }, + { + "id": "time", + "ver_code": 1 + }, + { + "id": "device_local_time", + "ver_code": 1 + }, + { + "id": "schedule", + "ver_code": 2 + }, + { + "id": "countdown", + "ver_code": 2 + }, + { + "id": "antitheft", + "ver_code": 1 + }, + { + "id": "account", + "ver_code": 1 + }, + { + "id": "synchronize", + "ver_code": 1 + }, + { + "id": "sunrise_sunset", + "ver_code": 1 + }, + { + "id": "cloud_connect", + "ver_code": 1 + }, + { + "id": "iot_cloud", + "ver_code": 1 + }, + { + "id": "firmware", + "ver_code": 2 + }, + { + "id": "default_states", + "ver_code": 1 + }, + { + "id": "overheat_protection", + "ver_code": 1 + }, + { + "id": "auto_off", + "ver_code": 2 + } + ] + }, + "get_antitheft_rules": { + "antitheft_rule_max_count": 1, + "enable": false, + "rule_list": [] + }, + "get_auto_off_config": { + "delay_min": 120, + "enable": false + }, + "get_connect_cloud_state": { + "status": 0 + }, + "get_countdown_rules": { + "countdown_rule_max_count": 1, + "enable": false, + "rule_list": [] + }, + "get_device_info": { + "auto_off_remain_time": 0, + "auto_off_status": "off", + "avatar": "", + "bind_count": 1, + "category": "plug.powerstrip.sub-plug", + "default_states": { + "state": { + "on": false + }, + "type": "custom" + }, + "device_id": "SCRUBBED_CHILD_DEVICE_ID_1", + "device_on": true, + "fw_id": "00000000000000000000000000000000", + "fw_ver": "1.0.15 Build 231130 Rel.122554", + "has_set_location_info": true, + "hw_id": "00000000000000000000000000000000", + "hw_ver": "1.0", + "latitude": 0, + "longitude": 0, + "mac": "788CB5000000", + "model": "P300", + "nickname": "I01BU0tFRF9OQU1FIw==", + "oem_id": "00000000000000000000000000000000", + "on_time": 441974, + "original_device_id": "0000000000000000000000000000000000000000", + "overheat_status": "normal", + "position": 3, + "region": "Europe/Berlin", + "slot_number": 3, + "status_follow_edge": true, + "type": "SMART.TAPOPLUG" + }, + "get_device_usage": { + "time_usage": { + "past30": 30367, + "past7": 4909, + "today": 756 + } + }, + "get_next_event": {}, + "get_schedule_rules": { + "enable": false, + "rule_list": [], + "schedule_rule_max_count": 32, + "start_index": 0, + "sum": 0 + } + }, + "SCRUBBED_CHILD_DEVICE_ID_2": { + "component_nego": { + "component_list": [ + { + "id": "device", + "ver_code": 2 + }, + { + "id": "quick_setup", + "ver_code": 3 + }, + { + "id": "time", + "ver_code": 1 + }, + { + "id": "device_local_time", + "ver_code": 1 + }, + { + "id": "schedule", + "ver_code": 2 + }, + { + "id": "countdown", + "ver_code": 2 + }, + { + "id": "antitheft", + "ver_code": 1 + }, + { + "id": "account", + "ver_code": 1 + }, + { + "id": "synchronize", + "ver_code": 1 + }, + { + "id": "sunrise_sunset", + "ver_code": 1 + }, + { + "id": "cloud_connect", + "ver_code": 1 + }, + { + "id": "iot_cloud", + "ver_code": 1 + }, + { + "id": "firmware", + "ver_code": 2 + }, + { + "id": "default_states", + "ver_code": 1 + }, + { + "id": "overheat_protection", + "ver_code": 1 + }, + { + "id": "auto_off", + "ver_code": 2 + } + ] + }, + "get_antitheft_rules": { + "antitheft_rule_max_count": 1, + "enable": false, + "rule_list": [] + }, + "get_auto_off_config": { + "delay_min": 120, + "enable": false + }, + "get_connect_cloud_state": { + "status": 0 + }, + "get_countdown_rules": { + "countdown_rule_max_count": 1, + "enable": false, + "rule_list": [] + }, + "get_device_info": { + "auto_off_remain_time": 0, + "auto_off_status": "off", + "avatar": "", + "bind_count": 1, + "category": "plug.powerstrip.sub-plug", + "default_states": { + "type": "last_states" + }, + "device_id": "SCRUBBED_CHILD_DEVICE_ID_2", + "device_on": true, + "fw_id": "00000000000000000000000000000000", + "fw_ver": "1.0.15 Build 231130 Rel.122554", + "has_set_location_info": true, + "hw_id": "00000000000000000000000000000000", + "hw_ver": "1.0", + "latitude": 0, + "longitude": 0, + "mac": "788CB5000000", + "model": "P300", + "nickname": "I01BU0tFRF9OQU1FIw==", + "oem_id": "00000000000000000000000000000000", + "on_time": 441975, + "original_device_id": "0000000000000000000000000000000000000000", + "overheat_status": "normal", + "position": 2, + "region": "Europe/Berlin", + "slot_number": 3, + "status_follow_edge": true, + "type": "SMART.TAPOPLUG" + }, + "get_device_usage": { + "time_usage": { + "past30": 18287, + "past7": 4909, + "today": 756 + } + }, + "get_next_event": {}, + "get_schedule_rules": { + "enable": false, + "rule_list": [], + "schedule_rule_max_count": 32, + "start_index": 0, + "sum": 0 + } + }, + "SCRUBBED_CHILD_DEVICE_ID_3": { + "component_nego": { + "component_list": [ + { + "id": "device", + "ver_code": 2 + }, + { + "id": "quick_setup", + "ver_code": 3 + }, + { + "id": "time", + "ver_code": 1 + }, + { + "id": "device_local_time", + "ver_code": 1 + }, + { + "id": "schedule", + "ver_code": 2 + }, + { + "id": "countdown", + "ver_code": 2 + }, + { + "id": "antitheft", + "ver_code": 1 + }, + { + "id": "account", + "ver_code": 1 + }, + { + "id": "synchronize", + "ver_code": 1 + }, + { + "id": "sunrise_sunset", + "ver_code": 1 + }, + { + "id": "cloud_connect", + "ver_code": 1 + }, + { + "id": "iot_cloud", + "ver_code": 1 + }, + { + "id": "firmware", + "ver_code": 2 + }, + { + "id": "default_states", + "ver_code": 1 + }, + { + "id": "overheat_protection", + "ver_code": 1 + }, + { + "id": "auto_off", + "ver_code": 2 + } + ] + }, + "get_antitheft_rules": { + "antitheft_rule_max_count": 1, + "enable": false, + "rule_list": [] + }, + "get_auto_off_config": { + "delay_min": 120, + "enable": false + }, + "get_connect_cloud_state": { + "status": 0 + }, + "get_countdown_rules": { + "countdown_rule_max_count": 1, + "enable": false, + "rule_list": [] + }, + "get_device_info": { + "auto_off_remain_time": 0, + "auto_off_status": "off", + "avatar": "", + "bind_count": 1, + "category": "plug.powerstrip.sub-plug", + "default_states": { + "state": { + "on": true + }, + "type": "custom" + }, + "device_id": "SCRUBBED_CHILD_DEVICE_ID_3", + "device_on": true, + "fw_id": "00000000000000000000000000000000", + "fw_ver": "1.0.15 Build 231130 Rel.122554", + "has_set_location_info": true, + "hw_id": "00000000000000000000000000000000", + "hw_ver": "1.0", + "latitude": 0, + "longitude": 0, + "mac": "788CB5000000", + "model": "P300", + "nickname": "I01BU0tFRF9OQU1FIw==", + "oem_id": "00000000000000000000000000000000", + "on_time": 441975, + "original_device_id": "0000000000000000000000000000000000000000", + "overheat_status": "normal", + "position": 1, + "region": "Europe/Berlin", + "slot_number": 3, + "status_follow_edge": true, + "type": "SMART.TAPOPLUG" + }, + "get_device_usage": { + "time_usage": { + "past30": 30383, + "past7": 4909, + "today": 756 + } + }, + "get_next_event": {}, + "get_schedule_rules": { + "enable": false, + "rule_list": [], + "schedule_rule_max_count": 32, + "start_index": 0, + "sum": 0 + } + } + }, + "component_nego": { + "component_list": [ + { + "id": "device", + "ver_code": 2 + }, + { + "id": "firmware", + "ver_code": 2 + }, + { + "id": "quick_setup", + "ver_code": 3 + }, + { + "id": "time", + "ver_code": 1 + }, + { + "id": "wireless", + "ver_code": 1 + }, + { + "id": "schedule", + "ver_code": 2 + }, + { + "id": "countdown", + "ver_code": 2 + }, + { + "id": "antitheft", + "ver_code": 1 + }, + { + "id": "account", + "ver_code": 1 + }, + { + "id": "synchronize", + "ver_code": 1 + }, + { + "id": "sunrise_sunset", + "ver_code": 1 + }, + { + "id": "led", + "ver_code": 1 + }, + { + "id": "cloud_connect", + "ver_code": 1 + }, + { + "id": "iot_cloud", + "ver_code": 1 + }, + { + "id": "device_local_time", + "ver_code": 1 + }, + { + "id": "default_states", + "ver_code": 1 + }, + { + "id": "control_child", + "ver_code": 2 + }, + { + "id": "child_device", + "ver_code": 2 + }, + { + "id": "auto_off", + "ver_code": 2 + }, + { + "id": "homekit", + "ver_code": 2 + }, + { + "id": "overheat_protection", + "ver_code": 1 + } + ] + }, + "discovery_result": { + "device_id": "00000000000000000000000000000000", + "device_model": "P300(EU)", + "device_type": "SMART.TAPOPLUG", + "factory_default": false, + "ip": "127.0.0.123", + "is_support_iot_cloud": true, + "mac": "78-8C-B5-00-00-00", + "mgt_encrypt_schm": { + "encrypt_type": "KLAP", + "http_port": 80, + "is_support_https": false, + "lv": 2 + }, + "obd_src": "tplink", + "owner": "00000000000000000000000000000000" + }, + "get_auto_update_info": { + "enable": false, + "random_range": 120, + "time": 180 + }, + "get_child_device_component_list": { + "child_component_list": [ + { + "component_list": [ + { + "id": "device", + "ver_code": 2 + }, + { + "id": "quick_setup", + "ver_code": 3 + }, + { + "id": "time", + "ver_code": 1 + }, + { + "id": "device_local_time", + "ver_code": 1 + }, + { + "id": "schedule", + "ver_code": 2 + }, + { + "id": "countdown", + "ver_code": 2 + }, + { + "id": "antitheft", + "ver_code": 1 + }, + { + "id": "account", + "ver_code": 1 + }, + { + "id": "synchronize", + "ver_code": 1 + }, + { + "id": "sunrise_sunset", + "ver_code": 1 + }, + { + "id": "cloud_connect", + "ver_code": 1 + }, + { + "id": "iot_cloud", + "ver_code": 1 + }, + { + "id": "firmware", + "ver_code": 2 + }, + { + "id": "default_states", + "ver_code": 1 + }, + { + "id": "overheat_protection", + "ver_code": 1 + }, + { + "id": "auto_off", + "ver_code": 2 + } + ], + "device_id": "SCRUBBED_CHILD_DEVICE_ID_1" + }, + { + "component_list": [ + { + "id": "device", + "ver_code": 2 + }, + { + "id": "quick_setup", + "ver_code": 3 + }, + { + "id": "time", + "ver_code": 1 + }, + { + "id": "device_local_time", + "ver_code": 1 + }, + { + "id": "schedule", + "ver_code": 2 + }, + { + "id": "countdown", + "ver_code": 2 + }, + { + "id": "antitheft", + "ver_code": 1 + }, + { + "id": "account", + "ver_code": 1 + }, + { + "id": "synchronize", + "ver_code": 1 + }, + { + "id": "sunrise_sunset", + "ver_code": 1 + }, + { + "id": "cloud_connect", + "ver_code": 1 + }, + { + "id": "iot_cloud", + "ver_code": 1 + }, + { + "id": "firmware", + "ver_code": 2 + }, + { + "id": "default_states", + "ver_code": 1 + }, + { + "id": "overheat_protection", + "ver_code": 1 + }, + { + "id": "auto_off", + "ver_code": 2 + } + ], + "device_id": "SCRUBBED_CHILD_DEVICE_ID_2" + }, + { + "component_list": [ + { + "id": "device", + "ver_code": 2 + }, + { + "id": "quick_setup", + "ver_code": 3 + }, + { + "id": "time", + "ver_code": 1 + }, + { + "id": "device_local_time", + "ver_code": 1 + }, + { + "id": "schedule", + "ver_code": 2 + }, + { + "id": "countdown", + "ver_code": 2 + }, + { + "id": "antitheft", + "ver_code": 1 + }, + { + "id": "account", + "ver_code": 1 + }, + { + "id": "synchronize", + "ver_code": 1 + }, + { + "id": "sunrise_sunset", + "ver_code": 1 + }, + { + "id": "cloud_connect", + "ver_code": 1 + }, + { + "id": "iot_cloud", + "ver_code": 1 + }, + { + "id": "firmware", + "ver_code": 2 + }, + { + "id": "default_states", + "ver_code": 1 + }, + { + "id": "overheat_protection", + "ver_code": 1 + }, + { + "id": "auto_off", + "ver_code": 2 + } + ], + "device_id": "SCRUBBED_CHILD_DEVICE_ID_3" + } + ], + "start_index": 0, + "sum": 3 + }, + "get_child_device_list": { + "child_device_list": [ + { + "auto_off_remain_time": 0, + "auto_off_status": "off", + "avatar": "", + "bind_count": 1, + "category": "plug.powerstrip.sub-plug", + "default_states": { + "state": { + "on": false + }, + "type": "custom" + }, + "device_id": "SCRUBBED_CHILD_DEVICE_ID_1", + "device_on": true, + "fw_id": "00000000000000000000000000000000", + "fw_ver": "1.0.15 Build 231130 Rel.122554", + "has_set_location_info": true, + "hw_id": "00000000000000000000000000000000", + "hw_ver": "1.0", + "latitude": 0, + "longitude": 0, + "mac": "788CB5000000", + "model": "P300", + "nickname": "I01BU0tFRF9OQU1FIw==", + "oem_id": "00000000000000000000000000000000", + "on_time": 441972, + "original_device_id": "0000000000000000000000000000000000000000", + "overheat_status": "normal", + "position": 3, + "region": "Europe/Berlin", + "slot_number": 3, + "status_follow_edge": true, + "type": "SMART.TAPOPLUG" + }, + { + "auto_off_remain_time": 0, + "auto_off_status": "off", + "avatar": "", + "bind_count": 1, + "category": "plug.powerstrip.sub-plug", + "default_states": { + "type": "last_states" + }, + "device_id": "SCRUBBED_CHILD_DEVICE_ID_2", + "device_on": true, + "fw_id": "00000000000000000000000000000000", + "fw_ver": "1.0.15 Build 231130 Rel.122554", + "has_set_location_info": true, + "hw_id": "00000000000000000000000000000000", + "hw_ver": "1.0", + "latitude": 0, + "longitude": 0, + "mac": "788CB5000000", + "model": "P300", + "nickname": "I01BU0tFRF9OQU1FIw==", + "oem_id": "00000000000000000000000000000000", + "on_time": 441972, + "original_device_id": "0000000000000000000000000000000000000000", + "overheat_status": "normal", + "position": 2, + "region": "Europe/Berlin", + "slot_number": 3, + "status_follow_edge": true, + "type": "SMART.TAPOPLUG" + }, + { + "auto_off_remain_time": 0, + "auto_off_status": "off", + "avatar": "", + "bind_count": 1, + "category": "plug.powerstrip.sub-plug", + "default_states": { + "state": { + "on": true + }, + "type": "custom" + }, + "device_id": "SCRUBBED_CHILD_DEVICE_ID_3", + "device_on": true, + "fw_id": "00000000000000000000000000000000", + "fw_ver": "1.0.15 Build 231130 Rel.122554", + "has_set_location_info": true, + "hw_id": "00000000000000000000000000000000", + "hw_ver": "1.0", + "latitude": 0, + "longitude": 0, + "mac": "788CB5000000", + "model": "P300", + "nickname": "I01BU0tFRF9OQU1FIw==", + "oem_id": "00000000000000000000000000000000", + "on_time": 441972, + "original_device_id": "0000000000000000000000000000000000000000", + "overheat_status": "normal", + "position": 1, + "region": "Europe/Berlin", + "slot_number": 3, + "status_follow_edge": true, + "type": "SMART.TAPOPLUG" + } + ], + "start_index": 0, + "sum": 3 + }, + "get_connect_cloud_state": { + "status": 0 + }, + "get_device_info": { + "avatar": "", + "device_id": "0000000000000000000000000000000000000000", + "fw_id": "00000000000000000000000000000000", + "fw_ver": "1.0.15 Build 231130 Rel.122554", + "has_set_location_info": true, + "hw_id": "00000000000000000000000000000000", + "hw_ver": "1.0", + "ip": "127.0.0.123", + "lang": "de_DE", + "latitude": 0, + "longitude": 0, + "mac": "78-8C-B5-00-00-00", + "model": "P300", + "nickname": "I01BU0tFRF9OQU1FIw==", + "oem_id": "00000000000000000000000000000000", + "region": "Europe/Berlin", + "rssi": -61, + "signal_level": 2, + "specs": "", + "ssid": "I01BU0tFRF9TU0lEIw==", + "time_diff": 60, + "type": "SMART.TAPOPLUG" + }, + "get_device_time": { + "region": "Europe/Berlin", + "time_diff": 60, + "timestamp": 1715622973 + }, + "get_device_usage": { + "time_usage": { + "past30": 30383, + "past7": 4909, + "today": 756 + } + }, + "get_fw_download_state": { + "auto_upgrade": false, + "download_progress": 0, + "reboot_time": 5, + "status": 0, + "upgrade_time": 5 + }, + "get_homekit_info": { + "mfi_setup_code": "000-00-000", + "mfi_setup_id": "0000", + "mfi_token_token": "00000000000000000000000000000000000000000000000000000000000000000000000000000/00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/000000000000000000==", + "mfi_token_uuid": "00000000-0000-0000-0000-000000000000" + }, + "get_latest_fw": { + "fw_size": 0, + "fw_ver": "1.0.15 Build 231130 Rel.122554", + "hw_id": "", + "need_to_upgrade": false, + "oem_id": "", + "release_date": "", + "release_note": "", + "type": 0 + }, + "get_led_info": { + "led_rule": "never", + "led_status": false, + "night_mode": { + "end_time": 340, + "night_mode_type": "sunrise_sunset", + "start_time": 1277, + "sunrise_offset": 0, + "sunset_offset": 0 + } + }, + "get_wireless_scan_info": { + "ap_list": [ + { + "bssid": "000000000000", + "channel": 0, + "cipher_type": 2, + "key_type": "wpa2_psk", + "signal_level": 2, + "ssid": "I01BU0tFRF9TU0lEIw==" + } + ], + "start_index": 0, + "sum": 19, + "wep_supported": false + }, + "qs_component_nego": { + "component_list": [ + { + "id": "quick_setup", + "ver_code": 3 + }, + { + "id": "sunrise_sunset", + "ver_code": 1 + }, + { + "id": "ble_whole_setup", + "ver_code": 1 + }, + { + "id": "iot_cloud", + "ver_code": 1 + }, + { + "id": "inherit", + "ver_code": 1 + }, + { + "id": "firmware", + "ver_code": 2 + }, + { + "id": "control_child", + "ver_code": 2 + }, + { + "id": "child_device", + "ver_code": 2 + } + ], + "extra_info": { + "device_model": "P300", + "device_type": "SMART.TAPOPLUG", + "is_klap": true + } + } +} From 57cbd3cb58fc746e0057a9e1a7a6dec6bd7734b1 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:59:17 +0100 Subject: [PATCH 6/6] Prepare 0.7.0.dev4 (#969) ## [0.7.0.dev4](https://github.com/python-kasa/python-kasa/tree/0.7.0.dev4) (2024-06-10) [Full Changelog](https://github.com/python-kasa/python-kasa/compare/0.7.0.dev3...0.7.0.dev4) **Implemented enhancements:** - Support smart child modules queries [\#967](https://github.com/python-kasa/python-kasa/pull/967) (@sdb9696) - Do not expose child modules on parent devices [\#964](https://github.com/python-kasa/python-kasa/pull/964) (@sdb9696) - Do not add parent only modules to strip sockets [\#963](https://github.com/python-kasa/python-kasa/pull/963) (@sdb9696) **Project maintenance:** - Better checking of child modules not supported by parent device [\#966](https://github.com/python-kasa/python-kasa/pull/966) (@sdb9696) - Add fixture for p300 1.0.15 [\#915](https://github.com/python-kasa/python-kasa/pull/915) (@rytilahti) --- CHANGELOG.md | 15 +++++++++++++++ poetry.lock | 26 +++++++++++++------------- pyproject.toml | 2 +- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4febcb7e..1b5f623b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [0.7.0.dev4](https://github.com/python-kasa/python-kasa/tree/0.7.0.dev4) (2024-06-10) + +[Full Changelog](https://github.com/python-kasa/python-kasa/compare/0.7.0.dev3...0.7.0.dev4) + +**Implemented enhancements:** + +- Support smart child modules queries [\#967](https://github.com/python-kasa/python-kasa/pull/967) (@sdb9696) +- Do not expose child modules on parent devices [\#964](https://github.com/python-kasa/python-kasa/pull/964) (@sdb9696) +- Do not add parent only modules to strip sockets [\#963](https://github.com/python-kasa/python-kasa/pull/963) (@sdb9696) + +**Project maintenance:** + +- Better checking of child modules not supported by parent device [\#966](https://github.com/python-kasa/python-kasa/pull/966) (@sdb9696) +- Add fixture for p300 1.0.15 [\#915](https://github.com/python-kasa/python-kasa/pull/915) (@rytilahti) + ## [0.7.0.dev3](https://github.com/python-kasa/python-kasa/tree/0.7.0.dev3) (2024-06-07) [Full Changelog](https://github.com/python-kasa/python-kasa/compare/0.7.0.dev2...0.7.0.dev3) diff --git a/poetry.lock b/poetry.lock index 71310f732..c2f9c7240 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1190,13 +1190,13 @@ files = [ [[package]] name = "packaging" -version = "24.0" +version = "24.1" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] @@ -1265,13 +1265,13 @@ virtualenv = ">=20.10.0" [[package]] name = "prompt-toolkit" -version = "3.0.46" +version = "3.0.47" description = "Library for building powerful interactive command lines in Python" optional = true python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.46-py3-none-any.whl", hash = "sha256:45abe60a8300f3c618b23c16c4bb98c6fc80af8ce8b17c7ae92db48db3ee63c1"}, - {file = "prompt_toolkit-3.0.46.tar.gz", hash = "sha256:869c50d682152336e23c4db7f74667639b5047494202ffe7670817053fd57795"}, + {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, + {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, ] [package.dependencies] @@ -1968,13 +1968,13 @@ testing = ["build[virtualenv] (>=1.0.3)", "covdefaults (>=2.3)", "detect-test-po [[package]] name = "typing-extensions" -version = "4.12.1" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.12.1-py3-none-any.whl", hash = "sha256:6024b58b69089e5a89c347397254e35f1bf02a907728ec7fee9bf0fe837d203a"}, - {file = "typing_extensions-4.12.1.tar.gz", hash = "sha256:915f5e35ff76f56588223f15fdd5938f9a1cf9195c0de25130c627e4d597f6d1"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] @@ -2038,13 +2038,13 @@ files = [ [[package]] name = "xdoctest" -version = "1.1.4" +version = "1.1.5" description = "A rewrite of the builtin doctest module" optional = false python-versions = ">=3.6" files = [ - {file = "xdoctest-1.1.4-py3-none-any.whl", hash = "sha256:2ee7920603e1a977749cabf611dfde1935165c6ac83dcfb2c9bdf8fc3ac1ec26"}, - {file = "xdoctest-1.1.4.tar.gz", hash = "sha256:eb3fbad5a9ac4d47b2fafa60435ac15f2cbcd33dc860bf1e759a1f63bfeddc10"}, + {file = "xdoctest-1.1.5-py3-none-any.whl", hash = "sha256:f36fe64d7c0ad0553dbff39ff05c43a0aab69d313466f24a38d00e757182ade0"}, + {file = "xdoctest-1.1.5.tar.gz", hash = "sha256:89b0c3ad7fe03a068e22a457ab18c38fc70c62329c2963f43954b83c29374e66"}, ] [package.extras] diff --git a/pyproject.toml b/pyproject.toml index feadb1ba8..d6fdb8cb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "python-kasa" -version = "0.7.0.dev3" +version = "0.7.0.dev4" description = "Python API for TP-Link Kasa Smarthome devices" license = "GPL-3.0-or-later" authors = ["python-kasa developers"]