mirror of
https://github.com/home-assistant/core.git
synced 2025-12-26 22:18:40 +00:00
Fix MAC address mix-ups between WLED devices (#155491)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
committed by
Franck Nijhof
parent
f8d5a8bc58
commit
57835efc9d
@@ -9,6 +9,7 @@ from wled import WLED, Device, WLEDConnectionError
|
||||
|
||||
from homeassistant.components import onboarding
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_RECONFIGURE,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlowWithReload,
|
||||
@@ -16,6 +17,7 @@ from homeassistant.config_entries import (
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from .const import CONF_KEEP_MAIN_LIGHT, DEFAULT_KEEP_MAIN_LIGHT, DOMAIN
|
||||
@@ -52,6 +54,19 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
await self.async_set_unique_id(
|
||||
device.info.mac_address, raise_on_progress=False
|
||||
)
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
entry = self._get_reconfigure_entry()
|
||||
self._abort_if_unique_id_mismatch(
|
||||
reason="unique_id_mismatch",
|
||||
description_placeholders={
|
||||
"expected_mac": format_mac(entry.unique_id).upper(),
|
||||
"actual_mac": format_mac(self.unique_id).upper(),
|
||||
},
|
||||
)
|
||||
return self.async_update_reload_and_abort(
|
||||
entry,
|
||||
data_updates=user_input,
|
||||
)
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_HOST: user_input[CONF_HOST]}
|
||||
)
|
||||
@@ -61,13 +76,26 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
},
|
||||
)
|
||||
data_schema = vol.Schema({vol.Required(CONF_HOST): str})
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
entry = self._get_reconfigure_entry()
|
||||
data_schema = self.add_suggested_values_to_schema(
|
||||
data_schema,
|
||||
entry.data,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||
data_schema=data_schema,
|
||||
errors=errors or {},
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfigure flow for WLED entry."""
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -14,7 +14,9 @@ from wled import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import (
|
||||
@@ -120,6 +122,16 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]):
|
||||
translation_placeholders={"error": str(error)},
|
||||
) from error
|
||||
|
||||
if device.info.mac_address != self.config_entry.unique_id:
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="mac_address_mismatch",
|
||||
translation_placeholders={
|
||||
"expected_mac": format_mac(self.config_entry.unique_id).upper(),
|
||||
"actual_mac": format_mac(device.info.mac_address).upper(),
|
||||
},
|
||||
)
|
||||
|
||||
# If the device supports a WebSocket, try activating it.
|
||||
if (
|
||||
device.info.websocket is not None
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"unique_id_mismatch": "MAC address does not match the configured device. Expected to connect to device with MAC: `{expected_mac}`, but connected to device with MAC: `{actual_mac}`. \n\nPlease ensure you reconfigure against the same device."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
@@ -133,6 +135,9 @@
|
||||
},
|
||||
"invalid_response_wled_error": {
|
||||
"message": "Invalid response from WLED API: {error}"
|
||||
},
|
||||
"mac_address_mismatch": {
|
||||
"message": "MAC address does not match the configured device. Expected to connect to device with MAC: {expected_mac}, but connected to device with MAC: {actual_mac}."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
||||
@@ -37,6 +37,98 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None:
|
||||
assert result["result"].unique_id == "aabbccddeeff"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry", "mock_wled")
|
||||
async def test_full_reconfigure_flow_success(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_wled: MagicMock
|
||||
) -> None:
|
||||
"""Test the full reconfigure flow from start to finish."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
|
||||
# Assert show form initially
|
||||
assert result.get("step_id") == "user"
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_HOST: "10.10.0.10"}
|
||||
)
|
||||
|
||||
# Assert show text message and close flow
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
assert result.get("reason") == "reconfigure_successful"
|
||||
|
||||
# Assert config entry has been updated.
|
||||
assert mock_config_entry.data[CONF_HOST] == "10.10.0.10"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry", "mock_wled")
|
||||
async def test_full_reconfigure_flow_unique_id_mismatch(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_wled: MagicMock
|
||||
) -> None:
|
||||
"""Test reconfiguration failure when the unique ID changes."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
# Change mac address
|
||||
device = mock_wled.update.return_value
|
||||
device.info.mac_address = "invalid"
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
|
||||
# Assert show form initially
|
||||
assert result.get("step_id") == "user"
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
|
||||
# Input new host value
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_HOST: "10.10.0.10"}
|
||||
)
|
||||
|
||||
# Assert Show text message and close flow
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
assert result.get("reason") == "unique_id_mismatch"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry", "mock_wled")
|
||||
async def test_full_reconfigure_flow_connection_error_and_success(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_wled: MagicMock
|
||||
) -> None:
|
||||
"""Test we show user form on WLED connection error and allows user to change host."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
# Mock connection error
|
||||
mock_wled.update.side_effect = WLEDConnectionError
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
|
||||
# Assert show form initially
|
||||
assert result.get("step_id") == "user"
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
|
||||
# Input new host value
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_HOST: "10.10.0.10"}
|
||||
)
|
||||
|
||||
# Assert form with errors
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
assert result.get("step_id") == "user"
|
||||
assert result.get("errors") == {"base": "cannot_connect"}
|
||||
|
||||
# Remove mock for connection error
|
||||
mock_wled.update.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_HOST: "10.10.0.10"}
|
||||
)
|
||||
|
||||
# Assert show text message and close flow
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
assert result.get("reason") == "reconfigure_successful"
|
||||
|
||||
# Assert config entry has been updated.
|
||||
assert mock_config_entry.data[CONF_HOST] == "10.10.0.10"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry", "mock_wled")
|
||||
async def test_full_zeroconf_flow_implementation(hass: HomeAssistant) -> None:
|
||||
"""Test the full manual user flow from start to finish."""
|
||||
|
||||
@@ -14,6 +14,7 @@ from wled import (
|
||||
)
|
||||
|
||||
from homeassistant.components.wled.const import SCAN_INTERVAL
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
STATE_OFF,
|
||||
@@ -195,3 +196,25 @@ async def test_websocket_disconnect_on_home_assistant_stop(
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
assert mock_wled.disconnect.call_count == 2
|
||||
|
||||
|
||||
async def test_fail_when_other_device(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_wled: MagicMock,
|
||||
) -> None:
|
||||
"""Ensure entry fails to setup when mac mismatch."""
|
||||
device = mock_wled.update.return_value
|
||||
device.info.mac_address = "invalid"
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state == ConfigEntryState.SETUP_ERROR
|
||||
assert mock_config_entry.reason
|
||||
assert (
|
||||
"MAC address does not match the configured device." in mock_config_entry.reason
|
||||
)
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.components.wled.const import SCAN_INTERVAL
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ICON,
|
||||
@@ -21,7 +23,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_wled")
|
||||
@@ -189,3 +191,28 @@ async def test_no_current_measurement(
|
||||
|
||||
assert hass.states.get("sensor.wled_rgb_light_max_current") is None
|
||||
assert hass.states.get("sensor.wled_rgb_light_estimated_current") is None
|
||||
|
||||
|
||||
async def test_fail_when_other_device(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_wled: MagicMock,
|
||||
) -> None:
|
||||
"""Ensure no data are updated when mac address mismatch."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (state := hass.states.get("sensor.wled_rgb_light_ip"))
|
||||
assert state.state == "127.0.0.1"
|
||||
|
||||
device = mock_wled.update.return_value
|
||||
device.info.mac_address = "invalid"
|
||||
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (state := hass.states.get("sensor.wled_rgb_light_ip"))
|
||||
assert state.state == "unavailable"
|
||||
|
||||
Reference in New Issue
Block a user