From b8e3d57fead662aff58516f436820659ef5fa287 Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Tue, 18 Nov 2025 11:09:38 -0500 Subject: [PATCH] Deprecate useless sensors in APCUPSD integration (#151525) --- homeassistant/components/apcupsd/const.py | 23 ++ homeassistant/components/apcupsd/sensor.py | 65 ++++- homeassistant/components/apcupsd/strings.json | 14 + .../apcupsd/snapshots/test_sensor.ambr | 240 ++++++++++++++++++ tests/components/apcupsd/test_sensor.py | 82 +++++- 5 files changed, 421 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apcupsd/const.py b/homeassistant/components/apcupsd/const.py index 974c860afb8..883e0246cdf 100644 --- a/homeassistant/components/apcupsd/const.py +++ b/homeassistant/components/apcupsd/const.py @@ -7,3 +7,26 @@ CONNECTION_TIMEOUT: int = 10 # Field name of last self test retrieved from apcupsd. LAST_S_TEST: Final = "laststest" + +# Mapping of deprecated sensor keys (as reported by apcupsd, lower-cased) to their deprecation +# repair issue translation keys. +DEPRECATED_SENSORS: Final = { + "apc": "apc_deprecated", + "end apc": "date_deprecated", + "date": "date_deprecated", + "apcmodel": "available_via_device_info", + "model": "available_via_device_info", + "firmware": "available_via_device_info", + "version": "available_via_device_info", + "upsname": "available_via_device_info", + "serialno": "available_via_device_info", +} + +AVAILABLE_VIA_DEVICE_ATTR: Final = { + "apcmodel": "model", + "model": "model", + "firmware": "hw_version", + "version": "sw_version", + "upsname": "name", + "serialno": "serial_number", +} diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 3a18bea1a8a..7fde54194e0 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -4,6 +4,8 @@ from __future__ import annotations import logging +from homeassistant.components.automation import automations_with_entity +from homeassistant.components.script import scripts_with_entity from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -22,9 +24,11 @@ from homeassistant.const import ( UnitOfTime, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +import homeassistant.helpers.issue_registry as ir -from .const import LAST_S_TEST +from .const import AVAILABLE_VIA_DEVICE_ATTR, DEPRECATED_SENSORS, DOMAIN, LAST_S_TEST from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator from .entity import APCUPSdEntity @@ -528,3 +532,62 @@ class APCUPSdSensor(APCUPSdEntity, SensorEntity): self._attr_native_value, inferred_unit = infer_unit(self.coordinator.data[key]) if not self.native_unit_of_measurement: self._attr_native_unit_of_measurement = inferred_unit + + async def async_added_to_hass(self) -> None: + """Handle when entity is added to Home Assistant. + + If this is a deprecated sensor entity, create a repair issue to guide + the user to disable it. + """ + await super().async_added_to_hass() + + if not self.enabled: + return + + reason = DEPRECATED_SENSORS.get(self.entity_description.key) + if not reason: + return + + automations = automations_with_entity(self.hass, self.entity_id) + scripts = scripts_with_entity(self.hass, self.entity_id) + if not automations and not scripts: + return + + entity_registry = er.async_get(self.hass) + items = [ + f"- [{entry.name or entry.original_name or entity_id}]" + f"(/config/{integration}/edit/{entry.unique_id or entity_id.split('.', 1)[-1]})" + for integration, entities in ( + ("automation", automations), + ("script", scripts), + ) + for entity_id in entities + if (entry := entity_registry.async_get(entity_id)) + ] + placeholders = { + "entity_name": str(self.name or self.entity_id), + "entity_id": self.entity_id, + "items": "\n".join(items), + } + if via_attr := AVAILABLE_VIA_DEVICE_ATTR.get(self.entity_description.key): + placeholders["available_via_device_attr"] = via_attr + if device_entry := self.device_entry: + placeholders["device_id"] = device_entry.id + + ir.async_create_issue( + self.hass, + DOMAIN, + f"{reason}_{self.entity_id}", + breaks_in_ha_version="2026.6.0", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key=reason, + translation_placeholders=placeholders, + ) + + async def async_will_remove_from_hass(self) -> None: + """Handle when entity will be removed from Home Assistant.""" + await super().async_will_remove_from_hass() + + if issue_key := DEPRECATED_SENSORS.get(self.entity_description.key): + ir.async_delete_issue(self.hass, DOMAIN, f"{issue_key}_{self.entity_id}") diff --git a/homeassistant/components/apcupsd/strings.json b/homeassistant/components/apcupsd/strings.json index 07ea917b54f..a3070982c80 100644 --- a/homeassistant/components/apcupsd/strings.json +++ b/homeassistant/components/apcupsd/strings.json @@ -241,5 +241,19 @@ "cannot_connect": { "message": "Cannot connect to APC UPS Daemon." } + }, + "issues": { + "apc_deprecated": { + "description": "The {entity_name} sensor (`{entity_id}`) is deprecated because it exposes internal details of the APC UPS Daemon response.\n\nIt is still referenced in the following automations or scripts:\n{items}\n\nUpdate those automations or scripts to use supported APC UPS entities instead. Reload the APC UPS Daemon integration afterwards to resolve this issue.", + "title": "{entity_name} sensor is deprecated" + }, + "available_via_device_info": { + "description": "The {entity_name} sensor (`{entity_id}`) is deprecated because the same value is available from the device registry via `device_attr(\"{device_id}\", \"{available_via_device_attr}\")`.\n\nIt is still referenced in the following automations or scripts:\n{items}\n\nUpdate those automations or scripts to use the `device_attr` helper instead of this sensor. Reload the APC UPS Daemon integration afterwards to resolve this issue.", + "title": "{entity_name} sensor is deprecated" + }, + "date_deprecated": { + "description": "The {entity_name} sensor (`{entity_id}`) is deprecated because the timestamp is already available from other APC UPS sensors via their last updated time.\n\nIt is still referenced in the following automations or scripts:\n{items}\n\nUpdate those automations or scripts to reference any entity's `last_updated` attribute instead (for example, `states.binary_sensor.apcups_online_status.last_updated`). Reload the APC UPS Daemon integration afterwards to resolve this issue.", + "title": "{entity_name} sensor is deprecated" + } } } diff --git a/tests/components/apcupsd/snapshots/test_sensor.ambr b/tests/components/apcupsd/snapshots/test_sensor.ambr index 4e9626bec6b..73b349650b8 100644 --- a/tests/components/apcupsd/snapshots/test_sensor.ambr +++ b/tests/components/apcupsd/snapshots/test_sensor.ambr @@ -1,4 +1,244 @@ # serializer version: 1 +# name: test_deprecated_sensor_issue[apc-apc_deprecated] + IssueRegistryItemSnapshot({ + 'active': True, + 'breaks_in_ha_version': '2026.6.0', + 'created': , + 'data': None, + 'dismissed_version': None, + 'domain': 'apcupsd', + 'is_fixable': False, + 'is_persistent': False, + 'issue_domain': None, + 'issue_id': 'apc_deprecated_sensor.myups_status_data', + 'learn_more_url': None, + 'severity': , + 'translation_key': 'apc_deprecated', + 'translation_placeholders': dict({ + 'device_id': '', + 'entity_id': 'sensor.myups_status_data', + 'entity_name': 'Status data', + 'items': ''' + - [APC UPS automation (apc)](/config/automation/edit/apcupsd_auto_apc) + - [APC UPS script (apc)](/config/script/edit/apcupsd_script_apc) + ''', + }), + }) +# --- +# name: test_deprecated_sensor_issue[apcmodel-available_via_device_info] + IssueRegistryItemSnapshot({ + 'active': True, + 'breaks_in_ha_version': '2026.6.0', + 'created': , + 'data': None, + 'dismissed_version': None, + 'domain': 'apcupsd', + 'is_fixable': False, + 'is_persistent': False, + 'issue_domain': None, + 'issue_id': 'available_via_device_info_sensor.myups_model', + 'learn_more_url': None, + 'severity': , + 'translation_key': 'available_via_device_info', + 'translation_placeholders': dict({ + 'available_via_device_attr': 'model', + 'device_id': '', + 'entity_id': 'sensor.myups_model', + 'entity_name': 'Model', + 'items': ''' + - [APC UPS automation (apcmodel)](/config/automation/edit/apcupsd_auto_apcmodel) + - [APC UPS script (apcmodel)](/config/script/edit/apcupsd_script_apcmodel) + ''', + }), + }) +# --- +# name: test_deprecated_sensor_issue[date-date_deprecated] + IssueRegistryItemSnapshot({ + 'active': True, + 'breaks_in_ha_version': '2026.6.0', + 'created': , + 'data': None, + 'dismissed_version': None, + 'domain': 'apcupsd', + 'is_fixable': False, + 'is_persistent': False, + 'issue_domain': None, + 'issue_id': 'date_deprecated_sensor.myups_status_date', + 'learn_more_url': None, + 'severity': , + 'translation_key': 'date_deprecated', + 'translation_placeholders': dict({ + 'device_id': '', + 'entity_id': 'sensor.myups_status_date', + 'entity_name': 'Status date', + 'items': ''' + - [APC UPS automation (date)](/config/automation/edit/apcupsd_auto_date) + - [APC UPS script (date)](/config/script/edit/apcupsd_script_date) + ''', + }), + }) +# --- +# name: test_deprecated_sensor_issue[end apc-date_deprecated] + IssueRegistryItemSnapshot({ + 'active': True, + 'breaks_in_ha_version': '2026.6.0', + 'created': , + 'data': None, + 'dismissed_version': None, + 'domain': 'apcupsd', + 'is_fixable': False, + 'is_persistent': False, + 'issue_domain': None, + 'issue_id': 'date_deprecated_sensor.myups_date_and_time', + 'learn_more_url': None, + 'severity': , + 'translation_key': 'date_deprecated', + 'translation_placeholders': dict({ + 'device_id': '', + 'entity_id': 'sensor.myups_date_and_time', + 'entity_name': 'Date and time', + 'items': ''' + - [APC UPS automation (end apc)](/config/automation/edit/apcupsd_auto_end_apc) + - [APC UPS script (end apc)](/config/script/edit/apcupsd_script_end_apc) + ''', + }), + }) +# --- +# name: test_deprecated_sensor_issue[firmware-available_via_device_info] + IssueRegistryItemSnapshot({ + 'active': True, + 'breaks_in_ha_version': '2026.6.0', + 'created': , + 'data': None, + 'dismissed_version': None, + 'domain': 'apcupsd', + 'is_fixable': False, + 'is_persistent': False, + 'issue_domain': None, + 'issue_id': 'available_via_device_info_sensor.myups_firmware_version', + 'learn_more_url': None, + 'severity': , + 'translation_key': 'available_via_device_info', + 'translation_placeholders': dict({ + 'available_via_device_attr': 'hw_version', + 'device_id': '', + 'entity_id': 'sensor.myups_firmware_version', + 'entity_name': 'Firmware version', + 'items': ''' + - [APC UPS automation (firmware)](/config/automation/edit/apcupsd_auto_firmware) + - [APC UPS script (firmware)](/config/script/edit/apcupsd_script_firmware) + ''', + }), + }) +# --- +# name: test_deprecated_sensor_issue[model-available_via_device_info] + IssueRegistryItemSnapshot({ + 'active': True, + 'breaks_in_ha_version': '2026.6.0', + 'created': , + 'data': None, + 'dismissed_version': None, + 'domain': 'apcupsd', + 'is_fixable': False, + 'is_persistent': False, + 'issue_domain': None, + 'issue_id': 'available_via_device_info_sensor.myups_model_2', + 'learn_more_url': None, + 'severity': , + 'translation_key': 'available_via_device_info', + 'translation_placeholders': dict({ + 'available_via_device_attr': 'model', + 'device_id': '', + 'entity_id': 'sensor.myups_model_2', + 'entity_name': 'Model', + 'items': ''' + - [APC UPS automation (model)](/config/automation/edit/apcupsd_auto_model) + - [APC UPS script (model)](/config/script/edit/apcupsd_script_model) + ''', + }), + }) +# --- +# name: test_deprecated_sensor_issue[serialno-available_via_device_info] + IssueRegistryItemSnapshot({ + 'active': True, + 'breaks_in_ha_version': '2026.6.0', + 'created': , + 'data': None, + 'dismissed_version': None, + 'domain': 'apcupsd', + 'is_fixable': False, + 'is_persistent': False, + 'issue_domain': None, + 'issue_id': 'available_via_device_info_sensor.myups_serial_number', + 'learn_more_url': None, + 'severity': , + 'translation_key': 'available_via_device_info', + 'translation_placeholders': dict({ + 'available_via_device_attr': 'serial_number', + 'device_id': '', + 'entity_id': 'sensor.myups_serial_number', + 'entity_name': 'Serial number', + 'items': ''' + - [APC UPS automation (serialno)](/config/automation/edit/apcupsd_auto_serialno) + - [APC UPS script (serialno)](/config/script/edit/apcupsd_script_serialno) + ''', + }), + }) +# --- +# name: test_deprecated_sensor_issue[upsname-available_via_device_info] + IssueRegistryItemSnapshot({ + 'active': True, + 'breaks_in_ha_version': '2026.6.0', + 'created': , + 'data': None, + 'dismissed_version': None, + 'domain': 'apcupsd', + 'is_fixable': False, + 'is_persistent': False, + 'issue_domain': None, + 'issue_id': 'available_via_device_info_sensor.myups_name', + 'learn_more_url': None, + 'severity': , + 'translation_key': 'available_via_device_info', + 'translation_placeholders': dict({ + 'available_via_device_attr': 'name', + 'device_id': '', + 'entity_id': 'sensor.myups_name', + 'entity_name': 'Name', + 'items': ''' + - [APC UPS automation (upsname)](/config/automation/edit/apcupsd_auto_upsname) + - [APC UPS script (upsname)](/config/script/edit/apcupsd_script_upsname) + ''', + }), + }) +# --- +# name: test_deprecated_sensor_issue[version-available_via_device_info] + IssueRegistryItemSnapshot({ + 'active': True, + 'breaks_in_ha_version': '2026.6.0', + 'created': , + 'data': None, + 'dismissed_version': None, + 'domain': 'apcupsd', + 'is_fixable': False, + 'is_persistent': False, + 'issue_domain': None, + 'issue_id': 'available_via_device_info_sensor.myups_daemon_version', + 'learn_more_url': None, + 'severity': , + 'translation_key': 'available_via_device_info', + 'translation_placeholders': dict({ + 'available_via_device_attr': 'sw_version', + 'device_id': '', + 'entity_id': 'sensor.myups_daemon_version', + 'entity_name': 'Daemon version', + 'items': ''' + - [APC UPS automation (version)](/config/automation/edit/apcupsd_auto_version) + - [APC UPS script (version)](/config/script/edit/apcupsd_script_version) + ''', + }), + }) +# --- # name: test_sensor[sensor.myups_alarm_delay-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/apcupsd/test_sensor.py b/tests/components/apcupsd/test_sensor.py index 9dadffe6fb3..2842bdf1258 100644 --- a/tests/components/apcupsd/test_sensor.py +++ b/tests/components/apcupsd/test_sensor.py @@ -6,7 +6,8 @@ from unittest.mock import AsyncMock import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.apcupsd.const import DOMAIN +from homeassistant.components import automation, script +from homeassistant.components.apcupsd.const import DEPRECATED_SENSORS, DOMAIN from homeassistant.components.apcupsd.coordinator import REQUEST_REFRESH_COOLDOWN from homeassistant.const import ( ATTR_ENTITY_ID, @@ -15,7 +16,11 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + device_registry as dr, + entity_registry as er, + issue_registry as ir, +) from homeassistant.setup import async_setup_component from homeassistant.util import slugify from homeassistant.util.dt import utcnow @@ -161,3 +166,76 @@ async def test_sensor_unknown( await hass.async_block_till_done() # The state should become unknown again. assert hass.states.get(last_self_test_id).state == STATE_UNKNOWN + + +@pytest.mark.parametrize(("entity_key", "issue_key"), DEPRECATED_SENSORS.items()) +async def test_deprecated_sensor_issue( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_request_status: AsyncMock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + entity_key: str, + issue_key: str, +) -> None: + """Ensure the issue lists automations and scripts referencing a deprecated sensor.""" + issue_registry = ir.async_get(hass) + unique_id = f"{mock_request_status.return_value['SERIALNO']}_{entity_key}" + entity_id = entity_registry.async_get_entity_id("sensor", DOMAIN, unique_id) + assert entity_id + + # No issue yet. + issue_id = f"{issue_key}_{entity_id}" + assert issue_registry.async_get_issue(DOMAIN, issue_id) is None + + # Add automations and scripts referencing the deprecated sensor. + entity_slug = slugify(entity_key) + automation_object_id = f"apcupsd_auto_{entity_slug}" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": automation_object_id, + "alias": f"APC UPS automation ({entity_key})", + "trigger": {"platform": "state", "entity_id": entity_id}, + "action": { + "action": "automation.turn_on", + "target": {"entity_id": f"automation.{automation_object_id}"}, + }, + } + }, + ) + + assert await async_setup_component( + hass, + script.DOMAIN, + { + script.DOMAIN: { + f"apcupsd_script_{entity_slug}": { + "alias": f"APC UPS script ({entity_key})", + "sequence": [ + { + "condition": "state", + "entity_id": entity_id, + "state": "on", + } + ], + } + } + }, + ) + await hass.config_entries.async_reload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + issue = issue_registry.async_get_issue(DOMAIN, issue_id) + # Redact the device ID in the placeholder for consistency. + issue.translation_placeholders["device_id"] = "" + assert issue == snapshot + + await hass.config_entries.async_unload(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