Skip to content

Commit d02e017

Browse files
sdb9696rytilahti
andcommitted
Fix repr for device created with no sysinfo or discovery info" (python-kasa#1266)
Co-authored-by: Teemu R. <tpr@iki.fi>
1 parent 822b6f9 commit d02e017

File tree

7 files changed

+101
-18
lines changed

7 files changed

+101
-18
lines changed

kasa/device.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,12 +210,12 @@ def __init__(
210210
self.protocol: BaseProtocol = protocol or IotProtocol(
211211
transport=XorTransport(config=config or DeviceConfig(host=host)),
212212
)
213-
_LOGGER.debug("Initializing %s of type %s", self.host, type(self))
213+
self._last_update: Any = None
214+
_LOGGER.debug("Initializing %s of type %s", host, type(self))
214215
self._device_type = DeviceType.Unknown
215216
# TODO: typing Any is just as using Optional[Dict] would require separate
216217
# checks in accessors. the @updated_required decorator does not ensure
217218
# mypy that these are not accessed incorrectly.
218-
self._last_update: Any = None
219219
self._discovery_info: dict[str, Any] | None = None
220220

221221
self._features: dict[str, Feature] = {}
@@ -492,6 +492,8 @@ async def factory_reset(self) -> None:
492492

493493
def __repr__(self) -> str:
494494
update_needed = " - update() needed" if not self._last_update else ""
495+
if not self._last_update and not self._discovery_info:
496+
return f"<{self.device_type} at {self.host}{update_needed}>"
495497
return (
496498
f"<{self.device_type} at {self.host} -"
497499
f" {self.alias} ({self.model}){update_needed}>"

kasa/iot/iotdevice.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ def requires_update(f: Callable) -> Any:
4242
@functools.wraps(f)
4343
async def wrapped(*args: Any, **kwargs: Any) -> Any:
4444
self = args[0]
45-
if self._last_update is None and f.__name__ not in self._sys_info:
45+
if self._last_update is None and (
46+
self._sys_info is None or f.__name__ not in self._sys_info
47+
):
4648
raise KasaException("You need to await update() to access the data")
4749
return await f(*args, **kwargs)
4850

@@ -51,7 +53,9 @@ async def wrapped(*args: Any, **kwargs: Any) -> Any:
5153
@functools.wraps(f)
5254
def wrapped(*args: Any, **kwargs: Any) -> Any:
5355
self = args[0]
54-
if self._last_update is None and f.__name__ not in self._sys_info:
56+
if self._last_update is None and (
57+
self._sys_info is None or f.__name__ not in self._sys_info
58+
):
5559
raise KasaException("You need to await update() to access the data")
5660
return f(*args, **kwargs)
5761

kasa/smart/smartchilddevice.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -107,16 +107,26 @@ async def create(
107107
@property
108108
def device_type(self) -> DeviceType:
109109
"""Return child device type."""
110-
category = self.sys_info["category"]
111-
dev_type = self.CHILD_DEVICE_TYPE_MAP.get(category)
112-
if dev_type is None:
113-
_LOGGER.warning(
114-
"Unknown child device type %s for model %s, please open issue",
115-
category,
116-
self.model,
117-
)
118-
dev_type = DeviceType.Unknown
119-
return dev_type
110+
if self._device_type is not DeviceType.Unknown:
111+
return self._device_type
112+
113+
if self.sys_info and (category := self.sys_info.get("category")):
114+
dev_type = self.CHILD_DEVICE_TYPE_MAP.get(category)
115+
if dev_type is None:
116+
_LOGGER.warning(
117+
"Unknown child device type %s for model %s, please open issue",
118+
category,
119+
self.model,
120+
)
121+
self._device_type = DeviceType.Unknown
122+
else:
123+
self._device_type = dev_type
124+
125+
return self._device_type
120126

121127
def __repr__(self) -> str:
128+
if not self._parent:
129+
return f"<{self.device_type}(child) without parent>"
130+
if not self._parent._last_update:
131+
return f"<{self.device_type}(child) of {self._parent}>"
122132
return f"<{self.device_type} {self.alias} ({self.model}) of {self._parent}>"

kasa/smart/smartdevice.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,10 @@ def device_type(self) -> DeviceType:
757757

758758
# Fallback to device_type (from disco info)
759759
type_str = self._info.get("type", self._info.get("device_type"))
760+
761+
if not type_str: # no update or discovery info
762+
return self._device_type
763+
760764
self._device_type = self._get_device_type_from_components(
761765
list(self._components.keys()), type_str
762766
)

kasa/smartcamera/smartcamera.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ class SmartCamera(SmartDevice):
2525
@staticmethod
2626
def _get_device_type_from_sysinfo(sysinfo: dict[str, Any]) -> DeviceType:
2727
"""Find type to be displayed as a supported device category."""
28-
device_type = sysinfo["device_type"]
29-
if device_type.endswith("HUB"):
28+
if (
29+
sysinfo
30+
and (device_type := sysinfo.get("device_type"))
31+
and device_type.endswith("HUB")
32+
):
3033
return DeviceType.Hub
3134
return DeviceType.Camera
3235

tests/test_childdevice.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from kasa.device_type import DeviceType
1010
from kasa.protocols.smartprotocol import _ChildProtocolWrapper
1111
from kasa.smart.smartchilddevice import SmartChildDevice
12-
from kasa.smart.smartdevice import NON_HUB_PARENT_ONLY_MODULES
12+
from kasa.smart.smartdevice import NON_HUB_PARENT_ONLY_MODULES, SmartDevice
1313

1414
from .conftest import (
1515
parametrize,
@@ -139,3 +139,19 @@ async def test_child_time(dev: Device, freezer: FrozenDateTimeFactory):
139139
assert dev.parent is None
140140
for child in dev.children:
141141
assert child.time != fallback_time
142+
143+
144+
async def test_child_device_type_unknown(caplog):
145+
"""Test for device type when category is unknown."""
146+
147+
class DummyDevice(SmartChildDevice):
148+
def __init__(self):
149+
super().__init__(
150+
SmartDevice("127.0.0.1"),
151+
{"device_id": "1", "category": "foobar"},
152+
{"device", 1},
153+
)
154+
155+
assert DummyDevice().device_type is DeviceType.Unknown
156+
msg = "Unknown child device type foobar for model None, please open issue"
157+
assert msg in caplog.text

tests/test_device.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,23 @@
1414

1515
import kasa
1616
from kasa import Credentials, Device, DeviceConfig, DeviceType, KasaException, Module
17-
from kasa.iot import IotDevice
17+
from kasa.iot import (
18+
IotBulb,
19+
IotDevice,
20+
IotDimmer,
21+
IotLightStrip,
22+
IotPlug,
23+
IotStrip,
24+
IotWallSwitch,
25+
)
1826
from kasa.iot.iottimezone import (
1927
TIMEZONE_INDEX,
2028
get_timezone,
2129
get_timezone_index,
2230
)
2331
from kasa.iot.modules import IotLightPreset
2432
from kasa.smart import SmartChildDevice, SmartDevice
33+
from kasa.smartcamera import SmartCamera
2534

2635

2736
def _get_subclasses(of_class):
@@ -80,6 +89,41 @@ async def test_device_class_ctors(device_class_name_obj):
8089
assert dev.credentials == credentials
8190

8291

92+
@device_classes
93+
async def test_device_class_repr(device_class_name_obj):
94+
"""Test device repr when update() not called and no discovery info."""
95+
host = "127.0.0.2"
96+
port = 1234
97+
credentials = Credentials("foo", "bar")
98+
config = DeviceConfig(host, port_override=port, credentials=credentials)
99+
klass = device_class_name_obj[1]
100+
if issubclass(klass, SmartChildDevice):
101+
parent = SmartDevice(host, config=config)
102+
dev = klass(
103+
parent, {"dummy": "info", "device_id": "dummy"}, {"dummy": "components"}
104+
)
105+
else:
106+
dev = klass(host, config=config)
107+
108+
CLASS_TO_DEFAULT_TYPE = {
109+
IotDevice: DeviceType.Unknown,
110+
IotBulb: DeviceType.Bulb,
111+
IotPlug: DeviceType.Plug,
112+
IotDimmer: DeviceType.Dimmer,
113+
IotStrip: DeviceType.Strip,
114+
IotWallSwitch: DeviceType.WallSwitch,
115+
IotLightStrip: DeviceType.LightStrip,
116+
SmartChildDevice: DeviceType.Unknown,
117+
SmartDevice: DeviceType.Unknown,
118+
SmartCamera: DeviceType.Camera,
119+
}
120+
type_ = CLASS_TO_DEFAULT_TYPE[klass]
121+
child_repr = "<DeviceType.Unknown(child) of <DeviceType.Unknown at 127.0.0.2 - update() needed>>"
122+
not_child_repr = f"<{type_} at 127.0.0.2 - update() needed>"
123+
expected_repr = child_repr if klass is SmartChildDevice else not_child_repr
124+
assert repr(dev) == expected_repr
125+
126+
83127
async def test_create_device_with_timeout():
84128
"""Make sure timeout is passed to the protocol."""
85129
host = "127.0.0.1"

0 commit comments

Comments
 (0)