diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..e2cb6d82a
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,88 @@
+name: CI
+
+on:
+ push:
+ branches: ["master"]
+ pull_request:
+ branches: ["master"]
+ workflow_dispatch: # to allow manual re-runs
+
+
+jobs:
+ linting:
+ name: "Perform linting checks"
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ python-version: ["3.9"]
+
+ steps:
+ - uses: "actions/checkout@v2"
+ - uses: "actions/setup-python@v2"
+ with:
+ python-version: "${{ matrix.python-version }}"
+ - name: "Install dependencies"
+ run: |
+ python -m pip install --upgrade pip poetry
+ poetry install
+ - name: "Code formating (black)"
+ run: |
+ poetry run pre-commit run black --all-files
+ - name: "Code formating (flake8)"
+ run: |
+ poetry run pre-commit run flake8 --all-files
+ - name: "Order of imports (isort)"
+ run: |
+ poetry run pre-commit run isort --all-files
+ - name: "Typing checks (mypy)"
+ run: |
+ poetry run pre-commit run mypy --all-files
+ - name: "Run trailing-whitespace"
+ run: |
+ poetry run pre-commit run trailing-whitespace --all-files
+ - name: "Run end-of-file-fixer"
+ run: |
+ poetry run pre-commit run end-of-file-fixer --all-files
+ - name: "Run check-docstring-first"
+ run: |
+ poetry run pre-commit run check-docstring-first --all-files
+ - name: "Run debug-statements"
+ run: |
+ poetry run pre-commit run debug-statements --all-files
+ - name: "Run check-ast"
+ run: |
+ poetry run pre-commit run check-ast --all-files
+
+ tests:
+ name: "Python ${{ matrix.python-version}} on ${{ matrix.os }}"
+ needs: linting
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ matrix:
+ python-version: ["3.7", "3.8", "3.9", "pypy-3.7"]
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ # exclude pypy on windows, as the poetry install seems to be very flaky:
+ # PermissionError(13, 'The process cannot access the file because it is being used by another process'))
+ # at C:\hostedtoolcache\windows\PyPy\3.7.10\x86\site-packages\requests\models.py:761 in generate
+ exclude:
+ - python-version: pypy-3.7
+ os: windows-latest
+
+ steps:
+ - uses: "actions/checkout@v2"
+ - uses: "actions/setup-python@v2"
+ with:
+ python-version: "${{ matrix.python-version }}"
+ - name: "Install dependencies"
+ run: |
+ python -m pip install --upgrade pip poetry
+ poetry install
+ - name: "Run tests"
+ run: |
+ poetry run pytest --cov kasa --cov-report xml
+ - name: "Upload coverage to Codecov"
+ uses: "codecov/codecov-action@v1"
+ with:
+ fail_ci_if_error: true
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 39b02f975..a0e1ced0b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -10,13 +10,13 @@ repos:
- id: check-ast
- repo: https://github.com/asottile/pyupgrade
- rev: v2.19.4
+ rev: v2.27.0
hooks:
- id: pyupgrade
args: ['--py36-plus']
- repo: https://github.com/python/black
- rev: 21.6b0
+ rev: 21.9b0
hooks:
- id: black
@@ -27,13 +27,13 @@ repos:
additional_dependencies: [flake8-docstrings]
- repo: https://github.com/pre-commit/mirrors-isort
- rev: v5.8.0
+ rev: v5.9.3
hooks:
- id: isort
additional_dependencies: [toml]
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v0.902
+ rev: v0.910
hooks:
- id: mypy
additional_dependencies: [types-click]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6f0b089b4..437c756dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,50 @@
# Changelog
-## [0.4.0.dev3](https://github.com/python-kasa/python-kasa/tree/0.4.0.dev3) (2021-06-14)
+## [0.4.0.dev4](https://github.com/python-kasa/python-kasa/tree/0.4.0.dev4) (2021-09-23)
+
+[Full Changelog](https://github.com/python-kasa/python-kasa/compare/0.4.0.dev3...0.4.0.dev4)
+
+**Implemented enhancements:**
+
+- HS300 Children plugs have emeter [\#64](https://github.com/python-kasa/python-kasa/issues/64)
+- Improve emeterstatus API, move into own module [\#205](https://github.com/python-kasa/python-kasa/pull/205) ([rytilahti](https://github.com/rytilahti))
+- Avoid temp array during encrypt and decrypt [\#204](https://github.com/python-kasa/python-kasa/pull/204) ([bdraco](https://github.com/bdraco))
+- Add emeter support for strip sockets [\#203](https://github.com/python-kasa/python-kasa/pull/203) ([bdraco](https://github.com/bdraco))
+- Add own device type for smartstrip children [\#201](https://github.com/python-kasa/python-kasa/pull/201) ([rytilahti](https://github.com/rytilahti))
+- bulb: allow set\_hsv without v, add fallback ct range [\#200](https://github.com/python-kasa/python-kasa/pull/200) ([rytilahti](https://github.com/rytilahti))
+- Improve bulb support \(alias, time settings\) [\#198](https://github.com/python-kasa/python-kasa/pull/198) ([rytilahti](https://github.com/rytilahti))
+- Improve testing harness to allow tests on real devices [\#197](https://github.com/python-kasa/python-kasa/pull/197) ([rytilahti](https://github.com/rytilahti))
+- cli: add human-friendly printout when calling temperature on non-supported devices [\#196](https://github.com/python-kasa/python-kasa/pull/196) ([JaydenRA](https://github.com/JaydenRA))
+
+**Fixed bugs:**
+
+- KL430: Throw error for Device specific information [\#189](https://github.com/python-kasa/python-kasa/issues/189)
+- dump\_devinfo: handle latitude/longitude keys properly [\#175](https://github.com/python-kasa/python-kasa/pull/175) ([rytilahti](https://github.com/rytilahti))
+
+**Closed issues:**
+
+- Feature Request - Toggle Command [\#188](https://github.com/python-kasa/python-kasa/issues/188)
+- Is It Compatible With HS105? [\#186](https://github.com/python-kasa/python-kasa/issues/186)
+- Cannot use some functions with KP303 [\#181](https://github.com/python-kasa/python-kasa/issues/181)
+- Help needed - awaiting game [\#179](https://github.com/python-kasa/python-kasa/issues/179)
+- Version inconsistency between CLI and pip [\#177](https://github.com/python-kasa/python-kasa/issues/177)
+- Release 0.4.0.dev3? [\#169](https://github.com/python-kasa/python-kasa/issues/169)
+- Discover does not support specifying network interface [\#167](https://github.com/python-kasa/python-kasa/issues/167)
+- Can't command or query HS200 v5 switch [\#161](https://github.com/python-kasa/python-kasa/issues/161)
+
+**Merged pull requests:**
+
+- More CI fixes [\#208](https://github.com/python-kasa/python-kasa/pull/208) ([rytilahti](https://github.com/rytilahti))
+- Fix CI dep installation [\#207](https://github.com/python-kasa/python-kasa/pull/207) ([rytilahti](https://github.com/rytilahti))
+- Use github actions instead of azure pipelines [\#206](https://github.com/python-kasa/python-kasa/pull/206) ([rytilahti](https://github.com/rytilahti))
+- Add KP115 fixture [\#202](https://github.com/python-kasa/python-kasa/pull/202) ([rytilahti](https://github.com/rytilahti))
+- Perform initial update only using the sysinfo query [\#199](https://github.com/python-kasa/python-kasa/pull/199) ([rytilahti](https://github.com/rytilahti))
+- Add real kasa KL430\(UN\) device dump [\#192](https://github.com/python-kasa/python-kasa/pull/192) ([iprodanovbg](https://github.com/iprodanovbg))
+- Use less strict matcher for kl430 color temperature [\#190](https://github.com/python-kasa/python-kasa/pull/190) ([rytilahti](https://github.com/rytilahti))
+- Add EP10\(US\) 1.0 1.0.2 fixture [\#174](https://github.com/python-kasa/python-kasa/pull/174) ([nbrew](https://github.com/nbrew))
+- Add a note about using the discovery target parameter [\#168](https://github.com/python-kasa/python-kasa/pull/168) ([leandroreox](https://github.com/leandroreox))
+
+## [0.4.0.dev3](https://github.com/python-kasa/python-kasa/tree/0.4.0.dev3) (2021-06-16)
[Full Changelog](https://github.com/python-kasa/python-kasa/compare/0.4.0.dev2...0.4.0.dev3)
@@ -29,6 +73,7 @@
**Merged pull requests:**
+- Prepare 0.4.0.dev3 [\#172](https://github.com/python-kasa/python-kasa/pull/172) ([rytilahti](https://github.com/rytilahti))
- Simplify mac address handling [\#162](https://github.com/python-kasa/python-kasa/pull/162) ([rytilahti](https://github.com/rytilahti))
- Added KL125 and HS200 fixture dumps and updated tests to run on new format [\#160](https://github.com/python-kasa/python-kasa/pull/160) ([brianthedavis](https://github.com/brianthedavis))
- Add KL125 bulb definition [\#143](https://github.com/python-kasa/python-kasa/pull/143) ([mdarnol](https://github.com/mdarnol))
diff --git a/README.md b/README.md
index 9cb1b3c68..e1136d1f5 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
# python-kasa
[](https://badge.fury.io/py/python-kasa)
-[](https://dev.azure.com/python-kasa/python-kasa/_build/latest?definitionId=2&branchName=master)
-[](https://coveralls.io/github/python-kasa/python-kasa?branch=master)
+[](https://github.com/python-kasa/python-kasa/actions/workflows/ci.yml)
+[](https://codecov.io/gh/python-kasa/python-kasa)
[](https://python-kasa.readthedocs.io/en/latest/?badge=latest)
python-kasa is a Python library to control TPLink smart home devices (plugs, wall switches, power strips, and bulbs) using asyncio.
@@ -94,6 +94,15 @@ This will make sure that the checks are passing when you do a commit.
You can also execute the checks by running either `tox -e lint` to only do the linting checks, or `tox` to also execute the tests.
+### Running tests
+
+You can run tests on the library by executing `pytest` in the source directory.
+This will run the tests against contributed example responses, but you can also execute the tests against a real device:
+```
+pytest --ip
+```
+Note that this will perform state changes on the device.
+
### Analyzing network captures
The simplest way to add support for a new device or to improve existing ones is to capture traffic between the mobile app and the device.
@@ -110,6 +119,7 @@ or the `parse_pcap.py` script contained inside the `devtools` directory.
* HS105
* HS107
* HS110
+* KP115
### Power Strips
diff --git a/RELEASING.md b/RELEASING.md
index 75a775edb..fc01a87de 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -3,7 +3,7 @@
```bash
# export PREVIOUS_RELEASE=$(git describe --abbrev=0)
export PREVIOUS_RELEASE=0.3.5 # generate the full changelog since last pyhs100 release
-export NEW_RELEASE=0.4.0.pre1
+export NEW_RELEASE=0.4.0.dev4
```
2. Update the version number
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
deleted file mode 100644
index 2b201ae08..000000000
--- a/azure-pipelines.yml
+++ /dev/null
@@ -1,118 +0,0 @@
-trigger:
-- master
-pr:
-- master
-
-
-stages:
-- stage: "Linting"
- jobs:
- - job: "LintChecks"
- pool:
- vmImage: "ubuntu-latest"
- strategy:
- matrix:
- Python 3.8:
- python.version: '3.8'
- steps:
- - task: UsePythonVersion@0
- inputs:
- versionSpec: '$(python.version)'
- displayName: 'Use Python $(python.version)'
-
- - script: |
- python -m pip install --upgrade pip poetry
- poetry install
- displayName: 'Install dependencies'
-
- - script: |
- poetry run pre-commit run black --all-files
- displayName: 'Code formating (black)'
-
- - script: |
- poetry run pre-commit run flake8 --all-files
- displayName: 'Code formating (flake8)'
-
- - script: |
- poetry run pre-commit run mypy --all-files
- displayName: 'Typing checks (mypy)'
-
- - script: |
- poetry run pre-commit run isort --all-files
- displayName: 'Order of imports (isort)'
-
- - script: |
- poetry run pre-commit run trailing-whitespace --all-files
- displayName: 'Run trailing-whitespace'
-
- - script: |
- poetry run pre-commit run end-of-file-fixer --all-files
- displayName: 'Run end-of-file-fixer'
-
- - script: |
- poetry run pre-commit run check-docstring-first --all-files
- displayName: 'Run check-docstring-first'
-
- - script: |
- poetry run pre-commit run check-yaml --all-files
- displayName: 'Run check-yaml'
-
- - script: |
- poetry run pre-commit run debug-statements --all-files
- displayName: 'Run debug-statements'
-
- - script: |
- poetry run pre-commit run check-ast --all-files
- displayName: 'Run check-ast'
-
-
-- stage: "Tests"
- jobs:
- - job: "Tests"
- strategy:
- matrix:
- Python 3.7 Ubuntu:
- python.version: '3.7'
- vmImage: 'ubuntu-latest'
-
- Python 3.8 Ubuntu:
- python.version: '3.8'
- vmImage: 'ubuntu-latest'
-
- Python 3.7 Windows:
- python.version: '3.7'
- vmImage: 'windows-latest'
-
- Python 3.8 Windows:
- python.version: '3.8'
- vmImage: 'windows-latest'
-
- Python 3.7 OSX:
- python.version: '3.7'
- vmImage: 'macOS-latest'
-
- Python 3.8 OSX:
- python.version: '3.8'
- vmImage: 'macOS-latest'
-
- pool:
- vmImage: $(vmImage)
-
- steps:
- - task: UsePythonVersion@0
- inputs:
- versionSpec: '$(python.version)'
- displayName: 'Use Python $(python.version)'
-
- - script: |
- python -m pip install --upgrade pip poetry
- poetry install
- displayName: 'Install dependencies'
-
- - script: |
- poetry run pytest --cov kasa --cov-report=xml --cov-report=html
- displayName: 'Run tests'
-
- - script: |
- poetry run codecov -t $(codecov.token)
- displayName: 'Report code coverage'
diff --git a/devtools/dump_devinfo.py b/devtools/dump_devinfo.py
index 433846d34..9d30f9674 100644
--- a/devtools/dump_devinfo.py
+++ b/devtools/dump_devinfo.py
@@ -41,7 +41,7 @@ def scrub(res):
res[k] = scrub(res.get(k))
else:
if k in keys_to_scrub:
- if k in ["latitude_i", "longitude_i"]:
+ if k in ["latitude", "latitude_i", "longitude", "longitude_i"]:
v = 0
else:
v = re.sub(r"\w", "0", v)
@@ -62,7 +62,7 @@ def default_to_regular(d):
@click.command()
@click.argument("host")
-@click.option("--debug")
+@click.option("-d", "--debug", is_flag=True)
def cli(host, debug):
"""Generate devinfo file for given device."""
if debug:
diff --git a/kasa/__init__.py b/kasa/__init__.py
index 51b5291b4..fc798fb37 100755
--- a/kasa/__init__.py
+++ b/kasa/__init__.py
@@ -14,10 +14,11 @@
from importlib_metadata import version # type: ignore
from kasa.discover import Discover
+from kasa.emeterstatus import EmeterStatus
from kasa.exceptions import SmartDeviceException
from kasa.protocol import TPLinkSmartHomeProtocol
from kasa.smartbulb import SmartBulb
-from kasa.smartdevice import DeviceType, EmeterStatus, SmartDevice
+from kasa.smartdevice import DeviceType, SmartDevice
from kasa.smartdimmer import SmartDimmer
from kasa.smartlightstrip import SmartLightStrip
from kasa.smartplug import SmartPlug
diff --git a/kasa/cli.py b/kasa/cli.py
index 0473dc97b..626eadc2b 100755
--- a/kasa/cli.py
+++ b/kasa/cli.py
@@ -247,6 +247,7 @@ async def alias(dev, new_alias, index):
if new_alias is not None:
click.echo(f"Setting alias to {new_alias}")
click.echo(await dev.set_alias(new_alias))
+ await dev.update()
click.echo(f"Alias: {dev.alias}")
if dev.is_strip:
@@ -345,6 +346,9 @@ async def brightness(dev: SmartBulb, brightness: int, transition: int):
async def temperature(dev: SmartBulb, temperature: int, transition: int):
"""Get or set color temperature."""
await dev.update()
+ if not dev.is_variable_color_temp:
+ click.echo("Device does not support color temperature")
+ return
if temperature is None:
click.echo(f"Color temperature: {dev.color_temp}")
valid_temperature_range = dev.valid_temperature_range
diff --git a/kasa/discover.py b/kasa/discover.py
index 2ee2079ba..b12b79264 100755
--- a/kasa/discover.py
+++ b/kasa/discover.py
@@ -152,13 +152,14 @@ async def discover(
Sends discovery message to 255.255.255.255:9999 in order
to detect available supported devices in the local network,
and waits for given timeout for answers from devices.
+ If you have multiple interfaces, you can use target parameter to specify the network for discovery.
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 target: The target address where to send the broadcast discovery queries if multi-homing (e.g. 192.168.xxx.255).
: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.
diff --git a/kasa/emeterstatus.py b/kasa/emeterstatus.py
new file mode 100644
index 000000000..da636551d
--- /dev/null
+++ b/kasa/emeterstatus.py
@@ -0,0 +1,82 @@
+"""Module for emeter container."""
+import logging
+from typing import Optional
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class EmeterStatus(dict):
+ """Container for converting different representations of emeter data.
+
+ Newer FW/HW versions postfix the variable names with the used units,
+ where-as the olders do not have this feature.
+
+ This class automatically converts between these two to allow
+ backwards and forwards compatibility.
+ """
+
+ @property
+ def voltage(self) -> Optional[float]:
+ """Return voltage in V."""
+ try:
+ return self["voltage"]
+ except ValueError:
+ return None
+
+ @property
+ def power(self) -> Optional[float]:
+ """Return power in W."""
+ try:
+ return self["power"]
+ except ValueError:
+ return None
+
+ @property
+ def current(self) -> Optional[float]:
+ """Return current in A."""
+ try:
+ return self["current"]
+ except ValueError:
+ return None
+
+ @property
+ def total(self) -> Optional[float]:
+ """Return total in kWh."""
+ try:
+ return self["total"]
+ except ValueError:
+ return None
+
+ def __repr__(self):
+ return f""
+
+ def __getitem__(self, item):
+ valid_keys = [
+ "voltage_mv",
+ "power_mw",
+ "current_ma",
+ "energy_wh",
+ "total_wh",
+ "voltage",
+ "power",
+ "current",
+ "total",
+ "energy",
+ ]
+
+ # 1. if requested data is available, return it
+ if item in super().keys():
+ return super().__getitem__(item)
+ # otherwise decide how to convert it
+ else:
+ if item not in valid_keys:
+ raise KeyError(item)
+ if "_" in item: # upscale
+ return super().__getitem__(item[: item.find("_")]) * 1000
+ else: # downscale
+ for i in super().keys():
+ if i.startswith(item):
+ return self.__getitem__(i) / 1000
+
+ _LOGGER.debug(f"Unable to find value for '{item}'")
+ return None
diff --git a/kasa/protocol.py b/kasa/protocol.py
index 6ee6f72d6..bbf13b995 100755
--- a/kasa/protocol.py
+++ b/kasa/protocol.py
@@ -89,6 +89,13 @@ async def query(host: str, request: Union[str, Dict], retry_count: int = 3) -> D
# make mypy happy, this should never be reached..
raise SmartDeviceException("Query reached somehow to unreachable")
+ @staticmethod
+ def _xor_payload(unencrypted):
+ key = TPLinkSmartHomeProtocol.INITIALIZATION_VECTOR
+ for unencryptedbyte in unencrypted:
+ key = key ^ unencryptedbyte
+ yield key
+
@staticmethod
def encrypt(request: str) -> bytes:
"""Encrypt a request for a TP-Link Smart Home Device.
@@ -96,17 +103,18 @@ def encrypt(request: str) -> bytes:
:param request: plaintext request data
:return: ciphertext to be send over wire, in bytes
"""
- key = TPLinkSmartHomeProtocol.INITIALIZATION_VECTOR
-
plainbytes = request.encode()
- buffer = bytearray(struct.pack(">I", len(plainbytes)))
+ return struct.pack(">I", len(plainbytes)) + bytes(
+ TPLinkSmartHomeProtocol._xor_payload(plainbytes)
+ )
- for plainbyte in plainbytes:
- cipherbyte = key ^ plainbyte
+ @staticmethod
+ def _xor_encrypted_payload(ciphertext):
+ key = TPLinkSmartHomeProtocol.INITIALIZATION_VECTOR
+ for cipherbyte in ciphertext:
+ plainbyte = key ^ cipherbyte
key = cipherbyte
- buffer.append(cipherbyte)
-
- return bytes(buffer)
+ yield plainbyte
@staticmethod
def decrypt(ciphertext: bytes) -> str:
@@ -115,14 +123,6 @@ def decrypt(ciphertext: bytes) -> str:
:param ciphertext: encrypted response data
:return: plaintext response
"""
- key = TPLinkSmartHomeProtocol.INITIALIZATION_VECTOR
- buffer = []
-
- for cipherbyte in ciphertext:
- plainbyte = key ^ cipherbyte
- key = cipherbyte
- buffer.append(plainbyte)
-
- plaintext = bytes(buffer)
-
- return plaintext.decode()
+ return bytes(
+ TPLinkSmartHomeProtocol._xor_encrypted_payload(ciphertext)
+ ).decode()
diff --git a/kasa/smartbulb.py b/kasa/smartbulb.py
index 351f916b9..aad2ce8ce 100644
--- a/kasa/smartbulb.py
+++ b/kasa/smartbulb.py
@@ -1,26 +1,40 @@
"""Module for bulbs (LB*, KL*, KB*)."""
+import logging
import re
-from typing import Any, Dict, Tuple, cast
+from typing import Any, Dict, NamedTuple, cast
+
+from .smartdevice import DeviceType, SmartDevice, SmartDeviceException, requires_update
+
+
+class ColorTempRange(NamedTuple):
+ """Color temperature range."""
+
+ min: int
+ max: int
+
+
+class HSV(NamedTuple):
+ """Hue-saturation-value."""
+
+ hue: int
+ saturation: int
+ value: int
-from kasa.smartdevice import (
- DeviceType,
- SmartDevice,
- SmartDeviceException,
- requires_update,
-)
TPLINK_KELVIN = {
- "LB130": (2500, 9000),
- "LB120": (2700, 6500),
- "LB230": (2500, 9000),
- "KB130": (2500, 9000),
- "KL130": (2500, 9000),
- "KL125": (2500, 6500),
- r"KL120\(EU\)": (2700, 6500),
- r"KL120\(US\)": (2700, 5000),
- r"KL430\(US\)": (2500, 9000),
+ "LB130": ColorTempRange(2500, 9000),
+ "LB120": ColorTempRange(2700, 6500),
+ "LB230": ColorTempRange(2500, 9000),
+ "KB130": ColorTempRange(2500, 9000),
+ "KL130": ColorTempRange(2500, 9000),
+ "KL125": ColorTempRange(2500, 6500),
+ r"KL120\(EU\)": ColorTempRange(2700, 6500),
+ r"KL120\(US\)": ColorTempRange(2700, 5000),
+ r"KL430": ColorTempRange(2500, 9000),
}
+_LOGGER = logging.getLogger(__name__)
+
class SmartBulb(SmartDevice):
"""Representation of a TP-Link Smart Bulb.
@@ -69,7 +83,7 @@ class SmartBulb(SmartDevice):
Bulbs supporting color temperature can be queried to know which range is accepted:
>>> bulb.valid_temperature_range
- (2500, 9000)
+ ColorTempRange(min=2500, max=9000)
>>> asyncio.run(bulb.set_color_temp(3000))
>>> asyncio.run(bulb.update())
>>> bulb.color_temp
@@ -80,7 +94,7 @@ class SmartBulb(SmartDevice):
>>> asyncio.run(bulb.set_hsv(180, 100, 80))
>>> asyncio.run(bulb.update())
>>> bulb.hsv
- (180, 100, 80)
+ HSV(hue=180, saturation=100, value=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).
@@ -91,6 +105,7 @@ class SmartBulb(SmartDevice):
"""
LIGHT_SERVICE = "smartlife.iot.smartbulb.lightingservice"
+ TIME_SERVICE = "smartlife.iot.common.timesetting"
SET_LIGHT_METHOD = "transition_light_state"
def __init__(self, host: str) -> None:
@@ -121,21 +136,21 @@ def is_variable_color_temp(self) -> bool:
@property # type: ignore
@requires_update
- def valid_temperature_range(self) -> Tuple[int, int]:
+ def valid_temperature_range(self) -> ColorTempRange:
"""Return the device-specific white temperature range (in Kelvin).
:return: White temperature range in Kelvin (minimum, maximum)
"""
if not self.is_variable_color_temp:
raise SmartDeviceException("Color temperature not supported")
+
for model, temp_range in TPLINK_KELVIN.items():
sys_info = self.sys_info
if re.match(model, sys_info["model"]):
return temp_range
- raise SmartDeviceException(
- "Unknown color temperature range, please open an issue on github"
- )
+ _LOGGER.warning("Unknown color temperature range, fallback to 2700-5000")
+ return ColorTempRange(2700, 5000)
@property # type: ignore
@requires_update
@@ -199,7 +214,7 @@ async def set_light_state(self, state: Dict, *, transition: int = None) -> Dict:
@property # type: ignore
@requires_update
- def hsv(self) -> Tuple[int, int, int]:
+ def hsv(self) -> HSV:
"""Return the current HSV state of the bulb.
:return: hue, saturation and value (degrees, %, %)
@@ -213,7 +228,7 @@ def hsv(self) -> Tuple[int, int, int]:
saturation = light_state["saturation"]
value = light_state["brightness"]
- return hue, saturation, value
+ return HSV(hue, saturation, value)
def _raise_for_invalid_brightness(self, value):
if not isinstance(value, int) or not (0 <= value <= 100):
@@ -223,7 +238,7 @@ def _raise_for_invalid_brightness(self, value):
@requires_update
async def set_hsv(
- self, hue: int, saturation: int, value: int, *, transition: int = None
+ self, hue: int, saturation: int, value: int = None, *, transition: int = None
) -> Dict:
"""Set new HSV.
@@ -246,15 +261,16 @@ async def set_hsv(
"(valid range: 0-100%)".format(saturation)
)
- self._raise_for_invalid_brightness(value)
-
light_state = {
"hue": hue,
"saturation": saturation,
- "brightness": value,
"color_temp": 0,
}
+ if value is not None:
+ self._raise_for_invalid_brightness(value)
+ light_state["brightness"] = value
+
return await self.set_light_state(light_state, transition=transition)
@property # type: ignore
@@ -283,7 +299,7 @@ async def set_color_temp(
if temp < valid_temperature_range[0] or temp > valid_temperature_range[1]:
raise ValueError(
"Temperature should be between {} "
- "and {}".format(*valid_temperature_range)
+ "and {}, was {}".format(*valid_temperature_range, temp)
)
light_state = {"color_temp": temp}
@@ -359,3 +375,12 @@ async def turn_on(self, *, transition: int = None, **kwargs) -> Dict:
def has_emeter(self) -> bool:
"""Return that the bulb has an emeter."""
return True
+
+ async def set_alias(self, alias: str) -> None:
+ """Set the device name (alias).
+
+ Overridden to use a different module name.
+ """
+ return await self._query_helper(
+ "smartlife.iot.common.system", "set_dev_alias", {"alias": alias}
+ )
diff --git a/kasa/smartdevice.py b/kasa/smartdevice.py
index 3b21aa010..11c7d1c96 100755
--- a/kasa/smartdevice.py
+++ b/kasa/smartdevice.py
@@ -11,14 +11,16 @@
You may obtain a copy of the license at
http://www.apache.org/licenses/LICENSE-2.0
"""
+import collections.abc
import functools
import inspect
import logging
from dataclasses import dataclass
from datetime import datetime, timedelta
-from enum import Enum
-from typing import Any, Dict, List, Optional
+from enum import Enum, auto
+from typing import Any, Dict, List, Optional, Set
+from .emeterstatus import EmeterStatus
from .exceptions import SmartDeviceException
from .protocol import TPLinkSmartHomeProtocol
@@ -28,11 +30,12 @@
class DeviceType(Enum):
"""Device type enum."""
- Plug = 1
- Bulb = 2
- Strip = 3
- Dimmer = 4
- LightStrip = 5
+ Plug = auto()
+ Bulb = auto()
+ Strip = auto()
+ StripSocket = auto()
+ Dimmer = auto()
+ LightStrip = auto()
Unknown = -1
@@ -49,46 +52,14 @@ class WifiNetwork:
rssi: Optional[int] = None
-class EmeterStatus(dict):
- """Container for converting different representations of emeter data.
-
- Newer FW/HW versions postfix the variable names with the used units,
- where-as the olders do not have this feature.
-
- This class automatically converts between these two to allow
- backwards and forwards compatibility.
- """
-
- def __getitem__(self, item):
- valid_keys = [
- "voltage_mv",
- "power_mw",
- "current_ma",
- "energy_wh",
- "total_wh",
- "voltage",
- "power",
- "current",
- "total",
- "energy",
- ]
-
- # 1. if requested data is available, return it
- if item in super().keys():
- return super().__getitem__(item)
- # otherwise decide how to convert it
+def merge(d, u):
+ """Update dict recursively."""
+ for k, v in u.items():
+ if isinstance(v, collections.abc.Mapping):
+ d[k] = merge(d.get(k, {}), v)
else:
- if item not in valid_keys:
- raise KeyError(item)
- if "_" in item: # upscale
- return super().__getitem__(item[: item.find("_")]) * 1000
- else: # downscale
- for i in super().keys():
- if i.startswith(item):
- return self.__getitem__(i) / 1000
-
- _LOGGER.debug(f"Unable to find value for '{item}'")
- return None
+ d[k] = v
+ return d
def requires_update(f):
@@ -201,7 +172,7 @@ class SmartDevice:
>>> 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
@@ -214,6 +185,8 @@ class SmartDevice:
"""
+ TIME_SERVICE = "time"
+
def __init__(self, host: str) -> None:
"""Create a new SmartDevice instance.
@@ -242,6 +215,11 @@ def _create_request(
return request
+ def _verify_emeter(self) -> None:
+ """Raise an exception if there is no emeter."""
+ if not self.has_emeter:
+ raise SmartDeviceException("Device has no emeter")
+
async def _query_helper(
self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None
) -> Any:
@@ -278,31 +256,50 @@ async def _query_helper(
return result
+ @property # type: ignore
+ @requires_update
+ def features(self) -> Set[str]:
+ """Return a set of features that the device supports."""
+ return set(self.sys_info["feature"].split(":"))
+
@property # type: ignore
@requires_update
def has_emeter(self) -> bool:
"""Return True if device has an energy meter."""
- sys_info = self.sys_info
- features = sys_info["feature"].split(":")
- return "ENE" in features
+ return "ENE" in self.features
async def get_sys_info(self) -> Dict[str, Any]:
"""Retrieve system information."""
return await self._query_helper("system", "get_sysinfo")
async def update(self):
- """Update some of the attributes.
+ """Query the device to update the data.
- Needed for methods that are decorated with `requires_update`.
+ Needed for properties that are decorated with `requires_update`.
"""
req = {}
req.update(self._create_request("system", "get_sysinfo"))
- # Check for emeter if we were never updated, or if the device has emeter
- if self._last_update is None or self.has_emeter:
+ # If this is the initial update, check only for the sysinfo
+ # This is necessary as some devices crash on unexpected modules
+ # See #105, #120, #161
+ if self._last_update is None:
+ _LOGGER.debug("Performing the initial update to obtain sysinfo")
+ self._last_update = await self.protocol.query(self.host, req)
+ self._sys_info = self._last_update["system"]["get_sysinfo"]
+ # If the device has no emeter, we are done for the initial update
+ # Otherwise we will follow the regular code path to also query
+ # the emeter data also during the initial update
+ if not self.has_emeter:
+ return
+
+ if self.has_emeter:
+ _LOGGER.debug(
+ "The device has emeter, querying its information along sysinfo"
+ )
req.update(self._create_emeter_request())
+
self._last_update = await self.protocol.query(self.host, req)
- # TODO: keep accessible for tests
self._sys_info = self._last_update["system"]["get_sysinfo"]
def update_from_discover_info(self, info):
@@ -337,7 +334,7 @@ async def set_alias(self, alias: str) -> None:
async def get_time(self) -> Optional[datetime]:
"""Return current time from the device, if available."""
try:
- res = await self._query_helper("time", "get_time")
+ res = await self._query_helper(self.TIME_SERVICE, "get_time")
return datetime(
res["year"],
res["month"],
@@ -351,7 +348,7 @@ async def get_time(self) -> Optional[datetime]:
async def get_timezone(self) -> Dict:
"""Return timezone information."""
- return await self._query_helper("time", "get_timezone")
+ return await self._query_helper(self.TIME_SERVICE, "get_timezone")
@property # type: ignore
@requires_update
@@ -397,10 +394,8 @@ def location(self) -> Dict:
@requires_update
def rssi(self) -> Optional[int]:
"""Return WiFi signal strenth (rssi)."""
- sys_info = self.sys_info
- if "rssi" in sys_info:
- return int(sys_info["rssi"])
- return None
+ rssi = self.sys_info.get("rssi")
+ return None if rssi is None else int(rssi)
@property # type: ignore
@requires_update
@@ -433,16 +428,12 @@ async def set_mac(self, mac):
@requires_update
def emeter_realtime(self) -> EmeterStatus:
"""Return current energy readings."""
- if not self.has_emeter:
- raise SmartDeviceException("Device has no emeter")
-
+ self._verify_emeter()
return EmeterStatus(self._last_update[self.emeter_type]["get_realtime"])
async def get_emeter_realtime(self) -> EmeterStatus:
"""Retrieve current energy readings."""
- if not self.has_emeter:
- raise SmartDeviceException("Device has no emeter")
-
+ self._verify_emeter()
return EmeterStatus(await self._query_helper(self.emeter_type, "get_realtime"))
def _create_emeter_request(self, year: int = None, month: int = None):
@@ -452,23 +443,12 @@ def _create_emeter_request(self, year: int = None, month: int = None):
if month is None:
month = datetime.now().month
- import collections.abc
-
- def update(d, u):
- """Update dict recursively."""
- for k, v in u.items():
- if isinstance(v, collections.abc.Mapping):
- d[k] = update(d.get(k, {}), v)
- else:
- d[k] = v
- return d
-
req: Dict[str, Any] = {}
- update(req, self._create_request(self.emeter_type, "get_realtime"))
- update(
+ merge(req, self._create_request(self.emeter_type, "get_realtime"))
+ merge(
req, self._create_request(self.emeter_type, "get_monthstat", {"year": year})
)
- update(
+ merge(
req,
self._create_request(
self.emeter_type, "get_daystat", {"month": month, "year": year}
@@ -481,9 +461,7 @@ def update(d, u):
@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")
-
+ self._verify_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
@@ -497,9 +475,7 @@ def emeter_today(self) -> Optional[float]:
@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")
-
+ self._verify_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
@@ -539,9 +515,7 @@ async def get_emeter_daily(
:param kwh: return usage in kWh (default: True)
:return: mapping of day of month to value
"""
- if not self.has_emeter:
- raise SmartDeviceException("Device has no emeter")
-
+ self._verify_emeter()
if year is None:
year = datetime.now().year
if month is None:
@@ -561,9 +535,7 @@ async def get_emeter_monthly(self, year: int = None, kwh: bool = True) -> Dict:
:param kwh: return usage in kWh (default: True)
:return: dict: mapping of month to value
"""
- if not self.has_emeter:
- raise SmartDeviceException("Device has no emeter")
-
+ self._verify_emeter()
if year is None:
year = datetime.now().year
@@ -576,19 +548,15 @@ async def get_emeter_monthly(self, year: int = None, kwh: bool = True) -> Dict:
@requires_update
async def erase_emeter_stats(self) -> Dict:
"""Erase energy meter statistics."""
- if not self.has_emeter:
- raise SmartDeviceException("Device has no emeter")
-
+ self._verify_emeter()
return await self._query_helper(self.emeter_type, "erase_emeter_stat", None)
@requires_update
async def current_consumption(self) -> float:
"""Get the current power consumption in Watt."""
- if not self.has_emeter:
- raise SmartDeviceException("Device has no emeter")
-
+ self._verify_emeter()
response = EmeterStatus(await self.get_emeter_realtime())
- return response["power"]
+ return float(response["power"])
async def reboot(self, delay: int = 1) -> None:
"""Reboot the device.
@@ -643,7 +611,8 @@ def state_information(self) -> Dict[str, Any]:
def device_id(self) -> str:
"""Return unique ID for the device.
- This is the MAC address of the device.
+ If not overridden, this is the MAC address of the device.
+ Individual sockets on strips will override this.
"""
return self.mac
@@ -725,6 +694,11 @@ def is_strip(self) -> bool:
"""Return True if the device is a strip."""
return self._device_type == DeviceType.Strip
+ @property
+ def is_strip_socket(self) -> bool:
+ """Return True if the device is a strip socket."""
+ return self._device_type == DeviceType.StripSocket
+
@property
def is_dimmer(self) -> bool:
"""Return True if the device is a dimmer."""
diff --git a/kasa/smartstrip.py b/kasa/smartstrip.py
index 9c8064dd5..c1235920d 100755
--- a/kasa/smartstrip.py
+++ b/kasa/smartstrip.py
@@ -6,6 +6,7 @@
from kasa.smartdevice import (
DeviceType,
+ EmeterStatus,
SmartDevice,
SmartDeviceException,
requires_update,
@@ -15,6 +16,15 @@
_LOGGER = logging.getLogger(__name__)
+def merge_sums(dicts):
+ """Merge the sum of dicts."""
+ total_dict: DefaultDict[int, float] = defaultdict(lambda: 0.0)
+ for sum_dict in dicts:
+ for day, value in sum_dict.items():
+ total_dict[day] += value
+ return total_dict
+
+
class SmartStrip(SmartDevice):
"""Representation of a TP-Link Smart Power Strip.
@@ -75,11 +85,7 @@ def __init__(self, host: str) -> None:
@requires_update
def is_on(self) -> bool:
"""Return if any of the outlets are on."""
- for plug in self.children:
- is_on = plug.is_on
- if is_on:
- return True
- return False
+ return any(plug.is_on for plug in self.children)
async def update(self):
"""Update some of the attributes.
@@ -97,15 +103,17 @@ async def update(self):
SmartStripPlug(self.host, parent=self, child_id=child["id"])
)
+ if self.has_emeter:
+ for plug in self.children:
+ await plug.update()
+
async def turn_on(self, **kwargs):
"""Turn the strip on."""
await self._query_helper("system", "set_relay_state", {"state": 1})
- await self.update()
async def turn_off(self, **kwargs):
"""Turn the strip off."""
await self._query_helper("system", "set_relay_state", {"state": 0})
- await self.update()
@property # type: ignore
@requires_update
@@ -126,7 +134,6 @@ def led(self) -> bool:
async def set_led(self, state: bool):
"""Set the state of the led (night mode)."""
await self._query_helper("system", "set_led_off", {"off": int(not state)})
- await self.update()
@property # type: ignore
@requires_update
@@ -143,16 +150,16 @@ def state_information(self) -> Dict[str, Any]:
async def current_consumption(self) -> float:
"""Get the current power consumption in watts."""
- consumption = sum(await plug.current_consumption() for plug in self.children)
-
- return consumption
+ return sum([await plug.current_consumption() for plug in self.children])
- async def set_alias(self, alias: str) -> None:
- """Set the alias for the strip.
-
- :param alias: new alias
- """
- return await super().set_alias(alias)
+ @requires_update
+ async def get_emeter_realtime(self) -> EmeterStatus:
+ """Retrieve current energy readings."""
+ emeter_rt = await self._async_get_emeter_sum("get_emeter_realtime", {})
+ # Voltage is averaged since each read will result
+ # in a slightly different voltage since they are not atomic
+ emeter_rt["voltage_mv"] = int(emeter_rt["voltage_mv"] / len(self.children))
+ return EmeterStatus(emeter_rt)
@requires_update
async def get_emeter_daily(
@@ -166,14 +173,9 @@ async def get_emeter_daily(
:param kwh: return usage in kWh (default: True)
:return: mapping of day of month to value
"""
- emeter_daily: DefaultDict[int, float] = defaultdict(lambda: 0.0)
- for plug in self.children:
- plug_emeter_daily = await plug.get_emeter_daily(
- year=year, month=month, kwh=kwh
- )
- for day, value in plug_emeter_daily.items():
- emeter_daily[day] += value
- return emeter_daily
+ return await self._async_get_emeter_sum(
+ "get_emeter_daily", {"year": year, "month": month, "kwh": kwh}
+ )
@requires_update
async def get_emeter_monthly(self, year: int = None, kwh: bool = True) -> Dict:
@@ -182,13 +184,16 @@ async def get_emeter_monthly(self, year: int = None, kwh: bool = True) -> Dict:
:param year: year for which to retrieve statistics (default: this year)
:param kwh: return usage in kWh (default: True)
"""
- emeter_monthly: DefaultDict[int, float] = defaultdict(lambda: 0.0)
- for plug in self.children:
- plug_emeter_monthly = await plug.get_emeter_monthly(year=year, kwh=kwh)
- for month, value in plug_emeter_monthly:
- emeter_monthly[month] += value
+ return await self._async_get_emeter_sum(
+ "get_emeter_monthly", {"year": year, "kwh": kwh}
+ )
- return emeter_monthly
+ async def _async_get_emeter_sum(self, func: str, kwargs: Dict[str, Any]) -> Dict:
+ """Retreive emeter stats for a time period from children."""
+ self._verify_emeter()
+ return merge_sums(
+ [await getattr(plug, func)(**kwargs) for plug in self.children]
+ )
@requires_update
async def erase_emeter_stats(self):
@@ -196,6 +201,28 @@ async def erase_emeter_stats(self):
for plug in self.children:
await plug.erase_emeter_stats()
+ @property # type: ignore
+ @requires_update
+ def emeter_this_month(self) -> Optional[float]:
+ """Return this month's energy consumption in kWh."""
+ return sum([plug.emeter_this_month for plug in self.children])
+
+ @property # type: ignore
+ @requires_update
+ def emeter_today(self) -> Optional[float]:
+ """Return this month's energy consumption in kWh."""
+ return sum([plug.emeter_today for plug in self.children])
+
+ @property # type: ignore
+ @requires_update
+ def emeter_realtime(self) -> EmeterStatus:
+ """Return current energy readings."""
+ emeter = merge_sums([plug.emeter_realtime for plug in self.children])
+ # Voltage is averaged since each read will result
+ # in a slightly different voltage since they are not atomic
+ emeter["voltage_mv"] = int(emeter["voltage_mv"] / len(self.children))
+ return EmeterStatus(emeter)
+
class SmartStripPlug(SmartPlug):
"""Representation of a single socket in a power strip.
@@ -214,14 +241,25 @@ def __init__(self, host: str, parent: "SmartStrip", child_id: str) -> None:
self.child_id = child_id
self._last_update = parent._last_update
self._sys_info = parent._sys_info
+ self._device_type = DeviceType.StripSocket
async def update(self):
- """Override the update to no-op and inform the user."""
- _LOGGER.warning(
- "You called update() on a child device, which has no effect."
- "Call update() on the parent device instead."
+ """Query the device to update the data.
+
+ Needed for properties that are decorated with `requires_update`.
+ """
+ self._last_update = await self.parent.protocol.query(
+ self.host, self._create_emeter_request()
)
- return
+
+ def _create_request(
+ self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None
+ ):
+ request: Dict[str, Any] = {
+ "context": {"child_ids": [self.child_id]},
+ target: {cmd: arg},
+ }
+ return request
async def _query_helper(
self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None
@@ -247,12 +285,6 @@ def led(self) -> bool:
"""
return False
- @property # type: ignore
- @requires_update
- def has_emeter(self) -> bool:
- """Children have no emeter to my knowledge."""
- return False
-
@property # type: ignore
@requires_update
def device_id(self) -> str:
diff --git a/kasa/tests/conftest.py b/kasa/tests/conftest.py
index 456fe3ab5..46cb86216 100644
--- a/kasa/tests/conftest.py
+++ b/kasa/tests/conftest.py
@@ -30,12 +30,12 @@
BULBS = {"KL60", "LB100", *VARIABLE_TEMP, *COLOR_BULBS, *LIGHT_STRIPS}
-PLUGS = {"HS100", "HS103", "HS105", "HS110", "HS200", "HS210"}
+PLUGS = {"HS100", "HS103", "HS105", "HS110", "HS200", "HS210", "EP10", "KP115"}
STRIPS = {"HS107", "HS300", "KP303", "KP400"}
DIMMERS = {"HS220"}
DIMMABLE = {*BULBS, *DIMMERS}
-WITH_EMETER = {"HS110", "HS300", *BULBS, *STRIPS}
+WITH_EMETER = {"HS110", "HS300", "KP115", *BULBS}
ALL_DEVICES = BULBS.union(PLUGS).union(STRIPS).union(DIMMERS)
@@ -147,13 +147,13 @@ def get_device_for_file(file):
with open(p) as f:
sysinfo = json.load(f)
model = basename(file)
- p = device_for_file(model)(host="123.123.123.123")
+ p = device_for_file(model)(host="127.0.0.123")
p.protocol = FakeTransportProtocol(sysinfo)
asyncio.run(p.update())
return p
-@pytest.fixture(params=SUPPORTED_DEVICES)
+@pytest.fixture(params=SUPPORTED_DEVICES, scope="session")
def dev(request):
"""Device fixture.
@@ -168,21 +168,29 @@ def dev(request):
asyncio.run(d.update())
if d.model in file:
return d
- raise Exception("Unable to find type for %s" % ip)
+ else:
+ pytest.skip(f"skipping file {file}")
return get_device_for_file(file)
def pytest_addoption(parser):
- parser.addoption("--ip", action="store", default=None, help="run against device")
+ parser.addoption(
+ "--ip", action="store", default=None, help="run against device on given ip"
+ )
def pytest_collection_modifyitems(config, items):
if not config.getoption("--ip"):
print("Testing against fixtures.")
- return
else:
print("Running against ip %s" % config.getoption("--ip"))
+ requires_dummy = pytest.mark.skip(
+ reason="test requires to be run against dummy data"
+ )
+ for item in items:
+ if "requires_dummy" in item.keywords:
+ item.add_marker(requires_dummy)
# allow mocks to be awaited
diff --git a/kasa/tests/fixtures/EP10(US)_1.0_1.0.2.json b/kasa/tests/fixtures/EP10(US)_1.0_1.0.2.json
new file mode 100755
index 000000000..e40543d6b
--- /dev/null
+++ b/kasa/tests/fixtures/EP10(US)_1.0_1.0.2.json
@@ -0,0 +1,32 @@
+{
+ "system": {
+ "get_sysinfo": {
+ "active_mode": "none",
+ "alias": "167 lamp",
+ "dev_name": "Smart Wi-Fi Plug Mini",
+ "deviceId": "0000000000000000000000000000000000000000",
+ "err_code": 0,
+ "feature": "TIM",
+ "hwId": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "icon_hash": "",
+ "latitude_i": 0,
+ "led_off": 0,
+ "longitude_i": 0,
+ "mac": "00:00:00:00:00:00",
+ "mic_type": "IOT.SMARTPLUGSWITCH",
+ "model": "EP10(US)",
+ "next_action": {
+ "type": -1
+ },
+ "obd_src": "tplink",
+ "oemId": "00000000000000000000000000000000",
+ "on_time": 0,
+ "relay_state": 0,
+ "rssi": -64,
+ "status": "new",
+ "sw_ver": "1.0.2 Build 200915 Rel.085940",
+ "updating": 0
+ }
+ }
+}
diff --git a/kasa/tests/fixtures/KL430(UN)_2.0_1.0.8.json b/kasa/tests/fixtures/KL430(UN)_2.0_1.0.8.json
new file mode 100644
index 000000000..a956575be
--- /dev/null
+++ b/kasa/tests/fixtures/KL430(UN)_2.0_1.0.8.json
@@ -0,0 +1,90 @@
+{
+ "smartlife.iot.common.emeter": {
+ "get_realtime": {
+ "err_code": 0,
+ "power_mw": 16760,
+ "total_wh": 120
+ }
+ },
+ "system": {
+ "get_sysinfo": {
+ "LEF": 1,
+ "active_mode": "none",
+ "alias": "Bedroom light strip",
+ "ctrl_protocols": {
+ "name": "Linkie",
+ "version": "1.0"
+ },
+ "description": "Kasa Smart Light Strip, Multicolor",
+ "dev_state": "normal",
+ "deviceId": "0000000000000000000000000000000000000000",
+ "disco_ver": "1.0",
+ "err_code": 0,
+ "hwId": "00000000000000000000000000000000",
+ "hw_ver": "2.0",
+ "is_color": 1,
+ "is_dimmable": 1,
+ "is_factory": false,
+ "is_variable_color_temp": 1,
+ "latitude_i": 0,
+ "length": 16,
+ "light_state": {
+ "brightness": 100,
+ "color_temp": 9000,
+ "hue": 0,
+ "mode": "normal",
+ "on_off": 1,
+ "saturation": 0
+ },
+ "lighting_effect_state": {
+ "brightness": 100,
+ "custom": 1,
+ "enable": 1,
+ "id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN",
+ "name": "Aurora 1"
+ },
+ "longitude_i": 0,
+ "mic_mac": "00:00:00:00:00:00",
+ "mic_type": "IOT.SMARTBULB",
+ "model": "KL430(UN)",
+ "oemId": "00000000000000000000000000000000",
+ "preferred_state": [
+ {
+ "brightness": 100,
+ "color_temp": 9000,
+ "hue": 0,
+ "index": 0,
+ "mode": 1,
+ "saturation": 0
+ },
+ {
+ "brightness": 100,
+ "color_temp": 0,
+ "hue": 217,
+ "index": 1,
+ "mode": 1,
+ "saturation": 99
+ },
+ {
+ "brightness": 100,
+ "color_temp": 0,
+ "hue": 194,
+ "index": 2,
+ "mode": 1,
+ "saturation": 50
+ },
+ {
+ "brightness": 100,
+ "color_temp": 0,
+ "hue": 277,
+ "index": 3,
+ "mode": 1,
+ "saturation": 86
+ }
+ ],
+ "rssi": -43,
+ "status": "new",
+ "sw_ver": "1.0.8 Build 210121 Rel.084339"
+ }
+ }
+}
diff --git a/kasa/tests/fixtures/KP115(EU)_1.0_1.0.16.json b/kasa/tests/fixtures/KP115(EU)_1.0_1.0.16.json
new file mode 100644
index 000000000..790597dc0
--- /dev/null
+++ b/kasa/tests/fixtures/KP115(EU)_1.0_1.0.16.json
@@ -0,0 +1,42 @@
+{
+ "emeter": {
+ "get_realtime": {
+ "current_ma": 0,
+ "err_code": 0,
+ "power_mw": 0,
+ "total_wh": 0,
+ "voltage_mv": 231561
+ }
+ },
+ "system": {
+ "get_sysinfo": {
+ "active_mode": "none",
+ "alias": "TP-LINK_Smart Plug_330B",
+ "dev_name": "Smart Wi-Fi Plug Mini",
+ "deviceId": "0000000000000000000000000000000000000000",
+ "err_code": 0,
+ "feature": "TIM:ENE",
+ "hwId": "00000000000000000000000000000000",
+ "hw_ver": "1.0",
+ "icon_hash": "",
+ "latitude_i": 0,
+ "led_off": 0,
+ "longitude_i": 0,
+ "mac": "00:00:00:00:00:00",
+ "mic_type": "IOT.SMARTPLUGSWITCH",
+ "model": "KP115(EU)",
+ "next_action": {
+ "type": -1
+ },
+ "ntc_state": 0,
+ "obd_src": "tplink",
+ "oemId": "00000000000000000000000000000000",
+ "on_time": 197,
+ "relay_state": 1,
+ "rssi": -70,
+ "status": "new",
+ "sw_ver": "1.0.16 Build 210205 Rel.163735",
+ "updating": 0
+ }
+ }
+}
diff --git a/kasa/tests/newfakes.py b/kasa/tests/newfakes.py
index 73eed0f37..a37bb4147 100644
--- a/kasa/tests/newfakes.py
+++ b/kasa/tests/newfakes.py
@@ -124,7 +124,7 @@ def lb_dev_state(x):
"dft_on_state": Optional(
{
"brightness": All(int, Range(min=0, max=100)),
- "color_temp": All(int, Range(min=2000, max=9000)),
+ "color_temp": All(int, Range(min=0, max=9000)),
"hue": All(int, Range(min=0, max=255)),
"mode": str,
"saturation": All(int, Range(min=0, max=255)),
@@ -150,9 +150,9 @@ def lb_dev_state(x):
{
"brightness": All(int, Range(min=0, max=100)),
"color_temp": int,
- "hue": All(int, Range(min=0, max=255)),
+ "hue": All(int, Range(min=0, max=360)),
"index": int,
- "saturation": All(int, Range(min=0, max=255)),
+ "saturation": All(int, Range(min=0, max=100)),
}
],
}
@@ -252,6 +252,27 @@ def success(res):
return res
+# plugs and bulbs use a different module for time information,
+# so we define the contents here to avoid repeating ourselves
+TIME_MODULE = {
+ "get_time": {
+ "year": 2017,
+ "month": 1,
+ "mday": 2,
+ "hour": 3,
+ "min": 4,
+ "sec": 5,
+ },
+ "get_timezone": {
+ "zone_str": "test",
+ "dst_offset": -1,
+ "index": 12,
+ "tz_str": "test2",
+ },
+ "set_timezone": None,
+}
+
+
class FakeTransportProtocol(TPLinkSmartHomeProtocol):
def __init__(self, info):
self.discovery_data = info
@@ -393,23 +414,11 @@ def light_state(self, x, *args):
"set_light_state": transition_light_state,
"get_light_state": light_state,
},
- "time": {
- "get_time": {
- "year": 2017,
- "month": 1,
- "mday": 2,
- "hour": 3,
- "min": 4,
- "sec": 5,
- },
- "get_timezone": {
- "zone_str": "test",
- "dst_offset": -1,
- "index": 12,
- "tz_str": "test2",
- },
- "set_timezone": None,
+ "smartlife.iot.common.system": {
+ "set_dev_alias": set_alias,
},
+ "time": TIME_MODULE,
+ "smartlife.iot.common.timesetting": TIME_MODULE,
# HS220 brightness, different setter and getter
"smartlife.iot.dimmer": {
"set_brightness": set_hs220_brightness,
diff --git a/kasa/tests/test_bulb.py b/kasa/tests/test_bulb.py
index 7d6e45e02..28fcd4cb7 100644
--- a/kasa/tests/test_bulb.py
+++ b/kasa/tests/test_bulb.py
@@ -66,6 +66,7 @@ async def test_hsv(dev, turn_on):
await dev.set_hsv(hue=1, saturation=1, value=1)
+ await dev.update()
hue, saturation, brightness = dev.hsv
assert hue == 1
assert saturation == 1
@@ -134,6 +135,7 @@ async def test_variable_temp_state_information(dev):
async def test_try_set_colortemp(dev, turn_on):
await handle_turn_on(dev, turn_on)
await dev.set_color_temp(2700)
+ await dev.update()
assert dev.color_temp == 2700
@@ -146,10 +148,11 @@ async def test_set_color_temp_transition(dev, mocker):
@variable_temp
-async def test_unknown_temp_range(dev, monkeypatch):
- with pytest.raises(SmartDeviceException):
- monkeypatch.setitem(dev._sys_info, "model", "unknown bulb")
- dev.valid_temperature_range()
+async def test_unknown_temp_range(dev, monkeypatch, caplog):
+ monkeypatch.setitem(dev._sys_info, "model", "unknown bulb")
+
+ assert dev.valid_temperature_range == (2700, 5000)
+ assert "Unknown color temperature range, fallback to 2700-5000" in caplog.text
@variable_temp
@@ -179,9 +182,11 @@ async def test_dimmable_brightness(dev, turn_on):
assert dev.is_dimmable
await dev.set_brightness(50)
+ await dev.update()
assert dev.brightness == 50
await dev.set_brightness(10)
+ await dev.update()
assert dev.brightness == 10
with pytest.raises(ValueError):
diff --git a/kasa/tests/test_cli.py b/kasa/tests/test_cli.py
index 1b94d4897..864adb21d 100644
--- a/kasa/tests/test_cli.py
+++ b/kasa/tests/test_cli.py
@@ -18,7 +18,7 @@ async def test_state(dev, turn_on):
await handle_turn_on(dev, turn_on)
runner = CliRunner()
res = await runner.invoke(state, obj=dev)
- print(res.output)
+ await dev.update()
if dev.is_on:
assert "Device state: ON" in res.output
@@ -32,6 +32,8 @@ async def test_alias(dev):
res = await runner.invoke(alias, obj=dev)
assert f"Alias: {dev.alias}" in res.output
+ old_alias = dev.alias
+
new_alias = "new alias"
res = await runner.invoke(alias, [new_alias], obj=dev)
assert f"Setting alias to {new_alias}" in res.output
@@ -39,6 +41,8 @@ async def test_alias(dev):
res = await runner.invoke(alias, obj=dev)
assert f"Alias: {new_alias}" in res.output
+ await dev.set_alias(old_alias)
+
async def test_raw_command(dev):
runner = CliRunner()
@@ -63,11 +67,13 @@ async def test_emeter(dev: SmartDevice, mocker):
assert "== Emeter ==" in res.output
monthly = mocker.patch.object(dev, "get_emeter_monthly")
+ monthly.return_value = []
res = await runner.invoke(emeter, ["--year", "1900"], obj=dev)
assert "For year" in res.output
monthly.assert_called()
daily = mocker.patch.object(dev, "get_emeter_daily")
+ daily.return_value = []
res = await runner.invoke(emeter, ["--month", "1900-12"], obj=dev)
assert "For month" in res.output
daily.assert_called()
diff --git a/kasa/tests/test_discovery.py b/kasa/tests/test_discovery.py
index 529ad8d63..1356892e9 100644
--- a/kasa/tests/test_discovery.py
+++ b/kasa/tests/test_discovery.py
@@ -8,14 +8,14 @@
@plug
async def test_type_detection_plug(dev: SmartDevice):
- d = Discover._get_device_class(dev.protocol.discovery_data)("localhost")
+ d = Discover._get_device_class(dev._last_update)("localhost")
assert d.is_plug
assert d.device_type == DeviceType.Plug
@bulb
async def test_type_detection_bulb(dev: SmartDevice):
- d = Discover._get_device_class(dev.protocol.discovery_data)("localhost")
+ d = Discover._get_device_class(dev._last_update)("localhost")
# TODO: light_strip is a special case for now to force bulb tests on it
if not d.is_light_strip:
assert d.is_bulb
@@ -24,21 +24,21 @@ async def test_type_detection_bulb(dev: SmartDevice):
@strip
async def test_type_detection_strip(dev: SmartDevice):
- d = Discover._get_device_class(dev.protocol.discovery_data)("localhost")
+ d = Discover._get_device_class(dev._last_update)("localhost")
assert d.is_strip
assert d.device_type == DeviceType.Strip
@dimmer
async def test_type_detection_dimmer(dev: SmartDevice):
- d = Discover._get_device_class(dev.protocol.discovery_data)("localhost")
+ d = Discover._get_device_class(dev._last_update)("localhost")
assert d.is_dimmer
assert d.device_type == DeviceType.Dimmer
@lightstrip
async def test_type_detection_lightstrip(dev: SmartDevice):
- d = Discover._get_device_class(dev.protocol.discovery_data)("localhost")
+ d = Discover._get_device_class(dev._last_update)("localhost")
assert d.is_light_strip
assert d.device_type == DeviceType.LightStrip
diff --git a/kasa/tests/test_emeter.py b/kasa/tests/test_emeter.py
index 907f24787..b3d567dd6 100644
--- a/kasa/tests/test_emeter.py
+++ b/kasa/tests/test_emeter.py
@@ -1,6 +1,6 @@
import pytest
-from kasa import SmartDeviceException
+from kasa import EmeterStatus, SmartDeviceException
from .conftest import has_emeter, no_emeter, pytestmark
from .newfakes import CURRENT_CONSUMPTION_SCHEMA
@@ -22,9 +22,6 @@ async def test_no_emeter(dev):
@has_emeter
async def test_get_emeter_realtime(dev):
- if dev.is_strip:
- pytest.skip("Disabled for strips temporarily")
-
assert dev.has_emeter
current_emeter = await dev.get_emeter_realtime()
@@ -32,10 +29,8 @@ async def test_get_emeter_realtime(dev):
@has_emeter
+@pytest.mark.requires_dummy
async def test_get_emeter_daily(dev):
- if dev.is_strip:
- pytest.skip("Disabled for strips temporarily")
-
assert dev.has_emeter
assert await dev.get_emeter_daily(year=1900, month=1) == {}
@@ -54,10 +49,8 @@ async def test_get_emeter_daily(dev):
@has_emeter
+@pytest.mark.requires_dummy
async def test_get_emeter_monthly(dev):
- if dev.is_strip:
- pytest.skip("Disabled for strips temporarily")
-
assert dev.has_emeter
assert await dev.get_emeter_monthly(year=1900) == {}
@@ -77,9 +70,6 @@ async def test_get_emeter_monthly(dev):
@has_emeter
async def test_emeter_status(dev):
- if dev.is_strip:
- pytest.skip("Disabled for strips temporarily")
-
assert dev.has_emeter
d = await dev.get_emeter_realtime()
@@ -89,7 +79,7 @@ async def test_emeter_status(dev):
assert d["power_mw"] == d["power"] * 1000
# bulbs have only power according to tplink simulator.
- if not dev.is_bulb:
+ if not dev.is_bulb and not dev.is_light_strip:
assert d["voltage_mv"] == d["voltage"] * 1000
assert d["current_ma"] == d["current"] * 1000
@@ -106,9 +96,6 @@ async def test_erase_emeter_stats(dev):
@has_emeter
async def test_current_consumption(dev):
- if dev.is_strip:
- pytest.skip("Disabled for strips temporarily")
-
if dev.has_emeter:
x = await dev.current_consumption()
assert isinstance(x, float)
@@ -119,8 +106,6 @@ async def test_current_consumption(dev):
async def test_emeterstatus_missing_current():
"""KL125 does not report 'current' for emeter."""
- from kasa import EmeterStatus
-
regular = EmeterStatus(
{"err_code": 0, "power_mw": 0, "total_wh": 13, "current_ma": 123}
)
diff --git a/kasa/tests/test_plug.py b/kasa/tests/test_plug.py
index a301095e6..3de3a1461 100644
--- a/kasa/tests/test_plug.py
+++ b/kasa/tests/test_plug.py
@@ -20,9 +20,11 @@ async def test_led(dev):
original = dev.led
await dev.set_led(False)
+ await dev.update()
assert not dev.led
await dev.set_led(True)
+ await dev.update()
assert dev.led
await dev.set_led(original)
diff --git a/kasa/tests/test_smartdevice.py b/kasa/tests/test_smartdevice.py
index 67081b034..380cdd1fb 100644
--- a/kasa/tests/test_smartdevice.py
+++ b/kasa/tests/test_smartdevice.py
@@ -4,8 +4,9 @@
import pytest # type: ignore # https://github.com/pytest-dev/pytest/issues/3342
from kasa import SmartDeviceException
+from kasa.smartstrip import SmartStripPlug
-from .conftest import handle_turn_on, pytestmark, turn_on
+from .conftest import handle_turn_on, has_emeter, no_emeter, pytestmark, turn_on
from .newfakes import PLUG_SCHEMA, TZ_SCHEMA, FakeTransportProtocol
@@ -13,11 +14,29 @@ async def test_state_info(dev):
assert isinstance(dev.state_information, dict)
+@pytest.mark.requires_dummy
async def test_invalid_connection(dev):
with patch.object(FakeTransportProtocol, "query", side_effect=SmartDeviceException):
with pytest.raises(SmartDeviceException):
await dev.update()
- dev.is_on
+
+
+@has_emeter
+async def test_initial_update_emeter(dev, mocker):
+ """Test that the initial update performs second query if emeter is available."""
+ dev._last_update = None
+ spy = mocker.spy(dev.protocol, "query")
+ await dev.update()
+ assert spy.call_count == 2 + len(dev.children)
+
+
+@no_emeter
+async def test_initial_update_no_emeter(dev, mocker):
+ """Test that the initial update performs second query if emeter is available."""
+ dev._last_update = None
+ spy = mocker.spy(dev.protocol, "query")
+ await dev.update()
+ assert spy.call_count == 1
async def test_query_helper(dev):
@@ -32,18 +51,22 @@ async def test_state(dev, turn_on):
orig_state = dev.is_on
if orig_state:
await dev.turn_off()
+ await dev.update()
assert not dev.is_on
assert dev.is_off
await dev.turn_on()
+ await dev.update()
assert dev.is_on
assert not dev.is_off
else:
await dev.turn_on()
+ await dev.update()
assert dev.is_on
assert not dev.is_off
await dev.turn_off()
+ await dev.update()
assert not dev.is_on
assert dev.is_off
@@ -54,9 +77,11 @@ async def test_alias(dev):
assert isinstance(original, str)
await dev.set_alias(test_alias)
+ await dev.update()
assert dev.alias == test_alias
await dev.set_alias(original)
+ await dev.update()
assert dev.alias == original
diff --git a/kasa/tests/test_strip.py b/kasa/tests/test_strip.py
index 861b56ed3..21a11e372 100644
--- a/kasa/tests/test_strip.py
+++ b/kasa/tests/test_strip.py
@@ -15,20 +15,24 @@ async def test_children_change_state(dev, turn_on):
orig_state = plug.is_on
if orig_state:
await plug.turn_off()
- assert not plug.is_on
- assert plug.is_off
+ await dev.update()
+ assert plug.is_on is False
+ assert plug.is_off is True
await plug.turn_on()
- assert plug.is_on
- assert not plug.is_off
+ await dev.update()
+ assert plug.is_on is True
+ assert plug.is_off is False
else:
await plug.turn_on()
- assert plug.is_on
- assert not plug.is_off
+ await dev.update()
+ assert plug.is_on is True
+ assert plug.is_off is False
await plug.turn_off()
- assert not plug.is_on
- assert plug.is_off
+ await dev.update()
+ assert plug.is_on is False
+ assert plug.is_off is True
@strip
diff --git a/poetry.lock b/poetry.lock
index 534afae8b..63bbc2c22 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -8,7 +8,7 @@ python-versions = "*"
[[package]]
name = "anyio"
-version = "3.1.0"
+version = "3.3.1"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
category = "main"
optional = false
@@ -24,14 +24,6 @@ doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"]
test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"]
trio = ["trio (>=0.16)"]
-[[package]]
-name = "appdirs"
-version = "1.4.4"
-description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
-category = "dev"
-optional = false
-python-versions = "*"
-
[[package]]
name = "asyncclick"
version = "7.1.2.3"
@@ -80,6 +72,21 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
pytz = ">=2015.7"
+[[package]]
+name = "backports.entry-points-selectable"
+version = "1.1.0"
+description = "Compatibility shim providing selectable entry points for older implementations"
+category = "dev"
+optional = false
+python-versions = ">=2.7"
+
+[package.dependencies]
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"]
+
[[package]]
name = "certifi"
version = "2021.5.30"
@@ -90,23 +97,26 @@ python-versions = "*"
[[package]]
name = "cfgv"
-version = "3.3.0"
+version = "3.3.1"
description = "Validate configuration and produce human readable error messages."
category = "dev"
optional = false
python-versions = ">=3.6.1"
[[package]]
-name = "chardet"
-version = "4.0.0"
-description = "Universal encoding detector for Python 2 and 3"
+name = "charset-normalizer"
+version = "2.0.6"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = ">=3.5.0"
+
+[package.extras]
+unicode_backport = ["unicodedata2"]
[[package]]
name = "codecov"
-version = "2.1.11"
+version = "2.1.12"
description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab"
category = "dev"
optional = false
@@ -137,7 +147,7 @@ toml = ["toml"]
[[package]]
name = "distlib"
-version = "0.3.2"
+version = "0.3.3"
description = "Distribution utilities"
category = "dev"
optional = false
@@ -161,7 +171,7 @@ python-versions = "*"
[[package]]
name = "identify"
-version = "2.2.10"
+version = "2.2.15"
description = "File identification library for Python"
category = "dev"
optional = false
@@ -172,11 +182,11 @@ license = ["editdistance-s"]
[[package]]
name = "idna"
-version = "2.10"
+version = "3.2"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=3.5"
[[package]]
name = "imagesize"
@@ -188,7 +198,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "importlib-metadata"
-version = "4.5.0"
+version = "4.8.1"
description = "Read metadata from Python packages"
category = "main"
optional = false
@@ -200,7 +210,8 @@ zipp = ">=0.5"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
-testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
+perf = ["ipython"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
[[package]]
name = "jinja2"
@@ -246,7 +257,7 @@ python-versions = "*"
[[package]]
name = "more-itertools"
-version = "8.8.0"
+version = "8.10.0"
description = "More routines for operating on iterables, beyond itertools"
category = "dev"
optional = false
@@ -262,15 +273,27 @@ python-versions = "*"
[[package]]
name = "packaging"
-version = "20.9"
+version = "21.0"
description = "Core utilities for Python packages"
category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=3.6"
[package.dependencies]
pyparsing = ">=2.0.2"
+[[package]]
+name = "platformdirs"
+version = "2.3.0"
+description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
+test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
+
[[package]]
name = "pluggy"
version = "0.13.1"
@@ -287,7 +310,7 @@ dev = ["pre-commit", "tox"]
[[package]]
name = "pre-commit"
-version = "2.13.0"
+version = "2.15.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "dev"
optional = false
@@ -312,7 +335,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pygments"
-version = "2.9.0"
+version = "2.10.0"
description = "Pygments is a syntax highlighting package written in Python."
category = "main"
optional = true
@@ -363,17 +386,6 @@ pytest = ">=5.4.0"
[package.extras]
testing = ["coverage", "hypothesis (>=5.7.1)"]
-[[package]]
-name = "pytest-azurepipelines"
-version = "0.8.0"
-description = "Formatting PyTest output for Azure Pipelines UI"
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-
-[package.dependencies]
-pytest = ">=3.5.0"
-
[[package]]
name = "pytest-cov"
version = "2.12.1"
@@ -435,21 +447,21 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[[package]]
name = "requests"
-version = "2.25.1"
+version = "2.26.0"
description = "Python HTTP for Humans."
category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[package.dependencies]
certifi = ">=2017.4.17"
-chardet = ">=3.0.2,<5"
-idna = ">=2.5,<3"
+charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
+idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
urllib3 = ">=1.21.1,<1.27"
[package.extras]
-security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
+use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
[[package]]
name = "six"
@@ -621,7 +633,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "tox"
-version = "3.23.1"
+version = "3.24.4"
description = "tox is a generic virtualenv management and test command line tool"
category = "dev"
optional = false
@@ -644,7 +656,7 @@ testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "psutil (>=5.6.1)", "pytes
[[package]]
name = "typing-extensions"
-version = "3.10.0.0"
+version = "3.10.0.2"
description = "Backported and Experimental Type Hints for Python 3.5+"
category = "main"
optional = false
@@ -652,7 +664,7 @@ python-versions = "*"
[[package]]
name = "urllib3"
-version = "1.26.5"
+version = "1.26.7"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
@@ -665,26 +677,27 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "virtualenv"
-version = "20.4.7"
+version = "20.8.0"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[package.dependencies]
-appdirs = ">=1.4.3,<2"
+"backports.entry-points-selectable" = ">=1.0.4"
distlib = ">=0.3.1,<1"
filelock = ">=3.0.0,<4"
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+platformdirs = ">=2,<3"
six = ">=1.9.0,<2"
[package.extras]
docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"]
-testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)", "xonsh (>=0.9.16)"]
+testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"]
[[package]]
name = "voluptuous"
-version = "0.12.1"
+version = "0.12.2"
description = ""
category = "dev"
optional = false
@@ -700,7 +713,7 @@ python-versions = "*"
[[package]]
name = "xdoctest"
-version = "0.15.4"
+version = "0.15.8"
description = "A rewrite of the builtin doctest module"
category = "dev"
optional = false
@@ -710,15 +723,15 @@ python-versions = "*"
six = "*"
[package.extras]
-all = ["six", "pytest", "pytest-cov", "codecov", "scikit-build", "cmake", "ninja", "pybind11", "pygments", "colorama", "nbformat", "nbconvert", "jupyter-client", "ipython", "ipykernel"]
+all = ["six", "codecov", "scikit-build", "cmake", "ninja", "pybind11", "pygments", "colorama", "pytest", "pytest", "pytest-cov", "pytest", "pytest", "pytest-cov", "typing", "nbformat", "nbconvert", "jupyter-client", "ipython", "ipykernel", "pytest", "pytest-cov"]
colors = ["pygments", "colorama"]
jupyter = ["nbformat", "nbconvert", "jupyter-client", "ipython", "ipykernel"]
optional = ["pygments", "colorama", "nbformat", "nbconvert", "jupyter-client", "ipython", "ipykernel"]
-tests = ["pytest", "pytest-cov", "codecov", "scikit-build", "cmake", "ninja", "pybind11", "nbformat", "nbconvert", "jupyter-client", "ipython", "ipykernel"]
+tests = ["codecov", "scikit-build", "cmake", "ninja", "pybind11", "pytest", "pytest", "pytest-cov", "pytest", "pytest", "pytest-cov", "typing", "nbformat", "nbconvert", "jupyter-client", "ipython", "ipykernel", "pytest", "pytest-cov"]
[[package]]
name = "zipp"
-version = "3.4.1"
+version = "3.5.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "main"
optional = false
@@ -726,7 +739,7 @@ python-versions = ">=3.6"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
-testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
[extras]
docs = ["sphinx", "sphinx_rtd_theme", "m2r", "sphinxcontrib-programoutput"]
@@ -734,7 +747,7 @@ docs = ["sphinx", "sphinx_rtd_theme", "m2r", "sphinxcontrib-programoutput"]
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
-content-hash = "c73c14c7f8588e3be3cd04a1b8cdcbcc32f2d042d8e30b58b7084b2b544ddb90"
+content-hash = "e388fa366e9423e60697bfa77c37151094ca0367eb3ed441d61bb8cc7f055675"
[metadata.files]
alabaster = [
@@ -742,12 +755,8 @@ alabaster = [
{file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
]
anyio = [
- {file = "anyio-3.1.0-py3-none-any.whl", hash = "sha256:5e335cef65fbd1a422bbfbb4722e8e9a9fadbd8c06d5afe9cd614d12023f6e5a"},
- {file = "anyio-3.1.0.tar.gz", hash = "sha256:43e20711a9d003d858d694c12356dc44ab82c03ccc5290313c3392fa349dad0e"},
-]
-appdirs = [
- {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
- {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
+ {file = "anyio-3.3.1-py3-none-any.whl", hash = "sha256:d7c604dd491eca70e19c78664d685d5e4337612d574419d503e76f5d7d1590bd"},
+ {file = "anyio-3.3.1.tar.gz", hash = "sha256:85913b4e2fec030e8c72a8f9f98092eeb9e25847a6e00d567751b77e34f856fe"},
]
asyncclick = [
{file = "asyncclick-7.1.2.3.tar.gz", hash = "sha256:c26962b9957abe7ae09c058afbfea199dedea1b54343c1cc2ae1a6a291fab333"},
@@ -764,22 +773,26 @@ babel = [
{file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"},
{file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"},
]
+"backports.entry-points-selectable" = [
+ {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"},
+ {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"},
+]
certifi = [
{file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
{file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"},
]
cfgv = [
- {file = "cfgv-3.3.0-py2.py3-none-any.whl", hash = "sha256:b449c9c6118fe8cca7fa5e00b9ec60ba08145d281d52164230a69211c5d597a1"},
- {file = "cfgv-3.3.0.tar.gz", hash = "sha256:9e600479b3b99e8af981ecdfc80a0296104ee610cab48a5ae4ffd0b668650eb1"},
+ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
+ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
]
-chardet = [
- {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
- {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
+charset-normalizer = [
+ {file = "charset-normalizer-2.0.6.tar.gz", hash = "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"},
+ {file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"},
]
codecov = [
- {file = "codecov-2.1.11-py2.py3-none-any.whl", hash = "sha256:ba8553a82942ce37d4da92b70ffd6d54cf635fc1793ab0a7dc3fecd6ebfb3df8"},
- {file = "codecov-2.1.11-py3.8.egg", hash = "sha256:e95901d4350e99fc39c8353efa450050d2446c55bac91d90fcfd2354e19a6aef"},
- {file = "codecov-2.1.11.tar.gz", hash = "sha256:6cde272454009d27355f9434f4e49f238c0273b216beda8472a65dc4957f473b"},
+ {file = "codecov-2.1.12-py2.py3-none-any.whl", hash = "sha256:585dc217dc3d8185198ceb402f85d5cb5dbfa0c5f350a5abcdf9e347776a5b47"},
+ {file = "codecov-2.1.12-py3.8.egg", hash = "sha256:782a8e5352f22593cbc5427a35320b99490eb24d9dcfa2155fd99d2b75cfb635"},
+ {file = "codecov-2.1.12.tar.gz", hash = "sha256:a0da46bb5025426da895af90938def8ee12d37fcbcbbbc15b6dc64cf7ebc51c1"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
@@ -840,8 +853,8 @@ coverage = [
{file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"},
]
distlib = [
- {file = "distlib-0.3.2-py2.py3-none-any.whl", hash = "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c"},
- {file = "distlib-0.3.2.zip", hash = "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736"},
+ {file = "distlib-0.3.3-py2.py3-none-any.whl", hash = "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31"},
+ {file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"},
]
docutils = [
{file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"},
@@ -852,20 +865,20 @@ filelock = [
{file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"},
]
identify = [
- {file = "identify-2.2.10-py2.py3-none-any.whl", hash = "sha256:18d0c531ee3dbc112fa6181f34faa179de3f57ea57ae2899754f16a7e0ff6421"},
- {file = "identify-2.2.10.tar.gz", hash = "sha256:5b41f71471bc738e7b586308c3fca172f78940195cb3bf6734c1e66fdac49306"},
+ {file = "identify-2.2.15-py2.py3-none-any.whl", hash = "sha256:de83a84d774921669774a2000bf87ebba46b4d1c04775f4a5d37deff0cf39f73"},
+ {file = "identify-2.2.15.tar.gz", hash = "sha256:528a88021749035d5a39fe2ba67c0642b8341aaf71889da0e1ed669a429b87f0"},
]
idna = [
- {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
- {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
+ {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"},
+ {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"},
]
imagesize = [
{file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"},
{file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"},
]
importlib-metadata = [
- {file = "importlib_metadata-4.5.0-py3-none-any.whl", hash = "sha256:833b26fb89d5de469b24a390e9df088d4e52e4ba33b01dc5e0e4f41b81a16c00"},
- {file = "importlib_metadata-4.5.0.tar.gz", hash = "sha256:b142cc1dd1342f31ff04bb7d022492b09920cb64fed867cd3ea6f80fe3ebd139"},
+ {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"},
+ {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"},
]
jinja2 = [
{file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"},
@@ -915,32 +928,36 @@ mistune = [
{file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"},
]
more-itertools = [
- {file = "more-itertools-8.8.0.tar.gz", hash = "sha256:83f0308e05477c68f56ea3a888172c78ed5d5b3c282addb67508e7ba6c8f813a"},
- {file = "more_itertools-8.8.0-py3-none-any.whl", hash = "sha256:2cf89ec599962f2ddc4d568a05defc40e0a587fbc10d5989713638864c36be4d"},
+ {file = "more-itertools-8.10.0.tar.gz", hash = "sha256:1debcabeb1df793814859d64a81ad7cb10504c24349368ccf214c664c474f41f"},
+ {file = "more_itertools-8.10.0-py3-none-any.whl", hash = "sha256:56ddac45541718ba332db05f464bebfb0768110111affd27f66e0051f276fa43"},
]
nodeenv = [
{file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
{file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
]
packaging = [
- {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
- {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
+ {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
+ {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
+]
+platformdirs = [
+ {file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"},
+ {file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"},
]
pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
]
pre-commit = [
- {file = "pre_commit-2.13.0-py2.py3-none-any.whl", hash = "sha256:b679d0fddd5b9d6d98783ae5f10fd0c4c59954f375b70a58cbe1ce9bcf9809a4"},
- {file = "pre_commit-2.13.0.tar.gz", hash = "sha256:764972c60693dc668ba8e86eb29654ec3144501310f7198742a767bec385a378"},
+ {file = "pre_commit-2.15.0-py2.py3-none-any.whl", hash = "sha256:a4ed01000afcb484d9eb8d504272e642c4c4099bbad3a6b27e519bd6a3e928a6"},
+ {file = "pre_commit-2.15.0.tar.gz", hash = "sha256:3c25add78dbdfb6a28a651780d5c311ac40dd17f160eb3954a0c59da40a505a7"},
]
py = [
{file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
{file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
]
pygments = [
- {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"},
- {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"},
+ {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"},
+ {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"},
]
pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
@@ -954,10 +971,6 @@ pytest-asyncio = [
{file = "pytest-asyncio-0.15.1.tar.gz", hash = "sha256:2564ceb9612bbd560d19ca4b41347b54e7835c2f792c504f698e05395ed63f6f"},
{file = "pytest_asyncio-0.15.1-py3-none-any.whl", hash = "sha256:3042bcdf1c5d978f6b74d96a151c4cfb9dcece65006198389ccd7e6c60eb1eea"},
]
-pytest-azurepipelines = [
- {file = "pytest-azurepipelines-0.8.0.tar.gz", hash = "sha256:944ae2c0790b792d123aa7312fe307bc35214dd26531728923ae5085a1d1feab"},
- {file = "pytest_azurepipelines-0.8.0-py3-none-any.whl", hash = "sha256:38b841a90e88d1966715966d7ea35619ed710386138a6a0b8fb5954c991ca4f1"},
-]
pytest-cov = [
{file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"},
{file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"},
@@ -997,8 +1010,8 @@ pyyaml = [
{file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
]
requests = [
- {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
- {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
+ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
+ {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
@@ -1056,36 +1069,35 @@ toml = [
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
tox = [
- {file = "tox-3.23.1-py2.py3-none-any.whl", hash = "sha256:b0b5818049a1c1997599d42012a637a33f24c62ab8187223fdd318fa8522637b"},
- {file = "tox-3.23.1.tar.gz", hash = "sha256:307a81ddb82bd463971a273f33e9533a24ed22185f27db8ce3386bff27d324e3"},
+ {file = "tox-3.24.4-py2.py3-none-any.whl", hash = "sha256:5e274227a53dc9ef856767c21867377ba395992549f02ce55eb549f9fb9a8d10"},
+ {file = "tox-3.24.4.tar.gz", hash = "sha256:c30b57fa2477f1fb7c36aa1d83292d5c2336cd0018119e1b1c17340e2c2708ca"},
]
typing-extensions = [
- {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"},
- {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"},
- {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"},
+ {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
+ {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
+ {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
]
urllib3 = [
- {file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"},
- {file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"},
+ {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"},
+ {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"},
]
virtualenv = [
- {file = "virtualenv-20.4.7-py2.py3-none-any.whl", hash = "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76"},
- {file = "virtualenv-20.4.7.tar.gz", hash = "sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467"},
+ {file = "virtualenv-20.8.0-py2.py3-none-any.whl", hash = "sha256:a4b987ec31c3c9996cf1bc865332f967fe4a0512c41b39652d6224f696e69da5"},
+ {file = "virtualenv-20.8.0.tar.gz", hash = "sha256:4da4ac43888e97de9cf4fdd870f48ed864bbfd133d2c46cbdec941fed4a25aef"},
]
voluptuous = [
- {file = "voluptuous-0.12.1-py3-none-any.whl", hash = "sha256:8ace33fcf9e6b1f59406bfaf6b8ec7bcc44266a9f29080b4deb4fe6ff2492386"},
- {file = "voluptuous-0.12.1.tar.gz", hash = "sha256:663572419281ddfaf4b4197fd4942d181630120fb39b333e3adad70aeb56444b"},
+ {file = "voluptuous-0.12.2.tar.gz", hash = "sha256:4db1ac5079db9249820d49c891cb4660a6f8cae350491210abce741fabf56513"},
]
wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
]
xdoctest = [
- {file = "xdoctest-0.15.4-py2.py3-none-any.whl", hash = "sha256:4b91eb67e45e51a254ff9370adb72a0c82b08289844c95cfd3a1186d7ec4f694"},
- {file = "xdoctest-0.15.4-py3-none-any.whl", hash = "sha256:33d4a12cf70da245ca3f71be9ef03e0615fa862826bf6a08e8f025ce693e496d"},
- {file = "xdoctest-0.15.4.tar.gz", hash = "sha256:ef1f93d2147988d3cb6f35c026ec32aca971923f86945a775f61e2f8de8505d1"},
+ {file = "xdoctest-0.15.8-py2.py3-none-any.whl", hash = "sha256:566e2bb2135e144e66ccd390affbe4504a2e96c25ef16260843b9680326cadc9"},
+ {file = "xdoctest-0.15.8-py3-none-any.whl", hash = "sha256:80a57af2f8ca709ab9da111ab3b16ec474f11297b9efcc34709a2c3e56ed9ce6"},
+ {file = "xdoctest-0.15.8.tar.gz", hash = "sha256:ddd128780593161a7398fcfefc49f5f6dfe4c2eb2816942cb53768d43bcab7b9"},
]
zipp = [
- {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"},
- {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"},
+ {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"},
+ {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"},
]
diff --git a/pyproject.toml b/pyproject.toml
index f35605993..87ddba9d6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "python-kasa"
-version = "0.4.0.dev3"
+version = "0.4.0.dev4"
description = "Python API for TP-Link Kasa Smarthome devices"
license = "GPL-3.0-or-later"
authors = ["Your Name "]
@@ -26,7 +26,6 @@ sphinxcontrib-programoutput = { version = "^0", optional = true }
[tool.poetry.dev-dependencies]
pytest = "^5"
-pytest-azurepipelines = "^0"
pytest-cov = "^2"
pytest-asyncio = "^0"
pytest-sugar = "*"
@@ -72,6 +71,11 @@ fail-under = 100
exclude = ['kasa/tests/*']
verbose = 2
+[tool.pytest.ini_options]
+markers = [
+ "requires_dummy: test requires dummy data to pass, skipped on real devices",
+]
+
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"