From fdf4f398a79e775e11dc9fdd391a8b53f7b773c5 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Wed, 9 Oct 2019 21:04:11 +0200
Subject: [PATCH] Support async validation of device trigger (#27333)

---
 .../components/automation/__init__.py         |   8 +-
 homeassistant/components/automation/config.py |  10 +-
 homeassistant/components/automation/device.py |   3 +
 .../components/deconz/device_trigger.py       |  16 ++-
 .../components/zha/device_trigger.py          |  14 ++-
 tests/components/zha/test_device_trigger.py   | 104 +++++++++---------
 6 files changed, 89 insertions(+), 66 deletions(-)

diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py
index f669d415854..3409ce832dd 100644
--- a/homeassistant/components/automation/__init__.py
+++ b/homeassistant/components/automation/__init__.py
@@ -7,9 +7,6 @@ from typing import Any, Awaitable, Callable
 
 import voluptuous as vol
 
-from homeassistant.components.device_automation.exceptions import (
-    InvalidDeviceAutomationConfig,
-)
 from homeassistant.const import (
     ATTR_ENTITY_ID,
     ATTR_NAME,
@@ -476,10 +473,7 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action):
     for conf in trigger_configs:
         platform = importlib.import_module(".{}".format(conf[CONF_PLATFORM]), __name__)
 
-        try:
-            remove = await platform.async_attach_trigger(hass, conf, action, info)
-        except InvalidDeviceAutomationConfig:
-            remove = False
+        remove = await platform.async_attach_trigger(hass, conf, action, info)
 
         if not remove:
             _LOGGER.error("Error setting up trigger %s", name)
diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py
index ebbd1771e84..5733cd2e83e 100644
--- a/homeassistant/components/automation/config.py
+++ b/homeassistant/components/automation/config.py
@@ -4,6 +4,9 @@ import importlib
 
 import voluptuous as vol
 
+from homeassistant.components.device_automation.exceptions import (
+    InvalidDeviceAutomationConfig,
+)
 from homeassistant.const import CONF_PLATFORM
 from homeassistant.config import async_log_exception, config_without_domain
 from homeassistant.exceptions import HomeAssistantError
@@ -52,7 +55,12 @@ async def _try_async_validate_config_item(hass, config, full_config=None):
     """Validate config item."""
     try:
         config = await async_validate_config_item(hass, config, full_config)
-    except (vol.Invalid, HomeAssistantError, IntegrationNotFound) as ex:
+    except (
+        vol.Invalid,
+        HomeAssistantError,
+        IntegrationNotFound,
+        InvalidDeviceAutomationConfig,
+    ) as ex:
         async_log_exception(ex, DOMAIN, full_config or config, hass)
         return None
 
diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py
index dc65008c3fb..ced8f65cbf5 100644
--- a/homeassistant/components/automation/device.py
+++ b/homeassistant/components/automation/device.py
@@ -18,6 +18,9 @@ async def async_validate_trigger_config(hass, config):
     platform = await async_get_device_automation_platform(
         hass, config[CONF_DOMAIN], "trigger"
     )
+    if hasattr(platform, "async_validate_trigger_config"):
+        return await getattr(platform, "async_validate_trigger_config")(hass, config)
+
     return platform.TRIGGER_SCHEMA(config)
 
 
diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py
index 9f66cf156aa..27ff6fcd590 100644
--- a/homeassistant/components/deconz/device_trigger.py
+++ b/homeassistant/components/deconz/device_trigger.py
@@ -204,8 +204,10 @@ def _get_deconz_event_from_device_id(hass, device_id):
     return None
 
 
-async def async_attach_trigger(hass, config, action, automation_info):
-    """Listen for state changes based on configuration."""
+async def async_validate_trigger_config(hass, config):
+    """Validate config."""
+    config = TRIGGER_SCHEMA(config)
+
     device_registry = await hass.helpers.device_registry.async_get_registry()
     device = device_registry.async_get(config[CONF_DEVICE_ID])
 
@@ -214,6 +216,16 @@ async def async_attach_trigger(hass, config, action, automation_info):
     if device.model not in REMOTES or trigger not in REMOTES[device.model]:
         raise InvalidDeviceAutomationConfig
 
+    return config
+
+
+async def async_attach_trigger(hass, config, action, automation_info):
+    """Listen for state changes based on configuration."""
+    device_registry = await hass.helpers.device_registry.async_get_registry()
+    device = device_registry.async_get(config[CONF_DEVICE_ID])
+
+    trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
+
     trigger = REMOTES[device.model][trigger]
 
     deconz_event = _get_deconz_event_from_device_id(hass, device.id)
diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py
index ddf7465e0c0..8d74ae108a2 100644
--- a/homeassistant/components/zha/device_trigger.py
+++ b/homeassistant/components/zha/device_trigger.py
@@ -21,8 +21,10 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
 )
 
 
-async def async_attach_trigger(hass, config, action, automation_info):
-    """Listen for state changes based on configuration."""
+async def async_validate_trigger_config(hass, config):
+    """Validate config."""
+    config = TRIGGER_SCHEMA(config)
+
     trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
     zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID])
 
@@ -32,6 +34,14 @@ async def async_attach_trigger(hass, config, action, automation_info):
     ):
         raise InvalidDeviceAutomationConfig
 
+    return config
+
+
+async def async_attach_trigger(hass, config, action, automation_info):
+    """Listen for state changes based on configuration."""
+    trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
+    zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID])
+
     trigger = zha_device.device_automation_triggers[trigger]
 
     event_config = {
diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py
index 2f4ddb6b8b2..8df1a072801 100644
--- a/tests/components/zha/test_device_trigger.py
+++ b/tests/components/zha/test_device_trigger.py
@@ -1,6 +1,4 @@
 """ZHA device automation trigger tests."""
-from unittest.mock import patch
-
 import pytest
 
 import homeassistant.components.automation as automation
@@ -197,7 +195,7 @@ async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls):
     assert calls[0].data["message"] == "service called"
 
 
-async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls):
+async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls, caplog):
     """Test for exception on event triggers firing."""
     from zigpy.zcl.clusters.general import OnOff, Basic
 
@@ -219,33 +217,32 @@ async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls):
     ha_device_registry = await async_get_registry(hass)
     reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set())
 
-    with patch("logging.Logger.error") as mock:
-        await async_setup_component(
-            hass,
-            automation.DOMAIN,
-            {
-                automation.DOMAIN: [
-                    {
-                        "trigger": {
-                            "device_id": reg_device.id,
-                            "domain": "zha",
-                            "platform": "device",
-                            "type": "junk",
-                            "subtype": "junk",
-                        },
-                        "action": {
-                            "service": "test.automation",
-                            "data": {"message": "service called"},
-                        },
-                    }
-                ]
-            },
-        )
-        await hass.async_block_till_done()
-        mock.assert_called_with("Error setting up trigger %s", "automation 0")
-
-
-async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls):
+    await async_setup_component(
+        hass,
+        automation.DOMAIN,
+        {
+            automation.DOMAIN: [
+                {
+                    "trigger": {
+                        "device_id": reg_device.id,
+                        "domain": "zha",
+                        "platform": "device",
+                        "type": "junk",
+                        "subtype": "junk",
+                    },
+                    "action": {
+                        "service": "test.automation",
+                        "data": {"message": "service called"},
+                    },
+                }
+            ]
+        },
+    )
+    await hass.async_block_till_done()
+    assert "Invalid config for [automation]" in caplog.text
+
+
+async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls, caplog):
     """Test for exception on event triggers firing."""
     from zigpy.zcl.clusters.general import OnOff, Basic
 
@@ -275,27 +272,26 @@ async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls):
     ha_device_registry = await async_get_registry(hass)
     reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set())
 
-    with patch("logging.Logger.error") as mock:
-        await async_setup_component(
-            hass,
-            automation.DOMAIN,
-            {
-                automation.DOMAIN: [
-                    {
-                        "trigger": {
-                            "device_id": reg_device.id,
-                            "domain": "zha",
-                            "platform": "device",
-                            "type": "junk",
-                            "subtype": "junk",
-                        },
-                        "action": {
-                            "service": "test.automation",
-                            "data": {"message": "service called"},
-                        },
-                    }
-                ]
-            },
-        )
-        await hass.async_block_till_done()
-        mock.assert_called_with("Error setting up trigger %s", "automation 0")
+    await async_setup_component(
+        hass,
+        automation.DOMAIN,
+        {
+            automation.DOMAIN: [
+                {
+                    "trigger": {
+                        "device_id": reg_device.id,
+                        "domain": "zha",
+                        "platform": "device",
+                        "type": "junk",
+                        "subtype": "junk",
+                    },
+                    "action": {
+                        "service": "test.automation",
+                        "data": {"message": "service called"},
+                    },
+                }
+            ]
+        },
+    )
+    await hass.async_block_till_done()
+    assert "Invalid config for [automation]" in caplog.text
-- 
GitLab