diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 54490af3cfa..15df6907468 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -45,7 +45,7 @@ SCRIPT_ENTRY_SCHEMA = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({cv.slug: SCRIPT_ENTRY_SCHEMA}) + DOMAIN: cv.schema_with_slug_keys(SCRIPT_ENTRY_SCHEMA) }, extra=vol.ALLOW_EXTRA) SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) diff --git a/homeassistant/config.py b/homeassistant/config.py index 10d3ce21a00..0edadf6a78d 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -170,10 +170,9 @@ def _no_duplicate_auth_mfa_module(configs: Sequence[Dict[str, Any]]) \ return configs -PACKAGES_CONFIG_SCHEMA = vol.Schema({ - cv.slug: vol.Schema( # Package names are slugs - {cv.string: vol.Any(dict, list, None)}) # Component configuration -}) +PACKAGES_CONFIG_SCHEMA = cv.schema_with_slug_keys( # Package names are slugs + vol.Schema({cv.string: vol.Any(dict, list, None)}) # Component config +) CUSTOMIZE_DICT_SCHEMA = vol.Schema({ vol.Optional(ATTR_FRIENDLY_NAME): cv.string, @@ -627,7 +626,7 @@ def _identify_config_schema(module: ModuleType) -> \ except (AttributeError, KeyError): return None, None t_schema = str(schema) - if t_schema.startswith('{'): + if t_schema.startswith('{') or 'schema_with_slug_keys' in t_schema: return ('dict', schema) if t_schema.startswith(('[', 'All(.) -ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$") - # How long to wait till things that run on startup have to finish. TIMEOUT_EVENT_START = 15 @@ -77,8 +73,12 @@ def split_entity_id(entity_id: str) -> List[str]: def valid_entity_id(entity_id: str) -> bool: - """Test if an entity ID is a valid format.""" - return ENTITY_ID_PATTERN.match(entity_id) is not None + """Test if an entity ID is a valid format. + + Format: . where both are slugs. + """ + return ('.' in entity_id and + slugify(entity_id) == entity_id.replace('.', '_', 1)) def valid_state(state: str) -> bool: diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 92fe935085a..ef0166bc16d 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -319,7 +319,23 @@ def service(value): .format(value)) -def slug(value): +def schema_with_slug_keys(value_schema: Union[T, Callable]) -> Callable: + """Ensure dicts have slugs as keys. + + Replacement of vol.Schema({cv.slug: value_schema}) to prevent misleading + "Extra keys" errors from voluptuous. + """ + schema = vol.Schema({str: value_schema}) + + def verify(value: Dict) -> Dict: + """Validate all keys are slugs and then the value_schema.""" + for key in value.keys(): + slug(key) + return schema(value) + return verify + + +def slug(value: Any) -> str: """Validate value is a valid slug.""" if value is None: raise vol.Invalid('Slug should not be None') @@ -330,7 +346,7 @@ def slug(value): raise vol.Invalid('invalid slug {} (try {})'.format(value, slg)) -def slugify(value): +def slugify(value: Any) -> str: """Coerce a value to a slug.""" if value is None: raise vol.Invalid('Slug should not be None') diff --git a/tests/components/switch/test_wake_on_lan.py b/tests/components/switch/test_wake_on_lan.py index c3f4e04057d..312a49b5183 100644 --- a/tests/components/switch/test_wake_on_lan.py +++ b/tests/components/switch/test_wake_on_lan.py @@ -139,11 +139,11 @@ class TestWOLSwitch(unittest.TestCase): 'mac_address': '00-01-02-03-04-05', 'host': 'validhostname', 'turn_off': { - 'service': 'shell_command.turn_off_TARGET', + 'service': 'shell_command.turn_off_target', }, } }) - calls = mock_service(self.hass, 'shell_command', 'turn_off_TARGET') + calls = mock_service(self.hass, 'shell_command', 'turn_off_target') state = self.hass.states.get('switch.wake_on_lan') assert STATE_OFF == state.state