Skip to content

Commit

Permalink
Implement support for remote client alerts (#136)
Browse files Browse the repository at this point in the history
* Continuation of incomplete changes accidentally included in #135
Improved Message handling to maintain context in alert-related messages
Logs deprecation warning for legacy Klat support
Refactors `neon.alert` to `neon.alert_expired` for clarity
Adds handler to acknowledge an expired alert as missed or dismissed
Adds documentation for integrating with the Messagebus API

* Update skill.json

* Add TODO for future refactor

---------

Co-authored-by: Daniel McKnight <[email protected]>
Co-authored-by: NeonDaniel <[email protected]>
  • Loading branch information
3 people authored Feb 22, 2024
1 parent fdd0f75 commit f4d011a
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 31 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Summary

A skill to schedule alarms, timers, and reminders
A skill to schedule alarms, timers, and reminders.


## Description
Expand All @@ -14,6 +14,15 @@ was off, or you had quiet hours enabled.
Alarms and reminders may be set to recur daily or weekly. An active alert may be snoozed for a specified amount of time
while it is active. Any alerts that are not acknowledged will be added to a list of missed alerts that may be read and
cleared when requested.

Other modules may integrate with the alerts skill by listening for `neon.alert_expired` events. This event will be
emitted when a scheduled alert expires and will include any context associated with the event creation. If the event
was created with `mq` context, the mq connector module will forward the expired alert for the client module to handle
and the alert will be marked `active` until the client module emits a `neon.acknowledge_alert` Message with the `alert_id`
and `missed` data, i.e.:
```
Message("neon.acknowledge_alert", {"alert_id": <alert_id>, "missed": False}, <context>)
```


## Examples
Expand Down
72 changes: 45 additions & 27 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from ovos_utils import create_daemon
from ovos_utils.file_utils import resolve_resource_file
from ovos_utils.process_utils import RuntimeRequirements
from ovos_utils.log import LOG
from ovos_utils.log import LOG, log_deprecation
from ovos_utils.sound import play_audio
from adapt.intent import IntentBuilder
from lingua_franca.format import nice_duration, nice_time, nice_date_time
Expand Down Expand Up @@ -195,6 +195,7 @@ def initialize(self):
self.add_event("mycroft.ready", self.on_ready)

self.add_event("neon.get_events", self._get_events)
self.add_event("neon.acknowledge_alert", self._ack_alert)
self.add_event("alerts.gui.dismiss_notification",
self._gui_dismiss_notification)
self.add_event("ovos.gui.show.active.timers", self._on_display_gui)
Expand Down Expand Up @@ -983,45 +984,43 @@ def _alert_expired(self, alert: Alert):
:param alert: expired Alert object
"""
LOG.info(f'alert expired: {get_alert_id(alert)}')
# TODO: Emit generic event for remote clients
self.bus.emit(Message("neon.alert", alert.data, alert.context))
alert_msg = Message("neon.alert_expired", alert.data, alert.context)
self.bus.emit(alert_msg)
if alert.context.get("mq"):
LOG.warning("Alert from remote client; do nothing locally")
LOG.info("Alert from remote client; do nothing locally")
return
self.make_active()
self._gui_notify_expired(alert)

if alert.script_filename:
self._run_notify_expired(alert)
self._run_notify_expired(alert, alert_msg)
elif alert.audio_file:
self._play_notify_expired(alert)
self._play_notify_expired(alert, alert_msg)
elif alert.alert_type == AlertType.ALARM and not self.speak_alarm:
self._play_notify_expired(alert)
self._play_notify_expired(alert, alert_msg)
elif alert.alert_type == AlertType.TIMER and not self.speak_timer:
self._play_notify_expired(alert)
self._play_notify_expired(alert, alert_msg)
else:
self._speak_notify_expired(alert)
self._speak_notify_expired(alert, alert_msg)

def _run_notify_expired(self, alert: Alert):
def _run_notify_expired(self, alert: Alert, message: Message):
"""
Handle script file run on alert expiration
:param alert: Alert that has expired
"""
message = Message("neon.run_alert_script",
{"file_to_run": alert.script_filename},
alert.context)
# TODO: This is redundant, listeners should just use `neon.alert_expired`
message = message.forward("neon.run_alert_script",
{"file_to_run": alert.script_filename})
# emit a message telling CustomConversations to run a script
self.bus.emit(message)
# TODO: Validate alert was handled
LOG.info("The script has been executed with CC")
self.alert_manager.dismiss_active_alert(get_alert_id(alert))

def _play_notify_expired(self, alert: Alert):
def _play_notify_expired(self, alert: Alert, message: Message):
"""
Handle audio playback on alert expiration
:param alert: Alert that has expired
"""
alert_message = Message("neon.alert", alert.data, alert.context)
if alert.audio_file:
LOG.debug(alert.audio_file)
self.speak_dialog("expired_audio_alert_intro", private=True)
Expand All @@ -1035,41 +1034,43 @@ def _play_notify_expired(self, alert: Alert):
to_play = None

if not to_play:
self._speak_notify_expired(alert)
LOG.warning("Falling back to spoken notification")
self._speak_notify_expired(alert, message)
return

timeout = time.time() + self.alert_timeout_seconds
alert_id = get_alert_id(alert)
volume_message = Message("mycroft.volume.get")
volume_message = message.forward("mycroft.volume.get")
resp = self.bus.wait_for_response(volume_message)
if resp:
volume = resp.data.get('percent')
else:
volume = None
while self.alert_manager.get_alert_status(alert_id) == \
AlertState.ACTIVE and time.time() < timeout:
if alert_message.context.get("klat_data"):
# TODO: Deprecated
if message.context.get("klat_data"):
log_deprecation("`klat.response` emit will be removed. Listen "
"for `neon.alert_expired", "3.0.0")
self.send_with_audio(self.dialog_renderer.render(
"expired_alert", {'name': alert.alert_name}),
to_play, alert_message, private=True)
to_play, message, private=True)
else:
# TODO: refactor to `self.play_audio`
LOG.debug(f"Playing file: {to_play}")
play_audio(to_play).wait(60)
time.sleep(1) # TODO: Skip this and play continuously?
if self.escalate_volume:
self.bus.emit(Message("mycroft.volume.increase"))
self.bus.emit(message.forward("mycroft.volume.increase"))

if volume:
# Reset initial volume
self.bus.emit(Message("mycroft.volume.set", {"percent": volume}))
self.bus.emit(message.forward("mycroft.volume.set",
{"percent": volume}))
if self.alert_manager.get_alert_status(alert_id) == AlertState.ACTIVE:
self._missed_alert(alert_id)

def _speak_notify_expired(self, alert: Alert):
def _speak_notify_expired(self, alert: Alert, message: Message):
LOG.debug(f"notify alert expired: {get_alert_id(alert)}")
alert_message = Message("neon.alert", alert.data, alert.context)

# Notify user until they dismiss the alert
timeout = time.time() + self.alert_timeout_seconds
Expand All @@ -1079,11 +1080,11 @@ def _speak_notify_expired(self, alert: Alert):
if alert.alert_type == AlertType.REMINDER:
self.speak_dialog('expired_reminder',
{'name': alert.alert_name},
message=alert_message,
message=message,
private=True, wait=True)
else:
self.speak_dialog('expired_alert', {'name': alert.alert_name},
message=alert_message,
message=message,
private=True, wait=True)
self.make_active()
time.sleep(10)
Expand All @@ -1107,6 +1108,23 @@ def _missed_alert(self, alert_id: str):
self._create_notification(alert)
self._update_homescreen(do_alarms=True)

def _ack_alert(self, message: Message):
"""
Handle an emitted message acknowledging an expired alert.
@param message: neon.acknowledge_alert message
"""
alert_id = message.data.get('alert_id')
if not alert_id:
raise ValueError(f"Message data missing `alert_id`: {message.data}")
alert: Alert = self.alert_manager.active_alerts.get(alert_id)
if not alert:
LOG.error(f"Alert not active!: {alert_id}")
return
if message.data.get('missed'):
self._missed_alert(alert_id)
else:
self._dismiss_alert(alert_id, alert.alert_type)

def _dismiss_alert(self, alert_id: str, alert_type: AlertType,
speak: bool = False):
"""
Expand Down
6 changes: 3 additions & 3 deletions skill.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"title": "Alerts",
"url": "https://github.com/NeonGeckoCom/skill-alerts",
"summary": "A skill to schedule alarms, timers, and reminders",
"short_description": "A skill to schedule alarms, timers, and reminders",
"description": "The skill provides functionality to create alarms, timers and reminders, remove them by name, time, or type, and ask for what is active. You may also silence all alerts and ask for a summary of what was missed if you were away, your device was off, or you had quiet hours enabled. Alarms and reminders may be set to recur daily or weekly. An active alert may be snoozed for a specified amount of time while it is active. Any alerts that are not acknowledged will be added to a list of missed alerts that may be read and cleared when requested.",
"summary": "A skill to schedule alarms, timers, and reminders.",
"short_description": "A skill to schedule alarms, timers, and reminders.",
"description": "The skill provides functionality to create alarms, timers and reminders, remove them by name, time, or type, and ask for what is active. You may also silence all alerts and ask for a summary of what was missed if you were away, your device was off, or you had quiet hours enabled. Alarms and reminders may be set to recur daily or weekly. An active alert may be snoozed for a specified amount of time while it is active. Any alerts that are not acknowledged will be added to a list of missed alerts that may be read and cleared when requested. Other modules may integrate with the alerts skill by listening for `neon.alert_expired` events. This event will be emitted when a scheduled alert expires and will include any context associated with the event creation. If the event was created with `mq` context, the mq connector module will forward the expired alert for the client module to handle and the alert will be marked `active` until the client module emits a `neon.acknowledge_alert` Message with the `alert_id` and `missed` data, i.e.: ``` Message(\"neon.acknowledge_alert\", {\"alert_id\": <alert_id>, \"missed\": False}, <context>) ```",
"examples": [
"Set an alarm for 8 AM.",
"When is my next alarm?",
Expand Down

0 comments on commit f4d011a

Please sign in to comment.