diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py index c648ec903cd4..321fb2812e51 100644 --- a/homeassistant/components/sma/config_flow.py +++ b/homeassistant/components/sma/config_flow.py @@ -3,6 +3,7 @@ from collections.abc import Mapping import dataclasses import logging +import re from typing import Any, override import probatio @@ -38,6 +39,9 @@ from .const import CONF_GROUP, DOMAIN, GROUPS _LOGGER = logging.getLogger(__name__) +# Example hostnames: sma3015638141, sma3015598606-2856 +HOSTNAME_SERIAL = re.compile(r"^sma-?(\d+)(?:-|$)", re.IGNORECASE) + STEP_USER_DATA_SCHEMA = probatio.Schema( { @@ -291,15 +295,9 @@ class SmaConfigFlow(ConfigFlow, domain=DOMAIN): entry, data_updates={CONF_MAC: self._data[CONF_MAC]} ) - # Finally, check if the hostname - # (which represents the SMA serial number) is unique - serial_number = discovery_info.hostname.lower() - # Example hostname: sma12345678-01 - # Remove 'sma' prefix and strip everything after the dash (including the dash) - if serial_number.startswith("sma"): - serial_number = serial_number.removeprefix("sma") - serial_number = serial_number.split("-", 1)[0] - await self.async_set_unique_id(serial_number) + if not (match := HOSTNAME_SERIAL.match(discovery_info.hostname)): + return self.async_abort(reason="not_supported") + await self.async_set_unique_id(match.group(1)) self._abort_if_unique_id_configured() return await self.async_step_discovery_confirm() diff --git a/homeassistant/components/sma/strings.json b/homeassistant/components/sma/strings.json index 482e9375a072..4e16f3b635a3 100644 --- a/homeassistant/components/sma/strings.json +++ b/homeassistant/components/sma/strings.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "not_supported": "This SMA device is not supported", "unique_id_mismatch": "You selected a different SMA device than the one this config entry was configured with, this is not allowed." }, "error": { diff --git a/tests/components/sma/test_config_flow.py b/tests/components/sma/test_config_flow.py index 9ccb90abb610..2d362da92843 100644 --- a/tests/components/sma/test_config_flow.py +++ b/tests/components/sma/test_config_flow.py @@ -222,6 +222,56 @@ async def test_dhcp_already_configured_duplicate( ) +@pytest.mark.parametrize( + ("hostname", "unique_id"), + [ + pytest.param("SMA123456789", "123456789", id="serial"), + pytest.param("SMA-123456789", "123456789", id="dash_serial"), + pytest.param("sma123456789-2856", "123456789", id="serial_suffix"), + ], +) +@pytest.mark.usefixtures("mock_setup_entry") +async def test_dhcp_hostname_serial( + hass: HomeAssistant, hostname: str, unique_id: str +) -> None: + """Test the serial number is read from the DHCP hostname.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + ip="1.1.1.2", hostname=hostname, macaddress="0015bb00abcd" + ), + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "discovery_confirm" + flow = hass.config_entries.flow.async_get(result["flow_id"]) + assert flow["context"]["unique_id"] == unique_id + + +@pytest.mark.parametrize( + "hostname", + [ + pytest.param("SMA", id="prefix_only"), + pytest.param("SMA-EVCharger", id="name"), + pytest.param("smaevc22", id="no_serial"), + pytest.param("sma12abc", id="partial_serial"), + ], +) +async def test_dhcp_not_supported(hass: HomeAssistant, hostname: str) -> None: + """Test DHCP discovery aborts for hostnames without a serial number.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + ip="1.1.1.2", hostname=hostname, macaddress="0015bb00abcd" + ), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "not_supported" + + @pytest.mark.parametrize( ("exception", "error"), [