From b616ac706014267d19dccdce7799f13910b6e4ce Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 Mar 2016 16:45:37 -0800 Subject: [PATCH] MQTT: Start embedded server if no config given --- homeassistant/components/mqtt/__init__.py | 51 ++++++-- homeassistant/components/mqtt/server.py | 115 ++++++++++++++++++ requirements_all.txt | 3 + .../{test_mqtt.py => mqtt/test_init.py} | 3 - 4 files changed, 158 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/mqtt/server.py rename tests/components/{test_mqtt.py => mqtt/test_init.py} (98%) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 44b11b3df1bd8..4f233fa9188e9 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -10,11 +10,11 @@ import time +from homeassistant.bootstrap import prepare_setup_platform from homeassistant.config import load_yaml_config_file from homeassistant.exceptions import HomeAssistantError import homeassistant.util as util from homeassistant.helpers import template -from homeassistant.helpers import validate_config from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) @@ -29,6 +29,7 @@ REQUIREMENTS = ['paho-mqtt==1.1'] +CONF_EMBEDDED = 'embedded' CONF_BROKER = 'broker' CONF_PORT = 'port' CONF_CLIENT_ID = 'client_id' @@ -92,21 +93,49 @@ def mqtt_topic_subscriber(event): MQTT_CLIENT.subscribe(topic, qos) +def _setup_server(hass, config): + """Try to start embedded MQTT broker.""" + conf = config.get(DOMAIN, {}) + + # Only setup if embedded config passed in or no broker specified + if CONF_EMBEDDED not in conf and CONF_BROKER in conf: + return None + + server = prepare_setup_platform(hass, config, DOMAIN, 'server') + + if server is None: + _LOGGER.error('Unable to load embedded server.') + return None + + success, broker_config = server.start(hass, conf.get(CONF_EMBEDDED)) + + return success and broker_config + + def setup(hass, config): """Start the MQTT protocol service.""" - if not validate_config(config, {DOMAIN: ['broker']}, _LOGGER): - return False - - conf = config[DOMAIN] + # pylint: disable=too-many-locals + conf = config.get(DOMAIN, {}) - broker = conf[CONF_BROKER] - port = util.convert(conf.get(CONF_PORT), int, DEFAULT_PORT) client_id = util.convert(conf.get(CONF_CLIENT_ID), str) keepalive = util.convert(conf.get(CONF_KEEPALIVE), int, DEFAULT_KEEPALIVE) - username = util.convert(conf.get(CONF_USERNAME), str) - password = util.convert(conf.get(CONF_PASSWORD), str) - certificate = util.convert(conf.get(CONF_CERTIFICATE), str) - protocol = util.convert(conf.get(CONF_PROTOCOL), str, DEFAULT_PROTOCOL) + + broker_config = _setup_server(hass, config) + + # Only auto config if no server config was passed in + if broker_config and CONF_EMBEDDED not in conf: + broker, port, username, password, certificate, protocol = broker_config + elif not broker_config and CONF_BROKER not in conf: + _LOGGER.error('Unable to start broker and auto-configure MQTT.') + return False + + if CONF_BROKER in conf: + broker = conf[CONF_BROKER] + port = util.convert(conf.get(CONF_PORT), int, DEFAULT_PORT) + username = util.convert(conf.get(CONF_USERNAME), str) + password = util.convert(conf.get(CONF_PASSWORD), str) + certificate = util.convert(conf.get(CONF_CERTIFICATE), str) + protocol = util.convert(conf.get(CONF_PROTOCOL), str, DEFAULT_PROTOCOL) if protocol not in (PROTOCOL_31, PROTOCOL_311): _LOGGER.error('Invalid protocol specified: %s. Allowed values: %s, %s', diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py new file mode 100644 index 0000000000000..abb02ac0cc94c --- /dev/null +++ b/homeassistant/components/mqtt/server.py @@ -0,0 +1,115 @@ +"""MQTT server.""" +import asyncio +import logging +import tempfile +import threading + +from homeassistant.components.mqtt import PROTOCOL_311 +from homeassistant.const import EVENT_HOMEASSISTANT_STOP + +REQUIREMENTS = ['hbmqtt==0.6.2'] +DEPENDENCIES = ['http'] + + +@asyncio.coroutine +def broker_coro(loop, config): + """Start broker coroutine.""" + from hbmqtt.broker import Broker + broker = Broker(config, loop) + yield from broker.start() + return broker + + +def loop_run(loop, broker, shutdown_complete): + """Run broker and clean up when done.""" + # Temp workaround because hbmqtt does not respect passed in loop everywhere + # https://github.com/beerfactory/hbmqtt/issues/22 + asyncio.set_event_loop(loop) + loop.run_forever() + # run_forever ends when stop is called because we're shutting down + loop.run_until_complete(broker.shutdown()) + loop.close() + shutdown_complete.set() + + +def start(hass, server_config=None, http_config=None): + """Initialize MQTT Server.""" + from hbmqtt.broker import BrokerException + + loop = asyncio.new_event_loop() + # Workaround !! + asyncio.set_event_loop(loop) + + try: + passwd = tempfile.NamedTemporaryFile() + + if server_config is None: + server_config, client_config = generate_config(hass, http_config, + passwd) + else: + client_config = None + + start_server = asyncio.gather(broker_coro(loop, server_config), + loop=loop) + loop.run_until_complete(start_server) + # Result raises exception if one was raised during startup + broker = start_server.result()[0] + except BrokerException: + logging.getLogger(__name__).exception('Error initializing MQTT server') + loop.close() + return False, None + finally: + passwd.close() + + shutdown_complete = threading.Event() + + def shutdown(event): + """Gracefully shutdown MQTT broker.""" + loop.call_soon_threadsafe(loop.stop) + shutdown_complete.wait() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) + + threading.Thread(target=loop_run, args=(loop, broker, shutdown_complete), + daemon=True, name="MQTT-server").start() + + return True, client_config + + +def generate_config(hass, http_config, passwd): + """Generate a configuration based on current Home Assistant instance.""" + config = { + 'listeners': { + 'default': { + 'type': 'tcp', + 'bind': '0.0.0.0:1883', + }, + }, + 'auth': { + 'allow-anonymous': hass.config.api.api_password is None + }, + 'plugins': ['auth_anonymous'], + } + + if hass.config.api.api_password: + username = 'homeassistant' + password = hass.config.api.api_password + + # Encrypt with what hbmqtt uses to verify + from passlib.apps import custom_app_context + + passwd.write( + 'homeassistant:{}\n'.format( + custom_app_context.encrypt( + hass.config.api.api_password)).encode('utf-8')) + passwd.flush() + + config['auth']['password-file'] = passwd.name + config['plugins'].append('auth_file') + else: + username = None + password = None + + client_config = ('localhost', 1883, username, password, None, PROTOCOL_311) + + return config, client_config diff --git a/requirements_all.txt b/requirements_all.txt index 6bc16e54eb557..c8f01a33f5ad5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -54,6 +54,9 @@ freesms==0.1.0 # homeassistant.components.conversation fuzzywuzzy==0.8.0 +# homeassistant.components.mqtt.server +hbmqtt==0.6.2 + # homeassistant.components.thermostat.heatmiser heatmiserV3==0.9.1 diff --git a/tests/components/test_mqtt.py b/tests/components/mqtt/test_init.py similarity index 98% rename from tests/components/test_mqtt.py rename to tests/components/mqtt/test_init.py index 9ca51c687bcdb..9be9b7f1d93ae 100644 --- a/tests/components/test_mqtt.py +++ b/tests/components/mqtt/test_init.py @@ -45,9 +45,6 @@ def test_client_stops_on_home_assistant_start(self): self.hass.pool.block_till_done() self.assertTrue(mqtt.MQTT_CLIENT.stop.called) - def test_setup_fails_if_no_broker_config(self): - self.assertFalse(mqtt.setup(self.hass, {mqtt.DOMAIN: {}})) - def test_setup_fails_if_no_connect_broker(self): with mock.patch('homeassistant.components.mqtt.MQTT', side_effect=socket.error()):