diff --git a/custom_components/localtuya/__init__.py b/custom_components/localtuya/__init__.py index 67f0b9d..cfce242 100644 --- a/custom_components/localtuya/__init__.py +++ b/custom_components/localtuya/__init__.py @@ -140,7 +140,7 @@ def get_entity_config(config_entry, dps_id): class TuyaDevice: - """Cache wrapper for pytuya.TuyaInterface""" + """Cache wrapper for pytuya.TuyaInterface.""" def __init__(self, interface, friendly_name): """Initialize the cache.""" @@ -226,6 +226,7 @@ class LocalTuyaEntity(Entity): @property def device_info(self): + """Return device information for the device registry.""" return { "identifiers": { # Serial numbers are unique identifiers within a specific domain diff --git a/custom_components/localtuya/cover.py b/custom_components/localtuya/cover.py index 49b3c88..a88be5a 100644 --- a/custom_components/localtuya/cover.py +++ b/custom_components/localtuya/cover.py @@ -78,7 +78,7 @@ def flow_schema(dps): async def async_setup_entry(hass, config_entry, async_add_entities): - """Setup 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( config_entry, DOMAIN ) @@ -103,7 +103,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class LocaltuyaCover(LocalTuyaEntity, CoverEntity): - """Tuya cover devices.""" + """Tuya cover device.""" def __init__( self, @@ -112,6 +112,7 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity): switchid, **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 @@ -137,6 +138,7 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity): @property def current_cover_position(self): + """Return current cover position in percent.""" # self.update() # state = self._state # _LOGGER.info("curr_pos() : %i", self._position) @@ -145,6 +147,7 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity): @property def is_opening(self): + """Return if cover is opening.""" # self.update() state = self._state # print('is_opening() : state [{}]'.format(state)) @@ -154,6 +157,7 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity): @property def is_closing(self): + """Return if cover is closing.""" # self.update() state = self._state # print('is_closing() : state [{}]'.format(state)) @@ -177,7 +181,6 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity): def set_cover_position(self, **kwargs): # _LOGGER.info("running set_cover_position from cover") """Move the cover to a specific position.""" - newpos = float(kwargs["position"]) # _LOGGER.info("Set new pos: %f", newpos) @@ -212,5 +215,5 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity): self._device.set_dps(self._config[CONF_STOP_CMD], self._dps_id) def status_updated(self): - """Device status was updated. """ + """Device status was updated.""" self._state = self.dps(self._dps_id) diff --git a/custom_components/localtuya/discovery.py b/custom_components/localtuya/discovery.py index bca785f..297626f 100644 --- a/custom_components/localtuya/discovery.py +++ b/custom_components/localtuya/discovery.py @@ -72,6 +72,7 @@ async def discover(timeout, loop): def main(): + """Run discovery and print result.""" loop = asyncio.get_event_loop() res = loop.run_until_complete(discover(5, loop)) print(res) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 80cb0df..9f3f411 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -49,7 +49,7 @@ def flow_schema(dps): async def async_setup_entry(hass, config_entry, async_add_entities): - """Setup a Tuya fan based on a config entry.""" + """Set up a Tuya fan based on a config entry.""" tuyainterface, entities_to_setup = prepare_setup_entities(config_entry, DOMAIN) if not entities_to_setup: return diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index 8923fa1..6750b18 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -59,7 +59,7 @@ def flow_schema(dps): async def async_setup_entry(hass, config_entry, async_add_entities): - """Setup a Tuya light based on a config entry.""" + """Set up a Tuya light based on a config entry.""" tuyainterface, entities_to_setup = prepare_setup_entities(config_entry, DOMAIN) if not entities_to_setup: return diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 02239b8..0b5741f 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -1,37 +1,38 @@ # PyTuya Module # -*- coding: utf-8 -*- """ - Python module to interface with Tuya WiFi smart devices - Mostly derived from Shenzhen Xenon ESP8266MOD WiFi smart devices - E.g. https://wikidevi.com/wiki/Xenon_SM-PW701U +Python module to interface with Tuya WiFi smart devices. - Author: clach04 - Maintained by: rospogrigio +Mostly derived from Shenzhen Xenon ESP8266MOD WiFi smart devices +E.g. https://wikidevi.com/wiki/Xenon_SM-PW701U - For more information see https://github.com/clach04/python-tuya +Author: clach04 +Maintained by: rospogrigio - Classes - TuyaInterface(dev_id, address, local_key=None) - dev_id (str): Device ID e.g. 01234567891234567890 - address (str): Device Network IP Address e.g. 10.0.1.99 - local_key (str, optional): The encryption key. Defaults to None. +For more information see https://github.com/clach04/python-tuya - 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) - set_dps(on, dps_index) # Set value of any dps index. - set_timer(num_secs): +Classes + TuyaInterface(dev_id, address, local_key=None) + dev_id (str): Device ID e.g. 01234567891234567890 + address (str): Device Network IP Address e.g. 10.0.1.99 + local_key (str, optional): The encryption key. Defaults to None. + +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) + 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 - * 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 - Updated pytuya to support devices with Device IDs of 22 characters +Credits + * TuyaAPI https://github.com/codetheweb/tuyapi by codetheweb and blackrozes + 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 + Updated pytuya to support devices with Device IDs of 22 characters """ import base64 @@ -80,12 +81,16 @@ PROTOCOL_VERSION_BYTES_33 = b"3.3" IS_PY2 = sys.version_info[0] == 2 -class AESCipher(object): +class AESCipher: + """Cipher module for Tuya communication.""" + def __init__(self, key): + """Initialize a new AESCipher.""" self.bs = 16 self.key = key def encrypt(self, raw, use_base64=True): + """Encrypt data to be sent to device.""" if Crypto: raw = self._pad(raw) cipher = AES.new(self.key, mode=AES.MODE_ECB) @@ -104,6 +109,7 @@ class AESCipher(object): return crypted_text def decrypt(self, enc, use_base64=True): + """Decrypt data from device.""" if use_base64: enc = base64.b64decode(enc) # print('enc (%d) %r' % (len(enc), enc)) @@ -134,6 +140,7 @@ class AESCipher(object): def bin2hex(x, pretty=False): + """Convert binary data to hex string.""" if pretty: space = " " else: @@ -146,6 +153,7 @@ def bin2hex(x, pretty=False): def hex2bin(x): + """Convert hex string to binary.""" if IS_PY2: return x.decode("hex") else: @@ -171,12 +179,14 @@ payload_dict = { } -class TuyaInterface(object): +class TuyaInterface: + """Represent a Tuya device.""" + def __init__( self, dev_id, address, local_key, protocol_version, connection_timeout=10 ): """ - Represents a Tuya device. + Initialize a new TuyaInterface. Args: dev_id (str): The device id. @@ -186,7 +196,6 @@ class TuyaInterface(object): Attributes: port (int): The port to connect to. """ - self.id = dev_id self.address = address self.local_key = local_key.encode("latin1") @@ -198,6 +207,7 @@ class TuyaInterface(object): self.port = 6668 # default - do not expect caller to pass in def __repr__(self): + """Return internal string representation of object.""" return "%r" % ((self.id, self.address),) # FIXME can do better than this def _send_receive(self, payload): @@ -239,6 +249,7 @@ class TuyaInterface(object): return data 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) @@ -288,6 +299,7 @@ class TuyaInterface(object): return detected_dps def add_dps_to_request(self, dps_index): + """Add a datapoint (DP) to be included in requests.""" if isinstance(dps_index, int): self.dps_to_request[str(dps_index)] = None else: @@ -393,6 +405,7 @@ class TuyaInterface(object): return buffer def status(self): + """Return device status.""" log.debug("status() entry (dev_type is %s)", self.dev_type) # open device, send request, then close connection payload = self.generate_payload("status") diff --git a/custom_components/localtuya/sensor.py b/custom_components/localtuya/sensor.py index 473cd72..5d90de6 100644 --- a/custom_components/localtuya/sensor.py +++ b/custom_components/localtuya/sensor.py @@ -56,7 +56,7 @@ def flow_schema(dps): async def async_setup_entry(hass, config_entry, async_add_entities): - """Setup a Tuya sensor based on a config entry.""" + """Set up a Tuya sensor based on a config entry.""" tuyainterface, entities_to_setup = prepare_setup_entities(config_entry, DOMAIN) if not entities_to_setup: return diff --git a/custom_components/localtuya/switch.py b/custom_components/localtuya/switch.py index faa6270..37df479 100644 --- a/custom_components/localtuya/switch.py +++ b/custom_components/localtuya/switch.py @@ -94,7 +94,7 @@ def flow_schema(dps): async def async_setup_entry(hass, config_entry, async_add_entities): - """Setup a Tuya switch based on a config entry.""" + """Set up a Tuya switch based on a config entry.""" tuyainterface, entities_to_setup = prepare_setup_entities(config_entry, DOMAIN) if not entities_to_setup: return @@ -152,6 +152,7 @@ class LocaltuyaSwitch(LocalTuyaEntity, SwitchEntity): @property def device_state_attributes(self): + """Return device state attributes.""" attrs = {} if self._config.get(CONF_CURRENT, "-1") != "-1": attrs[ATTR_CURRENT] = self.dps(self._config[CONF_CURRENT]) diff --git a/requirements_test.txt b/requirements_test.txt index 06b650a..98d0b43 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,2 +1,3 @@ black==20.8b1 mypy==0.782 +pydocstyle==5.1.1 \ No newline at end of file diff --git a/tox.ini b/tox.ini index db954ae..5cca5b4 100644 --- a/tox.ini +++ b/tox.ini @@ -30,7 +30,7 @@ commands = #codespell -q 4 -L {[tox]cs_exclude_words} --skip="*.pyc,*.pyi,*~" custom_components #flake8 cu black --fast --check . - #pydocstyle -v custom_components + pydocstyle -v custom_components [testenv:typing] whitelist_externals =