Fire event when core config is updated (#23922)
* Fire event when core config is updated
This commit is contained in:
parent
eb912be47a
commit
afe9fc221e
6 changed files with 180 additions and 97 deletions
|
@ -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."""
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."""
|
||||
|
|
Loading…
Add table
Reference in a new issue