Add Mqtt WebSockets support (#82078)

* Add Mqtt WebSockets support

* Fix tests

* Add testing websockets options

* Add tests transport settings

* Do not use templates for ws_headers

* Use json helper - small corrections
This commit is contained in:
Jan Bouwhuis 2022-11-23 15:03:31 +01:00 committed by GitHub
parent 4ea9926497
commit 32d68f375b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 253 additions and 8 deletions

View File

@ -76,7 +76,10 @@ from .const import ( # noqa: F401
CONF_TLS_INSECURE,
CONF_TLS_VERSION,
CONF_TOPIC,
CONF_TRANSPORT,
CONF_WILL_MESSAGE,
CONF_WS_HEADERS,
CONF_WS_PATH,
DATA_MQTT,
DEFAULT_ENCODING,
DEFAULT_QOS,
@ -134,6 +137,9 @@ CONFIG_ENTRY_CONFIG_KEYS = [
CONF_PORT,
CONF_PROTOCOL,
CONF_TLS_INSECURE,
CONF_TRANSPORT,
CONF_WS_PATH,
CONF_WS_HEADERS,
CONF_USERNAME,
CONF_WILL_MESSAGE,
]

View File

@ -51,14 +51,19 @@ from .const import (
CONF_CLIENT_KEY,
CONF_KEEPALIVE,
CONF_TLS_INSECURE,
CONF_TRANSPORT,
CONF_WILL_MESSAGE,
CONF_WS_HEADERS,
CONF_WS_PATH,
DEFAULT_ENCODING,
DEFAULT_PROTOCOL,
DEFAULT_QOS,
DEFAULT_TRANSPORT,
MQTT_CONNECTED,
MQTT_DISCONNECTED,
PROTOCOL_5,
PROTOCOL_31,
TRANSPORT_WEBSOCKETS,
)
from .models import (
AsyncMessageCallbackType,
@ -284,7 +289,8 @@ class MqttClientSetup:
# PAHO MQTT relies on the MQTT server to generate random client IDs.
# However, that feature is not mandatory so we generate our own.
client_id = mqtt.base62(uuid.uuid4().int, padding=22)
self._client = mqtt.Client(client_id, protocol=proto)
transport = config.get(CONF_TRANSPORT, DEFAULT_TRANSPORT)
self._client = mqtt.Client(client_id, protocol=proto, transport=transport)
# Enable logging
self._client.enable_logger()
@ -302,6 +308,10 @@ class MqttClientSetup:
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 transport == TRANSPORT_WEBSOCKETS:
ws_path = config.get(CONF_WS_PATH)
ws_headers = config.get(CONF_WS_HEADERS)
self._client.ws_set_options(ws_path, ws_headers)
if certificate is not None:
self._client.tls_set(
certificate,

View File

@ -27,6 +27,8 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_dumps, json_loads
from homeassistant.helpers.selector import (
BooleanSelector,
FileSelector,
@ -58,7 +60,10 @@ from .const import (
CONF_DISCOVERY_PREFIX,
CONF_KEEPALIVE,
CONF_TLS_INSECURE,
CONF_TRANSPORT,
CONF_WILL_MESSAGE,
CONF_WS_HEADERS,
CONF_WS_PATH,
DEFAULT_BIRTH,
DEFAULT_DISCOVERY,
DEFAULT_ENCODING,
@ -66,9 +71,14 @@ from .const import (
DEFAULT_PORT,
DEFAULT_PREFIX,
DEFAULT_PROTOCOL,
DEFAULT_TRANSPORT,
DEFAULT_WILL,
DEFAULT_WS_PATH,
DOMAIN,
SUPPORTED_PROTOCOLS,
SUPPORTED_TRANSPORTS,
TRANSPORT_TCP,
TRANSPORT_WEBSOCKETS,
)
from .util import (
async_create_certificate_temp_files,
@ -109,6 +119,15 @@ PROTOCOL_SELECTOR = SelectSelector(
mode=SelectSelectorMode.DROPDOWN,
)
)
TRANSPORT_SELECTOR = SelectSelector(
SelectSelectorConfig(
options=SUPPORTED_TRANSPORTS,
mode=SelectSelectorMode.DROPDOWN,
)
)
WS_HEADERS_SELECTOR = TextSelector(
TextSelectorConfig(type=TextSelectorType.TEXT, multiline=True)
)
CA_VERIFICATION_MODES = [
SelectOptionDict(value="off", label="Off"),
SelectOptionDict(value="auto", label="Auto"),
@ -493,6 +512,8 @@ async def async_get_broker_settings(
or not certificate
and user_input.get(SET_CA_CERT, "off") == "custom"
and not certificate_id
or user_input.get(CONF_TRANSPORT) == TRANSPORT_WEBSOCKETS
and CONF_WS_PATH not in user_input
):
return False
@ -526,6 +547,23 @@ async def async_get_broker_settings(
del validated_user_input[SET_CA_CERT]
if SET_CLIENT_CERT in validated_user_input:
del validated_user_input[SET_CLIENT_CERT]
if validated_user_input.get(CONF_TRANSPORT, TRANSPORT_TCP) == TRANSPORT_TCP:
if CONF_WS_PATH in validated_user_input:
del validated_user_input[CONF_WS_PATH]
if CONF_WS_HEADERS in validated_user_input:
del validated_user_input[CONF_WS_HEADERS]
return True
try:
validated_user_input[CONF_WS_HEADERS] = json_loads(
validated_user_input.get(CONF_WS_HEADERS, "{}")
)
schema = vol.Schema({cv.string: cv.template})
schema(validated_user_input[CONF_WS_HEADERS])
except JSON_DECODE_EXCEPTIONS + ( # pylint: disable=wrong-exception-operation
vol.MultipleInvalid,
):
errors["base"] = "bad_ws_headers"
return False
return True
if user_input:
@ -562,6 +600,13 @@ async def async_get_broker_settings(
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)
current_transport = current_config.get(CONF_TRANSPORT, DEFAULT_TRANSPORT)
current_ws_path = current_config.get(CONF_WS_PATH, DEFAULT_WS_PATH)
current_ws_headers = (
json_dumps(current_config.get(CONF_WS_HEADERS))
if CONF_WS_HEADERS in current_config
else None
)
advanced_broker_options |= bool(
current_client_id
or current_keepalive != DEFAULT_KEEPALIVE
@ -572,6 +617,7 @@ async def async_get_broker_settings(
or current_protocol != DEFAULT_PROTOCOL
or current_config.get(SET_CA_CERT, "off") != "off"
or current_config.get(SET_CLIENT_CERT)
or current_transport == TRANSPORT_WEBSOCKETS
)
# Build form
@ -665,6 +711,21 @@ async def async_get_broker_settings(
description={"suggested_value": current_protocol},
)
] = PROTOCOL_SELECTOR
fields[
vol.Optional(
CONF_TRANSPORT,
description={"suggested_value": current_transport},
)
] = TRANSPORT_SELECTOR
if current_transport == TRANSPORT_WEBSOCKETS:
fields[
vol.Optional(CONF_WS_PATH, description={"suggested_value": current_ws_path})
] = TEXT_SELECTOR
fields[
vol.Optional(
CONF_WS_HEADERS, description={"suggested_value": current_ws_headers}
)
] = WS_HEADERS_SELECTOR
# Show form
return False

View File

@ -45,15 +45,21 @@ from .const import (
CONF_KEEPALIVE,
CONF_TLS_INSECURE,
CONF_TLS_VERSION,
CONF_TRANSPORT,
CONF_WILL_MESSAGE,
CONF_WS_HEADERS,
CONF_WS_PATH,
DEFAULT_BIRTH,
DEFAULT_DISCOVERY,
DEFAULT_KEEPALIVE,
DEFAULT_PORT,
DEFAULT_PREFIX,
DEFAULT_PROTOCOL,
DEFAULT_TRANSPORT,
DEFAULT_WILL,
SUPPORTED_PROTOCOLS,
TRANSPORT_TCP,
TRANSPORT_WEBSOCKETS,
)
from .util import valid_birth_will, valid_publish_topic
@ -66,6 +72,7 @@ DEFAULT_VALUES = {
CONF_PORT: DEFAULT_PORT,
CONF_PROTOCOL: DEFAULT_PROTOCOL,
CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL,
CONF_TRANSPORT: DEFAULT_TRANSPORT,
CONF_WILL_MESSAGE: DEFAULT_WILL,
CONF_KEEPALIVE: DEFAULT_KEEPALIVE,
}
@ -160,6 +167,11 @@ CONFIG_SCHEMA_ENTRY = vol.Schema(
# 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,
vol.Optional(CONF_TRANSPORT, default=DEFAULT_TRANSPORT): vol.All(
cv.string, vol.In([TRANSPORT_TCP, TRANSPORT_WEBSOCKETS])
),
vol.Optional(CONF_WS_PATH, default="/"): cv.string,
vol.Optional(CONF_WS_HEADERS, default={}): {cv.string: cv.string},
}
)

View File

@ -23,6 +23,9 @@ CONF_SCHEMA = "schema"
CONF_STATE_TOPIC = "state_topic"
CONF_STATE_VALUE_TEMPLATE = "state_value_template"
CONF_TOPIC = "topic"
CONF_TRANSPORT = "transport"
CONF_WS_PATH = "ws_path"
CONF_WS_HEADERS = "ws_headers"
CONF_WILL_MESSAGE = "will_message"
CONF_CERTIFICATE = "certificate"
@ -42,15 +45,21 @@ DEFAULT_PAYLOAD_AVAILABLE = "online"
DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline"
DEFAULT_PORT = 1883
DEFAULT_RETAIN = False
DEFAULT_WS_PATH = "/"
PROTOCOL_31 = "3.1"
PROTOCOL_311 = "3.1.1"
PROTOCOL_5 = "5"
SUPPORTED_PROTOCOLS = [PROTOCOL_31, PROTOCOL_311, PROTOCOL_5]
TRANSPORT_TCP = "tcp"
TRANSPORT_WEBSOCKETS = "websockets"
SUPPORTED_TRANSPORTS = [TRANSPORT_TCP, TRANSPORT_WEBSOCKETS]
DEFAULT_PORT = 1883
DEFAULT_KEEPALIVE = 60
DEFAULT_PROTOCOL = PROTOCOL_311
DEFAULT_TRANSPORT = TRANSPORT_TCP
DEFAULT_BIRTH = {
ATTR_TOPIC: DEFAULT_BIRTH_WILL_TOPIC,

View File

@ -27,7 +27,10 @@
"tls_insecure": "Ignore broker certificate validation",
"protocol": "MQTT protocol",
"set_ca_cert": "Broker certificate validation",
"set_client_cert": "Use a client certificate"
"set_client_cert": "Use a client certificate",
"transport": "MQTT transport",
"ws_headers": "WebSocket headers in JSON format",
"ws_path": "WebSocket path"
}
},
"hassio_confirm": {
@ -50,6 +53,7 @@
"bad_client_cert": "Invalid client certificate, 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",
"bad_ws_headers": "Supply valid HTTP headers as a JSON object",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_inclusion": "The client certificate and private key must be configurered together"
}
@ -95,7 +99,10 @@
"tls_insecure": "[%key:component::mqtt::config::step::broker::data::tls_insecure%]",
"protocol": "[%key:component::mqtt::config::step::broker::data::protocol%]",
"set_ca_cert": "[%key:component::mqtt::config::step::broker::data::set_ca_cert%]",
"set_client_cert": "[%key:component::mqtt::config::step::broker::data::set_client_cert%]"
"set_client_cert": "[%key:component::mqtt::config::step::broker::data::set_client_cert%]",
"transport": "[%key:component::mqtt::config::step::broker::data::transport%]",
"ws_headers": "[%key:component::mqtt::config::step::broker::data::ws_headers%]",
"ws_path": "[%key:component::mqtt::config::step::broker::data::ws_path%]"
}
},
"options": {
@ -125,6 +132,7 @@
"bad_client_cert": "[%key:component::mqtt::config::error::bad_client_cert%]",
"bad_client_key": "[%key:component::mqtt::config::error::bad_client_key%]",
"bad_client_cert_key": "[%key:component::mqtt::config::error::bad_client_cert_key%]",
"bad_ws_headers": "[%key:component::mqtt::config::error::bad_ws_headers%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_inclusion": "[%key:component::mqtt::config::error::invalid_inclusion%]"
}

View File

@ -12,6 +12,7 @@
"bad_client_key": "Invalid private key, ensure a PEM coded file is supplied without password",
"bad_discovery_prefix": "Invalid discovery prefix",
"bad_will": "Invalid will topic",
"bad_ws_headers": "Supply valid HTTP headers as a JSON object",
"cannot_connect": "Failed to connect",
"invalid_inclusion": "The client certificate and private key must be configurered together"
},
@ -32,7 +33,10 @@
"set_ca_cert": "Broker certificate validation",
"set_client_cert": "Use a client certificate",
"tls_insecure": "Ignore broker certificate validation",
"username": "Username"
"transport": "MQTT transport",
"username": "Username",
"ws_path": "WebSocket path",
"ws_headers": "WebSocket headers"
},
"description": "Please enter the connection information of your MQTT broker."
},
@ -86,6 +90,7 @@
"bad_client_key": "Invalid private key, ensure a PEM coded file is supplied without password",
"bad_discovery_prefix": "Invalid discovery prefix",
"bad_will": "Invalid will topic",
"bad_ws_headers": "Supply valid HTTP headers as a JSON object",
"cannot_connect": "Failed to connect",
"invalid_inclusion": "The client certificate and private key must be configurered together"
},
@ -105,7 +110,10 @@
"set_ca_cert": "Broker certificate validation",
"set_client_cert": "Use a client certificate",
"tls_insecure": "Ignore broker certificate validation",
"username": "Username"
"transport": "MQTT transport",
"username": "Username",
"ws_path": "WebSocket path",
"ws_headers": "WebSocket headers"
},
"description": "Please enter the connection information of your MQTT broker.",
"title": "Broker options"

View File

@ -1140,7 +1140,6 @@ async def test_options_bad_will_message_fails(hass, mock_try_connection):
async def test_try_connection_with_advanced_parameters(
hass,
mqtt_mock_entry_with_yaml_config,
mock_try_connection_success,
tmp_path,
mock_ssl_context,
@ -1171,6 +1170,9 @@ async def test_try_connection_with_advanced_parameters(
mqtt.CONF_PORT: 1234,
mqtt.CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "pass",
mqtt.CONF_TRANSPORT: "websockets",
mqtt.CONF_WS_PATH: "/path/",
mqtt.CONF_WS_HEADERS: {"h1": "v1", "h2": "v2"},
mqtt.CONF_KEEPALIVE: 30,
mqtt.CONF_DISCOVERY: True,
mqtt.CONF_BIRTH_MESSAGE: {
@ -1205,6 +1207,9 @@ async def test_try_connection_with_advanced_parameters(
mqtt.CONF_PASSWORD: "pass",
mqtt.CONF_TLS_INSECURE: True,
mqtt.CONF_PROTOCOL: "3.1.1",
mqtt.CONF_TRANSPORT: "websockets",
mqtt.CONF_WS_PATH: "/path/",
mqtt.CONF_WS_HEADERS: '{"h1":"v1","h2":"v2"}',
}
for k, v in defaults.items():
assert get_default(result["data_schema"].schema, k) == v
@ -1220,7 +1225,7 @@ async def test_try_connection_with_advanced_parameters(
)
assert config_entry.data[mqtt.CONF_CERTIFICATE] == "auto"
# test we can chante username and password
# test we can change 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(
@ -1233,6 +1238,9 @@ async def test_try_connection_with_advanced_parameters(
"set_ca_cert": "auto",
"set_client_cert": True,
mqtt.CONF_TLS_INSECURE: True,
mqtt.CONF_TRANSPORT: "websockets",
mqtt.CONF_WS_PATH: "/new/path",
mqtt.CONF_WS_HEADERS: '{"h3": "v3"}',
},
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
@ -1256,6 +1264,12 @@ async def test_try_connection_with_advanced_parameters(
"keyfile"
] == mqtt.util.get_file_path(mqtt.CONF_CLIENT_KEY)
# check if websockets options are set
assert mock_try_connection_success.ws_set_options.mock_calls[0][1] == (
"/new/path",
{"h3": "v3"},
)
# Accept default option
result = await hass.config_entries.options.async_configure(
result["flow_id"],
@ -1305,6 +1319,7 @@ async def test_setup_with_advanced_settings(
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_TRANSPORT]
assert mqtt.CONF_CLIENT_CERT not in result["data_schema"].schema
assert mqtt.CONF_CLIENT_KEY not in result["data_schema"].schema
@ -1320,6 +1335,8 @@ async def test_setup_with_advanced_settings(
"set_ca_cert": "auto",
"set_client_cert": True,
mqtt.CONF_TLS_INSECURE: True,
mqtt.CONF_PROTOCOL: "3.1.1",
mqtt.CONF_TRANSPORT: "websockets",
},
)
assert result["type"] == "form"
@ -1333,8 +1350,11 @@ async def test_setup_with_advanced_settings(
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]
assert result["data_schema"].schema[mqtt.CONF_TRANSPORT]
assert result["data_schema"].schema[mqtt.CONF_WS_PATH]
assert result["data_schema"].schema[mqtt.CONF_WS_HEADERS]
# third iteration, advanced settings with client cert and key set
# third iteration, advanced settings with client cert and key set and bad json payload
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
@ -1348,6 +1368,34 @@ async def test_setup_with_advanced_settings(
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,
mqtt.CONF_TRANSPORT: "websockets",
mqtt.CONF_WS_PATH: "/custom_path/",
mqtt.CONF_WS_HEADERS: '{"header_1": "content_header_1", "header_2": "content_header_2"',
},
)
assert result["type"] == "form"
assert result["step_id"] == "broker"
assert result["errors"]["base"] == "bad_ws_headers"
# fourth iteration, advanced settings with client cert and key set
# and correct json payload for ws_headers
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,
mqtt.CONF_TRANSPORT: "websockets",
mqtt.CONF_WS_PATH: "/custom_path/",
mqtt.CONF_WS_HEADERS: '{"header_1": "content_header_1", "header_2": "content_header_2"}',
},
)
@ -1362,3 +1410,80 @@ async def test_setup_with_advanced_settings(
},
)
assert result["type"] == "create_entry"
# Check config entry result
assert config_entry.data == {
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 2345,
mqtt.CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "secret",
mqtt.CONF_KEEPALIVE: 30,
mqtt.CONF_CLIENT_CERT: "## mock client certificate file ##",
mqtt.CONF_CLIENT_KEY: "## mock key file ##",
"tls_insecure": True,
mqtt.CONF_TRANSPORT: "websockets",
mqtt.CONF_WS_PATH: "/custom_path/",
mqtt.CONF_WS_HEADERS: {
"header_1": "content_header_1",
"header_2": "content_header_2",
},
mqtt.CONF_CERTIFICATE: "auto",
mqtt.CONF_DISCOVERY: True,
mqtt.CONF_DISCOVERY_PREFIX: "homeassistant_test",
}
async def test_change_websockets_transport_to_tcp(
hass, mock_try_connection, tmp_path, mock_ssl_context, mock_process_uploaded_file
):
"""Test option flow setup with websockets transport settings."""
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_TRANSPORT: "websockets",
mqtt.CONF_WS_HEADERS: {"header_1": "custom_header1"},
mqtt.CONF_WS_PATH: "/some_path",
}
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["transport"]
assert result["data_schema"].schema["ws_path"]
assert result["data_schema"].schema["ws_headers"]
# Change transport to tcp
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234,
mqtt.CONF_TRANSPORT: "tcp",
mqtt.CONF_WS_HEADERS: '{"header_1": "custom_header1"}',
mqtt.CONF_WS_PATH: "/some_path",
},
)
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"
# Check config entry result
assert config_entry.data == {
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234,
mqtt.CONF_TRANSPORT: "tcp",
mqtt.CONF_DISCOVERY: True,
mqtt.CONF_DISCOVERY_PREFIX: "homeassistant_test",
}

View File

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

View File

@ -1179,7 +1179,10 @@ ABBREVIATIONS_WHITE_LIST = [
"CONF_KEEPALIVE",
"CONF_TLS_INSECURE",
"CONF_TLS_VERSION",
"CONF_TRANSPORT",
"CONF_WILL_MESSAGE",
"CONF_WS_PATH",
"CONF_WS_HEADERS",
# Undocumented device configuration
"CONF_DEPRECATED_VIA_HUB",
"CONF_VIA_DEVICE",