镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

IOT devices connected over KLAP are always built as IotPlug, never IotStrip — HS300 loses all child sockets #1748

Description

@luxardolabs

Summary

An HS300 power strip reached over KLAP is instantiated as IotPlug instead of IotStrip, so it exposes no child sockets — no per-outlet switching and no per-outlet emeter. The same strip on XOR firmware works correctly.

This is separate from, and in addition to, the authentication issue in #1604 / PR #1731. With #1731 applied, the device authenticates but is still childless.

Environment

  • Device: HS300(US) hardware 2.0, firmware 1.1.2 Build 241220 Rel.171333
  • python-kasa 0.10.2
  • Home Assistant 2026.7.4 (also reproducible outside HA)
  • tcp/9999 is ConnectionRefused on this firmware; tcp/80 serves KLAP
  • Connection parameters recorded for the device: IOT.SMARTPLUGSWITCH / KLAP / login_version: 2

Observed

strip transport entities outlet switches
HS300 on old firmware XOR 18 6
HS300 on new firmware KLAP 11 0

On the KLAP strips the parent switch stays unavailable while strip-level sensors report real values (voltage, consumption), so communication is fine — only the child devices are missing.

Cause

kasa/device_factory.py::_connect() gates sysinfo-based class detection on XorTransport:

if isinstance(protocol, IotProtocol) and isinstance(
    protocol._transport, XorTransport
):
    info = await protocol.query(GET_SYSINFO_QUERY)
    device_class = get_device_class_from_sys_info(info)   # only path that can return IotStrip
    ...
elif device_class := get_device_class_from_family(
    config.connection_type.device_family.value, https=config.connection_type.https
):

get_device_class_from_sys_info() is the only path that inspects the device's own sysinfo and can therefore return DeviceType.Strip -> IotStrip. An IotProtocol running over KlapTransportV2 skips that branch and falls through to get_device_class_from_family(), where:

"IOT.SMARTPLUGSWITCH": IotPlug,

is unconditional and has no concept of children. So any IoT device that reaches over KLAP loses its children, regardless of what it actually is.

Suggested fix

Drop the transport check — GET_SYSINFO_QUERY works over KLAP:

if isinstance(protocol, IotProtocol):
    info = await protocol.query(GET_SYSINFO_QUERY)
    device_class = get_device_class_from_sys_info(info)
    ...

Verified

Applied to three HS300s on KLAP firmware. All three went from 11 entities / 0 outlets to 18 entities with 6 switchable outlets and per-outlet emeter. No regression on XOR devices, which already take that branch.

Happy to open a PR if the approach looks right — there may be a preferred way to handle non-IoT IotProtocol transports (e.g. LinkieTransportV2 for IOT cameras) that I haven't considered.

Activity

  1. luxardolabs commented on Aug 30, 2026

    @luxardolabs
    Author

    Credit where due — this was independently identified by @iamteedoh in #1604 three weeks ago: #1604 (comment)

    I hadn't seen it when filing. I've opened this as its own issue because that analysis is a comment inside a 48-comment thread about the authentication bug, and this is a separate defect that survives #1731 — but I'm happy for this to be closed in favour of that comment if you'd rather keep it in one place.

    Two things to add.

    1. Second independent confirmation, on a different firmware build. @iamteedoh reported HS300 hw2.0; mine are HS300(US) hw2.0 on 1.1.2 Build 241220 Rel.171333. Three units here, all reproduced, all fixed by the same change — so at least it isn't unique to a single build.

    2. Discover._get_device_instance() needs the same treatment, which my original post missed. Quoting @iamteedoh:

    Keying on the discovery model alone isn't enough. HA's coordinator reaches the device through a direct-connect path that carries no discovery info, so the model string is empty there. sys_info["children"] is the reliable signal, and both device_factory._connect() and Discover._get_device_instance() need covering.

    I only patched _connect(). That is sufficient for Home Assistant's setup path — all three strips came back with 18 entities, working per-outlet switching and per-outlet emeter — but a fix covering only _connect() would leave the discovery path still mis-typing strips as IotPlug.

    One more data point, in case it saves someone time: the Third-Party Integration workaround (re-opening port 9999 from the Kasa app) didn't work for me. The setting was already enabled on my account, and my three strips on 1.1.2 Build 241220 still return ConnectionRefused on tcp/9999, while another strip on the older 1.0.21 Build 210524 serves it normally. @beckerben's units were on 1.1.6 Build 240130 and it worked there, so I'd guess it varies by build — but that's only my two builds against theirs, so treat it as one report rather than a general conclusion.

  2. luxardolabs commented on Oct 10, 2026

    @luxardolabs
    Author

    Update after 0.11.0.1: #1769 fixed the connect path, but discovery still builds these strips as IotPlug.

    Tested on 0.11.0.1 with no local patches, against three HS300(US) hw 2.0 on fw 1.1.2 Build 241220 (IOT.SMARTPLUGSWITCH / KLAP / login_version: 2):

    Path Class Transport Children
    Device.connect(config=...) IotStrip KlapTransportV2 6 (per-outlet emeter works)
    Discover.discover() + update() IotPlug KlapTransportV2 0

    So #1731 fixed authentication, and #1769 fixed device_factory._connect. Discover._get_device_instance() still picks the class with get_device_class_from_family(type_, ...), where IOT.SMARTPLUGSWITCH maps to IotPlug. That path never sees sysinfo, so a strip found by discovery loses all its child sockets. update() now succeeds on it (on 0.10.2 it raised), so nothing signals that the class is wrong.

    Strips on fw 1.0.21 answer over XOR and come out of both paths as IotStrip, as expected.

    The workaround we run is to rebuild the device after update() when sys_info["children"] is non-empty, on the same protocol object:

    if isinstance(device, IotDevice) and not isinstance(device, IotStrip) and device.sys_info.get("children"):
        strip = IotStrip(device.host, protocol=device.protocol)
        await strip.update()

    A fix upstream could do the same in the discovery flow, or have _get_device_instance defer the IOT class choice until sysinfo is available, as _connect now does.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions