Skip to content

Commit

Permalink
Continuation of incomplete changes accidentally included in #135
Browse files Browse the repository at this point in the history
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
  • Loading branch information
NeonDaniel committed Jan 30, 2024
1 parent 8942be1 commit 292f5e2
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 27 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
70 changes: 44 additions & 26 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)
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

0 comments on commit 292f5e2

Please sign in to comment.