Merge branch 'master' into pytuya_refactoring

This commit is contained in:
rospogrigio
2020-09-14 23:32:55 +02:00
13 changed files with 811 additions and 429 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*~
__pycache__

View File

@@ -112,22 +112,7 @@ Alternatively, you can install localtuya through HACS by adding this repository.
2020-09-04 02:08:26 DEBUG (SyncWorker_26) [custom_components.localtuya.pytuya] decrypted result='{"devId":"REDACTED","dps":{"1":"stop","2":100,"3":40,"5":false,"7":"closing","8":"cancel","9":0,"10":0}}' 2020-09-04 02:08:26 DEBUG (SyncWorker_26) [custom_components.localtuya.pytuya] decrypted result='{"devId":"REDACTED","dps":{"1":"stop","2":100,"3":40,"5":false,"7":"closing","8":"cancel","9":0,"10":0}}'
``` ```
5. Make any necessary edits to the python file(s) for your device(s). For example, if you are using a switch device, you may want to edit switch.py to account for the IDs/DPs that are applciable to your specific device. 5. Add any applicable sensors, using the below configuration.yaml entry as a guide:
```
#### switch.py snippet
@property
def device_state_attributes(self):
attrs = {}
try:
attrs[ATTR_CURRENT] = "{}".format(self._status['dps']['104']) # Modify to match your device's DPs
attrs[ATTR_CURRENT_CONSUMPTION] = "{}".format(self._status['dps']['105']/10) # Modify to match your device's DPs
attrs[ATTR_VOLTAGE] = "{}".format(self._status['dps']['106']/10) # Modify to match your device's DPs
except KeyError:
pass
return attrs
```
6. Add any applicable sensors, using the below configuration.yaml entry as a guide:
``` ```
sensor: sensor:
- platform: template - platform: template

View File

@@ -1 +1,88 @@
"""The Tuya local integration.""" """The LocalTuya integration integration."""
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.core import HomeAssistant
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 . 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.TuyaDevice(
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):
"""Set up the LocalTuya integration component."""
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up LocalTuya integration from a config entry."""
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
# Nothing is stored and no persistent connections exist, so nothing to do
return True

View File

@@ -0,0 +1,213 @@
"""Config flow for LocalTuya integration integration."""
import logging
from importlib import import_module
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
from homeassistant.const import (
CONF_ENTITIES,
CONF_ID,
CONF_HOST,
CONF_DEVICE_ID,
CONF_NAME,
CONF_FRIENDLY_NAME,
CONF_PLATFORM,
CONF_SWITCHES,
)
from . import pytuya
from .const import ( # pylint: disable=unused-import
CONF_LOCAL_KEY,
CONF_PROTOCOL_VERSION,
DOMAIN,
PLATFORMS,
)
_LOGGER = logging.getLogger(__name__)
PLATFORM_TO_ADD = "platform_to_add"
NO_ADDITIONAL_PLATFORMS = "no_additional_platforms"
USER_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): str,
vol.Required(CONF_HOST): str,
vol.Required(CONF_DEVICE_ID): str,
vol.Required(CONF_LOCAL_KEY): str,
vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]),
}
)
PICK_ENTITY_SCHEMA = vol.Schema(
{vol.Required(PLATFORM_TO_ADD, default=PLATFORMS[0]): vol.In(PLATFORMS)}
)
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."""
return vol.Schema(
{
vol.Required(CONF_ID): vol.In(dps_strings),
vol.Required(CONF_FRIENDLY_NAME): str,
}
).extend(schema)
def strip_dps_values(user_input, dps_strings):
"""Remove values and keep only index for DPS config items."""
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):
"""Validate the user input allows us to connect."""
pytuyadevice = pytuya.Device(
data[CONF_DEVICE_ID], data[CONF_HOST], data[CONF_LOCAL_KEY]
)
pytuyadevice.set_version(float(data[CONF_PROTOCOL_VERSION]))
pytuyadevice.set_dpsUsed({})
try:
data = await hass.async_add_executor_job(pytuyadevice.status)
except (ConnectionRefusedError, ConnectionResetError):
raise CannotConnect
except ValueError:
raise InvalidAuth
return dps_string_list(data["dps"])
class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for LocalTuya integration."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
def __init__(self):
"""Initialize a new LocaltuyaConfigFlow."""
self.basic_info = None
self.dps_strings = []
self.platform = None
self.platform_schema = None
self.entities = []
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
await self.async_set_unique_id(user_input[CONF_DEVICE_ID])
self._abort_if_unique_id_configured()
try:
self.basic_info = 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"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
return self.async_show_form(
step_id="user", data_schema=USER_SCHEMA, errors=errors
)
async def async_step_pick_entity_type(self, user_input=None):
"""Handle asking if user wants to add another entity."""
if user_input is not None:
if user_input.get(NO_ADDITIONAL_PLATFORMS):
config = {**self.basic_info, CONF_ENTITIES: self.entities}
return self.async_create_entry(title=config[CONF_NAME], data=config)
self._set_platform(user_input[PLATFORM_TO_ADD])
return await self.async_step_add_entity()
# Add a checkbox that allows bailing out from config flow iff at least one
# entity has been added
schema = PICK_ENTITY_SCHEMA
if self.platform is not None:
schema = schema.extend(
{vol.Required(NO_ADDITIONAL_PLATFORMS, default=True): bool}
)
return self.async_show_form(step_id="pick_entity_type", data_schema=schema)
async def async_step_add_entity(self, user_input=None):
"""Handle adding a new entity."""
errors = {}
if user_input is not None:
already_configured = any(
switch[CONF_ID] == user_input[CONF_ID] for switch in self.entities
)
if not already_configured:
user_input[CONF_PLATFORM] = self.platform
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_strings, self.platform_schema),
errors=errors,
description_placeholders={"platform": self.platform},
)
async def async_step_import(self, user_input):
"""Handle import from YAML."""
def _convert_entity(conf):
converted = {
CONF_ID: conf[CONF_ID],
CONF_FRIENDLY_NAME: conf[CONF_FRIENDLY_NAME],
CONF_PLATFORM: self.platform,
}
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])
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:
self.entities.append(_convert_entity(user_input))
config = {
CONF_NAME: f"{user_input[CONF_DEVICE_ID]} (import from configuration.yaml)",
CONF_HOST: user_input[CONF_HOST],
CONF_DEVICE_ID: user_input[CONF_DEVICE_ID],
CONF_LOCAL_KEY: user_input[CONF_LOCAL_KEY],
CONF_PROTOCOL_VERSION: user_input[CONF_PROTOCOL_VERSION],
CONF_ENTITIES: self.entities,
}
self._abort_if_unique_id_configured(updates=config)
return self.async_create_entry(title=config[CONF_NAME], data=config)
def _set_platform(self, platform):
integration_module = ".".join(__name__.split(".")[:-1])
self.platform = platform
self.platform_schema = import_module(
"." + platform, integration_module
).flow_schema(self.dps_strings)
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""

View File

@@ -0,0 +1,23 @@
"""Constants for localtuya integration."""
ATTR_CURRENT = 'current'
ATTR_CURRENT_CONSUMPTION = 'current_consumption'
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 = ["cover", "fan", "light", "switch"]

View File

@@ -19,7 +19,8 @@ cover:
""" """
import logging import logging
import requests from time import time, sleep
from threading import Lock
import voluptuous as vol import voluptuous as vol
@@ -31,81 +32,77 @@ from homeassistant.components.cover import (
SUPPORT_STOP, SUPPORT_STOP,
SUPPORT_SET_POSITION, SUPPORT_SET_POSITION,
) )
from homeassistant.const import (
"""from . import DATA_TUYA, TuyaDevice""" CONF_ID,
from homeassistant.components.cover import CoverEntity, PLATFORM_SCHEMA CONF_FRIENDLY_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 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 TuyaDevice
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'localtuyacover' PLATFORM = "cover"
CONF_DEVICE_ID = 'device_id' DEFAULT_OPEN_CMD = "on"
CONF_LOCAL_KEY = 'local_key' DEFAULT_CLOSE_CMD = "off"
CONF_PROTOCOL_VERSION = 'protocol_version' DEFAULT_STOP_CMD = "stop"
CONF_OPEN_CMD = 'open_cmd'
CONF_CLOSE_CMD = 'close_cmd'
CONF_STOP_CMD = 'stop_cmd'
DEFAULT_ID = '1' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(BASE_PLATFORM_SCHEMA).extend(
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_OPEN_CMD, default=DEFAULT_OPEN_CMD): cv.string,
vol.Optional(CONF_CLOSE_CMD, default=DEFAULT_CLOSE_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, vol.Optional(CONF_STOP_CMD, default=DEFAULT_STOP_CMD): cv.string,
}) }
)
def setup_platform(hass, config, add_entities, discovery_info=None): def flow_schema(dps):
"""Set up Tuya cover devices.""" """Return schema used in config flow."""
from . import pytuya 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)) async def async_setup_entry(hass, config_entry, async_add_entities):
#_LOGGER.info("conf_close_cmd is %s", config.get(CONF_CLOSE_CMD)) """Setup a Tuya cover based on a config entry."""
#_LOGGER.info("conf_STOP_cmd is %s", config.get(CONF_STOP_CMD)) device, entities_to_setup = prepare_setup_entities(
config_entry, PLATFORM
)
if not entities_to_setup:
return
# TODO: keeping for now but should be removed
dps = {}
covers = [] covers = []
pytuyadevice = pytuya.TuyaDevice(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) for device_config in entities_to_setup:
pytuyadevice.set_version(float(config.get(CONF_PROTOCOL_VERSION))) dps[device_config[CONF_ID]] = None
dps = {}
dps[config.get(CONF_ID)]=None
pytuyadevice.set_dpsUsed(dps)
cover_device = TuyaCache(pytuyadevice)
covers.append( covers.append(
LocaltuyaCover( LocaltuyaCover(
cover_device, TuyaCache(device),
config.get(CONF_NAME), device_config[CONF_FRIENDLY_NAME],
config.get(CONF_FRIENDLY_NAME), device_config[CONF_ID],
config.get(CONF_ICON), device_config.get(CONF_OPEN_CMD),
config.get(CONF_ID), device_config.get(CONF_CLOSE_CMD),
config.get(CONF_OPEN_CMD), device_config.get(CONF_STOP_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) )
) )
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) )
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, PLATFORM)
class TuyaCache: class TuyaCache:
@@ -113,7 +110,7 @@ class TuyaCache:
def __init__(self, device): def __init__(self, device):
"""Initialize the cache.""" """Initialize the cache."""
self._cached_status = '' self._cached_status = ""
self._cached_status_time = 0 self._cached_status_time = 0
self._device = device self._device = device
self._lock = Lock() self._lock = Lock()
@@ -130,27 +127,38 @@ class TuyaCache:
status = self._device.status() status = self._device.status()
return status return status
except Exception: 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) sleep(1.0)
if i + 1 == 3: if i + 1 == 3:
_LOGGER.error("Failed to update status of device %s", self._device.address ) _LOGGER.error(
"Failed to update status of device %s", self._device.address
)
# return None # return None
raise ConnectionError("Failed to update status .") raise ConnectionError("Failed to update status .")
def set_dps(self, state, dps_index): def set_dps(self, state, dps_index):
#_LOGGER.info("running def set_dps from cover") #_LOGGER.info("running def set_dps from cover")
"""Change the Tuya switch status and clear the cache.""" """Change the Tuya switch status and clear the cache."""
self._cached_status = '' self._cached_status = ""
self._cached_status_time = 0 self._cached_status_time = 0
for i in range(5): for i in range(5):
try: try:
#_LOGGER.info("Running a try from def set_dps from cover where state=%s and dps_index=%s", state, dps_index) #_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) return self._device.set_dps(state, dps_index)
except Exception: except Exception:
print('Failed to set status of device [{}]'.format(self._device.address)) print(
"Failed to set status of device [{}]".format(self._device.address)
)
if i + 1 == 3: if i + 1 == 3:
_LOGGER.error("Failed to set status of device %s", self._device.address ) _LOGGER.error(
"Failed to set status of device %s", self._device.address
)
return return
# raise ConnectionError("Failed to set status.") # raise ConnectionError("Failed to set status.")
def status(self): def status(self):
@@ -170,21 +178,23 @@ class TuyaCache:
class LocaltuyaCover(CoverEntity): class LocaltuyaCover(CoverEntity):
"""Tuya cover devices.""" """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._device = device
self._available = False self._available = False
self._name = friendly_name self._name = friendly_name
self._friendly_name = friendly_name
self._icon = icon
self._switch_id = switchid self._switch_id = switchid
self._status = self._device.status() self._status = None
self._state = self._status['dps'][self._switch_id] self._state = None
self._position = 50 self._position = 50
self._open_cmd = open_cmd self._open_cmd = open_cmd
self._close_cmd = close_cmd self._close_cmd = close_cmd
self._stop_cmd = stop_cmd self._stop_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) #_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)) print(
"Initialized tuya cover [{}] with switch status [{}] and state [{}]".format(
self._name, self._status, self._state
)
)
@property @property
def name(self): def name(self):
@@ -219,14 +229,11 @@ class LocaltuyaCover(CoverEntity):
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """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 return supported_features
@property
def icon(self):
"""Return the icon."""
return self._icon
@property @property
def current_cover_position(self): def current_cover_position(self):
# self.update() # self.update()
@@ -240,7 +247,7 @@ class LocaltuyaCover(CoverEntity):
# self.update() # self.update()
state = self._state state = self._state
# print('is_opening() : state [{}]'.format(state)) # print('is_opening() : state [{}]'.format(state))
if state == 'on': if state == "on":
return True return True
return False return False
@@ -249,7 +256,7 @@ class LocaltuyaCover(CoverEntity):
# self.update() # self.update()
state = self._state state = self._state
# print('is_closing() : state [{}]'.format(state)) # print('is_closing() : state [{}]'.format(state))
if state == 'off': if state == "off":
return True return True
return False return False
@@ -260,9 +267,9 @@ class LocaltuyaCover(CoverEntity):
# self.update() # self.update()
state = self._state state = self._state
# print('is_closed() : state [{}]'.format(state)) # print('is_closed() : state [{}]'.format(state))
if state == 'off': if state == "off":
return False return False
if state == 'on': if state == "on":
return True return True
return None return None
@@ -286,6 +293,7 @@ class LocaltuyaCover(CoverEntity):
sleep(mydelay) sleep(mydelay)
self.stop_cover() self.stop_cover()
self._position = 50 # newpos self._position = 50 # newpos
# self._state = 'on' # self._state = 'on'
# self._device._device.open_cover() # self._device._device.open_cover()
@@ -316,7 +324,7 @@ class LocaltuyaCover(CoverEntity):
# _LOGGER.info("running update(self) from cover") # _LOGGER.info("running update(self) from cover")
try: try:
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('update() : state [{}]'.format(self._state)) # print('update() : state [{}]'.format(self._state))
except Exception: except Exception:
self._available = False self._available = False

View File

@@ -15,76 +15,72 @@ fan:
""" """
import logging import logging
import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, from homeassistant.components.fan import (
FanEntity, SUPPORT_SET_SPEED, FanEntity,
SUPPORT_OSCILLATE, SUPPORT_DIRECTION, PLATFORM_SCHEMA) PLATFORM_SCHEMA,
SPEED_LOW,
from homeassistant.const import STATE_OFF SPEED_MEDIUM,
"""from . import DATA_TUYA, TuyaDevice""" SPEED_HIGH,
from homeassistant.const import (CONF_HOST, CONF_ID, CONF_FRIENDLY_NAME, CONF_ICON, CONF_NAME) SUPPORT_SET_SPEED,
SUPPORT_OSCILLATE,
SUPPORT_DIRECTION,
)
from homeassistant.const import CONF_ID, CONF_FRIENDLY_NAME, STATE_OFF
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from . import BASE_PLATFORM_SCHEMA, prepare_setup_entities, import_from_yaml
from .pytuya import TuyaDevice
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'localtuyafan' PLATFORM = "fan"
CONF_DEVICE_ID = 'device_id' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(BASE_PLATFORM_SCHEMA)
CONF_LOCAL_KEY = 'local_key'
CONF_PROTOCOL_VERSION = 'protocol_version'
DEFAULT_ID = '1'
DEFAULT_PROTOCOL_VERSION = 3.3
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def flow_schema(dps):
vol.Optional(CONF_ICON): cv.icon, """Return schema used in config flow."""
vol.Required(CONF_HOST): cv.string, return {}
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): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up of the Tuya switch.""" """Setup a Tuya fan based on a config entry."""
from . import pytuya device, entities_to_setup = prepare_setup_entities(
config_entry, PLATFORM
)
if not entities_to_setup:
return
fans = [] fans = []
pytuyadevice = pytuya.TuyaDevice(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) for device_config in entities_to_setup:
pytuyadevice.set_version(float(config.get(CONF_PROTOCOL_VERSION)))
_LOGGER.debug("localtuya fan: setup_platform: %s", pytuyadevice)
fan_device = pytuyadevice
fans.append( fans.append(
LocaltuyaFan( LocaltuyaFan(
fan_device, device,
config.get(CONF_NAME), device_config[CONF_FRIENDLY_NAME],
config.get(CONF_FRIENDLY_NAME), device_config[CONF_ID],
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) 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, PLATFORM)
class LocaltuyaFan(FanEntity): class LocaltuyaFan(FanEntity):
"""Representation of a Tuya fan.""" """Representation of a Tuya fan."""
# def __init__(self, hass, name: str, supported_features: int) -> None: def __init__(self, device, friendly_name, switchid):
def __init__(self, device, name, friendly_name, icon, switchid):
"""Initialize the entity.""" """Initialize the entity."""
self._device = device self._device = device
self._name = friendly_name self._name = friendly_name
self._available = False self._available = False
self._friendly_name = friendly_name self._friendly_name = friendly_name
self._icon = icon
self._switch_id = switchid self._switch_id = switchid
self._status = self._device.status() self._status = self._device.status()
self._state = False self._state = False
@@ -163,7 +159,7 @@ class LocaltuyaFan(FanEntity):
def oscillate(self, oscillating: bool) -> None: def oscillate(self, oscillating: bool) -> None:
"""Set oscillation.""" """Set oscillation."""
self._oscillating = oscillating self._oscillating = oscillating
self._device.set_value('8', oscillating) self._device.set_value("8", oscillating)
self.schedule_update_ha_state() self.schedule_update_ha_state()
# @property # @property
@@ -174,8 +170,7 @@ class LocaltuyaFan(FanEntity):
@property @property
def unique_id(self): def unique_id(self):
"""Return unique device identifier.""" """Return unique device identifier."""
_LOGGER.debug("localtuya fan unique_id = %s", self._device) return f"local_{self._device.id}_{self._switch_id}"
return self._device.id
@property @property
def available(self): def available(self):
@@ -195,17 +190,17 @@ class LocaltuyaFan(FanEntity):
try: try:
status = self._device.status() status = self._device.status()
_LOGGER.debug("localtuya fan: status = %s", status) _LOGGER.debug("localtuya fan: status = %s", status)
self._state = status['dps']['1'] self._state = status["dps"]["1"]
if status['dps']['1'] == False: if status["dps"]["1"] == False:
self._speed = STATE_OFF self._speed = STATE_OFF
elif int(status['dps']['2']) == 1: elif int(status["dps"]["2"]) == 1:
self._speed = SPEED_LOW self._speed = SPEED_LOW
elif int(status['dps']['2']) == 2: elif int(status["dps"]["2"]) == 2:
self._speed = SPEED_MEDIUM self._speed = SPEED_MEDIUM
elif int(status['dps']['2']) == 3: elif int(status["dps"]["2"]) == 3:
self._speed = SPEED_HIGH self._speed = SPEED_HIGH
# self._speed = status['dps']['2'] # self._speed = status['dps']['2']
self._oscillating = status['dps']['8'] self._oscillating = status["dps"]["8"]
success = True success = True
except ConnectionError: except ConnectionError:
if i + 1 == 3: if i + 1 == 3:

View File

@@ -12,76 +12,78 @@ light:
friendly_name: This Light friendly_name: This Light
protocol_version: 3.3 protocol_version: 3.3
""" """
import voluptuous as vol import socket
from homeassistant.const import (CONF_HOST, CONF_ID, CONF_SWITCHES, CONF_FRIENDLY_NAME, CONF_ICON, CONF_NAME) import logging
import homeassistant.helpers.config_validation as cv
from time import time, sleep from time import time, sleep
from threading import Lock from threading import Lock
import logging
from homeassistant.const import (
CONF_ID,
CONF_FRIENDLY_NAME,
)
from homeassistant.components.light import ( from homeassistant.components.light import (
LightEntity,
PLATFORM_SCHEMA,
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP, ATTR_COLOR_TEMP,
ATTR_HS_COLOR, ATTR_HS_COLOR,
SUPPORT_BRIGHTNESS, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR, SUPPORT_COLOR,
SUPPORT_COLOR_TEMP, SUPPORT_COLOR_TEMP,
LightEntity,
PLATFORM_SCHEMA
) )
from homeassistant.util import color as colorutil from homeassistant.util import color as colorutil
import socket
CONF_DEVICE_ID = 'device_id' from . import BASE_PLATFORM_SCHEMA, import_from_yaml, prepare_setup_entities
CONF_LOCAL_KEY = 'local_key' from .pytuya import TuyaDevice
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. _LOGGER = logging.getLogger(__name__)
DEFAULT_ID = '1'
DEFAULT_PROTOCOL_VERSION = 3.3 PLATFORM = "light"
MIN_MIRED = 153 MIN_MIRED = 153
MAX_MIRED = 370 MAX_MIRED = 370
UPDATE_RETRY_LIMIT = 3 UPDATE_RETRY_LIMIT = 3
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(BASE_PLATFORM_SCHEMA)
vol.Optional(CONF_ICON): cv.icon,
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_DEVICE_ID): cv.string, def flow_schema(dps):
vol.Required(CONF_LOCAL_KEY): cv.string, """Return schema used in config flow."""
vol.Required(CONF_NAME): cv.string, return {}
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, async def async_setup_entry(hass, config_entry, async_add_entities):
}) """Setup a Tuya switch based on a config entry."""
log = logging.getLogger(__name__) device, entities_to_setup = prepare_setup_entities(
log.setLevel(level=logging.DEBUG) # Debug hack! config_entry, PLATFORM
)
if not entities_to_setup:
return
lights = []
for device_config in entities_to_setup:
lights.append(
LocaltuyaLight(
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): 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 return import_from_yaml(hass, config, PLATFORM)
lights = []
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)
lights.append(
LocaltuyaLight(
bulb_device,
config.get(CONF_NAME),
config.get(CONF_FRIENDLY_NAME),
config.get(CONF_ICON),
config.get(CONF_ID)
)
)
add_devices(lights)
class TuyaCache: class TuyaCache:
"""Cache wrapper for pytuya.TuyaDevices""" """Cache wrapper for pytuya.TuyaDevices"""
def __init__(self, device): def __init__(self, device):
"""Initialize the cache.""" """Initialize the cache."""
self._cached_status = '' self._cached_status = ""
self._cached_status_time = 0 self._cached_status_time = 0
self._device = device self._device = device
self._lock = Lock() self._lock = Lock()
@@ -94,18 +96,14 @@ class TuyaCache:
def __get_status(self, switchid): def __get_status(self, switchid):
for _ in range(UPDATE_RETRY_LIMIT): for _ in range(UPDATE_RETRY_LIMIT):
try: try:
status = self._device.status()['dps'][switchid] return self._device.status()["dps"][switchid]
return status except (ConnectionError, socket.timeout):
except ConnectionError:
pass pass
except socket.timeout: _LOGGER.warning("Failed to get status after %d tries", UPDATE_RETRY_LIMIT)
pass
log.warn(
"Failed to get status after {} tries".format(UPDATE_RETRY_LIMIT))
def set_dps(self, state, dps_index): def set_dps(self, state, dps_index):
"""Change the Tuya switch status and clear the cache.""" """Change the Tuya switch status and clear the cache."""
self._cached_status = '' self._cached_status = ""
self._cached_status_time = 0 self._cached_status_time = 0
for _ in range(UPDATE_RETRY_LIMIT): for _ in range(UPDATE_RETRY_LIMIT):
try: try:
@@ -114,21 +112,17 @@ class TuyaCache:
pass pass
except socket.timeout: except socket.timeout:
pass pass
log.warn( _LOGGER.warning("Failed to set status after %d tries", UPDATE_RETRY_LIMIT)
"Failed to set status after {} tries".format(UPDATE_RETRY_LIMIT))
def status(self, switchid): def status(self, switchid):
"""Get state of Tuya switch and cache the results.""" """Get state of Tuya switch and cache the results."""
self._lock.acquire() with self._lock:
try:
now = time() now = time()
if not self._cached_status or now - self._cached_status_time > 15: if not self._cached_status or now - self._cached_status_time > 15:
sleep(0.5) sleep(0.5)
self._cached_status = self.__get_status(switchid) self._cached_status = self.__get_status(switchid)
self._cached_status_time = time() self._cached_status_time = time()
return self._cached_status return self._cached_status
finally:
self._lock.release()
def cached_status(self): def cached_status(self):
return self._cached_status return self._cached_status
@@ -144,7 +138,7 @@ class LocaltuyaLight(LightEntity):
DPS_INDEX_COLOURTEMP = '4' DPS_INDEX_COLOURTEMP = '4'
DPS_INDEX_COLOUR = '5' DPS_INDEX_COLOUR = '5'
def __init__(self, device, name, friendly_name, icon, bulbid): def __init__(self, device, friendly_name, bulbid):
"""Initialize the Tuya switch.""" """Initialize the Tuya switch."""
self._device = device self._device = device
self._available = False self._available = False
@@ -152,9 +146,11 @@ class LocaltuyaLight(LightEntity):
self._state = False self._state = False
self._brightness = 127 self._brightness = 127
self._color_temp = 127 self._color_temp = 127
self._icon = icon
self._bulb_id = bulbid self._bulb_id = bulbid
def state(self):
self._device.state()
@property @property
def name(self): def name(self):
"""Get name of Tuya switch.""" """Get name of Tuya switch."""
@@ -175,11 +171,6 @@ class LocaltuyaLight(LightEntity):
"""Check if Tuya switch is on.""" """Check if Tuya switch is on."""
return self._state return self._state
@property
def icon(self):
"""Return the icon."""
return self._icon
def update(self): def update(self):
"""Get state of Tuya switch.""" """Get state of Tuya switch."""
try: try:
@@ -233,7 +224,7 @@ class LocaltuyaLight(LightEntity):
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn on or control the light.""" """Turn on or control the light."""
log.debug("Turning on, state: " + str(self._device.cached_status())) _LOGGER.debug("Turning on, state: %s", self._device.cached_status())
if not self._device.cached_status(): if not self._device.cached_status():
self._device.set_dps(True, self._bulb_id) self._device.set_dps(True, self._bulb_id)
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
@@ -244,7 +235,11 @@ class LocaltuyaLight(LightEntity):
if ATTR_HS_COLOR in kwargs: if ATTR_HS_COLOR in kwargs:
raise ValueError(" TODO implement RGB from HS") raise ValueError(" TODO implement RGB from HS")
if ATTR_COLOR_TEMP in kwargs: 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) self._device.set_color_temp(color_temp)
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
@@ -319,7 +314,7 @@ class LocaltuyaLight(LightEntity):
an RGB value. an RGB value.
Args: Args:
hexvalue(string): The hex representation generated by BulbDevice._rgb_to_hexvalue() hexvalue(string): The hex representation generated by TuyaDevice._rgb_to_hexvalue()
""" """
r = int(hexvalue[0:2], 16) r = int(hexvalue[0:2], 16)
g = int(hexvalue[2:4], 16) g = int(hexvalue[2:4], 16)
@@ -334,7 +329,7 @@ class LocaltuyaLight(LightEntity):
an HSV value. an HSV value.
Args: Args:
hexvalue(string): The hex representation generated by BulbDevice._rgb_to_hexvalue() hexvalue(string): The hex representation generated by TuyaDevice._rgb_to_hexvalue()
""" """
h = int(hexvalue[7:10], 16) / 360 h = int(hexvalue[7:10], 16) / 360
s = int(hexvalue[10:12], 16) / 255 s = int(hexvalue[10:12], 16) / 255

View File

@@ -3,6 +3,9 @@
"name": "LocalTuya integration", "name": "LocalTuya integration",
"documentation": "https://github.com/rospogrigio/localtuya-homeassistant/", "documentation": "https://github.com/rospogrigio/localtuya-homeassistant/",
"dependencies": [], "dependencies": [],
"codeowners": ["@rospogrigio"], "codeowners": [
"requirements": ["pycryptodome==3.9.8"] "@rospogrigio"
],
"requirements": ["pycryptodome==3.9.8"],
"config_flow": true
} }

View File

@@ -0,0 +1,42 @@
{
"config": {
"abort": {
"already_configured": "Device has already been configured.",
"unsupported_device_type": "Unsupported device type!"
},
"error": {
"cannot_connect": "Cannot connect to device. Verify that address is correct.",
"invalid_auth": "Failed to authenticate with device. Verify that device id and local key are correct.",
"unknown": "An unknown error occurred. See log for details.",
"switch_already_configured": "Switch with this ID has already been configured."
},
"step": {
"user": {
"title": "Add Tuya device",
"description": "Fill in the basic details and pick device type. The name entered here will be used to identify the integration itself (as seen in the `Integrations` page). You will name each sub-device in the following steps.",
"data": {
"name": "Name",
"host": "Host",
"device_id": "Device ID",
"local_key": "Local key",
"protocol_version": "Protocol Version",
"device_type": "Device type"
}
},
"power_outlet": {
"title": "Add subswitch",
"description": "You are about to add subswitch number `{number}`. If you want to add another, tick `Add another switch` before continuing.",
"data": {
"id": "ID",
"name": "Name",
"friendly_name": "Friendly name",
"current": "Current",
"current_consumption": "Current Consumption",
"voltage": "Voltage",
"add_another_switch": "Add another switch"
}
}
}
},
"title": "LocalTuya"
}

View File

@@ -25,129 +25,106 @@ switch:
id: 7 id: 7
""" """
import logging import logging
import voluptuous as vol
from homeassistant.components.switch import SwitchEntity, 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 from time import time, sleep
from threading import Lock from threading import Lock
import voluptuous as vol
from homeassistant.components.switch import (
SwitchEntity,
PLATFORM_SCHEMA,
)
from homeassistant.const import (
CONF_ID,
CONF_SWITCHES,
CONF_FRIENDLY_NAME,
CONF_NAME,
)
import homeassistant.helpers.config_validation as cv
from . import BASE_PLATFORM_SCHEMA, prepare_setup_entities, import_from_yaml
from .const import (
ATTR_CURRENT,
ATTR_CURRENT_CONSUMPTION,
ATTR_VOLTAGE,
CONF_CURRENT,
CONF_CURRENT_CONSUMPTION,
CONF_VOLTAGE,
)
from .pytuya import TuyaDevice
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_DEVICE_ID = 'device_id' PLATFORM = "switch"
CONF_LOCAL_KEY = 'local_key'
CONF_PROTOCOL_VERSION = 'protocol_version'
CONF_CURRENT = 'current'
CONF_CURRENT_CONSUMPTION = 'current_consumption'
CONF_VOLTAGE = 'voltage'
DEFAULT_ID = '1' DEFAULT_ID = "1"
DEFAULT_PROTOCOL_VERSION = 3.3
ATTR_CURRENT = 'current' # TODO: This will eventully merge with flow_schema
ATTR_CURRENT_CONSUMPTION = 'current_consumption' SWITCH_SCHEMA = vol.Schema(
ATTR_VOLTAGE = 'voltage' {
SWITCH_SCHEMA = vol.Schema({
vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, 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.Required(CONF_FRIENDLY_NAME): cv.string,
vol.Optional(CONF_CURRENT, default='-1'): cv.string, vol.Optional(CONF_CURRENT, default="-1"): cv.string,
vol.Optional(CONF_CURRENT_CONSUMPTION, default='-1'): cv.string, vol.Optional(CONF_CURRENT_CONSUMPTION, default="-1"): cv.string,
vol.Optional(CONF_VOLTAGE, default='-1'): cv.string, vol.Optional(CONF_VOLTAGE, default="-1"): cv.string,
}) }
)
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.Optional(CONF_CURRENT, default="-1"): cv.string,
vol.Required(CONF_DEVICE_ID): cv.string, vol.Optional(CONF_CURRENT_CONSUMPTION, default="-1"): cv.string,
vol.Required(CONF_LOCAL_KEY): cv.string, vol.Optional(CONF_VOLTAGE, default="-1"): cv.string,
vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_SWITCHES, default={}): vol.Schema({cv.slug: SWITCH_SCHEMA}),
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, def flow_schema(dps):
vol.Optional(CONF_VOLTAGE, default='-1'): cv.string, """Return schema used in config flow."""
vol.Optional(CONF_SWITCHES, default={}): return {
vol.Schema({cv.slug: SWITCH_SCHEMA}), 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):
"""Setup a Tuya switch based on a config entry."""
device, entities_to_setup = prepare_setup_entities(
config_entry, PLATFORM
)
if not entities_to_setup:
return
switches = []
for device_config in entities_to_setup:
switches.append(
LocaltuyaSwitch(
TuyaCache(device),
device_config[CONF_FRIENDLY_NAME],
device_config[CONF_ID],
device_config.get(CONF_CURRENT),
device_config.get(CONF_CURRENT_CONSUMPTION),
device_config.get(CONF_VOLTAGE),
)
)
async_add_entities(switches, True)
def setup_platform(hass, config, add_devices, discovery_info=None): 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 return import_from_yaml(hass, config, PLATFORM)
devices = config.get(CONF_SWITCHES)
switches = []
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:
for object_id, device_config in devices.items():
dps = {}
dps[device_config.get(CONF_ID)]=None
if device_config.get(CONF_CURRENT) != '-1':
dps[device_config.get(CONF_CURRENT)]=None
if device_config.get(CONF_CURRENT_CONSUMPTION) != '-1':
dps[device_config.get(CONF_CURRENT_CONSUMPTION)]=None
if device_config.get(CONF_VOLTAGE) != '-1':
dps[device_config.get(CONF_VOLTAGE)]=None
pytuyadevice.set_dpsUsed(dps)
outlet_device = TuyaCache(pytuyadevice)
switches.append(
LocaltuyaSwitch(
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 subswitch [{}] with device ID [{}] '.format(device_config.get(CONF_FRIENDLY_NAME, object_id), device_config.get(CONF_ID)))
_LOGGER.info("Setup localtuya subswitch %s with device ID %s ", device_config.get(CONF_FRIENDLY_NAME, object_id), device_config.get(CONF_ID) )
else:
dps = {}
dps[config.get(CONF_ID)]=None
if config.get(CONF_CURRENT) != '-1':
dps[config.get(CONF_CURRENT)]=None
if config.get(CONF_CURRENT_CONSUMPTION) != '-1':
dps[config.get(CONF_CURRENT_CONSUMPTION)]=None
if config.get(CONF_VOLTAGE) != '-1':
dps[config.get(CONF_VOLTAGE)]=None
pytuyadevice.set_dpsUsed(dps)
outlet_device = TuyaCache(pytuyadevice)
switches.append(
LocaltuyaSwitch(
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)))
_LOGGER.info("Setup localtuya switch %s with device ID %s ", config.get(CONF_FRIENDLY_NAME), config.get(CONF_ID) )
add_devices(switches, True)
class TuyaCache: class TuyaCache:
"""Cache wrapper for pytuya.TuyaDevice""" """Cache wrapper for pytuya.TuyaDevice"""
def __init__(self, device): def __init__(self, device):
"""Initialize the cache.""" """Initialize the cache."""
self._cached_status = '' self._cached_status = ""
self._cached_status_time = 0 self._cached_status_time = 0
self._device = device self._device = device
self._lock = Lock() self._lock = Lock()
@@ -163,25 +140,36 @@ class TuyaCache:
status = self._device.status() status = self._device.status()
return status return status
except Exception: 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) sleep(1.0)
if i + 1 == 3: if i + 1 == 3:
_LOGGER.error("Failed to update status of device %s", self._device.address ) _LOGGER.error(
"Failed to update status of device %s", self._device.address
)
# return None # return None
raise ConnectionError("Failed to update status .") raise ConnectionError("Failed to update status .")
def set_dps(self, state, dps_index): def set_dps(self, state, dps_index):
"""Change the Tuya switch status and clear the cache.""" """Change the Tuya switch status and clear the cache."""
self._cached_status = '' self._cached_status = ""
self._cached_status_time = 0 self._cached_status_time = 0
for i in range(5): for i in range(5):
try: try:
return self._device.set_dps(state, dps_index) return self._device.set_dps(state, dps_index)
except Exception: except Exception:
print('Failed to set status of device [{}]'.format(self._device.address)) print(
"Failed to set status of device [{}]".format(self._device.address)
)
if i + 1 == 3: if i + 1 == 3:
_LOGGER.error("Failed to set status of device %s", self._device.address ) _LOGGER.error(
"Failed to set status of device %s", self._device.address
)
return return
# raise ConnectionError("Failed to set status.") # raise ConnectionError("Failed to set status.")
def status(self): def status(self):
@@ -200,19 +188,30 @@ class TuyaCache:
class LocaltuyaSwitch(SwitchEntity): class LocaltuyaSwitch(SwitchEntity):
"""Representation of a Tuya switch.""" """Representation of a Tuya switch."""
def __init__(self, device, name, friendly_name, icon, switchid, attr_current, attr_consumption, attr_voltage): def __init__(
self,
device,
friendly_name,
switchid,
attr_current,
attr_consumption,
attr_voltage,
):
"""Initialize the Tuya switch.""" """Initialize the Tuya switch."""
self._device = device self._device = device
self._name = friendly_name self._name = friendly_name
self._available = False self._available = False
self._icon = icon
self._switch_id = switchid self._switch_id = switchid
self._attr_current = attr_current self._attr_current = attr_current
self._attr_consumption = attr_consumption self._attr_consumption = attr_consumption
self._attr_voltage = attr_voltage self._attr_voltage = attr_voltage
self._status = self._device.status() self._status = None
self._state = self._status['dps'][self._switch_id] self._state = None
print('Initialized tuya switch [{}] with switch status [{}] and state [{}]'.format(self._name, self._status, self._state)) print(
"Initialized tuya switch [{}] with switch status [{}] and state [{}]".format(
self._name, self._status, self._state
)
)
@property @property
def name(self): def name(self):
@@ -237,23 +236,16 @@ class LocaltuyaSwitch(SwitchEntity):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
attrs = {} attrs = {}
try: if self._attr_current:
attrs[ATTR_CURRENT] = "{}".format(self._status['dps'][self._attr_current]) attrs[ATTR_CURRENT] = self._status["dps"][self._attr_current]
attrs[ATTR_CURRENT_CONSUMPTION] = "{}".format(self._status['dps'][self._attr_consumption]/10) if self._attr_consumption:
attrs[ATTR_VOLTAGE] = "{}".format(self._status['dps'][self._attr_voltage]/10) attrs[ATTR_CURRENT_CONSUMPTION] = (
# print('attrs[ATTR_CURRENT]: [{}]'.format(attrs[ATTR_CURRENT])) self._status["dps"][self._attr_consumption] / 10
# print('attrs[ATTR_CURRENT_CONSUMPTION]: [{}]'.format(attrs[ATTR_CURRENT_CONSUMPTION])) )
# print('attrs[ATTR_VOLTAGE]: [{}]'.format(attrs[ATTR_VOLTAGE])) if self._attr_voltage:
attrs[ATTR_VOLTAGE] = self._status["dps"][self._attr_voltage] / 10
except KeyError:
pass
return attrs return attrs
@property
def icon(self):
"""Return the icon."""
return self._icon
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn Tuya switch on.""" """Turn Tuya switch on."""
self._device.set_dps(True, self._switch_id) self._device.set_dps(True, self._switch_id)
@@ -266,7 +258,7 @@ class LocaltuyaSwitch(SwitchEntity):
"""Get state of Tuya switch.""" """Get state of Tuya switch."""
try: try:
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]
except Exception: except Exception:
self._available = False self._available = False
else: else:

View File

@@ -0,0 +1,49 @@
{
"config": {
"abort": {
"already_configured": "Device has already been configured."
},
"error": {
"cannot_connect": "Cannot connect to device. Verify that address is correct and try again.",
"invalid_auth": "Failed to authenticate with device. Verify that device id and local key are correct.",
"unknown": "An unknown error occurred. See log for details.",
"entity_already_configured": "Entity with this ID has already been configured."
},
"step": {
"user": {
"title": "Add Tuya device",
"description": "Fill in the basic details and pick device type. The name entered here will be used to identify the integration itself (as seen in the `Integrations` page). You will add entities and give them names in the following steps.",
"data": {
"name": "Name",
"host": "Host",
"device_id": "Device ID",
"local_key": "Local key",
"protocol_version": "Protocol Version"
}
},
"pick_entity_type": {
"title": "Entity type selection",
"description": "Please pick the type of entity you want to add.",
"data": {
"platform_to_add": "Platform",
"no_additional_platforms": "Do not add any more entities"
}
},
"add_entity": {
"title": "Add new entity",
"description": "Please fill out the details for an entity with type `{platform}`.",
"data": {
"id": "ID",
"friendly_name": "Friendly name",
"current": "Current",
"current_consumption": "Current Consumption",
"voltage": "Voltage",
"open_cmd": "Open Command",
"close_cmd": "Close Command",
"stop_cmd": "Stop Command"
}
}
}
},
"title": "LocalTuya"
}

28
info.md
View File

@@ -6,7 +6,7 @@
Local handling for Tuya Switches under Home-Assistant and Hassio, getting parameters from them (as Power Meters: Voltage, Current, Watt). Supports 3 types of switches: one-gang switches, two-gang switches and wifi plug (with additional USB plugs). Local handling for Tuya Switches under Home-Assistant and Hassio, getting parameters from them (as Power Meters: Voltage, Current, Watt). Supports 3 types of switches: one-gang switches, two-gang switches and wifi plug (with additional USB plugs).
Also introduced handling for Tuya Covers and Lights, introducing pytuya library 7.0.9. Also introduced handling for Tuya Covers and Lights, introducing pytuya library 7.1.0 that finally handles the 'json obj data unvalid' error correctly.
Developed substantially by merging the codes of NameLessJedi, mileperhour and TradeFace (see Thanks paragraph). Developed substantially by merging the codes of NameLessJedi, mileperhour and TradeFace (see Thanks paragraph).
@@ -16,22 +16,7 @@ Developed substantially by merging the codes of NameLessJedi, mileperhour and Tr
2. Identify on your Home-Assistant logs (putting your logging into debug mode), the different attributes you want to handle by HA. 2. Identify on your Home-Assistant logs (putting your logging into debug mode), the different attributes you want to handle by HA.
3. Find in the switch.py file that part, and edit it for ID/DPS that is correct for your device. 3. Use this declaration on your configuration.yaml file (you need to get the 'device_id' and 'local_key' parameters for your device, as it can be obtained on other tutorials on the web:
```
@property
def device_state_attributes(self):
attrs = {}
try:
attrs[ATTR_CURRENT] = "{}".format(self._status['dps']['104'])
attrs[ATTR_CURRENT_CONSUMPTION] = "{}".format(self._status['dps']['105']/10)
attrs[ATTR_VOLTAGE] = "{}".format(self._status['dps']['106']/10)
except KeyError:
pass
return attrs
```
NOTE: Original data from the device for Voltage and Watt, includes the first decimal. So if the value is 2203, the correct value is 220,3V. By this reason, this values are divided by 10 ('/10' in the script). While Current is sent in mA (int number, no decimals), so it don't need any conversion factor to be added on the declaration.
4. Use this declaration on your configuration.yaml file (you need to get the 'device_id' and 'local_key' parameters for your device, as it can be obtained on other tutorials on the web:
``` ```
##### FOR ONE-GANG SWITCHES ##### ##### FOR ONE-GANG SWITCHES #####
switch: switch:
@@ -71,7 +56,7 @@ switch:
NOTE2: for each switch/subswitch both name and friendly_name must be specified: name will be used as the entity ID, while friendly_name will be used as the name in the frontend. NOTE2: for each switch/subswitch both name and friendly_name must be specified: name will be used as the entity ID, while friendly_name will be used as the name in the frontend.
5. Use this declaration on your configuration.yaml file, for stating sensors that handle its attributes: 4. Use this declaration on your configuration.yaml file, for stating sensors that handle its attributes:
``` ```
sensor: sensor:
- platform: template - platform: template
@@ -89,11 +74,11 @@ switch:
{{ states.switch.sw01.attributes.current_consumption }} {{ states.switch.sw01.attributes.current_consumption }}
unit_of_measurement: 'W' unit_of_measurement: 'W'
``` ```
6. If all gone OK (your device's parameters local_key and device_id are correct), your switch is working, so the sensors are working too. 5. If all gone OK (your device's parameters local_key and device_id are correct), your switch is working, so the sensors are working too.
NOTE: You can do as changes as you want in scripts ant/or yaml files. But: You can't declare your "custom_component" as "tuya", tuya is a forbidden word from 0.88 version or so. So if you declare a switch.tuya, the embedded (cloud based) Tuya component will be load instead custom_component one. NOTE: You can do as changes as you want in scripts ant/or yaml files. But: You can't declare your "custom_component" as "tuya", tuya is a forbidden word from 0.88 version or so. So if you declare a switch.tuya, the embedded (cloud based) Tuya component will be load instead custom_component one.
7. If you are using a cover device, this is the configuration to be used (as explained in cover.py): 6. If you are using a cover device, this is the configuration to be used (as explained in cover.py):
``` ```
cover: cover:
- platform: localtuya - platform: localtuya
@@ -115,6 +100,9 @@ cover:
Create a switch for cover backlight (dps 101): pytuya library already supports it Create a switch for cover backlight (dps 101): pytuya library already supports it
climate (thermostats) devices handling
# Thanks to: # Thanks to:
NameLessJedi https://github.com/NameLessJedi/localtuya-homeassistant and mileperhour https://github.com/mileperhour/localtuya-homeassistant being the major sources of inspiration, and whose code for switches is substantially unchanged. NameLessJedi https://github.com/NameLessJedi/localtuya-homeassistant and mileperhour https://github.com/mileperhour/localtuya-homeassistant being the major sources of inspiration, and whose code for switches is substantially unchanged.