mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-08-09 20:24:02 +00:00
Update documentation structure and start migrating to markdown (#934)
Starts structuring the documentation library usage into Tutorials, Guides, Explanations and Reference. Continues migrating new docs from rst to markdown. Extends the test framework discovery mocks to allow easy writing and testing of code examples.
This commit is contained in:
@@ -1,10 +1,35 @@
|
||||
"""Module for holding connection parameters.
|
||||
"""Configuration for connecting directly to a device without discovery.
|
||||
|
||||
If you are connecting to a newer KASA or TAPO device you can get the device
|
||||
via discovery or connect directly with :class:`DeviceConfig`.
|
||||
|
||||
Discovery returns a list of discovered devices:
|
||||
|
||||
>>> from kasa import Discover, Credentials, Device, DeviceConfig
|
||||
>>> device = await Discover.discover_single(
|
||||
>>> "127.0.0.3",
|
||||
>>> credentials=Credentials("myusername", "mypassword"),
|
||||
>>> discovery_timeout=10
|
||||
>>> )
|
||||
>>> print(device.alias) # Alias is None because update() has not been called
|
||||
None
|
||||
|
||||
>>> config_dict = device.config.to_dict()
|
||||
>>> # DeviceConfig.to_dict() can be used to store for later
|
||||
>>> print(config_dict)
|
||||
{'host': '127.0.0.3', 'timeout': 5, 'credentials': Credentials(), 'connection_type'\
|
||||
: {'device_family': 'SMART.TAPOBULB', 'encryption_type': 'KLAP', 'login_version': 2},\
|
||||
'uses_http': True}
|
||||
|
||||
>>> later_device = await Device.connect(config=DeviceConfig.from_dict(config_dict))
|
||||
>>> print(later_device.alias) # Alias is available as connect() calls update()
|
||||
Living Room Bulb
|
||||
|
||||
Note that this module does not work with from __future__ import annotations
|
||||
due to it's use of type returned by fields() which becomes a string with the import.
|
||||
https://bugs.python.org/issue39442
|
||||
"""
|
||||
|
||||
# Note that this module does not work with from __future__ import annotations
|
||||
# due to it's use of type returned by fields() which becomes a string with the import.
|
||||
# https://bugs.python.org/issue39442
|
||||
# ruff: noqa: FA100
|
||||
import logging
|
||||
from dataclasses import asdict, dataclass, field, fields, is_dataclass
|
||||
|
119
kasa/discover.py
119
kasa/discover.py
@@ -1,4 +1,81 @@
|
||||
"""Discovery module for TP-Link Smart Home devices."""
|
||||
"""Discover TPLink Smart Home devices.
|
||||
|
||||
The main entry point for this library is :func:`Discover.discover()`,
|
||||
which returns a dictionary of the found devices. The key is the IP address
|
||||
of the device and the value contains ready-to-use, SmartDevice-derived
|
||||
device object.
|
||||
|
||||
:func:`discover_single()` can be used to initialize a single device given its
|
||||
IP address. If the :class:`DeviceConfig` of the device is already known,
|
||||
you can initialize the corresponding device class directly without discovery.
|
||||
|
||||
The protocol uses UDP broadcast datagrams on port 9999 and 20002 for discovery.
|
||||
Legacy devices support discovery on port 9999 and newer devices on 20002.
|
||||
|
||||
Newer devices that respond on port 20002 will most likely require TP-Link cloud
|
||||
credentials to be passed if queries or updates are to be performed on the returned
|
||||
devices.
|
||||
|
||||
Discovery returns a dict of {ip: discovered devices}:
|
||||
|
||||
>>> import asyncio
|
||||
>>> from kasa import Discover, Credentials
|
||||
>>>
|
||||
>>> found_devices = await Discover.discover()
|
||||
>>> [dev.model for dev in found_devices.values()]
|
||||
['KP303(UK)', 'HS110(EU)', 'L530E', 'KL430(US)', 'HS220(US)']
|
||||
|
||||
Discovery can also be targeted to a specific broadcast address instead of
|
||||
the default 255.255.255.255:
|
||||
|
||||
>>> found_devices = await Discover.discover(target="127.0.0.255")
|
||||
>>> print(len(found_devices))
|
||||
5
|
||||
|
||||
Basic information is available on the device from the discovery broadcast response
|
||||
but it is important to call device.update() after discovery if you want to access
|
||||
all the attributes without getting errors or None.
|
||||
|
||||
>>> dev = found_devices["127.0.0.3"]
|
||||
>>> dev.alias
|
||||
None
|
||||
>>> await dev.update()
|
||||
>>> dev.alias
|
||||
'Living Room Bulb'
|
||||
|
||||
It is also possible to pass a coroutine to be executed for each found device:
|
||||
|
||||
>>> async def print_dev_info(dev):
|
||||
>>> await dev.update()
|
||||
>>> print(f"Discovered {dev.alias} (model: {dev.model})")
|
||||
>>>
|
||||
>>> devices = await Discover.discover(on_discovered=print_dev_info)
|
||||
Discovered Bedroom Power Strip (model: KP303(UK))
|
||||
Discovered Bedroom Lamp Plug (model: HS110(EU))
|
||||
Discovered Living Room Bulb (model: L530)
|
||||
Discovered Bedroom Lightstrip (model: KL430(US))
|
||||
Discovered Living Room Dimmer Switch (model: HS220(US))
|
||||
|
||||
You can pass credentials for devices requiring authentication
|
||||
|
||||
>>> devices = await Discover.discover(
|
||||
>>> credentials=Credentials("myusername", "mypassword"),
|
||||
>>> discovery_timeout=10
|
||||
>>> )
|
||||
>>> print(len(devices))
|
||||
5
|
||||
|
||||
Discovering a single device returns a kasa.Device object.
|
||||
|
||||
>>> device = await Discover.discover_single(
|
||||
>>> "127.0.0.1",
|
||||
>>> credentials=Credentials("myusername", "mypassword"),
|
||||
>>> discovery_timeout=10
|
||||
>>> )
|
||||
>>> device.model
|
||||
'KP303(UK)'
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -198,45 +275,7 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
|
||||
|
||||
|
||||
class Discover:
|
||||
"""Discover TPLink Smart Home devices.
|
||||
|
||||
The main entry point for this library is :func:`Discover.discover()`,
|
||||
which returns a dictionary of the found devices. The key is the IP address
|
||||
of the device and the value contains ready-to-use, SmartDevice-derived
|
||||
device object.
|
||||
|
||||
:func:`discover_single()` can be used to initialize a single device given its
|
||||
IP address. If the :class:`DeviceConfig` of the device is already known,
|
||||
you can initialize the corresponding device class directly without discovery.
|
||||
|
||||
The protocol uses UDP broadcast datagrams on port 9999 and 20002 for discovery.
|
||||
Legacy devices support discovery on port 9999 and newer devices on 20002.
|
||||
|
||||
Newer devices that respond on port 20002 will most likely require TP-Link cloud
|
||||
credentials to be passed if queries or updates are to be performed on the returned
|
||||
devices.
|
||||
|
||||
Examples:
|
||||
Discovery returns a list of discovered devices:
|
||||
|
||||
>>> import asyncio
|
||||
>>> found_devices = asyncio.run(Discover.discover())
|
||||
>>> [dev.alias for dev in found_devices]
|
||||
['TP-LINK_Power Strip_CF69']
|
||||
|
||||
Discovery can also be targeted to a specific broadcast address instead of
|
||||
the default 255.255.255.255:
|
||||
|
||||
>>> asyncio.run(Discover.discover(target="192.168.8.255"))
|
||||
|
||||
It is also possible to pass a coroutine to be executed for each found device:
|
||||
|
||||
>>> async def print_alias(dev):
|
||||
>>> print(f"Discovered {dev.alias}")
|
||||
>>> devices = asyncio.run(Discover.discover(on_discovered=print_alias))
|
||||
|
||||
|
||||
"""
|
||||
"""Class for discovering devices."""
|
||||
|
||||
DISCOVERY_PORT = 9999
|
||||
|
||||
|
@@ -30,7 +30,8 @@ class Feature:
|
||||
#: Action triggers some action on device
|
||||
Action = auto()
|
||||
#: Number defines a numeric setting
|
||||
#: See :ref:`range_getter`, :ref:`minimum_value`, and :ref:`maximum_value`
|
||||
#: See :attr:`range_getter`, :attr:`Feature.minimum_value`,
|
||||
#: and :attr:`maximum_value`
|
||||
Number = auto()
|
||||
#: Choice defines a setting with pre-defined values
|
||||
Choice = auto()
|
||||
|
@@ -105,7 +105,7 @@ class IotDevice(Device):
|
||||
All devices provide several informational properties:
|
||||
|
||||
>>> dev.alias
|
||||
Kitchen
|
||||
Bedroom Lamp Plug
|
||||
>>> dev.model
|
||||
HS110(EU)
|
||||
>>> dev.rssi
|
||||
|
@@ -23,7 +23,7 @@ class IotLightStrip(IotBulb):
|
||||
>>> strip = IotLightStrip("127.0.0.1")
|
||||
>>> asyncio.run(strip.update())
|
||||
>>> print(strip.alias)
|
||||
KL430 pantry lightstrip
|
||||
Bedroom Lightstrip
|
||||
|
||||
Getting the length of the strip:
|
||||
|
||||
|
@@ -32,7 +32,7 @@ class IotPlug(IotDevice):
|
||||
>>> plug = IotPlug("127.0.0.1")
|
||||
>>> asyncio.run(plug.update())
|
||||
>>> plug.alias
|
||||
Kitchen
|
||||
Bedroom Lamp Plug
|
||||
|
||||
Setting the LED state:
|
||||
|
||||
|
@@ -55,7 +55,7 @@ class IotStrip(IotDevice):
|
||||
>>> strip = IotStrip("127.0.0.1")
|
||||
>>> asyncio.run(strip.update())
|
||||
>>> strip.alias
|
||||
TP-LINK_Power Strip_CF69
|
||||
Bedroom Power Strip
|
||||
|
||||
All methods act on the whole strip:
|
||||
|
||||
|
@@ -396,6 +396,13 @@ async def get_device_for_fixture_protocol(fixture, protocol):
|
||||
return await get_device_for_fixture(fixture_info)
|
||||
|
||||
|
||||
def get_fixture_info(fixture, protocol):
|
||||
finfo = FixtureInfo(name=fixture, protocol=protocol, data={})
|
||||
for fixture_info in FIXTURE_DATA:
|
||||
if finfo == fixture_info:
|
||||
return fixture_info
|
||||
|
||||
|
||||
@pytest.fixture(params=filter_fixtures("main devices"), ids=idgenerator)
|
||||
async def dev(request) -> AsyncGenerator[Device, None]:
|
||||
"""Device fixture.
|
||||
|
@@ -44,9 +44,14 @@ UNSUPPORTED_DEVICES = {
|
||||
}
|
||||
|
||||
|
||||
def parametrize_discovery(desc, *, data_root_filter, protocol_filter=None):
|
||||
def parametrize_discovery(
|
||||
desc, *, data_root_filter=None, protocol_filter=None, model_filter=None
|
||||
):
|
||||
filtered_fixtures = filter_fixtures(
|
||||
desc, data_root_filter=data_root_filter, protocol_filter=protocol_filter
|
||||
desc,
|
||||
data_root_filter=data_root_filter,
|
||||
protocol_filter=protocol_filter,
|
||||
model_filter=model_filter,
|
||||
)
|
||||
return pytest.mark.parametrize(
|
||||
"discovery_mock",
|
||||
@@ -65,10 +70,14 @@ new_discovery = parametrize_discovery(
|
||||
params=filter_fixtures("discoverable", protocol_filter={"SMART", "IOT"}),
|
||||
ids=idgenerator,
|
||||
)
|
||||
def discovery_mock(request, mocker):
|
||||
async def discovery_mock(request, mocker):
|
||||
"""Mock discovery and patch protocol queries to use Fake protocols."""
|
||||
fixture_info: FixtureInfo = request.param
|
||||
fixture_data = fixture_info.data
|
||||
yield patch_discovery({"127.0.0.123": fixture_info}, mocker)
|
||||
|
||||
|
||||
def create_discovery_mock(ip: str, fixture_data: dict):
|
||||
"""Mock discovery and patch protocol queries to use Fake protocols."""
|
||||
|
||||
@dataclass
|
||||
class _DiscoveryMock:
|
||||
@@ -79,6 +88,7 @@ def discovery_mock(request, mocker):
|
||||
query_data: dict
|
||||
device_type: str
|
||||
encrypt_type: str
|
||||
_datagram: bytes
|
||||
login_version: int | None = None
|
||||
port_override: int | None = None
|
||||
|
||||
@@ -94,13 +104,14 @@ def discovery_mock(request, mocker):
|
||||
+ json_dumps(discovery_data).encode()
|
||||
)
|
||||
dm = _DiscoveryMock(
|
||||
"127.0.0.123",
|
||||
ip,
|
||||
80,
|
||||
20002,
|
||||
discovery_data,
|
||||
fixture_data,
|
||||
device_type,
|
||||
encrypt_type,
|
||||
datagram,
|
||||
login_version,
|
||||
)
|
||||
else:
|
||||
@@ -111,45 +122,87 @@ def discovery_mock(request, mocker):
|
||||
login_version = None
|
||||
datagram = XorEncryption.encrypt(json_dumps(discovery_data))[4:]
|
||||
dm = _DiscoveryMock(
|
||||
"127.0.0.123",
|
||||
ip,
|
||||
9999,
|
||||
9999,
|
||||
discovery_data,
|
||||
fixture_data,
|
||||
device_type,
|
||||
encrypt_type,
|
||||
datagram,
|
||||
login_version,
|
||||
)
|
||||
|
||||
async def mock_discover(self):
|
||||
port = (
|
||||
dm.port_override
|
||||
if dm.port_override and dm.discovery_port != 20002
|
||||
else dm.discovery_port
|
||||
)
|
||||
self.datagram_received(
|
||||
datagram,
|
||||
(dm.ip, port),
|
||||
)
|
||||
return dm
|
||||
|
||||
|
||||
def patch_discovery(fixture_infos: dict[str, FixtureInfo], mocker):
|
||||
"""Mock discovery and patch protocol queries to use Fake protocols."""
|
||||
discovery_mocks = {
|
||||
ip: create_discovery_mock(ip, fixture_info.data)
|
||||
for ip, fixture_info in fixture_infos.items()
|
||||
}
|
||||
protos = {
|
||||
ip: FakeSmartProtocol(fixture_info.data, fixture_info.name)
|
||||
if "SMART" in fixture_info.protocol
|
||||
else FakeIotProtocol(fixture_info.data, fixture_info.name)
|
||||
for ip, fixture_info in fixture_infos.items()
|
||||
}
|
||||
first_ip = list(fixture_infos.keys())[0]
|
||||
first_host = None
|
||||
|
||||
async def mock_discover(self):
|
||||
"""Call datagram_received for all mock fixtures.
|
||||
|
||||
Handles test cases modifying the ip and hostname of the first fixture
|
||||
for discover_single testing.
|
||||
"""
|
||||
for ip, dm in discovery_mocks.items():
|
||||
first_ip = list(discovery_mocks.values())[0].ip
|
||||
fixture_info = fixture_infos[ip]
|
||||
# Ip of first fixture could have been modified by a test
|
||||
if dm.ip == first_ip:
|
||||
# hostname could have been used
|
||||
host = first_host if first_host else first_ip
|
||||
else:
|
||||
host = dm.ip
|
||||
# update the protos for any host testing or the test overriding the first ip
|
||||
protos[host] = (
|
||||
FakeSmartProtocol(fixture_info.data, fixture_info.name)
|
||||
if "SMART" in fixture_info.protocol
|
||||
else FakeIotProtocol(fixture_info.data, fixture_info.name)
|
||||
)
|
||||
port = (
|
||||
dm.port_override
|
||||
if dm.port_override and dm.discovery_port != 20002
|
||||
else dm.discovery_port
|
||||
)
|
||||
self.datagram_received(
|
||||
dm._datagram,
|
||||
(dm.ip, port),
|
||||
)
|
||||
|
||||
async def _query(self, request, retry_count: int = 3):
|
||||
return await protos[self._host].query(request)
|
||||
|
||||
def _getaddrinfo(host, *_, **__):
|
||||
nonlocal first_host, first_ip
|
||||
first_host = host # Store the hostname used by discover single
|
||||
first_ip = list(discovery_mocks.values())[
|
||||
0
|
||||
].ip # ip could have been overridden in test
|
||||
return [(None, None, None, None, (first_ip, 0))]
|
||||
|
||||
mocker.patch("kasa.IotProtocol.query", _query)
|
||||
mocker.patch("kasa.SmartProtocol.query", _query)
|
||||
mocker.patch("kasa.discover._DiscoverProtocol.do_discover", mock_discover)
|
||||
mocker.patch(
|
||||
"socket.getaddrinfo",
|
||||
side_effect=lambda *_, **__: [(None, None, None, None, (dm.ip, 0))],
|
||||
# side_effect=lambda *_, **__: [(None, None, None, None, (first_ip, 0))],
|
||||
side_effect=_getaddrinfo,
|
||||
)
|
||||
|
||||
if "SMART" in fixture_info.protocol:
|
||||
proto = FakeSmartProtocol(fixture_data, fixture_info.name)
|
||||
else:
|
||||
proto = FakeIotProtocol(fixture_data)
|
||||
|
||||
async def _query(request, retry_count: int = 3):
|
||||
return await proto.query(request)
|
||||
|
||||
mocker.patch("kasa.IotProtocol.query", side_effect=_query)
|
||||
mocker.patch("kasa.SmartProtocol.query", side_effect=_query)
|
||||
|
||||
yield dm
|
||||
# Only return the first discovery mock to be used for testing discover single
|
||||
return discovery_mocks[first_ip]
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
|
@@ -3,7 +3,7 @@ import logging
|
||||
|
||||
from ..deviceconfig import DeviceConfig
|
||||
from ..iotprotocol import IotProtocol
|
||||
from ..xortransport import XorTransport
|
||||
from ..protocol import BaseTransport
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -178,17 +178,26 @@ MOTION_MODULE = {
|
||||
|
||||
|
||||
class FakeIotProtocol(IotProtocol):
|
||||
def __init__(self, info):
|
||||
def __init__(self, info, fixture_name=None):
|
||||
super().__init__(
|
||||
transport=XorTransport(
|
||||
config=DeviceConfig("127.0.0.123"),
|
||||
)
|
||||
transport=FakeIotTransport(info, fixture_name),
|
||||
)
|
||||
|
||||
async def query(self, request, retry_count: int = 3):
|
||||
"""Implement query here so tests can still patch IotProtocol.query."""
|
||||
resp_dict = await self._query(request, retry_count)
|
||||
return resp_dict
|
||||
|
||||
|
||||
class FakeIotTransport(BaseTransport):
|
||||
def __init__(self, info, fixture_name=None):
|
||||
super().__init__(config=DeviceConfig("127.0.0.123"))
|
||||
info = copy.deepcopy(info)
|
||||
self.discovery_data = info
|
||||
self.fixture_name = fixture_name
|
||||
self.writer = None
|
||||
self.reader = None
|
||||
proto = copy.deepcopy(FakeIotProtocol.baseproto)
|
||||
proto = copy.deepcopy(FakeIotTransport.baseproto)
|
||||
|
||||
for target in info:
|
||||
# print("target %s" % target)
|
||||
@@ -220,6 +229,14 @@ class FakeIotProtocol(IotProtocol):
|
||||
|
||||
self.proto = proto
|
||||
|
||||
@property
|
||||
def default_port(self) -> int:
|
||||
return 9999
|
||||
|
||||
@property
|
||||
def credentials_hash(self) -> str:
|
||||
return ""
|
||||
|
||||
def set_alias(self, x, child_ids=None):
|
||||
if child_ids is None:
|
||||
child_ids = []
|
||||
@@ -367,7 +384,7 @@ class FakeIotProtocol(IotProtocol):
|
||||
"smartlife.iot.common.cloud": CLOUD_MODULE,
|
||||
}
|
||||
|
||||
async def query(self, request, port=9999):
|
||||
async def send(self, request, port=9999):
|
||||
proto = self.proto
|
||||
|
||||
# collect child ids from context
|
||||
@@ -414,3 +431,9 @@ class FakeIotProtocol(IotProtocol):
|
||||
response.update(get_response_for_module(target))
|
||||
|
||||
return copy.deepcopy(response)
|
||||
|
||||
async def close(self) -> None:
|
||||
pass
|
||||
|
||||
async def reset(self) -> None:
|
||||
pass
|
||||
|
2
kasa/tests/fixtures/HS110(EU)_1.0_1.2.5.json
vendored
2
kasa/tests/fixtures/HS110(EU)_1.0_1.2.5.json
vendored
@@ -11,7 +11,7 @@
|
||||
"system": {
|
||||
"get_sysinfo": {
|
||||
"active_mode": "schedule",
|
||||
"alias": "Kitchen",
|
||||
"alias": "Bedroom Lamp Plug",
|
||||
"dev_name": "Wi-Fi Smart Plug With Energy Monitoring",
|
||||
"deviceId": "0000000000000000000000000000000000000000",
|
||||
"err_code": 0,
|
||||
|
2
kasa/tests/fixtures/HS220(US)_1.0_1.5.7.json
vendored
2
kasa/tests/fixtures/HS220(US)_1.0_1.5.7.json
vendored
@@ -28,7 +28,7 @@
|
||||
"system": {
|
||||
"get_sysinfo": {
|
||||
"active_mode": "none",
|
||||
"alias": "Living room left dimmer",
|
||||
"alias": "Living Room Dimmer Switch",
|
||||
"brightness": 25,
|
||||
"dev_name": "Smart Wi-Fi Dimmer",
|
||||
"deviceId": "000000000000000000000000000000000000000",
|
||||
|
2
kasa/tests/fixtures/HS220(US)_2.0_1.0.3.json
vendored
2
kasa/tests/fixtures/HS220(US)_2.0_1.0.3.json
vendored
@@ -17,7 +17,7 @@
|
||||
"system": {
|
||||
"get_sysinfo": {
|
||||
"active_mode": "none",
|
||||
"alias": "Living Room Lights",
|
||||
"alias": "Living Room Dimmer Switch",
|
||||
"brightness": 100,
|
||||
"dev_name": "Wi-Fi Smart Dimmer",
|
||||
"deviceId": "0000000000000000000000000000000000000000",
|
||||
|
@@ -23,7 +23,7 @@
|
||||
"system": {
|
||||
"get_sysinfo": {
|
||||
"active_mode": "none",
|
||||
"alias": "KL430 pantry lightstrip",
|
||||
"alias": "Bedroom Lightstrip",
|
||||
"ctrl_protocols": {
|
||||
"name": "Linkie",
|
||||
"version": "1.0"
|
||||
|
2
kasa/tests/fixtures/KP303(UK)_1.0_1.0.3.json
vendored
2
kasa/tests/fixtures/KP303(UK)_1.0_1.0.3.json
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"system": {
|
||||
"get_sysinfo": {
|
||||
"alias": "TP-LINK_Power Strip_CF69",
|
||||
"alias": "Bedroom Power Strip",
|
||||
"child_num": 3,
|
||||
"children": [
|
||||
{
|
||||
|
@@ -175,7 +175,7 @@
|
||||
"longitude": 0,
|
||||
"mac": "5C-E9-31-00-00-00",
|
||||
"model": "L530",
|
||||
"nickname": "TGl2aW5nIFJvb20=",
|
||||
"nickname": "TGl2aW5nIFJvb20gQnVsYg==",
|
||||
"oem_id": "00000000000000000000000000000000",
|
||||
"overheated": false,
|
||||
"region": "Europe/Berlin",
|
||||
|
@@ -107,7 +107,6 @@ async def test_type_unknown():
|
||||
|
||||
|
||||
@pytest.mark.parametrize("custom_port", [123, None])
|
||||
# @pytest.mark.parametrize("discovery_mock", [("127.0.0.1",123), ("127.0.0.1",None)], indirect=True)
|
||||
async def test_discover_single(discovery_mock, custom_port, mocker):
|
||||
"""Make sure that discover_single returns an initialized SmartDevice instance."""
|
||||
host = "127.0.0.1"
|
||||
@@ -115,7 +114,8 @@ async def test_discover_single(discovery_mock, custom_port, mocker):
|
||||
discovery_mock.port_override = custom_port
|
||||
|
||||
device_class = Discover._get_device_class(discovery_mock.discovery_data)
|
||||
update_mock = mocker.patch.object(device_class, "update")
|
||||
# discovery_mock patches protocol query methods so use spy here.
|
||||
update_mock = mocker.spy(device_class, "update")
|
||||
|
||||
x = await Discover.discover_single(
|
||||
host, port=custom_port, credentials=Credentials()
|
||||
@@ -123,6 +123,7 @@ async def test_discover_single(discovery_mock, custom_port, mocker):
|
||||
assert issubclass(x.__class__, Device)
|
||||
assert x._discovery_info is not None
|
||||
assert x.port == custom_port or x.port == discovery_mock.default_port
|
||||
# Make sure discovery does not call update()
|
||||
assert update_mock.call_count == 0
|
||||
if discovery_mock.default_port == 80:
|
||||
assert x.alias is None
|
||||
|
@@ -3,8 +3,11 @@ import asyncio
|
||||
import pytest
|
||||
import xdoctest
|
||||
|
||||
from kasa import Discover
|
||||
from kasa.tests.conftest import get_device_for_fixture_protocol
|
||||
from kasa.tests.conftest import (
|
||||
get_device_for_fixture_protocol,
|
||||
get_fixture_info,
|
||||
patch_discovery,
|
||||
)
|
||||
|
||||
|
||||
def test_bulb_examples(mocker):
|
||||
@@ -62,34 +65,39 @@ def test_lightstrip_examples(mocker):
|
||||
assert not res["failed"]
|
||||
|
||||
|
||||
def test_discovery_examples(mocker):
|
||||
def test_discovery_examples(readmes_mock):
|
||||
"""Test discovery examples."""
|
||||
p = asyncio.run(get_device_for_fixture_protocol("KP303(UK)_1.0_1.0.3.json", "IOT"))
|
||||
|
||||
mocker.patch("kasa.discover.Discover.discover", return_value=[p])
|
||||
res = xdoctest.doctest_module("kasa.discover", "all")
|
||||
assert res["n_passed"] > 0
|
||||
assert not res["failed"]
|
||||
|
||||
|
||||
def test_tutorial_examples(mocker, top_level_await):
|
||||
def test_deviceconfig_examples(readmes_mock):
|
||||
"""Test discovery examples."""
|
||||
a = asyncio.run(
|
||||
get_device_for_fixture_protocol("L530E(EU)_3.0_1.1.6.json", "SMART")
|
||||
)
|
||||
b = asyncio.run(get_device_for_fixture_protocol("HS110(EU)_1.0_1.2.5.json", "IOT"))
|
||||
a.host = "127.0.0.1"
|
||||
b.host = "127.0.0.2"
|
||||
|
||||
# Note autospec does not work for staticmethods in python < 3.12
|
||||
# https://github.com/python/cpython/issues/102978
|
||||
mocker.patch(
|
||||
"kasa.discover.Discover.discover_single", return_value=a, autospec=True
|
||||
)
|
||||
mocker.patch.object(Discover, "discover", return_value=[a, b], autospec=True)
|
||||
res = xdoctest.doctest_module("docs/tutorial.py", "all")
|
||||
res = xdoctest.doctest_module("kasa.deviceconfig", "all")
|
||||
assert res["n_passed"] > 0
|
||||
assert not res["failed"]
|
||||
|
||||
|
||||
def test_tutorial_examples(readmes_mock):
|
||||
"""Test discovery examples."""
|
||||
res = xdoctest.doctest_module("docs/tutorial.py", "all")
|
||||
assert res["n_passed"] > 0
|
||||
assert not res["failed"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def readmes_mock(mocker, top_level_await):
|
||||
fixture_infos = {
|
||||
"127.0.0.1": get_fixture_info("KP303(UK)_1.0_1.0.3.json", "IOT"), # Strip
|
||||
"127.0.0.2": get_fixture_info("HS110(EU)_1.0_1.2.5.json", "IOT"), # Plug
|
||||
"127.0.0.3": get_fixture_info("L530E(EU)_3.0_1.1.6.json", "SMART"), # Bulb
|
||||
"127.0.0.4": get_fixture_info("KL430(US)_1.0_1.0.10.json", "IOT"), # Lightstrip
|
||||
"127.0.0.5": get_fixture_info("HS220(US)_1.0_1.5.7.json", "IOT"), # Dimmer
|
||||
}
|
||||
yield patch_discovery(fixture_infos, mocker)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def top_level_await(mocker):
|
||||
"""Fixture to enable top level awaits in doctests.
|
||||
@@ -99,19 +107,26 @@ def top_level_await(mocker):
|
||||
"""
|
||||
import ast
|
||||
from inspect import CO_COROUTINE
|
||||
from types import CodeType
|
||||
|
||||
orig_exec = exec
|
||||
orig_eval = eval
|
||||
orig_compile = compile
|
||||
|
||||
def patch_exec(source, globals=None, locals=None, /, **kwargs):
|
||||
if source.co_flags & CO_COROUTINE == CO_COROUTINE:
|
||||
if (
|
||||
isinstance(source, CodeType)
|
||||
and source.co_flags & CO_COROUTINE == CO_COROUTINE
|
||||
):
|
||||
asyncio.run(orig_eval(source, globals, locals))
|
||||
else:
|
||||
orig_exec(source, globals, locals, **kwargs)
|
||||
|
||||
def patch_eval(source, globals=None, locals=None, /, **kwargs):
|
||||
if source.co_flags & CO_COROUTINE == CO_COROUTINE:
|
||||
if (
|
||||
isinstance(source, CodeType)
|
||||
and source.co_flags & CO_COROUTINE == CO_COROUTINE
|
||||
):
|
||||
return asyncio.run(orig_eval(source, globals, locals, **kwargs))
|
||||
else:
|
||||
return orig_eval(source, globals, locals, **kwargs)
|
||||
|
Reference in New Issue
Block a user