Allow ADR 0007 compliant schema for mqtt (#94305)

* Enforce listed entities in MQTT yaml config

* Add tests for setup with listed items

* Fix test

* Remove validator add comment

* Update homeassistant/components/mqtt/__init__.py

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

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Jan Bouwhuis 2023-07-18 14:29:45 +02:00 committed by GitHub
parent f9a0877bb9
commit 7c22225cd1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 176 additions and 48 deletions

View file

@ -5,7 +5,7 @@ import asyncio
from collections.abc import Callable
from datetime import datetime
import logging
from typing import Any, cast
from typing import Any, TypeVar, cast
import jinja2
import voluptuous as vol
@ -42,7 +42,7 @@ from .client import ( # noqa: F401
publish,
subscribe,
)
from .config_integration import PLATFORM_CONFIG_SCHEMA_BASE
from .config_integration import CONFIG_SCHEMA_BASE
from .const import ( # noqa: F401
ATTR_PAYLOAD,
ATTR_QOS,
@ -130,25 +130,54 @@ CONFIG_ENTRY_CONFIG_KEYS = [
CONF_WILL_MESSAGE,
]
_T = TypeVar("_T")
REMOVED_OPTIONS = vol.All(
cv.removed(CONF_BIRTH_MESSAGE), # Removed in HA Core 2023.4
cv.removed(CONF_BROKER), # Removed in HA Core 2023.4
cv.removed(CONF_CERTIFICATE), # Removed in HA Core 2023.4
cv.removed(CONF_CLIENT_ID), # Removed in HA Core 2023.4
cv.removed(CONF_CLIENT_CERT), # Removed in HA Core 2023.4
cv.removed(CONF_CLIENT_KEY), # Removed in HA Core 2023.4
cv.removed(CONF_DISCOVERY), # Removed in HA Core 2022.3
cv.removed(CONF_DISCOVERY_PREFIX), # Removed in HA Core 2023.4
cv.removed(CONF_KEEPALIVE), # Removed in HA Core 2023.4
cv.removed(CONF_PASSWORD), # Removed in HA Core 2023.4
cv.removed(CONF_PORT), # Removed in HA Core 2023.4
cv.removed(CONF_PROTOCOL), # Removed in HA Core 2023.4
cv.removed(CONF_TLS_INSECURE), # Removed in HA Core 2023.4
cv.removed(CONF_USERNAME), # Removed in HA Core 2023.4
cv.removed(CONF_WILL_MESSAGE), # Removed in HA Core 2023.4
)
# We accept 2 schemes for configuring manual MQTT items
#
# Preferred style:
#
# mqtt:
# - {domain}:
# name: ""
# ...
# - {domain}:
# name: ""
# ...
# ```
#
# Legacy supported style:
#
# mqtt:
# {domain}:
# - name: ""
# ...
# - name: ""
# ...
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
cv.removed(CONF_BIRTH_MESSAGE), # Removed in HA Core 2023.4
cv.removed(CONF_BROKER), # Removed in HA Core 2023.4
cv.removed(CONF_CERTIFICATE), # Removed in HA Core 2023.4
cv.removed(CONF_CLIENT_ID), # Removed in HA Core 2023.4
cv.removed(CONF_CLIENT_CERT), # Removed in HA Core 2023.4
cv.removed(CONF_CLIENT_KEY), # Removed in HA Core 2023.4
cv.removed(CONF_DISCOVERY), # Removed in HA Core 2022.3
cv.removed(CONF_DISCOVERY_PREFIX), # Removed in HA Core 2023.4
cv.removed(CONF_KEEPALIVE), # Removed in HA Core 2023.4
cv.removed(CONF_PASSWORD), # Removed in HA Core 2023.4
cv.removed(CONF_PORT), # Removed in HA Core 2023.4
cv.removed(CONF_PROTOCOL), # Removed in HA Core 2023.4
cv.removed(CONF_TLS_INSECURE), # Removed in HA Core 2023.4
cv.removed(CONF_USERNAME), # Removed in HA Core 2023.4
cv.removed(CONF_WILL_MESSAGE), # Removed in HA Core 2023.4
PLATFORM_CONFIG_SCHEMA_BASE,
cv.ensure_list,
cv.remove_falsy,
[REMOVED_OPTIONS],
[CONFIG_SCHEMA_BASE],
)
},
extra=vol.ALLOW_EXTRA,
@ -190,7 +219,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Fetch configuration
conf = dict(entry.data)
hass_config = await conf_util.async_hass_config_yaml(hass)
mqtt_yaml = PLATFORM_CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {}))
mqtt_yaml = CONFIG_SCHEMA(hass_config).get(DOMAIN, [])
await async_create_certificate_temp_files(hass, conf)
client = MQTT(hass, entry, conf)
if DOMAIN in hass.data:

View file

@ -52,7 +52,7 @@ from .const import (
DEFAULT_TLS_PROTOCOL = "auto"
PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema(
CONFIG_SCHEMA_BASE = vol.Schema(
{
Platform.ALARM_CONTROL_PANEL.value: vol.All(
cv.ensure_list,

View file

@ -309,9 +309,16 @@ async def async_setup_entry_helper(
mqtt_data = get_mqtt_data(hass)
if not (config_yaml := mqtt_data.config):
return
if domain not in config_yaml:
setups: list[Coroutine[Any, Any, None]] = [
async_setup(config)
for config_item in config_yaml
for config_domain, configs in config_item.items()
for config in configs
if config_domain == domain
]
if not setups:
return
await asyncio.gather(*[async_setup(config) for config in config_yaml[domain]])
await asyncio.gather(*setups)
# discover manual configured MQTT items
mqtt_data.reload_handlers[domain] = _async_setup_entities

View file

@ -289,7 +289,7 @@ class MqttData:
"""Keep the MQTT entry data."""
client: MQTT
config: ConfigType
config: list[ConfigType]
debug_info_entities: dict[str, EntityDebugInfo] = field(default_factory=dict)
debug_info_triggers: dict[tuple[str, str], TriggerDebugInfo] = field(
default_factory=dict

View file

@ -1096,7 +1096,11 @@ async def test_reloadable(
await help_test_reloadable(hass, mqtt_client_mock, domain, config)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -1203,7 +1203,11 @@ async def test_skip_restoring_state_with_over_due_expire_trigger(
assert state.state == STATE_UNAVAILABLE
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -545,7 +545,11 @@ async def test_reloadable(
await help_test_reloadable(hass, mqtt_client_mock, domain, config)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -439,7 +439,11 @@ async def test_reloadable(
await help_test_reloadable(hass, mqtt_client_mock, domain, config)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -2484,7 +2484,11 @@ async def test_reloadable(
await help_test_reloadable(hass, mqtt_client_mock, domain, config)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -3642,7 +3642,11 @@ async def test_encoding_subscribable_topics(
)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -2220,7 +2220,11 @@ async def test_reloadable(
await help_test_reloadable(hass, mqtt_client_mock, domain, config)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -1545,7 +1545,11 @@ async def test_reloadable(
await help_test_reloadable(hass, mqtt_client_mock, domain, config)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -2106,8 +2106,8 @@ async def test_setup_manual_mqtt_with_invalid_config(
with pytest.raises(AssertionError):
await mqtt_mock_entry()
assert (
"Invalid config for [mqtt]: required key not provided @ data['mqtt']['light'][0]['command_topic']."
" Got None. (See ?, line ?)" in caplog.text
"Invalid config for [mqtt]: required key not provided @ data['mqtt'][0]['light'][0]['command_topic']. "
"Got None. (See ?, line ?)" in caplog.text
)

View file

@ -1087,7 +1087,11 @@ async def test_encoding_subscribable_topics(
)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -3440,7 +3440,11 @@ async def test_sending_mqtt_xy_command_with_template(
assert state.attributes["xy_color"] == (0.151, 0.343)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -2441,7 +2441,11 @@ async def test_encoding_subscribable_topics(
)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -1354,7 +1354,11 @@ async def test_encoding_subscribable_topics(
)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -1006,7 +1006,11 @@ async def test_encoding_subscribable_topics(
)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -1097,7 +1097,11 @@ async def test_encoding_subscribable_topics(
)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -251,7 +251,11 @@ async def test_reloadable(
await help_test_reloadable(hass, mqtt_client_mock, domain, config)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -762,7 +762,11 @@ async def test_encoding_subscribable_topics(
)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -1385,7 +1385,11 @@ async def test_encoding_subscribable_topics(
)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -1067,7 +1067,11 @@ async def test_encoding_subscribable_topics(
)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -809,7 +809,11 @@ async def test_encoding_subscribable_topics(
)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -738,7 +738,11 @@ async def test_encoding_subscribable_topics(
)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -738,7 +738,11 @@ async def test_encoding_subscribable_topics(
)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -696,7 +696,11 @@ async def test_entity_id_update_discovery_update(
)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:

View file

@ -1087,7 +1087,11 @@ async def test_reloadable(
await help_test_reloadable(hass, mqtt_client_mock, domain, config)
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@pytest.mark.parametrize(
"hass_config",
[DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}],
ids=["platform_key", "listed"],
)
async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None: