Merge branch 'master' into pytuya_refactoring
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*~
|
||||||
|
__pycache__
|
17
README.md
17
README.md
@@ -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
|
||||||
|
@@ -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
|
||||||
|
213
custom_components/localtuya/config_flow.py
Normal file
213
custom_components/localtuya/config_flow.py
Normal 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."""
|
23
custom_components/localtuya/const.py
Normal file
23
custom_components/localtuya/const.py
Normal 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"]
|
@@ -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
|
||||||
|
@@ -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:
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
}
|
}
|
42
custom_components/localtuya/strings.json
Normal file
42
custom_components/localtuya/strings.json
Normal 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"
|
||||||
|
}
|
@@ -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:
|
||||||
|
49
custom_components/localtuya/translations/en.json
Normal file
49
custom_components/localtuya/translations/en.json
Normal 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
28
info.md
@@ -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.
|
||||||
|
Reference in New Issue
Block a user