diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 7f3dc2ac8f52..e02e074189cc 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -13,7 +13,8 @@ from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS) -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.helpers.config_validation import ( # noqa + PLATFORM_SCHEMA_BASE, PLATFORM_SCHEMA_2 as PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent diff --git a/homeassistant/config.py b/homeassistant/config.py index 0edadf6a78db..3fd138f54e4f 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -742,13 +742,19 @@ def async_process_component_config( async_log_exception(ex, domain, config, hass) return None - elif hasattr(component, 'PLATFORM_SCHEMA'): + elif (hasattr(component, 'PLATFORM_SCHEMA') or + hasattr(component, 'PLATFORM_SCHEMA_BASE')): platforms = [] for p_name, p_config in config_per_platform(config, domain): # Validate component specific platform schema try: - p_validated = component.PLATFORM_SCHEMA( # type: ignore - p_config) + if hasattr(component, 'PLATFORM_SCHEMA_BASE'): + p_validated = \ + component.PLATFORM_SCHEMA_BASE( # type: ignore + p_config) + else: + p_validated = component.PLATFORM_SCHEMA( # type: ignore + p_config) except vol.Invalid as ex: async_log_exception(ex, domain, config, hass) continue diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 56d64cd8fd94..f3371a267250 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -557,6 +557,15 @@ PLATFORM_SCHEMA = vol.Schema({ vol.Optional(CONF_SCAN_INTERVAL): time_period }, extra=vol.ALLOW_EXTRA) +# This will replace PLATFORM_SCHEMA once all base components are updated +PLATFORM_SCHEMA_2 = vol.Schema({ + vol.Required(CONF_PLATFORM): string, + vol.Optional(CONF_SCAN_INTERVAL): time_period +}) + +PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA_2.extend({ +}, extra=vol.ALLOW_EXTRA) + EVENT_SCHEMA = vol.Schema({ vol.Optional(CONF_ALIAS): string, vol.Required('event'): string, diff --git a/tests/common.py b/tests/common.py index 3452d945f224..0f9b372c161a 100644 --- a/tests/common.py +++ b/tests/common.py @@ -450,8 +450,8 @@ class MockModule: # pylint: disable=invalid-name def __init__(self, domain=None, dependencies=None, setup=None, requirements=None, config_schema=None, platform_schema=None, - async_setup=None, async_setup_entry=None, - async_unload_entry=None): + platform_schema_base=None, async_setup=None, + async_setup_entry=None, async_unload_entry=None): """Initialize the mock module.""" self.DOMAIN = domain self.DEPENDENCIES = dependencies or [] @@ -463,6 +463,9 @@ class MockModule: if platform_schema is not None: self.PLATFORM_SCHEMA = platform_schema + if platform_schema_base is not None: + self.PLATFORM_SCHEMA_BASE = platform_schema_base + if setup is not None: # We run this in executor, wrap it in function self.setup = lambda *args: setup(*args) diff --git a/tests/test_setup.py b/tests/test_setup.py index 2e44ee539d7b..6d2cc7700136 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -14,7 +14,8 @@ from homeassistant.const import ( import homeassistant.config as config_util from homeassistant import setup, loader import homeassistant.util.dt as dt_util -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers.config_validation import ( + PLATFORM_SCHEMA_2 as PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) from homeassistant.helpers import discovery from tests.common import \ @@ -94,18 +95,24 @@ class TestSetup: platform_schema = PLATFORM_SCHEMA.extend({ 'hello': str, }) + platform_schema_base = PLATFORM_SCHEMA_BASE.extend({ + }) loader.set_component( self.hass, 'platform_conf', - MockModule('platform_conf', platform_schema=platform_schema)) + MockModule('platform_conf', + platform_schema_base=platform_schema_base)) loader.set_component( self.hass, - 'platform_conf.whatever', MockPlatform('whatever')) + 'platform_conf.whatever', + MockPlatform('whatever', + platform_schema=platform_schema)) with assert_setup_component(0): assert setup.setup_component(self.hass, 'platform_conf', { 'platform_conf': { + 'platform': 'whatever', 'hello': 'world', 'invalid': 'extra', } @@ -121,6 +128,7 @@ class TestSetup: 'hello': 'world', }, 'platform_conf 2': { + 'platform': 'whatever', 'invalid': True } }) @@ -175,6 +183,107 @@ class TestSetup: assert 'platform_conf' in self.hass.config.components assert not config['platform_conf'] # empty + def test_validate_platform_config_2(self): + """Test component PLATFORM_SCHEMA_BASE prio over PLATFORM_SCHEMA.""" + platform_schema = PLATFORM_SCHEMA.extend({ + 'hello': str, + }) + platform_schema_base = PLATFORM_SCHEMA_BASE.extend({ + 'hello': 'world', + }) + loader.set_component( + self.hass, + 'platform_conf', + MockModule('platform_conf', + platform_schema=platform_schema, + platform_schema_base=platform_schema_base)) + + loader.set_component( + self.hass, + 'platform_conf.whatever', + MockPlatform('whatever', + platform_schema=platform_schema)) + + with assert_setup_component(0): + assert setup.setup_component(self.hass, 'platform_conf', { + # fail: no extra keys allowed in platform schema + 'platform_conf': { + 'platform': 'whatever', + 'hello': 'world', + 'invalid': 'extra', + } + }) + + self.hass.data.pop(setup.DATA_SETUP) + self.hass.config.components.remove('platform_conf') + + with assert_setup_component(1): + assert setup.setup_component(self.hass, 'platform_conf', { + # pass + 'platform_conf': { + 'platform': 'whatever', + 'hello': 'world', + }, + # fail: key hello violates component platform_schema_base + 'platform_conf 2': { + 'platform': 'whatever', + 'hello': 'there' + } + }) + + self.hass.data.pop(setup.DATA_SETUP) + self.hass.config.components.remove('platform_conf') + + def test_validate_platform_config_3(self): + """Test fallback to component PLATFORM_SCHEMA.""" + component_schema = PLATFORM_SCHEMA_BASE.extend({ + 'hello': str, + }) + platform_schema = PLATFORM_SCHEMA.extend({ + 'cheers': str, + 'hello': 'world', + }) + loader.set_component( + self.hass, + 'platform_conf', + MockModule('platform_conf', + platform_schema=component_schema)) + + loader.set_component( + self.hass, + 'platform_conf.whatever', + MockPlatform('whatever', + platform_schema=platform_schema)) + + with assert_setup_component(0): + assert setup.setup_component(self.hass, 'platform_conf', { + 'platform_conf': { + # fail: no extra keys allowed + 'hello': 'world', + 'invalid': 'extra', + } + }) + + self.hass.data.pop(setup.DATA_SETUP) + self.hass.config.components.remove('platform_conf') + + with assert_setup_component(1): + assert setup.setup_component(self.hass, 'platform_conf', { + # pass + 'platform_conf': { + 'platform': 'whatever', + 'hello': 'world', + }, + # fail: key hello violates component platform_schema + 'platform_conf 2': { + 'platform': 'whatever', + 'hello': 'there' + } + }) + + self.hass.data.pop(setup.DATA_SETUP) + self.hass.config.components.remove('platform_conf') + def test_component_not_found(self): """setup_component should not crash if component doesn't exist.""" assert not setup.setup_component(self.hass, 'non_existing')