From 58ba012ad70f834a7b4c70dc586679724fa809fd Mon Sep 17 00:00:00 2001 From: regevbr Date: Fri, 1 Oct 2021 22:41:35 +0300 Subject: [PATCH 01/17] 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 02/17] 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 03/17] 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 04/17] 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 05/17] 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 06/17] 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 07/17] 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 08/17] 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 09/17] 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 10/17] 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 11/17] 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 12/17] 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"] } From a5245c79f7ade6a85404c4148f74b7649e77d9f3 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 11 Dec 2021 21:54:57 +1100 Subject: [PATCH 13/17] Implement optional light variable to reverse color temp --- custom_components/localtuya/const.py | 1 + custom_components/localtuya/light.py | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 12fa66c..167490f 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -16,6 +16,7 @@ CONF_COLOR = "color" CONF_COLOR_MODE = "color_mode" CONF_COLOR_TEMP_MIN_KELVIN = "color_temp_min_kelvin" CONF_COLOR_TEMP_MAX_KELVIN = "color_temp_max_kelvin" +CONF_COLOR_TEMP_REVERSE = "color_temp_reverse" CONF_MUSIC_MODE = "music_mode" # switch diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index 49fade8..a910997 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -27,6 +27,7 @@ from .const import ( CONF_COLOR_MODE, CONF_COLOR_TEMP_MAX_KELVIN, CONF_COLOR_TEMP_MIN_KELVIN, + CONF_COLOR_TEMP_REVERSE, CONF_MUSIC_MODE, ) @@ -35,6 +36,7 @@ _LOGGER = logging.getLogger(__name__) MIRED_TO_KELVIN_CONST = 1000000 DEFAULT_MIN_KELVIN = 2700 # MIRED 370 DEFAULT_MAX_KELVIN = 6500 # MIRED 153 +DEFAULT_COLOR_TEMP_REVERSE = False DEFAULT_LOWER_BRIGHTNESS = 29 DEFAULT_UPPER_BRIGHTNESS = 1000 @@ -117,6 +119,9 @@ def flow_schema(dps): vol.Optional(CONF_COLOR_TEMP_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All( vol.Coerce(int), vol.Range(min=1500, max=8000) ), + vol.Optional( + CONF_COLOR_TEMP_REVERSE, default=False, description={"suggested_value": False} + ): bool, vol.Optional(CONF_SCENE): vol.In(dps), vol.Optional( CONF_MUSIC_MODE, default=False, description={"suggested_value": False} @@ -154,6 +159,9 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): MIRED_TO_KELVIN_CONST / self._config.get(CONF_COLOR_TEMP_MAX_KELVIN, DEFAULT_MAX_KELVIN) ) + self._color_temp_reverse = self._config.get( + CONF_COLOR_TEMP_REVERSE, DEFAULT_COLOR_TEMP_REVERSE + ) self._hs = None self._effect = None self._effect_list = [] @@ -199,13 +207,15 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): def color_temp(self): """Return the color_temp of the light.""" if self.has_config(CONF_COLOR_TEMP) and self.is_white_mode: - return int( + color_temp_value = self._upper_color_temp - self._color_temp if self._color_temp_reverse else self._color_temp + color_temp_scaled = int( self._max_mired - ( ((self._max_mired - self._min_mired) / self._upper_color_temp) - * self._color_temp + * color_temp_value ) ) + return color_temp_scaled return None @property @@ -364,14 +374,16 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): if ATTR_COLOR_TEMP in kwargs and (features & SUPPORT_COLOR_TEMP): if brightness is None: brightness = self._brightness - color_temp = int( + + color_temp_value = (self._max_mired - self._min_mired) - (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) if self._color_temp_reverse else (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) + color_temp_scaled = int( self._upper_color_temp - (self._upper_color_temp / (self._max_mired - self._min_mired)) - * (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) + * color_temp_value ) states[self._config.get(CONF_COLOR_MODE)] = MODE_WHITE states[self._config.get(CONF_BRIGHTNESS)] = brightness - states[self._config.get(CONF_COLOR_TEMP)] = color_temp + states[self._config.get(CONF_COLOR_TEMP)] = color_temp_scaled await self._device.set_dps(states) async def async_turn_off(self, **kwargs): From a9ef48bcfa4f13f34656d1271b53ce0c8f770b5d Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 11 Dec 2021 22:07:42 +1100 Subject: [PATCH 14/17] update light config info --- info.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/info.md b/info.md index 3c69026..9e4c1d5 100644 --- a/info.md +++ b/info.md @@ -66,6 +66,13 @@ localtuya: - platform: light friendly_name: Device Light + brightness: 10 # Optional + brightness_lower: 0 # Optional + brightness_upper: 100 # Optional + color_temp: 11 # Optional + color_temp_reverse: false # Optional + color_temp_min_kelvin: 2700 # Optional + color_temp_max_kelvin: 6500 # Optional id: 4 - platform: sensor From 39cbd73d66093bdcab47ccda7574decd1dd257ab Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 11 Dec 2021 22:19:52 +1100 Subject: [PATCH 15/17] Formatting and variables --- custom_components/localtuya/light.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index a910997..8e76dca 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -36,6 +36,7 @@ _LOGGER = logging.getLogger(__name__) MIRED_TO_KELVIN_CONST = 1000000 DEFAULT_MIN_KELVIN = 2700 # MIRED 370 DEFAULT_MAX_KELVIN = 6500 # MIRED 153 + DEFAULT_COLOR_TEMP_REVERSE = False DEFAULT_LOWER_BRIGHTNESS = 29 @@ -120,7 +121,9 @@ def flow_schema(dps): vol.Coerce(int), vol.Range(min=1500, max=8000) ), vol.Optional( - CONF_COLOR_TEMP_REVERSE, default=False, description={"suggested_value": False} + CONF_COLOR_TEMP_REVERSE, + default=DEFAULT_COLOR_TEMP_REVERSE, + description={"suggested_value": DEFAULT_COLOR_TEMP_REVERSE}, ): bool, vol.Optional(CONF_SCENE): vol.In(dps), vol.Optional( @@ -207,7 +210,11 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): def color_temp(self): """Return the color_temp of the light.""" if self.has_config(CONF_COLOR_TEMP) and self.is_white_mode: - color_temp_value = self._upper_color_temp - self._color_temp if self._color_temp_reverse else self._color_temp + color_temp_value = ( + self._upper_color_temp - self._color_temp + if self._color_temp_reverse + else self._color_temp + ) color_temp_scaled = int( self._max_mired - ( @@ -374,8 +381,13 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): if ATTR_COLOR_TEMP in kwargs and (features & SUPPORT_COLOR_TEMP): if brightness is None: brightness = self._brightness - - color_temp_value = (self._max_mired - self._min_mired) - (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) if self._color_temp_reverse else (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) + + color_temp_value = ( + (self._max_mired - self._min_mired) + - (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) + if self._color_temp_reverse + else (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) + ) color_temp_scaled = int( self._upper_color_temp - (self._upper_color_temp / (self._max_mired - self._min_mired)) From d6418200ce20e0bea731b097017ca453da52a0c5 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 11 Dec 2021 22:22:45 +1100 Subject: [PATCH 16/17] update readme and info --- README.md | 2 +- info.md | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a4ccf04..d645eaa 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ localtuya: color_mode: 21 # Optional, usually 2 or 21, default: "none" brightness: 22 # Optional, usually 3 or 22, default: "none" color_temp: 23 # Optional, usually 4 or 23, default: "none" + color_temp_reverse: false # Optional, default: false color: 24 # Optional, usually 5 (RGB_HSV) or 24 (HSV), default: "none" brightness_lower: 29 # Optional, usually 0 or 29, default: 29 brightness_upper: 1000 # Optional, usually 255 or 1000, default: 1000 @@ -79,7 +80,6 @@ localtuya: scene: 25 # Optional, usually 6 (RGB_HSV) or 25 (HSV), default: "none" music_mode: False # Optional, some use internal mic, others, phone mic. Only internal mic is supported, default: "False" - - platform: sensor friendly_name: Plug Voltage id: 20 diff --git a/info.md b/info.md index 9e4c1d5..1954f9d 100644 --- a/info.md +++ b/info.md @@ -66,14 +66,18 @@ localtuya: - platform: light friendly_name: Device Light - brightness: 10 # Optional - brightness_lower: 0 # Optional - brightness_upper: 100 # Optional - color_temp: 11 # Optional - color_temp_reverse: false # Optional - color_temp_min_kelvin: 2700 # Optional - color_temp_max_kelvin: 6500 # Optional - id: 4 + id: 4 # Usually 1 or 20 + color_mode: 21 # Optional, usually 2 or 21, default: "none" + brightness: 22 # Optional, usually 3 or 22, default: "none" + color_temp: 23 # Optional, usually 4 or 23, default: "none" + color_temp_reverse: false # Optional, default: false + color: 24 # Optional, usually 5 (RGB_HSV) or 24 (HSV), default: "none" + brightness_lower: 29 # Optional, usually 0 or 29, default: 29 + brightness_upper: 1000 # Optional, usually 255 or 1000, default: 1000 + color_temp_min_kelvin: 2700 # Optional, default: 2700 + color_temp_max_kelvin: 6500 # Optional, default: 6500 + scene: 25 # Optional, usually 6 (RGB_HSV) or 25 (HSV), default: "none" + music_mode: False # Optional, some use internal mic, others, phone mic. Only internal mic is supported, default: "False" - platform: sensor friendly_name: Plug Voltage From 6add1f7d1a7c1130e6e4cc0b3c3cd043bc7f5d71 Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 13 Dec 2021 11:09:13 +1100 Subject: [PATCH 17/17] rename variables --- custom_components/localtuya/light.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index 8e76dca..e99414e 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -215,14 +215,13 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): if self._color_temp_reverse else self._color_temp ) - color_temp_scaled = int( + return int( self._max_mired - ( ((self._max_mired - self._min_mired) / self._upper_color_temp) * color_temp_value ) ) - return color_temp_scaled return None @property @@ -381,21 +380,20 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): if ATTR_COLOR_TEMP in kwargs and (features & SUPPORT_COLOR_TEMP): if brightness is None: brightness = self._brightness - color_temp_value = ( (self._max_mired - self._min_mired) - (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) if self._color_temp_reverse else (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) ) - color_temp_scaled = int( + color_temp = int( self._upper_color_temp - (self._upper_color_temp / (self._max_mired - self._min_mired)) * color_temp_value ) states[self._config.get(CONF_COLOR_MODE)] = MODE_WHITE states[self._config.get(CONF_BRIGHTNESS)] = brightness - states[self._config.get(CONF_COLOR_TEMP)] = color_temp_scaled + states[self._config.get(CONF_COLOR_TEMP)] = color_temp await self._device.set_dps(states) async def async_turn_off(self, **kwargs):