From d3df4dd3c1821e3256d9e8d6ee695ea44047dd3b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 10 Dec 2022 22:26:42 -0500 Subject: [PATCH] ESPHome: Use MAC as unique ID (#83741) * ESPHome: Use MAC as unique ID * Normalize incoming zeroconf/dhcp macs * Update comment * Test ESPHome without mac in zeroconf * Use format_mac * Remove unique ID index from DomainData --- homeassistant/components/esphome/__init__.py | 9 + .../components/esphome/bluetooth/__init__.py | 5 +- .../components/esphome/bluetooth/client.py | 8 +- .../components/esphome/config_flow.py | 109 +++--------- .../components/esphome/domain_data.py | 9 - homeassistant/components/esphome/strings.json | 3 +- tests/components/esphome/conftest.py | 42 ++++- tests/components/esphome/test_config_flow.py | 167 +++++++----------- tests/components/esphome/test_diagnostics.py | 2 +- tests/components/esphome/test_init.py | 30 ++++ 10 files changed, 183 insertions(+), 201 deletions(-) create mode 100644 tests/components/esphome/test_init.py diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index c4fb0e941cd..c0a3b1ed767 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -43,6 +43,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -252,6 +253,14 @@ async def async_setup_entry( # noqa: C901 nonlocal device_id try: device_info = await cli.device_info() + + # Migrate config entry to new unique ID if necessary + # This was changed in 2023.1 + if entry.unique_id != format_mac(device_info.mac_address): + hass.config_entries.async_update_entry( + entry, unique_id=format_mac(device_info.mac_address) + ) + entry_data.device_info = device_info assert cli.api_version is not None entry_data.api_version = cli.api_version diff --git a/homeassistant/components/esphome/bluetooth/__init__.py b/homeassistant/components/esphome/bluetooth/__init__.py index 4b6281c7c5a..594cf575e17 100644 --- a/homeassistant/components/esphome/bluetooth/__init__.py +++ b/homeassistant/components/esphome/bluetooth/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Callable +from functools import partial import logging from aioesphomeapi import APIClient @@ -63,7 +64,9 @@ async def async_connect_scanner( connectable, ) connector = HaBluetoothConnector( - client=ESPHomeClient, + # MyPy doesn't like partials, but this is correct + # https://github.com/python/mypy/issues/1484 + client=partial(ESPHomeClient, config_entry=entry), # type: ignore[arg-type] source=source, can_connect=_async_can_connect_factory(entry_data, source), ) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 541eb831ca5..878e305da00 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -23,6 +23,7 @@ from bleak.backends.service import BleakGATTServiceCollection from bleak.exc import BleakError from homeassistant.components.bluetooth import async_scanner_by_source +from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant from ..domain_data import DomainData @@ -125,7 +126,11 @@ class ESPHomeClient(BaseBleakClient): """ESPHome Bleak Client.""" def __init__( - self, address_or_ble_device: BLEDevice | str, *args: Any, **kwargs: Any + self, + address_or_ble_device: BLEDevice | str, + *args: Any, + config_entry: ConfigEntry, + **kwargs: Any, ) -> None: """Initialize the ESPHomeClient.""" assert isinstance(address_or_ble_device, BLEDevice) @@ -136,7 +141,6 @@ class ESPHomeClient(BaseBleakClient): assert self._ble_device.details is not None self._source = self._ble_device.details["source"] self.domain_data = DomainData.get(self._hass) - config_entry = self.domain_data.get_by_unique_id(self._source) self.entry_data = self.domain_data.get_entry_data(config_entry) self._client = self.entry_data.client self._is_connected = False diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 542aa011a7b..7186eda039b 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -21,8 +21,9 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.device_registry import format_mac -from . import CONF_NOISE_PSK, DOMAIN, DomainData +from . import CONF_NOISE_PSK, DOMAIN ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key" ESPHOME_URL = "https://esphome.io/" @@ -149,93 +150,35 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" + mac_address: str | None = discovery_info.properties.get("mac") + + # Mac address was added in Sept 20, 2021. + # https://github.com/esphome/esphome/pull/2303 + if mac_address is None: + return self.async_abort(reason="mdns_missing_mac") + + # mac address is lowercase and without :, normalize it + mac_address = format_mac(mac_address) + # Hostname is format: livingroom.local. - local_name = discovery_info.hostname[:-1] - node_name = local_name[: -len(".local")] - address = discovery_info.properties.get("address", local_name) - - # Check if already configured - await self.async_set_unique_id(node_name) - self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.host}) - - for entry in self._async_current_entries(): - already_configured = False - - if CONF_HOST in entry.data and entry.data[CONF_HOST] in ( - address, - discovery_info.host, - ): - # Is this address or IP address already configured? - already_configured = True - elif DomainData.get(self.hass).is_entry_loaded(entry): - # Does a config entry with this name already exist? - data = DomainData.get(self.hass).get_entry_data(entry) - - # Node names are unique in the network - if data.device_info is not None: - already_configured = data.device_info.name == node_name - - if already_configured: - # Backwards compat, we update old entries - if not entry.unique_id: - self.hass.config_entries.async_update_entry( - entry, - data={ - **entry.data, - CONF_HOST: discovery_info.host, - }, - unique_id=node_name, - ) - - return self.async_abort(reason="already_configured") - + self._name = discovery_info.hostname[: -len(".local.")] self._host = discovery_info.host self._port = discovery_info.port - self._name = node_name + + # Check if already configured + await self.async_set_unique_id(mac_address) + self._abort_if_unique_id_configured( + updates={CONF_HOST: self._host, CONF_PORT: self._port} + ) return await self.async_step_discovery_confirm() async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle DHCP discovery.""" - node_name = discovery_info.hostname - - await self.async_set_unique_id(node_name) + await self.async_set_unique_id(format_mac(discovery_info.macaddress)) self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) - - for entry in self._async_current_entries(): - found = False - - if CONF_HOST in entry.data and entry.data[CONF_HOST] in ( - discovery_info.ip, - f"{node_name}.local", - ): - # Is this address or IP address already configured? - found = True - elif DomainData.get(self.hass).is_entry_loaded(entry): - # Does a config entry with this name already exist? - data = DomainData.get(self.hass).get_entry_data(entry) - - # Node names are unique in the network - if data.device_info is not None: - found = data.device_info.name == node_name - - if found: - # Backwards compat, we update old entries - if not entry.unique_id: - self.hass.config_entries.async_update_entry( - entry, - data={ - **entry.data, - CONF_HOST: discovery_info.ip, - }, - unique_id=node_name, - ) - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) - ) - - break - + # This should never happen since we only listen to DHCP requests + # for configured devices. return self.async_abort(reason="already_configured") @callback @@ -334,9 +277,13 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): await cli.disconnect(force=True) self._name = self._device_info.name - await self.async_set_unique_id(self._name, raise_on_progress=False) + await self.async_set_unique_id( + self._device_info.mac_address, raise_on_progress=False + ) if not self._reauth_entry: - self._abort_if_unique_id_configured(updates={CONF_HOST: self._host}) + self._abort_if_unique_id_configured( + updates={CONF_HOST: self._host, CONF_PORT: self._port} + ) return None diff --git a/homeassistant/components/esphome/domain_data.py b/homeassistant/components/esphome/domain_data.py index 01f0a4d6b13..29226fdd792 100644 --- a/homeassistant/components/esphome/domain_data.py +++ b/homeassistant/components/esphome/domain_data.py @@ -23,11 +23,6 @@ class DomainData: _entry_datas: dict[str, RuntimeEntryData] = field(default_factory=dict) _stores: dict[str, Store] = field(default_factory=dict) - _entry_by_unique_id: dict[str, ConfigEntry] = field(default_factory=dict) - - def get_by_unique_id(self, unique_id: str) -> ConfigEntry: - """Get the config entry by its unique ID.""" - return self._entry_by_unique_id[unique_id] def get_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: """Return the runtime entry data associated with this config entry. @@ -41,13 +36,9 @@ class DomainData: if entry.entry_id in self._entry_datas: raise ValueError("Entry data for this entry is already set") self._entry_datas[entry.entry_id] = entry_data - if entry.unique_id: - self._entry_by_unique_id[entry.unique_id] = entry def pop_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: """Pop the runtime entry data instance associated with this config entry.""" - if entry.unique_id: - del self._entry_by_unique_id[entry.unique_id] return self._entry_datas.pop(entry.entry_id) def is_entry_loaded(self, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index 0ec4d93b405..d28bbd1b8a6 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "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." }, "error": { "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address", diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 4252e9d605b..cc4c8af9d73 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -1,5 +1,11 @@ """esphome session fixtures.""" +from __future__ import annotations + +from unittest.mock import AsyncMock, Mock, patch + +from aioesphomeapi import APIClient import pytest +from zeroconf import Zeroconf from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT @@ -25,7 +31,7 @@ def mock_config_entry() -> MockConfigEntry: CONF_PASSWORD: "pwd", CONF_NOISE_PSK: "12345678123456781234567812345678", }, - unique_id="esphome-device", + unique_id="11:22:33:44:55:aa", ) @@ -40,3 +46,37 @@ async def init_integration( await hass.async_block_till_done() return mock_config_entry + + +@pytest.fixture +def mock_client(): + """Mock APIClient.""" + mock_client = Mock(spec=APIClient) + + def mock_constructor( + address: str, + port: int, + password: str | None, + *, + client_info: str = "aioesphomeapi", + keepalive: float = 15.0, + zeroconf_instance: Zeroconf = None, + noise_psk: str | None = None, + expected_name: str | None = None, + ): + """Fake the client constructor.""" + mock_client.host = address + mock_client.port = port + mock_client.password = password + mock_client.zeroconf_instance = zeroconf_instance + mock_client.noise_psk = noise_psk + return mock_client + + mock_client.side_effect = mock_constructor + mock_client.connect = AsyncMock() + mock_client.disconnect = AsyncMock() + + with patch("homeassistant.components.esphome.APIClient", mock_client), patch( + "homeassistant.components.esphome.config_flow.APIClient", mock_client + ): + yield mock_client diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index efeb2d376cf..4a40f4ec4d7 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -1,9 +1,9 @@ """Test config flow.""" -from collections import namedtuple from unittest.mock import AsyncMock, MagicMock, patch from aioesphomeapi import ( APIConnectionError, + DeviceInfo, InvalidAuthAPIError, InvalidEncryptionKeyAPIError, RequiresEncryptionAPIError, @@ -19,34 +19,10 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -MockDeviceInfo = namedtuple("DeviceInfo", ["uses_password", "name"]) VALID_NOISE_PSK = "bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU=" INVALID_NOISE_PSK = "lSYBYEjQI1bVL8s2Vask4YytGMj1f1epNtmoim2yuTM=" -@pytest.fixture -def mock_client(): - """Mock APIClient.""" - with patch("homeassistant.components.esphome.config_flow.APIClient") as mock_client: - - def mock_constructor( - host, port, password, zeroconf_instance=None, noise_psk=None - ): - """Fake the client constructor.""" - mock_client.host = host - mock_client.port = port - mock_client.password = password - mock_client.zeroconf_instance = zeroconf_instance - mock_client.noise_psk = noise_psk - return mock_client - - mock_client.side_effect = mock_constructor - mock_client.connect = AsyncMock() - mock_client.disconnect = AsyncMock() - - yield mock_client - - @pytest.fixture(autouse=True) def mock_setup_entry(): """Mock setting up a config entry.""" @@ -65,7 +41,11 @@ async def test_user_connection_works(hass, mock_client, mock_zeroconf): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) + mock_client.device_info = AsyncMock( + return_value=DeviceInfo( + uses_password=False, name="test", mac_address="mock-mac" + ) + ) result = await hass.config_entries.flow.async_init( "esphome", @@ -81,7 +61,7 @@ async def test_user_connection_works(hass, mock_client, mock_zeroconf): CONF_NOISE_PSK: "", } assert result["title"] == "test" - assert result["result"].unique_id == "test" + assert result["result"].unique_id == "mock-mac" assert len(mock_client.connect.mock_calls) == 1 assert len(mock_client.device_info.mock_calls) == 1 @@ -97,7 +77,7 @@ async def test_user_connection_updates_host(hass, mock_client, mock_zeroconf): entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "test.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, - unique_id="test", + unique_id="mock-mac", ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -109,7 +89,11 @@ async def test_user_connection_updates_host(hass, mock_client, mock_zeroconf): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) + mock_client.device_info = AsyncMock( + return_value=DeviceInfo( + uses_password=False, name="test", mac_address="mock-mac" + ) + ) result = await hass.config_entries.flow.async_init( "esphome", @@ -165,7 +149,9 @@ async def test_user_connection_error(hass, mock_client, mock_zeroconf): async def test_user_with_password(hass, mock_client, mock_zeroconf): """Test user step with password.""" - mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(True, "test")) + mock_client.device_info = AsyncMock( + return_value=DeviceInfo(uses_password=True, name="test") + ) result = await hass.config_entries.flow.async_init( "esphome", @@ -192,7 +178,9 @@ async def test_user_with_password(hass, mock_client, mock_zeroconf): async def test_user_invalid_password(hass, mock_client, mock_zeroconf): """Test user step with invalid password.""" - mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(True, "test")) + mock_client.device_info = AsyncMock( + return_value=DeviceInfo(uses_password=True, name="test") + ) result = await hass.config_entries.flow.async_init( "esphome", @@ -216,7 +204,9 @@ async def test_user_invalid_password(hass, mock_client, mock_zeroconf): async def test_login_connection_error(hass, mock_client, mock_zeroconf): """Test user step with connection error on login attempt.""" - mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(True, "test")) + mock_client.device_info = AsyncMock( + return_value=DeviceInfo(uses_password=True, name="test") + ) result = await hass.config_entries.flow.async_init( "esphome", @@ -240,7 +230,11 @@ async def test_login_connection_error(hass, mock_client, mock_zeroconf): async def test_discovery_initiation(hass, mock_client, mock_zeroconf): """Test discovery importing works.""" - mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test8266")) + mock_client.device_info = AsyncMock( + return_value=DeviceInfo( + uses_password=False, name="test8266", mac_address="11:22:33:44:55:aa" + ) + ) service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", @@ -248,7 +242,9 @@ async def test_discovery_initiation(hass, mock_client, mock_zeroconf): hostname="test8266.local.", name="mock_name", port=6053, - properties={}, + properties={ + "mac": "1122334455aa", + }, type="mock_type", ) flow = await hass.config_entries.flow.async_init( @@ -265,18 +261,11 @@ async def test_discovery_initiation(hass, mock_client, mock_zeroconf): assert result["data"][CONF_PORT] == 6053 assert result["result"] - assert result["result"].unique_id == "test8266" + assert result["result"].unique_id == "11:22:33:44:55:aa" -async def test_discovery_already_configured_hostname(hass, mock_client): - """Test discovery aborts if already configured via hostname.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_HOST: "test8266.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, - ) - - entry.add_to_hass(hass) - +async def test_discovery_no_mac(hass, mock_client, mock_zeroconf): + """Test discovery aborted if old ESPHome without mac in zeroconf.""" service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", addresses=["192.168.43.183"], @@ -286,21 +275,19 @@ async def test_discovery_already_configured_hostname(hass, mock_client): properties={}, type="mock_type", ) - result = await hass.config_entries.flow.async_init( + flow = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" - - assert entry.unique_id == "test8266" + assert flow["type"] == FlowResultType.ABORT + assert flow["reason"] == "mdns_missing_mac" -async def test_discovery_already_configured_ip(hass, mock_client): - """Test discovery aborts if already configured via static IP.""" +async def test_discovery_already_configured(hass, mock_client): + """Test discovery aborts if already configured via hostname.""" entry = MockConfigEntry( domain=DOMAIN, - data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + data={CONF_HOST: "test8266.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, + unique_id="11:22:33:44:55:aa", ) entry.add_to_hass(hass) @@ -311,7 +298,7 @@ async def test_discovery_already_configured_ip(hass, mock_client): hostname="test8266.local.", name="mock_name", port=6053, - properties={"address": "192.168.43.183"}, + properties={"mac": "1122334455aa"}, type="mock_type", ) result = await hass.config_entries.flow.async_init( @@ -321,41 +308,6 @@ async def test_discovery_already_configured_ip(hass, mock_client): assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" - assert entry.unique_id == "test8266" - - -async def test_discovery_already_configured_name(hass, mock_client): - """Test discovery aborts if already configured via name.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, - ) - entry.add_to_hass(hass) - - mock_entry_data = MagicMock() - mock_entry_data.device_info.name = "test8266" - domain_data = DomainData.get(hass) - domain_data.set_entry_data(entry, mock_entry_data) - - service_info = zeroconf.ZeroconfServiceInfo( - host="192.168.43.184", - addresses=["192.168.43.184"], - hostname="test8266.local.", - name="mock_name", - port=6053, - properties={"address": "test8266.local"}, - type="mock_type", - ) - result = await hass.config_entries.flow.async_init( - "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info - ) - - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" - - assert entry.unique_id == "test8266" - assert entry.data[CONF_HOST] == "192.168.43.184" - async def test_discovery_duplicate_data(hass, mock_client): """Test discovery aborts if same mDNS packet arrives.""" @@ -365,11 +317,13 @@ async def test_discovery_duplicate_data(hass, mock_client): hostname="test8266.local.", name="mock_name", port=6053, - properties={"address": "test8266.local"}, + properties={"address": "test8266.local", "mac": "1122334455aa"}, type="mock_type", ) - mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test8266")) + mock_client.device_info = AsyncMock( + return_value=DeviceInfo(uses_password=False, name="test8266") + ) result = await hass.config_entries.flow.async_init( "esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF} @@ -389,6 +343,7 @@ async def test_discovery_updates_unique_id(hass, mock_client): entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + unique_id="11:22:33:44:55:aa", ) entry.add_to_hass(hass) @@ -399,7 +354,7 @@ async def test_discovery_updates_unique_id(hass, mock_client): hostname="test8266.local.", name="mock_name", port=6053, - properties={"address": "test8266.local"}, + properties={"address": "test8266.local", "mac": "1122334455aa"}, type="mock_type", ) result = await hass.config_entries.flow.async_init( @@ -409,7 +364,7 @@ async def test_discovery_updates_unique_id(hass, mock_client): assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" - assert entry.unique_id == "test8266" + assert entry.unique_id == "11:22:33:44:55:aa" async def test_user_requires_psk(hass, mock_client, mock_zeroconf): @@ -445,7 +400,9 @@ async def test_encryption_key_valid_psk(hass, mock_client, mock_zeroconf): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "encryption_key" - mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) + mock_client.device_info = AsyncMock( + return_value=DeviceInfo(uses_password=False, name="test") + ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) @@ -522,7 +479,9 @@ async def test_reauth_confirm_valid(hass, mock_client, mock_zeroconf): }, ) - mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) + mock_client.device_info = AsyncMock( + return_value=DeviceInfo(uses_password=False, name="test") + ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) @@ -559,7 +518,9 @@ async def test_reauth_confirm_invalid(hass, mock_client, mock_zeroconf): assert result["errors"] assert result["errors"]["base"] == "invalid_psk" - mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) + mock_client.device_info = AsyncMock( + return_value=DeviceInfo(uses_password=False, name="test") + ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) @@ -597,7 +558,9 @@ async def test_reauth_confirm_invalid_with_unique_id(hass, mock_client, mock_zer assert result["errors"] assert result["errors"]["base"] == "invalid_psk" - mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) + mock_client.device_info = AsyncMock( + return_value=DeviceInfo(uses_password=False, name="test") + ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) @@ -612,18 +575,14 @@ async def test_discovery_dhcp_updates_host(hass, mock_client): entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + unique_id="11:22:33:44:55:aa", ) entry.add_to_hass(hass) - mock_entry_data = MagicMock() - mock_entry_data.device_info.name = "test8266" - domain_data = DomainData.get(hass) - domain_data.set_entry_data(entry, mock_entry_data) - service_info = dhcp.DhcpServiceInfo( ip="192.168.43.184", hostname="test8266", - macaddress="00:00:00:00:00:00", + macaddress="1122334455aa", ) result = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info @@ -632,7 +591,6 @@ async def test_discovery_dhcp_updates_host(hass, mock_client): assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" - assert entry.unique_id == "test8266" assert entry.data[CONF_HOST] == "192.168.43.184" @@ -661,5 +619,4 @@ async def test_discovery_dhcp_no_changes(hass, mock_client): assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" - assert entry.unique_id == "test8266" assert entry.data[CONF_HOST] == "192.168.43.183" diff --git a/tests/components/esphome/test_diagnostics.py b/tests/components/esphome/test_diagnostics.py index 522ae0c8345..9f55b83a47c 100644 --- a/tests/components/esphome/test_diagnostics.py +++ b/tests/components/esphome/test_diagnostics.py @@ -27,4 +27,4 @@ async def test_diagnostics( CONF_PASSWORD: "**REDACTED**", CONF_NOISE_PSK: "**REDACTED**", } - assert result["config"]["unique_id"] == "esphome-device" + assert result["config"]["unique_id"] == "11:22:33:44:55:aa" diff --git a/tests/components/esphome/test_init.py b/tests/components/esphome/test_init.py new file mode 100644 index 00000000000..15fe57ffba8 --- /dev/null +++ b/tests/components/esphome/test_init.py @@ -0,0 +1,30 @@ +"""ESPHome set up tests.""" +from unittest.mock import AsyncMock + +from aioesphomeapi import DeviceInfo + +from homeassistant.components.esphome import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT + +from tests.common import MockConfigEntry + + +async def test_unique_id_updated_to_mac(hass, mock_client, mock_zeroconf): + """Test we update config entry unique ID to MAC address.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "test.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, + unique_id="mock-config-name", + ) + entry.add_to_hass(hass) + + mock_client.device_info = AsyncMock( + return_value=DeviceInfo( + mac_address="1122334455aa", + ) + ) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.unique_id == "11:22:33:44:55:aa"