Improve ESPHome abort messages for already-configured devices (#143289)

* Improve ESPHome abort messages for already-configured devices

Users often struggle to identify which ESPHome device is already configured—especially when replacing a device or renaming an existing one.
This PR improves the abort messages to include more helpful details, so users can pinpoint the conflicting device without needing to dig through the `core.config_entries` file manually.

* Update homeassistant/components/esphome/strings.json
This commit is contained in:
J. Nick Koston 2025-04-21 03:41:15 -10:00 committed by GitHub
parent 352ef0d009
commit 6698b3a1dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 98 additions and 16 deletions

View File

@ -306,7 +306,32 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
updates[CONF_HOST] = host updates[CONF_HOST] = host
if port is not None: if port is not None:
updates[CONF_PORT] = port updates[CONF_PORT] = port
self._abort_if_unique_id_configured(updates=updates) self._abort_unique_id_configured_with_details(updates=updates)
@callback
def _abort_unique_id_configured_with_details(self, updates: dict[str, Any]) -> None:
"""Abort if unique_id is already configured with details."""
assert self.unique_id is not None
if not (
conflict_entry := self.hass.config_entries.async_entry_for_domain_unique_id(
self.handler, self.unique_id
)
):
return
assert conflict_entry.unique_id is not None
if updates:
error = "already_configured_updates"
else:
error = "already_configured_detailed"
self._abort_if_unique_id_configured(
updates=updates,
error=error,
description_placeholders={
"title": conflict_entry.title,
"name": conflict_entry.data.get(CONF_DEVICE_NAME, "unknown"),
"mac": format_mac(conflict_entry.unique_id),
},
)
async def async_step_mqtt( async def async_step_mqtt(
self, discovery_info: MqttServiceInfo self, discovery_info: MqttServiceInfo
@ -341,7 +366,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
# Check if already configured # Check if already configured
await self.async_set_unique_id(mac_address) await self.async_set_unique_id(mac_address)
self._abort_if_unique_id_configured( self._abort_unique_id_configured_with_details(
updates={CONF_HOST: self._host, CONF_PORT: self._port} updates={CONF_HOST: self._host, CONF_PORT: self._port}
) )
@ -479,7 +504,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
data=self._reauth_entry.data | self._async_make_config_data(), data=self._reauth_entry.data | self._async_make_config_data(),
) )
assert self._host is not None assert self._host is not None
self._abort_if_unique_id_configured( self._abort_unique_id_configured_with_details(
updates={ updates={
CONF_HOST: self._host, CONF_HOST: self._host,
CONF_PORT: self._port, CONF_PORT: self._port,
@ -510,7 +535,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
if not ( if not (
unique_id_matches := (self.unique_id == self._reconfig_entry.unique_id) unique_id_matches := (self.unique_id == self._reconfig_entry.unique_id)
): ):
self._abort_if_unique_id_configured( self._abort_unique_id_configured_with_details(
updates={ updates={
CONF_HOST: self._host, CONF_HOST: self._host,
CONF_PORT: self._port, CONF_PORT: self._port,
@ -640,7 +665,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
mac_address = format_mac(self._device_info.mac_address) mac_address = format_mac(self._device_info.mac_address)
await self.async_set_unique_id(mac_address, raise_on_progress=False) await self.async_set_unique_id(mac_address, raise_on_progress=False)
if self.source not in (SOURCE_REAUTH, SOURCE_RECONFIGURE): if self.source not in (SOURCE_REAUTH, SOURCE_RECONFIGURE):
self._abort_if_unique_id_configured( self._abort_unique_id_configured_with_details(
updates={ updates={
CONF_HOST: self._host, CONF_HOST: self._host,
CONF_PORT: self._port, CONF_PORT: self._port,

View File

@ -2,6 +2,8 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_configured_detailed": "A device `{name}`, with MAC address `{mac}` is already configured as `{title}`.",
"already_configured_updates": "A device `{name}`, with MAC address `{mac}` is already configured as `{title}`; the existing configuration will be updated with the validated data.",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"mdns_missing_mac": "Missing MAC address in mDNS properties.", "mdns_missing_mac": "Missing MAC address in mDNS properties.",

View File

@ -119,7 +119,12 @@ async def test_user_connection_updates_host(
data={CONF_HOST: "127.0.0.1", CONF_PORT: 80}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 80},
) )
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured_updates"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "unknown",
"mac": "11:22:33:44:55:aa",
}
assert entry.data[CONF_HOST] == "127.0.0.1" assert entry.data[CONF_HOST] == "127.0.0.1"
@ -173,7 +178,12 @@ async def test_user_sets_unique_id(
{CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, {CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
) )
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured_updates"
assert result["description_placeholders"] == {
"title": "test",
"name": "test",
"mac": "11:22:33:44:55:aa",
}
@pytest.mark.usefixtures("mock_zeroconf") @pytest.mark.usefixtures("mock_zeroconf")
@ -645,7 +655,12 @@ async def test_discovery_already_configured(
) )
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured_updates"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "unknown",
"mac": "11:22:33:44:55:aa",
}
async def test_discovery_duplicate_data( async def test_discovery_duplicate_data(
@ -701,7 +716,12 @@ async def test_discovery_updates_unique_id(
) )
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured_updates"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "unknown",
"mac": "11:22:33:44:55:aa",
}
assert entry.unique_id == "11:22:33:44:55:aa" assert entry.unique_id == "11:22:33:44:55:aa"
@ -1159,7 +1179,12 @@ async def test_discovery_dhcp_updates_host(
) )
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured_updates"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "unknown",
"mac": "11:22:33:44:55:aa",
}
assert entry.data[CONF_HOST] == "192.168.43.184" assert entry.data[CONF_HOST] == "192.168.43.184"
@ -1188,7 +1213,12 @@ async def test_discovery_dhcp_does_not_update_host_wrong_mac(
) )
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured_detailed"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "unknown",
"mac": "11:22:33:44:55:aa",
}
# Mac was wrong, should not update # Mac was wrong, should not update
assert entry.data[CONF_HOST] == "192.168.43.183" assert entry.data[CONF_HOST] == "192.168.43.183"
@ -1217,7 +1247,12 @@ async def test_discovery_dhcp_does_not_update_host_wrong_mac_bad_key(
) )
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured_detailed"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "unknown",
"mac": "11:22:33:44:55:aa",
}
# Mac was wrong, should not update # Mac was wrong, should not update
assert entry.data[CONF_HOST] == "192.168.43.183" assert entry.data[CONF_HOST] == "192.168.43.183"
@ -1246,7 +1281,12 @@ async def test_discovery_dhcp_does_not_update_host_missing_mac_bad_key(
) )
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured_detailed"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "unknown",
"mac": "11:22:33:44:55:aa",
}
# Mac was missing, should not update # Mac was missing, should not update
assert entry.data[CONF_HOST] == "192.168.43.183" assert entry.data[CONF_HOST] == "192.168.43.183"
@ -1999,7 +2039,12 @@ async def test_reconfig_mac_used_by_other_entry(
) )
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured_updates"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "test4",
"mac": "11:22:33:44:55:bb",
}
@pytest.mark.usefixtures("mock_zeroconf", "mock_setup_entry") @pytest.mark.usefixtures("mock_zeroconf", "mock_setup_entry")

View File

@ -743,7 +743,12 @@ async def test_connection_aborted_wrong_device(
) )
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured_updates"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "test",
"mac": "11:22:33:44:55:aa",
}
assert entry.data[CONF_HOST] == "192.168.43.184" assert entry.data[CONF_HOST] == "192.168.43.184"
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(new_info.mock_calls) == 2 assert len(new_info.mock_calls) == 2
@ -812,7 +817,12 @@ async def test_connection_aborted_wrong_device_same_name(
) )
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured_updates"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "test",
"mac": "11:22:33:44:55:aa",
}
assert entry.data[CONF_HOST] == "192.168.43.184" assert entry.data[CONF_HOST] == "192.168.43.184"
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(new_info.mock_calls) == 2 assert len(new_info.mock_calls) == 2