From bfa86b8138b0ab42ee07f53f9e491f151a1f18a6 Mon Sep 17 00:00:00 2001 From: thoscut Date: Thu, 1 Nov 2018 09:48:11 +0100 Subject: [PATCH] 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 7be519bf7fb58356b80bfa459ca4ae229ccd483c. * 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 --- homeassistant/components/alert.py | 100 +++++++++++++++++++++--------- tests/components/test_alert.py | 63 ++++++++++++++++++- 2 files changed, 130 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py index e224351f9db..759a2185047 100644 --- a/homeassistant/components/alert.py +++ b/homeassistant/components/alert.py @@ -24,23 +24,25 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'alert' ENTITY_ID_FORMAT = DOMAIN + '.{}' -CONF_DONE_MESSAGE = 'done_message' CONF_CAN_ACK = 'can_acknowledge' CONF_NOTIFIERS = 'notifiers' CONF_REPEAT = 'repeat' CONF_SKIP_FIRST = 'skip_first' +CONF_ALERT_MESSAGE = 'message' +CONF_DONE_MESSAGE = 'done_message' DEFAULT_CAN_ACK = True DEFAULT_SKIP_FIRST = False ALERT_SCHEMA = vol.Schema({ vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_DONE_MESSAGE): cv.string, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_STATE, default=STATE_ON): cv.string, 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_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}) CONFIG_SCHEMA = vol.Schema({ @@ -62,31 +64,47 @@ def is_on(hass, entity_id): async def async_setup(hass, config): """Set up the Alert component.""" - alerts = config.get(DOMAIN) - all_alerts = {} + entities = [] + + 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): """Handle calls to alert services.""" alert_ids = service.extract_entity_ids(hass, service_call) for alert_id in alert_ids: - alert = all_alerts[alert_id] - alert.async_set_context(service_call.context) - if service_call.service == SERVICE_TURN_ON: - await alert.async_turn_on() - elif service_call.service == SERVICE_TOGGLE: - await alert.async_toggle() - else: - await alert.async_turn_off() + for alert in entities: + if alert.entity_id != alert_id: + continue - # Setup alerts - for entity_id, alert in alerts.items(): - entity = Alert(hass, entity_id, - alert[CONF_NAME], alert.get(CONF_DONE_MESSAGE), - alert[CONF_ENTITY_ID], alert[CONF_STATE], - alert[CONF_REPEAT], alert[CONF_SKIP_FIRST], - alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK]) - all_alerts[entity.entity_id] = entity + alert.async_set_context(service_call.context) + if service_call.service == SERVICE_TURN_ON: + await alert.async_turn_on() + elif service_call.service == SERVICE_TOGGLE: + await alert.async_toggle() + else: + await alert.async_turn_off() # Setup service calls hass.services.async_register( @@ -99,7 +117,7 @@ async def async_setup(hass, config): DOMAIN, SERVICE_TOGGLE, async_handle_alert_service, 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: await asyncio.wait(tasks, loop=hass.loop) @@ -109,16 +127,25 @@ async def async_setup(hass, config): class Alert(ToggleEntity): """Representation of an alert.""" - def __init__(self, hass, entity_id, name, done_message, watched_entity_id, - state, repeat, skip_first, notifiers, can_ack): + def __init__(self, hass, entity_id, name, watched_entity_id, + state, repeat, skip_first, message_template, + done_message_template, notifiers, can_ack): """Initialize the alert.""" self.hass = hass self._name = name self._alert_state = state 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._can_ack = can_ack - self._done_message = done_message self._delay = [timedelta(minutes=val) for val in repeat] self._next_delay = 0 @@ -184,7 +211,7 @@ class Alert(ToggleEntity): self._cancel() self._ack = False self._firing = False - if self._done_message and self._send_done_message: + if self._send_done_message: await self._notify_done_message() self.async_schedule_update_ha_state() @@ -204,18 +231,31 @@ class Alert(ToggleEntity): if not self._ack: _LOGGER.info("Alerting: %s", self._name) self._send_done_message = True - for target in self._notifiers: - await self.hass.services.async_call( - DOMAIN_NOTIFY, target, {ATTR_MESSAGE: self._name}) + + if self._message_template is not None: + message = self._message_template.async_render() + else: + message = self._name + + await self._send_notification_message(message) await self._schedule_notify() async def _notify_done_message(self, *args): """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 + + 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: 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 Unacknowledge alert.""" diff --git a/tests/components/test_alert.py b/tests/components/test_alert.py index d7aaa3dd038..76610421563 100644 --- a/tests/components/test_alert.py +++ b/tests/components/test_alert.py @@ -17,19 +17,21 @@ from tests.common import get_test_home_assistant NAME = "alert_test" DONE_MESSAGE = "alert_gone" NOTIFIER = 'test' +TEMPLATE = "{{ states.sensor.test.entity_id }}" +TEST_ENTITY = "sensor.test" TEST_CONFIG = \ {alert.DOMAIN: { NAME: { CONF_NAME: NAME, alert.CONF_DONE_MESSAGE: DONE_MESSAGE, - CONF_ENTITY_ID: "sensor.test", + CONF_ENTITY_ID: TEST_ENTITY, CONF_STATE: STATE_ON, alert.CONF_REPEAT: 30, alert.CONF_SKIP_FIRST: False, alert.CONF_NOTIFIERS: [NOTIFIER]} }} -TEST_NOACK = [NAME, NAME, DONE_MESSAGE, "sensor.test", - STATE_ON, [30], False, NOTIFIER, False] +TEST_NOACK = [NAME, NAME, "sensor.test", + STATE_ON, [30], False, None, None, NOTIFIER, False] ENTITY_ID = alert.ENTITY_ID_FORMAT.format(NAME) @@ -102,6 +104,19 @@ class TestAlert(unittest.TestCase): """Stop everything that was started.""" 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): """Test is_on method.""" self.hass.states.set(ENTITY_ID, STATE_ON) @@ -228,6 +243,48 @@ class TestAlert(unittest.TestCase): self.hass.block_till_done() 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): """Test skipping first notification.""" config = deepcopy(TEST_CONFIG)