mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 19:23:34 +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:
parent
767156421b
commit
6616d68d42
@ -1,4 +1,4 @@
|
|||||||
<h2 align="center">python-kasa</h2>
|
# python-kasa
|
||||||
|
|
||||||
[![PyPI version](https://badge.fury.io/py/python-kasa.svg)](https://badge.fury.io/py/python-kasa)
|
[![PyPI version](https://badge.fury.io/py/python-kasa.svg)](https://badge.fury.io/py/python-kasa)
|
||||||
[![Build Status](https://github.com/python-kasa/python-kasa/actions/workflows/ci.yml/badge.svg)](https://github.com/python-kasa/python-kasa/actions/workflows/ci.yml)
|
[![Build Status](https://github.com/python-kasa/python-kasa/actions/workflows/ci.yml/badge.svg)](https://github.com/python-kasa/python-kasa/actions/workflows/ci.yml)
|
||||||
|
@ -37,6 +37,10 @@ extensions = [
|
|||||||
"myst_parser",
|
"myst_parser",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
myst_enable_extensions = [
|
||||||
|
"colon_fence",
|
||||||
|
]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ["_templates"]
|
templates_path = ["_templates"]
|
||||||
|
|
||||||
|
24
docs/source/deprecated.md
Normal file
24
docs/source/deprecated.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Deprecated API
|
||||||
|
|
||||||
|
```{currentmodule} kasa
|
||||||
|
```
|
||||||
|
The page contains the documentation for the deprecated library API that only works with the older kasa devices.
|
||||||
|
|
||||||
|
If you want to continue to use the old API for older devices,
|
||||||
|
you can use the classes in the `iot` module to avoid deprecation warnings.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from kasa.iot import IotDevice, IotBulb, IotPlug, IotDimmer, IotStrip, IotLightStrip
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```{toctree}
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
smartdevice
|
||||||
|
smartbulb
|
||||||
|
smartplug
|
||||||
|
smartdimmer
|
||||||
|
smartstrip
|
||||||
|
smartlightstrip
|
||||||
|
```
|
@ -1,62 +0,0 @@
|
|||||||
.. py:module:: kasa.discover
|
|
||||||
|
|
||||||
Discovering devices
|
|
||||||
===================
|
|
||||||
|
|
||||||
.. contents:: Contents
|
|
||||||
:local:
|
|
||||||
|
|
||||||
Discovery
|
|
||||||
*********
|
|
||||||
|
|
||||||
Discovery works by sending broadcast UDP packets to two known TP-link discovery ports, 9999 and 20002.
|
|
||||||
Port 9999 is used for legacy devices that do not use strong encryption and 20002 is for newer devices that use different
|
|
||||||
levels of encryption.
|
|
||||||
If a device uses port 20002 for discovery you will obtain some basic information from the device via discovery, but you
|
|
||||||
will need to await :func:`Device.update() <kasa.SmartDevice.update()>` to get full device information.
|
|
||||||
Credentials will most likely be required for port 20002 devices although if the device has never been connected to the tplink
|
|
||||||
cloud it may work without credentials.
|
|
||||||
|
|
||||||
To query or update the device requires authentication via :class:`Credentials <kasa.Credentials>` and if this is invalid or not provided it
|
|
||||||
will raise an :class:`AuthenticationException <kasa.AuthenticationException>`.
|
|
||||||
|
|
||||||
If discovery encounters an unsupported device when calling via :meth:`Discover.discover_single() <kasa.Discover.discover_single>`
|
|
||||||
it will raise a :class:`UnsupportedDeviceException <kasa.UnsupportedDeviceException>`.
|
|
||||||
If discovery encounters a device when calling :meth:`Discover.discover() <kasa.Discover.discover>`,
|
|
||||||
you can provide a callback to the ``on_unsupported`` parameter
|
|
||||||
to handle these.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from kasa import Discover, Credentials
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
device = await Discover.discover_single(
|
|
||||||
"127.0.0.1",
|
|
||||||
credentials=Credentials("myusername", "mypassword"),
|
|
||||||
discovery_timeout=10
|
|
||||||
)
|
|
||||||
|
|
||||||
await device.update() # Request the update
|
|
||||||
print(device.alias) # Print out the alias
|
|
||||||
|
|
||||||
devices = await Discover.discover(
|
|
||||||
credentials=Credentials("myusername", "mypassword"),
|
|
||||||
discovery_timeout=10
|
|
||||||
)
|
|
||||||
for ip, device in devices.items():
|
|
||||||
await device.update()
|
|
||||||
print(device.alias)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
|
|
||||||
API documentation
|
|
||||||
*****************
|
|
||||||
|
|
||||||
.. autoclass:: kasa.Discover
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
42
docs/source/guides.md
Normal file
42
docs/source/guides.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# How-to Guides
|
||||||
|
|
||||||
|
This page contains guides of how to perform common actions using the library.
|
||||||
|
|
||||||
|
## Discover devices
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: kasa.discover
|
||||||
|
```
|
||||||
|
|
||||||
|
## Connect without discovery
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: kasa.deviceconfig
|
||||||
|
```
|
||||||
|
|
||||||
|
## Get Energy Consumption and Usage Statistics
|
||||||
|
|
||||||
|
:::{note}
|
||||||
|
In order to use the helper methods to calculate the statistics correctly, your devices need to have correct time set.
|
||||||
|
The devices use NTP and public servers from [NTP Pool Project](https://www.ntppool.org/) to synchronize their time.
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Energy Consumption
|
||||||
|
|
||||||
|
The availability of energy consumption sensors depend on the device.
|
||||||
|
While most of the bulbs support it, only specific switches (e.g., HS110) or strips (e.g., HS300) support it.
|
||||||
|
You can use {attr}`~Device.has_emeter` to check for the availability.
|
||||||
|
|
||||||
|
|
||||||
|
### Usage statistics
|
||||||
|
|
||||||
|
You can use {attr}`~Device.on_since` to query for the time the device has been turned on.
|
||||||
|
Some devices also support reporting the usage statistics on daily or monthly basis.
|
||||||
|
You can access this information using through the usage module ({class}`kasa.modules.Usage`):
|
||||||
|
|
||||||
|
```py
|
||||||
|
dev = SmartPlug("127.0.0.1")
|
||||||
|
usage = dev.modules["usage"]
|
||||||
|
print(f"Minutes on this month: {usage.usage_this_month}")
|
||||||
|
print(f"Minutes on today: {usage.usage_today}")
|
||||||
|
```
|
12
docs/source/index.md
Normal file
12
docs/source/index.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
```{include} ../../README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
```{toctree}
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
Home <self>
|
||||||
|
cli
|
||||||
|
library
|
||||||
|
contribute
|
||||||
|
SUPPORTED
|
||||||
|
```
|
@ -1,20 +0,0 @@
|
|||||||
.. include:: ../../README.md
|
|
||||||
:parser: myst_parser.sphinx_
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
|
|
||||||
Home <self>
|
|
||||||
cli
|
|
||||||
tutorial
|
|
||||||
discover
|
|
||||||
device
|
|
||||||
design
|
|
||||||
contribute
|
|
||||||
smartbulb
|
|
||||||
smartplug
|
|
||||||
smartdimmer
|
|
||||||
smartstrip
|
|
||||||
smartlightstrip
|
|
||||||
SUPPORTED
|
|
15
docs/source/library.md
Normal file
15
docs/source/library.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Library usage
|
||||||
|
|
||||||
|
```{currentmodule} kasa
|
||||||
|
```
|
||||||
|
The page contains all information about the library usage:
|
||||||
|
|
||||||
|
```{toctree}
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
tutorial
|
||||||
|
guides
|
||||||
|
topics
|
||||||
|
reference
|
||||||
|
deprecated
|
||||||
|
```
|
134
docs/source/reference.md
Normal file
134
docs/source/reference.md
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
# API Reference
|
||||||
|
|
||||||
|
```{currentmodule} kasa
|
||||||
|
```
|
||||||
|
|
||||||
|
## Discover
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. autoclass:: kasa.Discover
|
||||||
|
:members:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Device
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. autoclass:: kasa.Device
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modules and Features
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. autoclass:: kasa.Module
|
||||||
|
:noindex:
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: kasa.interfaces
|
||||||
|
:noindex:
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. autoclass:: kasa.Feature
|
||||||
|
:noindex:
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Protocols and transports
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. autoclass:: kasa.protocol.BaseProtocol
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. autoclass:: kasa.iotprotocol.IotProtocol
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. autoclass:: kasa.smartprotocol.SmartProtocol
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. autoclass:: kasa.protocol.BaseTransport
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. autoclass:: kasa.xortransport.XorTransport
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. autoclass:: kasa.klaptransport.KlapTransport
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. autoclass:: kasa.klaptransport.KlapTransportV2
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. autoclass:: kasa.aestransport.AesTransport
|
||||||
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors and exceptions
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. autoclass:: kasa.exceptions.KasaException
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. autoclass:: kasa.exceptions.DeviceError
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. autoclass:: kasa.exceptions.AuthenticationError
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. autoclass:: kasa.exceptions.UnsupportedDeviceError
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. autoclass:: kasa.exceptions.TimeoutError
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
@ -1,32 +1,32 @@
|
|||||||
.. py:module:: kasa
|
.. py:currentmodule:: kasa
|
||||||
|
|
||||||
Common API
|
Base Device
|
||||||
==========
|
===========
|
||||||
|
|
||||||
.. contents:: Contents
|
.. contents:: Contents
|
||||||
:local:
|
:local:
|
||||||
|
|
||||||
Device class
|
SmartDevice class
|
||||||
************
|
*****************
|
||||||
|
|
||||||
The basic functionalities of all supported devices are accessible using the common :class:`Device` base class.
|
The basic functionalities of all supported devices are accessible using the common :class:`SmartDevice` base class.
|
||||||
|
|
||||||
The property accesses use the data obtained before by awaiting :func:`Device.update()`.
|
The property accesses use the data obtained before by awaiting :func:`SmartDevice.update()`.
|
||||||
The values are cached until the next update call. In practice this means that property accesses do no I/O and are dependent, while I/O producing methods need to be awaited.
|
The values are cached until the next update call. In practice this means that property accesses do no I/O and are dependent, while I/O producing methods need to be awaited.
|
||||||
See :ref:`library_design` for more detailed information.
|
See :ref:`topics-update-cycle` for more detailed information.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
The device instances share the communication socket in background to optimize I/O accesses.
|
The device instances share the communication socket in background to optimize I/O accesses.
|
||||||
This means that you need to use the same event loop for subsequent requests.
|
This means that you need to use the same event loop for subsequent requests.
|
||||||
The library gives a warning ("Detected protocol reuse between different event loop") to hint if you are accessing the device incorrectly.
|
The library gives a warning ("Detected protocol reuse between different event loop") to hint if you are accessing the device incorrectly.
|
||||||
|
|
||||||
Methods changing the state of the device do not invalidate the cache (i.e., there is no implicit :func:`Device.update()` call made by the library).
|
Methods changing the state of the device do not invalidate the cache (i.e., there is no implicit :func:`SmartDevice.update()` call made by the library).
|
||||||
You can assume that the operation has succeeded if no exception is raised.
|
You can assume that the operation has succeeded if no exception is raised.
|
||||||
These methods will return the device response, which can be useful for some use cases.
|
These methods will return the device response, which can be useful for some use cases.
|
||||||
|
|
||||||
Errors are raised as :class:`KasaException` instances for the library user to handle.
|
Errors are raised as :class:`SmartDeviceException` instances for the library user to handle.
|
||||||
|
|
||||||
Simple example script showing some functionality for legacy devices:
|
Simple example script showing some functionality:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@ -45,31 +45,6 @@ Simple example script showing some functionality for legacy devices:
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
|
||||||
If you are connecting to a newer KASA or TAPO device you can get the device via discovery or
|
|
||||||
connect directly with :class:`DeviceConfig`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from kasa import Discover, Credentials
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
device = await Discover.discover_single(
|
|
||||||
"127.0.0.1",
|
|
||||||
credentials=Credentials("myusername", "mypassword"),
|
|
||||||
discovery_timeout=10
|
|
||||||
)
|
|
||||||
|
|
||||||
config = device.config # DeviceConfig.to_dict() can be used to store for later
|
|
||||||
|
|
||||||
# To connect directly later without discovery
|
|
||||||
|
|
||||||
later_device = await SmartDevice.connect(config=config)
|
|
||||||
|
|
||||||
await later_device.update()
|
|
||||||
|
|
||||||
print(later_device.alias) # Print out the alias
|
|
||||||
|
|
||||||
If you want to perform updates in a loop, you need to make sure that the device accesses are done in the same event loop:
|
If you want to perform updates in a loop, you need to make sure that the device accesses are done in the same event loop:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
@ -92,22 +67,6 @@ Refer to device type specific classes for more examples:
|
|||||||
:class:`SmartPlug`, :class:`SmartBulb`, :class:`SmartStrip`,
|
:class:`SmartPlug`, :class:`SmartBulb`, :class:`SmartStrip`,
|
||||||
:class:`SmartDimmer`, :class:`SmartLightStrip`.
|
:class:`SmartDimmer`, :class:`SmartLightStrip`.
|
||||||
|
|
||||||
DeviceConfig class
|
|
||||||
******************
|
|
||||||
|
|
||||||
The :class:`DeviceConfig` class can be used to initialise devices with parameters to allow them to be connected to without using
|
|
||||||
discovery.
|
|
||||||
This is required for newer KASA and TAPO devices that use different protocols for communication and will not respond
|
|
||||||
on port 9999 but instead use different encryption protocols over http port 80.
|
|
||||||
Currently there are three known types of encryption for TP-Link devices and two different protocols.
|
|
||||||
Devices with automatic firmware updates enabled may update to newer versions of the encryption without separate notice,
|
|
||||||
so discovery can be helpful to determine the correct config.
|
|
||||||
|
|
||||||
To connect directly pass a :class:`DeviceConfig` object to :meth:`Device.connect()`.
|
|
||||||
|
|
||||||
A :class:`DeviceConfig` can be constucted manually if you know the :attr:`DeviceConfig.connection_type` values for the device or
|
|
||||||
alternatively the config can be retrieved from :attr:`Device.config` post discovery and then re-used.
|
|
||||||
|
|
||||||
Energy Consumption and Usage Statistics
|
Energy Consumption and Usage Statistics
|
||||||
***************************************
|
***************************************
|
||||||
|
|
||||||
@ -141,16 +100,6 @@ You can access this information using through the usage module (:class:`kasa.mod
|
|||||||
API documentation
|
API documentation
|
||||||
*****************
|
*****************
|
||||||
|
|
||||||
.. autoclass:: Device
|
.. autoclass:: SmartDevice
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. autoclass:: DeviceConfig
|
|
||||||
:members:
|
|
||||||
:inherited-members:
|
|
||||||
:undoc-members:
|
|
||||||
:member-order: bysource
|
|
||||||
|
|
||||||
.. autoclass:: Credentials
|
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
@ -1,70 +1,96 @@
|
|||||||
.. py:module:: kasa.modules
|
|
||||||
|
|
||||||
|
# Topics
|
||||||
|
|
||||||
.. _library_design:
|
```{contents} Contents
|
||||||
|
:local:
|
||||||
|
```
|
||||||
|
|
||||||
Library Design & Modules
|
These topics aim to provide some details on the design and internals of this library.
|
||||||
========================
|
|
||||||
|
|
||||||
This page aims to provide some details on the design and internals of this library.
|
|
||||||
You might be interested in this if you want to improve this library,
|
You might be interested in this if you want to improve this library,
|
||||||
or if you are just looking to access some information that is not currently exposed.
|
or if you are just looking to access some information that is not currently exposed.
|
||||||
|
|
||||||
.. contents:: Contents
|
(topics-initialization)=
|
||||||
:local:
|
## Initialization
|
||||||
|
|
||||||
.. _initialization:
|
Use {func}`~kasa.Discover.discover` to perform udp-based broadcast discovery on the network.
|
||||||
|
|
||||||
Initialization
|
|
||||||
**************
|
|
||||||
|
|
||||||
Use :func:`~kasa.Discover.discover` to perform udp-based broadcast discovery on the network.
|
|
||||||
This will return you a list of device instances based on the discovery replies.
|
This will return you a list of device instances based on the discovery replies.
|
||||||
|
|
||||||
If the device's host is already known, you can use to construct a device instance with
|
If the device's host is already known, you can use to construct a device instance with
|
||||||
:meth:`~kasa.Device.connect()`.
|
{meth}`~kasa.Device.connect()`.
|
||||||
|
|
||||||
The :meth:`~kasa.Device.connect()` also enables support for connecting to new
|
The {meth}`~kasa.Device.connect()` also enables support for connecting to new
|
||||||
KASA SMART protocol and TAPO devices directly using the parameter :class:`~kasa.DeviceConfig`.
|
KASA SMART protocol and TAPO devices directly using the parameter {class}`~kasa.DeviceConfig`.
|
||||||
Simply serialize the :attr:`~kasa.Device.config` property via :meth:`~kasa.DeviceConfig.to_dict()`
|
Simply serialize the {attr}`~kasa.Device.config` property via {meth}`~kasa.DeviceConfig.to_dict()`
|
||||||
and then deserialize it later with :func:`~kasa.DeviceConfig.from_dict()`
|
and then deserialize it later with {func}`~kasa.DeviceConfig.from_dict()`
|
||||||
and then pass it into :meth:`~kasa.Device.connect()`.
|
and then pass it into {meth}`~kasa.Device.connect()`.
|
||||||
|
|
||||||
|
|
||||||
.. _update_cycle:
|
(topics-discovery)=
|
||||||
|
## Discovery
|
||||||
|
|
||||||
Update Cycle
|
Discovery works by sending broadcast UDP packets to two known TP-link discovery ports, 9999 and 20002.
|
||||||
************
|
Port 9999 is used for legacy devices that do not use strong encryption and 20002 is for newer devices that use different
|
||||||
|
levels of encryption.
|
||||||
|
If a device uses port 20002 for discovery you will obtain some basic information from the device via discovery, but you
|
||||||
|
will need to await {func}`Device.update() <kasa.Device.update()>` to get full device information.
|
||||||
|
Credentials will most likely be required for port 20002 devices although if the device has never been connected to the tplink
|
||||||
|
cloud it may work without credentials.
|
||||||
|
|
||||||
When :meth:`~kasa.Device.update()` is called,
|
To query or update the device requires authentication via {class}`Credentials <kasa.Credentials>` and if this is invalid or not provided it
|
||||||
|
will raise an {class}`AuthenticationException <kasa.AuthenticationException>`.
|
||||||
|
|
||||||
|
If discovery encounters an unsupported device when calling via {meth}`Discover.discover_single() <kasa.Discover.discover_single>`
|
||||||
|
it will raise a {class}`UnsupportedDeviceException <kasa.UnsupportedDeviceException>`.
|
||||||
|
If discovery encounters a device when calling {func}`Discover.discover() <kasa.Discover.discover>`,
|
||||||
|
you can provide a callback to the ``on_unsupported`` parameter
|
||||||
|
to handle these.
|
||||||
|
|
||||||
|
(topics-deviceconfig)=
|
||||||
|
## DeviceConfig
|
||||||
|
|
||||||
|
The {class}`DeviceConfig` class can be used to initialise devices with parameters to allow them to be connected to without using
|
||||||
|
discovery.
|
||||||
|
This is required for newer KASA and TAPO devices that use different protocols for communication and will not respond
|
||||||
|
on port 9999 but instead use different encryption protocols over http port 80.
|
||||||
|
Currently there are three known types of encryption for TP-Link devices and two different protocols.
|
||||||
|
Devices with automatic firmware updates enabled may update to newer versions of the encryption without separate notice,
|
||||||
|
so discovery can be helpful to determine the correct config.
|
||||||
|
|
||||||
|
To connect directly pass a {class}`DeviceConfig` object to {meth}`Device.connect()`.
|
||||||
|
|
||||||
|
A {class}`DeviceConfig` can be constucted manually if you know the {attr}`DeviceConfig.connection_type` values for the device or
|
||||||
|
alternatively the config can be retrieved from {attr}`Device.config` post discovery and then re-used.
|
||||||
|
|
||||||
|
(topics-update-cycle)=
|
||||||
|
## Update Cycle
|
||||||
|
|
||||||
|
When {meth}`~kasa.Device.update()` is called,
|
||||||
the library constructs a query to send to the device based on :ref:`supported modules <modules>`.
|
the library constructs a query to send to the device based on :ref:`supported modules <modules>`.
|
||||||
Internally, each module defines :meth:`~kasa.modules.Module.query()` to describe what they want query during the update.
|
Internally, each module defines {meth}`~kasa.modules.Module.query()` to describe what they want query during the update.
|
||||||
|
|
||||||
The returned data is cached internally to avoid I/O on property accesses.
|
The returned data is cached internally to avoid I/O on property accesses.
|
||||||
All properties defined both in the device class and in the module classes follow this principle.
|
All properties defined both in the device class and in the module classes follow this principle.
|
||||||
|
|
||||||
While the properties are designed to provide a nice API to use for common use cases,
|
While the properties are designed to provide a nice API to use for common use cases,
|
||||||
you may sometimes want to access the raw, cached data as returned by the device.
|
you may sometimes want to access the raw, cached data as returned by the device.
|
||||||
This can be done using the :attr:`~kasa.Device.internal_state` property.
|
This can be done using the {attr}`~kasa.Device.internal_state` property.
|
||||||
|
|
||||||
|
|
||||||
.. _modules:
|
(topics-modules-and-features)=
|
||||||
|
## Modules and Features
|
||||||
|
|
||||||
Modules
|
The functionality provided by all {class}`~kasa.Device` instances is (mostly) done inside separate modules.
|
||||||
*******
|
|
||||||
|
|
||||||
The functionality provided by all :class:`~kasa.Device` instances is (mostly) done inside separate modules.
|
|
||||||
While the individual device-type specific classes provide an easy access for the most import features,
|
While the individual device-type specific classes provide an easy access for the most import features,
|
||||||
you can also access individual modules through :attr:`kasa.SmartDevice.modules`.
|
you can also access individual modules through {attr}`kasa.Device.modules`.
|
||||||
You can get the list of supported modules for a given device instance using :attr:`~kasa.Device.supported_modules`.
|
You can get the list of supported modules for a given device instance using {attr}`~kasa.Device.supported_modules`.
|
||||||
|
|
||||||
.. note::
|
```{note}
|
||||||
|
If you only need some module-specific information,
|
||||||
|
you can call the wanted method on the module to avoid using {meth}`~kasa.Device.update`.
|
||||||
|
```
|
||||||
|
|
||||||
If you only need some module-specific information,
|
(topics-protocols-and-transports)=
|
||||||
you can call the wanted method on the module to avoid using :meth:`~kasa.Device.update`.
|
## Protocols and Transports
|
||||||
|
|
||||||
Protocols and Transports
|
|
||||||
************************
|
|
||||||
|
|
||||||
The library supports two different TP-Link protocols, ``IOT`` and ``SMART``.
|
The library supports two different TP-Link protocols, ``IOT`` and ``SMART``.
|
||||||
``IOT`` is the original Kasa protocol and ``SMART`` is the newer protocol supported by TAPO devices and newer KASA devices.
|
``IOT`` is the original Kasa protocol and ``SMART`` is the newer protocol supported by TAPO devices and newer KASA devices.
|
||||||
@ -90,27 +116,29 @@ In order to support these different configurations the library migrated from a s
|
|||||||
to support pluggable transports and protocols.
|
to support pluggable transports and protocols.
|
||||||
The classes providing this functionality are:
|
The classes providing this functionality are:
|
||||||
|
|
||||||
- :class:`BaseProtocol <kasa.protocol.BaseProtocol>`
|
- {class}`BaseProtocol <kasa.protocol.BaseProtocol>`
|
||||||
- :class:`IotProtocol <kasa.iotprotocol.IotProtocol>`
|
- {class}`IotProtocol <kasa.iotprotocol.IotProtocol>`
|
||||||
- :class:`SmartProtocol <kasa.smartprotocol.SmartProtocol>`
|
- {class}`SmartProtocol <kasa.smartprotocol.SmartProtocol>`
|
||||||
|
|
||||||
- :class:`BaseTransport <kasa.protocol.BaseTransport>`
|
- {class}`BaseTransport <kasa.protocol.BaseTransport>`
|
||||||
- :class:`XorTransport <kasa.xortransport.XorTransport>`
|
- {class}`XorTransport <kasa.xortransport.XorTransport>`
|
||||||
- :class:`AesTransport <kasa.aestransport.AesTransport>`
|
- {class}`AesTransport <kasa.aestransport.AesTransport>`
|
||||||
- :class:`KlapTransport <kasa.klaptransport.KlapTransport>`
|
- {class}`KlapTransport <kasa.klaptransport.KlapTransport>`
|
||||||
- :class:`KlapTransportV2 <kasa.klaptransport.KlapTransportV2>`
|
- {class}`KlapTransportV2 <kasa.klaptransport.KlapTransportV2>`
|
||||||
|
|
||||||
Errors and Exceptions
|
(topics-errors-and-exceptions)=
|
||||||
*********************
|
## Errors and Exceptions
|
||||||
|
|
||||||
The base exception for all library errors is :class:`KasaException <kasa.exceptions.KasaException>`.
|
The base exception for all library errors is {class}`KasaException <kasa.exceptions.KasaException>`.
|
||||||
|
|
||||||
- If the device returns an error the library raises a :class:`DeviceError <kasa.exceptions.DeviceError>` which will usually contain an ``error_code`` with the detail.
|
- If the device returns an error the library raises a {class}`DeviceError <kasa.exceptions.DeviceError>` which will usually contain an ``error_code`` with the detail.
|
||||||
- If the device fails to authenticate the library raises an :class:`AuthenticationError <kasa.exceptions.AuthenticationError>` which is derived
|
- If the device fails to authenticate the library raises an {class}`AuthenticationError <kasa.exceptions.AuthenticationError>` which is derived
|
||||||
from :class:`DeviceError <kasa.exceptions.DeviceError>` and could contain an ``error_code`` depending on the type of failure.
|
from {class}`DeviceError <kasa.exceptions.DeviceError>` and could contain an ``error_code`` depending on the type of failure.
|
||||||
- If the library encounters and unsupported deviceit raises an :class:`UnsupportedDeviceError <kasa.exceptions.UnsupportedDeviceError>`.
|
- If the library encounters and unsupported deviceit raises an {class}`UnsupportedDeviceError <kasa.exceptions.UnsupportedDeviceError>`.
|
||||||
- If the device fails to respond within a timeout the library raises a :class:`TimeoutError <kasa.exceptions.TimeoutError>`.
|
- If the device fails to respond within a timeout the library raises a {class}`TimeoutError <kasa.exceptions.TimeoutError>`.
|
||||||
- All other failures will raise the base :class:`KasaException <kasa.exceptions.KasaException>` class.
|
- All other failures will raise the base {class}`KasaException <kasa.exceptions.KasaException>` class.
|
||||||
|
|
||||||
|
<!-- Commenting out this section keeps git seeing the change as a rename.
|
||||||
|
|
||||||
API documentation for modules and features
|
API documentation for modules and features
|
||||||
******************************************
|
******************************************
|
||||||
@ -200,3 +228,5 @@ API documentation for errors and exceptions
|
|||||||
.. autoclass:: kasa.exceptions.TimeoutError
|
.. autoclass:: kasa.exceptions.TimeoutError
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
|
-->
|
@ -1,4 +1,4 @@
|
|||||||
# Tutorial
|
# Getting started
|
||||||
|
|
||||||
```{eval-rst}
|
```{eval-rst}
|
||||||
.. automodule:: tutorial
|
.. automodule:: tutorial
|
||||||
|
@ -13,21 +13,24 @@ Most newer devices require your TP-Link cloud username and password, but this ca
|
|||||||
|
|
||||||
>>> from kasa import Device, Discover, Credentials
|
>>> from kasa import Device, Discover, Credentials
|
||||||
|
|
||||||
:func:`~kasa.Discover.discover` returns a list of devices on your network:
|
:func:`~kasa.Discover.discover` returns a dict[str,Device] of devices on your network:
|
||||||
|
|
||||||
>>> devices = await Discover.discover(credentials=Credentials("user@example.com", "great_password"))
|
>>> devices = await Discover.discover(credentials=Credentials("user@example.com", "great_password"))
|
||||||
>>> for dev in devices:
|
>>> for dev in devices.values():
|
||||||
>>> await dev.update()
|
>>> await dev.update()
|
||||||
>>> print(dev.host)
|
>>> print(dev.host)
|
||||||
127.0.0.1
|
127.0.0.1
|
||||||
127.0.0.2
|
127.0.0.2
|
||||||
|
127.0.0.3
|
||||||
|
127.0.0.4
|
||||||
|
127.0.0.5
|
||||||
|
|
||||||
:meth:`~kasa.Discover.discover_single` returns a single device by hostname:
|
:meth:`~kasa.Discover.discover_single` returns a single device by hostname:
|
||||||
|
|
||||||
>>> dev = await Discover.discover_single("127.0.0.1", credentials=Credentials("user@example.com", "great_password"))
|
>>> dev = await Discover.discover_single("127.0.0.3", credentials=Credentials("user@example.com", "great_password"))
|
||||||
>>> await dev.update()
|
>>> await dev.update()
|
||||||
>>> dev.alias
|
>>> dev.alias
|
||||||
Living Room
|
Living Room Bulb
|
||||||
>>> dev.model
|
>>> dev.model
|
||||||
L530
|
L530
|
||||||
>>> dev.rssi
|
>>> dev.rssi
|
||||||
|
@ -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
|
# ruff: noqa: FA100
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import asdict, dataclass, field, fields, is_dataclass
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
@ -198,45 +275,7 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
|
|||||||
|
|
||||||
|
|
||||||
class Discover:
|
class Discover:
|
||||||
"""Discover TPLink Smart Home devices.
|
"""Class for discovering 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))
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
DISCOVERY_PORT = 9999
|
DISCOVERY_PORT = 9999
|
||||||
|
|
||||||
|
@ -30,7 +30,8 @@ class Feature:
|
|||||||
#: Action triggers some action on device
|
#: Action triggers some action on device
|
||||||
Action = auto()
|
Action = auto()
|
||||||
#: Number defines a numeric setting
|
#: 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()
|
Number = auto()
|
||||||
#: Choice defines a setting with pre-defined values
|
#: Choice defines a setting with pre-defined values
|
||||||
Choice = auto()
|
Choice = auto()
|
||||||
|
@ -105,7 +105,7 @@ class IotDevice(Device):
|
|||||||
All devices provide several informational properties:
|
All devices provide several informational properties:
|
||||||
|
|
||||||
>>> dev.alias
|
>>> dev.alias
|
||||||
Kitchen
|
Bedroom Lamp Plug
|
||||||
>>> dev.model
|
>>> dev.model
|
||||||
HS110(EU)
|
HS110(EU)
|
||||||
>>> dev.rssi
|
>>> dev.rssi
|
||||||
|
@ -23,7 +23,7 @@ class IotLightStrip(IotBulb):
|
|||||||
>>> strip = IotLightStrip("127.0.0.1")
|
>>> strip = IotLightStrip("127.0.0.1")
|
||||||
>>> asyncio.run(strip.update())
|
>>> asyncio.run(strip.update())
|
||||||
>>> print(strip.alias)
|
>>> print(strip.alias)
|
||||||
KL430 pantry lightstrip
|
Bedroom Lightstrip
|
||||||
|
|
||||||
Getting the length of the strip:
|
Getting the length of the strip:
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ class IotPlug(IotDevice):
|
|||||||
>>> plug = IotPlug("127.0.0.1")
|
>>> plug = IotPlug("127.0.0.1")
|
||||||
>>> asyncio.run(plug.update())
|
>>> asyncio.run(plug.update())
|
||||||
>>> plug.alias
|
>>> plug.alias
|
||||||
Kitchen
|
Bedroom Lamp Plug
|
||||||
|
|
||||||
Setting the LED state:
|
Setting the LED state:
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ class IotStrip(IotDevice):
|
|||||||
>>> strip = IotStrip("127.0.0.1")
|
>>> strip = IotStrip("127.0.0.1")
|
||||||
>>> asyncio.run(strip.update())
|
>>> asyncio.run(strip.update())
|
||||||
>>> strip.alias
|
>>> strip.alias
|
||||||
TP-LINK_Power Strip_CF69
|
Bedroom Power Strip
|
||||||
|
|
||||||
All methods act on the whole 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)
|
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)
|
@pytest.fixture(params=filter_fixtures("main devices"), ids=idgenerator)
|
||||||
async def dev(request) -> AsyncGenerator[Device, None]:
|
async def dev(request) -> AsyncGenerator[Device, None]:
|
||||||
"""Device fixture.
|
"""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(
|
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(
|
return pytest.mark.parametrize(
|
||||||
"discovery_mock",
|
"discovery_mock",
|
||||||
@ -65,10 +70,14 @@ new_discovery = parametrize_discovery(
|
|||||||
params=filter_fixtures("discoverable", protocol_filter={"SMART", "IOT"}),
|
params=filter_fixtures("discoverable", protocol_filter={"SMART", "IOT"}),
|
||||||
ids=idgenerator,
|
ids=idgenerator,
|
||||||
)
|
)
|
||||||
def discovery_mock(request, mocker):
|
async def discovery_mock(request, mocker):
|
||||||
"""Mock discovery and patch protocol queries to use Fake protocols."""
|
"""Mock discovery and patch protocol queries to use Fake protocols."""
|
||||||
fixture_info: FixtureInfo = request.param
|
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
|
@dataclass
|
||||||
class _DiscoveryMock:
|
class _DiscoveryMock:
|
||||||
@ -79,6 +88,7 @@ def discovery_mock(request, mocker):
|
|||||||
query_data: dict
|
query_data: dict
|
||||||
device_type: str
|
device_type: str
|
||||||
encrypt_type: str
|
encrypt_type: str
|
||||||
|
_datagram: bytes
|
||||||
login_version: int | None = None
|
login_version: int | None = None
|
||||||
port_override: int | None = None
|
port_override: int | None = None
|
||||||
|
|
||||||
@ -94,13 +104,14 @@ def discovery_mock(request, mocker):
|
|||||||
+ json_dumps(discovery_data).encode()
|
+ json_dumps(discovery_data).encode()
|
||||||
)
|
)
|
||||||
dm = _DiscoveryMock(
|
dm = _DiscoveryMock(
|
||||||
"127.0.0.123",
|
ip,
|
||||||
80,
|
80,
|
||||||
20002,
|
20002,
|
||||||
discovery_data,
|
discovery_data,
|
||||||
fixture_data,
|
fixture_data,
|
||||||
device_type,
|
device_type,
|
||||||
encrypt_type,
|
encrypt_type,
|
||||||
|
datagram,
|
||||||
login_version,
|
login_version,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -111,45 +122,87 @@ def discovery_mock(request, mocker):
|
|||||||
login_version = None
|
login_version = None
|
||||||
datagram = XorEncryption.encrypt(json_dumps(discovery_data))[4:]
|
datagram = XorEncryption.encrypt(json_dumps(discovery_data))[4:]
|
||||||
dm = _DiscoveryMock(
|
dm = _DiscoveryMock(
|
||||||
"127.0.0.123",
|
ip,
|
||||||
9999,
|
9999,
|
||||||
9999,
|
9999,
|
||||||
discovery_data,
|
discovery_data,
|
||||||
fixture_data,
|
fixture_data,
|
||||||
device_type,
|
device_type,
|
||||||
encrypt_type,
|
encrypt_type,
|
||||||
|
datagram,
|
||||||
login_version,
|
login_version,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def mock_discover(self):
|
return dm
|
||||||
port = (
|
|
||||||
dm.port_override
|
|
||||||
if dm.port_override and dm.discovery_port != 20002
|
|
||||||
else dm.discovery_port
|
|
||||||
)
|
|
||||||
self.datagram_received(
|
|
||||||
datagram,
|
|
||||||
(dm.ip, port),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
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("kasa.discover._DiscoverProtocol.do_discover", mock_discover)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"socket.getaddrinfo",
|
"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,
|
||||||
)
|
)
|
||||||
|
# Only return the first discovery mock to be used for testing discover single
|
||||||
if "SMART" in fixture_info.protocol:
|
return discovery_mocks[first_ip]
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(
|
@pytest.fixture(
|
||||||
|
@ -3,7 +3,7 @@ import logging
|
|||||||
|
|
||||||
from ..deviceconfig import DeviceConfig
|
from ..deviceconfig import DeviceConfig
|
||||||
from ..iotprotocol import IotProtocol
|
from ..iotprotocol import IotProtocol
|
||||||
from ..xortransport import XorTransport
|
from ..protocol import BaseTransport
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -178,17 +178,26 @@ MOTION_MODULE = {
|
|||||||
|
|
||||||
|
|
||||||
class FakeIotProtocol(IotProtocol):
|
class FakeIotProtocol(IotProtocol):
|
||||||
def __init__(self, info):
|
def __init__(self, info, fixture_name=None):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
transport=XorTransport(
|
transport=FakeIotTransport(info, fixture_name),
|
||||||
config=DeviceConfig("127.0.0.123"),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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)
|
info = copy.deepcopy(info)
|
||||||
self.discovery_data = info
|
self.discovery_data = info
|
||||||
|
self.fixture_name = fixture_name
|
||||||
self.writer = None
|
self.writer = None
|
||||||
self.reader = None
|
self.reader = None
|
||||||
proto = copy.deepcopy(FakeIotProtocol.baseproto)
|
proto = copy.deepcopy(FakeIotTransport.baseproto)
|
||||||
|
|
||||||
for target in info:
|
for target in info:
|
||||||
# print("target %s" % target)
|
# print("target %s" % target)
|
||||||
@ -220,6 +229,14 @@ class FakeIotProtocol(IotProtocol):
|
|||||||
|
|
||||||
self.proto = proto
|
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):
|
def set_alias(self, x, child_ids=None):
|
||||||
if child_ids is None:
|
if child_ids is None:
|
||||||
child_ids = []
|
child_ids = []
|
||||||
@ -367,7 +384,7 @@ class FakeIotProtocol(IotProtocol):
|
|||||||
"smartlife.iot.common.cloud": CLOUD_MODULE,
|
"smartlife.iot.common.cloud": CLOUD_MODULE,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def query(self, request, port=9999):
|
async def send(self, request, port=9999):
|
||||||
proto = self.proto
|
proto = self.proto
|
||||||
|
|
||||||
# collect child ids from context
|
# collect child ids from context
|
||||||
@ -414,3 +431,9 @@ class FakeIotProtocol(IotProtocol):
|
|||||||
response.update(get_response_for_module(target))
|
response.update(get_response_for_module(target))
|
||||||
|
|
||||||
return copy.deepcopy(response)
|
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": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "schedule",
|
"active_mode": "schedule",
|
||||||
"alias": "Kitchen",
|
"alias": "Bedroom Lamp Plug",
|
||||||
"dev_name": "Wi-Fi Smart Plug With Energy Monitoring",
|
"dev_name": "Wi-Fi Smart Plug With Energy Monitoring",
|
||||||
"deviceId": "0000000000000000000000000000000000000000",
|
"deviceId": "0000000000000000000000000000000000000000",
|
||||||
"err_code": 0,
|
"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": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "Living room left dimmer",
|
"alias": "Living Room Dimmer Switch",
|
||||||
"brightness": 25,
|
"brightness": 25,
|
||||||
"dev_name": "Smart Wi-Fi Dimmer",
|
"dev_name": "Smart Wi-Fi Dimmer",
|
||||||
"deviceId": "000000000000000000000000000000000000000",
|
"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": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "Living Room Lights",
|
"alias": "Living Room Dimmer Switch",
|
||||||
"brightness": 100,
|
"brightness": 100,
|
||||||
"dev_name": "Wi-Fi Smart Dimmer",
|
"dev_name": "Wi-Fi Smart Dimmer",
|
||||||
"deviceId": "0000000000000000000000000000000000000000",
|
"deviceId": "0000000000000000000000000000000000000000",
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
"system": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"active_mode": "none",
|
"active_mode": "none",
|
||||||
"alias": "KL430 pantry lightstrip",
|
"alias": "Bedroom Lightstrip",
|
||||||
"ctrl_protocols": {
|
"ctrl_protocols": {
|
||||||
"name": "Linkie",
|
"name": "Linkie",
|
||||||
"version": "1.0"
|
"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": {
|
"system": {
|
||||||
"get_sysinfo": {
|
"get_sysinfo": {
|
||||||
"alias": "TP-LINK_Power Strip_CF69",
|
"alias": "Bedroom Power Strip",
|
||||||
"child_num": 3,
|
"child_num": 3,
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
|
@ -175,7 +175,7 @@
|
|||||||
"longitude": 0,
|
"longitude": 0,
|
||||||
"mac": "5C-E9-31-00-00-00",
|
"mac": "5C-E9-31-00-00-00",
|
||||||
"model": "L530",
|
"model": "L530",
|
||||||
"nickname": "TGl2aW5nIFJvb20=",
|
"nickname": "TGl2aW5nIFJvb20gQnVsYg==",
|
||||||
"oem_id": "00000000000000000000000000000000",
|
"oem_id": "00000000000000000000000000000000",
|
||||||
"overheated": false,
|
"overheated": false,
|
||||||
"region": "Europe/Berlin",
|
"region": "Europe/Berlin",
|
||||||
|
@ -107,7 +107,6 @@ async def test_type_unknown():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("custom_port", [123, None])
|
@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):
|
async def test_discover_single(discovery_mock, custom_port, mocker):
|
||||||
"""Make sure that discover_single returns an initialized SmartDevice instance."""
|
"""Make sure that discover_single returns an initialized SmartDevice instance."""
|
||||||
host = "127.0.0.1"
|
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
|
discovery_mock.port_override = custom_port
|
||||||
|
|
||||||
device_class = Discover._get_device_class(discovery_mock.discovery_data)
|
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(
|
x = await Discover.discover_single(
|
||||||
host, port=custom_port, credentials=Credentials()
|
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 issubclass(x.__class__, Device)
|
||||||
assert x._discovery_info is not None
|
assert x._discovery_info is not None
|
||||||
assert x.port == custom_port or x.port == discovery_mock.default_port
|
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
|
assert update_mock.call_count == 0
|
||||||
if discovery_mock.default_port == 80:
|
if discovery_mock.default_port == 80:
|
||||||
assert x.alias is None
|
assert x.alias is None
|
||||||
|
@ -3,8 +3,11 @@ import asyncio
|
|||||||
import pytest
|
import pytest
|
||||||
import xdoctest
|
import xdoctest
|
||||||
|
|
||||||
from kasa import Discover
|
from kasa.tests.conftest import (
|
||||||
from kasa.tests.conftest import get_device_for_fixture_protocol
|
get_device_for_fixture_protocol,
|
||||||
|
get_fixture_info,
|
||||||
|
patch_discovery,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_bulb_examples(mocker):
|
def test_bulb_examples(mocker):
|
||||||
@ -62,34 +65,39 @@ def test_lightstrip_examples(mocker):
|
|||||||
assert not res["failed"]
|
assert not res["failed"]
|
||||||
|
|
||||||
|
|
||||||
def test_discovery_examples(mocker):
|
def test_discovery_examples(readmes_mock):
|
||||||
"""Test discovery examples."""
|
"""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")
|
res = xdoctest.doctest_module("kasa.discover", "all")
|
||||||
|
assert res["n_passed"] > 0
|
||||||
assert not res["failed"]
|
assert not res["failed"]
|
||||||
|
|
||||||
|
|
||||||
def test_tutorial_examples(mocker, top_level_await):
|
def test_deviceconfig_examples(readmes_mock):
|
||||||
"""Test discovery examples."""
|
"""Test discovery examples."""
|
||||||
a = asyncio.run(
|
res = xdoctest.doctest_module("kasa.deviceconfig", "all")
|
||||||
get_device_for_fixture_protocol("L530E(EU)_3.0_1.1.6.json", "SMART")
|
assert res["n_passed"] > 0
|
||||||
)
|
|
||||||
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")
|
|
||||||
assert not res["failed"]
|
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
|
@pytest.fixture
|
||||||
def top_level_await(mocker):
|
def top_level_await(mocker):
|
||||||
"""Fixture to enable top level awaits in doctests.
|
"""Fixture to enable top level awaits in doctests.
|
||||||
@ -99,19 +107,26 @@ def top_level_await(mocker):
|
|||||||
"""
|
"""
|
||||||
import ast
|
import ast
|
||||||
from inspect import CO_COROUTINE
|
from inspect import CO_COROUTINE
|
||||||
|
from types import CodeType
|
||||||
|
|
||||||
orig_exec = exec
|
orig_exec = exec
|
||||||
orig_eval = eval
|
orig_eval = eval
|
||||||
orig_compile = compile
|
orig_compile = compile
|
||||||
|
|
||||||
def patch_exec(source, globals=None, locals=None, /, **kwargs):
|
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))
|
asyncio.run(orig_eval(source, globals, locals))
|
||||||
else:
|
else:
|
||||||
orig_exec(source, globals, locals, **kwargs)
|
orig_exec(source, globals, locals, **kwargs)
|
||||||
|
|
||||||
def patch_eval(source, globals=None, locals=None, /, **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))
|
return asyncio.run(orig_eval(source, globals, locals, **kwargs))
|
||||||
else:
|
else:
|
||||||
return orig_eval(source, globals, locals, **kwargs)
|
return orig_eval(source, globals, locals, **kwargs)
|
||||||
|
Loading…
Reference in New Issue
Block a user