From 683c1527d5207d493b5589ba037c2bd02f1fe95d Mon Sep 17 00:00:00 2001 From: ollo69 Date: Sat, 9 May 2020 16:23:27 +0200 Subject: [PATCH 01/17] Added Tuya config flow --- homeassistant/components/tuya/__init__.py | 207 ++++++++++++------ homeassistant/components/tuya/climate.py | 87 +++++--- homeassistant/components/tuya/config_flow.py | 91 ++++++++ homeassistant/components/tuya/const.py | 14 ++ homeassistant/components/tuya/cover.py | 54 +++-- homeassistant/components/tuya/fan.py | 63 ++++-- homeassistant/components/tuya/light.py | 72 +++--- homeassistant/components/tuya/manifest.json | 3 +- homeassistant/components/tuya/scene.py | 49 +++-- homeassistant/components/tuya/strings.json | 25 +++ homeassistant/components/tuya/switch.py | 53 +++-- .../components/tuya/translations/en.json | 25 +++ homeassistant/generated/config_flows.py | 1 + 13 files changed, 554 insertions(+), 190 deletions(-) create mode 100644 homeassistant/components/tuya/config_flow.py create mode 100644 homeassistant/components/tuya/const.py create mode 100644 homeassistant/components/tuya/strings.json create mode 100644 homeassistant/components/tuya/translations/en.json diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index c50e6787d898c1..a6172f8b75b518 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -1,4 +1,5 @@ """Support for Tuya Smart devices.""" +import asyncio from datetime import timedelta import logging @@ -6,32 +7,38 @@ 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" +CONFIG_ENTRY_IS_SETUP = "tuya_config_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,6 +48,8 @@ "switch": "switch", } +TUYA_TRACKER = "tuya_tracker" + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -56,33 +65,35 @@ ) -def setup(hass, config, retry_delay=FIRST_RETRY_TIME): - """Set up Tuya Component.""" - - _LOGGER.debug("Setting up integration") +async def async_setup(hass, config): + """Set up the Tuya integration.""" - tuya = TuyaApi() - username = config[DOMAIN][CONF_USERNAME] - password = config[DOMAIN][CONF_PASSWORD] - country_code = config[DOMAIN][CONF_COUNTRYCODE] - platform = config[DOMAIN][CONF_PLATFORM] + 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 + ) + ) - try: - tuya.init(username, password, country_code, platform) - except (TuyaNetException, TuyaServerException): + return True - _LOGGER.warning( - "Connection error during integration setup. Will retry in %s seconds", - retry_delay, - ) - 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)) +async def async_setup_entry(hass, entry): + """Set up Tuya platform.""" - call_later(hass, retry_delay, retry_setup) + tuya = TuyaApi() + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + country_code = entry.data[CONF_COUNTRYCODE] + platform = entry.data[CONF_PLATFORM] - return True + try: + await hass.async_add_executor_job( + tuya.init, username, password, country_code, platform + ) + except (TuyaNetException, TuyaServerException) as exc: + raise ConfigEntryNotReady() from exc except TuyaAPIException as exc: _LOGGER.error( @@ -90,10 +101,14 @@ def retry_setup(now): ) return False - hass.data[DATA_TUYA] = tuya - hass.data[DOMAIN] = {"entities": {}} + hass.data[TUYA_DATA] = tuya + hass.data[CONFIG_ENTRY_IS_SETUP] = set() + hass.data[DOMAIN] = { + "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 +122,100 @@ def load_devices(device_list): 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) - device_list = tuya.get_all_devices() - load_devices(device_list) - - def poll_devices_update(event_time): + for ha_type, dev_ids in device_type_list.items(): + config_entries_key = f"{ha_type}.tuya" + if config_entries_key not in hass.data[CONFIG_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[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) + else: + async_dispatcher_send(hass, TUYA_DISCOVERY_NEW.format(ha_type), dev_ids) + + device_list = await hass.async_add_executor_job(tuya.get_all_devices) + await async_load_devices(device_list) + + 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() + await hass.async_add_executor_job(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(tuya.get_all_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[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[CONFIG_ENTRY_IS_SETUP] + ] + ) + ) + if unload_ok: + hass.data[CONFIG_ENTRY_IS_SETUP] = set() + hass.data[TUYA_TRACKER]() + hass.data[TUYA_TRACKER] = None + hass.data[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 + + +async def cleanup_device_registry(hass, device_id): + """Remove device registry entry if there are no remaining entities.""" + + device_registry = await hass.helpers.device_registry.async_get_registry() + entity_registry = await hass.helpers.entity_registry.async_get_registry() + if device_id and not hass.helpers.entity_registry.async_entries_for_device( + entity_registry, device_id + ): + device_registry.async_remove_device(device_id) + + 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,34 +223,53 @@ async def async_added_to_hass(self): @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_entry = entity_registry.async_get(self.entity_id) + entity_registry.async_remove(self.entity_id) + await cleanup_device_registry(self.hass, entity_entry.device_id) + else: + await self.async_remove() @callback - def _update_callback(self): + async def _update_callback(self): """Call update method.""" self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index d2f7dba4c334ef..ef09d91d7d4cc2 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -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.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,50 @@ 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") +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 + await _async_setup_entity(hass, async_add_entities, dev_ids, platform) + + 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) + + +async def _async_setup_entity(hass, async_add_entities, dev_ids, platform): + """Set up Tuya Climate device.""" + tuya = hass.data[TUYA_DATA] devices = [] for dev_id in dev_ids: - device = tuya.get_device_by_id(dev_id) + device = await hass.async_add_executor_job(tuya.get_device_by_id, dev_id) if device is None: continue - devices.append(TuyaClimateEntity(device)) - add_entities(devices) + devices.append(TuyaClimateEntity(device, platform)) + async_add_entities(devices) 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 +103,7 @@ def precision(self): @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 +111,10 @@ def temperature_unit(self): @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 +127,63 @@ def hvac_modes(self): @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() diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py new file mode 100644 index 00000000000000..5f4414284f4504 --- /dev/null +++ b/homeassistant/components/tuya/config_flow.py @@ -0,0 +1,91 @@ +"""Config flow for Tuya.""" +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 + +from .const import CONF_COUNTRYCODE, DOMAIN, TUYA_PLATFORMS + +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_NOT_FOUND = "not_found" +RESULT_SUCCESS = "success" + + +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 + + 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_NOT_FOUND + except TuyaAPIException: + return RESULT_AUTH_FAILED + + return RESULT_SUCCESS + + async def async_step_import(self, user_input=None): + """Handle configuration by yaml file.""" + 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: + return self.async_abort(reason=result) + errors["base"] = result + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors + ) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py new file mode 100644 index 00000000000000..4e395750b235b2 --- /dev/null +++ b/homeassistant/components/tuya/const.py @@ -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", +} diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index d7528cf609222f..438e7b2f286da6 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -1,52 +1,72 @@ """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") +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 + await _async_setup_entity(hass, async_add_entities, dev_ids, platform) + + 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) + + +async def _async_setup_entity(hass, async_add_entities, dev_ids, platform): + """Set up Tuya Switch device.""" + tuya = hass.data[TUYA_DATA] devices = [] for dev_id in dev_ids: - device = tuya.get_device_by_id(dev_id) + device = await hass.async_add_executor_job(tuya.get_device_by_id, dev_id) if device is None: continue - devices.append(TuyaCover(device)) - add_entities(devices) + devices.append(TuyaCover(device, platform)) + async_add_entities(devices) 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 +75,12 @@ def is_closed(self): 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() diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 90cf452db5b31b..525a9f3adb7229 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -1,67 +1,86 @@ """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") +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 + await _async_setup_entity(hass, async_add_entities, dev_ids, platform) + + 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) + + +async def _async_setup_entity(hass, async_add_entities, dev_ids, platform): + """Set up Tuya Switch device.""" + tuya = hass.data[TUYA_DATA] devices = [] for dev_id in dev_ids: - device = tuya.get_device_by_id(dev_id) + device = await hass.async_add_executor_job(tuya.get_device_by_id, dev_id) if device is None: continue - devices.append(TuyaFanDevice(device)) - add_entities(devices) + devices.append(TuyaFanDevice(device, platform)) + async_add_entities(devices) 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 +89,18 @@ def oscillating(self): 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 +112,6 @@ def speed_list(self) -> list: 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 diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 2959b9239bd937..1ae4fcfa57dc12 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -3,58 +3,78 @@ 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") +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 + await _async_setup_entity(hass, async_add_entities, dev_ids, platform) + + 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) + + +async def _async_setup_entity(hass, async_add_entities, dev_ids, platform): + """Set up Tuya Switch device.""" + tuya = hass.data[TUYA_DATA] devices = [] for dev_id in dev_ids: - device = tuya.get_device_by_id(dev_id) + device = await hass.async_add_executor_job(tuya.get_device_by_id, dev_id) if device is None: continue - devices.append(TuyaLight(device)) - add_entities(devices) + devices.append(TuyaLight(device, platform)) + async_add_entities(devices) 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 +82,17 @@ def color_temp(self): @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 +101,27 @@ def turn_on(self, **kwargs): 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 diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 1279f0a2f664c2..92749741f0aedf 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -3,5 +3,6 @@ "name": "Tuya", "documentation": "https://www.home-assistant.io/integrations/tuya", "requirements": ["tuyaha==0.0.6"], - "codeowners": [] + "codeowners": [], + "config_flow": true } diff --git a/homeassistant/components/tuya/scene.py b/homeassistant/components/tuya/scene.py index 71d83417ca8df9..90793dba434bbe 100644 --- a/homeassistant/components/tuya/scene.py +++ b/homeassistant/components/tuya/scene.py @@ -1,38 +1,57 @@ """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") +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 + await _async_setup_entity(hass, async_add_entities, dev_ids, platform) + + 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) + + +async def _async_setup_entity(hass, async_add_entities, dev_ids, platform): + """Set up Tuya Switch device.""" + tuya = hass.data[TUYA_DATA] devices = [] for dev_id in dev_ids: - device = tuya.get_device_by_id(dev_id) + device = await hass.async_add_executor_job(tuya.get_device_by_id, dev_id) if device is None: continue - devices.append(TuyaScene(device)) - add_entities(devices) + devices.append(TuyaScene(device, platform)) + async_add_entities(devices) 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() diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json new file mode 100644 index 00000000000000..af18a22ee10334 --- /dev/null +++ b/homeassistant/components/tuya/strings.json @@ -0,0 +1,25 @@ +{ + "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": "Your password to log in to Tuya", + "platform": "The app where your account register", + "username": "Your username to log in to Tuya" + } + } + }, + "abort": { + "already_in_progress": "Tuya configuration is already in progress.", + "not_found": "Connection to Tuya failed.", + "single_instance_allowed": "Only a single configuration of Tuya is allowed." + }, + "error": { + "auth_failed": "Provided credential are incorrect." + } + } +} diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 30684c16da560e..03ed42d4f515fa 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -1,43 +1,66 @@ """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 + await _async_setup_entity(hass, async_add_entities, dev_ids, platform) + + 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) + + +async def _async_setup_entity(hass, async_add_entities, 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") + tuya = hass.data[TUYA_DATA] devices = [] for dev_id in dev_ids: - device = tuya.get_device_by_id(dev_id) + device = await hass.async_add_executor_job(tuya.get_device_by_id, dev_id) if device is None: continue - devices.append(TuyaSwitch(device)) - add_entities(devices) + devices.append(TuyaSwitch(device, platform)) + async_add_entities(devices) 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() diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json new file mode 100644 index 00000000000000..1821c548600915 --- /dev/null +++ b/homeassistant/components/tuya/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_in_progress": "Tuya configuration is already in progress.", + "not_found": "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" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 9189af4cdd31da..27f8260f112bdc 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -135,6 +135,7 @@ "traccar", "tradfri", "transmission", + "tuya", "twentemilieu", "twilio", "unifi", From 85b21de182f360633b956d49bf927b6b49c791a0 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sat, 9 May 2020 19:04:47 +0200 Subject: [PATCH 02/17] Added test config_flow --- homeassistant/components/tuya/config_flow.py | 9 +- homeassistant/components/tuya/strings.json | 1 + .../components/tuya/translations/en.json | 1 + tests/components/tuya/__init__.py | 1 + tests/components/tuya/test_config_flow.py | 176 ++++++++++++++++++ 5 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 tests/components/tuya/__init__.py create mode 100644 tests/components/tuya/test_config_flow.py diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index 5f4414284f4504..f147b613347454 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Tuya.""" +import logging from tuyaha import TuyaApi from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException, TuyaServerException import voluptuous as vol @@ -8,6 +9,8 @@ from .const import CONF_COUNTRYCODE, DOMAIN, TUYA_PLATFORMS +_LOGGER = logging.getLogger(__name__) + DATA_SCHEMA_USER = vol.Schema( { vol.Required(CONF_USERNAME): str, @@ -34,6 +37,7 @@ def __init__(self): self._password = None self._platform = None self._username = None + self._is_import = False def _get_entry(self): return self.async_create_entry( @@ -62,6 +66,7 @@ def _try_connect(self): 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): @@ -82,7 +87,9 @@ async def async_step_user(self, user_input=None): if result == RESULT_SUCCESS: return self._get_entry() - if result != RESULT_AUTH_FAILED: + if result != RESULT_AUTH_FAILED or self._is_import: + if self._is_import: + _LOGGER.error("Error importing configuration") return self.async_abort(reason=result) errors["base"] = result diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index af18a22ee10334..79b5844c648c31 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -15,6 +15,7 @@ }, "abort": { "already_in_progress": "Tuya configuration is already in progress.", + "auth_failed": "Configured Tuya credential are incorrect.", "not_found": "Connection to Tuya failed.", "single_instance_allowed": "Only a single configuration of Tuya is allowed." }, diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json index 1821c548600915..8d1a69956ab3d7 100644 --- a/homeassistant/components/tuya/translations/en.json +++ b/homeassistant/components/tuya/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_in_progress": "Tuya configuration is already in progress.", + "auth_failed": "Configured Tuya credential are incorrect.", "not_found": "Connection to Tuya failed.", "single_instance_allowed": "Only a single configuration of Tuya is allowed." }, diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py new file mode 100644 index 00000000000000..56bfc0867c640d --- /dev/null +++ b/tests/components/tuya/__init__.py @@ -0,0 +1 @@ +"""Tests for the Tuya component.""" diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py new file mode 100644 index 00000000000000..fd1669e56f94ca --- /dev/null +++ b/tests/components/tuya/test_config_flow.py @@ -0,0 +1,176 @@ +"""Tests for the Tuya config flow.""" +from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.tuya import config_flow +from homeassistant.components.tuya.const import CONF_COUNTRYCODE, DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +USERNAME = "myUsername" +PASSWORD = "myPassword" +COUNTRY_CODE = "1" +TUYA_PLATFORM = "tuya" + + +@pytest.fixture(name="account") +def mock_controller_login(): + """Mock a successful login.""" + with patch("homeassistant.components.tuya.config_flow.Account", return_value=True): + yield + + +def init_config_flow(hass): + """Init a configuration flow.""" + flow = config_flow.TuyaConfigFlow() + flow.hass = hass + return flow + + +async def test_user(hass, account): + """Test user config.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await flow.async_step_user( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_COUNTRYCODE: COUNTRY_CODE, + CONF_PLATFORM: TUYA_PLATFORM, + } + ) + + 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 + + +async def test_import(hass, account): + """Test import step.""" + flow = init_config_flow(hass) + + result = await flow.async_step_import( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_COUNTRYCODE: COUNTRY_CODE, + CONF_PLATFORM: TUYA_PLATFORM, + } + ) + + 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 + + +async def test_abort_if_already_setup(hass, account): + """Test we abort if Neato is already setup.""" + flow = init_config_flow(hass) + MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_COUNTRYCODE: COUNTRY_CODE, + CONF_PLATFORM: TUYA_PLATFORM, + }, + ).add_to_hass(hass) + + # Should fail, config exist (import) + result = await flow.async_step_import( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_COUNTRYCODE: COUNTRY_CODE, + CONF_PLATFORM: TUYA_PLATFORM, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + + # Should fail, config exist (flow) + result = await flow.async_step_user( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_COUNTRYCODE: COUNTRY_CODE, + CONF_PLATFORM: TUYA_PLATFORM, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + + +async def test_abort_on_invalid_credentials(hass): + """Test when we have invalid credentials.""" + flow = init_config_flow(hass) + + with patch( + "homeassistant.components.tuya.config_flow.Account", + side_effect=TuyaAPIException(), + ): + result = await flow.async_step_user( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_COUNTRYCODE: COUNTRY_CODE, + CONF_PLATFORM: TUYA_PLATFORM, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "auth_failed"} + + result = await flow.async_step_import( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_COUNTRYCODE: COUNTRY_CODE, + CONF_PLATFORM: TUYA_PLATFORM, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "auth_failed" + + +async def test_abort_on_connection_error(hass): + """Test when we have a network error.""" + flow = init_config_flow(hass) + + with patch( + "homeassistant.components.tuya.config_flow.Account", + side_effect=TuyaNetException(), + ): + result = await flow.async_step_user( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_COUNTRYCODE: COUNTRY_CODE, + CONF_PLATFORM: TUYA_PLATFORM, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "not_found" + + result = await flow.async_step_import( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_COUNTRYCODE: COUNTRY_CODE, + CONF_PLATFORM: TUYA_PLATFORM, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "not_found" From 2788f5072f79ce0957342c7c4ffbeb811baab664 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sat, 9 May 2020 19:23:05 +0200 Subject: [PATCH 03/17] Fixed log error message --- homeassistant/components/tuya/config_flow.py | 14 +++++++++++--- homeassistant/components/tuya/strings.json | 2 +- homeassistant/components/tuya/translations/en.json | 2 +- tests/components/tuya/test_config_flow.py | 4 ++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index f147b613347454..a052fd88c0529a 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -21,9 +21,14 @@ ) RESULT_AUTH_FAILED = "auth_failed" -RESULT_NOT_FOUND = "not_found" +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.""" @@ -58,7 +63,7 @@ def _try_connect(self): self._username, self._password, self._country_code, self._platform ) except (TuyaNetException, TuyaServerException): - return RESULT_NOT_FOUND + return RESULT_CONN_ERROR except TuyaAPIException: return RESULT_AUTH_FAILED @@ -89,7 +94,10 @@ async def async_step_user(self, user_input=None): return self._get_entry() if result != RESULT_AUTH_FAILED or self._is_import: if self._is_import: - _LOGGER.error("Error importing configuration") + _LOGGER.error( + "Error importing from configuration.yaml: %s", + RESULT_LOG_MESSAGE.get(result, "Generic Error") + ) return self.async_abort(reason=result) errors["base"] = result diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 79b5844c648c31..686d7f3db2d7a4 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -16,7 +16,7 @@ "abort": { "already_in_progress": "Tuya configuration is already in progress.", "auth_failed": "Configured Tuya credential are incorrect.", - "not_found": "Connection to Tuya failed.", + "conn_error": "Connection to Tuya failed.", "single_instance_allowed": "Only a single configuration of Tuya is allowed." }, "error": { diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json index 8d1a69956ab3d7..e70c9c1081538d 100644 --- a/homeassistant/components/tuya/translations/en.json +++ b/homeassistant/components/tuya/translations/en.json @@ -3,7 +3,7 @@ "abort": { "already_in_progress": "Tuya configuration is already in progress.", "auth_failed": "Configured Tuya credential are incorrect.", - "not_found": "Connection to Tuya failed.", + "conn_error": "Connection to Tuya failed.", "single_instance_allowed": "Only a single configuration of Tuya is allowed." }, "error": { diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index fd1669e56f94ca..5ed651b157b8c9 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -162,7 +162,7 @@ async def test_abort_on_connection_error(hass): } ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "not_found" + assert result["reason"] == "conn_error" result = await flow.async_step_import( { @@ -173,4 +173,4 @@ async def test_abort_on_connection_error(hass): } ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "not_found" + assert result["reason"] == "conn_error" From 99a8b978c9557884e7ff9b0ef72d19b4cd1e7d5e Mon Sep 17 00:00:00 2001 From: ollo69 Date: Sat, 9 May 2020 19:49:00 +0200 Subject: [PATCH 04/17] Add test requirements --- requirements_test_all.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b83aa29a53b67..aa4e277d0fdbb6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -810,6 +810,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 From 64684ba25596d3b004f52d20fa0191f975c8eed0 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sat, 9 May 2020 20:16:53 +0200 Subject: [PATCH 05/17] Lint Fix --- homeassistant/components/tuya/config_flow.py | 1 + tests/components/tuya/test_config_flow.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index a052fd88c0529a..9916e593956d88 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -1,5 +1,6 @@ """Config flow for Tuya.""" import logging + from tuyaha import TuyaApi from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException, TuyaServerException import voluptuous as vol diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index 5ed651b157b8c9..52a5519c29e701 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -1,6 +1,6 @@ """Tests for the Tuya config flow.""" -from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException import pytest +from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException from homeassistant import data_entry_flow from homeassistant.components.tuya import config_flow From d76245faf692acf4d88f7c3342fffbedb6c40a3f Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sat, 9 May 2020 20:25:31 +0200 Subject: [PATCH 06/17] Fix Black formatting --- homeassistant/components/tuya/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index 9916e593956d88..9a2bf3b57ec78c 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -97,7 +97,7 @@ async def async_step_user(self, user_input=None): if self._is_import: _LOGGER.error( "Error importing from configuration.yaml: %s", - RESULT_LOG_MESSAGE.get(result, "Generic Error") + RESULT_LOG_MESSAGE.get(result, "Generic Error"), ) return self.async_abort(reason=result) errors["base"] = result From a816565ca417ce3e2eada139b5a6de000ca4cb72 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sat, 9 May 2020 21:05:15 +0200 Subject: [PATCH 07/17] Added pylint directive Added pylint:disable=unused-import in config_flow.py --- homeassistant/components/tuya/config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index 9a2bf3b57ec78c..c905334aac78d5 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -8,6 +8,7 @@ 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__) From 13f3aa01a67e109a88d8f4137c7c44833690e2aa Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sun, 10 May 2020 02:54:06 +0200 Subject: [PATCH 08/17] Implementation requested changes --- homeassistant/components/tuya/__init__.py | 34 +++--- homeassistant/components/tuya/climate.py | 19 +-- homeassistant/components/tuya/cover.py | 21 ++-- homeassistant/components/tuya/fan.py | 21 ++-- homeassistant/components/tuya/light.py | 21 ++-- homeassistant/components/tuya/manifest.json | 2 +- homeassistant/components/tuya/scene.py | 21 ++-- homeassistant/components/tuya/strings.json | 12 +- homeassistant/components/tuya/switch.py | 19 +-- tests/components/tuya/test_config_flow.py | 122 ++++++++++---------- 10 files changed, 154 insertions(+), 138 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index a6172f8b75b518..da0bbeff4fa27b 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -29,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) -CONFIG_ENTRY_IS_SETUP = "tuya_config_entry_is_setup" +ENTRY_IS_SETUP = "tuya_entry_is_setup" PARALLEL_UPDATES = 0 @@ -92,8 +92,8 @@ async def async_setup_entry(hass, entry): await hass.async_add_executor_job( tuya.init, username, password, country_code, platform ) - except (TuyaNetException, TuyaServerException) as exc: - raise ConfigEntryNotReady() from exc + except (TuyaNetException, TuyaServerException): + raise ConfigEntryNotReady() except TuyaAPIException as exc: _LOGGER.error( @@ -101,9 +101,10 @@ async def async_setup_entry(hass, entry): ) return False - hass.data[TUYA_DATA] = tuya - hass.data[CONFIG_ENTRY_IS_SETUP] = set() hass.data[DOMAIN] = { + TUYA_DATA: tuya, + TUYA_TRACKER: None, + ENTRY_IS_SETUP: set(), "entities": {}, "pending": {}, } @@ -125,24 +126,27 @@ async def async_load_devices(device_list): for ha_type, dev_ids in device_type_list.items(): config_entries_key = f"{ha_type}.tuya" - if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: + 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[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) + 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 = await hass.async_add_executor_job(tuya.get_all_devices) await async_load_devices(device_list) + 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.") - await hass.async_add_executor_job(tuya.poll_devices_update) # Add new discover device. - device_list = await hass.async_add_executor_job(tuya.get_all_devices) + device_list = await hass.async_add_executor_job(_get_updated_devices) await async_load_devices(device_list) # Delete not exist device. newlist_ids = [] @@ -153,7 +157,7 @@ async def async_poll_devices_update(event_time): async_dispatcher_send(hass, SIGNAL_DELETE_ENTITY, dev_id) hass.data[DOMAIN]["entities"].pop(dev_id) - hass.data[TUYA_TRACKER] = async_track_time_interval( + hass.data[DOMAIN][TUYA_TRACKER] = async_track_time_interval( hass, async_poll_devices_update, timedelta(minutes=5) ) @@ -178,15 +182,15 @@ async def async_unload_entry(hass, entry): hass.config_entries.async_forward_entry_unload( entry, component.split(".", 1)[0] ) - for component in hass.data[CONFIG_ENTRY_IS_SETUP] + for component in hass.data[DOMAIN][ENTRY_IS_SETUP] ] ) ) if unload_ok: - hass.data[CONFIG_ENTRY_IS_SETUP] = set() - hass.data[TUYA_TRACKER]() - hass.data[TUYA_TRACKER] = None - hass.data[TUYA_DATA] = None + 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) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index ef09d91d7d4cc2..4f645418ccbae4 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -53,7 +53,10 @@ async def async_discover_sensor(dev_ids): """Discover and add a discovered tuya sensor.""" if not dev_ids: return - await _async_setup_entity(hass, async_add_entities, dev_ids, platform) + 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 @@ -63,16 +66,16 @@ async def async_discover_sensor(dev_ids): await async_discover_sensor(devices_ids) -async def _async_setup_entity(hass, async_add_entities, dev_ids, platform): +def _setup_entities(hass, dev_ids, platform): """Set up Tuya Climate device.""" - tuya = hass.data[TUYA_DATA] - devices = [] + tuya = hass.data[DOMAIN][TUYA_DATA] + entities = [] for dev_id in dev_ids: - device = await hass.async_add_executor_job(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, platform)) - async_add_entities(devices) + entities.append(TuyaClimateEntity(entity, platform)) + return entities class TuyaClimateEntity(TuyaDevice, ClimateEntity): diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index 438e7b2f286da6..b5336aa8bc4364 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -25,7 +25,10 @@ async def async_discover_sensor(dev_ids): """Discover and add a discovered tuya sensor.""" if not dev_ids: return - await _async_setup_entity(hass, async_add_entities, dev_ids, platform) + 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 @@ -35,16 +38,16 @@ async def async_discover_sensor(dev_ids): await async_discover_sensor(devices_ids) -async def _async_setup_entity(hass, async_add_entities, dev_ids, platform): - """Set up Tuya Switch device.""" - tuya = hass.data[TUYA_DATA] - devices = [] +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 = await hass.async_add_executor_job(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, platform)) - async_add_entities(devices) + entities.append(TuyaCover(entity, platform)) + return entities class TuyaCover(TuyaDevice, CoverEntity): diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 525a9f3adb7229..be40b5f3e3b603 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -24,7 +24,10 @@ async def async_discover_sensor(dev_ids): """Discover and add a discovered tuya sensor.""" if not dev_ids: return - await _async_setup_entity(hass, async_add_entities, dev_ids, platform) + 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 @@ -34,16 +37,16 @@ async def async_discover_sensor(dev_ids): await async_discover_sensor(devices_ids) -async def _async_setup_entity(hass, async_add_entities, dev_ids, platform): - """Set up Tuya Switch device.""" - tuya = hass.data[TUYA_DATA] - devices = [] +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 = await hass.async_add_executor_job(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, platform)) - async_add_entities(devices) + entities.append(TuyaFanDevice(entity, platform)) + return entities class TuyaFanDevice(TuyaDevice, FanEntity): diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 1ae4fcfa57dc12..894833386eaef4 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -29,7 +29,10 @@ async def async_discover_sensor(dev_ids): """Discover and add a discovered tuya sensor.""" if not dev_ids: return - await _async_setup_entity(hass, async_add_entities, dev_ids, platform) + 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 @@ -39,16 +42,16 @@ async def async_discover_sensor(dev_ids): await async_discover_sensor(devices_ids) -async def _async_setup_entity(hass, async_add_entities, dev_ids, platform): - """Set up Tuya Switch device.""" - tuya = hass.data[TUYA_DATA] - devices = [] +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 = await hass.async_add_executor_job(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, platform)) - async_add_entities(devices) + entities.append(TuyaLight(entity, platform)) + return entities class TuyaLight(TuyaDevice, LightEntity): diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 92749741f0aedf..8053dc8f697e60 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -3,6 +3,6 @@ "name": "Tuya", "documentation": "https://www.home-assistant.io/integrations/tuya", "requirements": ["tuyaha==0.0.6"], - "codeowners": [], + "codeowners": ["@ollo69"], "config_flow": true } diff --git a/homeassistant/components/tuya/scene.py b/homeassistant/components/tuya/scene.py index 90793dba434bbe..60119ad713822f 100644 --- a/homeassistant/components/tuya/scene.py +++ b/homeassistant/components/tuya/scene.py @@ -22,7 +22,10 @@ async def async_discover_sensor(dev_ids): """Discover and add a discovered tuya sensor.""" if not dev_ids: return - await _async_setup_entity(hass, async_add_entities, dev_ids, platform) + 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 @@ -32,16 +35,16 @@ async def async_discover_sensor(dev_ids): await async_discover_sensor(devices_ids) -async def _async_setup_entity(hass, async_add_entities, dev_ids, platform): - """Set up Tuya Switch device.""" - tuya = hass.data[TUYA_DATA] - devices = [] +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 = await hass.async_add_executor_job(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, platform)) - async_add_entities(devices) + entities.append(TuyaScene(entity, platform)) + return entities class TuyaScene(TuyaDevice, Scene): diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 686d7f3db2d7a4..10008242cb645b 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -7,20 +7,20 @@ "description": "Enter your Tuya credential.", "data": { "country_code": "Your account country code (e.g., 1 for USA or 86 for China)", - "password": "Your password to log in to Tuya", + "password": "[%key:common::config_flow::data::password%]", "platform": "The app where your account register", - "username": "Your username to log in to Tuya" + "username": "[%key:common::config_flow::data::username%]" } } }, "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." + "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": "Provided credential are incorrect." + "auth_failed": "[%key:common::config_flow::error::invalid_auth%]" } } } diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 03ed42d4f515fa..4a5d2026b017f6 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -22,7 +22,10 @@ async def async_discover_sensor(dev_ids): """Discover and add a discovered tuya sensor.""" if not dev_ids: return - await _async_setup_entity(hass, async_add_entities, dev_ids, platform) + 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 @@ -32,16 +35,16 @@ async def async_discover_sensor(dev_ids): await async_discover_sensor(devices_ids) -async def _async_setup_entity(hass, async_add_entities, dev_ids, platform): +def _setup_entities(hass, dev_ids, platform): """Set up Tuya Switch device.""" - tuya = hass.data[TUYA_DATA] - devices = [] + tuya = hass.data[DOMAIN][TUYA_DATA] + entities = [] for dev_id in dev_ids: - device = await hass.async_add_executor_job(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, platform)) - async_add_entities(devices) + entities.append(TuyaSwitch(entity, platform)) + return entities class TuyaSwitch(TuyaDevice, SwitchEntity): diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index 52a5519c29e701..8ee398d685abd9 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -7,7 +7,7 @@ from homeassistant.components.tuya.const import CONF_COUNTRYCODE, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME -from tests.async_mock import patch +from tests.async_mock import Mock, patch from tests.common import MockConfigEntry USERNAME = "myUsername" @@ -16,11 +16,11 @@ TUYA_PLATFORM = "tuya" -@pytest.fixture(name="account") -def mock_controller_login(): - """Mock a successful login.""" - with patch("homeassistant.components.tuya.config_flow.Account", return_value=True): - yield +@pytest.fixture(name="tuya") +def tuya_fixture() -> Mock: + """Patch libraries.""" + with patch("homeassistant.components.tuya.config_flow.TuyaApi") as tuya: + yield tuya def init_config_flow(hass): @@ -30,7 +30,7 @@ def init_config_flow(hass): return flow -async def test_user(hass, account): +async def test_user(hass, tuya): """Test user config.""" flow = init_config_flow(hass) @@ -55,7 +55,7 @@ async def test_user(hass, account): assert result["data"][CONF_PLATFORM] == TUYA_PLATFORM -async def test_import(hass, account): +async def test_import(hass, tuya): """Test import step.""" flow = init_config_flow(hass) @@ -76,8 +76,8 @@ async def test_import(hass, account): assert result["data"][CONF_PLATFORM] == TUYA_PLATFORM -async def test_abort_if_already_setup(hass, account): - """Test we abort if Neato is already setup.""" +async def test_abort_if_already_setup(hass, tuya): + """Test we abort if Tuya is already setup.""" flow = init_config_flow(hass) MockConfigEntry( domain=DOMAIN, @@ -114,63 +114,57 @@ async def test_abort_if_already_setup(hass, account): assert result["reason"] == "single_instance_allowed" -async def test_abort_on_invalid_credentials(hass): +async def test_abort_on_invalid_credentials(hass, tuya): """Test when we have invalid credentials.""" flow = init_config_flow(hass) + tuya().init.side_effect = TuyaAPIException("Boom") - with patch( - "homeassistant.components.tuya.config_flow.Account", - side_effect=TuyaAPIException(), - ): - result = await flow.async_step_user( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_COUNTRYCODE: COUNTRY_CODE, - CONF_PLATFORM: TUYA_PLATFORM, - } - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "auth_failed"} - - result = await flow.async_step_import( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_COUNTRYCODE: COUNTRY_CODE, - CONF_PLATFORM: TUYA_PLATFORM, - } - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "auth_failed" - - -async def test_abort_on_connection_error(hass): + result = await flow.async_step_user( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_COUNTRYCODE: COUNTRY_CODE, + CONF_PLATFORM: TUYA_PLATFORM, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "auth_failed"} + + result = await flow.async_step_import( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_COUNTRYCODE: COUNTRY_CODE, + CONF_PLATFORM: TUYA_PLATFORM, + } + ) + 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.""" flow = init_config_flow(hass) + tuya().init.side_effect = TuyaNetException("Boom") + + result = await flow.async_step_user( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_COUNTRYCODE: COUNTRY_CODE, + CONF_PLATFORM: TUYA_PLATFORM, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "conn_error" - with patch( - "homeassistant.components.tuya.config_flow.Account", - side_effect=TuyaNetException(), - ): - result = await flow.async_step_user( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_COUNTRYCODE: COUNTRY_CODE, - CONF_PLATFORM: TUYA_PLATFORM, - } - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "conn_error" - - result = await flow.async_step_import( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_COUNTRYCODE: COUNTRY_CODE, - CONF_PLATFORM: TUYA_PLATFORM, - } - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "conn_error" + result = await flow.async_step_import( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_COUNTRYCODE: COUNTRY_CODE, + CONF_PLATFORM: TUYA_PLATFORM, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "conn_error" From 0e20022f8954960e0331f49185ecc1c44e96491f Mon Sep 17 00:00:00 2001 From: ollo69 Date: Sun, 10 May 2020 03:01:11 +0200 Subject: [PATCH 09/17] Update CodeOwners --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index e518f19c9f9451..4e4823244c58b3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -412,6 +412,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 From e531b2298845ee0d5b8001e7c4d57c239e14e2d3 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sun, 10 May 2020 12:13:18 +0200 Subject: [PATCH 10/17] Removed device registry cleanup --- homeassistant/components/tuya/__init__.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index da0bbeff4fa27b..f0b7a09a5d20d3 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -198,17 +198,6 @@ async def async_unload_entry(hass, entry): return unload_ok -async def cleanup_device_registry(hass, device_id): - """Remove device registry entry if there are no remaining entities.""" - - device_registry = await hass.helpers.device_registry.async_get_registry() - entity_registry = await hass.helpers.entity_registry.async_get_registry() - if device_id and not hass.helpers.entity_registry.async_entries_for_device( - entity_registry, device_id - ): - device_registry.async_remove_device(device_id) - - class TuyaDevice(Entity): """Tuya base device.""" @@ -267,9 +256,7 @@ async def _delete_callback(self, dev_id): await self.hass.helpers.entity_registry.async_get_registry() ) if entity_registry.async_is_registered(self.entity_id): - entity_entry = entity_registry.async_get(self.entity_id) entity_registry.async_remove(self.entity_id) - await cleanup_device_registry(self.hass, entity_entry.device_id) else: await self.async_remove() From cd5bbedd77a406d0435eb0bdc763b5ea051bee8a Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sun, 10 May 2020 14:58:17 +0200 Subject: [PATCH 11/17] Force checks --- homeassistant/components/tuya/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index f0b7a09a5d20d3..86bf2b7caa39ae 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -1,4 +1,4 @@ -"""Support for Tuya Smart devices.""" +"""Support for Tuya Smart devices.""" import asyncio from datetime import timedelta import logging From 4747dd79950e1fbafe71b658f7b31e8a1663e79d Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sun, 10 May 2020 14:58:43 +0200 Subject: [PATCH 12/17] Force checks --- homeassistant/components/tuya/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 86bf2b7caa39ae..f0b7a09a5d20d3 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -1,4 +1,4 @@ -"""Support for Tuya Smart devices.""" +"""Support for Tuya Smart devices.""" import asyncio from datetime import timedelta import logging From 5a15b78f5818c91ae5457ad826d844896550b558 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sun, 10 May 2020 17:36:01 +0200 Subject: [PATCH 13/17] Fix implemetation - Set config schema "deprecated" - Removed async from update_callback --- homeassistant/components/tuya/__init__.py | 25 +++++++++++++---------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index f0b7a09a5d20d3..2f8b838e7d9459 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -51,16 +51,19 @@ 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, ) @@ -261,6 +264,6 @@ async def _delete_callback(self, dev_id): await self.async_remove() @callback - async def _update_callback(self): + def _update_callback(self): """Call update method.""" self.async_schedule_update_ha_state(True) From a2d539a244836516095a1eab9ec73b8a0498001c Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sun, 10 May 2020 20:44:51 +0200 Subject: [PATCH 14/17] Updating test --- tests/components/tuya/test_config_flow.py | 114 +++++++--------------- 1 file changed, 36 insertions(+), 78 deletions(-) diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index 8ee398d685abd9..234ada4b5e9350 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -2,8 +2,7 @@ import pytest from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException -from homeassistant import data_entry_flow -from homeassistant.components.tuya import config_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.tuya.const import CONF_COUNTRYCODE, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME @@ -15,6 +14,13 @@ 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: @@ -23,28 +29,17 @@ def tuya_fixture() -> Mock: yield tuya -def init_config_flow(hass): - """Init a configuration flow.""" - flow = config_flow.TuyaConfigFlow() - flow.hass = hass - return flow - - async def test_user(hass, tuya): """Test user config.""" - flow = init_config_flow(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) - result = await flow.async_step_user() assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - result = await flow.async_step_user( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_COUNTRYCODE: COUNTRY_CODE, - CONF_PLATFORM: TUYA_PLATFORM, - } + 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 @@ -53,19 +48,13 @@ async def test_user(hass, tuya): 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 async def test_import(hass, tuya): """Test import step.""" - flow = init_config_flow(hass) - - result = await flow.async_step_import( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_COUNTRYCODE: COUNTRY_CODE, - CONF_PLATFORM: TUYA_PLATFORM, - } + 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 @@ -74,97 +63,66 @@ async def test_import(hass, tuya): 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 async def test_abort_if_already_setup(hass, tuya): """Test we abort if Tuya is already setup.""" - flow = init_config_flow(hass) MockConfigEntry( domain=DOMAIN, - data={ - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_COUNTRYCODE: COUNTRY_CODE, - CONF_PLATFORM: TUYA_PLATFORM, - }, + data=TUYA_USER_DATA, ).add_to_hass(hass) # Should fail, config exist (import) - result = await flow.async_step_import( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_COUNTRYCODE: COUNTRY_CODE, - CONF_PLATFORM: TUYA_PLATFORM, - } + 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 flow.async_step_user( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_COUNTRYCODE: COUNTRY_CODE, - CONF_PLATFORM: TUYA_PLATFORM, - } + 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.""" - flow = init_config_flow(hass) tuya().init.side_effect = TuyaAPIException("Boom") - result = await flow.async_step_user( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_COUNTRYCODE: COUNTRY_CODE, - CONF_PLATFORM: TUYA_PLATFORM, - } + 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 flow.async_step_import( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_COUNTRYCODE: COUNTRY_CODE, - CONF_PLATFORM: TUYA_PLATFORM, - } + 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.""" - flow = init_config_flow(hass) tuya().init.side_effect = TuyaNetException("Boom") - result = await flow.async_step_user( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_COUNTRYCODE: COUNTRY_CODE, - CONF_PLATFORM: TUYA_PLATFORM, - } + 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 flow.async_step_import( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_COUNTRYCODE: COUNTRY_CODE, - CONF_PLATFORM: TUYA_PLATFORM, - } + 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" From a0fe4ae5003c557f29c8c2fac06eab48dd07cabf Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sun, 10 May 2020 20:52:40 +0200 Subject: [PATCH 15/17] Fix formatting --- tests/components/tuya/test_config_flow.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index 234ada4b5e9350..4a875573e72f50 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -68,10 +68,7 @@ async def test_import(hass, tuya): 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) + MockConfigEntry(domain=DOMAIN, data=TUYA_USER_DATA).add_to_hass(hass) # Should fail, config exist (import) result = await hass.config_entries.flow.async_init( From 6b2bbf79714fcc2407d78566b795fcfc7e426f34 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sun, 10 May 2020 22:28:02 +0200 Subject: [PATCH 16/17] Config Flow test fix - mock out async_setup and async_setup_entry --- tests/components/tuya/test_config_flow.py | 34 ++++++++++++++++++----- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index 4a875573e72f50..ee41e45252a246 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -2,7 +2,7 @@ import pytest from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException -from homeassistant import config_entries, data_entry_flow +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 @@ -31,6 +31,7 @@ def tuya_fixture() -> Mock: 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} ) @@ -38,9 +39,14 @@ async def test_user(hass, tuya): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=TUYA_USER_DATA - ) + 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 @@ -50,12 +56,22 @@ async def test_user(hass, tuya): 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.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA - ) + 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 @@ -65,6 +81,10 @@ async def test_import(hass, tuya): 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.""" From f59586fc445125da7ead0289a3c7045504a5f37a Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sun, 10 May 2020 22:36:45 +0200 Subject: [PATCH 17/17] Fix formatting --- tests/components/tuya/test_config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index ee41e45252a246..eeda68cd2d3763 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -70,7 +70,9 @@ async def test_import(hass, tuya): "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 + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=TUYA_USER_DATA, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY