From 427e64444b78fb44c10d2ce11681dfd0e972c6e3 Mon Sep 17 00:00:00 2001 From: rospogrigio <49229287+rospogrigio@users.noreply.github.com> Date: Sat, 27 Nov 2021 15:20:53 +0100 Subject: [PATCH 01/15] Update tox.ini --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 49ccccb..c7d9e69 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,13 @@ [tox] skipsdist = true -envlist = py{37,38}, lint, typing +envlist = py{38,39}, lint, typing skip_missing_interpreters = True cs_exclude_words = hass,unvalid [gh-actions] python = - 3.7: clean, py37, lint, typing 3.8: clean, py38, lint, typing + 3.9: clean, py39, lint, typing [testenv] passenv = TOXENV CI From fe55cc1dc0ca205ca07edb06ae804af0a29fc6f2 Mon Sep 17 00:00:00 2001 From: rospogrigio <49229287+rospogrigio@users.noreply.github.com> Date: Sat, 27 Nov 2021 15:21:29 +0100 Subject: [PATCH 02/15] Update pyproject.toml --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d90fe71..cf08c01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.black] -target-version = ["py37", "py38"] +target-version = ["py38", "py39"] include = 'custom_components/localtuya/.*\.py' # pylint config stolen from Home Assistant @@ -79,4 +79,4 @@ score = false expected-line-ending-format = "LF" [tool.pylint.MISCELLANEOUS] -notes = "XXX" \ No newline at end of file +notes = "XXX" From f5e6f72232f590cd0403a6bbd034dc8a26fd3967 Mon Sep 17 00:00:00 2001 From: Brenda Wallace Date: Thu, 25 Nov 2021 15:02:11 +1300 Subject: [PATCH 03/15] Update README.md --- README.md | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 9981bd3..0030cf3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A Home Assistant custom Integration for local handling of Tuya-based devices. -Device status is updated receiving push updates from the device instead of polling, so status updates are extremely fast (even if manually operated). +This custom integration updates device status via push updates instead of polling, so status updates are fast (even when manually operated). The following Tuya device types are currently supported: * 1 and multiple gang switches @@ -14,13 +14,14 @@ The following Tuya device types are currently supported: Energy monitoring (voltage, current, watts, etc.) is supported for compatible devices. -This repository's development has substantially started by utilizing and merging code from NameLessJedi, mileperhour and TradeFace, and then was deeply refactored to provide proper integration with Home Assistant environment, adding config flow and other features. Refer to the "Thanks to" section below. +This repository's development began as code from [@NameLessJedi](https://github.com/NameLessJedi), [@mileperhour](https://github.com/mileperhour) and [@TradeFace](https://github.com/TradeFace). Their code was then deeply refactored to provide proper integration with Home Assistant environment, adding config flow and other features. Refer to the "Thanks to" section below. + # Installation: -Copy the localtuya folder and all of its contents into your Home Assistant's custom_components folder. This is often located inside of your /config folder. If you are running Hass.io, use SAMBA to copy the folder over. If you are running Home Assistant Supervised, the custom_components folder might be located at /usr/share/hassio/homeassistant. It is possible that your custom_components folder does not exist. If that is the case, create the folder in the proper location, and then copy the localtuya folder and all of its contents inside the newly created custom_components folder. +Copy the localtuya folder and all of its contents into your Home Assistant's custom_components folder. This folder is usually inside your `/config` folder. If you are running Hass.io, use SAMBA to copy the folder over. If you are running Home Assistant Supervised, the custom_components folder might be located at `/usr/share/hassio/homeassistant`. You may need to create the `custom_components` folder and then copy the localtuya folder and all of its contents into it -Alternatively, you can install localtuya through HACS by adding this repository. +Alternatively, you can install localtuya through [HACS](https://hacs.xyz/) by adding this repository. # Usage: @@ -28,15 +29,15 @@ Alternatively, you can install localtuya through HACS by adding this repository. **NOTE: You must have your Tuya device's Key and ID in order to use localtuya. There are several ways to obtain the localKey depending on your environment and the devices you own. A good place to start getting info is https://github.com/codetheweb/tuyapi/blob/master/docs/SETUP.md .** -**NOTE - Nov 2020: If you plan on integrating these devices on a network that has internet and blocking their internet access, you must block DNS requests too (to the local DNS server eg 192.168.1.1). If you only block outbound internet then the device will sit in zombie state, it will refuse / not respond to any connections with the localkey. Connect the devices first with an active internet connection, grab each device localkey and then implement the block.** +**NOTE - Nov 2020: If you plan to integrate these devices on a network that has internet and blocking their internet access, you must also block DNS requests (to the local DNS server, e.g. 192.168.1.1). If you only block outbound internet, then the device will sit in a zombie state; it will refuse / not respond to any connections with the localkey. Therefore, you must first connect the devices with an active internet connection, grab each device localkey, and implement the block.** Devices can be configured in two ways: -# 1. YAML config files +# Option one: YAML config files Add the proper entry to your configuration.yaml file. Several example configurations for different device types are provided below. Make sure to save when you are finished editing configuration.yaml. -``` +```yaml localtuya: - host: 192.168.1.x device_id: xxxxx @@ -100,40 +101,43 @@ Note that a single device can contain several different entities. Some examples: Restart Home Assistant when finished editing. -# 2. Using config flow +# Option two: Using config flow Start by going to Configuration - Integration and pressing the "+" button to create a new Integration, then select LocalTuya in the drop-down menu. -Wait for 6 seconds for the scanning of the devices in your LAN. Then, a drop-down menu will appear containing the list of detectes devices: you can +Wait for 6 seconds for the scanning of the devices in your LAN. Then, a drop-down menu will appear containing the list of detected devices: you can select one of these, or manually input all the parameters. ![discovery](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/1-discovery.png) -If you have selected one entry, you just have to input the Friendly Name of the Device, and the localKey. -Once you press "Submit", the connection will be tested to check that everything works, in order to proceed. +If you have selected one entry, you only need to input the device's Friendly Name and the localKey. +Once you press "Submit", the connection is tested to check that everything works. ![device](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/2-device.png) -Then, it's time to add the entities: this step will take place several times. Select the entity type from the drop-down menu to set it up. -After you have defined all the needed entities leave the "Do not add more entities" checkbox checked: this will complete the procedure. +Then, it's time to add the entities: this step will take place several times. First, select the entity type from the drop-down menu to set it up. +After you have defined all the needed entities, leave the "Do not add more entities" checkbox checked: this will complete the procedure. ![entity_type](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/3-entity_type.png) For each entity, the associated DP has to be selected. All the options requiring to select a DP will provide a drop-down menu showing -all the avaliable DPs found on the device (with their current status!!) for an easy identification. Each entity type has different options -to be configured, here is an example for the "switch" entity: +all the available DPs found on the device (with their current status!!) for easy identification. Each entity type has different options +to be configured. Here is an example for the "switch" entity: ![entity](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/4-entity.png) -After all the entities have been configured, the procedure is complete, and the Device can be associated to the Area desired. +Once you configure the entities, the procedure is complete. You can now associate the device with an Area in Home Assistant ![success](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/5-success.png) # Energy monitoring values -Energy monitoring (voltage, current...) values can be obtained in two different ways: -1) creating individual sensors, each one with the desired name. Note: Voltage and Consumption usually include the first decimal, so 0.1 as "scaling" parameter shall be used in order to get the correct values. -2) accessing the voltage/current/current_consumption attributes of a switch, and then defining template sensors like this (please note that in this case the values are already divided by 10 for Voltage and Consumption): +You can obtain Energy monitoring (voltage, current) in two different ways: + +1) Creating individual sensors, each one with the desired name. + Note: Voltage and Consumption usually include the first decimal. You will need to scale the parament by 0.1 to get the correct values. +1) Access the voltage/current/current_consumption attributes of a switch, and define template sensors + Note: these values are already divided by 10 for Voltage and Consumption ``` sensor: From 58ba012ad70f834a7b4c70dc586679724fa809fd Mon Sep 17 00:00:00 2001 From: regevbr Date: Fri, 1 Oct 2021 22:41:35 +0300 Subject: [PATCH 04/15] fix sync bug --- custom_components/localtuya/common.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index 88b434f..d2aa265 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -116,6 +116,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): self.dps_to_request = {} self._is_closing = False self._connect_task = None + self._disconnect_task = None self.set_logger(_LOGGER, config_entry[CONF_DEVICE_ID]) # This has to be done in case the device type is type_0d @@ -151,6 +152,14 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): raise Exception("Failed to retrieve status") self.status_updated(status) + + def _new_entity_handler(entity_id): + self.debug("New entity %s was added to %s", entity_id, self._config_entry[CONF_HOST]) + self._dispatch_status() + + """Subscribe localtuya entity events.""" + signal = f"localtuya_entity_{self._config_entry[CONF_DEVICE_ID]}" + self._disconnect_task = async_dispatcher_connect(self._hass, signal, _new_entity_handler) except Exception: # pylint: disable=broad-except self.exception(f"Connect to {self._config_entry[CONF_HOST]} failed") if self._interface is not None: @@ -166,6 +175,8 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): await self._connect_task if self._interface is not None: await self._interface.close() + if self._disconnect_task is not None: + self._disconnect_task() async def set_dp(self, state, dp_index): """Change value of a DP of the Tuya device.""" @@ -195,7 +206,9 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): def status_updated(self, status): """Device updated status.""" self._status.update(status) + self._dispatch_status() + def _dispatch_status(self): signal = f"localtuya_{self._config_entry[CONF_DEVICE_ID]}" async_dispatcher_send(self._hass, signal, self._status) @@ -223,7 +236,6 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): self.set_logger(logger, self._config_entry.data[CONF_DEVICE_ID]) async def async_added_to_hass(self): - """Subscribe localtuya events.""" await super().async_added_to_hass() self.debug("Adding %s with configuration: %s", self.entity_id, self._config) @@ -243,9 +255,14 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): self.schedule_update_ha_state() signal = f"localtuya_{self._config_entry.data[CONF_DEVICE_ID]}" + + """Subscribe localtuya events.""" self.async_on_remove( async_dispatcher_connect(self.hass, signal, _update_handler) ) + """Signal to device entity was added""" + signal = f"localtuya_entity_{self._config_entry.data[CONF_DEVICE_ID]}" + async_dispatcher_send(self.hass, signal, self.entity_id) @property def device_info(self): From 8e94b5f41174e437d743d3a0866ce4bc7db105bc Mon Sep 17 00:00:00 2001 From: regevbr Date: Fri, 1 Oct 2021 22:58:14 +0300 Subject: [PATCH 05/15] fix sync bug --- custom_components/localtuya/common.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index d2aa265..68b0d28 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -134,6 +134,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): self._connect_task = asyncio.create_task(self._make_connection()) async def _make_connection(self): + """Subscribe localtuya entity events.""" self.debug("Connecting to %s", self._config_entry[CONF_HOST]) try: @@ -154,12 +155,17 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): self.status_updated(status) def _new_entity_handler(entity_id): - self.debug("New entity %s was added to %s", entity_id, self._config_entry[CONF_HOST]) + self.debug( + "New entity %s was added to %s", + entity_id, + self._config_entry[CONF_HOST], + ) self._dispatch_status() - """Subscribe localtuya entity events.""" signal = f"localtuya_entity_{self._config_entry[CONF_DEVICE_ID]}" - self._disconnect_task = async_dispatcher_connect(self._hass, signal, _new_entity_handler) + self._disconnect_task = async_dispatcher_connect( + self._hass, signal, _new_entity_handler + ) except Exception: # pylint: disable=broad-except self.exception(f"Connect to {self._config_entry[CONF_HOST]} failed") if self._interface is not None: @@ -236,6 +242,7 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): self.set_logger(logger, self._config_entry.data[CONF_DEVICE_ID]) async def async_added_to_hass(self): + """Subscribe localtuya events.""" await super().async_added_to_hass() self.debug("Adding %s with configuration: %s", self.entity_id, self._config) @@ -256,11 +263,10 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): signal = f"localtuya_{self._config_entry.data[CONF_DEVICE_ID]}" - """Subscribe localtuya events.""" self.async_on_remove( async_dispatcher_connect(self.hass, signal, _update_handler) ) - """Signal to device entity was added""" + signal = f"localtuya_entity_{self._config_entry.data[CONF_DEVICE_ID]}" async_dispatcher_send(self.hass, signal, self.entity_id) From 5a28cec51762b0aa6b9c3d12443d07761ab3b91d Mon Sep 17 00:00:00 2001 From: Bashir <37988348+Brahmah@users.noreply.github.com> Date: Sun, 17 Oct 2021 21:55:38 +1100 Subject: [PATCH 06/15] Update README.md This addition suggests a user close the tuya app prior to proceeding with the config flow to avoid timeouts as experienced by #591 #570 #507 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0030cf3..a4ccf04 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,8 @@ Start by going to Configuration - Integration and pressing the "+" button to cre Wait for 6 seconds for the scanning of the devices in your LAN. Then, a drop-down menu will appear containing the list of detected devices: you can select one of these, or manually input all the parameters. +> **Note: The tuya app on your device must be closed for the following steps to work reliably.** + ![discovery](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/1-discovery.png) If you have selected one entry, you only need to input the device's Friendly Name and the localKey. From e1bb45f2a27e97d724a53579f881fb1e11c82bbf Mon Sep 17 00:00:00 2001 From: sibowler Date: Sat, 16 Oct 2021 07:41:31 +1100 Subject: [PATCH 07/15] Adding Number and Select as Tuya platforms. Provides support for a wider range of devices. --- custom_components/localtuya/const.py | 2 +- custom_components/localtuya/number.py | 86 +++++++++++++++++ custom_components/localtuya/select.py | 95 +++++++++++++++++++ .../localtuya/translations/en.json | 14 ++- 4 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 custom_components/localtuya/number.py create mode 100644 custom_components/localtuya/select.py diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index bd8a5d3..1bd74ca 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -46,6 +46,6 @@ DATA_DISCOVERY = "discovery" DOMAIN = "localtuya" # Platforms in this list must support config flows -PLATFORMS = ["binary_sensor", "cover", "fan", "light", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "cover", "fan", "light", "sensor", "switch", "number", "select"] TUYA_DEVICE = "tuya_device" diff --git a/custom_components/localtuya/number.py b/custom_components/localtuya/number.py new file mode 100644 index 0000000..50c1919 --- /dev/null +++ b/custom_components/localtuya/number.py @@ -0,0 +1,86 @@ +"""Platform to present any Tuya DP as a number.""" +import logging +from functools import partial + +import voluptuous as vol +from homeassistant.components.number import DOMAIN, NumberEntity +from homeassistant.const import ( + CONF_DEVICE_CLASS, + STATE_UNKNOWN, +) + +from .common import LocalTuyaEntity, async_setup_entry + +_LOGGER = logging.getLogger(__name__) + +CONF_MIN_VALUE = "min_value" +CONF_MAX_VALUE = "max_value" + +DEFAULT_MIN = 0 +DEFAULT_MAX = 100000 + +def flow_schema(dps): +# """Return schema used in config flow.""" + return { + vol.Optional(CONF_MIN_VALUE, default=DEFAULT_MIN): vol.All( + vol.Coerce(float), vol.Range(min=-1000000.0, max=1000000.0), + ), + vol.Required(CONF_MAX_VALUE, default=DEFAULT_MAX): vol.All( + vol.Coerce(float), vol.Range(min=-1000000.0, max=1000000.0), + ), + } + + +class LocaltuyaNumber(LocalTuyaEntity, NumberEntity): + """Representation of a Tuya Number.""" + + def __init__( + self, + device, + config_entry, + sensorid, + **kwargs, + ): + """Initialize the Tuya sensor.""" + super().__init__(device, config_entry, sensorid, _LOGGER, **kwargs) + self._state = STATE_UNKNOWN + + self._minValue = DEFAULT_MIN + if (CONF_MIN_VALUE in self._config): + self._minValue = self._config.get(CONF_MIN_VALUE) + + self._maxValue = self._config.get(CONF_MAX_VALUE) + + @property + def value(self) -> float: + """Return sensor state.""" + return self._state + + @property + def min_value(self) -> float: + """Return the minimum value.""" + + return self._minValue + + @property + def max_value(self) -> float: + """Return the maximum value.""" + return self._maxValue + + @property + def device_class(self): + """Return the class of this device.""" + return self._config.get(CONF_DEVICE_CLASS) + + async def async_set_value(self, value: float) -> None: + """Update the current value.""" + await self._device.set_dp(value, self._dp_id) + + + def status_updated(self): + """Device status was updated.""" + state = self.dps(self._dp_id) + self._state = state + + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaNumber, flow_schema) diff --git a/custom_components/localtuya/select.py b/custom_components/localtuya/select.py new file mode 100644 index 0000000..2f70d99 --- /dev/null +++ b/custom_components/localtuya/select.py @@ -0,0 +1,95 @@ +"""Platform to present any Tuya DP as an enumeration.""" +import logging +from functools import partial + +import voluptuous as vol +from homeassistant.components.select import DOMAIN, SelectEntity +from homeassistant.const import ( + CONF_DEVICE_CLASS, + STATE_UNKNOWN, +) + +from .common import LocalTuyaEntity, async_setup_entry + +_LOGGER = logging.getLogger(__name__) + +CONF_OPTIONS = "select_options" +CONF_OPTIONS_FRIENDLY = "select_options_friendly" + + +def flow_schema(dps): +# """Return schema used in config flow.""" + return { + vol.Required(CONF_OPTIONS): str, + vol.Optional(CONF_OPTIONS_FRIENDLY): str, + } + + +class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): + """Representation of a Tuya Enumeration.""" + + def __init__( + self, + device, + config_entry, + sensorid, + **kwargs, + ): + """Initialize the Tuya sensor.""" + super().__init__(device, config_entry, sensorid, _LOGGER, **kwargs) + self._state = STATE_UNKNOWN + self._validOptions = self._config.get(CONF_OPTIONS).split(';') + + #Set Display options + self._displayOptions = [] + displayOptionsStr = "" + if (CONF_OPTIONS_FRIENDLY in self._config): + displayOptionsStr = self._config.get(CONF_OPTIONS_FRIENDLY).strip() + _LOGGER.debug("Display Options Configured: " + displayOptionsStr) + + if (displayOptionsStr.find(";") >= 0): + self._displayOptions = displayOptionsStr.split(';') + elif (len(displayOptionsStr.strip()) > 0): + self._displayOptions.append(displayOptionsStr) + else: + #Default display string to raw string + _LOGGER.debug("No Display options configured - defaulting to raw values") + self._displayOptions = self._validOptions + + _LOGGER.debug("Total Raw Options: " + str(len(self._validOptions)) + " - Total Display Options: " + str(len(self._displayOptions))) + if (len(self._validOptions) > len(self._displayOptions)): + #If list of display items smaller than list of valid items, then default remaining items to be the raw value + _LOGGER.debug("Valid options is larger than display options - filling up with raw values") + for i in range(len(self._displayOptions), len(self._validOptions)): + self._displayOptions.append(self._validOptions[i]) + + @property + def current_option(self) -> str: + """Return the current value.""" + return self._stateFriendly + + @property + def options(self) -> list: + """Return the list of values.""" + return self._displayOptions + + @property + def device_class(self): + """Return the class of this device.""" + return self._config.get(CONF_DEVICE_CLASS) + + async def async_select_option(self, option: str) -> None: + """Update the current value.""" + optionValue = self._validOptions[self._displayOptions.index(option)] + _LOGGER.debug("Sending Option: " + option + " -> " + optionValue) + await self._device.set_dp(optionValue, self._dp_id) + + + def status_updated(self): + """Device status was updated.""" + state = self.dps(self._dp_id) + self._stateFriendly = self._displayOptions[self._validOptions.index(state)] + self._state = state + + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaSelect, flow_schema) diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 6ce233a..a833f73 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -74,7 +74,11 @@ "fan_oscillating_control": "Fan Oscillating Control", "fan_speed_low": "Fan Low Speed Setting", "fan_speed_medium": "Fan Medium Speed Setting", - "fan_speed_high": "Fan High Speed Setting" + "fan_speed_high": "Fan High Speed Setting", + "max_value": "Maximum Value", + "min_value": "Minimum Value", + "select_options": "Valid entries, separate entries by a ;", + "select_options_friendly": "User Friendly options, separate entries by a ;" } } } @@ -124,9 +128,13 @@ "scene": "Scene", "fan_speed_control": "Fan Speed Control", "fan_oscillating_control": "Fan Oscillating Control", - "fan_speed_low": "Fan Low Speed Setting", + "fan_speed_low": "Fan Low Speed Settingaa", "fan_speed_medium": "Fan Medium Speed Setting", - "fan_speed_high": "Fan High Speed Setting" + "fan_speed_high": "Fan High Speed Setting", + "max_value": "Maximum Value", + "min_value": "Minimum Value", + "select_options": "Valid entries, separate entries by a ;", + "select_options_friendly": "User Friendly options, separate entries by a ;" } }, "yaml_import": { From 23a16babad08ba3b43f3e564ac2e673331d832c5 Mon Sep 17 00:00:00 2001 From: sibowler Date: Sat, 16 Oct 2021 07:43:33 +1100 Subject: [PATCH 08/15] Fix for typo introduced during debugging. --- custom_components/localtuya/translations/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index a833f73..f12c4ab 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -128,7 +128,7 @@ "scene": "Scene", "fan_speed_control": "Fan Speed Control", "fan_oscillating_control": "Fan Oscillating Control", - "fan_speed_low": "Fan Low Speed Settingaa", + "fan_speed_low": "Fan Low Speed Setting", "fan_speed_medium": "Fan Medium Speed Setting", "fan_speed_high": "Fan High Speed Setting", "max_value": "Maximum Value", From fad7e0b1ac1e647f2f545a29745ecb5c671994c7 Mon Sep 17 00:00:00 2001 From: sibowler Date: Sat, 16 Oct 2021 07:46:56 +1100 Subject: [PATCH 09/15] Reordering platforms to be alphabetical --- custom_components/localtuya/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 1bd74ca..af67876 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -46,6 +46,6 @@ DATA_DISCOVERY = "discovery" DOMAIN = "localtuya" # Platforms in this list must support config flows -PLATFORMS = ["binary_sensor", "cover", "fan", "light", "sensor", "switch", "number", "select"] +PLATFORMS = ["binary_sensor", "cover", "fan", "light", "number", "select", "sensor", "switch"] TUYA_DEVICE = "tuya_device" From 61896605b852a3e4f73b960edd7e2245be035b58 Mon Sep 17 00:00:00 2001 From: sibowler Date: Mon, 13 Dec 2021 06:08:53 +1100 Subject: [PATCH 10/15] Fixing up style issues --- custom_components/localtuya/const.py | 3 ++- custom_components/localtuya/number.py | 5 ++--- custom_components/localtuya/select.py | 16 +++++++++------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index af67876..b4b5817 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -46,6 +46,7 @@ DATA_DISCOVERY = "discovery" DOMAIN = "localtuya" # Platforms in this list must support config flows -PLATFORMS = ["binary_sensor", "cover", "fan", "light", "number", "select", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "cover", "fan", "light", "number", + "select", "sensor", "switch"] TUYA_DEVICE = "tuya_device" diff --git a/custom_components/localtuya/number.py b/custom_components/localtuya/number.py index 50c1919..1bd7ec4 100644 --- a/custom_components/localtuya/number.py +++ b/custom_components/localtuya/number.py @@ -19,8 +19,9 @@ CONF_MAX_VALUE = "max_value" DEFAULT_MIN = 0 DEFAULT_MAX = 100000 + def flow_schema(dps): -# """Return schema used in config flow.""" + """Return schema used in config flow.""" return { vol.Optional(CONF_MIN_VALUE, default=DEFAULT_MIN): vol.All( vol.Coerce(float), vol.Range(min=-1000000.0, max=1000000.0), @@ -59,7 +60,6 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity): @property def min_value(self) -> float: """Return the minimum value.""" - return self._minValue @property @@ -76,7 +76,6 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity): """Update the current value.""" await self._device.set_dp(value, self._dp_id) - def status_updated(self): """Device status was updated.""" state = self.dps(self._dp_id) diff --git a/custom_components/localtuya/select.py b/custom_components/localtuya/select.py index 2f70d99..90187b8 100644 --- a/custom_components/localtuya/select.py +++ b/custom_components/localtuya/select.py @@ -18,7 +18,7 @@ CONF_OPTIONS_FRIENDLY = "select_options_friendly" def flow_schema(dps): -# """Return schema used in config flow.""" + """Return schema used in config flow.""" return { vol.Required(CONF_OPTIONS): str, vol.Optional(CONF_OPTIONS_FRIENDLY): str, @@ -40,7 +40,7 @@ class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): self._state = STATE_UNKNOWN self._validOptions = self._config.get(CONF_OPTIONS).split(';') - #Set Display options + # Set Display options self._displayOptions = [] displayOptionsStr = "" if (CONF_OPTIONS_FRIENDLY in self._config): @@ -52,14 +52,17 @@ class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): elif (len(displayOptionsStr.strip()) > 0): self._displayOptions.append(displayOptionsStr) else: - #Default display string to raw string + # Default display string to raw string _LOGGER.debug("No Display options configured - defaulting to raw values") self._displayOptions = self._validOptions - _LOGGER.debug("Total Raw Options: " + str(len(self._validOptions)) + " - Total Display Options: " + str(len(self._displayOptions))) + _LOGGER.debug("Total Raw Options: " + str(len(self._validOptions)) + + " - Total Display Options: " + str(len(self._displayOptions))) if (len(self._validOptions) > len(self._displayOptions)): - #If list of display items smaller than list of valid items, then default remaining items to be the raw value - _LOGGER.debug("Valid options is larger than display options - filling up with raw values") + # If list of display items smaller than list of valid items, + # then default remaining items to be the raw value + _LOGGER.debug("Valid options is larger than display options - \ + filling up with raw values") for i in range(len(self._displayOptions), len(self._validOptions)): self._displayOptions.append(self._validOptions[i]) @@ -84,7 +87,6 @@ class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): _LOGGER.debug("Sending Option: " + option + " -> " + optionValue) await self._device.set_dp(optionValue, self._dp_id) - def status_updated(self): """Device status was updated.""" state = self.dps(self._dp_id) From a0fff6ee2f9e0982bb14cb338b88765f4117f34d Mon Sep 17 00:00:00 2001 From: sibowler Date: Tue, 14 Dec 2021 06:16:31 +1100 Subject: [PATCH 11/15] Fixing up style errors in line with tox. --- custom_components/localtuya/const.py | 12 +++++- custom_components/localtuya/number.py | 18 +++++---- custom_components/localtuya/select.py | 56 +++++++++++++++------------ 3 files changed, 51 insertions(+), 35 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index b4b5817..12fa66c 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -46,7 +46,15 @@ DATA_DISCOVERY = "discovery" DOMAIN = "localtuya" # Platforms in this list must support config flows -PLATFORMS = ["binary_sensor", "cover", "fan", "light", "number", - "select", "sensor", "switch"] +PLATFORMS = [ + "binary_sensor", + "cover", + "fan", + "light", + "number", + "select", + "sensor", + "switch", +] TUYA_DEVICE = "tuya_device" diff --git a/custom_components/localtuya/number.py b/custom_components/localtuya/number.py index 1bd7ec4..328f6a2 100644 --- a/custom_components/localtuya/number.py +++ b/custom_components/localtuya/number.py @@ -24,10 +24,12 @@ def flow_schema(dps): """Return schema used in config flow.""" return { vol.Optional(CONF_MIN_VALUE, default=DEFAULT_MIN): vol.All( - vol.Coerce(float), vol.Range(min=-1000000.0, max=1000000.0), + vol.Coerce(float), + vol.Range(min=-1000000.0, max=1000000.0), ), vol.Required(CONF_MAX_VALUE, default=DEFAULT_MAX): vol.All( - vol.Coerce(float), vol.Range(min=-1000000.0, max=1000000.0), + vol.Coerce(float), + vol.Range(min=-1000000.0, max=1000000.0), ), } @@ -46,11 +48,11 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity): super().__init__(device, config_entry, sensorid, _LOGGER, **kwargs) self._state = STATE_UNKNOWN - self._minValue = DEFAULT_MIN - if (CONF_MIN_VALUE in self._config): - self._minValue = self._config.get(CONF_MIN_VALUE) + self._min_value = DEFAULT_MIN + if CONF_MIN_VALUE in self._config: + self._min_value = self._config.get(CONF_MIN_VALUE) - self._maxValue = self._config.get(CONF_MAX_VALUE) + self._max_value = self._config.get(CONF_MAX_VALUE) @property def value(self) -> float: @@ -60,12 +62,12 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity): @property def min_value(self) -> float: """Return the minimum value.""" - return self._minValue + return self._min_value @property def max_value(self) -> float: """Return the maximum value.""" - return self._maxValue + return self._max_value @property def device_class(self): diff --git a/custom_components/localtuya/select.py b/custom_components/localtuya/select.py index 90187b8..4d9ce54 100644 --- a/custom_components/localtuya/select.py +++ b/custom_components/localtuya/select.py @@ -38,43 +38,49 @@ class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): """Initialize the Tuya sensor.""" super().__init__(device, config_entry, sensorid, _LOGGER, **kwargs) self._state = STATE_UNKNOWN - self._validOptions = self._config.get(CONF_OPTIONS).split(';') + self._state_friendly = "" + self._valid_options = self._config.get(CONF_OPTIONS).split(";") # Set Display options - self._displayOptions = [] - displayOptionsStr = "" - if (CONF_OPTIONS_FRIENDLY in self._config): - displayOptionsStr = self._config.get(CONF_OPTIONS_FRIENDLY).strip() - _LOGGER.debug("Display Options Configured: " + displayOptionsStr) + self._display_options = [] + display_options_str = "" + if CONF_OPTIONS_FRIENDLY in self._config: + display_options_str = self._config.get(CONF_OPTIONS_FRIENDLY).strip() + _LOGGER.debug("Display Options Configured: %s", display_options_str) - if (displayOptionsStr.find(";") >= 0): - self._displayOptions = displayOptionsStr.split(';') - elif (len(displayOptionsStr.strip()) > 0): - self._displayOptions.append(displayOptionsStr) + if display_options_str.find(";") >= 0: + self._display_options = display_options_str.split(";") + elif len(display_options_str.strip()) > 0: + self._display_options.append(display_options_str) else: # Default display string to raw string _LOGGER.debug("No Display options configured - defaulting to raw values") - self._displayOptions = self._validOptions + self._display_options = self._valid_options - _LOGGER.debug("Total Raw Options: " + str(len(self._validOptions)) + - " - Total Display Options: " + str(len(self._displayOptions))) - if (len(self._validOptions) > len(self._displayOptions)): - # If list of display items smaller than list of valid items, + _LOGGER.debug( + "Total Raw Options: %s - Total Display Options: %s", + str(len(self._valid_options)), + str(len(self._display_options)) + ) + if len(self._valid_options) > len(self._display_options): + # If list of display items smaller than list of valid items, # then default remaining items to be the raw value - _LOGGER.debug("Valid options is larger than display options - \ - filling up with raw values") - for i in range(len(self._displayOptions), len(self._validOptions)): - self._displayOptions.append(self._validOptions[i]) + _LOGGER.debug( + "Valid options is larger than display options - \ + filling up with raw values" + ) + for i in range(len(self._display_options), len(self._valid_options)): + self._display_options.append(self._valid_options[i]) @property def current_option(self) -> str: """Return the current value.""" - return self._stateFriendly + return self._state_friendly @property def options(self) -> list: """Return the list of values.""" - return self._displayOptions + return self._display_options @property def device_class(self): @@ -83,14 +89,14 @@ class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Update the current value.""" - optionValue = self._validOptions[self._displayOptions.index(option)] - _LOGGER.debug("Sending Option: " + option + " -> " + optionValue) - await self._device.set_dp(optionValue, self._dp_id) + option_value = self._valid_options[self._display_options.index(option)] + _LOGGER.debug("Sending Option: " + option + " -> " + option_value) + await self._device.set_dp(option_value, self._dp_id) def status_updated(self): """Device status was updated.""" state = self.dps(self._dp_id) - self._stateFriendly = self._displayOptions[self._validOptions.index(state)] + self._state_friendly = self._display_options[self._valid_options.index(state)] self._state = state From 1b53c227918bde30107c0874426eaad6fcf1183c Mon Sep 17 00:00:00 2001 From: sibowler Date: Tue, 14 Dec 2021 08:51:53 +1100 Subject: [PATCH 12/15] black styling --- custom_components/localtuya/select.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/select.py b/custom_components/localtuya/select.py index 4d9ce54..43b32c9 100644 --- a/custom_components/localtuya/select.py +++ b/custom_components/localtuya/select.py @@ -60,7 +60,7 @@ class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): _LOGGER.debug( "Total Raw Options: %s - Total Display Options: %s", str(len(self._valid_options)), - str(len(self._display_options)) + str(len(self._display_options)), ) if len(self._valid_options) > len(self._display_options): # If list of display items smaller than list of valid items, From 5a4167fe5b18cb1e422f1f363bafd70a59b9b485 Mon Sep 17 00:00:00 2001 From: sibowler Date: Tue, 14 Dec 2021 19:49:10 +1100 Subject: [PATCH 13/15] Fix dependencies to account for Select type. Also fixup errors in Fan due to movement in dependencies. --- custom_components/localtuya/fan.py | 8 +++++++- requirements_test.txt | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 834e199..0c65dfa 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -79,7 +79,13 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): """Get the list of available speeds.""" return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - async def async_turn_on(self, speed: str = None, **kwargs) -> None: + async def async_turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: """Turn on the entity.""" await self._device.set_dp(True, self._dp_id) if speed is not None: diff --git a/requirements_test.txt b/requirements_test.txt index df62e77..a4e209a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -3,7 +3,7 @@ codespell==2.0.0 flake8==3.9.2 mypy==0.901 pydocstyle==6.1.1 -cryptography==3.2 +cryptography==3.3.2 pylint==2.8.2 pylint-strict-informational==0.1 -homeassistant==2021.1.4 +homeassistant==2021.7.1 From a6738691202a6b59eecf7c4541440c10223bf47c Mon Sep 17 00:00:00 2001 From: sibowler Date: Tue, 14 Dec 2021 19:50:17 +1100 Subject: [PATCH 14/15] Adding Select and Number dependencies --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 2d94fd3..562dc77 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,7 +12,7 @@ warn_incomplete_stub = true warn_redundant_casts = true warn_unused_configs = true -[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*] +[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*,homeassistant.components.select.*,homeassistant.components.number.*] strict = true ignore_errors = false warn_unreachable = true From 76b86eb6e11452d6cffea541a63805b24a1ee718 Mon Sep 17 00:00:00 2001 From: sibowler Date: Tue, 14 Dec 2021 19:51:39 +1100 Subject: [PATCH 15/15] Adding number and select types to hacs definition. --- hacs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hacs.json b/hacs.json index 4b6ec05..3c5a41a 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "Local Tuya", - "domains": ["climate", "cover", "fan", "light", "sensor", "switch"], + "domains": ["climate", "cover", "fan", "light", "number", "select", "sensor", "switch"], "homeassistant": "0.116.0", "iot_class": ["Local Push"] }