Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit dd22c78

Browse files
authored
Migrate ZHA config entries to derive unique_id from the Zigbee EPID (home-assistant#154489)
1 parent 1a732ac commit dd22c78

File tree

8 files changed

+78
-16
lines changed

8 files changed

+78
-16
lines changed

‎homeassistant/components/zha/__init__.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
HAZHAData,
4949
ZHAGatewayProxy,
5050
create_zha_config,
51+
get_config_entry_unique_id,
5152
get_zha_data,
5253
)
5354
from .radio_manager import ZhaRadioManager
@@ -198,6 +199,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
198199

199200
repairs.async_delete_blocking_issues(hass)
200201

202+
# Set unique_id if it was not migrated previously
203+
if not config_entry.unique_id or not config_entry.unique_id.startswith("epid="):
204+
unique_id = get_config_entry_unique_id(zha_gateway.state.network_info)
205+
hass.config_entries.async_update_entry(config_entry, unique_id=unique_id)
206+
201207
ha_zha_data.gateway_proxy = ZHAGatewayProxy(hass, config_entry, zha_gateway)
202208

203209
manufacturer = zha_gateway.state.node_info.manufacturer
@@ -313,5 +319,26 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
313319

314320
hass.config_entries.async_update_entry(config_entry, data=data, version=4)
315321

322+
if config_entry.version == 4:
323+
radio_mgr = ZhaRadioManager.from_config_entry(hass, config_entry)
324+
await radio_mgr.async_read_backups_from_database()
325+
326+
if radio_mgr.backups:
327+
# We migrate all ZHA config entries to use a `unique_id` specific to the
328+
# Zigbee network, not to the hardware
329+
backup = radio_mgr.backups[0]
330+
hass.config_entries.async_update_entry(
331+
config_entry,
332+
unique_id=get_config_entry_unique_id(backup.network_info),
333+
version=5,
334+
)
335+
else:
336+
# If no backups are available, the unique_id will be set when the network is
337+
# loaded during setup
338+
hass.config_entries.async_update_entry(
339+
config_entry,
340+
version=5,
341+
)
342+
316343
_LOGGER.info("Migration to version %s successful", config_entry.version)
317344
return True

‎homeassistant/components/zha/config_flow.py‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
from homeassistant.util import dt as dt_util
4848

4949
from .const import CONF_BAUDRATE, CONF_FLOW_CONTROL, CONF_RADIO_TYPE, DOMAIN
50-
from .helpers import get_zha_gateway
50+
from .helpers import get_config_entry_unique_id, get_zha_gateway
5151
from .radio_manager import (
5252
DEVICE_SCHEMA,
5353
HARDWARE_DISCOVERY_SCHEMA,
@@ -544,6 +544,8 @@ async def async_step_form_new_network(
544544
) -> ConfigFlowResult:
545545
"""Form a brand-new network."""
546546
await self._radio_mgr.async_form_network()
547+
# Load the newly formed network settings to get the network info
548+
await self._radio_mgr.async_load_network_settings()
547549
return await self._async_create_radio_entry()
548550

549551
def _parse_uploaded_backup(
@@ -668,7 +670,7 @@ async def async_step_maybe_confirm_ezsp_restore(
668670
class ZhaConfigFlowHandler(BaseZhaFlow, ConfigFlow, domain=DOMAIN):
669671
"""Handle a config flow."""
670672

671-
VERSION = 4
673+
VERSION = 5
672674

673675
async def _set_unique_id_and_update_ignored_flow(
674676
self, unique_id: str, device_path: str
@@ -927,6 +929,15 @@ async def _async_create_radio_entry(self) -> ConfigFlowResult:
927929
reason="reconfigure_successful",
928930
)
929931
if not zha_config_entries:
932+
# Load network settings from the radio to get the EPID
933+
await self._radio_mgr.async_load_network_settings()
934+
assert self._radio_mgr.current_settings is not None
935+
936+
unique_id = get_config_entry_unique_id(
937+
self._radio_mgr.current_settings.network_info
938+
)
939+
await self.async_set_unique_id(unique_id)
940+
930941
return self.async_create_entry(
931942
title=self._title,
932943
data=data,

‎homeassistant/components/zha/helpers.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
)
9191
import zigpy.exceptions
9292
from zigpy.profiles import PROFILES
93+
from zigpy.state import NetworkInfo
9394
import zigpy.types
9495
from zigpy.types import EUI64
9596
import zigpy.util
@@ -1390,3 +1391,8 @@ async def handler(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None:
13901391
def exclude_none_values(obj: Mapping[str, Any]) -> dict[str, Any]:
13911392
"""Return a new dictionary excluding keys with None values."""
13921393
return {k: v for k, v in obj.items() if v is not None}
1394+
1395+
1396+
def get_config_entry_unique_id(network_info: NetworkInfo) -> str:
1397+
"""Generate a unique id for a config entry based on the network info."""
1398+
return f"epid={network_info.extended_pan_id}".lower()

‎tests/components/zha/conftest.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ async def zigpy_app_controller():
195195
async def config_entry_fixture() -> MockConfigEntry:
196196
"""Fixture representing a config entry."""
197197
return MockConfigEntry(
198-
version=4,
198+
version=5,
199199
domain=zha_const.DOMAIN,
200200
data={
201201
zigpy.config.CONF_DEVICE: {

‎tests/components/zha/snapshots/test_diagnostics.ambr‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,8 @@
117117
'subentries': list([
118118
]),
119119
'title': 'Mock Title',
120-
'unique_id': None,
121-
'version': 4,
120+
'unique_id': '**REDACTED**',
121+
'version': 5,
122122
}),
123123
'devices': list([
124124
dict({

‎tests/components/zha/test_config_flow.py‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ def mock_app() -> Generator[AsyncMock]:
9696
mock_app = AsyncMock()
9797
mock_app.backups = create_autospec(BackupManager, instance=True)
9898
mock_app.backups.backups = []
99+
mock_app.state.network_info.extended_pan_id = zigpy.types.EUI64.convert(
100+
"AABBCCDDEE000000"
101+
)
99102
mock_app.state.network_info.metadata = {
100103
"ezsp": {
101104
"can_burn_userdata_custom_eui64": True,
@@ -175,7 +178,7 @@ def com_port(device="/dev/ttyUSB1234") -> ListPortInfo:
175178
(
176179
# TubesZB, old ESPHome devices (ZNP)
177180
"tubeszb-cc2652-poe",
178-
"tubeszb-cc2652-poe",
181+
"epid=aa:bb:cc:dd:ee:00:00:00",
179182
RadioType.znp,
180183
ZeroconfServiceInfo(
181184
ip_address=ip_address("192.168.1.200"),
@@ -198,7 +201,7 @@ def com_port(device="/dev/ttyUSB1234") -> ListPortInfo:
198201
(
199202
# TubesZB, old ESPHome device (EFR32)
200203
"tubeszb-efr32-poe",
201-
"tubeszb-efr32-poe",
204+
"epid=aa:bb:cc:dd:ee:00:00:00",
202205
RadioType.ezsp,
203206
ZeroconfServiceInfo(
204207
ip_address=ip_address("192.168.1.200"),
@@ -221,7 +224,7 @@ def com_port(device="/dev/ttyUSB1234") -> ListPortInfo:
221224
(
222225
# TubesZB, newer devices
223226
"TubeZB",
224-
"tubeszb-cc2652-poe",
227+
"epid=aa:bb:cc:dd:ee:00:00:00",
225228
RadioType.znp,
226229
ZeroconfServiceInfo(
227230
ip_address=ip_address("192.168.1.200"),
@@ -242,7 +245,7 @@ def com_port(device="/dev/ttyUSB1234") -> ListPortInfo:
242245
(
243246
# Expected format for all new devices
244247
"Some Zigbee Gateway (12345)",
245-
"aabbccddeeff",
248+
"epid=aa:bb:cc:dd:ee:00:00:00",
246249
RadioType.znp,
247250
ZeroconfServiceInfo(
248251
ip_address=ip_address("192.168.1.200"),
@@ -1627,8 +1630,15 @@ async def test_formation_strategy_form_initial_network(
16271630
advanced_pick_radio: RadioPicker, mock_app: AsyncMock, hass: HomeAssistant
16281631
) -> None:
16291632
"""Test forming a new network, with no previous settings on the radio."""
1633+
# Initially, no network is formed
16301634
mock_app.load_network_info = AsyncMock(side_effect=NetworkNotFormed())
16311635

1636+
# After form_network is called, load_network_info should return the network settings
1637+
async def form_network_side_effect(*args, **kwargs):
1638+
mock_app.load_network_info = AsyncMock(return_value=mock_app.state.network_info)
1639+
1640+
mock_app.form_network.side_effect = form_network_side_effect
1641+
16321642
result = await advanced_pick_radio(RadioType.ezsp)
16331643
result2 = await hass.config_entries.flow.async_configure(
16341644
result["flow_id"],
@@ -1648,8 +1658,16 @@ async def test_onboarding_auto_formation_new_hardware(
16481658
mock_app: AsyncMock, hass: HomeAssistant
16491659
) -> None:
16501660
"""Test auto network formation with new hardware during onboarding."""
1661+
# Initially, no network is formed
16511662
mock_app.load_network_info = AsyncMock(side_effect=NetworkNotFormed())
16521663
mock_app.get_device = MagicMock(return_value=MagicMock(spec=zigpy.device.Device))
1664+
1665+
# After form_network is called, load_network_info should return the network settings
1666+
async def form_network_side_effect(*args, **kwargs):
1667+
mock_app.load_network_info = AsyncMock(return_value=mock_app.state.network_info)
1668+
1669+
mock_app.form_network.side_effect = form_network_side_effect
1670+
16531671
discovery_info = UsbServiceInfo(
16541672
device="/dev/ttyZIGBEE",
16551673
pid="AAAA",

‎tests/components/zha/test_homeassistant_hardware.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ async def test_get_firmware_info_normal(hass: HomeAssistant) -> None:
3535
},
3636
"radio_type": "ezsp",
3737
},
38-
version=4,
38+
version=5,
3939
)
4040
zha.add_to_hass(hass)
4141
zha.mock_state(hass, ConfigEntryState.LOADED)
@@ -87,7 +87,7 @@ async def test_get_firmware_info_errors(
8787
domain="zha",
8888
unique_id="some_unique_id",
8989
data=data,
90-
version=4,
90+
version=5,
9191
)
9292
zha.add_to_hass(hass)
9393

‎tests/components/zha/test_init.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ async def test_migration_from_v1_no_baudrate(
7373
assert CONF_DEVICE in config_entry_v1.data
7474
assert config_entry_v1.data[CONF_DEVICE][CONF_DEVICE_PATH] == DATA_PORT_PATH
7575
assert CONF_USB_PATH not in config_entry_v1.data
76-
assert config_entry_v1.version == 4
76+
assert config_entry_v1.version == 5
7777

7878

7979
@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True))
@@ -90,7 +90,7 @@ async def test_migration_from_v1_with_baudrate(
9090
assert CONF_USB_PATH not in config_entry_v1.data
9191
assert CONF_BAUDRATE in config_entry_v1.data[CONF_DEVICE]
9292
assert config_entry_v1.data[CONF_DEVICE][CONF_BAUDRATE] == 115200
93-
assert config_entry_v1.version == 4
93+
assert config_entry_v1.version == 5
9494

9595

9696
@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True))
@@ -105,7 +105,7 @@ async def test_migration_from_v1_wrong_baudrate(
105105
assert CONF_DEVICE in config_entry_v1.data
106106
assert config_entry_v1.data[CONF_DEVICE][CONF_DEVICE_PATH] == DATA_PORT_PATH
107107
assert CONF_USB_PATH not in config_entry_v1.data
108-
assert config_entry_v1.version == 4
108+
assert config_entry_v1.version == 5
109109

110110

111111
@pytest.mark.skipif(
@@ -167,7 +167,7 @@ async def test_setup_with_v3_cleaning_uri(
167167
CONF_FLOW_CONTROL: None,
168168
},
169169
},
170-
version=4,
170+
version=5,
171171
)
172172
config_entry_v4.add_to_hass(hass)
173173

@@ -177,7 +177,7 @@ async def test_setup_with_v3_cleaning_uri(
177177

178178
assert config_entry_v4.data[CONF_RADIO_TYPE] == DATA_RADIO_TYPE
179179
assert config_entry_v4.data[CONF_DEVICE][CONF_DEVICE_PATH] == cleaned_path
180-
assert config_entry_v4.version == 4
180+
assert config_entry_v4.version == 5
181181

182182

183183
@pytest.mark.parametrize(

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /