diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 5ed31659baea..35c629c8af35 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -28,6 +28,9 @@ from homeassistant.helpers import discovery, issue_registry as ir from homeassistant.helpers.device import ( async_remove_stale_devices_links_keep_current_device, ) +from homeassistant.helpers.helper_integration import ( + async_remove_helper_config_entry_from_source_device, +) from homeassistant.helpers.reload import async_reload_integration_platforms from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType @@ -116,6 +119,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" + # This can be removed in HA Core 2026.7 async_remove_stale_devices_links_keep_current_device( hass, entry.entry_id, @@ -154,6 +158,41 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old entry.""" + + _LOGGER.debug( + "Migrating configuration from version %s.%s", + config_entry.version, + config_entry.minor_version, + ) + + if config_entry.version > 1: + # This means the user has downgraded from a future version + return False + + if config_entry.version == 1: + if config_entry.minor_version < 2: + # Remove the template config entry from the source device + if source_device_id := config_entry.options.get(CONF_DEVICE_ID): + async_remove_helper_config_entry_from_source_device( + hass, + helper_config_entry_id=config_entry.entry_id, + source_device_id=source_device_id, + ) + hass.config_entries.async_update_entry( + config_entry, version=1, minor_version=2 + ) + + _LOGGER.debug( + "Migration to configuration version %s.%s successful", + config_entry.version, + config_entry.minor_version, + ) + + return True + + async def _process_config(hass: HomeAssistant, hass_config: ConfigType) -> None: """Process config.""" coordinators = hass.data.pop(DATA_COORDINATORS, None) diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 15ed1ed2126c..fef25d5a12a4 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -697,6 +697,9 @@ class TemplateConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): options_flow = OPTIONS_FLOW options_flow_reloads = True + MINOR_VERSION = 2 + VERSION = 1 + @callback def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index 9d0320db22aa..10b6a89f7dc4 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -8,6 +8,7 @@ import pytest from homeassistant import config from homeassistant.components import labs from homeassistant.components.template import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import SERVICE_RELOAD from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import ( @@ -754,3 +755,80 @@ async def test_config_entry_reload_when_labs_flag_changes( assert hass.states.get("sensor.hello") is not None assert hass.states.get("sensor.hello").state == set_state + + +async def test_migration_1_1( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test migration from v1.1 removes template config entry from device.""" + + device_config_entry = MockConfigEntry() + device_config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=device_config_entry.entry_id, + identifiers={("test", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, + ) + + template_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My template", + "template_type": "sensor", + "state": "{{ 'foo' }}", + "device_id": device_entry.id, + }, + title="My template", + version=1, + minor_version=1, + ) + template_config_entry.add_to_hass(hass) + + # Add the helper config entry to the device + device_registry.async_update_device( + device_entry.id, add_config_entry_id=template_config_entry.entry_id + ) + + # Check preconditions + device_entry = device_registry.async_get(device_entry.id) + assert template_config_entry.entry_id in device_entry.config_entries + + await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + assert template_config_entry.state is ConfigEntryState.LOADED + + # Check that the helper config entry is removed from the device and the helper + # entity is linked to the source device + device_entry = device_registry.async_get(device_entry.id) + assert template_config_entry.entry_id not in device_entry.config_entries + template_entity_entry = entity_registry.async_get("sensor.my_template") + assert template_entity_entry.device_id == device_entry.id + + assert template_config_entry.version == 1 + assert template_config_entry.minor_version == 2 + + +async def test_migration_from_future_version( + hass: HomeAssistant, +) -> None: + """Test migration from future version.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "hello", + "template_type": "sensor", + "state": "{{ 'foo' }}", + }, + title="My template", + version=2, + minor_version=1, + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.MIGRATION_ERROR