diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 162e344f852..e17a31480b1 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -81,6 +81,8 @@ from .const import ( CONF_TLS_VERSION, CONF_TOPIC, CONF_WILL_MESSAGE, + CONFIG_ENTRY_IS_SETUP, + DATA_CONFIG_ENTRY_LOCK, DATA_MQTT_CONFIG, DATA_MQTT_RELOAD_NEEDED, DEFAULT_BIRTH, @@ -171,7 +173,6 @@ PLATFORMS = [ Platform.VACUUM, ] - CLIENT_KEY_AUTH_MSG = ( "client_key and client_cert must both be present in " "the MQTT broker configuration" @@ -187,7 +188,14 @@ MQTT_WILL_BIRTH_SCHEMA = vol.Schema( required=True, ) -CONFIG_SCHEMA_BASE = vol.Schema( +PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( + { + vol.Optional(Platform.FAN.value): cv.ensure_list, + vol.Optional(Platform.LIGHT.value): cv.ensure_list, + } +) + +CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend( { vol.Optional(CONF_CLIENT_ID): cv.string, vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( @@ -253,10 +261,28 @@ SCHEMA_BASE = { vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, } +MQTT_BASE_SCHEMA = vol.Schema(SCHEMA_BASE) + +# Will be removed when all platforms support a modern platform schema MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE) +# Will be removed when all platforms support a modern platform schema +MQTT_RO_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + } +) +# Will be removed when all platforms support a modern platform schema +MQTT_RW_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, + } +) # Sensor type platforms subscribe to MQTT events -MQTT_RO_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend( +MQTT_RO_SCHEMA = MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, @@ -264,7 +290,7 @@ MQTT_RO_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend( ) # Switch type platforms publish to MQTT and may subscribe -MQTT_RW_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend( +MQTT_RW_SCHEMA = MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, @@ -774,6 +800,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ), ) + # setup platforms and discovery + hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() + hass.data[CONFIG_ENTRY_IS_SETUP] = set() + + async with hass.data[DATA_CONFIG_ENTRY_LOCK]: + for component in PLATFORMS: + config_entries_key = f"{component}.mqtt" + if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: + hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + if conf.get(CONF_DISCOVERY): await _async_setup_discovery(hass, conf, entry) diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 69865733763..106d0310158 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -28,6 +28,8 @@ CONF_CLIENT_CERT = "client_cert" CONF_TLS_INSECURE = "tls_insecure" CONF_TLS_VERSION = "tls_version" +CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" +DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" DATA_MQTT_CONFIG = "mqtt_config" DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index fae443dc411..8685c790fd2 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -27,6 +27,8 @@ from .const import ( ATTR_DISCOVERY_TOPIC, CONF_AVAILABILITY, CONF_TOPIC, + CONFIG_ENTRY_IS_SETUP, + DATA_CONFIG_ENTRY_LOCK, DOMAIN, ) @@ -62,8 +64,6 @@ SUPPORTED_COMPONENTS = [ ALREADY_DISCOVERED = "mqtt_discovered_components" PENDING_DISCOVERED = "mqtt_pending_components" -CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" -DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" DATA_CONFIG_FLOW_LOCK = "mqtt_discovery_config_flow_lock" DISCOVERY_UNSUBSCRIBE = "mqtt_discovery_unsubscribe" INTEGRATION_UNSUBSCRIBE = "mqtt_integration_discovery_unsubscribe" @@ -258,9 +258,7 @@ async def async_start( # noqa: C901 hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None ) - hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[DATA_CONFIG_FLOW_LOCK] = asyncio.Lock() - hass.data[CONFIG_ENTRY_IS_SETUP] = set() hass.data[ALREADY_DISCOVERED] = {} hass.data[PENDING_DISCOVERED] = {} diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index ff2eab7a68f..f2b738cd2bb 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -1,6 +1,7 @@ """Support for MQTT fans.""" from __future__ import annotations +import asyncio import functools import logging import math @@ -49,8 +50,10 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_get_platform_config_from_yaml, async_setup_entry_helper, async_setup_platform_helper, + warn_for_legacy_schema, ) CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic" @@ -122,7 +125,7 @@ def valid_preset_mode_configuration(config): return config -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, @@ -172,7 +175,15 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) +# Configuring MQTT Fans under the fan platform key is deprecated in HA Core 2022.6 PLATFORM_SCHEMA = vol.All( + cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), + valid_speed_range_configuration, + valid_preset_mode_configuration, + warn_for_legacy_schema(fan.DOMAIN), +) + +PLATFORM_SCHEMA_MODERN = vol.All( _PLATFORM_SCHEMA_BASE, valid_speed_range_configuration, valid_preset_mode_configuration, @@ -201,7 +212,8 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up MQTT fan through configuration.yaml.""" + """Set up MQTT fans configured under the fan platform key (deprecated).""" + # Deprecated in HA Core 2022.6 await async_setup_platform_helper( hass, fan.DOMAIN, config, async_add_entities, _async_setup_entity ) @@ -212,7 +224,17 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT fan dynamically through MQTT discovery.""" + """Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery.""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, fan.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index d78cd5e7baa..ab2a3462615 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -1,36 +1,47 @@ """Support for MQTT lights.""" from __future__ import annotations +import asyncio import functools import voluptuous as vol from homeassistant.components import light +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from ..mixins import async_setup_entry_helper, async_setup_platform_helper +from ..mixins import ( + async_get_platform_config_from_yaml, + async_setup_entry_helper, + async_setup_platform_helper, + warn_for_legacy_schema, +) from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import ( DISCOVERY_SCHEMA_BASIC, PLATFORM_SCHEMA_BASIC, + PLATFORM_SCHEMA_MODERN_BASIC, async_setup_entity_basic, ) from .schema_json import ( DISCOVERY_SCHEMA_JSON, PLATFORM_SCHEMA_JSON, + PLATFORM_SCHEMA_MODERN_JSON, async_setup_entity_json, ) from .schema_template import ( DISCOVERY_SCHEMA_TEMPLATE, + PLATFORM_SCHEMA_MODERN_TEMPLATE, PLATFORM_SCHEMA_TEMPLATE, async_setup_entity_template, ) def validate_mqtt_light_discovery(value): - """Validate MQTT light schema.""" + """Validate MQTT light schema for.""" schemas = { "basic": DISCOVERY_SCHEMA_BASIC, "json": DISCOVERY_SCHEMA_JSON, @@ -49,14 +60,31 @@ def validate_mqtt_light(value): return schemas[value[CONF_SCHEMA]](value) +def validate_mqtt_light_modern(value): + """Validate MQTT light schema.""" + schemas = { + "basic": PLATFORM_SCHEMA_MODERN_BASIC, + "json": PLATFORM_SCHEMA_MODERN_JSON, + "template": PLATFORM_SCHEMA_MODERN_TEMPLATE, + } + return schemas[value[CONF_SCHEMA]](value) + + DISCOVERY_SCHEMA = vol.All( MQTT_LIGHT_SCHEMA_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_light_discovery, ) - +# Configuring MQTT Lights under the light platform key is deprecated in HA Core 2022.6 PLATFORM_SCHEMA = vol.All( - MQTT_LIGHT_SCHEMA_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_light + cv.PLATFORM_SCHEMA.extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema, extra=vol.ALLOW_EXTRA), + validate_mqtt_light, + warn_for_legacy_schema(light.DOMAIN), +) + +PLATFORM_SCHEMA_MODERN = vol.All( + MQTT_LIGHT_SCHEMA_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), + validate_mqtt_light_modern, ) @@ -66,14 +94,29 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up MQTT light through configuration.yaml.""" + """Set up MQTT light through configuration.yaml (deprecated).""" + # Deprecated in HA Core 2022.6 await async_setup_platform_helper( hass, light.DOMAIN, config, async_add_entities, _async_setup_entity ) -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up MQTT light dynamically through MQTT discovery.""" +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up MQTT lights configured under the light platform key (deprecated).""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, light.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 497e8186fd0..eb4ec264981 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -156,7 +156,7 @@ VALUE_TEMPLATE_KEYS = [ ] _PLATFORM_SCHEMA_BASE = ( - mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + mqtt.MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_BRIGHTNESS_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic, @@ -220,13 +220,14 @@ _PLATFORM_SCHEMA_BASE = ( .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) +# The use of PLATFORM_SCHEMA is deprecated in HA Core 2022.6 PLATFORM_SCHEMA_BASIC = vol.All( # CONF_WHITE_VALUE_* is deprecated, support will be removed in release 2022.9 cv.deprecated(CONF_WHITE_VALUE_COMMAND_TOPIC), cv.deprecated(CONF_WHITE_VALUE_SCALE), cv.deprecated(CONF_WHITE_VALUE_STATE_TOPIC), cv.deprecated(CONF_WHITE_VALUE_TEMPLATE), - _PLATFORM_SCHEMA_BASE, + cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), ) DISCOVERY_SCHEMA_BASIC = vol.All( @@ -240,6 +241,8 @@ DISCOVERY_SCHEMA_BASIC = vol.All( _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), ) +PLATFORM_SCHEMA_MODERN_BASIC = _PLATFORM_SCHEMA_BASE + async def async_setup_entity_basic( hass, config, async_add_entities, config_entry, discovery_data=None diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index c1e0d7467e0..2049818ab31 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -103,7 +103,7 @@ def valid_color_configuration(config): _PLATFORM_SCHEMA_BASE = ( - mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + mqtt.MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_BRIGHTNESS, default=DEFAULT_BRIGHTNESS): cv.boolean, vol.Optional( @@ -146,10 +146,11 @@ _PLATFORM_SCHEMA_BASE = ( .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) +# Configuring MQTT Lights under the light platform key is deprecated in HA Core 2022.6 PLATFORM_SCHEMA_JSON = vol.All( # CONF_WHITE_VALUE is deprecated, support will be removed in release 2022.9 cv.deprecated(CONF_WHITE_VALUE), - _PLATFORM_SCHEMA_BASE, + cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), valid_color_configuration, ) @@ -160,6 +161,11 @@ DISCOVERY_SCHEMA_JSON = vol.All( valid_color_configuration, ) +PLATFORM_SCHEMA_MODERN_JSON = vol.All( + _PLATFORM_SCHEMA_BASE, + valid_color_configuration, +) + async def async_setup_entity_json( hass, config: ConfigType, async_add_entities, config_entry, discovery_data diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index a98f634642d..0165bfc8efa 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -67,7 +67,7 @@ CONF_RED_TEMPLATE = "red_template" CONF_WHITE_VALUE_TEMPLATE = "white_value_template" _PLATFORM_SCHEMA_BASE = ( - mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + mqtt.MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_BLUE_TEMPLATE): cv.template, vol.Optional(CONF_BRIGHTNESS_TEMPLATE): cv.template, @@ -90,10 +90,11 @@ _PLATFORM_SCHEMA_BASE = ( .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) +# Configuring MQTT Lights under the light platform key is deprecated in HA Core 2022.6 PLATFORM_SCHEMA_TEMPLATE = vol.All( # CONF_WHITE_VALUE_TEMPLATE is deprecated, support will be removed in release 2022.9 cv.deprecated(CONF_WHITE_VALUE_TEMPLATE), - _PLATFORM_SCHEMA_BASE, + cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), ) DISCOVERY_SCHEMA_TEMPLATE = vol.All( @@ -102,6 +103,8 @@ DISCOVERY_SCHEMA_TEMPLATE = vol.All( _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), ) +PLATFORM_SCHEMA_MODERN_TEMPLATE = _PLATFORM_SCHEMA_BASE + async def async_setup_entity_template( hass, config, async_add_entities, config_entry, discovery_data diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 43f75f08459..a46debeae54 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -9,6 +9,7 @@ from typing import Any, Protocol, cast, final import voluptuous as vol +from homeassistant.config import async_log_exception from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CONFIGURATION_URL, @@ -64,6 +65,7 @@ from .const import ( CONF_ENCODING, CONF_QOS, CONF_TOPIC, + DATA_MQTT_CONFIG, DATA_MQTT_RELOAD_NEEDED, DEFAULT_ENCODING, DEFAULT_PAYLOAD_AVAILABLE, @@ -223,6 +225,31 @@ MQTT_ENTITY_COMMON_SCHEMA = MQTT_AVAILABILITY_SCHEMA.extend( ) +def warn_for_legacy_schema(domain: str) -> Callable: + """Warn once when a legacy platform schema is used.""" + warned = set() + + def validator(config: ConfigType) -> ConfigType: + """Return a validator.""" + nonlocal warned + + if domain in warned: + return config + + _LOGGER.warning( + "Manually configured MQTT %s(s) found under platform key '%s', " + "please move to the mqtt integration key, see " + "https://www.home-assistant.io/integrations/%s.mqtt/#new_format", + domain, + domain, + domain, + ) + warned.add(domain) + return config + + return validator + + class SetupEntity(Protocol): """Protocol type for async_setup_entities.""" @@ -237,6 +264,31 @@ class SetupEntity(Protocol): """Define setup_entities type.""" +async def async_get_platform_config_from_yaml( + hass: HomeAssistant, domain: str, schema: vol.Schema +) -> list[ConfigType]: + """Return a list of validated configurations for the domain.""" + + def async_validate_config( + hass: HomeAssistant, + config: list[ConfigType], + ) -> list[ConfigType]: + """Validate config.""" + validated_config = [] + for config_item in config: + try: + validated_config.append(schema(config_item)) + except vol.MultipleInvalid as err: + async_log_exception(err, domain, config_item, hass) + + return validated_config + + config_yaml: ConfigType = hass.data.get(DATA_MQTT_CONFIG, {}) + if not (platform_configs := config_yaml.get(domain)): + return [] + return async_validate_config(hass, platform_configs) + + async def async_setup_entry_helper(hass, domain, async_setup, schema): """Set up entity, automation or tag creation dynamically through MQTT discovery.""" diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 8cf7353d196..b5bb5732617 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -1690,3 +1690,24 @@ async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): assert hass.states.get(f"{domain}.test_new_1") assert hass.states.get(f"{domain}.test_new_2") assert hass.states.get(f"{domain}.test_new_3") + + +async def help_test_setup_manual_entity_from_yaml( + hass, + caplog, + tmp_path, + platform, + config, +): + """Help to test setup from yaml through configuration entry.""" + config_structure = {mqtt.DOMAIN: {platform: config}} + + await async_setup_component(hass, mqtt.DOMAIN, config_structure) + # Mock config entry + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry.add_to_hass(hass) + + with patch("paho.mqtt.client.Client") as mock_client: + mock_client().connect = lambda *args: 0 + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 64b5d272af8..1a533db63c0 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -55,6 +55,7 @@ from .test_common import ( 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_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -1805,3 +1806,15 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): domain = fan.DOMAIN config = DEFAULT_CONFIG[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = fan.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 543174653ec..a370bd67ec1 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1,5 +1,6 @@ """The tests for the MQTT component.""" import asyncio +import copy from datetime import datetime, timedelta from functools import partial import json @@ -30,6 +31,8 @@ from homeassistant.helpers.entity import Entity from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow +from .test_common import help_test_setup_manual_entity_from_yaml + from tests.common import ( MockConfigEntry, async_fire_mqtt_message, @@ -1279,6 +1282,51 @@ async def test_setup_override_configuration(hass, caplog, tmp_path): assert calls_username_password_set[0][1] == "somepassword" +async def test_setup_manual_mqtt_with_platform_key(hass, caplog, tmp_path): + """Test set up a manual MQTT item with a platform key.""" + config = {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} + await help_test_setup_manual_entity_from_yaml( + hass, + caplog, + tmp_path, + "light", + config, + ) + assert ( + "Invalid config for [light]: [platform] is an invalid option for [light]. " + "Check: light->platform. (See ?, line ?)" in caplog.text + ) + + +async def test_setup_manual_mqtt_with_invalid_config(hass, caplog, tmp_path): + """Test set up a manual MQTT item with an invalid config.""" + config = {"name": "test"} + await help_test_setup_manual_entity_from_yaml( + hass, + caplog, + tmp_path, + "light", + config, + ) + assert ( + "Invalid config for [light]: required key not provided @ data['command_topic']." + " Got None. (See ?, line ?)" in caplog.text + ) + + +async def test_setup_manual_mqtt_empty_platform(hass, caplog, tmp_path): + """Test set up a manual MQTT platform without items.""" + config = None + await help_test_setup_manual_entity_from_yaml( + hass, + caplog, + tmp_path, + "light", + config, + ) + assert "voluptuous.error.MultipleInvalid" not in caplog.text + + async def test_setup_mqtt_client_protocol(hass): """Test MQTT client protocol setup.""" entry = MockConfigEntry( @@ -1628,7 +1676,8 @@ async def test_setup_entry_with_config_override(hass, device_reg, mqtt_client_mo # User sets up a config entry entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) + with patch("homeassistant.components.mqtt.PLATFORMS", []): + assert await hass.config_entries.async_setup(entry.entry_id) # Discover a device to verify the entry was setup correctly async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) @@ -2413,3 +2462,23 @@ async def test_subscribe_connection_status(hass, mqtt_mock, mqtt_client_mock): assert len(mqtt_connected_calls) == 2 assert mqtt_connected_calls[0] is True assert mqtt_connected_calls[1] is False + + +async def test_one_deprecation_warning_per_platform(hass, mqtt_mock, caplog): + """Test a deprecation warning is is logged once per platform.""" + platform = "light" + config = {"platform": "mqtt", "command_topic": "test-topic"} + config1 = copy.deepcopy(config) + config1["name"] = "test1" + config2 = copy.deepcopy(config) + config2["name"] = "test2" + await async_setup_component(hass, platform, {platform: [config1, config2]}) + await hass.async_block_till_done() + count = 0 + for record in caplog.records: + if record.levelname == "WARNING" and ( + f"Manually configured MQTT {platform}(s) found under platform key '{platform}'" + in record.message + ): + count += 1 + assert count == 1 diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index ee59928c0c8..957178da14f 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -237,6 +237,7 @@ from .test_common import ( 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_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -3674,3 +3675,15 @@ async def test_sending_mqtt_effect_command_with_template(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("effect") == "colorloop" + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = light.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 93bb9b0f573..962bf534370 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -131,6 +131,7 @@ from .test_common import ( 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_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -2038,3 +2039,15 @@ async def test_encoding_subscribable_topics( init_payload, skip_raw_test=True, ) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = light.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 4461cf14ef4..a88fc094f6d 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -69,6 +69,7 @@ from .test_common import ( 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_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -1213,3 +1214,15 @@ async def test_encoding_subscribable_topics( attribute_value, init_payload, ) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = light.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/conftest.py b/tests/conftest.py index 37bdb05faf7..8ea6e114e9a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -561,9 +561,10 @@ async def mqtt_mock(hass, mqtt_client_mock, mqtt_config): ) entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + # Do not forward the entry setup to the components here + with patch("homeassistant.components.mqtt.PLATFORMS", []): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() mqtt_component_mock = MagicMock( return_value=hass.data["mqtt"],