Improve SmartThings deprecation (#141939)

* Improve SmartThings deprecation

* Improve SmartThings deprecation
This commit is contained in:
Joost Lekkerkerker 2025-04-01 19:36:14 +02:00 committed by GitHub
parent faac51d219
commit 4bfc96c02b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 273 additions and 237 deletions

View File

@ -7,26 +7,21 @@ from dataclasses import dataclass
from pysmartthings import Attribute, Capability, Category, SmartThings, Status
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.components.script import scripts_with_entity
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from . import FullDevice, SmartThingsConfigEntry
from .const import DOMAIN, MAIN
from .const import MAIN
from .entity import SmartThingsEntity
from .util import deprecate_entity
@dataclass(frozen=True, kw_only=True)
@ -192,24 +187,64 @@ async def async_setup_entry(
) -> None:
"""Add binary sensors for a config entry."""
entry_data = entry.runtime_data
async_add_entities(
SmartThingsBinarySensor(
entry_data.client, device, description, capability, attribute, component
)
for device in entry_data.devices.values()
for capability, attribute_map in CAPABILITY_TO_SENSORS.items()
for attribute, description in attribute_map.items()
for component in device.status
if capability in device.status[component]
and (
component == MAIN
or (description.exists_fn is not None and description.exists_fn(component))
)
and (
not description.category
or get_main_component_category(device) in description.category
)
)
entities = []
entity_registry = er.async_get(hass)
for device in entry_data.devices.values(): # pylint: disable=too-many-nested-blocks
for capability, attribute_map in CAPABILITY_TO_SENSORS.items():
for attribute, description in attribute_map.items():
for component in device.status:
if (
capability in device.status[component]
and (
component == MAIN
or (
description.exists_fn is not None
and description.exists_fn(component)
)
)
and (
not description.category
or get_main_component_category(device)
in description.category
)
):
if (
component == MAIN
and (issue := description.deprecated_fn(device.status))
is not None
):
if deprecate_entity(
hass,
entity_registry,
BINARY_SENSOR_DOMAIN,
f"{device.device.device_id}_{component}_{capability}_{attribute}_{attribute}",
f"deprecated_binary_{issue}",
):
entities.append(
SmartThingsBinarySensor(
entry_data.client,
device,
description,
capability,
attribute,
component,
)
)
continue
entities.append(
SmartThingsBinarySensor(
entry_data.client,
device,
description,
capability,
attribute,
component,
)
)
async_add_entities(entities)
class SmartThingsBinarySensor(SmartThingsEntity, BinarySensorEntity):
@ -257,57 +292,3 @@ class SmartThingsBinarySensor(SmartThingsEntity, BinarySensorEntity):
self.get_attribute_value(self.capability, self._attribute)
== self.entity_description.is_on_key
)
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
await super().async_added_to_hass()
if (issue := self.entity_description.deprecated_fn(self.device.status)) is None:
return
automations = automations_with_entity(self.hass, self.entity_id)
scripts = scripts_with_entity(self.hass, self.entity_id)
items = automations + scripts
if not items:
return
entity_reg: er.EntityRegistry = er.async_get(self.hass)
entity_automations = [
automation_entity
for automation_id in automations
if (automation_entity := entity_reg.async_get(automation_id))
]
entity_scripts = [
script_entity
for script_id in scripts
if (script_entity := entity_reg.async_get(script_id))
]
items_list = [
f"- [{item.original_name}](/config/automation/edit/{item.unique_id})"
for item in entity_automations
] + [
f"- [{item.original_name}](/config/script/edit/{item.unique_id})"
for item in entity_scripts
]
async_create_issue(
self.hass,
DOMAIN,
f"deprecated_binary_{issue}_{self.entity_id}",
breaks_in_ha_version="2025.10.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key=f"deprecated_binary_{issue}",
translation_placeholders={
"entity": self.entity_id,
"items": "\n".join(items_list),
},
)
async def async_will_remove_from_hass(self) -> None:
"""Call when entity will be removed from hass."""
await super().async_will_remove_from_hass()
if (issue := self.entity_description.deprecated_fn(self.device.status)) is None:
return
async_delete_issue(
self.hass, DOMAIN, f"deprecated_binary_{issue}_{self.entity_id}"
)

View File

@ -480,12 +480,20 @@
},
"issues": {
"deprecated_binary_valve": {
"title": "Deprecated valve binary sensor detected in some automations or scripts",
"description": "The valve binary sensor `{entity}` is deprecated and is used in the following automations or scripts:\n{items}\n\nA valve entity with controls is available and should be used going forward. Please use the new valve entity in the above automations or scripts to fix this issue."
"title": "Valve binary sensor deprecated",
"description": "The valve binary sensor {entity_name} (`{entity_id}`) is deprecated and will be removed in the future. A valve entity with controls is available and should be used going forward. Please update your dashboards, templates accordingly and disable the entity to fix this issue."
},
"deprecated_binary_valve_scripts": {
"title": "[%key:component::smartthings::issues::deprecated_binary_valve::title%]",
"description": "The valve binary sensor {entity_name} (`{entity_id}`) is deprecated and will be removed in the future. The entity is used in the following automations or scripts:\n{items}\n\nA valve entity with controls is available and should be used going forward. Please use the new valve entity in the above automations or scripts and disable the entity to fix this issue."
},
"deprecated_binary_fridge_door": {
"title": "Deprecated refrigerator door binary sensor detected in some automations or scripts",
"description": "The refrigerator door binary sensor `{entity}` is deprecated and is used in the following automations or scripts:\n{items}\n\nSeparate entities for cooler and freezer door are available and should be used going forward. Please use them in the above automations or scripts to fix this issue."
"title": "Refrigerator door binary sensor deprecated",
"description": "The refrigerator door binary sensor {entity_name} (`{entity_id}`) is deprecated and will be removed in the future. Separate entities for cooler and freezer door are available and should be used going forward. Please update your dashboards, templates accordingly and disable the entity to fix this issue."
},
"deprecated_binary_fridge_door_scripts": {
"title": "[%key:component::smartthings::issues::deprecated_binary_fridge_door::title%]",
"description": "The refrigerator door binary sensor {entity_name} (`{entity_id}`) is deprecated and will be removed in the future. The entity is used in the following automations or scripts:\n{items}\n\nSeparate entities for cooler and freezer door are available and should be used going forward. Please use them in the above automations or scripts and disable the entity to fix this issue."
},
"deprecated_switch_appliance": {
"title": "Deprecated switch detected in some automations or scripts",

View File

@ -0,0 +1,83 @@
"""Utility functions for SmartThings integration."""
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,
async_delete_issue,
)
from .const import DOMAIN
def deprecate_entity(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
platform_domain: str,
entity_unique_id: str,
issue_string: str,
) -> bool:
"""Create an issue for deprecated entities."""
if entity_id := entity_registry.async_get_entity_id(
platform_domain, DOMAIN, entity_unique_id
):
entity_entry = entity_registry.async_get(entity_id)
if not entity_entry:
return False
if entity_entry.disabled:
entity_registry.async_remove(entity_id)
async_delete_issue(
hass,
DOMAIN,
f"{issue_string}_{entity_id}",
)
return False
translation_key = issue_string
placeholders = {
"entity_id": entity_id,
"entity_name": entity_entry.name or entity_entry.original_name or "Unknown",
}
if items := get_automations_and_scripts_using_entity(hass, entity_id):
translation_key = f"{translation_key}_scripts"
placeholders.update(
{
"items": "\n".join(items),
}
)
async_create_issue(
hass,
DOMAIN,
f"{issue_string}_{entity_id}",
breaks_in_ha_version="2025.10.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key=translation_key,
translation_placeholders=placeholders,
)
return True
return False
def get_automations_and_scripts_using_entity(
hass: HomeAssistant,
entity_id: str,
) -> list[str]:
"""Get automations and scripts using an entity."""
automations = automations_with_entity(hass, entity_id)
scripts = scripts_with_entity(hass, entity_id)
if not automations and not scripts:
return []
entity_reg = er.async_get(hass)
return [
f"- [{item.original_name}](/config/{integration}/edit/{item.unique_id})"
for integration, entities in (
("automation", automations),
("script", scripts),
)
for entity_id in entities
if (item := entity_reg.async_get(entity_id))
]

View File

@ -713,54 +713,6 @@
'state': 'off',
})
# ---
# name: test_all_entities[da_ref_normal_000001][binary_sensor.refrigerator_door-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.refrigerator_door',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
'original_icon': None,
'original_name': 'Door',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_main_contactSensor_contact_contact',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ref_normal_000001][binary_sensor.refrigerator_door-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'door',
'friendly_name': 'Refrigerator Door',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.refrigerator_door',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[da_ref_normal_000001][binary_sensor.refrigerator_freezer_door-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@ -857,54 +809,6 @@
'state': 'off',
})
# ---
# name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_door-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.frigo_door',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
'original_icon': None,
'original_name': 'Door',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_main_contactSensor_contact_contact',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_door-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'door',
'friendly_name': 'Frigo Door',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.frigo_door',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_freezer_door-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@ -2139,54 +2043,6 @@
'state': 'off',
})
# ---
# name: test_all_entities[virtual_valve][binary_sensor.volvo_valve-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.volvo_valve',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.OPENING: 'opening'>,
'original_icon': None,
'original_name': 'Valve',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'valve',
'unique_id': '612ab3c2-3bb0-48f7-b2c0-15b169cb2fc3_main_valve_valve_valve',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[virtual_valve][binary_sensor.volvo_valve-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'opening',
'friendly_name': 'volvo Valve',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.volvo_valve',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[virtual_water_sensor][binary_sensor.asd_moisture-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -8,8 +8,9 @@ from syrupy import SnapshotAssertion
from homeassistant.components import automation, script
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.script import scripts_with_entity
from homeassistant.components.smartthings import DOMAIN
from homeassistant.components.smartthings import DOMAIN, MAIN
from homeassistant.const import STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er, issue_registry as ir
@ -44,7 +45,7 @@ async def test_state_update(
"""Test state update."""
await setup_integration(hass, mock_config_entry)
assert hass.states.get("binary_sensor.refrigerator_door").state == STATE_OFF
assert hass.states.get("binary_sensor.refrigerator_cooler_door").state == STATE_OFF
await trigger_update(
hass,
@ -53,35 +54,60 @@ async def test_state_update(
Capability.CONTACT_SENSOR,
Attribute.CONTACT,
"open",
component="cooler",
)
assert hass.states.get("binary_sensor.refrigerator_door").state == STATE_ON
assert hass.states.get("binary_sensor.refrigerator_cooler_door").state == STATE_ON
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize(
("device_fixture", "issue_string", "entity_id"),
("device_fixture", "unique_id", "suggested_object_id", "issue_string", "entity_id"),
[
("virtual_valve", "valve", "binary_sensor.volvo_valve"),
("da_ref_normal_000001", "fridge_door", "binary_sensor.refrigerator_door"),
(
"virtual_valve",
f"612ab3c2-3bb0-48f7-b2c0-15b169cb2fc3_{MAIN}_{Capability.VALVE}_{Attribute.VALVE}_{Attribute.VALVE}",
"volvo_valve",
"valve",
"binary_sensor.volvo_valve",
),
(
"da_ref_normal_000001",
f"7db87911-7dce-1cf2-7119-b953432a2f09_{MAIN}_{Capability.CONTACT_SENSOR}_{Attribute.CONTACT}_{Attribute.CONTACT}",
"refrigerator_door",
"fridge_door",
"binary_sensor.refrigerator_door",
),
],
)
async def test_create_issue(
async def test_create_issue_with_items(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
issue_registry: ir.IssueRegistry,
unique_id: str,
suggested_object_id: str,
issue_string: str,
entity_id: str,
) -> None:
"""Test we create an issue when an automation or script is using a deprecated entity."""
issue_id = f"deprecated_binary_{issue_string}_{entity_id}"
entity_entry = entity_registry.async_get_or_create(
BINARY_SENSOR_DOMAIN,
DOMAIN,
unique_id,
suggested_object_id=suggested_object_id,
original_name=suggested_object_id,
)
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"id": "test",
"alias": "test",
"trigger": {"platform": "state", "entity_id": entity_id},
"action": {
@ -113,13 +139,95 @@ async def test_create_issue(
await setup_integration(hass, mock_config_entry)
assert hass.states.get(entity_id).state == STATE_OFF
assert automations_with_entity(hass, entity_id)[0] == "automation.test"
assert scripts_with_entity(hass, entity_id)[0] == "script.test"
assert len(issue_registry.issues) == 1
assert issue_registry.async_get_issue(DOMAIN, issue_id)
issue = issue_registry.async_get_issue(DOMAIN, issue_id)
assert issue is not None
assert issue.translation_key == f"deprecated_binary_{issue_string}_scripts"
assert issue.translation_placeholders == {
"entity_id": entity_id,
"entity_name": suggested_object_id,
"items": "- [test](/config/automation/edit/test)\n- [test](/config/script/edit/test)",
}
await hass.config_entries.async_unload(mock_config_entry.entry_id)
entity_registry.async_update_entity(
entity_entry.entity_id,
disabled_by=er.RegistryEntryDisabler.USER,
)
await hass.config_entries.async_reload(mock_config_entry.entry_id)
await hass.async_block_till_done()
# Assert the issue is no longer present
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
assert len(issue_registry.issues) == 0
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize(
("device_fixture", "unique_id", "suggested_object_id", "issue_string", "entity_id"),
[
(
"virtual_valve",
f"612ab3c2-3bb0-48f7-b2c0-15b169cb2fc3_{MAIN}_{Capability.VALVE}_{Attribute.VALVE}_{Attribute.VALVE}",
"volvo_valve",
"valve",
"binary_sensor.volvo_valve",
),
(
"da_ref_normal_000001",
f"7db87911-7dce-1cf2-7119-b953432a2f09_{MAIN}_{Capability.CONTACT_SENSOR}_{Attribute.CONTACT}_{Attribute.CONTACT}",
"refrigerator_door",
"fridge_door",
"binary_sensor.refrigerator_door",
),
],
)
async def test_create_issue(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
issue_registry: ir.IssueRegistry,
unique_id: str,
suggested_object_id: str,
issue_string: str,
entity_id: str,
) -> None:
"""Test we create an issue when an automation or script is using a deprecated entity."""
issue_id = f"deprecated_binary_{issue_string}_{entity_id}"
entity_entry = entity_registry.async_get_or_create(
BINARY_SENSOR_DOMAIN,
DOMAIN,
unique_id,
suggested_object_id=suggested_object_id,
original_name=suggested_object_id,
)
await setup_integration(hass, mock_config_entry)
assert hass.states.get(entity_id).state == STATE_OFF
assert len(issue_registry.issues) == 1
issue = issue_registry.async_get_issue(DOMAIN, issue_id)
assert issue is not None
assert issue.translation_key == f"deprecated_binary_{issue_string}"
assert issue.translation_placeholders == {
"entity_id": entity_id,
"entity_name": suggested_object_id,
}
entity_registry.async_update_entity(
entity_entry.entity_id,
disabled_by=er.RegistryEntryDisabler.USER,
)
await hass.config_entries.async_reload(mock_config_entry.entry_id)
await hass.async_block_till_done()
# Assert the issue is no longer present