core/homeassistant/bootstrap.py

524 lines
17 KiB
Python
Raw Normal View History

2016-02-20 07:20:14 +00:00
"""Provides methods to bootstrap a home assistant instance."""
import asyncio
import logging
import logging.handlers
import os
import sys
from time import time
from collections import OrderedDict
from types import ModuleType
from typing import Any, Optional, Dict
2016-03-28 01:48:51 +00:00
import voluptuous as vol
2016-02-19 05:27:50 +00:00
import homeassistant.components as core_components
from homeassistant.components import persistent_notification
import homeassistant.config as conf_util
from homeassistant.config import async_notify_setup_error
2015-08-17 03:44:46 +00:00
import homeassistant.core as core
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
2016-02-19 05:27:50 +00:00
import homeassistant.loader as loader
import homeassistant.util.package as pkg_util
from homeassistant.util.async import run_coroutine_threadsafe
from homeassistant.util.logging import AsyncHandler
from homeassistant.util.yaml import clear_secret_cache
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import event_decorators, service
from homeassistant.helpers.signal import async_register_signal_handling
2014-01-24 05:34:08 +00:00
_LOGGER = logging.getLogger(__name__)
2015-05-12 05:23:38 +00:00
ATTR_COMPONENT = 'component'
DATA_SETUP = 'setup_tasks'
DATA_PIP_LOCK = 'pip_lock'
2015-11-07 09:44:02 +00:00
ERROR_LOG_FILENAME = 'home-assistant.log'
FIRST_INIT_COMPONENT = set((
'recorder', 'mqtt', 'mqtt_eventstream', 'logger', 'introduction'))
def setup_component(hass: core.HomeAssistant, domain: str,
config: Optional[Dict]=None) -> bool:
2016-03-07 23:06:04 +00:00
"""Setup a component and all its dependencies."""
return run_coroutine_threadsafe(
async_setup_component(hass, domain, config), loop=hass.loop).result()
@asyncio.coroutine
def async_setup_component(hass: core.HomeAssistant, domain: str,
config: Optional[Dict]=None) -> bool:
"""Setup a component and all its dependencies.
This method is a coroutine.
"""
setup_tasks = hass.data.get(DATA_SETUP)
if setup_tasks is not None and domain in setup_tasks:
return (yield from setup_tasks[domain])
if config is None:
config = {}
if setup_tasks is None:
setup_tasks = hass.data[DATA_SETUP] = {}
task = setup_tasks[domain] = hass.async_add_job(
_async_setup_component(hass, domain, config))
return (yield from task)
@asyncio.coroutine
def _async_process_requirements(hass: core.HomeAssistant, name: str,
requirements) -> bool:
"""Install the requirements for a component.
This method is a coroutine.
"""
if hass.config.skip_pip:
2015-08-04 20:21:09 +00:00
return True
pip_lock = hass.data.get(DATA_PIP_LOCK)
if pip_lock is None:
pip_lock = hass.data[DATA_PIP_LOCK] = asyncio.Lock(loop=hass.loop)
def pip_install(mod):
"""Install packages."""
return pkg_util.install_package(mod, target=hass.config.path('deps'))
with (yield from pip_lock):
for req in requirements:
ret = yield from hass.loop.run_in_executor(None, pip_install, req)
if not ret:
_LOGGER.error('Not initializing %s because could not install '
'dependency %s', name, req)
async_notify_setup_error(hass, name)
return False
2015-08-04 20:21:09 +00:00
return True
@asyncio.coroutine
def _async_process_dependencies(hass, config, name, dependencies):
"""Ensure all dependencies are set up."""
blacklisted = [dep for dep in dependencies
if dep in loader.DEPENDENCY_BLACKLIST]
if blacklisted:
_LOGGER.error('Unable to setup dependencies of %s: '
'found blacklisted dependencies: %s',
name, ', '.join(blacklisted))
return False
tasks = [async_setup_component(hass, dep, config) for dep
in dependencies]
if not tasks:
return True
results = yield from asyncio.gather(*tasks, loop=hass.loop)
failed = [dependencies[idx] for idx, res
in enumerate(results) if not res]
if failed:
_LOGGER.error('Unable to setup dependencies of %s. '
'Setup failed for dependencies: %s',
name, ', '.join(failed))
return False
return True
@asyncio.coroutine
def _async_setup_component(hass: core.HomeAssistant,
domain: str, config) -> bool:
"""Setup a component for Home Assistant.
This method is a coroutine.
hass: Home Assistant instance.
domain: Domain of component to setup.
config: The Home Assistant configuration.
"""
def log_error(msg):
"""Log helper."""
_LOGGER.error('Setup failed for %s: %s', domain, msg)
async_notify_setup_error(hass, domain, True)
2015-08-03 15:05:33 +00:00
# Validate no circular dependencies
components = loader.load_order_component(domain)
2016-02-20 07:20:14 +00:00
# OrderedSet is empty if component or dependencies could not be resolved
if not components:
log_error('Unable to resolve component or dependencies')
return False
2016-02-20 07:20:14 +00:00
component = loader.get_component(domain)
processed_config = \
conf_util.async_process_component_config(hass, config, domain)
if processed_config is None:
log_error('Invalid config')
return False
if not hass.config.skip_pip and hasattr(component, 'REQUIREMENTS'):
req_success = yield from _async_process_requirements(
hass, domain, component.REQUIREMENTS)
if not req_success:
log_error('Could not install all requirements.')
return False
if hasattr(component, 'DEPENDENCIES'):
dep_success = yield from _async_process_dependencies(
hass, config, domain, component.DEPENDENCIES)
if not dep_success:
log_error('Could not setup all dependencies.')
return False
async_comp = hasattr(component, 'async_setup')
try:
_LOGGER.info("Setting up %s", domain)
if async_comp:
result = yield from component.async_setup(hass, processed_config)
else:
result = yield from hass.loop.run_in_executor(
None, component.setup, hass, processed_config)
except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error during setup of component %s', domain)
async_notify_setup_error(hass, domain, True)
return False
if result is False:
log_error('Component failed to initialize.')
return False
elif result is not True:
log_error('Component did not return boolean if setup was successful. '
'Disabling component.')
loader.set_component(domain, None)
return False
hass.config.components.add(component.DOMAIN)
hass.bus.async_fire(
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}
)
return True
@asyncio.coroutine
def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
platform_name: str) \
-> Optional[ModuleType]:
"""Load a platform and makes sure dependencies are setup.
This method is a coroutine.
"""
platform_path = PLATFORM_FORMAT.format(domain, platform_name)
def log_error(msg):
"""Log helper."""
_LOGGER.error('Unable to prepare setup for platform %s: %s',
platform_path, msg)
async_notify_setup_error(hass, platform_path)
platform = loader.get_platform(domain, platform_name)
# Not found
if platform is None:
log_error('Unable to find platform')
return None
# Already loaded
elif platform_path in hass.config.components:
return platform
# Load dependencies
if hasattr(platform, 'DEPENDENCIES'):
dep_success = yield from _async_process_dependencies(
hass, config, platform_path, platform.DEPENDENCIES)
if not dep_success:
log_error('Could not setup all dependencies.')
return False
if not hass.config.skip_pip and hasattr(platform, 'REQUIREMENTS'):
req_success = yield from _async_process_requirements(
hass, platform_path, platform.REQUIREMENTS)
if not req_success:
log_error('Could not install all requirements.')
return None
return platform
def from_config_dict(config: Dict[str, Any],
2016-08-09 03:42:25 +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) \
-> Optional[core.HomeAssistant]:
2016-03-07 23:06:04 +00:00
"""Try to configure Home Assistant from a config dict.
Dynamically loads required components and its dependencies.
"""
if hass is None:
2015-08-17 03:44:46 +00:00
hass = core.HomeAssistant()
if config_dir is not None:
config_dir = os.path.abspath(config_dir)
hass.config.config_dir = config_dir
mount_local_lib_path(config_dir)
# run task
hass = hass.loop.run_until_complete(
async_from_config_dict(
config, hass, config_dir, enable_log, verbose, skip_pip,
log_rotate_days)
)
return hass
@asyncio.coroutine
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) \
-> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a config dict.
Dynamically loads required components and its dependencies.
This method is a coroutine.
"""
start = time()
hass.async_track_tasks()
core_config = config.get(core.DOMAIN, {})
2016-03-28 01:48:51 +00:00
try:
yield from conf_util.async_process_ha_core_config(hass, core_config)
except vol.Invalid as ex:
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
2016-03-28 01:48:51 +00:00
return None
yield from hass.loop.run_in_executor(
None, conf_util.process_ha_config_upgrade, hass)
2015-08-30 06:02:07 +00:00
if enable_log:
async_enable_logging(hass, verbose, log_rotate_days)
hass.config.skip_pip = skip_pip
if skip_pip:
_LOGGER.warning('Skipping pip installation of required modules. '
'This may cause issues.')
if not loader.PREPARED:
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
2014-11-28 23:34:42 +00:00
# Merge packages
conf_util.merge_packages_config(
config, core_config.get(conf_util.CONF_PACKAGES, {}))
# Make a copy because we are mutating it.
# Use OrderedDict in case original one was one.
# Convert values to dictionaries if they are None
new_config = OrderedDict()
for key, value in config.items():
new_config[key] = value or {}
config = new_config
# 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)
# setup components
# pylint: disable=not-an-iterable
res = yield from core_components.async_setup(hass, config)
if not res:
_LOGGER.error('Home Assistant core failed to initialize. '
'Further initialization aborted.')
return hass
yield from persistent_notification.async_setup(hass, config)
_LOGGER.info('Home Assistant core initialized')
# Give event decorators access to HASS
event_decorators.HASS = hass
service.HASS = hass
# stage 1
for component in components:
if component not in FIRST_INIT_COMPONENT:
continue
hass.async_add_job(async_setup_component(hass, component, config))
yield from hass.async_block_till_done()
# stage 2
for component in components:
if component in FIRST_INIT_COMPONENT:
continue
hass.async_add_job(async_setup_component(hass, component, config))
yield from hass.async_stop_track_tasks()
stop = time()
_LOGGER.info('Home Assistant initialized in %ss', round(stop-start, 2))
async_register_signal_handling(hass)
return hass
def from_config_file(config_path: str,
hass: Optional[core.HomeAssistant]=None,
verbose: bool=False,
skip_pip: bool=True,
log_rotate_days: Any=None):
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,
instantiates a new Home Assistant object if 'hass' is not given.
"""
if hass is None:
2015-08-17 03:44:46 +00:00
hass = core.HomeAssistant()
# run task
hass = hass.loop.run_until_complete(
async_from_config_file(
config_path, hass, verbose, skip_pip, log_rotate_days)
)
return hass
@asyncio.coroutine
def async_from_config_file(config_path: str,
hass: core.HomeAssistant,
verbose: bool=False,
skip_pip: bool=True,
log_rotate_days: Any=None):
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter.
This method is a coroutine.
"""
# Set config dir to directory holding config file
config_dir = os.path.abspath(os.path.dirname(config_path))
hass.config.config_dir = config_dir
yield from hass.loop.run_in_executor(
None, mount_local_lib_path, config_dir)
async_enable_logging(hass, verbose, log_rotate_days)
2015-08-30 06:02:07 +00:00
try:
config_dict = yield from hass.loop.run_in_executor(
None, conf_util.load_yaml_config_file, config_path)
except HomeAssistantError:
return None
finally:
clear_secret_cache()
hass = yield from async_from_config_dict(
config_dict, hass, enable_log=False, skip_pip=skip_pip)
return hass
@core.callback
def async_enable_logging(hass: core.HomeAssistant, verbose: bool=False,
log_rotate_days=None) -> None:
"""Setup the logging.
This method must be run in the event loop.
"""
logging.basicConfig(level=logging.INFO)
2017-01-20 05:31:44 +00:00
fmt = ("%(asctime)s %(levelname)s (%(threadName)s) "
"[%(name)s] %(message)s")
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
datefmt = '%y-%m-%d %H:%M:%S'
# 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)
try:
from colorlog import ColoredFormatter
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
2017-01-20 05:31:44 +00:00
colorfmt,
datefmt=datefmt,
reset=True,
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red',
}
))
except ImportError:
pass
# Log errors to a file if we have write access to file or config dir
2015-11-07 09:44:02 +00:00
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
err_path_exists = os.path.isfile(err_log_path)
# 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 \
(not err_path_exists and os.access(hass.config.config_dir, os.W_OK)):
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)
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))
async_handler = AsyncHandler(hass.loop, err_handler)
@asyncio.coroutine
def async_stop_async_handler(event):
"""Cleanup async handler."""
logging.getLogger('').removeHandler(async_handler)
yield from async_handler.async_close(blocking=True)
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
logger = logging.getLogger('')
logger.addHandler(async_handler)
2015-12-23 02:39:46 +00:00
logger.setLevel(logging.INFO)
else:
_LOGGER.error(
2015-05-12 05:23:38 +00:00
'Unable to setup error log %s (access denied)', err_log_path)
2015-01-30 16:26:06 +00:00
def mount_local_lib_path(config_dir: str) -> str:
"""Add local library to Python Path.
Async friendly.
"""
deps_dir = os.path.join(config_dir, 'deps')
if deps_dir not in sys.path:
sys.path.insert(0, os.path.join(config_dir, 'deps'))
return deps_dir