mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Add config flow for Tuya (#35422)
* Added Tuya config flow * Added test config_flow * Fixed log error message * Add test requirements * Lint Fix * Fix Black formatting * Added pylint directive Added pylint:disable=unused-import in config_flow.py * Implementation requested changes * Update CodeOwners * Removed device registry cleanup * Force checks * Force checks * Fix implemetation - Set config schema "deprecated" - Removed async from update_callback * Updating test * Fix formatting * Config Flow test fix - mock out async_setup and async_setup_entry * Fix formatting
This commit is contained in:
parent
c69d4943a5
commit
b3ee54b124
@ -417,6 +417,7 @@ homeassistant/components/tradfri/* @ggravlingen
|
||||
homeassistant/components/trafikverket_train/* @endor-force
|
||||
homeassistant/components/transmission/* @engrbm87 @JPHutchins
|
||||
homeassistant/components/tts/* @pvizeli
|
||||
homeassistant/components/tuya/* @ollo69
|
||||
homeassistant/components/twentemilieu/* @frenck
|
||||
homeassistant/components/twilio_call/* @robbiet480
|
||||
homeassistant/components/twilio_sms/* @robbiet480
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Support for Tuya Smart devices."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
@ -6,32 +7,38 @@ from tuyaha import TuyaApi
|
||||
from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException, TuyaServerException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import call_later, track_time_interval
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
from .const import (
|
||||
CONF_COUNTRYCODE,
|
||||
DOMAIN,
|
||||
TUYA_DATA,
|
||||
TUYA_DISCOVERY_NEW,
|
||||
TUYA_PLATFORMS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_COUNTRYCODE = "country_code"
|
||||
ENTRY_IS_SETUP = "tuya_entry_is_setup"
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
DOMAIN = "tuya"
|
||||
DATA_TUYA = "data_tuya"
|
||||
|
||||
FIRST_RETRY_TIME = 60
|
||||
MAX_RETRY_TIME = 900
|
||||
SERVICE_FORCE_UPDATE = "force_update"
|
||||
SERVICE_PULL_DEVICES = "pull_devices"
|
||||
|
||||
SIGNAL_DELETE_ENTITY = "tuya_delete"
|
||||
SIGNAL_UPDATE_ENTITY = "tuya_update"
|
||||
|
||||
SERVICE_FORCE_UPDATE = "force_update"
|
||||
SERVICE_PULL_DEVICES = "pull_devices"
|
||||
|
||||
TUYA_TYPE_TO_HA = {
|
||||
"climate": "climate",
|
||||
"cover": "cover",
|
||||
@ -41,48 +48,55 @@ TUYA_TYPE_TO_HA = {
|
||||
"switch": "switch",
|
||||
}
|
||||
|
||||
TUYA_TRACKER = "tuya_tracker"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_COUNTRYCODE): cv.string,
|
||||
vol.Optional(CONF_PLATFORM, default="tuya"): cv.string,
|
||||
}
|
||||
)
|
||||
},
|
||||
vol.All(
|
||||
cv.deprecated(DOMAIN),
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_COUNTRYCODE): cv.string,
|
||||
vol.Optional(CONF_PLATFORM, default="tuya"): cv.string,
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
def setup(hass, config, retry_delay=FIRST_RETRY_TIME):
|
||||
"""Set up Tuya Component."""
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the Tuya integration."""
|
||||
|
||||
_LOGGER.debug("Setting up integration")
|
||||
|
||||
tuya = TuyaApi()
|
||||
username = config[DOMAIN][CONF_USERNAME]
|
||||
password = config[DOMAIN][CONF_PASSWORD]
|
||||
country_code = config[DOMAIN][CONF_COUNTRYCODE]
|
||||
platform = config[DOMAIN][CONF_PLATFORM]
|
||||
|
||||
try:
|
||||
tuya.init(username, password, country_code, platform)
|
||||
except (TuyaNetException, TuyaServerException):
|
||||
|
||||
_LOGGER.warning(
|
||||
"Connection error during integration setup. Will retry in %s seconds",
|
||||
retry_delay,
|
||||
conf = config.get(DOMAIN)
|
||||
if conf is not None:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
|
||||
)
|
||||
)
|
||||
|
||||
def retry_setup(now):
|
||||
"""Retry setup if a error happens on tuya API."""
|
||||
setup(hass, config, retry_delay=min(2 * retry_delay, MAX_RETRY_TIME))
|
||||
return True
|
||||
|
||||
call_later(hass, retry_delay, retry_setup)
|
||||
|
||||
return True
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up Tuya platform."""
|
||||
|
||||
tuya = TuyaApi()
|
||||
username = entry.data[CONF_USERNAME]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
country_code = entry.data[CONF_COUNTRYCODE]
|
||||
platform = entry.data[CONF_PLATFORM]
|
||||
|
||||
try:
|
||||
await hass.async_add_executor_job(
|
||||
tuya.init, username, password, country_code, platform
|
||||
)
|
||||
except (TuyaNetException, TuyaServerException):
|
||||
raise ConfigEntryNotReady()
|
||||
|
||||
except TuyaAPIException as exc:
|
||||
_LOGGER.error(
|
||||
@ -90,10 +104,15 @@ def setup(hass, config, retry_delay=FIRST_RETRY_TIME):
|
||||
)
|
||||
return False
|
||||
|
||||
hass.data[DATA_TUYA] = tuya
|
||||
hass.data[DOMAIN] = {"entities": {}}
|
||||
hass.data[DOMAIN] = {
|
||||
TUYA_DATA: tuya,
|
||||
TUYA_TRACKER: None,
|
||||
ENTRY_IS_SETUP: set(),
|
||||
"entities": {},
|
||||
"pending": {},
|
||||
}
|
||||
|
||||
def load_devices(device_list):
|
||||
async def async_load_devices(device_list):
|
||||
"""Load new devices by device_list."""
|
||||
device_type_list = {}
|
||||
for device in device_list:
|
||||
@ -107,51 +126,92 @@ def setup(hass, config, retry_delay=FIRST_RETRY_TIME):
|
||||
device_type_list[ha_type] = []
|
||||
device_type_list[ha_type].append(device.object_id())
|
||||
hass.data[DOMAIN]["entities"][device.object_id()] = None
|
||||
|
||||
for ha_type, dev_ids in device_type_list.items():
|
||||
discovery.load_platform(hass, ha_type, DOMAIN, {"dev_ids": dev_ids}, config)
|
||||
config_entries_key = f"{ha_type}.tuya"
|
||||
if config_entries_key not in hass.data[DOMAIN][ENTRY_IS_SETUP]:
|
||||
hass.data[DOMAIN]["pending"][ha_type] = dev_ids
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, ha_type)
|
||||
)
|
||||
hass.data[DOMAIN][ENTRY_IS_SETUP].add(config_entries_key)
|
||||
else:
|
||||
async_dispatcher_send(hass, TUYA_DISCOVERY_NEW.format(ha_type), dev_ids)
|
||||
|
||||
device_list = tuya.get_all_devices()
|
||||
load_devices(device_list)
|
||||
device_list = await hass.async_add_executor_job(tuya.get_all_devices)
|
||||
await async_load_devices(device_list)
|
||||
|
||||
def poll_devices_update(event_time):
|
||||
def _get_updated_devices():
|
||||
tuya.poll_devices_update()
|
||||
return tuya.get_all_devices()
|
||||
|
||||
async def async_poll_devices_update(event_time):
|
||||
"""Check if accesstoken is expired and pull device list from server."""
|
||||
_LOGGER.debug("Pull devices from Tuya.")
|
||||
tuya.poll_devices_update()
|
||||
# Add new discover device.
|
||||
device_list = tuya.get_all_devices()
|
||||
load_devices(device_list)
|
||||
device_list = await hass.async_add_executor_job(_get_updated_devices)
|
||||
await async_load_devices(device_list)
|
||||
# Delete not exist device.
|
||||
newlist_ids = []
|
||||
for device in device_list:
|
||||
newlist_ids.append(device.object_id())
|
||||
for dev_id in list(hass.data[DOMAIN]["entities"]):
|
||||
if dev_id not in newlist_ids:
|
||||
dispatcher_send(hass, SIGNAL_DELETE_ENTITY, dev_id)
|
||||
async_dispatcher_send(hass, SIGNAL_DELETE_ENTITY, dev_id)
|
||||
hass.data[DOMAIN]["entities"].pop(dev_id)
|
||||
|
||||
track_time_interval(hass, poll_devices_update, timedelta(minutes=5))
|
||||
hass.data[DOMAIN][TUYA_TRACKER] = async_track_time_interval(
|
||||
hass, async_poll_devices_update, timedelta(minutes=5)
|
||||
)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_PULL_DEVICES, poll_devices_update)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_PULL_DEVICES, async_poll_devices_update
|
||||
)
|
||||
|
||||
def force_update(call):
|
||||
async def async_force_update(call):
|
||||
"""Force all devices to pull data."""
|
||||
dispatcher_send(hass, SIGNAL_UPDATE_ENTITY)
|
||||
async_dispatcher_send(hass, SIGNAL_UPDATE_ENTITY)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_FORCE_UPDATE, force_update)
|
||||
hass.services.async_register(DOMAIN, SERVICE_FORCE_UPDATE, async_force_update)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unloading the Tuya platforms."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(
|
||||
entry, component.split(".", 1)[0]
|
||||
)
|
||||
for component in hass.data[DOMAIN][ENTRY_IS_SETUP]
|
||||
]
|
||||
)
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN][ENTRY_IS_SETUP] = set()
|
||||
hass.data[DOMAIN][TUYA_TRACKER]()
|
||||
hass.data[DOMAIN][TUYA_TRACKER] = None
|
||||
hass.data[DOMAIN][TUYA_DATA] = None
|
||||
hass.services.async_remove(DOMAIN, SERVICE_FORCE_UPDATE)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_PULL_DEVICES)
|
||||
hass.data.pop(DOMAIN)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
class TuyaDevice(Entity):
|
||||
"""Tuya base device."""
|
||||
|
||||
def __init__(self, tuya):
|
||||
def __init__(self, tuya, platform):
|
||||
"""Init Tuya devices."""
|
||||
self.tuya = tuya
|
||||
self._tuya = tuya
|
||||
self._platform = platform
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity is added to hass."""
|
||||
dev_id = self.tuya.object_id()
|
||||
dev_id = self._tuya.object_id()
|
||||
self.hass.data[DOMAIN]["entities"][dev_id] = self.entity_id
|
||||
async_dispatcher_connect(self.hass, SIGNAL_DELETE_ENTITY, self._delete_callback)
|
||||
async_dispatcher_connect(self.hass, SIGNAL_UPDATE_ENTITY, self._update_callback)
|
||||
@ -159,32 +219,49 @@ class TuyaDevice(Entity):
|
||||
@property
|
||||
def object_id(self):
|
||||
"""Return Tuya device id."""
|
||||
return self.tuya.object_id()
|
||||
return self._tuya.object_id()
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return f"tuya.{self.tuya.object_id()}"
|
||||
return f"tuya.{self._tuya.object_id()}"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return Tuya device name."""
|
||||
return self.tuya.name()
|
||||
return self._tuya.name()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return if the device is available."""
|
||||
return self.tuya.available()
|
||||
return self._tuya.available()
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return a device description for device registry."""
|
||||
_device_info = {
|
||||
"identifiers": {(DOMAIN, f"{self.unique_id}")},
|
||||
"manufacturer": TUYA_PLATFORMS.get(self._platform, self._platform),
|
||||
"name": self.name,
|
||||
"model": self._tuya.object_type(),
|
||||
}
|
||||
return _device_info
|
||||
|
||||
def update(self):
|
||||
"""Refresh Tuya device data."""
|
||||
self.tuya.update()
|
||||
self._tuya.update()
|
||||
|
||||
@callback
|
||||
def _delete_callback(self, dev_id):
|
||||
async def _delete_callback(self, dev_id):
|
||||
"""Remove this entity."""
|
||||
if dev_id == self.object_id:
|
||||
self.hass.async_create_task(self.async_remove())
|
||||
entity_registry = (
|
||||
await self.hass.helpers.entity_registry.async_get_registry()
|
||||
)
|
||||
if entity_registry.async_is_registered(self.entity_id):
|
||||
entity_registry.async_remove(self.entity_id)
|
||||
else:
|
||||
await self.async_remove()
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
|
@ -1,5 +1,9 @@
|
||||
"""Support for the Tuya climate devices."""
|
||||
from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateEntity
|
||||
from homeassistant.components.climate import (
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
ENTITY_ID_FORMAT,
|
||||
ClimateEntity,
|
||||
)
|
||||
from homeassistant.components.climate.const import (
|
||||
FAN_HIGH,
|
||||
FAN_LOW,
|
||||
@ -14,12 +18,15 @@ from homeassistant.components.climate.const import (
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
CONF_PLATFORM,
|
||||
PRECISION_WHOLE,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from . import DATA_TUYA, TuyaDevice
|
||||
from . import TuyaDevice
|
||||
from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW
|
||||
|
||||
DEVICE_TYPE = "climate"
|
||||
|
||||
@ -37,34 +44,53 @@ TUYA_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_TUYA.items()}
|
||||
FAN_MODES = {FAN_LOW, FAN_MEDIUM, FAN_HIGH}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up Tuya Climate devices."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
tuya = hass.data[DATA_TUYA]
|
||||
dev_ids = discovery_info.get("dev_ids")
|
||||
devices = []
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up tuya sensors dynamically through tuya discovery."""
|
||||
|
||||
platform = config_entry.data[CONF_PLATFORM]
|
||||
|
||||
async def async_discover_sensor(dev_ids):
|
||||
"""Discover and add a discovered tuya sensor."""
|
||||
if not dev_ids:
|
||||
return
|
||||
entities = await hass.async_add_executor_job(
|
||||
_setup_entities, hass, dev_ids, platform,
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
async_dispatcher_connect(
|
||||
hass, TUYA_DISCOVERY_NEW.format(SENSOR_DOMAIN), async_discover_sensor
|
||||
)
|
||||
|
||||
devices_ids = hass.data[DOMAIN]["pending"].pop(SENSOR_DOMAIN)
|
||||
await async_discover_sensor(devices_ids)
|
||||
|
||||
|
||||
def _setup_entities(hass, dev_ids, platform):
|
||||
"""Set up Tuya Climate device."""
|
||||
tuya = hass.data[DOMAIN][TUYA_DATA]
|
||||
entities = []
|
||||
for dev_id in dev_ids:
|
||||
device = tuya.get_device_by_id(dev_id)
|
||||
if device is None:
|
||||
entity = tuya.get_device_by_id(dev_id)
|
||||
if entity is None:
|
||||
continue
|
||||
devices.append(TuyaClimateEntity(device))
|
||||
add_entities(devices)
|
||||
entities.append(TuyaClimateEntity(entity, platform))
|
||||
return entities
|
||||
|
||||
|
||||
class TuyaClimateEntity(TuyaDevice, ClimateEntity):
|
||||
"""Tuya climate devices,include air conditioner,heater."""
|
||||
|
||||
def __init__(self, tuya):
|
||||
def __init__(self, tuya, platform):
|
||||
"""Init climate device."""
|
||||
super().__init__(tuya)
|
||||
super().__init__(tuya, platform)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id())
|
||||
self.operations = [HVAC_MODE_OFF]
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Create operation list when add to hass."""
|
||||
await super().async_added_to_hass()
|
||||
modes = self.tuya.operation_list()
|
||||
modes = self._tuya.operation_list()
|
||||
if modes is None:
|
||||
return
|
||||
|
||||
@ -80,7 +106,7 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity):
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement used by the platform."""
|
||||
unit = self.tuya.temperature_unit()
|
||||
unit = self._tuya.temperature_unit()
|
||||
if unit == "FAHRENHEIT":
|
||||
return TEMP_FAHRENHEIT
|
||||
return TEMP_CELSIUS
|
||||
@ -88,10 +114,10 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity):
|
||||
@property
|
||||
def hvac_mode(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if not self.tuya.state():
|
||||
if not self._tuya.state():
|
||||
return HVAC_MODE_OFF
|
||||
|
||||
mode = self.tuya.current_operation()
|
||||
mode = self._tuya.current_operation()
|
||||
if mode is None:
|
||||
return None
|
||||
return TUYA_STATE_TO_HA.get(mode)
|
||||
@ -104,63 +130,63 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity):
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self.tuya.current_temperature()
|
||||
return self._tuya.current_temperature()
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self.tuya.target_temperature()
|
||||
return self._tuya.target_temperature()
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
return self.tuya.target_temperature_step()
|
||||
return self._tuya.target_temperature_step()
|
||||
|
||||
@property
|
||||
def fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self.tuya.current_fan_mode()
|
||||
return self._tuya.current_fan_mode()
|
||||
|
||||
@property
|
||||
def fan_modes(self):
|
||||
"""Return the list of available fan modes."""
|
||||
return self.tuya.fan_list()
|
||||
return self._tuya.fan_list()
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
if ATTR_TEMPERATURE in kwargs:
|
||||
self.tuya.set_temperature(kwargs[ATTR_TEMPERATURE])
|
||||
self._tuya.set_temperature(kwargs[ATTR_TEMPERATURE])
|
||||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
self.tuya.set_fan_mode(fan_mode)
|
||||
self._tuya.set_fan_mode(fan_mode)
|
||||
|
||||
def set_hvac_mode(self, hvac_mode):
|
||||
"""Set new target operation mode."""
|
||||
if hvac_mode == HVAC_MODE_OFF:
|
||||
self.tuya.turn_off()
|
||||
self._tuya.turn_off()
|
||||
|
||||
if not self.tuya.state():
|
||||
self.tuya.turn_on()
|
||||
if not self._tuya.state():
|
||||
self._tuya.turn_on()
|
||||
|
||||
self.tuya.set_operation_mode(HA_STATE_TO_TUYA.get(hvac_mode))
|
||||
self._tuya.set_operation_mode(HA_STATE_TO_TUYA.get(hvac_mode))
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
supports = 0
|
||||
if self.tuya.support_target_temperature():
|
||||
if self._tuya.support_target_temperature():
|
||||
supports = supports | SUPPORT_TARGET_TEMPERATURE
|
||||
if self.tuya.support_wind_speed():
|
||||
if self._tuya.support_wind_speed():
|
||||
supports = supports | SUPPORT_FAN_MODE
|
||||
return supports
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self.tuya.min_temp()
|
||||
return self._tuya.min_temp()
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self.tuya.max_temp()
|
||||
return self._tuya.max_temp()
|
||||
|
108
homeassistant/components/tuya/config_flow.py
Normal file
108
homeassistant/components/tuya/config_flow.py
Normal file
@ -0,0 +1,108 @@
|
||||
"""Config flow for Tuya."""
|
||||
import logging
|
||||
|
||||
from tuyaha import TuyaApi
|
||||
from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException, TuyaServerException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME
|
||||
|
||||
# pylint:disable=unused-import
|
||||
from .const import CONF_COUNTRYCODE, DOMAIN, TUYA_PLATFORMS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_SCHEMA_USER = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Required(CONF_COUNTRYCODE): vol.Coerce(int),
|
||||
vol.Required(CONF_PLATFORM): vol.In(TUYA_PLATFORMS),
|
||||
}
|
||||
)
|
||||
|
||||
RESULT_AUTH_FAILED = "auth_failed"
|
||||
RESULT_CONN_ERROR = "conn_error"
|
||||
RESULT_SUCCESS = "success"
|
||||
|
||||
RESULT_LOG_MESSAGE = {
|
||||
RESULT_AUTH_FAILED: "Invalid credential",
|
||||
RESULT_CONN_ERROR: "Connection error",
|
||||
}
|
||||
|
||||
|
||||
class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a tuya config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize flow."""
|
||||
self._country_code = None
|
||||
self._password = None
|
||||
self._platform = None
|
||||
self._username = None
|
||||
self._is_import = False
|
||||
|
||||
def _get_entry(self):
|
||||
return self.async_create_entry(
|
||||
title=self._username,
|
||||
data={
|
||||
CONF_COUNTRYCODE: self._country_code,
|
||||
CONF_PASSWORD: self._password,
|
||||
CONF_PLATFORM: self._platform,
|
||||
CONF_USERNAME: self._username,
|
||||
},
|
||||
)
|
||||
|
||||
def _try_connect(self):
|
||||
"""Try to connect and check auth."""
|
||||
tuya = TuyaApi()
|
||||
try:
|
||||
tuya.init(
|
||||
self._username, self._password, self._country_code, self._platform
|
||||
)
|
||||
except (TuyaNetException, TuyaServerException):
|
||||
return RESULT_CONN_ERROR
|
||||
except TuyaAPIException:
|
||||
return RESULT_AUTH_FAILED
|
||||
|
||||
return RESULT_SUCCESS
|
||||
|
||||
async def async_step_import(self, user_input=None):
|
||||
"""Handle configuration by yaml file."""
|
||||
self._is_import = True
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
|
||||
self._country_code = str(user_input[CONF_COUNTRYCODE])
|
||||
self._password = user_input[CONF_PASSWORD]
|
||||
self._platform = user_input[CONF_PLATFORM]
|
||||
self._username = user_input[CONF_USERNAME]
|
||||
|
||||
result = await self.hass.async_add_executor_job(self._try_connect)
|
||||
|
||||
if result == RESULT_SUCCESS:
|
||||
return self._get_entry()
|
||||
if result != RESULT_AUTH_FAILED or self._is_import:
|
||||
if self._is_import:
|
||||
_LOGGER.error(
|
||||
"Error importing from configuration.yaml: %s",
|
||||
RESULT_LOG_MESSAGE.get(result, "Generic Error"),
|
||||
)
|
||||
return self.async_abort(reason=result)
|
||||
errors["base"] = result
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors
|
||||
)
|
14
homeassistant/components/tuya/const.py
Normal file
14
homeassistant/components/tuya/const.py
Normal file
@ -0,0 +1,14 @@
|
||||
"""Constants for the Tuya integration."""
|
||||
|
||||
CONF_COUNTRYCODE = "country_code"
|
||||
|
||||
DOMAIN = "tuya"
|
||||
|
||||
TUYA_DATA = "tuya_data"
|
||||
TUYA_DISCOVERY_NEW = "tuya_discovery_new_{}"
|
||||
|
||||
TUYA_PLATFORMS = {
|
||||
"tuya": "Tuya",
|
||||
"smart_life": "Smart Life",
|
||||
"jinvoo_smart": "Jinvoo Smart",
|
||||
}
|
@ -1,52 +1,75 @@
|
||||
"""Support for Tuya covers."""
|
||||
from homeassistant.components.cover import (
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
ENTITY_ID_FORMAT,
|
||||
SUPPORT_CLOSE,
|
||||
SUPPORT_OPEN,
|
||||
SUPPORT_STOP,
|
||||
CoverEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from . import DATA_TUYA, TuyaDevice
|
||||
from . import TuyaDevice
|
||||
from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up Tuya cover devices."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
tuya = hass.data[DATA_TUYA]
|
||||
dev_ids = discovery_info.get("dev_ids")
|
||||
devices = []
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up tuya sensors dynamically through tuya discovery."""
|
||||
|
||||
platform = config_entry.data[CONF_PLATFORM]
|
||||
|
||||
async def async_discover_sensor(dev_ids):
|
||||
"""Discover and add a discovered tuya sensor."""
|
||||
if not dev_ids:
|
||||
return
|
||||
entities = await hass.async_add_executor_job(
|
||||
_setup_entities, hass, dev_ids, platform,
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
async_dispatcher_connect(
|
||||
hass, TUYA_DISCOVERY_NEW.format(SENSOR_DOMAIN), async_discover_sensor
|
||||
)
|
||||
|
||||
devices_ids = hass.data[DOMAIN]["pending"].pop(SENSOR_DOMAIN)
|
||||
await async_discover_sensor(devices_ids)
|
||||
|
||||
|
||||
def _setup_entities(hass, dev_ids, platform):
|
||||
"""Set up Tuya Cover device."""
|
||||
tuya = hass.data[DOMAIN][TUYA_DATA]
|
||||
entities = []
|
||||
for dev_id in dev_ids:
|
||||
device = tuya.get_device_by_id(dev_id)
|
||||
if device is None:
|
||||
entity = tuya.get_device_by_id(dev_id)
|
||||
if entity is None:
|
||||
continue
|
||||
devices.append(TuyaCover(device))
|
||||
add_entities(devices)
|
||||
entities.append(TuyaCover(entity, platform))
|
||||
return entities
|
||||
|
||||
|
||||
class TuyaCover(TuyaDevice, CoverEntity):
|
||||
"""Tuya cover devices."""
|
||||
|
||||
def __init__(self, tuya):
|
||||
def __init__(self, tuya, platform):
|
||||
"""Init tuya cover device."""
|
||||
super().__init__(tuya)
|
||||
super().__init__(tuya, platform)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id())
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
if self.tuya.support_stop():
|
||||
if self._tuya.support_stop():
|
||||
supported_features |= SUPPORT_STOP
|
||||
return supported_features
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed or not."""
|
||||
state = self.tuya.state()
|
||||
state = self._tuya.state()
|
||||
if state == 1:
|
||||
return False
|
||||
if state == 2:
|
||||
@ -55,12 +78,12 @@ class TuyaCover(TuyaDevice, CoverEntity):
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self.tuya.open_cover()
|
||||
self._tuya.open_cover()
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close cover."""
|
||||
self.tuya.close_cover()
|
||||
self._tuya.close_cover()
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self.tuya.stop_cover()
|
||||
self._tuya.stop_cover()
|
||||
|
@ -1,67 +1,89 @@
|
||||
"""Support for Tuya fans."""
|
||||
from homeassistant.components.fan import (
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
ENTITY_ID_FORMAT,
|
||||
SUPPORT_OSCILLATE,
|
||||
SUPPORT_SET_SPEED,
|
||||
FanEntity,
|
||||
)
|
||||
from homeassistant.const import STATE_OFF
|
||||
from homeassistant.const import CONF_PLATFORM, STATE_OFF
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from . import DATA_TUYA, TuyaDevice
|
||||
from . import TuyaDevice
|
||||
from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up Tuya fan platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
tuya = hass.data[DATA_TUYA]
|
||||
dev_ids = discovery_info.get("dev_ids")
|
||||
devices = []
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up tuya sensors dynamically through tuya discovery."""
|
||||
|
||||
platform = config_entry.data[CONF_PLATFORM]
|
||||
|
||||
async def async_discover_sensor(dev_ids):
|
||||
"""Discover and add a discovered tuya sensor."""
|
||||
if not dev_ids:
|
||||
return
|
||||
entities = await hass.async_add_executor_job(
|
||||
_setup_entities, hass, dev_ids, platform,
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
async_dispatcher_connect(
|
||||
hass, TUYA_DISCOVERY_NEW.format(SENSOR_DOMAIN), async_discover_sensor
|
||||
)
|
||||
|
||||
devices_ids = hass.data[DOMAIN]["pending"].pop(SENSOR_DOMAIN)
|
||||
await async_discover_sensor(devices_ids)
|
||||
|
||||
|
||||
def _setup_entities(hass, dev_ids, platform):
|
||||
"""Set up Tuya Fan device."""
|
||||
tuya = hass.data[DOMAIN][TUYA_DATA]
|
||||
entities = []
|
||||
for dev_id in dev_ids:
|
||||
device = tuya.get_device_by_id(dev_id)
|
||||
if device is None:
|
||||
entity = tuya.get_device_by_id(dev_id)
|
||||
if entity is None:
|
||||
continue
|
||||
devices.append(TuyaFanDevice(device))
|
||||
add_entities(devices)
|
||||
entities.append(TuyaFanDevice(entity, platform))
|
||||
return entities
|
||||
|
||||
|
||||
class TuyaFanDevice(TuyaDevice, FanEntity):
|
||||
"""Tuya fan devices."""
|
||||
|
||||
def __init__(self, tuya):
|
||||
def __init__(self, tuya, platform):
|
||||
"""Init Tuya fan device."""
|
||||
super().__init__(tuya)
|
||||
super().__init__(tuya, platform)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id())
|
||||
self.speeds = [STATE_OFF]
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Create fan list when add to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.speeds.extend(self.tuya.speed_list())
|
||||
self.speeds.extend(self._tuya.speed_list())
|
||||
|
||||
def set_speed(self, speed: str) -> None:
|
||||
"""Set the speed of the fan."""
|
||||
if speed == STATE_OFF:
|
||||
self.turn_off()
|
||||
else:
|
||||
self.tuya.set_speed(speed)
|
||||
self._tuya.set_speed(speed)
|
||||
|
||||
def turn_on(self, speed: str = None, **kwargs) -> None:
|
||||
"""Turn on the fan."""
|
||||
if speed is not None:
|
||||
self.set_speed(speed)
|
||||
else:
|
||||
self.tuya.turn_on()
|
||||
self._tuya.turn_on()
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Turn the entity off."""
|
||||
self.tuya.turn_off()
|
||||
self._tuya.turn_off()
|
||||
|
||||
def oscillate(self, oscillating) -> None:
|
||||
"""Oscillate the fan."""
|
||||
self.tuya.oscillate(oscillating)
|
||||
self._tuya.oscillate(oscillating)
|
||||
|
||||
@property
|
||||
def oscillating(self):
|
||||
@ -70,18 +92,18 @@ class TuyaFanDevice(TuyaDevice, FanEntity):
|
||||
return None
|
||||
if self.speed == STATE_OFF:
|
||||
return False
|
||||
return self.tuya.oscillating()
|
||||
return self._tuya.oscillating()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the entity is on."""
|
||||
return self.tuya.state()
|
||||
return self._tuya.state()
|
||||
|
||||
@property
|
||||
def speed(self) -> str:
|
||||
"""Return the current speed."""
|
||||
if self.is_on:
|
||||
return self.tuya.speed()
|
||||
return self._tuya.speed()
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
@ -93,6 +115,6 @@ class TuyaFanDevice(TuyaDevice, FanEntity):
|
||||
def supported_features(self) -> int:
|
||||
"""Flag supported features."""
|
||||
supports = SUPPORT_SET_SPEED
|
||||
if self.tuya.support_oscillate():
|
||||
if self._tuya.support_oscillate():
|
||||
supports = supports | SUPPORT_OSCILLATE
|
||||
return supports
|
||||
|
@ -3,58 +3,81 @@ from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_HS_COLOR,
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
ENTITY_ID_FORMAT,
|
||||
SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR,
|
||||
SUPPORT_COLOR_TEMP,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.util import color as colorutil
|
||||
|
||||
from . import DATA_TUYA, TuyaDevice
|
||||
from . import TuyaDevice
|
||||
from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up Tuya light platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
tuya = hass.data[DATA_TUYA]
|
||||
dev_ids = discovery_info.get("dev_ids")
|
||||
devices = []
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up tuya sensors dynamically through tuya discovery."""
|
||||
|
||||
platform = config_entry.data[CONF_PLATFORM]
|
||||
|
||||
async def async_discover_sensor(dev_ids):
|
||||
"""Discover and add a discovered tuya sensor."""
|
||||
if not dev_ids:
|
||||
return
|
||||
entities = await hass.async_add_executor_job(
|
||||
_setup_entities, hass, dev_ids, platform,
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
async_dispatcher_connect(
|
||||
hass, TUYA_DISCOVERY_NEW.format(SENSOR_DOMAIN), async_discover_sensor
|
||||
)
|
||||
|
||||
devices_ids = hass.data[DOMAIN]["pending"].pop(SENSOR_DOMAIN)
|
||||
await async_discover_sensor(devices_ids)
|
||||
|
||||
|
||||
def _setup_entities(hass, dev_ids, platform):
|
||||
"""Set up Tuya Light device."""
|
||||
tuya = hass.data[DOMAIN][TUYA_DATA]
|
||||
entities = []
|
||||
for dev_id in dev_ids:
|
||||
device = tuya.get_device_by_id(dev_id)
|
||||
if device is None:
|
||||
entity = tuya.get_device_by_id(dev_id)
|
||||
if entity is None:
|
||||
continue
|
||||
devices.append(TuyaLight(device))
|
||||
add_entities(devices)
|
||||
entities.append(TuyaLight(entity, platform))
|
||||
return entities
|
||||
|
||||
|
||||
class TuyaLight(TuyaDevice, LightEntity):
|
||||
"""Tuya light device."""
|
||||
|
||||
def __init__(self, tuya):
|
||||
def __init__(self, tuya, platform):
|
||||
"""Init Tuya light device."""
|
||||
super().__init__(tuya)
|
||||
super().__init__(tuya, platform)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id())
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of the light."""
|
||||
if self.tuya.brightness() is None:
|
||||
if self._tuya.brightness() is None:
|
||||
return None
|
||||
return int(self.tuya.brightness())
|
||||
return int(self._tuya.brightness())
|
||||
|
||||
@property
|
||||
def hs_color(self):
|
||||
"""Return the hs_color of the light."""
|
||||
return tuple(map(int, self.tuya.hs_color()))
|
||||
return tuple(map(int, self._tuya.hs_color()))
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
"""Return the color_temp of the light."""
|
||||
color_temp = int(self.tuya.color_temp())
|
||||
color_temp = int(self._tuya.color_temp())
|
||||
if color_temp is None:
|
||||
return None
|
||||
return colorutil.color_temperature_kelvin_to_mired(color_temp)
|
||||
@ -62,17 +85,17 @@ class TuyaLight(TuyaDevice, LightEntity):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if light is on."""
|
||||
return self.tuya.state()
|
||||
return self._tuya.state()
|
||||
|
||||
@property
|
||||
def min_mireds(self):
|
||||
"""Return color temperature min mireds."""
|
||||
return colorutil.color_temperature_kelvin_to_mired(self.tuya.min_color_temp())
|
||||
return colorutil.color_temperature_kelvin_to_mired(self._tuya.min_color_temp())
|
||||
|
||||
@property
|
||||
def max_mireds(self):
|
||||
"""Return color temperature max mireds."""
|
||||
return colorutil.color_temperature_kelvin_to_mired(self.tuya.max_color_temp())
|
||||
return colorutil.color_temperature_kelvin_to_mired(self._tuya.max_color_temp())
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn on or control the light."""
|
||||
@ -81,27 +104,27 @@ class TuyaLight(TuyaDevice, LightEntity):
|
||||
and ATTR_HS_COLOR not in kwargs
|
||||
and ATTR_COLOR_TEMP not in kwargs
|
||||
):
|
||||
self.tuya.turn_on()
|
||||
self._tuya.turn_on()
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self.tuya.set_brightness(kwargs[ATTR_BRIGHTNESS])
|
||||
self._tuya.set_brightness(kwargs[ATTR_BRIGHTNESS])
|
||||
if ATTR_HS_COLOR in kwargs:
|
||||
self.tuya.set_color(kwargs[ATTR_HS_COLOR])
|
||||
self._tuya.set_color(kwargs[ATTR_HS_COLOR])
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
color_temp = colorutil.color_temperature_mired_to_kelvin(
|
||||
kwargs[ATTR_COLOR_TEMP]
|
||||
)
|
||||
self.tuya.set_color_temp(color_temp)
|
||||
self._tuya.set_color_temp(color_temp)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Instruct the light to turn off."""
|
||||
self.tuya.turn_off()
|
||||
self._tuya.turn_off()
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supports = SUPPORT_BRIGHTNESS
|
||||
if self.tuya.support_color():
|
||||
if self._tuya.support_color():
|
||||
supports = supports | SUPPORT_COLOR
|
||||
if self.tuya.support_color_temp():
|
||||
if self._tuya.support_color_temp():
|
||||
supports = supports | SUPPORT_COLOR_TEMP
|
||||
return supports
|
||||
|
@ -3,5 +3,6 @@
|
||||
"name": "Tuya",
|
||||
"documentation": "https://www.home-assistant.io/integrations/tuya",
|
||||
"requirements": ["tuyaha==0.0.6"],
|
||||
"codeowners": []
|
||||
"codeowners": ["@ollo69"],
|
||||
"config_flow": true
|
||||
}
|
||||
|
@ -1,38 +1,60 @@
|
||||
"""Support for the Tuya scenes."""
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.scene import DOMAIN, Scene
|
||||
from homeassistant.components.scene import DOMAIN as SENSOR_DOMAIN, Scene
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from . import DATA_TUYA, TuyaDevice
|
||||
from . import TuyaDevice
|
||||
from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
ENTITY_ID_FORMAT = SENSOR_DOMAIN + ".{}"
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up Tuya scenes."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
tuya = hass.data[DATA_TUYA]
|
||||
dev_ids = discovery_info.get("dev_ids")
|
||||
devices = []
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up tuya sensors dynamically through tuya discovery."""
|
||||
|
||||
platform = config_entry.data[CONF_PLATFORM]
|
||||
|
||||
async def async_discover_sensor(dev_ids):
|
||||
"""Discover and add a discovered tuya sensor."""
|
||||
if not dev_ids:
|
||||
return
|
||||
entities = await hass.async_add_executor_job(
|
||||
_setup_entities, hass, dev_ids, platform,
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
async_dispatcher_connect(
|
||||
hass, TUYA_DISCOVERY_NEW.format(SENSOR_DOMAIN), async_discover_sensor
|
||||
)
|
||||
|
||||
devices_ids = hass.data[DOMAIN]["pending"].pop(SENSOR_DOMAIN)
|
||||
await async_discover_sensor(devices_ids)
|
||||
|
||||
|
||||
def _setup_entities(hass, dev_ids, platform):
|
||||
"""Set up Tuya Scene."""
|
||||
tuya = hass.data[DOMAIN][TUYA_DATA]
|
||||
entities = []
|
||||
for dev_id in dev_ids:
|
||||
device = tuya.get_device_by_id(dev_id)
|
||||
if device is None:
|
||||
entity = tuya.get_device_by_id(dev_id)
|
||||
if entity is None:
|
||||
continue
|
||||
devices.append(TuyaScene(device))
|
||||
add_entities(devices)
|
||||
entities.append(TuyaScene(entity, platform))
|
||||
return entities
|
||||
|
||||
|
||||
class TuyaScene(TuyaDevice, Scene):
|
||||
"""Tuya Scene."""
|
||||
|
||||
def __init__(self, tuya):
|
||||
def __init__(self, tuya, platform):
|
||||
"""Init Tuya scene."""
|
||||
super().__init__(tuya)
|
||||
super().__init__(tuya, platform)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id())
|
||||
|
||||
def activate(self, **kwargs: Any) -> None:
|
||||
"""Activate the scene."""
|
||||
self.tuya.activate()
|
||||
self._tuya.activate()
|
||||
|
26
homeassistant/components/tuya/strings.json
Normal file
26
homeassistant/components/tuya/strings.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Tuya configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Tuya",
|
||||
"description": "Enter your Tuya credential.",
|
||||
"data": {
|
||||
"country_code": "Your account country code (e.g., 1 for USA or 86 for China)",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"platform": "The app where your account register",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_in_progress": "Tuya configuration is already in progress.",
|
||||
"auth_failed": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"conn_error": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||
},
|
||||
"error": {
|
||||
"auth_failed": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,43 +1,69 @@
|
||||
"""Support for Tuya switches."""
|
||||
from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity
|
||||
from homeassistant.components.switch import (
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
ENTITY_ID_FORMAT,
|
||||
SwitchEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from . import DATA_TUYA, TuyaDevice
|
||||
from . import TuyaDevice
|
||||
from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up tuya sensors dynamically through tuya discovery."""
|
||||
|
||||
platform = config_entry.data[CONF_PLATFORM]
|
||||
|
||||
async def async_discover_sensor(dev_ids):
|
||||
"""Discover and add a discovered tuya sensor."""
|
||||
if not dev_ids:
|
||||
return
|
||||
entities = await hass.async_add_executor_job(
|
||||
_setup_entities, hass, dev_ids, platform,
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
async_dispatcher_connect(
|
||||
hass, TUYA_DISCOVERY_NEW.format(SENSOR_DOMAIN), async_discover_sensor
|
||||
)
|
||||
|
||||
devices_ids = hass.data[DOMAIN]["pending"].pop(SENSOR_DOMAIN)
|
||||
await async_discover_sensor(devices_ids)
|
||||
|
||||
|
||||
def _setup_entities(hass, dev_ids, platform):
|
||||
"""Set up Tuya Switch device."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
tuya = hass.data[DATA_TUYA]
|
||||
dev_ids = discovery_info.get("dev_ids")
|
||||
devices = []
|
||||
tuya = hass.data[DOMAIN][TUYA_DATA]
|
||||
entities = []
|
||||
for dev_id in dev_ids:
|
||||
device = tuya.get_device_by_id(dev_id)
|
||||
if device is None:
|
||||
entity = tuya.get_device_by_id(dev_id)
|
||||
if entity is None:
|
||||
continue
|
||||
devices.append(TuyaSwitch(device))
|
||||
add_entities(devices)
|
||||
entities.append(TuyaSwitch(entity, platform))
|
||||
return entities
|
||||
|
||||
|
||||
class TuyaSwitch(TuyaDevice, SwitchEntity):
|
||||
"""Tuya Switch Device."""
|
||||
|
||||
def __init__(self, tuya):
|
||||
def __init__(self, tuya, platform):
|
||||
"""Init Tuya switch device."""
|
||||
super().__init__(tuya)
|
||||
super().__init__(tuya, platform)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id())
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if switch is on."""
|
||||
return self.tuya.state()
|
||||
return self._tuya.state()
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the switch on."""
|
||||
self.tuya.turn_on()
|
||||
self._tuya.turn_on()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
self.tuya.turn_off()
|
||||
self._tuya.turn_off()
|
||||
|
26
homeassistant/components/tuya/translations/en.json
Normal file
26
homeassistant/components/tuya/translations/en.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_in_progress": "Tuya configuration is already in progress.",
|
||||
"auth_failed": "Configured Tuya credential are incorrect.",
|
||||
"conn_error": "Connection to Tuya failed.",
|
||||
"single_instance_allowed": "Only a single configuration of Tuya is allowed."
|
||||
},
|
||||
"error": {
|
||||
"auth_failed": "Provided credential are incorrect."
|
||||
},
|
||||
"flow_title": "Tuya configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"country_code": "Your account country code (e.g., 1 for USA or 86 for China)",
|
||||
"password": "Your password to log in to Tuya",
|
||||
"platform": "The app where your account register",
|
||||
"username": "Your username to log in to Tuya"
|
||||
},
|
||||
"description": "Enter your Tuya credential.",
|
||||
"title": "Tuya"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -141,6 +141,7 @@ FLOWS = [
|
||||
"traccar",
|
||||
"tradfri",
|
||||
"transmission",
|
||||
"tuya",
|
||||
"twentemilieu",
|
||||
"twilio",
|
||||
"unifi",
|
||||
|
@ -828,6 +828,9 @@ total_connect_client==0.54.1
|
||||
# homeassistant.components.transmission
|
||||
transmissionrpc==0.11
|
||||
|
||||
# homeassistant.components.tuya
|
||||
tuyaha==0.0.6
|
||||
|
||||
# homeassistant.components.twentemilieu
|
||||
twentemilieu==0.3.0
|
||||
|
||||
|
1
tests/components/tuya/__init__.py
Normal file
1
tests/components/tuya/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Tuya component."""
|
147
tests/components/tuya/test_config_flow.py
Normal file
147
tests/components/tuya/test_config_flow.py
Normal file
@ -0,0 +1,147 @@
|
||||
"""Tests for the Tuya config flow."""
|
||||
import pytest
|
||||
from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow, setup
|
||||
from homeassistant.components.tuya.const import CONF_COUNTRYCODE, DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME
|
||||
|
||||
from tests.async_mock import Mock, patch
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
USERNAME = "myUsername"
|
||||
PASSWORD = "myPassword"
|
||||
COUNTRY_CODE = "1"
|
||||
TUYA_PLATFORM = "tuya"
|
||||
|
||||
TUYA_USER_DATA = {
|
||||
CONF_USERNAME: USERNAME,
|
||||
CONF_PASSWORD: PASSWORD,
|
||||
CONF_COUNTRYCODE: COUNTRY_CODE,
|
||||
CONF_PLATFORM: TUYA_PLATFORM,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(name="tuya")
|
||||
def tuya_fixture() -> Mock:
|
||||
"""Patch libraries."""
|
||||
with patch("homeassistant.components.tuya.config_flow.TuyaApi") as tuya:
|
||||
yield tuya
|
||||
|
||||
|
||||
async def test_user(hass, tuya):
|
||||
"""Test user config."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tuya.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.tuya.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=TUYA_USER_DATA
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == USERNAME
|
||||
assert result["data"][CONF_USERNAME] == USERNAME
|
||||
assert result["data"][CONF_PASSWORD] == PASSWORD
|
||||
assert result["data"][CONF_COUNTRYCODE] == COUNTRY_CODE
|
||||
assert result["data"][CONF_PLATFORM] == TUYA_PLATFORM
|
||||
assert not result["result"].unique_id
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_import(hass, tuya):
|
||||
"""Test import step."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
with patch(
|
||||
"homeassistant.components.tuya.async_setup", return_value=True,
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.tuya.async_setup_entry", return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data=TUYA_USER_DATA,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == USERNAME
|
||||
assert result["data"][CONF_USERNAME] == USERNAME
|
||||
assert result["data"][CONF_PASSWORD] == PASSWORD
|
||||
assert result["data"][CONF_COUNTRYCODE] == COUNTRY_CODE
|
||||
assert result["data"][CONF_PLATFORM] == TUYA_PLATFORM
|
||||
assert not result["result"].unique_id
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_abort_if_already_setup(hass, tuya):
|
||||
"""Test we abort if Tuya is already setup."""
|
||||
MockConfigEntry(domain=DOMAIN, data=TUYA_USER_DATA).add_to_hass(hass)
|
||||
|
||||
# Should fail, config exist (import)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=TUYA_USER_DATA
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "single_instance_allowed"
|
||||
|
||||
# Should fail, config exist (flow)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "single_instance_allowed"
|
||||
|
||||
|
||||
async def test_abort_on_invalid_credentials(hass, tuya):
|
||||
"""Test when we have invalid credentials."""
|
||||
tuya().init.side_effect = TuyaAPIException("Boom")
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=TUYA_USER_DATA
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "auth_failed"}
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "auth_failed"
|
||||
|
||||
|
||||
async def test_abort_on_connection_error(hass, tuya):
|
||||
"""Test when we have a network error."""
|
||||
tuya().init.side_effect = TuyaNetException("Boom")
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=TUYA_USER_DATA
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "conn_error"
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "conn_error"
|
Loading…
x
Reference in New Issue
Block a user