mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 09:17:53 +00:00
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:
parent
a8bf8d449b
commit
5e7f571f01
@ -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:
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
]
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
49
tests/components/mqtt/test_util.py
Normal file
49
tests/components/mqtt/test_util.py
Normal 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
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user