Fire event when core config is updated (#23922)

* Fire event when core config is updated
This commit is contained in:
Erik Montnemery 2019-05-20 20:02:36 +02:00 committed by GitHub
parent eb912be47a
commit afe9fc221e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 180 additions and 97 deletions

View file

@ -23,14 +23,16 @@ from homeassistant.const import (
__version__, CONF_CUSTOMIZE, CONF_CUSTOMIZE_DOMAIN, CONF_CUSTOMIZE_GLOB,
CONF_WHITELIST_EXTERNAL_DIRS, CONF_AUTH_PROVIDERS, CONF_AUTH_MFA_MODULES,
CONF_TYPE, CONF_ID)
from homeassistant.core import callback, DOMAIN as CONF_CORE, HomeAssistant
from homeassistant.core import (
DOMAIN as CONF_CORE, SOURCE_DISCOVERED, SOURCE_YAML, HomeAssistant,
callback)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import (
Integration, async_get_integration, IntegrationNotFound
)
from homeassistant.util.yaml import load_yaml, SECRET_YAML
import homeassistant.helpers.config_validation as cv
from homeassistant.util import dt as date_util, location as loc_util
from homeassistant.util import location as loc_util
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
from homeassistant.helpers.entity_values import EntityValues
from homeassistant.helpers import config_per_platform, extract_domain_configs
@ -50,13 +52,6 @@ FILE_MIGRATION = (
('ios.conf', '.ios.conf'),
)
CORE_STORAGE_KEY = 'homeassistant.core_config'
CORE_STORAGE_VERSION = 1
SOURCE_DISCOVERED = 'discovered'
SOURCE_STORAGE = 'storage'
SOURCE_YAML = 'yaml'
DEFAULT_CORE_CONFIG = (
# Tuples (attribute, default, auto detect property, description)
(CONF_NAME, 'Home', None, 'Name of the location where Home Assistant is '
@ -478,42 +473,6 @@ def _format_config_error(ex: vol.Invalid, domain: str, config: Dict) -> str:
return message
def _set_time_zone(hass: HomeAssistant, time_zone_str: Optional[str]) -> None:
"""Help to set the time zone."""
if time_zone_str is None:
return
time_zone = date_util.get_time_zone(time_zone_str)
if time_zone:
hass.config.time_zone = time_zone
date_util.set_default_time_zone(time_zone)
else:
_LOGGER.error("Received invalid time zone %s", time_zone_str)
async def async_load_ha_core_config(hass: HomeAssistant) -> None:
"""Store [homeassistant] core config."""
store = hass.helpers.storage.Store(CORE_STORAGE_VERSION, CORE_STORAGE_KEY,
private=True)
data = await store.async_load()
if not data:
return
hac = hass.config
hac.config_source = SOURCE_STORAGE
hac.latitude = data['latitude']
hac.longitude = data['longitude']
hac.elevation = data['elevation']
unit_system = data['unit_system']
if unit_system == CONF_UNIT_SYSTEM_IMPERIAL:
hac.units = IMPERIAL_SYSTEM
else:
hac.units = METRIC_SYSTEM
hac.location_name = data['location_name']
_set_time_zone(hass, data['time_zone'])
async def async_process_ha_core_config(
hass: HomeAssistant, config: Dict,
api_password: Optional[str] = None,
@ -552,7 +511,7 @@ async def async_process_ha_core_config(
auth_conf,
mfa_conf))
await async_load_ha_core_config(hass)
await hass.config.async_load()
hac = hass.config
@ -568,7 +527,8 @@ async def async_process_ha_core_config(
if key in config:
setattr(hac, attr, config[key])
_set_time_zone(hass, config.get(CONF_TIME_ZONE))
if CONF_TIME_ZONE in config:
hac.set_time_zone(config[CONF_TIME_ZONE])
# Init whitelist external dir
hac.whitelist_external_dirs = {hass.config.path('www')}
@ -649,7 +609,7 @@ async def async_process_ha_core_config(
discovered.append(('name', info.city))
if hac.time_zone is None:
_set_time_zone(hass, info.time_zone)
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 \
@ -666,24 +626,6 @@ async def async_process_ha_core_config(
", ".join('{}: {}'.format(key, val) for key, val in discovered))
async def async_store_ha_core_config(hass: HomeAssistant) -> None:
"""Store [homeassistant] core config."""
config = hass.config.as_dict()
data = {
'latitude': config['latitude'],
'longitude': config['longitude'],
'elevation': config['elevation'],
'unit_system': hass.config.units.name,
'location_name': config['location_name'],
'time_zone': config['time_zone'],
}
store = hass.helpers.storage.Store(CORE_STORAGE_VERSION, CORE_STORAGE_KEY,
private=True)
await store.async_save(data)
def _log_pkg_error(
package: str, component: str, config: Dict, message: str) -> None:
"""Log an error while merging packages."""

View file

@ -160,21 +160,23 @@ CONF_XY = 'xy'
CONF_ZONE = 'zone'
# #### EVENTS ####
EVENT_AUTOMATION_TRIGGERED = 'automation_triggered'
EVENT_CALL_SERVICE = 'call_service'
EVENT_COMPONENT_LOADED = 'component_loaded'
EVENT_CORE_CONFIG_UPDATE = 'core_config_updated'
EVENT_HOMEASSISTANT_CLOSE = 'homeassistant_close'
EVENT_HOMEASSISTANT_START = 'homeassistant_start'
EVENT_HOMEASSISTANT_STOP = 'homeassistant_stop'
EVENT_HOMEASSISTANT_CLOSE = 'homeassistant_close'
EVENT_STATE_CHANGED = 'state_changed'
EVENT_TIME_CHANGED = 'time_changed'
EVENT_CALL_SERVICE = 'call_service'
EVENT_LOGBOOK_ENTRY = 'logbook_entry'
EVENT_PLATFORM_DISCOVERED = 'platform_discovered'
EVENT_COMPONENT_LOADED = 'component_loaded'
EVENT_SCRIPT_STARTED = 'script_started'
EVENT_SERVICE_REGISTERED = 'service_registered'
EVENT_SERVICE_REMOVED = 'service_removed'
EVENT_LOGBOOK_ENTRY = 'logbook_entry'
EVENT_STATE_CHANGED = 'state_changed'
EVENT_THEMES_UPDATED = 'themes_updated'
EVENT_TIMER_OUT_OF_SYNC = 'timer_out_of_sync'
EVENT_AUTOMATION_TRIGGERED = 'automation_triggered'
EVENT_SCRIPT_STARTED = 'script_started'
EVENT_TIME_CHANGED = 'time_changed'
# #### DEVICE CLASSES ####
DEVICE_CLASS_BATTERY = 'battery'

View file

@ -27,12 +27,12 @@ import attr
import voluptuous as vol
from homeassistant.const import (
ATTR_DOMAIN, ATTR_FRIENDLY_NAME, ATTR_NOW, ATTR_SERVICE,
ATTR_SERVICE_DATA, ATTR_SECONDS, EVENT_CALL_SERVICE,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
EVENT_HOMEASSISTANT_CLOSE, EVENT_SERVICE_REMOVED,
EVENT_SERVICE_REGISTERED, EVENT_STATE_CHANGED,
EVENT_TIME_CHANGED, EVENT_TIMER_OUT_OF_SYNC, MATCH_ALL, __version__)
ATTR_DOMAIN, ATTR_FRIENDLY_NAME, ATTR_NOW, ATTR_SERVICE, ATTR_SERVICE_DATA,
ATTR_SECONDS, CONF_UNIT_SYSTEM_IMPERIAL, EVENT_CALL_SERVICE,
EVENT_CORE_CONFIG_UPDATE, EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_CLOSE, EVENT_SERVICE_REMOVED,
EVENT_SERVICE_REGISTERED, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED,
EVENT_TIMER_OUT_OF_SYNC, MATCH_ALL, __version__)
from homeassistant import loader
from homeassistant.exceptions import (
HomeAssistantError, InvalidEntityFormatError, InvalidStateError,
@ -43,7 +43,8 @@ from homeassistant.util.async_ import (
from homeassistant import util
import homeassistant.util.dt as dt_util
from homeassistant.util import location, slugify
from homeassistant.util.unit_system import UnitSystem, METRIC_SYSTEM # NOQA
from homeassistant.util.unit_system import ( # NOQA
UnitSystem, IMPERIAL_SYSTEM, METRIC_SYSTEM)
# Typing imports that create a circular dependency
# pylint: disable=using-constant-test
@ -56,11 +57,19 @@ CALLABLE_T = TypeVar('CALLABLE_T', bound=Callable)
CALLBACK_TYPE = Callable[[], None]
# pylint: enable=invalid-name
CORE_STORAGE_KEY = 'homeassistant.core_config'
CORE_STORAGE_VERSION = 1
DOMAIN = 'homeassistant'
# How long we wait for the result of a service call
SERVICE_CALL_LIMIT = 10 # seconds
# Source of core configuration
SOURCE_DISCOVERED = 'discovered'
SOURCE_STORAGE = 'storage'
SOURCE_YAML = 'yaml'
# How long to wait till things that run on startup have to finish.
TIMEOUT_EVENT_START = 15
@ -144,7 +153,7 @@ class HomeAssistant:
self.bus = EventBus(self)
self.services = ServiceRegistry(self)
self.states = StateMachine(self.bus, self.loop)
self.config = Config() # type: Config
self.config = Config(self) # type: Config
self.components = loader.Components(self)
self.helpers = loader.Helpers(self)
# This is a dictionary that any component can store any data on.
@ -1168,8 +1177,10 @@ class ServiceRegistry:
class Config:
"""Configuration settings for Home Assistant."""
def __init__(self) -> None:
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize a new config object."""
self.hass = hass
self.latitude = None # type: Optional[float]
self.longitude = None # type: Optional[float]
self.elevation = None # type: Optional[int]
@ -1235,7 +1246,7 @@ class Config:
return False
def as_dict(self) -> Dict:
"""Create a dictionary representation of this dict.
"""Create a dictionary representation of the configuration.
Async friendly.
"""
@ -1257,6 +1268,87 @@ class Config:
'config_source': self.config_source
}
def set_time_zone(self, time_zone_str: str) -> None:
"""Help to set the time zone."""
time_zone = dt_util.get_time_zone(time_zone_str)
if time_zone:
self.time_zone = time_zone
dt_util.set_default_time_zone(time_zone)
else:
raise ValueError(
"Received invalid time zone {}".format(time_zone_str))
@callback
def _update(self, *,
source: str,
latitude: Optional[float] = None,
longitude: Optional[float] = None,
elevation: Optional[int] = None,
unit_system: Optional[str] = None,
location_name: Optional[str] = None,
time_zone: Optional[str] = None) -> None:
"""Update the configuration from a dictionary.
Async friendly.
"""
self.config_source = source
if latitude is not None:
self.latitude = latitude
if longitude is not None:
self.longitude = longitude
if elevation is not None:
self.elevation = elevation
if unit_system is not None:
if unit_system == CONF_UNIT_SYSTEM_IMPERIAL:
self.units = IMPERIAL_SYSTEM
else:
self.units = METRIC_SYSTEM
if location_name is not None:
self.location_name = location_name
if time_zone is not None:
self.set_time_zone(time_zone)
async def update(self, **kwargs: Any) -> None:
"""Update the configuration from a dictionary.
Async friendly.
"""
self._update(source=SOURCE_STORAGE, **kwargs)
await self.async_store()
self.hass.bus.async_fire(
EVENT_CORE_CONFIG_UPDATE, kwargs
)
async def async_load(self) -> None:
"""Load [homeassistant] core config."""
store = self.hass.helpers.storage.Store(
CORE_STORAGE_VERSION, CORE_STORAGE_KEY, private=True)
data = await store.async_load()
if not data:
return
self._update(source=SOURCE_STORAGE, **data)
async def async_store(self) -> None:
"""Store [homeassistant] core config."""
time_zone = dt_util.UTC.zone
if self.time_zone and getattr(self.time_zone, 'zone'):
time_zone = getattr(self.time_zone, 'zone')
data = {
'latitude': self.latitude,
'longitude': self.longitude,
'elevation': self.elevation,
'unit_system': self.units.name,
'location_name': self.location_name,
'time_zone': time_zone,
}
store = self.hass.helpers.storage.Store(
CORE_STORAGE_VERSION, CORE_STORAGE_KEY, private=True)
await store.async_save(data)
def _async_create_timer(hass: HomeAssistant) -> None:
"""Create a timer that will start on HOMEASSISTANT_START."""

View file

@ -122,7 +122,6 @@ def get_test_home_assistant():
async def async_test_home_assistant(loop):
"""Return a Home Assistant object pointing at test config dir."""
hass = ha.HomeAssistant(loop)
hass.config.async_load = Mock()
store = auth_store.AuthStore(hass)
hass.auth = auth.AuthManager(hass, store, {}, {})
ensure_auth_manager_loaded(hass.auth)

View file

@ -1,6 +1,7 @@
"""Test config utils."""
# pylint: disable=protected-access
import asyncio
import copy
import os
import unittest.mock as mock
from collections import OrderedDict
@ -11,7 +12,8 @@ import pytest
from voluptuous import MultipleInvalid, Invalid
import yaml
from homeassistant.core import DOMAIN, HomeAssistantError, Config
from homeassistant.core import (
DOMAIN, SOURCE_STORAGE, Config, HomeAssistantError)
import homeassistant.config as config_util
from homeassistant.loader import async_get_integration
from homeassistant.const import (
@ -439,7 +441,32 @@ async def test_loading_configuration_from_storage(hass, hass_storage):
assert hass.config.time_zone.zone == 'Europe/Copenhagen'
assert len(hass.config.whitelist_external_dirs) == 2
assert '/tmp' in hass.config.whitelist_external_dirs
assert hass.config.config_source == config_util.SOURCE_STORAGE
assert hass.config.config_source == SOURCE_STORAGE
async def test_updating_configuration(hass, hass_storage):
"""Test updating configuration stores the new configuration."""
core_data = {
'data': {
'elevation': 10,
'latitude': 55,
'location_name': 'Home',
'longitude': 13,
'time_zone': 'Europe/Copenhagen',
'unit_system': 'metric'
},
'key': 'homeassistant.core_config',
'version': 1
}
hass_storage["homeassistant.core_config"] = dict(core_data)
await config_util.async_process_ha_core_config(
hass, {'whitelist_external_dirs': '/tmp'})
await hass.config.update(latitude=50)
new_core_data = copy.deepcopy(core_data)
new_core_data['data']['latitude'] = 50
assert hass_storage["homeassistant.core_config"] == new_core_data
assert hass.config.latitude == 50
async def test_override_stored_configuration(hass, hass_storage):
@ -474,8 +501,6 @@ async def test_override_stored_configuration(hass, hass_storage):
async def test_loading_configuration(hass):
"""Test loading core config onto hass object."""
hass.config = mock.Mock()
await config_util.async_process_ha_core_config(hass, {
'latitude': 60,
'longitude': 50,
@ -499,8 +524,6 @@ async def test_loading_configuration(hass):
async def test_loading_configuration_temperature_unit(hass):
"""Test backward compatibility when loading core config."""
hass.config = mock.Mock()
await config_util.async_process_ha_core_config(hass, {
'latitude': 60,
'longitude': 50,
@ -521,8 +544,6 @@ async def test_loading_configuration_temperature_unit(hass):
async def test_loading_configuration_from_packages(hass):
"""Test loading packages config onto hass object config."""
hass.config = mock.Mock()
await config_util.async_process_ha_core_config(hass, {
'latitude': 39,
'longitude': -1,
@ -586,12 +607,12 @@ async def test_discovering_configuration_auto_detect_fails(mock_detect,
mock_elevation,
hass):
"""Test config remains unchanged if discovery fails."""
hass.config = Config()
hass.config = Config(hass)
hass.config.config_dir = "/test/config"
await config_util.async_process_ha_core_config(hass, {})
blankConfig = Config()
blankConfig = Config(hass)
assert hass.config.latitude == blankConfig.latitude
assert hass.config.longitude == blankConfig.longitude
assert hass.config.elevation == blankConfig.elevation

View file

@ -23,7 +23,8 @@ from homeassistant.const import (
__version__, EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, CONF_UNIT_SYSTEM,
ATTR_NOW, EVENT_TIME_CHANGED, EVENT_TIMER_OUT_OF_SYNC, ATTR_SECONDS,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_CLOSE,
EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, EVENT_CALL_SERVICE)
EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, EVENT_CALL_SERVICE,
EVENT_CORE_CONFIG_UPDATE)
from tests.common import get_test_home_assistant, async_mock_service
@ -871,7 +872,7 @@ class TestConfig(unittest.TestCase):
# pylint: disable=invalid-name
def setUp(self):
"""Set up things to be run when tests are started."""
self.config = ha.Config()
self.config = ha.Config(None)
assert self.config.config_dir is None
def test_path_with_file(self):
@ -942,6 +943,32 @@ class TestConfig(unittest.TestCase):
self.config.is_allowed_path(None)
async def test_event_on_update(hass, hass_storage):
"""Test that event is fired on update."""
events = []
@ha.callback
def callback(event):
events.append(event)
hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, callback)
assert hass.config.latitude != 12
await hass.config.update(latitude=12)
await hass.async_block_till_done()
assert hass.config.latitude == 12
assert len(events) == 1
assert events[0].data == {'latitude': 12}
def test_bad_timezone_raises_value_error(hass):
"""Test bad timezone raises ValueError."""
with pytest.raises(ValueError):
hass.config.set_time_zone('not_a_timezone')
@patch('homeassistant.core.monotonic')
def test_create_timer(mock_monotonic, loop):
"""Test create timer."""