Use switch format unique ids for tplink dimmers (#57346)

pull/57413/head
J. Nick Koston 2021-10-09 21:02:33 -10:00 committed by GitHub
parent a58085639e
commit 5b3711ed19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 163 additions and 4 deletions

View File

@ -11,6 +11,7 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import network
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.config_entries import ConfigEntry, ConfigEntryNotReady
from homeassistant.const import (
CONF_HOST,
@ -19,7 +20,11 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STARTED,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType
@ -158,12 +163,52 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except SmartDeviceException as ex:
raise ConfigEntryNotReady from ex
if device.is_dimmer:
async_fix_dimmer_unique_id(hass, entry, device)
hass.data[DOMAIN][entry.entry_id] = TPLinkDataUpdateCoordinator(hass, device)
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
@callback
def async_fix_dimmer_unique_id(
hass: HomeAssistant, entry: ConfigEntry, device: SmartDevice
) -> None:
"""Migrate the unique id of dimmers back to the legacy one.
Dimmers used to use the switch format since pyHS100 treated them as SmartPlug but
the old code created them as lights
https://github.com/home-assistant/core/blob/2021.9.7/homeassistant/components/tplink/common.py#L86
"""
# This is the unique id before 2021.0/2021.1
original_unique_id = legacy_device_id(device)
# This is the unique id that was used in 2021.0/2021.1 rollout
rollout_unique_id = device.mac.replace(":", "").upper()
entity_registry = er.async_get(hass)
rollout_entity_id = entity_registry.async_get_entity_id(
LIGHT_DOMAIN, DOMAIN, rollout_unique_id
)
original_entry_id = entity_registry.async_get_entity_id(
LIGHT_DOMAIN, DOMAIN, original_unique_id
)
# If they are now using the 2021.0/2021.1 rollout entity id
# and have deleted the original entity id, we want to update that entity id
# so they don't end up with another _2 entity, but only if they deleted
# the original
if rollout_entity_id and not original_entry_id:
entity_registry.async_update_entity(
rollout_entity_id, new_unique_id=original_unique_id
)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
hass_data: dict[str, Any] = hass.data[DOMAIN]

View File

@ -26,6 +26,7 @@ from homeassistant.util.color import (
color_temperature_mired_to_kelvin as mired_to_kelvin,
)
from . import legacy_device_id
from .const import DOMAIN
from .coordinator import TPLinkDataUpdateCoordinator
from .entity import CoordinatedTPLinkEntity, async_refresh_after
@ -58,7 +59,14 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
"""Initialize the switch."""
super().__init__(device, coordinator)
# For backwards compat with pyHS100
self._attr_unique_id = self.device.mac.replace(":", "").upper()
if self.device.is_dimmer:
# Dimmers used to use the switch format since
# pyHS100 treated them as SmartPlug but the old code
# created them as lights
# https://github.com/home-assistant/core/blob/2021.9.7/homeassistant/components/tplink/common.py#L86
self._attr_unique_id = legacy_device_id(device)
else:
self._attr_unique_id = self.device.mac.replace(":", "").upper()
@async_refresh_after
async def async_turn_on(self, **kwargs: Any) -> None:

View File

@ -2,7 +2,7 @@
from unittest.mock import AsyncMock, MagicMock, patch
from kasa import SmartBulb, SmartPlug, SmartStrip
from kasa import SmartBulb, SmartDimmer, SmartPlug, SmartStrip
from kasa.exceptions import SmartDeviceException
from kasa.protocol import TPLinkSmartHomeProtocol
@ -48,6 +48,33 @@ def _mocked_bulb() -> SmartBulb:
return bulb
def _mocked_dimmer() -> SmartDimmer:
dimmer = MagicMock(auto_spec=SmartDimmer)
dimmer.update = AsyncMock()
dimmer.mac = MAC_ADDRESS
dimmer.alias = ALIAS
dimmer.model = MODEL
dimmer.host = IP_ADDRESS
dimmer.brightness = 50
dimmer.color_temp = 4000
dimmer.is_color = True
dimmer.is_strip = False
dimmer.is_plug = False
dimmer.is_dimmer = True
dimmer.hsv = (10, 30, 5)
dimmer.device_id = MAC_ADDRESS
dimmer.valid_temperature_range.min = 4000
dimmer.valid_temperature_range.max = 9000
dimmer.hw_info = {"sw_ver": "1.0.0"}
dimmer.turn_off = AsyncMock()
dimmer.turn_on = AsyncMock()
dimmer.set_brightness = AsyncMock()
dimmer.set_hsv = AsyncMock()
dimmer.set_color_temp = AsyncMock()
dimmer.protocol = _mock_protocol()
return dimmer
def _mocked_plug() -> SmartPlug:
plug = MagicMock(auto_spec=SmartPlug)
plug.update = AsyncMock()

View File

@ -4,14 +4,23 @@ from __future__ import annotations
from datetime import timedelta
from unittest.mock import MagicMock, patch
from homeassistant import setup
from homeassistant.components import tplink
from homeassistant.components.tplink.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_registry import EntityRegistry
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from . import IP_ADDRESS, MAC_ADDRESS, _patch_discovery, _patch_single_discovery
from . import (
IP_ADDRESS,
MAC_ADDRESS,
_mocked_dimmer,
_patch_discovery,
_patch_single_discovery,
)
from tests.common import MockConfigEntry, async_fire_time_changed
@ -63,3 +72,73 @@ async def test_config_entry_retry(hass):
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
await hass.async_block_till_done()
assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY
async def test_dimmer_switch_unique_id_fix_original_entity_was_deleted(
hass: HomeAssistant, entity_reg: EntityRegistry
):
"""Test that roll out unique id entity id changed to the original unique id."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS)
config_entry.add_to_hass(hass)
dimmer = _mocked_dimmer()
rollout_unique_id = MAC_ADDRESS.replace(":", "").upper()
original_unique_id = tplink.legacy_device_id(dimmer)
rollout_dimmer_entity_reg = entity_reg.async_get_or_create(
config_entry=config_entry,
platform=DOMAIN,
domain="light",
unique_id=rollout_unique_id,
original_name="Rollout dimmer",
)
with _patch_discovery(device=dimmer), _patch_single_discovery(device=dimmer):
await setup.async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
migrated_dimmer_entity_reg = entity_reg.async_get_or_create(
config_entry=config_entry,
platform=DOMAIN,
domain="light",
unique_id=original_unique_id,
original_name="Migrated dimmer",
)
assert migrated_dimmer_entity_reg.entity_id == rollout_dimmer_entity_reg.entity_id
async def test_dimmer_switch_unique_id_fix_original_entity_still_exists(
hass: HomeAssistant, entity_reg: EntityRegistry
):
"""Test no migration happens if the original entity id still exists."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS)
config_entry.add_to_hass(hass)
dimmer = _mocked_dimmer()
rollout_unique_id = MAC_ADDRESS.replace(":", "").upper()
original_unique_id = tplink.legacy_device_id(dimmer)
original_dimmer_entity_reg = entity_reg.async_get_or_create(
config_entry=config_entry,
platform=DOMAIN,
domain="light",
unique_id=original_unique_id,
original_name="Original dimmer",
)
rollout_dimmer_entity_reg = entity_reg.async_get_or_create(
config_entry=config_entry,
platform=DOMAIN,
domain="light",
unique_id=rollout_unique_id,
original_name="Rollout dimmer",
)
with _patch_discovery(device=dimmer), _patch_single_discovery(device=dimmer):
await setup.async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
migrated_dimmer_entity_reg = entity_reg.async_get_or_create(
config_entry=config_entry,
platform=DOMAIN,
domain="light",
unique_id=original_unique_id,
original_name="Migrated dimmer",
)
assert migrated_dimmer_entity_reg.entity_id == original_dimmer_entity_reg.entity_id
assert migrated_dimmer_entity_reg.entity_id != rollout_dimmer_entity_reg.entity_id