Classify IOT devices via sysinfo regardless of transport (fixes #1740) (#1769)

Drop the `XorTransport` guard and always query sysinfo for `IotProtocol`
connections, so `get_device_class_from_sys_info` sees `dev_name`
(\"Wi-Fi Smart Dimmer\", etc.) and picks the right class to initialize.

This is necessary, as some IOT-speaking devices have moved to use KLAP 
instead of XOR as their transport, which in turn caused devices like dimmers
to be detected incorrectly as plugs.

---------

Co-authored-by: bluntman001 <noreply@github.com>
Co-authored-by: Teemu Rytilahti <tpr@iki.fi>
This commit is contained in:
bluntman001
2026-10-04 07:48:22 -06:00
committed by GitHub
parent 53823ea9a5
commit b2a15afa7e
2 changed files with 65 additions and 3 deletions

View File

@@ -104,9 +104,9 @@ async def _connect(config: DeviceConfig, protocol: BaseProtocol) -> Device:
device_class: type[Device] | None
device: Device | None = None
if isinstance(protocol, IotProtocol) and isinstance(
protocol._transport, XorTransport
):
# For IOT devices, we need to obtain the initial sysinfo in order decide
# on the device class to initialize.
if isinstance(protocol, IotProtocol):
info = await protocol.query(GET_SYSINFO_QUERY)
_perf_log(True, "get_sysinfo")
device_class = get_device_class_from_sys_info(info)

View File

@@ -24,6 +24,8 @@ from kasa import (
from kasa.device_factory import (
Device,
IotDevice,
IotDimmer,
IotPlug,
SmartCamDevice,
SmartDevice,
connect,
@@ -307,3 +309,63 @@ async def test_get_protocol(
protocol = get_protocol(config)
assert isinstance(protocol, expected_protocol)
assert isinstance(protocol._transport, expected_transport)
async def test_connect_iot_klap_dimmer_classification(mocker):
"""Test that IOT dimmers reached via KLAP initialize IotDimmer.
Regression test for the bug where `_connect` only queried sysinfo for
IOT devices reached via XorTransport, so KLAP-authenticated
IOT.SMARTPLUGSWITCH devices were always classified incorrectly as IotPlugs.
"""
dimmer_sysinfo = {
"system": {
"get_sysinfo": {
"sw_ver": "1.1.4 Build 241220 Rel.171050",
"hw_ver": "3.0",
"model": "HS220(US)",
"deviceId": "0" * 40,
"hwId": "0" * 32,
"fwId": "0" * 32,
"oemId": "0" * 32,
"alias": "Dimmer under test",
"dev_name": "Wi-Fi Smart Dimmer",
"mic_type": "IOT.SMARTPLUGSWITCH",
"relay_state": 0,
"brightness": 25,
"on_time": 0,
"active_mode": "none",
"feature": "TIM",
"updating": 0,
"rssi": -60,
"led_off": 0,
"latitude_i": 0,
"longitude_i": 0,
"err_code": 0,
}
}
}
mocker.patch("kasa.IotProtocol.query", return_value=dimmer_sysinfo)
# Avoid touching the wire on the update() that runs after class selection.
mocker.patch.object(IotDevice, "update", return_value=None)
ctype = DeviceConnectionParameters(
device_family=DeviceFamily.IotSmartPlugSwitch,
encryption_type=DeviceEncryptionType.Klap,
login_version=2,
https=False,
)
config = DeviceConfig(
host=DISCOVERY_MOCK_IP,
credentials=Credentials("user", "pass"),
connection_type=ctype,
)
dev = await connect(config=config)
try:
assert isinstance(dev, IotDimmer), (
f"HS220 via KLAP must be IotDimmer, got {type(dev).__name__}"
)
assert not isinstance(dev, IotPlug) or isinstance(dev, IotDimmer)
finally:
await dev.disconnect()