From dad5d19a350c5d9c782f3821caf8d2d0a1336e7e Mon Sep 17 00:00:00 2001 From: Tim Rightnour <6556271+garbled1@users.noreply.github.com> Date: Mon, 25 Oct 2021 14:40:36 -0700 Subject: [PATCH] Add config flow to venstar (#58152) --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/venstar/__init__.py | 108 +++++++++++++++ homeassistant/components/venstar/climate.py | 85 +++++------- .../components/venstar/config_flow.py | 96 +++++++++++++ homeassistant/components/venstar/const.py | 29 ++++ .../components/venstar/manifest.json | 7 +- homeassistant/components/venstar/strings.json | 23 ++++ .../components/venstar/translations/en.json | 20 +++ homeassistant/generated/config_flows.py | 1 + tests/components/venstar/__init__.py | 61 +++++++++ tests/components/venstar/test_config_flow.py | 128 ++++++++++++++++++ tests/components/venstar/test_init.py | 71 ++++++++++ 13 files changed, 578 insertions(+), 53 deletions(-) create mode 100644 homeassistant/components/venstar/config_flow.py create mode 100644 homeassistant/components/venstar/const.py create mode 100644 homeassistant/components/venstar/strings.json create mode 100644 homeassistant/components/venstar/translations/en.json create mode 100644 tests/components/venstar/test_config_flow.py create mode 100644 tests/components/venstar/test_init.py diff --git a/.coveragerc b/.coveragerc index a4a221db69a..bb84820783b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1168,6 +1168,7 @@ omit = homeassistant/components/velbus/sensor.py homeassistant/components/velbus/switch.py homeassistant/components/velux/* + homeassistant/components/venstar/__init__.py homeassistant/components/venstar/climate.py homeassistant/components/verisure/__init__.py homeassistant/components/verisure/alarm_control_panel.py diff --git a/CODEOWNERS b/CODEOWNERS index e2494b8299a..12ab4d6b472 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -563,6 +563,7 @@ homeassistant/components/utility_meter/* @dgomes homeassistant/components/vallox/* @andre-richter homeassistant/components/velbus/* @Cereal2nd @brefra homeassistant/components/velux/* @Julius2342 +homeassistant/components/venstar/* @garbled1 homeassistant/components/vera/* @pavoni homeassistant/components/verisure/* @frenck homeassistant/components/versasense/* @flamm3blemuff1n diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index abc35a0d6bd..27d3e77754a 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -1 +1,109 @@ """The venstar component.""" +import asyncio + +from requests import RequestException +from venstarcolortouch import VenstarColorTouch + +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PIN, + CONF_SSL, + CONF_USERNAME, +) +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.entity import Entity + +from .const import _LOGGER, DOMAIN, VENSTAR_TIMEOUT + +PLATFORMS = ["climate"] + + +async def async_setup_entry(hass, config): + """Set up the Venstar thermostat.""" + username = config.data.get(CONF_USERNAME) + password = config.data.get(CONF_PASSWORD) + pin = config.data.get(CONF_PIN) + host = config.data[CONF_HOST] + timeout = VENSTAR_TIMEOUT + protocol = "https" if config.data[CONF_SSL] else "http" + + client = VenstarColorTouch( + addr=host, + timeout=timeout, + user=username, + password=password, + pin=pin, + proto=protocol, + ) + + try: + await hass.async_add_executor_job(client.update_info) + except (OSError, RequestException) as ex: + raise ConfigEntryNotReady(f"Unable to connect to the thermostat: {ex}") from ex + hass.data.setdefault(DOMAIN, {})[config.entry_id] = client + hass.config_entries.async_setup_platforms(config, PLATFORMS) + + return True + + +async def async_unload_entry(hass, config): + """Unload the config config and platforms.""" + unload_ok = await hass.config_entries.async_unload_platforms(config, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(config.entry_id) + return unload_ok + + +class VenstarEntity(Entity): + """Get the latest data and update.""" + + def __init__(self, config, client): + """Initialize the data object.""" + self._config = config + self._client = client + + async def async_update(self): + """Update the state.""" + try: + info_success = await self.hass.async_add_executor_job( + self._client.update_info + ) + except (OSError, RequestException) as ex: + _LOGGER.error("Exception during info update: %s", ex) + + # older venstars sometimes cannot handle rapid sequential connections + await asyncio.sleep(3) + + try: + sensor_success = await self.hass.async_add_executor_job( + self._client.update_sensors + ) + except (OSError, RequestException) as ex: + _LOGGER.error("Exception during sensor update: %s", ex) + + if not info_success or not sensor_success: + _LOGGER.error("Failed to update data") + + @property + def name(self): + """Return the name of the thermostat.""" + return self._client.name + + @property + def unique_id(self): + """Set unique_id for this entity.""" + return f"{self._config.entry_id}" + + @property + def device_info(self): + """Return the device information for this entity.""" + return { + "identifiers": {(DOMAIN, self._config.entry_id)}, + "name": self._client.name, + "manufacturer": "Venstar", + # pylint: disable=protected-access + "model": f"{self._client.model}-{self._client._type}", + # pylint: disable=protected-access + "sw_version": self._client._api_ver, + } diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 72e9ecc3de4..d86a5953169 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -1,7 +1,4 @@ """Support for Venstar WiFi Thermostats.""" -import logging - -from venstarcolortouch import VenstarColorTouch import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity @@ -27,6 +24,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, @@ -42,20 +40,18 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -_LOGGER = logging.getLogger(__name__) - -ATTR_FAN_STATE = "fan_state" -ATTR_HVAC_STATE = "hvac_mode" - -CONF_HUMIDIFIER = "humidifier" - -DEFAULT_SSL = False - -VALID_FAN_STATES = [STATE_ON, HVAC_MODE_AUTO] -VALID_THERMOSTAT_MODES = [HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_OFF, HVAC_MODE_AUTO] - -HOLD_MODE_OFF = "off" -HOLD_MODE_TEMPERATURE = "temperature" +from . import VenstarEntity +from .const import ( + _LOGGER, + ATTR_FAN_STATE, + ATTR_HVAC_STATE, + CONF_HUMIDIFIER, + DEFAULT_SSL, + DOMAIN, + HOLD_MODE_TEMPERATURE, + VALID_FAN_STATES, + VALID_THERMOSTAT_MODES, +) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -72,50 +68,42 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Venstar thermostat.""" + client = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities([VenstarThermostat(config_entry, client)], True) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - pin = config.get(CONF_PIN) - host = config.get(CONF_HOST) - timeout = config.get(CONF_TIMEOUT) - humidifier = config.get(CONF_HUMIDIFIER) - protocol = "https" if config[CONF_SSL] else "http" +async def async_setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Venstar thermostat platform. - client = VenstarColorTouch( - addr=host, - timeout=timeout, - user=username, - password=password, - pin=pin, - proto=protocol, + Venstar uses config flow for configuration now. If an entry exists in + configuration.yaml, the import flow will attempt to import it and create + a config entry. + """ + _LOGGER.warning( + "Loading venstar via platform config is deprecated; The configuration" + " has been migrated to a config entry and can be safely removed" ) - - add_entities([VenstarThermostat(client, humidifier)], True) + # No config entry exists and configuration.yaml config exists, trigger the import flow. + if not hass.config_entries.async_entries(DOMAIN): + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) -class VenstarThermostat(ClimateEntity): +class VenstarThermostat(VenstarEntity, ClimateEntity): """Representation of a Venstar thermostat.""" - def __init__(self, client, humidifier): + def __init__(self, config, client): """Initialize the thermostat.""" - self._client = client - self._humidifier = humidifier + super().__init__(config, client) self._mode_map = { HVAC_MODE_HEAT: self._client.MODE_HEAT, HVAC_MODE_COOL: self._client.MODE_COOL, HVAC_MODE_AUTO: self._client.MODE_AUTO, } - def update(self): - """Update the data from the thermostat.""" - info_success = self._client.update_info() - sensor_success = self._client.update_sensors() - if not info_success or not sensor_success: - _LOGGER.error("Failed to update data") - @property def supported_features(self): """Return the list of supported features.""" @@ -124,16 +112,11 @@ class VenstarThermostat(ClimateEntity): if self._client.mode == self._client.MODE_AUTO: features |= SUPPORT_TARGET_TEMPERATURE_RANGE - if self._humidifier and self._client.hum_setpoint is not None: + if self._client.hum_setpoint is not None: features |= SUPPORT_TARGET_HUMIDITY return features - @property - def name(self): - """Return the name of the thermostat.""" - return self._client.name - @property def precision(self): """Return the precision of the system. diff --git a/homeassistant/components/venstar/config_flow.py b/homeassistant/components/venstar/config_flow.py new file mode 100644 index 00000000000..d97c5ada9e6 --- /dev/null +++ b/homeassistant/components/venstar/config_flow.py @@ -0,0 +1,96 @@ +"""Config flow to configure the Venstar integration.""" +from venstarcolortouch import VenstarColorTouch +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PIN, + CONF_SSL, + CONF_USERNAME, +) + +from .const import _LOGGER, DOMAIN, VENSTAR_TIMEOUT + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + vol.Optional(CONF_PIN): str, + vol.Optional(CONF_SSL, default=False): bool, + } +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" + username = data.get(CONF_USERNAME) + password = data.get(CONF_PASSWORD) + pin = data.get(CONF_PIN) + host = data[CONF_HOST] + timeout = VENSTAR_TIMEOUT + protocol = "https" if data[CONF_SSL] else "http" + + client = VenstarColorTouch( + addr=host, + timeout=timeout, + user=username, + password=password, + pin=pin, + proto=protocol, + ) + + # perform a full info pull, because this calls login also. + + info_success = await hass.async_add_executor_job(client.update_info) + if not info_success: + raise CannotConnect + + return {"title": client.name} + + +class VenstarConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a venstar config flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Create config entry. Show the setup form to the user.""" + errors = {} + info = {} + + if user_input is not None: + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, import_data): + """Import entry from configuration.yaml.""" + self._async_abort_entries_match({CONF_HOST: import_data[CONF_HOST]}) + return await self.async_step_user( + { + CONF_HOST: import_data[CONF_HOST], + CONF_USERNAME: import_data.get(CONF_USERNAME), + CONF_PASSWORD: import_data.get(CONF_PASSWORD), + CONF_PIN: import_data.get(CONF_PIN), + CONF_SSL: import_data[CONF_SSL], + } + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/venstar/const.py b/homeassistant/components/venstar/const.py new file mode 100644 index 00000000000..999e08384dd --- /dev/null +++ b/homeassistant/components/venstar/const.py @@ -0,0 +1,29 @@ +"""The venstar component.""" +import logging + +from homeassistant.components.climate.const import ( + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, +) +from homeassistant.const import STATE_ON + +DOMAIN = "venstar" + +ATTR_FAN_STATE = "fan_state" +ATTR_HVAC_STATE = "hvac_mode" + +CONF_HUMIDIFIER = "humidifier" + +DEFAULT_SSL = False + +VALID_FAN_STATES = [STATE_ON, HVAC_MODE_AUTO] +VALID_THERMOSTAT_MODES = [HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_OFF, HVAC_MODE_AUTO] + +HOLD_MODE_OFF = "off" +HOLD_MODE_TEMPERATURE = "temperature" + +VENSTAR_TIMEOUT = 5 + +_LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index 444a3fabf9a..943790b532e 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -1,8 +1,11 @@ { "domain": "venstar", "name": "Venstar", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/venstar", - "requirements": ["venstarcolortouch==0.14"], - "codeowners": [], + "requirements": [ + "venstarcolortouch==0.14" + ], + "codeowners": ["@garbled1"], "iot_class": "local_polling" } diff --git a/homeassistant/components/venstar/strings.json b/homeassistant/components/venstar/strings.json new file mode 100644 index 00000000000..9b031d94188 --- /dev/null +++ b/homeassistant/components/venstar/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "title": "Connect to the Venstar Thermostat", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "pin": "[%key:common::config_flow::data::pin%]", + "ssl": "[%key:common::config_flow::data::ssl%]" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/components/venstar/translations/en.json b/homeassistant/components/venstar/translations/en.json new file mode 100644 index 00000000000..e56d0fe1a83 --- /dev/null +++ b/homeassistant/components/venstar/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "title": "Connect to the Venstar Thermostat", + "data": { + "host": "Hostname or IP", + "username": "Username for thermostat (optional)", + "password": "Password for thermostat (optional)", + "pin": "Pin for Lockscreen (required if lock screen enabled)", + "ssl": "Whether to use SSL or not when communicating" + } + } + }, + "error": { + "cannot_connect": "Unable to connect to thermostat, please validate username/password if supplied, hostname/ip, and that LOCAL API is enabled on the thermostat.", + "unknown": "An unknown error has occurred." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 39beb7bcb10..dba466e181c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -306,6 +306,7 @@ FLOWS = [ "upnp", "uptimerobot", "velbus", + "venstar", "vera", "verisure", "vesync", diff --git a/tests/components/venstar/__init__.py b/tests/components/venstar/__init__.py index 908755a585f..326aeeeb0e2 100644 --- a/tests/components/venstar/__init__.py +++ b/tests/components/venstar/__init__.py @@ -1 +1,62 @@ """Tests for the venstar integration.""" +from requests import RequestException + + +class VenstarColorTouchMock: + """Mock Venstar Library.""" + + def __init__( + self, + addr, + timeout, + user=None, + password=None, + pin=None, + proto="http", + SSLCert=False, + ): + """Initialize the Venstar library.""" + self.status = {} + self.model = "COLORTOUCH" + self._api_ver = 5 + self.name = "TestVenstar" + self._info = {} + self._sensors = {} + self.alerts = {} + self.MODE_OFF = 0 + self.MODE_HEAT = 1 + self.MODE_COOL = 2 + self.MODE_AUTO = 3 + self._type = "residential" + + def login(self): + """Mock login.""" + return True + + def _request(self, path, data=None): + """Mock request.""" + self.status = {} + + def update(self): + """Mock update.""" + return True + + def update_info(self): + """Mock update_info.""" + return True + + def broken_update_info(self): + """Mock a update_info that raises Exception.""" + raise RequestException + + def update_sensors(self): + """Mock update_sensors.""" + return True + + def update_runtimes(self): + """Mock update_runtimes.""" + return True + + def update_alerts(self): + """Mock update_alerts.""" + return True diff --git a/tests/components/venstar/test_config_flow.py b/tests/components/venstar/test_config_flow.py new file mode 100644 index 00000000000..f568655ec8d --- /dev/null +++ b/tests/components/venstar/test_config_flow.py @@ -0,0 +1,128 @@ +"""Test the Venstar config flow.""" +import logging +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.venstar.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PIN, + CONF_SSL, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import VenstarColorTouchMock + +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) + +TEST_DATA = { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PIN: "test-pin", + CONF_SSL: False, +} +TEST_ID = "VenstarUniqueID" + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.venstar.config_flow.VenstarColorTouch.update_info", + new=VenstarColorTouchMock.update_info, + ), patch( + "homeassistant.components.venstar.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_DATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == TEST_DATA + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.venstar.config_flow.VenstarColorTouch.update_info", + return_value=False, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_DATA, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_unknown_error(hass: HomeAssistant) -> None: + """Test we handle unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.venstar.config_flow.VenstarColorTouch.update_info", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_DATA, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_already_configured(hass: HomeAssistant) -> None: + """Test when provided credentials are already configured.""" + MockConfigEntry(domain=DOMAIN, data=TEST_DATA, unique_id=TEST_ID).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + + with patch( + "homeassistant.components.venstar.VenstarColorTouch.update_info", + new=VenstarColorTouchMock.update_info, + ), patch( + "homeassistant.components.venstar.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_DATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" diff --git a/tests/components/venstar/test_init.py b/tests/components/venstar/test_init.py new file mode 100644 index 00000000000..03739f19616 --- /dev/null +++ b/tests/components/venstar/test_init.py @@ -0,0 +1,71 @@ +"""Tests of the initialization of the venstar integration.""" +from unittest.mock import patch + +from homeassistant.components.venstar.const import DOMAIN as VENSTAR_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_HOST, CONF_SSL +from homeassistant.core import HomeAssistant + +from . import VenstarColorTouchMock + +from tests.common import MockConfigEntry + +TEST_HOST = "venstartest.localdomain" + + +async def test_setup_entry(hass: HomeAssistant): + """Validate that setup entry also configure the client.""" + config_entry = MockConfigEntry( + domain=VENSTAR_DOMAIN, + data={ + CONF_HOST: TEST_HOST, + CONF_SSL: False, + }, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.venstar.VenstarColorTouch._request", + new=VenstarColorTouchMock._request, + ), patch( + "homeassistant.components.venstar.VenstarColorTouch.update_sensors", + new=VenstarColorTouchMock.update_sensors, + ), patch( + "homeassistant.components.venstar.VenstarColorTouch.update_info", + new=VenstarColorTouchMock.update_info, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.LOADED + + await hass.config_entries.async_unload(config_entry.entry_id) + + assert config_entry.state == ConfigEntryState.NOT_LOADED + + +async def test_setup_entry_exception(hass: HomeAssistant): + """Validate that setup entry also configure the client.""" + config_entry = MockConfigEntry( + domain=VENSTAR_DOMAIN, + data={ + CONF_HOST: TEST_HOST, + CONF_SSL: False, + }, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.venstar.VenstarColorTouch._request", + new=VenstarColorTouchMock._request, + ), patch( + "homeassistant.components.venstar.VenstarColorTouch.update_sensors", + new=VenstarColorTouchMock.update_sensors, + ), patch( + "homeassistant.components.venstar.VenstarColorTouch.update_info", + new=VenstarColorTouchMock.broken_update_info, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.SETUP_RETRY