From 85a5d4ed40fa3ac6fdf012d544764eca173e5bca Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 25 Sep 2019 18:23:18 +0200 Subject: [PATCH 1/7] Improve validation of device trigger config --- .../components/automation/__init__.py | 67 ++++++++++++++++++- homeassistant/components/automation/device.py | 14 +++- homeassistant/components/config/__init__.py | 17 +++-- homeassistant/components/config/automation.py | 7 +- .../components/device_automation/__init__.py | 34 ++++++++++ homeassistant/config.py | 32 ++++++--- homeassistant/helpers/config_validation.py | 2 +- homeassistant/loader.py | 2 +- .../components/device_automation/test_init.py | 62 +++++++++++++++++ tests/helpers/test_check_config.py | 4 +- tests/scripts/test_check_config.py | 4 +- 11 files changed, 218 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index f669d415854b9d..bcf31455292c03 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -23,16 +23,22 @@ SERVICE_TURN_ON, STATE_ON, ) +from homeassistant.config import async_log_exception, config_without_domain from homeassistant.core import Context, CoreState, HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import condition, extract_domain_configs, script +from homeassistant.helpers import ( + condition, + config_per_platform, + extract_domain_configs, + script, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import TemplateVarsType -from homeassistant.loader import bind_hass +from homeassistant.loader import bind_hass, IntegrationNotFound from homeassistant.util.dt import parse_datetime, utcnow @@ -375,6 +381,63 @@ def device_state_attributes(self): return {CONF_ID: self._id} +async def async_validate_config_item(hass, config, full_config): + """Validate config item.""" + try: + config = PLATFORM_SCHEMA(config) + + triggers = [] + for trigger in config[CONF_TRIGGER]: + trigger_platform = importlib.import_module( + ".{}".format(trigger[CONF_PLATFORM]), __name__ + ) + if hasattr(trigger_platform, "async_validate_trigger_config"): + trigger = await trigger_platform.async_validate_trigger_config( + hass, trigger + ) + triggers.append(trigger) + config[CONF_TRIGGER] = triggers + + if CONF_CONDITION in config: + conditions = [] + for cond in config[CONF_CONDITION]: + cond = await condition.async_validate_condition_config(hass, cond) + conditions.append(cond) + config[CONF_CONDITION] = conditions + + actions = [] + for action in config[CONF_ACTION]: + action = await script.async_validate_action_config(hass, action) + actions.append(action) + config[CONF_ACTION] = actions + except (vol.Invalid, HomeAssistantError, IntegrationNotFound) as ex: + async_log_exception(ex, DOMAIN, full_config, hass) + return None + + return config + + +async def async_validate_config(hass, config): + """Validate config.""" + automations = [] + validated_automations = await asyncio.gather( + *( + async_validate_config_item(hass, p_config, config) + for _, p_config in config_per_platform(config, DOMAIN) + ) + ) + for validated_automation in validated_automations: + if validated_automation is not None: + automations.append(validated_automation) + + # Create a copy of the configuration with all config for current + # component removed and add validated config back in. + config = config_without_domain(config, DOMAIN) + config[DOMAIN] = automations + + return config + + async def _async_process_config(hass, config, component): """Process config and add automations. diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index fe2d65edef616b..d8c0eec0c5c51b 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -1,18 +1,28 @@ """Offer device oriented automation.""" import voluptuous as vol -from homeassistant.const import CONF_DOMAIN, CONF_PLATFORM +import homeassistant.components.device_automation as device_automation +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM from homeassistant.loader import async_get_integration # mypy: allow-untyped-defs, no-check-untyped-defs TRIGGER_SCHEMA = vol.Schema( - {vol.Required(CONF_PLATFORM): "device", vol.Required(CONF_DOMAIN): str}, + { + vol.Required(CONF_PLATFORM): "device", + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): str, + }, extra=vol.ALLOW_EXTRA, ) +async def async_validate_trigger_config(hass, config): + """Validate config.""" + return await device_automation.async_validate_trigger_config(hass, config) + + async def async_attach_trigger(hass, config, action, automation_info): """Listen for trigger.""" integration = await async_get_integration(hass, config[CONF_DOMAIN]) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 6d4b465fceb3dc..569d1de6a02ce8 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -5,10 +5,11 @@ import voluptuous as vol -from homeassistant.core import callback +from homeassistant.components.http import HomeAssistantView from homeassistant.const import EVENT_COMPONENT_LOADED, CONF_ID +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import ATTR_COMPONENT -from homeassistant.components.http import HomeAssistantView from homeassistant.util.yaml import load_yaml, dump DOMAIN = "config" @@ -80,6 +81,7 @@ def __init__( data_schema, *, post_write_hook=None, + data_validator=None, ): """Initialize a config view.""" self.url = f"/api/config/{component}/{config_type}/{{config_key}}" @@ -88,6 +90,7 @@ def __init__( self.key_schema = key_schema self.data_schema = data_schema self.post_write_hook = post_write_hook + self.data_validator = data_validator def _empty_config(self): """Empty config if file not found.""" @@ -128,14 +131,18 @@ async def post(self, request, config_key): except vol.Invalid as err: return self.json_message(f"Key malformed: {err}", 400) + hass = request.app["hass"] + try: # We just validate, we don't store that data because # we don't want to store the defaults. - self.data_schema(data) - except vol.Invalid as err: + if self.data_validator: + await self.data_validator(hass, data) + else: + self.data_schema(data) + except (vol.Invalid, HomeAssistantError) as err: return self.json_message(f"Message malformed: {err}", 400) - hass = request.app["hass"] path = hass.config.path(self.path) current = await self.read_config(hass) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 17efdba3fb573f..2ed30b5212f93a 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -2,7 +2,11 @@ from collections import OrderedDict import uuid -from homeassistant.components.automation import DOMAIN, PLATFORM_SCHEMA +from homeassistant.components.automation import ( + DOMAIN, + PLATFORM_SCHEMA, + async_validate_config_item, +) from homeassistant.const import CONF_ID, SERVICE_RELOAD import homeassistant.helpers.config_validation as cv @@ -26,6 +30,7 @@ async def hook(hass): cv.string, PLATFORM_SCHEMA, post_write_hook=hook, + data_validator=async_validate_config_item, ) ) return True diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index b444abd5238db4..3875c63fed5e0a 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -9,6 +9,8 @@ from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.loader import async_get_integration, IntegrationNotFound +from .exceptions import InvalidDeviceAutomationConfig + DOMAIN = "device_automation" _LOGGER = logging.getLogger(__name__) @@ -43,6 +45,38 @@ async def async_setup(hass, config): return True +async def async_get_device_automation_platform(hass, config, automation_type): + """Load device automation platform for integration. + + Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation. + """ + platform_name, _ = TYPES[automation_type] + try: + integration = await async_get_integration(hass, config[CONF_DOMAIN]) + platform = integration.get_platform(platform_name) + except IntegrationNotFound: + raise InvalidDeviceAutomationConfig( + f"Integration '{config[CONF_DOMAIN]}' not found" + ) + except ImportError: + raise InvalidDeviceAutomationConfig( + f"Integration '{config[CONF_DOMAIN]}' does not support device automation {automation_type}s" + ) + + return platform + + +async def async_validate_trigger_config(hass, config): + """Validate config.""" + platform = await async_get_device_automation_platform(hass, config, "trigger") + if not hasattr(platform, "async_get_triggers"): + raise InvalidDeviceAutomationConfig( + f"Integration '{config[CONF_DOMAIN]}' does not support device automation triggers" + ) + + return platform.TRIGGER_SCHEMA(config) + + async def _async_get_device_automations_from_domain( hass, domain, automation_type, device_id ): diff --git a/homeassistant/config.py b/homeassistant/config.py index d3bd97dad8f777..4b6f00a0605bde 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -416,7 +416,7 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: @callback def async_log_exception( - ex: vol.Invalid, domain: str, config: Dict, hass: HomeAssistant + ex: Exception, domain: str, config: Dict, hass: HomeAssistant ) -> None: """Log an error for configuration validation. @@ -428,23 +428,26 @@ def async_log_exception( @callback -def _format_config_error(ex: vol.Invalid, domain: str, config: Dict) -> str: +def _format_config_error(ex: Exception, domain: str, config: Dict) -> str: """Generate log exception for configuration validation. This method must be run in the event loop. """ message = f"Invalid config for [{domain}]: " - if "extra keys not allowed" in ex.error_message: - message += ( - "[{option}] is an invalid option for [{domain}]. " - "Check: {domain}->{path}.".format( - option=ex.path[-1], - domain=domain, - path="->".join(str(m) for m in ex.path), + if isinstance(ex, vol.Invalid): + if "extra keys not allowed" in ex.error_message: + message += ( + "[{option}] is an invalid option for [{domain}]. " + "Check: {domain}->{path}.".format( + option=ex.path[-1], + domain=domain, + path="->".join(str(m) for m in ex.path), + ) ) - ) + else: + message += "{}.".format(humanize_error(config, ex)) else: - message += "{}.".format(humanize_error(config, ex)) + message += str(ex) try: domain_config = config.get(domain, config) @@ -717,6 +720,13 @@ async def async_process_component_config( _LOGGER.error("Unable to import %s: %s", domain, ex) return None + if hasattr(component, "async_validate_config"): + try: + return await component.async_validate_config(hass, config) # type: ignore + except (vol.Invalid, HomeAssistantError) as ex: + async_log_exception(ex, domain, config, hass) + return None + if hasattr(component, "CONFIG_SCHEMA"): try: return component.CONFIG_SCHEMA(config) # type: ignore diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 113f2437ce8cea..d567962e328a8e 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -90,7 +90,7 @@ def validate(obj: Dict) -> Dict: for k in obj.keys(): if k in keys: return obj - raise vol.Invalid("must contain one of {}.".format(", ".join(keys))) + raise vol.Invalid("must contain at least one of {}.".format(", ".join(keys))) return validate diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 1a9a3d256acb2d..272271eefb31af 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -307,7 +307,7 @@ class IntegrationNotFound(LoaderError): def __init__(self, domain: str) -> None: """Initialize a component not found error.""" - super().__init__(f"Integration {domain} not found.") + super().__init__(f"Integration '{domain}' not found.") self.domain = domain diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index b05c04a16f1dc5..6dcd8391bf8954 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -2,6 +2,7 @@ import pytest from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.helpers import device_registry @@ -161,3 +162,64 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r assert msg["success"] triggers = msg["result"] assert _same_lists(triggers, expected_triggers) + + +async def test_automation_with_non_existing_integration(hass, caplog): + """Test device automation with non existing integration.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": { + "platform": "device", + "device_id": "none", + "domain": "beer", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert "Integration 'beer' not found" in caplog.text + + +async def test_automation_with_integration_without_device_trigger(hass, caplog): + """Test automation with integration without device trigger support.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": { + "platform": "device", + "device_id": "none", + "domain": "test", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert ( + "Integration 'test' does not support device automation triggers" in caplog.text + ) + + +async def test_automation_with_bad_trigger(hass, caplog): + """Test automation with bad device trigger.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "device", "domain": "light"}, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert "required key not provided" in caplog.text diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 9e5ea15293a484..5228f0d4882228 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -75,7 +75,7 @@ async def test_component_platform_not_found(hass, loop): assert res.keys() == {"homeassistant"} assert res.errors[0] == CheckConfigError( - "Component error: beer - Integration beer not found.", None, None + "Component error: beer - Integration 'beer' not found.", None, None ) # Only 1 error expected @@ -95,7 +95,7 @@ async def test_component_platform_not_found_2(hass, loop): assert res["light"] == [] assert res.errors[0] == CheckConfigError( - "Platform error light.beer - Integration beer not found.", None, None + "Platform error light.beer - Integration 'beer' not found.", None, None ) # Only 1 error expected diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index bd4f37bd1357c3..18143c088be58a 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -63,7 +63,7 @@ def test_component_platform_not_found(isfile_patch, loop): assert res["components"].keys() == {"homeassistant"} assert res["except"] == { check_config.ERROR_STR: [ - "Component error: beer - Integration beer not found." + "Component error: beer - Integration 'beer' not found." ] } assert res["secret_cache"] == {} @@ -77,7 +77,7 @@ def test_component_platform_not_found(isfile_patch, loop): assert res["components"]["light"] == [] assert res["except"] == { check_config.ERROR_STR: [ - "Platform error light.beer - Integration beer not found." + "Platform error light.beer - Integration 'beer' not found." ] } assert res["secret_cache"] == {} From aed2c694caa73bc4be168b2dbe11aebdde95131a Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 25 Sep 2019 19:46:41 +0200 Subject: [PATCH 2/7] Remove action and condition checks --- homeassistant/components/automation/__init__.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index bcf31455292c03..d13ce76462fbe2 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -397,19 +397,6 @@ async def async_validate_config_item(hass, config, full_config): ) triggers.append(trigger) config[CONF_TRIGGER] = triggers - - if CONF_CONDITION in config: - conditions = [] - for cond in config[CONF_CONDITION]: - cond = await condition.async_validate_condition_config(hass, cond) - conditions.append(cond) - config[CONF_CONDITION] = conditions - - actions = [] - for action in config[CONF_ACTION]: - action = await script.async_validate_action_config(hass, action) - actions.append(action) - config[CONF_ACTION] = actions except (vol.Invalid, HomeAssistantError, IntegrationNotFound) as ex: async_log_exception(ex, DOMAIN, full_config, hass) return None From a5563e8a7c87be798c037714fe4a01dea1613cc4 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 25 Sep 2019 20:24:25 +0200 Subject: [PATCH 3/7] Move config validation to own file --- .../components/automation/__init__.py | 54 +---------------- homeassistant/components/automation/config.py | 60 +++++++++++++++++++ homeassistant/components/config/automation.py | 7 +-- homeassistant/config.py | 13 +++- 4 files changed, 75 insertions(+), 59 deletions(-) create mode 100644 homeassistant/components/automation/config.py diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index d13ce76462fbe2..f669d415854b9d 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -23,22 +23,16 @@ SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.config import async_log_exception, config_without_domain from homeassistant.core import Context, CoreState, HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import ( - condition, - config_per_platform, - extract_domain_configs, - script, -) +from homeassistant.helpers import condition, extract_domain_configs, script import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import TemplateVarsType -from homeassistant.loader import bind_hass, IntegrationNotFound +from homeassistant.loader import bind_hass from homeassistant.util.dt import parse_datetime, utcnow @@ -381,50 +375,6 @@ def device_state_attributes(self): return {CONF_ID: self._id} -async def async_validate_config_item(hass, config, full_config): - """Validate config item.""" - try: - config = PLATFORM_SCHEMA(config) - - triggers = [] - for trigger in config[CONF_TRIGGER]: - trigger_platform = importlib.import_module( - ".{}".format(trigger[CONF_PLATFORM]), __name__ - ) - if hasattr(trigger_platform, "async_validate_trigger_config"): - trigger = await trigger_platform.async_validate_trigger_config( - hass, trigger - ) - triggers.append(trigger) - config[CONF_TRIGGER] = triggers - except (vol.Invalid, HomeAssistantError, IntegrationNotFound) as ex: - async_log_exception(ex, DOMAIN, full_config, hass) - return None - - return config - - -async def async_validate_config(hass, config): - """Validate config.""" - automations = [] - validated_automations = await asyncio.gather( - *( - async_validate_config_item(hass, p_config, config) - for _, p_config in config_per_platform(config, DOMAIN) - ) - ) - for validated_automation in validated_automations: - if validated_automation is not None: - automations.append(validated_automation) - - # Create a copy of the configuration with all config for current - # component removed and add validated config back in. - config = config_without_domain(config, DOMAIN) - config[DOMAIN] = automations - - return config - - async def _async_process_config(hass, config, component): """Process config and add automations. diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py new file mode 100644 index 00000000000000..46a599f2631735 --- /dev/null +++ b/homeassistant/components/automation/config.py @@ -0,0 +1,60 @@ +"""Config validation helper for the automation integration.""" +import asyncio +import importlib + +import voluptuous as vol + +from homeassistant.const import CONF_PLATFORM +from homeassistant.config import async_log_exception, config_without_domain +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_per_platform +from homeassistant.loader import IntegrationNotFound + +from . import CONF_TRIGGER, DOMAIN, PLATFORM_SCHEMA + +# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs, no-warn-return-any + + +async def async_validate_config_item(hass, config, full_config): + """Validate config item.""" + try: + config = PLATFORM_SCHEMA(config) + + triggers = [] + for trigger in config[CONF_TRIGGER]: + trigger_platform = importlib.import_module( + "..{}".format(trigger[CONF_PLATFORM]), __name__ + ) + if hasattr(trigger_platform, "async_validate_trigger_config"): + trigger = await trigger_platform.async_validate_trigger_config( + hass, trigger + ) + triggers.append(trigger) + config[CONF_TRIGGER] = triggers + except (vol.Invalid, HomeAssistantError, IntegrationNotFound) as ex: + async_log_exception(ex, DOMAIN, full_config, hass) + return None + + return config + + +async def async_validate_config(hass, config): + """Validate config.""" + automations = [] + validated_automations = await asyncio.gather( + *( + async_validate_config_item(hass, p_config, config) + for _, p_config in config_per_platform(config, DOMAIN) + ) + ) + for validated_automation in validated_automations: + if validated_automation is not None: + automations.append(validated_automation) + + # Create a copy of the configuration with all config for current + # component removed and add validated config back in. + config = config_without_domain(config, DOMAIN) + config[DOMAIN] = automations + + return config diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 2ed30b5212f93a..97ddf1e0714e69 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -2,11 +2,8 @@ from collections import OrderedDict import uuid -from homeassistant.components.automation import ( - DOMAIN, - PLATFORM_SCHEMA, - async_validate_config_item, -) +from homeassistant.components.automation import DOMAIN, PLATFORM_SCHEMA +from homeassistant.components.automation.config import async_validate_config_item from homeassistant.const import CONF_ID, SERVICE_RELOAD import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/config.py b/homeassistant/config.py index 4b6f00a0605bde..dc24112f193f5a 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -720,13 +720,22 @@ async def async_process_component_config( _LOGGER.error("Unable to import %s: %s", domain, ex) return None - if hasattr(component, "async_validate_config"): + # Check if the integration has a custom config validator + config_validator = None + try: + config_validator = integration.get_platform("config") + except ImportError: + pass + if config_validator is not None: try: - return await component.async_validate_config(hass, config) # type: ignore + return await config_validator.async_validate_config( + hass, config + ) # type: ignore except (vol.Invalid, HomeAssistantError) as ex: async_log_exception(ex, domain, config, hass) return None + # No custom config validator, proceed with schema validation if hasattr(component, "CONFIG_SCHEMA"): try: return component.CONFIG_SCHEMA(config) # type: ignore From f6f8826d5936b80695ae934abff6a33281ddd0e4 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 25 Sep 2019 20:45:56 +0200 Subject: [PATCH 4/7] Fix tests --- homeassistant/components/automation/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 46a599f2631735..04b764e271ca48 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -16,7 +16,7 @@ # mypy: no-check-untyped-defs, no-warn-return-any -async def async_validate_config_item(hass, config, full_config): +async def async_validate_config_item(hass, config, full_config=None): """Validate config item.""" try: config = PLATFORM_SCHEMA(config) @@ -33,7 +33,7 @@ async def async_validate_config_item(hass, config, full_config): triggers.append(trigger) config[CONF_TRIGGER] = triggers except (vol.Invalid, HomeAssistantError, IntegrationNotFound) as ex: - async_log_exception(ex, DOMAIN, full_config, hass) + async_log_exception(ex, DOMAIN, full_config or config, hass) return None return config From 93d7dc046afdf653bec9dd8dcac76c245698acf0 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Thu, 26 Sep 2019 16:27:05 +0200 Subject: [PATCH 5/7] Fixes --- homeassistant/components/automation/device.py | 30 +++++++++++-------- .../components/device_automation/__init__.py | 11 ------- homeassistant/config.py | 8 +++-- 3 files changed, 22 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index d8c0eec0c5c51b..77e702e0b3146e 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -2,29 +2,33 @@ import voluptuous as vol import homeassistant.components.device_automation as device_automation -from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM -from homeassistant.loader import async_get_integration +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.const import CONF_DOMAIN # mypy: allow-untyped-defs, no-check-untyped-defs -TRIGGER_SCHEMA = vol.Schema( - { - vol.Required(CONF_PLATFORM): "device", - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): str, - }, - extra=vol.ALLOW_EXTRA, -) +TRIGGER_SCHEMA = device_automation.TRIGGER_BASE_SCHEMA.extend(extra=vol.ALLOW_EXTRA) async def async_validate_trigger_config(hass, config): """Validate config.""" - return await device_automation.async_validate_trigger_config(hass, config) + platform = await device_automation.async_get_device_automation_platform( + hass, config, "trigger" + ) + if not hasattr(platform, "async_get_triggers"): + raise InvalidDeviceAutomationConfig( + f"Integration '{config[CONF_DOMAIN]}' does not support device automation triggers" + ) + + return platform.TRIGGER_SCHEMA(config) async def async_attach_trigger(hass, config, action, automation_info): """Listen for trigger.""" - integration = await async_get_integration(hass, config[CONF_DOMAIN]) - platform = integration.get_platform("device_trigger") + platform = await device_automation.async_get_device_automation_platform( + hass, config, "trigger" + ) return await platform.async_attach_trigger(hass, config, action, automation_info) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 3875c63fed5e0a..62d338ece54a49 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -66,17 +66,6 @@ async def async_get_device_automation_platform(hass, config, automation_type): return platform -async def async_validate_trigger_config(hass, config): - """Validate config.""" - platform = await async_get_device_automation_platform(hass, config, "trigger") - if not hasattr(platform, "async_get_triggers"): - raise InvalidDeviceAutomationConfig( - f"Integration '{config[CONF_DOMAIN]}' does not support device automation triggers" - ) - - return platform.TRIGGER_SCHEMA(config) - - async def _async_get_device_automations_from_domain( hass, domain, automation_type, device_id ): diff --git a/homeassistant/config.py b/homeassistant/config.py index dc24112f193f5a..0e840e1d003062 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -726,11 +726,13 @@ async def async_process_component_config( config_validator = integration.get_platform("config") except ImportError: pass - if config_validator is not None: + if config_validator is not None and hasattr( + config_validator, "async_validate_config" + ): try: - return await config_validator.async_validate_config( + return await config_validator.async_validate_config( # type: ignore hass, config - ) # type: ignore + ) except (vol.Invalid, HomeAssistantError) as ex: async_log_exception(ex, domain, config, hass) return None From 9982608c414b564b5560da5895b76da67e2b9265 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Thu, 26 Sep 2019 16:30:24 +0200 Subject: [PATCH 6/7] Fixes --- homeassistant/components/automation/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index 77e702e0b3146e..b675c8662eebcc 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -10,7 +10,7 @@ # mypy: allow-untyped-defs, no-check-untyped-defs -TRIGGER_SCHEMA = device_automation.TRIGGER_BASE_SCHEMA.extend(extra=vol.ALLOW_EXTRA) +TRIGGER_SCHEMA = device_automation.TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) async def async_validate_trigger_config(hass, config): From 079a442cd0b847c9c4d88022b88cd99b05d1f99c Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 27 Sep 2019 16:25:56 +0200 Subject: [PATCH 7/7] Small tweak --- homeassistant/components/automation/device.py | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index b675c8662eebcc..eb3e5a95c9c17e 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -1,34 +1,24 @@ """Offer device oriented automation.""" import voluptuous as vol -import homeassistant.components.device_automation as device_automation -from homeassistant.components.device_automation.exceptions import ( - InvalidDeviceAutomationConfig, +from homeassistant.components.device_automation import ( + TRIGGER_BASE_SCHEMA, + async_get_device_automation_platform, ) -from homeassistant.const import CONF_DOMAIN # mypy: allow-untyped-defs, no-check-untyped-defs -TRIGGER_SCHEMA = device_automation.TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) async def async_validate_trigger_config(hass, config): """Validate config.""" - platform = await device_automation.async_get_device_automation_platform( - hass, config, "trigger" - ) - if not hasattr(platform, "async_get_triggers"): - raise InvalidDeviceAutomationConfig( - f"Integration '{config[CONF_DOMAIN]}' does not support device automation triggers" - ) - + platform = await async_get_device_automation_platform(hass, config, "trigger") return platform.TRIGGER_SCHEMA(config) async def async_attach_trigger(hass, config, action, automation_info): """Listen for trigger.""" - platform = await device_automation.async_get_device_automation_platform( - hass, config, "trigger" - ) + platform = await async_get_device_automation_platform(hass, config, "trigger") return await platform.async_attach_trigger(hass, config, action, automation_info)