Add message template support for alert component (#17516)

* Add problem text to message if available

* Revert "Add problem text to message if available"

This reverts commit 7be519bf7f.

* Cleanup setup

* Add message template support

* Fix for failing test

* Added tests

* Refactor changes

* Fix lint violation

* Fix failing tests

* Unify handling for message and done_message parameter and sending function

* Update tests

* Fix lint warnings
pull/18083/head
thoscut 2018-11-01 09:48:11 +01:00 committed by Paulus Schoutsen
parent 4163889c6b
commit bfa86b8138
2 changed files with 130 additions and 33 deletions

View File

@ -24,23 +24,25 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'alert' DOMAIN = 'alert'
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
CONF_DONE_MESSAGE = 'done_message'
CONF_CAN_ACK = 'can_acknowledge' CONF_CAN_ACK = 'can_acknowledge'
CONF_NOTIFIERS = 'notifiers' CONF_NOTIFIERS = 'notifiers'
CONF_REPEAT = 'repeat' CONF_REPEAT = 'repeat'
CONF_SKIP_FIRST = 'skip_first' CONF_SKIP_FIRST = 'skip_first'
CONF_ALERT_MESSAGE = 'message'
CONF_DONE_MESSAGE = 'done_message'
DEFAULT_CAN_ACK = True DEFAULT_CAN_ACK = True
DEFAULT_SKIP_FIRST = False DEFAULT_SKIP_FIRST = False
ALERT_SCHEMA = vol.Schema({ ALERT_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_DONE_MESSAGE): cv.string,
vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_STATE, default=STATE_ON): cv.string, vol.Required(CONF_STATE, default=STATE_ON): cv.string,
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]), vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean, vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean,
vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean, vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
vol.Optional(CONF_ALERT_MESSAGE): cv.template,
vol.Optional(CONF_DONE_MESSAGE): cv.template,
vol.Required(CONF_NOTIFIERS): cv.ensure_list}) vol.Required(CONF_NOTIFIERS): cv.ensure_list})
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
@ -62,31 +64,47 @@ def is_on(hass, entity_id):
async def async_setup(hass, config): async def async_setup(hass, config):
"""Set up the Alert component.""" """Set up the Alert component."""
alerts = config.get(DOMAIN) entities = []
all_alerts = {}
for object_id, cfg in config[DOMAIN].items():
if not cfg:
cfg = {}
name = cfg.get(CONF_NAME)
watched_entity_id = cfg.get(CONF_ENTITY_ID)
alert_state = cfg.get(CONF_STATE)
repeat = cfg.get(CONF_REPEAT)
skip_first = cfg.get(CONF_SKIP_FIRST)
message_template = cfg.get(CONF_ALERT_MESSAGE)
done_message_template = cfg.get(CONF_DONE_MESSAGE)
notifiers = cfg.get(CONF_NOTIFIERS)
can_ack = cfg.get(CONF_CAN_ACK)
entities.append(Alert(hass, object_id, name,
watched_entity_id, alert_state, repeat,
skip_first, message_template,
done_message_template, notifiers,
can_ack))
if not entities:
return False
async def async_handle_alert_service(service_call): async def async_handle_alert_service(service_call):
"""Handle calls to alert services.""" """Handle calls to alert services."""
alert_ids = service.extract_entity_ids(hass, service_call) alert_ids = service.extract_entity_ids(hass, service_call)
for alert_id in alert_ids: for alert_id in alert_ids:
alert = all_alerts[alert_id] for alert in entities:
alert.async_set_context(service_call.context) if alert.entity_id != alert_id:
if service_call.service == SERVICE_TURN_ON: continue
await alert.async_turn_on()
elif service_call.service == SERVICE_TOGGLE:
await alert.async_toggle()
else:
await alert.async_turn_off()
# Setup alerts alert.async_set_context(service_call.context)
for entity_id, alert in alerts.items(): if service_call.service == SERVICE_TURN_ON:
entity = Alert(hass, entity_id, await alert.async_turn_on()
alert[CONF_NAME], alert.get(CONF_DONE_MESSAGE), elif service_call.service == SERVICE_TOGGLE:
alert[CONF_ENTITY_ID], alert[CONF_STATE], await alert.async_toggle()
alert[CONF_REPEAT], alert[CONF_SKIP_FIRST], else:
alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK]) await alert.async_turn_off()
all_alerts[entity.entity_id] = entity
# Setup service calls # Setup service calls
hass.services.async_register( hass.services.async_register(
@ -99,7 +117,7 @@ async def async_setup(hass, config):
DOMAIN, SERVICE_TOGGLE, async_handle_alert_service, DOMAIN, SERVICE_TOGGLE, async_handle_alert_service,
schema=ALERT_SERVICE_SCHEMA) schema=ALERT_SERVICE_SCHEMA)
tasks = [alert.async_update_ha_state() for alert in all_alerts.values()] tasks = [alert.async_update_ha_state() for alert in entities]
if tasks: if tasks:
await asyncio.wait(tasks, loop=hass.loop) await asyncio.wait(tasks, loop=hass.loop)
@ -109,16 +127,25 @@ async def async_setup(hass, config):
class Alert(ToggleEntity): class Alert(ToggleEntity):
"""Representation of an alert.""" """Representation of an alert."""
def __init__(self, hass, entity_id, name, done_message, watched_entity_id, def __init__(self, hass, entity_id, name, watched_entity_id,
state, repeat, skip_first, notifiers, can_ack): state, repeat, skip_first, message_template,
done_message_template, notifiers, can_ack):
"""Initialize the alert.""" """Initialize the alert."""
self.hass = hass self.hass = hass
self._name = name self._name = name
self._alert_state = state self._alert_state = state
self._skip_first = skip_first self._skip_first = skip_first
self._message_template = message_template
if self._message_template is not None:
self._message_template.hass = hass
self._done_message_template = done_message_template
if self._done_message_template is not None:
self._done_message_template.hass = hass
self._notifiers = notifiers self._notifiers = notifiers
self._can_ack = can_ack self._can_ack = can_ack
self._done_message = done_message
self._delay = [timedelta(minutes=val) for val in repeat] self._delay = [timedelta(minutes=val) for val in repeat]
self._next_delay = 0 self._next_delay = 0
@ -184,7 +211,7 @@ class Alert(ToggleEntity):
self._cancel() self._cancel()
self._ack = False self._ack = False
self._firing = False self._firing = False
if self._done_message and self._send_done_message: if self._send_done_message:
await self._notify_done_message() await self._notify_done_message()
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
@ -204,18 +231,31 @@ class Alert(ToggleEntity):
if not self._ack: if not self._ack:
_LOGGER.info("Alerting: %s", self._name) _LOGGER.info("Alerting: %s", self._name)
self._send_done_message = True self._send_done_message = True
for target in self._notifiers:
await self.hass.services.async_call( if self._message_template is not None:
DOMAIN_NOTIFY, target, {ATTR_MESSAGE: self._name}) message = self._message_template.async_render()
else:
message = self._name
await self._send_notification_message(message)
await self._schedule_notify() await self._schedule_notify()
async def _notify_done_message(self, *args): async def _notify_done_message(self, *args):
"""Send notification of complete alert.""" """Send notification of complete alert."""
_LOGGER.info("Alerting: %s", self._done_message) _LOGGER.info("Alerting: %s", self._done_message_template)
self._send_done_message = False self._send_done_message = False
if self._done_message_template is None:
return
message = self._done_message_template.async_render()
await self._send_notification_message(message)
async def _send_notification_message(self, message):
for target in self._notifiers: for target in self._notifiers:
await self.hass.services.async_call( await self.hass.services.async_call(
DOMAIN_NOTIFY, target, {ATTR_MESSAGE: self._done_message}) DOMAIN_NOTIFY, target, {ATTR_MESSAGE: message})
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Async Unacknowledge alert.""" """Async Unacknowledge alert."""

View File

@ -17,19 +17,21 @@ from tests.common import get_test_home_assistant
NAME = "alert_test" NAME = "alert_test"
DONE_MESSAGE = "alert_gone" DONE_MESSAGE = "alert_gone"
NOTIFIER = 'test' NOTIFIER = 'test'
TEMPLATE = "{{ states.sensor.test.entity_id }}"
TEST_ENTITY = "sensor.test"
TEST_CONFIG = \ TEST_CONFIG = \
{alert.DOMAIN: { {alert.DOMAIN: {
NAME: { NAME: {
CONF_NAME: NAME, CONF_NAME: NAME,
alert.CONF_DONE_MESSAGE: DONE_MESSAGE, alert.CONF_DONE_MESSAGE: DONE_MESSAGE,
CONF_ENTITY_ID: "sensor.test", CONF_ENTITY_ID: TEST_ENTITY,
CONF_STATE: STATE_ON, CONF_STATE: STATE_ON,
alert.CONF_REPEAT: 30, alert.CONF_REPEAT: 30,
alert.CONF_SKIP_FIRST: False, alert.CONF_SKIP_FIRST: False,
alert.CONF_NOTIFIERS: [NOTIFIER]} alert.CONF_NOTIFIERS: [NOTIFIER]}
}} }}
TEST_NOACK = [NAME, NAME, DONE_MESSAGE, "sensor.test", TEST_NOACK = [NAME, NAME, "sensor.test",
STATE_ON, [30], False, NOTIFIER, False] STATE_ON, [30], False, None, None, NOTIFIER, False]
ENTITY_ID = alert.ENTITY_ID_FORMAT.format(NAME) ENTITY_ID = alert.ENTITY_ID_FORMAT.format(NAME)
@ -102,6 +104,19 @@ class TestAlert(unittest.TestCase):
"""Stop everything that was started.""" """Stop everything that was started."""
self.hass.stop() self.hass.stop()
def _setup_notify(self):
events = []
@callback
def record_event(event):
"""Add recorded event to set."""
events.append(event)
self.hass.services.register(
notify.DOMAIN, NOTIFIER, record_event)
return events
def test_is_on(self): def test_is_on(self):
"""Test is_on method.""" """Test is_on method."""
self.hass.states.set(ENTITY_ID, STATE_ON) self.hass.states.set(ENTITY_ID, STATE_ON)
@ -228,6 +243,48 @@ class TestAlert(unittest.TestCase):
self.hass.block_till_done() self.hass.block_till_done()
assert 2 == len(events) assert 2 == len(events)
def test_sending_non_templated_notification(self):
"""Test notifications."""
events = self._setup_notify()
assert setup_component(self.hass, alert.DOMAIN, TEST_CONFIG)
self.hass.states.set(TEST_ENTITY, STATE_ON)
self.hass.block_till_done()
self.assertEqual(1, len(events))
last_event = events[-1]
self.assertEqual(last_event.data[notify.ATTR_MESSAGE], NAME)
def test_sending_templated_notification(self):
"""Test templated notification."""
events = self._setup_notify()
config = deepcopy(TEST_CONFIG)
config[alert.DOMAIN][NAME][alert.CONF_ALERT_MESSAGE] = TEMPLATE
assert setup_component(self.hass, alert.DOMAIN, config)
self.hass.states.set(TEST_ENTITY, STATE_ON)
self.hass.block_till_done()
self.assertEqual(1, len(events))
last_event = events[-1]
self.assertEqual(last_event.data[notify.ATTR_MESSAGE], TEST_ENTITY)
def test_sending_templated_done_notification(self):
"""Test templated notification."""
events = self._setup_notify()
config = deepcopy(TEST_CONFIG)
config[alert.DOMAIN][NAME][alert.CONF_DONE_MESSAGE] = TEMPLATE
assert setup_component(self.hass, alert.DOMAIN, config)
self.hass.states.set(TEST_ENTITY, STATE_ON)
self.hass.block_till_done()
self.hass.states.set(TEST_ENTITY, STATE_OFF)
self.hass.block_till_done()
self.assertEqual(2, len(events))
last_event = events[-1]
self.assertEqual(last_event.data[notify.ATTR_MESSAGE], TEST_ENTITY)
def test_skipfirst(self): def test_skipfirst(self):
"""Test skipping first notification.""" """Test skipping first notification."""
config = deepcopy(TEST_CONFIG) config = deepcopy(TEST_CONFIG)