From ca511231154a3f37eb0ff7996cf8c3d4900814c1 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 28 Mar 2026 21:59:36 +0100 Subject: [PATCH] Revert mqtt vacuum segments support (#166761) --- .../components/mqtt/abbreviations.py | 3 - homeassistant/components/mqtt/entity.py | 5 - homeassistant/components/mqtt/vacuum.py | 92 +---- tests/components/mqtt/test_vacuum.py | 371 +----------------- 4 files changed, 6 insertions(+), 465 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 9892384e804..4cc391e0ca7 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -18,8 +18,6 @@ ABBREVIATIONS = { "bri_stat_t": "brightness_state_topic", "bri_tpl": "brightness_template", "bri_val_tpl": "brightness_value_template", - "cln_segmnts_cmd_t": "clean_segments_command_topic", - "cln_segmnts_cmd_tpl": "clean_segments_command_template", "clr_temp_cmd_tpl": "color_temp_command_template", "clrm_stat_t": "color_mode_state_topic", "clrm_val_tpl": "color_mode_value_template", @@ -187,7 +185,6 @@ ABBREVIATIONS = { "rgbww_cmd_t": "rgbww_command_topic", "rgbww_stat_t": "rgbww_state_topic", "rgbww_val_tpl": "rgbww_value_template", - "segmnts": "segments", "send_cmd_t": "send_command_topic", "send_if_off": "send_if_off", "set_fan_spd_t": "set_fan_speed_topic", diff --git a/homeassistant/components/mqtt/entity.py b/homeassistant/components/mqtt/entity.py index a101612f793..12b6aac94bf 100644 --- a/homeassistant/components/mqtt/entity.py +++ b/homeassistant/components/mqtt/entity.py @@ -1484,7 +1484,6 @@ class MqttEntity( self._config = config self._setup_from_config(self._config) self._setup_common_attributes_from_config(self._config) - self._process_entity_update() # Prepare MQTT subscriptions self.attributes_prepare_discovery_update(config) @@ -1587,10 +1586,6 @@ class MqttEntity( def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" - @callback - def _process_entity_update(self) -> None: - """Process an entity discovery update.""" - @abstractmethod @callback def _prepare_subscribe_topics(self) -> None: diff --git a/homeassistant/components/mqtt/vacuum.py b/homeassistant/components/mqtt/vacuum.py index 3ec8566029d..6896d51ef93 100644 --- a/homeassistant/components/mqtt/vacuum.py +++ b/homeassistant/components/mqtt/vacuum.py @@ -10,13 +10,12 @@ import voluptuous as vol from homeassistant.components import vacuum from homeassistant.components.vacuum import ( ENTITY_ID_FORMAT, - Segment, StateVacuumEntity, VacuumActivity, VacuumEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_UNIQUE_ID +from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -28,7 +27,7 @@ from . import subscription from .config import MQTT_BASE_SCHEMA from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC from .entity import MqttEntity, async_setup_entity_entry_helper -from .models import MqttCommandTemplate, ReceiveMessage +from .models import ReceiveMessage from .schemas import MQTT_ENTITY_COMMON_SCHEMA from .util import valid_publish_topic @@ -53,9 +52,6 @@ POSSIBLE_STATES: dict[str, VacuumActivity] = { STATE_CLEANING: VacuumActivity.CLEANING, } -CONF_SEGMENTS = "segments" -CONF_CLEAN_SEGMENTS_COMMAND_TOPIC = "clean_segments_command_topic" -CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE = "clean_segments_command_template" CONF_SUPPORTED_FEATURES = ATTR_SUPPORTED_FEATURES CONF_PAYLOAD_TURN_ON = "payload_turn_on" CONF_PAYLOAD_TURN_OFF = "payload_turn_off" @@ -141,39 +137,8 @@ MQTT_VACUUM_ATTRIBUTES_BLOCKED = frozenset( MQTT_VACUUM_DOCS_URL = "https://www.home-assistant.io/integrations/vacuum.mqtt/" -def validate_clean_area_config(config: ConfigType) -> ConfigType: - """Check for a valid configuration and check segments.""" - if (config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC not in config) or ( - not config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC in config - ): - raise vol.Invalid( - f"Options `{CONF_SEGMENTS}` and " - f"`{CONF_CLEAN_SEGMENTS_COMMAND_TOPIC}` must be defined together" - ) - segments: list[str] - if segments := config[CONF_SEGMENTS]: - if not config.get(CONF_UNIQUE_ID): - raise vol.Invalid( - f"Option `{CONF_SEGMENTS}` requires `{CONF_UNIQUE_ID}` to be configured" - ) - unique_segments: set[str] = set() - for segment in segments: - segment_id, _, _ = segment.partition(".") - if not segment_id or segment_id in unique_segments: - raise vol.Invalid( - f"The `{CONF_SEGMENTS}` option contains an invalid or non-" - f"unique segment ID '{segment_id}'. Got {segments}" - ) - unique_segments.add(segment_id) - - return config - - -_BASE_SCHEMA = MQTT_BASE_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { - vol.Optional(CONF_SEGMENTS, default=[]): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_CLEAN_SEGMENTS_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All( cv.ensure_list, [cv.string] ), @@ -199,10 +164,7 @@ _BASE_SCHEMA = MQTT_BASE_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -PLATFORM_SCHEMA_MODERN = vol.All(_BASE_SCHEMA, validate_clean_area_config) -DISCOVERY_SCHEMA = vol.All( - _BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_clean_area_config -) +DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.ALLOW_EXTRA) async def async_setup_entry( @@ -229,11 +191,9 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_VACUUM_ATTRIBUTES_BLOCKED - _segments: list[Segment] _command_topic: str | None _set_fan_speed_topic: str | None _send_command_topic: str | None - _clean_segments_command_topic: str _payloads: dict[str, str | None] def __init__( @@ -269,23 +229,6 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): self._attr_supported_features = _strings_to_services( supported_feature_strings, STRING_TO_SERVICE ) - if config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC in config: - self._attr_supported_features |= VacuumEntityFeature.CLEAN_AREA - segments: list[str] = config[CONF_SEGMENTS] - self._segments = [ - Segment(id=segment_id, name=name or segment_id) - for segment_id, _, name in [ - segment.partition(".") for segment in segments - ] - ] - self._clean_segments_command_topic = config[ - CONF_CLEAN_SEGMENTS_COMMAND_TOPIC - ] - self._clean_segments_command_template = MqttCommandTemplate( - config.get(CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE), - entity=self, - ).async_render - self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST] self._command_topic = config.get(CONF_COMMAND_TOPIC) self._set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC) @@ -303,20 +246,6 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): ) } - @callback - def _process_entity_update(self) -> None: - """Check vacuum segments with registry entry.""" - if ( - self._attr_supported_features & VacuumEntityFeature.CLEAN_AREA - and (last_seen := self.last_seen_segments) is not None - and {s.id: s for s in last_seen} != {s.id: s for s in self._segments} - ): - self.async_create_segments_issue() - - async def mqtt_async_added_to_hass(self) -> None: - """Check vacuum segments with registry entry.""" - self._process_entity_update() - def _update_state_attributes(self, payload: dict[str, Any]) -> None: """Update the entity state attributes.""" self._state_attrs.update(payload) @@ -348,19 +277,6 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): """(Re)Subscribe to topics.""" subscription.async_subscribe_topics_internal(self.hass, self._sub_state) - async def async_clean_segments(self, segment_ids: list[str], **kwargs: Any) -> None: - """Perform an area clean.""" - await self.async_publish_with_config( - self._clean_segments_command_topic, - self._clean_segments_command_template( - json_dumps(segment_ids), {"value": segment_ids} - ), - ) - - async def async_get_segments(self) -> list[Segment]: - """Return the available segments.""" - return self._segments - async def _async_publish_command(self, feature: VacuumEntityFeature) -> None: """Publish a command.""" if self._command_topic is None: diff --git a/tests/components/mqtt/test_vacuum.py b/tests/components/mqtt/test_vacuum.py index 1ddb30c404c..ea5d9f8f8e7 100644 --- a/tests/components/mqtt/test_vacuum.py +++ b/tests/components/mqtt/test_vacuum.py @@ -3,7 +3,7 @@ from copy import deepcopy import json from typing import Any -from unittest.mock import call, patch +from unittest.mock import patch import pytest @@ -30,7 +30,6 @@ from homeassistant.components.vacuum import ( from homeassistant.const import CONF_NAME, ENTITY_MATCH_ALL, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er, issue_registry as ir from .common import ( help_custom_config, @@ -64,11 +63,7 @@ from .common import ( from tests.common import async_fire_mqtt_message from tests.components.vacuum import common -from tests.typing import ( - MqttMockHAClientGenerator, - MqttMockPahoClient, - WebSocketGenerator, -) +from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient COMMAND_TOPIC = "vacuum/command" SEND_COMMAND_TOPIC = "vacuum/send_command" @@ -87,27 +82,6 @@ DEFAULT_CONFIG = { } } -CONFIG_CLEAN_SEGMENTS_1 = { - mqtt.DOMAIN: { - vacuum.DOMAIN: { - "name": "test", - "unique_id": "veryunique", - "segments": ["Livingroom", "Kitchen"], - "clean_segments_command_topic": "vacuum/clean_segment", - } - } -} -CONFIG_CLEAN_SEGMENTS_2 = { - mqtt.DOMAIN: { - vacuum.DOMAIN: { - "name": "test", - "unique_id": "veryunique", - "segments": ["1.Livingroom", "2.Kitchen"], - "clean_segments_command_topic": "vacuum/clean_segment", - } - } -} - DEFAULT_CONFIG_2 = {mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test"}}} CONFIG_ALL_SERVICES = help_custom_config( @@ -320,347 +294,6 @@ async def test_command_without_command_topic( mqtt_mock.async_publish.reset_mock() -@pytest.mark.parametrize("hass_config", [CONFIG_CLEAN_SEGMENTS_1]) -async def test_clean_segments_initial_setup_without_repair_issue( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, -) -> None: - """Test cleanable segments initial setup does not fire repair flow.""" - await mqtt_mock_entry() - issue_registry = ir.async_get(hass) - assert len(issue_registry.issues) == 0 - - -@pytest.mark.parametrize("hass_config", [CONFIG_CLEAN_SEGMENTS_1]) -async def test_clean_segments_command_without_id( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - entity_registry: er.EntityRegistry, - mqtt_mock_entry: MqttMockHAClientGenerator, -) -> None: - """Test cleanable segments without ID.""" - config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] - entity_registry.async_get_or_create( - vacuum.DOMAIN, - mqtt.DOMAIN, - "veryunique", - config_entry=config_entry, - suggested_object_id="test", - ) - entity_registry.async_update_entity_options( - "vacuum.test", - vacuum.DOMAIN, - { - "area_mapping": {"Nabu Casa": ["Kitchen", "Livingroom"]}, - "last_seen_segments": [ - {"id": "Livingroom", "name": "Livingroom"}, - {"id": "Kitchen", "name": "Kitchen"}, - ], - }, - ) - mqtt_mock = await mqtt_mock_entry() - await hass.async_block_till_done() - issue_registry = ir.async_get(hass) - # We do not expect a repair flow - assert len(issue_registry.issues) == 0 - - state = hass.states.get("vacuum.test") - assert state.state == STATE_UNKNOWN - await common.async_clean_area(hass, ["Nabu Casa"], entity_id="vacuum.test") - assert ( - call("vacuum/clean_segment", '["Kitchen","Livingroom"]', 0, False) - in mqtt_mock.async_publish.mock_calls - ) - - client = await hass_ws_client(hass) - await client.send_json_auto_id( - {"type": "vacuum/get_segments", "entity_id": "vacuum.test"} - ) - msg = await client.receive_json() - assert msg["success"] - assert msg["result"]["segments"] == [ - {"id": "Livingroom", "name": "Livingroom", "group": None}, - {"id": "Kitchen", "name": "Kitchen", "group": None}, - ] - - -@pytest.mark.parametrize("hass_config", [CONFIG_CLEAN_SEGMENTS_2]) -async def test_clean_segments_command_with_id( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - entity_registry: er.EntityRegistry, - mqtt_mock_entry: MqttMockHAClientGenerator, -) -> None: - """Test cleanable segments with ID.""" - mqtt_mock = await mqtt_mock_entry() - # Set the area mapping - entity_registry.async_update_entity_options( - "vacuum.test", - vacuum.DOMAIN, - { - "area_mapping": {"Livingroom": ["1"], "Kitchen": ["2"]}, - "last_seen_segments": [ - {"id": "1", "name": "Livingroom"}, - {"id": "2", "name": "Kitchen"}, - ], - }, - ) - await hass.async_block_till_done() - - state = hass.states.get("vacuum.test") - assert state.state == STATE_UNKNOWN - await common.async_clean_area(hass, ["Kitchen"], entity_id="vacuum.test") - assert ( - call("vacuum/clean_segment", '["2"]', 0, False) - in mqtt_mock.async_publish.mock_calls - ) - - client = await hass_ws_client(hass) - await client.send_json_auto_id( - {"type": "vacuum/get_segments", "entity_id": "vacuum.test"} - ) - msg = await client.receive_json() - assert msg["success"] - assert msg["result"]["segments"] == [ - {"id": "1", "name": "Livingroom", "group": None}, - {"id": "2", "name": "Kitchen", "group": None}, - ] - - -async def test_clean_segments_command_update( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - entity_registry: er.EntityRegistry, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test cleanable segments update via discovery.""" - # Prepare original entity config entry - config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] - entity_registry.async_get_or_create( - vacuum.DOMAIN, - mqtt.DOMAIN, - "veryunique", - config_entry=config_entry, - suggested_object_id="test", - ) - entity_registry.async_update_entity_options( - "vacuum.test", - vacuum.DOMAIN, - { - "area_mapping": {"Livingroom": ["1"], "Kitchen": ["2"]}, - "last_seen_segments": [ - {"id": "1", "name": "Livingroom"}, - {"id": "2", "name": "Kitchen"}, - ], - }, - ) - await mqtt_mock_entry() - # Do initial discovery - config1 = CONFIG_CLEAN_SEGMENTS_2[mqtt.DOMAIN][vacuum.DOMAIN] - payload1 = json.dumps(config1) - config_topic = "homeassistant/vacuum/bla/config" - async_fire_mqtt_message(hass, config_topic, payload1) - await hass.async_block_till_done() - state = hass.states.get("vacuum.test") - assert state.state == STATE_UNKNOWN - - issue_registry = ir.async_get(hass) - # We do not expect a repair flow - assert len(issue_registry.issues) == 0 - - # Update the segments - config2 = config1.copy() - config2["segments"] = ["1.Livingroom", "2.Kitchen", "3.Diningroom"] - payload2 = json.dumps(config2) - async_fire_mqtt_message(hass, config_topic, payload2) - await hass.async_block_till_done() - - # A repair flow should start - assert len(issue_registry.issues) == 1 - - client = await hass_ws_client(hass) - await client.send_json_auto_id( - {"type": "vacuum/get_segments", "entity_id": "vacuum.test"} - ) - msg = await client.receive_json() - assert msg["success"] - assert msg["result"]["segments"] == [ - {"id": "1", "name": "Livingroom", "group": None}, - {"id": "2", "name": "Kitchen", "group": None}, - {"id": "3", "name": "Diningroom", "group": None}, - ] - - # Test update with a non-unique segment list fails - config3 = config1.copy() - config3["segments"] = ["1.Livingroom", "2.Kitchen", "2.Diningroom"] - payload3 = json.dumps(config3) - async_fire_mqtt_message(hass, config_topic, payload3) - await hass.async_block_till_done() - assert ( - "Error 'The `segments` option contains an invalid or non-unique segment ID '2'" - in caplog.text - ) - - -@pytest.mark.parametrize( - "hass_config", - [ - { - mqtt.DOMAIN: { - vacuum.DOMAIN: { - "name": "test", - "unique_id": "veryunique", - "segments": ["Livingroom", "Kitchen", "Kitchen"], - "clean_segments_command_topic": "vacuum/clean_segment", - } - } - }, - { - mqtt.DOMAIN: { - vacuum.DOMAIN: { - "name": "test", - "unique_id": "veryunique", - "segments": ["Livingroom", "Kitchen", ""], - "clean_segments_command_topic": "vacuum/clean_segment", - } - } - }, - { - mqtt.DOMAIN: { - vacuum.DOMAIN: { - "name": "test", - "unique_id": "veryunique", - "segments": ["1.Livingroom", "1.Kitchen"], - "clean_segments_command_topic": "vacuum/clean_segment", - } - } - }, - { - mqtt.DOMAIN: { - vacuum.DOMAIN: { - "name": "test", - "unique_id": "veryunique", - "segments": ["1.Livingroom", "1.Kitchen", ".Diningroom"], - "clean_segments_command_topic": "vacuum/clean_segment", - } - } - }, - ], -) -async def test_non_unique_segments( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test with non-unique list of cleanable segments with valid segment IDs.""" - await mqtt_mock_entry() - assert ( - "The `segments` option contains an invalid or non-unique segment ID" - in caplog.text - ) - - -@pytest.mark.usefixtures("hass") -@pytest.mark.parametrize( - ("hass_config", "error_message"), - [ - ( - help_custom_config( - vacuum.DOMAIN, - DEFAULT_CONFIG, - ({"clean_segments_command_topic": "test-topic"},), - ), - "Options `segments` and " - "`clean_segments_command_topic` must be defined together", - ), - ( - help_custom_config( - vacuum.DOMAIN, - DEFAULT_CONFIG, - ({"segments": ["Livingroom"]},), - ), - "Options `segments` and " - "`clean_segments_command_topic` must be defined together", - ), - ( - help_custom_config( - vacuum.DOMAIN, - DEFAULT_CONFIG, - ( - { - "segments": ["Livingroom"], - "clean_segments_command_topic": "test-topic", - }, - ), - ), - "Option `segments` requires `unique_id` to be configured", - ), - ], -) -async def test_clean_segments_config_validation( - mqtt_mock_entry: MqttMockHAClientGenerator, - error_message: str, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test status clean segment config validation.""" - await mqtt_mock_entry() - assert error_message in caplog.text - - -@pytest.mark.parametrize( - "hass_config", - [ - help_custom_config( - vacuum.DOMAIN, - CONFIG_CLEAN_SEGMENTS_2, - ({"clean_segments_command_template": "{{ ';'.join(value) }}"},), - ) - ], -) -async def test_clean_segments_command_with_id_and_command_template( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - entity_registry: er.EntityRegistry, - mqtt_mock_entry: MqttMockHAClientGenerator, -) -> None: - """Test clean segments with command template.""" - mqtt_mock = await mqtt_mock_entry() - entity_registry.async_update_entity_options( - "vacuum.test", - vacuum.DOMAIN, - { - "area_mapping": {"Livingroom": ["1"], "Kitchen": ["2"]}, - "last_seen_segments": [ - {"id": "1", "name": "Livingroom"}, - {"id": "2", "name": "Kitchen"}, - ], - }, - ) - await hass.async_block_till_done() - - state = hass.states.get("vacuum.test") - assert state.state == STATE_UNKNOWN - await common.async_clean_area( - hass, ["Livingroom", "Kitchen"], entity_id="vacuum.test" - ) - assert ( - call("vacuum/clean_segment", "1;2", 0, False) - in mqtt_mock.async_publish.mock_calls - ) - - client = await hass_ws_client(hass) - await client.send_json_auto_id( - {"type": "vacuum/get_segments", "entity_id": "vacuum.test"} - ) - msg = await client.receive_json() - assert msg["success"] - assert msg["result"]["segments"] == [ - {"id": "1", "name": "Livingroom", "group": None}, - {"id": "2", "name": "Kitchen", "group": None}, - ] - - @pytest.mark.parametrize("hass_config", [CONFIG_ALL_SERVICES]) async def test_status( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator