MQTT config entry (#16594)
* MQTT config entry * Solely rely on config entry * Improve wawrning * Lint * Lint2
This commit is contained in:
parent
05c0717167
commit
72e746d240
11 changed files with 435 additions and 133 deletions
|
@ -14,6 +14,7 @@
|
||||||
"data": {
|
"data": {
|
||||||
"2fa": "2FA Pin"
|
"2fa": "2FA Pin"
|
||||||
},
|
},
|
||||||
|
"description": "",
|
||||||
"title": "2-Factor-Authentication"
|
"title": "2-Factor-Authentication"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
|
@ -21,6 +22,7 @@
|
||||||
"email": "E-Mail Address",
|
"email": "E-Mail Address",
|
||||||
"password": "Password"
|
"password": "Password"
|
||||||
},
|
},
|
||||||
|
"description": "",
|
||||||
"title": "Google Hangouts Login"
|
"title": "Google Hangouts Login"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "Access point is already configured",
|
"already_configured": "Access point is already configured",
|
||||||
"conection_aborted": "Could not connect to HMIP server",
|
|
||||||
"connection_aborted": "Could not connect to HMIP server",
|
"connection_aborted": "Could not connect to HMIP server",
|
||||||
"unknown": "Unknown error occurred."
|
"unknown": "Unknown error occurred."
|
||||||
},
|
},
|
||||||
|
|
23
homeassistant/components/mqtt/.translations/en.json
Normal file
23
homeassistant/components/mqtt/.translations/en.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "Only a single configuration of MQTT is allowed."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Unable to connect to the broker."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"broker": {
|
||||||
|
"data": {
|
||||||
|
"broker": "Broker",
|
||||||
|
"password": "Password",
|
||||||
|
"port": "Port",
|
||||||
|
"username": "Username"
|
||||||
|
},
|
||||||
|
"description": "Please enter the connection information of your MQTT broker.",
|
||||||
|
"title": "MQTT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "MQTT"
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ import attr
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
from homeassistant.helpers.typing import HomeAssistantType, ConfigType, \
|
from homeassistant.helpers.typing import HomeAssistantType, ConfigType, \
|
||||||
ServiceDataType
|
ServiceDataType
|
||||||
from homeassistant.core import callback, Event, ServiceCall
|
from homeassistant.core import callback, Event, ServiceCall
|
||||||
|
@ -30,8 +31,12 @@ from homeassistant.util.async_ import (
|
||||||
run_coroutine_threadsafe, run_callback_threadsafe)
|
run_coroutine_threadsafe, run_callback_threadsafe)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE, CONF_USERNAME,
|
EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE, CONF_USERNAME,
|
||||||
CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD)
|
CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD,
|
||||||
|
EVENT_HOMEASSISTANT_START)
|
||||||
|
|
||||||
|
# Loading the config flow file will register the flow
|
||||||
|
from . import config_flow # noqa # pylint: disable=unused-import
|
||||||
|
from .const import CONF_BROKER
|
||||||
from .server import HBMQTT_CONFIG_SCHEMA
|
from .server import HBMQTT_CONFIG_SCHEMA
|
||||||
|
|
||||||
REQUIREMENTS = ['paho-mqtt==1.3.1']
|
REQUIREMENTS = ['paho-mqtt==1.3.1']
|
||||||
|
@ -41,11 +46,12 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
DOMAIN = 'mqtt'
|
DOMAIN = 'mqtt'
|
||||||
|
|
||||||
DATA_MQTT = 'mqtt'
|
DATA_MQTT = 'mqtt'
|
||||||
|
DATA_MQTT_CONFIG = 'mqtt_config'
|
||||||
|
|
||||||
SERVICE_PUBLISH = 'publish'
|
SERVICE_PUBLISH = 'publish'
|
||||||
|
|
||||||
CONF_EMBEDDED = 'embedded'
|
CONF_EMBEDDED = 'embedded'
|
||||||
CONF_BROKER = 'broker'
|
|
||||||
CONF_CLIENT_ID = 'client_id'
|
CONF_CLIENT_ID = 'client_id'
|
||||||
CONF_DISCOVERY = 'discovery'
|
CONF_DISCOVERY = 'discovery'
|
||||||
CONF_DISCOVERY_PREFIX = 'discovery_prefix'
|
CONF_DISCOVERY_PREFIX = 'discovery_prefix'
|
||||||
|
@ -311,6 +317,7 @@ async def _async_setup_server(hass: HomeAssistantType,
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return broker_config
|
return broker_config
|
||||||
|
|
||||||
|
|
||||||
|
@ -340,19 +347,15 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
||||||
conf = config.get(DOMAIN) # type: Optional[ConfigType]
|
conf = config.get(DOMAIN) # type: Optional[ConfigType]
|
||||||
|
|
||||||
if conf is None:
|
if conf is None:
|
||||||
conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN]
|
# If we have a config entry, setup is done by that config entry.
|
||||||
conf = cast(ConfigType, conf)
|
# If there is no config entry, this should fail.
|
||||||
|
return bool(hass.config_entries.async_entries(DOMAIN))
|
||||||
|
|
||||||
client_id = conf.get(CONF_CLIENT_ID) # type: Optional[str]
|
conf = dict(conf)
|
||||||
keepalive = conf.get(CONF_KEEPALIVE) # type: int
|
|
||||||
|
|
||||||
# Only setup if embedded config passed in or no broker specified
|
if CONF_EMBEDDED in conf or CONF_BROKER not in conf:
|
||||||
if CONF_EMBEDDED not in conf and CONF_BROKER in conf:
|
|
||||||
broker_config = None
|
|
||||||
else:
|
|
||||||
if (conf.get(CONF_PASSWORD) is None and
|
if (conf.get(CONF_PASSWORD) is None and
|
||||||
config.get('http') is not None and
|
config.get('http', {}).get('api_password') is not None):
|
||||||
config['http'].get('api_password') is not None):
|
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Starting from release 0.76, the embedded MQTT broker does not"
|
"Starting from release 0.76, the embedded MQTT broker does not"
|
||||||
" use api_password as default password anymore. Please set"
|
" use api_password as default password anymore. Please set"
|
||||||
|
@ -362,48 +365,98 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
||||||
|
|
||||||
broker_config = await _async_setup_server(hass, config)
|
broker_config = await _async_setup_server(hass, config)
|
||||||
|
|
||||||
if CONF_BROKER in conf:
|
if broker_config is None:
|
||||||
broker = conf[CONF_BROKER] # type: str
|
_LOGGER.error('Unable to start embedded MQTT broker')
|
||||||
port = conf[CONF_PORT] # type: int
|
return False
|
||||||
username = conf.get(CONF_USERNAME) # type: Optional[str]
|
|
||||||
password = conf.get(CONF_PASSWORD) # type: Optional[str]
|
conf.update({
|
||||||
certificate = conf.get(CONF_CERTIFICATE) # type: Optional[str]
|
CONF_BROKER: broker_config[0],
|
||||||
client_key = conf.get(CONF_CLIENT_KEY) # type: Optional[str]
|
CONF_PORT: broker_config[1],
|
||||||
client_cert = conf.get(CONF_CLIENT_CERT) # type: Optional[str]
|
CONF_USERNAME: broker_config[2],
|
||||||
tls_insecure = conf.get(CONF_TLS_INSECURE) # type: Optional[bool]
|
CONF_PASSWORD: broker_config[3],
|
||||||
protocol = conf[CONF_PROTOCOL] # type: str
|
CONF_CERTIFICATE: broker_config[4],
|
||||||
elif broker_config is not None:
|
CONF_PROTOCOL: broker_config[5],
|
||||||
# If no broker passed in, auto config to internal server
|
CONF_CLIENT_KEY: None,
|
||||||
broker, port, username, password, certificate, protocol = broker_config
|
CONF_CLIENT_CERT: None,
|
||||||
# Embedded broker doesn't have some ssl variables
|
CONF_TLS_INSECURE: None,
|
||||||
client_key, client_cert, tls_insecure = None, None, None
|
})
|
||||||
# hbmqtt requires a client id to be set.
|
|
||||||
if client_id is None:
|
hass.data[DATA_MQTT_CONFIG] = conf
|
||||||
client_id = 'home-assistant'
|
|
||||||
else:
|
# Only import if we haven't before.
|
||||||
err = "Unable to start MQTT broker."
|
if not hass.config_entries.async_entries(DOMAIN):
|
||||||
if conf.get(CONF_EMBEDDED) is not None:
|
hass.async_create_task(hass.config_entries.flow.async_init(
|
||||||
# Explicit embedded config, requires explicit broker config
|
DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
|
||||||
err += " (Broker configuration required.)"
|
data={}
|
||||||
_LOGGER.error(err)
|
))
|
||||||
|
|
||||||
|
if conf.get(CONF_DISCOVERY):
|
||||||
|
async def async_setup_discovery(event):
|
||||||
|
await _async_setup_discovery(hass, config)
|
||||||
|
|
||||||
|
hass.bus.async_listen_once(
|
||||||
|
EVENT_HOMEASSISTANT_START, async_setup_discovery)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, entry):
|
||||||
|
"""Load a config entry."""
|
||||||
|
conf = hass.data.get(DATA_MQTT_CONFIG)
|
||||||
|
|
||||||
|
# Config entry was created because user had configuration.yaml entry
|
||||||
|
# They removed that, so remove entry.
|
||||||
|
if conf is None and entry.source == config_entries.SOURCE_IMPORT:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_remove(entry.entry_id))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# If user didn't have configuration.yaml config, generate defaults
|
||||||
|
if conf is None:
|
||||||
|
conf = CONFIG_SCHEMA({
|
||||||
|
DOMAIN: entry.data
|
||||||
|
})[DOMAIN]
|
||||||
|
elif any(key in conf for key in entry.data):
|
||||||
|
_LOGGER.warning(
|
||||||
|
'Data in your config entry is going to override your '
|
||||||
|
'configuration.yaml: %s', entry.data)
|
||||||
|
|
||||||
|
conf.update(entry.data)
|
||||||
|
|
||||||
|
broker = conf[CONF_BROKER]
|
||||||
|
port = conf[CONF_PORT]
|
||||||
|
client_id = conf.get(CONF_CLIENT_ID)
|
||||||
|
keepalive = conf[CONF_KEEPALIVE]
|
||||||
|
username = conf.get(CONF_USERNAME)
|
||||||
|
password = conf.get(CONF_PASSWORD)
|
||||||
|
client_key = conf.get(CONF_CLIENT_KEY)
|
||||||
|
client_cert = conf.get(CONF_CLIENT_CERT)
|
||||||
|
tls_insecure = conf.get(CONF_TLS_INSECURE)
|
||||||
|
protocol = conf[CONF_PROTOCOL]
|
||||||
|
|
||||||
# For cloudmqtt.com, secured connection, auto fill in certificate
|
# For cloudmqtt.com, secured connection, auto fill in certificate
|
||||||
if (certificate is None and 19999 < port < 30000 and
|
if (conf.get(CONF_CERTIFICATE) is None and
|
||||||
broker.endswith('.cloudmqtt.com')):
|
19999 < conf[CONF_PORT] < 30000 and
|
||||||
|
conf[CONF_BROKER].endswith('.cloudmqtt.com')):
|
||||||
certificate = os.path.join(os.path.dirname(__file__),
|
certificate = os.path.join(os.path.dirname(__file__),
|
||||||
'addtrustexternalcaroot.crt')
|
'addtrustexternalcaroot.crt')
|
||||||
|
|
||||||
# When the certificate is set to auto, use bundled certs from requests
|
# When the certificate is set to auto, use bundled certs from requests
|
||||||
if certificate == 'auto':
|
elif conf.get(CONF_CERTIFICATE) == 'auto':
|
||||||
certificate = requests.certs.where()
|
certificate = requests.certs.where()
|
||||||
|
|
||||||
will_message = None # type: Optional[Message]
|
else:
|
||||||
if conf.get(CONF_WILL_MESSAGE) is not None:
|
certificate = None
|
||||||
will_message = Message(**conf.get(CONF_WILL_MESSAGE))
|
|
||||||
birth_message = None # type: Optional[Message]
|
if CONF_WILL_MESSAGE in conf:
|
||||||
if conf.get(CONF_BIRTH_MESSAGE) is not None:
|
will_message = Message(**conf[CONF_WILL_MESSAGE])
|
||||||
birth_message = Message(**conf.get(CONF_BIRTH_MESSAGE))
|
else:
|
||||||
|
will_message = None
|
||||||
|
|
||||||
|
if CONF_BIRTH_MESSAGE in conf:
|
||||||
|
birth_message = Message(**conf[CONF_BIRTH_MESSAGE])
|
||||||
|
else:
|
||||||
|
birth_message = None
|
||||||
|
|
||||||
# Be able to override versions other than TLSv1.0 under Python3.6
|
# Be able to override versions other than TLSv1.0 under Python3.6
|
||||||
conf_tls_version = conf.get(CONF_TLS_VERSION) # type: str
|
conf_tls_version = conf.get(CONF_TLS_VERSION) # type: str
|
||||||
|
@ -421,14 +474,27 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
||||||
else:
|
else:
|
||||||
tls_version = ssl.PROTOCOL_TLSv1
|
tls_version = ssl.PROTOCOL_TLSv1
|
||||||
|
|
||||||
try:
|
hass.data[DATA_MQTT] = MQTT(
|
||||||
hass.data[DATA_MQTT] = MQTT(
|
hass,
|
||||||
hass, broker, port, client_id, keepalive, username, password,
|
broker=broker,
|
||||||
certificate, client_key, client_cert, tls_insecure, protocol,
|
port=port,
|
||||||
will_message, birth_message, tls_version)
|
client_id=client_id,
|
||||||
except socket.error:
|
keepalive=keepalive,
|
||||||
_LOGGER.exception("Can't connect to the broker. "
|
username=username,
|
||||||
"Please check your settings and the broker itself")
|
password=password,
|
||||||
|
certificate=certificate,
|
||||||
|
client_key=client_key,
|
||||||
|
client_cert=client_cert,
|
||||||
|
tls_insecure=tls_insecure,
|
||||||
|
protocol=protocol,
|
||||||
|
will_message=will_message,
|
||||||
|
birth_message=birth_message,
|
||||||
|
tls_version=tls_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
success = await hass.data[DATA_MQTT].async_connect() # type: bool
|
||||||
|
|
||||||
|
if not success:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def async_stop_mqtt(event: Event):
|
async def async_stop_mqtt(event: Event):
|
||||||
|
@ -437,10 +503,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
||||||
|
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt)
|
||||||
|
|
||||||
success = await hass.data[DATA_MQTT].async_connect() # type: bool
|
|
||||||
if not success:
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def async_publish_service(call: ServiceCall):
|
async def async_publish_service(call: ServiceCall):
|
||||||
"""Handle MQTT publish service calls."""
|
"""Handle MQTT publish service calls."""
|
||||||
msg_topic = call.data[ATTR_TOPIC] # type: str
|
msg_topic = call.data[ATTR_TOPIC] # type: str
|
||||||
|
@ -466,9 +528,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
||||||
DOMAIN, SERVICE_PUBLISH, async_publish_service,
|
DOMAIN, SERVICE_PUBLISH, async_publish_service,
|
||||||
schema=MQTT_PUBLISH_SCHEMA)
|
schema=MQTT_PUBLISH_SCHEMA)
|
||||||
|
|
||||||
if conf.get(CONF_DISCOVERY):
|
|
||||||
await _async_setup_discovery(hass, config)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -501,7 +560,8 @@ class MQTT:
|
||||||
certificate: Optional[str], client_key: Optional[str],
|
certificate: Optional[str], client_key: Optional[str],
|
||||||
client_cert: Optional[str], tls_insecure: Optional[bool],
|
client_cert: Optional[str], tls_insecure: Optional[bool],
|
||||||
protocol: Optional[str], will_message: Optional[Message],
|
protocol: Optional[str], will_message: Optional[Message],
|
||||||
birth_message: Optional[Message], tls_version) -> None:
|
birth_message: Optional[Message],
|
||||||
|
tls_version: Optional[int]) -> None:
|
||||||
"""Initialize Home Assistant MQTT client."""
|
"""Initialize Home Assistant MQTT client."""
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
|
|
||||||
|
|
98
homeassistant/components/mqtt/config_flow.py
Normal file
98
homeassistant/components/mqtt/config_flow.py
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
"""Config flow for MQTT."""
|
||||||
|
from collections import OrderedDict
|
||||||
|
import queue
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_PORT
|
||||||
|
|
||||||
|
from .const import CONF_BROKER
|
||||||
|
|
||||||
|
|
||||||
|
@config_entries.HANDLERS.register('mqtt')
|
||||||
|
class FlowHandler(config_entries.ConfigFlow):
|
||||||
|
"""Handle a config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
if self._async_current_entries():
|
||||||
|
return self.async_abort(
|
||||||
|
reason='single_instance_allowed'
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self.async_step_broker()
|
||||||
|
|
||||||
|
async def async_step_broker(self, user_input=None):
|
||||||
|
"""Confirm setup."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
can_connect = await self.hass.async_add_executor_job(
|
||||||
|
try_connection, user_input[CONF_BROKER], user_input[CONF_PORT],
|
||||||
|
user_input.get(CONF_USERNAME), user_input.get(CONF_PASSWORD))
|
||||||
|
|
||||||
|
if can_connect:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=user_input[CONF_BROKER],
|
||||||
|
data=user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
errors['base'] = 'cannot_connect'
|
||||||
|
|
||||||
|
fields = OrderedDict()
|
||||||
|
fields[vol.Required(CONF_BROKER)] = str
|
||||||
|
fields[vol.Required(CONF_PORT, default=1883)] = vol.Coerce(int)
|
||||||
|
fields[vol.Optional(CONF_USERNAME)] = str
|
||||||
|
fields[vol.Optional(CONF_PASSWORD)] = str
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id='broker',
|
||||||
|
data_schema=vol.Schema(fields),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_import(self, user_input):
|
||||||
|
"""Import a config entry.
|
||||||
|
|
||||||
|
Special type of import, we're not actually going to store any data.
|
||||||
|
Instead, we're going to rely on the values that are in config file.
|
||||||
|
"""
|
||||||
|
if self._async_current_entries():
|
||||||
|
return self.async_abort(
|
||||||
|
reason='single_instance_allowed'
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title='configuration.yaml',
|
||||||
|
data={}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def try_connection(broker, port, username, password):
|
||||||
|
"""Test if we can connect to an MQTT broker."""
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
client = mqtt.Client()
|
||||||
|
if username and password:
|
||||||
|
client.username_pw_set(username, password)
|
||||||
|
|
||||||
|
result = queue.Queue(maxsize=1)
|
||||||
|
|
||||||
|
def on_connect(client_, userdata, flags, result_code):
|
||||||
|
"""Handle connection result."""
|
||||||
|
result.put(result_code == mqtt.CONNACK_ACCEPTED)
|
||||||
|
|
||||||
|
client.on_connect = on_connect
|
||||||
|
|
||||||
|
client.connect_async(broker, port)
|
||||||
|
client.loop_start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return result.get(timeout=5)
|
||||||
|
except queue.Empty:
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
client.disconnect()
|
||||||
|
client.loop_stop()
|
2
homeassistant/components/mqtt/const.py
Normal file
2
homeassistant/components/mqtt/const.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
"""Constants used by multiple MQTT modules."""
|
||||||
|
CONF_BROKER = 'broker'
|
23
homeassistant/components/mqtt/strings.json
Normal file
23
homeassistant/components/mqtt/strings.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": "MQTT",
|
||||||
|
"step": {
|
||||||
|
"broker": {
|
||||||
|
"title": "MQTT",
|
||||||
|
"description": "Please enter the connection information of your MQTT broker.",
|
||||||
|
"data": {
|
||||||
|
"broker": "Broker",
|
||||||
|
"port": "Port",
|
||||||
|
"username": "Username",
|
||||||
|
"password": "Password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "Only a single configuration of MQTT is allowed."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Unable to connect to the broker."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -141,6 +141,7 @@ FLOWS = [
|
||||||
'homematicip_cloud',
|
'homematicip_cloud',
|
||||||
'hue',
|
'hue',
|
||||||
'ios',
|
'ios',
|
||||||
|
'mqtt',
|
||||||
'nest',
|
'nest',
|
||||||
'openuv',
|
'openuv',
|
||||||
'sonos',
|
'sonos',
|
||||||
|
@ -463,3 +464,19 @@ class ConfigEntries:
|
||||||
async def _old_conf_migrator(old_config):
|
async def _old_conf_migrator(old_config):
|
||||||
"""Migrate the pre-0.73 config format to the latest version."""
|
"""Migrate the pre-0.73 config format to the latest version."""
|
||||||
return {'entries': old_config}
|
return {'entries': old_config}
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFlow(data_entry_flow.FlowHandler):
|
||||||
|
"""Base class for config flows with some helpers."""
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_current_entries(self):
|
||||||
|
"""Return current entries."""
|
||||||
|
return self.hass.config_entries.async_entries(self.handler)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_in_progress(self):
|
||||||
|
"""Return other in progress flows for current domain."""
|
||||||
|
return [flw for flw in self.hass.config_entries.flow.async_progress()
|
||||||
|
if flw['handler'] == self.handler and
|
||||||
|
flw['flow_id'] != self.flow_id]
|
||||||
|
|
85
tests/components/mqtt/test_config_flow.py
Normal file
85
tests/components/mqtt/test_config_flow.py
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
"""Test config flow."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.common import mock_coro
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_finish_setup():
|
||||||
|
"""Mock out the finish setup method."""
|
||||||
|
with patch('homeassistant.components.mqtt.MQTT.async_connect',
|
||||||
|
return_value=mock_coro(True)) as mock_finish:
|
||||||
|
yield mock_finish
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_try_connection():
|
||||||
|
"""Mock the try connection method."""
|
||||||
|
with patch(
|
||||||
|
'homeassistant.components.mqtt.config_flow.try_connection'
|
||||||
|
) as mock_try:
|
||||||
|
yield mock_try
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_connection_works(hass, mock_try_connection,
|
||||||
|
mock_finish_setup):
|
||||||
|
"""Test we can finish a config flow."""
|
||||||
|
mock_try_connection.return_value = True
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
'mqtt', context={'source': 'user'})
|
||||||
|
assert result['type'] == 'form'
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result['flow_id'], {
|
||||||
|
'broker': '127.0.0.1',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result['type'] == 'create_entry'
|
||||||
|
# Check we tried the connection
|
||||||
|
assert len(mock_try_connection.mock_calls) == 1
|
||||||
|
# Check config entry got setup
|
||||||
|
assert len(mock_finish_setup.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_connection_fails(hass, mock_try_connection,
|
||||||
|
mock_finish_setup):
|
||||||
|
"""Test if connnection cannot be made."""
|
||||||
|
mock_try_connection.return_value = False
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
'mqtt', context={'source': 'user'})
|
||||||
|
assert result['type'] == 'form'
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result['flow_id'], {
|
||||||
|
'broker': '127.0.0.1',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result['type'] == 'form'
|
||||||
|
assert result['errors']['base'] == 'cannot_connect'
|
||||||
|
|
||||||
|
# Check we tried the connection
|
||||||
|
assert len(mock_try_connection.mock_calls) == 1
|
||||||
|
# Check config entry did not setup
|
||||||
|
assert len(mock_finish_setup.mock_calls) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_manual_config_set(hass, mock_try_connection,
|
||||||
|
mock_finish_setup):
|
||||||
|
"""Test we ignore entry if manual config available."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass, 'mqtt', {'mqtt': {'broker': 'bla'}})
|
||||||
|
assert len(mock_finish_setup.mock_calls) == 1
|
||||||
|
|
||||||
|
mock_try_connection.return_value = True
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
'mqtt', context={'source': 'user'})
|
||||||
|
assert result['type'] == 'abort'
|
|
@ -2,21 +2,29 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
import socket
|
|
||||||
import ssl
|
import ssl
|
||||||
|
|
||||||
|
import pytest
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
import homeassistant.components.mqtt as mqtt
|
from homeassistant.components import mqtt
|
||||||
from homeassistant.const import (EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE,
|
from homeassistant.const import (EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE,
|
||||||
EVENT_HOMEASSISTANT_STOP)
|
EVENT_HOMEASSISTANT_STOP)
|
||||||
|
|
||||||
from tests.common import (get_test_home_assistant, mock_coro,
|
from tests.common import (get_test_home_assistant, mock_coro,
|
||||||
mock_mqtt_component,
|
mock_mqtt_component,
|
||||||
threadsafe_coroutine_factory, fire_mqtt_message,
|
threadsafe_coroutine_factory, fire_mqtt_message,
|
||||||
async_fire_mqtt_message)
|
async_fire_mqtt_message, MockConfigEntry)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_MQTT():
|
||||||
|
"""Make sure connection is established."""
|
||||||
|
with mock.patch('homeassistant.components.mqtt.MQTT') as mock_MQTT:
|
||||||
|
mock_MQTT.return_value.async_connect.return_value = mock_coro(True)
|
||||||
|
yield mock_MQTT
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -533,64 +541,59 @@ def test_setup_embedded_with_embedded(hass):
|
||||||
assert _start.call_count == 1
|
assert _start.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_setup_fails_if_no_connect_broker(hass):
|
||||||
def test_setup_fails_if_no_connect_broker(hass):
|
|
||||||
"""Test for setup failure if connection to broker is missing."""
|
"""Test for setup failure if connection to broker is missing."""
|
||||||
test_broker_cfg = {mqtt.DOMAIN: {mqtt.CONF_BROKER: 'test-broker'}}
|
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={
|
||||||
|
mqtt.CONF_BROKER: 'test-broker'
|
||||||
with mock.patch('homeassistant.components.mqtt.MQTT',
|
})
|
||||||
side_effect=socket.error()):
|
|
||||||
result = yield from async_setup_component(hass, mqtt.DOMAIN,
|
|
||||||
test_broker_cfg)
|
|
||||||
assert not result
|
|
||||||
|
|
||||||
with mock.patch('paho.mqtt.client.Client') as mock_client:
|
with mock.patch('paho.mqtt.client.Client') as mock_client:
|
||||||
mock_client().connect = lambda *args: 1
|
mock_client().connect = lambda *args: 1
|
||||||
result = yield from async_setup_component(hass, mqtt.DOMAIN,
|
assert not await mqtt.async_setup_entry(hass, entry)
|
||||||
test_broker_cfg)
|
|
||||||
assert not result
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_setup_uses_certificate_on_certificate_set_to_auto(
|
||||||
def test_setup_uses_certificate_on_certificate_set_to_auto(hass):
|
hass, mock_MQTT):
|
||||||
"""Test setup uses bundled certs when certificate is set to auto."""
|
"""Test setup uses bundled certs when certificate is set to auto."""
|
||||||
test_broker_cfg = {mqtt.DOMAIN: {mqtt.CONF_BROKER: 'test-broker',
|
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={
|
||||||
'certificate': 'auto'}}
|
mqtt.CONF_BROKER: 'test-broker',
|
||||||
|
'certificate': 'auto'
|
||||||
|
})
|
||||||
|
|
||||||
with mock.patch('homeassistant.components.mqtt.MQTT') as mock_MQTT:
|
assert await mqtt.async_setup_entry(hass, entry)
|
||||||
yield from async_setup_component(hass, mqtt.DOMAIN, test_broker_cfg)
|
|
||||||
|
|
||||||
assert mock_MQTT.called
|
assert mock_MQTT.called
|
||||||
|
|
||||||
import requests.certs
|
import requests.certs
|
||||||
expectedCertificate = requests.certs.where()
|
expectedCertificate = requests.certs.where()
|
||||||
assert mock_MQTT.mock_calls[0][1][7] == expectedCertificate
|
assert mock_MQTT.mock_calls[0][2]['certificate'] == expectedCertificate
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_setup_does_not_use_certificate_on_mqtts_port(hass, mock_MQTT):
|
||||||
def test_setup_does_not_use_certificate_on_mqtts_port(hass):
|
"""Test setup doesn't use bundled certs when ssl set."""
|
||||||
"""Test setup doesn't use bundled certs when certificate is not set."""
|
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={
|
||||||
test_broker_cfg = {mqtt.DOMAIN: {mqtt.CONF_BROKER: 'test-broker',
|
mqtt.CONF_BROKER: 'test-broker',
|
||||||
'port': 8883}}
|
'port': 8883
|
||||||
|
})
|
||||||
|
|
||||||
with mock.patch('homeassistant.components.mqtt.MQTT') as mock_MQTT:
|
assert await mqtt.async_setup_entry(hass, entry)
|
||||||
yield from async_setup_component(hass, mqtt.DOMAIN, test_broker_cfg)
|
|
||||||
|
|
||||||
assert mock_MQTT.called
|
assert mock_MQTT.called
|
||||||
assert mock_MQTT.mock_calls[0][1][2] == 8883
|
assert mock_MQTT.mock_calls[0][2]['port'] == 8883
|
||||||
|
|
||||||
import requests.certs
|
import requests.certs
|
||||||
mqttsCertificateBundle = requests.certs.where()
|
mqttsCertificateBundle = requests.certs.where()
|
||||||
assert mock_MQTT.mock_calls[0][1][7] != mqttsCertificateBundle
|
assert mock_MQTT.mock_calls[0][2]['port'] != mqttsCertificateBundle
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_setup_without_tls_config_uses_tlsv1_under_python36(
|
||||||
def test_setup_without_tls_config_uses_tlsv1_under_python36(hass):
|
hass, mock_MQTT):
|
||||||
"""Test setup defaults to TLSv1 under python3.6."""
|
"""Test setup defaults to TLSv1 under python3.6."""
|
||||||
test_broker_cfg = {mqtt.DOMAIN: {mqtt.CONF_BROKER: 'test-broker'}}
|
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={
|
||||||
|
mqtt.CONF_BROKER: 'test-broker',
|
||||||
|
})
|
||||||
|
|
||||||
with mock.patch('homeassistant.components.mqtt.MQTT') as mock_MQTT:
|
assert await mqtt.async_setup_entry(hass, entry)
|
||||||
yield from async_setup_component(hass, mqtt.DOMAIN, test_broker_cfg)
|
|
||||||
|
|
||||||
assert mock_MQTT.called
|
assert mock_MQTT.called
|
||||||
|
|
||||||
|
@ -600,34 +603,35 @@ def test_setup_without_tls_config_uses_tlsv1_under_python36(hass):
|
||||||
else:
|
else:
|
||||||
expectedTlsVersion = ssl.PROTOCOL_TLSv1
|
expectedTlsVersion = ssl.PROTOCOL_TLSv1
|
||||||
|
|
||||||
assert mock_MQTT.mock_calls[0][1][14] == expectedTlsVersion
|
assert mock_MQTT.mock_calls[0][2]['tls_version'] == expectedTlsVersion
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_setup_with_tls_config_uses_tls_version1_2(hass, mock_MQTT):
|
||||||
def test_setup_with_tls_config_uses_tls_version1_2(hass):
|
|
||||||
"""Test setup uses specified TLS version."""
|
"""Test setup uses specified TLS version."""
|
||||||
test_broker_cfg = {mqtt.DOMAIN: {mqtt.CONF_BROKER: 'test-broker',
|
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={
|
||||||
'tls_version': '1.2'}}
|
mqtt.CONF_BROKER: 'test-broker',
|
||||||
|
'tls_version': '1.2'
|
||||||
|
})
|
||||||
|
|
||||||
with mock.patch('homeassistant.components.mqtt.MQTT') as mock_MQTT:
|
assert await mqtt.async_setup_entry(hass, entry)
|
||||||
yield from async_setup_component(hass, mqtt.DOMAIN, test_broker_cfg)
|
|
||||||
|
|
||||||
assert mock_MQTT.called
|
assert mock_MQTT.called
|
||||||
|
|
||||||
assert mock_MQTT.mock_calls[0][1][14] == ssl.PROTOCOL_TLSv1_2
|
assert mock_MQTT.mock_calls[0][2]['tls_version'] == ssl.PROTOCOL_TLSv1_2
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_setup_with_tls_config_of_v1_under_python36_only_uses_v1(
|
||||||
def test_setup_with_tls_config_of_v1_under_python36_only_uses_v1(hass):
|
hass, mock_MQTT):
|
||||||
"""Test setup uses TLSv1.0 if explicitly chosen."""
|
"""Test setup uses TLSv1.0 if explicitly chosen."""
|
||||||
test_broker_cfg = {mqtt.DOMAIN: {mqtt.CONF_BROKER: 'test-broker',
|
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={
|
||||||
'tls_version': '1.0'}}
|
mqtt.CONF_BROKER: 'test-broker',
|
||||||
|
'tls_version': '1.0'
|
||||||
|
})
|
||||||
|
|
||||||
with mock.patch('homeassistant.components.mqtt.MQTT') as mock_MQTT:
|
assert await mqtt.async_setup_entry(hass, entry)
|
||||||
yield from async_setup_component(hass, mqtt.DOMAIN, test_broker_cfg)
|
|
||||||
|
|
||||||
assert mock_MQTT.called
|
assert mock_MQTT.called
|
||||||
assert mock_MQTT.mock_calls[0][1][14] == ssl.PROTOCOL_TLSv1
|
assert mock_MQTT.mock_calls[0][2]['tls_version'] == ssl.PROTOCOL_TLSv1
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -671,3 +675,8 @@ def test_mqtt_subscribes_topics_on_connect(hass):
|
||||||
}
|
}
|
||||||
calls = {call[1][1]: call[1][2] for call in hass.add_job.mock_calls}
|
calls = {call[1][1]: call[1][2] for call in hass.add_job.mock_calls}
|
||||||
assert calls == expected
|
assert calls == expected
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_fails_without_config(hass):
|
||||||
|
"""Test if the MQTT component fails to load with no config."""
|
||||||
|
assert not await async_setup_component(hass, mqtt.DOMAIN, {})
|
||||||
|
|
|
@ -57,8 +57,8 @@ class TestMQTT:
|
||||||
assert mock_mqtt.called
|
assert mock_mqtt.called
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
pprint(mock_mqtt.mock_calls)
|
pprint(mock_mqtt.mock_calls)
|
||||||
assert mock_mqtt.mock_calls[1][1][5] == 'homeassistant'
|
assert mock_mqtt.mock_calls[1][2]['username'] == 'homeassistant'
|
||||||
assert mock_mqtt.mock_calls[1][1][6] == password
|
assert mock_mqtt.mock_calls[1][2]['password'] == password
|
||||||
|
|
||||||
@patch('passlib.apps.custom_app_context', Mock(return_value=''))
|
@patch('passlib.apps.custom_app_context', Mock(return_value=''))
|
||||||
@patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock()))
|
@patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock()))
|
||||||
|
@ -82,24 +82,8 @@ class TestMQTT:
|
||||||
assert mock_mqtt.called
|
assert mock_mqtt.called
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
pprint(mock_mqtt.mock_calls)
|
pprint(mock_mqtt.mock_calls)
|
||||||
assert mock_mqtt.mock_calls[1][1][5] == 'homeassistant'
|
assert mock_mqtt.mock_calls[1][2]['username'] == 'homeassistant'
|
||||||
assert mock_mqtt.mock_calls[1][1][6] == password
|
assert mock_mqtt.mock_calls[1][2]['password'] == password
|
||||||
|
|
||||||
@patch('passlib.apps.custom_app_context', Mock(return_value=''))
|
|
||||||
@patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock()))
|
|
||||||
@patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock()))
|
|
||||||
@patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro()))
|
|
||||||
@patch('homeassistant.components.mqtt.MQTT')
|
|
||||||
def test_creating_config_without_pass(self, mock_mqtt):
|
|
||||||
"""Test if the MQTT server gets started without password."""
|
|
||||||
mock_mqtt().async_connect.return_value = mock_coro(True)
|
|
||||||
self.hass.bus.listen_once = MagicMock()
|
|
||||||
|
|
||||||
self.hass.config.api = MagicMock(api_password=None)
|
|
||||||
assert setup_component(self.hass, mqtt.DOMAIN, {})
|
|
||||||
assert mock_mqtt.called
|
|
||||||
assert mock_mqtt.mock_calls[1][1][5] is None
|
|
||||||
assert mock_mqtt.mock_calls[1][1][6] is None
|
|
||||||
|
|
||||||
@patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock()))
|
@patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock()))
|
||||||
@patch('hbmqtt.broker.Broker.start', return_value=mock_coro())
|
@patch('hbmqtt.broker.Broker.start', return_value=mock_coro())
|
||||||
|
|
Loading…
Add table
Reference in a new issue