diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 1daa4710f30..55686464637 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -12,6 +12,7 @@ from aioshelly.exceptions import ( CustomPortNotSupported, DeviceConnectionError, InvalidAuthError, + MacAddressMismatchError, ) from aioshelly.rpc_device import RpcDevice import voluptuous as vol @@ -176,6 +177,8 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): ) except DeviceConnectionError: errors["base"] = "cannot_connect" + except MacAddressMismatchError: + errors["base"] = "mac_address_mismatch" except CustomPortNotSupported: errors["base"] = "custom_port_not_supported" except Exception: # noqa: BLE001 @@ -215,6 +218,8 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_auth" except DeviceConnectionError: errors["base"] = "cannot_connect" + except MacAddressMismatchError: + errors["base"] = "mac_address_mismatch" except Exception: # noqa: BLE001 LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -378,6 +383,8 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): await validate_input(self.hass, host, port, info, user_input) except (DeviceConnectionError, InvalidAuthError): return self.async_abort(reason="reauth_unsuccessful") + except MacAddressMismatchError: + return self.async_abort(reason="mac_address_mismatch") return self.async_update_reload_and_abort( reauth_entry, data_updates=user_input diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index a66fbb20f48..f20b283cacf 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -11,7 +11,12 @@ from typing import Any, cast from aioshelly.ble import async_ensure_ble_enabled, async_stop_scanner from aioshelly.block_device import BlockDevice, BlockUpdateType from aioshelly.const import MODEL_NAMES, MODEL_VALVE -from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError +from aioshelly.exceptions import ( + DeviceConnectionError, + InvalidAuthError, + MacAddressMismatchError, + RpcCallError, +) from aioshelly.rpc_device import RpcDevice, RpcUpdateType from propcache import cached_property @@ -173,7 +178,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( try: await self.device.initialize() update_device_fw_info(self.hass, self.device, self.entry) - except DeviceConnectionError as err: + except (DeviceConnectionError, MacAddressMismatchError) as err: LOGGER.debug( "Error connecting to Shelly device %s, error: %r", self.name, err ) @@ -450,7 +455,7 @@ class ShellyRestCoordinator(ShellyCoordinatorBase[BlockDevice]): if self.device.status["uptime"] > 2 * REST_SENSORS_UPDATE_INTERVAL: return await self.device.update_shelly() - except DeviceConnectionError as err: + except (DeviceConnectionError, MacAddressMismatchError) as err: raise UpdateFailed(f"Error fetching data: {err!r}") from err except InvalidAuthError: await self.async_shutdown_device_and_start_reauth() diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 342a7418b2a..eb869b54e4c 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -45,7 +45,8 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]", "firmware_not_fully_provisioned": "Device not fully provisioned. Please contact Shelly support", - "custom_port_not_supported": "Gen1 device does not support custom port." + "custom_port_not_supported": "Gen1 device does not support custom port.", + "mac_address_mismatch": "The MAC address of the device does not match the one in the configuration, please reboot the device and try again." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", @@ -53,7 +54,8 @@ "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again.", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "another_device": "Re-configuration was unsuccessful, the IP address/hostname of another Shelly device was used.", - "ipv6_not_supported": "IPv6 is not supported." + "ipv6_not_supported": "IPv6 is not supported.", + "mac_address_mismatch": "[%key:component::shelly::config::error::mac_address_mismatch%]" } }, "device_automation": { diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 93b3a46910c..d9945706182 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -16,7 +16,7 @@ import pytest from homeassistant import config_entries from homeassistant.components import zeroconf -from homeassistant.components.shelly import config_flow +from homeassistant.components.shelly import MacAddressMismatchError, config_flow from homeassistant.components.shelly.const import ( CONF_BLE_SCANNER_MODE, DOMAIN, @@ -331,6 +331,7 @@ async def test_form_missing_model_key_zeroconf( ("exc", "base_error"), [ (DeviceConnectionError, "cannot_connect"), + (MacAddressMismatchError, "mac_address_mismatch"), (ValueError, "unknown"), ], ) @@ -436,6 +437,7 @@ async def test_user_setup_ignored_device( [ (InvalidAuthError, "invalid_auth"), (DeviceConnectionError, "cannot_connect"), + (MacAddressMismatchError, "mac_address_mismatch"), (ValueError, "unknown"), ], ) @@ -473,6 +475,7 @@ async def test_form_auth_errors_test_connection_gen1( [ (DeviceConnectionError, "cannot_connect"), (InvalidAuthError, "invalid_auth"), + (MacAddressMismatchError, "mac_address_mismatch"), (ValueError, "unknown"), ], ) @@ -844,8 +847,19 @@ async def test_reauth_successful( (3, {"password": "test2 password"}), ], ) +@pytest.mark.parametrize( + ("exc", "abort_reason"), + [ + (DeviceConnectionError, "reauth_unsuccessful"), + (MacAddressMismatchError, "mac_address_mismatch"), + ], +) async def test_reauth_unsuccessful( - hass: HomeAssistant, gen: int, user_input: dict[str, str] + hass: HomeAssistant, + gen: int, + user_input: dict[str, str], + exc: Exception, + abort_reason: str, ) -> None: """Test reauthentication flow failed.""" entry = MockConfigEntry( @@ -862,13 +876,9 @@ async def test_reauth_unsuccessful( return_value={"mac": "test-mac", "type": MODEL_1, "auth": True, "gen": gen}, ), patch( - "aioshelly.block_device.BlockDevice.create", - new=AsyncMock(side_effect=InvalidAuthError), - ), - patch( - "aioshelly.rpc_device.RpcDevice.create", - new=AsyncMock(side_effect=InvalidAuthError), + "aioshelly.block_device.BlockDevice.create", new=AsyncMock(side_effect=exc) ), + patch("aioshelly.rpc_device.RpcDevice.create", new=AsyncMock(side_effect=exc)), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -876,7 +886,7 @@ async def test_reauth_unsuccessful( ) assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "reauth_unsuccessful" + assert result["reason"] == abort_reason async def test_reauth_get_info_error(hass: HomeAssistant) -> None: diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 47c338e3fad..090c5e7207f 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -10,6 +10,7 @@ import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.shelly import MacAddressMismatchError from homeassistant.components.shelly.const import ( ATTR_CHANNEL, ATTR_CLICK_TYPE, @@ -254,11 +255,13 @@ async def test_block_polling_connection_error( assert get_entity_state(hass, "switch.test_name_channel_1") == STATE_UNAVAILABLE +@pytest.mark.parametrize("exc", [DeviceConnectionError, MacAddressMismatchError]) async def test_block_rest_update_connection_error( hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch, + exc: Exception, ) -> None: """Test block REST update connection error.""" entity_id = register_entity(hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud") @@ -269,11 +272,7 @@ async def test_block_rest_update_connection_error( await mock_rest_update(hass, freezer) assert get_entity_state(hass, entity_id) == STATE_ON - monkeypatch.setattr( - mock_block_device, - "update_shelly", - AsyncMock(side_effect=DeviceConnectionError), - ) + monkeypatch.setattr(mock_block_device, "update_shelly", AsyncMock(side_effect=exc)) await mock_rest_update(hass, freezer) assert get_entity_state(hass, entity_id) == STATE_UNAVAILABLE @@ -702,11 +701,13 @@ async def test_rpc_polling_auth_error( assert flow["context"].get("entry_id") == entry.entry_id +@pytest.mark.parametrize("exc", [DeviceConnectionError, MacAddressMismatchError]) async def test_rpc_reconnect_error( hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch, + exc: Exception, ) -> None: """Test RPC reconnect error.""" await init_integration(hass, 2) @@ -714,13 +715,7 @@ async def test_rpc_reconnect_error( assert get_entity_state(hass, "switch.test_switch_0") == STATE_ON monkeypatch.setattr(mock_rpc_device, "connected", False) - monkeypatch.setattr( - mock_rpc_device, - "initialize", - AsyncMock( - side_effect=DeviceConnectionError, - ), - ) + monkeypatch.setattr(mock_rpc_device, "initialize", AsyncMock(side_effect=exc)) # Move time to generate reconnect freezer.tick(timedelta(seconds=RPC_RECONNECT_INTERVAL))