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:
parent
f85b4f734c
commit
7d9014ae41
8 changed files with 44 additions and 95 deletions
|
@ -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)
|
||||||
|
|
|
@ -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]),
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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'"
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue