mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 08:47:57 +00:00
Add discovery to Russound RIO (#134245)
This commit is contained in:
parent
a345e80368
commit
b7541f098c
@ -8,12 +8,13 @@ from typing import Any
|
||||
from aiorussound import RussoundClient, RussoundTcpConnectionHandler
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_RECONFIGURE,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import DOMAIN, RUSSOUND_RIO_EXCEPTIONS
|
||||
@ -33,6 +34,53 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self.data: dict[str, Any] = {}
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle zeroconf discovery."""
|
||||
self.data[CONF_HOST] = host = discovery_info.host
|
||||
self.data[CONF_PORT] = port = discovery_info.port or 9621
|
||||
|
||||
client = RussoundClient(RussoundTcpConnectionHandler(host, port))
|
||||
try:
|
||||
await client.connect()
|
||||
controller = client.controllers[1]
|
||||
await client.disconnect()
|
||||
except RUSSOUND_RIO_EXCEPTIONS:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
await self.async_set_unique_id(controller.mac_address)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
|
||||
|
||||
self.data[CONF_NAME] = controller.controller_type
|
||||
|
||||
self.context["title_placeholders"] = {
|
||||
"name": self.data[CONF_NAME],
|
||||
}
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
||||
async def async_step_discovery_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm discovery."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=self.data[CONF_NAME],
|
||||
data={CONF_HOST: self.data[CONF_HOST], CONF_PORT: self.data[CONF_PORT]},
|
||||
)
|
||||
|
||||
self._set_confirm_only()
|
||||
return self.async_show_form(
|
||||
step_id="discovery_confirm",
|
||||
description_placeholders={
|
||||
"name": self.data[CONF_NAME],
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@ -51,7 +99,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Could not connect to Russound RIO")
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
await self.async_set_unique_id(controller.mac_address)
|
||||
await self.async_set_unique_id(
|
||||
controller.mac_address, raise_on_progress=False
|
||||
)
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_device")
|
||||
return self.async_update_reload_and_abort(
|
||||
|
@ -7,5 +7,6 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiorussound"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aiorussound==4.2.0"]
|
||||
"requirements": ["aiorussound==4.2.0"],
|
||||
"zeroconf": ["_rio._tcp.local."]
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ rules:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration doesn't have enough / noisy entities that warrant being disabled by default.
|
||||
discovery: todo
|
||||
discovery: done
|
||||
stale-devices: todo
|
||||
diagnostics: done
|
||||
exception-translations: done
|
||||
@ -67,7 +67,7 @@ rules:
|
||||
There are no entities that require icons.
|
||||
reconfiguration-flow: done
|
||||
dynamic-devices: todo
|
||||
discovery-update-info: todo
|
||||
discovery-update-info: done
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: |
|
||||
|
@ -15,6 +15,9 @@
|
||||
"port": "The port of the Russound controller."
|
||||
}
|
||||
},
|
||||
"discovery_confirm": {
|
||||
"description": "Do you want to setup {name}?"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Reconfigure your Russound controller.",
|
||||
"data": {
|
||||
|
@ -775,6 +775,11 @@ ZEROCONF = {
|
||||
},
|
||||
},
|
||||
],
|
||||
"_rio._tcp.local.": [
|
||||
{
|
||||
"domain": "russound_rio",
|
||||
},
|
||||
],
|
||||
"_sideplay._tcp.local.": [
|
||||
{
|
||||
"domain": "ecobee",
|
||||
|
@ -1,9 +1,11 @@
|
||||
"""Test the Russound RIO config flow."""
|
||||
|
||||
from ipaddress import ip_address
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from homeassistant.components.russound_rio.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER, ConfigFlowResult
|
||||
from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
||||
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
@ -12,6 +14,23 @@ from .const import MOCK_CONFIG, MOCK_RECONFIGURATION_CONFIG, MODEL
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
ZEROCONF_DISCOVERY = ZeroconfServiceInfo(
|
||||
ip_address=ip_address("192.168.20.17"),
|
||||
ip_addresses=[ip_address("192.168.20.17")],
|
||||
hostname="controller1.local.",
|
||||
name="controller1._stream-magic._tcp.local.",
|
||||
port=9621,
|
||||
type="_rio._tcp.local.",
|
||||
properties={
|
||||
"txtvers": "0",
|
||||
"productType": "2",
|
||||
"productId": "59",
|
||||
"version": "07.04.00",
|
||||
"buildDate": "Jul 8 2019",
|
||||
"localName": "0",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def test_form(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_russound_client: AsyncMock
|
||||
@ -89,6 +108,159 @@ async def test_duplicate(
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_zeroconf_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_russound_client: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test zeroconf flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=ZEROCONF_DISCOVERY,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "MCA-C5"
|
||||
assert result["data"] == {
|
||||
CONF_HOST: "192.168.20.17",
|
||||
CONF_PORT: 9621,
|
||||
}
|
||||
assert result["result"].unique_id == "00:11:22:33:44:55"
|
||||
|
||||
|
||||
async def test_zeroconf_flow_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_russound_client: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test zeroconf flow."""
|
||||
mock_russound_client.connect.side_effect = TimeoutError
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=ZEROCONF_DISCOVERY,
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
mock_russound_client.connect.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=ZEROCONF_DISCOVERY,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "MCA-C5"
|
||||
assert result["data"] == {
|
||||
CONF_HOST: "192.168.20.17",
|
||||
CONF_PORT: 9621,
|
||||
}
|
||||
assert result["result"].unique_id == "00:11:22:33:44:55"
|
||||
|
||||
|
||||
async def test_zeroconf_duplicate(
|
||||
hass: HomeAssistant,
|
||||
mock_russound_client: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test duplicate flow."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=ZEROCONF_DISCOVERY,
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_zeroconf_duplicate_different_ip(
|
||||
hass: HomeAssistant,
|
||||
mock_russound_client: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test duplicate flow with different IP."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
ZEROCONF_DISCOVERY_DIFFERENT_IP = ZeroconfServiceInfo(
|
||||
ip_address=ip_address("192.168.20.18"),
|
||||
ip_addresses=[ip_address("192.168.20.18")],
|
||||
hostname="controller1.local.",
|
||||
name="controller1._stream-magic._tcp.local.",
|
||||
port=9621,
|
||||
type="_rio._tcp.local.",
|
||||
properties={
|
||||
"txtvers": "0",
|
||||
"productType": "2",
|
||||
"productId": "59",
|
||||
"version": "07.04.00",
|
||||
"buildDate": "Jul 8 2019",
|
||||
"localName": "0",
|
||||
},
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=ZEROCONF_DISCOVERY_DIFFERENT_IP,
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
|
||||
assert entry
|
||||
assert entry.data == {
|
||||
CONF_HOST: "192.168.20.18",
|
||||
CONF_PORT: 9621,
|
||||
}
|
||||
|
||||
|
||||
async def test_user_flow_works_discovery(
|
||||
hass: HomeAssistant,
|
||||
mock_russound_client: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test user flow can continue after discovery happened."""
|
||||
await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=ZEROCONF_DISCOVERY,
|
||||
)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 2
|
||||
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 not hass.config_entries.flow.async_progress(DOMAIN)
|
||||
|
||||
|
||||
async def _start_reconfigure_flow(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> ConfigFlowResult:
|
||||
|
Loading…
x
Reference in New Issue
Block a user