Skip to content

Commit

Permalink
Merge 831d4f8 into 8ea0a8d
Browse files Browse the repository at this point in the history
  • Loading branch information
balloob committed Dec 13, 2018
2 parents 8ea0a8d + 831d4f8 commit 68a6022
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 4 deletions.
10 changes: 9 additions & 1 deletion homeassistant/components/automation/__init__.py
Expand Up @@ -375,7 +375,15 @@ def _async_get_action(hass, config, name):
async def action(entity_id, variables, context):
"""Execute an action."""
_LOGGER.info('Executing %s', name)
await script_obj.async_run(variables, context)
hass.components.logbook.async_log_entry(
name, 'has been triggered', DOMAIN, entity_id)

try:
await script_obj.async_run(variables, context)
except Exception as err: # pylint: disable=broad-except
script_obj.async_log_exception(
_LOGGER,
'Error while executing automation {}'.format(entity_id), err)

return action

Expand Down
44 changes: 41 additions & 3 deletions homeassistant/helpers/script.py
Expand Up @@ -85,6 +85,7 @@ def __init__(self, hass: HomeAssistant, sequence, name: str = None,
self.name = name
self._change_listener = change_listener
self._cur = -1
self._exception_step = None
self.last_action = None
self.last_triggered = None
self.can_cancel = any(CONF_DELAY in action or CONF_WAIT_TEMPLATE
Expand Down Expand Up @@ -136,10 +137,9 @@ async def async_run(self, variables: Optional[Sequence] = None,
return
except _StopScript:
break
except Exception as err:
except Exception:
# Store the step that had an exception
# pylint: disable=protected-access
err._script_step = cur
self._exception_step = cur
# Set script to not running
self._cur = -1
self.last_action = None
Expand All @@ -166,6 +166,44 @@ def async_stop(self) -> None:
if self._change_listener:
self.hass.async_add_job(self._change_listener)

@callback
def async_log_exception(self, logger, message_base, exception):
"""Log an exception for this script.
Should only be called on exceptions raised by this scripts async_run.
"""
# pylint: disable=protected-access
step = self._exception_step
action = self.sequence[step]
action_type = _determine_action(action)

error = None
meth = logger.error

if isinstance(exception, vol.Invalid):
error_desc = "Invalid data"

elif isinstance(exception, exceptions.TemplateError):
error_desc = "Error rendering template"

elif isinstance(exception, exceptions.Unauthorized):
error_desc = "Unauthorized"

elif isinstance(exception, exceptions.ServiceNotFound):
error_desc = "Service not found"

else:
# Print the full stack trace, unknown error
error_desc = 'Unknown error'
meth = logger.exception
error = ""

if error is None:
error = str(exception)

meth("%s. %s for %s at pos %s: %s",
message_base, error_desc, action_type, step + 1, error)

async def _handle_action(self, action, variables, context):
"""Handle an action."""
await self._actions[_determine_action(action)](
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/helpers/service.py
Expand Up @@ -55,9 +55,13 @@ async def async_call_from_config(hass, config, blocking=False, variables=None,
variables)
domain_service = cv.service(domain_service)
except TemplateError as ex:
if blocking:
raise
_LOGGER.error('Error rendering service name template: %s', ex)
return
except vol.Invalid:
if blocking:
raise
_LOGGER.error('Template rendered invalid service: %s',
domain_service)
return
Expand Down
21 changes: 21 additions & 0 deletions tests/components/automation/test_init.py
Expand Up @@ -864,3 +864,24 @@ def test_automation_not_trigger_on_bootstrap(hass):

assert len(calls) == 1
assert ['hello.world'] == calls[0].data.get(ATTR_ENTITY_ID)


async def test_automation_with_error_in_script(hass, caplog):
"""Test automation with an error in script."""
assert await async_setup_component(hass, automation.DOMAIN, {
automation.DOMAIN: {
'alias': 'hello',
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'action': {
'service': 'test.automation',
'entity_id': 'hello.world'
}
}
})

hass.bus.async_fire('test_event')
await hass.async_block_till_done()
assert 'Service test.automation not found' in caplog.text
35 changes: 35 additions & 0 deletions tests/helpers/test_script.py
Expand Up @@ -4,6 +4,7 @@
from unittest import mock
import unittest

import jinja2
import voluptuous as vol
import pytest

Expand Down Expand Up @@ -798,6 +799,7 @@ def record_event(event):
await script_obj.async_run()

assert len(events) == 0
assert script_obj._cur == -1


async def test_propagate_error_invalid_service_data(hass):
Expand Down Expand Up @@ -829,6 +831,7 @@ def record_call(service):

assert len(events) == 0
assert len(calls) == 0
assert script_obj._cur == -1


async def test_propagate_error_service_exception(hass):
Expand Down Expand Up @@ -859,3 +862,35 @@ def record_call(service):

assert len(events) == 0
assert len(calls) == 0
assert script_obj._cur == -1


def test_log_exception():
"""Test logged output."""
script_obj = script.Script(None, cv.SCRIPT_SCHEMA([
{'service': 'test.script'},
{'event': 'test_event'}]))
script_obj._exception_step = 1

for exc, msg in (
(vol.Invalid("Invalid number"), 'Invalid data'),
(exceptions.TemplateError(jinja2.TemplateError('Unclosed bracket')),
'Error rendering template'),
(exceptions.Unauthorized(), 'Unauthorized'),
(exceptions.ServiceNotFound('light', 'turn_on'), 'Service not found'),
(ValueError("Cannot parse JSON"), 'Unknown error'),
):
logger = mock.Mock()
script_obj.async_log_exception(logger, 'Test error', exc)

assert len(logger.mock_calls) == 1
p_format, p_msg_base, p_error_desc, p_action_type, p_step, p_error = \
logger.mock_calls[0][1]

assert p_error_desc == msg
assert p_action_type == script.ACTION_FIRE_EVENT
assert p_step == 2
if isinstance(exc, ValueError):
assert p_error == ""
else:
assert p_error == str(exc)

0 comments on commit 68a6022

Please sign in to comment.