Move manual configuration of MQTT fan and light to the integration key (#71676)

* Processing yaml config through entry setup

* Setup all platforms

* Update homeassistant/components/mqtt/__init__.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* adjust mock_mqtt - reference config from cache

* Fix test config entry override

* Add tests yaml setup

* additional tests

* Introduce PLATFORM_SCHEMA_MODERN

* recover temporary MQTT_BASE_PLATFORM_SCHEMA

* Allow extra key in light base schema, restore test

* Fix test for exception on platform key

* One deprecation message per platform

* Remove deprecation checks from modern schema

* Update homeassistant/components/mqtt/fan.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/components/mqtt/fan.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/components/mqtt/light/__init__.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/components/mqtt/light/__init__.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/components/mqtt/light/schema_json.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/components/mqtt/light/schema_template.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/components/mqtt/mixins.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* rename validate_modern_schema

* Do not fail platform if a single config is broken

* Update homeassistant/components/mqtt/__init__.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Fix tests on asserting log

* Update log. Make helper transparant, remove patch

* Perform parallel processing

* Update tests/components/mqtt/test_init.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/mqtt/mixins.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* black

* Fix tests and add #new_format anchor

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Jan Bouwhuis 2022-05-19 15:04:53 +02:00 committed by GitHub
parent 9d377aabdb
commit ed1c2ea2b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 339 additions and 28 deletions

View file

@ -81,6 +81,8 @@ from .const import (
CONF_TLS_VERSION, CONF_TLS_VERSION,
CONF_TOPIC, CONF_TOPIC,
CONF_WILL_MESSAGE, CONF_WILL_MESSAGE,
CONFIG_ENTRY_IS_SETUP,
DATA_CONFIG_ENTRY_LOCK,
DATA_MQTT_CONFIG, DATA_MQTT_CONFIG,
DATA_MQTT_RELOAD_NEEDED, DATA_MQTT_RELOAD_NEEDED,
DEFAULT_BIRTH, DEFAULT_BIRTH,
@ -171,7 +173,6 @@ PLATFORMS = [
Platform.VACUUM, Platform.VACUUM,
] ]
CLIENT_KEY_AUTH_MSG = ( CLIENT_KEY_AUTH_MSG = (
"client_key and client_cert must both be present in " "client_key and client_cert must both be present in "
"the MQTT broker configuration" "the MQTT broker configuration"
@ -187,7 +188,14 @@ MQTT_WILL_BIRTH_SCHEMA = vol.Schema(
required=True, 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_CLIENT_ID): cv.string,
vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All(
@ -253,10 +261,28 @@ SCHEMA_BASE = {
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, 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) 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 # 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.Required(CONF_STATE_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, 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 # 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.Required(CONF_COMMAND_TOPIC): valid_publish_topic,
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, 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): if conf.get(CONF_DISCOVERY):
await _async_setup_discovery(hass, conf, entry) await _async_setup_discovery(hass, conf, entry)

View file

@ -28,6 +28,8 @@ CONF_CLIENT_CERT = "client_cert"
CONF_TLS_INSECURE = "tls_insecure" CONF_TLS_INSECURE = "tls_insecure"
CONF_TLS_VERSION = "tls_version" 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_CONFIG = "mqtt_config"
DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed"

View file

@ -27,6 +27,8 @@ from .const import (
ATTR_DISCOVERY_TOPIC, ATTR_DISCOVERY_TOPIC,
CONF_AVAILABILITY, CONF_AVAILABILITY,
CONF_TOPIC, CONF_TOPIC,
CONFIG_ENTRY_IS_SETUP,
DATA_CONFIG_ENTRY_LOCK,
DOMAIN, DOMAIN,
) )
@ -62,8 +64,6 @@ SUPPORTED_COMPONENTS = [
ALREADY_DISCOVERED = "mqtt_discovered_components" ALREADY_DISCOVERED = "mqtt_discovered_components"
PENDING_DISCOVERED = "mqtt_pending_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" DATA_CONFIG_FLOW_LOCK = "mqtt_discovery_config_flow_lock"
DISCOVERY_UNSUBSCRIBE = "mqtt_discovery_unsubscribe" DISCOVERY_UNSUBSCRIBE = "mqtt_discovery_unsubscribe"
INTEGRATION_UNSUBSCRIBE = "mqtt_integration_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, 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[DATA_CONFIG_FLOW_LOCK] = asyncio.Lock()
hass.data[CONFIG_ENTRY_IS_SETUP] = set()
hass.data[ALREADY_DISCOVERED] = {} hass.data[ALREADY_DISCOVERED] = {}
hass.data[PENDING_DISCOVERED] = {} hass.data[PENDING_DISCOVERED] = {}

View file

@ -1,6 +1,7 @@
"""Support for MQTT fans.""" """Support for MQTT fans."""
from __future__ import annotations from __future__ import annotations
import asyncio
import functools import functools
import logging import logging
import math import math
@ -49,8 +50,10 @@ from .debug_info import log_messages
from .mixins import ( from .mixins import (
MQTT_ENTITY_COMMON_SCHEMA, MQTT_ENTITY_COMMON_SCHEMA,
MqttEntity, MqttEntity,
async_get_platform_config_from_yaml,
async_setup_entry_helper, async_setup_entry_helper,
async_setup_platform_helper, async_setup_platform_helper,
warn_for_legacy_schema,
) )
CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic" CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic"
@ -122,7 +125,7 @@ def valid_preset_mode_configuration(config):
return 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_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, 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) ).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( 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, _PLATFORM_SCHEMA_BASE,
valid_speed_range_configuration, valid_speed_range_configuration,
valid_preset_mode_configuration, valid_preset_mode_configuration,
@ -201,7 +212,8 @@ async def async_setup_platform(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = 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( await async_setup_platform_helper(
hass, fan.DOMAIN, config, async_add_entities, _async_setup_entity hass, fan.DOMAIN, config, async_add_entities, _async_setup_entity
) )
@ -212,7 +224,17 @@ async def async_setup_entry(
config_entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> 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( setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry _async_setup_entity, hass, async_add_entities, config_entry=config_entry
) )

View file

@ -1,36 +1,47 @@
"""Support for MQTT lights.""" """Support for MQTT lights."""
from __future__ import annotations from __future__ import annotations
import asyncio
import functools import functools
import voluptuous as vol import voluptuous as vol
from homeassistant.components import light from homeassistant.components import light
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType 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 import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA
from .schema_basic import ( from .schema_basic import (
DISCOVERY_SCHEMA_BASIC, DISCOVERY_SCHEMA_BASIC,
PLATFORM_SCHEMA_BASIC, PLATFORM_SCHEMA_BASIC,
PLATFORM_SCHEMA_MODERN_BASIC,
async_setup_entity_basic, async_setup_entity_basic,
) )
from .schema_json import ( from .schema_json import (
DISCOVERY_SCHEMA_JSON, DISCOVERY_SCHEMA_JSON,
PLATFORM_SCHEMA_JSON, PLATFORM_SCHEMA_JSON,
PLATFORM_SCHEMA_MODERN_JSON,
async_setup_entity_json, async_setup_entity_json,
) )
from .schema_template import ( from .schema_template import (
DISCOVERY_SCHEMA_TEMPLATE, DISCOVERY_SCHEMA_TEMPLATE,
PLATFORM_SCHEMA_MODERN_TEMPLATE,
PLATFORM_SCHEMA_TEMPLATE, PLATFORM_SCHEMA_TEMPLATE,
async_setup_entity_template, async_setup_entity_template,
) )
def validate_mqtt_light_discovery(value): def validate_mqtt_light_discovery(value):
"""Validate MQTT light schema.""" """Validate MQTT light schema for."""
schemas = { schemas = {
"basic": DISCOVERY_SCHEMA_BASIC, "basic": DISCOVERY_SCHEMA_BASIC,
"json": DISCOVERY_SCHEMA_JSON, "json": DISCOVERY_SCHEMA_JSON,
@ -49,14 +60,31 @@ def validate_mqtt_light(value):
return schemas[value[CONF_SCHEMA]](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( DISCOVERY_SCHEMA = vol.All(
MQTT_LIGHT_SCHEMA_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), MQTT_LIGHT_SCHEMA_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA),
validate_mqtt_light_discovery, validate_mqtt_light_discovery,
) )
# Configuring MQTT Lights under the light platform key is deprecated in HA Core 2022.6
PLATFORM_SCHEMA = vol.All( 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, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = 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( await async_setup_platform_helper(
hass, light.DOMAIN, config, async_add_entities, _async_setup_entity hass, light.DOMAIN, config, async_add_entities, _async_setup_entity
) )
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(
"""Set up MQTT light dynamically through MQTT discovery.""" 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( setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry _async_setup_entity, hass, async_add_entities, config_entry=config_entry
) )

View file

@ -156,7 +156,7 @@ VALUE_TEMPLATE_KEYS = [
] ]
_PLATFORM_SCHEMA_BASE = ( _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_TEMPLATE): cv.template,
vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic,
@ -220,13 +220,14 @@ _PLATFORM_SCHEMA_BASE = (
.extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema)
) )
# The use of PLATFORM_SCHEMA is deprecated in HA Core 2022.6
PLATFORM_SCHEMA_BASIC = vol.All( PLATFORM_SCHEMA_BASIC = vol.All(
# CONF_WHITE_VALUE_* is deprecated, support will be removed in release 2022.9 # 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_COMMAND_TOPIC),
cv.deprecated(CONF_WHITE_VALUE_SCALE), cv.deprecated(CONF_WHITE_VALUE_SCALE),
cv.deprecated(CONF_WHITE_VALUE_STATE_TOPIC), cv.deprecated(CONF_WHITE_VALUE_STATE_TOPIC),
cv.deprecated(CONF_WHITE_VALUE_TEMPLATE), cv.deprecated(CONF_WHITE_VALUE_TEMPLATE),
_PLATFORM_SCHEMA_BASE, cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema),
) )
DISCOVERY_SCHEMA_BASIC = vol.All( DISCOVERY_SCHEMA_BASIC = vol.All(
@ -240,6 +241,8 @@ DISCOVERY_SCHEMA_BASIC = vol.All(
_PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA),
) )
PLATFORM_SCHEMA_MODERN_BASIC = _PLATFORM_SCHEMA_BASE
async def async_setup_entity_basic( async def async_setup_entity_basic(
hass, config, async_add_entities, config_entry, discovery_data=None hass, config, async_add_entities, config_entry, discovery_data=None

View file

@ -103,7 +103,7 @@ def valid_color_configuration(config):
_PLATFORM_SCHEMA_BASE = ( _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(CONF_BRIGHTNESS, default=DEFAULT_BRIGHTNESS): cv.boolean,
vol.Optional( vol.Optional(
@ -146,10 +146,11 @@ _PLATFORM_SCHEMA_BASE = (
.extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) .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( PLATFORM_SCHEMA_JSON = vol.All(
# CONF_WHITE_VALUE is deprecated, support will be removed in release 2022.9 # CONF_WHITE_VALUE is deprecated, support will be removed in release 2022.9
cv.deprecated(CONF_WHITE_VALUE), cv.deprecated(CONF_WHITE_VALUE),
_PLATFORM_SCHEMA_BASE, cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema),
valid_color_configuration, valid_color_configuration,
) )
@ -160,6 +161,11 @@ DISCOVERY_SCHEMA_JSON = vol.All(
valid_color_configuration, valid_color_configuration,
) )
PLATFORM_SCHEMA_MODERN_JSON = vol.All(
_PLATFORM_SCHEMA_BASE,
valid_color_configuration,
)
async def async_setup_entity_json( async def async_setup_entity_json(
hass, config: ConfigType, async_add_entities, config_entry, discovery_data hass, config: ConfigType, async_add_entities, config_entry, discovery_data

View file

@ -67,7 +67,7 @@ CONF_RED_TEMPLATE = "red_template"
CONF_WHITE_VALUE_TEMPLATE = "white_value_template" CONF_WHITE_VALUE_TEMPLATE = "white_value_template"
_PLATFORM_SCHEMA_BASE = ( _PLATFORM_SCHEMA_BASE = (
mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( mqtt.MQTT_RW_SCHEMA.extend(
{ {
vol.Optional(CONF_BLUE_TEMPLATE): cv.template, vol.Optional(CONF_BLUE_TEMPLATE): cv.template,
vol.Optional(CONF_BRIGHTNESS_TEMPLATE): cv.template, vol.Optional(CONF_BRIGHTNESS_TEMPLATE): cv.template,
@ -90,10 +90,11 @@ _PLATFORM_SCHEMA_BASE = (
.extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) .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( PLATFORM_SCHEMA_TEMPLATE = vol.All(
# CONF_WHITE_VALUE_TEMPLATE is deprecated, support will be removed in release 2022.9 # CONF_WHITE_VALUE_TEMPLATE is deprecated, support will be removed in release 2022.9
cv.deprecated(CONF_WHITE_VALUE_TEMPLATE), cv.deprecated(CONF_WHITE_VALUE_TEMPLATE),
_PLATFORM_SCHEMA_BASE, cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema),
) )
DISCOVERY_SCHEMA_TEMPLATE = vol.All( DISCOVERY_SCHEMA_TEMPLATE = vol.All(
@ -102,6 +103,8 @@ DISCOVERY_SCHEMA_TEMPLATE = vol.All(
_PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA),
) )
PLATFORM_SCHEMA_MODERN_TEMPLATE = _PLATFORM_SCHEMA_BASE
async def async_setup_entity_template( async def async_setup_entity_template(
hass, config, async_add_entities, config_entry, discovery_data hass, config, async_add_entities, config_entry, discovery_data

View file

@ -9,6 +9,7 @@ from typing import Any, Protocol, cast, final
import voluptuous as vol import voluptuous as vol
from homeassistant.config import async_log_exception
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_CONFIGURATION_URL, ATTR_CONFIGURATION_URL,
@ -64,6 +65,7 @@ from .const import (
CONF_ENCODING, CONF_ENCODING,
CONF_QOS, CONF_QOS,
CONF_TOPIC, CONF_TOPIC,
DATA_MQTT_CONFIG,
DATA_MQTT_RELOAD_NEEDED, DATA_MQTT_RELOAD_NEEDED,
DEFAULT_ENCODING, DEFAULT_ENCODING,
DEFAULT_PAYLOAD_AVAILABLE, 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): class SetupEntity(Protocol):
"""Protocol type for async_setup_entities.""" """Protocol type for async_setup_entities."""
@ -237,6 +264,31 @@ class SetupEntity(Protocol):
"""Define setup_entities type.""" """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): async def async_setup_entry_helper(hass, domain, async_setup, schema):
"""Set up entity, automation or tag creation dynamically through MQTT discovery.""" """Set up entity, automation or tag creation dynamically through MQTT discovery."""

View file

@ -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_1")
assert hass.states.get(f"{domain}.test_new_2") assert hass.states.get(f"{domain}.test_new_2")
assert hass.states.get(f"{domain}.test_new_3") 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()

View file

@ -55,6 +55,7 @@ from .test_common import (
help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_via_mqtt_json_message,
help_test_setting_attribute_with_template, help_test_setting_attribute_with_template,
help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id, help_test_unique_id,
help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict, 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 domain = fan.DOMAIN
config = DEFAULT_CONFIG[domain] config = DEFAULT_CONFIG[domain]
await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) 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

View file

@ -1,5 +1,6 @@
"""The tests for the MQTT component.""" """The tests for the MQTT component."""
import asyncio import asyncio
import copy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import partial from functools import partial
import json import json
@ -30,6 +31,8 @@ from homeassistant.helpers.entity import Entity
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .test_common import help_test_setup_manual_entity_from_yaml
from tests.common import ( from tests.common import (
MockConfigEntry, MockConfigEntry,
async_fire_mqtt_message, 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" 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): async def test_setup_mqtt_client_protocol(hass):
"""Test MQTT client protocol setup.""" """Test MQTT client protocol setup."""
entry = MockConfigEntry( 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 # User sets up a config entry
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"})
entry.add_to_hass(hass) 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 # Discover a device to verify the entry was setup correctly
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) 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 len(mqtt_connected_calls) == 2
assert mqtt_connected_calls[0] is True assert mqtt_connected_calls[0] is True
assert mqtt_connected_calls[1] is False 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

View file

@ -237,6 +237,7 @@ from .test_common import (
help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_via_mqtt_json_message,
help_test_setting_attribute_with_template, help_test_setting_attribute_with_template,
help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id, help_test_unique_id,
help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict, 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") state = hass.states.get("light.test")
assert state.state == STATE_ON assert state.state == STATE_ON
assert state.attributes.get("effect") == "colorloop" 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

View file

@ -131,6 +131,7 @@ from .test_common import (
help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_via_mqtt_json_message,
help_test_setting_attribute_with_template, help_test_setting_attribute_with_template,
help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id, help_test_unique_id,
help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict, help_test_update_with_json_attrs_not_dict,
@ -2038,3 +2039,15 @@ async def test_encoding_subscribable_topics(
init_payload, init_payload,
skip_raw_test=True, 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

View file

@ -69,6 +69,7 @@ from .test_common import (
help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_via_mqtt_json_message,
help_test_setting_attribute_with_template, help_test_setting_attribute_with_template,
help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setting_blocked_attribute_via_mqtt_json_message,
help_test_setup_manual_entity_from_yaml,
help_test_unique_id, help_test_unique_id,
help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_bad_JSON,
help_test_update_with_json_attrs_not_dict, help_test_update_with_json_attrs_not_dict,
@ -1213,3 +1214,15 @@ async def test_encoding_subscribable_topics(
attribute_value, attribute_value,
init_payload, 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

View file

@ -561,9 +561,10 @@ async def mqtt_mock(hass, mqtt_client_mock, mqtt_config):
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
# Do not forward the entry setup to the components here
assert await hass.config_entries.async_setup(entry.entry_id) with patch("homeassistant.components.mqtt.PLATFORMS", []):
await hass.async_block_till_done() assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
mqtt_component_mock = MagicMock( mqtt_component_mock = MagicMock(
return_value=hass.data["mqtt"], return_value=hass.data["mqtt"],