Fix repr for device created with no sysinfo or discovery info" (#1266)

Co-authored-by: Teemu R. <tpr@iki.fi>
This commit is contained in:
Steven B. 2024-11-18 13:14:39 +00:00 committed by GitHub
parent fd5258c28b
commit 9d46996e9b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 101 additions and 18 deletions

View File

@ -210,12 +210,12 @@ class Device(ABC):
self.protocol: BaseProtocol = protocol or IotProtocol( self.protocol: BaseProtocol = protocol or IotProtocol(
transport=XorTransport(config=config or DeviceConfig(host=host)), transport=XorTransport(config=config or DeviceConfig(host=host)),
) )
_LOGGER.debug("Initializing %s of type %s", self.host, type(self)) self._last_update: Any = None
_LOGGER.debug("Initializing %s of type %s", host, type(self))
self._device_type = DeviceType.Unknown self._device_type = DeviceType.Unknown
# TODO: typing Any is just as using Optional[Dict] would require separate # TODO: typing Any is just as using Optional[Dict] would require separate
# checks in accessors. the @updated_required decorator does not ensure # checks in accessors. the @updated_required decorator does not ensure
# mypy that these are not accessed incorrectly. # mypy that these are not accessed incorrectly.
self._last_update: Any = None
self._discovery_info: dict[str, Any] | None = None self._discovery_info: dict[str, Any] | None = None
self._features: dict[str, Feature] = {} self._features: dict[str, Feature] = {}
@ -492,6 +492,8 @@ class Device(ABC):
def __repr__(self) -> str: def __repr__(self) -> str:
update_needed = " - update() needed" if not self._last_update else "" update_needed = " - update() needed" if not self._last_update else ""
if not self._last_update and not self._discovery_info:
return f"<{self.device_type} at {self.host}{update_needed}>"
return ( return (
f"<{self.device_type} at {self.host} -" f"<{self.device_type} at {self.host} -"
f" {self.alias} ({self.model}){update_needed}>" f" {self.alias} ({self.model}){update_needed}>"

View File

@ -42,7 +42,9 @@ def requires_update(f: Callable) -> Any:
@functools.wraps(f) @functools.wraps(f)
async def wrapped(*args: Any, **kwargs: Any) -> Any: async def wrapped(*args: Any, **kwargs: Any) -> Any:
self = args[0] self = args[0]
if self._last_update is None and f.__name__ not in self._sys_info: if self._last_update is None 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") raise KasaException("You need to await update() to access the data")
return await f(*args, **kwargs) return await f(*args, **kwargs)
@ -51,7 +53,9 @@ def requires_update(f: Callable) -> Any:
@functools.wraps(f) @functools.wraps(f)
def wrapped(*args: Any, **kwargs: Any) -> Any: def wrapped(*args: Any, **kwargs: Any) -> Any:
self = args[0] self = args[0]
if self._last_update is None and f.__name__ not in self._sys_info: if self._last_update is None 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") raise KasaException("You need to await update() to access the data")
return f(*args, **kwargs) return f(*args, **kwargs)

View File

@ -107,7 +107,10 @@ class SmartChildDevice(SmartDevice):
@property @property
def device_type(self) -> DeviceType: def device_type(self) -> DeviceType:
"""Return child device type.""" """Return child device type."""
category = self.sys_info["category"] if self._device_type is not DeviceType.Unknown:
return self._device_type
if self.sys_info and (category := self.sys_info.get("category")):
dev_type = self.CHILD_DEVICE_TYPE_MAP.get(category) dev_type = self.CHILD_DEVICE_TYPE_MAP.get(category)
if dev_type is None: if dev_type is None:
_LOGGER.warning( _LOGGER.warning(
@ -115,8 +118,15 @@ class SmartChildDevice(SmartDevice):
category, category,
self.model, self.model,
) )
dev_type = DeviceType.Unknown self._device_type = DeviceType.Unknown
return dev_type else:
self._device_type = dev_type
return self._device_type
def __repr__(self) -> str: def __repr__(self) -> str:
if not self._parent:
return f"<{self.device_type}(child) without parent>"
if not self._parent._last_update:
return f"<{self.device_type}(child) of {self._parent}>"
return f"<{self.device_type} {self.alias} ({self.model}) of {self._parent}>" return f"<{self.device_type} {self.alias} ({self.model}) of {self._parent}>"

View File

@ -757,6 +757,10 @@ class SmartDevice(Device):
# Fallback to device_type (from disco info) # Fallback to device_type (from disco info)
type_str = self._info.get("type", self._info.get("device_type")) type_str = self._info.get("type", self._info.get("device_type"))
if not type_str: # no update or discovery info
return self._device_type
self._device_type = self._get_device_type_from_components( self._device_type = self._get_device_type_from_components(
list(self._components.keys()), type_str list(self._components.keys()), type_str
) )

View File

@ -25,8 +25,11 @@ class SmartCamera(SmartDevice):
@staticmethod @staticmethod
def _get_device_type_from_sysinfo(sysinfo: dict[str, Any]) -> DeviceType: def _get_device_type_from_sysinfo(sysinfo: dict[str, Any]) -> DeviceType:
"""Find type to be displayed as a supported device category.""" """Find type to be displayed as a supported device category."""
device_type = sysinfo["device_type"] if (
if device_type.endswith("HUB"): sysinfo
and (device_type := sysinfo.get("device_type"))
and device_type.endswith("HUB")
):
return DeviceType.Hub return DeviceType.Hub
return DeviceType.Camera return DeviceType.Camera

View File

@ -9,7 +9,7 @@ from kasa import Device
from kasa.device_type import DeviceType from kasa.device_type import DeviceType
from kasa.protocols.smartprotocol import _ChildProtocolWrapper from kasa.protocols.smartprotocol import _ChildProtocolWrapper
from kasa.smart.smartchilddevice import SmartChildDevice from kasa.smart.smartchilddevice import SmartChildDevice
from kasa.smart.smartdevice import NON_HUB_PARENT_ONLY_MODULES from kasa.smart.smartdevice import NON_HUB_PARENT_ONLY_MODULES, SmartDevice
from .conftest import ( from .conftest import (
parametrize, parametrize,
@ -139,3 +139,19 @@ async def test_child_time(dev: Device, freezer: FrozenDateTimeFactory):
assert dev.parent is None assert dev.parent is None
for child in dev.children: for child in dev.children:
assert child.time != fallback_time assert child.time != fallback_time
async def test_child_device_type_unknown(caplog):
"""Test for device type when category is unknown."""
class DummyDevice(SmartChildDevice):
def __init__(self):
super().__init__(
SmartDevice("127.0.0.1"),
{"device_id": "1", "category": "foobar"},
{"device", 1},
)
assert DummyDevice().device_type is DeviceType.Unknown
msg = "Unknown child device type foobar for model None, please open issue"
assert msg in caplog.text

View File

@ -14,7 +14,15 @@ import zoneinfo
import kasa import kasa
from kasa import Credentials, Device, DeviceConfig, DeviceType, KasaException, Module from kasa import Credentials, Device, DeviceConfig, DeviceType, KasaException, Module
from kasa.iot import IotDevice from kasa.iot import (
IotBulb,
IotDevice,
IotDimmer,
IotLightStrip,
IotPlug,
IotStrip,
IotWallSwitch,
)
from kasa.iot.iottimezone import ( from kasa.iot.iottimezone import (
TIMEZONE_INDEX, TIMEZONE_INDEX,
get_timezone, get_timezone,
@ -22,6 +30,7 @@ from kasa.iot.iottimezone import (
) )
from kasa.iot.modules import IotLightPreset from kasa.iot.modules import IotLightPreset
from kasa.smart import SmartChildDevice, SmartDevice from kasa.smart import SmartChildDevice, SmartDevice
from kasa.smartcamera import SmartCamera
def _get_subclasses(of_class): def _get_subclasses(of_class):
@ -80,6 +89,41 @@ async def test_device_class_ctors(device_class_name_obj):
assert dev.credentials == credentials assert dev.credentials == credentials
@device_classes
async def test_device_class_repr(device_class_name_obj):
"""Test device repr when update() not called and no discovery info."""
host = "127.0.0.2"
port = 1234
credentials = Credentials("foo", "bar")
config = DeviceConfig(host, port_override=port, credentials=credentials)
klass = device_class_name_obj[1]
if issubclass(klass, SmartChildDevice):
parent = SmartDevice(host, config=config)
dev = klass(
parent, {"dummy": "info", "device_id": "dummy"}, {"dummy": "components"}
)
else:
dev = klass(host, config=config)
CLASS_TO_DEFAULT_TYPE = {
IotDevice: DeviceType.Unknown,
IotBulb: DeviceType.Bulb,
IotPlug: DeviceType.Plug,
IotDimmer: DeviceType.Dimmer,
IotStrip: DeviceType.Strip,
IotWallSwitch: DeviceType.WallSwitch,
IotLightStrip: DeviceType.LightStrip,
SmartChildDevice: DeviceType.Unknown,
SmartDevice: DeviceType.Unknown,
SmartCamera: DeviceType.Camera,
}
type_ = CLASS_TO_DEFAULT_TYPE[klass]
child_repr = "<DeviceType.Unknown(child) of <DeviceType.Unknown at 127.0.0.2 - update() needed>>"
not_child_repr = f"<{type_} at 127.0.0.2 - update() needed>"
expected_repr = child_repr if klass is SmartChildDevice else not_child_repr
assert repr(dev) == expected_repr
async def test_create_device_with_timeout(): async def test_create_device_with_timeout():
"""Make sure timeout is passed to the protocol.""" """Make sure timeout is passed to the protocol."""
host = "127.0.0.1" host = "127.0.0.1"