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 DomainDatapull/83742/head^2
parent
bd342ddc13
commit
d3df4dd3c1
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
Loading…
Reference in New Issue