Require core config detection to be triggerd manually (#24019)

* Detect core config

* Remove elevation

* Lint

* Lint

* Fix type
This commit is contained in:
Paulus Schoutsen 2019-05-22 17:24:46 -07:00 committed by GitHub
parent f207e01510
commit 9e96397e6a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 188 additions and 323 deletions

View file

@ -5,12 +5,18 @@ import voluptuous as vol
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.config import async_check_ha_config_file from homeassistant.config import async_check_ha_config_file
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.const import (
CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL
)
from homeassistant.helpers import config_validation as cv
from homeassistant.util import location
async def async_setup(hass): async def async_setup(hass):
"""Set up the Hassbian config.""" """Set up the Hassbian config."""
hass.http.register_view(CheckConfigView) hass.http.register_view(CheckConfigView)
hass.components.websocket_api.async_register_command(websocket_core_update) websocket_api.async_register_command(hass, websocket_update_config)
websocket_api.async_register_command(hass, websocket_detect_config)
return True return True
@ -35,18 +41,57 @@ class CheckConfigView(HomeAssistantView):
@websocket_api.require_admin @websocket_api.require_admin
@websocket_api.async_response @websocket_api.async_response
@websocket_api.websocket_command({ @websocket_api.websocket_command({
vol.Required('type'): 'config/core/update', 'type': 'config/core/update',
vol.Optional('latitude'): vol.Coerce(float), vol.Optional('latitude'): cv.latitude,
vol.Optional('longitude'): vol.Coerce(float), vol.Optional('longitude'): cv.longitude,
vol.Optional('elevation'): vol.Coerce(int), vol.Optional('elevation'): int,
vol.Optional('unit_system'): vol.Coerce(str), vol.Optional('unit_system'): cv.unit_system,
vol.Optional('location_name'): vol.Coerce(str), vol.Optional('location_name'): str,
vol.Optional('time_zone'): vol.Coerce(str), vol.Optional('time_zone'): cv.time_zone,
}) })
async def websocket_core_update(hass, connection, msg): async def websocket_update_config(hass, connection, msg):
"""Handle request for account info.""" """Handle update core config command."""
data = dict(msg) data = dict(msg)
data.pop('id') data.pop('id')
data.pop('type') data.pop('type')
await hass.config.update(**data)
connection.send_result(msg['id']) try:
await hass.config.update(**data)
connection.send_result(msg['id'])
except ValueError as err:
connection.send_error(
msg['id'], 'invalid_info', str(err)
)
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command({
'type': 'config/core/detect',
})
async def websocket_detect_config(hass, connection, msg):
"""Detect core config."""
session = hass.helpers.aiohttp_client.async_get_clientsession()
location_info = await location.async_detect_location_info(session)
info = {}
if location_info is None:
connection.send_result(msg['id'], info)
return
if location_info.use_metric:
info['unit_system'] = CONF_UNIT_SYSTEM_METRIC
else:
info['unit_system'] = CONF_UNIT_SYSTEM_IMPERIAL
if location_info.latitude:
info['latitude'] = location_info.latitude
if location_info.longitude:
info['longitude'] = location_info.longitude
if location_info.time_zone:
info['time_zone'] = location_info.time_zone
connection.send_result(msg['id'], info)

View file

@ -3,10 +3,11 @@ from homeassistant.core import callback
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
from .const import DOMAIN, STEP_USER, STEPS, STEP_INTEGRATION from .const import (
DOMAIN, STEP_USER, STEPS, STEP_INTEGRATION, STEP_CORE_CONFIG)
STORAGE_KEY = DOMAIN STORAGE_KEY = DOMAIN
STORAGE_VERSION = 2 STORAGE_VERSION = 3
class OnboadingStorage(Store): class OnboadingStorage(Store):
@ -15,7 +16,10 @@ class OnboadingStorage(Store):
async def _async_migrate_func(self, old_version, old_data): async def _async_migrate_func(self, old_version, old_data):
"""Migrate to the new version.""" """Migrate to the new version."""
# From version 1 -> 2, we automatically mark the integration step done # From version 1 -> 2, we automatically mark the integration step done
old_data['done'].append(STEP_INTEGRATION) if old_version < 2:
old_data['done'].append(STEP_INTEGRATION)
if old_version < 3:
old_data['done'].append(STEP_CORE_CONFIG)
return old_data return old_data

View file

@ -1,10 +1,12 @@
"""Constants for the onboarding component.""" """Constants for the onboarding component."""
DOMAIN = 'onboarding' DOMAIN = 'onboarding'
STEP_USER = 'user' STEP_USER = 'user'
STEP_CORE_CONFIG = 'core_config'
STEP_INTEGRATION = 'integration' STEP_INTEGRATION = 'integration'
STEPS = [ STEPS = [
STEP_USER, STEP_USER,
STEP_CORE_CONFIG,
STEP_INTEGRATION, STEP_INTEGRATION,
] ]

View file

@ -7,13 +7,16 @@ from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.http.view import HomeAssistantView
from homeassistant.core import callback from homeassistant.core import callback
from .const import DOMAIN, STEP_USER, STEPS, DEFAULT_AREAS, STEP_INTEGRATION from .const import (
DOMAIN, STEP_USER, STEPS, DEFAULT_AREAS, STEP_INTEGRATION,
STEP_CORE_CONFIG)
async def async_setup(hass, data, store): async def async_setup(hass, data, store):
"""Set up the onboarding view.""" """Set up the onboarding view."""
hass.http.register_view(OnboardingView(data, store)) hass.http.register_view(OnboardingView(data, store))
hass.http.register_view(UserOnboardingView(data, store)) hass.http.register_view(UserOnboardingView(data, store))
hass.http.register_view(CoreConfigOnboardingView(data, store))
hass.http.register_view(IntegrationOnboardingView(data, store)) hass.http.register_view(IntegrationOnboardingView(data, store))
@ -128,6 +131,26 @@ class UserOnboardingView(_BaseOnboardingView):
}) })
class CoreConfigOnboardingView(_BaseOnboardingView):
"""View to finish core config onboarding step."""
url = '/api/onboarding/core_config'
name = 'api:onboarding:core_config'
step = STEP_CORE_CONFIG
async def post(self, request):
"""Handle finishing core config step."""
hass = request.app['hass']
async with self._lock:
if self._async_is_done():
return self.json_message('Core config step already done', 403)
await self._async_mark_done(hass)
return self.json({})
class IntegrationOnboardingView(_BaseOnboardingView): class IntegrationOnboardingView(_BaseOnboardingView):
"""View to finish integration onboarding step.""" """View to finish integration onboarding step."""
@ -139,7 +162,7 @@ class IntegrationOnboardingView(_BaseOnboardingView):
vol.Required('client_id'): str, vol.Required('client_id'): str,
})) }))
async def post(self, request, data): async def post(self, request, data):
"""Handle user creation, area creation.""" """Handle token creation."""
hass = request.app['hass'] hass = request.app['hass']
user = request['hass_user'] user = request['hass_user']

View file

@ -18,13 +18,13 @@ from homeassistant.auth import providers as auth_providers,\
from homeassistant.const import ( from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE,
CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_PACKAGES, CONF_UNIT_SYSTEM, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_PACKAGES, CONF_UNIT_SYSTEM,
CONF_TIME_ZONE, CONF_ELEVATION, CONF_UNIT_SYSTEM_METRIC, CONF_TIME_ZONE, CONF_ELEVATION,
CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS,
__version__, CONF_CUSTOMIZE, CONF_CUSTOMIZE_DOMAIN, CONF_CUSTOMIZE_GLOB, __version__, CONF_CUSTOMIZE, CONF_CUSTOMIZE_DOMAIN, CONF_CUSTOMIZE_GLOB,
CONF_WHITELIST_EXTERNAL_DIRS, CONF_AUTH_PROVIDERS, CONF_AUTH_MFA_MODULES, CONF_WHITELIST_EXTERNAL_DIRS, CONF_AUTH_PROVIDERS, CONF_AUTH_MFA_MODULES,
CONF_TYPE, CONF_ID) CONF_TYPE, CONF_ID)
from homeassistant.core import ( from homeassistant.core import (
DOMAIN as CONF_CORE, SOURCE_DISCOVERED, SOURCE_YAML, HomeAssistant, DOMAIN as CONF_CORE, SOURCE_YAML, HomeAssistant,
callback) callback)
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import ( from homeassistant.loader import (
@ -32,7 +32,6 @@ from homeassistant.loader import (
) )
from homeassistant.util.yaml import load_yaml, SECRET_YAML from homeassistant.util.yaml import load_yaml, SECRET_YAML
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util import location as loc_util
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
from homeassistant.helpers.entity_values import EntityValues from homeassistant.helpers.entity_values import EntityValues
from homeassistant.helpers import config_per_platform, extract_domain_configs from homeassistant.helpers import config_per_platform, extract_domain_configs
@ -52,22 +51,6 @@ FILE_MIGRATION = (
('ios.conf', '.ios.conf'), ('ios.conf', '.ios.conf'),
) )
DEFAULT_CORE_CONFIG = (
# Tuples (attribute, default, auto detect property, description)
(CONF_NAME, 'Home', None, 'Name of the location where Home Assistant is '
'running'),
(CONF_LATITUDE, 0, 'latitude', 'Location required to calculate the time'
' the sun rises and sets'),
(CONF_LONGITUDE, 0, 'longitude', None),
(CONF_ELEVATION, 0, None, 'Impacts weather/sunrise data'
' (altitude above sea level in meters)'),
(CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_METRIC, None,
'{} for Metric, {} for Imperial'.format(CONF_UNIT_SYSTEM_METRIC,
CONF_UNIT_SYSTEM_IMPERIAL)),
(CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki'
'pedia.org/wiki/List_of_tz_database_time_zones'),
(CONF_CUSTOMIZE, '!include customize.yaml', None, 'Customization file'),
) # type: Tuple[Tuple[str, Any, Any, Optional[str]], ...]
DEFAULT_CONFIG = """ DEFAULT_CONFIG = """
# Configure a default setup of Home Assistant (frontend, api, etc) # Configure a default setup of Home Assistant (frontend, api, etc)
default_config: default_config:
@ -207,8 +190,7 @@ def get_default_config_dir() -> str:
return os.path.join(data_dir, CONFIG_DIR_NAME) # type: ignore return os.path.join(data_dir, CONFIG_DIR_NAME) # type: ignore
async def async_ensure_config_exists(hass: HomeAssistant, config_dir: str, async def async_ensure_config_exists(hass: HomeAssistant, config_dir: str) \
detect_location: bool = True)\
-> Optional[str]: -> Optional[str]:
"""Ensure a configuration file exists in given configuration directory. """Ensure a configuration file exists in given configuration directory.
@ -220,49 +202,22 @@ async def async_ensure_config_exists(hass: HomeAssistant, config_dir: str,
if config_path is None: if config_path is None:
print("Unable to find configuration. Creating default one in", print("Unable to find configuration. Creating default one in",
config_dir) config_dir)
config_path = await async_create_default_config( config_path = await async_create_default_config(hass, config_dir)
hass, config_dir, detect_location)
return config_path return config_path
async def async_create_default_config( async def async_create_default_config(hass: HomeAssistant, config_dir: str) \
hass: HomeAssistant, config_dir: str, detect_location: bool = True -> Optional[str]:
) -> Optional[str]:
"""Create a default configuration file in given configuration directory. """Create a default configuration file in given configuration directory.
Return path to new config file if success, None if failed. Return path to new config file if success, None if failed.
This method needs to run in an executor. This method needs to run in an executor.
""" """
info = {attr: default for attr, default, _, _ in DEFAULT_CORE_CONFIG} return await hass.async_add_executor_job(_write_default_config, config_dir)
if detect_location:
session = hass.helpers.aiohttp_client.async_get_clientsession()
location_info = await loc_util.async_detect_location_info(session)
else:
location_info = None
if location_info:
if location_info.use_metric:
info[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_METRIC
else:
info[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_IMPERIAL
for attr, default, prop, _ in DEFAULT_CORE_CONFIG:
if prop is None:
continue
info[attr] = getattr(location_info, prop) or default
if location_info.latitude and location_info.longitude:
info[CONF_ELEVATION] = await loc_util.async_get_elevation(
session, location_info.latitude, location_info.longitude)
return await hass.async_add_executor_job(
_write_default_config, config_dir, info
)
def _write_default_config(config_dir: str, info: Dict)\ def _write_default_config(config_dir: str)\
-> Optional[str]: -> Optional[str]:
"""Write the default config.""" """Write the default config."""
from homeassistant.components.config.group import ( from homeassistant.components.config.group import (
@ -271,8 +226,6 @@ def _write_default_config(config_dir: str, info: Dict)\
CONFIG_PATH as AUTOMATION_CONFIG_PATH) CONFIG_PATH as AUTOMATION_CONFIG_PATH)
from homeassistant.components.config.script import ( from homeassistant.components.config.script import (
CONFIG_PATH as SCRIPT_CONFIG_PATH) CONFIG_PATH as SCRIPT_CONFIG_PATH)
from homeassistant.components.config.customize import (
CONFIG_PATH as CUSTOMIZE_CONFIG_PATH)
config_path = os.path.join(config_dir, YAML_CONFIG_FILE) config_path = os.path.join(config_dir, YAML_CONFIG_FILE)
secret_path = os.path.join(config_dir, SECRET_YAML) secret_path = os.path.join(config_dir, SECRET_YAML)
@ -280,21 +233,11 @@ def _write_default_config(config_dir: str, info: Dict)\
group_yaml_path = os.path.join(config_dir, GROUP_CONFIG_PATH) group_yaml_path = os.path.join(config_dir, GROUP_CONFIG_PATH)
automation_yaml_path = os.path.join(config_dir, AUTOMATION_CONFIG_PATH) automation_yaml_path = os.path.join(config_dir, AUTOMATION_CONFIG_PATH)
script_yaml_path = os.path.join(config_dir, SCRIPT_CONFIG_PATH) script_yaml_path = os.path.join(config_dir, SCRIPT_CONFIG_PATH)
customize_yaml_path = os.path.join(config_dir, CUSTOMIZE_CONFIG_PATH)
# Writing files with YAML does not create the most human readable results # Writing files with YAML does not create the most human readable results
# So we're hard coding a YAML template. # So we're hard coding a YAML template.
try: try:
with open(config_path, 'wt') as config_file: with open(config_path, 'wt') as config_file:
config_file.write("homeassistant:\n")
for attr, _, _, description in DEFAULT_CORE_CONFIG:
if info[attr] is None:
continue
elif description:
config_file.write(" # {}\n".format(description))
config_file.write(" {}: {}\n".format(attr, info[attr]))
config_file.write(DEFAULT_CONFIG) config_file.write(DEFAULT_CONFIG)
with open(secret_path, 'wt') as secret_file: with open(secret_path, 'wt') as secret_file:
@ -312,9 +255,6 @@ def _write_default_config(config_dir: str, info: Dict)\
with open(script_yaml_path, 'wt'): with open(script_yaml_path, 'wt'):
pass pass
with open(customize_yaml_path, 'wt'):
pass
return config_path return config_path
except IOError: except IOError:
@ -576,55 +516,6 @@ async def async_process_ha_core_config(
"with '%s: %s'", CONF_TEMPERATURE_UNIT, unit, "with '%s: %s'", CONF_TEMPERATURE_UNIT, unit,
CONF_UNIT_SYSTEM, hac.units.name) CONF_UNIT_SYSTEM, hac.units.name)
# Shortcut if no auto-detection necessary
if None not in (hac.latitude, hac.longitude, hac.units,
hac.time_zone, hac.elevation):
return
discovered = [] # type: List[Tuple[str, Any]]
# If we miss some of the needed values, auto detect them
if None in (hac.latitude, hac.longitude, hac.units,
hac.time_zone):
hac.config_source = SOURCE_DISCOVERED
info = await loc_util.async_detect_location_info(
hass.helpers.aiohttp_client.async_get_clientsession()
)
if info is None:
_LOGGER.error("Could not detect location information")
return
if hac.latitude is None and hac.longitude is None:
hac.latitude, hac.longitude = (info.latitude, info.longitude)
discovered.append(('latitude', hac.latitude))
discovered.append(('longitude', hac.longitude))
if hac.units is None:
hac.units = METRIC_SYSTEM if info.use_metric else IMPERIAL_SYSTEM
discovered.append((CONF_UNIT_SYSTEM, hac.units.name))
if hac.location_name is None:
hac.location_name = info.city
discovered.append(('name', info.city))
if hac.time_zone is None:
hac.set_time_zone(info.time_zone)
discovered.append(('time_zone', info.time_zone))
if hac.elevation is None and hac.latitude is not None and \
hac.longitude is not None:
elevation = await loc_util.async_get_elevation(
hass.helpers.aiohttp_client.async_get_clientsession(),
hac.latitude, hac.longitude)
hac.elevation = elevation
discovered.append(('elevation', elevation))
if discovered:
_LOGGER.warning(
"Incomplete core configuration. Auto detected %s",
", ".join('{}: {}'.format(key, val) for key, val in discovered))
def _log_pkg_error( def _log_pkg_error(
package: str, component: str, config: Dict, message: str) -> None: package: str, component: str, config: Dict, message: str) -> None:

View file

@ -57,7 +57,7 @@ CALLABLE_T = TypeVar('CALLABLE_T', bound=Callable)
CALLBACK_TYPE = Callable[[], None] CALLBACK_TYPE = Callable[[], None]
# pylint: enable=invalid-name # pylint: enable=invalid-name
CORE_STORAGE_KEY = 'homeassistant.core_config' CORE_STORAGE_KEY = 'core.config'
CORE_STORAGE_VERSION = 1 CORE_STORAGE_VERSION = 1
DOMAIN = 'homeassistant' DOMAIN = 'homeassistant'
@ -1181,14 +1181,14 @@ class Config:
"""Initialize a new config object.""" """Initialize a new config object."""
self.hass = hass self.hass = hass
self.latitude = None # type: Optional[float] self.latitude = 0 # type: float
self.longitude = None # type: Optional[float] self.longitude = 0 # type: float
self.elevation = None # type: Optional[int] self.elevation = 0 # type: int
self.location_name = None # type: Optional[str] self.location_name = "Home" # type: str
self.time_zone = None # type: Optional[datetime.tzinfo] self.time_zone = dt_util.UTC # type: datetime.tzinfo
self.units = METRIC_SYSTEM # type: UnitSystem self.units = METRIC_SYSTEM # type: UnitSystem
self.config_source = None # type: Optional[str] self.config_source = "default" # type: str
# If True, pip install is skipped for requirements on startup # If True, pip install is skipped for requirements on startup
self.skip_pip = False # type: bool self.skip_pip = False # type: bool

View file

@ -65,30 +65,6 @@ def distance(lat1: Optional[float], lon1: Optional[float],
return result * 1000 return result * 1000
async def async_get_elevation(session: aiohttp.ClientSession, latitude: float,
longitude: float) -> int:
"""Return elevation for given latitude and longitude."""
try:
resp = await session.get(ELEVATION_URL, params={
'locations': '{},{}'.format(latitude, longitude),
}, timeout=5)
except (aiohttp.ClientError, asyncio.TimeoutError):
return 0
if resp.status != 200:
return 0
try:
raw_info = await resp.json()
except (aiohttp.ClientError, ValueError):
return 0
try:
return int(float(raw_info['results'][0]['elevation']))
except (ValueError, KeyError, IndexError):
return 0
# Author: https://github.com/maurycyp # Author: https://github.com/maurycyp
# Source: https://github.com/maurycyp/vincenty # Source: https://github.com/maurycyp/vincenty
# License: https://github.com/maurycyp/vincenty/blob/master/LICENSE # License: https://github.com/maurycyp/vincenty/blob/master/LICENSE

View file

@ -1,24 +1,31 @@
"""Test hassbian config.""" """Test hassbian config."""
import asyncio
from unittest.mock import patch from unittest.mock import patch
import pytest
from homeassistant.bootstrap import async_setup_component from homeassistant.bootstrap import async_setup_component
from homeassistant.components import config from homeassistant.components import config
from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.components.websocket_api.const import TYPE_RESULT
from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL
import homeassistant.util.dt as dt_util from homeassistant.util import dt as dt_util, location
from tests.common import mock_coro from tests.common import mock_coro
ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE
@pytest.fixture
async def client(hass, hass_ws_client):
"""Fixture that can interact with the config manager API."""
with patch.object(config, 'SECTIONS', ['core']):
assert await async_setup_component(hass, 'config', {})
return await hass_ws_client(hass)
async def test_validate_config_ok(hass, hass_client): async def test_validate_config_ok(hass, hass_client):
"""Test checking config.""" """Test checking config."""
with patch.object(config, 'SECTIONS', ['core']): with patch.object(config, 'SECTIONS', ['core']):
await async_setup_component(hass, 'config', {}) await async_setup_component(hass, 'config', {})
await asyncio.sleep(0.1, loop=hass.loop)
client = await hass_client() client = await hass_client()
with patch( with patch(
@ -42,11 +49,8 @@ async def test_validate_config_ok(hass, hass_client):
assert result['errors'] == 'beer' assert result['errors'] == 'beer'
async def test_websocket_core_update(hass, hass_ws_client): async def test_websocket_core_update(hass, client):
"""Test core config update websocket command.""" """Test core config update websocket command."""
with patch.object(config, 'SECTIONS', ['core']):
await async_setup_component(hass, 'config', {})
assert hass.config.latitude != 60 assert hass.config.latitude != 60
assert hass.config.longitude != 50 assert hass.config.longitude != 50
assert hass.config.elevation != 25 assert hass.config.elevation != 25
@ -54,7 +58,6 @@ async def test_websocket_core_update(hass, hass_ws_client):
assert hass.config.units.name != CONF_UNIT_SYSTEM_IMPERIAL assert hass.config.units.name != CONF_UNIT_SYSTEM_IMPERIAL
assert hass.config.time_zone.zone != 'America/New_York' assert hass.config.time_zone.zone != 'America/New_York'
client = await hass_ws_client(hass)
await client.send_json({ await client.send_json({
'id': 5, 'id': 5,
'type': 'config/core/update', 'type': 'config/core/update',
@ -92,7 +95,7 @@ async def test_websocket_core_update_not_admin(
await client.send_json({ await client.send_json({
'id': 6, 'id': 6,
'type': 'config/core/update', 'type': 'config/core/update',
'latitude': 123, 'latitude': 23,
}) })
msg = await client.receive_json() msg = await client.receive_json()
@ -103,16 +106,12 @@ async def test_websocket_core_update_not_admin(
assert msg['error']['code'] == 'unauthorized' assert msg['error']['code'] == 'unauthorized'
async def test_websocket_bad_core_update(hass, hass_ws_client): async def test_websocket_bad_core_update(hass, client):
"""Test core config update fails with bad parameters.""" """Test core config update fails with bad parameters."""
with patch.object(config, 'SECTIONS', ['core']):
await async_setup_component(hass, 'config', {})
client = await hass_ws_client(hass)
await client.send_json({ await client.send_json({
'id': 7, 'id': 7,
'type': 'config/core/update', 'type': 'config/core/update',
'latituude': 123, 'latituude': 23,
}) })
msg = await client.receive_json() msg = await client.receive_json()
@ -121,3 +120,48 @@ async def test_websocket_bad_core_update(hass, hass_ws_client):
assert msg['type'] == TYPE_RESULT assert msg['type'] == TYPE_RESULT
assert not msg['success'] assert not msg['success']
assert msg['error']['code'] == 'invalid_format' assert msg['error']['code'] == 'invalid_format'
async def test_detect_config(hass, client):
"""Test detect config."""
with patch('homeassistant.util.location.async_detect_location_info',
return_value=mock_coro(None)):
await client.send_json({
'id': 1,
'type': 'config/core/detect',
})
msg = await client.receive_json()
assert msg['success'] is True
assert msg['result'] == {}
async def test_detect_config_fail(hass, client):
"""Test detect config."""
with patch('homeassistant.util.location.async_detect_location_info',
return_value=mock_coro(location.LocationInfo(
ip=None,
country_code=None,
country_name=None,
region_code=None,
region_name=None,
city=None,
zip_code=None,
latitude=None,
longitude=None,
use_metric=True,
time_zone='Europe/Amsterdam',
))):
await client.send_json({
'id': 1,
'type': 'config/core/detect',
})
msg = await client.receive_json()
assert msg['success'] is True
assert msg['result'] == {
'unit_system': 'metric',
'time_zone': 'Europe/Amsterdam',
}

View file

@ -44,8 +44,6 @@ def check_real(func):
# Guard a few functions that would make network connections # Guard a few functions that would make network connections
location.async_detect_location_info = \ location.async_detect_location_info = \
check_real(location.async_detect_location_info) check_real(location.async_detect_location_info)
location.async_get_elevation = \
check_real(location.async_get_elevation)
util.get_local_ip = lambda: '127.0.0.1' util.get_local_ip = lambda: '127.0.0.1'

View file

@ -12,17 +12,16 @@ import pytest
from voluptuous import MultipleInvalid, Invalid from voluptuous import MultipleInvalid, Invalid
import yaml import yaml
from homeassistant.core import ( from homeassistant.core import SOURCE_STORAGE, HomeAssistantError
DOMAIN, SOURCE_STORAGE, Config, HomeAssistantError)
import homeassistant.config as config_util import homeassistant.config as config_util
from homeassistant.loader import async_get_integration from homeassistant.loader import async_get_integration
from homeassistant.const import ( from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE,
CONF_LATITUDE, CONF_LONGITUDE, CONF_UNIT_SYSTEM, CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_UNIT_SYSTEM, CONF_NAME,
CONF_TIME_ZONE, CONF_ELEVATION, CONF_CUSTOMIZE, __version__, CONF_CUSTOMIZE, __version__,
CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT,
CONF_AUTH_PROVIDERS, CONF_AUTH_MFA_MODULES) CONF_AUTH_PROVIDERS, CONF_AUTH_MFA_MODULES)
from homeassistant.util import location as location_util, dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util.yaml import SECRET_YAML from homeassistant.util.yaml import SECRET_YAML
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.components.config.group import ( from homeassistant.components.config.group import (
@ -31,12 +30,10 @@ from homeassistant.components.config.automation import (
CONFIG_PATH as AUTOMATIONS_CONFIG_PATH) CONFIG_PATH as AUTOMATIONS_CONFIG_PATH)
from homeassistant.components.config.script import ( from homeassistant.components.config.script import (
CONFIG_PATH as SCRIPTS_CONFIG_PATH) CONFIG_PATH as SCRIPTS_CONFIG_PATH)
from homeassistant.components.config.customize import (
CONFIG_PATH as CUSTOMIZE_CONFIG_PATH)
import homeassistant.scripts.check_config as check_config import homeassistant.scripts.check_config as check_config
from tests.common import ( from tests.common import (
get_test_config_dir, patch_yaml_files, mock_coro) get_test_config_dir, patch_yaml_files)
CONFIG_DIR = get_test_config_dir() CONFIG_DIR = get_test_config_dir()
YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE)
@ -45,7 +42,6 @@ VERSION_PATH = os.path.join(CONFIG_DIR, config_util.VERSION_FILE)
GROUP_PATH = os.path.join(CONFIG_DIR, GROUP_CONFIG_PATH) GROUP_PATH = os.path.join(CONFIG_DIR, GROUP_CONFIG_PATH)
AUTOMATIONS_PATH = os.path.join(CONFIG_DIR, AUTOMATIONS_CONFIG_PATH) AUTOMATIONS_PATH = os.path.join(CONFIG_DIR, AUTOMATIONS_CONFIG_PATH)
SCRIPTS_PATH = os.path.join(CONFIG_DIR, SCRIPTS_CONFIG_PATH) SCRIPTS_PATH = os.path.join(CONFIG_DIR, SCRIPTS_CONFIG_PATH)
CUSTOMIZE_PATH = os.path.join(CONFIG_DIR, CUSTOMIZE_CONFIG_PATH)
ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE
@ -77,20 +73,16 @@ def teardown():
if os.path.isfile(SCRIPTS_PATH): if os.path.isfile(SCRIPTS_PATH):
os.remove(SCRIPTS_PATH) os.remove(SCRIPTS_PATH)
if os.path.isfile(CUSTOMIZE_PATH):
os.remove(CUSTOMIZE_PATH)
async def test_create_default_config(hass): async def test_create_default_config(hass):
"""Test creation of default config.""" """Test creation of default config."""
await config_util.async_create_default_config(hass, CONFIG_DIR, False) await config_util.async_create_default_config(hass, CONFIG_DIR)
assert os.path.isfile(YAML_PATH) assert os.path.isfile(YAML_PATH)
assert os.path.isfile(SECRET_PATH) assert os.path.isfile(SECRET_PATH)
assert os.path.isfile(VERSION_PATH) assert os.path.isfile(VERSION_PATH)
assert os.path.isfile(GROUP_PATH) assert os.path.isfile(GROUP_PATH)
assert os.path.isfile(AUTOMATIONS_PATH) assert os.path.isfile(AUTOMATIONS_PATH)
assert os.path.isfile(CUSTOMIZE_PATH)
def test_find_config_file_yaml(): def test_find_config_file_yaml():
@ -106,7 +98,7 @@ async def test_ensure_config_exists_creates_config(hass):
If not creates a new config file. If not creates a new config file.
""" """
with mock.patch('builtins.print') as mock_print: with mock.patch('builtins.print') as mock_print:
await config_util.async_ensure_config_exists(hass, CONFIG_DIR, False) await config_util.async_ensure_config_exists(hass, CONFIG_DIR)
assert os.path.isfile(YAML_PATH) assert os.path.isfile(YAML_PATH)
assert mock_print.called assert mock_print.called
@ -115,7 +107,7 @@ async def test_ensure_config_exists_creates_config(hass):
async def test_ensure_config_exists_uses_existing_config(hass): async def test_ensure_config_exists_uses_existing_config(hass):
"""Test that calling ensure_config_exists uses existing config.""" """Test that calling ensure_config_exists uses existing config."""
create_file(YAML_PATH) create_file(YAML_PATH)
await config_util.async_ensure_config_exists(hass, CONFIG_DIR, False) await config_util.async_ensure_config_exists(hass, CONFIG_DIR)
with open(YAML_PATH) as f: with open(YAML_PATH) as f:
content = f.read() content = f.read()
@ -168,38 +160,6 @@ def test_load_yaml_config_preserves_key_order():
list(config_util.load_yaml_config_file(YAML_PATH).items()) list(config_util.load_yaml_config_file(YAML_PATH).items())
async def test_create_default_config_detect_location(hass):
"""Test that detect location sets the correct config keys."""
with mock.patch('homeassistant.util.location.async_detect_location_info',
return_value=mock_coro(location_util.LocationInfo(
'0.0.0.0', 'US', 'United States', 'CA', 'California',
'San Diego', '92122', 'America/Los_Angeles', 32.8594,
-117.2073, True))), \
mock.patch('homeassistant.util.location.async_get_elevation',
return_value=mock_coro(101)), \
mock.patch('builtins.print') as mock_print:
await config_util.async_ensure_config_exists(hass, CONFIG_DIR)
config = config_util.load_yaml_config_file(YAML_PATH)
assert DOMAIN in config
ha_conf = config[DOMAIN]
expected_values = {
CONF_LATITUDE: 32.8594,
CONF_LONGITUDE: -117.2073,
CONF_ELEVATION: 101,
CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
CONF_NAME: 'Home',
CONF_TIME_ZONE: 'America/Los_Angeles',
CONF_CUSTOMIZE: OrderedDict(),
}
assert expected_values == ha_conf
assert mock_print.called
async def test_create_default_config_returns_none_if_write_error(hass): async def test_create_default_config_returns_none_if_write_error(hass):
"""Test the writing of a default configuration. """Test the writing of a default configuration.
@ -207,7 +167,7 @@ async def test_create_default_config_returns_none_if_write_error(hass):
""" """
with mock.patch('builtins.print') as mock_print: with mock.patch('builtins.print') as mock_print:
assert await config_util.async_create_default_config( assert await config_util.async_create_default_config(
hass, os.path.join(CONFIG_DIR, 'non_existing_dir/'), False) is None hass, os.path.join(CONFIG_DIR, 'non_existing_dir/')) is None
assert mock_print.called assert mock_print.called
@ -418,7 +378,7 @@ def test_migrate_no_file_on_upgrade(mock_os, mock_shutil, hass):
async def test_loading_configuration_from_storage(hass, hass_storage): async def test_loading_configuration_from_storage(hass, hass_storage):
"""Test loading core config onto hass object.""" """Test loading core config onto hass object."""
hass_storage["homeassistant.core_config"] = { hass_storage["core.config"] = {
'data': { 'data': {
'elevation': 10, 'elevation': 10,
'latitude': 55, 'latitude': 55,
@ -427,7 +387,7 @@ async def test_loading_configuration_from_storage(hass, hass_storage):
'time_zone': 'Europe/Copenhagen', 'time_zone': 'Europe/Copenhagen',
'unit_system': 'metric' 'unit_system': 'metric'
}, },
'key': 'homeassistant.core_config', 'key': 'core.config',
'version': 1 'version': 1
} }
await config_util.async_process_ha_core_config( await config_util.async_process_ha_core_config(
@ -455,23 +415,23 @@ async def test_updating_configuration(hass, hass_storage):
'time_zone': 'Europe/Copenhagen', 'time_zone': 'Europe/Copenhagen',
'unit_system': 'metric' 'unit_system': 'metric'
}, },
'key': 'homeassistant.core_config', 'key': 'core.config',
'version': 1 'version': 1
} }
hass_storage["homeassistant.core_config"] = dict(core_data) hass_storage["core.config"] = dict(core_data)
await config_util.async_process_ha_core_config( await config_util.async_process_ha_core_config(
hass, {'whitelist_external_dirs': '/tmp'}) hass, {'whitelist_external_dirs': '/tmp'})
await hass.config.update(latitude=50) await hass.config.update(latitude=50)
new_core_data = copy.deepcopy(core_data) new_core_data = copy.deepcopy(core_data)
new_core_data['data']['latitude'] = 50 new_core_data['data']['latitude'] = 50
assert hass_storage["homeassistant.core_config"] == new_core_data assert hass_storage["core.config"] == new_core_data
assert hass.config.latitude == 50 assert hass.config.latitude == 50
async def test_override_stored_configuration(hass, hass_storage): async def test_override_stored_configuration(hass, hass_storage):
"""Test loading core and YAML config onto hass object.""" """Test loading core and YAML config onto hass object."""
hass_storage["homeassistant.core_config"] = { hass_storage["core.config"] = {
'data': { 'data': {
'elevation': 10, 'elevation': 10,
'latitude': 55, 'latitude': 55,
@ -480,7 +440,7 @@ async def test_override_stored_configuration(hass, hass_storage):
'time_zone': 'Europe/Copenhagen', 'time_zone': 'Europe/Copenhagen',
'unit_system': 'metric' 'unit_system': 'metric'
}, },
'key': 'homeassistant.core_config', 'key': 'core.config',
'version': 1 'version': 1
} }
await config_util.async_process_ha_core_config(hass, { await config_util.async_process_ha_core_config(hass, {
@ -571,59 +531,6 @@ async def test_loading_configuration_from_packages(hass):
}) })
@asynctest.mock.patch(
'homeassistant.util.location.async_detect_location_info',
autospec=True, return_value=mock_coro(location_util.LocationInfo(
'0.0.0.0', 'US', 'United States', 'CA',
'California', 'San Diego', '92122',
'America/Los_Angeles', 32.8594, -117.2073, True)))
@asynctest.mock.patch('homeassistant.util.location.async_get_elevation',
autospec=True, return_value=mock_coro(101))
async def test_discovering_configuration(mock_detect, mock_elevation, hass):
"""Test auto discovery for missing core configs."""
hass.config.latitude = None
hass.config.longitude = None
hass.config.elevation = None
hass.config.location_name = None
hass.config.time_zone = None
await config_util.async_process_ha_core_config(hass, {})
assert hass.config.latitude == 32.8594
assert hass.config.longitude == -117.2073
assert hass.config.elevation == 101
assert hass.config.location_name == 'San Diego'
assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC
assert hass.config.units.is_metric
assert hass.config.time_zone.zone == 'America/Los_Angeles'
assert hass.config.config_source == config_util.SOURCE_DISCOVERED
@asynctest.mock.patch('homeassistant.util.location.async_detect_location_info',
autospec=True, return_value=mock_coro(None))
@asynctest.mock.patch('homeassistant.util.location.async_get_elevation',
return_value=mock_coro(0))
async def test_discovering_configuration_auto_detect_fails(mock_detect,
mock_elevation,
hass):
"""Test config remains unchanged if discovery fails."""
hass.config = Config(hass)
hass.config.config_dir = "/test/config"
await config_util.async_process_ha_core_config(hass, {})
blankConfig = Config(hass)
assert hass.config.latitude == blankConfig.latitude
assert hass.config.longitude == blankConfig.longitude
assert hass.config.elevation == blankConfig.elevation
assert hass.config.location_name == blankConfig.location_name
assert hass.config.units == blankConfig.units
assert hass.config.time_zone == blankConfig.time_zone
assert len(hass.config.whitelist_external_dirs) == 1
assert "/test/config/www" in hass.config.whitelist_external_dirs
assert hass.config.config_source == config_util.SOURCE_DISCOVERED
@asynctest.mock.patch( @asynctest.mock.patch(
'homeassistant.scripts.check_config.check_ha_config_file') 'homeassistant.scripts.check_config.check_ha_config_file')
async def test_check_ha_config_file_correct(mock_check, hass): async def test_check_ha_config_file_correct(mock_check, hass):

View file

@ -891,17 +891,17 @@ class TestConfig(unittest.TestCase):
"""Test as dict.""" """Test as dict."""
self.config.config_dir = '/tmp/ha-config' self.config.config_dir = '/tmp/ha-config'
expected = { expected = {
'latitude': None, 'latitude': 0,
'longitude': None, 'longitude': 0,
'elevation': None, 'elevation': 0,
CONF_UNIT_SYSTEM: METRIC_SYSTEM.as_dict(), CONF_UNIT_SYSTEM: METRIC_SYSTEM.as_dict(),
'location_name': None, 'location_name': "Home",
'time_zone': 'UTC', 'time_zone': 'UTC',
'components': set(), 'components': set(),
'config_dir': '/tmp/ha-config', 'config_dir': '/tmp/ha-config',
'whitelist_external_dirs': set(), 'whitelist_external_dirs': set(),
'version': __version__, 'version': __version__,
'config_source': None, 'config_source': "default",
} }
assert expected == self.config.as_dict() assert expected == self.config.as_dict()

View file

@ -116,10 +116,8 @@ async def test_detect_location_info_ip_api(aioclient_mock, session):
async def test_detect_location_info_both_queries_fail(session): async def test_detect_location_info_both_queries_fail(session):
"""Ensure we return None if both queries fail.""" """Ensure we return None if both queries fail."""
with patch('homeassistant.util.location.async_get_elevation', with patch('homeassistant.util.location._get_ipapi',
return_value=mock_coro(0)), \ return_value=mock_coro(None)), \
patch('homeassistant.util.location._get_ipapi',
return_value=mock_coro(None)), \
patch('homeassistant.util.location._get_ip_api', patch('homeassistant.util.location._get_ip_api',
return_value=mock_coro(None)): return_value=mock_coro(None)):
info = await location_util.async_detect_location_info( info = await location_util.async_detect_location_info(
@ -137,26 +135,3 @@ async def test_ip_api_query_raises(raising_session):
"""Test ip api query when the request to API fails.""" """Test ip api query when the request to API fails."""
info = await location_util._get_ip_api(raising_session) info = await location_util._get_ip_api(raising_session)
assert info is None assert info is None
async def test_elevation_query_raises(raising_session):
"""Test elevation when the request to API fails."""
elevation = await location_util.async_get_elevation(
raising_session, 10, 10, _test_real=True)
assert elevation == 0
async def test_elevation_query_fails(aioclient_mock, session):
"""Test elevation when the request to API fails."""
aioclient_mock.get(location_util.ELEVATION_URL, text='{}', status=401)
elevation = await location_util.async_get_elevation(
session, 10, 10, _test_real=True)
assert elevation == 0
async def test_elevation_query_nonjson(aioclient_mock, session):
"""Test if elevation API returns a non JSON value."""
aioclient_mock.get(location_util.ELEVATION_URL, text='{ I am not JSON }')
elevation = await location_util.async_get_elevation(
session, 10, 10, _test_real=True)
assert elevation == 0