diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index b42ffa68dc9..d6fc85b2045 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -17,16 +17,26 @@ from habiticalib import ( deserialize_task, ) +from homeassistant.components.automation import automations_with_entity +from homeassistant.components.script import scripts_with_entity from homeassistant.components.sensor import ( + DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.helpers.typing import StateType -from .const import ASSETS_URL +from .const import ASSETS_URL, DOMAIN +from .coordinator import HabiticaDataUpdateCoordinator from .entity import HabiticaBase from .types import HabiticaConfigEntry from .util import get_attribute_points, get_attributes_total, inventory_list @@ -269,6 +279,13 @@ TASK_SENSOR_DESCRIPTION: tuple[HabiticaTaskSensorEntityDescription, ...] = ( ) +def entity_used_in(hass: HomeAssistant, entity_id: str) -> list[str]: + """Get list of related automations and scripts.""" + used_in = automations_with_entity(hass, entity_id) + used_in += scripts_with_entity(hass, entity_id) + return used_in + + async def async_setup_entry( hass: HomeAssistant, config_entry: HabiticaConfigEntry, @@ -277,14 +294,58 @@ async def async_setup_entry( """Set up the habitica sensors.""" coordinator = config_entry.runtime_data + ent_reg = er.async_get(hass) + entities: list[SensorEntity] = [] + description: SensorEntityDescription + + def add_deprecated_entity( + description: SensorEntityDescription, + entity_cls: Callable[ + [HabiticaDataUpdateCoordinator, SensorEntityDescription], SensorEntity + ], + ) -> None: + """Add deprecated entities.""" + if entity_id := ent_reg.async_get_entity_id( + SENSOR_DOMAIN, + DOMAIN, + f"{config_entry.unique_id}_{description.key}", + ): + entity_entry = ent_reg.async_get(entity_id) + if entity_entry and entity_entry.disabled: + ent_reg.async_remove(entity_id) + async_delete_issue( + hass, + DOMAIN, + f"deprecated_entity_{description.key}", + ) + elif entity_entry: + entities.append(entity_cls(coordinator, description)) + if entity_used_in(hass, entity_id): + async_create_issue( + hass, + DOMAIN, + f"deprecated_entity_{description.key}", + breaks_in_ha_version="2025.8.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_entity", + translation_placeholders={ + "name": str( + entity_entry.name or entity_entry.original_name + ), + "entity": entity_id, + }, + ) + + for description in SENSOR_DESCRIPTIONS: + if description.key is HabiticaSensorEntity.HEALTH_MAX: + add_deprecated_entity(description, HabiticaSensor) + else: + entities.append(HabiticaSensor(coordinator, description)) + + for description in TASK_SENSOR_DESCRIPTION: + add_deprecated_entity(description, HabiticaTaskSensor) - entities: list[SensorEntity] = [ - HabiticaSensor(coordinator, description) for description in SENSOR_DESCRIPTIONS - ] - entities.extend( - HabiticaTaskSensor(coordinator, description) - for description in TASK_SENSOR_DESCRIPTION - ) async_add_entities(entities, True) diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index 3e29b9110be..4e1a0ac9f64 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -421,6 +421,10 @@ } }, "issues": { + "deprecated_entity": { + "title": "The Habitica {name} entity is deprecated", + "description": "The Habitica entity `{entity}` is deprecated and will be removed in a future release.\nPlease update your automations and scripts, disable `{entity}` and reload the integration/restart Home Assistant to fix this issue." + }, "deprecated_api_call": { "title": "The Habitica action habitica.api_call is deprecated", "description": "The Habitica action `habitica.api_call` is deprecated and will be removed in Home Assistant 2025.5.0.\n\nPlease update your automations and scripts to use other Habitica actions and entities." diff --git a/homeassistant/components/habitica/util.py b/homeassistant/components/habitica/util.py index d265d56021a..757c675b045 100644 --- a/homeassistant/components/habitica/util.py +++ b/homeassistant/components/habitica/util.py @@ -23,9 +23,6 @@ from dateutil.rrule import ( ) from habiticalib import ContentData, Frequency, TaskData, UserData -from homeassistant.components.automation import automations_with_entity -from homeassistant.components.script import scripts_with_entity -from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -59,13 +56,6 @@ def next_due_date(task: TaskData, today: datetime.datetime) -> datetime.date | N return dt_util.as_local(task.nextDue[0]).date() -def entity_used_in(hass: HomeAssistant, entity_id: str) -> list[str]: - """Get list of related automations and scripts.""" - used_in = automations_with_entity(hass, entity_id) - used_in += scripts_with_entity(hass, entity_id) - return used_in - - FREQUENCY_MAP = {"daily": DAILY, "weekly": WEEKLY, "monthly": MONTHLY, "yearly": YEARLY} WEEKDAY_MAP = {"m": MO, "t": TU, "w": WE, "th": TH, "f": FR, "s": SA, "su": SU} diff --git a/tests/components/habitica/test_sensor.py b/tests/components/habitica/test_sensor.py index 9dde266d214..1c648e38720 100644 --- a/tests/components/habitica/test_sensor.py +++ b/tests/components/habitica/test_sensor.py @@ -6,10 +6,13 @@ from unittest.mock import patch import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.habitica.const import DOMAIN +from homeassistant.components.habitica.sensor import HabiticaSensorEntity +from homeassistant.components.sensor.const import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import entity_registry as er, issue_registry as ir from tests.common import MockConfigEntry, snapshot_platform @@ -33,6 +36,19 @@ async def test_sensors( ) -> None: """Test setup of the Habitica sensor platform.""" + for entity in ( + ("test_user_habits", "habits"), + ("test_user_rewards", "rewards"), + ("test_user_max_health", "health_max"), + ): + entity_registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + f"a380546a-94be-4b8e-8a0b-23e0d5c03303_{entity[1]}", + suggested_object_id=entity[0], + disabled_by=None, + ) + config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -40,3 +56,96 @@ async def test_sensors( assert config_entry.state is ConfigEntryState.LOADED await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.parametrize( + ("entity_id", "key"), + [ + ("test_user_habits", HabiticaSensorEntity.HABITS), + ("test_user_rewards", HabiticaSensorEntity.REWARDS), + ("test_user_max_health", HabiticaSensorEntity.HEALTH_MAX), + ], +) +@pytest.mark.usefixtures("habitica", "entity_registry_enabled_by_default") +async def test_sensor_deprecation_issue( + hass: HomeAssistant, + config_entry: MockConfigEntry, + issue_registry: ir.IssueRegistry, + entity_registry: er.EntityRegistry, + entity_id: str, + key: HabiticaSensorEntity, +) -> None: + """Test sensor deprecation issue.""" + entity_registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + f"a380546a-94be-4b8e-8a0b-23e0d5c03303_{key}", + suggested_object_id=entity_id, + disabled_by=None, + ) + + assert entity_registry is not None + with patch( + "homeassistant.components.habitica.sensor.entity_used_in", return_value=True + ): + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + assert entity_registry.async_get(f"sensor.{entity_id}") is not None + assert issue_registry.async_get_issue( + domain=DOMAIN, + issue_id=f"deprecated_entity_{key}", + ) + + +@pytest.mark.parametrize( + ("entity_id", "key"), + [ + ("test_user_habits", HabiticaSensorEntity.HABITS), + ("test_user_rewards", HabiticaSensorEntity.REWARDS), + ("test_user_max_health", HabiticaSensorEntity.HEALTH_MAX), + ], +) +@pytest.mark.usefixtures("habitica", "entity_registry_enabled_by_default") +async def test_sensor_deprecation_delete_disabled( + hass: HomeAssistant, + config_entry: MockConfigEntry, + issue_registry: ir.IssueRegistry, + entity_registry: er.EntityRegistry, + entity_id: str, + key: HabiticaSensorEntity, +) -> None: + """Test sensor deletion .""" + + entity_registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + f"a380546a-94be-4b8e-8a0b-23e0d5c03303_{key}", + suggested_object_id=entity_id, + disabled_by=er.RegistryEntryDisabler.USER, + ) + + assert entity_registry is not None + with patch( + "homeassistant.components.habitica.sensor.entity_used_in", return_value=True + ): + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + assert ( + issue_registry.async_get_issue( + domain=DOMAIN, + issue_id=f"deprecated_entity_{key}", + ) + is None + ) + + assert entity_registry.async_get(f"sensor.{entity_id}") is None