Skip to content
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

Implement support for remote client alerts #136

Merged
merged 3 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
NeonDaniel marked this conversation as resolved.
Show resolved Hide resolved
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"))
NeonDaniel marked this conversation as resolved.
Show resolved Hide resolved

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