Remove MQTT legacy vacuum support (#107274)

pull/105955/head
Jan Bouwhuis 2024-01-08 09:22:43 +01:00 committed by GitHub
parent 3eb81bc461
commit 9ad3c8dbc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 235 additions and 1860 deletions

View File

@ -22,10 +22,6 @@ ABBREVIATIONS = {
"bri_tpl": "brightness_template", "bri_tpl": "brightness_template",
"bri_val_tpl": "brightness_value_template", "bri_val_tpl": "brightness_value_template",
"clr_temp_cmd_tpl": "color_temp_command_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": "color_mode",
"clrm_stat_t": "color_mode_state_topic", "clrm_stat_t": "color_mode_state_topic",
"clrm_val_tpl": "color_mode_value_template", "clrm_val_tpl": "color_mode_value_template",
@ -33,8 +29,6 @@ ABBREVIATIONS = {
"clr_temp_stat_t": "color_temp_state_topic", "clr_temp_stat_t": "color_temp_state_topic",
"clr_temp_tpl": "color_temp_template", "clr_temp_tpl": "color_temp_template",
"clr_temp_val_tpl": "color_temp_value_template", "clr_temp_val_tpl": "color_temp_value_template",
"cln_t": "cleaning_topic",
"cln_tpl": "cleaning_template",
"cmd_off_tpl": "command_off_template", "cmd_off_tpl": "command_off_template",
"cmd_on_tpl": "command_on_template", "cmd_on_tpl": "command_on_template",
"cmd_t": "command_topic", "cmd_t": "command_topic",
@ -54,19 +48,13 @@ ABBREVIATIONS = {
"dir_cmd_tpl": "direction_command_template", "dir_cmd_tpl": "direction_command_template",
"dir_stat_t": "direction_state_topic", "dir_stat_t": "direction_state_topic",
"dir_val_tpl": "direction_value_template", "dir_val_tpl": "direction_value_template",
"dock_t": "docked_topic",
"dock_tpl": "docked_template",
"dock_cmd_t": "dock_command_topic", "dock_cmd_t": "dock_command_topic",
"dock_cmd_tpl": "dock_command_template", "dock_cmd_tpl": "dock_command_template",
"e": "encoding", "e": "encoding",
"en": "enabled_by_default", "en": "enabled_by_default",
"ent_cat": "entity_category", "ent_cat": "entity_category",
"ent_pic": "entity_picture", "ent_pic": "entity_picture",
"err_t": "error_topic",
"err_tpl": "error_template",
"evt_typ": "event_types", "evt_typ": "event_types",
"fanspd_t": "fan_speed_topic",
"fanspd_tpl": "fan_speed_template",
"fanspd_lst": "fan_speed_list", "fanspd_lst": "fan_speed_list",
"flsh_tlng": "flash_time_long", "flsh_tlng": "flash_time_long",
"flsh_tsht": "flash_time_short", "flsh_tsht": "flash_time_short",
@ -160,7 +148,6 @@ ABBREVIATIONS = {
"pl_rst_pr_mode": "payload_reset_preset_mode", "pl_rst_pr_mode": "payload_reset_preset_mode",
"pl_stop": "payload_stop", "pl_stop": "payload_stop",
"pl_strt": "payload_start", "pl_strt": "payload_start",
"pl_stpa": "payload_start_pause",
"pl_ret": "payload_return_to_base", "pl_ret": "payload_return_to_base",
"pl_toff": "payload_turn_off", "pl_toff": "payload_turn_off",
"pl_ton": "payload_turn_on", "pl_ton": "payload_turn_on",

View File

@ -1,12 +1,8 @@
{ {
"issues": { "issues": {
"deprecation_mqtt_legacy_vacuum_yaml": { "deprecation_mqtt_schema_vacuum_yaml": {
"title": "MQTT vacuum entities with legacy schema found in your configuration.yaml", "title": "MQTT vacuum entities with deprecated `schema` config settings 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." "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."
},
"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."
}, },
"deprecated_climate_aux_property": { "deprecated_climate_aux_property": {
"title": "MQTT entities with auxiliary heat support found", "title": "MQTT entities with auxiliary heat support found",

View File

@ -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 __future__ import annotations
from collections.abc import Callable
import logging
from typing import Any, cast from typing import Any, cast
import voluptuous as vol import voluptuous as vol
from homeassistant.components import vacuum
from homeassistant.components.vacuum import ( from homeassistant.components.vacuum import (
ENTITY_ID_FORMAT, ENTITY_ID_FORMAT,
STATE_CLEANING, STATE_CLEANING,
@ -21,58 +30,37 @@ from homeassistant.const import (
STATE_IDLE, STATE_IDLE,
STATE_PAUSED, STATE_PAUSED,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, async_get_hass, callback
import homeassistant.helpers.config_validation as cv 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.json import json_dumps
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.json import json_loads_object from homeassistant.util.json import json_loads_object
from .. import subscription from . import subscription
from ..config import MQTT_BASE_SCHEMA from .config import MQTT_BASE_SCHEMA
from ..const import ( from .const import (
CONF_COMMAND_TOPIC, CONF_COMMAND_TOPIC,
CONF_ENCODING, CONF_ENCODING,
CONF_QOS, CONF_QOS,
CONF_RETAIN, CONF_RETAIN,
CONF_SCHEMA,
CONF_STATE_TOPIC, CONF_STATE_TOPIC,
DOMAIN,
) )
from ..debug_info import log_messages from .debug_info import log_messages
from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, write_state_on_attr_change from .mixins import (
from ..models import ReceiveMessage MQTT_ENTITY_COMMON_SCHEMA,
from ..util import valid_publish_topic MqttEntity,
from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED async_setup_entity_entry_helper,
from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services write_state_on_attr_change,
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 .models import ReceiveMessage
from .util import valid_publish_topic
LEGACY = "legacy"
STATE = "state"
BATTERY = "battery_level" BATTERY = "battery_level"
FAN_SPEED = "fan_speed" FAN_SPEED = "fan_speed"
@ -102,7 +90,7 @@ CONF_SEND_COMMAND_TOPIC = "send_command_topic"
DEFAULT_NAME = "MQTT State Vacuum" DEFAULT_NAME = "MQTT State Vacuum"
DEFAULT_RETAIN = False DEFAULT_RETAIN = False
DEFAULT_SERVICE_STRINGS = services_to_strings(DEFAULT_SERVICES, SERVICE_TO_STRING)
DEFAULT_PAYLOAD_RETURN_TO_BASE = "return_to_base" DEFAULT_PAYLOAD_RETURN_TO_BASE = "return_to_base"
DEFAULT_PAYLOAD_STOP = "stop" DEFAULT_PAYLOAD_STOP = "stop"
DEFAULT_PAYLOAD_CLEAN_SPOT = "clean_spot" DEFAULT_PAYLOAD_CLEAN_SPOT = "clean_spot"
@ -110,6 +98,52 @@ DEFAULT_PAYLOAD_LOCATE = "locate"
DEFAULT_PAYLOAD_START = "start" DEFAULT_PAYLOAD_START = "start"
DEFAULT_PAYLOAD_PAUSE = "pause" 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 = { _FEATURE_PAYLOADS = {
VacuumEntityFeature.START: CONF_PAYLOAD_START, VacuumEntityFeature.START: CONF_PAYLOAD_START,
VacuumEntityFeature.STOP: CONF_PAYLOAD_STOP, VacuumEntityFeature.STOP: CONF_PAYLOAD_STOP,
@ -119,40 +153,105 @@ _FEATURE_PAYLOADS = {
VacuumEntityFeature.RETURN_HOME: CONF_PAYLOAD_RETURN_TO_BASE, VacuumEntityFeature.RETURN_HOME: CONF_PAYLOAD_RETURN_TO_BASE,
} }
PLATFORM_SCHEMA_STATE_MODERN = ( MQTT_VACUUM_ATTRIBUTES_BLOCKED = frozenset(
MQTT_BASE_SCHEMA.extend( {
{ vacuum.ATTR_BATTERY_ICON,
vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All( vacuum.ATTR_BATTERY_LEVEL,
cv.ensure_list, [cv.string] vacuum.ATTR_FAN_SPEED,
), }
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)
) )
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): class MqttStateVacuum(MqttEntity, StateVacuumEntity):
@ -182,12 +281,22 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
@staticmethod @staticmethod
def config_schema() -> vol.Schema: def config_schema() -> vol.Schema:
"""Return the config schema.""" """Return the config schema."""
return DISCOVERY_SCHEMA_STATE return DISCOVERY_SCHEMA
def _setup_from_config(self, config: ConfigType) -> None: def _setup_from_config(self, config: ConfigType) -> None:
"""(Re)Setup the entity.""" """(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] 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 supported_feature_strings, STRING_TO_SERVICE
) )
self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST] self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST]

View File

@ -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},
)

View File

@ -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,
}
)

View File

@ -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

View File

@ -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()

View File

@ -481,11 +481,11 @@ async def test_discover_alarm_control_panel(
"vacuum", "vacuum",
), ),
( (
"homeassistant/vacuum/object/bla/config", "homeassistant/valve/object/bla/config",
'{ "name": "Hello World 17", "obj_id": "hello_id", "state_topic": "test-topic", "schema": "legacy" }', '{ "name": "Hello World 17", "obj_id": "hello_id", "state_topic": "test-topic" }',
"vacuum.hello_id", "valve.hello_id",
"Hello World 17", "Hello World 17",
"vacuum", "valve",
), ),
( (
"homeassistant/lock/object/bla/config", "homeassistant/lock/object/bla/config",

File diff suppressed because it is too large Load Diff

View File

@ -7,13 +7,14 @@ from unittest.mock import patch
import pytest import pytest
from homeassistant.components import mqtt, vacuum 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.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 import (
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 (
ALL_SERVICES, ALL_SERVICES,
CONF_SCHEMA,
MQTT_VACUUM_ATTRIBUTES_BLOCKED,
SERVICE_TO_STRING, SERVICE_TO_STRING,
services_to_strings,
) )
from homeassistant.components.vacuum import ( from homeassistant.components.vacuum import (
ATTR_BATTERY_ICON, ATTR_BATTERY_ICON,
@ -586,7 +587,7 @@ async def test_discovery_update_unchanged_vacuum(
"""Test update of discovered vacuum.""" """Test update of discovered vacuum."""
data1 = '{ "schema": "state", "name": "Beer", "command_topic": "test_topic"}' data1 = '{ "schema": "state", "name": "Beer", "command_topic": "test_topic"}'
with patch( with patch(
"homeassistant.components.mqtt.vacuum.schema_state.MqttStateVacuum.discovery_update" "homeassistant.components.mqtt.vacuum.MqttStateVacuum.discovery_update"
) as discovery_update: ) as discovery_update:
await help_test_discovery_update_unchanged( await help_test_discovery_update_unchanged(
hass, hass,