Files
python-kasa/tests/cli/test_hub.py
Teemu R. a68f2b3304
Some checks failed
CI / Perform Lint Checks (3.15) (push) Has been cancelled
CodeQL Checks / Analyze (python) (push) Has been cancelled
CI / Python 3.11 on macos-latest (push) Has been cancelled
CI / Python 3.12 on macos-latest (push) Has been cancelled
CI / Python 3.13 on macos-latest (push) Has been cancelled
CI / Python 3.14 on macos-latest (push) Has been cancelled
CI / Python 3.15 on macos-latest (push) Has been cancelled
CI / Python 3.11 on ubuntu-latest (push) Has been cancelled
CI / Python 3.12 on ubuntu-latest (push) Has been cancelled
CI / Python 3.13 on ubuntu-latest (push) Has been cancelled
CI / Python 3.14 on ubuntu-latest (push) Has been cancelled
CI / Python 3.15 on ubuntu-latest (push) Has been cancelled
CI / Python 3.11 on windows-latest (push) Has been cancelled
CI / Python 3.12 on windows-latest (push) Has been cancelled
CI / Python 3.13 on windows-latest (push) Has been cancelled
CI / Python 3.14 on windows-latest (push) Has been cancelled
CI / Python 3.15 on windows-latest (push) Has been cancelled
Stale / stale (push) Has been cancelled
Detect offline hub children and fail fast on writes to them (#1830)
Hubs keep reporting for children they have lost contact with, so callers
see stale values as if they were live. Trying to write to such devices
fail after timeouts with a generic error.
This PR exposes the offline/online information as reported by the hub,
and refuses writes to unreachable child devices without waiting for
device errors.

In details:
- Add new API: `Device.is_available`; hub children report unavailable
when the hub marks them offline or no longer lists them.
- New feature `available` for child devices to make it visible to the
CLI.
- Refuse writes to unavailable children with the new
`DeviceUnavailableError` instead of waiting for the hub to time out.
2026-11-01 18:18:03 +01:00

85 lines
2.6 KiB
Python

import pytest
from asyncclick.testing import CliRunner
from pytest_mock import MockerFixture
from kasa import Device, DeviceType, Module
from kasa.cli.hub import hub
from ..device_fixtures import hubs, hubs_smart, plug_iot
from ..fakeprotocol_smart import FakeSmartTransport
@hubs
async def test_hub_pair(
dev: Device,
mocker: MockerFixture,
runner: CliRunner,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that pair calls the expected methods."""
cs = dev.modules.get(Module.ChildSetup)
# Patch if the device supports the module
if cs is not None:
mock_pair = mocker.patch.object(cs, "pair")
res = await runner.invoke(hub, ["pair"], obj=dev, catch_exceptions=False)
if cs is None:
assert "is not a hub" in res.output
return
mock_pair.assert_awaited()
assert "Finding new devices for 10 seconds" in res.output
assert res.exit_code == 0
@hubs
async def test_hub_unpair(
dev: Device, mocker: MockerFixture, runner: CliRunner
) -> None:
"""Test that unpair calls the expected method."""
if not dev.children:
pytest.skip("Cannot test without child devices")
id_ = next(iter(dev.children)).device_id
cs = dev.modules.get(Module.ChildSetup)
mock_unpair = mocker.spy(cs, "unpair")
res = await runner.invoke(hub, ["unpair", id_], obj=dev, catch_exceptions=False)
mock_unpair.assert_awaited()
assert f"Unpaired {id_}" in res.output
assert res.exit_code == 0
@hubs_smart
@pytest.mark.requires_dummy
async def test_hub_list(dev: Device, runner: CliRunner) -> None:
"""Test that list shows each child with its index and availability."""
if not dev.children:
pytest.skip("Cannot test without child devices")
assert isinstance(dev.protocol._transport, FakeSmartTransport)
child_list = dev.protocol._transport.info["get_child_device_list"]
child_list["child_device_list"][0]["status"] = "offline"
await dev.update()
res = await runner.invoke(hub, ["list"], obj=dev, catch_exceptions=False)
rows = {line.split()[1]: line.split() for line in res.output.splitlines()[1:]}
for index, child in enumerate(dev.children):
row = rows[child.device_id]
assert row[0] == str(index)
assert row[3] == str(index != 0)
assert res.exit_code == 0
@plug_iot
async def test_non_hub(dev: Device, mocker: MockerFixture, runner: CliRunner) -> None:
"""Test that hub commands return an error if executed on a non-hub."""
assert dev.device_type is not DeviceType.Hub
res = await runner.invoke(
hub, ["unpair", "dummy_id"], obj=dev, catch_exceptions=False
)
assert "is not a hub" in res.output