Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion homeassistant/components/wled/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@

from homeassistant.components import onboarding
from homeassistant.config_entries import (
SOURCE_RECONFIGURE,
ConfigFlow,
ConfigFlowResult,
OptionsFlowWithReload,
)
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
Expand Down Expand Up @@ -52,6 +54,19 @@ async def async_step_user(
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]}
)
Expand All @@ -61,13 +76,26 @@ async def async_step_user(
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:
Expand Down
12 changes: 12 additions & 0 deletions homeassistant/components/wled/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
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 (
Expand Down Expand Up @@ -120,6 +122,16 @@ async def _async_update_data(self) -> 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
Expand Down
7 changes: 6 additions & 1 deletion homeassistant/components/wled/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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%]"
Expand Down Expand Up @@ -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": {
Expand Down
92 changes: 92 additions & 0 deletions tests/components/wled/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In circumstances it can fail at reconfigure right? Since it uses the user_step, but that has a try-except that can fail. Normally it's good practice to write a test that initially fails and then demonstrates the flow can recover and successfully reconfigure. That would be a good additional test. :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a new test case: test_full_reconfigure_flow_connection_error_and_success . I think this is what you need.

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."""
Expand Down
23 changes: 23 additions & 0 deletions tests/components/wled/test_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
)

from homeassistant.components.wled.const import SCAN_INTERVAL
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
STATE_OFF,
Expand Down Expand Up @@ -195,3 +196,25 @@ async def connect(callback: Callable[[WLEDDevice], None]):
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
)
29 changes: 28 additions & 1 deletion tests/components/wled/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -21,7 +23,7 @@
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")
Expand Down Expand Up @@ -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"
Loading