Skip to content

Commit 64f4856

Browse files
Change device identifier and binary_sensor unique_id for airOS (home-assistant#153085)
Co-authored-by: G Johansson <[email protected]>
1 parent 06e4922 commit 64f4856

File tree

6 files changed

+144
-19
lines changed

6 files changed

+144
-19
lines changed

homeassistant/components/airos/__init__.py

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
import logging
6+
57
from airos.airos8 import AirOS8
68

79
from homeassistant.const import (
@@ -12,17 +14,20 @@
1214
CONF_VERIFY_SSL,
1315
Platform,
1416
)
15-
from homeassistant.core import HomeAssistant
17+
from homeassistant.core import HomeAssistant, callback
18+
from homeassistant.helpers import device_registry as dr, entity_registry as er
1619
from homeassistant.helpers.aiohttp_client import async_get_clientsession
1720

18-
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, SECTION_ADVANCED_SETTINGS
21+
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, SECTION_ADVANCED_SETTINGS
1922
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
2023

2124
_PLATFORMS: list[Platform] = [
2225
Platform.BINARY_SENSOR,
2326
Platform.SENSOR,
2427
]
2528

29+
_LOGGER = logging.getLogger(__name__)
30+
2631

2732
async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
2833
"""Set up Ubiquiti airOS from a config entry."""
@@ -54,11 +59,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
5459
async def async_migrate_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
5560
"""Migrate old config entry."""
5661

57-
if entry.version > 1:
58-
# This means the user has downgraded from a future version
62+
# This means the user has downgraded from a future version
63+
if entry.version > 2:
5964
return False
6065

66+
# 1.1 Migrate config_entry to add advanced ssl settings
6167
if entry.version == 1 and entry.minor_version == 1:
68+
new_minor_version = 2
6269
new_data = {**entry.data}
6370
advanced_data = {
6471
CONF_SSL: DEFAULT_SSL,
@@ -69,7 +76,52 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> b
6976
hass.config_entries.async_update_entry(
7077
entry,
7178
data=new_data,
72-
minor_version=2,
79+
minor_version=new_minor_version,
80+
)
81+
82+
# 2.1 Migrate binary_sensor entity unique_id from device_id to mac_address
83+
# Step 1 - migrate binary_sensor entity unique_id
84+
# Step 2 - migrate device entity identifier
85+
if entry.version == 1:
86+
new_version = 2
87+
new_minor_version = 1
88+
89+
mac_adress = dr.format_mac(entry.unique_id)
90+
91+
device_registry = dr.async_get(hass)
92+
if device_entry := device_registry.async_get_device(
93+
connections={(dr.CONNECTION_NETWORK_MAC, mac_adress)}
94+
):
95+
old_device_id = next(
96+
(
97+
device_id
98+
for domain, device_id in device_entry.identifiers
99+
if domain == DOMAIN
100+
),
101+
)
102+
103+
@callback
104+
def update_unique_id(
105+
entity_entry: er.RegistryEntry,
106+
) -> dict[str, str] | None:
107+
"""Update unique id from device_id to mac address."""
108+
if old_device_id and entity_entry.unique_id.startswith(old_device_id):
109+
suffix = entity_entry.unique_id.removeprefix(old_device_id)
110+
new_unique_id = f"{mac_adress}{suffix}"
111+
return {"new_unique_id": new_unique_id}
112+
return None
113+
114+
await er.async_migrate_entries(hass, entry.entry_id, update_unique_id)
115+
116+
new_identifiers = device_entry.identifiers.copy()
117+
new_identifiers.discard((DOMAIN, old_device_id))
118+
new_identifiers.add((DOMAIN, mac_adress))
119+
device_registry.async_update_device(
120+
device_entry.id, new_identifiers=new_identifiers
121+
)
122+
123+
hass.config_entries.async_update_entry(
124+
entry, version=new_version, minor_version=new_minor_version
73125
)
74126

75127
return True

homeassistant/components/airos/binary_sensor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def __init__(
9898
super().__init__(coordinator)
9999

100100
self.entity_description = description
101-
self._attr_unique_id = f"{coordinator.data.host.device_id}_{description.key}"
101+
self._attr_unique_id = f"{coordinator.data.derived.mac}_{description.key}"
102102

103103
@property
104104
def is_on(self) -> bool:

homeassistant/components/airos/config_flow.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@
5757
class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
5858
"""Handle a config flow for Ubiquiti airOS."""
5959

60-
VERSION = 1
61-
MINOR_VERSION = 2
60+
VERSION = 2
61+
MINOR_VERSION = 1
6262

6363
def __init__(self) -> None:
6464
"""Initialize the config flow."""

homeassistant/components/airos/entity.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def __init__(self, coordinator: AirOSDataUpdateCoordinator) -> None:
3333
self._attr_device_info = DeviceInfo(
3434
connections={(CONNECTION_NETWORK_MAC, airos_data.derived.mac)},
3535
configuration_url=configuration_url,
36-
identifiers={(DOMAIN, str(airos_data.host.device_id))},
36+
identifiers={(DOMAIN, airos_data.derived.mac)},
3737
manufacturer=MANUFACTURER,
3838
model=airos_data.host.devmodel,
3939
model_id=(

tests/components/airos/snapshots/test_binary_sensor.ambr

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
'suggested_object_id': None,
3131
'supported_features': 0,
3232
'translation_key': 'dhcp_client',
33-
'unique_id': '03aa0d0b40fed0a47088293584ef5432_dhcp_client',
33+
'unique_id': '01:23:45:67:89:AB_dhcp_client',
3434
'unit_of_measurement': None,
3535
})
3636
# ---
@@ -79,7 +79,7 @@
7979
'suggested_object_id': None,
8080
'supported_features': 0,
8181
'translation_key': 'dhcp_server',
82-
'unique_id': '03aa0d0b40fed0a47088293584ef5432_dhcp_server',
82+
'unique_id': '01:23:45:67:89:AB_dhcp_server',
8383
'unit_of_measurement': None,
8484
})
8585
# ---
@@ -128,7 +128,7 @@
128128
'suggested_object_id': None,
129129
'supported_features': 0,
130130
'translation_key': 'dhcp6_server',
131-
'unique_id': '03aa0d0b40fed0a47088293584ef5432_dhcp6_server',
131+
'unique_id': '01:23:45:67:89:AB_dhcp6_server',
132132
'unit_of_measurement': None,
133133
})
134134
# ---
@@ -177,7 +177,7 @@
177177
'suggested_object_id': None,
178178
'supported_features': 0,
179179
'translation_key': 'port_forwarding',
180-
'unique_id': '03aa0d0b40fed0a47088293584ef5432_portfw',
180+
'unique_id': '01:23:45:67:89:AB_portfw',
181181
'unit_of_measurement': None,
182182
})
183183
# ---
@@ -225,7 +225,7 @@
225225
'suggested_object_id': None,
226226
'supported_features': 0,
227227
'translation_key': 'pppoe',
228-
'unique_id': '03aa0d0b40fed0a47088293584ef5432_pppoe',
228+
'unique_id': '01:23:45:67:89:AB_pppoe',
229229
'unit_of_measurement': None,
230230
})
231231
# ---

tests/components/airos/test_init.py

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44

55
from unittest.mock import ANY, MagicMock
66

7+
import pytest
8+
79
from homeassistant.components.airos.const import (
810
DEFAULT_SSL,
911
DEFAULT_VERIFY_SSL,
1012
DOMAIN,
1113
SECTION_ADVANCED_SETTINGS,
1214
)
15+
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
16+
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
1317
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
1418
from homeassistant.const import (
1519
CONF_HOST,
@@ -19,6 +23,7 @@
1923
CONF_VERIFY_SSL,
2024
)
2125
from homeassistant.core import HomeAssistant
26+
from homeassistant.helpers import device_registry as dr, entity_registry as er
2227

2328
from tests.common import MockConfigEntry
2429

@@ -108,8 +113,10 @@ async def test_setup_entry_without_ssl(
108113
assert entry.data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL] is False
109114

110115

111-
async def test_migrate_entry(hass: HomeAssistant, mock_airos_client: MagicMock) -> None:
112-
"""Test migrate entry unique id."""
116+
async def test_ssl_migrate_entry(
117+
hass: HomeAssistant, mock_airos_client: MagicMock
118+
) -> None:
119+
"""Test migrate entry SSL options."""
113120
entry = MockConfigEntry(
114121
domain=DOMAIN,
115122
source=SOURCE_USER,
@@ -124,11 +131,77 @@ async def test_migrate_entry(hass: HomeAssistant, mock_airos_client: MagicMock)
124131
await hass.async_block_till_done()
125132

126133
assert entry.state is ConfigEntryState.LOADED
127-
assert entry.version == 1
128-
assert entry.minor_version == 2
134+
assert entry.version == 2
135+
assert entry.minor_version == 1
129136
assert entry.data == MOCK_CONFIG_V1_2
130137

131138

139+
@pytest.mark.parametrize(
140+
("sensor_domain", "sensor_name", "mock_id"),
141+
[
142+
(BINARY_SENSOR_DOMAIN, "port_forwarding", "device_id_12345"),
143+
(SENSOR_DOMAIN, "antenna_gain", "01:23:45:67:89:ab"),
144+
],
145+
)
146+
async def test_uid_migrate_entry(
147+
hass: HomeAssistant,
148+
mock_airos_client: MagicMock,
149+
device_registry: dr.DeviceRegistry,
150+
sensor_domain: str,
151+
sensor_name: str,
152+
mock_id: str,
153+
) -> None:
154+
"""Test migrate entry unique id."""
155+
entity_registry = er.async_get(hass)
156+
157+
MOCK_MAC = dr.format_mac("01:23:45:67:89:AB")
158+
MOCK_ID = "device_id_12345"
159+
old_unique_id = f"{mock_id}_{sensor_name}"
160+
new_unique_id = f"{MOCK_MAC}_{sensor_name}"
161+
162+
entry = MockConfigEntry(
163+
domain=DOMAIN,
164+
source=SOURCE_USER,
165+
data=MOCK_CONFIG_V1_2,
166+
entry_id="1",
167+
unique_id=mock_id,
168+
version=1,
169+
minor_version=2,
170+
)
171+
entry.add_to_hass(hass)
172+
173+
device_registry.async_get_or_create(
174+
config_entry_id=entry.entry_id,
175+
identifiers={(DOMAIN, MOCK_ID)},
176+
connections={
177+
(dr.CONNECTION_NETWORK_MAC, MOCK_MAC),
178+
},
179+
)
180+
await hass.async_block_till_done()
181+
182+
old_entity_entry = entity_registry.async_get_or_create(
183+
DOMAIN, sensor_domain, old_unique_id, config_entry=entry
184+
)
185+
original_entity_id = old_entity_entry.entity_id
186+
187+
hass.config_entries.async_update_entry(entry, unique_id=MOCK_MAC)
188+
await hass.async_block_till_done()
189+
190+
await hass.config_entries.async_setup(entry.entry_id)
191+
await hass.async_block_till_done()
192+
193+
updated_entity_entry = entity_registry.async_get(original_entity_id)
194+
195+
assert entry.state is ConfigEntryState.LOADED
196+
assert entry.version == 2
197+
assert entry.minor_version == 1
198+
assert (
199+
entity_registry.async_get_entity_id(sensor_domain, DOMAIN, old_unique_id)
200+
is None
201+
)
202+
assert updated_entity_entry.unique_id == new_unique_id
203+
204+
132205
async def test_migrate_future_return(
133206
hass: HomeAssistant,
134207
mock_airos_client: MagicMock,
@@ -140,7 +213,7 @@ async def test_migrate_future_return(
140213
data=MOCK_CONFIG_V1_2,
141214
entry_id="1",
142215
unique_id="airos_device",
143-
version=2,
216+
version=3,
144217
)
145218
entry.add_to_hass(hass)
146219

0 commit comments

Comments
 (0)