diff --git a/homeassistant/components/satel_integra/__init__.py b/homeassistant/components/satel_integra/__init__.py index 7ef7fd5708b..914a663d8ac 100644 --- a/homeassistant/components/satel_integra/__init__.py +++ b/homeassistant/components/satel_integra/__init__.py @@ -19,6 +19,7 @@ from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, issue_registry as ir from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries from homeassistant.helpers.typing import ConfigType from .const import ( @@ -257,10 +258,11 @@ async def async_migrate_entry( config_entry.minor_version, ) - if config_entry.version > 1: + if config_entry.version > 2: # This means the user has downgraded from a future version return False + # 1.2 Migrate subentries to include configured numbers to title if config_entry.version == 1 and config_entry.minor_version == 1: for subentry in config_entry.subentries.values(): property_map = { @@ -278,6 +280,21 @@ async def async_migrate_entry( hass.config_entries.async_update_entry(config_entry, minor_version=2) + # 2.1 Migrate all entity unique IDs to replace "satel" prefix with config entry ID, allows multiple entries to be configured + if config_entry.version == 1: + + @callback + def migrate_unique_id(entity_entry: RegistryEntry) -> dict[str, str]: + """Migrate the unique ID to a new format.""" + return { + "new_unique_id": entity_entry.unique_id.replace( + "satel", config_entry.entry_id + ) + } + + await async_migrate_entries(hass, config_entry.entry_id, migrate_unique_id) + hass.config_entries.async_update_entry(config_entry, version=2, minor_version=1) + _LOGGER.debug( "Migration to configuration version %s.%s successful", config_entry.version, diff --git a/homeassistant/components/satel_integra/alarm_control_panel.py b/homeassistant/components/satel_integra/alarm_control_panel.py index b00741e1971..510193b5de7 100644 --- a/homeassistant/components/satel_integra/alarm_control_panel.py +++ b/homeassistant/components/satel_integra/alarm_control_panel.py @@ -52,7 +52,11 @@ async def async_setup_entry( async_add_entities( [ SatelIntegraAlarmPanel( - controller, zone_name, arm_home_mode, partition_num + controller, + zone_name, + arm_home_mode, + partition_num, + config_entry.entry_id, ) ], config_subentry_id=subentry.subentry_id, @@ -69,10 +73,12 @@ class SatelIntegraAlarmPanel(AlarmControlPanelEntity): | AlarmControlPanelEntityFeature.ARM_AWAY ) - def __init__(self, controller, name, arm_home_mode, partition_id) -> None: + def __init__( + self, controller, name, arm_home_mode, partition_id, config_entry_id + ) -> None: """Initialize the alarm panel.""" self._attr_name = name - self._attr_unique_id = f"satel_alarm_panel_{partition_id}" + self._attr_unique_id = f"{config_entry_id}_alarm_panel_{partition_id}" self._arm_home_mode = arm_home_mode self._partition_id = partition_id self._satel = controller diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py index fdeef7cffc4..d0c799269f1 100644 --- a/homeassistant/components/satel_integra/binary_sensor.py +++ b/homeassistant/components/satel_integra/binary_sensor.py @@ -53,6 +53,7 @@ async def async_setup_entry( zone_type, CONF_ZONES, SIGNAL_ZONES_UPDATED, + config_entry.entry_id, ) ], config_subentry_id=subentry.subentry_id, @@ -77,6 +78,7 @@ async def async_setup_entry( ouput_type, CONF_OUTPUTS, SIGNAL_OUTPUTS_UPDATED, + config_entry.entry_id, ) ], config_subentry_id=subentry.subentry_id, @@ -96,10 +98,11 @@ class SatelIntegraBinarySensor(BinarySensorEntity): zone_type, sensor_type, react_to_signal, + config_entry_id, ): """Initialize the binary_sensor.""" self._device_number = device_number - self._attr_unique_id = f"satel_{sensor_type}_{device_number}" + self._attr_unique_id = f"{config_entry_id}_{sensor_type}_{device_number}" self._name = device_name self._zone_type = zone_type self._state = 0 diff --git a/homeassistant/components/satel_integra/config_flow.py b/homeassistant/components/satel_integra/config_flow.py index 47cff421a19..66fdb93b533 100644 --- a/homeassistant/components/satel_integra/config_flow.py +++ b/homeassistant/components/satel_integra/config_flow.py @@ -90,8 +90,8 @@ SWITCHABLE_OUTPUT_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) class SatelConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a Satel Integra config flow.""" - VERSION = 1 - MINOR_VERSION = 2 + VERSION = 2 + MINOR_VERSION = 1 @staticmethod @callback diff --git a/homeassistant/components/satel_integra/switch.py b/homeassistant/components/satel_integra/switch.py index 85139069ce6..cca312619aa 100644 --- a/homeassistant/components/satel_integra/switch.py +++ b/homeassistant/components/satel_integra/switch.py @@ -46,6 +46,7 @@ async def async_setup_entry( switchable_output_num, switchable_output_name, config_entry.options.get(CONF_CODE), + config_entry.entry_id, ), ], config_subentry_id=subentry.subentry_id, @@ -57,10 +58,10 @@ class SatelIntegraSwitch(SwitchEntity): _attr_should_poll = False - def __init__(self, controller, device_number, device_name, code): + def __init__(self, controller, device_number, device_name, code, config_entry_id): """Initialize the binary_sensor.""" self._device_number = device_number - self._attr_unique_id = f"satel_switch_{device_number}" + self._attr_unique_id = f"{config_entry_id}_switch_{device_number}" self._name = device_name self._state = False self._code = code diff --git a/tests/components/satel_integra/__init__.py b/tests/components/satel_integra/__init__.py index f339aa4e424..d8128ca0d25 100644 --- a/tests/components/satel_integra/__init__.py +++ b/tests/components/satel_integra/__init__.py @@ -20,6 +20,8 @@ from homeassistant.const import CONF_CODE, CONF_HOST, CONF_NAME, CONF_PORT MOCK_CONFIG_DATA = {CONF_HOST: "192.168.0.2", CONF_PORT: DEFAULT_PORT} MOCK_CONFIG_OPTIONS = {CONF_CODE: "1234"} +MOCK_ENTRY_ID = "1234567890" + MOCK_PARTITION_SUBENTRY = ConfigSubentry( subentry_type=SUBENTRY_TYPE_PARTITION, subentry_id="ID_PARTITION", diff --git a/tests/components/satel_integra/conftest.py b/tests/components/satel_integra/conftest.py index ca3c34dd2c9..c2aa8c61e47 100644 --- a/tests/components/satel_integra/conftest.py +++ b/tests/components/satel_integra/conftest.py @@ -11,6 +11,7 @@ from homeassistant.components.satel_integra.const import DOMAIN from . import ( MOCK_CONFIG_DATA, MOCK_CONFIG_OPTIONS, + MOCK_ENTRY_ID, MOCK_OUTPUT_SUBENTRY, MOCK_PARTITION_SUBENTRY, MOCK_SWITCHABLE_OUTPUT_SUBENTRY, @@ -58,9 +59,9 @@ def mock_config_entry() -> MockConfigEntry: title="192.168.0.2", data=MOCK_CONFIG_DATA, options=MOCK_CONFIG_OPTIONS, - entry_id="SATEL_INTEGRA_CONFIG_ENTRY_1", - version=1, - minor_version=2, + entry_id=MOCK_ENTRY_ID, + version=2, + minor_version=1, ) diff --git a/tests/components/satel_integra/snapshots/test_init.ambr b/tests/components/satel_integra/snapshots/test_init.ambr index fe629bac344..5dfa1dfb5e7 100644 --- a/tests/components/satel_integra/snapshots/test_init.ambr +++ b/tests/components/satel_integra/snapshots/test_init.ambr @@ -1,69 +1,192 @@ # serializer version: 1 -# name: test_config_flow_migration_version_1_2 - ConfigEntrySnapshot({ +# name: test_config_flow_migration_version_1_2[original0-partition_number] + dict({ 'data': dict({ - 'host': '192.168.0.2', - 'port': 7094, + 'arm_home_mode': 1, + 'name': 'Home', + 'partition_number': 1, }), - 'disabled_by': None, - 'discovery_keys': dict({ - }), - 'domain': 'satel_integra', - 'entry_id': , - 'minor_version': 2, - 'options': dict({ - 'code': '1234', - }), - 'pref_disable_new_entities': False, - 'pref_disable_polling': False, - 'source': 'user', - 'subentries': list([ - dict({ - 'data': dict({ - 'arm_home_mode': 1, - 'name': 'Home', - 'partition_number': 1, - }), - 'subentry_id': 'ID_PARTITION', - 'subentry_type': 'partition', - 'title': 'Home (1) (1)', - 'unique_id': 'partition_1', - }), - dict({ - 'data': dict({ - 'name': 'Zone', - 'type': , - 'zone_number': 1, - }), - 'subentry_id': 'ID_ZONE', - 'subentry_type': 'zone', - 'title': 'Zone (1) (1)', - 'unique_id': 'zone_1', - }), - dict({ - 'data': dict({ - 'name': 'Output', - 'output_number': 1, - 'type': , - }), - 'subentry_id': 'ID_OUTPUT', - 'subentry_type': 'output', - 'title': 'Output (1) (1)', - 'unique_id': 'output_1', - }), - dict({ - 'data': dict({ - 'name': 'Switchable Output', - 'switchable_output_number': 1, - }), - 'subentry_id': 'ID_SWITCHABLE_OUTPUT', - 'subentry_type': 'switchable_output', - 'title': 'Switchable Output (1) (1)', - 'unique_id': 'switchable_output_1', - }), - ]), - 'title': '192.168.0.2', - 'unique_id': None, - 'version': 1, + 'subentry_id': 'ID_PARTITION', + 'subentry_type': 'partition', + 'title': 'Home (1) (1)', + 'unique_id': 'partition_1', + }) +# --- +# name: test_config_flow_migration_version_1_2[original1-zone_number] + dict({ + 'data': dict({ + 'name': 'Zone', + 'type': , + 'zone_number': 1, + }), + 'subentry_id': 'ID_ZONE', + 'subentry_type': 'zone', + 'title': 'Zone (1) (1)', + 'unique_id': 'zone_1', + }) +# --- +# name: test_config_flow_migration_version_1_2[original2-output_number] + dict({ + 'data': dict({ + 'name': 'Output', + 'output_number': 1, + 'type': , + }), + 'subentry_id': 'ID_OUTPUT', + 'subentry_type': 'output', + 'title': 'Output (1) (1)', + 'unique_id': 'output_1', + }) +# --- +# name: test_config_flow_migration_version_1_2[original3-switchable_output_number] + dict({ + 'data': dict({ + 'name': 'Switchable Output', + 'switchable_output_number': 1, + }), + 'subentry_id': 'ID_SWITCHABLE_OUTPUT', + 'subentry_type': 'switchable_output', + 'title': 'Switchable Output (1) (1)', + 'unique_id': 'switchable_output_1', + }) +# --- +# name: test_unique_id_migration_from_single_config[alarm_control_panel-satel_alarm_panel_1-1234567890_alarm_panel_1] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'alarm_control_panel', + 'entity_category': None, + 'entity_id': 'alarm_control_panel.satel_integra_satel_alarm_panel_1', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'satel_integra', + 'previous_unique_id': 'satel_alarm_panel_1', + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567890_alarm_panel_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_unique_id_migration_from_single_config[binary_sensor-satel_output_1-1234567890_output_1] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.satel_integra_satel_output_1', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'satel_integra', + 'previous_unique_id': 'satel_output_1', + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567890_output_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_unique_id_migration_from_single_config[binary_sensor-satel_zone_1-1234567890_zone_1] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.satel_integra_satel_zone_1', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'satel_integra', + 'previous_unique_id': 'satel_zone_1', + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567890_zone_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_unique_id_migration_from_single_config[switch-satel_switch_1-1234567890_switch_1] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.satel_integra_satel_switch_1', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'satel_integra', + 'previous_unique_id': 'satel_switch_1', + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567890_switch_1', + 'unit_of_measurement': None, }) # --- diff --git a/tests/components/satel_integra/test_init.py b/tests/components/satel_integra/test_init.py index 566348430cb..e057cc0974e 100644 --- a/tests/components/satel_integra/test_init.py +++ b/tests/components/satel_integra/test_init.py @@ -3,14 +3,25 @@ from copy import deepcopy from unittest.mock import AsyncMock +import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_PANEL_DOMAIN +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.satel_integra.const import DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigSubentry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import EntityRegistry from . import ( + CONF_OUTPUT_NUMBER, + CONF_PARTITION_NUMBER, + CONF_SWITCHABLE_OUTPUT_NUMBER, + CONF_ZONE_NUMBER, MOCK_CONFIG_DATA, MOCK_CONFIG_OPTIONS, + MOCK_ENTRY_ID, MOCK_OUTPUT_SUBENTRY, MOCK_PARTITION_SUBENTRY, MOCK_SWITCHABLE_OUTPUT_SUBENTRY, @@ -20,37 +31,94 @@ from . import ( from tests.common import MockConfigEntry +@pytest.mark.parametrize( + ("original", "number_property"), + [ + (MOCK_PARTITION_SUBENTRY, CONF_PARTITION_NUMBER), + (MOCK_ZONE_SUBENTRY, CONF_ZONE_NUMBER), + (MOCK_OUTPUT_SUBENTRY, CONF_OUTPUT_NUMBER), + (MOCK_SWITCHABLE_OUTPUT_SUBENTRY, CONF_SWITCHABLE_OUTPUT_NUMBER), + ], +) async def test_config_flow_migration_version_1_2( hass: HomeAssistant, snapshot: SnapshotAssertion, mock_satel: AsyncMock, + original: ConfigSubentry, + number_property: str, ) -> None: - """Test that the unique ID is migrated to the new format.""" + """Test that the configured number is added to the subentry title.""" config_entry = MockConfigEntry( domain=DOMAIN, title="192.168.0.2", data=MOCK_CONFIG_DATA, options=MOCK_CONFIG_OPTIONS, - entry_id="SATEL_INTEGRA_CONFIG_ENTRY_1", + entry_id=MOCK_ENTRY_ID, version=1, minor_version=1, ) - config_entry.subentries = deepcopy( - { - MOCK_PARTITION_SUBENTRY.subentry_id: MOCK_PARTITION_SUBENTRY, - MOCK_ZONE_SUBENTRY.subentry_id: MOCK_ZONE_SUBENTRY, - MOCK_OUTPUT_SUBENTRY.subentry_id: MOCK_OUTPUT_SUBENTRY, - MOCK_SWITCHABLE_OUTPUT_SUBENTRY.subentry_id: MOCK_SWITCHABLE_OUTPUT_SUBENTRY, - } - ) + config_entry.subentries = deepcopy({original.subentry_id: original}) 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.version == 1 - assert config_entry.minor_version == 2 + assert config_entry.version == 2 + assert config_entry.minor_version == 1 - assert config_entry == snapshot + subentry = config_entry.subentries.get(original.subentry_id) + assert subentry is not None + assert subentry.title == f"{original.title} ({original.data[number_property]})" + + assert subentry == snapshot + + +@pytest.mark.parametrize( + ("platform", "old_id", "new_id"), + [ + (ALARM_PANEL_DOMAIN, "satel_alarm_panel_1", f"{MOCK_ENTRY_ID}_alarm_panel_1"), + (BINARY_SENSOR_DOMAIN, "satel_zone_1", f"{MOCK_ENTRY_ID}_zone_1"), + (BINARY_SENSOR_DOMAIN, "satel_output_1", f"{MOCK_ENTRY_ID}_output_1"), + (SWITCH_DOMAIN, "satel_switch_1", f"{MOCK_ENTRY_ID}_switch_1"), + ], +) +async def test_unique_id_migration_from_single_config( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_satel: AsyncMock, + entity_registry: EntityRegistry, + platform: str, + old_id: str, + new_id: str, +) -> None: + """Test that the unique ID is migrated to use the config entry id.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + title="192.168.0.2", + data=MOCK_CONFIG_DATA, + options=MOCK_CONFIG_OPTIONS, + entry_id=MOCK_ENTRY_ID, + version=1, + minor_version=1, + ) + config_entry.add_to_hass(hass) + + entity = entity_registry.async_get_or_create( + platform, + DOMAIN, + old_id, + config_entry=config_entry, + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entity = entity_registry.async_get(entity.entity_id) + + assert entity is not None + assert entity.unique_id == new_id + + assert entity == snapshot