diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 3081c1d0d9f..d5487bbe29f 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -31,7 +31,6 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import callback -from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -531,9 +530,6 @@ class MqttCover( async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" - if ATTR_TILT_POSITION not in kwargs: - return - position = float(kwargs[ATTR_TILT_POSITION]) # The position needs to be between min and max @@ -550,36 +546,31 @@ class MqttCover( async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" set_position_template = self._config.get(CONF_SET_POSITION_TEMPLATE) - if ATTR_POSITION in kwargs: - position = kwargs[ATTR_POSITION] - percentage_position = position - if set_position_template is not None: - try: - position = set_position_template.async_render(**kwargs) - except TemplateError as ex: - _LOGGER.error(ex) - self._state = None - elif ( - self._config[CONF_POSITION_OPEN] != 100 - and self._config[CONF_POSITION_CLOSED] != 0 - ): - position = self.find_in_range_from_percent(position, COVER_PAYLOAD) + position = kwargs[ATTR_POSITION] + percentage_position = position + if set_position_template is not None: + position = set_position_template.async_render(**kwargs) + elif ( + self._config[CONF_POSITION_OPEN] != 100 + and self._config[CONF_POSITION_CLOSED] != 0 + ): + position = self.find_in_range_from_percent(position, COVER_PAYLOAD) - mqtt.async_publish( - self.hass, - self._config.get(CONF_SET_POSITION_TOPIC), - position, - self._config[CONF_QOS], - self._config[CONF_RETAIN], + mqtt.async_publish( + self.hass, + self._config.get(CONF_SET_POSITION_TOPIC), + position, + self._config[CONF_QOS], + self._config[CONF_RETAIN], + ) + if self._optimistic: + self._state = ( + STATE_CLOSED + if percentage_position == self._config[CONF_POSITION_CLOSED] + else STATE_OPEN ) - if self._optimistic: - self._state = ( - STATE_CLOSED - if percentage_position == self._config[CONF_POSITION_CLOSED] - else STATE_OPEN - ) - self._position = percentage_position - self.async_write_ha_state() + self._position = percentage_position + self.async_write_ha_state() async def async_toggle_tilt(self, **kwargs): """Toggle the entity.""" diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 689b279c5e7..47a7f5a2037 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -6,7 +6,6 @@ import re from homeassistant.components import mqtt from homeassistant.const import CONF_DEVICE, CONF_PLATFORM -from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import HomeAssistantType @@ -35,21 +34,6 @@ SUPPORTED_COMPONENTS = [ "vacuum", ] -CONFIG_ENTRY_COMPONENTS = [ - "alarm_control_panel", - "binary_sensor", - "camera", - "climate", - "cover", - "device_automation", - "fan", - "light", - "lock", - "sensor", - "switch", - "vacuum", -] - ALREADY_DISCOVERED = "mqtt_discovered_components" DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" @@ -159,10 +143,6 @@ async def async_start( _LOGGER.info("Found new component: %s %s", component, discovery_id) hass.data[ALREADY_DISCOVERED][discovery_hash] = None - if component not in CONFIG_ENTRY_COMPONENTS: - await async_load_platform(hass, component, "mqtt", payload, hass_config) - return - config_entries_key = f"{component}.mqtt" async with hass.data[DATA_CONFIG_ENTRY_LOCK]: if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index ec7c729d597..9ce3809bfe8 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -401,9 +401,6 @@ class MqttFan( This method is a coroutine. """ - if self._topic[CONF_SPEED_COMMAND_TOPIC] is None: - return - if speed == SPEED_LOW: mqtt_payload = self._payload["SPEED_LOW"] elif speed == SPEED_MEDIUM: @@ -432,9 +429,6 @@ class MqttFan( This method is a coroutine. """ - if self._topic[CONF_OSCILLATION_COMMAND_TOPIC] is None: - return - if oscillating is False: payload = self._payload["OSCILLATE_OFF_PAYLOAD"] else: diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 687fefa94e2..85851bcf696 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -410,24 +410,26 @@ class MqttVacuum( @property def fan_speed_list(self): - """Return the status of the vacuum.""" - if self.supported_features & SUPPORT_FAN_SPEED == 0: - return [] + """Return the status of the vacuum. + + No need to check SUPPORT_FAN_SPEED, this won't be called if fan_speed is None. + """ return self._fan_speed_list @property def battery_level(self): """Return the status of the vacuum.""" if self.supported_features & SUPPORT_BATTERY == 0: - return + return None return max(0, min(100, self._battery_level)) @property def battery_icon(self): - """Return the battery icon for the vacuum cleaner.""" - if self.supported_features & SUPPORT_BATTERY == 0: - return + """Return the battery icon for the vacuum cleaner. + + No need to check SUPPORT_BATTERY, this won't be called if battery_level is None. + """ return icon_for_battery_level( battery_level=self.battery_level, charging=self._charging diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 254d841aebc..a59beae1d34 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -282,9 +282,10 @@ class MqttStateVacuum( @property def fan_speed_list(self): - """Return fan speed list of the vacuum.""" - if self.supported_features & SUPPORT_FAN_SPEED == 0: - return None + """Return fan speed list of the vacuum. + + No need to check SUPPORT_FAN_SPEED, this won't be called if fan_speed is None. + """ return self._fan_speed_list @property diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index ba1855247fe..f8c10516f24 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -42,7 +42,8 @@ from tests.common import ( ) from tests.components.alarm_control_panel import common -CODE = "HELLO_CODE" +CODE_NUMBER = "1234" +CODE_TEXT = "HELLO_CODE" DEFAULT_CONFIG = { alarm_control_panel.DOMAIN: { @@ -341,6 +342,34 @@ async def test_update_state_via_state_topic_template(hass, mqtt_mock): assert state.state == STATE_ALARM_ARMED_AWAY +async def test_attributes_code_number(hass, mqtt_mock): + """Test attributes which are not supported by the vacuum.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config[alarm_control_panel.DOMAIN]["code"] = CODE_NUMBER + + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config) + + state = hass.states.get("alarm_control_panel.test") + assert ( + state.attributes.get(alarm_control_panel.ATTR_CODE_FORMAT) + == alarm_control_panel.FORMAT_NUMBER + ) + + +async def test_attributes_code_text(hass, mqtt_mock): + """Test attributes which are not supported by the vacuum.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config[alarm_control_panel.DOMAIN]["code"] = CODE_TEXT + + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config) + + state = hass.states.get("alarm_control_panel.test") + assert ( + state.attributes.get(alarm_control_panel.ATTR_CODE_FORMAT) + == alarm_control_panel.FORMAT_TEXT + ) + + async def test_availability_without_topic(hass, mqtt_mock): """Test availability without defined availability topic.""" await help_test_availability_without_topic( diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index d5b303a51f0..03e22cd72e2 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -534,6 +534,22 @@ async def test_set_hold(hass, mqtt_mock): assert state.attributes.get("preset_mode") is None +async def test_set_preset_mode_twice(hass, mqtt_mock): + """Test setting of the same mode twice only publishes once.""" + assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") is None + await common.async_set_preset_mode(hass, "hold-on", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with("hold-topic", "hold-on", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "hold-on" + + await common.async_set_preset_mode(hass, "hold-on", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_not_called() + + async def test_set_aux_pessimistic(hass, mqtt_mock): """Test setting of the aux heating in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 7d0753a9de2..6de462b9020 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -1,6 +1,11 @@ """The tests for the MQTT cover platform.""" from homeassistant.components import cover -from homeassistant.components.cover import ATTR_POSITION, ATTR_TILT_POSITION +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, + ATTR_POSITION, + ATTR_TILT_POSITION, +) from homeassistant.components.mqtt.cover import MqttCover from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -335,6 +340,68 @@ async def test_optimistic_state_change(hass, mqtt_mock): assert state.state == STATE_CLOSED +async def test_optimistic_state_change_with_position(hass, mqtt_mock): + """Test changing state optimistically.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "optimistic": True, + "command_topic": "command-topic", + "position_topic": "position-topic", + "qos": 0, + } + }, + ) + + state = hass.states.get("cover.test") + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(ATTR_CURRENT_POSITION) is None + + await hass.services.async_call( + cover.DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: "cover.test"}, blocking=True + ) + + mqtt_mock.async_publish.assert_called_once_with("command-topic", "OPEN", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("cover.test") + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_POSITION) == 100 + + await hass.services.async_call( + cover.DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: "cover.test"}, blocking=True + ) + + mqtt_mock.async_publish.assert_called_once_with("command-topic", "CLOSE", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("cover.test") + assert STATE_CLOSED == state.state + assert state.attributes.get(ATTR_CURRENT_POSITION) == 0 + + await hass.services.async_call( + cover.DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: "cover.test"}, blocking=True + ) + + mqtt_mock.async_publish.assert_called_once_with("command-topic", "OPEN", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("cover.test") + assert STATE_OPEN == state.state + assert state.attributes.get(ATTR_CURRENT_POSITION) == 100 + + await hass.services.async_call( + cover.DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: "cover.test"}, blocking=True + ) + + mqtt_mock.async_publish.assert_called_once_with("command-topic", "CLOSE", 0, False) + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSED + assert state.attributes.get(ATTR_CURRENT_POSITION) == 0 + + async def test_send_open_cover_command(hass, mqtt_mock): """Test the sending of open_cover.""" assert await async_setup_component( @@ -440,31 +507,31 @@ async def test_current_cover_position(hass, mqtt_mock): ) state_attributes_dict = hass.states.get("cover.test").attributes - assert not ("current_position" in state_attributes_dict) - assert not ("current_tilt_position" in state_attributes_dict) + assert not (ATTR_CURRENT_POSITION in state_attributes_dict) + assert not (ATTR_CURRENT_TILT_POSITION in state_attributes_dict) assert not (4 & hass.states.get("cover.test").attributes["supported_features"] == 4) async_fire_mqtt_message(hass, "get-position-topic", "0") current_cover_position = hass.states.get("cover.test").attributes[ - "current_position" + ATTR_CURRENT_POSITION ] assert current_cover_position == 0 async_fire_mqtt_message(hass, "get-position-topic", "50") current_cover_position = hass.states.get("cover.test").attributes[ - "current_position" + ATTR_CURRENT_POSITION ] assert current_cover_position == 50 async_fire_mqtt_message(hass, "get-position-topic", "non-numeric") current_cover_position = hass.states.get("cover.test").attributes[ - "current_position" + ATTR_CURRENT_POSITION ] assert current_cover_position == 50 async_fire_mqtt_message(hass, "get-position-topic", "101") current_cover_position = hass.states.get("cover.test").attributes[ - "current_position" + ATTR_CURRENT_POSITION ] assert current_cover_position == 100 @@ -490,48 +557,67 @@ async def test_current_cover_position_inverted(hass, mqtt_mock): ) state_attributes_dict = hass.states.get("cover.test").attributes - assert not ("current_position" in state_attributes_dict) - assert not ("current_tilt_position" in state_attributes_dict) + assert not (ATTR_CURRENT_POSITION in state_attributes_dict) + assert not (ATTR_CURRENT_TILT_POSITION in state_attributes_dict) assert not (4 & hass.states.get("cover.test").attributes["supported_features"] == 4) async_fire_mqtt_message(hass, "get-position-topic", "100") current_percentage_cover_position = hass.states.get("cover.test").attributes[ - "current_position" + ATTR_CURRENT_POSITION ] assert current_percentage_cover_position == 0 assert hass.states.get("cover.test").state == STATE_CLOSED async_fire_mqtt_message(hass, "get-position-topic", "0") current_percentage_cover_position = hass.states.get("cover.test").attributes[ - "current_position" + ATTR_CURRENT_POSITION ] assert current_percentage_cover_position == 100 assert hass.states.get("cover.test").state == STATE_OPEN async_fire_mqtt_message(hass, "get-position-topic", "50") current_percentage_cover_position = hass.states.get("cover.test").attributes[ - "current_position" + ATTR_CURRENT_POSITION ] assert current_percentage_cover_position == 50 assert hass.states.get("cover.test").state == STATE_OPEN async_fire_mqtt_message(hass, "get-position-topic", "non-numeric") current_percentage_cover_position = hass.states.get("cover.test").attributes[ - "current_position" + ATTR_CURRENT_POSITION ] assert current_percentage_cover_position == 50 assert hass.states.get("cover.test").state == STATE_OPEN async_fire_mqtt_message(hass, "get-position-topic", "101") current_percentage_cover_position = hass.states.get("cover.test").attributes[ - "current_position" + ATTR_CURRENT_POSITION ] assert current_percentage_cover_position == 0 assert hass.states.get("cover.test").state == STATE_CLOSED -async def test_set_cover_position(hass, mqtt_mock): - """Test setting cover position.""" +async def test_optimistic_position(hass, mqtt_mock): + """Test optimistic position is not supported.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + } + }, + ) + + state = hass.states.get("cover.test") + assert state is None + + +async def test_position_update(hass, mqtt_mock): + """Test cover position update from received MQTT message.""" assert await async_setup_component( hass, cover.DOMAIN, @@ -552,16 +638,16 @@ async def test_set_cover_position(hass, mqtt_mock): ) state_attributes_dict = hass.states.get("cover.test").attributes - assert not ("current_position" in state_attributes_dict) - assert not ("current_tilt_position" in state_attributes_dict) + assert not (ATTR_CURRENT_POSITION in state_attributes_dict) + assert not (ATTR_CURRENT_TILT_POSITION in state_attributes_dict) assert 4 & hass.states.get("cover.test").attributes["supported_features"] == 4 async_fire_mqtt_message(hass, "get-position-topic", "22") state_attributes_dict = hass.states.get("cover.test").attributes - assert "current_position" in state_attributes_dict - assert not ("current_tilt_position" in state_attributes_dict) + assert ATTR_CURRENT_POSITION in state_attributes_dict + assert not (ATTR_CURRENT_TILT_POSITION in state_attributes_dict) current_cover_position = hass.states.get("cover.test").attributes[ - "current_position" + ATTR_CURRENT_POSITION ] assert current_cover_position == 22 @@ -629,6 +715,37 @@ async def test_set_position_untemplated(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("position-topic", 62, 0, False) +async def test_set_position_untemplated_custom_percentage_range(hass, mqtt_mock): + """Test setting cover position via template.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "position_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "position-topic", + "position_open": 0, + "position_closed": 100, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } + }, + ) + + await hass.services.async_call( + cover.DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.test", ATTR_POSITION: 38}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with("position-topic", 62, 0, False) + + async def test_no_command_topic(hass, mqtt_mock): """Test with no command topic.""" assert await async_setup_component( @@ -717,10 +834,10 @@ async def test_tilt_defaults(hass, mqtt_mock): ) state_attributes_dict = hass.states.get("cover.test").attributes - assert "current_tilt_position" in state_attributes_dict + assert ATTR_CURRENT_TILT_POSITION in state_attributes_dict current_cover_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_position == STATE_UNKNOWN @@ -770,7 +887,7 @@ async def test_tilt_via_invocation_defaults(hass, mqtt_mock): async_fire_mqtt_message(hass, "tilt-status-topic", "0") current_cover_tilt_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_tilt_position == 0 @@ -788,7 +905,7 @@ async def test_tilt_via_invocation_defaults(hass, mqtt_mock): async_fire_mqtt_message(hass, "tilt-status-topic", "100") current_cover_tilt_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_tilt_position == 100 @@ -849,7 +966,7 @@ async def test_tilt_given_value(hass, mqtt_mock): async_fire_mqtt_message(hass, "tilt-status-topic", "25") current_cover_tilt_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_tilt_position == 25 @@ -867,7 +984,7 @@ async def test_tilt_given_value(hass, mqtt_mock): async_fire_mqtt_message(hass, "tilt-status-topic", "80") current_cover_tilt_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_tilt_position == 80 @@ -913,7 +1030,7 @@ async def test_tilt_given_value_optimistic(hass, mqtt_mock): ) current_cover_tilt_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_tilt_position == 80 @@ -928,7 +1045,7 @@ async def test_tilt_given_value_optimistic(hass, mqtt_mock): ) current_cover_tilt_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_tilt_position == 25 @@ -969,7 +1086,7 @@ async def test_tilt_given_value_altered_range(hass, mqtt_mock): ) current_cover_tilt_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_tilt_position == 50 @@ -984,7 +1101,7 @@ async def test_tilt_given_value_altered_range(hass, mqtt_mock): ) current_cover_tilt_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_tilt_position == 0 @@ -999,7 +1116,7 @@ async def test_tilt_given_value_altered_range(hass, mqtt_mock): ) current_cover_tilt_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_tilt_position == 50 @@ -1030,14 +1147,14 @@ async def test_tilt_via_topic(hass, mqtt_mock): async_fire_mqtt_message(hass, "tilt-status-topic", "0") current_cover_tilt_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_tilt_position == 0 async_fire_mqtt_message(hass, "tilt-status-topic", "50") current_cover_tilt_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_tilt_position == 50 @@ -1069,14 +1186,14 @@ async def test_tilt_via_topic_template(hass, mqtt_mock): async_fire_mqtt_message(hass, "tilt-status-topic", "99") current_cover_tilt_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_tilt_position == 0 async_fire_mqtt_message(hass, "tilt-status-topic", "5000") current_cover_tilt_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_tilt_position == 50 @@ -1107,21 +1224,21 @@ async def test_tilt_via_topic_altered_range(hass, mqtt_mock): async_fire_mqtt_message(hass, "tilt-status-topic", "0") current_cover_tilt_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_tilt_position == 0 async_fire_mqtt_message(hass, "tilt-status-topic", "50") current_cover_tilt_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_tilt_position == 100 async_fire_mqtt_message(hass, "tilt-status-topic", "25") current_cover_tilt_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_tilt_position == 50 @@ -1155,21 +1272,21 @@ async def test_tilt_via_topic_template_altered_range(hass, mqtt_mock): async_fire_mqtt_message(hass, "tilt-status-topic", "99") current_cover_tilt_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_tilt_position == 0 async_fire_mqtt_message(hass, "tilt-status-topic", "5000") current_cover_tilt_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_tilt_position == 100 async_fire_mqtt_message(hass, "tilt-status-topic", "2500") current_cover_tilt_position = hass.states.get("cover.test").attributes[ - "current_tilt_position" + ATTR_CURRENT_TILT_POSITION ] assert current_cover_tilt_position == 50 diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 8c925bdf315..71e694f6807 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -51,32 +51,32 @@ async def test_subscribing_config_topic(hass, mqtt_mock): async def test_invalid_topic(hass, mqtt_mock): """Test sending to invalid topic.""" with patch( - "homeassistant.components.mqtt.discovery.async_load_platform" - ) as mock_load_platform: + "homeassistant.components.mqtt.discovery.async_dispatcher_send" + ) as mock_dispatcher_send: entry = MockConfigEntry( domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} ) - mock_load_platform.return_value = mock_coro() + mock_dispatcher_send.return_value = mock_coro() await async_start(hass, "homeassistant", {}, entry) async_fire_mqtt_message( hass, "homeassistant/binary_sensor/bla/not_config", "{}" ) await hass.async_block_till_done() - assert not mock_load_platform.called + assert not mock_dispatcher_send.called async def test_invalid_json(hass, mqtt_mock, caplog): """Test sending in invalid JSON.""" with patch( - "homeassistant.components.mqtt.discovery.async_load_platform" - ) as mock_load_platform: + "homeassistant.components.mqtt.discovery.async_dispatcher_send" + ) as mock_dispatcher_send: entry = MockConfigEntry( domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} ) - mock_load_platform.return_value = mock_coro() + mock_dispatcher_send.return_value = mock_coro() await async_start(hass, "homeassistant", {}, entry) async_fire_mqtt_message( @@ -84,19 +84,19 @@ async def test_invalid_json(hass, mqtt_mock, caplog): ) await hass.async_block_till_done() assert "Unable to parse JSON" in caplog.text - assert not mock_load_platform.called + assert not mock_dispatcher_send.called async def test_only_valid_components(hass, mqtt_mock, caplog): """Test for a valid component.""" with patch( - "homeassistant.components.mqtt.discovery.async_load_platform" - ) as mock_load_platform: + "homeassistant.components.mqtt.discovery.async_dispatcher_send" + ) as mock_dispatcher_send: entry = MockConfigEntry(domain=mqtt.DOMAIN) invalid_component = "timer" - mock_load_platform.return_value = mock_coro() + mock_dispatcher_send.return_value = mock_coro() await async_start(hass, "homeassistant", {}, entry) async_fire_mqtt_message( @@ -107,7 +107,7 @@ async def test_only_valid_components(hass, mqtt_mock, caplog): assert "Integration {} is not supported".format(invalid_component) in caplog.text - assert not mock_load_platform.called + assert not mock_dispatcher_send.called async def test_correct_config_discovery(hass, mqtt_mock, caplog): diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 6b774e9d5ce..6ef1c0aab86 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -1,6 +1,11 @@ """Test MQTT fans.""" from homeassistant.components import fan -from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_SUPPORTED_FEATURES, + STATE_OFF, + STATE_ON, +) from homeassistant.setup import async_setup_component from .test_common import ( @@ -278,6 +283,54 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get(ATTR_ASSUMED_STATE) +async def test_on_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): + """Test on with speed.""" + assert await async_setup_component( + hass, + fan.DOMAIN, + { + fan.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "oscillation_command_topic": "oscillation-command-topic", + "speed_command_topic": "speed-command-topic", + } + }, + ) + + state = hass.states.get("fan.test") + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await common.async_turn_on(hass, "fan.test") + mqtt_mock.async_publish.assert_called_once_with("command-topic", "ON", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("fan.test") + assert state.state is STATE_ON + assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(fan.ATTR_SPEED) is None + assert state.attributes.get(fan.ATTR_OSCILLATING) is None + + await common.async_turn_off(hass, "fan.test") + mqtt_mock.async_publish.assert_called_once_with("command-topic", "OFF", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("fan.test") + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await common.async_turn_on(hass, "fan.test", speed="low") + assert mqtt_mock.async_publish.call_count == 2 + mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) + mqtt_mock.async_publish.assert_any_call("speed-command-topic", "low", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("fan.test") + assert state.state is STATE_ON + assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(fan.ATTR_SPEED) == "low" + assert state.attributes.get(fan.ATTR_OSCILLATING) is None + + async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): """Test optimistic mode with state topic.""" assert await async_setup_component( @@ -370,6 +423,171 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): assert state.state is STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) + await common.async_set_speed(hass, "fan.test", "cUsToM") + mqtt_mock.async_publish.assert_called_once_with( + "speed-command-topic", "cUsToM", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("fan.test") + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + +async def test_attributes(hass, mqtt_mock): + """Test attributes.""" + assert await async_setup_component( + hass, + fan.DOMAIN, + { + fan.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "oscillation_command_topic": "oscillation-command-topic", + "speed_command_topic": "speed-command-topic", + } + }, + ) + + state = hass.states.get("fan.test") + assert state.state is STATE_OFF + assert state.attributes.get(fan.ATTR_SPEED_LIST) == ["off", "low", "medium", "high"] + + await common.async_turn_on(hass, "fan.test") + state = hass.states.get("fan.test") + assert state.state is STATE_ON + assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(fan.ATTR_SPEED) is None + assert state.attributes.get(fan.ATTR_OSCILLATING) is None + + await common.async_turn_off(hass, "fan.test") + state = hass.states.get("fan.test") + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(fan.ATTR_SPEED) is None + assert state.attributes.get(fan.ATTR_OSCILLATING) is None + + await common.async_oscillate(hass, "fan.test", True) + state = hass.states.get("fan.test") + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(fan.ATTR_SPEED) is None + assert state.attributes.get(fan.ATTR_OSCILLATING) is True + + await common.async_oscillate(hass, "fan.test", False) + state = hass.states.get("fan.test") + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(fan.ATTR_SPEED) is None + assert state.attributes.get(fan.ATTR_OSCILLATING) is False + + await common.async_set_speed(hass, "fan.test", fan.SPEED_LOW) + state = hass.states.get("fan.test") + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(fan.ATTR_SPEED) == "low" + assert state.attributes.get(fan.ATTR_OSCILLATING) is False + + await common.async_set_speed(hass, "fan.test", fan.SPEED_MEDIUM) + state = hass.states.get("fan.test") + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(fan.ATTR_SPEED) == "medium" + assert state.attributes.get(fan.ATTR_OSCILLATING) is False + + await common.async_set_speed(hass, "fan.test", fan.SPEED_HIGH) + state = hass.states.get("fan.test") + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(fan.ATTR_SPEED) == "high" + assert state.attributes.get(fan.ATTR_OSCILLATING) is False + + await common.async_set_speed(hass, "fan.test", fan.SPEED_OFF) + state = hass.states.get("fan.test") + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(fan.ATTR_SPEED) == "off" + assert state.attributes.get(fan.ATTR_OSCILLATING) is False + + await common.async_set_speed(hass, "fan.test", "cUsToM") + state = hass.states.get("fan.test") + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(fan.ATTR_SPEED) == "cUsToM" + assert state.attributes.get(fan.ATTR_OSCILLATING) is False + + +async def test_custom_speed_list(hass, mqtt_mock): + """Test optimistic mode without state topic.""" + assert await async_setup_component( + hass, + fan.DOMAIN, + { + fan.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "oscillation_command_topic": "oscillation-command-topic", + "oscillation_state_topic": "oscillation-state-topic", + "speed_command_topic": "speed-command-topic", + "speed_state_topic": "speed-state-topic", + "speeds": ["off", "high"], + } + }, + ) + + state = hass.states.get("fan.test") + assert state.state is STATE_OFF + assert state.attributes.get(fan.ATTR_SPEED_LIST) == ["off", "high"] + + +async def test_supported_features(hass, mqtt_mock): + """Test optimistic mode without state topic.""" + assert await async_setup_component( + hass, + fan.DOMAIN, + { + fan.DOMAIN: [ + { + "platform": "mqtt", + "name": "test1", + "command_topic": "command-topic", + }, + { + "platform": "mqtt", + "name": "test2", + "command_topic": "command-topic", + "oscillation_command_topic": "oscillation-command-topic", + }, + { + "platform": "mqtt", + "name": "test3", + "command_topic": "command-topic", + "speed_command_topic": "speed-command-topic", + }, + { + "platform": "mqtt", + "name": "test4", + "command_topic": "command-topic", + "oscillation_command_topic": "oscillation-command-topic", + "speed_command_topic": "speed-command-topic", + }, + ] + }, + ) + + state = hass.states.get("fan.test1") + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 + state = hass.states.get("fan.test2") + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_OSCILLATE + state = hass.states.get("fan.test3") + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_SET_SPEED + state = hass.states.get("fan.test4") + assert ( + state.attributes.get(ATTR_SUPPORTED_FEATURES) + == fan.SUPPORT_OSCILLATE | fan.SUPPORT_SET_SPEED + ) + async def test_availability_without_topic(hass, mqtt_mock): """Test availability without defined availability topic.""" diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 60ca91b0052..861b2fa30e8 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -9,6 +9,7 @@ import pytest import voluptuous as vol from homeassistant.components import mqtt, websocket_api +from homeassistant.components.mqtt import debug_info from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import ( ATTR_DOMAIN, @@ -36,6 +37,7 @@ from tests.common import ( mock_storage, threadsafe_coroutine_factory, ) +from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES @pytest.fixture @@ -363,6 +365,56 @@ class TestMQTTCallbacks(unittest.TestCase): self.hass.block_till_done() assert len(self.calls) == 1 + def test_subscribe_deprecated(self): + """Test the subscription of a topic using deprecated callback signature.""" + calls = [] + + @callback + def record_calls(topic, payload, qos): + """Record calls.""" + calls.append((topic, payload, qos)) + + unsub = mqtt.subscribe(self.hass, "test-topic", record_calls) + + fire_mqtt_message(self.hass, "test-topic", "test-payload") + + self.hass.block_till_done() + assert len(calls) == 1 + assert calls[0][0] == "test-topic" + assert calls[0][1] == "test-payload" + + unsub() + + fire_mqtt_message(self.hass, "test-topic", "test-payload") + + self.hass.block_till_done() + assert len(calls) == 1 + + def test_subscribe_deprecated_async(self): + """Test the subscription of a topic using deprecated callback signature.""" + calls = [] + + @callback + async def record_calls(topic, payload, qos): + """Record calls.""" + calls.append((topic, payload, qos)) + + unsub = mqtt.subscribe(self.hass, "test-topic", record_calls) + + fire_mqtt_message(self.hass, "test-topic", "test-payload") + + self.hass.block_till_done() + assert len(calls) == 1 + assert calls[0][0] == "test-topic" + assert calls[0][1] == "test-payload" + + unsub() + + fire_mqtt_message(self.hass, "test-topic", "test-payload") + + self.hass.block_till_done() + assert len(calls) == 1 + def test_subscribe_topic_not_match(self): """Test if subscribed topic is not a match.""" mqtt.subscribe(self.hass, "test-topic", self.record_calls) @@ -988,3 +1040,198 @@ async def test_mqtt_ws_get_device_debug_info( "triggers": [], } assert response["result"] == expected_result + + +async def test_debug_info_multiple_devices(hass, mqtt_mock): + """Test we get correct debug_info when multiple devices are present.""" + devices = [ + { + "domain": "sensor", + "config": { + "device": {"identifiers": ["0AFFD0"]}, + "platform": "mqtt", + "state_topic": "test-topic-sensor", + "unique_id": "unique", + }, + }, + { + "domain": "binary_sensor", + "config": { + "device": {"identifiers": ["0AFFD1"]}, + "platform": "mqtt", + "state_topic": "test-topic-binary-sensor", + "unique_id": "unique", + }, + }, + { + "domain": "device_automation", + "config": { + "automation_type": "trigger", + "device": {"identifiers": ["0AFFD2"]}, + "platform": "mqtt", + "topic": "test-topic1", + "type": "foo", + "subtype": "bar", + }, + }, + { + "domain": "device_automation", + "config": { + "automation_type": "trigger", + "device": {"identifiers": ["0AFFD3"]}, + "platform": "mqtt", + "topic": "test-topic2", + "type": "ikk", + "subtype": "baz", + }, + }, + ] + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + for d in devices: + data = json.dumps(d["config"]) + domain = d["domain"] + id = d["config"]["device"]["identifiers"][0] + async_fire_mqtt_message(hass, f"homeassistant/{domain}/{id}/config", data) + await hass.async_block_till_done() + + for d in devices: + domain = d["domain"] + id = d["config"]["device"]["identifiers"][0] + device = registry.async_get_device({("mqtt", id)}, set()) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + if d["domain"] != "device_automation": + assert len(debug_info_data["entities"]) == 1 + assert len(debug_info_data["triggers"]) == 0 + discovery_data = debug_info_data["entities"][0]["discovery_data"] + assert len(debug_info_data["entities"][0]["topics"]) == 1 + topic = d["config"]["state_topic"] + assert {"topic": topic, "messages": []} in debug_info_data["entities"][0][ + "topics" + ] + else: + assert len(debug_info_data["entities"]) == 0 + assert len(debug_info_data["triggers"]) == 1 + discovery_data = debug_info_data["triggers"][0]["discovery_data"] + + assert discovery_data["topic"] == f"homeassistant/{domain}/{id}/config" + assert discovery_data["payload"] == d["config"] + + +async def test_debug_info_multiple_entities_triggers(hass, mqtt_mock): + """Test we get correct debug_info for a device with multiple entities and triggers.""" + config = [ + { + "domain": "sensor", + "config": { + "device": {"identifiers": ["0AFFD0"]}, + "platform": "mqtt", + "state_topic": "test-topic-sensor", + "unique_id": "unique", + }, + }, + { + "domain": "binary_sensor", + "config": { + "device": {"identifiers": ["0AFFD0"]}, + "platform": "mqtt", + "state_topic": "test-topic-binary-sensor", + "unique_id": "unique", + }, + }, + { + "domain": "device_automation", + "config": { + "automation_type": "trigger", + "device": {"identifiers": ["0AFFD0"]}, + "platform": "mqtt", + "topic": "test-topic1", + "type": "foo", + "subtype": "bar", + }, + }, + { + "domain": "device_automation", + "config": { + "automation_type": "trigger", + "device": {"identifiers": ["0AFFD0"]}, + "platform": "mqtt", + "topic": "test-topic2", + "type": "ikk", + "subtype": "baz", + }, + }, + ] + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + for c in config: + data = json.dumps(c["config"]) + domain = c["domain"] + # Use topic as discovery_id + id = c["config"].get("topic", c["config"].get("state_topic")) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/{id}/config", data) + await hass.async_block_till_done() + + device_id = config[0]["config"]["device"]["identifiers"][0] + device = registry.async_get_device({("mqtt", device_id)}, set()) + assert device is not None + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 2 + assert len(debug_info_data["triggers"]) == 2 + + for c in config: + # Test we get debug info for each entity and trigger + domain = c["domain"] + # Use topic as discovery_id + id = c["config"].get("topic", c["config"].get("state_topic")) + + if c["domain"] != "device_automation": + discovery_data = [e["discovery_data"] for e in debug_info_data["entities"]] + topic = c["config"]["state_topic"] + assert {"topic": topic, "messages": []} in [ + t for e in debug_info_data["entities"] for t in e["topics"] + ] + else: + discovery_data = [e["discovery_data"] for e in debug_info_data["triggers"]] + + assert { + "topic": f"homeassistant/{domain}/{id}/config", + "payload": c["config"], + } in discovery_data + + +async def test_debug_info_non_mqtt(hass, device_reg, entity_reg): + """Test we get empty debug_info for a device with non MQTT entities.""" + DOMAIN = "sensor" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + for device_class in DEVICE_CLASSES: + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES[device_class].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"platform": "test"}}) + + debug_info_data = await debug_info.info_for_device(hass, device_entry.id) + assert len(debug_info_data["entities"]) == 0 + assert len(debug_info_data["triggers"]) == 0 diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 0ddb661cf85..9e774bfdf1e 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -14,6 +14,7 @@ from homeassistant.components.vacuum import ( ATTR_BATTERY_ICON, ATTR_BATTERY_LEVEL, ATTR_FAN_SPEED, + ATTR_FAN_SPEED_LIST, ATTR_STATUS, ) from homeassistant.const import CONF_NAME, CONF_PLATFORM, STATE_OFF, STATE_ON @@ -223,10 +224,20 @@ async def test_attributes_without_supported_features(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + message = """{ + "battery_level": 54, + "cleaning": true, + "docked": false, + "charging": false, + "fan_speed": "max" + }""" + async_fire_mqtt_message(hass, "vacuum/state", message) state = hass.states.get("vacuum.mqtttest") - assert state.state == STATE_OFF + assert state.state == STATE_ON assert state.attributes.get(ATTR_BATTERY_LEVEL) is None assert state.attributes.get(ATTR_BATTERY_ICON) is None + assert state.attributes.get(ATTR_FAN_SPEED) is None + assert state.attributes.get(ATTR_FAN_SPEED_LIST) is None async def test_status(hass, mqtt_mock): @@ -353,6 +364,36 @@ async def test_status_fan_speed(hass, mqtt_mock): assert state.attributes.get(ATTR_FAN_SPEED) == "max" +async def test_status_fan_speed_list(hass, mqtt_mock): + """Test status updates from the vacuum.""" + config = deepcopy(DEFAULT_CONFIG) + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( + ALL_SERVICES, SERVICE_TO_STRING + ) + + assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + + state = hass.states.get("vacuum.mqtttest") + assert state.attributes.get(ATTR_FAN_SPEED_LIST) == ["min", "medium", "high", "max"] + + +async def test_status_no_fan_speed_list(hass, mqtt_mock): + """Test status updates from the vacuum. + + If the vacuum doesn't support fan speed, fan speed list should be None. + """ + config = deepcopy(DEFAULT_CONFIG) + services = ALL_SERVICES - mqttvacuum.SUPPORT_FAN_SPEED + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( + services, SERVICE_TO_STRING + ) + + assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + + state = hass.states.get("vacuum.mqtttest") + assert state.attributes.get(ATTR_FAN_SPEED_LIST) is None + + async def test_status_error(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG)