From ee96ff28465b90c533624699747366d87958e003 Mon Sep 17 00:00:00 2001 From: Steffen Zimmermann Date: Wed, 13 May 2020 10:40:58 +0200 Subject: [PATCH] Add wiffi integration (#30784) * Add integration for wiffi devices wiffi devices are DIY board manufactured by stall.biz. Several devices are available, e.g. a weather station (weatherman), an indoor environmental sensor (wiffi-wz) and some more. This intgration has been developed using a weatherman device, but should also work for other devices from stall.biz. * Fix pylint warning * Use WIFFI / STALL WIFFI instead of wiffi to be consistent with stall.biz * Don't update disabled entities. * fix complains - move wiffi specific code to pypi - remove yaml configuration code * incorporate various suggestions from code review * fix remaining comments from Martin * fix comments * add tests for config flow * fix comments * add missing requirements for tests * fix pylint warnings * fix comments * fix comments remove debug log rename .translations to translations * rebase and adapt to latest dev branch * Update homeassistant/components/wiffi/config_flow.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/wiffi/config_flow.py Co-Authored-By: Martin Hjelmare * fix missing import Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/wiffi/__init__.py | 230 ++++++++++++++++++ .../components/wiffi/binary_sensor.py | 53 ++++ homeassistant/components/wiffi/config_flow.py | 57 +++++ homeassistant/components/wiffi/const.py | 12 + homeassistant/components/wiffi/manifest.json | 11 + homeassistant/components/wiffi/sensor.py | 125 ++++++++++ homeassistant/components/wiffi/strings.json | 16 ++ .../components/wiffi/translations/en.json | 16 ++ .../components/wiffi/wiffi_strings.py | 8 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/wiffi/__init__.py | 1 + tests/components/wiffi/test_config_flow.py | 109 +++++++++ 16 files changed, 647 insertions(+) create mode 100644 homeassistant/components/wiffi/__init__.py create mode 100644 homeassistant/components/wiffi/binary_sensor.py create mode 100644 homeassistant/components/wiffi/config_flow.py create mode 100644 homeassistant/components/wiffi/const.py create mode 100644 homeassistant/components/wiffi/manifest.json create mode 100644 homeassistant/components/wiffi/sensor.py create mode 100644 homeassistant/components/wiffi/strings.json create mode 100644 homeassistant/components/wiffi/translations/en.json create mode 100644 homeassistant/components/wiffi/wiffi_strings.py create mode 100644 tests/components/wiffi/__init__.py create mode 100644 tests/components/wiffi/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index eedb567bf46..1281ac1aca0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -849,6 +849,7 @@ omit = homeassistant/components/webostv/* homeassistant/components/wemo/* homeassistant/components/whois/sensor.py + homeassistant/components/wiffi/* homeassistant/components/wink/* homeassistant/components/wirelesstag/* homeassistant/components/worldtidesinfo/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 31a478d6fdb..c8f81b00ec8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -449,6 +449,7 @@ homeassistant/components/weather/* @fabaff homeassistant/components/webostv/* @bendavid homeassistant/components/websocket_api/* @home-assistant/core homeassistant/components/wemo/* @sqldiablo +homeassistant/components/wiffi/* @mampfes homeassistant/components/withings/* @vangorra homeassistant/components/wled/* @frenck homeassistant/components/workday/* @fabaff diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py new file mode 100644 index 00000000000..05d0d381e18 --- /dev/null +++ b/homeassistant/components/wiffi/__init__.py @@ -0,0 +1,230 @@ +"""Component for wiffi support.""" +import asyncio +from datetime import timedelta +import errno +import logging + +from wiffi import WiffiTcpServer + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PORT +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.dt import utcnow + +from .const import ( + CHECK_ENTITIES_SIGNAL, + CREATE_ENTITY_SIGNAL, + DOMAIN, + UPDATE_ENTITY_SIGNAL, +) + +_LOGGER = logging.getLogger(__name__) + + +PLATFORMS = ["sensor", "binary_sensor"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the wiffi component. config contains data from configuration.yaml.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Set up wiffi from a config entry, config_entry contains data from config entry database.""" + # create api object + api = WiffiIntegrationApi(hass) + api.async_setup(config_entry) + + # store api object + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = api + + try: + await api.server.start_server() + except OSError as exc: + if exc.errno != errno.EADDRINUSE: + _LOGGER.error("Start_server failed, errno: %d", exc.errno) + return False + _LOGGER.error("Port %s already in use", config_entry.data[CONF_PORT]) + raise ConfigEntryNotReady from exc + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Unload a config entry.""" + api: "WiffiIntegrationApi" = hass.data[DOMAIN][config_entry.entry_id] + await api.server.close_server() + + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + api = hass.data[DOMAIN].pop(config_entry.entry_id) + api.shutdown() + + return unload_ok + + +def generate_unique_id(device, metric): + """Generate a unique string for the entity.""" + return f"{device.mac_address.replace(':', '')}-{metric.name}" + + +class WiffiIntegrationApi: + """API object for wiffi handling. Stored in hass.data.""" + + def __init__(self, hass): + """Initialize the instance.""" + self._hass = hass + self._server = None + self._known_devices = {} + self._periodic_callback = None + + def async_setup(self, config_entry): + """Set up api instance.""" + self._server = WiffiTcpServer(config_entry.data[CONF_PORT], self) + self._periodic_callback = async_track_time_interval( + self._hass, self._periodic_tick, timedelta(seconds=10) + ) + + def shutdown(self): + """Shutdown wiffi api. + + Remove listener for periodic callbacks. + """ + remove_listener = self._periodic_callback + if remove_listener is not None: + remove_listener() + + async def __call__(self, device, metrics): + """Process callback from TCP server if new data arrives from a device.""" + if device.mac_address not in self._known_devices: + # add empty set for new device + self._known_devices[device.mac_address] = set() + + for metric in metrics: + if metric.id not in self._known_devices[device.mac_address]: + self._known_devices[device.mac_address].add(metric.id) + async_dispatcher_send(self._hass, CREATE_ENTITY_SIGNAL, device, metric) + else: + async_dispatcher_send( + self._hass, + f"{UPDATE_ENTITY_SIGNAL}-{generate_unique_id(device, metric)}", + device, + metric, + ) + + @property + def server(self): + """Return TCP server instance for start + close.""" + return self._server + + @callback + def _periodic_tick(self, now=None): + """Check if any entity has timed out because it has not been updated.""" + async_dispatcher_send(self._hass, CHECK_ENTITIES_SIGNAL) + + +class WiffiEntity(Entity): + """Common functionality for all wiffi entities.""" + + def __init__(self, device, metric): + """Initialize the base elements of a wiffi entity.""" + self._id = generate_unique_id(device, metric) + self._device_info = { + "connections": { + (device_registry.CONNECTION_NETWORK_MAC, device.mac_address) + }, + "identifiers": {(DOMAIN, device.mac_address)}, + "manufacturer": "stall.biz", + "name": f"{device.moduletype} {device.mac_address}", + "model": device.moduletype, + "sw_version": device.sw_version, + } + self._name = metric.description + self._expiration_date = None + self._value = None + + async def async_added_to_hass(self): + """Entity has been added to hass.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{UPDATE_ENTITY_SIGNAL}-{self._id}", + self._update_value_callback, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, CHECK_ENTITIES_SIGNAL, self._check_expiration_date + ) + ) + + @property + def should_poll(self): + """Disable polling because data driven .""" + return False + + @property + def device_info(self): + """Return wiffi device info which is shared between all entities of a device.""" + return self._device_info + + @property + def unique_id(self): + """Return unique id for entity.""" + return self._id + + @property + def name(self): + """Return entity name.""" + return self._name + + @property + def available(self): + """Return true if value is valid.""" + return self._value is not None + + def reset_expiration_date(self): + """Reset value expiration date. + + Will be called by derived classes after a value update has been received. + """ + self._expiration_date = utcnow() + timedelta(minutes=3) + + @callback + def _update_value_callback(self, device, metric): + """Update the value of the entity.""" + + @callback + def _check_expiration_date(self): + """Periodically check if entity value has been updated. + + If there are no more updates from the wiffi device, the value will be + set to unavailable. + """ + if ( + self._value is not None + and self._expiration_date is not None + and utcnow() > self._expiration_date + ): + self._value = None + self.async_write_ha_state() diff --git a/homeassistant/components/wiffi/binary_sensor.py b/homeassistant/components/wiffi/binary_sensor.py new file mode 100644 index 00000000000..009fc2b4a67 --- /dev/null +++ b/homeassistant/components/wiffi/binary_sensor.py @@ -0,0 +1,53 @@ +"""Binary sensor platform support for wiffi devices.""" + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import WiffiEntity +from .const import CREATE_ENTITY_SIGNAL + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up platform for a new integration. + + Called by the HA framework after async_forward_entry_setup has been called + during initialization of a new integration (= wiffi). + """ + + @callback + def _create_entity(device, metric): + """Create platform specific entities.""" + entities = [] + + if metric.is_bool: + entities.append(BoolEntity(device, metric)) + + async_add_entities(entities) + + async_dispatcher_connect(hass, CREATE_ENTITY_SIGNAL, _create_entity) + + +class BoolEntity(WiffiEntity, BinarySensorEntity): + """Entity for wiffi metrics which have a boolean value.""" + + def __init__(self, device, metric): + """Initialize the entity.""" + super().__init__(device, metric) + self._value = metric.value + self.reset_expiration_date() + + @property + def is_on(self): + """Return the state of the entity.""" + return self._value + + @callback + def _update_value_callback(self, device, metric): + """Update the value of the entity. + + Called if a new message has been received from the wiffi device. + """ + self.reset_expiration_date() + self._value = metric.value + self.async_write_ha_state() diff --git a/homeassistant/components/wiffi/config_flow.py b/homeassistant/components/wiffi/config_flow.py new file mode 100644 index 00000000000..82dbbb040ef --- /dev/null +++ b/homeassistant/components/wiffi/config_flow.py @@ -0,0 +1,57 @@ +"""Config flow for wiffi component. + +Used by UI to setup a wiffi integration. +""" +import errno + +import voluptuous as vol +from wiffi import WiffiTcpServer + +from homeassistant import config_entries +from homeassistant.const import CONF_PORT +from homeassistant.core import callback + +from .const import DEFAULT_PORT, DOMAIN # pylint: disable=unused-import + + +class WiffiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Wiffi server setup config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + async def async_step_user(self, user_input=None): + """Handle the start of the config flow. + + Called after wiffi integration has been selected in the 'add integration + UI'. The user_input is set to None in this case. We will open a config + flow form then. + This function is also called if the form has been submitted. user_input + contains a dict with the user entered values then. + """ + if user_input is None: + return self._async_show_form() + + # received input from form or configuration.yaml + + try: + # try to start server to check whether port is in use + server = WiffiTcpServer(user_input[CONF_PORT]) + await server.start_server() + await server.close_server() + return self.async_create_entry( + title=f"Port {user_input[CONF_PORT]}", data=user_input + ) + except OSError as exc: + if exc.errno == errno.EADDRINUSE: + return self.async_abort(reason="addr_in_use") + return self.async_abort(reason="start_server_failed") + + @callback + def _async_show_form(self, errors=None): + """Show the config flow form to the user.""" + data_schema = {vol.Required(CONF_PORT, default=DEFAULT_PORT): int} + + return self.async_show_form( + step_id="user", data_schema=vol.Schema(data_schema), errors=errors or {} + ) diff --git a/homeassistant/components/wiffi/const.py b/homeassistant/components/wiffi/const.py new file mode 100644 index 00000000000..6b71c89002f --- /dev/null +++ b/homeassistant/components/wiffi/const.py @@ -0,0 +1,12 @@ +"""Constants for the wiffi component.""" + +# Component domain, used to store component data in hass data. +DOMAIN = "wiffi" + +# Default port for TCP server +DEFAULT_PORT = 8189 + +# Signal name to send create/update to platform (sensor/binary_sensor) +CREATE_ENTITY_SIGNAL = "wiffi_create_entity_signal" +UPDATE_ENTITY_SIGNAL = "wiffi_update_entity_signal" +CHECK_ENTITIES_SIGNAL = "wiffi_check_entities_signal" diff --git a/homeassistant/components/wiffi/manifest.json b/homeassistant/components/wiffi/manifest.json new file mode 100644 index 00000000000..5be1286ad6f --- /dev/null +++ b/homeassistant/components/wiffi/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "wiffi", + "name": "Wiffi", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/wiffi", + "requirements": ["wiffi==1.0.0"], + "dependencies": [], + "codeowners": [ + "@mampfes" + ] +} diff --git a/homeassistant/components/wiffi/sensor.py b/homeassistant/components/wiffi/sensor.py new file mode 100644 index 00000000000..cc6befaf067 --- /dev/null +++ b/homeassistant/components/wiffi/sensor.py @@ -0,0 +1,125 @@ +"""Sensor platform support for wiffi devices.""" + +from homeassistant.components.sensor import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, +) +from homeassistant.const import DEGREE, PRESSURE_MBAR, TEMP_CELSIUS +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import WiffiEntity +from .const import CREATE_ENTITY_SIGNAL +from .wiffi_strings import ( + WIFFI_UOM_DEGREE, + WIFFI_UOM_LUX, + WIFFI_UOM_MILLI_BAR, + WIFFI_UOM_PERCENT, + WIFFI_UOM_TEMP_CELSIUS, +) + +# map to determine HA device class from wiffi's unit of measurement +UOM_TO_DEVICE_CLASS_MAP = { + WIFFI_UOM_TEMP_CELSIUS: DEVICE_CLASS_TEMPERATURE, + WIFFI_UOM_PERCENT: DEVICE_CLASS_HUMIDITY, + WIFFI_UOM_MILLI_BAR: DEVICE_CLASS_PRESSURE, + WIFFI_UOM_LUX: DEVICE_CLASS_ILLUMINANCE, +} + +# map to convert wiffi unit of measurements to common HA uom's +UOM_MAP = { + WIFFI_UOM_DEGREE: DEGREE, + WIFFI_UOM_TEMP_CELSIUS: TEMP_CELSIUS, + WIFFI_UOM_MILLI_BAR: PRESSURE_MBAR, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up platform for a new integration. + + Called by the HA framework after async_forward_entry_setup has been called + during initialization of a new integration (= wiffi). + """ + + @callback + def _create_entity(device, metric): + """Create platform specific entities.""" + entities = [] + + if metric.is_number: + entities.append(NumberEntity(device, metric)) + elif metric.is_string: + entities.append(StringEntity(device, metric)) + + async_add_entities(entities) + + async_dispatcher_connect(hass, CREATE_ENTITY_SIGNAL, _create_entity) + + +class NumberEntity(WiffiEntity): + """Entity for wiffi metrics which have a number value.""" + + def __init__(self, device, metric): + """Initialize the entity.""" + super().__init__(device, metric) + self._device_class = UOM_TO_DEVICE_CLASS_MAP.get(metric.unit_of_measurement) + self._unit_of_measurement = UOM_MAP.get( + metric.unit_of_measurement, metric.unit_of_measurement + ) + self._value = metric.value + self.reset_expiration_date() + + @property + def device_class(self): + """Return the automatically determined device class.""" + return self._device_class + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return self._unit_of_measurement + + @property + def state(self): + """Return the value of the entity.""" + return self._value + + @callback + def _update_value_callback(self, device, metric): + """Update the value of the entity. + + Called if a new message has been received from the wiffi device. + """ + self.reset_expiration_date() + self._unit_of_measurement = UOM_MAP.get( + metric.unit_of_measurement, metric.unit_of_measurement + ) + self._value = metric.value + self.async_write_ha_state() + + +class StringEntity(WiffiEntity): + """Entity for wiffi metrics which have a string value.""" + + def __init__(self, device, metric): + """Initialize the entity.""" + super().__init__(device, metric) + self._value = metric.value + self.reset_expiration_date() + + @property + def state(self): + """Return the value of the entity.""" + return self._value + + @callback + def _update_value_callback(self, device, metric): + """Update the value of the entity. + + Called if a new message has been received from the wiffi device. + """ + self.reset_expiration_date() + self._value = metric.value + self.async_write_ha_state() diff --git a/homeassistant/components/wiffi/strings.json b/homeassistant/components/wiffi/strings.json new file mode 100644 index 00000000000..36f836366a5 --- /dev/null +++ b/homeassistant/components/wiffi/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "user": { + "title": "Setup TCP server for WIFFI devices", + "data": { + "port": "Server Port" + } + } + }, + "abort": { + "addr_in_use": "Server port already in use.", + "start_server_failed": "Start server failed." + } + } +} diff --git a/homeassistant/components/wiffi/translations/en.json b/homeassistant/components/wiffi/translations/en.json new file mode 100644 index 00000000000..bcaf0820bd5 --- /dev/null +++ b/homeassistant/components/wiffi/translations/en.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "addr_in_use": "Server port already in use.", + "start_server_failed": "Start server failed." + }, + "step": { + "user": { + "data": { + "port": "Server Port" + }, + "title": "Setup TCP server for WIFFI devices" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/wiffi_strings.py b/homeassistant/components/wiffi/wiffi_strings.py new file mode 100644 index 00000000000..1bcd582feae --- /dev/null +++ b/homeassistant/components/wiffi/wiffi_strings.py @@ -0,0 +1,8 @@ +"""Definition of string used in wiffi json telegrams.""" + +# units of measurement +WIFFI_UOM_TEMP_CELSIUS = "gradC" +WIFFI_UOM_DEGREE = "grad" +WIFFI_UOM_PERCENT = "%" +WIFFI_UOM_MILLI_BAR = "mb" +WIFFI_UOM_LUX = "lux" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 67573957ee6..9fa17c7a772 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -154,6 +154,7 @@ FLOWS = [ "vilfo", "vizio", "wemo", + "wiffi", "withings", "wled", "wwlln", diff --git a/requirements_all.txt b/requirements_all.txt index 1888d314c6b..5aa14be13c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2176,6 +2176,9 @@ webexteamssdk==1.1.1 # homeassistant.components.gpmdp websocket-client==0.54.0 +# homeassistant.components.wiffi +wiffi==1.0.0 + # homeassistant.components.wirelesstag wirelesstagpy==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f601b5fff62..22755c295bf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -873,6 +873,9 @@ wakeonlan==1.1.6 # homeassistant.components.folder_watcher watchdog==0.8.3 +# homeassistant.components.wiffi +wiffi==1.0.0 + # homeassistant.components.withings withings-api==2.1.3 diff --git a/tests/components/wiffi/__init__.py b/tests/components/wiffi/__init__.py new file mode 100644 index 00000000000..501fcbb5883 --- /dev/null +++ b/tests/components/wiffi/__init__.py @@ -0,0 +1 @@ +"""Tests for the wiffi integration.""" diff --git a/tests/components/wiffi/test_config_flow.py b/tests/components/wiffi/test_config_flow.py new file mode 100644 index 00000000000..ef6ce528623 --- /dev/null +++ b/tests/components/wiffi/test_config_flow.py @@ -0,0 +1,109 @@ +"""Test the wiffi integration config flow.""" +import errno + +from asynctest import patch +import pytest + +from homeassistant import config_entries +from homeassistant.components.wiffi.const import DOMAIN +from homeassistant.const import CONF_PORT +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + + +@pytest.fixture(name="dummy_tcp_server") +def mock_dummy_tcp_server(): + """Mock a valid WiffiTcpServer.""" + + class Dummy: + async def start_server(self): + pass + + async def close_server(self): + pass + + server = Dummy() + with patch( + "homeassistant.components.wiffi.config_flow.WiffiTcpServer", return_value=server + ): + yield server + + +@pytest.fixture(name="addr_in_use") +def mock_addr_in_use_server(): + """Mock a WiffiTcpServer with addr_in_use.""" + + class Dummy: + async def start_server(self): + raise OSError(errno.EADDRINUSE, "") + + async def close_server(self): + pass + + server = Dummy() + with patch( + "homeassistant.components.wiffi.config_flow.WiffiTcpServer", return_value=server + ): + yield server + + +@pytest.fixture(name="start_server_failed") +def mock_start_server_failed(): + """Mock a WiffiTcpServer with start_server_failed.""" + + class Dummy: + async def start_server(self): + raise OSError(errno.EACCES, "") + + async def close_server(self): + pass + + server = Dummy() + with patch( + "homeassistant.components.wiffi.config_flow.WiffiTcpServer", return_value=server + ): + yield server + + +async def test_form(hass, dummy_tcp_server): + """Test how 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"] == {} + assert result["step_id"] == config_entries.SOURCE_USER + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PORT: 8765}, + ) + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + + +async def test_form_addr_in_use(hass, addr_in_use): + """Test how we handle addr_in_use error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PORT: 8765}, + ) + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "addr_in_use" + + +async def test_form_start_server_failed(hass, start_server_failed): + """Test how we handle start_server_failed error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PORT: 8765}, + ) + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "start_server_failed"