Skip to content

Commit

Permalink
Deprecate tplink alarm button entities
Browse files Browse the repository at this point in the history
  • Loading branch information
sdb9696 committed Sep 20, 2024
1 parent e8d5ebe commit ad0dcbc
Show file tree
Hide file tree
Showing 12 changed files with 286 additions and 22 deletions.
1 change: 1 addition & 0 deletions homeassistant/components/tplink/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ async def async_setup_entry(
device = parent_coordinator.device

entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
hass=hass,
device=device,
coordinator=parent_coordinator,
feature_type=Feature.Type.BinarySensor,
Expand Down
19 changes: 18 additions & 1 deletion homeassistant/components/tplink/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@

from kasa import Feature

from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.components.button import (
DOMAIN as BUTTON_DOMAIN,
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import TPLinkConfigEntry
from .deprecate import DeprecatedInfo
from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription


Expand All @@ -25,9 +31,19 @@ class TPLinkButtonEntityDescription(
BUTTON_DESCRIPTIONS: Final = [
TPLinkButtonEntityDescription(
key="test_alarm",
deprecated_info=DeprecatedInfo(
platform=BUTTON_DOMAIN,
new_platform=SIREN_DOMAIN,
breaks_in_ha_version="2025.4.0",
),
),
TPLinkButtonEntityDescription(
key="stop_alarm",
deprecated_info=DeprecatedInfo(
platform=BUTTON_DOMAIN,
new_platform=SIREN_DOMAIN,
breaks_in_ha_version="2025.4.0",
),
),
]

Expand All @@ -46,6 +62,7 @@ async def async_setup_entry(
device = parent_coordinator.device

entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
hass=hass,
device=device,
coordinator=parent_coordinator,
feature_type=Feature.Type.Action,
Expand Down
85 changes: 85 additions & 0 deletions homeassistant/components/tplink/deprecate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Helper class for deprecating entities."""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING

from homeassistant.components.automation import automations_with_entity
from homeassistant.components.script import scripts_with_entity
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue

from .const import DOMAIN

if TYPE_CHECKING:
from .entity import TPLinkFeatureEntityDescription


@dataclass(slots=True)
class DeprecatedInfo:
"""Class to define deprecation info for deprecated entities."""

platform: str
new_platform: str
breaks_in_ha_version: str


def async_check_create_deprecated(
hass: HomeAssistant,
unique_id: str,
entity_description: TPLinkFeatureEntityDescription,
) -> bool:
"""Return true if the entity should be created based on the deprecated_info.
If deprecated_info is not defined will return true.
If entity not yet created will return false.
If entity disabled will delete it and return false.
Otherwise will return true and create issues for scripts or automations.
"""
if not entity_description.deprecated_info:
return True

deprecated_info = entity_description.deprecated_info
platform = deprecated_info.platform

ent_reg = er.async_get(hass)
entity_id = ent_reg.async_get_entity_id(
platform,
DOMAIN,
unique_id,
)
if not entity_id:
return False

Check warning on line 54 in homeassistant/components/tplink/deprecate.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/tplink/deprecate.py#L54

Added line #L54 was not covered by tests

entity_entry = ent_reg.async_get(entity_id)
assert entity_entry
if entity_entry.disabled:
# If the entity exists and is disabled then we want to remove
# the entity so that the user is just using the new entity.
ent_reg.async_remove(entity_id)
return False

# Check for issues that need to be created
entity_automations = automations_with_entity(hass, entity_id)
entity_scripts = scripts_with_entity(hass, entity_id)

for item in entity_automations + entity_scripts:
async_create_issue(
hass,
DOMAIN,
f"deprecated_entity_{entity_id}_{item}",
breaks_in_ha_version=deprecated_info.breaks_in_ha_version,
is_fixable=False,
is_persistent=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_entity",
translation_placeholders={
"entity": entity_id,
"info": item,
"platform": platform,
"new_platform": deprecated_info.new_platform,
},
)
return True
27 changes: 23 additions & 4 deletions homeassistant/components/tplink/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
)

from homeassistant.const import EntityCategory
from homeassistant.core import callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
Expand All @@ -36,6 +36,7 @@
PRIMARY_STATE_ID,
)
from .coordinator import TPLinkDataUpdateCoordinator
from .deprecate import DeprecatedInfo, async_check_create_deprecated

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -87,6 +88,8 @@
class TPLinkFeatureEntityDescription(EntityDescription):
"""Base class for a TPLink feature based entity description."""

deprecated_info: DeprecatedInfo | None = None


def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P](
func: Callable[Concatenate[_T, _P], Awaitable[None]],
Expand Down Expand Up @@ -251,18 +254,25 @@ def __init__(

def _get_unique_id(self) -> str:
"""Return unique ID for the entity."""
key = self.entity_description.key
return self._get_feature_unique_id(self._device, self.entity_description)

@staticmethod
def _get_feature_unique_id(
device: Device, entity_description: TPLinkFeatureEntityDescription
) -> str:
"""Return unique ID for the entity."""
key = entity_description.key
# The unique id for the state feature in the switch platform is the
# device_id
if key == PRIMARY_STATE_ID:
return legacy_device_id(self._device)
return legacy_device_id(device)

# Historically the legacy device emeter attributes which are now
# replaced with features used slightly different keys. This ensures
# that those entities are not orphaned. Returns the mapped key or the
# provided key if not mapped.
key = LEGACY_KEY_MAPPING.get(key, key)
return f"{legacy_device_id(self._device)}_{key}"
return f"{legacy_device_id(device)}_{key}"

@classmethod
def _category_for_feature(cls, feature: Feature | None) -> EntityCategory | None:
Expand Down Expand Up @@ -334,6 +344,7 @@ def _entities_for_device[
_D: TPLinkFeatureEntityDescription,
](
cls,
hass: HomeAssistant,
device: Device,
coordinator: TPLinkDataUpdateCoordinator,
*,
Expand Down Expand Up @@ -368,6 +379,11 @@ def _entities_for_device[
feat, descriptions, device=device, parent=parent
)
)
and async_check_create_deprecated(
hass,
cls._get_feature_unique_id(device, desc),
desc,
)
]
return entities

Expand All @@ -377,6 +393,7 @@ def entities_for_device_and_its_children[
_D: TPLinkFeatureEntityDescription,
](
cls,
hass: HomeAssistant,
device: Device,
coordinator: TPLinkDataUpdateCoordinator,
*,
Expand All @@ -393,6 +410,7 @@ def entities_for_device_and_its_children[
# Add parent entities before children so via_device id works.
entities.extend(
cls._entities_for_device(
hass,
device,
coordinator=coordinator,
feature_type=feature_type,
Expand All @@ -412,6 +430,7 @@ def entities_for_device_and_its_children[
child_coordinator = coordinator
entities.extend(
cls._entities_for_device(
hass,
child,
coordinator=child_coordinator,
feature_type=feature_type,
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/tplink/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

from dataclasses import dataclass
import logging
from typing import Final

Expand All @@ -26,6 +27,7 @@
_LOGGER = logging.getLogger(__name__)


@dataclass(frozen=True, kw_only=True)
class TPLinkNumberEntityDescription(
NumberEntityDescription, TPLinkFeatureEntityDescription
):
Expand Down Expand Up @@ -65,6 +67,7 @@ async def async_setup_entry(
children_coordinators = data.children_coordinators
device = parent_coordinator.device
entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
hass=hass,
device=device,
coordinator=parent_coordinator,
feature_type=Feature.Type.Number,
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/tplink/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ async def async_setup_entry(
device = parent_coordinator.device

entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
hass=hass,
device=device,
coordinator=parent_coordinator,
feature_type=Feature.Type.Choice,
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/tplink/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ async def async_setup_entry(
device = parent_coordinator.device

entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
hass=hass,
device=device,
coordinator=parent_coordinator,
feature_type=Feature.Type.Sensor,
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/tplink/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -311,5 +311,11 @@
"device_authentication": {
"message": "Device authentication error {func}: {exc}"
}
},
"issues": {
"deprecated_entity": {
"title": "Detected deprecated `{platform}` entity usage",
"description": "We detected that entity `{entity}` is being used in `{info}`\n\nWe have created a new `{new_platform}` entity and you should migrate `{info}` to use this new entity.\n\nWhen you are done migrating `{info}` and are ready to have the deprecated `{entity}` entity removed, disable the entity and restart Home Assistant."
}
}
}
3 changes: 2 additions & 1 deletion homeassistant/components/tplink/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ async def async_setup_entry(
device = parent_coordinator.device

entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children(
device,
hass=hass,
device=device,
coordinator=parent_coordinator,
feature_type=Feature.Switch,
entity_class=TPLinkSwitch,
Expand Down
16 changes: 16 additions & 0 deletions tests/components/tplink/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from kasa.smart.modules.alarm import Alarm
from syrupy import SnapshotAssertion

from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN
from homeassistant.components.tplink import (
CONF_AES_KEYS,
CONF_ALIAS,
Expand Down Expand Up @@ -184,6 +185,21 @@ async def snapshot_platform(
), f"state snapshot failed for {entity_entry.entity_id}"


async def setup_automation(hass: HomeAssistant, alias: str, entity_id: str) -> None:
"""Set up an automation for tests."""
assert await async_setup_component(
hass,
AUTOMATION_DOMAIN,
{
AUTOMATION_DOMAIN: {
"alias": alias,
"trigger": {"platform": "state", "entity_id": entity_id, "to": "on"},
"action": {"action": "notify.notify", "metadata": {}, "data": {}},
}
},
)


def _mock_protocol() -> BaseProtocol:
protocol = MagicMock(spec=BaseProtocol)
protocol.close = AsyncMock()
Expand Down
Loading

0 comments on commit ad0dcbc

Please sign in to comment.