Remove dependency on async_setup from mqtt integration (#87987)

* Remove async_setup from mqtt integration

* Final update common tests

* Related tests init

* Related tests diagnostics

* Related tests config_flow

* Cleanup and correct test

* Keep websockets_api commands in async_setup
This commit is contained in:
Jan Bouwhuis 2023-03-28 09:37:07 +02:00 committed by GitHub
parent 5e03272821
commit 14ffda9758
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 153 additions and 428 deletions

View file

@ -10,7 +10,7 @@ from typing import Any, cast
import jinja2 import jinja2
import voluptuous as vol import voluptuous as vol
from homeassistant import config as conf_util, config_entries from homeassistant import config as conf_util
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@ -25,16 +25,10 @@ from homeassistant.const import (
) )
from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.exceptions import TemplateError, Unauthorized
from homeassistant.helpers import ( from homeassistant.helpers import config_validation as cv, event, template
config_validation as cv,
discovery_flow,
event,
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 IssueSeverity, async_create_issue
from homeassistant.helpers.reload import ( from homeassistant.helpers.reload import (
async_integration_yaml_config, async_integration_yaml_config,
async_reload_integration_platforms, async_reload_integration_platforms,
@ -52,11 +46,9 @@ from .client import ( # noqa: F401
subscribe, subscribe,
) )
from .config_integration import ( from .config_integration import (
CONFIG_SCHEMA_BASE,
CONFIG_SCHEMA_ENTRY, CONFIG_SCHEMA_ENTRY,
DEFAULT_VALUES, DEFAULT_VALUES,
DEPRECATED_CERTIFICATE_CONFIG_KEYS, PLATFORM_CONFIG_SCHEMA_BASE,
DEPRECATED_CONFIG_KEYS,
) )
from .const import ( # noqa: F401 from .const import ( # noqa: F401
ATTR_PAYLOAD, ATTR_PAYLOAD,
@ -99,7 +91,6 @@ from .models import ( # noqa: F401
from .util import ( from .util import (
async_create_certificate_temp_files, async_create_certificate_temp_files,
get_mqtt_data, get_mqtt_data,
migrate_certificate_file_to_content,
mqtt_config_entry_enabled, mqtt_config_entry_enabled,
valid_publish_topic, valid_publish_topic,
valid_qos_schema, valid_qos_schema,
@ -146,22 +137,22 @@ CONFIG_ENTRY_CONFIG_KEYS = [
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.All( DOMAIN: vol.All(
cv.deprecated(CONF_BIRTH_MESSAGE), # Deprecated in HA Core 2022.3 cv.removed(CONF_BIRTH_MESSAGE), # Removed in HA Core 2023.4
cv.deprecated(CONF_BROKER), # Deprecated in HA Core 2022.3 cv.removed(CONF_BROKER), # Removed in HA Core 2023.4
cv.deprecated(CONF_CERTIFICATE), # Deprecated in HA Core 2022.11 cv.removed(CONF_CERTIFICATE), # Removed in HA Core 2023.4
cv.deprecated(CONF_CLIENT_ID), # Deprecated in HA Core 2022.11 cv.removed(CONF_CLIENT_ID), # Removed in HA Core 2023.4
cv.deprecated(CONF_CLIENT_CERT), # Deprecated in HA Core 2022.11 cv.removed(CONF_CLIENT_CERT), # Removed in HA Core 2023.4
cv.deprecated(CONF_CLIENT_KEY), # Deprecated in HA Core 2022.11 cv.removed(CONF_CLIENT_KEY), # Removed in HA Core 2023.4
cv.deprecated(CONF_DISCOVERY), # Deprecated in HA Core 2022.3 cv.removed(CONF_DISCOVERY), # Removed in HA Core 2022.3
cv.deprecated(CONF_DISCOVERY_PREFIX), # Deprecated in HA Core 2022.11 cv.removed(CONF_DISCOVERY_PREFIX), # Removed in HA Core 2023.4
cv.deprecated(CONF_KEEPALIVE), # Deprecated in HA Core 2022.11 cv.removed(CONF_KEEPALIVE), # Removed in HA Core 2023.4
cv.deprecated(CONF_PASSWORD), # Deprecated in HA Core 2022.3 cv.removed(CONF_PASSWORD), # Removed in HA Core 2023.4
cv.deprecated(CONF_PORT), # Deprecated in HA Core 2022.3 cv.removed(CONF_PORT), # Removed in HA Core 2023.4
cv.deprecated(CONF_PROTOCOL), # Deprecated in HA Core 2022.11 cv.removed(CONF_PROTOCOL), # Removed in HA Core 2023.4
cv.deprecated(CONF_TLS_INSECURE), # Deprecated in HA Core 2022.11 cv.removed(CONF_TLS_INSECURE), # Removed in HA Core 2023.4
cv.deprecated(CONF_USERNAME), # Deprecated in HA Core 2022.3 cv.removed(CONF_USERNAME), # Removed in HA Core 2023.4
cv.deprecated(CONF_WILL_MESSAGE), # Deprecated in HA Core 2022.3 cv.removed(CONF_WILL_MESSAGE), # Removed in HA Core 2023.4
CONFIG_SCHEMA_BASE, PLATFORM_CONFIG_SCHEMA_BASE,
) )
}, },
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
@ -197,34 +188,8 @@ async def _async_setup_discovery(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the MQTT protocol service.""" """Set up the MQTT protocol service."""
mqtt_data = get_mqtt_data(hass, True)
conf: ConfigType | None = config.get(DOMAIN)
websocket_api.async_register_command(hass, websocket_subscribe) websocket_api.async_register_command(hass, websocket_subscribe)
websocket_api.async_register_command(hass, websocket_mqtt_info) websocket_api.async_register_command(hass, websocket_mqtt_info)
if conf:
conf = dict(conf)
mqtt_data.config = conf
if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is None:
# Create an import flow if the user has yaml configured entities etc.
# but no broker configuration. Note: The intention is not for this to
# import broker configuration from YAML because that has been deprecated.
discovery_flow.async_create_flow(
hass,
DOMAIN,
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
data={},
)
mqtt_data.reload_needed = True
elif mqtt_entry_status is False:
_LOGGER.info(
"MQTT will be not available until the config entry is enabled",
)
mqtt_data.reload_needed = True
return True return True
@ -247,30 +212,15 @@ def _filter_entry_config(hass: HomeAssistant, entry: ConfigEntry) -> None:
hass.config_entries.async_update_entry(entry, data=filtered_data) hass.config_entries.async_update_entry(entry, data=filtered_data)
async def _async_merge_basic_config( async def _async_auto_mend_config(
hass: HomeAssistant, entry: ConfigEntry, yaml_config: dict[str, Any] hass: HomeAssistant, entry: ConfigEntry, yaml_config: dict[str, Any]
) -> None: ) -> None:
"""Merge basic options in configuration.yaml config with config entry. """Mends config fetched from config entry and adds missing values.
This mends incomplete migration from old version of HA Core. This mends incomplete migration from old version of HA Core.
""" """
entry_updated = False entry_updated = False
entry_config = {**entry.data} entry_config = {**entry.data}
for key in DEPRECATED_CERTIFICATE_CONFIG_KEYS:
if key in yaml_config and key not in entry_config:
if (
content := await hass.async_add_executor_job(
migrate_certificate_file_to_content, yaml_config[key]
)
) is not None:
entry_config[key] = content
entry_updated = True
for key in DEPRECATED_CONFIG_KEYS:
if key in yaml_config and key not in entry_config:
entry_config[key] = yaml_config[key]
entry_updated = True
for key in MANDATORY_DEFAULT_VALUES: for key in MANDATORY_DEFAULT_VALUES:
if key not in entry_config: if key not in entry_config:
entry_config[key] = DEFAULT_VALUES[key] entry_config[key] = DEFAULT_VALUES[key]
@ -298,17 +248,16 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -
async def async_fetch_config( async def async_fetch_config(
hass: HomeAssistant, entry: ConfigEntry hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any] | None: ) -> dict[str, Any] | None:
"""Fetch fresh MQTT yaml config from the hass config when (re)loading the entry.""" """Fetch fresh MQTT yaml config from the hass config."""
mqtt_data = get_mqtt_data(hass) mqtt_data = get_mqtt_data(hass)
if mqtt_data.reload_entry: hass_config = await conf_util.async_hass_config_yaml(hass)
hass_config = await conf_util.async_hass_config_yaml(hass) mqtt_data.config = PLATFORM_CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {}))
mqtt_data.config = CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {}))
# Remove unknown keys from config entry data # Remove unknown keys from config entry data
_filter_entry_config(hass, entry) _filter_entry_config(hass, entry)
# Merge basic configuration, and add missing defaults for basic options # Add missing defaults to migrate older config entries
await _async_merge_basic_config(hass, entry, mqtt_data.config or {}) await _async_auto_mend_config(hass, entry, mqtt_data.config or {})
# Bail out if broker setting is missing # Bail out if broker setting is missing
if CONF_BROKER not in entry.data: if CONF_BROKER not in entry.data:
_LOGGER.error("MQTT broker is not configured, please configure it") _LOGGER.error("MQTT broker is not configured, please configure it")
@ -319,37 +268,6 @@ async def async_fetch_config(
if (conf := mqtt_data.config) is None: if (conf := mqtt_data.config) is None:
conf = CONFIG_SCHEMA_ENTRY(dict(entry.data)) conf = CONFIG_SCHEMA_ENTRY(dict(entry.data))
# User has configuration.yaml config, warn about config entry overrides
elif any(key in conf for key in entry.data):
shared_keys = conf.keys() & entry.data.keys()
override = {k: entry.data[k] for k in shared_keys if conf[k] != entry.data[k]}
if CONF_PASSWORD in override:
override[CONF_PASSWORD] = "********"
if CONF_CLIENT_KEY in override:
override[CONF_CLIENT_KEY] = "-----PRIVATE KEY-----"
if override:
_LOGGER.warning(
(
"Deprecated configuration settings found in configuration.yaml. "
"These settings from your configuration entry will override: %s"
),
override,
)
# Register a repair issue
async_create_issue(
hass,
DOMAIN,
"deprecated_yaml_broker_settings",
breaks_in_ha_version="2023.4.0", # Warning first added in 2022.11.0
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml_broker_settings",
translation_placeholders={
"more_info_url": "https://www.home-assistant.io/integrations/mqtt/",
"deprecated_settings": str(shared_keys)[1:-1],
},
)
# Merge advanced configuration values from configuration.yaml # Merge advanced configuration values from configuration.yaml
conf = _merge_extended_config(entry, conf) conf = _merge_extended_config(entry, conf)
return conf return conf
@ -359,10 +277,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Load a config entry.""" """Load a config entry."""
mqtt_data = get_mqtt_data(hass, True) mqtt_data = get_mqtt_data(hass, True)
# Merge basic configuration, and add missing defaults for basic options # Fetch configuration and add missing defaults for basic options
if (conf := await async_fetch_config(hass, entry)) is None: if (conf := await async_fetch_config(hass, entry)) is None:
# Bail out # Bail out
return False return False
await async_create_certificate_temp_files(hass, dict(entry.data)) await async_create_certificate_temp_files(hass, dict(entry.data))
mqtt_data.client = MQTT(hass, entry, conf) mqtt_data.client = MQTT(hass, entry, conf)
# Restore saved subscriptions # Restore saved subscriptions
@ -480,6 +399,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def _reload_config(call: ServiceCall) -> None: async def _reload_config(call: ServiceCall) -> None:
"""Reload the platforms.""" """Reload the platforms."""
# Fetch updated manual configured items and validate
config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {}
mqtt_data.updated_config = config_yaml.get(DOMAIN, {})
# Reload the modern yaml platforms # Reload the modern yaml platforms
mqtt_platforms = async_get_platforms(hass, DOMAIN) mqtt_platforms = async_get_platforms(hass, DOMAIN)
tasks = [ tasks = [
@ -493,8 +416,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
] ]
await asyncio.gather(*tasks) await asyncio.gather(*tasks)
config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {}
mqtt_data.updated_config = config_yaml.get(DOMAIN, {})
await asyncio.gather( await asyncio.gather(
*( *(
[ [

View file

@ -588,7 +588,7 @@ async def async_get_broker_settings(
current_user = user_input_basic.get(CONF_USERNAME) current_user = user_input_basic.get(CONF_USERNAME)
current_pass = user_input_basic.get(CONF_PASSWORD) current_pass = user_input_basic.get(CONF_PASSWORD)
else: else:
# Get default settings from entry or yaml (if any) # Get default settings from entry (if any)
current_broker = current_config.get(CONF_BROKER) current_broker = current_config.get(CONF_BROKER)
current_port = current_config.get(CONF_PORT, DEFAULT_PORT) current_port = current_config.get(CONF_PORT, DEFAULT_PORT)
current_user = current_config.get(CONF_USERNAME) current_user = current_config.get(CONF_USERNAME)

View file

@ -247,7 +247,7 @@ def warn_for_legacy_schema(domain: str) -> Callable[[ConfigType], ConfigType]:
( (
"Manually configured MQTT %s(s) found under platform key '%s', " "Manually configured MQTT %s(s) found under platform key '%s', "
"please move to the mqtt integration key, see " "please move to the mqtt integration key, see "
"https://www.home-assistant.io/integrations/%s.mqtt/#new_format" "https://www.home-assistant.io/integrations/%s.mqtt/"
), ),
domain, domain,
domain, domain,

View file

@ -36,7 +36,6 @@ from homeassistant.helpers import (
) )
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, async_fire_mqtt_message from tests.common import MockConfigEntry, async_fire_mqtt_message
from tests.typing import MqttMockHAClient, MqttMockHAClientGenerator, MqttMockPahoClient from tests.typing import MqttMockHAClient, MqttMockHAClientGenerator, MqttMockPahoClient
@ -109,14 +108,15 @@ async def help_setup_component(
item += 1 item += 1
topic = f"homeassistant/{domain}/item_{item}/config" topic = f"homeassistant/{domain}/item_{item}/config"
async_fire_mqtt_message(hass, topic, json.dumps(comp)) async_fire_mqtt_message(hass, topic, json.dumps(comp))
await hass.async_block_till_done()
else: else:
await async_setup_component( entry = MockConfigEntry(
hass, domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}
mqtt.DOMAIN,
config,
) )
entry.add_to_hass(hass)
with patch("homeassistant.config.load_yaml_config_file", return_value=config):
await entry.async_setup(hass)
mqtt_mock = None mqtt_mock = None
await hass.async_block_till_done()
return mqtt_mock return mqtt_mock
@ -226,7 +226,7 @@ async def help_test_default_availability_payload(
async def help_test_default_availability_list_payload( async def help_test_default_availability_list_payload(
hass: HomeAssistant, hass: HomeAssistant,
mqtt_mock_entry_with_no_config: MqttMockHAClientGenerator, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
domain: str, domain: str,
config: ConfigType, config: ConfigType,
no_assumed_state: bool = False, no_assumed_state: bool = False,
@ -243,7 +243,7 @@ async def help_test_default_availability_list_payload(
{"topic": "availability-topic1"}, {"topic": "availability-topic1"},
{"topic": "availability-topic2"}, {"topic": "availability-topic2"},
] ]
await help_setup_component(hass, mqtt_mock_entry_with_no_config, domain, config) await help_setup_component(hass, mqtt_mock_entry_no_yaml_config, domain, config)
state = hass.states.get(f"{domain}.test") state = hass.states.get(f"{domain}.test")
assert state and state.state == STATE_UNAVAILABLE assert state and state.state == STATE_UNAVAILABLE
@ -1169,7 +1169,7 @@ async def help_test_entity_id_update_subscriptions(
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
mqtt_mock = await help_setup_component( mqtt_mock = await help_setup_component(
hass, mqtt_mock_entry_no_yaml_config, domain, config, True hass, mqtt_mock_entry_no_yaml_config, domain, config, use_discovery=True
) )
assert mqtt_mock is not None assert mqtt_mock is not None
@ -1796,27 +1796,6 @@ async def help_test_reload_with_config(
await hass.async_block_till_done() await hass.async_block_till_done()
async def help_test_entry_reload_with_new_config(
hass: HomeAssistant, tmp_path: Path, new_config: ConfigType
) -> None:
"""Test reloading with supplied config."""
mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
assert mqtt_config_entry.state is ConfigEntryState.LOADED
new_yaml_config_file = tmp_path / "configuration.yaml"
new_yaml_config = yaml.dump(new_config)
new_yaml_config_file.write_text(new_yaml_config)
assert new_yaml_config_file.read_text() == new_yaml_config
with patch.object(
module_hass_config, "YAML_CONFIG_FILE", new_yaml_config_file
), patch("paho.mqtt.client.Client") as mock_client:
mock_client().connect = lambda *args: 0
# reload the config entry
assert await hass.config_entries.async_reload(mqtt_config_entry.entry_id)
assert mqtt_config_entry.state is ConfigEntryState.LOADED
await hass.async_block_till_done()
async def help_test_reloadable( async def help_test_reloadable(
hass: HomeAssistant, hass: HomeAssistant,
mqtt_client_mock: MqttMockPahoClient, mqtt_client_mock: MqttMockPahoClient,
@ -1839,10 +1818,8 @@ async def help_test_reloadable(
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"})
entry.add_to_hass(hass) entry.add_to_hass(hass)
mqtt_client_mock.connect.return_value = 0 mqtt_client_mock.connect.return_value = 0
# We should call await mqtt.async_setup_entry(hass, entry) when async_setup with patch("homeassistant.config.load_yaml_config_file", return_value=old_config):
# is removed (this is planned with #87987). Until then we set up the mqtt component await entry.async_setup(hass)
# to test reload after the async_setup setup has set the initial config
await help_setup_component(hass, None, domain, old_config, use_discovery=False)
assert hass.states.get(f"{domain}.test_old_1") assert hass.states.get(f"{domain}.test_old_1")
assert hass.states.get(f"{domain}.test_old_2") assert hass.states.get(f"{domain}.test_old_2")
@ -1860,15 +1837,15 @@ async def help_test_reloadable(
new_config = { new_config = {
mqtt.DOMAIN: {domain: [new_config_1, new_config_2, new_config_extra]}, mqtt.DOMAIN: {domain: [new_config_1, new_config_2, new_config_extra]},
} }
module_hass_config.load_yaml_config_file.return_value = new_config with patch("homeassistant.config.load_yaml_config_file", return_value=new_config):
# Reload the mqtt entry with the new config # Reload the mqtt entry with the new config
await hass.services.async_call( await hass.services.async_call(
"mqtt", "mqtt",
SERVICE_RELOAD, SERVICE_RELOAD,
{}, {},
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_all(domain)) == 3 assert len(hass.states.async_all(domain)) == 3
@ -1900,9 +1877,9 @@ async def help_test_unload_config_entry_with_platform(
config_setup: dict[str, dict[str, Any]] = copy.deepcopy(config) config_setup: dict[str, dict[str, Any]] = copy.deepcopy(config)
config_setup[mqtt.DOMAIN][domain]["name"] = "config_setup" config_setup[mqtt.DOMAIN][domain]["name"] = "config_setup"
config_name = config_setup config_name = config_setup
await help_setup_component(
hass, mqtt_mock_entry_no_yaml_config, domain, config_setup with patch("homeassistant.config.load_yaml_config_file", return_value=config_name):
) await mqtt_mock_entry_no_yaml_config()
# prepare setup through discovery # prepare setup through discovery
discovery_setup = copy.deepcopy(config[mqtt.DOMAIN][domain]) discovery_setup = copy.deepcopy(config[mqtt.DOMAIN][domain])

View file

@ -9,13 +9,11 @@ from uuid import uuid4
import pytest import pytest
import voluptuous as vol import voluptuous as vol
import yaml
from homeassistant import config as hass_config, config_entries, data_entry_flow from homeassistant import config_entries, data_entry_flow
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient
@ -267,39 +265,13 @@ async def test_user_connection_fails(
assert len(mock_finish_setup.mock_calls) == 0 assert len(mock_finish_setup.mock_calls) == 0
async def test_manual_config_starts_discovery_flow( @pytest.mark.parametrize("hass_config", [{"mqtt": {"sensor": {"state_topic": "test"}}}])
hass: HomeAssistant,
mock_try_connection: MqttMockPahoClient,
mock_finish_setup: MagicMock,
) -> None:
"""Test manual config initiates a discovery flow."""
# No flows in progress
assert hass.config_entries.flow.async_progress() == []
# MQTT config present in yaml config
assert await async_setup_component(hass, "mqtt", {"mqtt": {}})
await hass.async_block_till_done()
assert len(mock_finish_setup.mock_calls) == 0
# There should now be a discovery flow
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["context"]["source"] == "integration_discovery"
assert flows[0]["handler"] == "mqtt"
assert flows[0]["step_id"] == "broker"
async def test_manual_config_set( async def test_manual_config_set(
hass: HomeAssistant, hass: HomeAssistant,
mock_try_connection: MqttMockPahoClient, mock_try_connection: MqttMockPahoClient,
mock_finish_setup: MagicMock, mock_finish_setup: MagicMock,
) -> None: ) -> None:
"""Test manual config does not create an entry, and entry can be setup late.""" """Test manual config does not create an entry, and entry can be setup late."""
# MQTT config present in yaml config
assert await async_setup_component(hass, "mqtt", {"mqtt": {"broker": "bla"}})
await hass.async_block_till_done()
# do not try to reload
hass.data["mqtt"].reload_needed = False
assert len(mock_finish_setup.mock_calls) == 0 assert len(mock_finish_setup.mock_calls) == 0
mock_try_connection.return_value = True mock_try_connection.return_value = True
@ -1162,6 +1134,9 @@ async def test_options_bad_will_message_fails(
} }
@pytest.mark.parametrize(
"hass_config", [{"mqtt": {"sensor": [{"state_topic": "some-topic"}]}}]
)
async def test_try_connection_with_advanced_parameters( async def test_try_connection_with_advanced_parameters(
hass: HomeAssistant, hass: HomeAssistant,
mock_try_connection_success: MqttMockPahoClient, mock_try_connection_success: MqttMockPahoClient,
@ -1170,23 +1145,6 @@ async def test_try_connection_with_advanced_parameters(
mock_process_uploaded_file: MagicMock, mock_process_uploaded_file: MagicMock,
) -> None: ) -> None:
"""Test config flow with advanced parameters from config.""" """Test config flow with advanced parameters from config."""
with open(tmp_path / "client.crt", "wb") as certfile:
certfile.write(MOCK_CLIENT_CERT)
with open(tmp_path / "client.key", "wb") as keyfile:
keyfile.write(MOCK_CLIENT_KEY)
config = {
"certificate": "auto",
"tls_insecure": True,
"client_cert": str(tmp_path / "client.crt"),
"client_key": str(tmp_path / "client.key"),
}
new_yaml_config_file = tmp_path / "configuration.yaml"
new_yaml_config = yaml.dump({mqtt.DOMAIN: config})
new_yaml_config_file.write_text(new_yaml_config)
assert new_yaml_config_file.read_text() == new_yaml_config
config_entry = MockConfigEntry(domain=mqtt.DOMAIN) config_entry = MockConfigEntry(domain=mqtt.DOMAIN)
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
config_entry.data = { config_entry.data = {
@ -1195,6 +1153,10 @@ async def test_try_connection_with_advanced_parameters(
mqtt.CONF_USERNAME: "user", mqtt.CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "pass", mqtt.CONF_PASSWORD: "pass",
mqtt.CONF_TRANSPORT: "websockets", mqtt.CONF_TRANSPORT: "websockets",
mqtt.CONF_CERTIFICATE: "auto",
mqtt.CONF_TLS_INSECURE: True,
mqtt.CONF_CLIENT_CERT: MOCK_CLIENT_CERT.decode(encoding="utf-8)"),
mqtt.CONF_CLIENT_KEY: MOCK_CLIENT_KEY.decode(encoding="utf-8"),
mqtt.CONF_WS_PATH: "/path/", mqtt.CONF_WS_PATH: "/path/",
mqtt.CONF_WS_HEADERS: {"h1": "v1", "h2": "v2"}, mqtt.CONF_WS_HEADERS: {"h1": "v1", "h2": "v2"},
mqtt.CONF_KEEPALIVE: 30, mqtt.CONF_KEEPALIVE: 30,
@ -1212,95 +1174,81 @@ async def test_try_connection_with_advanced_parameters(
mqtt.ATTR_RETAIN: False, mqtt.ATTR_RETAIN: False,
}, },
} }
# Test default/suggested values from config
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "broker"
defaults = {
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234,
"set_client_cert": True,
"set_ca_cert": "auto",
}
suggested = {
mqtt.CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "pass",
mqtt.CONF_TLS_INSECURE: True,
mqtt.CONF_PROTOCOL: "3.1.1",
mqtt.CONF_TRANSPORT: "websockets",
mqtt.CONF_WS_PATH: "/path/",
mqtt.CONF_WS_HEADERS: '{"h1":"v1","h2":"v2"}',
}
for k, v in defaults.items():
assert get_default(result["data_schema"].schema, k) == v
for k, v in suggested.items():
assert get_suggested(result["data_schema"].schema, k) == v
with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): # test we can change username and password
await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) # as it was configured as auto in configuration.yaml is is migrated now
await hass.async_block_till_done() mock_try_connection_success.reset_mock()
# Test default/suggested values from config result = await hass.config_entries.options.async_configure(
result = await hass.config_entries.options.async_init(config_entry.entry_id) result["flow_id"],
assert result["type"] == data_entry_flow.FlowResultType.FORM user_input={
assert result["step_id"] == "broker" mqtt.CONF_BROKER: "another-broker",
defaults = { mqtt.CONF_PORT: 2345,
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_USERNAME: "us3r",
mqtt.CONF_PORT: 1234, mqtt.CONF_PASSWORD: "p4ss",
"set_client_cert": True,
"set_ca_cert": "auto", "set_ca_cert": "auto",
} "set_client_cert": True,
suggested = {
mqtt.CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "pass",
mqtt.CONF_TLS_INSECURE: True, mqtt.CONF_TLS_INSECURE: True,
mqtt.CONF_PROTOCOL: "3.1.1",
mqtt.CONF_TRANSPORT: "websockets", mqtt.CONF_TRANSPORT: "websockets",
mqtt.CONF_WS_PATH: "/path/", mqtt.CONF_WS_PATH: "/new/path",
mqtt.CONF_WS_HEADERS: '{"h1":"v1","h2":"v2"}', mqtt.CONF_WS_HEADERS: '{"h3": "v3"}',
} },
for k, v in defaults.items(): )
assert get_default(result["data_schema"].schema, k) == v assert result["type"] == data_entry_flow.FlowResultType.FORM
for k, v in suggested.items(): assert result["errors"] == {}
assert get_suggested(result["data_schema"].schema, k) == v assert result["step_id"] == "options"
await hass.async_block_till_done()
# test the client cert and key were migrated to the entry # check if the username and password was set from config flow and not from configuration.yaml
assert config_entry.data[mqtt.CONF_CLIENT_CERT] == MOCK_CLIENT_CERT.decode( assert mock_try_connection_success.username_pw_set.mock_calls[0][1] == (
"utf-8" "us3r",
) "p4ss",
assert config_entry.data[mqtt.CONF_CLIENT_KEY] == MOCK_CLIENT_KEY.decode( )
"utf-8" # check if tls_insecure_set is called
) assert mock_try_connection_success.tls_insecure_set.mock_calls[0][1] == (True,)
assert config_entry.data[mqtt.CONF_CERTIFICATE] == "auto"
# test we can change username and password # check if the ca certificate settings were not set during connection test
# as it was configured as auto in configuration.yaml is is migrated now assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[
mock_try_connection_success.reset_mock() "certfile"
result = await hass.config_entries.options.async_configure( ] == mqtt.util.get_file_path(mqtt.CONF_CLIENT_CERT)
result["flow_id"], assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[
user_input={ "keyfile"
mqtt.CONF_BROKER: "another-broker", ] == mqtt.util.get_file_path(mqtt.CONF_CLIENT_KEY)
mqtt.CONF_PORT: 2345,
mqtt.CONF_USERNAME: "us3r",
mqtt.CONF_PASSWORD: "p4ss",
"set_ca_cert": "auto",
"set_client_cert": True,
mqtt.CONF_TLS_INSECURE: True,
mqtt.CONF_TRANSPORT: "websockets",
mqtt.CONF_WS_PATH: "/new/path",
mqtt.CONF_WS_HEADERS: '{"h3": "v3"}',
},
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["errors"] == {}
assert result["step_id"] == "options"
await hass.async_block_till_done()
# check if the username and password was set from config flow and not from configuration.yaml # check if websockets options are set
assert mock_try_connection_success.username_pw_set.mock_calls[0][1] == ( assert mock_try_connection_success.ws_set_options.mock_calls[0][1] == (
"us3r", "/new/path",
"p4ss", {"h3": "v3"},
) )
# check if tls_insecure_set is called # Accept default option
assert mock_try_connection_success.tls_insecure_set.mock_calls[0][1] == (True,) result = await hass.config_entries.options.async_configure(
result["flow_id"],
# check if the ca certificate settings were not set during connection test user_input={},
assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[ )
"certfile" assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
] == mqtt.util.get_file_path(mqtt.CONF_CLIENT_CERT) await hass.async_block_till_done()
assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[
"keyfile"
] == mqtt.util.get_file_path(mqtt.CONF_CLIENT_KEY)
# check if websockets options are set
assert mock_try_connection_success.ws_set_options.mock_calls[0][1] == (
"/new/path",
{"h3": "v3"},
)
# Accept default option
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={},
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
await hass.async_block_till_done()
async def test_setup_with_advanced_settings( async def test_setup_with_advanced_settings(

View file

@ -25,8 +25,6 @@ default_config = {
"port": 1883, "port": 1883,
"protocol": "3.1.1", "protocol": "3.1.1",
"transport": "tcp", "transport": "tcp",
"ws_headers": {},
"ws_path": "/",
"will_message": { "will_message": {
"payload": "offline", "payload": "offline",
"qos": 0, "qos": 0,

View file

@ -5,18 +5,15 @@ import copy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import partial from functools import partial
import json import json
from pathlib import Path
import ssl import ssl
from typing import Any, TypedDict from typing import Any, TypedDict
from unittest.mock import ANY, MagicMock, call, mock_open, patch from unittest.mock import ANY, MagicMock, call, mock_open, patch
import pytest import pytest
import voluptuous as vol import voluptuous as vol
import yaml
from homeassistant import config as module_hass_config
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.components.mqtt import CONFIG_SCHEMA, debug_info from homeassistant.components.mqtt import debug_info
from homeassistant.components.mqtt.client import EnsureJobAfterCooldown from homeassistant.components.mqtt.client import EnsureJobAfterCooldown
from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA
from homeassistant.components.mqtt.models import MessageCallbackType, ReceiveMessage from homeassistant.components.mqtt.models import MessageCallbackType, ReceiveMessage
@ -40,10 +37,7 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .test_common import ( from .test_common import help_test_validate_platform_config
help_test_entry_reload_with_new_config,
help_test_validate_platform_config,
)
from tests.common import ( from tests.common import (
MockConfigEntry, MockConfigEntry,
@ -1529,18 +1523,17 @@ async def test_subscribed_at_highest_qos(
async def test_reload_entry_with_restored_subscriptions( async def test_reload_entry_with_restored_subscriptions(
hass: HomeAssistant, hass: HomeAssistant,
tmp_path: Path,
mqtt_client_mock: MqttMockPahoClient, mqtt_client_mock: MqttMockPahoClient,
record_calls: MessageCallbackType, record_calls: MessageCallbackType,
calls: list[ReceiveMessage], calls: list[ReceiveMessage],
) -> None: ) -> None:
"""Test reloading the config entry with with subscriptions restored.""" """Test reloading the config entry with with subscriptions restored."""
# Setup the MQTT entry
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"})
entry.add_to_hass(hass) entry.add_to_hass(hass)
mqtt_client_mock.connect.return_value = 0 mqtt_client_mock.connect.return_value = 0
assert await mqtt.async_setup_entry(hass, entry) with patch("homeassistant.config.load_yaml_config_file", return_value={}):
await hass.async_block_till_done() await entry.async_setup(hass)
await mqtt.async_subscribe(hass, "test-topic", record_calls) await mqtt.async_subscribe(hass, "test-topic", record_calls)
await mqtt.async_subscribe(hass, "wild/+/card", record_calls) await mqtt.async_subscribe(hass, "wild/+/card", record_calls)
@ -1557,10 +1550,10 @@ async def test_reload_entry_with_restored_subscriptions(
calls.clear() calls.clear()
# Reload the entry # Reload the entry
config_yaml_new = {} with patch("homeassistant.config.load_yaml_config_file", return_value={}):
await help_test_entry_reload_with_new_config(hass, tmp_path, config_yaml_new) assert await hass.config_entries.async_reload(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
await hass.async_block_till_done() await hass.async_block_till_done()
async_fire_mqtt_message(hass, "test-topic", "test-payload2") async_fire_mqtt_message(hass, "test-topic", "test-payload2")
async_fire_mqtt_message(hass, "wild/any/card", "wild-card-payload2") async_fire_mqtt_message(hass, "wild/any/card", "wild-card-payload2")
@ -1574,10 +1567,10 @@ async def test_reload_entry_with_restored_subscriptions(
calls.clear() calls.clear()
# Reload the entry again # Reload the entry again
config_yaml_new = {} with patch("homeassistant.config.load_yaml_config_file", return_value={}):
await help_test_entry_reload_with_new_config(hass, tmp_path, config_yaml_new) assert await hass.config_entries.async_reload(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
await hass.async_block_till_done() await hass.async_block_till_done()
async_fire_mqtt_message(hass, "test-topic", "test-payload3") async_fire_mqtt_message(hass, "test-topic", "test-payload3")
async_fire_mqtt_message(hass, "wild/any/card", "wild-card-payload3") async_fire_mqtt_message(hass, "wild/any/card", "wild-card-payload3")
@ -1804,55 +1797,6 @@ async def test_handle_message_callback(
assert callbacks[0].payload == "test-payload" assert callbacks[0].payload == "test-payload"
async def test_setup_override_configuration(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path
) -> None:
"""Test override setup from configuration entry."""
calls_username_password_set = []
def mock_usename_password_set(username: str, password: str) -> None:
calls_username_password_set.append((username, password))
# Mock password setup from config
config = {
"username": "someuser",
"password": "someyamlconfiguredpassword",
"protocol": "3.1",
}
new_yaml_config_file = tmp_path / "configuration.yaml"
new_yaml_config = yaml.dump({mqtt.DOMAIN: config})
new_yaml_config_file.write_text(new_yaml_config)
assert new_yaml_config_file.read_text() == new_yaml_config
with patch.object(module_hass_config, "YAML_CONFIG_FILE", new_yaml_config_file):
# Mock config entry
entry = MockConfigEntry(
domain=mqtt.DOMAIN,
data={mqtt.CONF_BROKER: "test-broker", "password": "somepassword"},
)
entry.add_to_hass(hass)
with patch("paho.mqtt.client.Client") as mock_client:
mock_client().username_pw_set = mock_usename_password_set
mock_client.on_connect(return_value=0)
await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
await entry.async_setup(hass)
await hass.async_block_till_done()
assert (
"Deprecated configuration settings found in configuration.yaml. "
"These settings from your configuration entry will override:"
in caplog.text
)
# Check if the protocol was set to 3.1 from configuration.yaml
assert mock_client.call_args[1]["protocol"] == 3
# Check if the password override worked
assert calls_username_password_set[0][0] == "someuser"
assert calls_username_password_set[0][1] == "somepassword"
@patch("homeassistant.components.mqtt.PLATFORMS", []) @patch("homeassistant.components.mqtt.PLATFORMS", [])
async def test_setup_manual_mqtt_with_platform_key( async def test_setup_manual_mqtt_with_platform_key(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture hass: HomeAssistant, caplog: pytest.LogCaptureFixture
@ -2312,39 +2256,10 @@ async def test_mqtt_subscribes_topics_on_connect(
mqtt_client_mock.subscribe.assert_any_call("still/pending", 1) mqtt_client_mock.subscribe.assert_any_call("still/pending", 1)
async def test_setup_entry_with_config_override(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mqtt_mock_entry_with_yaml_config: MqttMockHAClientGenerator,
) -> None:
"""Test if the MQTT component loads with no config and config entry can be setup."""
data = (
'{ "device":{"identifiers":["0AFFD2"]},'
' "state_topic": "foobar/sensor",'
' "unique_id": "unique" }'
)
# mqtt present in yaml config
assert await async_setup_component(hass, mqtt.DOMAIN, {})
await hass.async_block_till_done()
# User sets up a config entry
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"})
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
# Discover a device to verify the entry was setup correctly
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data)
await hass.async_block_till_done()
device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")})
assert device_entry is not None
async def test_update_incomplete_entry( async def test_update_incomplete_entry(
hass: HomeAssistant, hass: HomeAssistant,
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
mqtt_client_mock: MqttMockPahoClient, mqtt_client_mock: MqttMockPahoClient,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
@ -2356,24 +2271,17 @@ async def test_update_incomplete_entry(
) )
# Config entry data is incomplete # Config entry data is incomplete
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={"port": 1234}) entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
entry.add_to_hass(hass) entry.data = {"broker": "test-broker", "port": 1234}
# Mqtt present in yaml config await mqtt_mock_entry_no_yaml_config()
config = {"broker": "yaml_broker"}
await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
await hass.async_block_till_done() await hass.async_block_till_done()
# Config entry data should now be updated # Config entry data should now be updated
assert dict(entry.data) == { assert dict(entry.data) == {
"broker": "test-broker",
"port": 1234, "port": 1234,
"discovery_prefix": "homeassistant", "discovery_prefix": "homeassistant",
"broker": "yaml_broker",
} }
# Warnings about broker deprecated, but not about other keys with default values
assert (
"The 'broker' option is deprecated, please remove it from your configuration"
in caplog.text
)
# Discover a device to verify the entry was setup correctly # Discover a device to verify the entry was setup correctly
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data)
@ -3219,7 +3127,7 @@ async def test_subscribe_connection_status(
# This warning and test is to be removed from HA core 2023.6 # This warning and test is to be removed from HA core 2023.6
async def test_one_deprecation_warning_per_platform( async def test_one_deprecation_warning_per_platform(
hass: HomeAssistant, hass: HomeAssistant,
mqtt_mock_entry_with_yaml_config: MqttMockHAClientGenerator, mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test a deprecation warning is is logged once per platform.""" """Test a deprecation warning is is logged once per platform."""
@ -3230,8 +3138,6 @@ async def test_one_deprecation_warning_per_platform(
config2 = copy.deepcopy(config) config2 = copy.deepcopy(config)
config2["name"] = "test2" config2["name"] = "test2"
await async_setup_component(hass, platform, {platform: [config1, config2]}) await async_setup_component(hass, platform, {platform: [config1, config2]})
await hass.async_block_till_done()
await mqtt_mock_entry_with_yaml_config()
count = 0 count = 0
for record in caplog.records: for record in caplog.records:
if record.levelname == "ERROR" and ( if record.levelname == "ERROR" and (
@ -3242,13 +3148,6 @@ async def test_one_deprecation_warning_per_platform(
assert count == 1 assert count == 1
async def test_config_schema_validation(hass: HomeAssistant) -> None:
"""Test invalid platform options in the config schema do not pass the config validation."""
config = {"mqtt": {"sensor": [{"some_illegal_topic": "mystate/topic/path"}]}}
with pytest.raises(vol.MultipleInvalid):
CONFIG_SCHEMA(config)
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT])
async def test_unload_config_entry( async def test_unload_config_entry(
hass: HomeAssistant, hass: HomeAssistant,
@ -3277,24 +3176,6 @@ async def test_unload_config_entry(
assert "No ACK from MQTT server" not in caplog.text assert "No ACK from MQTT server" not in caplog.text
@patch("homeassistant.components.mqtt.PLATFORMS", [])
async def test_setup_with_disabled_entry(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test setting up the platform with a disabled config entry."""
# Try to setup the platform with a disabled config entry
config_entry = MockConfigEntry(
domain=mqtt.DOMAIN, data={}, disabled_by=ConfigEntryDisabler.USER
)
config_entry.add_to_hass(hass)
config: ConfigType = {mqtt.DOMAIN: {}}
await async_setup_component(hass, mqtt.DOMAIN, config)
await hass.async_block_till_done()
assert "MQTT will be not available until the config entry is enabled" in caplog.text
@patch("homeassistant.components.mqtt.PLATFORMS", []) @patch("homeassistant.components.mqtt.PLATFORMS", [])
async def test_publish_or_subscribe_without_valid_config_entry( async def test_publish_or_subscribe_without_valid_config_entry(
hass: HomeAssistant, record_calls: MessageCallbackType hass: HomeAssistant, record_calls: MessageCallbackType