Introduced different support for switches with 20 and 22 bytes length for dev_id. Introduced name to set the entity_id and friendly_name for the frontend name.

This commit is contained in:
rospogrigio
2020-04-07 17:27:50 +02:00
parent 9b78f4a8e6
commit 6e82ab0632
3 changed files with 104 additions and 53 deletions

View File

@@ -3,16 +3,17 @@ Simple platform to control LOCALLY Tuya cover devices.
Sample config yaml Sample config yaml
cover: switch:
- platform: localtuya - platform: localtuya
host: 192.168.0.123 host: 192.168.0.123
local_key: 1234567891234567 local_key: 1234567891234567
device_id: 123456789123456789abcd device_id: 123456789123456789abcd
name: Cover guests name: cover_guests
friendly_name: Cover guests
protocol_version: 3.3 protocol_version: 3.3
id: 1 id: 1
"""
"""
import logging import logging
import requests import requests
@@ -27,7 +28,7 @@ from homeassistant.components.cover import (
) )
"""from . import DATA_TUYA, TuyaDevice""" """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) from homeassistant.const import (CONF_HOST, CONF_ID, CONF_FRIENDLY_NAME, CONF_ICON, CONF_NAME)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from time import time, sleep from time import time, sleep
@@ -52,6 +53,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICE_ID): cv.string, vol.Required(CONF_DEVICE_ID): cv.string,
vol.Required(CONF_LOCAL_KEY): cv.string, vol.Required(CONF_LOCAL_KEY): cv.string,
vol.Required(CONF_NAME): 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.Required(CONF_PROTOCOL_VERSION, default=DEFAULT_PROTOCOL_VERSION): vol.Coerce(float),
vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string,
}) })
@@ -72,11 +74,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
TuyaDevice( TuyaDevice(
cover_device, cover_device,
config.get(CONF_NAME), config.get(CONF_NAME),
config.get(CONF_FRIENDLY_NAME),
config.get(CONF_ICON), config.get(CONF_ICON),
config.get(CONF_ID), 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) add_entities(covers)
@@ -127,12 +130,13 @@ class TuyaCoverCache:
class TuyaDevice(CoverDevice): class TuyaDevice(CoverDevice):
"""Tuya cover devices.""" """Tuya cover devices."""
def __init__(self, device, name, icon, switchid): def __init__(self, device, name, friendly_name, icon, switchid):
self._device = device 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._icon = icon
self._switch_id = switchid self._switch_id = switchid
#self.entity_id = ENTITY_ID_FORMAT.format(_device.object_id())
self._status = self._device.status() self._status = self._device.status()
self._state = self._status['dps'][self._switch_id] self._state = self._status['dps'][self._switch_id]
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))

View File

@@ -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/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 # Tested with Python 2.7 and Python 3.6.1 only
@@ -119,8 +120,9 @@ def hex2bin(x):
return bytes.fromhex(x) return bytes.fromhex(x)
# This is intended to match requests.json payload at https://github.com/codetheweb/tuyapi # 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 = { payload_dict = {
"device": { "device20": {
"status": { "status": {
"hexByte": "0a", "hexByte": "0a",
"command": {"gwId": "", "devId": ""} "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) "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" "suffix": "000000000000aa55"
}, },
"cover": { "device22": {
"status": { "status": {
"hexByte": "0d", "hexByte": "0d",
"command": {"devId": "", "uid": "", "t": ""} "command": {"devId": "", "uid": "", "t": ""}
@@ -175,7 +177,7 @@ class XenonDevice(object):
def __repr__(self): def __repr__(self):
return '%r' % ((self.id, self.address),) # FIXME can do better than this 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. Send single buffer `payload` and receive a single buffer.
@@ -188,11 +190,12 @@ class XenonDevice(object):
s.connect((self.address, self.port)) s.connect((self.address, self.port))
s.send(payload) s.send(payload)
data = s.recv(1024) data = s.recv(1024)
#print("FIRST: Received %d bytes" % len(data) ) # print("FIRST: Received %d bytes" % len(data) )
if times > 1: # sometimes the first packet does not contain data (typically 28 bytes): need to read again
if len(data) < 40:
time.sleep(0.1) time.sleep(0.1)
data = s.recv(1024) data = s.recv(1024)
#print("SECOND: Received %d bytes" % len(data) ) # print("SECOND: Received %d bytes" % len(data) )
s.close() s.close()
return data return data
@@ -224,7 +227,7 @@ class XenonDevice(object):
if data is not None: if data is not None:
json_data['dps'] = data json_data['dps'] = data
if command_hb == '0d': if command_hb == '0d':
json_data['dps'] = {"1": None} json_data['dps'] = {"1": None,"101": None,"102": None}
# Create byte buffer from hex data # Create byte buffer from hex data
json_payload = json.dumps(json_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) super(Device, self).__init__(dev_id, address, local_key, dev_type)
def status(self): def status(self):
if self.dev_type == 'cover': log.debug('status() entry (dev_type is %s)', self.dev_type)
recv_times = 2
else:
recv_times = 1
log.debug('status() entry (dev_type is %s, recv_times = %d)', self.dev_type, recv_times)
# open device, send request, then close connection # open device, send request, then close connection
payload = self.generate_payload('status') payload = self.generate_payload('status')
data = self._send_receive(payload, recv_times) data = self._send_receive(payload)
log.debug('status received data=%r', data) log.debug('status received data=%r', data)
result = data[20:-8] # hard coded offsets result = data[20:-8] # hard coded offsets
if self.dev_type == 'cover': if self.dev_type != 'device20':
result = result[15:] result = result[15:]
log.debug('result=%r', result) log.debug('result=%r', result)
@@ -414,7 +413,10 @@ class Device(XenonDevice):
class OutletDevice(Device): class OutletDevice(Device):
def __init__(self, dev_id, address, local_key=None): 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) super(OutletDevice, self).__init__(dev_id, address, local_key, dev_type)
@@ -422,15 +424,13 @@ class CoverDevice(Device):
DPS_INDEX_MOVE = '1' DPS_INDEX_MOVE = '1'
DPS_INDEX_BL = '101' DPS_INDEX_BL = '101'
DPS = 'dps'
DPS_2_STATE = { DPS_2_STATE = {
'1':'movement', '1':'movement',
'101':'backlight', '101':'backlight',
} }
def __init__(self, dev_id, address, local_key=None): def __init__(self, dev_id, address, local_key=None):
dev_type = 'cover' dev_type = 'device22'
print('%s version %s' % ( __name__, version)) print('%s version %s' % ( __name__, version))
print('Python %s on %s' % (sys.version, sys.platform)) print('Python %s on %s' % (sys.version, sys.platform))
if Crypto is None: if Crypto is None:
@@ -474,7 +474,7 @@ class BulbDevice(Device):
} }
def __init__(self, dev_id, address, local_key=None): 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) super(BulbDevice, self).__init__(dev_id, address, local_key, dev_type)
@staticmethod @staticmethod

View File

@@ -9,10 +9,23 @@ switch:
local_key: 1234567891234567 local_key: 1234567891234567
device_id: 12345678912345671234 device_id: 12345678912345671234
name: tuya_01 name: tuya_01
friendly_name: tuya_01
protocol_version: 3.3 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 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) 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 homeassistant.helpers.config_validation as cv
from time import time, sleep from time import time, sleep
@@ -34,17 +47,29 @@ ATTR_CURRENT = 'current'
ATTR_CURRENT_CONSUMPTION = 'current_consumption' ATTR_CURRENT_CONSUMPTION = 'current_consumption'
ATTR_VOLTAGE = 'voltage' 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({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_ICON): cv.icon,
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_DEVICE_ID): cv.string, vol.Required(CONF_DEVICE_ID): cv.string,
vol.Required(CONF_LOCAL_KEY): cv.string, vol.Required(CONF_LOCAL_KEY): cv.string,
vol.Required(CONF_NAME): 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.Required(CONF_PROTOCOL_VERSION, default=DEFAULT_PROTOCOL_VERSION): vol.Coerce(float),
vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string,
vol.Optional(CONF_CURRENT, default='4'): cv.string, vol.Optional(CONF_CURRENT, default='-1'): cv.string,
vol.Optional(CONF_CURRENT_CONSUMPTION, default='5'): cv.string, vol.Optional(CONF_CURRENT_CONSUMPTION, default='-1'): cv.string,
vol.Optional(CONF_VOLTAGE, default='6'): 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.""" """Set up of the Tuya switch."""
from . import pytuya from . import pytuya
devices = config.get(CONF_SWITCHES)
switches = [] switches = []
pytuyadevice = pytuya.OutletDevice(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) 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))) pytuyadevice.set_version(float(config.get(CONF_PROTOCOL_VERSION)))
outlet_device = TuyaCache(pytuyadevice) if len(devices) > 0:
switches.append( for object_id, device_config in devices.items():
TuyaDevice( outlet_device = TuyaCache(pytuyadevice)
outlet_device, switches.append(
config.get(CONF_NAME), TuyaDevice(
config.get(CONF_ICON), outlet_device,
config.get(CONF_ID), device_config.get(CONF_NAME),
config.get(CONF_CURRENT), device_config.get(CONF_FRIENDLY_NAME, object_id),
config.get(CONF_CURRENT_CONSUMPTION), device_config.get(CONF_ICON),
config.get(CONF_VOLTAGE) device_config.get(CONF_ID),
device_config.get(CONF_CURRENT),
device_config.get(CONF_CURRENT_CONSUMPTION),
device_config.get(CONF_VOLTAGE)
)
) )
) print('Setup localtuya subswitch [{}] with device ID [{}] '.format(device_config.get(CONF_FRIENDLY_NAME, object_id), device_config.get(CONF_ID)))
#print('Setup localtuya switch [{}] with device ID [{}] '.format(config.get(CONF_NAME), 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) add_devices(switches)
@@ -107,7 +152,7 @@ class TuyaCache:
self._lock.acquire() self._lock.acquire()
try: try:
now = time() 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) sleep(0.5)
self._cached_status = self.__get_status() self._cached_status = self.__get_status()
self._cached_status_time = time() self._cached_status_time = time()
@@ -118,10 +163,11 @@ class TuyaCache:
class TuyaDevice(SwitchDevice): class TuyaDevice(SwitchDevice):
"""Representation of a Tuya switch.""" """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.""" """Initialize the Tuya switch."""
self._device = device self._device = device
self._name = name self.entity_id = ENTITY_ID_FORMAT.format(name)
self._name = friendly_name
self._icon = icon self._icon = icon
self._switch_id = switchid self._switch_id = switchid
self._attr_current = attr_current self._attr_current = attr_current
@@ -136,6 +182,7 @@ class TuyaDevice(SwitchDevice):
"""Get name of Tuya switch.""" """Get name of Tuya switch."""
return self._name return self._name
@property @property
def is_on(self): def is_on(self):
"""Check if Tuya switch is on.""" """Check if Tuya switch is on."""
@@ -146,11 +193,11 @@ class TuyaDevice(SwitchDevice):
attrs = {} attrs = {}
try: try:
attrs[ATTR_CURRENT] = "{}".format(self._status['dps'][self._attr_current]) 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) 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) 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: except KeyError:
pass pass