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:
Teemu Rytilahti 2019-11-11 22:14:34 +01:00
parent 59424d2738
commit 8a131e1eeb
17 changed files with 354 additions and 549 deletions

View File

@ -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

View File

@ -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
View 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'

View File

@ -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

View File

@ -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:

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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):

View File

@ -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__)

View File

@ -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

View File

@ -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"

View File

@ -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__)

View File

@ -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))

View File

@ -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):

View File

@ -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
View File

@ -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