From 2e6d313d0ee19144e754cac4e9d2f25ea9687d98 Mon Sep 17 00:00:00 2001 From: fancygaphtrn Date: Tue, 30 Jun 2020 07:22:33 -0400 Subject: [PATCH 1/4] Fan support. My fan is a technical pro pedestal fan. supports on/off, set_speed off/low/medium/high and oscillate on/off --- custom_components/localtuya/fan.py | 203 +++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 custom_components/localtuya/fan.py diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py new file mode 100644 index 0000000..ab57887 --- /dev/null +++ b/custom_components/localtuya/fan.py @@ -0,0 +1,203 @@ +""" +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 (ENTITY_ID_FORMAT, 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.7'] + +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))) + + 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_ID) ) + + add_entities(fans) + +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.entity_id = ENTITY_ID_FORMAT.format(name) + self._name = friendly_name + 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 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.") From 87a0ac236397cc048e11e52a114f8a651f6feaae Mon Sep 17 00:00:00 2001 From: fancygaphtrn Date: Tue, 30 Jun 2020 07:23:14 -0400 Subject: [PATCH 2/4] Fan support --- custom_components/localtuya/pytuya/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 8e2a01c..46064e0 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -367,6 +367,7 @@ class Device(XenonDevice): on(bool): True for 'on', False for 'off'. switch(int): The switch to set """ + log.debug("set_status: %s", on) # open device, send request, then close connection if isinstance(switch, int): switch = str(switch) # index and payload is a string @@ -386,6 +387,7 @@ class Device(XenonDevice): index(int): index to set value(int): new value for the index """ + log.debug("set_value: index=%s value=%s", index, value) # open device, send request, then close connection if isinstance(index, int): index = str(index) # index and payload is a string @@ -435,7 +437,16 @@ class OutletDevice(Device): dev_type = 'device20' super(OutletDevice, self).__init__(dev_id, address, local_key, dev_type) +class FanDevice(Device): + DPS_INDEX_SPEED = '2' + def __init__(self, dev_id, address, local_key=None): + if len(dev_id) == 22: + dev_type = 'device22' + else: + dev_type = 'device20' + super(FanDevice, self).__init__(dev_id, address, local_key, dev_type) + class CoverEntity(Device): DPS_INDEX_MOVE = '1' DPS_INDEX_BL = '101' From 8ae6a7179777146fe78d8d61ed4739c97bf61fee Mon Sep 17 00:00:00 2001 From: fancygaphtrn Date: Fri, 4 Sep 2020 10:01:07 -0400 Subject: [PATCH 3/4] Updated to be inline with upstream 7.0.9 --- .../localtuya/pytuya/__init__.py | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 46064e0..bd847d2 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -28,7 +28,7 @@ except ImportError: import pyaes # https://github.com/ricmoo/pyaes -version_tuple = (7, 0, 7) +version_tuple = (7, 0, 9) version = version_string = __version__ = '%d.%d.%d' % version_tuple __author__ = 'rospogrigio' @@ -149,7 +149,7 @@ payload_dict = { } class XenonDevice(object): - def __init__(self, dev_id, address, local_key=None, dev_type=None, connection_timeout=10): + def __init__(self, dev_id, address, local_key=None, connection_timeout=10): """ Represents a Tuya device. @@ -157,20 +157,21 @@ class XenonDevice(object): dev_id (str): The device id. address (str): The network address. local_key (str, optional): The encryption key. Defaults to None. - dev_type (str, optional): The device type. - It will be used as key for lookups in payload_dict. - Defaults to None. Attributes: port (int): The port to connect to. """ + self.id = dev_id self.address = address self.local_key = local_key self.local_key = local_key.encode('latin1') - self.dev_type = dev_type self.connection_timeout = connection_timeout self.version = 3.1 + if len(dev_id) == 22: + self.dev_type = 'device22' + else: + self.dev_type = 'device20' self.port = 6668 # default - do not expect caller to pass in @@ -218,6 +219,9 @@ class XenonDevice(object): def set_version(self, version): self.version = version + def set_dpsUsed(self, dpsUsed): + self.dpsUsed = dpsUsed + def generate_payload(self, command, data=None): """ Generate the payload to send. @@ -243,7 +247,8 @@ class XenonDevice(object): if data is not None: json_data['dps'] = data if command_hb == '0d': - json_data['dps'] = {"1": None,"101": None,"102": None} + json_data['dps'] = self.dpsUsed +# log.info('******** COMMAND IS %r', self.dpsUsed) # Create byte buffer from hex data json_payload = json.dumps(json_data) @@ -367,7 +372,6 @@ class Device(XenonDevice): on(bool): True for 'on', False for 'off'. switch(int): The switch to set """ - log.debug("set_status: %s", on) # open device, send request, then close connection if isinstance(switch, int): switch = str(switch) # index and payload is a string @@ -387,7 +391,6 @@ class Device(XenonDevice): index(int): index to set value(int): new value for the index """ - log.debug("set_value: index=%s value=%s", index, value) # open device, send request, then close connection if isinstance(index, int): index = str(index) # index and payload is a string @@ -431,22 +434,15 @@ class Device(XenonDevice): class OutletDevice(Device): def __init__(self, dev_id, address, local_key=None): - if len(dev_id) == 22: - dev_type = 'device22' - else: - dev_type = 'device20' - super(OutletDevice, self).__init__(dev_id, address, local_key, dev_type) + super(OutletDevice, self).__init__(dev_id, address, local_key) + class FanDevice(Device): DPS_INDEX_SPEED = '2' def __init__(self, dev_id, address, local_key=None): - if len(dev_id) == 22: - dev_type = 'device22' - else: - dev_type = 'device20' - super(FanDevice, self).__init__(dev_id, address, local_key, dev_type) - + super(FanDevice, self).__init__(dev_id, address, local_key) + class CoverEntity(Device): DPS_INDEX_MOVE = '1' DPS_INDEX_BL = '101' @@ -457,7 +453,6 @@ class CoverEntity(Device): } def __init__(self, dev_id, address, local_key=None): - dev_type = 'device22' print('%s version %s' % ( __name__, version)) print('Python %s on %s' % (sys.version, sys.platform)) if Crypto is None: @@ -466,7 +461,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, dev_type) + super(CoverEntity, self).__init__(dev_id, address, local_key) def open_cover(self, switch=1): """Turn the device on""" @@ -501,8 +496,7 @@ class BulbDevice(Device): } def __init__(self, dev_id, address, local_key=None): - dev_type = 'device20' - super(BulbDevice, self).__init__(dev_id, address, local_key, dev_type) + super(BulbDevice, self).__init__(dev_id, address, local_key) @staticmethod def _rgb_to_hexvalue(r, g, b): From 8eacf15762a541edf2d4b9dd20506bcec8cf5bad Mon Sep 17 00:00:00 2001 From: fancygaphtrn Date: Fri, 4 Sep 2020 10:02:54 -0400 Subject: [PATCH 4/4] Added unique_id and available in line with upstream --- custom_components/localtuya/fan.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index ab57887..3fbdf6c 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -19,7 +19,7 @@ import requests import voluptuous as vol -from homeassistant.components.fan import (ENTITY_ID_FORMAT, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, +from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, FanEntity, SUPPORT_SET_SPEED, SUPPORT_OSCILLATE, SUPPORT_DIRECTION, PLATFORM_SCHEMA) @@ -32,7 +32,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'localtuyafan' -REQUIREMENTS = ['pytuya==7.0.7'] +REQUIREMENTS = ['pytuya==7.0.9'] CONF_DEVICE_ID = 'device_id' CONF_LOCAL_KEY = 'local_key' @@ -60,6 +60,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): 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( @@ -71,9 +72,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): config.get(CONF_ID), ) ) - _LOGGER.info("Setup localtuya fan %s with device ID %s ", config.get(CONF_FRIENDLY_NAME), 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) + add_entities(fans, True) class TuyaDevice(FanEntity): """A demonstration fan component.""" @@ -82,8 +83,8 @@ class TuyaDevice(FanEntity): def __init__(self, device, name, friendly_name, icon, switchid): """Initialize the entity.""" self._device = device - self.entity_id = ENTITY_ID_FORMAT.format(name) self._name = friendly_name + self._available = False self._friendly_name = friendly_name self._icon = icon self._switch_id = switchid @@ -172,6 +173,17 @@ class TuyaDevice(FanEntity): # """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.""" @@ -201,3 +213,4 @@ class TuyaDevice(FanEntity): if i+1 == 3: success = False raise ConnectionError("Failed to update status.") + self._available = success