Add Safe Mode (#30723)

* Store last working HTTP settings

* Add safe mode

* Fix tests

* Add cloud to safe mode

* Update logging text

* Fix camera tests leaving files behind

* Make emulated_hue tests not leave files behind

* Make logbook tests not leave files behind

* Make tts tests not leave files behind

* Make image_processing tests not leave files behind

* Make manual_mqtt tests not leave files behind
This commit is contained in:
Paulus Schoutsen 2020-01-14 13:03:02 -08:00 committed by GitHub
parent c4673ddee1
commit 5fdc60e067
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 480 additions and 430 deletions

View file

@ -278,6 +278,7 @@ homeassistant/components/rfxtrx/* @danielhiversen
homeassistant/components/ring/* @balloob
homeassistant/components/rmvtransport/* @cgtobi
homeassistant/components/roomba/* @pschmitt
homeassistant/components/safe_mode/* @home-assistant/core
homeassistant/components/saj/* @fredericvl
homeassistant/components/samsungtv/* @escoand
homeassistant/components/scene/* @home-assistant/core

View file

@ -6,13 +6,10 @@ import platform
import subprocess
import sys
import threading
from typing import TYPE_CHECKING, Any, Dict, List
from typing import List
from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
if TYPE_CHECKING:
from homeassistant import core
def set_loop() -> None:
"""Attempt to use different loop."""
@ -78,19 +75,6 @@ def ensure_config_path(config_dir: str) -> None:
sys.exit(1)
async def ensure_config_file(hass: "core.HomeAssistant", config_dir: str) -> str:
"""Ensure configuration file exists."""
import homeassistant.config as config_util
config_path = await config_util.async_ensure_config_exists(hass, config_dir)
if config_path is None:
print("Error getting configuration path")
sys.exit(1)
return config_path
def get_arguments() -> argparse.Namespace:
"""Get parsed passed in arguments."""
import homeassistant.config as config_util
@ -107,7 +91,7 @@ def get_arguments() -> argparse.Namespace:
help="Directory that contains the Home Assistant configuration",
)
parser.add_argument(
"--demo-mode", action="store_true", help="Start Home Assistant in demo mode"
"--safe-mode", action="store_true", help="Start Home Assistant in safe mode"
)
parser.add_argument(
"--debug", action="store_true", help="Start Home Assistant in debug mode"
@ -253,35 +237,21 @@ def cmdline() -> List[str]:
async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int:
"""Set up Home Assistant and run."""
from homeassistant import bootstrap, core
from homeassistant import bootstrap
hass = core.HomeAssistant()
if args.demo_mode:
config: Dict[str, Any] = {"frontend": {}, "demo": {}}
bootstrap.async_from_config_dict(
config,
hass,
hass = await bootstrap.async_setup_hass(
config_dir=config_dir,
verbose=args.verbose,
skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days,
log_file=args.log_file,
log_no_color=args.log_no_color,
)
else:
config_file = await ensure_config_file(hass, config_dir)
print("Config directory:", config_dir)
await bootstrap.async_from_config_file(
config_file,
hass,
verbose=args.verbose,
skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days,
log_file=args.log_file,
log_no_color=args.log_no_color,
safe_mode=args.safe_mode,
)
if hass is None:
return 1
if args.open_ui and hass.config.api is not None:
import webbrowser
@ -358,7 +328,7 @@ def main() -> int:
return scripts.run(args.script)
config_dir = os.path.join(os.getcwd(), args.config)
config_dir = os.path.abspath(os.path.join(os.getcwd(), args.config))
ensure_config_path(config_dir)
# Daemon functions

View file

@ -1,6 +1,5 @@
"""Provide methods to bootstrap a Home Assistant instance."""
import asyncio
from collections import OrderedDict
import logging
import logging.handlers
import os
@ -11,6 +10,7 @@ from typing import Any, Dict, Optional, Set
import voluptuous as vol
from homeassistant import config as conf_util, config_entries, core, loader
from homeassistant.components import http
from homeassistant.const import (
EVENT_HOMEASSISTANT_CLOSE,
REQUIRED_NEXT_PYTHON_DATE,
@ -42,25 +42,20 @@ STAGE_1_INTEGRATIONS = {
}
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,
async def async_setup_hass(
*,
config_dir: str,
verbose: bool,
log_rotate_days: int,
log_file: str,
log_no_color: bool,
skip_pip: bool,
safe_mode: bool,
) -> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary.
"""Set up Home Assistant."""
hass = core.HomeAssistant()
hass.config.config_dir = config_dir
Dynamically loads required components and its dependencies.
This method is a coroutine.
"""
start = time()
if enable_log:
async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color)
hass.config.skip_pip = skip_pip
@ -69,6 +64,54 @@ async def async_from_config_dict(
"Skipping pip installation of required modules. This may cause issues"
)
if not await conf_util.async_ensure_config_exists(hass):
_LOGGER.error("Error getting configuration path")
return None
_LOGGER.info("Config directory: %s", config_dir)
config_dict = None
if not safe_mode:
await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass)
try:
config_dict = await conf_util.async_hass_config_yaml(hass)
except HomeAssistantError as err:
_LOGGER.error(
"Failed to parse configuration.yaml: %s. Falling back to safe mode",
err,
)
else:
if not is_virtual_env():
await async_mount_local_lib_path(config_dir)
await async_from_config_dict(config_dict, hass)
finally:
clear_secret_cache()
if safe_mode or config_dict is None:
_LOGGER.info("Starting in safe mode")
http_conf = (await http.async_get_last_config(hass)) or {}
await async_from_config_dict(
{"safe_mode": {}, "http": http_conf}, hass,
)
return hass
async def async_from_config_dict(
config: Dict[str, Any], hass: core.HomeAssistant
) -> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary.
Dynamically loads required components and its dependencies.
This method is a coroutine.
"""
start = time()
core_config = config.get(core.DOMAIN, {})
try:
@ -83,14 +126,6 @@ async def async_from_config_dict(
)
return None
# Make a copy because we are mutating it.
config = OrderedDict(config)
# Merge packages
await conf_util.merge_packages_config(
hass, config, core_config.get(conf_util.CONF_PACKAGES, {})
)
hass.config_entries = config_entries.ConfigEntries(hass, config)
await hass.config_entries.async_initialize()
@ -116,46 +151,6 @@ async def async_from_config_dict(
return hass
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,
) -> Optional[core.HomeAssistant]:
"""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
if not is_virtual_env():
await async_mount_local_lib_path(config_dir)
async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color)
await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass)
try:
config_dict = await hass.async_add_executor_job(
conf_util.load_yaml_config_file, config_path
)
except HomeAssistantError as err:
_LOGGER.error("Error loading %s: %s", config_path, err)
return None
finally:
clear_secret_cache()
return await async_from_config_dict(
config_dict, hass, enable_log=False, skip_pip=skip_pip
)
@core.callback
def async_enable_logging(
hass: core.HomeAssistant,
@ -269,6 +264,7 @@ def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]:
domains = set(key.split(" ")[0] for key in config.keys() if key != core.DOMAIN)
# Add config entry domains
if "safe_mode" not in config:
domains.update(hass.config_entries.async_domains())
# Make sure the Hass.io component is loaded

View file

@ -13,7 +13,7 @@ from yarl import URL
from homeassistant.components import websocket_api
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.config import find_config_file, load_yaml_config_file
from homeassistant.config import async_hass_config_yaml
from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
@ -362,11 +362,10 @@ def _async_setup_themes(hass, themes):
else:
_LOGGER.warning("Theme %s is not defined.", name)
@callback
def reload_themes(_):
async def reload_themes(_):
"""Reload themes."""
path = find_config_file(hass.config.config_dir)
new_themes = load_yaml_config_file(path)[DOMAIN].get(CONF_THEMES, {})
config = await async_hass_config_yaml(hass)
new_themes = config[DOMAIN].get(CONF_THEMES, {})
hass.data[DATA_THEMES] = new_themes
if hass.data[DATA_DEFAULT_THEME] not in new_themes:
hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME

View file

@ -3,7 +3,7 @@ from ipaddress import ip_network
import logging
import os
import ssl
from typing import Optional
from typing import Optional, cast
from aiohttp import web
from aiohttp.web_exceptions import HTTPMovedPermanently
@ -14,7 +14,10 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
SERVER_PORT,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import storage
import homeassistant.helpers.config_validation as cv
from homeassistant.loader import bind_hass
import homeassistant.util as hass_util
from homeassistant.util import ssl as ssl_util
@ -56,6 +59,9 @@ NO_LOGIN_ATTEMPT_THRESHOLD = -1
MAX_CLIENT_SIZE: int = 1024 ** 2 * 16
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
HTTP_SCHEMA = vol.Schema(
{
@ -85,6 +91,13 @@ HTTP_SCHEMA = vol.Schema(
CONFIG_SCHEMA = vol.Schema({DOMAIN: HTTP_SCHEMA}, extra=vol.ALLOW_EXTRA)
@bind_hass
async def async_get_last_config(hass: HomeAssistant) -> Optional[dict]:
"""Return the last known working config."""
store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY)
return cast(Optional[dict], await store.async_load())
class ApiConfig:
"""Configuration settings for API server."""
@ -151,6 +164,10 @@ async def async_setup(hass, config):
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server)
await server.start()
# If we are set up successful, we store the HTTP settings for safe mode.
store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY)
await store.async_save(conf)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_server)
hass.http = server

View file

@ -0,0 +1,15 @@
"""The Safe Mode integration."""
from homeassistant.components import persistent_notification
from homeassistant.core import HomeAssistant
DOMAIN = "safe_mode"
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Safe Mode component."""
persistent_notification.async_create(
hass,
"Home Assistant is running in safe mode. Check [the error log](/developer-tools/logs) to see what went wrong.",
"Safe Mode",
)
return True

View file

@ -0,0 +1,12 @@
{
"domain": "safe_mode",
"name": "Safe Mode",
"config_flow": false,
"documentation": "https://www.home-assistant.io/integrations/safe_mode",
"requirements": [],
"ssdp": [],
"zeroconf": [],
"homekit": {},
"dependencies": ["frontend", "config", "persistent_notification", "cloud"],
"codeowners": ["@home-assistant/core"]
}

View file

@ -226,35 +226,34 @@ def get_default_config_dir() -> str:
return os.path.join(data_dir, CONFIG_DIR_NAME) # type: ignore
async def async_ensure_config_exists(
hass: HomeAssistant, config_dir: str
) -> Optional[str]:
async def async_ensure_config_exists(hass: HomeAssistant) -> bool:
"""Ensure a configuration file exists in given configuration directory.
Creating a default one if needed.
Return path to the configuration file.
Return boolean if configuration dir is ready to go.
"""
config_path = find_config_file(config_dir)
config_path = hass.config.path(YAML_CONFIG_FILE)
if config_path is None:
print("Unable to find configuration. Creating default one in", config_dir)
config_path = await async_create_default_config(hass, config_dir)
if os.path.isfile(config_path):
return True
return config_path
print(
"Unable to find configuration. Creating default one in", hass.config.config_dir
)
return await async_create_default_config(hass)
async def async_create_default_config(
hass: HomeAssistant, config_dir: str
) -> Optional[str]:
async def async_create_default_config(hass: HomeAssistant) -> bool:
"""Create a default configuration file in given configuration directory.
Return path to new config file if success, None if failed.
This method needs to run in an executor.
Return if creation was successful.
"""
return await hass.async_add_executor_job(_write_default_config, config_dir)
return await hass.async_add_executor_job(
_write_default_config, hass.config.config_dir
)
def _write_default_config(config_dir: str) -> Optional[str]:
def _write_default_config(config_dir: str) -> bool:
"""Write the default config."""
config_path = os.path.join(config_dir, YAML_CONFIG_FILE)
secret_path = os.path.join(config_dir, SECRET_YAML)
@ -288,11 +287,11 @@ def _write_default_config(config_dir: str) -> Optional[str]:
with open(scene_yaml_path, "wt"):
pass
return config_path
return True
except OSError:
print("Unable to create default configuration file", config_path)
return None
return False
async def async_hass_config_yaml(hass: HomeAssistant) -> Dict:
@ -300,35 +299,16 @@ async def async_hass_config_yaml(hass: HomeAssistant) -> Dict:
This function allow a component inside the asyncio loop to reload its
configuration by itself. Include package merge.
This method is a coroutine.
"""
def _load_hass_yaml_config() -> Dict:
path = find_config_file(hass.config.config_dir)
if path is None:
raise HomeAssistantError(
f"Config file not found in: {hass.config.config_dir}"
)
config = load_yaml_config_file(path)
return config
# Not using async_add_executor_job because this is an internal method.
config = await hass.loop.run_in_executor(None, _load_hass_yaml_config)
config = await hass.loop.run_in_executor(
None, load_yaml_config_file, hass.config.path(YAML_CONFIG_FILE)
)
core_config = config.get(CONF_CORE, {})
await merge_packages_config(hass, config, core_config.get(CONF_PACKAGES, {}))
return config
def find_config_file(config_dir: Optional[str]) -> Optional[str]:
"""Look in given directory for supported configuration files."""
if config_dir is None:
return None
config_path = os.path.join(config_dir, YAML_CONFIG_FILE)
return config_path if os.path.isfile(config_path) else None
def load_yaml_config_file(config_path: str) -> Dict[Any, Any]:
"""Parse a YAML configuration file.
@ -382,8 +362,7 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None:
if version_obj < LooseVersion("0.92"):
# 0.92 moved google/tts.py to google_translate/tts.py
config_path = find_config_file(hass.config.config_dir)
assert config_path is not None
config_path = hass.config.path(YAML_CONFIG_FILE)
with open(config_path, "rt", encoding="utf-8") as config_file:
config_raw = config_file.read()

View file

@ -1,5 +1,6 @@
"""Helper to check the configuration file."""
from collections import OrderedDict
import os
from typing import List, NamedTuple, Optional
import attr
@ -10,10 +11,10 @@ from homeassistant.config import (
CONF_CORE,
CONF_PACKAGES,
CORE_CONFIG_SCHEMA,
YAML_CONFIG_FILE,
_format_config_error,
config_per_platform,
extract_domain_configs,
find_config_file,
load_yaml_config_file,
merge_packages_config,
)
@ -62,7 +63,6 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig
This method is a coroutine.
"""
config_dir = hass.config.config_dir
result = HomeAssistantConfig()
def _pack_error(
@ -79,9 +79,9 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig
result.add_error(_format_config_error(ex, domain, config), domain, config)
# Load configuration.yaml
config_path = hass.config.path(YAML_CONFIG_FILE)
try:
config_path = await hass.async_add_executor_job(find_config_file, config_dir)
if not config_path:
if not await hass.async_add_executor_job(os.path.isfile, config_path):
return result.add_error("File configuration.yaml not found.")
config = await hass.async_add_executor_job(load_yaml_config_file, config_path)
except FileNotFoundError:

View file

@ -32,13 +32,14 @@ def run(args):
os.makedirs(config_dir)
hass = HomeAssistant()
config_path = hass.loop.run_until_complete(async_run(hass, config_dir))
hass.config.config_dir = config_dir
config_path = hass.loop.run_until_complete(async_run(hass))
print("Configuration file:", config_path)
return 0
async def async_run(hass, config_dir):
async def async_run(hass):
"""Make sure config exists."""
path = await config_util.async_ensure_config_exists(hass, config_dir)
path = await config_util.async_ensure_config_exists(hass)
await hass.async_stop(force=True)
return path

View file

@ -55,7 +55,7 @@ async def _async_process_dependencies(
"""Ensure all dependencies are set up."""
blacklisted = [dep for dep in dependencies if dep in loader.DEPENDENCY_BLACKLIST]
if blacklisted and name != "default_config":
if blacklisted and name not in ("default_config", "safe_mode"):
_LOGGER.error(
"Unable to set up dependencies of %s: "
"found blacklisted dependencies: %s",

View file

@ -496,7 +496,6 @@ async def test_reload_config_service(hass, calls, hass_admin_user, hass_read_onl
}
},
):
with patch("homeassistant.config.find_config_file", return_value=""):
with pytest.raises(Unauthorized):
await common.async_reload(hass, Context(user_id=hass_read_only_user.id))
await hass.async_block_till_done()
@ -551,7 +550,6 @@ async def test_reload_config_when_invalid_config(hass, calls):
autospec=True,
return_value={automation.DOMAIN: "not valid"},
):
with patch("homeassistant.config.find_config_file", return_value=""):
await common.async_reload(hass)
await hass.async_block_till_done()
@ -590,7 +588,6 @@ async def test_reload_config_handles_load_fails(hass, calls):
"homeassistant.config.load_yaml_config_file",
side_effect=HomeAssistantError("bla"),
):
with patch("homeassistant.config.find_config_file", return_value=""):
await common.async_reload(hass)
await hass.async_block_till_done()

View file

@ -6,24 +6,15 @@ from unittest.mock import PropertyMock, mock_open, patch
import pytest
from homeassistant.components import camera, http
from homeassistant.components import camera
from homeassistant.components.camera.const import DOMAIN, PREF_PRELOAD_STREAM
from homeassistant.components.camera.prefs import CameraEntityPreferences
from homeassistant.components.websocket_api.const import TYPE_RESULT
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_ENTITY_PICTURE,
EVENT_HOMEASSISTANT_START,
)
from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component, setup_component
from homeassistant.setup import async_setup_component
from tests.common import (
assert_setup_component,
get_test_home_assistant,
get_test_instance_port,
mock_coro,
)
from tests.common import mock_coro
from tests.components.camera import common
@ -55,96 +46,53 @@ def setup_camera_prefs(hass):
return common.mock_camera_prefs(hass, "camera.demo_camera")
class TestSetupCamera:
"""Test class for setup camera."""
def setup_method(self):
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
def teardown_method(self):
"""Stop everything that was started."""
self.hass.stop()
def test_setup_component(self):
"""Set up demo platform on camera component."""
config = {camera.DOMAIN: {"platform": "demo"}}
with assert_setup_component(1, camera.DOMAIN):
setup_component(self.hass, camera.DOMAIN, config)
class TestGetImage:
"""Test class for camera."""
def setup_method(self):
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
setup_component(
self.hass,
http.DOMAIN,
{http.DOMAIN: {http.CONF_SERVER_PORT: get_test_instance_port()}},
@pytest.fixture
async def image_mock_url(hass):
"""Fixture for get_image tests."""
await async_setup_component(
hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}}
)
config = {camera.DOMAIN: {"platform": "demo"}}
setup_component(self.hass, camera.DOMAIN, config)
async def test_get_image_from_camera(hass, image_mock_url):
"""Grab an image from camera entity."""
state = self.hass.states.get("camera.demo_camera")
self.url = "{0}{1}".format(
self.hass.config.api.base_url, state.attributes.get(ATTR_ENTITY_PICTURE)
)
def teardown_method(self):
"""Stop everything that was started."""
self.hass.stop()
@patch(
with patch(
"homeassistant.components.demo.camera.DemoCamera.camera_image",
autospec=True,
return_value=b"Test",
)
def test_get_image_from_camera(self, mock_camera):
"""Grab an image from camera entity."""
self.hass.start()
image = asyncio.run_coroutine_threadsafe(
camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop
).result()
) as mock_camera:
image = await camera.async_get_image(hass, "camera.demo_camera")
assert mock_camera.called
assert image.content == b"Test"
def test_get_image_without_exists_camera(self):
async def test_get_image_without_exists_camera(hass, image_mock_url):
"""Try to get image without exists camera."""
with patch(
"homeassistant.helpers.entity_component.EntityComponent.get_entity",
return_value=None,
), pytest.raises(HomeAssistantError):
asyncio.run_coroutine_threadsafe(
camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop
).result()
await camera.async_get_image(hass, "camera.demo_camera")
def test_get_image_with_timeout(self):
async def test_get_image_with_timeout(hass, image_mock_url):
"""Try to get image with timeout."""
with patch(
"homeassistant.components.camera.Camera.async_camera_image",
side_effect=asyncio.TimeoutError,
), pytest.raises(HomeAssistantError):
asyncio.run_coroutine_threadsafe(
camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop
).result()
await camera.async_get_image(hass, "camera.demo_camera")
def test_get_image_fails(self):
async def test_get_image_fails(hass, image_mock_url):
"""Try to get image with timeout."""
with patch(
"homeassistant.components.camera.Camera.async_camera_image",
return_value=mock_coro(None),
), pytest.raises(HomeAssistantError):
asyncio.run_coroutine_threadsafe(
camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop
).result()
await camera.async_get_image(hass, "camera.demo_camera")
async def test_snapshot_service(hass, mock_camera):

View file

@ -4,10 +4,11 @@ import unittest
from unittest.mock import patch
from aiohttp.hdrs import CONTENT_TYPE
import defusedxml.ElementTree as ET
import requests
from homeassistant import const, setup
from homeassistant.components import emulated_hue, http
from homeassistant.components import emulated_hue
from tests.common import get_test_home_assistant, get_test_instance_port
@ -28,10 +29,6 @@ class TestEmulatedHue(unittest.TestCase):
"""Set up the class."""
cls.hass = hass = get_test_home_assistant()
setup.setup_component(
hass, http.DOMAIN, {http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}}
)
with patch("homeassistant.components.emulated_hue.UPNPResponderThread"):
setup.setup_component(
hass,
@ -52,8 +49,6 @@ class TestEmulatedHue(unittest.TestCase):
def test_description_xml(self):
"""Test the description."""
import defusedxml.ElementTree as ET
result = requests.get(BRIDGE_URL_BASE.format("/description.xml"), timeout=5)
assert result.status_code == 200

View file

@ -1,7 +1,7 @@
"""The tests for Home Assistant frontend."""
import re
from unittest.mock import patch
from asynctest import patch
import pytest
from homeassistant.components.frontend import (
@ -173,7 +173,7 @@ async def test_themes_reload_themes(hass, hass_ws_client):
client = await hass_ws_client(hass)
with patch(
"homeassistant.components.frontend.load_yaml_config_file",
"homeassistant.components.frontend.async_hass_config_yaml",
return_value={DOMAIN: {CONF_THEMES: {"sad": {"primary-color": "blue"}}}},
):
await hass.services.async_call(

View file

@ -429,7 +429,6 @@ class TestComponentsGroup(unittest.TestCase):
}
},
):
with patch("homeassistant.config.find_config_file", return_value=""):
common.reload(self.hass)
self.hass.block_till_done()

View file

@ -18,7 +18,7 @@ async def test_reload_config_service(hass):
"homeassistant.config.load_yaml_config_file",
autospec=True,
return_value={"scene": {"name": "Hallo", "entities": {"light.kitchen": "on"}}},
), patch("homeassistant.config.find_config_file", return_value=""):
):
await hass.services.async_call("scene", "reload", blocking=True)
await hass.async_block_till_done()
@ -28,7 +28,7 @@ async def test_reload_config_service(hass):
"homeassistant.config.load_yaml_config_file",
autospec=True,
return_value={"scene": {"name": "Bye", "entities": {"light.kitchen": "on"}}},
), patch("homeassistant.config.find_config_file", return_value=""):
):
await hass.services.async_call("scene", "reload", blocking=True)
await hass.async_block_till_done()

View file

@ -240,3 +240,16 @@ async def test_cors_defaults(hass):
assert len(mock_setup.mock_calls) == 1
assert mock_setup.mock_calls[0][1][1] == ["https://cast.home-assistant.io"]
async def test_storing_config(hass, aiohttp_client, aiohttp_unused_port):
"""Test that we store last working config."""
config = {http.CONF_SERVER_PORT: aiohttp_unused_port()}
await async_setup_component(hass, http.DOMAIN, {http.DOMAIN: config})
await hass.async_start()
assert await hass.components.http.async_get_last_config() == http.HTTP_SCHEMA(
config
)

View file

@ -77,8 +77,6 @@ class TestImageProcessing:
)
def test_get_image_from_camera(self, mock_camera):
"""Grab an image from camera entity."""
self.hass.start()
common.scan(self.hass, entity_id="image_processing.test")
self.hass.block_till_done()

View file

@ -240,7 +240,6 @@ async def test_reload(hass, hass_admin_user):
}
},
):
with patch("homeassistant.config.find_config_file", return_value=""):
await hass.services.async_call(
DOMAIN,
SERVICE_RELOAD,

View file

@ -345,7 +345,6 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user):
}
},
):
with patch("homeassistant.config.find_config_file", return_value=""):
with pytest.raises(Unauthorized):
await hass.services.async_call(
DOMAIN,

View file

@ -338,7 +338,6 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user):
}
},
):
with patch("homeassistant.config.find_config_file", return_value=""):
with pytest.raises(Unauthorized):
await hass.services.async_call(
DOMAIN,

View file

@ -415,7 +415,6 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user):
}
},
):
with patch("homeassistant.config.find_config_file", return_value=""):
with pytest.raises(Unauthorized):
await hass.services.async_call(
DOMAIN,

View file

@ -288,7 +288,6 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user):
}
},
):
with patch("homeassistant.config.find_config_file", return_value=""):
with pytest.raises(Unauthorized):
await hass.services.async_call(
DOMAIN,

View file

@ -48,7 +48,6 @@ class TestComponentLogbook(unittest.TestCase):
self.hass = get_test_home_assistant()
init_recorder_component(self.hass) # Force an in memory DB
assert setup_component(self.hass, logbook.DOMAIN, self.EMPTY_CONFIG)
self.hass.start()
def tearDown(self):
"""Stop everything that was started."""
@ -90,7 +89,7 @@ class TestComponentLogbook(unittest.TestCase):
dt_util.utcnow() + timedelta(hours=1),
)
)
assert len(events) == 2
assert len(events) == 1
assert 1 == len(calls)
last_call = calls[-1]

View file

@ -269,9 +269,6 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase):
entity_id = "alarm_control_panel.test"
self.hass.start()
self.hass.block_till_done()
assert STATE_ALARM_DISARMED == self.hass.states.get(entity_id).state
common.alarm_arm_home(self.hass, "abc")
@ -1471,9 +1468,6 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase):
entity_id = "alarm_control_panel.test"
self.hass.start()
self.hass.block_till_done()
assert STATE_ALARM_DISARMED == self.hass.states.get(entity_id).state
common.alarm_arm_home(self.hass, "def")

View file

@ -753,7 +753,7 @@ async def test_reload(hass, hass_admin_user):
{"name": "Person 3", "id": "id-3"},
]
},
), patch("homeassistant.config.find_config_file", return_value=""):
):
await hass.services.async_call(
DOMAIN,
SERVICE_RELOAD,

View file

@ -0,0 +1 @@
"""Tests for the Safe Mode integration."""

View file

@ -0,0 +1,9 @@
"""Tests for safe mode integration."""
from homeassistant.setup import async_setup_component
async def test_works(hass):
"""Test safe mode works."""
assert await async_setup_component(hass, "safe_mode", {})
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids()) == 1

View file

@ -229,7 +229,6 @@ class TestScriptComponent(unittest.TestCase):
"script": {"test2": {"sequence": [{"delay": {"seconds": 5}}]}}
},
):
with patch("homeassistant.config.find_config_file", return_value=""):
reload(self.hass)
self.hass.block_till_done()
@ -262,7 +261,6 @@ async def test_service_descriptions(hass):
assert not descriptions[DOMAIN]["test"]["fields"]
# Test 2: has "fields" but no "description"
await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True)
with patch(
"homeassistant.config.load_yaml_config_file",
return_value={
@ -279,7 +277,6 @@ async def test_service_descriptions(hass):
}
},
):
with patch("homeassistant.config.find_config_file", return_value=""):
await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True)
descriptions = await async_get_all_descriptions(hass)

View file

@ -259,7 +259,6 @@ async def test_config_reload(hass, hass_admin_user, hass_read_only_user):
}
},
):
with patch("homeassistant.config.find_config_file", return_value=""):
with pytest.raises(Unauthorized):
await hass.services.async_call(
DOMAIN,

View file

@ -24,6 +24,7 @@ from tests.common import (
get_test_home_assistant,
get_test_instance_port,
mock_service,
mock_storage,
)
@ -45,6 +46,8 @@ class TestTTS:
self.hass = get_test_home_assistant()
self.demo_provider = DemoProvider("en")
self.default_tts_cache = self.hass.config.path(tts.DEFAULT_CACHE_DIR)
self.mock_storage = mock_storage()
self.mock_storage.__enter__()
setup_component(
self.hass,
@ -55,6 +58,7 @@ class TestTTS:
def teardown_method(self):
"""Stop everything that was started."""
self.hass.stop()
self.mock_storage.__exit__(None, None, None)
if os.path.isdir(self.default_tts_cache):
shutil.rmtree(self.default_tts_cache)

View file

@ -120,7 +120,7 @@ def test_secrets(isfile_patch, loop):
@patch("os.path.isfile", return_value=True)
def test_package_invalid(isfile_patch, loop):
"""Test a valid platform setup."""
"""Test an invalid package."""
files = {
YAML_CONFIG_FILE: BASE_CONFIG + (" packages:\n p1:\n" ' group: ["a"]')
}

View file

@ -3,18 +3,23 @@
import asyncio
import logging
import os
from unittest.mock import Mock, patch
from unittest.mock import Mock
from asynctest import patch
import pytest
from homeassistant import bootstrap
import homeassistant.config as config_util
from homeassistant.exceptions import HomeAssistantError
import homeassistant.util.dt as dt_util
from tests.common import (
MockConfigEntry,
MockModule,
flush_store,
get_test_config_dir,
mock_coro,
mock_integration,
patch_yaml_files,
)
ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE
@ -23,26 +28,6 @@ VERSION_PATH = os.path.join(get_test_config_dir(), config_util.VERSION_FILE)
_LOGGER = logging.getLogger(__name__)
# prevent .HA_VERSION file from being written
@patch("homeassistant.bootstrap.conf_util.process_ha_config_upgrade", Mock())
@patch(
"homeassistant.util.location.async_detect_location_info",
Mock(return_value=mock_coro(None)),
)
@patch("os.path.isfile", Mock(return_value=True))
@patch("os.access", Mock(return_value=True))
@patch("homeassistant.bootstrap.async_enable_logging", Mock(return_value=True))
def test_from_config_file(hass):
"""Test with configuration file."""
components = set(["browser", "conversation", "script"])
files = {"config.yaml": "".join(f"{comp}:\n" for comp in components)}
with patch_yaml_files(files, True):
yield from bootstrap.async_from_config_file("config.yaml", hass)
assert components == hass.config.components
@patch("homeassistant.bootstrap.async_enable_logging", Mock())
@asyncio.coroutine
def test_home_assistant_core_config_validation(hass):
@ -54,33 +39,6 @@ def test_home_assistant_core_config_validation(hass):
assert result is None
async def test_async_from_config_file_not_mount_deps_folder(loop):
"""Test that we not mount the deps folder inside async_from_config_file."""
hass = Mock(async_add_executor_job=Mock(side_effect=lambda *args: mock_coro()))
with patch("homeassistant.bootstrap.is_virtual_env", return_value=False), patch(
"homeassistant.bootstrap.async_enable_logging", return_value=mock_coro()
), patch(
"homeassistant.bootstrap.async_mount_local_lib_path", return_value=mock_coro()
) as mock_mount, patch(
"homeassistant.bootstrap.async_from_config_dict", return_value=mock_coro()
):
await bootstrap.async_from_config_file("mock-path", hass)
assert len(mock_mount.mock_calls) == 1
with patch("homeassistant.bootstrap.is_virtual_env", return_value=True), patch(
"homeassistant.bootstrap.async_enable_logging", return_value=mock_coro()
), patch(
"homeassistant.bootstrap.async_mount_local_lib_path", return_value=mock_coro()
) as mock_mount, patch(
"homeassistant.bootstrap.async_from_config_dict", return_value=mock_coro()
):
await bootstrap.async_from_config_file("mock-path", hass)
assert len(mock_mount.mock_calls) == 0
async def test_load_hassio(hass):
"""Test that we load Hass.io component."""
with patch.dict(os.environ, {}, clear=True):
@ -233,3 +191,169 @@ async def test_setup_after_deps_not_present(hass, caplog):
assert "first_dep" not in hass.config.components
assert "second_dep" in hass.config.components
assert order == ["root", "second_dep"]
@pytest.fixture
def mock_is_virtual_env():
"""Mock enable logging."""
with patch(
"homeassistant.bootstrap.is_virtual_env", return_value=False
) as is_virtual_env:
yield is_virtual_env
@pytest.fixture
def mock_enable_logging():
"""Mock enable logging."""
with patch("homeassistant.bootstrap.async_enable_logging") as enable_logging:
yield enable_logging
@pytest.fixture
def mock_mount_local_lib_path():
"""Mock enable logging."""
with patch(
"homeassistant.bootstrap.async_mount_local_lib_path"
) as mount_local_lib_path:
yield mount_local_lib_path
@pytest.fixture
def mock_process_ha_config_upgrade():
"""Mock enable logging."""
with patch(
"homeassistant.config.process_ha_config_upgrade"
) as process_ha_config_upgrade:
yield process_ha_config_upgrade
@pytest.fixture
def mock_ensure_config_exists():
"""Mock enable logging."""
with patch(
"homeassistant.config.async_ensure_config_exists", return_value=True
) as ensure_config_exists:
yield ensure_config_exists
async def test_setup_hass(
mock_enable_logging,
mock_is_virtual_env,
mock_mount_local_lib_path,
mock_ensure_config_exists,
mock_process_ha_config_upgrade,
):
"""Test it works."""
verbose = Mock()
log_rotate_days = Mock()
log_file = Mock()
log_no_color = Mock()
with patch(
"homeassistant.config.async_hass_config_yaml", return_value={"browser": {}}
):
hass = await bootstrap.async_setup_hass(
config_dir=get_test_config_dir(),
verbose=verbose,
log_rotate_days=log_rotate_days,
log_file=log_file,
log_no_color=log_no_color,
skip_pip=True,
safe_mode=False,
)
assert "browser" in hass.config.components
assert len(mock_enable_logging.mock_calls) == 1
assert mock_enable_logging.mock_calls[0][1] == (
hass,
verbose,
log_rotate_days,
log_file,
log_no_color,
)
assert len(mock_mount_local_lib_path.mock_calls) == 1
assert len(mock_ensure_config_exists.mock_calls) == 1
assert len(mock_process_ha_config_upgrade.mock_calls) == 1
async def test_setup_hass_invalid_yaml(
mock_enable_logging,
mock_is_virtual_env,
mock_mount_local_lib_path,
mock_ensure_config_exists,
mock_process_ha_config_upgrade,
):
"""Test it works."""
with patch(
"homeassistant.config.async_hass_config_yaml", side_effect=HomeAssistantError
):
hass = await bootstrap.async_setup_hass(
config_dir=get_test_config_dir(),
verbose=False,
log_rotate_days=10,
log_file="",
log_no_color=False,
skip_pip=True,
safe_mode=False,
)
assert "safe_mode" in hass.config.components
assert len(mock_mount_local_lib_path.mock_calls) == 0
async def test_setup_hass_config_dir_nonexistent(
mock_enable_logging,
mock_is_virtual_env,
mock_mount_local_lib_path,
mock_ensure_config_exists,
mock_process_ha_config_upgrade,
):
"""Test it works."""
mock_ensure_config_exists.return_value = False
assert (
await bootstrap.async_setup_hass(
config_dir=get_test_config_dir(),
verbose=False,
log_rotate_days=10,
log_file="",
log_no_color=False,
skip_pip=True,
safe_mode=False,
)
is None
)
async def test_setup_hass_safe_mode(
hass,
mock_enable_logging,
mock_is_virtual_env,
mock_mount_local_lib_path,
mock_ensure_config_exists,
mock_process_ha_config_upgrade,
):
"""Test it works."""
# Add a config entry to storage.
MockConfigEntry(domain="browser").add_to_hass(hass)
hass.config_entries._async_schedule_save()
await flush_store(hass.config_entries._store)
with patch("homeassistant.components.browser.setup") as browser_setup:
hass = await bootstrap.async_setup_hass(
config_dir=get_test_config_dir(),
verbose=False,
log_rotate_days=10,
log_file="",
log_no_color=False,
skip_pip=True,
safe_mode=True,
)
assert "safe_mode" in hass.config.components
assert len(mock_mount_local_lib_path.mock_calls) == 0
# Validate we didn't try to set up config entry.
assert "browser" not in hass.config.components
assert len(browser_setup.mock_calls) == 0

View file

@ -82,7 +82,7 @@ def teardown():
async def test_create_default_config(hass):
"""Test creation of default config."""
await config_util.async_create_default_config(hass, CONFIG_DIR)
await config_util.async_create_default_config(hass)
assert os.path.isfile(YAML_PATH)
assert os.path.isfile(SECRET_PATH)
@ -91,20 +91,13 @@ async def test_create_default_config(hass):
assert os.path.isfile(AUTOMATIONS_PATH)
def test_find_config_file_yaml():
"""Test if it finds a YAML config file."""
create_file(YAML_PATH)
assert YAML_PATH == config_util.find_config_file(CONFIG_DIR)
async def test_ensure_config_exists_creates_config(hass):
"""Test that calling ensure_config_exists.
If not creates a new config file.
"""
with mock.patch("builtins.print") as mock_print:
await config_util.async_ensure_config_exists(hass, CONFIG_DIR)
await config_util.async_ensure_config_exists(hass)
assert os.path.isfile(YAML_PATH)
assert mock_print.called
@ -113,7 +106,7 @@ async def test_ensure_config_exists_creates_config(hass):
async def test_ensure_config_exists_uses_existing_config(hass):
"""Test that calling ensure_config_exists uses existing config."""
create_file(YAML_PATH)
await config_util.async_ensure_config_exists(hass, CONFIG_DIR)
await config_util.async_ensure_config_exists(hass)
with open(YAML_PATH) as f:
content = f.read()
@ -172,13 +165,9 @@ async def test_create_default_config_returns_none_if_write_error(hass):
Non existing folder returns None.
"""
hass.config.config_dir = os.path.join(CONFIG_DIR, "non_existing_dir/")
with mock.patch("builtins.print") as mock_print:
assert (
await config_util.async_create_default_config(
hass, os.path.join(CONFIG_DIR, "non_existing_dir/")
)
is None
)
assert await config_util.async_create_default_config(hass) is False
assert mock_print.called
@ -331,7 +320,6 @@ def test_config_upgrade_same_version(hass):
assert opened_file.write.call_count == 0
@mock.patch("homeassistant.config.find_config_file", mock.Mock())
def test_config_upgrade_no_file(hass):
"""Test update of version on upgrade, with no version file."""
mock_open = mock.mock_open()