mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-08-09 20:24:02 +00:00
Add WallSwitch device type and autogenerate supported devices docs (#758)
This commit is contained in:
12
kasa/cli.py
12
kasa/cli.py
@@ -26,7 +26,15 @@ from kasa import (
|
||||
UnsupportedDeviceError,
|
||||
)
|
||||
from kasa.discover import DiscoveryResult
|
||||
from kasa.iot import IotBulb, IotDevice, IotDimmer, IotLightStrip, IotPlug, IotStrip
|
||||
from kasa.iot import (
|
||||
IotBulb,
|
||||
IotDevice,
|
||||
IotDimmer,
|
||||
IotLightStrip,
|
||||
IotPlug,
|
||||
IotStrip,
|
||||
IotWallSwitch,
|
||||
)
|
||||
from kasa.smart import SmartBulb, SmartDevice
|
||||
|
||||
try:
|
||||
@@ -63,11 +71,13 @@ echo = _do_echo
|
||||
|
||||
TYPE_TO_CLASS = {
|
||||
"plug": IotPlug,
|
||||
"switch": IotWallSwitch,
|
||||
"bulb": IotBulb,
|
||||
"dimmer": IotDimmer,
|
||||
"strip": IotStrip,
|
||||
"lightstrip": IotLightStrip,
|
||||
"iot.plug": IotPlug,
|
||||
"iot.switch": IotWallSwitch,
|
||||
"iot.bulb": IotBulb,
|
||||
"iot.dimmer": IotDimmer,
|
||||
"iot.strip": IotStrip,
|
||||
|
@@ -212,6 +212,11 @@ class Device(ABC):
|
||||
"""Return True if the device is a plug."""
|
||||
return self.device_type == DeviceType.Plug
|
||||
|
||||
@property
|
||||
def is_wallswitch(self) -> bool:
|
||||
"""Return True if the device is a switch."""
|
||||
return self.device_type == DeviceType.WallSwitch
|
||||
|
||||
@property
|
||||
def is_strip(self) -> bool:
|
||||
"""Return True if the device is a strip."""
|
||||
|
@@ -5,9 +5,18 @@ from typing import Any, Dict, Optional, Tuple, Type
|
||||
|
||||
from .aestransport import AesTransport
|
||||
from .device import Device
|
||||
from .device_type import DeviceType
|
||||
from .deviceconfig import DeviceConfig
|
||||
from .exceptions import KasaException, UnsupportedDeviceError
|
||||
from .iot import IotBulb, IotDevice, IotDimmer, IotLightStrip, IotPlug, IotStrip
|
||||
from .iot import (
|
||||
IotBulb,
|
||||
IotDevice,
|
||||
IotDimmer,
|
||||
IotLightStrip,
|
||||
IotPlug,
|
||||
IotStrip,
|
||||
IotWallSwitch,
|
||||
)
|
||||
from .iotprotocol import IotProtocol
|
||||
from .klaptransport import KlapTransport, KlapTransportV2
|
||||
from .protocol import (
|
||||
@@ -105,7 +114,7 @@ async def _connect(config: DeviceConfig, protocol: BaseProtocol) -> "Device":
|
||||
)
|
||||
|
||||
|
||||
def get_device_class_from_sys_info(info: Dict[str, Any]) -> Type[IotDevice]:
|
||||
def _get_device_type_from_sys_info(info: Dict[str, Any]) -> DeviceType:
|
||||
"""Find SmartDevice subclass for device described by passed data."""
|
||||
if "system" not in info or "get_sysinfo" not in info["system"]:
|
||||
raise KasaException("No 'system' or 'get_sysinfo' in response")
|
||||
@@ -116,22 +125,36 @@ def get_device_class_from_sys_info(info: Dict[str, Any]) -> Type[IotDevice]:
|
||||
raise KasaException("Unable to find the device type field!")
|
||||
|
||||
if "dev_name" in sysinfo and "Dimmer" in sysinfo["dev_name"]:
|
||||
return IotDimmer
|
||||
return DeviceType.Dimmer
|
||||
|
||||
if "smartplug" in type_.lower():
|
||||
if "children" in sysinfo:
|
||||
return IotStrip
|
||||
|
||||
return IotPlug
|
||||
return DeviceType.Strip
|
||||
if (dev_name := sysinfo.get("dev_name")) and "light" in dev_name.lower():
|
||||
return DeviceType.WallSwitch
|
||||
return DeviceType.Plug
|
||||
|
||||
if "smartbulb" in type_.lower():
|
||||
if "length" in sysinfo: # strips have length
|
||||
return IotLightStrip
|
||||
return DeviceType.LightStrip
|
||||
|
||||
return IotBulb
|
||||
return DeviceType.Bulb
|
||||
raise UnsupportedDeviceError("Unknown device type: %s" % type_)
|
||||
|
||||
|
||||
def get_device_class_from_sys_info(sysinfo: Dict[str, Any]) -> Type[IotDevice]:
|
||||
"""Find SmartDevice subclass for device described by passed data."""
|
||||
TYPE_TO_CLASS = {
|
||||
DeviceType.Bulb: IotBulb,
|
||||
DeviceType.Plug: IotPlug,
|
||||
DeviceType.Dimmer: IotDimmer,
|
||||
DeviceType.Strip: IotStrip,
|
||||
DeviceType.WallSwitch: IotWallSwitch,
|
||||
DeviceType.LightStrip: IotLightStrip,
|
||||
}
|
||||
return TYPE_TO_CLASS[_get_device_type_from_sys_info(sysinfo)]
|
||||
|
||||
|
||||
def get_device_class_from_family(device_type: str) -> Optional[Type[Device]]:
|
||||
"""Return the device class from the type name."""
|
||||
supported_device_types: Dict[str, Type[Device]] = {
|
||||
|
@@ -11,6 +11,7 @@ class DeviceType(Enum):
|
||||
Plug = "plug"
|
||||
Bulb = "bulb"
|
||||
Strip = "strip"
|
||||
WallSwitch = "wallswitch"
|
||||
StripSocket = "stripsocket"
|
||||
Dimmer = "dimmer"
|
||||
LightStrip = "lightstrip"
|
||||
|
@@ -3,7 +3,7 @@ from .iotbulb import IotBulb
|
||||
from .iotdevice import IotDevice
|
||||
from .iotdimmer import IotDimmer
|
||||
from .iotlightstrip import IotLightStrip
|
||||
from .iotplug import IotPlug
|
||||
from .iotplug import IotPlug, IotWallSwitch
|
||||
from .iotstrip import IotStrip
|
||||
|
||||
__all__ = [
|
||||
@@ -13,4 +13,5 @@ __all__ = [
|
||||
"IotStrip",
|
||||
"IotDimmer",
|
||||
"IotLightStrip",
|
||||
"IotWallSwitch",
|
||||
]
|
||||
|
@@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IotPlug(IotDevice):
|
||||
r"""Representation of a TP-Link Smart Switch.
|
||||
r"""Representation of a TP-Link Smart Plug.
|
||||
|
||||
To initialize, you have to await :func:`update()` at least once.
|
||||
This will allow accessing the properties using the exposed properties.
|
||||
@@ -101,3 +101,17 @@ class IotPlug(IotDevice):
|
||||
def state_information(self) -> Dict[str, Any]:
|
||||
"""Return switch-specific state information."""
|
||||
return {}
|
||||
|
||||
|
||||
class IotWallSwitch(IotPlug):
|
||||
"""Representation of a TP-Link Smart Wall Switch."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
*,
|
||||
config: Optional[DeviceConfig] = None,
|
||||
protocol: Optional[BaseProtocol] = None,
|
||||
) -> None:
|
||||
super().__init__(host=host, config=config, protocol=protocol)
|
||||
self._device_type = DeviceType.WallSwitch
|
||||
|
@@ -63,12 +63,6 @@ class SmartDevice(Device):
|
||||
)
|
||||
for child_info in children
|
||||
}
|
||||
# TODO: This may not be the best approach, but it allows distinguishing
|
||||
# between power strips and hubs for the time being.
|
||||
if all(child.is_plug for child in self._children.values()):
|
||||
self._device_type = DeviceType.Strip
|
||||
else:
|
||||
self._device_type = DeviceType.Hub
|
||||
|
||||
@property
|
||||
def children(self) -> Sequence["SmartDevice"]:
|
||||
@@ -519,21 +513,30 @@ class SmartDevice(Device):
|
||||
if self._device_type is not DeviceType.Unknown:
|
||||
return self._device_type
|
||||
|
||||
if self.children:
|
||||
if "SMART.TAPOHUB" in self.sys_info["type"]:
|
||||
self._device_type = DeviceType.Hub
|
||||
else:
|
||||
self._device_type = DeviceType.Strip
|
||||
elif "light_strip" in self._components:
|
||||
self._device_type = DeviceType.LightStrip
|
||||
elif "dimmer_calibration" in self._components:
|
||||
self._device_type = DeviceType.Dimmer
|
||||
elif "brightness" in self._components:
|
||||
self._device_type = DeviceType.Bulb
|
||||
elif "PLUG" in self.sys_info["type"]:
|
||||
self._device_type = DeviceType.Plug
|
||||
else:
|
||||
_LOGGER.warning("Unknown device type, falling back to plug")
|
||||
self._device_type = DeviceType.Plug
|
||||
self._device_type = self._get_device_type_from_components(
|
||||
list(self._components.keys()), self._info["type"]
|
||||
)
|
||||
|
||||
return self._device_type
|
||||
|
||||
@staticmethod
|
||||
def _get_device_type_from_components(
|
||||
components: List[str], device_type: str
|
||||
) -> DeviceType:
|
||||
"""Find type to be displayed as a supported device category."""
|
||||
if "HUB" in device_type:
|
||||
return DeviceType.Hub
|
||||
if "PLUG" in device_type:
|
||||
if "child_device" in components:
|
||||
return DeviceType.Strip
|
||||
return DeviceType.Plug
|
||||
if "light_strip" in components:
|
||||
return DeviceType.LightStrip
|
||||
if "dimmer_calibration" in components:
|
||||
return DeviceType.Dimmer
|
||||
if "brightness" in components:
|
||||
return DeviceType.Bulb
|
||||
if "SWITCH" in device_type:
|
||||
return DeviceType.WallSwitch
|
||||
_LOGGER.warning("Unknown device type, falling back to plug")
|
||||
return DeviceType.Plug
|
||||
|
@@ -7,7 +7,7 @@ from kasa import (
|
||||
Device,
|
||||
Discover,
|
||||
)
|
||||
from kasa.iot import IotBulb, IotDimmer, IotLightStrip, IotPlug, IotStrip
|
||||
from kasa.iot import IotBulb, IotDimmer, IotLightStrip, IotPlug, IotStrip, IotWallSwitch
|
||||
from kasa.smart import SmartBulb, SmartDevice
|
||||
|
||||
from .fakeprotocol_iot import FakeIotProtocol
|
||||
@@ -60,15 +60,12 @@ PLUGS_IOT = {
|
||||
"HS103",
|
||||
"HS105",
|
||||
"HS110",
|
||||
"HS200",
|
||||
"HS210",
|
||||
"EP10",
|
||||
"KP100",
|
||||
"KP105",
|
||||
"KP115",
|
||||
"KP125",
|
||||
"KP401",
|
||||
"KS200M",
|
||||
}
|
||||
# P135 supports dimming, but its not currently support
|
||||
# by the library
|
||||
@@ -77,15 +74,25 @@ PLUGS_SMART = {
|
||||
"P110",
|
||||
"KP125M",
|
||||
"EP25",
|
||||
"KS205",
|
||||
"P125M",
|
||||
"S505",
|
||||
"TP15",
|
||||
}
|
||||
PLUGS = {
|
||||
*PLUGS_IOT,
|
||||
*PLUGS_SMART,
|
||||
}
|
||||
SWITCHES_IOT = {
|
||||
"HS200",
|
||||
"HS210",
|
||||
"KS200M",
|
||||
}
|
||||
SWITCHES_SMART = {
|
||||
"KS205",
|
||||
"KS225",
|
||||
"S500D",
|
||||
"S505",
|
||||
}
|
||||
SWITCHES = {*SWITCHES_IOT, *SWITCHES_SMART}
|
||||
STRIPS_IOT = {"HS107", "HS300", "KP303", "KP200", "KP400", "EP40"}
|
||||
STRIPS_SMART = {"P300", "TP25"}
|
||||
STRIPS = {*STRIPS_IOT, *STRIPS_SMART}
|
||||
@@ -105,12 +112,15 @@ WITH_EMETER = {*WITH_EMETER_IOT, *WITH_EMETER_SMART}
|
||||
|
||||
DIMMABLE = {*BULBS, *DIMMERS}
|
||||
|
||||
ALL_DEVICES_IOT = BULBS_IOT.union(PLUGS_IOT).union(STRIPS_IOT).union(DIMMERS_IOT)
|
||||
ALL_DEVICES_IOT = (
|
||||
BULBS_IOT.union(PLUGS_IOT).union(STRIPS_IOT).union(DIMMERS_IOT).union(SWITCHES_IOT)
|
||||
)
|
||||
ALL_DEVICES_SMART = (
|
||||
BULBS_SMART.union(PLUGS_SMART)
|
||||
.union(STRIPS_SMART)
|
||||
.union(DIMMERS_SMART)
|
||||
.union(HUBS_SMART)
|
||||
.union(SWITCHES_SMART)
|
||||
)
|
||||
ALL_DEVICES = ALL_DEVICES_IOT.union(ALL_DEVICES_SMART)
|
||||
|
||||
@@ -160,7 +170,14 @@ no_emeter_iot = parametrize(
|
||||
)
|
||||
|
||||
bulb = parametrize("bulbs", model_filter=BULBS, protocol_filter={"SMART", "IOT"})
|
||||
plug = parametrize("plugs", model_filter=PLUGS, protocol_filter={"IOT"})
|
||||
plug = parametrize("plugs", model_filter=PLUGS, protocol_filter={"IOT", "SMART"})
|
||||
plug_iot = parametrize("plugs iot", model_filter=PLUGS, protocol_filter={"IOT"})
|
||||
wallswitch = parametrize(
|
||||
"wall switches", model_filter=SWITCHES, protocol_filter={"IOT", "SMART"}
|
||||
)
|
||||
wallswitch_iot = parametrize(
|
||||
"wall switches iot", model_filter=SWITCHES, protocol_filter={"IOT"}
|
||||
)
|
||||
strip = parametrize("strips", model_filter=STRIPS, protocol_filter={"SMART", "IOT"})
|
||||
dimmer = parametrize("dimmers", model_filter=DIMMERS, protocol_filter={"IOT"})
|
||||
lightstrip = parametrize(
|
||||
@@ -213,6 +230,9 @@ strip_smart = parametrize(
|
||||
plug_smart = parametrize(
|
||||
"plug devices smart", model_filter=PLUGS_SMART, protocol_filter={"SMART"}
|
||||
)
|
||||
switch_smart = parametrize(
|
||||
"switch devices smart", model_filter=SWITCHES_SMART, protocol_filter={"SMART"}
|
||||
)
|
||||
bulb_smart = parametrize(
|
||||
"bulb devices smart", model_filter=BULBS_SMART, protocol_filter={"SMART"}
|
||||
)
|
||||
@@ -239,8 +259,8 @@ def check_categories():
|
||||
+ strip.args[1]
|
||||
+ plug.args[1]
|
||||
+ bulb.args[1]
|
||||
+ wallswitch.args[1]
|
||||
+ lightstrip.args[1]
|
||||
+ plug_smart.args[1]
|
||||
+ bulb_smart.args[1]
|
||||
+ dimmers_smart.args[1]
|
||||
+ hubs_smart.args[1]
|
||||
@@ -263,6 +283,9 @@ def device_for_fixture_name(model, protocol):
|
||||
for d in PLUGS_SMART:
|
||||
if d in model:
|
||||
return SmartDevice
|
||||
for d in SWITCHES_SMART:
|
||||
if d in model:
|
||||
return SmartDevice
|
||||
for d in BULBS_SMART:
|
||||
if d in model:
|
||||
return SmartBulb
|
||||
@@ -283,6 +306,9 @@ def device_for_fixture_name(model, protocol):
|
||||
for d in PLUGS_IOT:
|
||||
if d in model:
|
||||
return IotPlug
|
||||
for d in SWITCHES_IOT:
|
||||
if d in model:
|
||||
return IotWallSwitch
|
||||
|
||||
# Light strips are recognized also as bulbs, so this has to go first
|
||||
for d in BULBS_IOT_LIGHT_STRIP:
|
||||
@@ -325,6 +351,13 @@ async def get_device_for_fixture(fixture_data: FixtureInfo):
|
||||
d.protocol = FakeSmartProtocol(fixture_data.data, fixture_data.name)
|
||||
else:
|
||||
d.protocol = FakeIotProtocol(fixture_data.data)
|
||||
if "discovery_result" in fixture_data.data:
|
||||
discovery_data = {"result": fixture_data.data["discovery_result"]}
|
||||
else:
|
||||
discovery_data = {
|
||||
"system": {"get_sysinfo": fixture_data.data["system"]["get_sysinfo"]}
|
||||
}
|
||||
d.update_from_discover_info(discovery_data)
|
||||
await _update_and_close(d)
|
||||
return d
|
||||
|
||||
|
@@ -10,7 +10,11 @@ from kasa import (
|
||||
Discover,
|
||||
KasaException,
|
||||
)
|
||||
from kasa.device_factory import connect, get_protocol
|
||||
from kasa.device_factory import (
|
||||
_get_device_type_from_sys_info,
|
||||
connect,
|
||||
get_protocol,
|
||||
)
|
||||
from kasa.deviceconfig import (
|
||||
ConnectionType,
|
||||
DeviceConfig,
|
||||
@@ -18,6 +22,7 @@ from kasa.deviceconfig import (
|
||||
EncryptType,
|
||||
)
|
||||
from kasa.discover import DiscoveryResult
|
||||
from kasa.smart.smartdevice import SmartDevice
|
||||
|
||||
|
||||
def _get_connection_type_device_class(discovery_info):
|
||||
@@ -146,3 +151,16 @@ async def test_connect_http_client(discovery_data, mocker):
|
||||
assert dev.protocol._transport._http_client.client == http_client
|
||||
await dev.disconnect()
|
||||
await http_client.close()
|
||||
|
||||
|
||||
async def test_device_types(dev: Device):
|
||||
await dev.update()
|
||||
if isinstance(dev, SmartDevice):
|
||||
device_type = dev._discovery_info["result"]["device_type"]
|
||||
res = SmartDevice._get_device_type_from_components(
|
||||
dev._components.keys(), device_type
|
||||
)
|
||||
else:
|
||||
res = _get_device_type_from_sys_info(dev._last_update)
|
||||
|
||||
assert dev.device_type == res
|
||||
|
@@ -29,8 +29,9 @@ from .conftest import (
|
||||
dimmer,
|
||||
lightstrip,
|
||||
new_discovery,
|
||||
plug,
|
||||
plug_iot,
|
||||
strip_iot,
|
||||
wallswitch_iot,
|
||||
)
|
||||
|
||||
UNSUPPORTED = {
|
||||
@@ -55,7 +56,14 @@ UNSUPPORTED = {
|
||||
}
|
||||
|
||||
|
||||
@plug
|
||||
@wallswitch_iot
|
||||
async def test_type_detection_switch(dev: Device):
|
||||
d = Discover._get_device_class(dev._last_update)("localhost")
|
||||
assert d.is_wallswitch
|
||||
assert d.device_type == DeviceType.WallSwitch
|
||||
|
||||
|
||||
@plug_iot
|
||||
async def test_type_detection_plug(dev: Device):
|
||||
d = Discover._get_device_class(dev._last_update)("localhost")
|
||||
assert d.is_plug
|
||||
|
@@ -1,6 +1,6 @@
|
||||
from kasa import DeviceType
|
||||
|
||||
from .conftest import plug, plug_smart
|
||||
from .conftest import plug_iot, plug_smart, switch_smart, wallswitch_iot
|
||||
from .test_smartdevice import SYSINFO_SCHEMA
|
||||
|
||||
# these schemas should go to the mainlib as
|
||||
@@ -8,7 +8,7 @@ from .test_smartdevice import SYSINFO_SCHEMA
|
||||
# as well as to check that faked devices are operating properly.
|
||||
|
||||
|
||||
@plug
|
||||
@plug_iot
|
||||
async def test_plug_sysinfo(dev):
|
||||
assert dev.sys_info is not None
|
||||
SYSINFO_SCHEMA(dev.sys_info)
|
||||
@@ -19,8 +19,34 @@ async def test_plug_sysinfo(dev):
|
||||
assert dev.is_plug or dev.is_strip
|
||||
|
||||
|
||||
@plug
|
||||
async def test_led(dev):
|
||||
@wallswitch_iot
|
||||
async def test_switch_sysinfo(dev):
|
||||
assert dev.sys_info is not None
|
||||
SYSINFO_SCHEMA(dev.sys_info)
|
||||
|
||||
assert dev.model is not None
|
||||
|
||||
assert dev.device_type == DeviceType.WallSwitch
|
||||
assert dev.is_wallswitch
|
||||
|
||||
|
||||
@plug_iot
|
||||
async def test_plug_led(dev):
|
||||
original = dev.led
|
||||
|
||||
await dev.set_led(False)
|
||||
await dev.update()
|
||||
assert not dev.led
|
||||
|
||||
await dev.set_led(True)
|
||||
await dev.update()
|
||||
assert dev.led
|
||||
|
||||
await dev.set_led(original)
|
||||
|
||||
|
||||
@wallswitch_iot
|
||||
async def test_switch_led(dev):
|
||||
original = dev.led
|
||||
|
||||
await dev.set_led(False)
|
||||
@@ -40,3 +66,13 @@ async def test_plug_device_info(dev):
|
||||
assert dev.model is not None
|
||||
|
||||
assert dev.device_type == DeviceType.Plug or dev.device_type == DeviceType.Strip
|
||||
|
||||
|
||||
@switch_smart
|
||||
async def test_switch_device_info(dev):
|
||||
assert dev._info is not None
|
||||
assert dev.model is not None
|
||||
|
||||
assert (
|
||||
dev.device_type == DeviceType.WallSwitch or dev.device_type == DeviceType.Dimmer
|
||||
)
|
||||
|
Reference in New Issue
Block a user