Move advanced MQTT options to entry (#79351)

* Move advanced broker settings to entry

* Add repair issue for deprecated settings

* Split CONFIG_SCHEMA

* Do not store certificate UI flags in entry

* Keep entered password in next dialog

* Do not process yaml config in flow

* Correct typo
This commit is contained in:
Jan Bouwhuis 2022-10-24 09:58:23 +02:00 committed by GitHub
parent a8bf8d449b
commit 5e7f571f01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1159 additions and 116 deletions

View File

@ -14,10 +14,12 @@ from homeassistant import config as conf_util, config_entries
from homeassistant.components import websocket_api
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_CLIENT_ID,
CONF_DISCOVERY,
CONF_PASSWORD,
CONF_PAYLOAD,
CONF_PORT,
CONF_PROTOCOL,
CONF_USERNAME,
SERVICE_RELOAD,
)
@ -32,6 +34,7 @@ from homeassistant.helpers import (
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import async_get_platforms
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.reload import (
async_integration_yaml_config,
async_reload_integration_platforms,
@ -50,7 +53,9 @@ from .client import ( # noqa: F401
)
from .config_integration import (
CONFIG_SCHEMA_BASE,
CONFIG_SCHEMA_ENTRY,
DEFAULT_VALUES,
DEPRECATED_CERTIFICATE_CONFIG_KEYS,
DEPRECATED_CONFIG_KEYS,
)
from .const import ( # noqa: F401
@ -60,10 +65,15 @@ from .const import ( # noqa: F401
ATTR_TOPIC,
CONF_BIRTH_MESSAGE,
CONF_BROKER,
CONF_CERTIFICATE,
CONF_CLIENT_CERT,
CONF_CLIENT_KEY,
CONF_COMMAND_TOPIC,
CONF_DISCOVERY_PREFIX,
CONF_KEEPALIVE,
CONF_QOS,
CONF_STATE_TOPIC,
CONF_TLS_INSECURE,
CONF_TLS_VERSION,
CONF_TOPIC,
CONF_WILL_MESSAGE,
@ -86,7 +96,9 @@ from .models import ( # noqa: F401
)
from .util import (
_VALID_QOS_SCHEMA,
async_create_certificate_temp_files,
get_mqtt_data,
migrate_certificate_file_to_content,
mqtt_config_entry_enabled,
valid_publish_topic,
valid_subscribe_topic,
@ -97,7 +109,7 @@ _LOGGER = logging.getLogger(__name__)
SERVICE_PUBLISH = "publish"
SERVICE_DUMP = "dump"
MANDATORY_DEFAULT_VALUES = (CONF_PORT,)
MANDATORY_DEFAULT_VALUES = (CONF_PORT, CONF_DISCOVERY_PREFIX)
ATTR_TOPIC_TEMPLATE = "topic_template"
ATTR_PAYLOAD_TEMPLATE = "payload_template"
@ -111,9 +123,17 @@ CONNECTION_FAILED_RECOVERABLE = "connection_failed_recoverable"
CONFIG_ENTRY_CONFIG_KEYS = [
CONF_BIRTH_MESSAGE,
CONF_BROKER,
CONF_CERTIFICATE,
CONF_CLIENT_ID,
CONF_CLIENT_CERT,
CONF_CLIENT_KEY,
CONF_DISCOVERY,
CONF_DISCOVERY_PREFIX,
CONF_KEEPALIVE,
CONF_PASSWORD,
CONF_PORT,
CONF_PROTOCOL,
CONF_TLS_INSECURE,
CONF_USERNAME,
CONF_WILL_MESSAGE,
]
@ -123,9 +143,17 @@ CONFIG_SCHEMA = vol.Schema(
DOMAIN: vol.All(
cv.deprecated(CONF_BIRTH_MESSAGE), # Deprecated in HA Core 2022.3
cv.deprecated(CONF_BROKER), # Deprecated in HA Core 2022.3
cv.deprecated(CONF_CERTIFICATE), # Deprecated in HA Core 2022.11
cv.deprecated(CONF_CLIENT_ID), # Deprecated in HA Core 2022.11
cv.deprecated(CONF_CLIENT_CERT), # Deprecated in HA Core 2022.11
cv.deprecated(CONF_CLIENT_KEY), # Deprecated in HA Core 2022.11
cv.deprecated(CONF_DISCOVERY), # Deprecated in HA Core 2022.3
cv.deprecated(CONF_DISCOVERY_PREFIX), # Deprecated in HA Core 2022.11
cv.deprecated(CONF_KEEPALIVE), # Deprecated in HA Core 2022.11
cv.deprecated(CONF_PASSWORD), # Deprecated in HA Core 2022.3
cv.deprecated(CONF_PORT), # Deprecated in HA Core 2022.3
cv.deprecated(CONF_PROTOCOL), # Deprecated in HA Core 2022.11
cv.deprecated(CONF_TLS_INSECURE), # Deprecated in HA Core 2022.11
cv.deprecated(CONF_TLS_VERSION), # Deprecated June 2020
cv.deprecated(CONF_USERNAME), # Deprecated in HA Core 2022.3
cv.deprecated(CONF_WILL_MESSAGE), # Deprecated in HA Core 2022.3
@ -207,22 +235,31 @@ def _filter_entry_config(hass: HomeAssistant, entry: ConfigEntry) -> None:
if entry.data.keys() != filtered_data.keys():
_LOGGER.warning(
"The following unsupported configuration options were removed from the "
"MQTT config entry: %s. Add them to configuration.yaml if they are needed",
"MQTT config entry: %s",
entry.data.keys() - filtered_data.keys(),
)
hass.config_entries.async_update_entry(entry, data=filtered_data)
def _merge_basic_config(
async def _async_merge_basic_config(
hass: HomeAssistant, entry: ConfigEntry, yaml_config: dict[str, Any]
) -> None:
"""Merge basic options in configuration.yaml config with config entry.
This mends incomplete migration from old version of HA Core.
"""
entry_updated = False
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]
@ -265,7 +302,7 @@ async def async_fetch_config(
_filter_entry_config(hass, entry)
# Merge basic configuration, and add missing defaults for basic options
_merge_basic_config(hass, entry, mqtt_data.config or {})
await _async_merge_basic_config(hass, entry, mqtt_data.config or {})
# Bail out if broker setting is missing
if CONF_BROKER not in entry.data:
_LOGGER.error("MQTT broker is not configured, please configure it")
@ -274,7 +311,7 @@ async def async_fetch_config(
# If user doesn't have configuration.yaml config, generate default values
# for options not in config entry data
if (conf := mqtt_data.config) is None:
conf = CONFIG_SCHEMA_BASE(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):
@ -282,12 +319,28 @@ async def async_fetch_config(
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
conf = _merge_extended_config(entry, conf)
@ -302,6 +355,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if (conf := await async_fetch_config(hass, entry)) is None:
# Bail out
return False
await async_create_certificate_temp_files(hass, dict(entry.data))
mqtt_data.client = MQTT(hass, entry, conf)
# Restore saved subscriptions
if mqtt_data.subscriptions_to_restore:

View File

@ -68,7 +68,7 @@ from .models import (
ReceiveMessage,
ReceivePayloadType,
)
from .util import get_mqtt_data, mqtt_config_entry_enabled
from .util import get_file_path, get_mqtt_data, mqtt_config_entry_enabled
if TYPE_CHECKING:
# Only import for paho-mqtt type checking here, imports are done locally
@ -292,11 +292,13 @@ class MqttClientSetup:
if username is not None:
self._client.username_pw_set(username, password)
if (certificate := config.get(CONF_CERTIFICATE)) == "auto":
if (
certificate := get_file_path(CONF_CERTIFICATE, config.get(CONF_CERTIFICATE))
) == "auto":
certificate = certifi.where()
client_key = config.get(CONF_CLIENT_KEY)
client_cert = config.get(CONF_CLIENT_CERT)
client_key = get_file_path(CONF_CLIENT_KEY, config.get(CONF_CLIENT_KEY))
client_cert = get_file_path(CONF_CLIENT_CERT, config.get(CONF_CLIENT_CERT))
tls_insecure = config.get(CONF_TLS_INSECURE)
if certificate is not None:
self._client.tls_set(

View File

@ -4,35 +4,47 @@ from __future__ import annotations
from collections import OrderedDict
from collections.abc import Callable
import queue
from ssl import PROTOCOL_TLS, SSLContext, SSLError
from types import MappingProxyType
from typing import Any
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509 import load_pem_x509_certificate
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.file_upload import process_uploaded_file
from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.const import (
CONF_CLIENT_ID,
CONF_DISCOVERY,
CONF_HOST,
CONF_PASSWORD,
CONF_PAYLOAD,
CONF_PORT,
CONF_PROTOCOL,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.selector import (
BooleanSelector,
FileSelector,
FileSelectorConfig,
NumberSelector,
NumberSelectorConfig,
NumberSelectorMode,
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
TextSelector,
TextSelectorConfig,
TextSelectorType,
)
from homeassistant.helpers.typing import ConfigType
from .client import MqttClientSetup
from .config_integration import CONFIG_SCHEMA_ENTRY
from .const import (
ATTR_PAYLOAD,
ATTR_QOS,
@ -40,17 +52,37 @@ from .const import (
ATTR_TOPIC,
CONF_BIRTH_MESSAGE,
CONF_BROKER,
CONF_CERTIFICATE,
CONF_CLIENT_CERT,
CONF_CLIENT_KEY,
CONF_DISCOVERY_PREFIX,
CONF_KEEPALIVE,
CONF_TLS_INSECURE,
CONF_WILL_MESSAGE,
DEFAULT_BIRTH,
DEFAULT_DISCOVERY,
DEFAULT_ENCODING,
DEFAULT_KEEPALIVE,
DEFAULT_PORT,
DEFAULT_PREFIX,
DEFAULT_PROTOCOL,
DEFAULT_WILL,
DOMAIN,
SUPPORTED_PROTOCOLS,
)
from .util import (
MQTT_WILL_BIRTH_SCHEMA,
async_create_certificate_temp_files,
get_file_path,
valid_publish_topic,
)
from .util import MQTT_WILL_BIRTH_SCHEMA, get_mqtt_data
MQTT_TIMEOUT = 5
ADVANCED_OPTIONS = "advanced_options"
SET_CA_CERT = "set_ca_cert"
SET_CLIENT_CERT = "set_client_cert"
BOOLEAN_SELECTOR = BooleanSelector()
TEXT_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT))
PUBLISH_TOPIC_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT))
@ -63,6 +95,40 @@ QOS_SELECTOR = vol.All(
NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0, max=2)),
vol.Coerce(int),
)
KEEPALIVE_SELECTOR = vol.All(
NumberSelector(
NumberSelectorConfig(
mode=NumberSelectorMode.BOX, min=15, step="any", unit_of_measurement="sec"
)
),
vol.Coerce(int),
)
PROTOCOL_SELECTOR = SelectSelector(
SelectSelectorConfig(
options=SUPPORTED_PROTOCOLS,
mode=SelectSelectorMode.DROPDOWN,
)
)
CA_VERIFICATION_MODES = [
SelectOptionDict(value="off", label="Off"),
SelectOptionDict(value="auto", label="Auto"),
SelectOptionDict(value="custom", label="Custom"),
]
BROKER_VERIFICATION_SELECTOR = SelectSelector(
SelectSelectorConfig(
options=CA_VERIFICATION_MODES,
mode=SelectSelectorMode.DROPDOWN,
)
)
# mime configuration from https://pki-tutorial.readthedocs.io/en/latest/mime.html
CA_CERT_UPLOAD_SELECTOR = FileSelector(
FileSelectorConfig(accept=".crt,application/x-x509-ca-cert")
)
CERT_UPLOAD_SELECTOR = FileSelector(
FileSelectorConfig(accept=".crt,application/x-x509-user-cert")
)
KEY_UPLOAD_SELECTOR = FileSelector(FileSelectorConfig(accept=".key,application/pkcs8"))
class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@ -93,24 +159,20 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm the setup."""
yaml_config: ConfigType = get_mqtt_data(self.hass, True).config or {}
errors: dict[str, str] = {}
fields: OrderedDict[Any, Any] = OrderedDict()
validated_user_input: dict[str, Any] = {}
if await async_get_broker_settings(
self.hass,
fields,
yaml_config,
None,
user_input,
validated_user_input,
errors,
):
test_config: dict[str, Any] = yaml_config.copy()
test_config.update(validated_user_input)
can_connect = await self.hass.async_add_executor_job(
try_connection,
test_config,
validated_user_input,
)
if can_connect:
@ -177,7 +239,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
"""Initialize MQTT options flow."""
self.config_entry = config_entry
self.broker_config: dict[str, str | int] = {}
self.options = dict(config_entry.options)
self.options = config_entry.options
async def async_step_init(self, user_input: None = None) -> FlowResult:
"""Manage the MQTT options."""
@ -188,23 +250,19 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
) -> FlowResult:
"""Manage the MQTT broker configuration."""
errors: dict[str, str] = {}
yaml_config: ConfigType = get_mqtt_data(self.hass, True).config or {}
fields: OrderedDict[Any, Any] = OrderedDict()
validated_user_input: dict[str, Any] = {}
if await async_get_broker_settings(
self.hass,
fields,
yaml_config,
self.config_entry.data,
user_input,
validated_user_input,
errors,
):
test_config: dict[str, Any] = yaml_config.copy()
test_config.update(validated_user_input)
can_connect = await self.hass.async_add_executor_job(
try_connection,
test_config,
validated_user_input,
)
if can_connect:
@ -226,7 +284,6 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
"""Manage the MQTT options."""
errors = {}
current_config = self.config_entry.data
yaml_config = get_mqtt_data(self.hass, True).config or {}
options_config: dict[str, Any] = {}
bad_input: bool = False
@ -255,6 +312,12 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
if user_input is not None:
# validate input
options_config[CONF_DISCOVERY] = user_input[CONF_DISCOVERY]
_validate(
CONF_DISCOVERY_PREFIX,
user_input[CONF_DISCOVERY_PREFIX],
"bad_discovery_prefix",
valid_publish_topic,
)
if "birth_topic" in user_input:
_validate(
CONF_BIRTH_MESSAGE,
@ -279,6 +342,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
updated_config = {}
updated_config.update(self.broker_config)
updated_config.update(options_config)
CONFIG_SCHEMA_ENTRY(updated_config)
self.hass.config_entries.async_update_entry(
self.config_entry,
data=updated_config,
@ -288,23 +352,21 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
birth = {
**DEFAULT_BIRTH,
**current_config.get(
CONF_BIRTH_MESSAGE, yaml_config.get(CONF_BIRTH_MESSAGE, {})
),
**current_config.get(CONF_BIRTH_MESSAGE, {}),
}
will = {
**DEFAULT_WILL,
**current_config.get(
CONF_WILL_MESSAGE, yaml_config.get(CONF_WILL_MESSAGE, {})
),
**current_config.get(CONF_WILL_MESSAGE, {}),
}
discovery = current_config.get(
CONF_DISCOVERY, yaml_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY)
)
discovery = current_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY)
discovery_prefix = current_config.get(CONF_DISCOVERY_PREFIX, DEFAULT_PREFIX)
# build form
fields: OrderedDict[vol.Marker, Any] = OrderedDict()
fields[vol.Optional(CONF_DISCOVERY, default=discovery)] = BOOLEAN_SELECTOR
fields[
vol.Optional(CONF_DISCOVERY_PREFIX, default=discovery_prefix)
] = PUBLISH_TOPIC_SELECTOR
# Birth message is disabled if CONF_BIRTH_MESSAGE = {}
fields[
@ -363,7 +425,6 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
async def async_get_broker_settings(
hass: HomeAssistant,
fields: OrderedDict[Any, Any],
yaml_config: ConfigType,
entry_config: MappingProxyType[str, Any] | None,
user_input: dict[str, Any] | None,
validated_user_input: dict[str, Any],
@ -371,25 +432,144 @@ async def async_get_broker_settings(
) -> bool:
"""Build the config flow schema to collect the broker settings.
Shows advanced options if one or more are configured
or when the advanced_broker_options checkbox was selected.
Returns True when settings are collected successfully.
"""
advanced_broker_options: bool = False
user_input_basic: dict[str, Any] = {}
current_config = entry_config.copy() if entry_config is not None else {}
current_config: dict[str, Any] = (
entry_config.copy() if entry_config is not None else {}
)
if user_input is not None:
async def _async_validate_broker_settings(
config: dict[str, Any],
user_input: dict[str, Any],
validated_user_input: dict[str, Any],
errors: dict[str, str],
) -> bool:
"""Additional validation on broker settings for better error messages."""
# Get current certificate settings from config entry
certificate: str | None = (
"auto"
if user_input.get(SET_CA_CERT, "off") == "auto"
else config.get(CONF_CERTIFICATE)
if user_input.get(SET_CA_CERT, "off") == "custom"
else None
)
client_certificate: str | None = (
config.get(CONF_CLIENT_CERT) if user_input.get(SET_CLIENT_CERT) else None
)
client_key: str | None = (
config.get(CONF_CLIENT_KEY) if user_input.get(SET_CLIENT_CERT) else None
)
# Prepare entry update with uploaded files
validated_user_input.update(user_input)
client_certificate_id: str | None = user_input.get(CONF_CLIENT_CERT)
client_key_id: str | None = user_input.get(CONF_CLIENT_KEY)
if (
client_certificate_id
and not client_key_id
or not client_certificate_id
and client_key_id
):
errors["base"] = "invalid_inclusion"
return False
certificate_id: str | None = user_input.get(CONF_CERTIFICATE)
if certificate_id:
with process_uploaded_file(hass, certificate_id) as certiticate_file:
certificate = certiticate_file.read_text(encoding=DEFAULT_ENCODING)
# Return to form for file upload CA cert or client cert and key
if (
not client_certificate
and user_input.get(SET_CLIENT_CERT)
and not client_certificate_id
or not certificate
and user_input.get(SET_CA_CERT, "off") == "custom"
and not certificate_id
):
return False
if client_certificate_id:
with process_uploaded_file(
hass, client_certificate_id
) as client_certiticate_file:
client_certificate = client_certiticate_file.read_text(
encoding=DEFAULT_ENCODING
)
if client_key_id:
with process_uploaded_file(hass, client_key_id) as key_file:
client_key = key_file.read_text(encoding=DEFAULT_ENCODING)
certificate_data: dict[str, Any] = {}
if certificate:
certificate_data[CONF_CERTIFICATE] = certificate
if client_certificate:
certificate_data[CONF_CLIENT_CERT] = client_certificate
certificate_data[CONF_CLIENT_KEY] = client_key
validated_user_input.update(certificate_data)
await async_create_certificate_temp_files(hass, certificate_data)
if error := await hass.async_add_executor_job(
check_certicate_chain,
):
errors["base"] = error
return False
if SET_CA_CERT in validated_user_input:
del validated_user_input[SET_CA_CERT]
if SET_CLIENT_CERT in validated_user_input:
del validated_user_input[SET_CLIENT_CERT]
return True
# Update the current settings the the new posted data to fill the defaults
if user_input:
user_input_basic = user_input.copy()
advanced_broker_options = user_input_basic.get(ADVANCED_OPTIONS, False)
if ADVANCED_OPTIONS not in user_input or advanced_broker_options is False:
if await _async_validate_broker_settings(
current_config,
user_input_basic,
validated_user_input,
errors,
):
return True
# Get defaults settings from previous post
current_broker = user_input_basic.get(CONF_BROKER)
current_port = user_input_basic.get(CONF_PORT, DEFAULT_PORT)
current_user = user_input_basic.get(CONF_USERNAME)
current_pass = user_input_basic.get(CONF_PASSWORD)
else:
# Get default settings from entry or yaml (if any)
current_broker = current_config.get(CONF_BROKER)
current_port = current_config.get(CONF_PORT, DEFAULT_PORT)
current_user = current_config.get(CONF_USERNAME)
current_pass = current_config.get(CONF_PASSWORD)
# Treat the previous post as an update of the current settings (if there was a basic broker setup step)
current_config.update(user_input_basic)
# Get default settings (if any)
current_broker = current_config.get(CONF_BROKER, yaml_config.get(CONF_BROKER))
current_port = current_config.get(
CONF_PORT, yaml_config.get(CONF_PORT, DEFAULT_PORT)
# Get default settings for advanced broker options
current_client_id = current_config.get(CONF_CLIENT_ID)
current_keepalive = current_config.get(CONF_KEEPALIVE, DEFAULT_KEEPALIVE)
current_ca_certificate = current_config.get(CONF_CERTIFICATE)
current_client_certificate = current_config.get(CONF_CLIENT_CERT)
current_client_key = current_config.get(CONF_CLIENT_KEY)
current_tls_insecure = current_config.get(CONF_TLS_INSECURE, False)
current_protocol = current_config.get(CONF_PROTOCOL, DEFAULT_PROTOCOL)
advanced_broker_options |= bool(
current_client_id
or current_keepalive != DEFAULT_KEEPALIVE
or current_ca_certificate
or current_client_certificate
or current_client_key
or current_tls_insecure
or current_protocol != DEFAULT_PROTOCOL
or current_config.get(SET_CA_CERT, "off") != "off"
or current_config.get(SET_CLIENT_CERT)
)
current_user = current_config.get(CONF_USERNAME, yaml_config.get(CONF_USERNAME))
current_pass = current_config.get(CONF_PASSWORD, yaml_config.get(CONF_PASSWORD))
# Build form
fields[vol.Required(CONF_BROKER, default=current_broker)] = TEXT_SELECTOR
@ -406,6 +586,82 @@ async def async_get_broker_settings(
description={"suggested_value": current_pass},
)
] = PASSWORD_SELECTOR
# show advanced options checkbox if requested
# or when the defaults of advanced options are overridden
if not advanced_broker_options:
fields[
vol.Optional(
ADVANCED_OPTIONS,
)
] = BOOLEAN_SELECTOR
return False
fields[
vol.Optional(
CONF_CLIENT_ID,
description={"suggested_value": current_client_id},
)
] = TEXT_SELECTOR
fields[
vol.Optional(
CONF_KEEPALIVE,
description={"suggested_value": current_keepalive},
)
] = KEEPALIVE_SELECTOR
fields[
vol.Optional(
SET_CLIENT_CERT,
default=current_client_certificate is not None
or current_config.get(SET_CLIENT_CERT) is True,
)
] = BOOLEAN_SELECTOR
if (
current_client_certificate is not None
or current_config.get(SET_CLIENT_CERT) is True
):
fields[
vol.Optional(
CONF_CLIENT_CERT,
description={"suggested_value": user_input_basic.get(CONF_CLIENT_CERT)},
)
] = CERT_UPLOAD_SELECTOR
fields[
vol.Optional(
CONF_CLIENT_KEY,
description={"suggested_value": user_input_basic.get(CONF_CLIENT_KEY)},
)
] = KEY_UPLOAD_SELECTOR
verification_mode = current_config.get(SET_CA_CERT) or (
"off"
if current_ca_certificate is None
else "auto"
if current_ca_certificate == "auto"
else "custom"
)
fields[
vol.Optional(
SET_CA_CERT,
default=verification_mode,
)
] = BROKER_VERIFICATION_SELECTOR
if current_ca_certificate is not None or verification_mode == "custom":
fields[
vol.Optional(
CONF_CERTIFICATE,
user_input_basic.get(CONF_CERTIFICATE),
)
] = CA_CERT_UPLOAD_SELECTOR
fields[
vol.Optional(
CONF_TLS_INSECURE,
description={"suggested_value": current_tls_insecure},
)
] = BOOLEAN_SELECTOR
fields[
vol.Optional(
CONF_PROTOCOL,
description={"suggested_value": current_protocol},
)
] = PROTOCOL_SELECTOR
# Show form
return False
@ -439,3 +695,36 @@ def try_connection(
finally:
client.disconnect()
client.loop_stop()
def check_certicate_chain() -> str | None:
"""Check the MQTT certificates."""
if client_certiticate := get_file_path(CONF_CLIENT_CERT):
try:
with open(client_certiticate, "rb") as client_certiticate_file:
load_pem_x509_certificate(client_certiticate_file.read())
except ValueError:
return "bad_client_cert"
# Check we can serialize the private key file
if private_key := get_file_path(CONF_CLIENT_KEY):
try:
with open(private_key, "rb") as client_key_file:
load_pem_private_key(client_key_file.read(), password=None)
except (TypeError, ValueError):
return "bad_client_key"
# Check the certificate chain
context = SSLContext(PROTOCOL_TLS)
if client_certiticate and private_key:
try:
context.load_cert_chain(client_certiticate, private_key)
except SSLError:
return "bad_client_cert_key"
# try to load the custom CA file
if (ca_cert := get_file_path(CONF_CERTIFICATE)) is None:
return None
try:
context.load_verify_locations(ca_cert)
except SSLError:
return "bad_certificate"
return None

View File

@ -51,26 +51,28 @@ from .const import (
CONF_WILL_MESSAGE,
DEFAULT_BIRTH,
DEFAULT_DISCOVERY,
DEFAULT_KEEPALIVE,
DEFAULT_PORT,
DEFAULT_PREFIX,
DEFAULT_PROTOCOL,
DEFAULT_QOS,
DEFAULT_RETAIN,
DEFAULT_WILL,
PROTOCOL_31,
PROTOCOL_311,
SUPPORTED_PROTOCOLS,
)
from .util import _VALID_QOS_SCHEMA, valid_publish_topic
DEFAULT_PORT = 1883
DEFAULT_KEEPALIVE = 60
DEFAULT_PROTOCOL = PROTOCOL_311
DEFAULT_TLS_PROTOCOL = "auto"
DEFAULT_VALUES = {
CONF_BIRTH_MESSAGE: DEFAULT_BIRTH,
CONF_DISCOVERY: DEFAULT_DISCOVERY,
CONF_DISCOVERY_PREFIX: DEFAULT_PREFIX,
CONF_PORT: DEFAULT_PORT,
CONF_PROTOCOL: DEFAULT_PROTOCOL,
CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL,
CONF_WILL_MESSAGE: DEFAULT_WILL,
CONF_KEEPALIVE: DEFAULT_KEEPALIVE,
}
PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema(
@ -148,12 +150,35 @@ MQTT_WILL_BIRTH_SCHEMA = vol.Schema(
required=True,
)
CONFIG_SCHEMA_ENTRY = vol.Schema(
{
vol.Optional(CONF_CLIENT_ID): cv.string,
vol.Optional(CONF_KEEPALIVE): vol.All(vol.Coerce(int), vol.Range(min=15)),
vol.Optional(CONF_BROKER): cv.string,
vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_CERTIFICATE): str,
vol.Inclusive(CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG): str,
vol.Inclusive(
CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG
): str,
vol.Optional(CONF_TLS_INSECURE): cv.boolean,
vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"),
vol.Optional(CONF_PROTOCOL): vol.All(cv.string, vol.In(SUPPORTED_PROTOCOLS)),
vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA,
vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA,
vol.Optional(CONF_DISCOVERY): cv.boolean,
# discovery_prefix must be a valid publish topic because if no
# state topic is specified, it will be created with the given prefix.
vol.Optional(CONF_DISCOVERY_PREFIX): valid_publish_topic,
}
)
CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend(
{
vol.Optional(CONF_CLIENT_ID): cv.string,
vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All(
vol.Coerce(int), vol.Range(min=15)
),
vol.Optional(CONF_KEEPALIVE): vol.All(vol.Coerce(int), vol.Range(min=15)),
vol.Optional(CONF_BROKER): cv.string,
vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_USERNAME): cv.string,
@ -167,27 +192,34 @@ CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend(
): cv.isfile,
vol.Optional(CONF_TLS_INSECURE): cv.boolean,
vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"),
vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All(
cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])
),
vol.Optional(CONF_PROTOCOL): vol.All(cv.string, vol.In(SUPPORTED_PROTOCOLS)),
vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA,
vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA,
vol.Optional(CONF_DISCOVERY): cv.boolean,
# discovery_prefix must be a valid publish topic because if no
# state topic is specified, it will be created with the given prefix.
vol.Optional(
CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX
): valid_publish_topic,
vol.Optional(CONF_DISCOVERY_PREFIX): valid_publish_topic,
}
)
DEPRECATED_CONFIG_KEYS = [
CONF_BIRTH_MESSAGE,
CONF_BROKER,
CONF_CLIENT_ID,
CONF_DISCOVERY,
CONF_DISCOVERY_PREFIX,
CONF_KEEPALIVE,
CONF_PASSWORD,
CONF_PORT,
CONF_PROTOCOL,
CONF_TLS_INSECURE,
CONF_TLS_VERSION,
CONF_USERNAME,
CONF_WILL_MESSAGE,
]
DEPRECATED_CERTIFICATE_CONFIG_KEYS = [
CONF_CERTIFICATE,
CONF_CLIENT_CERT,
CONF_CLIENT_KEY,
]

View File

@ -43,6 +43,14 @@ DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline"
DEFAULT_PORT = 1883
DEFAULT_RETAIN = False
PROTOCOL_31 = "3.1"
PROTOCOL_311 = "3.1.1"
SUPPORTED_PROTOCOLS = [PROTOCOL_31, PROTOCOL_311]
DEFAULT_PORT = 1883
DEFAULT_KEEPALIVE = 60
DEFAULT_PROTOCOL = PROTOCOL_311
DEFAULT_BIRTH = {
ATTR_TOPIC: DEFAULT_BIRTH_WILL_TOPIC,
CONF_PAYLOAD: DEFAULT_PAYLOAD_AVAILABLE,
@ -65,11 +73,6 @@ MQTT_DISCONNECTED = "mqtt_disconnected"
PAYLOAD_EMPTY_JSON = "{}"
PAYLOAD_NONE = "None"
PROTOCOL_31 = "3.1"
PROTOCOL_311 = "3.1.1"
DEFAULT_PROTOCOL = PROTOCOL_311
PLATFORMS = [
Platform.ALARM_CONTROL_PANEL,
Platform.BINARY_SENSOR,

View File

@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/mqtt",
"requirements": ["paho-mqtt==1.6.1"],
"dependencies": ["http"],
"dependencies": ["file_upload", "http"],
"codeowners": ["@emontnemery"],
"iot_class": "local_push"
}

View File

@ -3,6 +3,10 @@
"deprecated_yaml": {
"title": "Your manually configured MQTT {platform}(s) needs attention",
"description": "Manually configured MQTT {platform}(s) found under platform key `{platform}`.\n\nPlease move the configuration to the `mqtt` integration key and restart Home Assistant to fix this issue. See the [documentation]({more_info_url}), for more information."
},
"deprecated_yaml_broker_settings": {
"title": "Deprecated MQTT settings found in `configuration.yaml`",
"description": "The following settings found in `configuration.yaml` were migrated to MQTT config entry and will now override the settings in `configuration.yaml`:\n`{deprecated_settings}`\n\nPlease remove these settings from `configuration.yaml` and restart Home Assistant to fix this issue. See the [documentation]({more_info_url}), for more information."
}
},
"config": {
@ -14,7 +18,16 @@
"port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"discovery": "Enable discovery"
"advanced_options": "Advanced options",
"certificate": "Path to custom CA certificate file",
"client_id": "Client ID (leave empty to randomly generated one)",
"client_cert": "Path to a client certificate file",
"client_key": "Path to a private key file",
"keepalive": "The time between sending keep alive messages",
"tls_insecure": "Ignore broker certificate validation",
"protocol": "MQTT protocol",
"set_ca_cert": "Broker certificate validation",
"set_client_cert": "Use a client certificate"
}
},
"hassio_confirm": {
@ -30,7 +43,15 @@
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
"bad_birth": "Invalid birth topic",
"bad_will": "Invalid will topic",
"bad_discovery_prefix": "Invalid discovery prefix",
"bad_certificate": "The CA certificate is invalid",
"bad_client_cert": "Invalid client certiticate, ensure a PEM coded file is supplied",
"bad_client_key": "Invalid private key, ensure a PEM coded file is supplied without password",
"bad_client_cert_key": "Client certificate and private are no valid pair",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_inclusion": "The client certificate and private key must be configurered together"
}
},
"device_automation": {
@ -64,14 +85,25 @@
"broker": "Broker",
"port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
"password": "[%key:common::config_flow::data::password%]",
"advanced_options": "Advanced options",
"certificate": "Upload custom CA certificate file",
"client_id": "Client ID (leave empty to randomly generated one)",
"client_cert": "Upload client certificate file",
"client_key": "Upload private key file",
"keepalive": "The time between sending keep alive messages",
"tls_insecure": "Ignore broker certificate validation",
"protocol": "MQTT protocol",
"set_ca_cert": "Broker certificate validation",
"set_client_cert": "Use a client certificate"
}
},
"options": {
"title": "MQTT options",
"description": "Discovery - If discovery is enabled (recommended), Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually.\nBirth message - The birth message will be sent each time Home Assistant (re)connects to the MQTT broker.\nWill message - The will message will be sent each time Home Assistant loses its connection to the broker, both in case of a clean (e.g. Home Assistant shutting down) and in case of an unclean (e.g. Home Assistant crashing or losing its network connection) disconnect.",
"description": "Discovery - If discovery is enabled (recommended), Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually.\nDiscovery prefix - The prefix a configuration topic for automatic discovery must start with.\nBirth message - The birth message will be sent each time Home Assistant (re)connects to the MQTT broker.\nWill message - The will message will be sent each time Home Assistant loses its connection to the broker, both in case of a clean (e.g. Home Assistant shutting down) and in case of an unclean (e.g. Home Assistant crashing or losing its network connection) disconnect.",
"data": {
"discovery": "Enable discovery",
"discovery_prefix": "Discovery prefix",
"birth_enable": "Enable birth message",
"birth_topic": "Birth message topic",
"birth_payload": "Birth message payload",
@ -86,9 +118,15 @@
}
},
"error": {
"bad_birth": "Invalid birth topic",
"bad_will": "Invalid will topic",
"bad_discovery_prefix": "Invalid discovery prefix",
"bad_certificate": "The CA certificate is invalid",
"bad_client_cert": "Invalid client certiticate, ensure a PEM coded file is supplied",
"bad_client_key": "Invalid private key, ensure a PEM coded file is supplied without password",
"bad_client_cert_key": "Client certificate and private are no valid pair",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"bad_birth": "Invalid birth topic.",
"bad_will": "Invalid will topic."
"invalid_inclusion": "The client certificate and private key must be configured together"
}
}
}

View File

@ -5,15 +5,32 @@
"single_instance_allowed": "Already configured. Only a single configuration possible."
},
"error": {
"cannot_connect": "Failed to connect"
"bad_birth": "Invalid birth topic",
"bad_discovery_prefix": "Invalid discovery prefix",
"bad_certificate": "The CA certificate is invalid",
"bad_client_cert": "Invalid client certiticate, ensure a PEM coded file is supplied",
"bad_client_cert_key": "Client certificate and private are no valid pair",
"bad_client_key": "Invalid private key, ensure a PEM coded file is supplied without password",
"bad_will": "Invalid will topic",
"cannot_connect": "Failed to connect",
"invalid_inclusion": "The client certificate and private key must be configurered together"
},
"step": {
"broker": {
"data": {
"advanced_options": "Advanced options",
"broker": "Broker",
"discovery": "Enable discovery",
"certificate": "Path to custom CA certificate file",
"client_id": "Client ID (leave empty to randomly generated one)",
"client_cert": "Path to a client certificate file",
"client_key": "Path to a private key file",
"keepalive": "The time between sending keep alive messages",
"password": "Password",
"port": "Port",
"protocol": "MQTT protocol",
"set_ca_cert": "Broker certificate validation",
"set_client_cert": "Use a client certificate",
"tls_insecure": "Ignore broker certificate validation",
"username": "Username"
},
"description": "Please enter the connection information of your MQTT broker."
@ -53,20 +70,40 @@
"deprecated_yaml": {
"description": "Manually configured MQTT {platform}(s) found under platform key `{platform}`.\n\nPlease move the configuration to the `mqtt` integration key and restart Home Assistant to fix this issue. See the [documentation]({more_info_url}), for more information.",
"title": "Your manually configured MQTT {platform}(s) needs attention"
},
"deprecated_yaml_broker_settings": {
"title": "Deprecated MQTT settings found in `configuration.yaml`",
"description": "The following settings found in `configuration.yaml` were migrated to MQTT config entry and will now override the settings in `configuration.yaml`:\n`{deprecated_settings}`\n\nPlease remove these settings from `configuration.yaml` and restart Home Assistant to fix this issue. See the [documentation]({more_info_url}), for more information."
}
},
"options": {
"error": {
"bad_birth": "Invalid birth topic.",
"bad_will": "Invalid will topic.",
"cannot_connect": "Failed to connect"
"bad_birth": "Invalid birth topic",
"bad_discovery_prefix": "Invalid discovery prefix",
"bad_certificate": "The CA certificate is invalid",
"bad_client_cert": "Invalid client certiticate, ensure a PEM coded file is supplied",
"bad_client_cert_key": "Client certificate and private are no valid pair",
"bad_client_key": "Invalid private key, ensure a PEM coded file is supplied without password",
"bad_will": "Invalid will topic",
"cannot_connect": "Failed to connect",
"invalid_inclusion": "The client certificate and private key must be configured together"
},
"step": {
"broker": {
"data": {
"advanced_options": "Advanced options",
"broker": "Broker",
"certificate": "Upload custom CA certificate file",
"client_id": "Client ID (leave empty to randomly generated one)",
"client_cert": "Upload client certificate file",
"client_key": "Upload private key file",
"keepalive": "The time between sending keep alive messages",
"password": "Password",
"port": "Port",
"protocol": "MQTT protocol",
"set_ca_cert": "Broker certificate validation",
"set_client_cert": "Use a client certificate",
"tls_insecure": "Ignore broker certificate validation",
"username": "Username"
},
"description": "Please enter the connection information of your MQTT broker.",
@ -80,13 +117,14 @@
"birth_retain": "Birth message retain",
"birth_topic": "Birth message topic",
"discovery": "Enable discovery",
"discovery_prefix": "Discovery prefix",
"will_enable": "Enable will message",
"will_payload": "Will message payload",
"will_qos": "Will message QoS",
"will_retain": "Will message retain",
"will_topic": "Will message topic"
},
"description": "Discovery - If discovery is enabled (recommended), Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually.\nBirth message - The birth message will be sent each time Home Assistant (re)connects to the MQTT broker.\nWill message - The will message will be sent each time Home Assistant loses its connection to the broker, both in case of a clean (e.g. Home Assistant shutting down) and in case of an unclean (e.g. Home Assistant crashing or losing its network connection) disconnect.",
"description": "Discovery - If discovery is enabled (recommended), Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually.\nDiscovery prefix - The prefix a configuration topic for automatic discovery must start with.\nBirth message - The birth message will be sent each time Home Assistant (re)connects to the MQTT broker.\nWill message - The will message will be sent each time Home Assistant loses its connection to the broker, both in case of a clean (e.g. Home Assistant shutting down) and in case of an unclean (e.g. Home Assistant crashing or losing its network connection) disconnect.",
"title": "MQTT options"
}
}

View File

@ -2,6 +2,9 @@
from __future__ import annotations
import os
from pathlib import Path
import tempfile
from typing import Any
import voluptuous as vol
@ -9,19 +12,26 @@ import voluptuous as vol
from homeassistant.const import CONF_PAYLOAD
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers.typing import ConfigType
from .const import (
ATTR_PAYLOAD,
ATTR_QOS,
ATTR_RETAIN,
ATTR_TOPIC,
CONF_CERTIFICATE,
CONF_CLIENT_CERT,
CONF_CLIENT_KEY,
DATA_MQTT,
DEFAULT_ENCODING,
DEFAULT_QOS,
DEFAULT_RETAIN,
DOMAIN,
)
from .models import MqttData
TEMP_DIR_NAME = f"home-assistant-{DOMAIN}"
def mqtt_config_entry_enabled(hass: HomeAssistant) -> bool | None:
"""Return true when the MQTT config entry is enabled."""
@ -120,3 +130,57 @@ def get_mqtt_data(hass: HomeAssistant, ensure_exists: bool = False) -> MqttData:
if ensure_exists:
return hass.data.setdefault(DATA_MQTT, MqttData())
return hass.data[DATA_MQTT]
async def async_create_certificate_temp_files(
hass: HomeAssistant, config: ConfigType
) -> None:
"""Create certificate temporary files for the MQTT client."""
def _create_temp_file(temp_file: Path, data: str | None) -> None:
if data is None or data == "auto":
if temp_file.exists():
os.remove(Path(temp_file))
return
temp_file.write_text(data)
def _create_temp_dir_and_files() -> None:
"""Create temporary directory."""
temp_dir = Path(tempfile.gettempdir()) / TEMP_DIR_NAME
if (
config.get(CONF_CERTIFICATE)
or config.get(CONF_CLIENT_CERT)
or config.get(CONF_CLIENT_KEY)
) and not temp_dir.exists():
temp_dir.mkdir(0o700)
_create_temp_file(temp_dir / CONF_CERTIFICATE, config.get(CONF_CERTIFICATE))
_create_temp_file(temp_dir / CONF_CLIENT_CERT, config.get(CONF_CLIENT_CERT))
_create_temp_file(temp_dir / CONF_CLIENT_KEY, config.get(CONF_CLIENT_KEY))
await hass.async_add_executor_job(_create_temp_dir_and_files)
def get_file_path(option: str, default: str | None = None) -> Path | str | None:
"""Get file path of a certificate file."""
temp_dir = Path(tempfile.gettempdir()) / TEMP_DIR_NAME
if not temp_dir.exists():
return default
file_path: Path = temp_dir / option
if not file_path.exists():
return default
return temp_dir / option
def migrate_certificate_file_to_content(file_name_or_auto: str) -> str | None:
"""Convert certificate file or setting to config entry setting."""
if file_name_or_auto == "auto":
return "auto"
try:
with open(file_name_or_auto, encoding=DEFAULT_ENCODING) as certiticate_file:
return certiticate_file.read()
except OSError:
return None

View File

@ -1,5 +1,8 @@
"""Test config flow."""
from random import getrandbits
from ssl import SSLError
from unittest.mock import AsyncMock, patch
from uuid import uuid4
import pytest
import voluptuous as vol
@ -13,6 +16,9 @@ from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
MOCK_CLIENT_CERT = b"## mock client certificate file ##"
MOCK_CLIENT_KEY = b"## mock key file ##"
@pytest.fixture(autouse=True)
def mock_finish_setup():
@ -23,6 +29,43 @@ def mock_finish_setup():
yield mock_finish
@pytest.fixture
def mock_client_cert_check_fail():
"""Mock the client certificate check."""
with patch(
"homeassistant.components.mqtt.config_flow.load_pem_x509_certificate",
side_effect=ValueError,
) as mock_cert_check:
yield mock_cert_check
@pytest.fixture
def mock_client_key_check_fail():
"""Mock the client key file check."""
with patch(
"homeassistant.components.mqtt.config_flow.load_pem_private_key",
side_effect=ValueError,
) as mock_key_check:
yield mock_key_check
@pytest.fixture
def mock_ssl_context():
"""Mock the SSL context used to load the cert chain and to load verify locations."""
with patch(
"homeassistant.components.mqtt.config_flow.SSLContext"
) as mock_context, patch(
"homeassistant.components.mqtt.config_flow.load_pem_private_key"
) as mock_key_check, patch(
"homeassistant.components.mqtt.config_flow.load_pem_x509_certificate"
) as mock_cert_check:
yield {
"context": mock_context,
"load_pem_x509_certificate": mock_cert_check,
"load_pem_private_key": mock_key_check,
}
@pytest.fixture
def mock_reload_after_entry_update():
"""Mock out the reload after updating the entry."""
@ -84,6 +127,45 @@ def mock_try_connection_time_out():
yield mock_client()
@pytest.fixture
def mock_process_uploaded_file(tmp_path):
"""Mock upload certificate files."""
file_id_ca = str(uuid4())
file_id_cert = str(uuid4())
file_id_key = str(uuid4())
def _mock_process_uploaded_file(hass, file_id):
if file_id == file_id_ca:
with open(tmp_path / "ca.crt", "wb") as cafile:
cafile.write(b"## mock CA certificate file ##")
return tmp_path / "ca.crt"
elif file_id == file_id_cert:
with open(tmp_path / "client.crt", "wb") as certfile:
certfile.write(b"## mock client certificate file ##")
return tmp_path / "client.crt"
elif file_id == file_id_key:
with open(tmp_path / "client.key", "wb") as keyfile:
keyfile.write(b"## mock key file ##")
return tmp_path / "client.key"
else:
assert False
with patch(
"homeassistant.components.mqtt.config_flow.process_uploaded_file",
side_effect=_mock_process_uploaded_file,
) as mock_upload, patch(
# Patch temp dir name to avoid tests fail running in parallel
"homeassistant.components.mqtt.util.TEMP_DIR_NAME",
"home-assistant-mqtt" + f"-{getrandbits(10):03x}",
):
mock_upload.file_id = {
mqtt.CONF_CERTIFICATE: file_id_ca,
mqtt.CONF_CLIENT_CERT: file_id_cert,
mqtt.CONF_CLIENT_KEY: file_id_key,
}
yield mock_upload
async def test_user_connection_works(
hass, mock_try_connection, mock_finish_setup, mqtt_client_mock
):
@ -96,7 +178,7 @@ async def test_user_connection_works(
assert result["type"] == "form"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"broker": "127.0.0.1"}
result["flow_id"], {"broker": "127.0.0.1", "advanced_options": False}
)
assert result["type"] == "create_entry"
@ -104,6 +186,7 @@ async def test_user_connection_works(
"broker": "127.0.0.1",
"port": 1883,
"discovery": True,
"discovery_prefix": "homeassistant",
}
# Check we tried the connection
assert len(mock_try_connection.mock_calls) == 1
@ -184,15 +267,14 @@ async def test_manual_config_set(
"broker": "127.0.0.1",
"port": 1883,
"discovery": True,
"discovery_prefix": "homeassistant",
}
# Check we tried the connection, with precedence for config entry settings
mock_try_connection.assert_called_once_with(
{
"broker": "127.0.0.1",
"protocol": "3.1.1",
"keepalive": 60,
"discovery_prefix": "homeassistant",
"port": 1883,
"discovery": True,
},
)
# Check config entry got setup
@ -285,6 +367,7 @@ async def test_hassio_confirm(hass, mock_try_connection_success, mock_finish_set
"username": "mock-user",
"password": "mock-pass",
"discovery": True,
"discovery_prefix": "homeassistant",
}
# Check we tried the connection
assert len(mock_try_connection_success.mock_calls)
@ -376,6 +459,7 @@ async def test_option_flow(
result["flow_id"],
user_input={
mqtt.CONF_DISCOVERY: True,
"discovery_prefix": "homeassistant",
"birth_enable": True,
"birth_topic": "ha_state/online",
"birth_payload": "online",
@ -396,6 +480,7 @@ async def test_option_flow(
mqtt.CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "pass",
mqtt.CONF_DISCOVERY: True,
mqtt.CONF_DISCOVERY_PREFIX: "homeassistant",
mqtt.CONF_BIRTH_MESSAGE: {
mqtt.ATTR_TOPIC: "ha_state/online",
mqtt.ATTR_PAYLOAD: "online",
@ -419,6 +504,160 @@ async def test_option_flow(
)
@pytest.mark.parametrize(
"test_error",
[
"bad_certificate",
"bad_client_cert",
"bad_client_key",
"bad_client_cert_key",
"invalid_inclusion",
None,
],
)
async def test_bad_certificate(
hass,
mqtt_mock_entry_no_yaml_config,
mock_try_connection_success,
tmp_path,
mock_ssl_context,
test_error,
mock_process_uploaded_file,
):
"""Test bad certificate tests."""
# Mock certificate files
file_id = mock_process_uploaded_file.file_id
test_input = {
mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345,
mqtt.CONF_CERTIFICATE: file_id[mqtt.CONF_CERTIFICATE],
mqtt.CONF_CLIENT_CERT: file_id[mqtt.CONF_CLIENT_CERT],
mqtt.CONF_CLIENT_KEY: file_id[mqtt.CONF_CLIENT_KEY],
"set_ca_cert": True,
"set_client_cert": True,
}
set_client_cert = True
set_ca_cert = "custom"
tls_insecure = False
if test_error == "bad_certificate":
# CA chain is not loading
mock_ssl_context["context"]().load_verify_locations.side_effect = SSLError
elif test_error == "bad_client_cert":
# Client certificate is invalid
mock_ssl_context["load_pem_x509_certificate"].side_effect = ValueError
elif test_error == "bad_client_key":
# Client key file is invalid
mock_ssl_context["load_pem_private_key"].side_effect = ValueError
elif test_error == "bad_client_cert_key":
# Client key file file and certificate do not pair
mock_ssl_context["context"]().load_cert_chain.side_effect = SSLError
elif test_error == "invalid_inclusion":
# Client key file without client cert, client cert without key file
test_input.pop(mqtt.CONF_CLIENT_KEY)
mqtt_mock = await mqtt_mock_entry_no_yaml_config()
mock_try_connection.return_value = True
config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
# Add at least one advanced option to get the full form
config_entry.data = {
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234,
mqtt.CONF_CLIENT_ID: "custom1234",
mqtt.CONF_KEEPALIVE: 60,
mqtt.CONF_TLS_INSECURE: False,
mqtt.CONF_PROTOCOL: "3.1.1",
}
mqtt_mock.async_connect.reset_mock()
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"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345,
mqtt.CONF_KEEPALIVE: 60,
"set_client_cert": set_client_cert,
"set_ca_cert": set_ca_cert,
mqtt.CONF_TLS_INSECURE: tls_insecure,
mqtt.CONF_PROTOCOL: "3.1.1",
mqtt.CONF_CLIENT_ID: "custom1234",
},
)
test_input["set_client_cert"] = set_client_cert
test_input["set_ca_cert"] = set_ca_cert
test_input["tls_insecure"] = tls_insecure
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input=test_input,
)
if test_error is not None:
assert result["errors"]["base"] == test_error
return
assert result["errors"] == {}
@pytest.mark.parametrize(
"input_value, error",
[
("", True),
("-10", True),
("10", True),
("15", False),
("26", False),
("100", False),
],
)
async def test_keepalive_validation(
hass,
mqtt_mock_entry_no_yaml_config,
mock_try_connection,
mock_reload_after_entry_update,
input_value,
error,
):
"""Test validation of the keep alive option."""
test_input = {
mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345,
mqtt.CONF_KEEPALIVE: input_value,
}
mqtt_mock = await mqtt_mock_entry_no_yaml_config()
mock_try_connection.return_value = True
config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
# Add at least one advanced option to get the full form
config_entry.data = {
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234,
mqtt.CONF_CLIENT_ID: "custom1234",
}
mqtt_mock.async_connect.reset_mock()
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"
if error:
with pytest.raises(vol.MultipleInvalid):
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input=test_input,
)
return
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input=test_input,
)
assert not result["errors"]
async def test_disable_birth_will(
hass,
mqtt_mock_entry_no_yaml_config,
@ -459,6 +698,7 @@ async def test_disable_birth_will(
result["flow_id"],
user_input={
mqtt.CONF_DISCOVERY: True,
mqtt.CONF_DISCOVERY_PREFIX: "homeassistant",
"birth_enable": False,
"birth_topic": "ha_state/online",
"birth_payload": "online",
@ -479,6 +719,7 @@ async def test_disable_birth_will(
mqtt.CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "pass",
mqtt.CONF_DISCOVERY: True,
mqtt.CONF_DISCOVERY_PREFIX: "homeassistant",
mqtt.CONF_BIRTH_MESSAGE: {},
mqtt.CONF_WILL_MESSAGE: {},
}
@ -488,6 +729,64 @@ async def test_disable_birth_will(
assert mock_reload_after_entry_update.call_count == 1
async def test_invalid_discovery_prefix(
hass,
mqtt_mock_entry_no_yaml_config,
mock_try_connection,
mock_reload_after_entry_update,
):
"""Test setting an invalid discovery prefix."""
mqtt_mock = await mqtt_mock_entry_no_yaml_config()
mock_try_connection.return_value = True
config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
config_entry.data = {
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234,
mqtt.CONF_DISCOVERY: True,
mqtt.CONF_DISCOVERY_PREFIX: "homeassistant",
}
mqtt_mock.async_connect.reset_mock()
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"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345,
},
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "options"
await hass.async_block_till_done()
assert mqtt_mock.async_connect.call_count == 0
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
mqtt.CONF_DISCOVERY: True,
mqtt.CONF_DISCOVERY_PREFIX: "homeassistant#invalid",
},
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "options"
assert result["errors"]["base"] == "bad_discovery_prefix"
assert config_entry.data == {
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234,
mqtt.CONF_DISCOVERY: True,
mqtt.CONF_DISCOVERY_PREFIX: "homeassistant",
}
await hass.async_block_till_done()
# assert that the entry was not reloaded with the new config
assert mock_reload_after_entry_update.call_count == 0
def get_default(schema, key):
"""Get default value for key in voluptuous schema."""
for k in schema.keys():
@ -658,6 +957,47 @@ async def test_option_flow_default_suggested_values(
await hass.async_block_till_done()
@pytest.mark.parametrize(
"advanced_options, step_id", [(False, "options"), (True, "broker")]
)
async def test_skipping_advanced_options(
hass,
mqtt_mock_entry_no_yaml_config,
mock_try_connection,
mock_reload_after_entry_update,
advanced_options,
step_id,
):
"""Test advanced options option."""
test_input = {
mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345,
"advanced_options": advanced_options,
}
mqtt_mock = await mqtt_mock_entry_no_yaml_config()
mock_try_connection.return_value = True
config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
# Initiate with a basic setup
config_entry.data = {
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234,
}
mqtt_mock.async_connect.reset_mock()
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"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input=test_input,
)
assert result["step_id"] == step_id
async def test_options_user_connection_fails(hass, mock_try_connection_time_out):
"""Test if connection cannot be made."""
config_entry = MockConfigEntry(domain=mqtt.DOMAIN)
@ -760,50 +1100,57 @@ async def test_options_bad_will_message_fails(hass, mock_try_connection):
async def test_try_connection_with_advanced_parameters(
hass, mock_try_connection_success, tmp_path
hass,
mqtt_mock_entry_with_yaml_config,
mock_try_connection_success,
tmp_path,
mock_ssl_context,
mock_process_uploaded_file,
):
"""Test config flow with advanced parameters from config."""
# Mock certificate files
certfile = tmp_path / "cert.pem"
certfile.write_text("## mock certificate file ##")
keyfile = tmp_path / "key.pem"
keyfile.write_text("## mock key file ##")
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": certfile,
"client_key": keyfile,
"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.add_to_hass(hass)
config_entry.data = {
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234,
mqtt.CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "pass",
mqtt.CONF_KEEPALIVE: 30,
mqtt.CONF_DISCOVERY: True,
mqtt.CONF_BIRTH_MESSAGE: {
mqtt.ATTR_TOPIC: "ha_state/online",
mqtt.ATTR_PAYLOAD: "online",
mqtt.ATTR_QOS: 1,
mqtt.ATTR_RETAIN: True,
},
mqtt.CONF_WILL_MESSAGE: {
mqtt.ATTR_TOPIC: "ha_state/offline",
mqtt.ATTR_PAYLOAD: "offline",
mqtt.ATTR_QOS: 2,
mqtt.ATTR_RETAIN: False,
},
}
with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file):
await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
await hass.async_block_till_done()
config_entry = MockConfigEntry(domain=mqtt.DOMAIN)
config_entry.add_to_hass(hass)
config_entry.data = {
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234,
mqtt.CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "pass",
mqtt.CONF_DISCOVERY: True,
mqtt.CONF_BIRTH_MESSAGE: {
mqtt.ATTR_TOPIC: "ha_state/online",
mqtt.ATTR_PAYLOAD: "online",
mqtt.ATTR_QOS: 1,
mqtt.ATTR_RETAIN: True,
},
mqtt.CONF_WILL_MESSAGE: {
mqtt.ATTR_TOPIC: "ha_state/offline",
mqtt.ATTR_PAYLOAD: "offline",
mqtt.ATTR_QOS: 2,
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
@ -811,16 +1158,32 @@ async def test_try_connection_with_advanced_parameters(
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",
}
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
# test the client cert and key were migrated to the entry
assert config_entry.data[mqtt.CONF_CLIENT_CERT] == MOCK_CLIENT_CERT.decode(
"utf-8"
)
assert config_entry.data[mqtt.CONF_CLIENT_KEY] == MOCK_CLIENT_KEY.decode(
"utf-8"
)
assert config_entry.data[mqtt.CONF_CERTIFICATE] == "auto"
# test we can chante username and password
# as it was configured as auto in configuration.yaml is is migrated now
mock_try_connection_success.reset_mock()
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
@ -828,24 +1191,135 @@ async def test_try_connection_with_advanced_parameters(
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,
},
)
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
assert mock_try_connection_success.username_pw_set.mock_calls[0][1] == (
"us3r",
"p4ss",
)
# check if tls_insecure_set is called
assert mock_try_connection_success.tls_insecure_set.mock_calls[0][1] == (True,)
# check if the certificate settings were set from configuration.yaml
# check if the ca certificate settings were not set during connection test
assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[
"certfile"
] == str(certfile)
] == mqtt.util.get_file_path(mqtt.CONF_CLIENT_CERT)
assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[
"keyfile"
] == str(keyfile)
] == mqtt.util.get_file_path(mqtt.CONF_CLIENT_KEY)
# 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(
hass, mock_try_connection, tmp_path, mock_ssl_context, mock_process_uploaded_file
):
"""Test config flow setup with advanced parameters."""
file_id = mock_process_uploaded_file.file_id
config_entry = MockConfigEntry(domain=mqtt.DOMAIN)
config_entry.add_to_hass(hass)
config_entry.data = {
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234,
}
mock_try_connection.return_value = True
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == "form"
assert result["step_id"] == "broker"
assert result["data_schema"].schema["advanced_options"]
# first iteration, basic settings
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 2345,
mqtt.CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "secret",
"advanced_options": True,
},
)
assert result["type"] == "form"
assert result["step_id"] == "broker"
assert "advanced_options" not in result["data_schema"].schema
assert result["data_schema"].schema[mqtt.CONF_CLIENT_ID]
assert result["data_schema"].schema[mqtt.CONF_KEEPALIVE]
assert result["data_schema"].schema["set_client_cert"]
assert result["data_schema"].schema["set_ca_cert"]
assert result["data_schema"].schema[mqtt.CONF_TLS_INSECURE]
assert result["data_schema"].schema[mqtt.CONF_PROTOCOL]
assert mqtt.CONF_CLIENT_CERT not in result["data_schema"].schema
assert mqtt.CONF_CLIENT_KEY not in result["data_schema"].schema
# second iteration, advanced settings with request for client cert
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 2345,
mqtt.CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "secret",
mqtt.CONF_KEEPALIVE: 30,
"set_ca_cert": "auto",
"set_client_cert": True,
mqtt.CONF_TLS_INSECURE: True,
},
)
assert result["type"] == "form"
assert result["step_id"] == "broker"
assert "advanced_options" not in result["data_schema"].schema
assert result["data_schema"].schema[mqtt.CONF_CLIENT_ID]
assert result["data_schema"].schema[mqtt.CONF_KEEPALIVE]
assert result["data_schema"].schema["set_client_cert"]
assert result["data_schema"].schema["set_ca_cert"]
assert result["data_schema"].schema[mqtt.CONF_TLS_INSECURE]
assert result["data_schema"].schema[mqtt.CONF_PROTOCOL]
assert result["data_schema"].schema[mqtt.CONF_CLIENT_CERT]
assert result["data_schema"].schema[mqtt.CONF_CLIENT_KEY]
# third iteration, advanced settings with client cert and key set
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 2345,
mqtt.CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "secret",
mqtt.CONF_KEEPALIVE: 30,
"set_ca_cert": "auto",
"set_client_cert": True,
mqtt.CONF_CLIENT_CERT: file_id[mqtt.CONF_CLIENT_CERT],
mqtt.CONF_CLIENT_KEY: file_id[mqtt.CONF_CLIENT_KEY],
mqtt.CONF_TLS_INSECURE: True,
},
)
assert result["type"] == "form"
assert result["step_id"] == "options"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
mqtt.CONF_DISCOVERY: True,
mqtt.CONF_DISCOVERY_PREFIX: "homeassistant_test",
},
)
assert result["type"] == "create_entry"

View File

@ -1940,6 +1940,7 @@ async def test_update_incomplete_entry(
# Config entry data should now be updated
assert entry.data == {
"port": 1234,
"discovery_prefix": "homeassistant",
"broker": "yaml_broker",
}
# Warnings about broker deprecated, but not about other keys with default values
@ -2969,7 +2970,7 @@ async def test_remove_unknown_conf_entry_options(hass, mqtt_client_mock, caplog)
mqtt_config_entry_data = {
mqtt.CONF_BROKER: "mock-broker",
mqtt.CONF_BIRTH_MESSAGE: {},
mqtt.client.CONF_PROTOCOL: mqtt.const.PROTOCOL_311,
"old_option": "old_value",
}
entry = MockConfigEntry(
@ -2985,8 +2986,7 @@ async def test_remove_unknown_conf_entry_options(hass, mqtt_client_mock, caplog)
assert mqtt.client.CONF_PROTOCOL not in entry.data
assert (
"The following unsupported configuration options were removed from the "
"MQTT config entry: {'protocol'}. Add them to configuration.yaml if they "
"are needed"
"MQTT config entry: {'old_option'}"
) in caplog.text

View File

@ -0,0 +1,49 @@
"""Test MQTT utils."""
from random import getrandbits
from unittest.mock import patch
import pytest
from homeassistant.components import mqtt
@pytest.fixture(autouse=True)
def mock_temp_dir():
"""Mock the certificate temp directory."""
with patch(
# Patch temp dir name to avoid tests fail running in parallel
"homeassistant.components.mqtt.util.TEMP_DIR_NAME",
"home-assistant-mqtt" + f"-{getrandbits(10):03x}",
) as mocked_temp_dir:
yield mocked_temp_dir
@pytest.mark.parametrize(
"option,content,file_created",
[
(mqtt.CONF_CERTIFICATE, "auto", False),
(mqtt.CONF_CERTIFICATE, "### CA CERTIFICATE ###", True),
(mqtt.CONF_CLIENT_CERT, "### CLIENT CERTIFICATE ###", True),
(mqtt.CONF_CLIENT_KEY, "### PRIVATE KEY ###", True),
],
)
async def test_async_create_certificate_temp_files(
hass, mock_temp_dir, option, content, file_created
):
"""Test creating and reading certificate files."""
config = {option: content}
await mqtt.util.async_create_certificate_temp_files(hass, config)
file_path = mqtt.util.get_file_path(option)
assert bool(file_path) is file_created
assert (
mqtt.util.migrate_certificate_file_to_content(file_path or content) == content
)
async def test_reading_non_exitisting_certificate_file():
"""Test reading a non existing certificate file."""
assert (
mqtt.util.migrate_certificate_file_to_content("/home/file_not_exists") is None
)