hass-core/tests/components/esphome/test_config_flow.py
J. Nick Koston f0cb03e631
Handle missing or incorrect device name and unique id for ESPHome during manual add (#95678)
* Handle incorrect or missing device name for ESPHome noise encryption

If we did not have the device name during setup we could never
get the key from the dashboard. The device will send us
its name if we try encryption which allows us to find the
right key from the dashboard.

This should help get users unstuck when they change the key
and cannot get the device back online after deleting and
trying to set it up again manually

* bump lib to get name

* tweak

* reduce number of connections

* less connections when we know we will fail

* coverage shows it works but it does not

* add more coverage

* fix test

* bump again
2023-07-02 10:29:45 -04:00

1304 lines
41 KiB
Python

"""Test config flow."""
import asyncio
from unittest.mock import AsyncMock, MagicMock, patch
from aioesphomeapi import (
APIClient,
APIConnectionError,
DeviceInfo,
InvalidAuthAPIError,
InvalidEncryptionKeyAPIError,
RequiresEncryptionAPIError,
ResolveAPIError,
)
import aiohttp
import pytest
from homeassistant import config_entries, data_entry_flow
from homeassistant.components import dhcp, zeroconf
from homeassistant.components.esphome import (
CONF_DEVICE_NAME,
CONF_NOISE_PSK,
DOMAIN,
DomainData,
dashboard,
)
from homeassistant.components.esphome.const import (
CONF_ALLOW_SERVICE_CALLS,
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
)
from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from . import VALID_NOISE_PSK
from tests.common import MockConfigEntry
INVALID_NOISE_PSK = "lSYBYEjQI1bVL8s2Vask4YytGMj1f1epNtmoim2yuTM="
WRONG_NOISE_PSK = "GP+ciK+nVfTQ/gcz6uOdS+oKEdJgesU+jeu8Ssj2how="
@pytest.fixture(autouse=False)
def mock_setup_entry():
"""Mock setting up a config entry."""
with patch("homeassistant.components.esphome.async_setup_entry", return_value=True):
yield
async def test_user_connection_works(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test we can finish a config flow."""
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data=None,
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 80},
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_HOST: "127.0.0.1",
CONF_PORT: 80,
CONF_PASSWORD: "",
CONF_NOISE_PSK: "",
CONF_DEVICE_NAME: "test",
}
assert result["options"] == {
CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS
}
assert result["title"] == "test"
assert result["result"].unique_id == "11:22:33:44:55:aa"
assert len(mock_client.connect.mock_calls) == 1
assert len(mock_client.device_info.mock_calls) == 1
assert len(mock_client.disconnect.mock_calls) == 1
assert mock_client.host == "127.0.0.1"
assert mock_client.port == 80
assert mock_client.password == ""
assert mock_client.noise_psk is None
async def test_user_connection_updates_host(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test setup up the same name updates the host."""
entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_HOST: "test.local", CONF_PORT: 6053, CONF_PASSWORD: ""},
unique_id="11:22:33:44:55:aa",
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data=None,
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 80},
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert entry.data[CONF_HOST] == "127.0.0.1"
async def test_user_sets_unique_id(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test that the user flow sets the unique id."""
service_info = zeroconf.ZeroconfServiceInfo(
host="192.168.43.183",
addresses=["192.168.43.183"],
hostname="test8266.local.",
name="mock_name",
port=6053,
properties={
"mac": "1122334455aa",
},
type="mock_type",
)
discovery_result = await hass.config_entries.flow.async_init(
"esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info
)
assert discovery_result["type"] == FlowResultType.FORM
assert discovery_result["step_id"] == "discovery_confirm"
discovery_result = await hass.config_entries.flow.async_configure(
discovery_result["flow_id"],
{},
)
assert discovery_result["type"] == FlowResultType.CREATE_ENTRY
assert discovery_result["data"] == {
CONF_HOST: "192.168.43.183",
CONF_PORT: 6053,
CONF_PASSWORD: "",
CONF_NOISE_PSK: "",
CONF_DEVICE_NAME: "test",
}
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data=None,
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_user_resolve_error(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test user step with IP resolve error."""
with patch(
"homeassistant.components.esphome.config_flow.APIConnectionError",
new_callable=lambda: ResolveAPIError,
) as exc:
mock_client.device_info.side_effect = exc
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "resolve_error"}
assert len(mock_client.connect.mock_calls) == 1
assert len(mock_client.device_info.mock_calls) == 1
assert len(mock_client.disconnect.mock_calls) == 1
async def test_user_causes_zeroconf_to_abort(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test that the user flow sets the unique id and aborts the zeroconf flow."""
service_info = zeroconf.ZeroconfServiceInfo(
host="192.168.43.183",
addresses=["192.168.43.183"],
hostname="test8266.local.",
name="mock_name",
port=6053,
properties={
"mac": "1122334455aa",
},
type="mock_type",
)
discovery_result = await hass.config_entries.flow.async_init(
"esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info
)
assert discovery_result["type"] == FlowResultType.FORM
assert discovery_result["step_id"] == "discovery_confirm"
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data=None,
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_HOST: "127.0.0.1",
CONF_PORT: 6053,
CONF_PASSWORD: "",
CONF_NOISE_PSK: "",
CONF_DEVICE_NAME: "test",
}
assert not hass.config_entries.flow.async_progress_by_handler(DOMAIN)
async def test_user_connection_error(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test user step with connection error."""
mock_client.device_info.side_effect = APIConnectionError
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "connection_error"}
assert len(mock_client.connect.mock_calls) == 1
assert len(mock_client.device_info.mock_calls) == 1
assert len(mock_client.disconnect.mock_calls) == 1
async def test_user_with_password(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test user step with password."""
mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test")
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "authenticate"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PASSWORD: "password1"}
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_HOST: "127.0.0.1",
CONF_PORT: 6053,
CONF_PASSWORD: "password1",
CONF_NOISE_PSK: "",
CONF_DEVICE_NAME: "test",
}
assert mock_client.password == "password1"
async def test_user_invalid_password(
hass: HomeAssistant, mock_client, mock_zeroconf: None
) -> None:
"""Test user step with invalid password."""
mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test")
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "authenticate"
mock_client.connect.side_effect = InvalidAuthAPIError
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PASSWORD: "invalid"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "authenticate"
assert result["errors"] == {"base": "invalid_auth"}
async def test_user_dashboard_has_wrong_key(
hass: HomeAssistant,
mock_client,
mock_dashboard,
mock_zeroconf: None,
mock_setup_entry: None,
) -> None:
"""Test user step with key from dashboard that is incorrect."""
mock_client.device_info.side_effect = [
RequiresEncryptionAPIError,
InvalidEncryptionKeyAPIError,
DeviceInfo(
uses_password=False,
name="test",
mac_address="11:22:33:44:55:AA",
),
]
with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key",
return_value=WRONG_NOISE_PSK,
):
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "encryption_key"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK}
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_HOST: "127.0.0.1",
CONF_PORT: 6053,
CONF_PASSWORD: "",
CONF_NOISE_PSK: VALID_NOISE_PSK,
CONF_DEVICE_NAME: "test",
}
assert mock_client.noise_psk == VALID_NOISE_PSK
async def test_user_discovers_name_and_gets_key_from_dashboard(
hass: HomeAssistant,
mock_client,
mock_dashboard,
mock_zeroconf: None,
mock_setup_entry: None,
) -> None:
"""Test user step can discover the name and get the key from the dashboard."""
mock_client.device_info.side_effect = [
RequiresEncryptionAPIError,
InvalidEncryptionKeyAPIError("Wrong key", "test"),
DeviceInfo(
uses_password=False,
name="test",
mac_address="11:22:33:44:55:AA",
),
]
mock_dashboard["configured"].append(
{
"name": "test",
"configuration": "test.yaml",
}
)
await dashboard.async_get_dashboard(hass).async_refresh()
with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key",
return_value=VALID_NOISE_PSK,
):
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_HOST: "127.0.0.1",
CONF_PORT: 6053,
CONF_PASSWORD: "",
CONF_NOISE_PSK: VALID_NOISE_PSK,
CONF_DEVICE_NAME: "test",
}
assert mock_client.noise_psk == VALID_NOISE_PSK
async def test_user_discovers_name_and_gets_key_from_dashboard_fails(
hass: HomeAssistant,
mock_client,
mock_dashboard,
mock_zeroconf: None,
mock_setup_entry: None,
) -> None:
"""Test user step can discover the name and get the key from the dashboard."""
mock_client.device_info.side_effect = [
RequiresEncryptionAPIError,
InvalidEncryptionKeyAPIError("Wrong key", "test"),
DeviceInfo(
uses_password=False,
name="test",
mac_address="11:22:33:44:55:aa",
),
]
mock_dashboard["configured"].append(
{
"name": "test",
"configuration": "test.yaml",
}
)
await dashboard.async_get_dashboard(hass).async_refresh()
with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key",
side_effect=aiohttp.ClientError,
):
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "encryption_key"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK}
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_HOST: "127.0.0.1",
CONF_PORT: 6053,
CONF_PASSWORD: "",
CONF_NOISE_PSK: VALID_NOISE_PSK,
CONF_DEVICE_NAME: "test",
}
assert mock_client.noise_psk == VALID_NOISE_PSK
async def test_user_discovers_name_and_dashboard_is_unavailable(
hass: HomeAssistant,
mock_client,
mock_dashboard,
mock_zeroconf: None,
mock_setup_entry: None,
) -> None:
"""Test user step can discover the name but the dashboard is unavailable."""
mock_client.device_info.side_effect = [
RequiresEncryptionAPIError,
InvalidEncryptionKeyAPIError("Wrong key", "test"),
DeviceInfo(
uses_password=False,
name="test",
mac_address="11:22:33:44:55:AA",
),
]
mock_dashboard["configured"].append(
{
"name": "test",
"configuration": "test.yaml",
}
)
with patch(
"esphome_dashboard_api.ESPHomeDashboardAPI.get_devices",
side_effect=asyncio.TimeoutError,
):
await dashboard.async_get_dashboard(hass).async_refresh()
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "encryption_key"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK}
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_HOST: "127.0.0.1",
CONF_PORT: 6053,
CONF_PASSWORD: "",
CONF_NOISE_PSK: VALID_NOISE_PSK,
CONF_DEVICE_NAME: "test",
}
assert mock_client.noise_psk == VALID_NOISE_PSK
async def test_login_connection_error(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test user step with connection error on login attempt."""
mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test")
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "authenticate"
mock_client.connect.side_effect = APIConnectionError
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PASSWORD: "valid"}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "authenticate"
assert result["errors"] == {"base": "connection_error"}
async def test_discovery_initiation(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test discovery importing works."""
service_info = zeroconf.ZeroconfServiceInfo(
host="192.168.43.183",
addresses=["192.168.43.183"],
hostname="test.local.",
name="mock_name",
port=6053,
properties={
"mac": "1122334455aa",
},
type="mock_type",
)
flow = await hass.config_entries.flow.async_init(
"esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info
)
result = await hass.config_entries.flow.async_configure(
flow["flow_id"], user_input={}
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == "test"
assert result["data"][CONF_HOST] == "192.168.43.183"
assert result["data"][CONF_PORT] == 6053
assert result["result"]
assert result["result"].unique_id == "11:22:33:44:55:aa"
async def test_discovery_no_mac(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test discovery aborted if old ESPHome without mac in zeroconf."""
service_info = zeroconf.ZeroconfServiceInfo(
host="192.168.43.183",
addresses=["192.168.43.183"],
hostname="test8266.local.",
name="mock_name",
port=6053,
properties={},
type="mock_type",
)
flow = await hass.config_entries.flow.async_init(
"esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info
)
assert flow["type"] == FlowResultType.ABORT
assert flow["reason"] == "mdns_missing_mac"
async def test_discovery_already_configured(
hass: HomeAssistant, mock_client: APIClient, mock_setup_entry: None
) -> None:
"""Test discovery aborts if already configured via hostname."""
entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_HOST: "test8266.local", CONF_PORT: 6053, CONF_PASSWORD: ""},
unique_id="11:22:33:44:55:aa",
)
entry.add_to_hass(hass)
service_info = zeroconf.ZeroconfServiceInfo(
host="192.168.43.183",
addresses=["192.168.43.183"],
hostname="test8266.local.",
name="mock_name",
port=6053,
properties={"mac": "1122334455aa"},
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"
async def test_discovery_duplicate_data(
hass: HomeAssistant, mock_client: APIClient, mock_setup_entry: None
) -> None:
"""Test discovery aborts if same mDNS packet arrives."""
service_info = zeroconf.ZeroconfServiceInfo(
host="192.168.43.183",
addresses=["192.168.43.183"],
hostname="test.local.",
name="mock_name",
port=6053,
properties={"address": "test.local", "mac": "1122334455aa"},
type="mock_type",
)
result = await hass.config_entries.flow.async_init(
"esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "discovery_confirm"
result = await hass.config_entries.flow.async_init(
"esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF}
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_in_progress"
async def test_discovery_updates_unique_id(
hass: HomeAssistant, mock_client: APIClient, mock_setup_entry: None
) -> None:
"""Test a duplicate discovery host aborts and updates existing entry."""
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)
service_info = zeroconf.ZeroconfServiceInfo(
host="192.168.43.183",
addresses=["192.168.43.183"],
hostname="test8266.local.",
name="mock_name",
port=6053,
properties={"address": "test8266.local", "mac": "1122334455aa"},
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 == "11:22:33:44:55:aa"
async def test_user_requires_psk(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test user step with requiring encryption key."""
mock_client.device_info.side_effect = RequiresEncryptionAPIError
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "encryption_key"
assert result["errors"] == {}
assert len(mock_client.connect.mock_calls) == 2
assert len(mock_client.device_info.mock_calls) == 2
assert len(mock_client.disconnect.mock_calls) == 2
async def test_encryption_key_valid_psk(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test encryption key step with valid key."""
mock_client.device_info.side_effect = RequiresEncryptionAPIError
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "encryption_key"
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}
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_HOST: "127.0.0.1",
CONF_PORT: 6053,
CONF_PASSWORD: "",
CONF_NOISE_PSK: VALID_NOISE_PSK,
CONF_DEVICE_NAME: "test",
}
assert mock_client.noise_psk == VALID_NOISE_PSK
async def test_encryption_key_invalid_psk(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test encryption key step with invalid key."""
mock_client.device_info.side_effect = RequiresEncryptionAPIError
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "encryption_key"
mock_client.device_info.side_effect = InvalidEncryptionKeyAPIError
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_NOISE_PSK: INVALID_NOISE_PSK}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "encryption_key"
assert result["errors"] == {"base": "invalid_psk"}
assert mock_client.noise_psk == INVALID_NOISE_PSK
async def test_reauth_initiation(
hass: HomeAssistant, mock_client, mock_zeroconf: None
) -> None:
"""Test reauth initiation shows form."""
entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053, CONF_PASSWORD: ""},
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
"esphome",
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": entry.entry_id,
"unique_id": entry.unique_id,
},
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
async def test_reauth_confirm_valid(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test reauth initiation with valid PSK."""
entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053, CONF_PASSWORD: ""},
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
"esphome",
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": entry.entry_id,
"unique_id": entry.unique_id,
},
)
mock_client.device_info.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}
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK
async def test_reauth_fixed_via_dashboard(
hass: HomeAssistant,
mock_client,
mock_zeroconf: None,
mock_dashboard,
mock_setup_entry: None,
) -> None:
"""Test reauth fixed automatically via dashboard."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "127.0.0.1",
CONF_PORT: 6053,
CONF_PASSWORD: "",
CONF_DEVICE_NAME: "test",
},
)
entry.add_to_hass(hass)
mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test")
mock_dashboard["configured"].append(
{
"name": "test",
"configuration": "test.yaml",
}
)
await dashboard.async_get_dashboard(hass).async_refresh()
with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key",
return_value=VALID_NOISE_PSK,
) as mock_get_encryption_key:
result = await hass.config_entries.flow.async_init(
"esphome",
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": entry.entry_id,
"unique_id": entry.unique_id,
},
)
assert result["type"] == FlowResultType.ABORT, result
assert result["reason"] == "reauth_successful"
assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK
assert len(mock_get_encryption_key.mock_calls) == 1
async def test_reauth_fixed_via_dashboard_add_encryption_remove_password(
hass: HomeAssistant,
mock_client,
mock_zeroconf: None,
mock_dashboard,
mock_config_entry,
mock_setup_entry: None,
) -> None:
"""Test reauth fixed automatically via dashboard with password removed."""
mock_client.device_info.side_effect = (
InvalidAuthAPIError,
DeviceInfo(uses_password=False, name="test"),
)
mock_dashboard["configured"].append(
{
"name": "test",
"configuration": "test.yaml",
}
)
await dashboard.async_get_dashboard(hass).async_refresh()
with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key",
return_value=VALID_NOISE_PSK,
) as mock_get_encryption_key:
result = await hass.config_entries.flow.async_init(
"esphome",
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": mock_config_entry.entry_id,
"unique_id": mock_config_entry.unique_id,
},
)
assert result["type"] == FlowResultType.ABORT, result
assert result["reason"] == "reauth_successful"
assert mock_config_entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK
assert mock_config_entry.data[CONF_PASSWORD] == ""
assert len(mock_get_encryption_key.mock_calls) == 1
async def test_reauth_fixed_via_remove_password(
hass: HomeAssistant,
mock_client,
mock_config_entry,
mock_dashboard,
mock_setup_entry: None,
) -> None:
"""Test reauth fixed automatically by seeing password removed."""
mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test")
result = await hass.config_entries.flow.async_init(
"esphome",
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": mock_config_entry.entry_id,
"unique_id": mock_config_entry.unique_id,
},
)
assert result["type"] == FlowResultType.ABORT, result
assert result["reason"] == "reauth_successful"
assert mock_config_entry.data[CONF_PASSWORD] == ""
async def test_reauth_fixed_via_dashboard_at_confirm(
hass: HomeAssistant,
mock_client,
mock_zeroconf: None,
mock_dashboard,
mock_setup_entry: None,
) -> None:
"""Test reauth fixed automatically via dashboard at confirm step."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "127.0.0.1",
CONF_PORT: 6053,
CONF_PASSWORD: "",
CONF_DEVICE_NAME: "test",
},
)
entry.add_to_hass(hass)
mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test")
result = await hass.config_entries.flow.async_init(
"esphome",
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": entry.entry_id,
"unique_id": entry.unique_id,
},
)
assert result["type"] == FlowResultType.FORM, result
assert result["step_id"] == "reauth_confirm"
mock_dashboard["configured"].append(
{
"name": "test",
"configuration": "test.yaml",
}
)
await dashboard.async_get_dashboard(hass).async_refresh()
with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key",
return_value=VALID_NOISE_PSK,
) as mock_get_encryption_key:
# We just fetch the form
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == FlowResultType.ABORT, result
assert result["reason"] == "reauth_successful"
assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK
assert len(mock_get_encryption_key.mock_calls) == 1
async def test_reauth_confirm_invalid(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test reauth initiation with invalid PSK."""
entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053, CONF_PASSWORD: ""},
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
"esphome",
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": entry.entry_id,
"unique_id": entry.unique_id,
},
)
mock_client.device_info.side_effect = InvalidEncryptionKeyAPIError
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_NOISE_PSK: INVALID_NOISE_PSK}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
assert result["errors"]
assert result["errors"]["base"] == "invalid_psk"
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}
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK
async def test_reauth_confirm_invalid_with_unique_id(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test reauth initiation with invalid PSK."""
entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053, CONF_PASSWORD: ""},
unique_id="test",
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
"esphome",
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": entry.entry_id,
"unique_id": entry.unique_id,
},
)
mock_client.device_info.side_effect = InvalidEncryptionKeyAPIError
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_NOISE_PSK: INVALID_NOISE_PSK}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
assert result["errors"]
assert result["errors"]["base"] == "invalid_psk"
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}
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK
async def test_discovery_dhcp_updates_host(
hass: HomeAssistant, mock_client: APIClient, mock_setup_entry: None
) -> None:
"""Test dhcp discovery updates host and aborts."""
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)
service_info = dhcp.DhcpServiceInfo(
ip="192.168.43.184",
hostname="test8266",
macaddress="1122334455aa",
)
result = await hass.config_entries.flow.async_init(
"esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert entry.data[CONF_HOST] == "192.168.43.184"
async def test_discovery_dhcp_no_changes(
hass: HomeAssistant, mock_client: APIClient, mock_setup_entry: None
) -> None:
"""Test dhcp discovery updates host and aborts."""
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 = dhcp.DhcpServiceInfo(
ip="192.168.43.183",
hostname="test8266",
macaddress="00:00:00:00:00:00",
)
result = await hass.config_entries.flow.async_init(
"esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert entry.data[CONF_HOST] == "192.168.43.183"
async def test_discovery_hassio(hass: HomeAssistant, mock_dashboard) -> None:
"""Test dashboard discovery."""
result = await hass.config_entries.flow.async_init(
"esphome",
data=HassioServiceInfo(
config={
"host": "mock-esphome",
"port": 6052,
},
name="ESPHome",
slug="mock-slug",
uuid="1234",
),
context={"source": config_entries.SOURCE_HASSIO},
)
assert result
assert result["type"] == data_entry_flow.FlowResultType.ABORT
assert result["reason"] == "service_received"
dash = dashboard.async_get_dashboard(hass)
assert dash is not None
assert dash.addon_slug == "mock-slug"
async def test_zeroconf_encryption_key_via_dashboard(
hass: HomeAssistant,
mock_client,
mock_zeroconf: None,
mock_dashboard,
mock_setup_entry: None,
) -> None:
"""Test encryption key retrieved from dashboard."""
service_info = zeroconf.ZeroconfServiceInfo(
host="192.168.43.183",
addresses=["192.168.43.183"],
hostname="test8266.local.",
name="mock_name",
port=6053,
properties={
"mac": "1122334455aa",
},
type="mock_type",
)
flow = await hass.config_entries.flow.async_init(
"esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info
)
assert flow["type"] == FlowResultType.FORM
assert flow["step_id"] == "discovery_confirm"
mock_dashboard["configured"].append(
{
"name": "test8266",
"configuration": "test8266.yaml",
}
)
await dashboard.async_get_dashboard(hass).async_refresh()
mock_client.device_info.side_effect = [
RequiresEncryptionAPIError,
DeviceInfo(
uses_password=False,
name="test8266",
mac_address="11:22:33:44:55:AA",
),
]
with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key",
return_value=VALID_NOISE_PSK,
) as mock_get_encryption_key:
result = await hass.config_entries.flow.async_configure(
flow["flow_id"], user_input={}
)
assert len(mock_get_encryption_key.mock_calls) == 1
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == "test8266"
assert result["data"][CONF_HOST] == "192.168.43.183"
assert result["data"][CONF_PORT] == 6053
assert result["data"][CONF_NOISE_PSK] == VALID_NOISE_PSK
assert result["result"]
assert result["result"].unique_id == "11:22:33:44:55:aa"
assert mock_client.noise_psk == VALID_NOISE_PSK
async def test_zeroconf_no_encryption_key_via_dashboard(
hass: HomeAssistant,
mock_client,
mock_zeroconf: None,
mock_dashboard,
mock_setup_entry: None,
) -> None:
"""Test encryption key not retrieved from dashboard."""
service_info = zeroconf.ZeroconfServiceInfo(
host="192.168.43.183",
addresses=["192.168.43.183"],
hostname="test8266.local.",
name="mock_name",
port=6053,
properties={
"mac": "1122334455aa",
},
type="mock_type",
)
flow = await hass.config_entries.flow.async_init(
"esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info
)
assert flow["type"] == FlowResultType.FORM
assert flow["step_id"] == "discovery_confirm"
await dashboard.async_get_dashboard(hass).async_refresh()
mock_client.device_info.side_effect = RequiresEncryptionAPIError
result = await hass.config_entries.flow.async_configure(
flow["flow_id"], user_input={}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "encryption_key"
@pytest.mark.parametrize("option_value", [True, False])
async def test_option_flow(
hass: HomeAssistant,
option_value: bool,
mock_client: APIClient,
mock_generic_device_entry,
) -> None:
"""Test config flow options."""
entry = await mock_generic_device_entry(
mock_client=mock_client,
entity_info=[],
user_service=[],
states=[],
)
result = await hass.config_entries.options.async_init(entry.entry_id)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "init"
assert result["data_schema"]({}) == {
CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS
}
with patch(
"homeassistant.components.esphome.async_setup_entry", return_value=True
) as mock_reload:
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_ALLOW_SERVICE_CALLS: option_value,
},
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["data"] == {CONF_ALLOW_SERVICE_CALLS: option_value}
assert len(mock_reload.mock_calls) == int(option_value)