mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Make ignoring Z-Wave JS discoveries work (#179595)
Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
co-authored by
Claude
Paulus Schoutsen
parent
414bd3d878
commit
8379f19107
@@ -24,6 +24,7 @@ from homeassistant.components.hassio import (
|
||||
)
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_ESPHOME,
|
||||
SOURCE_IGNORE,
|
||||
SOURCE_USB,
|
||||
SOURCE_ZEROCONF,
|
||||
ConfigEntry,
|
||||
@@ -235,6 +236,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._recommended_install = False
|
||||
self._rf_region: str | None = None
|
||||
self._entry_unloaded_by_flow = False
|
||||
# Set if the flow unique id is a placeholder that must be replaced
|
||||
# with the home ID before a config entry is created.
|
||||
self._unique_id_is_placeholder = False
|
||||
|
||||
async def async_step_install_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -571,10 +575,13 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
await self.async_set_unique_id(
|
||||
f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}"
|
||||
)
|
||||
# We don't need to check if the unique_id is already configured
|
||||
# since we will update the unique_id before finishing the flow.
|
||||
# The unique_id set above is just a temporary value to avoid
|
||||
# duplicate discovery flows.
|
||||
# The unique id set above is a placeholder that is replaced with the
|
||||
# home ID before an entry is created, so only check ignored entries.
|
||||
if any(
|
||||
entry.source == SOURCE_IGNORE and entry.unique_id == self.unique_id
|
||||
for entry in self._async_current_entries(include_ignore=True)
|
||||
):
|
||||
return self.async_abort(reason="already_configured")
|
||||
dev_path = discovery_info.device
|
||||
self.usb_path = dev_path
|
||||
if manufacturer == "Nabu Casa" and description == "ZWA-2 - Nabu Casa ZWA-2":
|
||||
@@ -1025,7 +1032,11 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
discovery_info = await self._async_get_addon_discovery_info()
|
||||
self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}"
|
||||
|
||||
if not self.unique_id or self.source == SOURCE_USB:
|
||||
if (
|
||||
not self.unique_id
|
||||
or self.source == SOURCE_USB
|
||||
or self._unique_id_is_placeholder
|
||||
):
|
||||
if not self.version_info:
|
||||
try:
|
||||
self.version_info = await async_get_version_info(
|
||||
@@ -1037,6 +1048,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
await self.async_set_unique_id(
|
||||
str(self.version_info.home_id), raise_on_progress=False
|
||||
)
|
||||
self._unique_id_is_placeholder = False
|
||||
|
||||
if (
|
||||
existing_entry := next(
|
||||
@@ -1659,6 +1671,11 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if not is_hassio(self.hass):
|
||||
return self.async_abort(reason="not_hassio")
|
||||
|
||||
# The adapter may first be discovered without a home ID and get the
|
||||
# placeholder unique id below, then report a home ID on a later
|
||||
# discovery. Track the placeholder id so such a discovery can be
|
||||
# deduplicated against a pending prompt or an ignored entry.
|
||||
placeholder_unique_id = f"esphome_{discovery_info.name}"
|
||||
if discovery_info.zwave_home_id:
|
||||
existing_entry: ConfigEntry | None = None
|
||||
if (
|
||||
@@ -1706,6 +1723,11 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
if any(
|
||||
flow["context"].get("unique_id") == placeholder_unique_id
|
||||
for flow in self._async_in_progress()
|
||||
):
|
||||
return self.async_abort(reason="already_in_progress")
|
||||
# We are not aborting if home ID configured
|
||||
# here, we just want to make sure that it's set
|
||||
# We will update a USB based config entry
|
||||
@@ -1714,6 +1736,19 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
await self.async_set_unique_id(
|
||||
str(discovery_info.zwave_home_id), raise_on_progress=False
|
||||
)
|
||||
else:
|
||||
# Set a placeholder unique id so the discovery can be ignored
|
||||
# also when the adapter doesn't report a home ID yet.
|
||||
# It is replaced with the home ID before an entry is created.
|
||||
self._unique_id_is_placeholder = True
|
||||
await self.async_set_unique_id(placeholder_unique_id)
|
||||
|
||||
if any(
|
||||
entry.source == SOURCE_IGNORE
|
||||
and entry.unique_id in (self.unique_id, placeholder_unique_id)
|
||||
for entry in self._async_current_entries(include_ignore=True)
|
||||
):
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
self.socket_path = discovery_info.socket_path
|
||||
home_id_display = format_home_id_for_display(discovery_info.zwave_home_id)
|
||||
|
||||
@@ -1722,6 +1722,66 @@ async def test_esphome_discovery_no_home_id_configured_socket_no_migration(
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("supervisor", "addon_running")
|
||||
async def test_esphome_discovery_placeholder_then_home_id(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test a home ID discovery dedups against a pending placeholder prompt."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ESPHOME},
|
||||
data=ESPHOME_DISCOVERY_INFO_CLEAN,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "installation_type"
|
||||
|
||||
# The same adapter now reports a home ID while its prompt is open.
|
||||
home_id_info = ESPHomeServiceInfo(
|
||||
name=ESPHOME_DISCOVERY_INFO_CLEAN.name,
|
||||
zwave_home_id=1234,
|
||||
ip_address=ESPHOME_DISCOVERY_INFO_CLEAN.ip_address,
|
||||
port=ESPHOME_DISCOVERY_INFO_CLEAN.port,
|
||||
)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ESPHOME},
|
||||
data=home_id_info,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_in_progress"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("supervisor", "addon_running")
|
||||
async def test_esphome_discovery_placeholder_ignored_then_home_id(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test a home ID discovery honors a placeholder-based ignore."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
source=config_entries.SOURCE_IGNORE,
|
||||
unique_id="esphome_mock-name",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
# The adapter that was ignored without a home ID now reports one.
|
||||
home_id_info = ESPHomeServiceInfo(
|
||||
name="mock-name",
|
||||
zwave_home_id=1234,
|
||||
ip_address="192.168.1.100",
|
||||
port=6053,
|
||||
)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ESPHOME},
|
||||
data=home_id_info,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("supervisor", "addon_running", "addon_info")
|
||||
async def test_esphome_discovery_same_socket_no_reload(
|
||||
hass: HomeAssistant,
|
||||
@@ -1765,6 +1825,78 @@ async def test_esphome_discovery_same_socket_no_reload(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("supervisor", "addon_running")
|
||||
@pytest.mark.parametrize(
|
||||
("esphome_discovery_info", "ignored_unique_id"),
|
||||
[
|
||||
pytest.param(ESPHOME_DISCOVERY_INFO, "1234", id="home_id"),
|
||||
pytest.param(
|
||||
ESPHOME_DISCOVERY_INFO_CLEAN, "esphome_mock-name", id="no_home_id"
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_esphome_discovery_ignored(
|
||||
hass: HomeAssistant,
|
||||
esphome_discovery_info: ESPHomeServiceInfo,
|
||||
ignored_unique_id: str,
|
||||
) -> None:
|
||||
"""Test ESPHome discovery aborts when the discovery was ignored."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
source=config_entries.SOURCE_IGNORE,
|
||||
unique_id=ignored_unique_id,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ESPHOME},
|
||||
data=esphome_discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("supervisor", "addon_running")
|
||||
async def test_esphome_discovery_without_home_id_can_be_ignored(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test a discovery without a home ID gets a unique id for ignoring."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ESPHOME},
|
||||
data=ESPHOME_DISCOVERY_INFO_CLEAN,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "installation_type"
|
||||
|
||||
flows = hass.config_entries.flow.async_progress_by_handler(
|
||||
DOMAIN, match_context={"source": config_entries.SOURCE_ESPHOME}
|
||||
)
|
||||
assert len(flows) == 1
|
||||
assert flows[0]["context"]["unique_id"] == "esphome_mock-name"
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IGNORE},
|
||||
data={"unique_id": "esphome_mock-name", "title": "ZWA-2 proxy"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
# The discovery prompt is gone and rediscovery aborts.
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ESPHOME},
|
||||
data=ESPHOME_DISCOVERY_INFO_CLEAN,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("supervisor", "addon_running", "addon_info")
|
||||
async def test_esphome_discovery_already_configured_unmanaged_addon(
|
||||
hass: HomeAssistant,
|
||||
@@ -2420,6 +2552,29 @@ async def test_usb_discovery_leaves_manual_entry_alone(
|
||||
assert entry.data == {"url": "ws://external-server:3000"}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("supervisor", "addon_info")
|
||||
async def test_usb_discovery_ignored(
|
||||
hass: HomeAssistant,
|
||||
mock_usb_serial_by_id: MagicMock,
|
||||
) -> None:
|
||||
"""Test USB discovery aborts when the discovery was ignored."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
source=config_entries.SOURCE_IGNORE,
|
||||
unique_id="AAAA:AAAA_1234_test_zwave radio",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USB},
|
||||
data=USB_DISCOVERY_INFO,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("supervisor", "addon_info")
|
||||
async def test_abort_usb_discovery_addon_required(hass: HomeAssistant) -> None:
|
||||
"""Test usb discovery aborted when existing entry not using add-on."""
|
||||
|
||||
Reference in New Issue
Block a user