Do not fail MQTT setup if events or sensors configured via yaml can't be validated (#102309)

* Add event and sensor

* Cleanup unused code

* Schema cannot be None for supported platform
This commit is contained in:
Jan Bouwhuis 2023-10-20 12:09:52 +02:00 committed by GitHub
parent f85b4f734c
commit 7d9014ae41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 44 additions and 95 deletions

View file

@ -235,8 +235,7 @@ async def async_check_config_schema(
mqtt_config: list[dict[str, list[ConfigType]]] = config_yaml[DOMAIN] mqtt_config: list[dict[str, list[ConfigType]]] = config_yaml[DOMAIN]
for mqtt_config_item in mqtt_config: for mqtt_config_item in mqtt_config:
for domain, config_items in mqtt_config_item.items(): for domain, config_items in mqtt_config_item.items():
if (schema := mqtt_data.reload_schema.get(domain)) is None: schema = mqtt_data.reload_schema[domain]
continue
for config in config_items: for config in config_items:
try: try:
schema(config) schema(config)

View file

@ -14,10 +14,6 @@ from homeassistant.const import (
) )
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from . import (
event as event_platform,
sensor as sensor_platform,
)
from .const import ( from .const import (
CONF_BIRTH_MESSAGE, CONF_BIRTH_MESSAGE,
CONF_BROKER, CONF_BROKER,
@ -41,10 +37,7 @@ CONFIG_SCHEMA_BASE = vol.Schema(
Platform.CLIMATE.value: vol.All(cv.ensure_list, [dict]), Platform.CLIMATE.value: vol.All(cv.ensure_list, [dict]),
Platform.COVER.value: vol.All(cv.ensure_list, [dict]), Platform.COVER.value: vol.All(cv.ensure_list, [dict]),
Platform.DEVICE_TRACKER.value: vol.All(cv.ensure_list, [dict]), Platform.DEVICE_TRACKER.value: vol.All(cv.ensure_list, [dict]),
Platform.EVENT.value: vol.All( Platform.EVENT.value: vol.All(cv.ensure_list, [dict]),
cv.ensure_list,
[event_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type]
),
Platform.FAN.value: vol.All(cv.ensure_list, [dict]), Platform.FAN.value: vol.All(cv.ensure_list, [dict]),
Platform.HUMIDIFIER.value: vol.All(cv.ensure_list, [dict]), Platform.HUMIDIFIER.value: vol.All(cv.ensure_list, [dict]),
Platform.IMAGE.value: vol.All(cv.ensure_list, [dict]), Platform.IMAGE.value: vol.All(cv.ensure_list, [dict]),
@ -54,10 +47,7 @@ CONFIG_SCHEMA_BASE = vol.Schema(
Platform.NUMBER.value: vol.All(cv.ensure_list, [dict]), Platform.NUMBER.value: vol.All(cv.ensure_list, [dict]),
Platform.SCENE.value: vol.All(cv.ensure_list, [dict]), Platform.SCENE.value: vol.All(cv.ensure_list, [dict]),
Platform.SELECT.value: vol.All(cv.ensure_list, [dict]), Platform.SELECT.value: vol.All(cv.ensure_list, [dict]),
Platform.SENSOR.value: vol.All( Platform.SENSOR.value: vol.All(cv.ensure_list, [dict]),
cv.ensure_list,
[sensor_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type]
),
Platform.SIREN.value: vol.All(cv.ensure_list, [dict]), Platform.SIREN.value: vol.All(cv.ensure_list, [dict]),
Platform.SWITCH.value: vol.All(cv.ensure_list, [dict]), Platform.SWITCH.value: vol.All(cv.ensure_list, [dict]),
Platform.TEXT.value: vol.All(cv.ensure_list, [dict]), Platform.TEXT.value: vol.All(cv.ensure_list, [dict]),

View file

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
import functools
import logging import logging
from typing import Any from typing import Any
@ -19,7 +18,7 @@ from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_VALUE_TEMPLAT
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType
from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads_object from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads_object
from . import subscription from . import subscription
@ -35,7 +34,7 @@ from .debug_info import log_messages
from .mixins import ( from .mixins import (
MQTT_ENTITY_COMMON_SCHEMA, MQTT_ENTITY_COMMON_SCHEMA,
MqttEntity, MqttEntity,
async_setup_entry_helper, async_mqtt_entry_helper,
write_state_on_attr_change, write_state_on_attr_change,
) )
from .models import ( from .models import (
@ -83,21 +82,15 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up MQTT event through YAML and through MQTT discovery.""" """Set up MQTT event through YAML and through MQTT discovery."""
setup = functools.partial( await async_mqtt_entry_helper(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry hass,
config_entry,
MqttEvent,
event.DOMAIN,
async_add_entities,
DISCOVERY_SCHEMA,
PLATFORM_SCHEMA_MODERN,
) )
await async_setup_entry_helper(hass, event.DOMAIN, setup, DISCOVERY_SCHEMA)
async def _async_setup_entity(
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry,
discovery_data: DiscoveryInfoType | None = None,
) -> None:
"""Set up MQTT event."""
async_add_entities([MqttEvent(hass, config, config_entry, discovery_data)])
class MqttEvent(MqttEntity, EventEntity): class MqttEvent(MqttEntity, EventEntity):

View file

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
import asyncio
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine
import functools import functools
from functools import partial, wraps from functools import partial, wraps
@ -312,7 +311,7 @@ async def async_setup_entry_helper(
async_setup: partial[Coroutine[Any, Any, None]], async_setup: partial[Coroutine[Any, Any, None]],
discovery_schema: vol.Schema, discovery_schema: vol.Schema,
) -> None: ) -> None:
"""Set up entity, automation or tag creation dynamically through MQTT discovery.""" """Set up automation or tag creation dynamically through MQTT discovery."""
mqtt_data = get_mqtt_data(hass) mqtt_data = get_mqtt_data(hass)
async def async_setup_from_discovery( async def async_setup_from_discovery(
@ -332,28 +331,6 @@ async def async_setup_entry_helper(
) )
) )
# The setup of manual configured MQTT entities will be migrated to async_mqtt_entry_helper.
# The following setup code will be cleaned up after the last entity platform has been migrated.
async def _async_setup_entities() -> None:
"""Set up MQTT items from configuration.yaml."""
mqtt_data = get_mqtt_data(hass)
if not (config_yaml := mqtt_data.config):
return
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(*setups)
# discover manual configured MQTT items
mqtt_data.reload_handlers[domain] = _async_setup_entities
await _async_setup_entities()
async def async_mqtt_entry_helper( async def async_mqtt_entry_helper(
hass: HomeAssistant, hass: HomeAssistant,

View file

@ -3,7 +3,6 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from datetime import datetime, timedelta from datetime import datetime, timedelta
import functools
import logging import logging
from typing import Any from typing import Any
@ -33,7 +32,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import subscription from . import subscription
@ -44,7 +43,7 @@ from .mixins import (
MQTT_ENTITY_COMMON_SCHEMA, MQTT_ENTITY_COMMON_SCHEMA,
MqttAvailability, MqttAvailability,
MqttEntity, MqttEntity,
async_setup_entry_helper, async_mqtt_entry_helper,
write_state_on_attr_change, write_state_on_attr_change,
) )
from .models import ( from .models import (
@ -106,21 +105,15 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up MQTT sensor through YAML and through MQTT discovery.""" """Set up MQTT sensor through YAML and through MQTT discovery."""
setup = functools.partial( await async_mqtt_entry_helper(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry hass,
config_entry,
MqttSensor,
sensor.DOMAIN,
async_add_entities,
DISCOVERY_SCHEMA,
PLATFORM_SCHEMA_MODERN,
) )
await async_setup_entry_helper(hass, sensor.DOMAIN, setup, DISCOVERY_SCHEMA)
async def _async_setup_entity(
hass: HomeAssistant,
async_add_entities: AddEntitiesCallback,
config: ConfigType,
config_entry: ConfigEntry,
discovery_data: DiscoveryInfoType | None = None,
) -> None:
"""Set up MQTT sensor."""
async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)])
class MqttSensor(MqttEntity, RestoreSensor): class MqttSensor(MqttEntity, RestoreSensor):

View file

@ -9,6 +9,7 @@ from typing import Any
from unittest.mock import ANY, MagicMock, patch from unittest.mock import ANY, MagicMock, patch
import pytest import pytest
import voluptuous as vol
import yaml import yaml
from homeassistant import config as module_hass_config from homeassistant import config as module_hass_config
@ -363,6 +364,7 @@ async def help_test_default_availability_list_payload_any(
async def help_test_default_availability_list_single( async def help_test_default_availability_list_single(
hass: HomeAssistant, hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
domain: str, domain: str,
config: ConfigType, config: ConfigType,
@ -378,10 +380,10 @@ async def help_test_default_availability_list_single(
] ]
config[mqtt.DOMAIN][domain]["availability_topic"] = "availability-topic" config[mqtt.DOMAIN][domain]["availability_topic"] = "availability-topic"
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) with patch(
entry.add_to_hass(hass) "homeassistant.config.load_yaml_config_file", return_value=config
with patch("homeassistant.config.load_yaml_config_file", return_value=config): ), suppress(vol.MultipleInvalid):
await entry.async_setup(hass) await mqtt_mock_entry()
assert ( assert (
"two or more values in the same group of exclusion 'availability'" "two or more values in the same group of exclusion 'availability'"

View file

@ -224,11 +224,13 @@ async def test_default_availability_list_payload_any(
async def test_default_availability_list_single( async def test_default_availability_list_single(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test availability list and availability_topic are mutually exclusive.""" """Test availability list and availability_topic are mutually exclusive."""
await help_test_default_availability_list_single( await help_test_default_availability_list_single(
hass, caplog, event.DOMAIN, DEFAULT_CONFIG hass, mqtt_mock_entry, caplog, event.DOMAIN, DEFAULT_CONFIG
) )
@ -271,11 +273,8 @@ async def test_invalid_device_class(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test device_class option with invalid value.""" """Test device_class option with invalid value."""
with pytest.raises(AssertionError): assert await mqtt_mock_entry()
await mqtt_mock_entry() assert "expected EventDeviceClass or one of" in caplog.text
assert (
"Invalid config for [mqtt]: expected EventDeviceClass or one of" in caplog.text
)
async def test_setting_attribute_via_mqtt_json_message( async def test_setting_attribute_via_mqtt_json_message(

View file

@ -708,11 +708,13 @@ async def test_default_availability_list_payload_any(
async def test_default_availability_list_single( async def test_default_availability_list_single(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test availability list and availability_topic are mutually exclusive.""" """Test availability list and availability_topic are mutually exclusive."""
await help_test_default_availability_list_single( await help_test_default_availability_list_single(
hass, caplog, sensor.DOMAIN, DEFAULT_CONFIG hass, mqtt_mock_entry, caplog, sensor.DOMAIN, DEFAULT_CONFIG
) )
@ -754,11 +756,8 @@ async def test_invalid_device_class(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test device_class option with invalid value.""" """Test device_class option with invalid value."""
with pytest.raises(AssertionError): assert await mqtt_mock_entry()
await mqtt_mock_entry() assert "expected SensorDeviceClass or one of" in caplog.text
assert (
"Invalid config for [mqtt]: expected SensorDeviceClass or one of" in caplog.text
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -818,11 +817,8 @@ async def test_invalid_state_class(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test state_class option with invalid value.""" """Test state_class option with invalid value."""
with pytest.raises(AssertionError): assert await mqtt_mock_entry()
await mqtt_mock_entry() assert "expected SensorStateClass or one of" in caplog.text
assert (
"Invalid config for [mqtt]: expected SensorStateClass or one of" in caplog.text
)
@pytest.mark.parametrize( @pytest.mark.parametrize(