diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 288f40727d0..47e18e9e9dd 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -63,6 +63,7 @@ SERVICE_STOP = "stop" DEFAULT_NAME = "Vacuum cleaner robot" ISSUE_SEGMENTS_CHANGED = "segments_changed" +ISSUE_SEGMENTS_MAPPING_NOT_CONFIGURED = "segments_mapping_not_configured" _BATTERY_DEPRECATION_IGNORED_PLATFORMS = ("template",) @@ -189,6 +190,9 @@ class StateVacuumEntity( _attr_activity: VacuumActivity | None = None _attr_supported_features: VacuumEntityFeature = VacuumEntityFeature(0) + _segments_not_configured_issue_created: bool = False + _segments_changed_last_seen: list[dict[str, Any]] | None = None + __vacuum_legacy_battery_level: bool = False __vacuum_legacy_battery_icon: bool = False __vacuum_legacy_battery_feature: bool = False @@ -232,6 +236,17 @@ class StateVacuumEntity( if self.__vacuum_legacy_battery_icon: self._report_deprecated_battery_properties("battery_icon") + @callback + def async_write_ha_state(self) -> None: + """Write the state to the state machine.""" + super().async_write_ha_state() + self._async_check_segments_issues() + + @callback + def async_registry_entry_updated(self) -> None: + """Run when the entity registry entry has been updated.""" + self._async_check_segments_issues() + @callback def _report_deprecated_battery_properties(self, property: str) -> None: """Report on deprecated use of battery properties. @@ -489,6 +504,61 @@ class StateVacuumEntity( "entity_id": self.entity_id, }, ) + options: Mapping[str, Any] = self.registry_entry.options.get(DOMAIN, {}) + self._segments_changed_last_seen = options.get("last_seen_segments") + + @callback + def _async_check_segments_issues(self) -> None: + """Create or delete segment-related repair issues.""" + if self.registry_entry is None: + return + + options: Mapping[str, Any] = self.registry_entry.options.get(DOMAIN, {}) + should_have_not_configured_issue = ( + VacuumEntityFeature.CLEAN_AREA in self.supported_features + and options.get("area_mapping") is None + ) + + if ( + should_have_not_configured_issue + and not self._segments_not_configured_issue_created + ): + issue_id = ( + f"{ISSUE_SEGMENTS_MAPPING_NOT_CONFIGURED}_{self.registry_entry.id}" + ) + ir.async_create_issue( + self.hass, + DOMAIN, + issue_id, + data={ + "entry_id": self.registry_entry.id, + "entity_id": self.entity_id, + }, + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key=ISSUE_SEGMENTS_MAPPING_NOT_CONFIGURED, + translation_placeholders={ + "entity_id": self.entity_id, + }, + ) + self._segments_not_configured_issue_created = True + elif ( + not should_have_not_configured_issue + and self._segments_not_configured_issue_created + ): + issue_id = ( + f"{ISSUE_SEGMENTS_MAPPING_NOT_CONFIGURED}_{self.registry_entry.id}" + ) + ir.async_delete_issue(self.hass, DOMAIN, issue_id) + self._segments_not_configured_issue_created = False + + if self._segments_changed_last_seen is not None and ( + VacuumEntityFeature.CLEAN_AREA not in self.supported_features + or options.get("last_seen_segments") != self._segments_changed_last_seen + ): + issue_id = f"{ISSUE_SEGMENTS_CHANGED}_{self.registry_entry.id}" + ir.async_delete_issue(self.hass, DOMAIN, issue_id) + self._segments_changed_last_seen = None def locate(self, **kwargs: Any) -> None: """Locate the vacuum cleaner.""" diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index 1695e1f2a4c..778261713b0 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -93,6 +93,10 @@ "segments_changed": { "description": "", "title": "Vacuum segments have changed for {entity_id}" + }, + "segments_mapping_not_configured": { + "description": "", + "title": "Vacuum segment mapping not configured for {entity_id}" } }, "selector": { diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py index 549802d6e79..7da53a66213 100644 --- a/tests/components/vacuum/test_init.py +++ b/tests/components/vacuum/test_init.py @@ -430,10 +430,10 @@ async def test_last_seen_segments( @pytest.mark.usefixtures("config_flow_fixture") -async def test_last_seen_segments_and_issue_creation( +async def test_segments_changed_issue( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: - """Test last_seen_segments property and segments issue creation.""" + """Test segments changed issue.""" mock_vacuum = MockVacuumWithCleanArea(name="Testing", entity_id="vacuum.testing") config_entry = MockConfigEntry(domain="test") @@ -452,6 +452,17 @@ async def test_last_seen_segments_and_issue_creation( await hass.async_block_till_done() entity_entry = entity_registry.async_get(mock_vacuum.entity_id) + + entity_registry.async_update_entity_options( + mock_vacuum.entity_id, + DOMAIN, + { + "area_mapping": {"area_1": ["seg_1"]}, + "last_seen_segments": [asdict(segment) for segment in mock_vacuum.segments], + }, + ) + await hass.async_block_till_done() + mock_vacuum.async_create_segments_issue() issue_id = f"segments_changed_{entity_entry.id}" @@ -460,6 +471,95 @@ async def test_last_seen_segments_and_issue_creation( assert issue.severity == ir.IssueSeverity.WARNING assert issue.translation_key == "segments_changed" + entity_registry.async_update_entity_options( + mock_vacuum.entity_id, + DOMAIN, + { + "area_mapping": {"area_1": ["seg_1"], "area_2": ["seg_new"]}, + "last_seen_segments": [ + {"id": "seg_1", "name": "Kitchen"}, + {"id": "seg_new", "name": "New Room"}, + ], + }, + ) + await hass.async_block_till_done() + + assert ir.async_get(hass).async_get_issue(DOMAIN, issue_id) is None + + +@pytest.mark.usefixtures("config_flow_fixture") +@pytest.mark.parametrize("area_mapping", [{"area_1": ["seg_1"]}, {}]) +async def test_segments_mapping_not_configured_issue( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + area_mapping: dict[str, list[str]], +) -> None: + """Test segments_mapping_not_configured issue.""" + mock_vacuum = MockVacuumWithCleanArea(name="Testing", entity_id="vacuum.testing") + + 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, + ), + ) + setup_test_component_platform(hass, DOMAIN, [mock_vacuum], from_config_entry=True) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entity_entry = entity_registry.async_get(mock_vacuum.entity_id) + + issue_id = f"segments_mapping_not_configured_{entity_entry.id}" + issue = ir.async_get(hass).async_get_issue(DOMAIN, issue_id) + assert issue is not None + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.translation_key == "segments_mapping_not_configured" + + entity_registry.async_update_entity_options( + mock_vacuum.entity_id, + DOMAIN, + { + "area_mapping": area_mapping, + "last_seen_segments": [asdict(segment) for segment in mock_vacuum.segments], + }, + ) + await hass.async_block_till_done() + + assert ir.async_get(hass).async_get_issue(DOMAIN, issue_id) is None + + +@pytest.mark.usefixtures("config_flow_fixture") +async def test_no_segments_mapping_issue_without_clean_area( + hass: HomeAssistant, +) -> None: + """Test no repair issue is created when CLEAN_AREA is not supported.""" + mock_vacuum = MockVacuum(name="Testing", entity_id="vacuum.testing") + + 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, + ), + ) + setup_test_component_platform(hass, DOMAIN, [mock_vacuum], from_config_entry=True) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + issues = ir.async_get(hass).issues + assert not any( + issue_id[1].startswith("segments_mapping_not_configured") for issue_id in issues + ) + @pytest.mark.parametrize(("is_built_in", "log_warnings"), [(True, 0), (False, 3)]) async def test_vacuum_log_deprecated_battery_using_properties(