diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 64d8c27f1de..524448e02a8 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -22,10 +22,6 @@ ABBREVIATIONS = { "bri_tpl": "brightness_template", "bri_val_tpl": "brightness_value_template", "clr_temp_cmd_tpl": "color_temp_command_template", - "bat_lev_t": "battery_level_topic", - "bat_lev_tpl": "battery_level_template", - "chrg_t": "charging_topic", - "chrg_tpl": "charging_template", "clrm": "color_mode", "clrm_stat_t": "color_mode_state_topic", "clrm_val_tpl": "color_mode_value_template", @@ -33,8 +29,6 @@ ABBREVIATIONS = { "clr_temp_stat_t": "color_temp_state_topic", "clr_temp_tpl": "color_temp_template", "clr_temp_val_tpl": "color_temp_value_template", - "cln_t": "cleaning_topic", - "cln_tpl": "cleaning_template", "cmd_off_tpl": "command_off_template", "cmd_on_tpl": "command_on_template", "cmd_t": "command_topic", @@ -54,19 +48,13 @@ ABBREVIATIONS = { "dir_cmd_tpl": "direction_command_template", "dir_stat_t": "direction_state_topic", "dir_val_tpl": "direction_value_template", - "dock_t": "docked_topic", - "dock_tpl": "docked_template", "dock_cmd_t": "dock_command_topic", "dock_cmd_tpl": "dock_command_template", "e": "encoding", "en": "enabled_by_default", "ent_cat": "entity_category", "ent_pic": "entity_picture", - "err_t": "error_topic", - "err_tpl": "error_template", "evt_typ": "event_types", - "fanspd_t": "fan_speed_topic", - "fanspd_tpl": "fan_speed_template", "fanspd_lst": "fan_speed_list", "flsh_tlng": "flash_time_long", "flsh_tsht": "flash_time_short", @@ -160,7 +148,6 @@ ABBREVIATIONS = { "pl_rst_pr_mode": "payload_reset_preset_mode", "pl_stop": "payload_stop", "pl_strt": "payload_start", - "pl_stpa": "payload_start_pause", "pl_ret": "payload_return_to_base", "pl_toff": "payload_turn_off", "pl_ton": "payload_turn_on", diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 5cd7676115b..ce892e97026 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -1,12 +1,8 @@ { "issues": { - "deprecation_mqtt_legacy_vacuum_yaml": { - "title": "MQTT vacuum entities with legacy schema found in your configuration.yaml", - "description": "MQTT vacuum entities that use the legacy schema are deprecated, please adjust your configuration.yaml and restart Home Assistant to fix this issue." - }, - "deprecation_mqtt_legacy_vacuum_discovery": { - "title": "MQTT vacuum entities with legacy schema added through MQTT discovery", - "description": "MQTT vacuum entities that use the legacy schema are deprecated, please adjust your devices to use the correct schema and restart Home Assistant to fix this issue." + "deprecation_mqtt_schema_vacuum_yaml": { + "title": "MQTT vacuum entities with deprecated `schema` config settings found in your configuration.yaml", + "description": "The `schema` setting for MQTT vacuum entities is deprecated and should be removed. Please adjust your configuration.yaml and restart Home Assistant to fix this issue." }, "deprecated_climate_aux_property": { "title": "MQTT entities with auxiliary heat support found", diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum.py similarity index 65% rename from homeassistant/components/mqtt/vacuum/schema_state.py rename to homeassistant/components/mqtt/vacuum.py index a51429f0c05..96c0871e27b 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum.py @@ -1,10 +1,19 @@ -"""Support for a State MQTT vacuum.""" +"""Support for MQTT vacuums.""" + +# The legacy schema for MQTT vacuum was deprecated with HA Core 2023.8.0 +# and was removed with HA Core 2024.2.0 +# The use of the schema attribute with MQTT vacuum was deprecated with HA Core 2024.2 +# the attribute will be remove with HA Core 2024.8 + from __future__ import annotations +from collections.abc import Callable +import logging from typing import Any, cast import voluptuous as vol +from homeassistant.components import vacuum from homeassistant.components.vacuum import ( ENTITY_ID_FORMAT, STATE_CLEANING, @@ -21,58 +30,37 @@ from homeassistant.const import ( STATE_IDLE, STATE_PAUSED, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, async_get_hass, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.json import json_dumps from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.json import json_loads_object -from .. import subscription -from ..config import MQTT_BASE_SCHEMA -from ..const import ( +from . import subscription +from .config import MQTT_BASE_SCHEMA +from .const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN, + CONF_SCHEMA, CONF_STATE_TOPIC, + DOMAIN, ) -from ..debug_info import log_messages -from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, write_state_on_attr_change -from ..models import ReceiveMessage -from ..util import valid_publish_topic -from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED -from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services - -SERVICE_TO_STRING: dict[VacuumEntityFeature, str] = { - VacuumEntityFeature.START: "start", - VacuumEntityFeature.PAUSE: "pause", - VacuumEntityFeature.STOP: "stop", - VacuumEntityFeature.RETURN_HOME: "return_home", - VacuumEntityFeature.FAN_SPEED: "fan_speed", - VacuumEntityFeature.BATTERY: "battery", - VacuumEntityFeature.STATUS: "status", - VacuumEntityFeature.SEND_COMMAND: "send_command", - VacuumEntityFeature.LOCATE: "locate", - VacuumEntityFeature.CLEAN_SPOT: "clean_spot", -} - -STRING_TO_SERVICE = {v: k for k, v in SERVICE_TO_STRING.items()} - - -DEFAULT_SERVICES = ( - VacuumEntityFeature.START - | VacuumEntityFeature.STOP - | VacuumEntityFeature.RETURN_HOME - | VacuumEntityFeature.BATTERY - | VacuumEntityFeature.CLEAN_SPOT -) -ALL_SERVICES = ( - DEFAULT_SERVICES - | VacuumEntityFeature.PAUSE - | VacuumEntityFeature.LOCATE - | VacuumEntityFeature.FAN_SPEED - | VacuumEntityFeature.SEND_COMMAND +from .debug_info import log_messages +from .mixins import ( + MQTT_ENTITY_COMMON_SCHEMA, + MqttEntity, + async_setup_entity_entry_helper, + write_state_on_attr_change, ) +from .models import ReceiveMessage +from .util import valid_publish_topic + +LEGACY = "legacy" +STATE = "state" BATTERY = "battery_level" FAN_SPEED = "fan_speed" @@ -102,7 +90,7 @@ CONF_SEND_COMMAND_TOPIC = "send_command_topic" DEFAULT_NAME = "MQTT State Vacuum" DEFAULT_RETAIN = False -DEFAULT_SERVICE_STRINGS = services_to_strings(DEFAULT_SERVICES, SERVICE_TO_STRING) + DEFAULT_PAYLOAD_RETURN_TO_BASE = "return_to_base" DEFAULT_PAYLOAD_STOP = "stop" DEFAULT_PAYLOAD_CLEAN_SPOT = "clean_spot" @@ -110,6 +98,52 @@ DEFAULT_PAYLOAD_LOCATE = "locate" DEFAULT_PAYLOAD_START = "start" DEFAULT_PAYLOAD_PAUSE = "pause" +_LOGGER = logging.getLogger(__name__) + +SERVICE_TO_STRING: dict[VacuumEntityFeature, str] = { + VacuumEntityFeature.START: "start", + VacuumEntityFeature.PAUSE: "pause", + VacuumEntityFeature.STOP: "stop", + VacuumEntityFeature.RETURN_HOME: "return_home", + VacuumEntityFeature.FAN_SPEED: "fan_speed", + VacuumEntityFeature.BATTERY: "battery", + VacuumEntityFeature.STATUS: "status", + VacuumEntityFeature.SEND_COMMAND: "send_command", + VacuumEntityFeature.LOCATE: "locate", + VacuumEntityFeature.CLEAN_SPOT: "clean_spot", +} + +STRING_TO_SERVICE = {v: k for k, v in SERVICE_TO_STRING.items()} +DEFAULT_SERVICES = ( + VacuumEntityFeature.START + | VacuumEntityFeature.STOP + | VacuumEntityFeature.RETURN_HOME + | VacuumEntityFeature.BATTERY + | VacuumEntityFeature.CLEAN_SPOT +) +ALL_SERVICES = ( + DEFAULT_SERVICES + | VacuumEntityFeature.PAUSE + | VacuumEntityFeature.LOCATE + | VacuumEntityFeature.FAN_SPEED + | VacuumEntityFeature.SEND_COMMAND +) + + +def services_to_strings( + services: VacuumEntityFeature, + service_to_string: dict[VacuumEntityFeature, str], +) -> list[str]: + """Convert SUPPORT_* service bitmask to list of service strings.""" + return [ + service_to_string[service] + for service in service_to_string + if service & services + ] + + +DEFAULT_SERVICE_STRINGS = services_to_strings(DEFAULT_SERVICES, SERVICE_TO_STRING) + _FEATURE_PAYLOADS = { VacuumEntityFeature.START: CONF_PAYLOAD_START, VacuumEntityFeature.STOP: CONF_PAYLOAD_STOP, @@ -119,40 +153,105 @@ _FEATURE_PAYLOADS = { VacuumEntityFeature.RETURN_HOME: CONF_PAYLOAD_RETURN_TO_BASE, } -PLATFORM_SCHEMA_STATE_MODERN = ( - MQTT_BASE_SCHEMA.extend( - { - vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_NAME): vol.Any(cv.string, None), - vol.Optional( - CONF_PAYLOAD_CLEAN_SPOT, default=DEFAULT_PAYLOAD_CLEAN_SPOT - ): cv.string, - vol.Optional( - CONF_PAYLOAD_LOCATE, default=DEFAULT_PAYLOAD_LOCATE - ): cv.string, - vol.Optional( - CONF_PAYLOAD_RETURN_TO_BASE, default=DEFAULT_PAYLOAD_RETURN_TO_BASE - ): cv.string, - vol.Optional(CONF_PAYLOAD_START, default=DEFAULT_PAYLOAD_START): cv.string, - vol.Optional(CONF_PAYLOAD_PAUSE, default=DEFAULT_PAYLOAD_PAUSE): cv.string, - vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string, - vol.Optional(CONF_SEND_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_SET_FAN_SPEED_TOPIC): valid_publish_topic, - vol.Optional(CONF_STATE_TOPIC): valid_publish_topic, - vol.Optional( - CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS - ): vol.All(cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())]), - vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - } - ) - .extend(MQTT_ENTITY_COMMON_SCHEMA.schema) - .extend(MQTT_VACUUM_SCHEMA.schema) +MQTT_VACUUM_ATTRIBUTES_BLOCKED = frozenset( + { + vacuum.ATTR_BATTERY_ICON, + vacuum.ATTR_BATTERY_LEVEL, + vacuum.ATTR_FAN_SPEED, + } ) -DISCOVERY_SCHEMA_STATE = PLATFORM_SCHEMA_STATE_MODERN.extend({}, extra=vol.REMOVE_EXTRA) +MQTT_VACUUM_DOCS_URL = "https://www.home-assistant.io/integrations/vacuum.mqtt/" + + +def _fail_legacy_config(discovery: bool) -> Callable[[ConfigType], ConfigType]: + @callback + def _fail_legacy_config_callback(config: ConfigType) -> ConfigType: + """Fail the legacy schema.""" + if CONF_SCHEMA not in config: + return config + + if config[CONF_SCHEMA] == "legacy": + raise vol.Invalid( + "The support for the `legacy` MQTT vacuum schema has been removed" + ) + + if discovery: + return config + + translation_key = "deprecation_mqtt_schema_vacuum_yaml" + hass = async_get_hass() + async_create_issue( + hass, + DOMAIN, + translation_key, + breaks_in_ha_version="2024.8.0", + is_fixable=False, + translation_key=translation_key, + learn_more_url=MQTT_VACUUM_DOCS_URL, + severity=IssueSeverity.WARNING, + ) + return config + + return _fail_legacy_config_callback + + +VACUUM_BASE_SCHEMA = MQTT_BASE_SCHEMA.extend( + { + vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_NAME): vol.Any(cv.string, None), + vol.Optional( + CONF_PAYLOAD_CLEAN_SPOT, default=DEFAULT_PAYLOAD_CLEAN_SPOT + ): cv.string, + vol.Optional(CONF_PAYLOAD_LOCATE, default=DEFAULT_PAYLOAD_LOCATE): cv.string, + vol.Optional( + CONF_PAYLOAD_RETURN_TO_BASE, default=DEFAULT_PAYLOAD_RETURN_TO_BASE + ): cv.string, + vol.Optional(CONF_PAYLOAD_START, default=DEFAULT_PAYLOAD_START): cv.string, + vol.Optional(CONF_PAYLOAD_PAUSE, default=DEFAULT_PAYLOAD_PAUSE): cv.string, + vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string, + vol.Optional(CONF_SEND_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_SET_FAN_SPEED_TOPIC): valid_publish_topic, + vol.Optional(CONF_STATE_TOPIC): valid_publish_topic, + vol.Optional(CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS): vol.All( + cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())] + ), + vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_SCHEMA): vol.All(vol.Lower, vol.Any(LEGACY, STATE)), + } +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) + +DISCOVERY_SCHEMA = vol.All( + _fail_legacy_config(discovery=True), + VACUUM_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), + cv.deprecated(CONF_SCHEMA), +) + +PLATFORM_SCHEMA_MODERN = vol.All( + _fail_legacy_config(discovery=False), + VACUUM_BASE_SCHEMA, + cv.deprecated(CONF_SCHEMA), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up MQTT vacuum through YAML and through MQTT discovery.""" + await async_setup_entity_entry_helper( + hass, + config_entry, + MqttStateVacuum, + vacuum.DOMAIN, + async_add_entities, + DISCOVERY_SCHEMA, + PLATFORM_SCHEMA_MODERN, + ) class MqttStateVacuum(MqttEntity, StateVacuumEntity): @@ -182,12 +281,22 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): @staticmethod def config_schema() -> vol.Schema: """Return the config schema.""" - return DISCOVERY_SCHEMA_STATE + return DISCOVERY_SCHEMA def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" + + def _strings_to_services( + strings: list[str], string_to_service: dict[str, VacuumEntityFeature] + ) -> VacuumEntityFeature: + """Convert service strings to SUPPORT_* service bitmask.""" + services = VacuumEntityFeature.STATE + for string in strings: + services |= string_to_service[string] + return services + supported_feature_strings: list[str] = config[CONF_SUPPORTED_FEATURES] - self._attr_supported_features = VacuumEntityFeature.STATE | strings_to_services( + self._attr_supported_features = _strings_to_services( supported_feature_strings, STRING_TO_SERVICE ) self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST] diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py deleted file mode 100644 index fabbb9868df..00000000000 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Support for MQTT vacuums.""" - -# The legacy schema for MQTT vacuum was deprecated with HA Core 2023.8.0 -# and will be removed with HA Core 2024.2.0 - -from __future__ import annotations - -import logging - -import voluptuous as vol - -from homeassistant.components import vacuum -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, async_get_hass, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType - -from ..const import DOMAIN -from ..mixins import async_setup_entity_entry_helper -from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE -from .schema_legacy import ( - DISCOVERY_SCHEMA_LEGACY, - PLATFORM_SCHEMA_LEGACY_MODERN, - MqttVacuum, -) -from .schema_state import ( - DISCOVERY_SCHEMA_STATE, - PLATFORM_SCHEMA_STATE_MODERN, - MqttStateVacuum, -) - -_LOGGER = logging.getLogger(__name__) - -MQTT_VACUUM_DOCS_URL = "https://www.home-assistant.io/integrations/vacuum.mqtt/" - - -# The legacy schema for MQTT vacuum was deprecated with HA Core 2023.8.0 -# and will be removed with HA Core 2024.2.0 -def warn_for_deprecation_legacy_schema( - hass: HomeAssistant, config: ConfigType, discovery: bool -) -> None: - """Warn for deprecation of legacy schema.""" - if config[CONF_SCHEMA] == STATE: - return - - key_suffix = "discovery" if discovery else "yaml" - translation_key = f"deprecation_mqtt_legacy_vacuum_{key_suffix}" - async_create_issue( - hass, - DOMAIN, - translation_key, - breaks_in_ha_version="2024.2.0", - is_fixable=False, - translation_key=translation_key, - learn_more_url=MQTT_VACUUM_DOCS_URL, - severity=IssueSeverity.WARNING, - ) - _LOGGER.warning( - "Deprecated `legacy` schema detected for MQTT vacuum, expected `state` schema, config found: %s", - config, - ) - - -@callback -def validate_mqtt_vacuum_discovery(config_value: ConfigType) -> ConfigType: - """Validate MQTT vacuum schema.""" - - # The legacy schema for MQTT vacuum was deprecated with HA Core 2023.8.0 - # and will be removed with HA Core 2024.2.0 - - schemas = {LEGACY: DISCOVERY_SCHEMA_LEGACY, STATE: DISCOVERY_SCHEMA_STATE} - config: ConfigType = schemas[config_value[CONF_SCHEMA]](config_value) - hass = async_get_hass() - warn_for_deprecation_legacy_schema(hass, config, True) - return config - - -@callback -def validate_mqtt_vacuum_modern(config_value: ConfigType) -> ConfigType: - """Validate MQTT vacuum modern schema.""" - - # The legacy schema for MQTT vacuum was deprecated with HA Core 2023.8.0 - # and will be removed with HA Core 2024.2.0 - - schemas = { - LEGACY: PLATFORM_SCHEMA_LEGACY_MODERN, - STATE: PLATFORM_SCHEMA_STATE_MODERN, - } - config: ConfigType = schemas[config_value[CONF_SCHEMA]](config_value) - # The legacy schema for MQTT vacuum was deprecated with HA Core 2023.8.0 - # and will be removed with HA Core 2024.2.0 - hass = async_get_hass() - warn_for_deprecation_legacy_schema(hass, config, False) - return config - - -DISCOVERY_SCHEMA = vol.All( - MQTT_VACUUM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_vacuum_discovery -) - -PLATFORM_SCHEMA_MODERN = vol.All( - MQTT_VACUUM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_vacuum_modern -) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up MQTT vacuum through YAML and through MQTT discovery.""" - await async_setup_entity_entry_helper( - hass, - config_entry, - None, - vacuum.DOMAIN, - async_add_entities, - DISCOVERY_SCHEMA, - PLATFORM_SCHEMA_MODERN, - {"legacy": MqttVacuum, "state": MqttStateVacuum}, - ) diff --git a/homeassistant/components/mqtt/vacuum/const.py b/homeassistant/components/mqtt/vacuum/const.py deleted file mode 100644 index 26e11125556..00000000000 --- a/homeassistant/components/mqtt/vacuum/const.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Shared constants.""" -from homeassistant.components import vacuum - -MQTT_VACUUM_ATTRIBUTES_BLOCKED = frozenset( - { - vacuum.ATTR_BATTERY_ICON, - vacuum.ATTR_BATTERY_LEVEL, - vacuum.ATTR_FAN_SPEED, - } -) diff --git a/homeassistant/components/mqtt/vacuum/schema.py b/homeassistant/components/mqtt/vacuum/schema.py deleted file mode 100644 index 78175f61255..00000000000 --- a/homeassistant/components/mqtt/vacuum/schema.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Shared schema code.""" -from __future__ import annotations - -import voluptuous as vol - -from homeassistant.components.vacuum import VacuumEntityFeature - -from ..const import CONF_SCHEMA - -LEGACY = "legacy" -STATE = "state" - -MQTT_VACUUM_SCHEMA = vol.Schema( - { - vol.Optional(CONF_SCHEMA, default=LEGACY): vol.All( - vol.Lower, vol.Any(LEGACY, STATE) - ) - } -) - - -def services_to_strings( - services: VacuumEntityFeature, - service_to_string: dict[VacuumEntityFeature, str], -) -> list[str]: - """Convert SUPPORT_* service bitmask to list of service strings.""" - return [ - service_to_string[service] - for service in service_to_string - if service & services - ] - - -def strings_to_services( - strings: list[str], string_to_service: dict[str, VacuumEntityFeature] -) -> VacuumEntityFeature: - """Convert service strings to SUPPORT_* service bitmask.""" - services = VacuumEntityFeature(0) - for string in strings: - services |= string_to_service[string] - return services diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py deleted file mode 100644 index ab13de59ede..00000000000 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ /dev/null @@ -1,496 +0,0 @@ -"""Support for Legacy MQTT vacuum. - -The legacy schema for MQTT vacuum was deprecated with HA Core 2023.8.0 -and is will be removed with HA Core 2024.2.0 -""" - -from __future__ import annotations - -from collections.abc import Callable -from typing import Any - -import voluptuous as vol - -from homeassistant.components.vacuum import ( - ATTR_STATUS, - ENTITY_ID_FORMAT, - VacuumEntity, - VacuumEntityFeature, -) -from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.icon import icon_for_battery_level -from homeassistant.helpers.json import json_dumps -from homeassistant.helpers.typing import ConfigType - -from .. import subscription -from ..config import MQTT_BASE_SCHEMA -from ..const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN -from ..debug_info import log_messages -from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, write_state_on_attr_change -from ..models import ( - MqttValueTemplate, - PayloadSentinel, - ReceiveMessage, - ReceivePayloadType, -) -from ..util import valid_publish_topic -from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED -from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services - -SERVICE_TO_STRING = { - VacuumEntityFeature.TURN_ON: "turn_on", - VacuumEntityFeature.TURN_OFF: "turn_off", - VacuumEntityFeature.PAUSE: "pause", - VacuumEntityFeature.STOP: "stop", - VacuumEntityFeature.RETURN_HOME: "return_home", - VacuumEntityFeature.FAN_SPEED: "fan_speed", - VacuumEntityFeature.BATTERY: "battery", - VacuumEntityFeature.STATUS: "status", - VacuumEntityFeature.SEND_COMMAND: "send_command", - VacuumEntityFeature.LOCATE: "locate", - VacuumEntityFeature.CLEAN_SPOT: "clean_spot", -} - -STRING_TO_SERVICE = {v: k for k, v in SERVICE_TO_STRING.items()} - -DEFAULT_SERVICES = ( - VacuumEntityFeature.TURN_ON - | VacuumEntityFeature.TURN_OFF - | VacuumEntityFeature.STOP - | VacuumEntityFeature.RETURN_HOME - | VacuumEntityFeature.STATUS - | VacuumEntityFeature.BATTERY - | VacuumEntityFeature.CLEAN_SPOT -) -ALL_SERVICES = ( - DEFAULT_SERVICES - | VacuumEntityFeature.PAUSE - | VacuumEntityFeature.LOCATE - | VacuumEntityFeature.FAN_SPEED - | VacuumEntityFeature.SEND_COMMAND -) - -CONF_SUPPORTED_FEATURES = ATTR_SUPPORTED_FEATURES -CONF_BATTERY_LEVEL_TEMPLATE = "battery_level_template" -CONF_BATTERY_LEVEL_TOPIC = "battery_level_topic" -CONF_CHARGING_TEMPLATE = "charging_template" -CONF_CHARGING_TOPIC = "charging_topic" -CONF_CLEANING_TEMPLATE = "cleaning_template" -CONF_CLEANING_TOPIC = "cleaning_topic" -CONF_DOCKED_TEMPLATE = "docked_template" -CONF_DOCKED_TOPIC = "docked_topic" -CONF_ERROR_TEMPLATE = "error_template" -CONF_ERROR_TOPIC = "error_topic" -CONF_FAN_SPEED_LIST = "fan_speed_list" -CONF_FAN_SPEED_TEMPLATE = "fan_speed_template" -CONF_FAN_SPEED_TOPIC = "fan_speed_topic" -CONF_PAYLOAD_CLEAN_SPOT = "payload_clean_spot" -CONF_PAYLOAD_LOCATE = "payload_locate" -CONF_PAYLOAD_RETURN_TO_BASE = "payload_return_to_base" -CONF_PAYLOAD_START_PAUSE = "payload_start_pause" -CONF_PAYLOAD_STOP = "payload_stop" -CONF_PAYLOAD_TURN_OFF = "payload_turn_off" -CONF_PAYLOAD_TURN_ON = "payload_turn_on" -CONF_SEND_COMMAND_TOPIC = "send_command_topic" -CONF_SET_FAN_SPEED_TOPIC = "set_fan_speed_topic" - -DEFAULT_NAME = "MQTT Vacuum" -DEFAULT_PAYLOAD_CLEAN_SPOT = "clean_spot" -DEFAULT_PAYLOAD_LOCATE = "locate" -DEFAULT_PAYLOAD_RETURN_TO_BASE = "return_to_base" -DEFAULT_PAYLOAD_START_PAUSE = "start_pause" -DEFAULT_PAYLOAD_STOP = "stop" -DEFAULT_PAYLOAD_TURN_OFF = "turn_off" -DEFAULT_PAYLOAD_TURN_ON = "turn_on" -DEFAULT_RETAIN = False -DEFAULT_SERVICE_STRINGS = services_to_strings(DEFAULT_SERVICES, SERVICE_TO_STRING) - -MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED = MQTT_VACUUM_ATTRIBUTES_BLOCKED | frozenset( - {ATTR_STATUS} -) - -PLATFORM_SCHEMA_LEGACY_MODERN = ( - MQTT_BASE_SCHEMA.extend( - { - vol.Inclusive(CONF_BATTERY_LEVEL_TEMPLATE, "battery"): cv.template, - vol.Inclusive(CONF_BATTERY_LEVEL_TOPIC, "battery"): valid_publish_topic, - vol.Inclusive(CONF_CHARGING_TEMPLATE, "charging"): cv.template, - vol.Inclusive(CONF_CHARGING_TOPIC, "charging"): valid_publish_topic, - vol.Inclusive(CONF_CLEANING_TEMPLATE, "cleaning"): cv.template, - vol.Inclusive(CONF_CLEANING_TOPIC, "cleaning"): valid_publish_topic, - vol.Inclusive(CONF_DOCKED_TEMPLATE, "docked"): cv.template, - vol.Inclusive(CONF_DOCKED_TOPIC, "docked"): valid_publish_topic, - vol.Inclusive(CONF_ERROR_TEMPLATE, "error"): cv.template, - vol.Inclusive(CONF_ERROR_TOPIC, "error"): valid_publish_topic, - vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Inclusive(CONF_FAN_SPEED_TEMPLATE, "fan_speed"): cv.template, - vol.Inclusive(CONF_FAN_SPEED_TOPIC, "fan_speed"): valid_publish_topic, - vol.Optional(CONF_NAME): vol.Any(cv.string, None), - vol.Optional( - CONF_PAYLOAD_CLEAN_SPOT, default=DEFAULT_PAYLOAD_CLEAN_SPOT - ): cv.string, - vol.Optional( - CONF_PAYLOAD_LOCATE, default=DEFAULT_PAYLOAD_LOCATE - ): cv.string, - vol.Optional( - CONF_PAYLOAD_RETURN_TO_BASE, default=DEFAULT_PAYLOAD_RETURN_TO_BASE - ): cv.string, - vol.Optional( - CONF_PAYLOAD_START_PAUSE, default=DEFAULT_PAYLOAD_START_PAUSE - ): cv.string, - vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string, - vol.Optional( - CONF_PAYLOAD_TURN_OFF, default=DEFAULT_PAYLOAD_TURN_OFF - ): cv.string, - vol.Optional( - CONF_PAYLOAD_TURN_ON, default=DEFAULT_PAYLOAD_TURN_ON - ): cv.string, - vol.Optional(CONF_SEND_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_SET_FAN_SPEED_TOPIC): valid_publish_topic, - vol.Optional( - CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS - ): vol.All(cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())]), - vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - } - ) - .extend(MQTT_ENTITY_COMMON_SCHEMA.schema) - .extend(MQTT_VACUUM_SCHEMA.schema) -) - -DISCOVERY_SCHEMA_LEGACY = PLATFORM_SCHEMA_LEGACY_MODERN.extend( - {}, extra=vol.REMOVE_EXTRA -) - - -_COMMANDS = { - VacuumEntityFeature.TURN_ON: { - "payload": CONF_PAYLOAD_TURN_ON, - "status": "Cleaning", - }, - VacuumEntityFeature.TURN_OFF: { - "payload": CONF_PAYLOAD_TURN_OFF, - "status": "Turning Off", - }, - VacuumEntityFeature.STOP: { - "payload": CONF_PAYLOAD_STOP, - "status": "Stopping the current task", - }, - VacuumEntityFeature.CLEAN_SPOT: { - "payload": CONF_PAYLOAD_CLEAN_SPOT, - "status": "Cleaning spot", - }, - VacuumEntityFeature.LOCATE: { - "payload": CONF_PAYLOAD_LOCATE, - "status": "Hi, I'm over here!", - }, - VacuumEntityFeature.PAUSE: { - "payload": CONF_PAYLOAD_START_PAUSE, - "status": "Pausing/Resuming cleaning...", - }, - VacuumEntityFeature.RETURN_HOME: { - "payload": CONF_PAYLOAD_RETURN_TO_BASE, - "status": "Returning home...", - }, -} - - -class MqttVacuum(MqttEntity, VacuumEntity): - """Representation of a MQTT-controlled legacy vacuum.""" - - _attr_battery_level = 0 - _attr_is_on = False - _attributes_extra_blocked = MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED - _charging: bool = False - _cleaning: bool = False - _command_topic: str | None - _docked: bool = False - _default_name = DEFAULT_NAME - _entity_id_format = ENTITY_ID_FORMAT - _encoding: str | None - _error: str | None = None - _qos: bool - _retain: bool - _payloads: dict[str, str] - _send_command_topic: str | None - _set_fan_speed_topic: str | None - _state_topics: dict[str, str | None] - _templates: dict[ - str, Callable[[ReceivePayloadType, PayloadSentinel], ReceivePayloadType] - ] - - @staticmethod - def config_schema() -> vol.Schema: - """Return the config schema.""" - return DISCOVERY_SCHEMA_LEGACY - - def _setup_from_config(self, config: ConfigType) -> None: - """(Re)Setup the entity.""" - supported_feature_strings = config[CONF_SUPPORTED_FEATURES] - self._attr_supported_features = strings_to_services( - supported_feature_strings, STRING_TO_SERVICE - ) - self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST] - self._qos = config[CONF_QOS] - self._retain = config[CONF_RETAIN] - self._encoding = config[CONF_ENCODING] or None - - self._command_topic = config.get(CONF_COMMAND_TOPIC) - self._set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC) - self._send_command_topic = config.get(CONF_SEND_COMMAND_TOPIC) - - self._payloads = { - key: config[key] - for key in ( - CONF_PAYLOAD_TURN_ON, - CONF_PAYLOAD_TURN_OFF, - CONF_PAYLOAD_RETURN_TO_BASE, - CONF_PAYLOAD_STOP, - CONF_PAYLOAD_CLEAN_SPOT, - CONF_PAYLOAD_LOCATE, - CONF_PAYLOAD_START_PAUSE, - ) - } - self._state_topics = { - key: config.get(key) - for key in ( - CONF_BATTERY_LEVEL_TOPIC, - CONF_CHARGING_TOPIC, - CONF_CLEANING_TOPIC, - CONF_DOCKED_TOPIC, - CONF_ERROR_TOPIC, - CONF_FAN_SPEED_TOPIC, - ) - } - self._templates = { - key: MqttValueTemplate( - config[key], entity=self - ).async_render_with_possible_json_value - for key in ( - CONF_BATTERY_LEVEL_TEMPLATE, - CONF_CHARGING_TEMPLATE, - CONF_CLEANING_TEMPLATE, - CONF_DOCKED_TEMPLATE, - CONF_ERROR_TEMPLATE, - CONF_FAN_SPEED_TEMPLATE, - ) - if key in config - } - - def _prepare_subscribe_topics(self) -> None: - """(Re)Subscribe to topics.""" - - @callback - @log_messages(self.hass, self.entity_id) - @write_state_on_attr_change( - self, - { - "_attr_battery_level", - "_attr_fan_speed", - "_attr_is_on", - # We track _attr_status and _charging as they are used to - # To determine the batery_icon. - # We do not need to track _docked as it is - # not leading to entity changes directly. - "_attr_status", - "_charging", - }, - ) - def message_received(msg: ReceiveMessage) -> None: - """Handle new MQTT message.""" - if ( - msg.topic == self._state_topics[CONF_BATTERY_LEVEL_TOPIC] - and CONF_BATTERY_LEVEL_TEMPLATE in self._config - ): - battery_level = self._templates[CONF_BATTERY_LEVEL_TEMPLATE]( - msg.payload, PayloadSentinel.DEFAULT - ) - if battery_level and battery_level is not PayloadSentinel.DEFAULT: - self._attr_battery_level = max(0, min(100, int(battery_level))) - - if ( - msg.topic == self._state_topics[CONF_CHARGING_TOPIC] - and CONF_CHARGING_TEMPLATE in self._templates - ): - charging = self._templates[CONF_CHARGING_TEMPLATE]( - msg.payload, PayloadSentinel.DEFAULT - ) - if charging and charging is not PayloadSentinel.DEFAULT: - self._charging = cv.boolean(charging) - - if ( - msg.topic == self._state_topics[CONF_CLEANING_TOPIC] - and CONF_CLEANING_TEMPLATE in self._config - ): - cleaning = self._templates[CONF_CLEANING_TEMPLATE]( - msg.payload, PayloadSentinel.DEFAULT - ) - if cleaning and cleaning is not PayloadSentinel.DEFAULT: - self._attr_is_on = cv.boolean(cleaning) - - if ( - msg.topic == self._state_topics[CONF_DOCKED_TOPIC] - and CONF_DOCKED_TEMPLATE in self._config - ): - docked = self._templates[CONF_DOCKED_TEMPLATE]( - msg.payload, PayloadSentinel.DEFAULT - ) - if docked and docked is not PayloadSentinel.DEFAULT: - self._docked = cv.boolean(docked) - - if ( - msg.topic == self._state_topics[CONF_ERROR_TOPIC] - and CONF_ERROR_TEMPLATE in self._config - ): - error = self._templates[CONF_ERROR_TEMPLATE]( - msg.payload, PayloadSentinel.DEFAULT - ) - if error is not PayloadSentinel.DEFAULT: - self._error = cv.string(error) - - if self._docked: - if self._charging: - self._attr_status = "Docked & Charging" - else: - self._attr_status = "Docked" - elif self.is_on: - self._attr_status = "Cleaning" - elif self._error: - self._attr_status = f"Error: {self._error}" - else: - self._attr_status = "Stopped" - - if ( - msg.topic == self._state_topics[CONF_FAN_SPEED_TOPIC] - and CONF_FAN_SPEED_TEMPLATE in self._config - ): - fan_speed = self._templates[CONF_FAN_SPEED_TEMPLATE]( - msg.payload, PayloadSentinel.DEFAULT - ) - if fan_speed and fan_speed is not PayloadSentinel.DEFAULT: - self._attr_fan_speed = str(fan_speed) - - topics_list = {topic for topic in self._state_topics.values() if topic} - self._sub_state = subscription.async_prepare_subscribe_topics( - self.hass, - self._sub_state, - { - f"topic{i}": { - "topic": topic, - "msg_callback": message_received, - "qos": self._qos, - "encoding": self._encoding, - } - for i, topic in enumerate(topics_list) - }, - ) - - async def _subscribe_topics(self) -> None: - """(Re)Subscribe to topics.""" - await subscription.async_subscribe_topics(self.hass, self._sub_state) - - @property - def battery_icon(self) -> str: - """Return the battery icon for the vacuum cleaner. - - No need to check VacuumEntityFeature.BATTERY, this won't be called if - battery_level is None. - """ - return icon_for_battery_level( - battery_level=self.battery_level, charging=self._charging - ) - - async def _async_publish_command(self, feature: VacuumEntityFeature) -> None: - """Publish a command.""" - - if self._command_topic is None: - return - - await self.async_publish( - self._command_topic, - self._payloads[_COMMANDS[feature]["payload"]], - qos=self._qos, - retain=self._retain, - encoding=self._encoding, - ) - self._attr_status = _COMMANDS[feature]["status"] - self.async_write_ha_state() - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the vacuum on.""" - await self._async_publish_command(VacuumEntityFeature.TURN_ON) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the vacuum off.""" - await self._async_publish_command(VacuumEntityFeature.TURN_OFF) - - async def async_stop(self, **kwargs: Any) -> None: - """Stop the vacuum.""" - await self._async_publish_command(VacuumEntityFeature.STOP) - - async def async_clean_spot(self, **kwargs: Any) -> None: - """Perform a spot clean-up.""" - await self._async_publish_command(VacuumEntityFeature.CLEAN_SPOT) - - async def async_locate(self, **kwargs: Any) -> None: - """Locate the vacuum (usually by playing a song).""" - await self._async_publish_command(VacuumEntityFeature.LOCATE) - - async def async_start_pause(self, **kwargs: Any) -> None: - """Start, pause or resume the cleaning task.""" - await self._async_publish_command(VacuumEntityFeature.PAUSE) - - async def async_return_to_base(self, **kwargs: Any) -> None: - """Tell the vacuum to return to its dock.""" - await self._async_publish_command(VacuumEntityFeature.RETURN_HOME) - - async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: - """Set fan speed.""" - if ( - self._set_fan_speed_topic is None - or (self.supported_features & VacuumEntityFeature.FAN_SPEED == 0) - or fan_speed not in self.fan_speed_list - ): - return None - - await self.async_publish( - self._set_fan_speed_topic, - fan_speed, - self._qos, - self._retain, - self._encoding, - ) - self._attr_status = f"Setting fan to {fan_speed}..." - self.async_write_ha_state() - - async def async_send_command( - self, - command: str, - params: dict[str, Any] | list[Any] | None = None, - **kwargs: Any, - ) -> None: - """Send a command to a vacuum cleaner.""" - if ( - self._send_command_topic is None - or self.supported_features & VacuumEntityFeature.SEND_COMMAND == 0 - ): - return - if params: - message: dict[str, Any] = {"command": command} - message.update(params) - message_payload = json_dumps(message) - else: - message_payload = command - await self.async_publish( - self._send_command_topic, - message_payload, - self._qos, - self._retain, - self._encoding, - ) - self._attr_status = f"Sending command {message_payload}..." - self.async_write_ha_state() diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 017d24a39ce..9acd15eea7c 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -481,11 +481,11 @@ async def test_discover_alarm_control_panel( "vacuum", ), ( - "homeassistant/vacuum/object/bla/config", - '{ "name": "Hello World 17", "obj_id": "hello_id", "state_topic": "test-topic", "schema": "legacy" }', - "vacuum.hello_id", + "homeassistant/valve/object/bla/config", + '{ "name": "Hello World 17", "obj_id": "hello_id", "state_topic": "test-topic" }', + "valve.hello_id", "Hello World 17", - "vacuum", + "valve", ), ( "homeassistant/lock/object/bla/config", diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 61a27c287ac..3e88d4a4335 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -1,125 +1,23 @@ """The tests for the Legacy Mqtt vacuum platform.""" # The legacy schema for MQTT vacuum was deprecated with HA Core 2023.8.0 -# and will be removed with HA Core 2024.2.0 +# and was removed with HA Core 2024.2.0 +# cleanup is planned with HA Core 2025.2 -from copy import deepcopy import json -from typing import Any from unittest.mock import patch import pytest from homeassistant.components import mqtt, vacuum -from homeassistant.components.mqtt.const import CONF_COMMAND_TOPIC -from homeassistant.components.mqtt.vacuum import schema_legacy as mqttvacuum -from homeassistant.components.mqtt.vacuum.schema import services_to_strings -from homeassistant.components.mqtt.vacuum.schema_legacy import ( - ALL_SERVICES, - CONF_BATTERY_LEVEL_TOPIC, - CONF_CHARGING_TOPIC, - CONF_CLEANING_TOPIC, - CONF_DOCKED_TOPIC, - CONF_ERROR_TOPIC, - CONF_FAN_SPEED_TOPIC, - CONF_SUPPORTED_FEATURES, - MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED, - SERVICE_TO_STRING, -) -from homeassistant.components.vacuum import ( - ATTR_BATTERY_ICON, - ATTR_BATTERY_LEVEL, - ATTR_FAN_SPEED, - ATTR_FAN_SPEED_LIST, - ATTR_STATUS, - VacuumEntityFeature, -) -from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.typing import ConfigType - -from .test_common import ( - help_custom_config, - help_test_availability_when_connection_lost, - help_test_availability_without_topic, - help_test_custom_availability_payload, - help_test_default_availability_payload, - help_test_discovery_broken, - help_test_discovery_removal, - help_test_discovery_update, - help_test_discovery_update_attr, - help_test_discovery_update_unchanged, - help_test_encoding_subscribable_topics, - help_test_entity_debug_info_message, - help_test_entity_device_info_remove, - help_test_entity_device_info_update, - help_test_entity_device_info_with_connection, - help_test_entity_device_info_with_identifier, - help_test_entity_id_update_discovery_update, - help_test_entity_id_update_subscriptions, - help_test_publishing_with_custom_encoding, - help_test_reloadable, - help_test_setting_attribute_via_mqtt_json_message, - help_test_setting_attribute_with_template, - help_test_setting_blocked_attribute_via_mqtt_json_message, - help_test_skipped_async_ha_write_state, - help_test_unique_id, - help_test_update_with_json_attrs_bad_json, - help_test_update_with_json_attrs_not_dict, -) +from homeassistant.helpers.typing import DiscoveryInfoType from tests.common import async_fire_mqtt_message -from tests.components.vacuum import common -from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient +from tests.typing import MqttMockHAClientGenerator -DEFAULT_CONFIG = { - mqtt.DOMAIN: { - vacuum.DOMAIN: { - CONF_NAME: "mqtttest", - CONF_COMMAND_TOPIC: "vacuum/command", - mqttvacuum.CONF_SEND_COMMAND_TOPIC: "vacuum/send_command", - mqttvacuum.CONF_BATTERY_LEVEL_TOPIC: "vacuum/state", - mqttvacuum.CONF_BATTERY_LEVEL_TEMPLATE: "{{ value_json.battery_level }}", - mqttvacuum.CONF_CHARGING_TOPIC: "vacuum/state", - mqttvacuum.CONF_CHARGING_TEMPLATE: "{{ value_json.charging }}", - mqttvacuum.CONF_CLEANING_TOPIC: "vacuum/state", - mqttvacuum.CONF_CLEANING_TEMPLATE: "{{ value_json.cleaning }}", - mqttvacuum.CONF_DOCKED_TOPIC: "vacuum/state", - mqttvacuum.CONF_DOCKED_TEMPLATE: "{{ value_json.docked }}", - mqttvacuum.CONF_ERROR_TOPIC: "vacuum/state", - mqttvacuum.CONF_ERROR_TEMPLATE: "{{ value_json.error }}", - mqttvacuum.CONF_FAN_SPEED_TOPIC: "vacuum/state", - mqttvacuum.CONF_FAN_SPEED_TEMPLATE: "{{ value_json.fan_speed }}", - mqttvacuum.CONF_SET_FAN_SPEED_TOPIC: "vacuum/set_fan_speed", - mqttvacuum.CONF_FAN_SPEED_LIST: ["min", "medium", "high", "max"], - } - } -} - -DEFAULT_CONFIG_2 = {mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test"}}} - -DEFAULT_CONFIG_ALL_SERVICES = help_custom_config( - vacuum.DOMAIN, - DEFAULT_CONFIG, - ( - { - mqttvacuum.CONF_SUPPORTED_FEATURES: services_to_strings( - ALL_SERVICES, SERVICE_TO_STRING - ) - }, - ), -) - - -def filter_options(default_config: ConfigType, options: set[str]) -> ConfigType: - """Generate a config from a default config with omitted options.""" - options_base: ConfigType = default_config[mqtt.DOMAIN][vacuum.DOMAIN] - config = deepcopy(default_config) - config[mqtt.DOMAIN][vacuum.DOMAIN] = { - key: value for key, value in options_base.items() if key not in options - } - return config +DEFAULT_CONFIG = {mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test"}}} @pytest.fixture(autouse=True) @@ -130,1009 +28,62 @@ def vacuum_platform_only(): @pytest.mark.parametrize( - ("hass_config", "deprecated"), + ("hass_config", "removed"), [ ({mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test", "schema": "legacy"}}}, True), - ({mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test"}}}, True), + ({mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test"}}}, False), ({mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test", "schema": "state"}}}, False), ], ) -async def test_deprecation( +async def test_removed_support_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, - deprecated: bool, + removed: bool, ) -> None: - """Test that the depration warning for the legacy schema works.""" + """Test that the removed support validation for the legacy schema works.""" assert await mqtt_mock_entry() entity = hass.states.get("vacuum.test") - assert entity is not None - if deprecated: - assert "Deprecated `legacy` schema detected for MQTT vacuum" in caplog.text + if removed: + assert entity is None + assert ( + "The support for the `legacy` MQTT " + "vacuum schema has been removed" in caplog.text + ) else: - assert "Deprecated `legacy` schema detected for MQTT vacuum" not in caplog.text - - -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) -async def test_default_supported_features( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test that the correct supported features.""" - await mqtt_mock_entry() - entity = hass.states.get("vacuum.mqtttest") - entity_features = entity.attributes.get(mqttvacuum.CONF_SUPPORTED_FEATURES, 0) - assert sorted(services_to_strings(entity_features, SERVICE_TO_STRING)) == sorted( - [ - "turn_on", - "turn_off", - "stop", - "return_home", - "battery", - "status", - "clean_spot", - ] - ) + assert entity is not None @pytest.mark.parametrize( - "hass_config", - [DEFAULT_CONFIG_ALL_SERVICES], -) -async def test_all_commands( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test simple commands to the vacuum.""" - mqtt_mock = await mqtt_mock_entry() - - await common.async_turn_on(hass, "vacuum.mqtttest") - mqtt_mock.async_publish.assert_called_once_with( - "vacuum/command", "turn_on", 0, False - ) - mqtt_mock.async_publish.reset_mock() - - await common.async_turn_off(hass, "vacuum.mqtttest") - mqtt_mock.async_publish.assert_called_once_with( - "vacuum/command", "turn_off", 0, False - ) - mqtt_mock.async_publish.reset_mock() - - await common.async_stop(hass, "vacuum.mqtttest") - mqtt_mock.async_publish.assert_called_once_with("vacuum/command", "stop", 0, False) - mqtt_mock.async_publish.reset_mock() - - await common.async_clean_spot(hass, "vacuum.mqtttest") - mqtt_mock.async_publish.assert_called_once_with( - "vacuum/command", "clean_spot", 0, False - ) - mqtt_mock.async_publish.reset_mock() - - await common.async_locate(hass, "vacuum.mqtttest") - mqtt_mock.async_publish.assert_called_once_with( - "vacuum/command", "locate", 0, False - ) - mqtt_mock.async_publish.reset_mock() - - await common.async_start_pause(hass, "vacuum.mqtttest") - mqtt_mock.async_publish.assert_called_once_with( - "vacuum/command", "start_pause", 0, False - ) - mqtt_mock.async_publish.reset_mock() - - await common.async_return_to_base(hass, "vacuum.mqtttest") - mqtt_mock.async_publish.assert_called_once_with( - "vacuum/command", "return_to_base", 0, False - ) - mqtt_mock.async_publish.reset_mock() - - await common.async_set_fan_speed(hass, "high", "vacuum.mqtttest") - mqtt_mock.async_publish.assert_called_once_with( - "vacuum/set_fan_speed", "high", 0, False - ) - mqtt_mock.async_publish.reset_mock() - - await common.async_send_command(hass, "44 FE 93", entity_id="vacuum.mqtttest") - mqtt_mock.async_publish.assert_called_once_with( - "vacuum/send_command", "44 FE 93", 0, False - ) - mqtt_mock.async_publish.reset_mock() - - await common.async_send_command( - hass, "44 FE 93", {"key": "value"}, entity_id="vacuum.mqtttest" - ) - assert json.loads(mqtt_mock.async_publish.mock_calls[-1][1][1]) == { - "command": "44 FE 93", - "key": "value", - } - - await common.async_send_command( - hass, "44 FE 93", {"key": "value"}, entity_id="vacuum.mqtttest" - ) - assert json.loads(mqtt_mock.async_publish.mock_calls[-1][1][1]) == { - "command": "44 FE 93", - "key": "value", - } - - -@pytest.mark.parametrize( - "hass_config", + ("config", "removed"), [ - help_custom_config( - vacuum.DOMAIN, - DEFAULT_CONFIG, - ( - { - mqttvacuum.CONF_SUPPORTED_FEATURES: services_to_strings( - mqttvacuum.STRING_TO_SERVICE["status"], SERVICE_TO_STRING - ) - }, - ), - ) + ({"name": "test", "schema": "legacy"}, True), + ({"name": "test"}, False), + ({"name": "test", "schema": "state"}, False), ], ) -async def test_commands_without_supported_features( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test commands which are not supported by the vacuum.""" - mqtt_mock = await mqtt_mock_entry() - - with pytest.raises(HomeAssistantError): - await common.async_turn_on(hass, "vacuum.mqtttest") - mqtt_mock.async_publish.assert_not_called() - mqtt_mock.async_publish.reset_mock() - - with pytest.raises(HomeAssistantError): - await common.async_turn_off(hass, "vacuum.mqtttest") - mqtt_mock.async_publish.assert_not_called() - mqtt_mock.async_publish.reset_mock() - - with pytest.raises(HomeAssistantError): - await common.async_stop(hass, "vacuum.mqtttest") - mqtt_mock.async_publish.assert_not_called() - mqtt_mock.async_publish.reset_mock() - - with pytest.raises(HomeAssistantError): - await common.async_clean_spot(hass, "vacuum.mqtttest") - mqtt_mock.async_publish.assert_not_called() - mqtt_mock.async_publish.reset_mock() - - with pytest.raises(HomeAssistantError): - await common.async_locate(hass, "vacuum.mqtttest") - mqtt_mock.async_publish.assert_not_called() - mqtt_mock.async_publish.reset_mock() - - with pytest.raises(HomeAssistantError): - await common.async_start_pause(hass, "vacuum.mqtttest") - mqtt_mock.async_publish.assert_not_called() - mqtt_mock.async_publish.reset_mock() - - with pytest.raises(HomeAssistantError): - await common.async_return_to_base(hass, "vacuum.mqtttest") - mqtt_mock.async_publish.assert_not_called() - mqtt_mock.async_publish.reset_mock() - - with pytest.raises(HomeAssistantError): - await common.async_set_fan_speed(hass, "high", "vacuum.mqtttest") - mqtt_mock.async_publish.assert_not_called() - mqtt_mock.async_publish.reset_mock() - - with pytest.raises(HomeAssistantError): - await common.async_send_command(hass, "44 FE 93", entity_id="vacuum.mqtttest") - mqtt_mock.async_publish.assert_not_called() - mqtt_mock.async_publish.reset_mock() - - -@pytest.mark.parametrize( - "hass_config", - [ - { - "mqtt": { - "vacuum": { - "name": "test", - "schema": "legacy", - mqttvacuum.CONF_SUPPORTED_FEATURES: services_to_strings( - ALL_SERVICES, SERVICE_TO_STRING - ), - } - } - } - ], -) -async def test_command_without_command_topic( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test commands which are not supported by the vacuum.""" - mqtt_mock = await mqtt_mock_entry() - - await common.async_turn_on(hass, "vacuum.test") - mqtt_mock.async_publish.assert_not_called() - mqtt_mock.async_publish.reset_mock() - - await common.async_set_fan_speed(hass, "low", "vacuum.test") - mqtt_mock.async_publish.assert_not_called() - mqtt_mock.async_publish.reset_mock() - - await common.async_send_command(hass, "some command", "vacuum.test") - mqtt_mock.async_publish.assert_not_called() - mqtt_mock.async_publish.reset_mock() - - -@pytest.mark.parametrize( - "hass_config", - [ - help_custom_config( - vacuum.DOMAIN, - DEFAULT_CONFIG, - ( - { - mqttvacuum.CONF_SUPPORTED_FEATURES: services_to_strings( - mqttvacuum.STRING_TO_SERVICE["turn_on"], SERVICE_TO_STRING - ) - }, - ), - ) - ], -) -async def test_attributes_without_supported_features( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test attributes which are not supported by the vacuum.""" - await mqtt_mock_entry() - - 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_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 - - -@pytest.mark.parametrize( - "hass_config", - [DEFAULT_CONFIG_ALL_SERVICES], -) -async def test_status( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test status updates from the vacuum.""" - await mqtt_mock_entry() - - 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_ON - assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-50" - assert state.attributes.get(ATTR_BATTERY_LEVEL) == 54 - assert state.attributes.get(ATTR_FAN_SPEED) == "max" - - message = """{ - "battery_level": 61, - "docked": true, - "cleaning": false, - "charging": true, - "fan_speed": "min" - }""" - - async_fire_mqtt_message(hass, "vacuum/state", message) - state = hass.states.get("vacuum.mqtttest") - assert state.state == STATE_OFF - assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-charging-60" - assert state.attributes.get(ATTR_BATTERY_LEVEL) == 61 - assert state.attributes.get(ATTR_FAN_SPEED) == "min" - - -@pytest.mark.parametrize( - "hass_config", - [DEFAULT_CONFIG_ALL_SERVICES], -) -async def test_status_battery( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test status updates from the vacuum.""" - await mqtt_mock_entry() - - message = """{ - "battery_level": 54 - }""" - async_fire_mqtt_message(hass, "vacuum/state", message) - state = hass.states.get("vacuum.mqtttest") - assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-50" - - -@pytest.mark.parametrize( - "hass_config", - [DEFAULT_CONFIG_ALL_SERVICES], -) -async def test_status_cleaning( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test status updates from the vacuum.""" - await hass.async_block_till_done() - await mqtt_mock_entry() - - message = """{ - "cleaning": true - }""" - async_fire_mqtt_message(hass, "vacuum/state", message) - state = hass.states.get("vacuum.mqtttest") - assert state.state == STATE_ON - - -@pytest.mark.parametrize( - "hass_config", - [DEFAULT_CONFIG_ALL_SERVICES], -) -async def test_status_docked( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test status updates from the vacuum.""" - await mqtt_mock_entry() - - message = """{ - "docked": true - }""" - async_fire_mqtt_message(hass, "vacuum/state", message) - state = hass.states.get("vacuum.mqtttest") - assert state.state == STATE_OFF - - -@pytest.mark.parametrize( - "hass_config", - [DEFAULT_CONFIG_ALL_SERVICES], -) -async def test_status_charging( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test status updates from the vacuum.""" - await mqtt_mock_entry() - - message = """{ - "charging": true - }""" - async_fire_mqtt_message(hass, "vacuum/state", message) - state = hass.states.get("vacuum.mqtttest") - assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-outline" - - -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG_ALL_SERVICES]) -async def test_status_fan_speed( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test status updates from the vacuum.""" - await mqtt_mock_entry() - - message = """{ - "fan_speed": "max" - }""" - async_fire_mqtt_message(hass, "vacuum/state", message) - state = hass.states.get("vacuum.mqtttest") - assert state.attributes.get(ATTR_FAN_SPEED) == "max" - - -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG_ALL_SERVICES]) -async def test_status_fan_speed_list( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test status updates from the vacuum.""" - await mqtt_mock_entry() - - state = hass.states.get("vacuum.mqtttest") - assert state.attributes.get(ATTR_FAN_SPEED_LIST) == ["min", "medium", "high", "max"] - - -@pytest.mark.parametrize( - "hass_config", - [ - help_custom_config( - vacuum.DOMAIN, - DEFAULT_CONFIG, - ( - { - mqttvacuum.CONF_SUPPORTED_FEATURES: services_to_strings( - ALL_SERVICES - VacuumEntityFeature.FAN_SPEED, SERVICE_TO_STRING - ) - }, - ), - ) - ], -) -async def test_status_no_fan_speed_list( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test status updates from the vacuum. - - If the vacuum doesn't support fan speed, fan speed list should be None. - """ - await mqtt_mock_entry() - - state = hass.states.get("vacuum.mqtttest") - assert state.attributes.get(ATTR_FAN_SPEED_LIST) is None - - -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG_ALL_SERVICES]) -async def test_status_error( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test status updates from the vacuum.""" - await mqtt_mock_entry() - - message = """{ - "error": "Error1" - }""" - async_fire_mqtt_message(hass, "vacuum/state", message) - state = hass.states.get("vacuum.mqtttest") - assert state.attributes.get(ATTR_STATUS) == "Error: Error1" - - message = """{ - "error": "" - }""" - async_fire_mqtt_message(hass, "vacuum/state", message) - state = hass.states.get("vacuum.mqtttest") - assert state.attributes.get(ATTR_STATUS) == "Stopped" - - -@pytest.mark.parametrize( - "hass_config", - [ - help_custom_config( - vacuum.DOMAIN, - DEFAULT_CONFIG, - ( - { - mqttvacuum.CONF_BATTERY_LEVEL_TOPIC: "retroroomba/battery_level", - mqttvacuum.CONF_BATTERY_LEVEL_TEMPLATE: "{{ value }}", - }, - ), - ) - ], -) -async def test_battery_template( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test that you can use non-default templates for battery_level.""" - await mqtt_mock_entry() - - async_fire_mqtt_message(hass, "retroroomba/battery_level", "54") - state = hass.states.get("vacuum.mqtttest") - assert state.attributes.get(ATTR_BATTERY_LEVEL) == 54 - assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-50" - - -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG_ALL_SERVICES]) -async def test_status_invalid_json( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test to make sure nothing breaks if the vacuum sends bad JSON.""" - await mqtt_mock_entry() - - async_fire_mqtt_message(hass, "vacuum/state", '{"asdfasas false}') - state = hass.states.get("vacuum.mqtttest") - assert state.state == STATE_OFF - assert state.attributes.get(ATTR_STATUS) == "Stopped" - - -@pytest.mark.parametrize( - "hass_config", - [ - filter_options(DEFAULT_CONFIG, {mqttvacuum.CONF_BATTERY_LEVEL_TEMPLATE}), - filter_options(DEFAULT_CONFIG, {mqttvacuum.CONF_CHARGING_TEMPLATE}), - filter_options(DEFAULT_CONFIG, {mqttvacuum.CONF_CLEANING_TEMPLATE}), - filter_options(DEFAULT_CONFIG, {mqttvacuum.CONF_DOCKED_TEMPLATE}), - filter_options(DEFAULT_CONFIG, {mqttvacuum.CONF_ERROR_TEMPLATE}), - filter_options(DEFAULT_CONFIG, {mqttvacuum.CONF_FAN_SPEED_TEMPLATE}), - ], -) -async def test_missing_templates( +async def test_removed_support_discovery( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, + config: DiscoveryInfoType, + removed: bool, ) -> None: - """Test to make sure missing template is not allowed.""" + """Test that the removed support validation for the legacy schema works.""" assert await mqtt_mock_entry() - assert "some but not all values in the same group of inclusion" in caplog.text + config_payload = json.dumps(config) + async_fire_mqtt_message(hass, "homeassistant/vacuum/test/config", config_payload) + await hass.async_block_till_done() -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG_2]) -async def test_availability_when_connection_lost( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test availability after MQTT disconnection.""" - await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry, vacuum.DOMAIN - ) + entity = hass.states.get("vacuum.test") - -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG_2]) -async def test_availability_without_topic( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test availability without defined availability topic.""" - await help_test_availability_without_topic( - hass, mqtt_mock_entry, vacuum.DOMAIN, DEFAULT_CONFIG_2 - ) - - -async def test_default_availability_payload( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test availability by default payload with defined topic.""" - await help_test_default_availability_payload( - hass, mqtt_mock_entry, vacuum.DOMAIN, DEFAULT_CONFIG_2 - ) - - -async def test_custom_availability_payload( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test availability by custom payload with defined topic.""" - await help_test_custom_availability_payload( - hass, mqtt_mock_entry, vacuum.DOMAIN, DEFAULT_CONFIG_2 - ) - - -async def test_setting_attribute_via_mqtt_json_message( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test the setting of attribute via MQTT with JSON payload.""" - await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry, vacuum.DOMAIN, DEFAULT_CONFIG_2 - ) - - -async def test_setting_blocked_attribute_via_mqtt_json_message( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test the setting of attribute via MQTT with JSON payload.""" - await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, - mqtt_mock_entry, - vacuum.DOMAIN, - DEFAULT_CONFIG_2, - MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED, - ) - - -async def test_setting_attribute_with_template( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test the setting of attribute via MQTT with JSON payload.""" - await help_test_setting_attribute_with_template( - hass, mqtt_mock_entry, vacuum.DOMAIN, DEFAULT_CONFIG_2 - ) - - -async def test_update_with_json_attrs_not_dict( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - vacuum.DOMAIN, - DEFAULT_CONFIG_2, - ) - - -async def test_update_with_json_attrs_bad_json( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test attributes get extracted from a JSON result.""" - await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - vacuum.DOMAIN, - DEFAULT_CONFIG_2, - ) - - -async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test update of discovered MQTTAttributes.""" - await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - vacuum.DOMAIN, - DEFAULT_CONFIG_2, - ) - - -@pytest.mark.parametrize( - "hass_config", - [ - { - mqtt.DOMAIN: { - vacuum.DOMAIN: [ - { - "name": "Test 1", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "name": "Test 2", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] - } - } - ], -) -async def test_unique_id( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test unique id option only creates one vacuum per unique_id.""" - await help_test_unique_id(hass, mqtt_mock_entry, vacuum.DOMAIN) - - -async def test_discovery_removal_vacuum( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test removal of discovered vacuum.""" - data = json.dumps(DEFAULT_CONFIG_2[mqtt.DOMAIN][vacuum.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, vacuum.DOMAIN, data - ) - - -async def test_discovery_update_vacuum( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test update of discovered vacuum.""" - config1 = {"name": "Beer", "command_topic": "test_topic"} - config2 = {"name": "Milk", "command_topic": "test_topic"} - await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, vacuum.DOMAIN, config1, config2 - ) - - -async def test_discovery_update_unchanged_vacuum( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test update of discovered vacuum.""" - data1 = '{ "name": "Beer", "command_topic": "test_topic" }' - with patch( - "homeassistant.components.mqtt.vacuum.schema_legacy.MqttVacuum.discovery_update" - ) as discovery_update: - await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - vacuum.DOMAIN, - data1, - discovery_update, + if removed: + assert entity is None + assert ( + "The support for the `legacy` MQTT " + "vacuum schema has been removed" in caplog.text ) - - -@pytest.mark.no_fail_on_log_exception -async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test handling of bad discovery message.""" - data1 = '{ "name": "Beer", "command_topic": "test_topic#" }' - data2 = '{ "name": "Milk", "command_topic": "test_topic" }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, vacuum.DOMAIN, data1, data2 - ) - - -async def test_entity_device_info_with_connection( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test MQTT vacuum device registry integration.""" - await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry, vacuum.DOMAIN, DEFAULT_CONFIG_2 - ) - - -async def test_entity_device_info_with_identifier( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test MQTT vacuum device registry integration.""" - await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry, vacuum.DOMAIN, DEFAULT_CONFIG_2 - ) - - -async def test_entity_device_info_update( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test device registry update.""" - await help_test_entity_device_info_update( - hass, mqtt_mock_entry, vacuum.DOMAIN, DEFAULT_CONFIG_2 - ) - - -async def test_entity_device_info_remove( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test device registry remove.""" - await help_test_entity_device_info_remove( - hass, mqtt_mock_entry, vacuum.DOMAIN, DEFAULT_CONFIG_2 - ) - - -async def test_entity_id_update_subscriptions( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test MQTT subscriptions are managed when entity_id is updated.""" - config = { - mqtt.DOMAIN: { - vacuum.DOMAIN: { - "name": "test", - "battery_level_topic": "test-topic", - "battery_level_template": "{{ value_json.battery_level }}", - "command_topic": "command-topic", - "availability_topic": "avty-topic", - } - } - } - await help_test_entity_id_update_subscriptions( - hass, - mqtt_mock_entry, - vacuum.DOMAIN, - config, - ["test-topic", "avty-topic"], - ) - - -async def test_entity_id_update_discovery_update( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test MQTT discovery update when entity_id is updated.""" - await help_test_entity_id_update_discovery_update( - hass, mqtt_mock_entry, vacuum.DOMAIN, DEFAULT_CONFIG_2 - ) - - -async def test_entity_debug_info_message( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test MQTT debug info.""" - config = { - mqtt.DOMAIN: { - vacuum.DOMAIN: { - "name": "test", - "battery_level_topic": "state-topic", - "battery_level_template": "{{ value_json.battery_level }}", - "command_topic": "command-topic", - "payload_turn_on": "ON", - } - } - } - await help_test_entity_debug_info_message( - hass, - mqtt_mock_entry, - vacuum.DOMAIN, - config, - vacuum.SERVICE_TURN_ON, - ) - - -@pytest.mark.parametrize( - ("service", "topic", "parameters", "payload", "template"), - [ - ( - vacuum.SERVICE_TURN_ON, - "command_topic", - None, - "turn_on", - None, - ), - ( - vacuum.SERVICE_CLEAN_SPOT, - "command_topic", - None, - "clean_spot", - None, - ), - ( - vacuum.SERVICE_SET_FAN_SPEED, - "set_fan_speed_topic", - {"fan_speed": "medium"}, - "medium", - None, - ), - ( - vacuum.SERVICE_SEND_COMMAND, - "send_command_topic", - {"command": "custom command"}, - "custom command", - None, - ), - ( - vacuum.SERVICE_TURN_OFF, - "command_topic", - None, - "turn_off", - None, - ), - ], -) -async def test_publishing_with_custom_encoding( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, - service: str, - topic: str, - parameters: dict[str, Any], - payload: str, - template: str | None, -) -> None: - """Test publishing MQTT payload with different encoding.""" - domain = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG) - config[mqtt.DOMAIN][domain]["supported_features"] = [ - "turn_on", - "turn_off", - "clean_spot", - "fan_speed", - "send_command", - ] - - await help_test_publishing_with_custom_encoding( - hass, - mqtt_mock_entry, - caplog, - domain, - config, - service, - topic, - parameters, - payload, - template, - ) - - -async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test reloading the MQTT platform.""" - domain = vacuum.DOMAIN - config = DEFAULT_CONFIG - await help_test_reloadable(hass, mqtt_client_mock, domain, config) - - -@pytest.mark.parametrize( - ("topic", "value", "attribute", "attribute_value"), - [ - (CONF_BATTERY_LEVEL_TOPIC, '{ "battery_level": 60 }', "battery_level", 60), - (CONF_CHARGING_TOPIC, '{ "charging": true }', "status", "Stopped"), - (CONF_CLEANING_TOPIC, '{ "cleaning": true }', "status", "Cleaning"), - (CONF_DOCKED_TOPIC, '{ "docked": true }', "status", "Docked"), - ( - CONF_ERROR_TOPIC, - '{ "error": "some error" }', - "status", - "Error: some error", - ), - ( - CONF_FAN_SPEED_TOPIC, - '{ "fan_speed": "medium" }', - "fan_speed", - "medium", - ), - ], -) -async def test_encoding_subscribable_topics( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - topic: str, - value: str, - attribute: str | None, - attribute_value: Any, -) -> None: - """Test handling of incoming encoded payload.""" - domain = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][domain]) - config[CONF_SUPPORTED_FEATURES] = [ - "turn_on", - "turn_off", - "pause", - "stop", - "return_home", - "battery", - "status", - "locate", - "clean_spot", - "fan_speed", - "send_command", - ] - - await help_test_encoding_subscribable_topics( - hass, - mqtt_mock_entry, - vacuum.DOMAIN, - config, - topic, - value, - attribute, - attribute_value, - skip_raw_test=True, - ) - - -@pytest.mark.parametrize( - "hass_config", - [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], - ids=["platform_key", "listed"], -) -async def test_setup_manual_entity_from_yaml( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test setup manual configured MQTT entity.""" - await mqtt_mock_entry() - platform = vacuum.DOMAIN - assert hass.states.get(f"{platform}.mqtttest") - - -@pytest.mark.parametrize( - "hass_config", - [ - help_custom_config( - vacuum.DOMAIN, - DEFAULT_CONFIG, - ( - { - "availability_topic": "availability-topic", - "json_attributes_topic": "json-attributes-topic", - }, - ), - ) - ], -) -@pytest.mark.parametrize( - ("topic", "payload1", "payload2"), - [ - ("availability-topic", "online", "offline"), - ("json-attributes-topic", '{"attr1": "val1"}', '{"attr1": "val2"}'), - ("vacuum/state", '{"battery_level": 71}', '{"battery_level": 60}'), - ("vacuum/state", '{"docked": true}', '{"docked": false}'), - ("vacuum/state", '{"cleaning": true}', '{"cleaning": false}'), - ("vacuum/state", '{"fan_speed": "max"}', '{"fan_speed": "min"}'), - ("vacuum/state", '{"error": "some error"}', '{"error": "other error"}'), - ("vacuum/state", '{"charging": true}', '{"charging": false}'), - ], -) -async def test_skipped_async_ha_write_state( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - topic: str, - payload1: str, - payload2: str, -) -> None: - """Test a write state command is only called when there is change.""" - await mqtt_mock_entry() - await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) + else: + assert entity is not None diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_vacuum.py similarity index 98% rename from tests/components/mqtt/test_state_vacuum.py rename to tests/components/mqtt/test_vacuum.py index 40bd5158280..f48b0b1b375 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_vacuum.py @@ -7,13 +7,14 @@ from unittest.mock import patch import pytest from homeassistant.components import mqtt, vacuum +from homeassistant.components.mqtt import vacuum as mqttvacuum from homeassistant.components.mqtt.const import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC -from homeassistant.components.mqtt.vacuum import CONF_SCHEMA, schema_state as mqttvacuum -from homeassistant.components.mqtt.vacuum.const import MQTT_VACUUM_ATTRIBUTES_BLOCKED -from homeassistant.components.mqtt.vacuum.schema import services_to_strings -from homeassistant.components.mqtt.vacuum.schema_state import ( +from homeassistant.components.mqtt.vacuum import ( ALL_SERVICES, + CONF_SCHEMA, + MQTT_VACUUM_ATTRIBUTES_BLOCKED, SERVICE_TO_STRING, + services_to_strings, ) from homeassistant.components.vacuum import ( ATTR_BATTERY_ICON, @@ -586,7 +587,7 @@ async def test_discovery_update_unchanged_vacuum( """Test update of discovered vacuum.""" data1 = '{ "schema": "state", "name": "Beer", "command_topic": "test_topic"}' with patch( - "homeassistant.components.mqtt.vacuum.schema_state.MqttStateVacuum.discovery_update" + "homeassistant.components.mqtt.vacuum.MqttStateVacuum.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( hass,