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
parent
4163889c6b
commit
bfa86b8138
|
@ -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."""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue