mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-04-26 16:46:23 +00:00
Add precommit hooks & tox env to enforce code structure, add azure pipelines
Instead of leveraging hound & travis, add an option for azure pipelines, which may replace the former in the future. This also streamlines the contributing guidelines by: * Adding pre-commit hooks to run isort, black, flake8, mypy * Adding lint environment to allow checks to be run `tox -e lint` This also contains a major cleanup to the SmartStrip handling which was due. After seeing #184 I thought I should push this in as it is for comments before the codebase diverges too much.
This commit is contained in:
parent
59424d2738
commit
8a131e1eeb
5
.flake8
5
.flake8
@ -1,5 +0,0 @@
|
|||||||
[flake8]
|
|
||||||
ignore = E203, E266, E501, W503, F403, F401
|
|
||||||
max-line-length = 79
|
|
||||||
max-complexity = 18
|
|
||||||
select = B,C,E,F,W,T4,B9
|
|
@ -1,6 +1,24 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/python/black
|
- repo: https://github.com/python/black
|
||||||
rev: stable
|
rev: stable
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
language_version: python3.7
|
language_version: python3.7
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v2.3.0
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
additional_dependencies: [flake8-docstrings]
|
||||||
|
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-isort
|
||||||
|
rev: v4.3.21
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
|
rev: v0.740
|
||||||
|
hooks:
|
||||||
|
- id: mypy
|
||||||
|
# args: [--no-strict-optional, --ignore-missing-imports]
|
||||||
|
47
azure-pipelines.yml
Normal file
47
azure-pipelines.yml
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
trigger:
|
||||||
|
- master
|
||||||
|
pr:
|
||||||
|
- master
|
||||||
|
|
||||||
|
pool:
|
||||||
|
vmImage: 'ubuntu-latest'
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
Python36:
|
||||||
|
python.version: '3.6'
|
||||||
|
Python37:
|
||||||
|
python.version: '3.7'
|
||||||
|
# Python38:
|
||||||
|
# python.version: '3.8'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- task: UsePythonVersion@0
|
||||||
|
inputs:
|
||||||
|
versionSpec: '$(python.version)'
|
||||||
|
displayName: 'Use Python $(python.version)'
|
||||||
|
|
||||||
|
- script: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install pytest pytest-azurepipelines pytest-cov
|
||||||
|
displayName: 'Install dependencies'
|
||||||
|
|
||||||
|
- script: |
|
||||||
|
pre-commit run black --all-files
|
||||||
|
displayName: 'Code formating (black)'
|
||||||
|
|
||||||
|
- script: |
|
||||||
|
pre-commit run flake8 --all-files
|
||||||
|
displayName: 'Code formating (flake8)'
|
||||||
|
|
||||||
|
- script: |
|
||||||
|
pre-commit run mypy --all-files
|
||||||
|
displayName: 'Typing checks (mypy)'
|
||||||
|
|
||||||
|
- script: |
|
||||||
|
pre-commit run isort --all-files
|
||||||
|
displayName: 'Order of imports (isort)'
|
||||||
|
|
||||||
|
- script: |
|
||||||
|
pytest --cov pyHS100 --cov-report html
|
||||||
|
displayName: 'Tests'
|
@ -1,22 +1,24 @@
|
|||||||
"""
|
"""Python interface for TP-Link's smart home devices.
|
||||||
This module provides a way to interface with TP-Link's smart home devices,
|
|
||||||
such as smart plugs (HS1xx), wall switches (HS2xx), and light bulbs (LB1xx).
|
|
||||||
|
|
||||||
All common, shared functionalities are available through `SmartDevice` class::
|
All common, shared functionalities are available through `SmartDevice` class::
|
||||||
|
|
||||||
x = SmartDevice("192.168.1.1")
|
x = SmartDevice("192.168.1.1")
|
||||||
print(x.sys_info)
|
print(x.sys_info)
|
||||||
|
|
||||||
For device type specific actions `SmartBulb` or `SmartPlug` must be used instead.
|
For device type specific actions `SmartBulb`, `SmartPlug`, or `SmartStrip`
|
||||||
|
should be used instead.
|
||||||
|
|
||||||
Module-specific errors are raised as `SmartDeviceException` and are expected
|
Module-specific errors are raised as `SmartDeviceException` and are expected
|
||||||
to be handled by the user of the library.
|
to be handled by the user of the library.
|
||||||
"""
|
"""
|
||||||
# flake8: noqa
|
from .discover import Discover # noqa
|
||||||
from .smartdevice import SmartDevice, SmartDeviceException, EmeterStatus
|
from .protocol import TPLinkSmartHomeProtocol # noqa
|
||||||
from .smartdevice import SmartDevice, SmartDeviceException, EmeterStatus, DeviceType
|
from .smartbulb import SmartBulb # noqa
|
||||||
from .smartplug import SmartPlug
|
from .smartdevice import ( # noqa
|
||||||
from .smartbulb import SmartBulb
|
DeviceType,
|
||||||
from .smartstrip import SmartStrip, SmartStripException
|
EmeterStatus,
|
||||||
from .protocol import TPLinkSmartHomeProtocol
|
SmartDevice,
|
||||||
from .discover import Discover
|
SmartDeviceException,
|
||||||
|
)
|
||||||
|
from .smartplug import SmartPlug # noqa
|
||||||
|
from .smartstrip import SmartStrip # noqa
|
||||||
|
@ -1,20 +1,17 @@
|
|||||||
"""pyHS100 cli tool."""
|
"""pyHS100 cli tool."""
|
||||||
import sys
|
|
||||||
import click
|
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
from pprint import pformat as pf
|
from pprint import pformat as pf
|
||||||
|
|
||||||
if sys.version_info < (3, 4):
|
import click
|
||||||
print("To use this script you need python 3.4 or newer! got %s" % sys.version_info)
|
|
||||||
|
from pyHS100 import SmartPlug # noqa: E402
|
||||||
|
from pyHS100 import Discover, SmartBulb, SmartDevice, SmartStrip
|
||||||
|
|
||||||
|
if sys.version_info < (3, 6):
|
||||||
|
print("To use this script you need python 3.6 or newer! got %s" % sys.version_info)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
from pyHS100 import (
|
|
||||||
SmartDevice,
|
|
||||||
SmartPlug,
|
|
||||||
SmartBulb,
|
|
||||||
SmartStrip,
|
|
||||||
Discover,
|
|
||||||
) # noqa: E402
|
|
||||||
|
|
||||||
pass_dev = click.make_pass_decorator(SmartDevice)
|
pass_dev = click.make_pass_decorator(SmartDevice)
|
||||||
|
|
||||||
@ -50,9 +47,10 @@ pass_dev = click.make_pass_decorator(SmartDevice)
|
|||||||
@click.option("--bulb", default=False, is_flag=True)
|
@click.option("--bulb", default=False, is_flag=True)
|
||||||
@click.option("--plug", default=False, is_flag=True)
|
@click.option("--plug", default=False, is_flag=True)
|
||||||
@click.option("--strip", default=False, is_flag=True)
|
@click.option("--strip", default=False, is_flag=True)
|
||||||
|
@click.version_option()
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def cli(ctx, ip, host, alias, target, debug, bulb, plug, strip):
|
def cli(ctx, ip, host, alias, target, debug, bulb, plug, strip):
|
||||||
"""A cli tool for controlling TP-Link smart home plugs."""
|
"""A cli tool for controlling TP-Link smart home plugs.""" # noqa
|
||||||
if debug:
|
if debug:
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
else:
|
else:
|
||||||
@ -100,6 +98,10 @@ def cli(ctx, ip, host, alias, target, debug, bulb, plug, strip):
|
|||||||
@click.option("--save")
|
@click.option("--save")
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def dump_discover(ctx, save):
|
def dump_discover(ctx, save):
|
||||||
|
"""Dump discovery information.
|
||||||
|
|
||||||
|
Useful for dumping into a file with `--save` to be added to the test suite.
|
||||||
|
"""
|
||||||
target = ctx.parent.params["target"]
|
target = ctx.parent.params["target"]
|
||||||
for dev in Discover.discover(target=target, return_raw=True).values():
|
for dev in Discover.discover(target=target, return_raw=True).values():
|
||||||
model = dev["system"]["get_sysinfo"]["model"]
|
model = dev["system"]["get_sysinfo"]["model"]
|
||||||
@ -164,7 +166,7 @@ def sysinfo(dev):
|
|||||||
@cli.command()
|
@cli.command()
|
||||||
@pass_dev
|
@pass_dev
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def state(ctx, dev):
|
def state(ctx, dev: SmartDevice):
|
||||||
"""Print out device state and versions."""
|
"""Print out device state and versions."""
|
||||||
click.echo(click.style("== %s - %s ==" % (dev.alias, dev.model), bold=True))
|
click.echo(click.style("== %s - %s ==" % (dev.alias, dev.model), bold=True))
|
||||||
|
|
||||||
@ -174,15 +176,12 @@ def state(ctx, dev):
|
|||||||
fg="green" if dev.is_on else "red",
|
fg="green" if dev.is_on else "red",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if dev.num_children > 0:
|
if dev.is_strip:
|
||||||
is_on = dev.get_is_on()
|
for plug in dev.plugs: # type: ignore
|
||||||
aliases = dev.get_alias()
|
|
||||||
for child in range(dev.num_children):
|
|
||||||
click.echo(
|
click.echo(
|
||||||
click.style(
|
click.style(
|
||||||
" * %s state: %s"
|
" * %s state: %s" % (plug.alias, ("ON" if plug.is_on else "OFF")),
|
||||||
% (aliases[child], ("ON" if is_on[child] else "OFF")),
|
fg="green" if plug.is_on else "red",
|
||||||
fg="green" if is_on[child] else "red",
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -303,7 +302,7 @@ def temperature(dev: SmartBulb, temperature):
|
|||||||
@click.pass_context
|
@click.pass_context
|
||||||
@pass_dev
|
@pass_dev
|
||||||
def hsv(dev, ctx, h, s, v):
|
def hsv(dev, ctx, h, s, v):
|
||||||
"""Get or set color in HSV. (Bulb only)"""
|
"""Get or set color in HSV. (Bulb only)."""
|
||||||
if h is None or s is None or v is None:
|
if h is None or s is None or v is None:
|
||||||
click.echo("Current HSV: %s %s %s" % dev.hsv)
|
click.echo("Current HSV: %s %s %s" % dev.hsv)
|
||||||
elif s is None or v is None:
|
elif s is None or v is None:
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
import socket
|
"""Discovery module for TP-Link Smart Home devices."""
|
||||||
import logging
|
|
||||||
import json
|
import json
|
||||||
from typing import Dict, Type, Optional
|
import logging
|
||||||
|
import socket
|
||||||
|
from typing import Dict, Optional, Type
|
||||||
|
|
||||||
from pyHS100 import (
|
from .protocol import TPLinkSmartHomeProtocol
|
||||||
TPLinkSmartHomeProtocol,
|
from .smartbulb import SmartBulb
|
||||||
SmartDevice,
|
from .smartdevice import SmartDevice, SmartDeviceException
|
||||||
SmartPlug,
|
from .smartplug import SmartPlug
|
||||||
SmartBulb,
|
from .smartstrip import SmartStrip
|
||||||
SmartStrip,
|
|
||||||
SmartDeviceException,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -28,8 +26,6 @@ class Discover:
|
|||||||
you can initialize the corresponding device class directly without this.
|
you can initialize the corresponding device class directly without this.
|
||||||
|
|
||||||
The protocol uses UDP broadcast datagrams on port 9999 for discovery.
|
The protocol uses UDP broadcast datagrams on port 9999 for discovery.
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DISCOVERY_QUERY = {
|
DISCOVERY_QUERY = {
|
||||||
@ -49,8 +45,8 @@ class Discover:
|
|||||||
discovery_packets=3,
|
discovery_packets=3,
|
||||||
return_raw=False,
|
return_raw=False,
|
||||||
) -> Dict[str, SmartDevice]:
|
) -> Dict[str, SmartDevice]:
|
||||||
|
"""Discover devices.
|
||||||
|
|
||||||
"""
|
|
||||||
Sends discovery message to 255.255.255.255:9999 in order
|
Sends discovery message to 255.255.255.255:9999 in order
|
||||||
to detect available supported devices in the local network,
|
to detect available supported devices in the local network,
|
||||||
and waits for given timeout for answers from devices.
|
and waits for given timeout for answers from devices.
|
||||||
|
@ -1,24 +1,25 @@
|
|||||||
|
"""Implementation of the TP-Link Smart Home Protocol.
|
||||||
|
|
||||||
|
Encryption/Decryption methods based on the works of
|
||||||
|
Lubomir Stroetmann and Tobias Esser
|
||||||
|
|
||||||
|
https://www.softscheck.com/en/reverse-engineering-tp-link-hs110/
|
||||||
|
https://github.com/softScheck/tplink-smartplug/
|
||||||
|
|
||||||
|
which are licensed under the Apache License, Version 2.0
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
"""
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
import logging
|
|
||||||
from typing import Any, Dict, Union
|
from typing import Any, Dict, Union
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TPLinkSmartHomeProtocol:
|
class TPLinkSmartHomeProtocol:
|
||||||
"""Implementation of the TP-Link Smart Home Protocol.
|
"""Implementation of the TP-Link Smart Home protocol."""
|
||||||
|
|
||||||
Encryption/Decryption methods based on the works of
|
|
||||||
Lubomir Stroetmann and Tobias Esser
|
|
||||||
|
|
||||||
https://www.softscheck.com/en/reverse-engineering-tp-link-hs110/
|
|
||||||
https://github.com/softScheck/tplink-smartplug/
|
|
||||||
|
|
||||||
which are licensed under the Apache License, Version 2.0
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
INITIALIZATION_VECTOR = 171
|
INITIALIZATION_VECTOR = 171
|
||||||
DEFAULT_PORT = 9999
|
DEFAULT_PORT = 9999
|
||||||
@ -78,7 +79,7 @@ class TPLinkSmartHomeProtocol:
|
|||||||
return json.loads(response)
|
return json.loads(response)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def encrypt(request: str) -> bytearray:
|
def encrypt(request: str) -> bytes:
|
||||||
"""
|
"""
|
||||||
Encrypt a request for a TP-Link Smart Home Device.
|
Encrypt a request for a TP-Link Smart Home Device.
|
||||||
|
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
from pyHS100 import DeviceType, SmartDevice, SmartDeviceException
|
"""Module for bulbs."""
|
||||||
from .protocol import TPLinkSmartHomeProtocol
|
|
||||||
from deprecation import deprecated
|
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
|
||||||
from typing import Any, Dict, Tuple
|
from typing import Any, Dict, Tuple
|
||||||
|
|
||||||
|
from deprecation import deprecated
|
||||||
|
|
||||||
|
from .protocol import TPLinkSmartHomeProtocol
|
||||||
|
from .smartdevice import DeviceType, SmartDevice, SmartDeviceException
|
||||||
|
|
||||||
TPLINK_KELVIN = {
|
TPLINK_KELVIN = {
|
||||||
"LB130": (2500, 9000),
|
"LB130": (2500, 9000),
|
||||||
@ -12,8 +13,8 @@ TPLINK_KELVIN = {
|
|||||||
"LB230": (2500, 9000),
|
"LB230": (2500, 9000),
|
||||||
"KB130": (2500, 9000),
|
"KB130": (2500, 9000),
|
||||||
"KL130": (2500, 9000),
|
"KL130": (2500, 9000),
|
||||||
"KL120\(EU\)": (2700, 6500),
|
r"KL120\(EU\)": (2700, 6500),
|
||||||
"KL120\(US\)": (2700, 5000),
|
r"KL120\(US\)": (2700, 5000),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -131,7 +132,6 @@ class SmartBulb(SmartDevice):
|
|||||||
:return: hue, saturation and value (degrees, %, %)
|
:return: hue, saturation and value (degrees, %, %)
|
||||||
:rtype: tuple
|
:rtype: tuple
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.is_color:
|
if not self.is_color:
|
||||||
raise SmartDeviceException("Bulb does not support color.")
|
raise SmartDeviceException("Bulb does not support color.")
|
||||||
|
|
||||||
@ -161,7 +161,9 @@ class SmartBulb(SmartDevice):
|
|||||||
def set_hsv(self, hue: int, saturation: int, value: int):
|
def set_hsv(self, hue: int, saturation: int, value: int):
|
||||||
"""Set new HSV.
|
"""Set new HSV.
|
||||||
|
|
||||||
:param tuple state: hue, saturation and value (degrees, %, %)
|
:param int hue: hue in degrees
|
||||||
|
:param int saturation: saturation in percentage [0,100]
|
||||||
|
:param int value: value in percentage [0, 100]
|
||||||
"""
|
"""
|
||||||
if not self.is_color:
|
if not self.is_color:
|
||||||
raise SmartDeviceException("Bulb does not support color.")
|
raise SmartDeviceException("Bulb does not support color.")
|
||||||
@ -230,7 +232,7 @@ class SmartBulb(SmartDevice):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def brightness(self) -> int:
|
def brightness(self) -> int:
|
||||||
"""Current brightness of the device.
|
"""Return the current brightness.
|
||||||
|
|
||||||
:return: brightness in percent
|
:return: brightness in percent
|
||||||
:rtype: int
|
:rtype: int
|
||||||
@ -250,7 +252,7 @@ class SmartBulb(SmartDevice):
|
|||||||
self.set_brightness(brightness)
|
self.set_brightness(brightness)
|
||||||
|
|
||||||
def set_brightness(self, brightness: int) -> None:
|
def set_brightness(self, brightness: int) -> None:
|
||||||
"""Set the current brightness of the device.
|
"""Set the brightness.
|
||||||
|
|
||||||
:param int brightness: brightness in percent
|
:param int brightness: brightness in percent
|
||||||
"""
|
"""
|
||||||
@ -303,10 +305,10 @@ class SmartBulb(SmartDevice):
|
|||||||
:return: Bulb information dict, keys in user-presentable form.
|
:return: Bulb information dict, keys in user-presentable form.
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
"""
|
"""
|
||||||
info = {
|
info: Dict[str, Any] = {
|
||||||
"Brightness": self.brightness,
|
"Brightness": self.brightness,
|
||||||
"Is dimmable": self.is_dimmable,
|
"Is dimmable": self.is_dimmable,
|
||||||
} # type: Dict[str, Any]
|
}
|
||||||
if self.is_variable_color_temp:
|
if self.is_variable_color_temp:
|
||||||
info["Color temperature"] = self.color_temp
|
info["Color temperature"] = self.color_temp
|
||||||
info["Valid temperature range"] = self.valid_temperature_range
|
info["Valid temperature range"] = self.valid_temperature_range
|
||||||
@ -331,4 +333,5 @@ class SmartBulb(SmartDevice):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def has_emeter(self) -> bool:
|
def has_emeter(self) -> bool:
|
||||||
|
"""Return that the bulb has an emeter."""
|
||||||
return True
|
return True
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
"""
|
"""Python library supporting TP-Link Smart Home devices.
|
||||||
pyHS100
|
|
||||||
Python library supporting TP-Link Smart Plugs/Switches (HS100/HS110/Hs200).
|
|
||||||
|
|
||||||
The communication protocol was reverse engineered by Lubomir Stroetmann and
|
The communication protocol was reverse engineered by Lubomir Stroetmann and
|
||||||
Tobias Esser in 'Reverse Engineering the TP-Link HS110':
|
Tobias Esser in 'Reverse Engineering the TP-Link HS110':
|
||||||
@ -13,11 +11,11 @@ Stroetmann which is licensed under the Apache License, Version 2.0.
|
|||||||
You may obtain a copy of the license at
|
You may obtain a copy of the license at
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
"""
|
"""
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Any, Dict, Optional
|
from datetime import datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from deprecation import deprecated
|
from deprecation import deprecated
|
||||||
|
|
||||||
@ -102,7 +100,7 @@ class SmartDevice:
|
|||||||
if protocol is None: # pragma: no cover
|
if protocol is None: # pragma: no cover
|
||||||
protocol = TPLinkSmartHomeProtocol()
|
protocol = TPLinkSmartHomeProtocol()
|
||||||
self.protocol = protocol
|
self.protocol = protocol
|
||||||
self.emeter_type = "emeter" # type: str
|
self.emeter_type = "emeter"
|
||||||
self.context = context
|
self.context = context
|
||||||
self.num_children = 0
|
self.num_children = 0
|
||||||
self.cache_ttl = timedelta(seconds=cache_ttl)
|
self.cache_ttl = timedelta(seconds=cache_ttl)
|
||||||
@ -112,11 +110,12 @@ class SmartDevice:
|
|||||||
self.context,
|
self.context,
|
||||||
self.cache_ttl,
|
self.cache_ttl,
|
||||||
)
|
)
|
||||||
self.cache = defaultdict(lambda: defaultdict(lambda: None))
|
self.cache = defaultdict(lambda: defaultdict(lambda: None)) # type: ignore
|
||||||
self._device_type = DeviceType.Unknown
|
self._device_type = DeviceType.Unknown
|
||||||
|
|
||||||
def _result_from_cache(self, target, cmd) -> Optional[Dict]:
|
def _result_from_cache(self, target, cmd) -> Optional[Dict]:
|
||||||
"""Return query result from cache if still fresh.
|
"""Return query result from cache if still fresh.
|
||||||
|
|
||||||
Only results from commands starting with `get_` are considered cacheable.
|
Only results from commands starting with `get_` are considered cacheable.
|
||||||
|
|
||||||
:param target: Target system
|
:param target: Target system
|
||||||
@ -141,7 +140,7 @@ class SmartDevice:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _insert_to_cache(self, target: str, cmd: str, response: Dict) -> None:
|
def _insert_to_cache(self, target: str, cmd: str, response: Dict) -> None:
|
||||||
"""Internal function to add response to cache.
|
"""Add response for a given command to the cache.
|
||||||
|
|
||||||
:param target: Target system
|
:param target: Target system
|
||||||
:param cmd: Command
|
:param cmd: Command
|
||||||
@ -160,9 +159,8 @@ class SmartDevice:
|
|||||||
:rtype: dict
|
:rtype: dict
|
||||||
:raises SmartDeviceException: if command was not executed correctly
|
:raises SmartDeviceException: if command was not executed correctly
|
||||||
"""
|
"""
|
||||||
if self.context is None:
|
request: Dict[str, Any] = {target: {cmd: arg}}
|
||||||
request = {target: {cmd: arg}}
|
if self.context is not None:
|
||||||
else:
|
|
||||||
request = {"context": {"child_ids": [self.context]}, target: {cmd: arg}}
|
request = {"context": {"child_ids": [self.context]}, target: {cmd: arg}}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -212,7 +210,6 @@ class SmartDevice:
|
|||||||
:return: System information dict.
|
:return: System information dict.
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.get_sysinfo()
|
return self.get_sysinfo()
|
||||||
|
|
||||||
def get_sysinfo(self) -> Dict:
|
def get_sysinfo(self) -> Dict:
|
||||||
@ -244,11 +241,13 @@ class SmartDevice:
|
|||||||
return str(self.sys_info["alias"])
|
return str(self.sys_info["alias"])
|
||||||
|
|
||||||
def get_alias(self) -> str:
|
def get_alias(self) -> str:
|
||||||
|
"""Return the alias."""
|
||||||
return self.alias
|
return self.alias
|
||||||
|
|
||||||
@alias.setter # type: ignore
|
@alias.setter # type: ignore
|
||||||
@deprecated(details="use set_alias")
|
@deprecated(details="use set_alias")
|
||||||
def alias(self, alias: str) -> None:
|
def alias(self, alias: str) -> None:
|
||||||
|
"""Set the device name, deprecated."""
|
||||||
self.set_alias(alias)
|
self.set_alias(alias)
|
||||||
|
|
||||||
def set_alias(self, alias: str) -> None:
|
def set_alias(self, alias: str) -> None:
|
||||||
@ -424,7 +423,7 @@ class SmartDevice:
|
|||||||
"Unknown mac, please submit a bug " "with sysinfo output."
|
"Unknown mac, please submit a bug " "with sysinfo output."
|
||||||
)
|
)
|
||||||
|
|
||||||
@mac.setter
|
@mac.setter # type: ignore
|
||||||
@deprecated(details="use set_mac")
|
@deprecated(details="use set_mac")
|
||||||
def mac(self, mac: str) -> None:
|
def mac(self, mac: str) -> None:
|
||||||
self.set_mac(mac)
|
self.set_mac(mac)
|
||||||
@ -438,11 +437,10 @@ class SmartDevice:
|
|||||||
self._query_helper("system", "set_mac_addr", {"mac": mac})
|
self._query_helper("system", "set_mac_addr", {"mac": mac})
|
||||||
|
|
||||||
def get_emeter_realtime(self) -> EmeterStatus:
|
def get_emeter_realtime(self) -> EmeterStatus:
|
||||||
"""Retrive current energy readings.
|
"""Retrieve current energy readings.
|
||||||
|
|
||||||
:returns: current readings or False
|
:returns: current readings or False
|
||||||
:rtype: dict, None
|
:rtype: dict, None
|
||||||
None if device has no energy meter or error occurred
|
|
||||||
:raises SmartDeviceException: on error
|
:raises SmartDeviceException: on error
|
||||||
"""
|
"""
|
||||||
if not self.has_emeter:
|
if not self.has_emeter:
|
||||||
@ -460,7 +458,6 @@ class SmartDevice:
|
|||||||
month)
|
month)
|
||||||
:param kwh: return usage in kWh (default: True)
|
:param kwh: return usage in kWh (default: True)
|
||||||
:return: mapping of day of month to value
|
:return: mapping of day of month to value
|
||||||
None if device has no energy meter or error occurred
|
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
:raises SmartDeviceException: on error
|
:raises SmartDeviceException: on error
|
||||||
"""
|
"""
|
||||||
@ -491,7 +488,6 @@ class SmartDevice:
|
|||||||
:param year: year for which to retrieve statistics (default: this year)
|
:param year: year for which to retrieve statistics (default: this year)
|
||||||
:param kwh: return usage in kWh (default: True)
|
:param kwh: return usage in kWh (default: True)
|
||||||
:return: dict: mapping of month to value
|
:return: dict: mapping of month to value
|
||||||
None if device has no energy meter
|
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
:raises SmartDeviceException: on error
|
:raises SmartDeviceException: on error
|
||||||
"""
|
"""
|
||||||
@ -510,12 +506,10 @@ class SmartDevice:
|
|||||||
|
|
||||||
return {entry["month"]: entry[key] for entry in response}
|
return {entry["month"]: entry[key] for entry in response}
|
||||||
|
|
||||||
def erase_emeter_stats(self) -> bool:
|
def erase_emeter_stats(self):
|
||||||
"""Erase energy meter statistics.
|
"""Erase energy meter statistics.
|
||||||
|
|
||||||
:return: True if statistics were deleted
|
:return: True if statistics were deleted
|
||||||
False if device has no energy meter.
|
|
||||||
:rtype: bool
|
|
||||||
:raises SmartDeviceException: on error
|
:raises SmartDeviceException: on error
|
||||||
"""
|
"""
|
||||||
if not self.has_emeter:
|
if not self.has_emeter:
|
||||||
@ -523,15 +517,10 @@ class SmartDevice:
|
|||||||
|
|
||||||
self._query_helper(self.emeter_type, "erase_emeter_stat", None)
|
self._query_helper(self.emeter_type, "erase_emeter_stat", None)
|
||||||
|
|
||||||
# As query_helper raises exception in case of failure, we have
|
def current_consumption(self) -> float:
|
||||||
# succeeded when we are this far.
|
|
||||||
return True
|
|
||||||
|
|
||||||
def current_consumption(self) -> Optional[float]:
|
|
||||||
"""Get the current power consumption in Watt.
|
"""Get the current power consumption in Watt.
|
||||||
|
|
||||||
:return: the current power consumption in Watts.
|
:return: the current power consumption in Watts.
|
||||||
None if device has no energy meter.
|
|
||||||
:raises SmartDeviceException: on error
|
:raises SmartDeviceException: on error
|
||||||
"""
|
"""
|
||||||
if not self.has_emeter:
|
if not self.has_emeter:
|
||||||
@ -594,22 +583,27 @@ class SmartDevice:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_bulb(self) -> bool:
|
def is_bulb(self) -> bool:
|
||||||
|
"""Return True if the device is a bulb."""
|
||||||
return self._device_type == DeviceType.Bulb
|
return self._device_type == DeviceType.Bulb
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_plug(self) -> bool:
|
def is_plug(self) -> bool:
|
||||||
|
"""Return True if the device is a plug."""
|
||||||
return self._device_type == DeviceType.Plug
|
return self._device_type == DeviceType.Plug
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_strip(self) -> bool:
|
def is_strip(self) -> bool:
|
||||||
|
"""Return True if the device is a strip."""
|
||||||
return self._device_type == DeviceType.Strip
|
return self._device_type == DeviceType.Strip
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_dimmable(self):
|
def is_dimmable(self):
|
||||||
|
"""Return True if the device is dimmable."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_variable_color_temp(self) -> bool:
|
def is_variable_color_temp(self) -> bool:
|
||||||
|
"""Return True if the device supports color temperature."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
|
"""Module for plugs."""
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from deprecation import deprecated
|
from deprecation import deprecated
|
||||||
|
|
||||||
from pyHS100 import SmartDevice, DeviceType, SmartDeviceException
|
|
||||||
from .protocol import TPLinkSmartHomeProtocol
|
from .protocol import TPLinkSmartHomeProtocol
|
||||||
|
from .smartdevice import DeviceType, SmartDevice, SmartDeviceException
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -1,35 +1,37 @@
|
|||||||
|
"""Module for multi-socket devices (HS300, HS107).
|
||||||
|
|
||||||
|
.. todo:: describe how this interfaces with single plugs.
|
||||||
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, Optional, Union
|
from collections import defaultdict
|
||||||
from deprecation import deprecated
|
from typing import Any, DefaultDict, Dict, List
|
||||||
|
|
||||||
from pyHS100 import SmartPlug, SmartDeviceException, EmeterStatus, DeviceType
|
|
||||||
from .protocol import TPLinkSmartHomeProtocol
|
from .protocol import TPLinkSmartHomeProtocol
|
||||||
|
from .smartplug import DeviceType, SmartPlug
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SmartStripException(SmartDeviceException):
|
|
||||||
"""SmartStripException gets raised for errors specific to the smart strip."""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SmartStrip(SmartPlug):
|
class SmartStrip(SmartPlug):
|
||||||
"""Representation of a TP-Link Smart Power Strip.
|
"""Representation of a TP-Link Smart Power Strip.
|
||||||
|
|
||||||
Usage example when used as library:
|
Usage example when used as library:
|
||||||
p = SmartStrip("192.168.1.105")
|
p = SmartStrip("192.168.1.105")
|
||||||
|
|
||||||
|
# query the state of the strip
|
||||||
|
print(p.is_on)
|
||||||
|
|
||||||
# change state of all outlets
|
# change state of all outlets
|
||||||
p.turn_on()
|
p.turn_on()
|
||||||
p.turn_off()
|
p.turn_off()
|
||||||
|
|
||||||
# change state of a single outlet
|
# individual outlets are accessible through plugs variable
|
||||||
p.turn_on(index=1)
|
for plug in p.plugs:
|
||||||
|
print("%s: %s" % (p, p.is_on))
|
||||||
|
|
||||||
# query and print current state of all outlets
|
# change state of a single outlet
|
||||||
print(p.get_state())
|
p.plugs[0].turn_on()
|
||||||
|
|
||||||
Errors reported by the device are raised as SmartDeviceExceptions,
|
Errors reported by the device are raised as SmartDeviceExceptions,
|
||||||
and should be handled by the user of the library.
|
and should be handled by the user of the library.
|
||||||
@ -41,366 +43,122 @@ class SmartStrip(SmartPlug):
|
|||||||
SmartPlug.__init__(self, host=host, protocol=protocol, cache_ttl=cache_ttl)
|
SmartPlug.__init__(self, host=host, protocol=protocol, cache_ttl=cache_ttl)
|
||||||
self.emeter_type = "emeter"
|
self.emeter_type = "emeter"
|
||||||
self._device_type = DeviceType.Strip
|
self._device_type = DeviceType.Strip
|
||||||
self.plugs = {}
|
self.plugs: List[SmartPlug] = []
|
||||||
children = self.sys_info["children"]
|
children = self.sys_info["children"]
|
||||||
self.num_children = len(children)
|
self.num_children = len(children)
|
||||||
for plug in range(self.num_children):
|
for plug in range(self.num_children):
|
||||||
self.plugs[plug] = SmartPlug(
|
self.plugs.append(
|
||||||
host, protocol, context=children[plug]["id"], cache_ttl=cache_ttl
|
SmartPlug(
|
||||||
|
host, protocol, context=children[plug]["id"], cache_ttl=cache_ttl
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def raise_for_index(self, index: int):
|
|
||||||
"""
|
|
||||||
Raises SmartStripException if the plug index is out of bounds
|
|
||||||
|
|
||||||
:param index: plug index to check
|
|
||||||
:raises SmartStripException: index out of bounds
|
|
||||||
"""
|
|
||||||
if index not in range(self.num_children):
|
|
||||||
raise SmartStripException("plug index of %d " "is out of bounds" % index)
|
|
||||||
|
|
||||||
@property
|
|
||||||
@deprecated(details="use is_on, get_is_on()")
|
|
||||||
def state(self) -> bool:
|
|
||||||
if self.is_on:
|
|
||||||
return self.STATE_ON
|
|
||||||
return self.STATE_OFF
|
|
||||||
|
|
||||||
def get_state(self, *, index=-1) -> Dict[int, str]:
|
|
||||||
"""Retrieve the switch state
|
|
||||||
|
|
||||||
:returns: list with the state of each child plug
|
|
||||||
STATE_ON
|
|
||||||
STATE_OFF
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _state_for_bool(b):
|
|
||||||
return SmartPlug.STATE_ON if b else SmartPlug.STATE_OFF
|
|
||||||
|
|
||||||
is_on = self.get_is_on(index=index)
|
|
||||||
if isinstance(is_on, bool):
|
|
||||||
return _state_for_bool(is_on)
|
|
||||||
|
|
||||||
print(is_on)
|
|
||||||
|
|
||||||
return {k: _state_for_bool(v) for k, v in self.get_is_on().items()}
|
|
||||||
|
|
||||||
@state.setter
|
|
||||||
@deprecated(details="use turn_on(), turn_off()")
|
|
||||||
def state(self, value: str):
|
|
||||||
"""Sets the state of all plugs in the strip
|
|
||||||
|
|
||||||
:param value: one of
|
|
||||||
STATE_ON
|
|
||||||
STATE_OFF
|
|
||||||
:raises ValueError: on invalid state
|
|
||||||
:raises SmartDeviceException: on error
|
|
||||||
"""
|
|
||||||
if not isinstance(value, str):
|
|
||||||
raise ValueError("State must be str, not of %s.", type(value))
|
|
||||||
elif value.upper() == SmartPlug.STATE_ON:
|
|
||||||
self.turn_on()
|
|
||||||
elif value.upper() == SmartPlug.STATE_OFF:
|
|
||||||
self.turn_off()
|
|
||||||
else:
|
|
||||||
raise ValueError("State %s is not valid.", value)
|
|
||||||
|
|
||||||
def set_state(self, value: str, *, index: int = -1):
|
|
||||||
"""Sets the state of a plug on the strip
|
|
||||||
|
|
||||||
:param value: one of
|
|
||||||
STATE_ON
|
|
||||||
STATE_OFF
|
|
||||||
:param index: plug index (-1 for all)
|
|
||||||
:raises ValueError: on invalid state
|
|
||||||
:raises SmartDeviceException: on error
|
|
||||||
:raises SmartStripException: index out of bounds
|
|
||||||
"""
|
|
||||||
if index < 0:
|
|
||||||
self.state = value
|
|
||||||
else:
|
|
||||||
self.raise_for_index(index)
|
|
||||||
self.plugs[index].state = value
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return if any of the outlets are on"""
|
"""Return if any of the outlets are on."""
|
||||||
return any(state == "ON" for state in self.get_state().values())
|
return any(plug.is_on for plug in self.plugs)
|
||||||
|
|
||||||
def get_is_on(self, *, index: int = -1) -> Any:
|
def turn_on(self):
|
||||||
"""
|
"""Turn the strip on.
|
||||||
Returns whether device is on.
|
|
||||||
|
|
||||||
:param index: plug index (-1 for all)
|
|
||||||
:return: True if device is on, False otherwise, Dict without index
|
|
||||||
:rtype: bool if index is provided
|
|
||||||
Dict[int, bool] if no index provided
|
|
||||||
:raises SmartStripException: index out of bounds
|
|
||||||
"""
|
|
||||||
children = self.sys_info["children"]
|
|
||||||
if index < 0:
|
|
||||||
is_on = {}
|
|
||||||
for plug in range(self.num_children):
|
|
||||||
is_on[plug] = bool(children[plug]["state"])
|
|
||||||
return is_on
|
|
||||||
else:
|
|
||||||
self.raise_for_index(index)
|
|
||||||
return bool(children[index]["state"])
|
|
||||||
|
|
||||||
def get_is_off(self, *, index: int = -1) -> Any:
|
|
||||||
is_on = self.get_is_on(index=index)
|
|
||||||
if isinstance(is_on, bool):
|
|
||||||
return not is_on
|
|
||||||
else:
|
|
||||||
return {k: not v for k, v in is_on}
|
|
||||||
|
|
||||||
def turn_on(self, *, index: int = -1):
|
|
||||||
"""
|
|
||||||
Turns outlets on
|
|
||||||
|
|
||||||
:param index: plug index (-1 for all)
|
|
||||||
:raises SmartDeviceException: on error
|
:raises SmartDeviceException: on error
|
||||||
:raises SmartStripException: index out of bounds
|
|
||||||
"""
|
"""
|
||||||
if index < 0:
|
self._query_helper("system", "set_relay_state", {"state": 1})
|
||||||
self._query_helper("system", "set_relay_state", {"state": 1})
|
|
||||||
else:
|
|
||||||
self.raise_for_index(index)
|
|
||||||
self.plugs[index].turn_on()
|
|
||||||
|
|
||||||
def turn_off(self, *, index: int = -1):
|
def turn_off(self):
|
||||||
"""
|
"""Turn the strip off.
|
||||||
Turns outlets off
|
|
||||||
|
|
||||||
:param index: plug index (-1 for all)
|
|
||||||
:raises SmartDeviceException: on error
|
:raises SmartDeviceException: on error
|
||||||
:raises SmartStripException: index out of bounds
|
|
||||||
"""
|
"""
|
||||||
if index < 0:
|
self._query_helper("system", "set_relay_state", {"state": 0})
|
||||||
self._query_helper("system", "set_relay_state", {"state": 0})
|
|
||||||
else:
|
|
||||||
self.raise_for_index(index)
|
|
||||||
self.plugs[index].turn_off()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def on_since(self) -> datetime:
|
def on_since(self) -> datetime.datetime:
|
||||||
"""Returns the maximum on-time of all outlets."""
|
"""Return the maximum on-time of all outlets."""
|
||||||
return max(v for v in self.get_on_since().values())
|
return max(plug.on_since for plug in self.plugs)
|
||||||
|
|
||||||
def get_on_since(self, *, index: int = -1) -> Any:
|
|
||||||
"""
|
|
||||||
Returns pretty-printed on-time
|
|
||||||
|
|
||||||
:param index: plug index (-1 for all)
|
|
||||||
:return: datetime for on since
|
|
||||||
:rtype: datetime with index
|
|
||||||
Dict[int, str] without index
|
|
||||||
:raises SmartStripException: index out of bounds
|
|
||||||
"""
|
|
||||||
if index < 0:
|
|
||||||
on_since = {}
|
|
||||||
children = self.sys_info["children"]
|
|
||||||
|
|
||||||
for plug in range(self.num_children):
|
|
||||||
child_ontime = children[plug]["on_time"]
|
|
||||||
on_since[plug] = datetime.datetime.now() - datetime.timedelta(
|
|
||||||
seconds=child_ontime
|
|
||||||
)
|
|
||||||
return on_since
|
|
||||||
else:
|
|
||||||
self.raise_for_index(index)
|
|
||||||
return self.plugs[index].on_since
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_information(self) -> Dict[str, Any]:
|
def state_information(self) -> Dict[str, Any]:
|
||||||
"""
|
"""Return strip-specific state information.
|
||||||
Returns strip-specific state information.
|
|
||||||
|
|
||||||
:return: Strip information dict, keys in user-presentable form.
|
:return: Strip information dict, keys in user-presentable form.
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
"""
|
"""
|
||||||
state = {"LED state": self.led}
|
state: Dict[str, Any] = {"LED state": self.led}
|
||||||
is_on = self.get_is_on()
|
for plug in self.plugs:
|
||||||
on_since = self.get_on_since()
|
if plug.is_on:
|
||||||
for plug_index in range(self.num_children):
|
state["Plug %s on since" % str(plug)] = plug.on_since
|
||||||
plug_number = plug_index + 1
|
|
||||||
if is_on[plug_index]:
|
|
||||||
state["Plug %d on since" % plug_number] = on_since[plug_index]
|
|
||||||
|
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def get_emeter_realtime(self, *, index: int = -1) -> Optional[Any]:
|
def current_consumption(self) -> float:
|
||||||
"""
|
"""Get the current power consumption in watts.
|
||||||
Retrieve current energy readings from device
|
|
||||||
|
|
||||||
:param index: plug index (-1 for all)
|
:return: the current power consumption in watts.
|
||||||
:returns: list of current readings or None
|
:rtype: float
|
||||||
:rtype: Dict, Dict[int, Dict], None
|
|
||||||
Dict if index is provided
|
|
||||||
Dict[int, Dict] if no index provided
|
|
||||||
None if device has no energy meter or error occurred
|
|
||||||
:raises SmartDeviceException: on error
|
:raises SmartDeviceException: on error
|
||||||
:raises SmartStripException: index out of bounds
|
|
||||||
"""
|
"""
|
||||||
if not self.has_emeter: # pragma: no cover
|
consumption = sum(plug.current_consumption() for plug in self.plugs)
|
||||||
raise SmartStripException("Device has no emeter")
|
|
||||||
|
|
||||||
if index < 0:
|
return consumption
|
||||||
emeter_status = {}
|
|
||||||
for plug in range(self.num_children):
|
|
||||||
emeter_status[plug] = self.plugs[plug].get_emeter_realtime()
|
|
||||||
return emeter_status
|
|
||||||
else:
|
|
||||||
self.raise_for_index(index)
|
|
||||||
return self.plugs[index].get_emeter_realtime()
|
|
||||||
|
|
||||||
def current_consumption(self, *, index: int = -1) -> Optional[Any]:
|
@property # type: ignore # required to avoid mypy error on non-implemented setter
|
||||||
"""
|
def icon(self) -> Dict:
|
||||||
Get the current power consumption in Watts.
|
"""Icon for the device.
|
||||||
|
|
||||||
:param index: plug index (-1 for all)
|
Overriden to keep the API, as the SmartStrip and children do not
|
||||||
:return: the current power consumption in Watts.
|
have icons, we just return dummy strings.
|
||||||
None if device has no energy meter.
|
|
||||||
:rtype: Dict, Dict[int, Dict], None
|
|
||||||
Dict if index is provided
|
|
||||||
Dict[int, Dict] if no index provided
|
|
||||||
None if device has no energy meter or error occurred
|
|
||||||
:raises SmartDeviceException: on error
|
|
||||||
:raises SmartStripException: index out of bounds
|
|
||||||
"""
|
|
||||||
if not self.has_emeter: # pragma: no cover
|
|
||||||
raise SmartStripException("Device has no emeter")
|
|
||||||
|
|
||||||
if index < 0:
|
|
||||||
consumption = {}
|
|
||||||
emeter_reading = self.get_emeter_realtime()
|
|
||||||
for plug in range(self.num_children):
|
|
||||||
response = EmeterStatus(emeter_reading[plug])
|
|
||||||
consumption[plug] = response["power"]
|
|
||||||
return consumption
|
|
||||||
else:
|
|
||||||
self.raise_for_index(index)
|
|
||||||
response = EmeterStatus(self.get_emeter_realtime(index=index))
|
|
||||||
return response["power"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def icon(self):
|
|
||||||
"""Override for base class icon property, SmartStrip and children do not
|
|
||||||
have icons so we return dummy strings.
|
|
||||||
"""
|
"""
|
||||||
return {"icon": "SMARTSTRIP-DUMMY", "hash": "SMARTSTRIP-DUMMY"}
|
return {"icon": "SMARTSTRIP-DUMMY", "hash": "SMARTSTRIP-DUMMY"}
|
||||||
|
|
||||||
def get_alias(self, *, index: int = -1) -> Union[str, Dict[int, str]]:
|
def set_alias(self, alias: str) -> None:
|
||||||
"""Gets the alias for a plug.
|
"""Set the alias for the strip.
|
||||||
|
|
||||||
:param index: plug index (-1 for all)
|
|
||||||
:return: the current power consumption in Watts.
|
|
||||||
None if device has no energy meter.
|
|
||||||
:rtype: str if index is provided
|
|
||||||
Dict[int, str] if no index provided
|
|
||||||
:raises SmartStripException: index out of bounds
|
|
||||||
"""
|
|
||||||
children = self.sys_info["children"]
|
|
||||||
|
|
||||||
if index < 0:
|
|
||||||
alias = {}
|
|
||||||
for plug in range(self.num_children):
|
|
||||||
alias[plug] = children[plug]["alias"]
|
|
||||||
return alias
|
|
||||||
else:
|
|
||||||
self.raise_for_index(index)
|
|
||||||
return children[index]["alias"]
|
|
||||||
|
|
||||||
def set_alias(self, alias: str, *, index: int = -1):
|
|
||||||
"""Sets the alias for a plug
|
|
||||||
|
|
||||||
:param index: plug index
|
|
||||||
:param alias: new alias
|
:param alias: new alias
|
||||||
:raises SmartDeviceException: on error
|
:raises SmartDeviceException: on error
|
||||||
:raises SmartStripException: index out of bounds
|
|
||||||
"""
|
"""
|
||||||
# Renaming the whole strip
|
return super().set_alias(alias)
|
||||||
if index < 0:
|
|
||||||
return super().set_alias(alias)
|
|
||||||
|
|
||||||
self.raise_for_index(index)
|
|
||||||
self.plugs[index].set_alias(alias)
|
|
||||||
|
|
||||||
def get_emeter_daily(
|
def get_emeter_daily(
|
||||||
self, year: int = None, month: int = None, kwh: bool = True, *, index: int = -1
|
self, year: int = None, month: int = None, kwh: bool = True
|
||||||
) -> Dict:
|
) -> Dict:
|
||||||
"""Retrieve daily statistics for a given month
|
"""Retrieve daily statistics for a given month.
|
||||||
|
|
||||||
:param year: year for which to retrieve statistics (default: this year)
|
:param year: year for which to retrieve statistics (default: this year)
|
||||||
:param month: month for which to retrieve statistics (default: this
|
:param month: month for which to retrieve statistics (default: this
|
||||||
month)
|
month)
|
||||||
:param kwh: return usage in kWh (default: True)
|
:param kwh: return usage in kWh (default: True)
|
||||||
:return: mapping of day of month to value
|
:return: mapping of day of month to value
|
||||||
None if device has no energy meter or error occurred
|
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
:raises SmartDeviceException: on error
|
:raises SmartDeviceException: on error
|
||||||
:raises SmartStripException: index out of bounds
|
|
||||||
"""
|
"""
|
||||||
if not self.has_emeter: # pragma: no cover
|
emeter_daily: DefaultDict[int, float] = defaultdict(lambda: 0.0)
|
||||||
raise SmartStripException("Device has no emeter")
|
for plug in self.plugs:
|
||||||
|
for day, value in plug.get_emeter_daily(
|
||||||
|
year=year, month=month, kwh=kwh
|
||||||
|
).items():
|
||||||
|
emeter_daily[day] += value
|
||||||
|
return emeter_daily
|
||||||
|
|
||||||
emeter_daily = {}
|
def get_emeter_monthly(self, year: int = None, kwh: bool = True) -> Dict:
|
||||||
if index < 0:
|
|
||||||
for plug in range(self.num_children):
|
|
||||||
emeter_daily = self.plugs[plug].get_emeter_daily(
|
|
||||||
year=year, month=month, kwh=kwh
|
|
||||||
)
|
|
||||||
return emeter_daily
|
|
||||||
else:
|
|
||||||
self.raise_for_index(index)
|
|
||||||
return self.plugs[index].get_emeter_daily(year=year, month=month, kwh=kwh)
|
|
||||||
|
|
||||||
def get_emeter_monthly(
|
|
||||||
self, year: int = None, kwh: bool = True, *, index: int = -1
|
|
||||||
) -> Dict:
|
|
||||||
"""Retrieve monthly statistics for a given year.
|
"""Retrieve monthly statistics for a given year.
|
||||||
|
|
||||||
:param year: year for which to retrieve statistics (default: this year)
|
:param year: year for which to retrieve statistics (default: this year)
|
||||||
:param kwh: return usage in kWh (default: True)
|
:param kwh: return usage in kWh (default: True)
|
||||||
:return: dict: mapping of month to value
|
:return: dict: mapping of month to value
|
||||||
None if device has no energy meter
|
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
:raises SmartDeviceException: on error
|
:raises SmartDeviceException: on error
|
||||||
:raises SmartStripException: index out of bounds
|
|
||||||
"""
|
"""
|
||||||
if not self.has_emeter: # pragma: no cover
|
emeter_monthly: DefaultDict[int, float] = defaultdict(lambda: 0.0)
|
||||||
raise SmartStripException("Device has no emeter")
|
for plug in self.plugs:
|
||||||
|
for month, value in plug.get_emeter_monthly(year=year, kwh=kwh):
|
||||||
|
emeter_monthly[month] += value
|
||||||
|
return emeter_monthly
|
||||||
|
|
||||||
emeter_monthly = {}
|
def erase_emeter_stats(self):
|
||||||
if index < 0:
|
"""Erase energy meter statistics for all plugs.
|
||||||
for plug in range(self.num_children):
|
|
||||||
emeter_monthly = self.plugs[plug].get_emeter_monthly(year=year, kwh=kwh)
|
|
||||||
return emeter_monthly
|
|
||||||
else:
|
|
||||||
self.raise_for_index(index)
|
|
||||||
return self.plugs[index].get_emeter_monthly(year=year, kwh=kwh)
|
|
||||||
|
|
||||||
def erase_emeter_stats(self, *, index: int = -1) -> bool:
|
|
||||||
"""Erase energy meter statistics
|
|
||||||
|
|
||||||
:param index: plug index (-1 for all)
|
|
||||||
:return: True if statistics were deleted
|
|
||||||
False if device has no energy meter.
|
|
||||||
:rtype: bool
|
|
||||||
:raises SmartDeviceException: on error
|
:raises SmartDeviceException: on error
|
||||||
:raises SmartStripException: index out of bounds
|
|
||||||
"""
|
"""
|
||||||
if not self.has_emeter: # pragma: no cover
|
for plug in self.plugs:
|
||||||
raise SmartStripException("Device has no emeter")
|
plug.erase_emeter_stats()
|
||||||
|
|
||||||
if index < 0:
|
|
||||||
for plug in range(self.num_children):
|
|
||||||
self.plugs[plug].erase_emeter_stats()
|
|
||||||
else:
|
|
||||||
self.raise_for_index(index)
|
|
||||||
self.plugs[index].erase_emeter_stats()
|
|
||||||
|
|
||||||
# As query_helper raises exception in case of failure, we have
|
|
||||||
# succeeded when we are this far.
|
|
||||||
return True
|
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import pytest
|
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from .newfakes import FakeTransportProtocol
|
|
||||||
from os.path import basename
|
from os.path import basename
|
||||||
from pyHS100 import SmartPlug, SmartBulb, SmartStrip, Discover
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pyHS100 import Discover, SmartBulb, SmartPlug, SmartStrip
|
||||||
|
|
||||||
|
from .newfakes import FakeTransportProtocol
|
||||||
|
|
||||||
SUPPORTED_DEVICES = glob.glob(
|
SUPPORTED_DEVICES = glob.glob(
|
||||||
os.path.dirname(os.path.abspath(__file__)) + "/fixtures/*.json"
|
os.path.dirname(os.path.abspath(__file__)) + "/fixtures/*.json"
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
from ..protocol import TPLinkSmartHomeProtocol
|
|
||||||
from .. import SmartDeviceException
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from voluptuous import Schema, Range, All, Any, Coerce, Invalid, Optional, REMOVE_EXTRA
|
|
||||||
|
from voluptuous import REMOVE_EXTRA, All, Any, Coerce, Invalid, Optional, Range, Schema
|
||||||
|
|
||||||
|
from .. import SmartDeviceException
|
||||||
|
from ..protocol import TPLinkSmartHomeProtocol
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -1,31 +1,31 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from pyHS100 import DeviceType, SmartStripException, SmartDeviceException
|
from pyHS100 import DeviceType, SmartDeviceException
|
||||||
from .newfakes import (
|
|
||||||
BULB_SCHEMA,
|
|
||||||
PLUG_SCHEMA,
|
|
||||||
FakeTransportProtocol,
|
|
||||||
CURRENT_CONSUMPTION_SCHEMA,
|
|
||||||
TZ_SCHEMA,
|
|
||||||
)
|
|
||||||
from .conftest import (
|
from .conftest import (
|
||||||
turn_on,
|
|
||||||
handle_turn_on,
|
|
||||||
plug,
|
|
||||||
strip,
|
|
||||||
bulb,
|
bulb,
|
||||||
color_bulb,
|
color_bulb,
|
||||||
non_color_bulb,
|
dimmable,
|
||||||
|
handle_turn_on,
|
||||||
has_emeter,
|
has_emeter,
|
||||||
no_emeter,
|
no_emeter,
|
||||||
dimmable,
|
non_color_bulb,
|
||||||
non_dimmable,
|
non_dimmable,
|
||||||
variable_temp,
|
|
||||||
non_variable_temp,
|
non_variable_temp,
|
||||||
|
plug,
|
||||||
|
strip,
|
||||||
|
turn_on,
|
||||||
|
variable_temp,
|
||||||
|
)
|
||||||
|
from .newfakes import (
|
||||||
|
BULB_SCHEMA,
|
||||||
|
CURRENT_CONSUMPTION_SCHEMA,
|
||||||
|
PLUG_SCHEMA,
|
||||||
|
TZ_SCHEMA,
|
||||||
|
FakeTransportProtocol,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -436,67 +436,44 @@ def test_deprecated_hsv(dev, turn_on):
|
|||||||
dev.hsv = (1, 1, 1)
|
dev.hsv = (1, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
@strip
|
|
||||||
def test_children_is_on(dev):
|
|
||||||
is_on = dev.get_is_on()
|
|
||||||
for i in range(dev.num_children):
|
|
||||||
assert is_on[i] == dev.get_is_on(index=i)
|
|
||||||
|
|
||||||
|
|
||||||
@strip
|
@strip
|
||||||
@turn_on
|
@turn_on
|
||||||
def test_children_change_state(dev, turn_on):
|
def test_children_change_state(dev, turn_on):
|
||||||
handle_turn_on(dev, turn_on)
|
handle_turn_on(dev, turn_on)
|
||||||
for i in range(dev.num_children):
|
for plug in dev.plugs:
|
||||||
orig_state = dev.get_is_on(index=i)
|
orig_state = plug.is_on
|
||||||
if orig_state:
|
if orig_state:
|
||||||
dev.turn_off(index=i)
|
plug.turn_off()
|
||||||
assert not dev.get_is_on(index=i)
|
assert not plug.is_on
|
||||||
assert dev.get_is_off(index=i)
|
assert plug.is_off
|
||||||
|
|
||||||
dev.turn_on(index=i)
|
plug.turn_on()
|
||||||
assert dev.get_is_on(index=i)
|
assert plug.is_on
|
||||||
assert not dev.get_is_off(index=i)
|
assert not plug.is_off
|
||||||
else:
|
else:
|
||||||
dev.turn_on(index=i)
|
plug.turn_on()
|
||||||
assert dev.get_is_on(index=i)
|
assert plug.is_on
|
||||||
assert not dev.get_is_off(index=i)
|
assert not plug.is_off
|
||||||
dev.turn_off(index=i)
|
plug.turn_off()
|
||||||
assert not dev.get_is_on(index=i)
|
assert not plug.is_on
|
||||||
assert dev.get_is_off(index=i)
|
assert plug.is_off
|
||||||
|
|
||||||
|
|
||||||
@strip
|
|
||||||
def test_children_bounds(dev):
|
|
||||||
out_of_bounds = dev.num_children + 100
|
|
||||||
|
|
||||||
with pytest.raises(SmartDeviceException):
|
|
||||||
dev.turn_off(index=out_of_bounds)
|
|
||||||
with pytest.raises(SmartDeviceException):
|
|
||||||
dev.turn_on(index=out_of_bounds)
|
|
||||||
with pytest.raises(SmartDeviceException):
|
|
||||||
dev.get_is_on(index=out_of_bounds)
|
|
||||||
with pytest.raises(SmartDeviceException):
|
|
||||||
dev.get_alias(index=out_of_bounds)
|
|
||||||
with pytest.raises(SmartDeviceException):
|
|
||||||
dev.get_on_since(index=out_of_bounds)
|
|
||||||
|
|
||||||
|
|
||||||
@strip
|
@strip
|
||||||
def test_children_alias(dev):
|
def test_children_alias(dev):
|
||||||
original = dev.get_alias()
|
|
||||||
test_alias = "TEST1234"
|
test_alias = "TEST1234"
|
||||||
for idx in range(dev.num_children):
|
for plug in dev.plugs:
|
||||||
dev.set_alias(alias=test_alias, index=idx)
|
original = plug.get_alias()
|
||||||
assert dev.get_alias(index=idx) == test_alias
|
plug.set_alias(alias=test_alias)
|
||||||
dev.set_alias(alias=original[idx], index=idx)
|
assert plug.get_alias() == test_alias
|
||||||
assert dev.get_alias(index=idx) == original[idx]
|
plug.set_alias(alias=original)
|
||||||
|
assert plug.get_alias() == original
|
||||||
|
|
||||||
|
|
||||||
@strip
|
@strip
|
||||||
def test_children_on_since(dev):
|
def test_children_on_since(dev):
|
||||||
for idx in range(dev.num_children):
|
for plug in dev.plugs:
|
||||||
assert dev.get_on_since(index=idx)
|
assert plug.get_on_since()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip("this test will wear out your relays")
|
@pytest.mark.skip("this test will wear out your relays")
|
||||||
@ -548,78 +525,61 @@ def test_all_binary_states(dev):
|
|||||||
def test_children_get_emeter_realtime(dev):
|
def test_children_get_emeter_realtime(dev):
|
||||||
assert dev.has_emeter
|
assert dev.has_emeter
|
||||||
# test with index
|
# test with index
|
||||||
for plug_index in range(dev.num_children):
|
for plug in dev.plugs:
|
||||||
emeter = dev.get_emeter_realtime(index=plug_index)
|
emeter = plug.get_emeter_realtime()
|
||||||
CURRENT_CONSUMPTION_SCHEMA(emeter)
|
CURRENT_CONSUMPTION_SCHEMA(emeter)
|
||||||
|
|
||||||
# test without index
|
# test without index
|
||||||
for index, emeter in dev.get_emeter_realtime().items():
|
# TODO test that sum matches the sum of individiaul plugs.
|
||||||
CURRENT_CONSUMPTION_SCHEMA(emeter)
|
|
||||||
|
|
||||||
# out of bounds
|
# for index, emeter in dev.get_emeter_realtime().items():
|
||||||
with pytest.raises(SmartStripException):
|
# CURRENT_CONSUMPTION_SCHEMA(emeter)
|
||||||
dev.get_emeter_realtime(index=dev.num_children + 100)
|
|
||||||
|
|
||||||
|
|
||||||
@strip
|
@strip
|
||||||
def test_children_get_emeter_daily(dev):
|
def test_children_get_emeter_daily(dev):
|
||||||
assert dev.has_emeter
|
assert dev.has_emeter
|
||||||
# test with index
|
# test individual emeters
|
||||||
for plug_index in range(dev.num_children):
|
for plug in dev.plugs:
|
||||||
emeter = dev.get_emeter_daily(year=1900, month=1, index=plug_index)
|
emeter = plug.get_emeter_daily(year=1900, month=1)
|
||||||
assert emeter == {}
|
assert emeter == {}
|
||||||
|
|
||||||
emeter = dev.get_emeter_daily(index=plug_index)
|
emeter = plug.get_emeter_daily()
|
||||||
assert len(emeter) > 0
|
assert len(emeter) > 0
|
||||||
|
|
||||||
k, v = emeter.popitem()
|
k, v = emeter.popitem()
|
||||||
assert isinstance(k, int)
|
assert isinstance(k, int)
|
||||||
assert isinstance(v, float)
|
assert isinstance(v, float)
|
||||||
|
|
||||||
# test without index
|
# test sum of emeters
|
||||||
all_emeter = dev.get_emeter_daily(year=1900, month=1)
|
all_emeter = dev.get_emeter_daily(year=1900, month=1)
|
||||||
for plug_index, emeter in all_emeter.items():
|
|
||||||
assert emeter == {}
|
|
||||||
|
|
||||||
emeter = dev.get_emeter_daily(index=plug_index)
|
k, v = all_emeter.popitem()
|
||||||
|
assert isinstance(k, int)
|
||||||
k, v = emeter.popitem()
|
assert isinstance(v, float)
|
||||||
assert isinstance(k, int)
|
|
||||||
assert isinstance(v, float)
|
|
||||||
|
|
||||||
# out of bounds
|
|
||||||
with pytest.raises(SmartStripException):
|
|
||||||
dev.get_emeter_daily(year=1900, month=1, index=dev.num_children + 100)
|
|
||||||
|
|
||||||
|
|
||||||
@strip
|
@strip
|
||||||
def test_children_get_emeter_monthly(dev):
|
def test_children_get_emeter_monthly(dev):
|
||||||
assert dev.has_emeter
|
assert dev.has_emeter
|
||||||
# test with index
|
# test individual emeters
|
||||||
for plug_index in range(dev.num_children):
|
for plug in dev.plugs:
|
||||||
emeter = dev.get_emeter_monthly(year=1900, index=plug_index)
|
emeter = plug.get_emeter_monthly(year=1900)
|
||||||
assert emeter == {}
|
assert emeter == {}
|
||||||
|
|
||||||
emeter = dev.get_emeter_monthly()
|
emeter = plug.get_emeter_monthly()
|
||||||
assert len(emeter) > 0
|
assert len(emeter) > 0
|
||||||
|
|
||||||
k, v = emeter.popitem()
|
k, v = emeter.popitem()
|
||||||
assert isinstance(k, int)
|
assert isinstance(k, int)
|
||||||
assert isinstance(v, float)
|
assert isinstance(v, float)
|
||||||
|
|
||||||
# test without index
|
# test sum of emeters
|
||||||
all_emeter = dev.get_emeter_monthly(year=1900)
|
all_emeter = dev.get_emeter_monthly(year=1900)
|
||||||
for index, emeter in all_emeter.items():
|
|
||||||
assert emeter == {}
|
|
||||||
assert len(emeter) > 0
|
|
||||||
|
|
||||||
k, v = emeter.popitem()
|
k, v = all_emeter.popitem()
|
||||||
assert isinstance(k, int)
|
assert isinstance(k, int)
|
||||||
assert isinstance(v, float)
|
assert isinstance(v, float)
|
||||||
|
|
||||||
# out of bounds
|
|
||||||
with pytest.raises(SmartStripException):
|
|
||||||
dev.get_emeter_monthly(year=1900, index=dev.num_children + 100)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cache(dev):
|
def test_cache(dev):
|
||||||
@ -658,6 +618,6 @@ def test_cache_invalidates(dev):
|
|||||||
|
|
||||||
def test_representation(dev):
|
def test_representation(dev):
|
||||||
import re
|
import re
|
||||||
|
|
||||||
pattern = re.compile("<.* model .* at .* (.*), is_on: .* - dev specific: .*>")
|
pattern = re.compile("<.* model .* at .* (.*), is_on: .* - dev specific: .*>")
|
||||||
assert pattern.match(str(dev))
|
assert pattern.match(str(dev))
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from unittest import TestCase
|
|
||||||
from ..protocol import TPLinkSmartHomeProtocol
|
|
||||||
import json
|
import json
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from ..protocol import TPLinkSmartHomeProtocol
|
||||||
|
|
||||||
|
|
||||||
class TestTPLinkSmartHomeProtocol(TestCase):
|
class TestTPLinkSmartHomeProtocol(TestCase):
|
||||||
|
33
setup.py
33
setup.py
@ -1,18 +1,19 @@
|
|||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
setup(name='pyHS100',
|
with open("pyHS100/version.py") as f:
|
||||||
version='0.3.5',
|
exec(f.read())
|
||||||
description='Interface for TPLink HS1xx plugs, HS2xx wall switches & LB1xx bulbs',
|
|
||||||
url='https://github.com/GadgetReactor/pyHS100',
|
setup(
|
||||||
author='Sean Seah (GadgetReactor)',
|
name="pyHS100",
|
||||||
author_email='sean@gadgetreactor.com',
|
version=__version__, # type: ignore # noqa: F821
|
||||||
license='GPLv3',
|
description="Python interface for TPLink KASA-enabled smart home devices",
|
||||||
packages=['pyHS100'],
|
url="https://github.com/GadgetReactor/pyHS100",
|
||||||
install_requires=['click', 'deprecation'],
|
author="Sean Seah (GadgetReactor)",
|
||||||
python_requires='>=3.5',
|
author_email="sean@gadgetreactor.com",
|
||||||
entry_points={
|
license="GPLv3",
|
||||||
'console_scripts': [
|
packages=["pyHS100"],
|
||||||
'pyhs100=pyHS100.cli:cli',
|
install_requires=["click", "deprecation"],
|
||||||
],
|
python_requires=">=3.6",
|
||||||
},
|
entry_points={"console_scripts": ["pyhs100=pyHS100.cli:cli"]},
|
||||||
zip_safe=False)
|
zip_safe=False,
|
||||||
|
)
|
||||||
|
34
tox.ini
34
tox.ini
@ -1,5 +1,5 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist=py35,py36,py37,flake8
|
envlist=py35,py36,py37,flake8,linting,typing
|
||||||
skip_missing_interpreters = True
|
skip_missing_interpreters = True
|
||||||
|
|
||||||
[tox:travis]
|
[tox:travis]
|
||||||
@ -20,16 +20,31 @@ commands=
|
|||||||
py.test --cov --cov-config=tox.ini pyHS100
|
py.test --cov --cov-config=tox.ini pyHS100
|
||||||
|
|
||||||
[testenv:flake8]
|
[testenv:flake8]
|
||||||
deps=flake8
|
deps=
|
||||||
|
flake8
|
||||||
|
flake8-docstrings
|
||||||
commands=flake8 pyHS100
|
commands=flake8 pyHS100
|
||||||
max-line-length=88
|
|
||||||
|
|
||||||
[testenv:typing]
|
[testenv:typing]
|
||||||
|
skip_install=true
|
||||||
deps=mypy
|
deps=mypy
|
||||||
commands=mypy --silent-imports pyHS100
|
commands=mypy --ignore-missing-imports pyHS100
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
exclude = .git,.tox,__pycache__,pyHS100/tests/fakes.py
|
exclude = .git,.tox,__pycache__,pyHS100/tests/newfakes.py,pyHS100/tests/test_fixtures.py
|
||||||
|
max-line-length = 88
|
||||||
|
per-file-ignores =
|
||||||
|
pyHS100/tests/*.py:D100,D101,D102,D103,D104
|
||||||
|
setup.py:D100
|
||||||
|
ignore = D105, D107, E203, E501, W503
|
||||||
|
#ignore = E203, E266, E501, W503, F403, F401
|
||||||
|
#max-complexity = 18
|
||||||
|
#select = B,C,E,F,W,T4,B9
|
||||||
|
|
||||||
|
[testenv:lint]
|
||||||
|
deps = pre-commit
|
||||||
|
skip_install = true
|
||||||
|
commands = pre-commit run --all-files
|
||||||
|
|
||||||
[coverage:run]
|
[coverage:run]
|
||||||
source = pyHS100
|
source = pyHS100
|
||||||
@ -43,3 +58,12 @@ exclude_lines =
|
|||||||
# ignore abstract methods
|
# ignore abstract methods
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
def __repr__
|
def __repr__
|
||||||
|
|
||||||
|
[isort]
|
||||||
|
multi_line_output=3
|
||||||
|
include_trailing_comma=True
|
||||||
|
force_grid_wrap=0
|
||||||
|
use_parentheses=True
|
||||||
|
line_length=88
|
||||||
|
known_first_party=pyHS100
|
||||||
|
known_third_party=click,deprecation,pytest,setuptools,voluptuous
|
||||||
|
Loading…
x
Reference in New Issue
Block a user