diff --git a/custom_components/localtuya/cover.py b/custom_components/localtuya/cover.py index 313f7d3..6e9acd5 100644 --- a/custom_components/localtuya/cover.py +++ b/custom_components/localtuya/cover.py @@ -3,16 +3,17 @@ Simple platform to control LOCALLY Tuya cover devices. Sample config yaml -cover: +switch: - platform: localtuya host: 192.168.0.123 local_key: 1234567891234567 device_id: 123456789123456789abcd - name: Cover guests + name: cover_guests + friendly_name: Cover guests protocol_version: 3.3 id: 1 -""" +""" import logging import requests @@ -27,7 +28,7 @@ from homeassistant.components.cover import ( ) """from . import DATA_TUYA, TuyaDevice""" -"""from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA""" +from homeassistant.components.cover import ENTITY_ID_FORMAT, CoverDevice, PLATFORM_SCHEMA from homeassistant.const import (CONF_HOST, CONF_ID, CONF_FRIENDLY_NAME, CONF_ICON, CONF_NAME) import homeassistant.helpers.config_validation as cv from time import time, sleep @@ -52,6 +53,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 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, }) @@ -72,11 +74,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): TuyaDevice( cover_device, config.get(CONF_NAME), + config.get(CONF_FRIENDLY_NAME), config.get(CONF_ICON), config.get(CONF_ID), ) ) - print('Setup localtuya cover [{}] with device ID [{}] '.format(config.get(CONF_NAME), config.get(CONF_ID))) + print('Setup localtuya cover [{}] with device ID [{}] '.format(config.get(CONF_FRIENDLY_NAME), config.get(CONF_ID))) add_entities(covers) @@ -127,12 +130,13 @@ class TuyaCoverCache: class TuyaDevice(CoverDevice): """Tuya cover devices.""" - def __init__(self, device, name, icon, switchid): + def __init__(self, device, name, friendly_name, icon, switchid): self._device = device - self._name = name + 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.entity_id = ENTITY_ID_FORMAT.format(_device.object_id()) self._status = self._device.status() self._state = self._status['dps'][self._switch_id] print('Initialized tuya cover [{}] with switch status [{}] and state [{}]'.format(self._name, self._status, self._state)) diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 0ad7fdd..2430f24 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -1,9 +1,10 @@ -# Python module to interface locally with Tuya WiFi smart devices (switches, lights and covers) +# Python module to interface with Shenzhen Xenon ESP8266MOD WiFi smart devices +# E.g. https://wikidevi.com/wiki/Xenon_SM-PW701U +# SKYROKU SM-PW701U Wi-Fi Plug Smart Plug +# Wuudi SM-S0301-US - WIFI Smart Power Socket Multi Plug with 4 AC Outlets and 4 USB Charging Works with Alexa # -# Developed by merging the code from: +# This would not exist without the protocol reverse engineering from # https://github.com/codetheweb/tuyapi by codetheweb and blackrozes -# https://github.com/mileperhour/localtuya-homeassistant -# https://github.com/NameLessJedi/localtuya-homeassistant # # Tested with Python 2.7 and Python 3.6.1 only @@ -119,8 +120,9 @@ def hex2bin(x): return bytes.fromhex(x) # This is intended to match requests.json payload at https://github.com/codetheweb/tuyapi +# device20 or device22 are to be used depending on the length of dev_id (20 or 22 chars) payload_dict = { - "device": { + "device20": { "status": { "hexByte": "0a", "command": {"gwId": "", "devId": ""} @@ -132,7 +134,7 @@ payload_dict = { "prefix": "000055aa00000000000000", # Next byte is command byte ("hexByte") some zero padding, then length of remaining payload, i.e. command + suffix (unclear if multiple bytes used for length, zero padding implies could be more than one byte) "suffix": "000000000000aa55" }, - "cover": { + "device22": { "status": { "hexByte": "0d", "command": {"devId": "", "uid": "", "t": ""} @@ -175,7 +177,7 @@ class XenonDevice(object): def __repr__(self): return '%r' % ((self.id, self.address),) # FIXME can do better than this - def _send_receive(self, payload, times=1): + def _send_receive(self, payload): """ Send single buffer `payload` and receive a single buffer. @@ -188,11 +190,12 @@ class XenonDevice(object): s.connect((self.address, self.port)) s.send(payload) data = s.recv(1024) - #print("FIRST: Received %d bytes" % len(data) ) - if times > 1: +# print("FIRST: Received %d bytes" % len(data) ) + # sometimes the first packet does not contain data (typically 28 bytes): need to read again + if len(data) < 40: time.sleep(0.1) data = s.recv(1024) - #print("SECOND: Received %d bytes" % len(data) ) +# print("SECOND: Received %d bytes" % len(data) ) s.close() return data @@ -224,7 +227,7 @@ class XenonDevice(object): if data is not None: json_data['dps'] = data if command_hb == '0d': - json_data['dps'] = {"1": None} + json_data['dps'] = {"1": None,"101": None,"102": None} # Create byte buffer from hex data json_payload = json.dumps(json_data) @@ -294,19 +297,15 @@ class Device(XenonDevice): super(Device, self).__init__(dev_id, address, local_key, dev_type) def status(self): - if self.dev_type == 'cover': - recv_times = 2 - else: - recv_times = 1 - log.debug('status() entry (dev_type is %s, recv_times = %d)', self.dev_type, recv_times) + log.debug('status() entry (dev_type is %s)', self.dev_type) # open device, send request, then close connection payload = self.generate_payload('status') - data = self._send_receive(payload, recv_times) + data = self._send_receive(payload) log.debug('status received data=%r', data) result = data[20:-8] # hard coded offsets - if self.dev_type == 'cover': + if self.dev_type != 'device20': result = result[15:] log.debug('result=%r', result) @@ -414,7 +413,10 @@ class Device(XenonDevice): class OutletDevice(Device): def __init__(self, dev_id, address, local_key=None): - dev_type = 'device' + if len(dev_id) == 22: + dev_type = 'device22' + else: + dev_type = 'device20' super(OutletDevice, self).__init__(dev_id, address, local_key, dev_type) @@ -422,15 +424,13 @@ class CoverDevice(Device): DPS_INDEX_MOVE = '1' DPS_INDEX_BL = '101' - DPS = 'dps' - DPS_2_STATE = { '1':'movement', '101':'backlight', } def __init__(self, dev_id, address, local_key=None): - dev_type = 'cover' + dev_type = 'device22' print('%s version %s' % ( __name__, version)) print('Python %s on %s' % (sys.version, sys.platform)) if Crypto is None: @@ -474,7 +474,7 @@ class BulbDevice(Device): } def __init__(self, dev_id, address, local_key=None): - dev_type = 'device' + dev_type = 'device20' super(BulbDevice, self).__init__(dev_id, address, local_key, dev_type) @staticmethod diff --git a/custom_components/localtuya/switch.py b/custom_components/localtuya/switch.py index 76e2be0..3586782 100644 --- a/custom_components/localtuya/switch.py +++ b/custom_components/localtuya/switch.py @@ -9,10 +9,23 @@ switch: local_key: 1234567891234567 device_id: 12345678912345671234 name: tuya_01 + friendly_name: tuya_01 protocol_version: 3.3 + switches: + sw01: + name: main_plug + friendly_name: Main Plug + id: 1 + current: 18 + current_consumption: 19 + voltage: 20 + sw02: + name: usb_plug + friendly_name: USB Plug + id: 7 """ import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchDevice, PLATFORM_SCHEMA from homeassistant.const import (CONF_HOST, CONF_ID, CONF_SWITCHES, CONF_FRIENDLY_NAME, CONF_ICON, CONF_NAME) import homeassistant.helpers.config_validation as cv from time import time, sleep @@ -34,17 +47,29 @@ ATTR_CURRENT = 'current' ATTR_CURRENT_CONSUMPTION = 'current_consumption' ATTR_VOLTAGE = 'voltage' +SWITCH_SCHEMA = vol.Schema({ + vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_FRIENDLY_NAME): 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, +}) + 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_CURRENT, default='4'): cv.string, - vol.Optional(CONF_CURRENT_CONSUMPTION, default='5'): cv.string, - vol.Optional(CONF_VOLTAGE, default='6'): 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, + vol.Optional(CONF_SWITCHES, default={}): + vol.Schema({cv.slug: SWITCH_SCHEMA}), }) @@ -52,23 +77,43 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up of the Tuya switch.""" from . import pytuya + devices = config.get(CONF_SWITCHES) + switches = [] pytuyadevice = pytuya.OutletDevice(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) pytuyadevice.set_version(float(config.get(CONF_PROTOCOL_VERSION))) - outlet_device = TuyaCache(pytuyadevice) - switches.append( - TuyaDevice( - outlet_device, - config.get(CONF_NAME), - config.get(CONF_ICON), - config.get(CONF_ID), - config.get(CONF_CURRENT), - config.get(CONF_CURRENT_CONSUMPTION), - config.get(CONF_VOLTAGE) + if len(devices) > 0: + for object_id, device_config in devices.items(): + outlet_device = TuyaCache(pytuyadevice) + switches.append( + TuyaDevice( + outlet_device, + device_config.get(CONF_NAME), + device_config.get(CONF_FRIENDLY_NAME, object_id), + device_config.get(CONF_ICON), + device_config.get(CONF_ID), + device_config.get(CONF_CURRENT), + device_config.get(CONF_CURRENT_CONSUMPTION), + device_config.get(CONF_VOLTAGE) + ) ) - ) - #print('Setup localtuya switch [{}] with device ID [{}] '.format(config.get(CONF_NAME), config.get(CONF_ID))) + print('Setup localtuya subswitch [{}] with device ID [{}] '.format(device_config.get(CONF_FRIENDLY_NAME, object_id), device_config.get(CONF_ID))) + else: + outlet_device = TuyaCache(pytuyadevice) + switches.append( + TuyaDevice( + outlet_device, + config.get(CONF_NAME), + config.get(CONF_FRIENDLY_NAME), + config.get(CONF_ICON), + config.get(CONF_ID), + config.get(CONF_CURRENT), + config.get(CONF_CURRENT_CONSUMPTION), + config.get(CONF_VOLTAGE) + ) + ) + print('Setup localtuya switch [{}] with device ID [{}] '.format(config.get(CONF_FRIENDLY_NAME), config.get(CONF_ID))) add_devices(switches) @@ -107,7 +152,7 @@ class TuyaCache: self._lock.acquire() try: now = time() - if not self._cached_status or now - self._cached_status_time > 30: + if not self._cached_status or now - self._cached_status_time > 15: sleep(0.5) self._cached_status = self.__get_status() self._cached_status_time = time() @@ -118,10 +163,11 @@ class TuyaCache: class TuyaDevice(SwitchDevice): """Representation of a Tuya switch.""" - def __init__(self, device, name, icon, switchid, attr_current, attr_consumption, attr_voltage): + def __init__(self, device, name, friendly_name, icon, switchid, attr_current, attr_consumption, attr_voltage): """Initialize the Tuya switch.""" self._device = device - self._name = name + self.entity_id = ENTITY_ID_FORMAT.format(name) + self._name = friendly_name self._icon = icon self._switch_id = switchid self._attr_current = attr_current @@ -136,6 +182,7 @@ class TuyaDevice(SwitchDevice): """Get name of Tuya switch.""" return self._name + @property def is_on(self): """Check if Tuya switch is on.""" @@ -146,11 +193,11 @@ class TuyaDevice(SwitchDevice): attrs = {} try: attrs[ATTR_CURRENT] = "{}".format(self._status['dps'][self._attr_current]) - #print('attrs[ATTR_CURRENT]: [{}]'.format(attrs[ATTR_CURRENT])) attrs[ATTR_CURRENT_CONSUMPTION] = "{}".format(self._status['dps'][self._attr_consumption]/10) - #print('attrs[ATTR_CURRENT_CONSUMPTION]: [{}]'.format(attrs[ATTR_CURRENT_CONSUMPTION])) attrs[ATTR_VOLTAGE] = "{}".format(self._status['dps'][self._attr_voltage]/10) - #print('attrs[ATTR_VOLTAGE]: [{}]'.format(attrs[ATTR_VOLTAGE])) +# print('attrs[ATTR_CURRENT]: [{}]'.format(attrs[ATTR_CURRENT])) +# print('attrs[ATTR_CURRENT_CONSUMPTION]: [{}]'.format(attrs[ATTR_CURRENT_CONSUMPTION])) +# print('attrs[ATTR_VOLTAGE]: [{}]'.format(attrs[ATTR_VOLTAGE])) except KeyError: pass