Use mac address in Twinkly for unique id (#133717)

pull/133724/head
Joost Lekkerkerker 2024-12-21 13:26:09 +01:00 committed by GitHub
parent a3fad89d0d
commit dc9133f919
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 110 additions and 21 deletions

View File

@ -1,6 +1,7 @@
"""The twinkly component."""
from dataclasses import dataclass
import logging
from typing import Any
from aiohttp import ClientError
@ -10,12 +11,15 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import ATTR_VERSION
from .const import ATTR_VERSION, DOMAIN
PLATFORMS = [Platform.LIGHT]
_LOGGER = logging.getLogger(__name__)
@dataclass
class TwinklyData:
@ -56,3 +60,39 @@ async def async_unload_entry(hass: HomeAssistant, entry: TwinklyConfigEntry) ->
"""Remove a twinkly entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_migrate_entry(hass: HomeAssistant, entry: TwinklyConfigEntry) -> bool:
"""Migrate old entry."""
if entry.minor_version == 1:
client = Twinkly(entry.data[CONF_HOST], async_get_clientsession(hass))
try:
device_info = await client.get_details()
except (TimeoutError, ClientError) as exception:
_LOGGER.error("Error while migrating: %s", exception)
return False
identifier = entry.unique_id
assert identifier is not None
entity_registry = er.async_get(hass)
entity_id = entity_registry.async_get_entity_id("light", DOMAIN, identifier)
if entity_id:
entity_entry = entity_registry.async_get(entity_id)
assert entity_entry is not None
entity_registry.async_update_entity(
entity_entry.entity_id, new_unique_id=device_info["mac"]
)
device_registry = dr.async_get(hass)
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, identifier)}
)
if device_entry:
device_registry.async_update_device(
device_entry.id, new_identifiers={(DOMAIN, device_info["mac"])}
)
hass.config_entries.async_update_entry(
entry,
unique_id=device_info["mac"],
minor_version=2,
)
return True

View File

@ -23,6 +23,7 @@ class TwinklyConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle twinkly config flow."""
VERSION = 1
MINOR_VERSION = 2
def __init__(self) -> None:
"""Initialize the config flow."""
@ -46,7 +47,7 @@ class TwinklyConfigFlow(ConfigFlow, domain=DOMAIN):
errors[CONF_HOST] = "cannot_connect"
else:
await self.async_set_unique_id(
device_info[DEV_ID], raise_on_progress=False
device_info["mac"], raise_on_progress=False
)
self._abort_if_unique_id_configured()
@ -64,7 +65,7 @@ class TwinklyConfigFlow(ConfigFlow, domain=DOMAIN):
device_info = await Twinkly(
discovery_info.ip, async_get_clientsession(self.hass)
).get_details()
await self.async_set_unique_id(device_info[DEV_ID])
await self.async_set_unique_id(device_info["mac"])
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip})
self._discovered_device = (device_info, discovery_info.ip)

View File

@ -60,8 +60,8 @@ class TwinklyLight(LightEntity):
entry: TwinklyConfigEntry,
) -> None:
"""Initialize a TwinklyLight entity."""
self._attr_unique_id: str = entry.data[CONF_ID]
device_info = entry.runtime_data.device_info
self._attr_unique_id: str = device_info["mac"]
self._conf = entry
if device_info.get(DEV_LED_PROFILE) == DEV_PROFILE_RGBW:
@ -98,7 +98,7 @@ class TwinklyLight(LightEntity):
def device_info(self) -> DeviceInfo | None:
"""Get device specific attributes."""
return DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)},
identifiers={(DOMAIN, self._mac)},
connections={(CONNECTION_NETWORK_MAC, self._mac)},
manufacturer="LEDWORKS",
model=self._model,

View File

@ -32,14 +32,14 @@
}),
'domain': 'twinkly',
'entry_id': '4c8fccf5-e08a-4173-92d5-49bf479252a2',
'minor_version': 1,
'minor_version': 2,
'options': dict({
}),
'pref_disable_new_entities': False,
'pref_disable_polling': False,
'source': 'user',
'title': 'Twinkly',
'unique_id': '4c8fccf5-e08a-4173-92d5-49bf479252a2',
'unique_id': 'aa:bb:cc:dd:ee:ff',
'version': 1,
}),
'sw_version': '2.8.10',

View File

@ -1,14 +1,16 @@
"""Tests of the initialization of the twinly integration."""
"""Tests of the initialization of the twinkly integration."""
from unittest.mock import patch
from uuid import uuid4
from homeassistant.components.twinkly.const import DOMAIN as TWINKLY_DOMAIN
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.twinkly.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from . import TEST_HOST, TEST_MODEL, TEST_NAME_ORIGINAL, ClientMock
from . import TEST_HOST, TEST_MAC, TEST_MODEL, TEST_NAME_ORIGINAL, ClientMock
from tests.common import MockConfigEntry
@ -19,7 +21,7 @@ async def test_load_unload_entry(hass: HomeAssistant) -> None:
device_id = str(uuid4())
config_entry = MockConfigEntry(
domain=TWINKLY_DOMAIN,
domain=DOMAIN,
data={
CONF_HOST: TEST_HOST,
CONF_ID: device_id,
@ -27,6 +29,8 @@ async def test_load_unload_entry(hass: HomeAssistant) -> None:
CONF_MODEL: TEST_MODEL,
},
entry_id=device_id,
unique_id=TEST_MAC,
minor_version=2,
)
config_entry.add_to_hass(hass)
@ -47,13 +51,15 @@ async def test_config_entry_not_ready(hass: HomeAssistant) -> None:
client.is_offline = True
config_entry = MockConfigEntry(
domain=TWINKLY_DOMAIN,
domain=DOMAIN,
data={
CONF_HOST: TEST_HOST,
CONF_ID: id,
CONF_NAME: TEST_NAME_ORIGINAL,
CONF_MODEL: TEST_MODEL,
},
minor_version=2,
unique_id=TEST_MAC,
)
config_entry.add_to_hass(hass)
@ -62,3 +68,45 @@ async def test_config_entry_not_ready(hass: HomeAssistant) -> None:
await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_mac_migration(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
) -> None:
"""Validate that the unique_id is migrated to the MAC address."""
client = ClientMock()
config_entry = MockConfigEntry(
domain=DOMAIN,
minor_version=1,
unique_id="unique_id",
data={
CONF_HOST: TEST_HOST,
CONF_ID: id,
CONF_NAME: TEST_NAME_ORIGINAL,
CONF_MODEL: TEST_MODEL,
},
)
config_entry.add_to_hass(hass)
entity_entry = entity_registry.async_get_or_create(
LIGHT_DOMAIN,
DOMAIN,
config_entry.unique_id,
)
device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, config_entry.unique_id)},
)
with patch("homeassistant.components.twinkly.Twinkly", return_value=client):
await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.LOADED
assert entity_registry.async_get(entity_entry.entity_id).unique_id == TEST_MAC
assert device_registry.async_get_device(
identifiers={(DOMAIN, config_entry.unique_id)}
).identifiers == {(DOMAIN, TEST_MAC)}
assert config_entry.unique_id == TEST_MAC

View File

@ -15,7 +15,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.entity_registry import RegistryEntry
from . import TEST_MODEL, TEST_NAME, TEST_NAME_ORIGINAL, ClientMock
from . import TEST_MAC, TEST_MODEL, TEST_NAME, TEST_NAME_ORIGINAL, ClientMock
from tests.common import MockConfigEntry, async_fire_time_changed
@ -301,7 +301,7 @@ async def test_update_name(
async_fire_time_changed(hass)
await hass.async_block_till_done()
dev_entry = device_registry.async_get_device({(TWINKLY_DOMAIN, client.id)})
dev_entry = device_registry.async_get_device({(TWINKLY_DOMAIN, TEST_MAC)})
assert dev_entry.name == "new_device_name"
assert config_entry.data[CONF_NAME] == "new_device_name"
@ -310,10 +310,9 @@ async def test_update_name(
async def test_unload(hass: HomeAssistant) -> None:
"""Validate that entities can be unloaded from the UI."""
_, _, client, _ = await _create_entries(hass)
entry_id = client.id
_, _, _, entry = await _create_entries(hass)
assert await hass.config_entries.async_unload(entry_id)
assert await hass.config_entries.async_unload(entry.entry_id)
async def _create_entries(
@ -330,18 +329,19 @@ async def _create_entries(
CONF_NAME: TEST_NAME_ORIGINAL,
CONF_MODEL: TEST_MODEL,
},
entry_id=client.id,
unique_id=TEST_MAC,
minor_version=2,
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(client.id)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
entity_id = entity_registry.async_get_entity_id("light", TWINKLY_DOMAIN, client.id)
entity_id = entity_registry.async_get_entity_id("light", TWINKLY_DOMAIN, TEST_MAC)
entity_entry = entity_registry.async_get(entity_id)
device = device_registry.async_get_device(identifiers={(TWINKLY_DOMAIN, client.id)})
device = device_registry.async_get_device(identifiers={(TWINKLY_DOMAIN, TEST_MAC)})
assert entity_entry is not None
assert device is not None