From c21d508c2d9d14ca136ff808e858833273640958 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 27 Mar 2024 16:10:32 +0100 Subject: [PATCH] Start deprecation of auxiliary heater in ClimateEntity (#112532) * Start deprecation of auxiliary heater in ClimateEntity * No issue for core integrations * Remove unneded strings * Move report to state_attributes --- homeassistant/components/climate/__init__.py | 53 +++ homeassistant/components/climate/strings.json | 10 + tests/components/climate/test_init.py | 308 ++++++++++++++++++ 3 files changed, 371 insertions(+) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 87327945c68..00fd69ce63b 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -24,6 +24,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import issue_registry as ir import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -40,6 +41,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.typing import ConfigType +from homeassistant.loader import async_get_issue_tracker, async_suggest_report_issue from homeassistant.util.unit_conversion import TemperatureConverter from . import group as group_pre_import # noqa: F401 @@ -309,6 +311,8 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): _attr_target_temperature: float | None = None _attr_temperature_unit: str + __climate_reported_legacy_aux = False + __mod_supported_features: ClimateEntityFeature = ClimateEntityFeature(0) # Integrations should set `_enable_turn_on_off_backwards_compatibility` to False # once migrated and set the feature flags TURN_ON/TURN_OFF as needed. @@ -404,6 +408,50 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF ) + def _report_legacy_aux(self) -> None: + """Log warning and create an issue if the entity implements legacy auxiliary heater.""" + + report_issue = async_suggest_report_issue( + self.hass, + integration_domain=self.platform.platform_name, + module=type(self).__module__, + ) + _LOGGER.warning( + ( + "%s::%s implements the `is_aux_heat` property or uses the auxiliary " + "heater methods in a subclass of ClimateEntity which is " + "deprecated and will be unsupported from Home Assistant 2024.10." + " Please %s" + ), + self.platform.platform_name, + self.__class__.__name__, + report_issue, + ) + + translation_placeholders = {"platform": self.platform.platform_name} + translation_key = "deprecated_climate_aux_no_url" + issue_tracker = async_get_issue_tracker( + self.hass, + integration_domain=self.platform.platform_name, + module=type(self).__module__, + ) + if issue_tracker: + translation_placeholders["issue_tracker"] = issue_tracker + translation_key = "deprecated_climate_aux_url_custom" + ir.async_create_issue( + self.hass, + DOMAIN, + f"deprecated_climate_aux_{self.platform.platform_name}", + breaks_in_ha_version="2024.10.0", + is_fixable=False, + is_persistent=False, + issue_domain=self.platform.platform_name, + severity=ir.IssueSeverity.WARNING, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + self.__climate_reported_legacy_aux = True + @final @property def state(self) -> str | None: @@ -508,6 +556,11 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if ClimateEntityFeature.AUX_HEAT in supported_features: data[ATTR_AUX_HEAT] = STATE_ON if self.is_aux_heat else STATE_OFF + if ( + self.__climate_reported_legacy_aux is False + and "custom_components" in type(self).__module__ + ): + self._report_legacy_aux() return data diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index eb9285b0c4f..c31d22ccbeb 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -238,6 +238,16 @@ } } }, + "issues": { + "deprecated_climate_aux_url_custom": { + "title": "The {platform} custom integration is using deprecated climate auxiliary heater", + "description": "The custom integration `{platform}` implements the `is_aux_heat` property or uses the auxiliary heater methods in a subclass of ClimateEntity.\n\nPlease create a bug report at {issue_tracker}.\n\nOnce an updated version of `{platform}` is available, install it and restart Home Assistant to fix this issue." + }, + "deprecated_climate_aux_no_url": { + "title": "[%key:component::climate::issues::deprecated_climate_aux_url_custom::title%]", + "description": "The custom integration `{platform}` implements the `is_aux_heat` property or uses the auxiliary heater methods in a subclass of ClimateEntity.\n\nPlease report it to the author of the {platform} integration.\n\nOnce an updated version of `{platform}` is available, install it and restart Home Assistant to fix this issue." + } + }, "exceptions": { "not_valid_preset_mode": { "message": "Preset mode {mode} is not valid. Valid preset modes are: {modes}." diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index 65b7287f549..ed942fb1464 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -23,12 +23,14 @@ from homeassistant.components.climate.const import ( SERVICE_SET_FAN_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_SWING_MODE, + SERVICE_SET_TEMPERATURE, ClimateEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.entity_platform import AddEntitiesCallback from tests.common import ( @@ -771,3 +773,309 @@ async def test_sync_toggle(hass: HomeAssistant) -> None: await climate.async_toggle() assert climate.toggle.called + + +ISSUE_TRACKER = "https://blablabla.com" + + +@pytest.mark.parametrize( + ( + "manifest_extra", + "translation_key", + "translation_placeholders_extra", + "report", + "module", + ), + [ + ( + {}, + "deprecated_climate_aux_no_url", + {}, + "report it to the author of the 'test' custom integration", + "custom_components.test.climate", + ), + ( + {"issue_tracker": ISSUE_TRACKER}, + "deprecated_climate_aux_url_custom", + {"issue_tracker": ISSUE_TRACKER}, + "create a bug report at https://blablabla.com", + "custom_components.test.climate", + ), + ], +) +async def test_issue_aux_property_deprecated( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + config_flow_fixture: None, + manifest_extra: dict[str, str], + translation_key: str, + translation_placeholders_extra: dict[str, str], + report: str, + module: str, +) -> None: + """Test the issue is raised on deprecated auxiliary heater attributes.""" + + class MockClimateEntityWithAux(MockClimateEntity): + """Mock climate class with mocked aux heater.""" + + _attr_supported_features = ( + ClimateEntityFeature.AUX_HEAT | ClimateEntityFeature.TARGET_TEMPERATURE + ) + + @property + def is_aux_heat(self) -> bool | None: + """Return true if aux heater. + + Requires ClimateEntityFeature.AUX_HEAT. + """ + return True + + async def async_turn_aux_heat_on(self) -> None: + """Turn auxiliary heater on.""" + await self.hass.async_add_executor_job(self.turn_aux_heat_on) + + async def async_turn_aux_heat_off(self) -> None: + """Turn auxiliary heater off.""" + await self.hass.async_add_executor_job(self.turn_aux_heat_off) + + # Fake the module is custom component or built in + MockClimateEntityWithAux.__module__ = module + + climate_entity = MockClimateEntityWithAux( + name="Testing", + entity_id="climate.testing", + ) + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) + return True + + async def async_setup_entry_climate_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test weather platform via config entry.""" + async_add_entities([climate_entity]) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=async_setup_entry_init, + partial_manifest=manifest_extra, + ), + built_in=False, + ) + mock_platform( + hass, + "test.climate", + MockPlatform(async_setup_entry=async_setup_entry_climate_platform), + ) + + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert climate_entity.state == HVACMode.HEAT + + issues = ir.async_get(hass) + issue = issues.async_get_issue("climate", "deprecated_climate_aux_test") + assert issue + assert issue.issue_domain == "test" + assert issue.issue_id == "deprecated_climate_aux_test" + assert issue.translation_key == translation_key + assert ( + issue.translation_placeholders + == {"platform": "test"} | translation_placeholders_extra + ) + + assert ( + "test::MockClimateEntityWithAux implements the `is_aux_heat` property or uses " + "the auxiliary heater methods in a subclass of ClimateEntity which is deprecated " + f"and will be unsupported from Home Assistant 2024.10. Please {report}" + ) in caplog.text + + # Assert we only log warning once + caplog.clear() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + "entity_id": "climate.test", + "temperature": "25", + }, + blocking=True, + ) + await hass.async_block_till_done() + + assert ("implements the `is_aux_heat` property") not in caplog.text + + +@pytest.mark.parametrize( + ( + "manifest_extra", + "translation_key", + "translation_placeholders_extra", + "report", + "module", + ), + [ + ( + {"issue_tracker": ISSUE_TRACKER}, + "deprecated_climate_aux_url", + {"issue_tracker": ISSUE_TRACKER}, + "create a bug report at https://blablabla.com", + "homeassistant.components.test.climate", + ), + ], +) +async def test_no_issue_aux_property_deprecated_for_core( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + config_flow_fixture: None, + manifest_extra: dict[str, str], + translation_key: str, + translation_placeholders_extra: dict[str, str], + report: str, + module: str, +) -> None: + """Test the no issue on deprecated auxiliary heater attributes for core integrations.""" + + class MockClimateEntityWithAux(MockClimateEntity): + """Mock climate class with mocked aux heater.""" + + _attr_supported_features = ClimateEntityFeature.AUX_HEAT + + @property + def is_aux_heat(self) -> bool | None: + """Return true if aux heater. + + Requires ClimateEntityFeature.AUX_HEAT. + """ + return True + + async def async_turn_aux_heat_on(self) -> None: + """Turn auxiliary heater on.""" + await self.hass.async_add_executor_job(self.turn_aux_heat_on) + + async def async_turn_aux_heat_off(self) -> None: + """Turn auxiliary heater off.""" + await self.hass.async_add_executor_job(self.turn_aux_heat_off) + + # Fake the module is custom component or built in + MockClimateEntityWithAux.__module__ = module + + climate_entity = MockClimateEntityWithAux( + name="Testing", + entity_id="climate.testing", + ) + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) + return True + + async def async_setup_entry_climate_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test weather platform via config entry.""" + async_add_entities([climate_entity]) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=async_setup_entry_init, + partial_manifest=manifest_extra, + ), + built_in=False, + ) + mock_platform( + hass, + "test.climate", + MockPlatform(async_setup_entry=async_setup_entry_climate_platform), + ) + + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert climate_entity.state == HVACMode.HEAT + + issues = ir.async_get(hass) + issue = issues.async_get_issue("climate", "deprecated_climate_aux_test") + assert not issue + + assert ( + "test::MockClimateEntityWithAux implements the `is_aux_heat` property or uses " + "the auxiliary heater methods in a subclass of ClimateEntity which is deprecated " + f"and will be unsupported from Home Assistant 2024.10. Please {report}" + ) not in caplog.text + + +async def test_no_issue_no_aux_property( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + config_flow_fixture: None, +) -> None: + """Test the issue is raised on deprecated auxiliary heater attributes.""" + + climate_entity = MockClimateEntity( + name="Testing", + entity_id="climate.testing", + ) + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) + return True + + async def async_setup_entry_climate_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test weather platform via config entry.""" + async_add_entities([climate_entity]) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=async_setup_entry_init, + ), + built_in=False, + ) + mock_platform( + hass, + "test.climate", + MockPlatform(async_setup_entry=async_setup_entry_climate_platform), + ) + + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert climate_entity.state == HVACMode.HEAT + + issues = ir.async_get(hass) + assert len(issues.issues) == 0 + + assert ( + "test::MockClimateEntityWithAux implements the `is_aux_heat` property or uses " + "the auxiliary heater methods in a subclass of ClimateEntity which is deprecated " + "and will be unsupported from Home Assistant 2024.10." + ) not in caplog.text