diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 83c68fb61b6..11d13431f9d 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -247,6 +247,9 @@ class StateVacuumEntity( _attr_supported_features: VacuumEntityFeature = VacuumEntityFeature(0) __vacuum_legacy_state: bool = False + __vacuum_legacy_battery_level: bool = False + __vacuum_legacy_battery_icon: bool = False + __vacuum_legacy_battery_feature: bool = False def __init_subclass__(cls, **kwargs: Any) -> None: """Post initialisation processing.""" @@ -255,15 +258,28 @@ class StateVacuumEntity( # Integrations should use the 'activity' property instead of # setting the state directly. cls.__vacuum_legacy_state = True + if any( + method in cls.__dict__ + for method in ("_attr_battery_level", "battery_level") + ): + # Integrations should use a separate battery sensor. + cls.__vacuum_legacy_battery_level = True + if any( + method in cls.__dict__ for method in ("_attr_battery_icon", "battery_icon") + ): + # Integrations should use a separate battery sensor. + cls.__vacuum_legacy_battery_icon = True def __setattr__(self, name: str, value: Any) -> None: """Set attribute. - Deprecation warning if setting '_attr_state' directly - unless already reported. + Deprecation warning if setting state, battery icon or battery level + attributes directly unless already reported. """ if name == "_attr_state": self._report_deprecated_activity_handling() + if name in {"_attr_battery_level", "_attr_battery_icon"}: + self._report_deprecated_battery_properties(name[6:]) return super().__setattr__(name, value) @callback @@ -277,6 +293,10 @@ class StateVacuumEntity( super().add_to_platform_start(hass, platform, parallel_updates) if self.__vacuum_legacy_state: self._report_deprecated_activity_handling() + if self.__vacuum_legacy_battery_level: + self._report_deprecated_battery_properties("battery_level") + if self.__vacuum_legacy_battery_icon: + self._report_deprecated_battery_properties("battery_icon") @callback def _report_deprecated_activity_handling(self) -> None: @@ -295,6 +315,42 @@ class StateVacuumEntity( exclude_integrations={DOMAIN}, ) + @callback + def _report_deprecated_battery_properties(self, property: str) -> None: + """Report on deprecated use of battery properties. + + Integrations should implement a sensor instead. + """ + report_usage( + f"is setting the {property} which has been deprecated." + f" Integration {self.platform.platform_name} should implement a sensor" + " instead with a correct device class and link it to the same device", + core_integration_behavior=ReportBehavior.LOG, + custom_integration_behavior=ReportBehavior.LOG, + breaks_in_ha_version="2026.7", + integration_domain=self.platform.platform_name if self.platform else None, + exclude_integrations={DOMAIN}, + ) + + @callback + def _report_deprecated_battery_feature(self) -> None: + """Report on deprecated use of battery supported features. + + Integrations should remove the battery supported feature when migrating + battery level and icon to a sensor. + """ + report_usage( + f"is setting the battery supported feature which has been deprecated." + f" Integration {self.platform.platform_name} should remove this as part of migrating" + " the battery level and icon to a sensor", + core_behavior=ReportBehavior.LOG, + core_integration_behavior=ReportBehavior.LOG, + custom_integration_behavior=ReportBehavior.LOG, + breaks_in_ha_version="2026.7", + integration_domain=self.platform.platform_name if self.platform else None, + exclude_integrations={DOMAIN}, + ) + @cached_property def battery_level(self) -> int | None: """Return the battery level of the vacuum cleaner.""" @@ -333,6 +389,9 @@ class StateVacuumEntity( supported_features = self.supported_features if VacuumEntityFeature.BATTERY in supported_features: + if self.__vacuum_legacy_battery_feature is False: + self._report_deprecated_battery_feature() + self.__vacuum_legacy_battery_feature = True data[ATTR_BATTERY_LEVEL] = self.battery_level data[ATTR_BATTERY_ICON] = self.battery_icon diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py index b3e5d17c728..77debf634ad 100644 --- a/tests/components/vacuum/test_init.py +++ b/tests/components/vacuum/test_init.py @@ -435,3 +435,181 @@ async def test_vacuum_deprecated_state_does_not_break_state( state = hass.states.get(entity.entity_id) assert state is not None assert state.state == "cleaning" + + +@pytest.mark.usefixtures("mock_as_custom_component") +async def test_vacuum_log_deprecated_battery_properties( + hass: HomeAssistant, + config_flow_fixture: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test incorrectly using battery properties logs warning.""" + + class MockLegacyVacuum(MockVacuum): + """Mocked vacuum entity.""" + + @property + def activity(self) -> str: + """Return the state of the entity.""" + return VacuumActivity.CLEANING + + @property + def battery_level(self) -> int: + """Return the battery level of the vacuum.""" + return 50 + + @property + def battery_icon(self) -> str: + """Return the battery icon of the vacuum.""" + return "mdi:battery-50" + + entity = MockLegacyVacuum( + name="Testing", + entity_id="vacuum.test", + ) + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=help_async_setup_entry_init, + async_unload_entry=help_async_unload_entry, + ), + built_in=False, + ) + setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True) + assert await hass.config_entries.async_setup(config_entry.entry_id) + + state = hass.states.get(entity.entity_id) + assert state is not None + + assert ( + "Detected that custom integration 'test' is setting the battery_icon which has been deprecated." + " Integration test should implement a sensor instead with a correct device class and link it" + " to the same device. This will stop working in Home Assistant 2026.7," + " please report it to the author of the 'test' custom integration" + in caplog.text + ) + assert ( + "Detected that custom integration 'test' is setting the battery_level which has been deprecated." + " Integration test should implement a sensor instead with a correct device class and link it" + " to the same device. This will stop working in Home Assistant 2026.7," + " please report it to the author of the 'test' custom integration" + in caplog.text + ) + + +@pytest.mark.usefixtures("mock_as_custom_component") +async def test_vacuum_log_deprecated_battery_properties_using_attr( + hass: HomeAssistant, + config_flow_fixture: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test incorrectly using _attr_battery_* attribute does log issue and raise repair.""" + + class MockLegacyVacuum(MockVacuum): + """Mocked vacuum entity.""" + + def start(self) -> None: + """Start cleaning.""" + self._attr_battery_level = 50 + self._attr_battery_icon = "mdi:battery-50" + + entity = MockLegacyVacuum( + name="Testing", + entity_id="vacuum.test", + ) + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=help_async_setup_entry_init, + async_unload_entry=help_async_unload_entry, + ), + built_in=False, + ) + setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True) + assert await hass.config_entries.async_setup(config_entry.entry_id) + + state = hass.states.get(entity.entity_id) + assert state is not None + entity.start() + + assert ( + "Detected that custom integration 'test' is setting the battery_level which has been deprecated." + " Integration test should implement a sensor instead with a correct device class and link it to" + " the same device. This will stop working in Home Assistant 2026.7," + " please report it to the author of the 'test' custom integration" + in caplog.text + ) + assert ( + "Detected that custom integration 'test' is setting the battery_icon which has been deprecated." + " Integration test should implement a sensor instead with a correct device class and link it to" + " the same device. This will stop working in Home Assistant 2026.7," + " please report it to the author of the 'test' custom integration" + in caplog.text + ) + + await async_start(hass, entity.entity_id) + + caplog.clear() + await async_start(hass, entity.entity_id) + # Test we only log once + assert ( + "Detected that custom integration 'test' is setting the battery_level which has been deprecated." + " Integration test should implement a sensor instead with a correct device class and link it to" + " the same device. This will stop working in Home Assistant 2026.7," + " please report it to the author of the 'test' custom integration" + not in caplog.text + ) + assert ( + "Detected that custom integration 'test' is setting the battery_icon which has been deprecated." + " Integration test should implement a sensor instead with a correct device class and link it to" + " the same device. This will stop working in Home Assistant 2026.7," + " please report it to the author of the 'test' custom integration" + not in caplog.text + ) + + +@pytest.mark.usefixtures("mock_as_custom_component") +async def test_vacuum_log_deprecated_battery_supported_feature( + hass: HomeAssistant, + config_flow_fixture: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test incorrectly setting battery supported feature logs warning.""" + + entity = MockVacuum( + name="Testing", + entity_id="vacuum.test", + ) + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=help_async_setup_entry_init, + async_unload_entry=help_async_unload_entry, + ), + built_in=False, + ) + setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True) + assert await hass.config_entries.async_setup(config_entry.entry_id) + + state = hass.states.get(entity.entity_id) + assert state is not None + + assert ( + "Detected that custom integration 'test' is setting the battery supported feature" + " which has been deprecated. Integration test should remove this as part of migrating" + " the battery level and icon to a sensor. This will stop working in Home Assistant 2026.7" + ", please report it to the author of the 'test' custom integration" + in caplog.text + )