2017-04-30 05:04:49 +00:00
|
|
|
"""Provide methods to bootstrap a Home Assistant instance."""
|
2016-10-27 07:16:23 +00:00
|
|
|
import asyncio
|
2013-10-22 05:06:22 +00:00
|
|
|
import logging
|
2015-09-04 22:22:42 +00:00
|
|
|
import logging.handlers
|
2015-11-15 10:05:46 +00:00
|
|
|
import os
|
|
|
|
import sys
|
2017-03-01 04:33:19 +00:00
|
|
|
from time import time
|
2016-11-19 16:18:33 +00:00
|
|
|
from collections import OrderedDict
|
2016-07-28 03:33:49 +00:00
|
|
|
|
2016-07-21 05:38:52 +00:00
|
|
|
from typing import Any, Optional, Dict
|
2013-10-13 17:42:22 +00:00
|
|
|
|
2016-03-28 01:48:51 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2017-10-08 15:17:54 +00:00
|
|
|
from homeassistant import (
|
2018-05-01 18:57:30 +00:00
|
|
|
core, config as conf_util, config_entries, components as core_components)
|
2016-09-07 13:59:16 +00:00
|
|
|
from homeassistant.components import persistent_notification
|
2017-02-13 05:24:07 +00:00
|
|
|
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
2017-03-05 09:41:54 +00:00
|
|
|
from homeassistant.setup import async_setup_component
|
2016-12-16 23:51:06 +00:00
|
|
|
from homeassistant.util.logging import AsyncHandler
|
2017-07-14 02:26:21 +00:00
|
|
|
from homeassistant.util.package import async_get_user_site, get_user_site
|
2016-08-20 19:39:56 +00:00
|
|
|
from homeassistant.util.yaml import clear_secret_cache
|
2016-04-09 22:25:01 +00:00
|
|
|
from homeassistant.exceptions import HomeAssistantError
|
2017-02-09 05:58:45 +00:00
|
|
|
from homeassistant.helpers.signal import async_register_signal_handling
|
2014-01-24 05:34:08 +00:00
|
|
|
|
2015-01-09 08:07:58 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2015-11-07 09:44:02 +00:00
|
|
|
ERROR_LOG_FILENAME = 'home-assistant.log'
|
2017-09-16 05:25:32 +00:00
|
|
|
|
|
|
|
# hass.data key for logging information.
|
|
|
|
DATA_LOGGING = 'logging'
|
|
|
|
|
2017-03-01 04:33:19 +00:00
|
|
|
FIRST_INIT_COMPONENT = set((
|
2017-11-15 04:35:56 +00:00
|
|
|
'system_log', 'recorder', 'mqtt', 'mqtt_eventstream', 'logger',
|
|
|
|
'introduction', 'frontend', 'history'))
|
2015-05-12 05:23:20 +00:00
|
|
|
|
2015-01-09 08:07:58 +00:00
|
|
|
|
2016-07-21 05:38:52 +00:00
|
|
|
def from_config_dict(config: Dict[str, Any],
|
2018-02-11 17:20:28 +00:00
|
|
|
hass: Optional[core.HomeAssistant] = None,
|
|
|
|
config_dir: Optional[str] = None,
|
|
|
|
enable_log: bool = True,
|
|
|
|
verbose: bool = False,
|
|
|
|
skip_pip: bool = False,
|
|
|
|
log_rotate_days: Any = None,
|
2018-04-18 14:18:44 +00:00
|
|
|
log_file: Any = None,
|
|
|
|
log_no_color: bool = False) \
|
2016-07-21 05:38:52 +00:00
|
|
|
-> Optional[core.HomeAssistant]:
|
2017-07-18 14:23:57 +00:00
|
|
|
"""Try to configure Home Assistant from a configuration dictionary.
|
2014-04-24 07:40:45 +00:00
|
|
|
|
2014-08-13 12:28:45 +00:00
|
|
|
Dynamically loads required components and its dependencies.
|
|
|
|
"""
|
|
|
|
if hass is None:
|
2015-08-17 03:44:46 +00:00
|
|
|
hass = core.HomeAssistant()
|
2015-08-30 01:11:24 +00:00
|
|
|
if config_dir is not None:
|
2015-08-30 02:19:52 +00:00
|
|
|
config_dir = os.path.abspath(config_dir)
|
|
|
|
hass.config.config_dir = config_dir
|
2017-07-14 02:26:21 +00:00
|
|
|
hass.loop.run_until_complete(
|
|
|
|
async_mount_local_lib_path(config_dir, hass.loop))
|
2014-04-24 07:40:45 +00:00
|
|
|
|
2016-10-27 07:16:23 +00:00
|
|
|
# run task
|
2017-03-01 04:33:19 +00:00
|
|
|
hass = hass.loop.run_until_complete(
|
|
|
|
async_from_config_dict(
|
|
|
|
config, hass, config_dir, enable_log, verbose, skip_pip,
|
2018-04-18 14:18:44 +00:00
|
|
|
log_rotate_days, log_file, log_no_color)
|
2017-03-01 04:33:19 +00:00
|
|
|
)
|
2016-10-27 07:16:23 +00:00
|
|
|
|
2017-03-01 04:33:19 +00:00
|
|
|
return hass
|
2016-10-27 07:16:23 +00:00
|
|
|
|
|
|
|
|
2018-04-28 23:26:20 +00:00
|
|
|
async def async_from_config_dict(config: Dict[str, Any],
|
|
|
|
hass: core.HomeAssistant,
|
|
|
|
config_dir: Optional[str] = None,
|
|
|
|
enable_log: bool = True,
|
|
|
|
verbose: bool = False,
|
|
|
|
skip_pip: bool = False,
|
|
|
|
log_rotate_days: Any = None,
|
|
|
|
log_file: Any = None,
|
|
|
|
log_no_color: bool = False) \
|
2016-10-27 07:16:23 +00:00
|
|
|
-> Optional[core.HomeAssistant]:
|
2017-07-18 14:23:57 +00:00
|
|
|
"""Try to configure Home Assistant from a configuration dictionary.
|
2016-10-27 07:16:23 +00:00
|
|
|
|
|
|
|
Dynamically loads required components and its dependencies.
|
|
|
|
This method is a coroutine.
|
|
|
|
"""
|
2017-03-01 04:33:19 +00:00
|
|
|
start = time()
|
2017-10-06 04:47:51 +00:00
|
|
|
|
|
|
|
if enable_log:
|
2018-04-18 14:18:44 +00:00
|
|
|
async_enable_logging(hass, verbose, log_rotate_days, log_file,
|
|
|
|
log_no_color)
|
2017-10-06 04:47:51 +00:00
|
|
|
|
2016-05-08 05:24:04 +00:00
|
|
|
core_config = config.get(core.DOMAIN, {})
|
|
|
|
|
2016-03-28 01:48:51 +00:00
|
|
|
try:
|
2018-04-28 23:26:20 +00:00
|
|
|
await conf_util.async_process_ha_core_config(hass, core_config)
|
2016-06-27 16:02:45 +00:00
|
|
|
except vol.Invalid as ex:
|
2017-03-01 04:33:19 +00:00
|
|
|
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
|
2016-03-28 01:48:51 +00:00
|
|
|
return None
|
|
|
|
|
2018-04-28 23:26:20 +00:00
|
|
|
await hass.async_add_job(conf_util.process_ha_config_upgrade, hass)
|
2015-03-19 06:02:58 +00:00
|
|
|
|
2015-09-04 21:50:57 +00:00
|
|
|
hass.config.skip_pip = skip_pip
|
|
|
|
if skip_pip:
|
2017-07-18 14:23:57 +00:00
|
|
|
_LOGGER.warning("Skipping pip installation of required modules. "
|
|
|
|
"This may cause issues")
|
2015-09-04 21:50:57 +00:00
|
|
|
|
2018-03-09 03:34:24 +00:00
|
|
|
# Make a copy because we are mutating it.
|
2018-03-10 18:02:04 +00:00
|
|
|
config = OrderedDict(config)
|
2018-03-09 03:34:24 +00:00
|
|
|
|
2017-01-14 06:01:47 +00:00
|
|
|
# Merge packages
|
|
|
|
conf_util.merge_packages_config(
|
2018-05-01 18:57:30 +00:00
|
|
|
hass, config, core_config.get(conf_util.CONF_PACKAGES, {}))
|
2017-01-14 06:01:47 +00:00
|
|
|
|
2018-03-10 18:02:04 +00:00
|
|
|
# Ensure we have no None values after merge
|
|
|
|
for key, value in config.items():
|
|
|
|
if not value:
|
|
|
|
config[key] = {}
|
|
|
|
|
2018-02-16 22:07:38 +00:00
|
|
|
hass.config_entries = config_entries.ConfigEntries(hass, config)
|
2018-04-28 23:26:20 +00:00
|
|
|
await hass.config_entries.async_load()
|
2018-02-16 22:07:38 +00:00
|
|
|
|
2014-12-07 07:57:02 +00:00
|
|
|
# Filter out the repeating and common config section [homeassistant]
|
2015-09-29 06:09:05 +00:00
|
|
|
components = set(key.split(' ')[0] for key in config.keys()
|
|
|
|
if key != core.DOMAIN)
|
2018-02-16 22:07:38 +00:00
|
|
|
components.update(hass.config_entries.async_domains())
|
2014-10-22 15:12:32 +00:00
|
|
|
|
2016-10-27 07:16:23 +00:00
|
|
|
# setup components
|
|
|
|
# pylint: disable=not-an-iterable
|
2018-04-28 23:26:20 +00:00
|
|
|
res = await core_components.async_setup(hass, config)
|
2016-10-27 07:16:23 +00:00
|
|
|
if not res:
|
2017-07-18 14:23:57 +00:00
|
|
|
_LOGGER.error("Home Assistant core failed to initialize. "
|
|
|
|
"further initialization aborted")
|
2016-10-27 07:16:23 +00:00
|
|
|
return hass
|
2016-06-25 23:40:33 +00:00
|
|
|
|
2018-04-28 23:26:20 +00:00
|
|
|
await persistent_notification.async_setup(hass, config)
|
2014-12-17 05:46:02 +00:00
|
|
|
|
2017-07-18 14:23:57 +00:00
|
|
|
_LOGGER.info("Home Assistant core initialized")
|
2016-01-24 22:46:05 +00:00
|
|
|
|
2017-03-01 04:33:19 +00:00
|
|
|
# stage 1
|
|
|
|
for component in components:
|
|
|
|
if component not in FIRST_INIT_COMPONENT:
|
|
|
|
continue
|
|
|
|
hass.async_add_job(async_setup_component(hass, component, config))
|
2017-02-18 19:31:37 +00:00
|
|
|
|
2018-04-28 23:26:20 +00:00
|
|
|
await hass.async_block_till_done()
|
2016-10-13 16:09:07 +00:00
|
|
|
|
2017-03-01 04:33:19 +00:00
|
|
|
# stage 2
|
|
|
|
for component in components:
|
|
|
|
if component in FIRST_INIT_COMPONENT:
|
|
|
|
continue
|
|
|
|
hass.async_add_job(async_setup_component(hass, component, config))
|
2016-11-19 16:18:33 +00:00
|
|
|
|
2018-04-28 23:26:20 +00:00
|
|
|
await hass.async_block_till_done()
|
2016-11-30 21:02:45 +00:00
|
|
|
|
2017-03-01 04:33:19 +00:00
|
|
|
stop = time()
|
2017-07-18 14:23:57 +00:00
|
|
|
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
|
2017-03-01 04:33:19 +00:00
|
|
|
|
2017-02-09 05:58:45 +00:00
|
|
|
async_register_signal_handling(hass)
|
2014-08-13 12:28:45 +00:00
|
|
|
return hass
|
2013-10-22 05:06:22 +00:00
|
|
|
|
2014-01-24 01:46:29 +00:00
|
|
|
|
2016-07-21 05:38:52 +00:00
|
|
|
def from_config_file(config_path: str,
|
2018-02-11 17:20:28 +00:00
|
|
|
hass: Optional[core.HomeAssistant] = None,
|
|
|
|
verbose: bool = False,
|
|
|
|
skip_pip: bool = True,
|
|
|
|
log_rotate_days: Any = None,
|
2018-04-18 14:18:44 +00:00
|
|
|
log_file: Any = None,
|
|
|
|
log_no_color: bool = False):
|
2016-03-07 23:06:04 +00:00
|
|
|
"""Read the configuration file and try to start all the functionality.
|
|
|
|
|
|
|
|
Will add functionality to 'hass' parameter if given,
|
2014-08-13 12:28:45 +00:00
|
|
|
instantiates a new Home Assistant object if 'hass' is not given.
|
|
|
|
"""
|
2014-09-21 02:19:39 +00:00
|
|
|
if hass is None:
|
2015-08-17 03:44:46 +00:00
|
|
|
hass = core.HomeAssistant()
|
2014-09-21 02:19:39 +00:00
|
|
|
|
2016-10-27 07:16:23 +00:00
|
|
|
# run task
|
2017-03-01 04:33:19 +00:00
|
|
|
hass = hass.loop.run_until_complete(
|
|
|
|
async_from_config_file(
|
2018-04-18 14:18:44 +00:00
|
|
|
config_path, hass, verbose, skip_pip,
|
|
|
|
log_rotate_days, log_file, log_no_color)
|
2017-03-01 04:33:19 +00:00
|
|
|
)
|
2016-10-27 07:16:23 +00:00
|
|
|
|
2017-03-01 04:33:19 +00:00
|
|
|
return hass
|
2016-10-27 07:16:23 +00:00
|
|
|
|
|
|
|
|
2018-04-28 23:26:20 +00:00
|
|
|
async def async_from_config_file(config_path: str,
|
|
|
|
hass: core.HomeAssistant,
|
|
|
|
verbose: bool = False,
|
|
|
|
skip_pip: bool = True,
|
|
|
|
log_rotate_days: Any = None,
|
|
|
|
log_file: Any = None,
|
|
|
|
log_no_color: bool = False):
|
2016-10-27 07:16:23 +00:00
|
|
|
"""Read the configuration file and try to start all the functionality.
|
|
|
|
|
|
|
|
Will add functionality to 'hass' parameter.
|
|
|
|
This method is a coroutine.
|
|
|
|
"""
|
2015-03-19 06:02:58 +00:00
|
|
|
# Set config dir to directory holding config file
|
2015-08-30 02:19:52 +00:00
|
|
|
config_dir = os.path.abspath(os.path.dirname(config_path))
|
|
|
|
hass.config.config_dir = config_dir
|
2018-04-28 23:26:20 +00:00
|
|
|
await async_mount_local_lib_path(config_dir, hass.loop)
|
2014-09-21 02:19:39 +00:00
|
|
|
|
2018-04-18 14:18:44 +00:00
|
|
|
async_enable_logging(hass, verbose, log_rotate_days, log_file,
|
|
|
|
log_no_color)
|
2015-08-30 06:02:07 +00:00
|
|
|
|
2016-04-09 22:25:01 +00:00
|
|
|
try:
|
2018-04-28 23:26:20 +00:00
|
|
|
config_dict = await hass.async_add_job(
|
2017-05-26 15:28:07 +00:00
|
|
|
conf_util.load_yaml_config_file, config_path)
|
2017-03-01 04:56:23 +00:00
|
|
|
except HomeAssistantError as err:
|
2017-07-18 14:23:57 +00:00
|
|
|
_LOGGER.error("Error loading %s: %s", config_path, err)
|
2016-04-09 22:25:01 +00:00
|
|
|
return None
|
2016-08-20 19:39:56 +00:00
|
|
|
finally:
|
|
|
|
clear_secret_cache()
|
2013-10-13 17:42:22 +00:00
|
|
|
|
2018-04-28 23:26:20 +00:00
|
|
|
hass = await async_from_config_dict(
|
2016-10-27 07:16:23 +00:00
|
|
|
config_dict, hass, enable_log=False, skip_pip=skip_pip)
|
|
|
|
return hass
|
2015-01-18 06:23:07 +00:00
|
|
|
|
|
|
|
|
2017-02-13 05:24:07 +00:00
|
|
|
@core.callback
|
2018-04-18 14:18:44 +00:00
|
|
|
def async_enable_logging(hass: core.HomeAssistant,
|
|
|
|
verbose: bool = False,
|
|
|
|
log_rotate_days=None,
|
|
|
|
log_file=None,
|
|
|
|
log_no_color: bool = False) -> None:
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Set up the logging.
|
2016-10-27 07:16:23 +00:00
|
|
|
|
2017-02-13 05:24:07 +00:00
|
|
|
This method must be run in the event loop.
|
2016-10-27 07:16:23 +00:00
|
|
|
"""
|
2017-01-20 05:31:44 +00:00
|
|
|
fmt = ("%(asctime)s %(levelname)s (%(threadName)s) "
|
|
|
|
"[%(name)s] %(message)s")
|
2017-04-27 16:30:34 +00:00
|
|
|
datefmt = '%Y-%m-%d %H:%M:%S'
|
2016-10-17 19:14:10 +00:00
|
|
|
|
2018-04-18 14:18:44 +00:00
|
|
|
if not log_no_color:
|
|
|
|
try:
|
|
|
|
from colorlog import ColoredFormatter
|
|
|
|
# basicConfig must be called after importing colorlog in order to
|
|
|
|
# ensure that the handlers it sets up wraps the correct streams.
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
|
|
|
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
|
|
|
|
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
|
|
|
|
colorfmt,
|
|
|
|
datefmt=datefmt,
|
|
|
|
reset=True,
|
|
|
|
log_colors={
|
|
|
|
'DEBUG': 'cyan',
|
|
|
|
'INFO': 'green',
|
|
|
|
'WARNING': 'yellow',
|
|
|
|
'ERROR': 'red',
|
|
|
|
'CRITICAL': 'red',
|
|
|
|
}
|
|
|
|
))
|
|
|
|
except ImportError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
# If the above initialization failed for any reason, setup the default
|
|
|
|
# formatting. If the above succeeds, this wil result in a no-op.
|
|
|
|
logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO)
|
|
|
|
|
2017-04-30 05:04:49 +00:00
|
|
|
# Suppress overly verbose logs from libraries that aren't helpful
|
|
|
|
logging.getLogger('requests').setLevel(logging.WARNING)
|
|
|
|
logging.getLogger('urllib3').setLevel(logging.WARNING)
|
|
|
|
logging.getLogger('aiohttp.access').setLevel(logging.WARNING)
|
2016-10-17 19:14:10 +00:00
|
|
|
|
2015-01-18 06:23:07 +00:00
|
|
|
# Log errors to a file if we have write access to file or config dir
|
2017-09-14 04:22:42 +00:00
|
|
|
if log_file is None:
|
|
|
|
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
|
|
|
else:
|
|
|
|
err_log_path = os.path.abspath(log_file)
|
|
|
|
|
2015-01-18 06:23:07 +00:00
|
|
|
err_path_exists = os.path.isfile(err_log_path)
|
2017-09-14 04:22:42 +00:00
|
|
|
err_dir = os.path.dirname(err_log_path)
|
2015-01-18 06:23:07 +00:00
|
|
|
|
|
|
|
# Check if we can write to the error log if it exists or that
|
|
|
|
# we can create files in the containing directory if not.
|
|
|
|
if (err_path_exists and os.access(err_log_path, os.W_OK)) or \
|
2017-09-14 04:22:42 +00:00
|
|
|
(not err_path_exists and os.access(err_dir, os.W_OK)):
|
2015-01-18 06:23:07 +00:00
|
|
|
|
2015-09-04 22:22:42 +00:00
|
|
|
if log_rotate_days:
|
|
|
|
err_handler = logging.handlers.TimedRotatingFileHandler(
|
|
|
|
err_log_path, when='midnight', backupCount=log_rotate_days)
|
|
|
|
else:
|
|
|
|
err_handler = logging.FileHandler(
|
|
|
|
err_log_path, mode='w', delay=True)
|
2015-01-18 06:23:07 +00:00
|
|
|
|
2015-09-01 06:12:00 +00:00
|
|
|
err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
|
2017-01-20 05:31:44 +00:00
|
|
|
err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt))
|
2016-12-16 23:51:06 +00:00
|
|
|
|
|
|
|
async_handler = AsyncHandler(hass.loop, err_handler)
|
2017-02-13 05:24:07 +00:00
|
|
|
|
2018-04-28 23:26:20 +00:00
|
|
|
async def async_stop_async_handler(event):
|
2017-02-13 05:24:07 +00:00
|
|
|
"""Cleanup async handler."""
|
|
|
|
logging.getLogger('').removeHandler(async_handler)
|
2018-04-28 23:26:20 +00:00
|
|
|
await async_handler.async_close(blocking=True)
|
2017-02-13 05:24:07 +00:00
|
|
|
|
|
|
|
hass.bus.async_listen_once(
|
|
|
|
EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
|
2016-12-16 23:51:06 +00:00
|
|
|
|
2015-09-01 06:12:00 +00:00
|
|
|
logger = logging.getLogger('')
|
2016-12-16 23:51:06 +00:00
|
|
|
logger.addHandler(async_handler)
|
2015-12-23 02:39:46 +00:00
|
|
|
logger.setLevel(logging.INFO)
|
2015-01-18 06:23:07 +00:00
|
|
|
|
2017-09-16 05:25:32 +00:00
|
|
|
# Save the log file location for access by other components.
|
|
|
|
hass.data[DATA_LOGGING] = err_log_path
|
2015-01-18 06:23:07 +00:00
|
|
|
else:
|
|
|
|
_LOGGER.error(
|
2017-04-30 05:04:49 +00:00
|
|
|
"Unable to setup error log %s (access denied)", err_log_path)
|
2015-01-30 07:56:04 +00:00
|
|
|
|
2015-01-30 16:26:06 +00:00
|
|
|
|
2016-08-10 06:54:34 +00:00
|
|
|
def mount_local_lib_path(config_dir: str) -> str:
|
2017-07-14 02:26:21 +00:00
|
|
|
"""Add local library to Python Path."""
|
|
|
|
deps_dir = os.path.join(config_dir, 'deps')
|
|
|
|
lib_dir = get_user_site(deps_dir)
|
|
|
|
if lib_dir not in sys.path:
|
|
|
|
sys.path.insert(0, lib_dir)
|
|
|
|
return deps_dir
|
|
|
|
|
|
|
|
|
2018-04-28 23:26:20 +00:00
|
|
|
async def async_mount_local_lib_path(config_dir: str,
|
|
|
|
loop: asyncio.AbstractEventLoop) -> str:
|
2016-10-27 07:16:23 +00:00
|
|
|
"""Add local library to Python Path.
|
|
|
|
|
2017-07-14 02:26:21 +00:00
|
|
|
This function is a coroutine.
|
2016-10-27 07:16:23 +00:00
|
|
|
"""
|
2016-08-10 06:54:34 +00:00
|
|
|
deps_dir = os.path.join(config_dir, 'deps')
|
2018-04-28 23:26:20 +00:00
|
|
|
lib_dir = await async_get_user_site(deps_dir, loop=loop)
|
2017-07-14 02:26:21 +00:00
|
|
|
if lib_dir not in sys.path:
|
|
|
|
sys.path.insert(0, lib_dir)
|
2016-08-10 06:54:34 +00:00
|
|
|
return deps_dir
|