diff --git a/homeassistant/config.py b/homeassistant/config.py index 2f916e69b76..44bf542f7cd 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -548,6 +548,31 @@ def _identify_config_schema(module): return '', schema +def _recursive_merge(pack_name, comp_name, config, conf, package): + """Merge package into conf, recursively.""" + for key, pack_conf in package.items(): + if isinstance(pack_conf, dict): + if not pack_conf: + continue + conf[key] = conf.get(key, OrderedDict()) + _recursive_merge(pack_name, comp_name, config, + conf=conf[key], package=pack_conf) + + elif isinstance(pack_conf, list): + if not pack_conf: + continue + conf[key] = cv.ensure_list(conf.get(key)) + conf[key].extend(cv.ensure_list(pack_conf)) + + else: + if conf.get(key) is not None: + _log_pkg_error( + pack_name, comp_name, config, + 'has keys that are defined multiple times') + else: + conf[key] = pack_conf + + def merge_packages_config(hass, config, packages, _log_pkg_error=_log_pkg_error): """Merge packages into the top-level configuration. Mutate config.""" @@ -607,11 +632,10 @@ def merge_packages_config(hass, config, packages, config[comp_name][key] = val continue - # The last merge type are sections that may occur only once + # The last merge type are sections that require recursive merging if comp_name in config: - _log_pkg_error( - pack_name, comp_name, config, "may occur only once" - " and it already exist in your main configuration") + _recursive_merge(pack_name, comp_name, config, + conf=config[comp_name], package=comp_conf) continue config[comp_name] = comp_conf diff --git a/tests/test_config.py b/tests/test_config.py index 4b1115c3814..d22d6b2acfd 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -654,21 +654,81 @@ def test_merge_type_mismatch(merge_log_err, hass): assert len(config['light']) == 2 -def test_merge_once_only(merge_log_err, hass): - """Test if we have a merge for a comp that may occur only once.""" - packages = { - 'pack_2': { - 'mqtt': {}, - 'api': {}, # No config schema - }, - } +def test_merge_once_only_keys(merge_log_err, hass): + """Test if we have a merge for a comp that may occur only once. Keys.""" + packages = {'pack_2': {'api': { + 'key_3': 3, + }}} config = { config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, - 'mqtt': {}, 'api': {} + 'api': { + 'key_1': 1, + 'key_2': 2, + } + } + config_util.merge_packages_config(hass, config, packages) + assert config['api'] == {'key_1': 1, 'key_2': 2, 'key_3': 3, } + + # Duplicate keys error + packages = {'pack_2': {'api': { + 'key': 2, + }}} + config = { + config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + 'api': {'key': 1, } } config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 1 - assert len(config) == 3 + + +def test_merge_once_only_lists(hass): + """Test if we have a merge for a comp that may occur only once. Lists.""" + packages = {'pack_2': {'api': { + 'list_1': ['item_2', 'item_3'], + 'list_2': ['item_1'], + 'list_3': [], + }}} + config = { + config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + 'api': { + 'list_1': ['item_1'], + } + } + config_util.merge_packages_config(hass, config, packages) + assert config['api'] == { + 'list_1': ['item_1', 'item_2', 'item_3'], + 'list_2': ['item_1'], + } + + +def test_merge_once_only_dictionaries(hass): + """Test if we have a merge for a comp that may occur only once. Dicts.""" + packages = {'pack_2': {'api': { + 'dict_1': { + 'key_2': 2, + 'dict_1.1': {'key_1.2': 1.2, }, + }, + 'dict_2': {'key_1': 1, }, + 'dict_3': {}, + }}} + config = { + config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + 'api': { + 'dict_1': { + 'key_1': 1, + 'dict_1.1': {'key_1.1': 1.1, } + }, + } + } + config_util.merge_packages_config(hass, config, packages) + assert config['api'] == { + 'dict_1': { + 'key_1': 1, + 'key_2': 2, + 'dict_1.1': {'key_1.1': 1.1, 'key_1.2': 1.2, }, + }, + 'dict_2': {'key_1': 1, }, + } def test_merge_id_schema(hass):