mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 13:47:35 +00:00
Add reconfigure step to Onkyo config flow (#129088)
This commit is contained in:
parent
0cd5deaa3f
commit
ed6123a3e6
@ -6,6 +6,7 @@ from typing import Any
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import (
|
from homeassistant.config_entries import (
|
||||||
|
SOURCE_RECONFIGURE,
|
||||||
ConfigEntry,
|
ConfigEntry,
|
||||||
ConfigFlow,
|
ConfigFlow,
|
||||||
ConfigFlowResult,
|
ConfigFlowResult,
|
||||||
@ -46,13 +47,11 @@ CONF_DEVICE = "device"
|
|||||||
INPUT_SOURCES_ALL_MEANINGS = [
|
INPUT_SOURCES_ALL_MEANINGS = [
|
||||||
input_source.value_meaning for input_source in InputSource
|
input_source.value_meaning for input_source in InputSource
|
||||||
]
|
]
|
||||||
|
STEP_MANUAL_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
|
||||||
STEP_CONFIGURE_SCHEMA = vol.Schema(
|
STEP_CONFIGURE_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(
|
vol.Required(OPTION_VOLUME_RESOLUTION): vol.In(VOLUME_RESOLUTION_ALLOWED),
|
||||||
OPTION_VOLUME_RESOLUTION,
|
vol.Required(OPTION_INPUT_SOURCES): SelectSelector(
|
||||||
default=OPTION_VOLUME_RESOLUTION_DEFAULT,
|
|
||||||
): vol.In(VOLUME_RESOLUTION_ALLOWED),
|
|
||||||
vol.Required(OPTION_INPUT_SOURCES, default=[]): SelectSelector(
|
|
||||||
SelectSelectorConfig(
|
SelectSelectorConfig(
|
||||||
options=INPUT_SOURCES_ALL_MEANINGS,
|
options=INPUT_SOURCES_ALL_MEANINGS,
|
||||||
multiple=True,
|
multiple=True,
|
||||||
@ -96,15 +95,28 @@ class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
else:
|
else:
|
||||||
self._receiver_info = info
|
self._receiver_info = info
|
||||||
|
|
||||||
await self.async_set_unique_id(
|
await self.async_set_unique_id(
|
||||||
info.identifier, raise_on_progress=False
|
info.identifier, raise_on_progress=False
|
||||||
)
|
)
|
||||||
self._abort_if_unique_id_configured(updates=user_input)
|
if self.source == SOURCE_RECONFIGURE:
|
||||||
|
self._abort_if_unique_id_mismatch()
|
||||||
|
else:
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
return await self.async_step_configure_receiver()
|
return await self.async_step_configure_receiver()
|
||||||
|
|
||||||
|
suggested_values = user_input
|
||||||
|
if suggested_values is None and self.source == SOURCE_RECONFIGURE:
|
||||||
|
suggested_values = {
|
||||||
|
CONF_HOST: self._get_reconfigure_entry().data[CONF_HOST]
|
||||||
|
}
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="manual",
|
step_id="manual",
|
||||||
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
|
STEP_MANUAL_SCHEMA, suggested_values
|
||||||
|
),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -160,6 +172,12 @@ class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle the configuration of a single receiver."""
|
"""Handle the configuration of a single receiver."""
|
||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
|
entry = None
|
||||||
|
entry_options = None
|
||||||
|
if self.source == SOURCE_RECONFIGURE:
|
||||||
|
entry = self._get_reconfigure_entry()
|
||||||
|
entry_options = entry.options
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
source_meanings: list[str] = user_input[OPTION_INPUT_SOURCES]
|
source_meanings: list[str] = user_input[OPTION_INPUT_SOURCES]
|
||||||
if not source_meanings:
|
if not source_meanings:
|
||||||
@ -168,33 +186,80 @@ class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
sources_store: dict[str, str] = {}
|
sources_store: dict[str, str] = {}
|
||||||
for source_meaning in source_meanings:
|
for source_meaning in source_meanings:
|
||||||
source = InputSource.from_meaning(source_meaning)
|
source = InputSource.from_meaning(source_meaning)
|
||||||
sources_store[source.value] = source_meaning
|
|
||||||
|
|
||||||
result = self.async_create_entry(
|
source_name = source_meaning
|
||||||
title=self._receiver_info.model_name,
|
if entry_options is not None:
|
||||||
data={
|
source_name = entry_options[OPTION_INPUT_SOURCES].get(
|
||||||
CONF_HOST: self._receiver_info.host,
|
source.value, source_name
|
||||||
},
|
)
|
||||||
options={
|
sources_store[source.value] = source_name
|
||||||
OPTION_VOLUME_RESOLUTION: user_input[OPTION_VOLUME_RESOLUTION],
|
|
||||||
OPTION_MAX_VOLUME: OPTION_MAX_VOLUME_DEFAULT,
|
volume_resolution = user_input[OPTION_VOLUME_RESOLUTION]
|
||||||
OPTION_INPUT_SOURCES: sources_store,
|
|
||||||
},
|
if entry_options is None:
|
||||||
)
|
result = self.async_create_entry(
|
||||||
|
title=self._receiver_info.model_name,
|
||||||
|
data={
|
||||||
|
CONF_HOST: self._receiver_info.host,
|
||||||
|
},
|
||||||
|
options={
|
||||||
|
OPTION_VOLUME_RESOLUTION: volume_resolution,
|
||||||
|
OPTION_MAX_VOLUME: OPTION_MAX_VOLUME_DEFAULT,
|
||||||
|
OPTION_INPUT_SOURCES: sources_store,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
assert entry is not None
|
||||||
|
result = self.async_update_reload_and_abort(
|
||||||
|
entry,
|
||||||
|
data={
|
||||||
|
CONF_HOST: self._receiver_info.host,
|
||||||
|
},
|
||||||
|
options={
|
||||||
|
OPTION_VOLUME_RESOLUTION: volume_resolution,
|
||||||
|
OPTION_MAX_VOLUME: entry_options[OPTION_MAX_VOLUME],
|
||||||
|
OPTION_INPUT_SOURCES: sources_store,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER.debug("Configured receiver, result: %s", result)
|
_LOGGER.debug("Configured receiver, result: %s", result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
_LOGGER.debug("Configuring receiver, info: %s", self._receiver_info)
|
_LOGGER.debug("Configuring receiver, info: %s", self._receiver_info)
|
||||||
|
|
||||||
|
suggested_values = user_input
|
||||||
|
if suggested_values is None:
|
||||||
|
if entry_options is None:
|
||||||
|
suggested_values = {
|
||||||
|
OPTION_VOLUME_RESOLUTION: OPTION_VOLUME_RESOLUTION_DEFAULT,
|
||||||
|
OPTION_INPUT_SOURCES: [],
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
suggested_values = {
|
||||||
|
OPTION_VOLUME_RESOLUTION: entry_options[OPTION_VOLUME_RESOLUTION],
|
||||||
|
OPTION_INPUT_SOURCES: [
|
||||||
|
InputSource(input_source).value_meaning
|
||||||
|
for input_source in entry_options[OPTION_INPUT_SOURCES]
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="configure_receiver",
|
step_id="configure_receiver",
|
||||||
data_schema=STEP_CONFIGURE_SCHEMA,
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
|
STEP_CONFIGURE_SCHEMA, suggested_values
|
||||||
|
),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
description_placeholders={
|
description_placeholders={
|
||||||
"name": f"{self._receiver_info.model_name} ({self._receiver_info.host})"
|
"name": f"{self._receiver_info.model_name} ({self._receiver_info.host})"
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_step_reconfigure(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle reconfiguration of the receiver."""
|
||||||
|
return await self.async_step_manual()
|
||||||
|
|
||||||
async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult:
|
async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult:
|
||||||
"""Import the yaml config."""
|
"""Import the yaml config."""
|
||||||
_LOGGER.debug("Import flow user input: %s", user_input)
|
_LOGGER.debug("Import flow user input: %s", user_input)
|
||||||
|
@ -33,6 +33,8 @@
|
|||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||||
|
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||||
|
"unique_id_mismatch": "The serial number of the device does not match the previous serial number",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -19,9 +19,9 @@ def create_receiver_info(id: int) -> ReceiverInfo:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_empty_config_entry() -> MockConfigEntry:
|
def create_config_entry_from_info(info: ReceiverInfo) -> MockConfigEntry:
|
||||||
"""Create an empty config entry for use in unit tests."""
|
"""Create a config entry from receiver info."""
|
||||||
config = {CONF_HOST: ""}
|
data = {CONF_HOST: info.host}
|
||||||
options = {
|
options = {
|
||||||
"volume_resolution": 80,
|
"volume_resolution": 80,
|
||||||
"input_sources": {"12": "tv"},
|
"input_sources": {"12": "tv"},
|
||||||
@ -29,7 +29,25 @@ def create_empty_config_entry() -> MockConfigEntry:
|
|||||||
}
|
}
|
||||||
|
|
||||||
return MockConfigEntry(
|
return MockConfigEntry(
|
||||||
data=config,
|
data=data,
|
||||||
|
options=options,
|
||||||
|
title=info.model_name,
|
||||||
|
domain="onkyo",
|
||||||
|
unique_id=info.identifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_empty_config_entry() -> MockConfigEntry:
|
||||||
|
"""Create an empty config entry for use in unit tests."""
|
||||||
|
data = {CONF_HOST: ""}
|
||||||
|
options = {
|
||||||
|
"volume_resolution": 80,
|
||||||
|
"input_sources": {"12": "tv"},
|
||||||
|
"max_volume": 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
return MockConfigEntry(
|
||||||
|
data=data,
|
||||||
options=options,
|
options=options,
|
||||||
title="Unit test Onkyo",
|
title="Unit test Onkyo",
|
||||||
domain="onkyo",
|
domain="onkyo",
|
||||||
|
@ -8,13 +8,22 @@ import pytest
|
|||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.onkyo import InputSource
|
from homeassistant.components.onkyo import InputSource
|
||||||
from homeassistant.components.onkyo.config_flow import OnkyoConfigFlow
|
from homeassistant.components.onkyo.config_flow import OnkyoConfigFlow
|
||||||
from homeassistant.components.onkyo.const import DOMAIN
|
from homeassistant.components.onkyo.const import (
|
||||||
|
DOMAIN,
|
||||||
|
OPTION_MAX_VOLUME,
|
||||||
|
OPTION_VOLUME_RESOLUTION,
|
||||||
|
)
|
||||||
from homeassistant.config_entries import SOURCE_USER
|
from homeassistant.config_entries import SOURCE_USER
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResultType, InvalidData
|
from homeassistant.data_entry_flow import FlowResultType, InvalidData
|
||||||
|
|
||||||
from . import create_empty_config_entry, create_receiver_info, setup_integration
|
from . import (
|
||||||
|
create_config_entry_from_info,
|
||||||
|
create_empty_config_entry,
|
||||||
|
create_receiver_info,
|
||||||
|
setup_integration,
|
||||||
|
)
|
||||||
|
|
||||||
from tests.common import Mock, MockConfigEntry
|
from tests.common import Mock, MockConfigEntry
|
||||||
|
|
||||||
@ -240,7 +249,7 @@ async def test_configure_empty_source_list(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
configure_result = await hass.config_entries.flow.async_configure(
|
configure_result = await hass.config_entries.flow.async_configure(
|
||||||
select_result["flow_id"],
|
select_result["flow_id"],
|
||||||
user_input={"input_sources": []},
|
user_input={"volume_resolution": 200, "input_sources": []},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert configure_result["errors"] == {
|
assert configure_result["errors"] == {
|
||||||
@ -273,13 +282,11 @@ async def test_configure_no_resolution(hass: HomeAssistant) -> None:
|
|||||||
user_input={CONF_HOST: "sample-host-name"},
|
user_input={CONF_HOST: "sample-host-name"},
|
||||||
)
|
)
|
||||||
|
|
||||||
configure_result = await hass.config_entries.flow.async_configure(
|
with pytest.raises(InvalidData):
|
||||||
select_result["flow_id"],
|
await hass.config_entries.flow.async_configure(
|
||||||
user_input={"input_sources": ["TV"]},
|
select_result["flow_id"],
|
||||||
)
|
user_input={"input_sources": ["TV"]},
|
||||||
|
)
|
||||||
assert configure_result["type"] is FlowResultType.CREATE_ENTRY
|
|
||||||
assert configure_result["options"]["volume_resolution"] == 50
|
|
||||||
|
|
||||||
|
|
||||||
async def test_configure_resolution_set(hass: HomeAssistant) -> None:
|
async def test_configure_resolution_set(hass: HomeAssistant) -> None:
|
||||||
@ -295,25 +302,24 @@ async def test_configure_resolution_set(hass: HomeAssistant) -> None:
|
|||||||
{"next_step_id": "manual"},
|
{"next_step_id": "manual"},
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_info = Mock()
|
receiver_info = create_receiver_info(1)
|
||||||
mock_info.identifier = "mock_id"
|
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.onkyo.config_flow.async_interview",
|
"homeassistant.components.onkyo.config_flow.async_interview",
|
||||||
return_value=mock_info,
|
return_value=receiver_info,
|
||||||
):
|
):
|
||||||
select_result = await hass.config_entries.flow.async_configure(
|
select_result = await hass.config_entries.flow.async_configure(
|
||||||
form_result["flow_id"],
|
form_result["flow_id"],
|
||||||
user_input={CONF_HOST: "sample-host-name"},
|
user_input={CONF_HOST: "sample-host-name"},
|
||||||
)
|
)
|
||||||
|
|
||||||
configure_result = await hass.config_entries.flow.async_configure(
|
configure_result = await hass.config_entries.flow.async_configure(
|
||||||
select_result["flow_id"],
|
select_result["flow_id"],
|
||||||
user_input={"volume_resolution": 200, "input_sources": ["TV"]},
|
user_input={"volume_resolution": 200, "input_sources": ["TV"]},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert configure_result["type"] is FlowResultType.CREATE_ENTRY
|
assert configure_result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
assert configure_result["options"]["volume_resolution"] == 200
|
assert configure_result["options"]["volume_resolution"] == 200
|
||||||
|
|
||||||
|
|
||||||
async def test_configure_invalid_resolution_set(hass: HomeAssistant) -> None:
|
async def test_configure_invalid_resolution_set(hass: HomeAssistant) -> None:
|
||||||
@ -348,6 +354,73 @@ async def test_configure_invalid_resolution_set(hass: HomeAssistant) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reconfigure(hass: HomeAssistant) -> None:
|
||||||
|
"""Test the reconfigure config flow."""
|
||||||
|
receiver_info = create_receiver_info(1)
|
||||||
|
config_entry = create_config_entry_from_info(receiver_info)
|
||||||
|
await setup_integration(hass, config_entry, receiver_info)
|
||||||
|
|
||||||
|
old_host = config_entry.data[CONF_HOST]
|
||||||
|
old_max_volume = config_entry.options[OPTION_MAX_VOLUME]
|
||||||
|
|
||||||
|
result = await config_entry.start_reconfigure_flow(hass)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "manual"
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.onkyo.config_flow.async_interview",
|
||||||
|
return_value=receiver_info,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={"host": receiver_info.host}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] is FlowResultType.FORM
|
||||||
|
assert result2["step_id"] == "configure_receiver"
|
||||||
|
|
||||||
|
result3 = await hass.config_entries.flow.async_configure(
|
||||||
|
result2["flow_id"],
|
||||||
|
user_input={"volume_resolution": 200, "input_sources": ["TUNER"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result3["type"] is FlowResultType.ABORT
|
||||||
|
assert result3["reason"] == "reconfigure_successful"
|
||||||
|
|
||||||
|
assert config_entry.data[CONF_HOST] == old_host
|
||||||
|
assert config_entry.options[OPTION_VOLUME_RESOLUTION] == 200
|
||||||
|
assert config_entry.options[OPTION_MAX_VOLUME] == old_max_volume
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reconfigure_new_device(hass: HomeAssistant) -> None:
|
||||||
|
"""Test the reconfigure config flow with new device."""
|
||||||
|
receiver_info = create_receiver_info(1)
|
||||||
|
config_entry = create_config_entry_from_info(receiver_info)
|
||||||
|
await setup_integration(hass, config_entry, receiver_info)
|
||||||
|
|
||||||
|
old_unique_id = receiver_info.identifier
|
||||||
|
|
||||||
|
result = await config_entry.start_reconfigure_flow(hass)
|
||||||
|
|
||||||
|
receiver_info_2 = create_receiver_info(2)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.onkyo.config_flow.async_interview",
|
||||||
|
return_value=receiver_info_2,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={"host": receiver_info_2.host}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] is FlowResultType.ABORT
|
||||||
|
assert result2["reason"] == "unique_id_mismatch"
|
||||||
|
|
||||||
|
# unique id should remain unchanged
|
||||||
|
assert config_entry.unique_id == old_unique_id
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("user_input", "exception", "error"),
|
("user_input", "exception", "error"),
|
||||||
[
|
[
|
||||||
|
Loading…
x
Reference in New Issue
Block a user