1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-08 17:49:37 +01:00

Migrate Satel Integra entities unique_id to use config flow entry_id (#154187)

This commit is contained in:
Tom Matheussen
2025-11-04 20:03:08 +01:00
committed by GitHub
parent fb1f258b2b
commit 57c69738e3
9 changed files with 310 additions and 89 deletions
@@ -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,
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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",
+4 -3
View File
@@ -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,
)
@@ -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': <ANY>,
'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': <BinarySensorDeviceClass.MOTION: 'motion'>,
'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': <BinarySensorDeviceClass.SAFETY: 'safety'>,
}),
'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': <BinarySensorDeviceClass.MOTION: 'motion'>,
'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': <BinarySensorDeviceClass.SAFETY: 'safety'>,
}),
'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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'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,
})
# ---
+81 -13
View File
@@ -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