From 7018625f049be07a69aeb699596f465e45e9fd10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Wed, 9 Sep 2020 10:55:22 +0200 Subject: [PATCH 01/11] Add light support and clean up schemas --- custom_components/localtuya/__init__.py | 68 +++++++- custom_components/localtuya/config_flow.py | 6 +- custom_components/localtuya/const.py | 2 +- custom_components/localtuya/light.py | 185 +++++++++------------ custom_components/localtuya/switch.py | 65 ++------ 5 files changed, 156 insertions(+), 170 deletions(-) diff --git a/custom_components/localtuya/__init__.py b/custom_components/localtuya/__init__.py index f3c215a..ff07ef8 100644 --- a/custom_components/localtuya/__init__.py +++ b/custom_components/localtuya/__init__.py @@ -1,13 +1,71 @@ """The LocalTuya integration integration.""" -import asyncio - import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.const import CONF_PLATFORM +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_ID, + CONF_ICON, + CONF_NAME, + CONF_FRIENDLY_NAME, + CONF_HOST, + CONF_PLATFORM, + CONF_ENTITIES, +) +import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, PLATFORMS +from . import pytuya +from .const import CONF_LOCAL_KEY, CONF_PROTOCOL_VERSION, DOMAIN, PLATFORMS + + +DEFAULT_ID = "1" +DEFAULT_PROTOCOL_VERSION = 3.3 + +BASE_PLATFORM_SCHEMA = { + vol.Optional(CONF_ICON): cv.icon, # Deprecated: not used + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(CONF_LOCAL_KEY): cv.string, + vol.Optional(CONF_NAME): cv.string, # Deprecated: not used + vol.Required(CONF_FRIENDLY_NAME): cv.string, + vol.Required(CONF_PROTOCOL_VERSION, default=DEFAULT_PROTOCOL_VERSION): vol.Coerce( + float + ), + vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, +} + + +def prepare_setup_entities(config_entry, platform): + """Prepare ro setup entities for a platform.""" + entities_to_setup = [ + entity + for entity in config_entry.data[CONF_ENTITIES] + if entity[CONF_PLATFORM] == platform + ] + if not entities_to_setup: + return None, None + + device = pytuya.BulbDevice( + config_entry.data[CONF_DEVICE_ID], + config_entry.data[CONF_HOST], + config_entry.data[CONF_LOCAL_KEY], + ) + device.set_version(float(config_entry.data[CONF_PROTOCOL_VERSION])) + device.set_dpsUsed({}) + return device, entities_to_setup + + +def import_from_yaml(hass, config, platform): + """Import configuration from YAML.""" + config[CONF_PLATFORM] = platform + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) + + return True async def async_setup(hass: HomeAssistant, config: dict): diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py index 6d814ef..d8ba181 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -174,7 +174,7 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(user_input[CONF_DEVICE_ID]) self._set_platform(user_input[CONF_PLATFORM]) - if len(user_input[CONF_SWITCHES]) > 0: + if len(user_input.get(CONF_SWITCHES, [])) > 0: for switch_conf in user_input[CONF_SWITCHES].values(): self.entities.append(_convert_entity(switch_conf)) else: @@ -194,7 +194,9 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def _set_platform(self, platform): integration_module = ".".join(__name__.split(".")[:-1]) self.platform = platform - self.platform_dps_fields = import_module("." + platform, integration_module).DPS_FIELDS + self.platform_dps_fields = import_module( + "." + platform, integration_module + ).DPS_FIELDS class CannotConnect(exceptions.HomeAssistantError): diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 9361216..532f260 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -13,4 +13,4 @@ CONF_VOLTAGE = "voltage" DOMAIN = "localtuya" # Platforms in this list must support config flows -PLATFORMS = ["switch"] +PLATFORMS = ["light", "switch"] diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index 1193eb6..d6ffe99 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -12,12 +12,15 @@ light: friendly_name: This Light protocol_version: 3.3 """ -import voluptuous as vol -from homeassistant.const import (CONF_HOST, CONF_ID, CONF_SWITCHES, CONF_FRIENDLY_NAME, CONF_ICON, CONF_NAME) -import homeassistant.helpers.config_validation as cv +import socket +import logging from time import time, sleep from threading import Lock -import logging + +from homeassistant.const import ( + CONF_ID, + CONF_FRIENDLY_NAME, +) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -26,64 +29,53 @@ from homeassistant.components.light import ( SUPPORT_COLOR, SUPPORT_COLOR_TEMP, LightEntity, - PLATFORM_SCHEMA + PLATFORM_SCHEMA, ) from homeassistant.util import color as colorutil -import socket -REQUIREMENTS = ['pytuya==7.0.9'] +from . import BASE_PLATFORM_SCHEMA, import_from_yaml, prepare_setup_entities + +_LOGGER = logging.getLogger(__name__) -CONF_DEVICE_ID = 'device_id' -CONF_LOCAL_KEY = 'local_key' -CONF_PROTOCOL_VERSION = 'protocol_version' -# IMPORTANT, id is used as key for state and turning on and off, 1 was fine switched apparently but my bulbs need 20, other feature attributes count up from this, e.g. 21 mode, 22 brightnes etc, see my pytuya modification. -DEFAULT_ID = '1' -DEFAULT_PROTOCOL_VERSION = 3.3 MIN_MIRED = 153 MAX_MIRED = 370 UPDATE_RETRY_LIMIT = 3 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ICON): cv.icon, - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Required(CONF_LOCAL_KEY): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_FRIENDLY_NAME): cv.string, - vol.Required(CONF_PROTOCOL_VERSION, default=DEFAULT_PROTOCOL_VERSION): vol.Coerce(float), - vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, -}) -log = logging.getLogger(__name__) -log.setLevel(level=logging.DEBUG) # Debug hack! +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(BASE_PLATFORM_SCHEMA) + +DPS_FIELDS = [] + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Setup a Tuya switch based on a config entry.""" + device, entities_to_setup = prepare_setup_entities(config_entry, "light") + if not entities_to_setup: + return + + lights = [] + for device_config in entities_to_setup: + lights.append( + TuyaDevice( + TuyaCache(device), + device_config[CONF_FRIENDLY_NAME], + device_config[CONF_ID], + ) + ) + + async_add_entities(lights, True) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up of the Tuya switch.""" - from . import pytuya + return import_from_yaml(hass, config, "light") - lights = [] - pytuyadevice = pytuya.BulbDevice(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) - pytuyadevice.set_version(float(config.get(CONF_PROTOCOL_VERSION))) - - bulb_device = TuyaCache(pytuyadevice) - lights.append( - TuyaDevice( - bulb_device, - config.get(CONF_NAME), - config.get(CONF_FRIENDLY_NAME), - config.get(CONF_ICON), - config.get(CONF_ID) - ) - ) - - add_devices(lights) class TuyaCache: """Cache wrapper for pytuya.BulbDevice""" def __init__(self, device): """Initialize the cache.""" - self._cached_status = '' + self._cached_status = "" self._cached_status_time = 0 self._device = device self._lock = Lock() @@ -96,41 +88,31 @@ class TuyaCache: def __get_status(self, switchid): for _ in range(UPDATE_RETRY_LIMIT): try: - status = self._device.status()['dps'][switchid] - return status - except ConnectionError: + return self._device.status()["dps"][switchid] + except (ConnectionError, socket.timeout): pass - except socket.timeout: - pass - log.warn( - "Failed to get status after {} tries".format(UPDATE_RETRY_LIMIT)) + _LOGGER.warning("Failed to get status after %d tries", UPDATE_RETRY_LIMIT) def set_status(self, state, switchid): """Change the Tuya switch status and clear the cache.""" - self._cached_status = '' + self._cached_status = "" self._cached_status_time = 0 for _ in range(UPDATE_RETRY_LIMIT): try: return self._device.set_status(state, switchid) - except ConnectionError: + except (ConnectionError, socket.timeout): pass - except socket.timeout: - pass - log.warn( - "Failed to set status after {} tries".format(UPDATE_RETRY_LIMIT)) + _LOGGER.warning("Failed to set status after %d tries", UPDATE_RETRY_LIMIT) def status(self, switchid): """Get state of Tuya switch and cache the results.""" - self._lock.acquire() - try: + with self._lock: now = time() if not self._cached_status or now - self._cached_status_time > 30: sleep(0.5) self._cached_status = self.__get_status(switchid) self._cached_status_time = time() return self._cached_status - finally: - self._lock.release() def cached_status(self): return self._cached_status @@ -145,67 +127,52 @@ class TuyaCache: for _ in range(UPDATE_RETRY_LIMIT): try: return self._device.brightness() - except ConnectionError: + except (ConnectionError, socket.timeout): pass except KeyError: return "999" - except socket.timeout: - pass - log.warn( - "Failed to get brightness after {} tries".format(UPDATE_RETRY_LIMIT)) + _LOGGER.warning("Failed to get brightness after %d tries", UPDATE_RETRY_LIMIT) def color_temp(self): for _ in range(UPDATE_RETRY_LIMIT): try: return self._device.colourtemp() - except ConnectionError: + except (ConnectionError, socket.timeout): pass except KeyError: return "999" - except socket.timeout: - pass - log.warn( - "Failed to get color temp after {} tries".format(UPDATE_RETRY_LIMIT)) + _LOGGER.warning("Failed to get color temp after %d tries", UPDATE_RETRY_LIMIT) def set_brightness(self, brightness): for _ in range(UPDATE_RETRY_LIMIT): try: return self._device.set_brightness(brightness) - except ConnectionError: + except (ConnectionError, KeyError, socket.timeout): pass - except KeyError: - pass - except socket.timeout: - pass - log.warn( - "Failed to set brightness after {} tries".format(UPDATE_RETRY_LIMIT)) + _LOGGER.warning("Failed to set brightness after %d tries", UPDATE_RETRY_LIMIT) def set_color_temp(self, color_temp): for _ in range(UPDATE_RETRY_LIMIT): try: return self._device.set_colourtemp(color_temp) - except ConnectionError: + except (ConnectionError, KeyError, socket.timeout): pass - except KeyError: - pass - except socket.timeout: - pass - log.warn( - "Failed to set color temp after {} tries".format(UPDATE_RETRY_LIMIT)) + _LOGGER.warning("Failed to set color temp after %d tries", UPDATE_RETRY_LIMIT) def state(self): - self._device.state(); - + self._device.state() + def turn_on(self): - self._device.turn_on(); + self._device.turn_on() def turn_off(self): - self._device.turn_off(); + self._device.turn_off() + class TuyaDevice(LightEntity): """Representation of a Tuya switch.""" - def __init__(self, device, name, friendly_name, icon, bulbid): + def __init__(self, device, friendly_name, bulbid): """Initialize the Tuya switch.""" self._device = device self._available = False @@ -213,7 +180,6 @@ class TuyaDevice(LightEntity): self._state = False self._brightness = 127 self._color_temp = 127 - self._icon = icon self._bulb_id = bulbid @property @@ -236,11 +202,6 @@ class TuyaDevice(LightEntity): """Check if Tuya switch is on.""" return self._state - @property - def icon(self): - """Return the icon.""" - return self._icon - def update(self): """Get state of Tuya switch.""" try: @@ -254,12 +215,12 @@ class TuyaDevice(LightEntity): status = self._device.status(self._bulb_id) self._state = status try: - brightness = int(self._device.brightness()) - if brightness > 254: - brightness = 255 - if brightness < 25: - brightness = 25 - self._brightness = brightness + brightness = int(self._device.brightness()) + if brightness > 254: + brightness = 255 + if brightness < 25: + brightness = 25 + self._brightness = brightness except TypeError: pass self._color_temp = self._device.color_temp() @@ -269,16 +230,16 @@ class TuyaDevice(LightEntity): """Return the brightness of the light.""" return self._brightness -# @property -# def hs_color(self): -# """Return the hs_color of the light.""" -# return (self._device.color_hsv()[0],self._device.color_hsv()[1]) + # @property + # def hs_color(self): + # """Return the hs_color of the light.""" + # return (self._device.color_hsv()[0],self._device.color_hsv()[1]) @property def color_temp(self): """Return the color_temp of the light.""" try: - return int(MAX_MIRED - (((MAX_MIRED - MIN_MIRED) / 255) * self._color_temp)) + return int(MAX_MIRED - (((MAX_MIRED - MIN_MIRED) / 255) * self._color_temp)) except TypeError: pass @@ -294,8 +255,8 @@ class TuyaDevice(LightEntity): def turn_on(self, **kwargs): """Turn on or control the light.""" - log.debug("Turning on, state: " + str(self._device.cached_status())) - if not self._device.cached_status(): + _LOGGER.debug("Turning on, state: %s", self._device.cached_status()) + if not self._device.cached_status(): self._device.set_status(True, self._bulb_id) if ATTR_BRIGHTNESS in kwargs: converted_brightness = int(kwargs[ATTR_BRIGHTNESS]) @@ -305,7 +266,11 @@ class TuyaDevice(LightEntity): if ATTR_HS_COLOR in kwargs: raise ValueError(" TODO implement RGB from HS") if ATTR_COLOR_TEMP in kwargs: - color_temp = int(255 - (255 / (MAX_MIRED - MIN_MIRED)) * (int(kwargs[ATTR_COLOR_TEMP]) - MIN_MIRED)) + color_temp = int( + 255 + - (255 / (MAX_MIRED - MIN_MIRED)) + * (int(kwargs[ATTR_COLOR_TEMP]) - MIN_MIRED) + ) self._device.set_color_temp(color_temp) def turn_off(self, **kwargs): @@ -318,5 +283,5 @@ class TuyaDevice(LightEntity): supports = SUPPORT_BRIGHTNESS if self._device.color_temp() != "999": supports = supports | SUPPORT_COLOR - #supports = supports | SUPPORT_COLOR_TEMP + # supports = supports | SUPPORT_COLOR_TEMP return supports diff --git a/custom_components/localtuya/switch.py b/custom_components/localtuya/switch.py index 772c126..9ae0473 100644 --- a/custom_components/localtuya/switch.py +++ b/custom_components/localtuya/switch.py @@ -22,52 +22,42 @@ switch: sw02: name: usb_plug friendly_name: USB Plug - id: 7 + id: 7 """ import logging +from time import time, sleep +from threading import Lock + import voluptuous as vol from homeassistant.components.switch import SwitchEntity, PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_ID, - CONF_ENTITIES, CONF_SWITCHES, - CONF_DEVICE_ID, CONF_FRIENDLY_NAME, - CONF_ICON, CONF_NAME, - CONF_PLATFORM, ) -from homeassistant.config_entries import SOURCE_IMPORT import homeassistant.helpers.config_validation as cv -from time import time, sleep -from threading import Lock -from . import pytuya +from . import BASE_PLATFORM_SCHEMA, prepare_setup_entities, import_from_yaml from .const import ( ATTR_CURRENT, ATTR_CURRENT_CONSUMPTION, ATTR_VOLTAGE, - CONF_LOCAL_KEY, - CONF_PROTOCOL_VERSION, CONF_CURRENT, CONF_CURRENT_CONSUMPTION, CONF_VOLTAGE, - DOMAIN, ) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ["pytuya==7.0.9"] - DEFAULT_ID = "1" -DEFAULT_PROTOCOL_VERSION = 3.3 +# TODO: This will eventully merge with DPS_FIELDS SWITCH_SCHEMA = vol.Schema( { vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, - vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_NAME): cv.string, # Deprecated: not used vol.Required(CONF_FRIENDLY_NAME): cv.string, vol.Optional(CONF_CURRENT, default="-1"): cv.string, vol.Optional(CONF_CURRENT_CONSUMPTION, default="-1"): cv.string, @@ -75,18 +65,8 @@ SWITCH_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(BASE_PLATFORM_SCHEMA).extend( { - vol.Optional(CONF_ICON): cv.icon, - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Required(CONF_LOCAL_KEY): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_FRIENDLY_NAME): cv.string, - vol.Required( - CONF_PROTOCOL_VERSION, default=DEFAULT_PROTOCOL_VERSION - ): vol.Coerce(float), - vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, vol.Optional(CONF_CURRENT, default="-1"): cv.string, vol.Optional(CONF_CURRENT_CONSUMPTION, default="-1"): cv.string, vol.Optional(CONF_VOLTAGE, default="-1"): cv.string, @@ -104,27 +84,15 @@ DPS_FIELDS = [ async def async_setup_entry(hass, config_entry, async_add_entities): """Setup a Tuya switch based on a config entry.""" - switches_to_setup = [ - entity - for entity in config_entry.data[CONF_ENTITIES] - if entity[CONF_PLATFORM] == "switch" - ] - if not switches_to_setup: + device, entities_to_setup = prepare_setup_entities(config_entry, "switch") + if not entities_to_setup: return switches = [] - pytuyadevice = pytuya.OutletDevice( - config_entry.data[CONF_DEVICE_ID], - config_entry.data[CONF_HOST], - config_entry.data[CONF_LOCAL_KEY], - ) - pytuyadevice.set_version(float(config_entry.data[CONF_PROTOCOL_VERSION])) - pytuyadevice.set_dpsUsed({}) - - for device_config in switches_to_setup: + for device_config in entities_to_setup: switches.append( TuyaDevice( - TuyaCache(pytuyadevice), + TuyaCache(device), device_config[CONF_FRIENDLY_NAME], device_config[CONF_ID], device_config.get(CONF_CURRENT), @@ -138,14 +106,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): def setup_platform(hass, config, add_devices, discovery_info=None): """Set up of the Tuya switch.""" - config[CONF_PLATFORM] = "switch" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - - return True + return import_from_yaml(hass, config, "switch") class TuyaCache: From 7efb3024fbb0118fe4da410ef5778587ed2ba0fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Wed, 9 Sep 2020 12:52:46 +0200 Subject: [PATCH 02/11] Support arbitrary flow schemas for platforms --- custom_components/localtuya/config_flow.py | 46 ++++++++++++---------- custom_components/localtuya/light.py | 5 ++- custom_components/localtuya/switch.py | 14 ++++--- 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py index d8ba181..173cffd 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -44,22 +44,30 @@ PICK_ENTITY_SCHEMA = vol.Schema( ) -def platform_schema(dps, additional_fields): +def dps_string_list(dps_data): + """Return list of friendly DPS values.""" + return [f"{id} (value: {value})" for id, value in dps_data.items()] + + +def platform_schema(dps_strings, schema): """Generate input validation schema for a platform.""" - dps_list = vol.In([f"{id} (value: {value})" for id, value in dps.items()]) return vol.Schema( { - vol.Required(CONF_ID): dps_list, + vol.Required(CONF_ID): vol.In(dps_strings), vol.Required(CONF_FRIENDLY_NAME): str, } - ).extend({conf: dps_list for conf in additional_fields}) + ).extend(schema) -def strip_dps_values(user_input, fields): +def strip_dps_values(user_input, dps_strings): """Remove values and keep only index for DPS config items.""" - for field in [CONF_ID] + fields: - user_input[field] = user_input[field].split(" ")[0] - return user_input + stripped = {} + for field, value in user_input.items(): + if value in dps_strings: + stripped[field] = user_input[field].split(" ")[0] + else: + stripped[field] = user_input[field] + return stripped async def validate_input(hass: core.HomeAssistant, data): @@ -75,7 +83,7 @@ async def validate_input(hass: core.HomeAssistant, data): raise CannotConnect except ValueError: raise InvalidAuth - return data["dps"] + return dps_string_list(data["dps"]) class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -87,9 +95,9 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize a new LocaltuyaConfigFlow.""" self.basic_info = None - self.dps_data = None + self.dps_strings = [] self.platform = None - self.platform_dps_fields = None + self.platform_schema = None self.entities = [] async def async_step_user(self, user_input=None): @@ -101,7 +109,7 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: self.basic_info = user_input - self.dps_data = await validate_input(self.hass, user_input) + self.dps_strings = await validate_input(self.hass, user_input) return await self.async_step_pick_entity_type() except CannotConnect: errors["base"] = "cannot_connect" @@ -144,16 +152,14 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) if not already_configured: user_input[CONF_PLATFORM] = self.platform - self.entities.append( - strip_dps_values(user_input, self.platform_dps_fields) - ) + self.entities.append(strip_dps_values(user_input, self.dps_strings)) return await self.async_step_pick_entity_type() errors["base"] = "entity_already_configured" return self.async_show_form( step_id="add_entity", - data_schema=platform_schema(self.dps_data, self.platform_dps_fields), + data_schema=platform_schema(self.dps_strings, self.platform_schema), errors=errors, description_placeholders={"platform": self.platform}, ) @@ -167,12 +173,12 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_FRIENDLY_NAME: conf[CONF_FRIENDLY_NAME], CONF_PLATFORM: self.platform, } - for field in self.platform_dps_fields: + for field in self.platform_schema.keys(): converted[str(field)] = conf[field] return converted await self.async_set_unique_id(user_input[CONF_DEVICE_ID]) - self._set_platform(user_input[CONF_PLATFORM]) + self._set_platform(user_input[CONF_PLATFORM], []) if len(user_input.get(CONF_SWITCHES, [])) > 0: for switch_conf in user_input[CONF_SWITCHES].values(): @@ -194,9 +200,9 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def _set_platform(self, platform): integration_module = ".".join(__name__.split(".")[:-1]) self.platform = platform - self.platform_dps_fields = import_module( + self.platform_schema = import_module( "." + platform, integration_module - ).DPS_FIELDS + ).flow_schema(self.dps_strings) class CannotConnect(exceptions.HomeAssistantError): diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index d6ffe99..37c57b0 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -43,7 +43,10 @@ UPDATE_RETRY_LIMIT = 3 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(BASE_PLATFORM_SCHEMA) -DPS_FIELDS = [] + +def flow_schema(dps): + """Return schema used in config flow.""" + return {} async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/custom_components/localtuya/switch.py b/custom_components/localtuya/switch.py index 9ae0473..8b12b6c 100644 --- a/custom_components/localtuya/switch.py +++ b/custom_components/localtuya/switch.py @@ -53,7 +53,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_ID = "1" -# TODO: This will eventully merge with DPS_FIELDS +# TODO: This will eventully merge with flow_schema SWITCH_SCHEMA = vol.Schema( { vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, @@ -75,11 +75,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(BASE_PLATFORM_SCHEMA).extend( ) -DPS_FIELDS = [ - vol.Optional(CONF_CURRENT), - vol.Optional(CONF_CURRENT_CONSUMPTION), - vol.Optional(CONF_VOLTAGE), -] +def flow_schema(dps): + """Return schema used in config flow.""" + return { + vol.Optional(CONF_CURRENT): vol.In(dps), + vol.Optional(CONF_CURRENT_CONSUMPTION): vol.In(dps), + vol.Optional(CONF_VOLTAGE): vol.In(dps), + } async def async_setup_entry(hass, config_entry, async_add_entities): From 11763d90c500984b1ca0901376a1bb7cbd7e2efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Wed, 9 Sep 2020 23:06:28 +0200 Subject: [PATCH 03/11] Use generic Device in config flow --- custom_components/localtuya/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py index 173cffd..eca5e1b 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -72,7 +72,7 @@ def strip_dps_values(user_input, dps_strings): async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect.""" - pytuyadevice = pytuya.OutletDevice( + pytuyadevice = pytuya.Device( data[CONF_DEVICE_ID], data[CONF_HOST], data[CONF_LOCAL_KEY] ) pytuyadevice.set_version(float(data[CONF_PROTOCOL_VERSION])) From ae64158da5b49232ce569c8d0ca4c95074d9ee1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Thu, 10 Sep 2020 10:42:44 +0200 Subject: [PATCH 04/11] Use correct device class from pytuya --- custom_components/localtuya/__init__.py | 4 ++-- custom_components/localtuya/light.py | 5 ++++- custom_components/localtuya/switch.py | 5 ++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/custom_components/localtuya/__init__.py b/custom_components/localtuya/__init__.py index ff07ef8..022bffb 100644 --- a/custom_components/localtuya/__init__.py +++ b/custom_components/localtuya/__init__.py @@ -36,7 +36,7 @@ BASE_PLATFORM_SCHEMA = { } -def prepare_setup_entities(config_entry, platform): +def prepare_setup_entities(config_entry, platform, device_class): """Prepare ro setup entities for a platform.""" entities_to_setup = [ entity @@ -46,7 +46,7 @@ def prepare_setup_entities(config_entry, platform): if not entities_to_setup: return None, None - device = pytuya.BulbDevice( + device = device_class( config_entry.data[CONF_DEVICE_ID], config_entry.data[CONF_HOST], config_entry.data[CONF_LOCAL_KEY], diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index 37c57b0..7f68c51 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -34,6 +34,7 @@ from homeassistant.components.light import ( from homeassistant.util import color as colorutil from . import BASE_PLATFORM_SCHEMA, import_from_yaml, prepare_setup_entities +from .pytuya import BulbDevice _LOGGER = logging.getLogger(__name__) @@ -51,7 +52,9 @@ def flow_schema(dps): async def async_setup_entry(hass, config_entry, async_add_entities): """Setup a Tuya switch based on a config entry.""" - device, entities_to_setup = prepare_setup_entities(config_entry, "light") + device, entities_to_setup = prepare_setup_entities( + config_entry, "light", BulbDevice + ) if not entities_to_setup: return diff --git a/custom_components/localtuya/switch.py b/custom_components/localtuya/switch.py index 8b12b6c..9dd64da 100644 --- a/custom_components/localtuya/switch.py +++ b/custom_components/localtuya/switch.py @@ -48,6 +48,7 @@ from .const import ( CONF_CURRENT_CONSUMPTION, CONF_VOLTAGE, ) +from .pytuya import OutletDevice _LOGGER = logging.getLogger(__name__) @@ -86,7 +87,9 @@ def flow_schema(dps): async def async_setup_entry(hass, config_entry, async_add_entities): """Setup a Tuya switch based on a config entry.""" - device, entities_to_setup = prepare_setup_entities(config_entry, "switch") + device, entities_to_setup = prepare_setup_entities( + config_entry, "switch", OutletDevice + ) if not entities_to_setup: return From edc9debf839a6553cc2ad26d754006454314e63a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Thu, 10 Sep 2020 10:20:14 +0200 Subject: [PATCH 05/11] Add config flow support for cover --- custom_components/localtuya/const.py | 9 +- custom_components/localtuya/cover.py | 263 +++++++++--------- .../localtuya/pytuya/__init__.py | 4 +- .../localtuya/translations/en.json | 5 +- 4 files changed, 148 insertions(+), 133 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 532f260..9cc9623 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -6,11 +6,18 @@ ATTR_VOLTAGE = 'voltage' CONF_LOCAL_KEY = "local_key" CONF_PROTOCOL_VERSION = "protocol_version" + +# switch CONF_CURRENT = "current" CONF_CURRENT_CONSUMPTION = "current_consumption" CONF_VOLTAGE = "voltage" +# cover +CONF_OPEN_CMD = 'open_cmd' +CONF_CLOSE_CMD = 'close_cmd' +CONF_STOP_CMD = 'stop_cmd' + DOMAIN = "localtuya" # Platforms in this list must support config flows -PLATFORMS = ["light", "switch"] +PLATFORMS = ["cover", "light", "switch"] diff --git a/custom_components/localtuya/cover.py b/custom_components/localtuya/cover.py index 8cd76cf..8f4af68 100644 --- a/custom_components/localtuya/cover.py +++ b/custom_components/localtuya/cover.py @@ -19,7 +19,8 @@ cover: """ import logging -import requests +from time import time, sleep +from threading import Lock import voluptuous as vol @@ -31,84 +32,74 @@ from homeassistant.components.cover import ( SUPPORT_STOP, SUPPORT_SET_POSITION, ) - -"""from . import DATA_TUYA, TuyaDevice""" -from homeassistant.components.cover import CoverEntity, PLATFORM_SCHEMA -from homeassistant.const import (CONF_HOST, CONF_ID, CONF_FRIENDLY_NAME, CONF_ICON, CONF_NAME) +from homeassistant.const import ( + CONF_ID, + CONF_FRIENDLY_NAME, +) import homeassistant.helpers.config_validation as cv -from time import time, sleep -from threading import Lock + +from . import BASE_PLATFORM_SCHEMA, prepare_setup_entities, import_from_yaml +from .const import CONF_OPEN_CMD, CONF_CLOSE_CMD, CONF_STOP_CMD +from .pytuya import CoverDevice _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'localtuyacover' +DEFAULT_OPEN_CMD = "on" +DEFAULT_CLOSE_CMD = "off" +DEFAULT_STOP_CMD = "stop" -REQUIREMENTS = ['pytuya==7.0.9'] - -CONF_DEVICE_ID = 'device_id' -CONF_LOCAL_KEY = 'local_key' -CONF_PROTOCOL_VERSION = 'protocol_version' - -CONF_OPEN_CMD = 'open_cmd' -CONF_CLOSE_CMD = 'close_cmd' -CONF_STOP_CMD = 'stop_cmd' - -DEFAULT_ID = '1' -DEFAULT_PROTOCOL_VERSION = 3.3 -DEFAULT_OPEN_CMD = 'on' -DEFAULT_CLOSE_CMD = 'off' -DEFAULT_STOP_CMD = 'stop' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ICON): cv.icon, - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Required(CONF_LOCAL_KEY): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_FRIENDLY_NAME): cv.string, - vol.Required(CONF_PROTOCOL_VERSION, default=DEFAULT_PROTOCOL_VERSION): vol.Coerce(float), - vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, - vol.Optional(CONF_OPEN_CMD, default=DEFAULT_OPEN_CMD): cv.string, - vol.Optional(CONF_CLOSE_CMD, default=DEFAULT_CLOSE_CMD): cv.string, - vol.Optional(CONF_STOP_CMD, default=DEFAULT_STOP_CMD): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(BASE_PLATFORM_SCHEMA).extend( + { + vol.Optional(CONF_OPEN_CMD, default=DEFAULT_OPEN_CMD): cv.string, + vol.Optional(CONF_CLOSE_CMD, default=DEFAULT_CLOSE_CMD): cv.string, + vol.Optional(CONF_STOP_CMD, default=DEFAULT_STOP_CMD): cv.string, + } +) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tuya cover devices.""" - from . import pytuya +def flow_schema(dps): + """Return schema used in config flow.""" + return { + vol.Optional(CONF_OPEN_CMD, default=DEFAULT_OPEN_CMD): str, + vol.Optional(CONF_CLOSE_CMD, default=DEFAULT_CLOSE_CMD): str, + vol.Optional(CONF_STOP_CMD, default=DEFAULT_STOP_CMD): str, + } - #_LOGGER.info("running def setup_platform from cover.py") - #_LOGGER.info("conf_open_cmd is %s", config.get(CONF_OPEN_CMD)) - #_LOGGER.info("conf_close_cmd is %s", config.get(CONF_CLOSE_CMD)) - #_LOGGER.info("conf_STOP_cmd is %s", config.get(CONF_STOP_CMD)) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Setup a Tuya cover based on a config entry.""" + device, entities_to_setup = prepare_setup_entities( + config_entry, "cover", CoverDevice + ) + if not entities_to_setup: + return + + # TODO: keeping for now but should be removed + dps = {} + dps["101"] = None + dps["102"] = None covers = [] - pytuyadevice = pytuya.CoverEntity(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) - pytuyadevice.set_version(float(config.get(CONF_PROTOCOL_VERSION))) - dps = {} - dps[config.get(CONF_ID)]=None - dps["101"]=None - dps["102"]=None - pytuyadevice.set_dpsUsed(dps) - - cover_device = TuyaCoverCache(pytuyadevice) - covers.append( + for device_config in entities_to_setup: + dps[device_config[CONF_ID]] = None + covers.append( TuyaDevice( - cover_device, - config.get(CONF_NAME), - config.get(CONF_FRIENDLY_NAME), - config.get(CONF_ICON), - config.get(CONF_ID), - config.get(CONF_OPEN_CMD), - config.get(CONF_CLOSE_CMD), - config.get(CONF_STOP_CMD), + TuyaCoverCache(device), + device_config[CONF_FRIENDLY_NAME], + device_config[CONF_ID], + device_config.get(CONF_OPEN_CMD), + device_config.get(CONF_CLOSE_CMD), + device_config.get(CONF_STOP_CMD), ) - ) - print('Setup localtuya cover [{}] with device ID [{}] '.format(config.get(CONF_FRIENDLY_NAME), config.get(CONF_ID))) - _LOGGER.info("Setup localtuya cover %s with device ID=%s open_cmd=%s close_cmd=%s stop_cmd=%s", config.get(CONF_FRIENDLY_NAME), config.get(CONF_ID), config.get(CONF_OPEN_CMD), config.get(CONF_CLOSE_CMD), config.get(CONF_STOP_CMD) ) + ) - add_entities(covers, True) + device.set_dpsUsed(dps) + async_add_entities(covers, True) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up of the Tuya cover.""" + return import_from_yaml(hass, config, "cover") class TuyaCoverCache: @@ -116,7 +107,7 @@ class TuyaCoverCache: def __init__(self, device): """Initialize the cache.""" - self._cached_status = '' + self._cached_status = "" self._cached_status_time = 0 self._device = device self._lock = Lock() @@ -127,38 +118,49 @@ class TuyaCoverCache: return self._device.id def __get_status(self): - #_LOGGER.info("running def __get_status from cover") + # _LOGGER.info("running def __get_status from cover") for i in range(5): try: status = self._device.status() return status except Exception: - print('Failed to update status of device [{}]'.format(self._device.address)) + print( + "Failed to update status of device [{}]".format( + self._device.address + ) + ) sleep(1.0) - if i+1 == 3: - _LOGGER.error("Failed to update status of device %s", self._device.address ) -# return None + if i + 1 == 3: + _LOGGER.error( + "Failed to update status of device %s", self._device.address + ) + # return None raise ConnectionError("Failed to update status .") def set_status(self, state, switchid): - #_LOGGER.info("running def set_status from cover") + # _LOGGER.info("running def set_status from cover") """Change the Tuya switch status and clear the cache.""" - self._cached_status = '' + self._cached_status = "" self._cached_status_time = 0 for i in range(5): try: - #_LOGGER.info("Running a try from def set_status from cover where state=%s and switchid=%s", state, switchid) + # _LOGGER.info("Running a try from def set_status from cover where state=%s and switchid=%s", state, switchid) return self._device.set_status(state, switchid) except Exception: - print('Failed to set status of device [{}]'.format(self._device.address)) - if i+1 == 3: - _LOGGER.error("Failed to set status of device %s", self._device.address ) + print( + "Failed to set status of device [{}]".format(self._device.address) + ) + if i + 1 == 3: + _LOGGER.error( + "Failed to set status of device %s", self._device.address + ) return -# raise ConnectionError("Failed to set status.") + + # raise ConnectionError("Failed to set status.") def status(self): """Get state of Tuya switch and cache the results.""" - #_LOGGER.info("running def status(self) from cover") + # _LOGGER.info("running def status(self) from cover") self._lock.acquire() try: now = time() @@ -170,24 +172,26 @@ class TuyaCoverCache: finally: self._lock.release() + class TuyaDevice(CoverEntity): """Tuya cover devices.""" - def __init__(self, device, name, friendly_name, icon, switchid, open_cmd, close_cmd, stop_cmd): + def __init__(self, device, friendly_name, switchid, open_cmd, close_cmd, stop_cmd): self._device = device self._available = False self._name = friendly_name - self._friendly_name = friendly_name - self._icon = icon self._switch_id = switchid - self._status = self._device.status() - self._state = self._status['dps'][self._switch_id] + self._status = None + self._state = None self._position = 50 self._open_cmd = open_cmd self._close_cmd = close_cmd self._stop_cmd = stop_cmd - _LOGGER.info("running def __init__ of TuyaDevice(CoverEntity) from cover.py 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) - print('Initialized tuya cover [{}] with switch status [{}] and state [{}]'.format(self._name, self._status, self._state)) + print( + "Initialized tuya cover [{}] with switch status [{}] and state [{}]".format( + self._name, self._status, self._state + ) + ) @property def name(self): @@ -222,105 +226,106 @@ class TuyaDevice(CoverEntity): @property def supported_features(self): """Flag supported features.""" - supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION + supported_features = ( + SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION + ) return supported_features - @property - def icon(self): - """Return the icon.""" - return self._icon - @property def current_cover_position(self): - #self.update() - #state = self._state -# _LOGGER.info("curr_pos() : %i", self._position) - #print('curr_pos() : state [{}]'.format(state)) + # self.update() + # state = self._state + # _LOGGER.info("curr_pos() : %i", self._position) + # print('curr_pos() : state [{}]'.format(state)) return self._position @property def is_opening(self): - #self.update() + # self.update() state = self._state - #print('is_opening() : state [{}]'.format(state)) - if state == 'on': + # print('is_opening() : state [{}]'.format(state)) + if state == "on": return True return False @property def is_closing(self): - #self.update() + # self.update() state = self._state - #print('is_closing() : state [{}]'.format(state)) - if state == 'off': + # print('is_closing() : state [{}]'.format(state)) + if state == "off": return True return False @property def is_closed(self): """Return if the cover is closed or not.""" - #_LOGGER.info("running is_closed from cover") - #self.update() + # _LOGGER.info("running is_closed from cover") + # self.update() state = self._state - #print('is_closed() : state [{}]'.format(state)) - if state == 'off': + # print('is_closed() : state [{}]'.format(state)) + if state == "off": return False - if state == 'on': + if state == "on": return True return None def set_cover_position(self, **kwargs): - #_LOGGER.info("running set_cover_position from cover") + # _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) + # _LOGGER.info("Set new pos: %f", newpos) currpos = self.current_cover_position posdiff = abs(newpos - currpos) -# 25 sec corrisponde alla chiusura/apertura completa + # 25 sec corrisponde alla chiusura/apertura completa mydelay = posdiff / 2.0 if newpos > currpos: -# _LOGGER.info("Opening to %f: delay %f", newpos, mydelay ) + # _LOGGER.info("Opening to %f: delay %f", newpos, mydelay ) self.open_cover() else: -# _LOGGER.info("Closing to %f: delay %f", newpos, mydelay ) + # _LOGGER.info("Closing to %f: delay %f", newpos, mydelay ) self.close_cover() - sleep( mydelay ) + sleep(mydelay) self.stop_cover() self._position = 50 # newpos -# self._state = 'on' -# self._device._device.open_cover() + + # self._state = 'on' + # self._device._device.open_cover() def open_cover(self, **kwargs): """Open the cover.""" - #_LOGGER.info("running open_cover from cover") + # _LOGGER.info("running open_cover from cover") self._device.set_status(self._open_cmd, self._switch_id) -# self._state = 'on' -# self._device._device.open_cover() + + # self._state = 'on' + # self._device._device.open_cover() def close_cover(self, **kwargs): - #_LOGGER.info("running close_cover from cover") + # _LOGGER.info("running close_cover from cover") """Close cover.""" - #_LOGGER.info('about to set_status from cover of off, %s', self._switch_id) + # _LOGGER.info('about to set_status from cover of off, %s', self._switch_id) self._device.set_status(self._close_cmd, self._switch_id) -# self._state = 'off' -# self._device._device.close_cover() + + # self._state = 'off' + # self._device._device.close_cover() def stop_cover(self, **kwargs): - #_LOGGER.info("running stop_cover from cover") + # _LOGGER.info("running stop_cover from cover") """Stop the cover.""" self._device.set_status(self._stop_cmd, self._switch_id) -# self._state = 'stop' -# self._device._device.stop_cover() + + # self._state = 'stop' + # self._device._device.stop_cover() def update(self): """Get state of Tuya switch.""" - #_LOGGER.info("running update(self) from cover") + # _LOGGER.info("running update(self) from cover") try: self._status = self._device.status() - self._state = self._status['dps'][self._switch_id] - #print('update() : state [{}]'.format(self._state)) + self._state = self._status["dps"][self._switch_id] + # print('update() : state [{}]'.format(self._state)) except Exception: self._available = False else: diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 0ac26fb..278a74a 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -442,7 +442,7 @@ class FanDevice(Device): def __init__(self, dev_id, address, local_key=None): super(FanDevice, self).__init__(dev_id, address, local_key) -class CoverEntity(Device): +class CoverDevice(Device): DPS_INDEX_MOVE = '1' DPS_INDEX_BL = '101' @@ -460,7 +460,7 @@ class CoverEntity(Device): else: print('Using PyCrypto ', Crypto.version_info) print('Using PyCrypto from ', Crypto.__file__) - super(CoverEntity, self).__init__(dev_id, address, local_key) + super(CoverDevice, self).__init__(dev_id, address, local_key) def open_cover(self, switch=1): """Turn the device on""" diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 6ed5cfc..c0be82a 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -37,7 +37,10 @@ "friendly_name": "Friendly name", "current": "Current", "current_consumption": "Current Consumption", - "voltage": "Voltage" + "voltage": "Voltage", + "open_cmd": "Open Command", + "close_cmd": "Close Command", + "stop_cmd": "Stop Command" } } } From f9a4b4ad8a57c3ad439c84d8557ddf2db9a7818d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Thu, 10 Sep 2020 12:07:59 +0200 Subject: [PATCH 06/11] Add config flow support for fan --- custom_components/localtuya/const.py | 2 +- custom_components/localtuya/fan.py | 421 +++++++++++++-------------- 2 files changed, 206 insertions(+), 217 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 9cc9623..c348a42 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -20,4 +20,4 @@ CONF_STOP_CMD = 'stop_cmd' DOMAIN = "localtuya" # Platforms in this list must support config flows -PLATFORMS = ["cover", "light", "switch"] +PLATFORMS = ["cover", "fan", "light", "switch"] diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 3fbdf6c..ac0a8af 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -1,216 +1,205 @@ -""" -Simple platform to control LOCALLY Tuya cover devices. - -Sample config yaml - -fan: - - platform: localtuya - host: 192.168.0.123 - local_key: 1234567891234567 - device_id: 123456789123456789abcd - name: fan guests - friendly_name: fan guests - protocol_version: 3.3 - id: 1 - -""" -import logging -import requests - -import voluptuous as vol - -from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, - FanEntity, SUPPORT_SET_SPEED, - SUPPORT_OSCILLATE, SUPPORT_DIRECTION, PLATFORM_SCHEMA) - -from homeassistant.const import STATE_OFF -"""from . import DATA_TUYA, TuyaDevice""" -from homeassistant.const import (CONF_HOST, CONF_ID, CONF_FRIENDLY_NAME, CONF_ICON, CONF_NAME) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = 'localtuyafan' - -REQUIREMENTS = ['pytuya==7.0.9'] - -CONF_DEVICE_ID = 'device_id' -CONF_LOCAL_KEY = 'local_key' -CONF_PROTOCOL_VERSION = 'protocol_version' - -DEFAULT_ID = '1' -DEFAULT_PROTOCOL_VERSION = 3.3 - - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ICON): cv.icon, - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Required(CONF_LOCAL_KEY): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_FRIENDLY_NAME): cv.string, - vol.Required(CONF_PROTOCOL_VERSION, default=DEFAULT_PROTOCOL_VERSION): vol.Coerce(float), - vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up of the Tuya switch.""" - from . import pytuya - fans = [] - localtuyadevice = pytuya.FanDevice(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) - localtuyadevice.set_version(float(config.get(CONF_PROTOCOL_VERSION))) - _LOGGER.debug("localtuya fan: setup_platform: %s", localtuyadevice) - - fan_device = localtuyadevice - fans.append( - TuyaDevice( - fan_device, - config.get(CONF_NAME), - config.get(CONF_FRIENDLY_NAME), - config.get(CONF_ICON), - config.get(CONF_ID), - ) - ) - _LOGGER.info("Setup localtuya fan %s with device ID %s ", config.get(CONF_FRIENDLY_NAME), config.get(CONF_DEVICE_ID) ) - - add_entities(fans, True) - -class TuyaDevice(FanEntity): - """A demonstration fan component.""" - - # def __init__(self, hass, name: str, supported_features: int) -> None: - def __init__(self, device, name, friendly_name, icon, switchid): - """Initialize the entity.""" - self._device = device - self._name = friendly_name - self._available = False - self._friendly_name = friendly_name - self._icon = icon - self._switch_id = switchid - self._status = self._device.status() - self._state = False - self._supported_features = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE - self._speed = STATE_OFF - self._oscillating = False - - @property - def oscillating(self): - """Return current oscillating status.""" - #if self._speed == STATE_OFF: - # return False - _LOGGER.debug("localtuya fan: oscillating = %s", self._oscillating) - return self._oscillating - - @property - def name(self) -> str: - """Get entity name.""" - return self._name - - @property - def is_on(self): - """Check if Tuya switch is on.""" - return self._state - - @property - def speed(self) -> str: - """Return the current speed.""" - return self._speed - - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - # return ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] - - def turn_on(self, speed: str = None, **kwargs) -> None: - """Turn on the entity.""" - _LOGGER.debug("localtuya fan: turn_on speed to: %s", speed) - # if speed is None: - # speed = SPEED_MEDIUM - # self.set_speed(speed) - self._device.turn_on() - if speed is not None: - self.set_speed(speed) - self.schedule_update_ha_state() - - def turn_off(self, **kwargs) -> None: - """Turn off the entity.""" - _LOGGER.debug("localtuya fan: turn_off") - self._device.set_status(False, '1') - self._state = False - self.schedule_update_ha_state() - - def set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - _LOGGER.debug("localtuya fan: set_speed to: %s", speed) - self._speed = speed - # self.schedule_update_ha_state() - if speed == STATE_OFF: - self._device.set_status(False, '1') - self._state = False - elif speed == SPEED_LOW: - self._device.set_value('2', '1') - elif speed == SPEED_MEDIUM: - self._device.set_value('2', '2') - elif speed == SPEED_HIGH: - self._device.set_value('2', '3') - self.schedule_update_ha_state() - - # def set_direction(self, direction: str) -> None: - # """Set the direction of the fan.""" - # self.direction = direction - # self.schedule_update_ha_state() - - def oscillate(self, oscillating: bool) -> None: - """Set oscillation.""" - self._oscillating = oscillating - self._device.set_value('8', oscillating) - self.schedule_update_ha_state() - - # @property - # def current_direction(self) -> str: - # """Fan direction.""" - # return self.direction - - @property - def unique_id(self): - """Return unique device identifier.""" - _LOGGER.debug("localtuya fan unique_id = %s", self._device) - return self._device.id - - @property - def available(self): - """Return if device is available or not.""" - return self._available - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return self._supported_features - - def update(self): - """Get state of Tuya switch.""" - success = False - for i in range(3): - if success is False: - try: - status = self._device.status() - _LOGGER.debug("localtuya fan: status = %s", status) - self._state = status['dps']['1'] - if status['dps']['1'] == False: - self._speed = STATE_OFF - elif int(status['dps']['2']) == 1: - self._speed = SPEED_LOW - elif int(status['dps']['2']) == 2: - self._speed = SPEED_MEDIUM - elif int(status['dps']['2']) == 3: - self._speed = SPEED_HIGH - # self._speed = status['dps']['2'] - self._oscillating = status['dps']['8'] - success = True - except ConnectionError: - if i+1 == 3: - success = False - raise ConnectionError("Failed to update status.") - self._available = success +""" +Simple platform to control LOCALLY Tuya cover devices. + +Sample config yaml + +fan: + - platform: localtuya + host: 192.168.0.123 + local_key: 1234567891234567 + device_id: 123456789123456789abcd + name: fan guests + friendly_name: fan guests + protocol_version: 3.3 + id: 1 + +""" +import logging + +import voluptuous as vol + +from homeassistant.components.fan import ( + SPEED_LOW, + SPEED_MEDIUM, + SPEED_HIGH, + FanEntity, + SUPPORT_SET_SPEED, + SUPPORT_OSCILLATE, + SUPPORT_DIRECTION, + PLATFORM_SCHEMA, +) +from homeassistant.const import CONF_ID, CONF_FRIENDLY_NAME, STATE_OFF +import homeassistant.helpers.config_validation as cv + +from . import BASE_PLATFORM_SCHEMA, prepare_setup_entities, import_from_yaml +from .pytuya import FanDevice + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(BASE_PLATFORM_SCHEMA) + + +def flow_schema(dps): + """Return schema used in config flow.""" + return {} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Setup a Tuya fan based on a config entry.""" + device, entities_to_setup = prepare_setup_entities(config_entry, "fan", FanDevice) + if not entities_to_setup: + return + + lights = [] + for device_config in entities_to_setup: + lights.append( + TuyaDevice( + device, + device_config[CONF_FRIENDLY_NAME], + device_config[CONF_ID], + ) + ) + + async_add_entities(lights, True) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up of the Tuya fan.""" + return import_from_yaml(hass, config, "fan") + + +class TuyaDevice(FanEntity): + """A demonstration fan component.""" + + def __init__(self, device, friendly_name, switchid): + """Initialize the entity.""" + self._device = device + self._name = friendly_name + self._available = False + self._friendly_name = friendly_name + self._switch_id = switchid + self._status = self._device.status() + self._state = False + self._supported_features = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE + self._speed = STATE_OFF + self._oscillating = False + + @property + def oscillating(self): + """Return current oscillating status.""" + # if self._speed == STATE_OFF: + # return False + _LOGGER.debug("localtuya fan: oscillating = %s", self._oscillating) + return self._oscillating + + @property + def name(self) -> str: + """Get entity name.""" + return self._name + + @property + def is_on(self): + """Check if Tuya switch is on.""" + return self._state + + @property + def speed(self) -> str: + """Return the current speed.""" + return self._speed + + @property + def speed_list(self) -> list: + """Get the list of available speeds.""" + return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + # return ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] + + def turn_on(self, speed: str = None, **kwargs) -> None: + """Turn on the entity.""" + _LOGGER.debug("localtuya fan: turn_on speed to: %s", speed) + # if speed is None: + # speed = SPEED_MEDIUM + # self.set_speed(speed) + self._device.turn_on() + if speed is not None: + self.set_speed(speed) + self.schedule_update_ha_state() + + def turn_off(self, **kwargs) -> None: + """Turn off the entity.""" + _LOGGER.debug("localtuya fan: turn_off") + self._device.set_status(False, "1") + self._state = False + self.schedule_update_ha_state() + + def set_speed(self, speed: str) -> None: + """Set the speed of the fan.""" + _LOGGER.debug("localtuya fan: set_speed to: %s", speed) + self._speed = speed + # self.schedule_update_ha_state() + if speed == STATE_OFF: + self._device.set_status(False, "1") + self._state = False + elif speed == SPEED_LOW: + self._device.set_value("2", "1") + elif speed == SPEED_MEDIUM: + self._device.set_value("2", "2") + elif speed == SPEED_HIGH: + self._device.set_value("2", "3") + self.schedule_update_ha_state() + + # def set_direction(self, direction: str) -> None: + # """Set the direction of the fan.""" + # self.direction = direction + # self.schedule_update_ha_state() + + def oscillate(self, oscillating: bool) -> None: + """Set oscillation.""" + self._oscillating = oscillating + self._device.set_value("8", oscillating) + self.schedule_update_ha_state() + + # @property + # def current_direction(self) -> str: + # """Fan direction.""" + # return self.direction + + @property + def unique_id(self): + """Return unique device identifier.""" + return f"local_{self._device.id}_{self._switch_id}" + + @property + def available(self): + """Return if device is available or not.""" + return self._available + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return self._supported_features + + def update(self): + """Get state of Tuya switch.""" + success = False + for i in range(3): + if success is False: + try: + status = self._device.status() + _LOGGER.debug("localtuya fan: status = %s", status) + self._state = status["dps"]["1"] + if status["dps"]["1"] == False: + self._speed = STATE_OFF + elif int(status["dps"]["2"]) == 1: + self._speed = SPEED_LOW + elif int(status["dps"]["2"]) == 2: + self._speed = SPEED_MEDIUM + elif int(status["dps"]["2"]) == 3: + self._speed = SPEED_HIGH + # self._speed = status['dps']['2'] + self._oscillating = status["dps"]["8"] + success = True + except ConnectionError: + if i + 1 == 3: + success = False + raise ConnectionError("Failed to update status.") + self._available = success From 47b92b01786406582f2f60d3bca19ce6772e23a3 Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Sun, 13 Sep 2020 14:03:08 +0200 Subject: [PATCH 07/11] Fixes suggested in PR #11 --- custom_components/localtuya/cover.py | 14 +- custom_components/localtuya/fan.py | 4 +- custom_components/localtuya/light.py | 166 ++++++++++-------- .../localtuya/pytuya/__init__.py | 24 +-- custom_components/localtuya/switch.py | 10 +- 5 files changed, 114 insertions(+), 104 deletions(-) diff --git a/custom_components/localtuya/cover.py b/custom_components/localtuya/cover.py index 7078343..d6bdceb 100644 --- a/custom_components/localtuya/cover.py +++ b/custom_components/localtuya/cover.py @@ -43,8 +43,6 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'localtuyacover' -REQUIREMENTS = ['pytuya>=8.0.0'] - CONF_DEVICE_ID = 'device_id' CONF_LOCAL_KEY = 'local_key' CONF_PROTOCOL_VERSION = 'protocol_version' @@ -84,7 +82,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): #_LOGGER.info("conf_STOP_cmd is %s", config.get(CONF_STOP_CMD)) covers = [] - pytuyadevice = pytuya.PytuyaDevice(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) + pytuyadevice = pytuya.TuyaDevice(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) pytuyadevice.set_version(float(config.get(CONF_PROTOCOL_VERSION))) dps = {} dps[config.get(CONF_ID)]=None @@ -111,7 +109,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class TuyaCache: - """Cache wrapper for pytuya.PytuyaDevice""" + """Cache wrapper for pytuya.TuyaDevice""" def __init__(self, device): """Initialize the cache.""" @@ -139,15 +137,15 @@ class TuyaCache: # return None raise ConnectionError("Failed to update status .") - def set_dps(self, state, switchid): + def set_dps(self, state, dps_index): #_LOGGER.info("running def set_dps from cover") """Change the Tuya switch status and clear the cache.""" self._cached_status = '' self._cached_status_time = 0 for i in range(5): try: - #_LOGGER.info("Running a try from def set_dps from cover where state=%s and switchid=%s", state, switchid) - return self._device.set_dps(state, switchid) + #_LOGGER.info("Running a try from def set_dps from cover where state=%s and dps_index=%s", state, dps_index) + return self._device.set_dps(state, dps_index) except Exception: print('Failed to set status of device [{}]'.format(self._device.address)) if i+1 == 3: @@ -185,7 +183,7 @@ class LocaltuyaCover(CoverEntity): self._open_cmd = open_cmd self._close_cmd = close_cmd self._stop_cmd = stop_cmd - #_LOGGER.info("running def __init__ of TuyaDevice(PytuyaDevice) from cover.py 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) + #_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) print('Initialized tuya cover [{}] with switch status [{}] and state [{}]'.format(self._name, self._status, self._state)) @property diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 445c79c..2b08fba 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -32,8 +32,6 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'localtuyafan' -REQUIREMENTS = ['pytuya>=8.0.0'] - CONF_DEVICE_ID = 'device_id' CONF_LOCAL_KEY = 'local_key' CONF_PROTOCOL_VERSION = 'protocol_version' @@ -58,7 +56,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up of the Tuya switch.""" from . import pytuya fans = [] - pytuyadevice = pytuya.PytuyaDevice(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) + pytuyadevice = pytuya.TuyaDevice(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) pytuyadevice.set_version(float(config.get(CONF_PROTOCOL_VERSION))) _LOGGER.debug("localtuya fan: setup_platform: %s", pytuyadevice) diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index dd07d20..dc7c0bf 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -31,8 +31,6 @@ from homeassistant.components.light import ( from homeassistant.util import color as colorutil import socket -REQUIREMENTS = ['pytuya>=8.0.0'] - CONF_DEVICE_ID = 'device_id' CONF_LOCAL_KEY = 'local_key' CONF_PROTOCOL_VERSION = 'protocol_version' @@ -62,7 +60,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): from . import pytuya lights = [] - pytuyadevice = pytuya.PytuyaDevice(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) + pytuyadevice = pytuya.TuyaDevice(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) pytuyadevice.set_version(float(config.get(CONF_PROTOCOL_VERSION))) bulb_device = TuyaCache(pytuyadevice) @@ -79,7 +77,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(lights) class TuyaCache: - """Cache wrapper for pytuya.PytuyaDevices""" + """Cache wrapper for pytuya.TuyaDevices""" def __init__(self, device): """Initialize the cache.""" @@ -105,13 +103,13 @@ class TuyaCache: log.warn( "Failed to get status after {} tries".format(UPDATE_RETRY_LIMIT)) - def set_dps(self, state, switchid): + def set_dps(self, state, dps_index): """Change the Tuya switch status and clear the cache.""" self._cached_status = '' self._cached_status_time = 0 for _ in range(UPDATE_RETRY_LIMIT): try: - return self._device.set_dps(state, switchid) + return self._device.set_dps(state, dps_index) except ConnectionError: pass except socket.timeout: @@ -135,75 +133,16 @@ class TuyaCache: def cached_status(self): return self._cached_status - def support_color(self): - return self._device.support_color() - - def support_color_temp(self): - return self._device.support_color_temp() - - def brightness(self): - for _ in range(UPDATE_RETRY_LIMIT): - try: - return self._device.brightness() - except ConnectionError: - pass - except KeyError: - return "999" - except socket.timeout: - pass - log.warn( - "Failed to get brightness after {} tries".format(UPDATE_RETRY_LIMIT)) - - def color_temp(self): - for _ in range(UPDATE_RETRY_LIMIT): - try: - return self._device.colourtemp() - except ConnectionError: - pass - except KeyError: - return "999" - except socket.timeout: - pass - log.warn( - "Failed to get color temp after {} tries".format(UPDATE_RETRY_LIMIT)) - - def set_brightness(self, brightness): - for _ in range(UPDATE_RETRY_LIMIT): - try: - return self._device.set_brightness(brightness) - except ConnectionError: - pass - except KeyError: - pass - except socket.timeout: - pass - log.warn( - "Failed to set brightness after {} tries".format(UPDATE_RETRY_LIMIT)) - - def set_color_temp(self, color_temp): - for _ in range(UPDATE_RETRY_LIMIT): - try: - return self._device.set_colourtemp(color_temp) - except ConnectionError: - pass - except KeyError: - pass - except socket.timeout: - pass - log.warn( - "Failed to set color temp after {} tries".format(UPDATE_RETRY_LIMIT)) - - def state(self): + def state(self): self._device.state(); - def turn_on(self): - self._device.turn_on(); - - def turn_off(self): - self._device.turn_off(); - class LocaltuyaLight(LightEntity): """Representation of a Tuya switch.""" + DPS_INDEX_ON = '1' + DPS_INDEX_MODE = '2' + DPS_INDEX_BRIGHTNESS = '3' + DPS_INDEX_COLOURTEMP = '4' + DPS_INDEX_COLOUR = '5' def __init__(self, device, name, friendly_name, icon, bulbid): """Initialize the Tuya switch.""" @@ -301,7 +240,7 @@ class LocaltuyaLight(LightEntity): converted_brightness = int(kwargs[ATTR_BRIGHTNESS]) if converted_brightness <= 25: converted_brightness = 25 - self._device.set_brightness(converted_brightness) + self.set_brightness(converted_brightness) if ATTR_HS_COLOR in kwargs: raise ValueError(" TODO implement RGB from HS") if ATTR_COLOR_TEMP in kwargs: @@ -320,3 +259,86 @@ class LocaltuyaLight(LightEntity): supports = supports | SUPPORT_COLOR #supports = supports | SUPPORT_COLOR_TEMP return supports + + def support_color(self): + return supported_features() & SUPPORT_COLOR + + def support_color_temp(self): + return supported_features() & SUPPORT_COLOR_TEMP + + def color_temp(self): + for _ in range(UPDATE_RETRY_LIMIT): + try: + return self._state[self.DPS][self.DPS_INDEX_COLOURTEMP] + except ConnectionError: + pass + except KeyError: + return "999" + except socket.timeout: + pass + log.warn( + "Failed to get color temp after {} tries".format(UPDATE_RETRY_LIMIT)) + + def set_color_temp(self, color_temp): + for _ in range(UPDATE_RETRY_LIMIT): + try: + if not 0 <= colourtemp <= 255: + raise ValueError("The colour temperature needs to be between 0 and 255.") + + self._device.set_dps(colourtemp, self.DPS_INDEX_COLOURTEMP) + except ConnectionError: + pass + except KeyError: + pass + except socket.timeout: + pass + log.warn( + "Failed to set color temp after {} tries".format(UPDATE_RETRY_LIMIT)) + + def set_brightness(self, brightness): + for _ in range(UPDATE_RETRY_LIMIT): + try: + if not 25 <= brightness <= 255: + raise ValueError("The brightness needs to be between 25 and 255.") + + self._device.set_dps(brightness, self.DPS_INDEX_BRIGHTNESS) + except ConnectionError: + pass + except KeyError: + pass + except socket.timeout: + pass + log.warn( + "Failed to set brightness after {} tries".format(UPDATE_RETRY_LIMIT)) + + + @staticmethod + def _hexvalue_to_rgb(hexvalue): + """ + Converts the hexvalue used by tuya for colour representation into + an RGB value. + + Args: + hexvalue(string): The hex representation generated by BulbDevice._rgb_to_hexvalue() + """ + r = int(hexvalue[0:2], 16) + g = int(hexvalue[2:4], 16) + b = int(hexvalue[4:6], 16) + + return (r, g, b) + + @staticmethod + def _hexvalue_to_hsv(hexvalue): + """ + Converts the hexvalue used by tuya for colour representation into + an HSV value. + + Args: + hexvalue(string): The hex representation generated by BulbDevice._rgb_to_hexvalue() + """ + h = int(hexvalue[7:10], 16) / 360 + s = int(hexvalue[10:12], 16) / 255 + v = int(hexvalue[12:14], 16) / 255 + + return (h, s, v) + diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 1fd57a3..2e67559 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -11,7 +11,7 @@ For more information see https://github.com/clach04/python-tuya Classes - PytuyaDevice(dev_id, address, local_key=None) + TuyaDevice(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. @@ -20,8 +20,7 @@ json = status() # returns json payload set_version(version) # 3.1 [default] or 3.3 set_dpsUsed(dpsUsed) - set_dps(on, switch=1) # Set the of the device to 'on' or 'off' (bool) - set_value(index, value) # Set int value of any index. + set_dps(on, dps_index) # Set int value of any dps index. set_timer(num_secs): @@ -172,12 +171,7 @@ payload_dict = { } } -#class PytuyaDevice(XenonDevice): -# def __init__(self, dev_id, address, local_key=None, dev_type=None): -# super(PytuyaDevice, self).__init__(dev_id, address, local_key, dev_type) - -#class XenonDevice(object): -class PytuyaDevice(object): +class TuyaDevice(object): def __init__(self, dev_id, address, local_key=None, connection_timeout=10): """ Represents a Tuya device. @@ -365,20 +359,20 @@ class PytuyaDevice(object): return result - def set_dps(self, value, dpsIndex): + def set_dps(self, value, dps_index): """ - Set int value of any index. + Set value (may be any type: bool, int or string) of any dps index. Args: - dpsIndex(int): dps index to set + dps_index(int): dps index to set value: new value for the dps index """ # open device, send request, then close connection - if isinstance(dpsIndex, int): - dpsIndex = str(dpsIndex) # index and payload is a string + if isinstance(dps_index, int): + dps_index = str(dps_index) # index and payload is a string payload = self.generate_payload(SET, { - dpsIndex: value}) + dps_index: value}) data = self._send_receive(payload) log.debug('set_dps received data=%r', data) diff --git a/custom_components/localtuya/switch.py b/custom_components/localtuya/switch.py index dd7cf78..1df12e1 100644 --- a/custom_components/localtuya/switch.py +++ b/custom_components/localtuya/switch.py @@ -35,8 +35,6 @@ from threading import Lock _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pytuya>=8.0.0'] - CONF_DEVICE_ID = 'device_id' CONF_LOCAL_KEY = 'local_key' CONF_PROTOCOL_VERSION = 'protocol_version' @@ -84,7 +82,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = config.get(CONF_SWITCHES) switches = [] - pytuyadevice = pytuya.PytuyaDevice(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) + pytuyadevice = pytuya.TuyaDevice(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) pytuyadevice.set_version(float(config.get(CONF_PROTOCOL_VERSION))) if len(devices) > 0: @@ -145,7 +143,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(switches, True) class TuyaCache: - """Cache wrapper for pytuya.PytuyaDevice""" + """Cache wrapper for pytuya.TuyaDevice""" def __init__(self, device): """Initialize the cache.""" @@ -172,13 +170,13 @@ class TuyaCache: # return None raise ConnectionError("Failed to update status .") - def set_dps(self, state, switchid): + def set_dps(self, state, dps_index): """Change the Tuya switch status and clear the cache.""" self._cached_status = '' self._cached_status_time = 0 for i in range(5): try: - return self._device.set_dps(state, switchid) + return self._device.set_dps(state, dps_index) except Exception: print('Failed to set status of device [{}]'.format(self._device.address)) if i+1 == 3: From 27edcacde24edc1aa5184ee28d6a096929a444e4 Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Sun, 13 Sep 2020 14:59:24 +0200 Subject: [PATCH 08/11] Fixed missing merging conflicts --- custom_components/localtuya/cover.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/custom_components/localtuya/cover.py b/custom_components/localtuya/cover.py index a7045cf..cd2a845 100644 --- a/custom_components/localtuya/cover.py +++ b/custom_components/localtuya/cover.py @@ -76,13 +76,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # TODO: keeping for now but should be removed dps = {} -<<<<<<< HEAD dps[config.get(CONF_ID)]=None pytuyadevice.set_dpsUsed(dps) -======= - dps["101"] = None - dps["102"] = None ->>>>>>> edc9debf839a6553cc2ad26d754006454314e63a covers = [] for device_config in entities_to_setup: @@ -96,19 +91,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): device_config.get(CONF_CLOSE_CMD), device_config.get(CONF_STOP_CMD), ) -<<<<<<< HEAD - ) - print('Setup localtuya cover [{}] with device ID [{}] '.format(config.get(CONF_FRIENDLY_NAME), config.get(CONF_ID))) - _LOGGER.info("Setup localtuya cover %s with device ID=%s", config.get(CONF_FRIENDLY_NAME), config.get(CONF_ID) ) - _LOGGER.debug("Cover %s uses open_cmd=%s close_cmd=%s stop_cmd=%s", config.get(CONF_FRIENDLY_NAME), config.get(CONF_OPEN_CMD), config.get(CONF_CLOSE_CMD), config.get(CONF_STOP_CMD) ) -======= +# print('Setup localtuya cover [{}] with device ID [{}] '.format(config.get(CONF_FRIENDLY_NAME), config.get(CONF_ID))) +# _LOGGER.info("Setup localtuya cover %s with device ID=%s", config.get(CONF_FRIENDLY_NAME), config.get(CONF_ID) ) +# _LOGGER.debug("Cover %s uses open_cmd=%s close_cmd=%s stop_cmd=%s", config.get(CONF_FRIENDLY_NAME), config.get(CONF_OPEN_CMD), config.get(CONF_CLOSE_CMD), config.get(CONF_STOP_CMD) ) ) device.set_dpsUsed(dps) async_add_entities(covers, True) ->>>>>>> edc9debf839a6553cc2ad26d754006454314e63a - def setup_platform(hass, config, add_devices, discovery_info=None): """Set up of the Tuya cover.""" return import_from_yaml(hass, config, "cover") @@ -199,16 +189,11 @@ class TuyaDevice(CoverEntity): self._open_cmd = open_cmd self._close_cmd = close_cmd self._stop_cmd = stop_cmd -<<<<<<< HEAD - #_LOGGER.info("running def __init__ of TuyaDevice(CoverEntity) from cover.py 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) - print('Initialized tuya cover [{}] with switch status [{}] and state [{}]'.format(self._name, self._status, self._state)) -======= print( "Initialized tuya cover [{}] with switch status [{}] and state [{}]".format( self._name, self._status, self._state ) ) ->>>>>>> edc9debf839a6553cc2ad26d754006454314e63a @property def name(self): From 50cbde330b7edc90c23e88e4153cc33dda47fb93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Thu, 10 Sep 2020 12:07:59 +0200 Subject: [PATCH 09/11] Fix fan platform after merge --- custom_components/localtuya/fan.py | 117 +++++++++++++---------------- 1 file changed, 53 insertions(+), 64 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 75e6cf6..c8bb9cb 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -15,78 +15,68 @@ fan: """ import logging -import requests import voluptuous as vol -from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, - FanEntity, SUPPORT_SET_SPEED, - SUPPORT_OSCILLATE, SUPPORT_DIRECTION, PLATFORM_SCHEMA) - -from homeassistant.const import STATE_OFF -"""from . import DATA_TUYA, TuyaDevice""" -from homeassistant.const import (CONF_HOST, CONF_ID, CONF_FRIENDLY_NAME, CONF_ICON, CONF_NAME) +from homeassistant.components.fan import ( + SPEED_LOW, + SPEED_MEDIUM, + SPEED_HIGH, + FanEntity, + SUPPORT_SET_SPEED, + SUPPORT_OSCILLATE, + SUPPORT_DIRECTION, + PLATFORM_SCHEMA, +) +from homeassistant.const import CONF_ID, CONF_FRIENDLY_NAME, STATE_OFF import homeassistant.helpers.config_validation as cv +from . import BASE_PLATFORM_SCHEMA, prepare_setup_entities, import_from_yaml +from .pytuya import FanDevice + _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'localtuyafan' - -REQUIREMENTS = ['pytuya>=7.1.0'] - -CONF_DEVICE_ID = 'device_id' -CONF_LOCAL_KEY = 'local_key' -CONF_PROTOCOL_VERSION = 'protocol_version' - -DEFAULT_ID = '1' -DEFAULT_PROTOCOL_VERSION = 3.3 +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(BASE_PLATFORM_SCHEMA) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ICON): cv.icon, - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Required(CONF_LOCAL_KEY): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_FRIENDLY_NAME): cv.string, - vol.Required(CONF_PROTOCOL_VERSION, default=DEFAULT_PROTOCOL_VERSION): vol.Coerce(float), - vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, -}) +def flow_schema(dps): + """Return schema used in config flow.""" + return {} -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up of the Tuya switch.""" - from . import pytuya +async def async_setup_entry(hass, config_entry, async_add_entities): + """Setup a Tuya fan based on a config entry.""" + device, entities_to_setup = prepare_setup_entities(config_entry, "fan", FanDevice) + if not entities_to_setup: + return + fans = [] - localtuyadevice = pytuya.FanDevice(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) - localtuyadevice.set_version(float(config.get(CONF_PROTOCOL_VERSION))) - _LOGGER.debug("localtuya fan: setup_platform: %s", localtuyadevice) - - fan_device = localtuyadevice - fans.append( + for device_config in entities_to_setup: + fans.append( TuyaDevice( - fan_device, - config.get(CONF_NAME), - config.get(CONF_FRIENDLY_NAME), - config.get(CONF_ICON), - config.get(CONF_ID), + device, + device_config[CONF_FRIENDLY_NAME], + device_config[CONF_ID], ) - ) - _LOGGER.info("Setup localtuya fan %s with device ID %s ", config.get(CONF_FRIENDLY_NAME), config.get(CONF_DEVICE_ID) ) + ) + + async_add_entities(fans, True) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up of the Tuya fan.""" + return import_from_yaml(hass, config, "fan") - add_entities(fans, True) class TuyaDevice(FanEntity): """A demonstration fan component.""" - # def __init__(self, hass, name: str, supported_features: int) -> None: - def __init__(self, device, name, friendly_name, icon, switchid): + def __init__(self, device, friendly_name, switchid): """Initialize the entity.""" self._device = device self._name = friendly_name self._available = False self._friendly_name = friendly_name - self._icon = icon self._switch_id = switchid self._status = self._device.status() self._state = False @@ -97,7 +87,7 @@ class TuyaDevice(FanEntity): @property def oscillating(self): """Return current oscillating status.""" - #if self._speed == STATE_OFF: + # if self._speed == STATE_OFF: # return False _LOGGER.debug("localtuya fan: oscillating = %s", self._oscillating) return self._oscillating @@ -137,7 +127,7 @@ class TuyaDevice(FanEntity): def turn_off(self, **kwargs) -> None: """Turn off the entity.""" _LOGGER.debug("localtuya fan: turn_off") - self._device.set_status(False, '1') + self._device.set_status(False, "1") self._state = False self.schedule_update_ha_state() @@ -147,14 +137,14 @@ class TuyaDevice(FanEntity): self._speed = speed # self.schedule_update_ha_state() if speed == STATE_OFF: - self._device.set_status(False, '1') + self._device.set_status(False, "1") self._state = False elif speed == SPEED_LOW: - self._device.set_value('2', '1') + self._device.set_value("2", "1") elif speed == SPEED_MEDIUM: - self._device.set_value('2', '2') + self._device.set_value("2", "2") elif speed == SPEED_HIGH: - self._device.set_value('2', '3') + self._device.set_value("2", "3") self.schedule_update_ha_state() # def set_direction(self, direction: str) -> None: @@ -165,7 +155,7 @@ class TuyaDevice(FanEntity): def oscillate(self, oscillating: bool) -> None: """Set oscillation.""" self._oscillating = oscillating - self._device.set_value('8', oscillating) + self._device.set_value("8", oscillating) self.schedule_update_ha_state() # @property @@ -176,8 +166,7 @@ class TuyaDevice(FanEntity): @property def unique_id(self): """Return unique device identifier.""" - _LOGGER.debug("localtuya fan unique_id = %s", self._device) - return self._device.id + return f"local_{self._device.id}_{self._switch_id}" @property def available(self): @@ -197,20 +186,20 @@ class TuyaDevice(FanEntity): try: status = self._device.status() _LOGGER.debug("localtuya fan: status = %s", status) - self._state = status['dps']['1'] - if status['dps']['1'] == False: + self._state = status["dps"]["1"] + if status["dps"]["1"] == False: self._speed = STATE_OFF - elif int(status['dps']['2']) == 1: + elif int(status["dps"]["2"]) == 1: self._speed = SPEED_LOW - elif int(status['dps']['2']) == 2: + elif int(status["dps"]["2"]) == 2: self._speed = SPEED_MEDIUM - elif int(status['dps']['2']) == 3: + elif int(status["dps"]["2"]) == 3: self._speed = SPEED_HIGH # self._speed = status['dps']['2'] - self._oscillating = status['dps']['8'] + self._oscillating = status["dps"]["8"] success = True except ConnectionError: - if i+1 == 3: + if i + 1 == 3: success = False raise ConnectionError("Failed to update status.") self._available = success From 0e94b681db696a2deb5e376b7535ea802808c5b9 Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Mon, 14 Sep 2020 20:44:55 +0200 Subject: [PATCH 10/11] Fixed import from YAML in config_flow.py --- custom_components/localtuya/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py index eca5e1b..737153c 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -178,7 +178,7 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return converted await self.async_set_unique_id(user_input[CONF_DEVICE_ID]) - self._set_platform(user_input[CONF_PLATFORM], []) + self._set_platform(user_input[CONF_PLATFORM]) if len(user_input.get(CONF_SWITCHES, [])) > 0: for switch_conf in user_input[CONF_SWITCHES].values(): From 285bffdc680baa24651104ba35492c471932fbe1 Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Mon, 14 Sep 2020 20:57:50 +0200 Subject: [PATCH 11/11] Fixed async_setup_entry in cover.py --- custom_components/localtuya/cover.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/custom_components/localtuya/cover.py b/custom_components/localtuya/cover.py index cd2a845..abeb170 100644 --- a/custom_components/localtuya/cover.py +++ b/custom_components/localtuya/cover.py @@ -76,8 +76,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # TODO: keeping for now but should be removed dps = {} - dps[config.get(CONF_ID)]=None - pytuyadevice.set_dpsUsed(dps) covers = [] for device_config in entities_to_setup: