Add ZHA config flow single instance checks for zeroconf and hardware (#77612)
This commit is contained in:
parent
67ccb6f25a
commit
f8fc90bc07
2 changed files with 136 additions and 62 deletions
|
@ -551,6 +551,36 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN
|
||||||
|
|
||||||
return await self.async_step_choose_serial_port(user_input)
|
return await self.async_step_choose_serial_port(user_input)
|
||||||
|
|
||||||
|
async def async_step_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Confirm a discovery."""
|
||||||
|
self._set_confirm_only()
|
||||||
|
|
||||||
|
# Don't permit discovery if ZHA is already set up
|
||||||
|
if self._async_current_entries():
|
||||||
|
return self.async_abort(reason="single_instance_allowed")
|
||||||
|
|
||||||
|
# Without confirmation, discovery can automatically progress into parts of the
|
||||||
|
# config flow logic that interacts with hardware!
|
||||||
|
if user_input is not None or not onboarding.async_is_onboarded(self.hass):
|
||||||
|
# Probe the radio type if we don't have one yet
|
||||||
|
if self._radio_type is None and not await self._detect_radio_type():
|
||||||
|
# This path probably will not happen now that we have
|
||||||
|
# more precise USB matching unless there is a problem
|
||||||
|
# with the device
|
||||||
|
return self.async_abort(reason="usb_probe_failed")
|
||||||
|
|
||||||
|
if self._device_settings is None:
|
||||||
|
return await self.async_step_manual_port_config()
|
||||||
|
|
||||||
|
return await self.async_step_choose_formation_strategy()
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="confirm",
|
||||||
|
description_placeholders={CONF_NAME: self._title},
|
||||||
|
)
|
||||||
|
|
||||||
async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult:
|
async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult:
|
||||||
"""Handle usb discovery."""
|
"""Handle usb discovery."""
|
||||||
vid = discovery_info.vid
|
vid = discovery_info.vid
|
||||||
|
@ -570,9 +600,6 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
# Check if already configured
|
|
||||||
if self._async_current_entries():
|
|
||||||
return self.async_abort(reason="single_instance_allowed")
|
|
||||||
|
|
||||||
# If they already have a discovery for deconz we ignore the usb discovery as
|
# If they already have a discovery for deconz we ignore the usb discovery as
|
||||||
# they probably want to use it there instead
|
# they probably want to use it there instead
|
||||||
|
@ -591,32 +618,14 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN
|
||||||
vid,
|
vid,
|
||||||
pid,
|
pid,
|
||||||
)
|
)
|
||||||
self._set_confirm_only()
|
|
||||||
self.context["title_placeholders"] = {CONF_NAME: self._title}
|
self.context["title_placeholders"] = {CONF_NAME: self._title}
|
||||||
return await self.async_step_confirm()
|
return await self.async_step_confirm()
|
||||||
|
|
||||||
async def async_step_confirm(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> FlowResult:
|
|
||||||
"""Confirm a discovery."""
|
|
||||||
if user_input is not None or not onboarding.async_is_onboarded(self.hass):
|
|
||||||
if not await self._detect_radio_type():
|
|
||||||
# This path probably will not happen now that we have
|
|
||||||
# more precise USB matching unless there is a problem
|
|
||||||
# with the device
|
|
||||||
return self.async_abort(reason="usb_probe_failed")
|
|
||||||
|
|
||||||
return await self.async_step_choose_formation_strategy()
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="confirm",
|
|
||||||
description_placeholders={CONF_NAME: self._title},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_zeroconf(
|
async def async_step_zeroconf(
|
||||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||||
) -> FlowResult:
|
) -> FlowResult:
|
||||||
"""Handle zeroconf discovery."""
|
"""Handle zeroconf discovery."""
|
||||||
|
|
||||||
# Hostname is format: livingroom.local.
|
# Hostname is format: livingroom.local.
|
||||||
local_name = discovery_info.hostname[:-1]
|
local_name = discovery_info.hostname[:-1]
|
||||||
radio_type = discovery_info.properties.get("radio_type") or local_name
|
radio_type = discovery_info.properties.get("radio_type") or local_name
|
||||||
|
@ -638,10 +647,6 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if already configured
|
|
||||||
if self._async_current_entries():
|
|
||||||
return self.async_abort(reason="single_instance_allowed")
|
|
||||||
|
|
||||||
self.context["title_placeholders"] = {CONF_NAME: node_name}
|
self.context["title_placeholders"] = {CONF_NAME: node_name}
|
||||||
self._title = device_path
|
self._title = device_path
|
||||||
self._device_path = device_path
|
self._device_path = device_path
|
||||||
|
@ -653,15 +658,12 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN
|
||||||
else:
|
else:
|
||||||
self._radio_type = RadioType.znp
|
self._radio_type = RadioType.znp
|
||||||
|
|
||||||
return await self.async_step_manual_port_config()
|
return await self.async_step_confirm()
|
||||||
|
|
||||||
async def async_step_hardware(
|
async def async_step_hardware(
|
||||||
self, data: dict[str, Any] | None = None
|
self, data: dict[str, Any] | None = None
|
||||||
) -> FlowResult:
|
) -> FlowResult:
|
||||||
"""Handle hardware flow."""
|
"""Handle hardware flow."""
|
||||||
if self._async_current_entries():
|
|
||||||
return self.async_abort(reason="single_instance_allowed")
|
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return self.async_abort(reason="invalid_hardware_data")
|
return self.async_abort(reason="invalid_hardware_data")
|
||||||
if data.get("radio_type") != "efr32":
|
if data.get("radio_type") != "efr32":
|
||||||
|
@ -691,7 +693,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN
|
||||||
self._device_path = device_settings[CONF_DEVICE_PATH]
|
self._device_path = device_settings[CONF_DEVICE_PATH]
|
||||||
self._device_settings = device_settings
|
self._device_settings = device_settings
|
||||||
|
|
||||||
return await self.async_step_choose_formation_strategy()
|
return await self.async_step_confirm()
|
||||||
|
|
||||||
|
|
||||||
class ZhaOptionsFlowHandler(BaseZhaFlow, config_entries.OptionsFlow):
|
class ZhaOptionsFlowHandler(BaseZhaFlow, config_entries.OptionsFlow):
|
||||||
|
|
|
@ -107,22 +107,31 @@ async def test_zeroconf_discovery_znp(hass):
|
||||||
flow = await hass.config_entries.flow.async_init(
|
flow = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info
|
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info
|
||||||
)
|
)
|
||||||
|
assert flow["step_id"] == "confirm"
|
||||||
|
|
||||||
|
# Confirm discovery
|
||||||
result1 = await hass.config_entries.flow.async_configure(
|
result1 = await hass.config_entries.flow.async_configure(
|
||||||
flow["flow_id"], user_input={}
|
flow["flow_id"], user_input={}
|
||||||
)
|
)
|
||||||
|
assert result1["step_id"] == "manual_port_config"
|
||||||
|
|
||||||
assert result1["type"] == FlowResultType.MENU
|
# Confirm port settings
|
||||||
assert result1["step_id"] == "choose_formation_strategy"
|
|
||||||
|
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result1["flow_id"],
|
result1["flow_id"], user_input={}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == FlowResultType.MENU
|
||||||
|
assert result2["step_id"] == "choose_formation_strategy"
|
||||||
|
|
||||||
|
result3 = await hass.config_entries.flow.async_configure(
|
||||||
|
result2["flow_id"],
|
||||||
user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS},
|
user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
||||||
assert result2["title"] == "socket://192.168.1.200:6638"
|
assert result3["title"] == "socket://192.168.1.200:6638"
|
||||||
assert result2["data"] == {
|
assert result3["data"] == {
|
||||||
CONF_DEVICE: {
|
CONF_DEVICE: {
|
||||||
CONF_BAUDRATE: 115200,
|
CONF_BAUDRATE: 115200,
|
||||||
CONF_FLOWCONTROL: None,
|
CONF_FLOWCONTROL: None,
|
||||||
|
@ -148,22 +157,31 @@ async def test_zigate_via_zeroconf(setup_entry_mock, hass):
|
||||||
flow = await hass.config_entries.flow.async_init(
|
flow = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info
|
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info
|
||||||
)
|
)
|
||||||
|
assert flow["step_id"] == "confirm"
|
||||||
|
|
||||||
|
# Confirm discovery
|
||||||
result1 = await hass.config_entries.flow.async_configure(
|
result1 = await hass.config_entries.flow.async_configure(
|
||||||
flow["flow_id"], user_input={}
|
flow["flow_id"], user_input={}
|
||||||
)
|
)
|
||||||
|
assert result1["step_id"] == "manual_port_config"
|
||||||
|
|
||||||
assert result1["type"] == FlowResultType.MENU
|
# Confirm port settings
|
||||||
assert result1["step_id"] == "choose_formation_strategy"
|
|
||||||
|
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result1["flow_id"],
|
result1["flow_id"], user_input={}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == FlowResultType.MENU
|
||||||
|
assert result2["step_id"] == "choose_formation_strategy"
|
||||||
|
|
||||||
|
result3 = await hass.config_entries.flow.async_configure(
|
||||||
|
result2["flow_id"],
|
||||||
user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS},
|
user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
||||||
assert result2["title"] == "socket://192.168.1.200:1234"
|
assert result3["title"] == "socket://192.168.1.200:1234"
|
||||||
assert result2["data"] == {
|
assert result3["data"] == {
|
||||||
CONF_DEVICE: {
|
CONF_DEVICE: {
|
||||||
CONF_DEVICE_PATH: "socket://192.168.1.200:1234",
|
CONF_DEVICE_PATH: "socket://192.168.1.200:1234",
|
||||||
},
|
},
|
||||||
|
@ -187,22 +205,31 @@ async def test_efr32_via_zeroconf(hass):
|
||||||
flow = await hass.config_entries.flow.async_init(
|
flow = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info
|
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info
|
||||||
)
|
)
|
||||||
|
assert flow["step_id"] == "confirm"
|
||||||
|
|
||||||
|
# Confirm discovery
|
||||||
result1 = await hass.config_entries.flow.async_configure(
|
result1 = await hass.config_entries.flow.async_configure(
|
||||||
flow["flow_id"], user_input={}
|
flow["flow_id"], user_input={}
|
||||||
)
|
)
|
||||||
|
assert result1["step_id"] == "manual_port_config"
|
||||||
|
|
||||||
assert result1["type"] == FlowResultType.MENU
|
# Confirm port settings
|
||||||
assert result1["step_id"] == "choose_formation_strategy"
|
|
||||||
|
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result1["flow_id"],
|
result1["flow_id"], user_input={}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == FlowResultType.MENU
|
||||||
|
assert result2["step_id"] == "choose_formation_strategy"
|
||||||
|
|
||||||
|
result3 = await hass.config_entries.flow.async_configure(
|
||||||
|
result2["flow_id"],
|
||||||
user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS},
|
user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
||||||
assert result2["title"] == "socket://192.168.1.200:6638"
|
assert result3["title"] == "socket://192.168.1.200:6638"
|
||||||
assert result2["data"] == {
|
assert result3["data"] == {
|
||||||
CONF_DEVICE: {
|
CONF_DEVICE: {
|
||||||
CONF_DEVICE_PATH: "socket://192.168.1.200:6638",
|
CONF_DEVICE_PATH: "socket://192.168.1.200:6638",
|
||||||
CONF_BAUDRATE: 115200,
|
CONF_BAUDRATE: 115200,
|
||||||
|
@ -282,6 +309,37 @@ async def test_discovery_via_zeroconf_ip_change_ignored(hass):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_discovery_confirm_final_abort_if_entries(hass):
|
||||||
|
"""Test discovery aborts if ZHA was set up after the confirmation dialog is shown."""
|
||||||
|
service_info = zeroconf.ZeroconfServiceInfo(
|
||||||
|
host="192.168.1.200",
|
||||||
|
addresses=["192.168.1.200"],
|
||||||
|
hostname="tube._tube_zb_gw._tcp.local.",
|
||||||
|
name="tube",
|
||||||
|
port=6053,
|
||||||
|
properties={"name": "tube_123456"},
|
||||||
|
type="mock_type",
|
||||||
|
)
|
||||||
|
flow = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info
|
||||||
|
)
|
||||||
|
assert flow["step_id"] == "confirm"
|
||||||
|
|
||||||
|
# ZHA was somehow set up while we were in the config flow
|
||||||
|
with patch(
|
||||||
|
"homeassistant.config_entries.ConfigFlow._async_current_entries",
|
||||||
|
return_value=[MagicMock()],
|
||||||
|
):
|
||||||
|
# Confirm discovery
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
flow["flow_id"], user_input={}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Config will fail
|
||||||
|
assert result["type"] == FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "single_instance_allowed"
|
||||||
|
|
||||||
|
|
||||||
@patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True))
|
@patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True))
|
||||||
async def test_discovery_via_usb(hass):
|
async def test_discovery_via_usb(hass):
|
||||||
"""Test usb flow -- radio detected."""
|
"""Test usb flow -- radio detected."""
|
||||||
|
@ -293,15 +351,16 @@ async def test_discovery_via_usb(hass):
|
||||||
description="zigbee radio",
|
description="zigbee radio",
|
||||||
manufacturer="test",
|
manufacturer="test",
|
||||||
)
|
)
|
||||||
result = await hass.config_entries.flow.async_init(
|
result1 = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": SOURCE_USB}, data=discovery_info
|
DOMAIN, context={"source": SOURCE_USB}, data=discovery_info
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert result["type"] == FlowResultType.FORM
|
|
||||||
assert result["step_id"] == "confirm"
|
assert result1["type"] == FlowResultType.FORM
|
||||||
|
assert result1["step_id"] == "confirm"
|
||||||
|
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], user_input={}
|
result1["flow_id"], user_input={}
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
@ -878,17 +937,30 @@ async def test_hardware(onboarded, hass):
|
||||||
DOMAIN, context={"source": "hardware"}, data=data
|
DOMAIN, context={"source": "hardware"}, data=data
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result1["type"] == FlowResultType.MENU
|
if onboarded:
|
||||||
assert result1["step_id"] == "choose_formation_strategy"
|
# Confirm discovery
|
||||||
|
assert result1["type"] == FlowResultType.FORM
|
||||||
|
assert result1["step_id"] == "confirm"
|
||||||
|
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result1["flow_id"],
|
result1["flow_id"],
|
||||||
|
user_input={},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# No need to confirm
|
||||||
|
result2 = result1
|
||||||
|
|
||||||
|
assert result2["type"] == FlowResultType.MENU
|
||||||
|
assert result2["step_id"] == "choose_formation_strategy"
|
||||||
|
|
||||||
|
result3 = await hass.config_entries.flow.async_configure(
|
||||||
|
result2["flow_id"],
|
||||||
user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS},
|
user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result2["title"] == "Yellow"
|
assert result3["title"] == "Yellow"
|
||||||
assert result2["data"] == {
|
assert result3["data"] == {
|
||||||
CONF_DEVICE: {
|
CONF_DEVICE: {
|
||||||
CONF_BAUDRATE: 115200,
|
CONF_BAUDRATE: 115200,
|
||||||
CONF_FLOWCONTROL: "hardware",
|
CONF_FLOWCONTROL: "hardware",
|
||||||
|
|
Loading…
Add table
Reference in a new issue