diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 335a0939b0513..28bca8f634a1e 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -5,6 +5,7 @@ from zoneinfo import ZoneInfo import voluptuous as vol +from yarl import URL from zha.application.const import BAUD_RATES, RadioType from zha.application.gateway import Gateway from zha.application.helpers import ZHAData @@ -32,6 +33,7 @@ from homeassistant.helpers.typing import ConfigType from . import homeassistant_hardware, repairs, websocket_api +from .config_flow import ZhaConfigFlowHandler from .const import ( CONF_BAUDRATE, CONF_CUSTOM_QUIRKS_PATH, @@ -43,6 +45,7 @@ CONF_ZIGPY, DATA_ZHA, DOMAIN, + LEGACY_ZEROCONF_PORT, ) from .helpers import ( SIGNAL_ADD_ENTITIES, @@ -301,7 +304,18 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate old entry.""" - _LOGGER.debug("Migrating from version %s", config_entry.version) + _LOGGER.debug( + "Migrating from version %s.%s", + config_entry.version, + config_entry.minor_version, + ) + + if (config_entry.version, config_entry.minor_version) > ( + ZhaConfigFlowHandler.VERSION, + ZhaConfigFlowHandler.MINOR_VERSION, + ): + # This means the user has downgraded from a future version + return False if config_entry.version == 1: data = { @@ -361,5 +375,24 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> version=5, ) - _LOGGER.info("Migration to version %s successful", config_entry.version) + if config_entry.version == 5 and config_entry.minor_version < 2: + data = {**config_entry.data, CONF_DEVICE: {**config_entry.data[CONF_DEVICE]}} + device_path = data[CONF_DEVICE][CONF_DEVICE_PATH] + + if device_path.startswith(("socket://", "tcp://")): + url = URL(device_path) + if url.explicit_port is None: + data[CONF_DEVICE][CONF_DEVICE_PATH] = str( + url.with_port(LEGACY_ZEROCONF_PORT) + ) + + hass.config_entries.async_update_entry( + config_entry, data=data, version=5, minor_version=2 + ) + + _LOGGER.info( + "Migration to version %s.%s successful", + config_entry.version, + config_entry.minor_version, + ) return True diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 2a7aa4fa46683..00ae0f86df458 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -45,7 +45,13 @@ from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.util import dt as dt_util -from .const import CONF_BAUDRATE, CONF_FLOW_CONTROL, CONF_RADIO_TYPE, DOMAIN +from .const import ( + CONF_BAUDRATE, + CONF_FLOW_CONTROL, + CONF_RADIO_TYPE, + DOMAIN, + LEGACY_ZEROCONF_PORT, +) from .helpers import get_config_entry_unique_id, get_zha_gateway from .radio_manager import ( DEVICE_SCHEMA, @@ -87,7 +93,6 @@ REPAIR_MY_URL = "https://my.home-assistant.io/redirect/repairs/" -LEGACY_ZEROCONF_PORT = 6638 LEGACY_ZEROCONF_ESPHOME_API_PORT = 6053 ZEROCONF_SERVICE_TYPE = "_zigbee-coordinator._tcp.local." @@ -758,6 +763,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 5 + MINOR_VERSION = 2 async def _set_unique_id_and_update_ignored_flow( self, unique_id: str, device_path: str diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 0428e6f16609f..b50a67898e8ee 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -64,6 +64,8 @@ DOMAIN = "zha" +LEGACY_ZEROCONF_PORT = 6638 + GROUP_ID = "group_id" diff --git a/homeassistant/components/zha/helpers.py b/homeassistant/components/zha/helpers.py index bc66ce489acb2..90c4ee1a31687 100644 --- a/homeassistant/components/zha/helpers.py +++ b/homeassistant/components/zha/helpers.py @@ -1263,19 +1263,6 @@ def async_add_entities( entities.clear() -def _clean_serial_port_path(path: str) -> str: - """Clean the serial port path, applying corrections where necessary.""" - - if path.startswith("socket://"): - path = path.strip() - - # Removes extraneous brackets from IP addresses (they don't parse in CPython 3.11.4) - if re.match(r"^socket://\[\d+\.\d+\.\d+\.\d+\]:\d+$", path): - path = path.replace("[", "").replace("]", "") - - return path - - CONF_ZHA_OPTIONS_SCHEMA = vol.Schema( { vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION, default=0): vol.All( @@ -1314,18 +1301,6 @@ def create_zha_config(hass: HomeAssistant, ha_zha_data: HAZHAData) -> ZHAData: assert ha_zha_data.config_entry is not None assert ha_zha_data.yaml_config is not None - # Remove brackets around IP addresses, this no longer works in CPython 3.11.4 - # This will be removed in 2023.11.0 - path = ha_zha_data.config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] - cleaned_path = _clean_serial_port_path(path) - - if path != cleaned_path: - _LOGGER.debug("Cleaned serial port path %r -> %r", path, cleaned_path) - ha_zha_data.config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] = cleaned_path - hass.config_entries.async_update_entry( - ha_zha_data.config_entry, data=ha_zha_data.config_entry.data - ) - # deep copy the yaml config to avoid modifying the original and to safely # pass it to the ZHA library app_config = copy.deepcopy(ha_zha_data.yaml_config.get(CONF_ZIGPY, {})) diff --git a/tests/components/zha/snapshots/test_diagnostics.ambr b/tests/components/zha/snapshots/test_diagnostics.ambr index 89ae58d2bc54a..6ebee4ee365ae 100644 --- a/tests/components/zha/snapshots/test_diagnostics.ambr +++ b/tests/components/zha/snapshots/test_diagnostics.ambr @@ -100,7 +100,7 @@ 'discovery_keys': dict({ }), 'domain': 'zha', - 'minor_version': 1, + 'minor_version': 2, 'options': dict({ 'custom_configuration': dict({ 'zha_alarm_options': dict({ diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index 50a5db9710032..9701f645c2c1a 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -4,7 +4,7 @@ from collections.abc import Callable import logging import typing -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, patch import zoneinfo import pytest @@ -141,52 +141,44 @@ async def test_config_depreciation(hass: HomeAssistant, zha_config) -> None: @pytest.mark.parametrize( - ("path", "cleaned_path"), + ("old_path", "new_path"), [ - # No corrections - ("/dev/path1", "/dev/path1"), - ("/dev/path1[asd]", "/dev/path1[asd]"), - ("/dev/path1 ", "/dev/path1 "), + ("/dev/ttyUSB0", "/dev/ttyUSB0"), ("socket://1.2.3.4:5678", "socket://1.2.3.4:5678"), - # Brackets around URI - ("socket://[1.2.3.4]:5678", "socket://1.2.3.4:5678"), - # Spaces - ("socket://dev/path1 ", "socket://dev/path1"), - # Both - ("socket://[1.2.3.4]:5678 ", "socket://1.2.3.4:5678"), + ("socket://1.2.3.4", "socket://1.2.3.4:6638"), + ("tcp://hostname", "tcp://hostname:6638"), + ("tcp://hostname:1234", "tcp://hostname:1234"), + ("socket://[::1]", "socket://[::1]:6638"), ], ) -@patch( - "homeassistant.components.zha.websocket_api.async_load_api", Mock(return_value=True) -) -async def test_setup_with_v3_cleaning_uri( +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +async def test_migration_v5_explicit_socket_port( + old_path: str, + new_path: str, hass: HomeAssistant, - path: str, - cleaned_path: str, - mock_zigpy_connect: ControllerApplication, + config_entry: MockConfigEntry, ) -> None: - """Test migration of config entry from v3, applying corrections to the port path.""" - config_entry_v4 = MockConfigEntry( - domain=DOMAIN, + """Test that socket:// and tcp:// paths get an explicit default port.""" + config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + config_entry, data={ - CONF_RADIO_TYPE: DATA_RADIO_TYPE, + **config_entry.data, CONF_DEVICE: { - CONF_DEVICE_PATH: path, - CONF_BAUDRATE: 115200, - CONF_FLOW_CONTROL: None, + **config_entry.data[CONF_DEVICE], + CONF_DEVICE_PATH: old_path, }, }, version=5, + minor_version=1, ) - config_entry_v4.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry_v4.entry_id) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - await hass.config_entries.async_unload(config_entry_v4.entry_id) - assert config_entry_v4.data[CONF_RADIO_TYPE] == DATA_RADIO_TYPE - assert config_entry_v4.data[CONF_DEVICE][CONF_DEVICE_PATH] == cleaned_path - assert config_entry_v4.version == 5 + assert config_entry.version == 5 + assert config_entry.minor_version == 2 + assert config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] == new_path @pytest.mark.parametrize(