mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 19:23:34 +00:00
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:
parent
fd5258c28b
commit
9d46996e9b
@ -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}>"
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -107,16 +107,26 @@ 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:
|
||||||
dev_type = self.CHILD_DEVICE_TYPE_MAP.get(category)
|
return self._device_type
|
||||||
if dev_type is None:
|
|
||||||
_LOGGER.warning(
|
if self.sys_info and (category := self.sys_info.get("category")):
|
||||||
"Unknown child device type %s for model %s, please open issue",
|
dev_type = self.CHILD_DEVICE_TYPE_MAP.get(category)
|
||||||
category,
|
if dev_type is None:
|
||||||
self.model,
|
_LOGGER.warning(
|
||||||
)
|
"Unknown child device type %s for model %s, please open issue",
|
||||||
dev_type = DeviceType.Unknown
|
category,
|
||||||
return dev_type
|
self.model,
|
||||||
|
)
|
||||||
|
self._device_type = DeviceType.Unknown
|
||||||
|
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}>"
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user