From 4e72767b30379381fe9efe634b61943f67895125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Wed, 23 Sep 2020 09:31:37 +0200 Subject: [PATCH] Fix flake8 issues and enable flake8 in tox --- custom_components/localtuya/__init__.py | 10 +-- custom_components/localtuya/const.py | 12 ++-- custom_components/localtuya/cover.py | 12 ++-- custom_components/localtuya/discovery.py | 3 +- custom_components/localtuya/fan.py | 4 +- custom_components/localtuya/light.py | 5 +- .../localtuya/pytuya/__init__.py | 63 +++++++++++-------- custom_components/localtuya/sensor.py | 4 +- custom_components/localtuya/switch.py | 9 ++- pyproject.toml | 2 +- requirements_test.txt | 1 + setup.cfg | 4 ++ tox.ini | 2 +- 13 files changed, 67 insertions(+), 64 deletions(-) create mode 100644 setup.cfg diff --git a/custom_components/localtuya/__init__.py b/custom_components/localtuya/__init__.py index cfce242..5ed369c 100644 --- a/custom_components/localtuya/__init__.py +++ b/custom_components/localtuya/__init__.py @@ -25,6 +25,7 @@ from .const import CONF_LOCAL_KEY, CONF_PROTOCOL_VERSION, DOMAIN import pprint + pp = pprint.PrettyPrinter(indent=4) _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}") - class TuyaDevice: """Cache wrapper for pytuya.TuyaInterface.""" @@ -160,7 +160,6 @@ class TuyaDevice: for i in range(5): try: status = self._interface.status() -# print("STATUS OF [{}] IS [{}]".format(self._interface.address,status)) return status except Exception: print( @@ -177,17 +176,18 @@ class TuyaDevice: raise ConnectionError("Failed to update status .") 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.""" self._cached_status = "" self._cached_status_time = 0 for i in range(5): 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) except Exception: 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: _LOGGER.error( diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 48652bb..16ae74b 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -1,8 +1,8 @@ """Constants for localtuya integration.""" -ATTR_CURRENT = 'current' -ATTR_CURRENT_CONSUMPTION = 'current_consumption' -ATTR_VOLTAGE = 'voltage' +ATTR_CURRENT = "current" +ATTR_CURRENT_CONSUMPTION = "current_consumption" +ATTR_VOLTAGE = "voltage" CONF_LOCAL_KEY = "local_key" CONF_PROTOCOL_VERSION = "protocol_version" @@ -15,9 +15,9 @@ CONF_CURRENT_CONSUMPTION = "current_consumption" CONF_VOLTAGE = "voltage" # cover -CONF_OPEN_CMD = 'open_cmd' -CONF_CLOSE_CMD = 'close_cmd' -CONF_STOP_CMD = 'stop_cmd' +CONF_OPEN_CMD = "open_cmd" +CONF_CLOSE_CMD = "close_cmd" +CONF_STOP_CMD = "stop_cmd" # sensor CONF_SCALING = "scaling" diff --git a/custom_components/localtuya/cover.py b/custom_components/localtuya/cover.py index a88be5a..2fa7878 100644 --- a/custom_components/localtuya/cover.py +++ b/custom_components/localtuya/cover.py @@ -19,7 +19,7 @@ cover: """ import logging -from time import time, sleep +from time import sleep import voluptuous as vol @@ -50,7 +50,6 @@ from .const import ( CONF_CLOSE_CMD, CONF_STOP_CMD, ) -from .const import CONF_OPEN_CMD, CONF_CLOSE_CMD, CONF_STOP_CMD _LOGGER = logging.getLogger(__name__) @@ -79,9 +78,7 @@ def flow_schema(dps): async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Tuya cover based on a config entry.""" - tuyainterface, entities_to_setup = prepare_setup_entities( - config_entry, DOMAIN - ) + tuyainterface, entities_to_setup = prepare_setup_entities(config_entry, DOMAIN) if not entities_to_setup: return @@ -97,6 +94,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(covers, True) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up of the Tuya cover.""" return import_from_yaml(hass, config, DOMAIN) @@ -113,12 +111,11 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity): **kwargs, ): """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) self._state = None self._position = 50 print( - "Initialized tuya cover [{}] with switch status [{}] and state [{}]".format( + "Initialized cover [{}] with status [{}] and state [{}]".format( self.name, self._status, self._state ) ) @@ -198,7 +195,6 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity): self.stop_cover() self._position = 50 # newpos - def open_cover(self, **kwargs): """Open the cover.""" _LOGGER.debug("Launching command %s to cover ", self._config[CONF_OPEN_CMD]) diff --git a/custom_components/localtuya/discovery.py b/custom_components/localtuya/discovery.py index 297626f..672eec0 100644 --- a/custom_components/localtuya/discovery.py +++ b/custom_components/localtuya/discovery.py @@ -18,8 +18,9 @@ UDP_KEY = md5(b"yGAdlopoPVldABfn").digest() def decrypt_udp(message): """Decrypt encrypted UDP broadcasts.""" + 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() diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 9f3f411..f5947c4 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -15,7 +15,6 @@ fan: """ import logging -from time import time, sleep from homeassistant.components.fan import ( FanEntity, @@ -59,7 +58,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for device_config in entities_to_setup: fans.append( LocaltuyaFan( - TuyaCache(tuyainterface, config_entry.data[CONF_FRIENDLY_NAME]), + TuyaDevice(tuyainterface, config_entry.data[CONF_FRIENDLY_NAME]), config_entry, 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) - class LocaltuyaFan(LocalTuyaEntity, FanEntity): """Representation of a Tuya fan.""" diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index 6750b18..12a72d5 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -11,9 +11,7 @@ light: friendly_name: This Light protocol_version: 3.3 """ -import socket import logging -from time import time, sleep from homeassistant.const import ( CONF_ID, @@ -71,7 +69,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): lights.append( LocaltuyaLight( - TuyaCache(tuyainterface, config_entry.data[CONF_FRIENDLY_NAME]), + TuyaDevice(tuyainterface, config_entry.data[CONF_FRIENDLY_NAME]), config_entry, 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) - class LocaltuyaLight(LocalTuyaEntity, LightEntity): """Representation of a Tuya light.""" diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 0b5741f..a60fd89 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -17,18 +17,19 @@ Classes address (str): Device Network IP Address e.g. 10.0.1.99 local_key (str, optional): The encryption key. Defaults to None. -Functions +Functions json = status() # returns json payload set_version(version) # 3.1 [default] or 3.3 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_timer(num_secs): - + Credits * 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 The origin of this python module (now abandoned) * LocalTuya https://github.com/rospogrigio/localtuya-homeassistant by rospogrigio @@ -37,13 +38,11 @@ Credits import base64 from hashlib import md5 -from itertools import chain import json import logging import socket import sys import time -import colorsys import binascii try: @@ -160,20 +159,26 @@ def hex2bin(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_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 = { "type_0a": { "status": {"hexByte": "0a", "command": {"gwId": "", "devId": ""}}, "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", }, "type_0d": { "status": {"hexByte": "0d", "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", }, } @@ -235,7 +240,8 @@ class TuyaInterface: try: data = s.recv(1024) # 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: time.sleep(0.1) data = s.recv(1024) @@ -250,19 +256,21 @@ class TuyaInterface: def detect_available_dps(self): """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 - # experience shows that the dps available are usually in the ranges [1-25] and [100-110] - # need to split the bruteforcing in different steps due to request payload limitation (max. length = 255) + # type_0d devices need a sort of bruteforce querying in order to detect the + # list of available dps experience shows that the dps available are usually + # 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 = {} - # 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.add_dps_to_request(range(2, 11)) try: data = self.status() except Exception as e: print("Failed to get status: [{}]".format(e)) - raise CannotConnect + raise detected_dps.update(data["dps"]) if self.dev_type == "type_0a": @@ -274,7 +282,7 @@ class TuyaInterface: data = self.status() except Exception as e: print("Failed to get status: [{}]".format(e)) - raise CannotConnect + raise detected_dps.update(data["dps"]) self.dps_to_request = {"1": None} @@ -283,7 +291,7 @@ class TuyaInterface: data = self.status() except Exception as e: print("Failed to get status: [{}]".format(e)) - raise CannotConnect + raise detected_dps.update(data["dps"]) self.dps_to_request = {"1": None} @@ -292,7 +300,7 @@ class TuyaInterface: data = self.status() except Exception as e: print("Failed to get status: [{}]".format(e)) - raise CannotConnect + raise detected_dps.update(data["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 hex_crc = format(binascii.crc32(buffer[:-8]) & 0xFFFFFFFF, "08X") 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 def status(self): @@ -418,7 +424,8 @@ class TuyaInterface: result = result[15:] 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) if result.startswith(b"{"): # this is the regular expected code path @@ -427,12 +434,13 @@ class TuyaInterface: result = json.loads(result) elif result.startswith(PROTOCOL_VERSION_BYTES_31): # 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 result = result[len(PROTOCOL_VERSION_BYTES_31) :] # remove version header - result = result[ - 16: - ] # remove (what I'm guessing, but not confirmed is) 16-bytes of MD5 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) result = cipher.decrypt(result) print("decrypted result=[{}]".format(result)) @@ -487,7 +495,8 @@ class TuyaInterface: """ # 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() devices = status["dps"] devices_numbers = list(devices.keys()) diff --git a/custom_components/localtuya/sensor.py b/custom_components/localtuya/sensor.py index 5d90de6..6ea19fc 100644 --- a/custom_components/localtuya/sensor.py +++ b/custom_components/localtuya/sensor.py @@ -15,7 +15,6 @@ sensor: device_class: current """ import logging -from time import time, sleep 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: sensors.append( LocaltuyaSensor( - TuyaCache(tuyainterface, config_entry.data[CONF_FRIENDLY_NAME]), + TuyaDevice(tuyainterface, config_entry.data[CONF_FRIENDLY_NAME]), config_entry, 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) - class LocaltuyaSensor(LocalTuyaEntity): """Representation of a Tuya sensor.""" diff --git a/custom_components/localtuya/switch.py b/custom_components/localtuya/switch.py index 37df479..5c66c35 100644 --- a/custom_components/localtuya/switch.py +++ b/custom_components/localtuya/switch.py @@ -25,7 +25,6 @@ switch: id: 7 """ import logging -from time import time, sleep 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": tuyainterface.add_dps_to_request(device_config.get(CONF_CURRENT)) 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": 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) - - class LocaltuyaSwitch(LocalTuyaEntity, SwitchEntity): """Representation of a Tuya switch.""" @@ -140,7 +139,7 @@ class LocaltuyaSwitch(LocalTuyaEntity, SwitchEntity): super().__init__(device, config_entry, switchid, **kwargs) self._state = None print( - "Initialized tuya switch [{}] with switch status [{}] and state [{}]".format( + "Initialized switch [{}] with status [{}] and state [{}]".format( self.name, self._status, self._state ) ) diff --git a/pyproject.toml b/pyproject.toml index 7bb6308..6af5e6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [tool.black] target-version = ["py37", "py38"] -include = "custom_components/localtuya/*.py" +include = 'custom_components/localtuya/.*\.py' diff --git a/requirements_test.txt b/requirements_test.txt index 98d0b43..0ec6a70 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,3 +1,4 @@ black==20.8b1 +flake8==3.8.3 mypy==0.782 pydocstyle==5.1.1 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..384cedc --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[flake8] +exclude = .git,.tox +max-line-length = 88 +ignore = E203, W503 diff --git a/tox.ini b/tox.ini index 5cca5b4..427fc19 100644 --- a/tox.ini +++ b/tox.ini @@ -28,7 +28,7 @@ deps = {[testenv]deps} commands = #codespell -q 4 -L {[tox]cs_exclude_words} --skip="*.pyc,*.pyi,*~" custom_components - #flake8 cu + flake8 custom_components black --fast --check . pydocstyle -v custom_components