Fix flake8 issues and enable flake8 in tox

This commit is contained in:
Pierre Ståhl
2020-09-23 09:31:37 +02:00
parent a18c69f8eb
commit 4e72767b30
13 changed files with 67 additions and 64 deletions

View File

@@ -25,6 +25,7 @@ from .const import CONF_LOCAL_KEY, CONF_PROTOCOL_VERSION, DOMAIN
import pprint import pprint
pp = pprint.PrettyPrinter(indent=4) pp = pprint.PrettyPrinter(indent=4)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -138,7 +139,6 @@ def get_entity_config(config_entry, dps_id):
raise Exception(f"missing entity config for id {dps_id}") raise Exception(f"missing entity config for id {dps_id}")
class TuyaDevice: class TuyaDevice:
"""Cache wrapper for pytuya.TuyaInterface.""" """Cache wrapper for pytuya.TuyaInterface."""
@@ -160,7 +160,6 @@ class TuyaDevice:
for i in range(5): for i in range(5):
try: try:
status = self._interface.status() status = self._interface.status()
# print("STATUS OF [{}] IS [{}]".format(self._interface.address,status))
return status return status
except Exception: except Exception:
print( print(
@@ -177,17 +176,18 @@ class TuyaDevice:
raise ConnectionError("Failed to update status .") raise ConnectionError("Failed to update status .")
def set_dps(self, state, dps_index): def set_dps(self, state, dps_index):
#_LOGGER.info("running def set_dps from cover") # _LOGGER.info("running def set_dps from cover")
"""Change the Tuya switch status and clear the cache.""" """Change the Tuya switch status and clear the cache."""
self._cached_status = "" self._cached_status = ""
self._cached_status_time = 0 self._cached_status_time = 0
for i in range(5): for i in range(5):
try: try:
#_LOGGER.info("Running a try from def set_dps from cover where state=%s and dps_index=%s", state, dps_index)
return self._interface.set_dps(state, dps_index) return self._interface.set_dps(state, dps_index)
except Exception: except Exception:
print( print(
"Failed to set status of device [{}]".format(self._interface.address) "Failed to set status of device [{}]".format(
self._interface.address
)
) )
if i + 1 == 3: if i + 1 == 3:
_LOGGER.error( _LOGGER.error(

View File

@@ -1,8 +1,8 @@
"""Constants for localtuya integration.""" """Constants for localtuya integration."""
ATTR_CURRENT = 'current' ATTR_CURRENT = "current"
ATTR_CURRENT_CONSUMPTION = 'current_consumption' ATTR_CURRENT_CONSUMPTION = "current_consumption"
ATTR_VOLTAGE = 'voltage' ATTR_VOLTAGE = "voltage"
CONF_LOCAL_KEY = "local_key" CONF_LOCAL_KEY = "local_key"
CONF_PROTOCOL_VERSION = "protocol_version" CONF_PROTOCOL_VERSION = "protocol_version"
@@ -15,9 +15,9 @@ CONF_CURRENT_CONSUMPTION = "current_consumption"
CONF_VOLTAGE = "voltage" CONF_VOLTAGE = "voltage"
# cover # cover
CONF_OPEN_CMD = 'open_cmd' CONF_OPEN_CMD = "open_cmd"
CONF_CLOSE_CMD = 'close_cmd' CONF_CLOSE_CMD = "close_cmd"
CONF_STOP_CMD = 'stop_cmd' CONF_STOP_CMD = "stop_cmd"
# sensor # sensor
CONF_SCALING = "scaling" CONF_SCALING = "scaling"

View File

@@ -19,7 +19,7 @@ cover:
""" """
import logging import logging
from time import time, sleep from time import sleep
import voluptuous as vol import voluptuous as vol
@@ -50,7 +50,6 @@ from .const import (
CONF_CLOSE_CMD, CONF_CLOSE_CMD,
CONF_STOP_CMD, CONF_STOP_CMD,
) )
from .const import CONF_OPEN_CMD, CONF_CLOSE_CMD, CONF_STOP_CMD
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -79,9 +78,7 @@ def flow_schema(dps):
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up a Tuya cover based on a config entry.""" """Set up a Tuya cover based on a config entry."""
tuyainterface, entities_to_setup = prepare_setup_entities( tuyainterface, entities_to_setup = prepare_setup_entities(config_entry, DOMAIN)
config_entry, DOMAIN
)
if not entities_to_setup: if not entities_to_setup:
return return
@@ -97,6 +94,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(covers, True) async_add_entities(covers, True)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up of the Tuya cover.""" """Set up of the Tuya cover."""
return import_from_yaml(hass, config, DOMAIN) return import_from_yaml(hass, config, DOMAIN)
@@ -113,12 +111,11 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity):
**kwargs, **kwargs,
): ):
"""Initialize a new LocaltuyaCover.""" """Initialize a new LocaltuyaCover."""
#_LOGGER.info("running def __init__ of LocaltuyaCover(CoverEntity) with self=%s device=%s name=%s friendly_name=%s icon=%s switchid=%s open_cmd=%s close_cmd=%s stop_cmd=%s", self, device, name, friendly_name, icon, switchid, open_cmd, close_cmd, stop_cmd)
super().__init__(device, config_entry, switchid, **kwargs) super().__init__(device, config_entry, switchid, **kwargs)
self._state = None self._state = None
self._position = 50 self._position = 50
print( print(
"Initialized tuya cover [{}] with switch status [{}] and state [{}]".format( "Initialized cover [{}] with status [{}] and state [{}]".format(
self.name, self._status, self._state self.name, self._status, self._state
) )
) )
@@ -198,7 +195,6 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity):
self.stop_cover() self.stop_cover()
self._position = 50 # newpos self._position = 50 # newpos
def open_cover(self, **kwargs): def open_cover(self, **kwargs):
"""Open the cover.""" """Open the cover."""
_LOGGER.debug("Launching command %s to cover ", self._config[CONF_OPEN_CMD]) _LOGGER.debug("Launching command %s to cover ", self._config[CONF_OPEN_CMD])

View File

@@ -18,8 +18,9 @@ UDP_KEY = md5(b"yGAdlopoPVldABfn").digest()
def decrypt_udp(message): def decrypt_udp(message):
"""Decrypt encrypted UDP broadcasts.""" """Decrypt encrypted UDP broadcasts."""
def _unpad(data): def _unpad(data):
return data[:-ord(data[len(data) - 1:])] return data[: -ord(data[len(data) - 1 :])]
return _unpad(AES.new(UDP_KEY, AES.MODE_ECB).decrypt(message)).decode() return _unpad(AES.new(UDP_KEY, AES.MODE_ECB).decrypt(message)).decode()

View File

@@ -15,7 +15,6 @@ fan:
""" """
import logging import logging
from time import time, sleep
from homeassistant.components.fan import ( from homeassistant.components.fan import (
FanEntity, FanEntity,
@@ -59,7 +58,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for device_config in entities_to_setup: for device_config in entities_to_setup:
fans.append( fans.append(
LocaltuyaFan( LocaltuyaFan(
TuyaCache(tuyainterface, config_entry.data[CONF_FRIENDLY_NAME]), TuyaDevice(tuyainterface, config_entry.data[CONF_FRIENDLY_NAME]),
config_entry, config_entry,
device_config[CONF_ID], device_config[CONF_ID],
) )
@@ -73,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return import_from_yaml(hass, config, DOMAIN) return import_from_yaml(hass, config, DOMAIN)
class LocaltuyaFan(LocalTuyaEntity, FanEntity): class LocaltuyaFan(LocalTuyaEntity, FanEntity):
"""Representation of a Tuya fan.""" """Representation of a Tuya fan."""

View File

@@ -11,9 +11,7 @@ light:
friendly_name: This Light friendly_name: This Light
protocol_version: 3.3 protocol_version: 3.3
""" """
import socket
import logging import logging
from time import time, sleep
from homeassistant.const import ( from homeassistant.const import (
CONF_ID, CONF_ID,
@@ -71,7 +69,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
lights.append( lights.append(
LocaltuyaLight( LocaltuyaLight(
TuyaCache(tuyainterface, config_entry.data[CONF_FRIENDLY_NAME]), TuyaDevice(tuyainterface, config_entry.data[CONF_FRIENDLY_NAME]),
config_entry, config_entry,
device_config[CONF_ID], device_config[CONF_ID],
) )
@@ -85,7 +83,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return import_from_yaml(hass, config, DOMAIN) return import_from_yaml(hass, config, DOMAIN)
class LocaltuyaLight(LocalTuyaEntity, LightEntity): class LocaltuyaLight(LocalTuyaEntity, LightEntity):
"""Representation of a Tuya light.""" """Representation of a Tuya light."""

View File

@@ -17,18 +17,19 @@ Classes
address (str): Device Network IP Address e.g. 10.0.1.99 address (str): Device Network IP Address e.g. 10.0.1.99
local_key (str, optional): The encryption key. Defaults to None. local_key (str, optional): The encryption key. Defaults to None.
Functions Functions
json = status() # returns json payload json = status() # returns json payload
set_version(version) # 3.1 [default] or 3.3 set_version(version) # 3.1 [default] or 3.3
detect_available_dps() # returns a list of available dps provided by the device detect_available_dps() # returns a list of available dps provided by the device
add_dps_to_request(dps_index) # adds dps_index to the list of dps used by the device (to be queried in the payload) add_dps_to_request(dps_index) # adds dps_index to the list of dps used by the
# device (to be queried in the payload)
set_dps(on, dps_index) # Set value of any dps index. set_dps(on, dps_index) # Set value of any dps index.
set_timer(num_secs): set_timer(num_secs):
Credits Credits
* TuyaAPI https://github.com/codetheweb/tuyapi by codetheweb and blackrozes * TuyaAPI https://github.com/codetheweb/tuyapi by codetheweb and blackrozes
For protocol reverse engineering For protocol reverse engineering
* PyTuya https://github.com/clach04/python-tuya by clach04 * PyTuya https://github.com/clach04/python-tuya by clach04
The origin of this python module (now abandoned) The origin of this python module (now abandoned)
* LocalTuya https://github.com/rospogrigio/localtuya-homeassistant by rospogrigio * LocalTuya https://github.com/rospogrigio/localtuya-homeassistant by rospogrigio
@@ -37,13 +38,11 @@ Credits
import base64 import base64
from hashlib import md5 from hashlib import md5
from itertools import chain
import json import json
import logging import logging
import socket import socket
import sys import sys
import time import time
import colorsys
import binascii import binascii
try: try:
@@ -160,20 +159,26 @@ def hex2bin(x):
return bytes.fromhex(x) return bytes.fromhex(x)
# This is intended to match requests.json payload at https://github.com/codetheweb/tuyapi : # This is intended to match requests.json payload at
# https://github.com/codetheweb/tuyapi :
# type_0a devices require the 0a command as the status request # type_0a devices require the 0a command as the status request
# type_0d devices require the 0d command as the status request, and the list of dps used set to null in the request payload (see generate_payload method) # type_0d devices require the 0d command as the status request, and the list of
# dps used set to null in the request payload (see generate_payload method)
# prefix: # Next byte is command byte ("hexByte") some zero padding, then length
# of remaining payload, i.e. command + suffix (unclear if multiple bytes used for
# length, zero padding implies could be more than one byte)
payload_dict = { payload_dict = {
"type_0a": { "type_0a": {
"status": {"hexByte": "0a", "command": {"gwId": "", "devId": ""}}, "status": {"hexByte": "0a", "command": {"gwId": "", "devId": ""}},
"set": {"hexByte": "07", "command": {"devId": "", "uid": "", "t": ""}}, "set": {"hexByte": "07", "command": {"devId": "", "uid": "", "t": ""}},
"prefix": "000055aa00000000000000", # Next byte is command byte ("hexByte") some zero padding, then length of remaining payload, i.e. command + suffix (unclear if multiple bytes used for length, zero padding implies could be more than one byte) "prefix": "000055aa00000000000000",
"suffix": "000000000000aa55", "suffix": "000000000000aa55",
}, },
"type_0d": { "type_0d": {
"status": {"hexByte": "0d", "command": {"devId": "", "uid": "", "t": ""}}, "status": {"hexByte": "0d", "command": {"devId": "", "uid": "", "t": ""}},
"set": {"hexByte": "07", "command": {"devId": "", "uid": "", "t": ""}}, "set": {"hexByte": "07", "command": {"devId": "", "uid": "", "t": ""}},
"prefix": "000055aa00000000000000", # Next byte is command byte ("hexByte") some zero padding, then length of remaining payload, i.e. command + suffix (unclear if multiple bytes used for length, zero padding implies could be more than one byte) "prefix": "000055aa00000000000000",
"suffix": "000000000000aa55", "suffix": "000000000000aa55",
}, },
} }
@@ -235,7 +240,8 @@ class TuyaInterface:
try: try:
data = s.recv(1024) data = s.recv(1024)
# print("FIRST: Received %d bytes" % len(data) ) # print("FIRST: Received %d bytes" % len(data) )
# sometimes the first packet does not contain data (typically 28 bytes): need to read again # sometimes the first packet does not contain data (typically 28 bytes):
# need to read again
if len(data) < 40: if len(data) < 40:
time.sleep(0.1) time.sleep(0.1)
data = s.recv(1024) data = s.recv(1024)
@@ -250,19 +256,21 @@ class TuyaInterface:
def detect_available_dps(self): def detect_available_dps(self):
"""Return which datapoints are supported by the device.""" """Return which datapoints are supported by the device."""
# type_0d devices need a sort of bruteforce querying in order to detect the list of available dps # type_0d devices need a sort of bruteforce querying in order to detect the
# experience shows that the dps available are usually in the ranges [1-25] and [100-110] # list of available dps experience shows that the dps available are usually
# need to split the bruteforcing in different steps due to request payload limitation (max. length = 255) # in the ranges [1-25] and [100-110] need to split the bruteforcing in
# different steps due to request payload limitation (max. length = 255)
detected_dps = {} detected_dps = {}
# dps 1 must always be sent, otherwise it might fail in case no dps is found in the requested range # dps 1 must always be sent, otherwise it might fail in case no dps is found
# in the requested range
self.dps_to_request = {"1": None} self.dps_to_request = {"1": None}
self.add_dps_to_request(range(2, 11)) self.add_dps_to_request(range(2, 11))
try: try:
data = self.status() data = self.status()
except Exception as e: except Exception as e:
print("Failed to get status: [{}]".format(e)) print("Failed to get status: [{}]".format(e))
raise CannotConnect raise
detected_dps.update(data["dps"]) detected_dps.update(data["dps"])
if self.dev_type == "type_0a": if self.dev_type == "type_0a":
@@ -274,7 +282,7 @@ class TuyaInterface:
data = self.status() data = self.status()
except Exception as e: except Exception as e:
print("Failed to get status: [{}]".format(e)) print("Failed to get status: [{}]".format(e))
raise CannotConnect raise
detected_dps.update(data["dps"]) detected_dps.update(data["dps"])
self.dps_to_request = {"1": None} self.dps_to_request = {"1": None}
@@ -283,7 +291,7 @@ class TuyaInterface:
data = self.status() data = self.status()
except Exception as e: except Exception as e:
print("Failed to get status: [{}]".format(e)) print("Failed to get status: [{}]".format(e))
raise CannotConnect raise
detected_dps.update(data["dps"]) detected_dps.update(data["dps"])
self.dps_to_request = {"1": None} self.dps_to_request = {"1": None}
@@ -292,7 +300,7 @@ class TuyaInterface:
data = self.status() data = self.status()
except Exception as e: except Exception as e:
print("Failed to get status: [{}]".format(e)) print("Failed to get status: [{}]".format(e))
raise CannotConnect raise
detected_dps.update(data["dps"]) detected_dps.update(data["dps"])
# print("DATA IS [{}] detected_dps [{}]".format(data,detected_dps)) # print("DATA IS [{}] detected_dps [{}]".format(data,detected_dps))
@@ -400,8 +408,6 @@ class TuyaInterface:
# calc the CRC of everything except where the CRC goes and the suffix # calc the CRC of everything except where the CRC goes and the suffix
hex_crc = format(binascii.crc32(buffer[:-8]) & 0xFFFFFFFF, "08X") hex_crc = format(binascii.crc32(buffer[:-8]) & 0xFFFFFFFF, "08X")
buffer = buffer[:-8] + hex2bin(hex_crc) + buffer[-4:] buffer = buffer[:-8] + hex2bin(hex_crc) + buffer[-4:]
# print('full buffer(%d) %r' % (len(buffer), bin2hex(buffer, pretty=True) ))
# print('full buffer(%d) %r' % (len(buffer), " ".join("{:02x}".format(ord(c)) for c in buffer)))
return buffer return buffer
def status(self): def status(self):
@@ -418,7 +424,8 @@ class TuyaInterface:
result = result[15:] result = result[15:]
log.debug("result=%r", result) log.debug("result=%r", result)
# result = data[data.find('{'):data.rfind('}')+1] # naive marker search, hope neither { nor } occur in header/footer # result = data[data.find('{'):data.rfind('}')+1] # naive marker search,
# hope neither { nor } occur in header/footer
# print('result %r' % result) # print('result %r' % result)
if result.startswith(b"{"): if result.startswith(b"{"):
# this is the regular expected code path # this is the regular expected code path
@@ -427,12 +434,13 @@ class TuyaInterface:
result = json.loads(result) result = json.loads(result)
elif result.startswith(PROTOCOL_VERSION_BYTES_31): elif result.startswith(PROTOCOL_VERSION_BYTES_31):
# got an encrypted payload, happens occasionally # got an encrypted payload, happens occasionally
# expect resulting json to look similar to:: {"devId":"ID","dps":{"1":true,"2":0},"t":EPOCH_SECS,"s":3_DIGIT_NUM} # expect resulting json to look similar to:
# {"devId":"ID","dps":{"1":true,"2":0},"t":EPOCH_SECS,"s":3_DIGIT_NUM}
# NOTE dps.2 may or may not be present # NOTE dps.2 may or may not be present
result = result[len(PROTOCOL_VERSION_BYTES_31) :] # remove version header result = result[len(PROTOCOL_VERSION_BYTES_31) :] # remove version header
result = result[ # remove (what I'm guessing, but not confirmed is) 16-bytes of MD5
16: # hexdigest of payload
] # remove (what I'm guessing, but not confirmed is) 16-bytes of MD5 hexdigest of payload result = result[16:]
cipher = AESCipher(self.local_key) cipher = AESCipher(self.local_key)
result = cipher.decrypt(result) result = cipher.decrypt(result)
print("decrypted result=[{}]".format(result)) print("decrypted result=[{}]".format(result))
@@ -487,7 +495,8 @@ class TuyaInterface:
""" """
# FIXME / TODO support schemas? Accept timer id number as parameter? # FIXME / TODO support schemas? Accept timer id number as parameter?
# Dumb heuristic; Query status, pick last device id as that is probably the timer # Dumb heuristic; Query status, pick last device id as that is probably
# the timer
status = self.status() status = self.status()
devices = status["dps"] devices = status["dps"]
devices_numbers = list(devices.keys()) devices_numbers = list(devices.keys())

View File

@@ -15,7 +15,6 @@ sensor:
device_class: current device_class: current
""" """
import logging import logging
from time import time, sleep
import voluptuous as vol import voluptuous as vol
@@ -65,7 +64,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for device_config in entities_to_setup: for device_config in entities_to_setup:
sensors.append( sensors.append(
LocaltuyaSensor( LocaltuyaSensor(
TuyaCache(tuyainterface, config_entry.data[CONF_FRIENDLY_NAME]), TuyaDevice(tuyainterface, config_entry.data[CONF_FRIENDLY_NAME]),
config_entry, config_entry,
device_config[CONF_ID], device_config[CONF_ID],
) )
@@ -79,7 +78,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return import_from_yaml(hass, config, DOMAIN) return import_from_yaml(hass, config, DOMAIN)
class LocaltuyaSensor(LocalTuyaEntity): class LocaltuyaSensor(LocalTuyaEntity):
"""Representation of a Tuya sensor.""" """Representation of a Tuya sensor."""

View File

@@ -25,7 +25,6 @@ switch:
id: 7 id: 7
""" """
import logging import logging
from time import time, sleep
import voluptuous as vol import voluptuous as vol
@@ -104,7 +103,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
if device_config.get(CONF_CURRENT, "-1") != "-1": if device_config.get(CONF_CURRENT, "-1") != "-1":
tuyainterface.add_dps_to_request(device_config.get(CONF_CURRENT)) tuyainterface.add_dps_to_request(device_config.get(CONF_CURRENT))
if device_config.get(CONF_CURRENT_CONSUMPTION, "-1") != "-1": if device_config.get(CONF_CURRENT_CONSUMPTION, "-1") != "-1":
tuyainterface.add_dps_to_request(device_config.get(CONF_CURRENT_CONSUMPTION)) tuyainterface.add_dps_to_request(
device_config.get(CONF_CURRENT_CONSUMPTION)
)
if device_config.get(CONF_VOLTAGE, "-1") != "-1": if device_config.get(CONF_VOLTAGE, "-1") != "-1":
tuyainterface.add_dps_to_request(device_config.get(CONF_VOLTAGE)) tuyainterface.add_dps_to_request(device_config.get(CONF_VOLTAGE))
@@ -124,8 +125,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return import_from_yaml(hass, config, DOMAIN) return import_from_yaml(hass, config, DOMAIN)
class LocaltuyaSwitch(LocalTuyaEntity, SwitchEntity): class LocaltuyaSwitch(LocalTuyaEntity, SwitchEntity):
"""Representation of a Tuya switch.""" """Representation of a Tuya switch."""
@@ -140,7 +139,7 @@ class LocaltuyaSwitch(LocalTuyaEntity, SwitchEntity):
super().__init__(device, config_entry, switchid, **kwargs) super().__init__(device, config_entry, switchid, **kwargs)
self._state = None self._state = None
print( print(
"Initialized tuya switch [{}] with switch status [{}] and state [{}]".format( "Initialized switch [{}] with status [{}] and state [{}]".format(
self.name, self._status, self._state self.name, self._status, self._state
) )
) )

View File

@@ -1,3 +1,3 @@
[tool.black] [tool.black]
target-version = ["py37", "py38"] target-version = ["py37", "py38"]
include = "custom_components/localtuya/*.py" include = 'custom_components/localtuya/.*\.py'

View File

@@ -1,3 +1,4 @@
black==20.8b1 black==20.8b1
flake8==3.8.3
mypy==0.782 mypy==0.782
pydocstyle==5.1.1 pydocstyle==5.1.1

4
setup.cfg Normal file
View File

@@ -0,0 +1,4 @@
[flake8]
exclude = .git,.tox
max-line-length = 88
ignore = E203, W503

View File

@@ -28,7 +28,7 @@ deps =
{[testenv]deps} {[testenv]deps}
commands = commands =
#codespell -q 4 -L {[tox]cs_exclude_words} --skip="*.pyc,*.pyi,*~" custom_components #codespell -q 4 -L {[tox]cs_exclude_words} --skip="*.pyc,*.pyi,*~" custom_components
#flake8 cu flake8 custom_components
black --fast --check . black --fast --check .
pydocstyle -v custom_components pydocstyle -v custom_components