Do not fail mqtt entry on single platform config schema error (#101373)

* Do not fail mqtt entry on platform config

* Raise on reload with invalid config

* Do not store issues

* Follow up
This commit is contained in:
Jan Bouwhuis 2023-10-19 12:06:33 +02:00 committed by GitHub
parent 857f2e1d86
commit c377cf1ce0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 320 additions and 38 deletions

View file

@ -29,9 +29,14 @@ from homeassistant.helpers import config_validation as cv, event as ev, template
from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.helpers.entity_platform import async_get_platforms
from homeassistant.helpers.issue_registry import (
async_delete_issue,
async_get as async_get_issue_registry,
)
from homeassistant.helpers.reload import async_integration_yaml_config from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_integration
# Loading the config flow file will register the flow # Loading the config flow file will register the flow
from . import debug_info, discovery from . import debug_info, discovery
@ -209,6 +214,41 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -
await hass.config_entries.async_reload(entry.entry_id) await hass.config_entries.async_reload(entry.entry_id)
@callback
def _async_remove_mqtt_issues(hass: HomeAssistant, mqtt_data: MqttData) -> None:
"""Unregister open config issues."""
issue_registry = async_get_issue_registry(hass)
open_issues = [
issue_id
for (domain, issue_id), issue_entry in issue_registry.issues.items()
if domain == DOMAIN and issue_entry.translation_key == "invalid_platform_config"
]
for issue in open_issues:
async_delete_issue(hass, DOMAIN, issue)
async def async_check_config_schema(
hass: HomeAssistant, config_yaml: ConfigType
) -> None:
"""Validate manually configured MQTT items."""
mqtt_data = get_mqtt_data(hass)
mqtt_config: list[dict[str, list[ConfigType]]] = config_yaml[DOMAIN]
for mqtt_config_item in mqtt_config:
for domain, config_items in mqtt_config_item.items():
if (schema := mqtt_data.reload_schema.get(domain)) is None:
continue
for config in config_items:
try:
schema(config)
except vol.Invalid as ex:
integration = await async_get_integration(hass, DOMAIN)
# pylint: disable-next=protected-access
message, _ = conf_util._format_config_error(
ex, domain, config, integration.documentation
)
raise HomeAssistantError(message) from ex
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Load a config entry.""" """Load a config entry."""
conf: dict[str, Any] conf: dict[str, Any]
@ -373,6 +413,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"Error reloading manually configured MQTT items, " "Error reloading manually configured MQTT items, "
"check your configuration.yaml" "check your configuration.yaml"
) )
# Check the schema before continuing reload
await async_check_config_schema(hass, config_yaml)
# Remove repair issues
_async_remove_mqtt_issues(hass, mqtt_data)
mqtt_data.config = config_yaml.get(DOMAIN, {}) mqtt_data.config = config_yaml.get(DOMAIN, {})
# Reload the modern yaml platforms # Reload the modern yaml platforms
@ -594,4 +640,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if subscriptions := mqtt_client.subscriptions: if subscriptions := mqtt_client.subscriptions:
mqtt_data.subscriptions_to_restore = subscriptions mqtt_data.subscriptions_to_restore = subscriptions
# Remove repair issues
_async_remove_mqtt_issues(hass, mqtt_data)
return True return True

View file

@ -1,7 +1,6 @@
"""Control a MQTT alarm.""" """Control a MQTT alarm."""
from __future__ import annotations from __future__ import annotations
import functools
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -27,7 +26,7 @@ from homeassistant.const import (
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 . import subscription from . import subscription
from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA
@ -44,7 +43,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 MqttCommandTemplate, MqttValueTemplate, ReceiveMessage from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage
@ -133,21 +132,15 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up MQTT alarm control panel through YAML and through MQTT discovery.""" """Set up MQTT alarm control panel 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,
MqttAlarm,
alarm.DOMAIN,
async_add_entities,
DISCOVERY_SCHEMA,
PLATFORM_SCHEMA_MODERN,
) )
await async_setup_entry_helper(hass, alarm.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 the MQTT Alarm Control Panel platform."""
async_add_entities([MqttAlarm(hass, config, config_entry, discovery_data)])
class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity):

View file

@ -15,7 +15,6 @@ from homeassistant.const import (
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from . import ( from . import (
alarm_control_panel as alarm_control_panel_platform,
binary_sensor as binary_sensor_platform, binary_sensor as binary_sensor_platform,
button as button_platform, button as button_platform,
camera as camera_platform, camera as camera_platform,
@ -56,10 +55,7 @@ DEFAULT_TLS_PROTOCOL = "auto"
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, [dict]),
cv.ensure_list,
[alarm_control_panel_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] # noqa: E501
),
Platform.BINARY_SENSOR.value: vol.All( Platform.BINARY_SENSOR.value: vol.All(
cv.ensure_list, cv.ensure_list,
[binary_sensor_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] [binary_sensor_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type]

View file

@ -9,6 +9,7 @@ import logging
from typing import TYPE_CHECKING, Any, Protocol, cast, final from typing import TYPE_CHECKING, Any, Protocol, cast, final
import voluptuous as vol import voluptuous as vol
import yaml
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@ -53,6 +54,7 @@ from homeassistant.helpers.event import (
async_track_device_registry_updated_event, async_track_device_registry_updated_event,
async_track_entity_registry_updated_event, async_track_entity_registry_updated_event,
) )
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ( from homeassistant.helpers.typing import (
UNDEFINED, UNDEFINED,
ConfigType, ConfigType,
@ -337,6 +339,117 @@ async def async_setup_entry_helper(
await _async_setup_entities() await _async_setup_entities()
async def async_mqtt_entry_helper(
hass: HomeAssistant,
entry: ConfigEntry,
entity_class: type[MqttEntity],
domain: str,
async_add_entities: AddEntitiesCallback,
discovery_schema: vol.Schema,
platform_schema_modern: vol.Schema,
) -> None:
"""Set up entity, automation or tag creation dynamically through MQTT discovery."""
mqtt_data = get_mqtt_data(hass)
async def async_discover(discovery_payload: MQTTDiscoveryPayload) -> None:
"""Discover and add an MQTT entity, automation or tag."""
if not mqtt_config_entry_enabled(hass):
_LOGGER.warning(
(
"MQTT integration is disabled, skipping setup of discovered item "
"MQTT %s, payload %s"
),
domain,
discovery_payload,
)
return
discovery_data = discovery_payload.discovery_data
try:
config: DiscoveryInfoType = discovery_schema(discovery_payload)
async_add_entities([entity_class(hass, config, entry, discovery_data)])
except vol.Invalid as err:
discovery_hash = discovery_data[ATTR_DISCOVERY_HASH]
clear_discovery_hash(hass, discovery_hash)
async_dispatcher_send(
hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None
)
async_handle_schema_error(discovery_payload, err)
except Exception:
discovery_hash = discovery_data[ATTR_DISCOVERY_HASH]
clear_discovery_hash(hass, discovery_hash)
async_dispatcher_send(
hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None
)
raise
mqtt_data.reload_dispatchers.append(
async_dispatcher_connect(
hass, MQTT_DISCOVERY_NEW.format(domain, "mqtt"), async_discover
)
)
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
yaml_configs: list[ConfigType] = [
config
for config_item in config_yaml
for config_domain, configs in config_item.items()
for config in configs
if config_domain == domain
]
entities: list[Entity] = []
for yaml_config in yaml_configs:
try:
config = platform_schema_modern(yaml_config)
entities.append(entity_class(hass, config, entry, None))
except vol.Invalid as ex:
error = str(ex)
config_file = getattr(yaml_config, "__config_file__", "?")
line = getattr(yaml_config, "__line__", "?")
issue_id = hex(hash(frozenset(yaml_config.items())))
yaml_config_str = yaml.dump(dict(yaml_config))
learn_more_url = (
f"https://www.home-assistant.io/integrations/{domain}.mqtt/"
)
async_create_issue(
hass,
DOMAIN,
issue_id,
issue_domain=domain,
is_fixable=False,
severity=IssueSeverity.ERROR,
learn_more_url=learn_more_url,
translation_placeholders={
"domain": domain,
"config_file": config_file,
"line": line,
"config": yaml_config_str,
"error": error,
},
translation_key="invalid_platform_config",
)
_LOGGER.error(
"%s for manual configured MQTT %s item, in %s, line %s Got %s",
error,
domain,
config_file,
line,
yaml_config,
)
async_add_entities(entities)
# When reloading we check manual configured items against the schema
# before reloading
mqtt_data.reload_schema[domain] = platform_schema_modern
# discover manual configured MQTT items
mqtt_data.reload_handlers[domain] = _async_setup_entities
await _async_setup_entities()
def init_entity_id_from_config( def init_entity_id_from_config(
hass: HomeAssistant, entity: Entity, config: ConfigType, entity_id_format: str hass: HomeAssistant, entity: Entity, config: ConfigType, entity_id_format: str
) -> None: ) -> None:

View file

@ -11,6 +11,8 @@ from enum import StrEnum
import logging import logging
from typing import TYPE_CHECKING, Any, TypedDict from typing import TYPE_CHECKING, Any, TypedDict
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers import template from homeassistant.helpers import template
@ -343,6 +345,7 @@ class MqttData:
reload_handlers: dict[str, Callable[[], Coroutine[Any, Any, None]]] = field( reload_handlers: dict[str, Callable[[], Coroutine[Any, Any, None]]] = field(
default_factory=dict default_factory=dict
) )
reload_schema: dict[str, vol.Schema] = field(default_factory=dict)
state_write_requests: EntityTopicState = field(default_factory=EntityTopicState) state_write_requests: EntityTopicState = field(default_factory=EntityTopicState)
subscriptions_to_restore: list[Subscription] = field(default_factory=list) subscriptions_to_restore: list[Subscription] = field(default_factory=list)
tags: dict[str, dict[str, MQTTTagScanner]] = field(default_factory=dict) tags: dict[str, dict[str, MQTTTagScanner]] = field(default_factory=dict)

View file

@ -19,6 +19,10 @@
"deprecated_climate_aux_property": { "deprecated_climate_aux_property": {
"title": "MQTT entities with auxiliary heat support found", "title": "MQTT entities with auxiliary heat support found",
"description": "Entity `{entity_id}` has auxiliary heat support enabled, which has been deprecated for MQTT climate devices. Please adjust your configuration and remove deprecated config options from your configuration and restart Home Assistant to fix this issue." "description": "Entity `{entity_id}` has auxiliary heat support enabled, which has been deprecated for MQTT climate devices. Please adjust your configuration and remove deprecated config options from your configuration and restart Home Assistant to fix this issue."
},
"invalid_platform_config": {
"title": "Invalid configured MQTT {domain} item",
"description": "Home Assistant detected an invalid config for a manual configured item.\n\nPlatform domain: **{domain}**\nConfiguration file: **{config_file}**\nNear line: **{line}**\nConfiguration found:\n```yaml\n{config}\n```\nError: **{error}**.\n\nMake sure the configuration is valid and [reload](/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue."
} }
}, },
"config": { "config": {

View file

@ -22,6 +22,7 @@ from homeassistant.const import (
SERVICE_ALARM_ARM_VACATION, SERVICE_ALARM_ARM_VACATION,
SERVICE_ALARM_DISARM, SERVICE_ALARM_DISARM,
SERVICE_ALARM_TRIGGER, SERVICE_ALARM_TRIGGER,
SERVICE_RELOAD,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_HOME,
@ -36,6 +37,7 @@ from homeassistant.const import (
Platform, Platform,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .test_common import ( from .test_common import (
help_custom_config, help_custom_config,
@ -184,11 +186,9 @@ async def test_fail_setup_without_state_or_command_topic(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, valid hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, valid
) -> None: ) -> None:
"""Test for failing setup with no state or command topic.""" """Test for failing setup with no state or command topic."""
if valid: assert await mqtt_mock_entry()
await mqtt_mock_entry() state = hass.states.get(f"{alarm_control_panel.DOMAIN}.test")
return assert (state is not None) == valid
with pytest.raises(AssertionError):
await mqtt_mock_entry()
@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG])
@ -306,15 +306,13 @@ async def test_supported_features(
valid: bool, valid: bool,
) -> None: ) -> None:
"""Test conditional enablement of supported features.""" """Test conditional enablement of supported features."""
assert await mqtt_mock_entry()
state = hass.states.get("alarm_control_panel.test")
if valid: if valid:
await mqtt_mock_entry() assert state is not None
assert ( assert state.attributes["supported_features"] == expected_features
hass.states.get("alarm_control_panel.test").attributes["supported_features"]
== expected_features
)
else: else:
with pytest.raises(AssertionError): assert state is None
await mqtt_mock_entry()
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -1269,3 +1267,90 @@ async def test_skipped_async_ha_write_state(
"""Test a write state command is only called when there is change.""" """Test a write state command is only called when there is change."""
await mqtt_mock_entry() await mqtt_mock_entry()
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2) await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2)
@pytest.mark.parametrize(
"hass_config",
[
{
"mqtt": [
{
"alarm_control_panel": {
"name": "test",
"invalid_topic": "test-topic",
}
},
]
}
],
)
async def test_reload_after_invalid_config(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test reloading yaml config fails."""
with patch(
"homeassistant.components.mqtt.async_delete_issue"
) as mock_async_remove_issue:
assert await mqtt_mock_entry()
assert hass.states.get("alarm_control_panel.test") is None
assert (
"extra keys not allowed @ data['invalid_topic'] for "
"manual configured MQTT alarm_control_panel item, "
"in ?, line ? Got {'name': 'test', 'invalid_topic': 'test-topic'}"
in caplog.text
)
# Reload with an valid config
valid_config = {
"mqtt": [
{
"alarm_control_panel": {
"name": "test",
"command_topic": "test-topic",
"state_topic": "alarm/state",
}
},
]
}
with patch(
"homeassistant.config.load_yaml_config_file", return_value=valid_config
):
await hass.services.async_call(
"mqtt",
SERVICE_RELOAD,
{},
blocking=True,
)
await hass.async_block_till_done()
# Test the config is loaded now and that the existing issue is removed
assert hass.states.get("alarm_control_panel.test") is not None
assert mock_async_remove_issue.call_count == 1
# Reload with an invalid config
invalid_config = {
"mqtt": [
{
"alarm_control_panel": {
"name": "test",
"command_topic": "test-topic",
"invalid_option": "should_fail",
}
},
]
}
with patch(
"homeassistant.config.load_yaml_config_file", return_value=invalid_config
), pytest.raises(HomeAssistantError):
await hass.services.async_call(
"mqtt",
SERVICE_RELOAD,
{},
blocking=True,
)
await hass.async_block_till_done()
# Make sure the config is loaded now
assert hass.states.get("alarm_control_panel.test") is not None

View file

@ -145,6 +145,25 @@ async def test_discovery_schema_error(
assert "AttributeError: Attribute abc not found" in caplog.text assert "AttributeError: Attribute abc not found" in caplog.text
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.ALARM_CONTROL_PANEL])
async def test_invalid_config(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test sending in JSON that violates the platform schema."""
await mqtt_mock_entry()
async_fire_mqtt_message(
hass,
"homeassistant/alarm_control_panel/bla/config",
'{"name": "abc", "state_topic": "home/alarm", '
'"command_topic": "home/alarm/set", '
'"qos": "some_invalid_value"}',
)
await hass.async_block_till_done()
assert "Error 'expected int for dictionary value @ data['qos']'" in caplog.text
async def test_only_valid_components( async def test_only_valid_components(
hass: HomeAssistant, hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator, mqtt_mock_entry: MqttMockHAClientGenerator,

View file

@ -3547,14 +3547,21 @@ async def test_publish_or_subscribe_without_valid_config_entry(
@patch( @patch(
"homeassistant.components.mqtt.PLATFORMS", "homeassistant.components.mqtt.PLATFORMS",
["tag", Platform.LIGHT], [Platform.ALARM_CONTROL_PANEL, Platform.LIGHT],
) )
@pytest.mark.parametrize( @pytest.mark.parametrize(
"hass_config", "hass_config",
[ [
{ {
"mqtt": { "mqtt": {
"light": [{"name": "test", "command_topic": "test-topic"}], "alarm_control_panel": [
{
"name": "test",
"state_topic": "home/alarm",
"command_topic": "home/alarm/set",
},
],
"light": [{"name": "test", "command_topic": "test-topic_new"}],
} }
} }
], ],
@ -3568,10 +3575,18 @@ async def test_disabling_and_enabling_entry(
await mqtt_mock_entry() await mqtt_mock_entry()
entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
# Late discovery of a mqtt entity/tag # Late discovery of a mqtt entity
config_tag = '{"topic": "0AFFD2/tag_scanned", "value_template": "{{ value_json.PN532.UID }}"}' config_tag = '{"topic": "0AFFD2/tag_scanned", "value_template": "{{ value_json.PN532.UID }}"}'
config_light = '{"name": "test2", "command_topic": "test-topic_new"}' config_alarm_control_panel = '{"name": "test_new", "state_topic": "home/alarm", "command_topic": "home/alarm/set"}'
config_light = '{"name": "test_new", "command_topic": "test-topic_new"}'
# Discovery of mqtt tag
async_fire_mqtt_message(hass, "homeassistant/tag/abc/config", config_tag) async_fire_mqtt_message(hass, "homeassistant/tag/abc/config", config_tag)
# Late discovery of mqtt entities
async_fire_mqtt_message(
hass, "homeassistant/alarm_control_panel/abc/config", config_alarm_control_panel
)
async_fire_mqtt_message(hass, "homeassistant/light/abc/config", config_light) async_fire_mqtt_message(hass, "homeassistant/light/abc/config", config_light)
# Disable MQTT config entry # Disable MQTT config entry
@ -3585,6 +3600,10 @@ async def test_disabling_and_enabling_entry(
"MQTT integration is disabled, skipping setup of discovered item MQTT tag" "MQTT integration is disabled, skipping setup of discovered item MQTT tag"
in caplog.text in caplog.text
) )
assert (
"MQTT integration is disabled, skipping setup of discovered item MQTT alarm_control_panel"
in caplog.text
)
assert ( assert (
"MQTT integration is disabled, skipping setup of discovered item MQTT light" "MQTT integration is disabled, skipping setup of discovered item MQTT light"
in caplog.text in caplog.text
@ -3601,6 +3620,7 @@ async def test_disabling_and_enabling_entry(
assert new_mqtt_config_entry.state is ConfigEntryState.LOADED assert new_mqtt_config_entry.state is ConfigEntryState.LOADED
assert hass.states.get("light.test") is not None assert hass.states.get("light.test") is not None
assert hass.states.get("alarm_control_panel.test") is not None
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT])