Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions kasa/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,12 @@ def __init__(
self.protocol: BaseProtocol = protocol or IotProtocol(
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
# TODO: typing Any is just as using Optional[Dict] would require separate
# checks in accessors. the @updated_required decorator does not ensure
# mypy that these are not accessed incorrectly.
self._last_update: Any = None
self._discovery_info: dict[str, Any] | None = None

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

def __repr__(self) -> str:
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 (
f"<{self.device_type} at {self.host} -"
f" {self.alias} ({self.model}){update_needed}>"
Expand Down
8 changes: 6 additions & 2 deletions kasa/iot/iotdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ 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 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")
return await f(*args, **kwargs)

Expand All @@ -51,7 +53,9 @@ 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 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")
return f(*args, **kwargs)

Expand Down
30 changes: 20 additions & 10 deletions kasa/smart/smartchilddevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,26 @@ async def create(
@property
def device_type(self) -> DeviceType:
"""Return child device type."""
category = self.sys_info["category"]
dev_type = self.CHILD_DEVICE_TYPE_MAP.get(category)
if dev_type is None:
_LOGGER.warning(
"Unknown child device type %s for model %s, please open issue",
category,
self.model,
)
dev_type = DeviceType.Unknown
return dev_type
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)
if dev_type is None:
_LOGGER.warning(
"Unknown child device type %s for model %s, please open issue",
category,
self.model,
)
self._device_type = DeviceType.Unknown
else:
self._device_type = dev_type

return self._device_type

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}>"
4 changes: 4 additions & 0 deletions kasa/smart/smartdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,10 @@ def device_type(self) -> DeviceType:

# Fallback to device_type (from disco info)
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(
list(self._components.keys()), type_str
)
Expand Down
7 changes: 5 additions & 2 deletions kasa/smartcamera/smartcamera.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ class SmartCamera(SmartDevice):
@staticmethod
def _get_device_type_from_sysinfo(sysinfo: dict[str, Any]) -> DeviceType:
"""Find type to be displayed as a supported device category."""
device_type = sysinfo["device_type"]
if device_type.endswith("HUB"):
if (
sysinfo
and (device_type := sysinfo.get("device_type"))
and device_type.endswith("HUB")
):
return DeviceType.Hub
return DeviceType.Camera

Expand Down
18 changes: 17 additions & 1 deletion tests/test_childdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from kasa.device_type import DeviceType
from kasa.protocols.smartprotocol import _ChildProtocolWrapper
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 (
parametrize,
Expand Down Expand Up @@ -139,3 +139,19 @@ async def test_child_time(dev: Device, freezer: FrozenDateTimeFactory):
assert dev.parent is None
for child in dev.children:
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
46 changes: 45 additions & 1 deletion tests/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,23 @@

import kasa
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 (
TIMEZONE_INDEX,
get_timezone,
get_timezone_index,
)
from kasa.iot.modules import IotLightPreset
from kasa.smart import SmartChildDevice, SmartDevice
from kasa.smartcamera import SmartCamera


def _get_subclasses(of_class):
Expand Down Expand Up @@ -80,6 +89,41 @@ async def test_device_class_ctors(device_class_name_obj):
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():
"""Make sure timeout is passed to the protocol."""
host = "127.0.0.1"
Expand Down
Loading