Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 52 additions & 2 deletions homeassistant/components/automation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -375,6 +381,50 @@ def device_state_attributes(self):
return {CONF_ID: self._id}


async def async_validate_config_item(hass, config, full_config):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this into a new file config.py

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

"""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):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This too .

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

"""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.

Expand Down
14 changes: 12 additions & 2 deletions homeassistant/components/automation/device.py
Original file line number Diff line number Diff line change
@@ -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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should expand the TRIGGER_BASE_SCHEMA from device_automation and add extra=vol.ALLOW_EXTRA

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
TRIGGER_SCHEMA = vol.Schema(
TRIGGER_SCHEMA = device_automation.TRIGGER_BASE_SCHEMA.extend(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

{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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's inline this function here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed



async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for trigger."""
integration = await async_get_integration(hass, config[CONF_DOMAIN])
Expand Down
17 changes: 12 additions & 5 deletions homeassistant/components/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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}}"
Expand All @@ -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."""
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion homeassistant/components/config/automation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -26,6 +30,7 @@ async def hook(hass):
cv.string,
PLATFORM_SCHEMA,
post_write_hook=hook,
data_validator=async_validate_config_item,
)
)
return True
Expand Down
34 changes: 34 additions & 0 deletions homeassistant/components/device_automation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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
):
Expand Down
32 changes: 21 additions & 11 deletions homeassistant/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/helpers/config_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
62 changes: 62 additions & 0 deletions tests/components/device_automation/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions tests/helpers/test_check_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading