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 collections.abc import Callable
from datetime import datetime from datetime import datetime
import logging import logging
from typing import Any, cast from typing import Any, TypeVar, cast
import jinja2 import jinja2
import voluptuous as vol import voluptuous as vol
@ -42,7 +42,7 @@ from .client import ( # noqa: F401
publish, publish,
subscribe, subscribe,
) )
from .config_integration import PLATFORM_CONFIG_SCHEMA_BASE from .config_integration import CONFIG_SCHEMA_BASE
from .const import ( # noqa: F401 from .const import ( # noqa: F401
ATTR_PAYLOAD, ATTR_PAYLOAD,
ATTR_QOS, ATTR_QOS,
@ -130,25 +130,54 @@ CONFIG_ENTRY_CONFIG_KEYS = [
CONF_WILL_MESSAGE, 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( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.All( DOMAIN: vol.All(
cv.removed(CONF_BIRTH_MESSAGE), # Removed in HA Core 2023.4 cv.ensure_list,
cv.removed(CONF_BROKER), # Removed in HA Core 2023.4 cv.remove_falsy,
cv.removed(CONF_CERTIFICATE), # Removed in HA Core 2023.4 [REMOVED_OPTIONS],
cv.removed(CONF_CLIENT_ID), # Removed in HA Core 2023.4 [CONFIG_SCHEMA_BASE],
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,
) )
}, },
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
@ -190,7 +219,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Fetch configuration # Fetch configuration
conf = dict(entry.data) conf = dict(entry.data)
hass_config = await conf_util.async_hass_config_yaml(hass) 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) await async_create_certificate_temp_files(hass, conf)
client = MQTT(hass, entry, conf) client = MQTT(hass, entry, conf)
if DOMAIN in hass.data: if DOMAIN in hass.data:

View file

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

View file

@ -309,9 +309,16 @@ async def async_setup_entry_helper(
mqtt_data = get_mqtt_data(hass) mqtt_data = get_mqtt_data(hass)
if not (config_yaml := mqtt_data.config): if not (config_yaml := mqtt_data.config):
return 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 return
await asyncio.gather(*[async_setup(config) for config in config_yaml[domain]]) await asyncio.gather(*setups)
# discover manual configured MQTT items # discover manual configured MQTT items
mqtt_data.reload_handlers[domain] = _async_setup_entities mqtt_data.reload_handlers[domain] = _async_setup_entities

View file

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

View file

@ -1096,7 +1096,11 @@ async def test_reloadable(
await help_test_reloadable(hass, mqtt_client_mock, domain, config) 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( async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None: ) -> None:

View file

@ -1203,7 +1203,11 @@ async def test_skip_restoring_state_with_over_due_expire_trigger(
assert state.state == STATE_UNAVAILABLE 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( async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None: ) -> None:

View file

@ -545,7 +545,11 @@ async def test_reloadable(
await help_test_reloadable(hass, mqtt_client_mock, domain, config) 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( async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None: ) -> None:

View file

@ -439,7 +439,11 @@ async def test_reloadable(
await help_test_reloadable(hass, mqtt_client_mock, domain, config) 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( async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None: ) -> None:

View file

@ -2484,7 +2484,11 @@ async def test_reloadable(
await help_test_reloadable(hass, mqtt_client_mock, domain, config) 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( async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None: ) -> 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( async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None: ) -> None:

View file

@ -2220,7 +2220,11 @@ async def test_reloadable(
await help_test_reloadable(hass, mqtt_client_mock, domain, config) 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( async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None: ) -> None:

View file

@ -1545,7 +1545,11 @@ async def test_reloadable(
await help_test_reloadable(hass, mqtt_client_mock, domain, config) 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( async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None: ) -> None:

View file

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

View file

@ -251,7 +251,11 @@ async def test_reloadable(
await help_test_reloadable(hass, mqtt_client_mock, domain, config) 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( async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None: ) -> 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( async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None: ) -> 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( async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None: ) -> 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( async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None: ) -> 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( async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None: ) -> 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( async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None: ) -> 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( async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None: ) -> 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( async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None: ) -> None:

View file

@ -1087,7 +1087,11 @@ async def test_reloadable(
await help_test_reloadable(hass, mqtt_client_mock, domain, config) 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( async def test_setup_manual_entity_from_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None: ) -> None: