Add tutorial doctest module and enable top level await (#919)

Add a tutorial module with examples that can be tested with `doctest`.

In order to simplify the examples they can be run with doctest allowing
top level await statements by adding a fixture to patch the builtins
that xdoctest uses to test code.

---------

Co-authored-by: Teemu R. <tpr@iki.fi>
This commit is contained in:
Steven B
2024-05-16 17:13:44 +01:00
committed by GitHub
parent a2e8d2c4e8
commit 3490a1ef84
17 changed files with 228 additions and 42 deletions

View File

@@ -10,9 +10,10 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
import os
import sys
sys.path.insert(0, os.path.abspath("..")) # Will find modules in the docs parent
# -- Project information -----------------------------------------------------

View File

@@ -22,13 +22,13 @@ Use :func:`~kasa.Discover.discover` to perform udp-based broadcast discovery on
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
:meth:`~kasa.SmartDevice.connect()`.
:meth:`~kasa.Device.connect()`.
The :meth:`~kasa.SmartDevice.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`.
Simply serialize the :attr:`~kasa.SmartDevice.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 pass it into :meth:`~kasa.SmartDevice.connect()`.
and then pass it into :meth:`~kasa.Device.connect()`.
.. _update_cycle:
@@ -36,7 +36,7 @@ and then pass it into :meth:`~kasa.SmartDevice.connect()`.
Update Cycle
************
When :meth:`~kasa.SmartDevice.update()` is called,
When :meth:`~kasa.Device.update()` is called,
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.
@@ -45,7 +45,7 @@ All properties defined both in the device class and in the module classes follow
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.
This can be done using the :attr:`~kasa.SmartDevice.internal_state` property.
This can be done using the :attr:`~kasa.Device.internal_state` property.
.. _modules:
@@ -53,15 +53,15 @@ This can be done using the :attr:`~kasa.SmartDevice.internal_state` property.
Modules
*******
The functionality provided by all :class:`~kasa.SmartDevice` 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,
you can also access individual modules through :attr:`kasa.SmartDevice.modules`.
You can get the list of supported modules for a given device instance using :attr:`~kasa.SmartDevice.supported_modules`.
You can get the list of supported modules for a given device instance using :attr:`~kasa.Device.supported_modules`.
.. note::
If you only need some module-specific information,
you can call the wanted method on the module to avoid using :meth:`~kasa.SmartDevice.update`.
you can call the wanted method on the module to avoid using :meth:`~kasa.Device.update`.
Protocols and Transports
************************
@@ -112,10 +112,22 @@ The base exception for all library errors is :class:`KasaException <kasa.excepti
- 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.
API documentation for modules
*****************************
API documentation for modules and features
******************************************
.. automodule:: kasa.modules
.. autoclass:: kasa.Module
:noindex:
:members:
:inherited-members:
:undoc-members:
.. automodule:: kasa.interfaces
:noindex:
:members:
:inherited-members:
:undoc-members:
.. autoclass:: kasa.Feature
:noindex:
:members:
:inherited-members:

View File

@@ -6,12 +6,12 @@ Common API
.. contents:: Contents
:local:
SmartDevice class
*****************
Device class
************
The basic functionalities of all supported devices are accessible using the common :class:`SmartDevice` base class.
The basic functionalities of all supported devices are accessible using the common :class:`Device` base class.
The property accesses use the data obtained before by awaiting :func:`SmartDevice.update()`.
The property accesses use the data obtained before by awaiting :func:`Device.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.
See :ref:`library_design` for more detailed information.
@@ -20,7 +20,7 @@ See :ref:`library_design` for more detailed information.
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.
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).
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).
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.
@@ -103,10 +103,10 @@ Currently there are three known types of encryption for TP-Link devices and two
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:`SmartDevice.connect()`.
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:`SmartDevice.config` post discovery and then re-used.
alternatively the config can be retrieved from :attr:`Device.config` post discovery and then re-used.
Energy Consumption and Usage Statistics
***************************************
@@ -141,7 +141,7 @@ You can access this information using through the usage module (:class:`kasa.mod
API documentation
*****************
.. autoclass:: SmartDevice
.. autoclass:: Device
:members:
:undoc-members:

View File

@@ -13,7 +13,7 @@ Discovery works by sending broadcast UDP packets to two known TP-link discovery
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:`SmartDevice.update() <kasa.SmartDevice.update()>` to get full device information.
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.

View File

@@ -7,8 +7,9 @@
Home <self>
cli
tutorial
discover
smartdevice
device
design
contribute
smartbulb

View File

@@ -67,13 +67,13 @@ API documentation
:members:
:undoc-members:
.. autoclass:: kasa.smartbulb.BehaviorMode
.. autoclass:: kasa.iot.iotbulb.BehaviorMode
:members:
.. autoclass:: kasa.TurnOnBehaviors
.. autoclass:: kasa.iot.iotbulb.TurnOnBehaviors
:members:
.. autoclass:: kasa.TurnOnBehavior
.. autoclass:: kasa.iot.iotbulb.TurnOnBehavior
:undoc-members:
:members:

8
docs/source/tutorial.md Normal file
View File

@@ -0,0 +1,8 @@
# Tutorial
```{eval-rst}
.. automodule:: tutorial
:members:
:inherited-members:
:undoc-members:
```

103
docs/tutorial.py Normal file
View File

@@ -0,0 +1,103 @@
# ruff: noqa
"""
The kasa library is fully async and methods that perform IO need to be run inside an async couroutine.
These examples assume you are following the tutorial inside `asyncio REPL` (python -m asyncio) or the code
is running inside an async function (`async def`).
The main entry point for the API is :meth:`~kasa.Discover.discover` and
:meth:`~kasa.Discover.discover_single` which return Device objects.
Most newer devices require your TP-Link cloud username and password, but this can be omitted for older devices.
>>> from kasa import Device, Discover, Credentials
:func:`~kasa.Discover.discover` returns a list of devices on your network:
>>> devices = await Discover.discover(credentials=Credentials("user@example.com", "great_password"))
>>> for dev in devices:
>>> await dev.update()
>>> print(dev.host)
127.0.0.1
127.0.0.2
: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"))
>>> await dev.update()
>>> dev.alias
Living Room
>>> dev.model
L530
>>> dev.rssi
-52
>>> dev.mac
5C:E9:31:00:00:00
You can update devices by calling different methods (e.g., ``set_``-prefixed ones).
Note, that these do not update the internal state, but you need to call :meth:`~kasa.Device.update()` to query the device again.
back to the device.
>>> await dev.set_alias("Dining Room")
>>> await dev.update()
>>> dev.alias
Dining Room
Different groups of functionality are supported by modules which you can access via :attr:`~kasa.Device.modules` with a typed
key from :class:`~kasa.Module`.
Modules will only be available on the device if they are supported but some individual features of a module may not be available for your device.
You can check the availability using ``is_``-prefixed properties like `is_color`.
>>> from kasa import Module
>>> Module.Light in dev.modules
True
>>> light = dev.modules[Module.Light]
>>> light.brightness
100
>>> await light.set_brightness(50)
>>> await dev.update()
>>> light.brightness
50
>>> light.is_color
True
>>> if light.is_color:
>>> print(light.hsv)
HSV(hue=0, saturation=100, value=50)
You can test if a module is supported by using `get` to access it.
>>> if effect := dev.modules.get(Module.LightEffect):
>>> print(effect.effect)
>>> print(effect.effect_list)
>>> if effect := dev.modules.get(Module.LightEffect):
>>> await effect.set_effect("Party")
>>> await dev.update()
>>> print(effect.effect)
Off
['Off', 'Party', 'Relax']
Party
Individual pieces of functionality are also exposed via features which you can access via :attr:`~kasa.Device.features` and will only be present if they are supported.
Features are similar to modules in that they provide functionality that may or may not be present.
Whereas modules group functionality into a common interface, features expose a single function that may or may not be part of a module.
The advantage of features is that they have a simple common interface of `id`, `name`, `value` and `set_value` so no need to learn the module API.
They are useful if you want write code that dynamically adapts as new features are added to the API.
>>> if auto_update := dev.features.get("auto_update_enabled"):
>>> print(auto_update.value)
False
>>> if auto_update:
>>> await auto_update.set_value(True)
>>> await dev.update()
>>> print(auto_update.value)
True
>>> for feat in dev.features.values():
>>> print(f"{feat.name}: {feat.value}")
Device ID: 0000000000000000000000000000000000000000\nState: True\nSignal Level: 2\nRSSI: -52\nSSID: #MASKED_SSID#\nOverheated: False\nBrightness: 50\nCloud connection: True\nHSV: HSV(hue=0, saturation=100, value=50)\nColor temperature: 2700\nAuto update enabled: True\nUpdate available: False\nCurrent firmware version: 1.1.6 Build 240130 Rel.173828\nAvailable firmware version: 1.1.6 Build 240130 Rel.173828\nLight effect: Party\nSmooth transition on: 2\nSmooth transition off: 2\nTime: 2024-02-23 02:40:15+01:00
"""