mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 11:13:34 +00:00
Simplify API documentation by using doctests (#73)
* Add doctests to SmartBulb * Add SmartDevice doctests, cleanup README.md * add doctests for smartplug and smartstrip * add discover doctests * Fix bulb mock * add smartdimmer doctests * add sphinx-generated docs, cleanup readme a bit * remove sphinx-click as it does not work with asyncclick * in preparation for rtd hooking, move doc deps to be separate from dev deps * pytestmark needs to be applied separately for each and every file, this fixes the tests * use pathlib for resolving relative paths * Skip discovery doctest on python3.7 The code is just fine, but some reason the mocking behaves differently between 3.7 and 3.8. The latter seems to accept a discrete object for asyncio.run where the former expects a coroutine..
This commit is contained in:
parent
99e0c4a418
commit
f9a987ca18
3
.flake8
3
.flake8
@ -2,6 +2,7 @@
|
||||
exclude = .git,.tox,__pycache__,kasa/tests/newfakes.py,kasa/tests/test_fixtures.py
|
||||
max-line-length = 88
|
||||
per-file-ignores =
|
||||
kasa/tests/*.py:D100,D101,D102,D103,D104
|
||||
kasa/tests/*.py:D100,D101,D102,D103,D104,F401
|
||||
docs/source/conf.py:D100,D103
|
||||
ignore = D105, D107, E203, E501, W503
|
||||
max-complexity = 18
|
||||
|
127
README.md
127
README.md
@ -37,30 +37,10 @@ This project is a maintainer-made fork of [pyHS100](https://github.com/GadgetRea
|
||||
**Contributions (be it adding missing features, fixing bugs or improving documentation) are more than welcome, feel free to submit pull requests! See below for instructions for setting up a development environment.**
|
||||
|
||||
|
||||
# Usage
|
||||
|
||||
The package is shipped with a console tool named kasa, please refer to ```kasa --help``` for detailed usage.
|
||||
The device to which the commands are sent is chosen by `KASA_HOST` environment variable or passing `--host <address>` as an option.
|
||||
To see what is being sent to and received from the device, specify option `--debug`.
|
||||
|
||||
To avoid discovering the devices when executing commands its type can be passed by specifying either `--plug` or `--bulb`,
|
||||
if no type is given its type will be discovered automatically with a small delay.
|
||||
Some commands (such as reading energy meter values and setting color of bulbs) additional parameters are required,
|
||||
which you can find by adding `--help` after the command, e.g. `kasa emeter --help` or `kasa hsv --help`.
|
||||
|
||||
If no command is given, the `state` command will be executed to query the device state.
|
||||
|
||||
## Initial Setup
|
||||
|
||||
You can provision your device without any extra apps by using the `kasa wifi` command:
|
||||
1. If the device is unprovisioned, connect to its open network
|
||||
2. Use `kasa discover` (or check the routes) to locate the IP address of the device (likely 192.168.0.1)
|
||||
3. Scan for available networks using `kasa wifi scan`
|
||||
4. Join/change the network using `kasa wifi join` command, see `--help` for details.
|
||||
## Discovering devices
|
||||
|
||||
The devices can be discovered either by using `kasa discover` or by calling `kasa` without any parameters.
|
||||
In both cases supported devices are discovered from the same broadcast domain, and their current state will be queried and printed out.
|
||||
|
||||
```
|
||||
$ kasa
|
||||
@ -108,112 +88,7 @@ The commands are straightforward, so feel free to check `--help` for instruction
|
||||
|
||||
# Library usage
|
||||
|
||||
The property accesses use the data obtained before by awaiting `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.
|
||||
|
||||
Methods changing the state of the device do not invalidate the cache (i.e., there is no implicit `update()`).
|
||||
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.
|
||||
|
||||
Errors are raised as `SmartDeviceException` instances for the library user to handle.
|
||||
|
||||
## Discovering devices
|
||||
|
||||
`Discover.discover()` can be used to discover supported devices in the local network.
|
||||
The return value is a dictionary keyed with the IP address and the value holds a ready-to-use instance of the detected device type.
|
||||
|
||||
Example:
|
||||
```python
|
||||
import asyncio
|
||||
from kasa import Discover
|
||||
|
||||
devices = asyncio.run(Discover.discover())
|
||||
for addr, dev in devices.items():
|
||||
asyncio.run(dev.update())
|
||||
print(f"{addr} >> {dev}")
|
||||
```
|
||||
```
|
||||
$ python example.py
|
||||
<SmartPlug at 192.168.XXX.XXX (My Smart Plug), is_on: True - dev specific: {'LED state': True, 'On since': datetime.datetime(2017, 3, 26, 18, 29, 17, 52073)}>
|
||||
```
|
||||
|
||||
## Querying basic information
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from kasa import SmartPlug
|
||||
from pprint import pformat as pf
|
||||
|
||||
plug = SmartPlug("192.168.XXX.XXX")
|
||||
asyncio.run(plug.update())
|
||||
print("Hardware: %s" % pf(plug.hw_info))
|
||||
print("Full sysinfo: %s" % pf(plug.sys_info))
|
||||
```
|
||||
|
||||
The rest of the examples assume that you have initialized an instance.
|
||||
|
||||
## State & switching
|
||||
|
||||
Devices can be turned on and off by either calling appropriate methods on the device object.
|
||||
|
||||
```python
|
||||
print("Current state: %s" % plug.is_on)
|
||||
await plug.turn_off()
|
||||
await plug.turn_on()
|
||||
```
|
||||
|
||||
## Getting emeter status (if applicable)
|
||||
The `update()` call will automatically fetch the following emeter information:
|
||||
* Current consumption (accessed through `emeter_realtime` property)
|
||||
* Today's consumption (`emeter_today`)
|
||||
* This month's consumption (`emeter_this_month`)
|
||||
|
||||
You can also request this information separately:
|
||||
|
||||
```python
|
||||
print("Current consumption: %s" % await plug.get_emeter_realtime())
|
||||
print("Per day: %s" % await plug.get_emeter_daily(year=2016, month=12))
|
||||
print("Per month: %s" % await plug.get_emeter_monthly(year=2016))
|
||||
```
|
||||
|
||||
## Bulb and dimmer-specific APIs
|
||||
|
||||
The bulb API is likewise straightforward, so please refer to its API documentation.
|
||||
Information about supported features can be queried by using properties prefixed with `is_`, e.g. `is_dimmable`.
|
||||
|
||||
### Setting the brightness
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from kasa import SmartBulb
|
||||
|
||||
bulb = SmartBulb("192.168.1.123")
|
||||
asyncio.run(bulb.update())
|
||||
|
||||
if bulb.is_dimmable:
|
||||
asyncio.run(bulb.set_brightness(100))
|
||||
asyncio.run(bulb.update())
|
||||
print(bulb.brightness)
|
||||
```
|
||||
|
||||
### Setting the color temperature
|
||||
```python
|
||||
if bulb.is_variable_color_temp:
|
||||
await bulb.set_color_temp(3000)
|
||||
await bulb.update()
|
||||
print(bulb.color_temp)
|
||||
```
|
||||
|
||||
### Setting the color
|
||||
|
||||
Hue is given in degrees (0-360) and saturation and value in percentage.
|
||||
|
||||
```python
|
||||
if bulb.is_color:
|
||||
await bulb.set_hsv(180, 100, 100) # set to cyan
|
||||
await bulb.update()
|
||||
print(bulb.hsv)
|
||||
```
|
||||
You can find several code examples in [the API documentation](broken link).
|
||||
|
||||
## Contributing
|
||||
|
||||
|
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = source
|
||||
BUILDDIR = build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
35
docs/make.bat
Normal file
35
docs/make.bat
Normal file
@ -0,0 +1,35 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=source
|
||||
set BUILDDIR=build
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
|
||||
:end
|
||||
popd
|
65
docs/source/_static/copybutton.js
Normal file
65
docs/source/_static/copybutton.js
Normal file
@ -0,0 +1,65 @@
|
||||
// Copyright 2014 PSF. Licensed under the PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||
// File originates from the cpython source found in Doc/tools/sphinxext/static/copybutton.js
|
||||
|
||||
$(document).ready(function() {
|
||||
/* Add a [>>>] button on the top-right corner of code samples to hide
|
||||
* the >>> and ... prompts and the output and thus make the code
|
||||
* copyable. */
|
||||
var div = $('.highlight-python .highlight,' +
|
||||
'.highlight-default .highlight,' +
|
||||
'.highlight-python3 .highlight')
|
||||
var pre = div.find('pre');
|
||||
|
||||
// get the styles from the current theme
|
||||
pre.parent().parent().css('position', 'relative');
|
||||
var hide_text = 'Hide the prompts and output';
|
||||
var show_text = 'Show the prompts and output';
|
||||
var border_width = pre.css('border-top-width');
|
||||
var border_style = pre.css('border-top-style');
|
||||
var border_color = pre.css('border-top-color');
|
||||
var button_styles = {
|
||||
'cursor':'pointer', 'position': 'absolute', 'top': '0', 'right': '0',
|
||||
'border-color': border_color, 'border-style': border_style,
|
||||
'border-width': border_width, 'color': border_color, 'text-size': '75%',
|
||||
'font-family': 'monospace', 'padding-left': '0.2em', 'padding-right': '0.2em',
|
||||
'border-radius': '0 3px 0 0'
|
||||
}
|
||||
|
||||
// create and add the button to all the code blocks that contain >>>
|
||||
div.each(function(index) {
|
||||
var jthis = $(this);
|
||||
if (jthis.find('.gp').length > 0) {
|
||||
var button = $('<span class="copybutton">>>></span>');
|
||||
button.css(button_styles)
|
||||
button.attr('title', hide_text);
|
||||
button.data('hidden', 'false');
|
||||
jthis.prepend(button);
|
||||
}
|
||||
// tracebacks (.gt) contain bare text elements that need to be
|
||||
// wrapped in a span to work with .nextUntil() (see later)
|
||||
jthis.find('pre:has(.gt)').contents().filter(function() {
|
||||
return ((this.nodeType == 3) && (this.data.trim().length > 0));
|
||||
}).wrap('<span>');
|
||||
});
|
||||
|
||||
// define the behavior of the button when it's clicked
|
||||
$('.copybutton').click(function(e){
|
||||
e.preventDefault();
|
||||
var button = $(this);
|
||||
if (button.data('hidden') === 'false') {
|
||||
// hide the code output
|
||||
button.parent().find('.go, .gp, .gt').hide();
|
||||
button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'hidden');
|
||||
button.css('text-decoration', 'line-through');
|
||||
button.attr('title', show_text);
|
||||
button.data('hidden', 'true');
|
||||
} else {
|
||||
// show the code output
|
||||
button.parent().find('.go, .gp, .gt').show();
|
||||
button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'visible');
|
||||
button.css('text-decoration', 'none');
|
||||
button.attr('title', hide_text);
|
||||
button.data('hidden', 'false');
|
||||
}
|
||||
});
|
||||
});
|
28
docs/source/cli.rst
Normal file
28
docs/source/cli.rst
Normal file
@ -0,0 +1,28 @@
|
||||
Command-line usage
|
||||
==================
|
||||
|
||||
The package is shipped with a console tool named kasa, please refer to ``kasa --help`` for detailed usage.
|
||||
The device to which the commands are sent is chosen by `KASA_HOST` environment variable or passing ``--host <address>`` as an option.
|
||||
To see what is being sent to and received from the device, specify option ``--debug``.
|
||||
|
||||
To avoid discovering the devices when executing commands its type can be passed by specifying either ``--plug`` or ``--bulb``,
|
||||
if no type is given its type will be discovered automatically with a small delay.
|
||||
Some commands (such as reading energy meter values and setting color of bulbs) additional parameters are required,
|
||||
which you can find by adding ``--help`` after the command, e.g. ``kasa emeter --help`` or ``kasa hsv --help``.
|
||||
|
||||
If no command is given, the ``state`` command will be executed to query the device state.
|
||||
|
||||
Provisioning
|
||||
~~~~~~~~~~~~
|
||||
|
||||
You can provision your device without any extra apps by using the ``kasa wifi`` command:
|
||||
|
||||
1. If the device is unprovisioned, connect to its open network
|
||||
2. Use ``kasa discover`` (or check the routes) to locate the IP address of the device (likely 192.168.0.1)
|
||||
3. Scan for available networks using ``kasa wifi scan``
|
||||
4. Join/change the network using ``kasa wifi join`` command, see ``--help`` for details.
|
||||
|
||||
``kasa --help``
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. program-output:: kasa --help
|
70
docs/source/conf.py
Normal file
70
docs/source/conf.py
Normal file
@ -0,0 +1,70 @@
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file only contains a selection of the most common options. For a full
|
||||
# list see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# 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('.'))
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = "python-kasa"
|
||||
copyright = "2020, python-kasa developers"
|
||||
author = "python-kasa developers"
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.coverage",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinxcontrib.programoutput",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = [] # type: ignore
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ["_static"]
|
||||
|
||||
|
||||
def setup(app):
|
||||
# add copybutton to hide the >>> prompts, see https://github.com/readthedocs/sphinx_rtd_theme/issues/167
|
||||
app.add_js_file("copybutton.js")
|
||||
|
||||
# see https://github.com/readthedocs/recommonmark/issues/191#issuecomment-622369992
|
||||
from m2r import MdInclude
|
||||
|
||||
app.add_config_value("no_underscore_emphasis", False, "env")
|
||||
app.add_config_value("m2r_parse_relative_links", False, "env")
|
||||
app.add_config_value("m2r_anonymous_references", False, "env")
|
||||
app.add_config_value("m2r_disable_inline_math", False, "env")
|
||||
app.add_directive("mdinclude", MdInclude)
|
17
docs/source/discover.rst
Normal file
17
docs/source/discover.rst
Normal file
@ -0,0 +1,17 @@
|
||||
Discovering devices
|
||||
===================
|
||||
|
||||
.. code-block::
|
||||
|
||||
import asyncio
|
||||
from kasa import Discover
|
||||
|
||||
devices = asyncio.run(Discover.discover())
|
||||
for addr, dev in devices.items():
|
||||
asyncio.run(dev.update())
|
||||
print(f"{addr} >> {dev}")
|
||||
|
||||
|
||||
.. autoclass:: kasa.Discover
|
||||
:members:
|
||||
:undoc-members:
|
17
docs/source/index.rst
Normal file
17
docs/source/index.rst
Normal file
@ -0,0 +1,17 @@
|
||||
python-kasa documentation
|
||||
=========================
|
||||
|
||||
.. mdinclude:: ../../README.md
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
Home <self>
|
||||
cli
|
||||
discover
|
||||
smartdevice
|
||||
smartbulb
|
||||
smartplug
|
||||
smartdimmer
|
||||
smartstrip
|
6
docs/source/smartbulb.rst
Normal file
6
docs/source/smartbulb.rst
Normal file
@ -0,0 +1,6 @@
|
||||
Bulbs
|
||||
===========
|
||||
|
||||
.. autoclass:: kasa.SmartBulb
|
||||
:members:
|
||||
:undoc-members:
|
18
docs/source/smartdevice.rst
Normal file
18
docs/source/smartdevice.rst
Normal file
@ -0,0 +1,18 @@
|
||||
Common API
|
||||
======================
|
||||
|
||||
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:`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.
|
||||
|
||||
Methods changing the state of the device do not invalidate the cache (i.e., there is no implicit `update()`).
|
||||
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.
|
||||
|
||||
Errors are raised as :class:`SmartDeviceException` instances for the library user to handle.
|
||||
|
||||
|
||||
.. autoclass:: kasa.SmartDevice
|
||||
:members:
|
||||
:undoc-members:
|
6
docs/source/smartdimmer.rst
Normal file
6
docs/source/smartdimmer.rst
Normal file
@ -0,0 +1,6 @@
|
||||
Dimmers
|
||||
=======
|
||||
|
||||
.. autoclass:: kasa.SmartDimmer
|
||||
:members:
|
||||
:undoc-members:
|
6
docs/source/smartplug.rst
Normal file
6
docs/source/smartplug.rst
Normal file
@ -0,0 +1,6 @@
|
||||
Plugs
|
||||
=====
|
||||
|
||||
.. autoclass:: kasa.SmartPlug
|
||||
:members:
|
||||
:undoc-members:
|
6
docs/source/smartstrip.rst
Normal file
6
docs/source/smartstrip.rst
Normal file
@ -0,0 +1,6 @@
|
||||
Smart strips
|
||||
============
|
||||
|
||||
.. autoclass:: kasa.SmartStrip
|
||||
:members:
|
||||
:undoc-members:
|
@ -21,7 +21,7 @@ OnDiscoveredCallable = Callable[[SmartDevice], Awaitable[None]]
|
||||
class _DiscoverProtocol(asyncio.DatagramProtocol):
|
||||
"""Implementation of the discovery protocol handler.
|
||||
|
||||
This is internal class, use :func:Discover.discover: instead.
|
||||
This is internal class, use :func:`Discover.discover`: instead.
|
||||
"""
|
||||
|
||||
discovered_devices: Dict[str, SmartDevice]
|
||||
@ -72,6 +72,7 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
|
||||
|
||||
device_class = Discover._get_device_class(info)
|
||||
device = device_class(ip)
|
||||
asyncio.ensure_future(device.update())
|
||||
|
||||
self.discovered_devices[ip] = device
|
||||
self.discovered_devices_raw[ip] = info
|
||||
@ -93,16 +94,36 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
|
||||
class Discover:
|
||||
"""Discover TPLink Smart Home devices.
|
||||
|
||||
The main entry point for this library is Discover.discover(),
|
||||
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.
|
||||
|
||||
discover_single() can be used to initialize a single device given its
|
||||
:func:`discover_single()` can be used to initialize a single device given its
|
||||
IP address. If the type of the device and its IP address is already known,
|
||||
you can initialize the corresponding device class directly without this.
|
||||
|
||||
The protocol uses UDP broadcast datagrams on port 9999 for discovery.
|
||||
|
||||
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 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
|
||||
@ -130,12 +151,13 @@ class Discover:
|
||||
to detect available supported devices in the local network,
|
||||
and waits for given timeout for answers from devices.
|
||||
|
||||
If given, `on_discovered` coroutine will get passed with the SmartDevice as parameter.
|
||||
The results of the discovery can be accessed either via `discovered_devices` (SmartDevice-derived) or
|
||||
`discovered_devices_raw` (JSON objects).
|
||||
If given, `on_discovered` coroutine will get passed with the :class:`SmartDevice`-derived object as parameter.
|
||||
|
||||
The results of the discovery are returned either as a list of :class:`SmartDevice`-derived objects
|
||||
or as raw response dictionaries objects (if `return_raw` is True).
|
||||
|
||||
:param target: The target broadcast address (e.g. 192.168.xxx.255).
|
||||
:param on_discovered:
|
||||
:param on_discovered: coroutine to execute on discovery
|
||||
:param timeout: How long to wait for responses, defaults to 5
|
||||
:param discovery_packets: Number of discovery packets are broadcasted.
|
||||
:param return_raw: True to return JSON objects instead of Devices.
|
||||
@ -180,7 +202,9 @@ class Discover:
|
||||
|
||||
device_class = Discover._get_device_class(info)
|
||||
if device_class is not None:
|
||||
return device_class(host)
|
||||
dev = device_class(host)
|
||||
await dev.update()
|
||||
return dev
|
||||
|
||||
raise SmartDeviceException("Unable to discover device, received: %s" % info)
|
||||
|
||||
|
@ -23,53 +23,69 @@ TPLINK_KELVIN = {
|
||||
class SmartBulb(SmartDevice):
|
||||
"""Representation of a TP-Link Smart Bulb.
|
||||
|
||||
Usage example:
|
||||
```python
|
||||
p = SmartBulb("192.168.1.105")
|
||||
await p.update()
|
||||
To initialize, you have to await :func:`update()` at least once.
|
||||
This will allow accessing the properties using the exposed properties.
|
||||
|
||||
# print the devices alias
|
||||
print(p.alias)
|
||||
All changes to the device are done using awaitable methods,
|
||||
which will not change the cached values, but you must await :func:`update()` separately.
|
||||
|
||||
# change state of bulb
|
||||
await p.turn_on()
|
||||
await p.update()
|
||||
assert p.is_on
|
||||
await p.turn_off()
|
||||
|
||||
# query and print current state of plug
|
||||
print(p.state_information)
|
||||
|
||||
# check whether the bulb supports color changes
|
||||
if p.is_color:
|
||||
print("we got color!")
|
||||
# set the color to an HSV tuple
|
||||
await p.set_hsv(180, 100, 100)
|
||||
await p.update()
|
||||
# get the current HSV value
|
||||
print(p.hsv)
|
||||
|
||||
# check whether the bulb supports setting color temperature
|
||||
if p.is_variable_color_temp:
|
||||
# set the color temperature in Kelvin
|
||||
await p.set_color_temp(3000)
|
||||
await p.update()
|
||||
|
||||
# get the current color temperature
|
||||
print(p.color_temp)
|
||||
|
||||
# check whether the bulb is dimmable
|
||||
if p.is_dimmable:
|
||||
# set the bulb to 50% brightness
|
||||
await p.set_brightness(50)
|
||||
await p.update()
|
||||
|
||||
# check the current brightness
|
||||
print(p.brightness)
|
||||
```
|
||||
|
||||
Errors reported by the device are raised as SmartDeviceExceptions,
|
||||
Errors reported by the device are raised as :class:`SmartDeviceException`s,
|
||||
and should be handled by the user of the library.
|
||||
|
||||
Examples:
|
||||
>>> import asyncio
|
||||
>>> bulb = SmartBulb("127.0.0.1")
|
||||
>>> asyncio.run(bulb.update())
|
||||
>>> print(bulb.alias)
|
||||
KL130 office bulb
|
||||
|
||||
Bulbs, like any other supported devices, can be turned on and off:
|
||||
|
||||
>>> asyncio.run(bulb.turn_off())
|
||||
>>> asyncio.run(bulb.turn_on())
|
||||
>>> asyncio.run(bulb.update())
|
||||
>>> print(bulb.is_on)
|
||||
True
|
||||
|
||||
You can use the is_-prefixed properties to check for supported features
|
||||
>>> bulb.is_dimmable
|
||||
True
|
||||
>>> bulb.is_color
|
||||
True
|
||||
>>> bulb.is_variable_color_temp
|
||||
True
|
||||
|
||||
All known bulbs support changing the brightness:
|
||||
|
||||
>>> bulb.brightness
|
||||
30
|
||||
>>> asyncio.run(bulb.set_brightness(50))
|
||||
>>> asyncio.run(bulb.update())
|
||||
>>> bulb.brightness
|
||||
50
|
||||
|
||||
Bulbs supporting color temperature can be queried to know which range is accepted:
|
||||
|
||||
>>> bulb.valid_temperature_range
|
||||
(2500, 9000)
|
||||
>>> asyncio.run(bulb.set_color_temp(3000))
|
||||
>>> asyncio.run(bulb.update())
|
||||
>>> bulb.color_temp
|
||||
3000
|
||||
|
||||
Color bulbs can be adjusted by passing hue, saturation and value:
|
||||
|
||||
>>> asyncio.run(bulb.set_hsv(180, 100, 80))
|
||||
>>> asyncio.run(bulb.update())
|
||||
>>> bulb.hsv
|
||||
(180, 100, 80)
|
||||
|
||||
If you don't want to use the default transitions, you can pass `transition` in milliseconds.
|
||||
This applies to all transitions (turn_on, turn_off, set_hsv, set_color_temp, set_brightness).
|
||||
The following changes the brightness over a period of 10 seconds:
|
||||
|
||||
>>> asyncio.run(bulb.set_brightness(100, transition=10_000))
|
||||
|
||||
"""
|
||||
|
||||
LIGHT_SERVICE = "smartlife.iot.smartbulb.lightingservice"
|
||||
|
@ -118,7 +118,98 @@ def requires_update(f):
|
||||
|
||||
|
||||
class SmartDevice:
|
||||
"""Base class for all supported device types."""
|
||||
"""Base class for all supported device types.
|
||||
|
||||
You don't usually want to construct this class which implements the shared common interfaces.
|
||||
The recommended way is to either use the discovery functionality, or construct one of the subclasses:
|
||||
|
||||
* :class:`SmartPlug`
|
||||
* :class:`SmartBulb`
|
||||
* :class:`SmartStrip`
|
||||
* :class:`SmartDimmer`
|
||||
|
||||
To initialize, you have to await :func:`update()` at least once.
|
||||
This will allow accessing the properties using the exposed properties.
|
||||
|
||||
All changes to the device are done using awaitable methods,
|
||||
which will not change the cached values, but you must await update() separately.
|
||||
|
||||
Errors reported by the device are raised as SmartDeviceExceptions,
|
||||
and should be handled by the user of the library.
|
||||
|
||||
Examples:
|
||||
>>> import asyncio
|
||||
>>> dev = SmartDevice("127.0.0.1")
|
||||
>>> asyncio.run(dev.update())
|
||||
|
||||
All devices provide several informational properties:
|
||||
|
||||
>>> dev.alias
|
||||
Kitchen
|
||||
>>> dev.model
|
||||
HS110(EU)
|
||||
>>> dev.rssi
|
||||
-71
|
||||
>>> dev.mac
|
||||
50:C7:BF:01:F8:CD
|
||||
|
||||
Some information can also be changed programatically:
|
||||
|
||||
>>> asyncio.run(dev.set_alias("new alias"))
|
||||
>>> asyncio.run(dev.set_mac("01:23:45:67:89:ab"))
|
||||
>>> asyncio.run(dev.update())
|
||||
>>> dev.alias
|
||||
new alias
|
||||
>>> dev.mac
|
||||
01:23:45:67:89:ab
|
||||
|
||||
When initialized using discovery or using a subclass, you can check the type of the device:
|
||||
|
||||
>>> dev.is_bulb
|
||||
False
|
||||
>>> dev.is_strip
|
||||
False
|
||||
>>> dev.is_plug
|
||||
True
|
||||
|
||||
You can also get the hardware and software as a dict, or access the full device response:
|
||||
|
||||
>>> dev.hw_info
|
||||
{'sw_ver': '1.2.5 Build 171213 Rel.101523',
|
||||
'hw_ver': '1.0',
|
||||
'mac': '01:23:45:67:89:ab',
|
||||
'type': 'IOT.SMARTPLUGSWITCH',
|
||||
'hwId': '45E29DA8382494D2E82688B52A0B2EB5',
|
||||
'fwId': '00000000000000000000000000000000',
|
||||
'oemId': '3D341ECE302C0642C99E31CE2430544B',
|
||||
'dev_name': 'Wi-Fi Smart Plug With Energy Monitoring'}
|
||||
>>> dev.sys_info
|
||||
|
||||
All devices can be turned on and off:
|
||||
|
||||
>>> asyncio.run(dev.turn_off())
|
||||
>>> asyncio.run(dev.turn_on())
|
||||
>>> asyncio.run(dev.update())
|
||||
>>> dev.is_on
|
||||
True
|
||||
|
||||
Some devices provide energy consumption meter, and regular update will already fetch some information:
|
||||
|
||||
>>> dev.has_emeter
|
||||
True
|
||||
>>> dev.emeter_realtime
|
||||
{'current': 0.015342, 'err_code': 0, 'power': 0.983971, 'total': 32.448, 'voltage': 235.595234}
|
||||
>>> dev.emeter_today
|
||||
>>> dev.emeter_this_month
|
||||
|
||||
You can also query the historical data (note that these needs to be awaited), keyed with month/day:
|
||||
|
||||
>>> asyncio.run(dev.get_emeter_monthly(year=2016))
|
||||
{11: 1.089, 12: 1.582}
|
||||
>>> asyncio.run(dev.get_emeter_daily(year=2016, month=11))
|
||||
{24: 0.026, 25: 0.109}
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, host: str) -> None:
|
||||
"""Create a new SmartDevice instance.
|
||||
@ -382,6 +473,9 @@ class SmartDevice:
|
||||
@requires_update
|
||||
def emeter_today(self) -> Optional[float]:
|
||||
"""Return today's energy consumption in kWh."""
|
||||
if not self.has_emeter:
|
||||
raise SmartDeviceException("Device has no emeter")
|
||||
|
||||
raw_data = self._last_update[self.emeter_type]["get_daystat"]["day_list"]
|
||||
data = self._emeter_convert_emeter_data(raw_data)
|
||||
today = datetime.now().day
|
||||
@ -395,6 +489,9 @@ class SmartDevice:
|
||||
@requires_update
|
||||
def emeter_this_month(self) -> Optional[float]:
|
||||
"""Return this month's energy consumption in kWh."""
|
||||
if not self.has_emeter:
|
||||
raise SmartDeviceException("Device has no emeter")
|
||||
|
||||
raw_data = self._last_update[self.emeter_type]["get_monthstat"]["month_list"]
|
||||
data = self._emeter_convert_emeter_data(raw_data)
|
||||
current_month = datetime.now().month
|
||||
@ -485,7 +582,7 @@ class SmartDevice:
|
||||
response = EmeterStatus(await self.get_emeter_realtime())
|
||||
return response["power"]
|
||||
|
||||
async def reboot(self, delay=1) -> None:
|
||||
async def reboot(self, delay: int = 1) -> None:
|
||||
"""Reboot the device.
|
||||
|
||||
Note that giving a delay of zero causes this to block,
|
||||
|
@ -9,18 +9,30 @@ class SmartDimmer(SmartPlug):
|
||||
"""Representation of a TP-Link Smart Dimmer.
|
||||
|
||||
Dimmers work similarly to plugs, but provide also support for
|
||||
adjusting the brightness. This class extends SmartPlug interface.
|
||||
adjusting the brightness. This class extends :class:`SmartPlug` interface.
|
||||
|
||||
Example:
|
||||
```
|
||||
dimmer = SmartDimmer("192.168.1.105")
|
||||
await dimmer.turn_on()
|
||||
print("Current brightness: %s" % dimmer.brightness)
|
||||
To initialize, you have to await :func:`update()` at least once.
|
||||
This will allow accessing the properties using the exposed properties.
|
||||
|
||||
await dimmer.set_brightness(100)
|
||||
```
|
||||
All changes to the device are done using awaitable methods,
|
||||
which will not change the cached values, but you must await :func:`update()` separately.
|
||||
|
||||
Refer to SmartPlug for the full API.
|
||||
Errors reported by the device are raised as :class:`SmartDeviceException`s,
|
||||
and should be handled by the user of the library.
|
||||
|
||||
Examples:
|
||||
>>> import asyncio
|
||||
>>> dimmer = SmartDimmer("192.168.1.105")
|
||||
>>> asyncio.run(dimmer.turn_on())
|
||||
>>> dimmer.brightness
|
||||
25
|
||||
|
||||
>>> asyncio.run(dimmer.set_brightness(50))
|
||||
>>> asyncio.run(dimmer.update())
|
||||
>>> dimmer.brightness
|
||||
50
|
||||
|
||||
Refer to :class:`SmartPlug` for the full API.
|
||||
"""
|
||||
|
||||
DIMMER_SERVICE = "smartlife.iot.dimmer"
|
||||
|
@ -10,24 +10,30 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class SmartPlug(SmartDevice):
|
||||
"""Representation of a TP-Link Smart Switch.
|
||||
|
||||
Usage example:
|
||||
```python
|
||||
p = SmartPlug("192.168.1.105")
|
||||
To initialize, you have to await :func:`update()` at least once.
|
||||
This will allow accessing the properties using the exposed properties.
|
||||
|
||||
# print the devices alias
|
||||
print(p.alias)
|
||||
All changes to the device are done using awaitable methods,
|
||||
which will not change the cached values, but you must await :func:`update()` separately.
|
||||
|
||||
# change state of plug
|
||||
await p.turn_on()
|
||||
assert p.is_on is True
|
||||
await p.turn_off()
|
||||
|
||||
# print current state of plug
|
||||
print(p.state_information)
|
||||
```
|
||||
|
||||
Errors reported by the device are raised as SmartDeviceExceptions,
|
||||
Errors reported by the device are raised as :class:`SmartDeviceException`s,
|
||||
and should be handled by the user of the library.
|
||||
|
||||
Examples:
|
||||
>>> import asyncio
|
||||
>>> plug = SmartPlug("127.0.0.1")
|
||||
>>> asyncio.run(plug.update())
|
||||
>>> plug.alias
|
||||
Kitchen
|
||||
|
||||
Setting the LED state:
|
||||
|
||||
>>> asyncio.run(plug.set_led(True))
|
||||
>>> asyncio.run(plug.update())
|
||||
>>> plug.led
|
||||
True
|
||||
|
||||
For more examples, see the :class:`SmartDevice` class.
|
||||
"""
|
||||
|
||||
def __init__(self, host: str) -> None:
|
||||
|
@ -18,28 +18,52 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class SmartStrip(SmartDevice):
|
||||
"""Representation of a TP-Link Smart Power Strip.
|
||||
|
||||
Usage example when used as library:
|
||||
```python
|
||||
p = SmartStrip("192.168.1.105")
|
||||
A strip consists of the parent device and its children.
|
||||
All methods of the parent act on all children, while the child devices
|
||||
share the common API with the :class:`SmartPlug` class.
|
||||
|
||||
# query the state of the strip
|
||||
await p.update()
|
||||
print(p.is_on)
|
||||
To initialize, you have to await :func:`update()` at least once.
|
||||
This will allow accessing the properties using the exposed properties.
|
||||
|
||||
# change state of all outlets
|
||||
await p.turn_on()
|
||||
await p.turn_off()
|
||||
All changes to the device are done using awaitable methods,
|
||||
which will not change the cached values, but you must await :func:`update()` separately.
|
||||
|
||||
# individual outlets are accessible through plugs variable
|
||||
for plug in p.plugs:
|
||||
print(f"{p}: {p.is_on}")
|
||||
|
||||
# change state of a single outlet
|
||||
await p.plugs[0].turn_on()
|
||||
```
|
||||
|
||||
Errors reported by the device are raised as SmartDeviceExceptions,
|
||||
Errors reported by the device are raised as :class:`SmartDeviceException`s,
|
||||
and should be handled by the user of the library.
|
||||
|
||||
Examples:
|
||||
>>> import asyncio
|
||||
>>> strip = SmartStrip("127.0.0.1")
|
||||
>>> asyncio.run(strip.update())
|
||||
>>> strip.alias
|
||||
TP-LINK_Power Strip_CF69
|
||||
|
||||
All methods act on the whole strip:
|
||||
|
||||
>>> for plug in strip.children:
|
||||
>>> print(f"{plug.alias}: {plug.is_on}")
|
||||
Plug 1: True
|
||||
Plug 2: False
|
||||
Plug 3: False
|
||||
>>> strip.is_on
|
||||
True
|
||||
>>> asyncio.run(strip.turn_off())
|
||||
|
||||
Accessing individual plugs can be done using the `children` property:
|
||||
|
||||
>>> len(strip.children)
|
||||
3
|
||||
>>> for plug in strip.children:
|
||||
>>> print(f"{plug.alias}: {plug.is_on}")
|
||||
Plug 1: False
|
||||
Plug 2: False
|
||||
Plug 3: False
|
||||
>>> asyncio.run(strip.children[1].turn_on())
|
||||
>>> asyncio.run(strip.update())
|
||||
>>> strip.is_on
|
||||
True
|
||||
|
||||
For more examples, see the :class:`SmartDevice` class.
|
||||
"""
|
||||
|
||||
def __init__(self, host: str) -> None:
|
||||
@ -212,7 +236,7 @@ class SmartStripPlug(SmartPlug):
|
||||
def is_on(self) -> bool:
|
||||
"""Return whether device is on."""
|
||||
info = self._get_child_info()
|
||||
return info["state"]
|
||||
return bool(info["state"])
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
|
@ -3,6 +3,7 @@ import glob
|
||||
import json
|
||||
import os
|
||||
from os.path import basename
|
||||
from pathlib import Path, PurePath
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest # type: ignore # see https://github.com/pytest-dev/pytest/issues/3342
|
||||
@ -100,6 +101,38 @@ async def handle_turn_on(dev, turn_on):
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
def device_for_file(model):
|
||||
for d in STRIPS:
|
||||
if d in model:
|
||||
return SmartStrip
|
||||
for d in PLUGS:
|
||||
if d in model:
|
||||
return SmartPlug
|
||||
for d in BULBS:
|
||||
if d in model:
|
||||
return SmartBulb
|
||||
for d in DIMMERS:
|
||||
if d in model:
|
||||
return SmartDimmer
|
||||
|
||||
raise Exception("Unable to find type for %s", model)
|
||||
|
||||
|
||||
def get_device_for_file(file):
|
||||
# if the wanted file is not an absolute path, prepend the fixtures directory
|
||||
p = Path(file)
|
||||
if not p.is_absolute():
|
||||
p = Path(__file__).parent / "fixtures" / file
|
||||
|
||||
with open(p) as f:
|
||||
sysinfo = json.load(f)
|
||||
model = basename(file)
|
||||
p = device_for_file(model)(host="123.123.123.123")
|
||||
p.protocol = FakeTransportProtocol(sysinfo)
|
||||
asyncio.run(p.update())
|
||||
return p
|
||||
|
||||
|
||||
@pytest.fixture(params=SUPPORTED_DEVICES)
|
||||
def dev(request):
|
||||
"""Device fixture.
|
||||
@ -117,29 +150,7 @@ def dev(request):
|
||||
return d
|
||||
raise Exception("Unable to find type for %s" % ip)
|
||||
|
||||
def device_for_file(model):
|
||||
for d in STRIPS:
|
||||
if d in model:
|
||||
return SmartStrip
|
||||
for d in PLUGS:
|
||||
if d in model:
|
||||
return SmartPlug
|
||||
for d in BULBS:
|
||||
if d in model:
|
||||
return SmartBulb
|
||||
for d in DIMMERS:
|
||||
if d in model:
|
||||
return SmartDimmer
|
||||
|
||||
raise Exception("Unable to find type for %s", model)
|
||||
|
||||
with open(file) as f:
|
||||
sysinfo = json.load(f)
|
||||
model = basename(file)
|
||||
p = device_for_file(model)(host="123.123.123.123")
|
||||
p.protocol = FakeTransportProtocol(sysinfo)
|
||||
asyncio.run(p.update())
|
||||
yield p
|
||||
return get_device_for_file(file)
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
|
4
kasa/tests/fixtures/KL130(US)_1.0.json
vendored
4
kasa/tests/fixtures/KL130(US)_1.0.json
vendored
@ -27,7 +27,7 @@
|
||||
"system": {
|
||||
"get_sysinfo": {
|
||||
"active_mode": "none",
|
||||
"alias": "Nick office tplink",
|
||||
"alias": "KL130 office bulb",
|
||||
"ctrl_protocols": {
|
||||
"name": "Linkie",
|
||||
"version": "1.0"
|
||||
@ -45,7 +45,7 @@
|
||||
"is_factory": false,
|
||||
"is_variable_color_temp": 1,
|
||||
"light_state": {
|
||||
"brightness": 0,
|
||||
"brightness": 30,
|
||||
"color_temp": 0,
|
||||
"hue": 15,
|
||||
"mode": "normal",
|
||||
|
@ -240,42 +240,41 @@ emeter_commands = {
|
||||
}
|
||||
|
||||
|
||||
def error(target, cmd="no-command", msg="default msg"):
|
||||
return {target: {cmd: {"err_code": -1323, "msg": msg}}}
|
||||
def error(msg="default msg"):
|
||||
return {"err_code": -1323, "msg": msg}
|
||||
|
||||
|
||||
def success(target, cmd, res):
|
||||
def success(res):
|
||||
if res:
|
||||
res.update({"err_code": 0})
|
||||
else:
|
||||
res = {"err_code": 0}
|
||||
return {target: {cmd: res}}
|
||||
return res
|
||||
|
||||
|
||||
class FakeTransportProtocol(TPLinkSmartHomeProtocol):
|
||||
def __init__(self, info):
|
||||
self.discovery_data = info
|
||||
proto = FakeTransportProtocol.baseproto
|
||||
|
||||
for target in info:
|
||||
# print("target %s" % target)
|
||||
for cmd in info[target]:
|
||||
# print("initializing tgt %s cmd %s" % (target, cmd))
|
||||
proto[target][cmd] = info[target][cmd]
|
||||
# if we have emeter support, check for it
|
||||
# if we have emeter support, we need to add the missing pieces
|
||||
for module in ["emeter", "smartlife.iot.common.emeter"]:
|
||||
if module not in info:
|
||||
# TODO required for old tests
|
||||
continue
|
||||
if "get_realtime" in info[module]:
|
||||
get_realtime_res = info[module]["get_realtime"]
|
||||
# TODO remove when removing old tests
|
||||
if callable(get_realtime_res):
|
||||
get_realtime_res = get_realtime_res()
|
||||
if (
|
||||
"err_code" not in get_realtime_res
|
||||
or not get_realtime_res["err_code"]
|
||||
):
|
||||
proto[module] = emeter_commands[module]
|
||||
for etype in ["get_realtime", "get_daystat", "get_monthstat"]:
|
||||
if etype in info[module]: # if the fixture has the data, use it
|
||||
# print("got %s %s from fixture: %s" % (module, etype, info[module][etype]))
|
||||
proto[module][etype] = info[module][etype]
|
||||
else: # otherwise fall back to the static one
|
||||
dummy_data = emeter_commands[module][etype]
|
||||
# print("got %s %s from dummy: %s" % (module, etype, dummy_data))
|
||||
proto[module][etype] = dummy_data
|
||||
|
||||
# print("initialized: %s" % proto[module])
|
||||
|
||||
self.proto = proto
|
||||
|
||||
def set_alias(self, x, child_ids=[]):
|
||||
@ -309,7 +308,7 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
|
||||
|
||||
def set_mac(self, x, *args):
|
||||
_LOGGER.debug("Setting mac to %s", x)
|
||||
self.proto["system"]["get_sysinfo"]["mac"] = x
|
||||
self.proto["system"]["get_sysinfo"]["mac"] = x["mac"]
|
||||
|
||||
def set_hs220_brightness(self, x, *args):
|
||||
_LOGGER.debug("Setting brightness to %s", x)
|
||||
@ -345,9 +344,10 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
|
||||
if not light_state["on_off"] and "on_off" not in x:
|
||||
light_state = light_state["dft_on_state"]
|
||||
|
||||
_LOGGER.debug("Current state: %s", light_state)
|
||||
_LOGGER.debug("Old state: %s", light_state)
|
||||
for key in x:
|
||||
light_state[key] = x[key]
|
||||
_LOGGER.debug("New state: %s", light_state)
|
||||
|
||||
def light_state(self, x, *args):
|
||||
light_state = self.proto["system"]["get_sysinfo"]["light_state"]
|
||||
@ -417,26 +417,39 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
|
||||
except KeyError:
|
||||
child_ids = []
|
||||
|
||||
target = next(iter(request))
|
||||
if target not in proto.keys():
|
||||
return error(target, msg="target not found")
|
||||
def get_response_for_module(target):
|
||||
|
||||
cmd = next(iter(request[target]))
|
||||
if cmd not in proto[target].keys():
|
||||
return error(target, cmd, msg="command not found")
|
||||
if target not in proto.keys():
|
||||
return error(msg="target not found")
|
||||
|
||||
params = request[target][cmd]
|
||||
_LOGGER.debug(f"Going to execute {target}.{cmd} (params: {params}).. ")
|
||||
def get_response_for_command(cmd):
|
||||
if cmd not in proto[target].keys():
|
||||
return error(msg=f"command {cmd} not found")
|
||||
|
||||
if callable(proto[target][cmd]):
|
||||
res = proto[target][cmd](self, params, child_ids)
|
||||
_LOGGER.debug("[callable] %s.%s: %s", target, cmd, res)
|
||||
# verify that change didn't break schema, requires refactoring..
|
||||
# TestSmartPlug.sysinfo_schema(self.proto["system"]["get_sysinfo"])
|
||||
return success(target, cmd, res)
|
||||
elif isinstance(proto[target][cmd], dict):
|
||||
res = proto[target][cmd]
|
||||
_LOGGER.debug("[static] %s.%s: %s", target, cmd, res)
|
||||
return success(target, cmd, res)
|
||||
else:
|
||||
raise NotImplementedError(f"target {target} cmd {cmd}")
|
||||
params = request[target][cmd]
|
||||
_LOGGER.debug(f"Going to execute {target}.{cmd} (params: {params}).. ")
|
||||
|
||||
if callable(proto[target][cmd]):
|
||||
res = proto[target][cmd](self, params, child_ids)
|
||||
_LOGGER.debug("[callable] %s.%s: %s", target, cmd, res)
|
||||
return success(res)
|
||||
elif isinstance(proto[target][cmd], dict):
|
||||
res = proto[target][cmd]
|
||||
_LOGGER.debug("[static] %s.%s: %s", target, cmd, res)
|
||||
return success(res)
|
||||
else:
|
||||
raise NotImplementedError(f"target {target} cmd {cmd}")
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
cmd_responses = defaultdict(dict)
|
||||
for cmd in request[target]:
|
||||
cmd_responses[target][cmd] = get_response_for_command(cmd)
|
||||
|
||||
return cmd_responses
|
||||
|
||||
response = {}
|
||||
for target in request:
|
||||
response.update(get_response_for_module(target))
|
||||
|
||||
return response
|
||||
|
@ -10,6 +10,7 @@ from .conftest import (
|
||||
non_color_bulb,
|
||||
non_dimmable,
|
||||
non_variable_temp,
|
||||
pytestmark,
|
||||
turn_on,
|
||||
variable_temp,
|
||||
)
|
||||
|
@ -1,12 +1,9 @@
|
||||
import pytest
|
||||
from asyncclick.testing import CliRunner
|
||||
|
||||
from kasa import SmartDevice
|
||||
from kasa.cli import alias, brightness, emeter, raw_command, state, sysinfo
|
||||
|
||||
from .conftest import handle_turn_on, turn_on
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
from .conftest import handle_turn_on, pytestmark, turn_on
|
||||
|
||||
|
||||
async def test_sysinfo(dev):
|
||||
|
@ -2,7 +2,7 @@ import pytest
|
||||
|
||||
from kasa import SmartDimmer
|
||||
|
||||
from .conftest import dimmer, handle_turn_on, turn_on
|
||||
from .conftest import dimmer, handle_turn_on, pytestmark, turn_on
|
||||
|
||||
|
||||
@dimmer
|
||||
|
@ -3,10 +3,7 @@ import pytest # type: ignore # https://github.com/pytest-dev/pytest/issues/3342
|
||||
|
||||
from kasa import DeviceType, Discover, SmartDevice, SmartDeviceException
|
||||
|
||||
from .conftest import bulb, dimmer, plug, strip
|
||||
|
||||
# to avoid adding this for each async function separately
|
||||
pytestmark = pytest.mark.asyncio
|
||||
from .conftest import bulb, dimmer, plug, pytestmark, strip
|
||||
|
||||
|
||||
@plug
|
||||
|
@ -2,7 +2,7 @@ import pytest
|
||||
|
||||
from kasa import SmartDeviceException
|
||||
|
||||
from .conftest import has_emeter, no_emeter
|
||||
from .conftest import has_emeter, no_emeter, pytestmark
|
||||
from .newfakes import CURRENT_CONSUMPTION_SCHEMA
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
from kasa import DeviceType
|
||||
|
||||
from .conftest import plug
|
||||
from .conftest import plug, pytestmark
|
||||
from .newfakes import PLUG_SCHEMA
|
||||
|
||||
|
||||
|
@ -4,6 +4,7 @@ import pytest
|
||||
|
||||
from ..exceptions import SmartDeviceException
|
||||
from ..protocol import TPLinkSmartHomeProtocol
|
||||
from .conftest import pytestmark
|
||||
|
||||
|
||||
@pytest.mark.parametrize("retry_count", [1, 3, 5])
|
||||
|
65
kasa/tests/test_readme_examples.py
Normal file
65
kasa/tests/test_readme_examples.py
Normal file
@ -0,0 +1,65 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
import xdoctest
|
||||
from kasa.tests.conftest import get_device_for_file
|
||||
|
||||
|
||||
def test_bulb_examples(mocker):
|
||||
"""Use KL130 (bulb with all features) to test the doctests."""
|
||||
p = get_device_for_file("KL130(US)_1.0.json")
|
||||
mocker.patch("kasa.smartbulb.SmartBulb", return_value=p)
|
||||
mocker.patch("kasa.smartbulb.SmartBulb.update")
|
||||
res = xdoctest.doctest_module("kasa.smartbulb", "all")
|
||||
assert not res["failed"]
|
||||
|
||||
|
||||
def test_smartdevice_examples(mocker):
|
||||
"""Use HS110 for emeter examples."""
|
||||
p = get_device_for_file("HS110(EU)_1.0_real.json")
|
||||
mocker.patch("kasa.smartdevice.SmartDevice", return_value=p)
|
||||
mocker.patch("kasa.smartdevice.SmartDevice.update")
|
||||
res = xdoctest.doctest_module("kasa.smartdevice", "all")
|
||||
assert not res["failed"]
|
||||
|
||||
|
||||
def test_plug_examples(mocker):
|
||||
"""Test plug examples."""
|
||||
p = get_device_for_file("HS110(EU)_1.0_real.json")
|
||||
mocker.patch("kasa.smartplug.SmartPlug", return_value=p)
|
||||
mocker.patch("kasa.smartplug.SmartPlug.update")
|
||||
res = xdoctest.doctest_module("kasa.smartplug", "all")
|
||||
assert not res["failed"]
|
||||
|
||||
|
||||
def test_strip_examples(mocker):
|
||||
"""Test strip examples."""
|
||||
p = get_device_for_file("KP303(UK)_1.0.json")
|
||||
mocker.patch("kasa.smartstrip.SmartStrip", return_value=p)
|
||||
mocker.patch("kasa.smartstrip.SmartStrip.update")
|
||||
res = xdoctest.doctest_module("kasa.smartstrip", "all")
|
||||
assert not res["failed"]
|
||||
|
||||
|
||||
def test_dimmer_examples(mocker):
|
||||
"""Test dimmer examples."""
|
||||
p = get_device_for_file("HS220(US)_1.0_real.json")
|
||||
mocker.patch("kasa.smartdimmer.SmartDimmer", return_value=p)
|
||||
mocker.patch("kasa.smartdimmer.SmartDimmer.update")
|
||||
res = xdoctest.doctest_module("kasa.smartdimmer", "all")
|
||||
assert not res["failed"]
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 8), reason="3.7 handles asyncio.run differently"
|
||||
)
|
||||
def test_discovery_examples(mocker):
|
||||
"""Test discovery examples."""
|
||||
p = get_device_for_file("KP303(UK)_1.0.json")
|
||||
|
||||
# This succeeds on python 3.8 but fails on 3.7
|
||||
# ValueError: a coroutine was expected, got [<DeviceType.Strip model KP303(UK) ...
|
||||
mocker.patch("kasa.discover.Discover.discover", return_value=[p])
|
||||
res = xdoctest.doctest_module("kasa.discover", "all")
|
||||
assert not res["failed"]
|
@ -5,7 +5,7 @@ import pytest # type: ignore # https://github.com/pytest-dev/pytest/issues/3342
|
||||
|
||||
from kasa import SmartDeviceException
|
||||
|
||||
from .conftest import handle_turn_on, turn_on
|
||||
from .conftest import handle_turn_on, pytestmark, turn_on
|
||||
from .newfakes import PLUG_SCHEMA, TZ_SCHEMA, FakeTransportProtocol
|
||||
|
||||
|
||||
|
@ -4,7 +4,7 @@ import pytest
|
||||
|
||||
from kasa import SmartDeviceException, SmartStrip
|
||||
|
||||
from .conftest import handle_turn_on, strip, turn_on
|
||||
from .conftest import handle_turn_on, pytestmark, strip, turn_on
|
||||
|
||||
|
||||
@strip
|
||||
|
@ -18,11 +18,17 @@ python = "^3.7"
|
||||
importlib-metadata = "*"
|
||||
asyncclick = "^7"
|
||||
|
||||
# required only for docs
|
||||
sphinx = { version = "^3.1.1", optional = true }
|
||||
m2r = { version = "^0.2.1", optional = true }
|
||||
sphinx_rtd_theme = { version = "^0.5.0", optional = true }
|
||||
sphinxcontrib-programoutput = { version = "^0.16", optional = true }
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^5"
|
||||
pytest-azurepipelines = "^0.8"
|
||||
pytest-cov = "^2.8"
|
||||
pytest-asyncio = "^0.11"
|
||||
pytest-asyncio = "^0.12"
|
||||
pytest-sugar = "*"
|
||||
pre-commit = "*"
|
||||
voluptuous = "*"
|
||||
@ -30,6 +36,11 @@ toml = "*"
|
||||
tox = "*"
|
||||
pytest-mock = "^3.1.0"
|
||||
codecov = "^2.0"
|
||||
xdoctest = "^0.12"
|
||||
|
||||
[tool.poetry.extras]
|
||||
docs = ["sphinx", "sphinx_rtd_theme", "m2r", "sphinxcontrib-programoutput"]
|
||||
|
||||
|
||||
[tool.isort]
|
||||
multi_line_output = 3
|
||||
|
Loading…
Reference in New Issue
Block a user