From 1fea88ef1c0b34115634101ef783e5826313ac62 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Wed, 4 Dec 2024 19:09:57 +0000 Subject: [PATCH 1/5] Use DeviceInfo consistently across devices --- kasa/cli/device.py | 10 ++++++++-- kasa/device.py | 20 ++++++++++++++++---- kasa/discover.py | 12 ++++++------ kasa/iot/iotdevice.py | 27 ++++++++++++--------------- kasa/smart/smartchilddevice.py | 15 +++++++++++++++ kasa/smart/smartdevice.py | 18 ++++++------------ kasa/smartcam/smartcamdevice.py | 4 ++-- tests/device_fixtures.py | 8 ++++++-- tests/iot/test_iotdevice.py | 4 ++-- tests/test_discovery.py | 15 ++++++++------- tests/test_readme_examples.py | 2 +- 11 files changed, 82 insertions(+), 53 deletions(-) diff --git a/kasa/cli/device.py b/kasa/cli/device.py index 2e621368e..61ccb1732 100644 --- a/kasa/cli/device.py +++ b/kasa/cli/device.py @@ -41,8 +41,14 @@ async def state(ctx, dev: Device): echo(f"Device state: {dev.is_on}") echo(f"Time: {dev.time} (tz: {dev.timezone})") - echo(f"Hardware: {dev.hw_info['hw_ver']}") - echo(f"Software: {dev.hw_info['sw_ver']}") + echo( + f"Hardware: {dev._device_info.hardware_version}" + f"{' (' + dev.region + ')' if dev.region else ''}" + ) + echo( + f"Firmware: {dev._device_info.firmware_version}" + f" {dev._device_info.firmware_build}" + ) echo(f"MAC (rssi): {dev.mac} ({dev.rssi})") if verbose: echo(f"Location: {dev.location}") diff --git a/kasa/device.py b/kasa/device.py index 76d7a7c59..4081ba13f 100644 --- a/kasa/device.py +++ b/kasa/device.py @@ -29,7 +29,7 @@ >>> dev.alias Bedroom Lamp Plug >>> dev.model -HS110(EU) +HS110 >>> dev.rssi -71 >>> dev.mac @@ -208,7 +208,7 @@ def __init__( self.protocol: BaseProtocol = protocol or IotProtocol( transport=XorTransport(config=config or DeviceConfig(host=host)), ) - self._last_update: Any = None + self._last_update: dict[str, Any] = {} _LOGGER.debug("Initializing %s of type %s", host, type(self)) self._device_type = DeviceType.Unknown # TODO: typing Any is just as using dict | None would require separate @@ -334,9 +334,21 @@ def model(self) -> str: """Returns the device model.""" @property + def region(self) -> str | None: + """Returns the device region.""" + return self._device_info.region + + @property + def _device_info(self) -> _DeviceInfo: + """Return device info.""" + return self._get_device_info(self._last_update, self._discovery_info) + + @staticmethod @abstractmethod - def _model_region(self) -> str: - """Return device full model name and region.""" + def _get_device_info( + info: dict[str, Any], discovery_info: dict[str, Any] | None + ) -> _DeviceInfo: + """Get device info.""" @property @abstractmethod diff --git a/kasa/discover.py b/kasa/discover.py index 771c3f5c1..d8ff31122 100755 --- a/kasa/discover.py +++ b/kasa/discover.py @@ -22,7 +22,7 @@ >>> >>> found_devices = await Discover.discover() >>> [dev.model for dev in found_devices.values()] -['KP303(UK)', 'HS110(EU)', 'L530E', 'KL430(US)', 'HS220(US)'] +['KP303', 'HS110', 'L530E', 'KL430', 'HS220'] You can pass username and password for devices requiring authentication @@ -65,17 +65,17 @@ >>> print(f"Discovered {dev.alias} (model: {dev.model})") >>> >>> devices = await Discover.discover(on_discovered=print_dev_info, credentials=creds) -Discovered Bedroom Power Strip (model: KP303(UK)) -Discovered Bedroom Lamp Plug (model: HS110(EU)) +Discovered Bedroom Power Strip (model: KP303) +Discovered Bedroom Lamp Plug (model: HS110) Discovered Living Room Bulb (model: L530) -Discovered Bedroom Lightstrip (model: KL430(US)) -Discovered Living Room Dimmer Switch (model: HS220(US)) +Discovered Bedroom Lightstrip (model: KL430) +Discovered Living Room Dimmer Switch (model: HS220) Discovering a single device returns a kasa.Device object. >>> device = await Discover.discover_single("127.0.0.1", credentials=creds) >>> device.model -'KP303(UK)' +'KP303' """ diff --git a/kasa/iot/iotdevice.py b/kasa/iot/iotdevice.py index f23ebc8bd..320a03bc9 100755 --- a/kasa/iot/iotdevice.py +++ b/kasa/iot/iotdevice.py @@ -43,7 +43,7 @@ def requires_update(f: Callable) -> Any: @functools.wraps(f) async def wrapped(*args: Any, **kwargs: Any) -> Any: self = args[0] - if self._last_update is None and ( + if not self._last_update and ( self._sys_info is None or f.__name__ not in self._sys_info ): raise KasaException("You need to await update() to access the data") @@ -54,7 +54,7 @@ async def wrapped(*args: Any, **kwargs: Any) -> Any: @functools.wraps(f) def wrapped(*args: Any, **kwargs: Any) -> Any: self = args[0] - if self._last_update is None and ( + if not self._last_update and ( self._sys_info is None or f.__name__ not in self._sys_info ): raise KasaException("You need to await update() to access the data") @@ -102,7 +102,7 @@ class IotDevice(Device): >>> dev.alias Bedroom Lamp Plug >>> dev.model - HS110(EU) + HS110 >>> dev.rssi -71 >>> dev.mac @@ -300,7 +300,7 @@ async def update(self, update_children: bool = True) -> None: # If this is the initial update, check only for the sysinfo # This is necessary as some devices crash on unexpected modules # See #105, #120, #161 - if self._last_update is None: + if not self._last_update: _LOGGER.debug("Performing the initial update to obtain sysinfo") response = await self.protocol.query(req) self._last_update = response @@ -442,7 +442,9 @@ def update_from_discover_info(self, info: dict[str, Any]) -> None: # This allows setting of some info properties directly # from partial discovery info that will then be found # by the requires_update decorator - self._set_sys_info(info) + discovery_model = info["device_model"] + no_region_model, _, _ = discovery_model.partition("(") + self._set_sys_info({**info, "model": no_region_model}) def _set_sys_info(self, sys_info: dict[str, Any]) -> None: """Set sys_info.""" @@ -461,18 +463,13 @@ class itself as @requires_update will be affected for other properties. """ return self._sys_info # type: ignore - @property # type: ignore - @requires_update - def model(self) -> str: - """Return device model.""" - sys_info = self._sys_info - return str(sys_info["model"]) - @property @requires_update - def _model_region(self) -> str: - """Return device full model name and region.""" - return self.model + def model(self) -> str: + """Returns the device model.""" + if self._last_update: + return self._device_info.short_name + return self._sys_info["model"] @property # type: ignore def alias(self) -> str | None: diff --git a/kasa/smart/smartchilddevice.py b/kasa/smart/smartchilddevice.py index db3319f3c..65f52c722 100644 --- a/kasa/smart/smartchilddevice.py +++ b/kasa/smart/smartchilddevice.py @@ -6,6 +6,7 @@ import time from typing import Any +from ..device import _DeviceInfo from ..device_type import DeviceType from ..deviceconfig import DeviceConfig from ..protocols.smartprotocol import SmartProtocol, _ChildProtocolWrapper @@ -49,6 +50,20 @@ def __init__( self._update_internal_state(info) self._components = component_info + @property + def _device_info(self) -> _DeviceInfo: + """Return device info.""" + components = [ + {"id": id, "ver_code": ver} for id, ver in self._components.items() + ] + return self._get_device_info( + { + "get_device_info": self._info, + "component_nego": {"component_list": components}, + }, + None, + ) + async def update(self, update_children: bool = True) -> None: """Update child module info. diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index adb4829d5..c5eae3080 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -67,7 +67,6 @@ def __init__( self._modules: dict[str | ModuleName[Module], SmartModule] = {} self._parent: SmartDevice | None = None self._children: Mapping[str, SmartDevice] = {} - self._last_update = {} self._last_update_time: float | None = None self._on_since: datetime | None = None self._info: dict[str, Any] = {} @@ -500,18 +499,13 @@ def sys_info(self) -> dict[str, Any]: @property def model(self) -> str: """Returns the device model.""" - return str(self._info.get("model")) + # If update hasn't been called self._device_info can't be used + if self._last_update: + return self._device_info.short_name - @property - def _model_region(self) -> str: - """Return device full model name and region.""" - if (disco := self._discovery_info) and ( - disco_model := disco.get("device_model") - ): - return disco_model - # Some devices have the region in the specs element. - region = f"({specs})" if (specs := self._info.get("specs")) else "" - return f"{self.model}{region}" + disco_model = str(self._info.get("device_model")) + long_name, _, _ = disco_model.partition("(") + return long_name @property def alias(self) -> str | None: diff --git a/kasa/smartcam/smartcamdevice.py b/kasa/smartcam/smartcamdevice.py index 0e49be264..dcc3cc94f 100644 --- a/kasa/smartcam/smartcamdevice.py +++ b/kasa/smartcam/smartcamdevice.py @@ -243,8 +243,8 @@ async def set_alias(self, alias: str) -> dict: def hw_info(self) -> dict: """Return hardware info for the device.""" return { - "sw_ver": self._info.get("hw_ver"), - "hw_ver": self._info.get("fw_ver"), + "sw_ver": self._info.get("fw_ver"), + "hw_ver": self._info.get("hw_ver"), "mac": self._info.get("mac"), "type": self._info.get("type"), "hwId": self._info.get("hwId"), diff --git a/tests/device_fixtures.py b/tests/device_fixtures.py index d206b714a..1b4eb8fd8 100644 --- a/tests/device_fixtures.py +++ b/tests/device_fixtures.py @@ -472,8 +472,12 @@ def get_nearest_fixture_to_ip(dev): assert protocol_fixtures, "Unknown device type" # This will get the best fixture with a match on model region - if model_region_fixtures := filter_fixtures( - "", model_filter={dev._model_region}, fixture_list=protocol_fixtures + if (di := dev._device_info) and ( + model_region_fixtures := filter_fixtures( + "", + model_filter={di.long_name + (f"({di.region})" if di.region else "")}, + fixture_list=protocol_fixtures, + ) ): return next(iter(model_region_fixtures)) diff --git a/tests/iot/test_iotdevice.py b/tests/iot/test_iotdevice.py index 124910b79..858c5fbcf 100644 --- a/tests/iot/test_iotdevice.py +++ b/tests/iot/test_iotdevice.py @@ -99,7 +99,7 @@ async def test_invalid_connection(mocker, dev): @has_emeter_iot async def test_initial_update_emeter(dev, mocker): """Test that the initial update performs second query if emeter is available.""" - dev._last_update = None + dev._last_update = {} dev._legacy_features = set() spy = mocker.spy(dev.protocol, "query") await dev.update() @@ -111,7 +111,7 @@ async def test_initial_update_emeter(dev, mocker): @no_emeter_iot async def test_initial_update_no_emeter(dev, mocker): """Test that the initial update performs second query if emeter is available.""" - dev._last_update = None + dev._last_update = {} dev._legacy_features = set() spy = mocker.spy(dev.protocol, "query") await dev.update() diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 7069e32f6..36f645ef0 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -390,13 +390,14 @@ async def test_device_update_from_new_discovery_info(discovery_mock): device_class = Discover._get_device_class(discovery_data) device = device_class("127.0.0.1") discover_info = DiscoveryResult.from_dict(discovery_data["result"]) - discover_dump = discover_info.to_dict() - model, _, _ = discover_dump["device_model"].partition("(") - discover_dump["model"] = model - device.update_from_discover_info(discover_dump) - - assert device.mac == discover_dump["mac"].replace("-", ":") - assert device.model == model + # discover_dump = discover_info.to_dict() + # model, _, _ = discover_dump["device_model"].partition("(") + # discover_dump["model"] = model + device.update_from_discover_info(discovery_data["result"]) + + assert device.mac == discover_info.mac.replace("-", ":") + no_region_model, _, _ = discover_info.device_model.partition("(") + assert device.model == no_region_model # TODO implement requires_update for SmartDevice if isinstance(device, IotDevice): diff --git a/tests/test_readme_examples.py b/tests/test_readme_examples.py index 394a3aff7..da928fdb3 100644 --- a/tests/test_readme_examples.py +++ b/tests/test_readme_examples.py @@ -19,7 +19,7 @@ def test_bulb_examples(mocker): assert not res["failed"] -def test_smartdevice_examples(mocker): +def test_iotdevice_examples(mocker): """Use HS110 for emeter examples.""" p = asyncio.run(get_device_for_fixture_protocol("HS110(EU)_1.0_1.2.5.json", "IOT")) mocker.patch("kasa.iot.iotdevice.IotDevice", return_value=p) From 0c7a3557770a196ddccd07f7d966c0900fbd68e1 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:49:19 +0000 Subject: [PATCH 2/5] Remove commented code --- tests/test_discovery.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 36f645ef0..59a337d2e 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -390,9 +390,7 @@ async def test_device_update_from_new_discovery_info(discovery_mock): device_class = Discover._get_device_class(discovery_data) device = device_class("127.0.0.1") discover_info = DiscoveryResult.from_dict(discovery_data["result"]) - # discover_dump = discover_info.to_dict() - # model, _, _ = discover_dump["device_model"].partition("(") - # discover_dump["model"] = model + device.update_from_discover_info(discovery_data["result"]) assert device.mac == discover_info.mac.replace("-", ":") From 26b1011934cb8368d746d965144605bc183f031a Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 5 Dec 2024 09:29:30 +0000 Subject: [PATCH 3/5] Fix test after merge with master --- tests/smart/test_smartdevice.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/smart/test_smartdevice.py b/tests/smart/test_smartdevice.py index 81707a11a..162fb777c 100644 --- a/tests/smart/test_smartdevice.py +++ b/tests/smart/test_smartdevice.py @@ -62,11 +62,14 @@ async def test_device_type_no_update(discovery_mock, caplog: pytest.LogCaptureFi assert repr(dev) == f"" discovery_result = copy.deepcopy(discovery_mock.discovery_data["result"]) + + disco_model = discovery_result["device_model"] + short_model, _, _ = disco_model.partition("(") dev.update_from_discover_info(discovery_result) assert dev.device_type is DeviceType.Unknown assert ( repr(dev) - == f"" + == f"" ) discovery_result["device_type"] = "SMART.FOOBAR" dev.update_from_discover_info(discovery_result) @@ -74,7 +77,7 @@ async def test_device_type_no_update(discovery_mock, caplog: pytest.LogCaptureFi assert dev.device_type is DeviceType.Plug assert ( repr(dev) - == f"" + == f"" ) assert "Unknown device type, falling back to plug" in caplog.text From af629c246b731465e6b73cf6199beba789508a86 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:43:44 +0000 Subject: [PATCH 4/5] Do not recreate raw components in child --- kasa/smart/smartchilddevice.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/kasa/smart/smartchilddevice.py b/kasa/smart/smartchilddevice.py index f03e33d46..181567a0d 100644 --- a/kasa/smart/smartchilddevice.py +++ b/kasa/smart/smartchilddevice.py @@ -53,14 +53,16 @@ def __init__( @property def _device_info(self) -> _DeviceInfo: - """Return device info.""" - components = [ - {"id": id, "ver_code": ver} for id, ver in self._components.items() - ] + """Return device info. + + Child device does not have it info and components in _last_update so + this overrides the base implementation to call _get_device_info with + info and components combined as they would be in _last_update. + """ return self._get_device_info( { "get_device_info": self._info, - "component_nego": {"component_list": components}, + "component_nego": self._components_raw, }, None, ) From ae9f1bb359105fd915d6126ba2a8aa88df8fdd3f Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:24:33 +0000 Subject: [PATCH 5/5] Make device_info public --- kasa/cli/device.py | 6 +++--- kasa/device.py | 8 ++++---- kasa/iot/iotdevice.py | 8 ++++---- kasa/smart/smartchilddevice.py | 4 ++-- kasa/smart/smartdevice.py | 8 ++++---- kasa/smartcam/smartcamdevice.py | 6 +++--- tests/device_fixtures.py | 2 +- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/kasa/cli/device.py b/kasa/cli/device.py index 61ccb1732..0ef8a76f8 100644 --- a/kasa/cli/device.py +++ b/kasa/cli/device.py @@ -42,12 +42,12 @@ async def state(ctx, dev: Device): echo(f"Time: {dev.time} (tz: {dev.timezone})") echo( - f"Hardware: {dev._device_info.hardware_version}" + f"Hardware: {dev.device_info.hardware_version}" f"{' (' + dev.region + ')' if dev.region else ''}" ) echo( - f"Firmware: {dev._device_info.firmware_version}" - f" {dev._device_info.firmware_build}" + f"Firmware: {dev.device_info.firmware_version}" + f" {dev.device_info.firmware_build}" ) echo(f"MAC (rssi): {dev.mac} ({dev.rssi})") if verbose: diff --git a/kasa/device.py b/kasa/device.py index 4081ba13f..360682323 100644 --- a/kasa/device.py +++ b/kasa/device.py @@ -151,7 +151,7 @@ class WifiNetwork: @dataclass -class _DeviceInfo: +class DeviceInfo: """Device Model Information.""" short_name: str @@ -336,10 +336,10 @@ def model(self) -> str: @property def region(self) -> str | None: """Returns the device region.""" - return self._device_info.region + return self.device_info.region @property - def _device_info(self) -> _DeviceInfo: + def device_info(self) -> DeviceInfo: """Return device info.""" return self._get_device_info(self._last_update, self._discovery_info) @@ -347,7 +347,7 @@ def _device_info(self) -> _DeviceInfo: @abstractmethod def _get_device_info( info: dict[str, Any], discovery_info: dict[str, Any] | None - ) -> _DeviceInfo: + ) -> DeviceInfo: """Get device info.""" @property diff --git a/kasa/iot/iotdevice.py b/kasa/iot/iotdevice.py index f3086385a..851f21ccc 100755 --- a/kasa/iot/iotdevice.py +++ b/kasa/iot/iotdevice.py @@ -22,7 +22,7 @@ from typing import TYPE_CHECKING, Any, cast from warnings import warn -from ..device import Device, WifiNetwork, _DeviceInfo +from ..device import Device, DeviceInfo, WifiNetwork from ..device_type import DeviceType from ..deviceconfig import DeviceConfig from ..exceptions import KasaException @@ -478,7 +478,7 @@ class itself as @requires_update will be affected for other properties. def model(self) -> str: """Returns the device model.""" if self._last_update: - return self._device_info.short_name + return self.device_info.short_name return self._sys_info["model"] @property # type: ignore @@ -745,7 +745,7 @@ def _get_device_type_from_sys_info(info: dict[str, Any]) -> DeviceType: @staticmethod def _get_device_info( info: dict[str, Any], discovery_info: dict[str, Any] | None - ) -> _DeviceInfo: + ) -> DeviceInfo: """Get model information for a device.""" sys_info = _extract_sys_info(info) @@ -763,7 +763,7 @@ def _get_device_info( firmware_version, firmware_build = fw_version_full.split(" ", maxsplit=1) auth = bool(discovery_info and ("mgt_encrypt_schm" in discovery_info)) - return _DeviceInfo( + return DeviceInfo( short_name=long_name, long_name=long_name, brand="kasa", diff --git a/kasa/smart/smartchilddevice.py b/kasa/smart/smartchilddevice.py index 181567a0d..2ef0454fe 100644 --- a/kasa/smart/smartchilddevice.py +++ b/kasa/smart/smartchilddevice.py @@ -6,7 +6,7 @@ import time from typing import Any -from ..device import _DeviceInfo +from ..device import DeviceInfo from ..device_type import DeviceType from ..deviceconfig import DeviceConfig from ..protocols.smartprotocol import SmartProtocol, _ChildProtocolWrapper @@ -52,7 +52,7 @@ def __init__( self._components = self._parse_components(self._components_raw) @property - def _device_info(self) -> _DeviceInfo: + def device_info(self) -> DeviceInfo: """Return device info. Child device does not have it info and components in _last_update so diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index 429027f51..5fd221157 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -9,7 +9,7 @@ from datetime import UTC, datetime, timedelta, tzinfo from typing import TYPE_CHECKING, Any, TypeAlias, cast -from ..device import Device, WifiNetwork, _DeviceInfo +from ..device import Device, DeviceInfo, WifiNetwork from ..device_type import DeviceType from ..deviceconfig import DeviceConfig from ..exceptions import AuthenticationError, DeviceError, KasaException, SmartErrorCode @@ -498,7 +498,7 @@ def model(self) -> str: """Returns the device model.""" # If update hasn't been called self._device_info can't be used if self._last_update: - return self._device_info.short_name + return self.device_info.short_name disco_model = str(self._info.get("device_model")) long_name, _, _ = disco_model.partition("(") @@ -802,7 +802,7 @@ def _get_device_type_from_components( @staticmethod def _get_device_info( info: dict[str, Any], discovery_info: dict[str, Any] | None - ) -> _DeviceInfo: + ) -> DeviceInfo: """Get model information for a device.""" di = info["get_device_info"] components = [comp["id"] for comp in info["component_nego"]["component_list"]] @@ -831,7 +831,7 @@ def _get_device_info( # Brand inferred from SMART.KASAPLUG/SMART.TAPOPLUG etc. brand = devicetype[:4].lower() - return _DeviceInfo( + return DeviceInfo( short_name=short_name, long_name=long_name, brand=brand, diff --git a/kasa/smartcam/smartcamdevice.py b/kasa/smartcam/smartcamdevice.py index 9cbc2d85b..b3058ab33 100644 --- a/kasa/smartcam/smartcamdevice.py +++ b/kasa/smartcam/smartcamdevice.py @@ -5,7 +5,7 @@ import logging from typing import Any, cast -from ..device import _DeviceInfo +from ..device import DeviceInfo from ..device_type import DeviceType from ..module import Module from ..protocols.smartcamprotocol import _ChildCameraProtocolWrapper @@ -37,7 +37,7 @@ def _get_device_type_from_sysinfo(sysinfo: dict[str, Any]) -> DeviceType: @staticmethod def _get_device_info( info: dict[str, Any], discovery_info: dict[str, Any] | None - ) -> _DeviceInfo: + ) -> DeviceInfo: """Get model information for a device.""" basic_info = info["getDeviceInfo"]["device_info"]["basic_info"] short_name = basic_info["device_model"] @@ -45,7 +45,7 @@ def _get_device_info( device_type = SmartCamDevice._get_device_type_from_sysinfo(basic_info) fw_version_full = basic_info["sw_version"] firmware_version, firmware_build = fw_version_full.split(" ", maxsplit=1) - return _DeviceInfo( + return DeviceInfo( short_name=basic_info["device_model"], long_name=long_name, brand="tapo", diff --git a/tests/device_fixtures.py b/tests/device_fixtures.py index a4cf8781a..6679d0a5c 100644 --- a/tests/device_fixtures.py +++ b/tests/device_fixtures.py @@ -473,7 +473,7 @@ def get_nearest_fixture_to_ip(dev): assert protocol_fixtures, "Unknown device type" # This will get the best fixture with a match on model region - if (di := dev._device_info) and ( + if (di := dev.device_info) and ( model_region_fixtures := filter_fixtures( "", model_filter={di.long_name + (f"({di.region})" if di.region else "")},