mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add config flow to Russound RIO integration (#121262)
* Add config flow to Russound RIO * Ensure Russound RIO connection is handled at entry setup * Add tests for Russound RIO config flow * Add yaml configuration import to Russound RIO * Use runtime_data to store Russound RIO client * Seperate common import and user config logic for Russound RIO * Update config flow to use aiorussound * Add MAC address as unique ID for Russound RIO * Fix pre-commit for Russound RIO * Refactor config flow error handling for Russound RIO * Add config flow import abort message for no primary controller * Add common strings to Russound RIO * Use reference strings for Russound RIO issue strings * Remove commented out test fixture from Russound RIO * Clean up test fixtures for Russound RIO * Remove model from entry data in Russound RIO * Clean up Russound client mock * Clean up Russound test fixtures * Remove init tests and clean up Russound config flow cases
This commit is contained in:
parent
924e767736
commit
abeac3f3aa
@ -1208,6 +1208,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/rtsp_to_webrtc/ @allenporter
|
||||
/homeassistant/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
|
||||
/tests/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
|
||||
/homeassistant/components/russound_rio/ @noahhusby
|
||||
/tests/components/russound_rio/ @noahhusby
|
||||
/homeassistant/components/ruuvi_gateway/ @akx
|
||||
/tests/components/ruuvi_gateway/ @akx
|
||||
/homeassistant/components/ruuvitag_ble/ @akx
|
||||
|
@ -1 +1,45 @@
|
||||
"""The russound_rio component."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from aiorussound import Russound
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
|
||||
from .const import CONNECT_TIMEOUT, RUSSOUND_RIO_EXCEPTIONS
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type RussoundConfigEntry = ConfigEntry[Russound]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: RussoundConfigEntry) -> bool:
|
||||
"""Set up a config entry."""
|
||||
|
||||
russ = Russound(hass.loop, entry.data[CONF_HOST], entry.data[CONF_PORT])
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(CONNECT_TIMEOUT):
|
||||
await russ.connect()
|
||||
except RUSSOUND_RIO_EXCEPTIONS as err:
|
||||
raise ConfigEntryError(err) from err
|
||||
|
||||
entry.runtime_data = russ
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
await entry.runtime_data.close()
|
||||
|
||||
return unload_ok
|
||||
|
114
homeassistant/components/russound_rio/config_flow.py
Normal file
114
homeassistant/components/russound_rio/config_flow.py
Normal file
@ -0,0 +1,114 @@
|
||||
"""Config flow to configure russound_rio component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiorussound import Russound
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import (
|
||||
CONNECT_TIMEOUT,
|
||||
DOMAIN,
|
||||
RUSSOUND_RIO_EXCEPTIONS,
|
||||
NoPrimaryControllerException,
|
||||
)
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=9621): cv.port,
|
||||
}
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def find_primary_controller_metadata(
|
||||
controllers: list[tuple[int, str, str]],
|
||||
) -> tuple[str, str]:
|
||||
"""Find the mac address of the primary Russound controller."""
|
||||
for controller_id, mac_address, controller_type in controllers:
|
||||
# The integration only cares about the primary controller linked by IP and not any downstream controllers
|
||||
if controller_id == 1:
|
||||
return (mac_address, controller_type)
|
||||
raise NoPrimaryControllerException
|
||||
|
||||
|
||||
class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Russound RIO configuration flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
host = user_input[CONF_HOST]
|
||||
port = user_input[CONF_PORT]
|
||||
|
||||
controllers = None
|
||||
russ = Russound(self.hass.loop, host, port)
|
||||
try:
|
||||
async with asyncio.timeout(CONNECT_TIMEOUT):
|
||||
await russ.connect()
|
||||
controllers = await russ.enumerate_controllers()
|
||||
metadata = find_primary_controller_metadata(controllers)
|
||||
await russ.close()
|
||||
except RUSSOUND_RIO_EXCEPTIONS:
|
||||
_LOGGER.exception("Could not connect to Russound RIO")
|
||||
errors["base"] = "cannot_connect"
|
||||
except NoPrimaryControllerException:
|
||||
_LOGGER.exception(
|
||||
"Russound RIO device doesn't have a primary controller",
|
||||
)
|
||||
errors["base"] = "no_primary_controller"
|
||||
else:
|
||||
await self.async_set_unique_id(metadata[0])
|
||||
self._abort_if_unique_id_configured()
|
||||
data = {CONF_HOST: host, CONF_PORT: port}
|
||||
return self.async_create_entry(title=metadata[1], data=data)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_import(
|
||||
self, import_config: dict[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Attempt to import the existing configuration."""
|
||||
self._async_abort_entries_match({CONF_HOST: import_config[CONF_HOST]})
|
||||
host = import_config[CONF_HOST]
|
||||
port = import_config.get(CONF_PORT, 9621)
|
||||
|
||||
# Connection logic is repeated here since this method will be removed in future releases
|
||||
russ = Russound(self.hass.loop, host, port)
|
||||
try:
|
||||
async with asyncio.timeout(CONNECT_TIMEOUT):
|
||||
await russ.connect()
|
||||
controllers = await russ.enumerate_controllers()
|
||||
metadata = find_primary_controller_metadata(controllers)
|
||||
await russ.close()
|
||||
except RUSSOUND_RIO_EXCEPTIONS:
|
||||
_LOGGER.exception("Could not connect to Russound RIO")
|
||||
return self.async_abort(
|
||||
reason="cannot_connect", description_placeholders={}
|
||||
)
|
||||
except NoPrimaryControllerException:
|
||||
_LOGGER.exception("Russound RIO device doesn't have a primary controller")
|
||||
return self.async_abort(
|
||||
reason="no_primary_controller", description_placeholders={}
|
||||
)
|
||||
else:
|
||||
await self.async_set_unique_id(metadata[0])
|
||||
self._abort_if_unique_id_configured()
|
||||
data = {CONF_HOST: host, CONF_PORT: port}
|
||||
return self.async_create_entry(title=metadata[1], data=data)
|
21
homeassistant/components/russound_rio/const.py
Normal file
21
homeassistant/components/russound_rio/const.py
Normal file
@ -0,0 +1,21 @@
|
||||
"""Constants used for Russound RIO."""
|
||||
|
||||
import asyncio
|
||||
|
||||
from aiorussound import CommandException
|
||||
|
||||
DOMAIN = "russound_rio"
|
||||
|
||||
RUSSOUND_RIO_EXCEPTIONS = (
|
||||
CommandException,
|
||||
ConnectionRefusedError,
|
||||
TimeoutError,
|
||||
asyncio.CancelledError,
|
||||
)
|
||||
|
||||
|
||||
class NoPrimaryControllerException(Exception):
|
||||
"""Thrown when the Russound device is not the primary unit in the RNET stack."""
|
||||
|
||||
|
||||
CONNECT_TIMEOUT = 5
|
@ -1,7 +1,8 @@
|
||||
{
|
||||
"domain": "russound_rio",
|
||||
"name": "Russound RIO",
|
||||
"codeowners": [],
|
||||
"codeowners": ["@noahhusby"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/russound_rio",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiorussound"],
|
||||
|
@ -2,34 +2,26 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aiorussound import Russound
|
||||
import voluptuous as vol
|
||||
import logging
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
MediaType,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PORT,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=9621): cv.port,
|
||||
}
|
||||
)
|
||||
from . import RussoundConfigEntry
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
@ -40,22 +32,69 @@ async def async_setup_platform(
|
||||
) -> None:
|
||||
"""Set up the Russound RIO platform."""
|
||||
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
if (
|
||||
result["type"] is FlowResultType.CREATE_ENTRY
|
||||
or result["reason"] == "single_instance_allowed"
|
||||
):
|
||||
async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.2.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Russound RIO",
|
||||
},
|
||||
)
|
||||
return
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"deprecated_yaml_import_issue_{result['reason']}",
|
||||
breaks_in_ha_version="2025.2.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=f"deprecated_yaml_import_issue_{result['reason']}",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Russound RIO",
|
||||
},
|
||||
)
|
||||
|
||||
russ = Russound(hass.loop, host, port)
|
||||
|
||||
await russ.connect()
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: RussoundConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Russound RIO platform."""
|
||||
russ = entry.runtime_data
|
||||
|
||||
# Discover sources and zones
|
||||
sources = await russ.enumerate_sources()
|
||||
valid_zones = await russ.enumerate_zones()
|
||||
|
||||
devices = []
|
||||
entities = []
|
||||
for zone_id, name in valid_zones:
|
||||
if zone_id.controller > 6:
|
||||
_LOGGER.debug(
|
||||
"Zone ID %s exceeds RIO controller maximum, skipping",
|
||||
zone_id.device_str(),
|
||||
)
|
||||
continue
|
||||
await russ.watch_zone(zone_id)
|
||||
dev = RussoundZoneDevice(russ, zone_id, name, sources)
|
||||
devices.append(dev)
|
||||
zone = RussoundZoneDevice(russ, zone_id, name, sources)
|
||||
entities.append(zone)
|
||||
|
||||
@callback
|
||||
def on_stop(event):
|
||||
@ -64,7 +103,7 @@ async def async_setup_platform(
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_stop)
|
||||
|
||||
async_add_entities(devices)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class RussoundZoneDevice(MediaPlayerEntity):
|
||||
@ -80,7 +119,7 @@ class RussoundZoneDevice(MediaPlayerEntity):
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
)
|
||||
|
||||
def __init__(self, russ, zone_id, name, sources):
|
||||
def __init__(self, russ, zone_id, name, sources) -> None:
|
||||
"""Initialize the zone device."""
|
||||
super().__init__()
|
||||
self._name = name
|
||||
|
40
homeassistant/components/russound_rio/strings.json
Normal file
40
homeassistant/components/russound_rio/strings.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"common": {
|
||||
"error_cannot_connect": "Failed to connect to Russound device. Please make sure the device is powered up and connected to the network. Try power-cycling the device if it does not connect.",
|
||||
"error_no_primary_controller": "No primary controller was detected for the Russound device. Please make sure that the target Russound device has it's controller ID set to 1 (using the selector on the back of the unit)."
|
||||
},
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:component::russound_rio::common::error_cannot_connect%]",
|
||||
"no_primary_controller": "[%key:component::russound_rio::common::error_no_primary_controller%]"
|
||||
},
|
||||
"abort": {
|
||||
"cannot_connect": "[%key:component::russound_rio::common::error_cannot_connect%]",
|
||||
"no_primary_controller": "[%key:component::russound_rio::common::error_no_primary_controller%]",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"title": "The {integration_title} YAML configuration import cannot connect to the Russound device",
|
||||
"description": "Configuring {integration_title} using YAML is being removed but there was a connection error importing your YAML configuration.\n\nPlease make sure {integration_title} is turned on, and restart Home Assistant to try importing again. Otherwise, please remove the YAML from your configuration and add the integration manually."
|
||||
},
|
||||
"deprecated_yaml_import_issue_no_primary_controller": {
|
||||
"title": "The {integration_title} YAML configuration import cannot configure the Russound Device.",
|
||||
"description": "Configuring {integration_title} using YAML is being removed but there was a connection error importing your YAML configuration.\n\nNo primary controller was detected for the Russound device. Please make sure that the target Russound device has it's controller ID set to 1 (using the selector on the back of the unit)."
|
||||
},
|
||||
"deprecated_yaml_import_issue_unknown": {
|
||||
"title": "[%key:component::russound_rio::issues::deprecated_yaml_import_issue_cannot_connect::title%]",
|
||||
"description": "[%key:component::russound_rio::issues::deprecated_yaml_import_issue_cannot_connect::description%]"
|
||||
}
|
||||
}
|
||||
}
|
@ -476,6 +476,7 @@ FLOWS = {
|
||||
"rpi_power",
|
||||
"rtsp_to_webrtc",
|
||||
"ruckus_unleashed",
|
||||
"russound_rio",
|
||||
"ruuvi_gateway",
|
||||
"ruuvitag_ble",
|
||||
"rympro",
|
||||
|
@ -5158,7 +5158,7 @@
|
||||
"integrations": {
|
||||
"russound_rio": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
"name": "Russound RIO"
|
||||
},
|
||||
|
@ -328,6 +328,9 @@ aioridwell==2024.01.0
|
||||
# homeassistant.components.ruckus_unleashed
|
||||
aioruckus==0.34
|
||||
|
||||
# homeassistant.components.russound_rio
|
||||
aiorussound==1.1.2
|
||||
|
||||
# homeassistant.components.ruuvi_gateway
|
||||
aioruuvigateway==0.1.0
|
||||
|
||||
|
1
tests/components/russound_rio/__init__.py
Normal file
1
tests/components/russound_rio/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Russound RIO integration."""
|
48
tests/components/russound_rio/conftest.py
Normal file
48
tests/components/russound_rio/conftest.py
Normal file
@ -0,0 +1,48 @@
|
||||
"""Test fixtures for Russound RIO integration."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.russound_rio.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import HARDWARE_MAC, MOCK_CONFIG, MODEL
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry():
|
||||
"""Prevent setup."""
|
||||
with patch(
|
||||
"homeassistant.components.russound_rio.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
"""Mock a Russound RIO config entry."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN, data=MOCK_CONFIG, unique_id=HARDWARE_MAC, title=MODEL
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
return entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_russound() -> Generator[AsyncMock]:
|
||||
"""Mock the Russound RIO client."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.russound_rio.Russound", autospec=True
|
||||
) as mock_client,
|
||||
patch(
|
||||
"homeassistant.components.russound_rio.config_flow.Russound",
|
||||
return_value=mock_client,
|
||||
),
|
||||
):
|
||||
mock_client.enumerate_controllers.return_value = [(1, HARDWARE_MAC, MODEL)]
|
||||
yield mock_client
|
11
tests/components/russound_rio/const.py
Normal file
11
tests/components/russound_rio/const.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""Constants for russound_rio tests."""
|
||||
|
||||
HOST = "127.0.0.1"
|
||||
PORT = 9621
|
||||
MODEL = "MCA-C5"
|
||||
HARDWARE_MAC = "00:11:22:33:44:55"
|
||||
|
||||
MOCK_CONFIG = {
|
||||
"host": HOST,
|
||||
"port": PORT,
|
||||
}
|
135
tests/components/russound_rio/test_config_flow.py
Normal file
135
tests/components/russound_rio/test_config_flow.py
Normal file
@ -0,0 +1,135 @@
|
||||
"""Test the Russound RIO config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from homeassistant.components.russound_rio.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from .const import HARDWARE_MAC, MOCK_CONFIG, MODEL
|
||||
|
||||
|
||||
async def test_form(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_russound: AsyncMock
|
||||
) -> None:
|
||||
"""Test we get the form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
MOCK_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == MODEL
|
||||
assert result["data"] == MOCK_CONFIG
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_cannot_connect(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_russound: AsyncMock
|
||||
) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
mock_russound.connect.side_effect = TimeoutError
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
MOCK_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
# Recover with correct information
|
||||
mock_russound.connect.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
MOCK_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == MODEL
|
||||
assert result["data"] == MOCK_CONFIG
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_no_primary_controller(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_russound: AsyncMock
|
||||
) -> None:
|
||||
"""Test we handle no primary controller error."""
|
||||
mock_russound.enumerate_controllers.return_value = []
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
user_input = MOCK_CONFIG
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "no_primary_controller"}
|
||||
|
||||
# Recover with correct information
|
||||
mock_russound.enumerate_controllers.return_value = [(1, HARDWARE_MAC, MODEL)]
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
MOCK_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == MODEL
|
||||
assert result["data"] == MOCK_CONFIG
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_import(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_russound: AsyncMock
|
||||
) -> None:
|
||||
"""Test we import a config entry."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=MOCK_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == MODEL
|
||||
assert result["data"] == MOCK_CONFIG
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_import_cannot_connect(
|
||||
hass: HomeAssistant, mock_russound: AsyncMock
|
||||
) -> None:
|
||||
"""Test we handle import cannot connect error."""
|
||||
mock_russound.connect.side_effect = TimeoutError
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_import_no_primary_controller(
|
||||
hass: HomeAssistant, mock_russound: AsyncMock
|
||||
) -> None:
|
||||
"""Test import with no primary controller error."""
|
||||
mock_russound.enumerate_controllers.return_value = []
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "no_primary_controller"
|
Loading…
x
Reference in New Issue
Block a user