-
-
Notifications
You must be signed in to change notification settings - Fork 37.2k
Add message template support for alert component #17516
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
d6ca0a2
Add problem text to message if available
thoscut a2cda72
Revert "Add problem text to message if available"
thoscut b5bd847
Cleanup setup
thoscut e188799
Add message template support
thoscut 5334bbe
Fix for failing test
thoscut 4640972
Added tests
thoscut 8aa2f8e
Refactor changes
thoscut 7ca6c31
Fix lint violation
thoscut 0aa9e84
Fix failing tests
thoscut 3fc5ff4
Unify handling for message and done_message parameter and sending fun…
thoscut 5bba043
Update tests
thoscut c072bee
Fix lint warnings
thoscut File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,23 +24,25 @@ | |
| 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() | ||
|
|
||
| # 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 | ||
| for alert in entities: | ||
| if alert.entity_id != alert_id: | ||
| continue | ||
|
|
||
| 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_handle_alert_service(service_call): | |
| 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_handle_alert_service(service_call): | |
| 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 @@ async def end_alerting(self): | |
| 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 @@ async def _notify(self, *args): | |
| 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: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. indentation contains mixed spaces and tabs |
||
| 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.""" | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,19 +17,21 @@ | |
| 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 @@ def tearDown(self): | |
| """Stop everything that was started.""" | ||
| self.hass.stop() | ||
|
|
||
| def _setup_notify(self): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. indentation contains mixed spaces and tabs |
||
| 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 @@ def record_event(event): | |
| 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) | ||
|
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indentation contains mixed spaces and tabs