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] 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: