From 365d101081cb3030aed74a200a04e58746bcadc8 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:24:13 -0800 Subject: [PATCH 01/12] Update tests to latest automation/shared classes (#135) * Update tests to use shared test class * Replace relative imports with full package spec * Update imports in test class to match skill module * Update leftover imports in test class to match skill module --------- Co-authored-by: Daniel McKnight --- __init__.py | 27 +++++++++------ requirements/test.txt | 1 + setup.py | 1 + test/test_skill.py | 79 +++++++++++++++++-------------------------- 4 files changed, 50 insertions(+), 58 deletions(-) create mode 100644 requirements/test.txt diff --git a/__init__.py b/__init__.py index 6c021329..2b3c30b7 100644 --- a/__init__.py +++ b/__init__.py @@ -47,16 +47,15 @@ from neon_utils.message_utils import request_from_mobile, dig_for_message from neon_utils.skills.neon_skill import NeonSkill from neon_utils.user_utils import get_user_prefs, get_message_user +from ovos_workshop.decorators import intent_handler -from mycroft.skills import intent_handler, intent_file_handler - -from .util import Weekdays, AlertState, MatchLevel, AlertPriority, WEEKDAYS, WEEKENDS, EVERYDAY -from .util.alert import Alert, AlertType -from .util.alert_manager import AlertManager, get_alert_id -from .util.parse_utils import build_alert_from_intent, spoken_time_remaining, \ +from skill_alerts.util import Weekdays, AlertState, MatchLevel, AlertPriority, WEEKDAYS, WEEKENDS, EVERYDAY +from skill_alerts.util.alert import Alert, AlertType +from skill_alerts.util.alert_manager import AlertManager, get_alert_id +from skill_alerts.util.parse_utils import build_alert_from_intent, spoken_time_remaining, \ parse_alert_name_from_message, tokenize_utterance, \ parse_alert_time_from_message -from .util.ui_models import build_timer_data, build_alarm_data +from skill_alerts.util.ui_models import build_timer_data, build_alarm_data class AlertSkill(NeonSkill): @@ -393,7 +392,7 @@ def handle_list_alerts(self, message): alerts_string = f"{alerts_string}\n{add_str}" self.speak(alerts_string, private=True) - @intent_file_handler('list_alerts.intent') + @intent_handler('list_alerts.intent') def alt_handle_list_alerts(self, message): """ Intent handler for "what are my alerts", "are there any alerts", etc. @@ -464,7 +463,7 @@ def handle_timer_status(self, message): to_speak = f"{to_speak}\n{part}" self.speak(to_speak.lstrip('\n'), private=True) - @intent_file_handler("quiet_hours_start.intent") + @intent_handler("quiet_hours_start.intent") def handle_start_quiet_hours(self, message): """ Handles starting quiet hours. @@ -477,7 +476,7 @@ def handle_start_quiet_hours(self, message): self.speak_dialog("quiet_hours_start", private=True) self.update_skill_settings({"quiet_hours": True}, message) - @intent_file_handler("quiet_hours_end.intent") + @intent_handler("quiet_hours_end.intent") def handle_end_quiet_hours(self, message): """ Handles ending quiet hours or requests for missed alerts. @@ -982,6 +981,11 @@ 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)) + if alert.context.get("mq"): + LOG.warning("Alert from remote client; do nothing locally") + return self.make_active() self._gui_notify_expired(alert) @@ -1043,6 +1047,7 @@ def _play_notify_expired(self, alert: Alert): while self.alert_manager.get_alert_status(alert_id) == \ AlertState.ACTIVE and time.time() < timeout: if alert_message.context.get("klat_data"): + # TODO: Deprecated self.send_with_audio(self.dialog_renderer.render( "expired_alert", {'name': alert.alert_name}), to_play, alert_message, private=True) @@ -1072,9 +1077,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, private=True, wait=True) else: self.speak_dialog('expired_alert', {'name': alert.alert_name}, + message=alert_message, private=True, wait=True) self.make_active() time.sleep(10) diff --git a/requirements/test.txt b/requirements/test.txt new file mode 100644 index 00000000..55bb597d --- /dev/null +++ b/requirements/test.txt @@ -0,0 +1 @@ +neon-minerva[padatious]~=0.1,>=0.1.1a5 diff --git a/setup.py b/setup.py index 88f480c7..1236fe9e 100644 --- a/setup.py +++ b/setup.py @@ -89,6 +89,7 @@ def find_resource_files(): url=f'https://github.com/NeonGeckoCom/{SKILL_NAME}', license='BSD-3-Clause', install_requires=get_requirements("requirements.txt"), + extras_require={"test": get_requirements("requirements/test.txt")}, author='Neongecko', author_email='developers@neon.ai', long_description=long_description, diff --git a/test/test_skill.py b/test/test_skill.py index 3bfd9164..df430568 100644 --- a/test/test_skill.py +++ b/test/test_skill.py @@ -16,11 +16,11 @@ # Specialized conversational reconveyance options from Conversation Processing Intelligence Corp. # US Patents 2008-2021: US7424516, US20140161250, US20140177813, US8638908, US8068604, US8553852, US10530923, US10530924 # China Patent: CN102017585 - Europe Patent: EU2156652 - Patents Pending + import datetime import json import os import time - import lingua_franca import pytest import random @@ -30,8 +30,8 @@ import datetime as dt from threading import Event -from os import mkdir, remove -from os.path import dirname, join, exists, isfile +from os import remove +from os.path import dirname, join, isfile from dateutil.tz import gettz from lingua_franca.format import nice_date_time, nice_duration from mock import Mock @@ -40,13 +40,13 @@ from ovos_utils.events import EventSchedulerInterface from ovos_utils.messagebus import FakeBus from lingua_franca import load_language +from lingua_franca.format import nice_time -from mycroft.util.format import nice_time +from neon_minerva.tests.skill_unit_test_base import SkillTestCase -sys.path.append(dirname(dirname(__file__))) -from util import AlertType, AlertState, AlertPriority, Weekdays, EVERYDAY -from util.alert import Alert -from util.alert_manager import AlertManager, get_alert_id +from skill_alerts.util import AlertType, AlertState, AlertPriority, Weekdays, EVERYDAY +from skill_alerts.util.alert import Alert +from skill_alerts.util.alert_manager import AlertManager, get_alert_id examples_dir = join(dirname(__file__), "example_messages") @@ -57,27 +57,10 @@ def _get_message_from_file(filename: str): return Message.deserialize(contents) -class TestSkill(unittest.TestCase): - +class TestSkillMethods(SkillTestCase): @classmethod def setUpClass(cls) -> None: - from mycroft.skills.skill_loader import SkillLoader - - cls.bus = FakeBus() - cls.bus.run_in_thread() - skill_loader = SkillLoader(cls.bus, dirname(dirname(__file__))) - skill_loader.load() - cls.skill = skill_loader.instance - cls.test_fs = join(dirname(__file__), "skill_fs") - if not exists(cls.test_fs): - mkdir(cls.test_fs) - cls.skill.settings_write_path = cls.test_fs - cls.skill.file_system.path = cls.test_fs - - # Override speak and speak_dialog to test passed arguments - cls.skill.speak = Mock() - cls.skill.speak_dialog = Mock() - + SkillTestCase.setUpClass() # Setup alerts load_language("en-us") @@ -1380,7 +1363,7 @@ def test_alert_manager_cache_load(self): remove(test_file) def test_get_user_alerts(self): - from util.alert_manager import get_alert_user + from skill_alerts.util.alert_manager import get_alert_user alert_manager = self._init_alert_manager() now_time = dt.datetime.now(dt.timezone.utc) for i in range(10): @@ -1424,7 +1407,7 @@ def test_get_all_alerts(self): all_alerts["pending"][i].next_expiration) def test_get_alert_user(self): - from util.alert_manager import get_alert_user, _DEFAULT_USER + from skill_alerts.util.alert_manager import get_alert_user, _DEFAULT_USER test_user = "tester" alert_time = dt.datetime.now(dt.timezone.utc) + dt.timedelta(minutes=5) alert_no_user = Alert.create(alert_time) @@ -1438,7 +1421,7 @@ def test_get_alert_user(self): self.assertEqual(get_alert_user(alert_no_user), "new_user") def test_get_alert_id(self): - from util.alert_manager import get_alert_id + from skill_alerts.util.alert_manager import get_alert_id alert_time = dt.datetime.now(dt.timezone.utc) + dt.timedelta(minutes=5) alert_no_id = Alert.create(alert_time) alert_with_id = Alert.create(alert_time, context={"ident": "test"}) @@ -1448,7 +1431,7 @@ def test_get_alert_id(self): def test_sort_alerts_list(self): from copy import deepcopy - from util.alert_manager import sort_alerts_list + from skill_alerts.util.alert_manager import sort_alerts_list now_time = dt.datetime.now(dt.timezone.utc) alerts = list() @@ -1466,7 +1449,7 @@ def test_sort_alerts_list(self): alerts[i].next_expiration) def test_get_alert_by_type(self): - from util.alert_manager import get_alerts_by_type + from skill_alerts.util.alert_manager import get_alerts_by_type now_time = dt.datetime.now(dt.timezone.utc) alerts = list() @@ -1554,7 +1537,7 @@ def test_timer_gui(self): class TestParseUtils(unittest.TestCase): def test_round_nearest_minute(self): - from util.parse_utils import round_nearest_minute + from skill_alerts.util.parse_utils import round_nearest_minute now_time = dt.datetime.now(dt.timezone.utc).replace(microsecond=0) alert_time = now_time + dt.timedelta(minutes=9, seconds=5) rounded = round_nearest_minute(alert_time) @@ -1564,7 +1547,7 @@ def test_round_nearest_minute(self): self.assertEqual(rounded, alert_time.replace(second=0)) def test_spoken_time_remaining(self): - from util.parse_utils import spoken_time_remaining + from skill_alerts.util.parse_utils import spoken_time_remaining now_time = dt.datetime.now(dt.timezone.utc).replace(microsecond=0) seconds_alert = now_time + dt.timedelta(minutes=59, seconds=59) to_speak = spoken_time_remaining(seconds_alert, now_time) @@ -1600,7 +1583,7 @@ def test_spoken_time_remaining(self): self.assertEqual(to_speak, "eight days") def test_get_default_alert_name(self): - from util.parse_utils import get_default_alert_name + from skill_alerts.util.parse_utils import get_default_alert_name now_time = dt.datetime.now(dt.timezone.utc) timer_time = now_time + dt.timedelta(minutes=10) self.assertEqual(get_default_alert_name(timer_time, @@ -1630,7 +1613,7 @@ def test_get_default_alert_name(self): "20:00 reminder") def test_tokenize_utterance_alarm(self): - from util.parse_utils import tokenize_utterance + from skill_alerts.util.parse_utils import tokenize_utterance daily = _get_message_from_file("create_alarm_daily.json") tokens = tokenize_utterance(daily) @@ -1670,7 +1653,7 @@ def test_tokenize_utterance_alarm(self): self.assertEqual(tokens, ['alarm', 'in 30 minutes']) def test_get_unmatched_tokens_alarm(self): - from util.parse_utils import get_unmatched_tokens + from skill_alerts.util.parse_utils import get_unmatched_tokens daily = _get_message_from_file("create_alarm_daily.json") tokens = get_unmatched_tokens(daily) @@ -1707,7 +1690,7 @@ def test_get_unmatched_tokens_alarm(self): self.assertEqual(tokens, ['monday and thursday at 9 am']) def test_parse_repeat_from_message(self): - from util.parse_utils import parse_repeat_from_message, \ + from skill_alerts.util.parse_utils import parse_repeat_from_message, \ tokenize_utterance daily = _get_message_from_file("create_alarm_daily.json") @@ -1780,7 +1763,7 @@ def test_parse_repeat_from_message(self): "until", "next sunday"]) def test_parse_end_condition_from_message(self): - from util.parse_utils import parse_end_condition_from_message + from skill_alerts.util.parse_utils import parse_end_condition_from_message now_time = dt.datetime.now(dt.timezone.utc) for_the_next_four_weeks = _get_message_from_file( @@ -1805,7 +1788,7 @@ def test_parse_end_condition_from_message(self): self.assertGreaterEqual(next_sunday, now_time) def test_parse_alert_time_from_message_alarm(self): - from util.parse_utils import parse_alert_time_from_message, \ + from skill_alerts.util.parse_utils import parse_alert_time_from_message, \ tokenize_utterance daily = _get_message_from_file("create_alarm_daily.json") @@ -1856,7 +1839,7 @@ def test_parse_alert_time_from_message_alarm(self): self.assertEqual(alert_time.time(), dt.time(hour=9)) def test_parse_alert_time_from_message_timer(self): - from util.parse_utils import parse_alert_time_from_message + from skill_alerts.util.parse_utils import parse_alert_time_from_message sea_tz = gettz("America/Los_Angeles") no_name_10_minutes = _get_message_from_file("set_time_timer.json") baking_12_minutes = _get_message_from_file("start_named_timer.json") @@ -1894,7 +1877,7 @@ def test_parse_script_file_from_message(self): pass def test_parse_alert_name_from_message(self): - from util.parse_utils import parse_alert_name_from_message + from skill_alerts.util.parse_utils import parse_alert_name_from_message monday_thursday_alarm = _get_message_from_file( "alarm_every_monday_thursday.json") daily_alarm = _get_message_from_file("create_alarm_daily.json") @@ -2012,7 +1995,7 @@ def test_parse_alert_name_from_message(self): "rotate logs") def test_parse_alert_context_from_message(self): - from util.parse_utils import parse_alert_context_from_message, \ + from skill_alerts.util.parse_utils import parse_alert_context_from_message, \ _DEFAULT_USER test_message_no_context = Message("test", {}, {}) test_message_local_user = Message("test", {}, @@ -2057,7 +2040,7 @@ def test_parse_alert_context_from_message(self): self.assertIsInstance(klat_user["klat_data"], dict) def test_build_alert_from_intent_alarm(self): - from util.parse_utils import build_alert_from_intent + from skill_alerts.util.parse_utils import build_alert_from_intent seattle_tz = gettz("America/Los_Angeles") utc_tz = dt.timezone.utc @@ -2153,7 +2136,7 @@ def _validate_wakeup_in(alert: Alert): .total_seconds(), delta=2) def test_build_alert_from_intent_timer(self): - from util.parse_utils import build_alert_from_intent + from skill_alerts.util.parse_utils import build_alert_from_intent sea_tz = gettz("America/Los_Angeles") no_name_10_minutes = _get_message_from_file("set_time_timer.json") baking_12_minutes = _get_message_from_file("start_named_timer.json") @@ -2198,7 +2181,7 @@ def _validate_alert_default_params(timer: Alert): self.assertEqual(bread_timer_sea.alert_name, "bread") def test_build_alert_from_intent_reminder(self): - from util.parse_utils import build_alert_from_intent + from skill_alerts.util.parse_utils import build_alert_from_intent sea_tz = gettz("America/Los_Angeles") now_local = dt.datetime.now(sea_tz).replace(microsecond=0) @@ -2319,7 +2302,7 @@ class TestUIModels(unittest.TestCase): lingua_franca.load_language('en') def test_build_timer_data(self): - from util.ui_models import build_timer_data + from skill_alerts.util.ui_models import build_timer_data now_time_valid = dt.datetime.now(dt.timezone.utc) invalid_alert = Alert.create( @@ -2355,7 +2338,7 @@ def test_build_timer_data(self): self.assertAlmostEqual(timer_data['percentRemaining'], 1, 1) def test_build_alarm_data(self): - from util.ui_models import build_alarm_data + from skill_alerts.util.ui_models import build_alarm_data us_context = { "username": "test_user", "user_profiles": [{ From d84878c94b624127afea8aabbc9dc8bfb4de5df5 Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Mon, 29 Jan 2024 23:24:30 +0000 Subject: [PATCH 02/12] Increment Version to 2.0.1a1 --- version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.py b/version.py index 098c2800..d724f24c 100644 --- a/version.py +++ b/version.py @@ -26,4 +26,4 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -__version__ = "2.0.0" +__version__ = "2.0.1a1" From 6ec1030541df9a0a29c4dd5125a814a852c79151 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:34:11 -0800 Subject: [PATCH 03/12] Update to default escalating volume True for alarms to be audible (#134) Co-authored-by: Daniel McKnight --- __init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index 2b3c30b7..b74a34f9 100644 --- a/__init__.py +++ b/__init__.py @@ -144,7 +144,7 @@ def escalate_volume(self) -> bool: """ If true, increase volume while alert expiration is playing """ - return self.preference_skill().get("escalate_volume", False) + return self.preference_skill().get("escalate_volume", True) @property def quiet_hours(self) -> bool: @@ -170,6 +170,8 @@ def alert_timeout_seconds(self) -> int: """ Return the number of seconds to repeat an alert before marking it missed """ + # TODO: This should be per-type; a user may want an alarm to go off for + # longer than a timer timeout_minutes = self.preference_skill().get('timeout_min') or 1 if not isinstance(timeout_minutes, int): LOG.error(f'Invalid `timeout_min` in settings. ' @@ -1055,7 +1057,7 @@ def _play_notify_expired(self, alert: Alert): # TODO: refactor to `self.play_audio` LOG.debug(f"Playing file: {to_play}") play_audio(to_play).wait(60) - time.sleep(1) + time.sleep(1) # TODO: Skip this and play continuously? if self.escalate_volume: self.bus.emit(Message("mycroft.volume.increase")) From 8942be10a8a60a8e8ad52cb58909996a35769201 Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Mon, 29 Jan 2024 23:34:27 +0000 Subject: [PATCH 04/12] Increment Version to 2.0.1a2 --- version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.py b/version.py index d724f24c..5eddbc66 100644 --- a/version.py +++ b/version.py @@ -26,4 +26,4 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -__version__ = "2.0.1a1" +__version__ = "2.0.1a2" From 06ee842c63f13c06b927afae7bbbf73307f678d4 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Mon, 5 Feb 2024 14:29:31 -0800 Subject: [PATCH 05/12] Support ovos-utils 0.1 (#137) * Update requirements to allow ovos-utils 0.1 * Update skill.json --------- Co-authored-by: Daniel McKnight Co-authored-by: NeonDaniel --- requirements.txt | 2 +- skill.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index d2e72be8..4fe8fa15 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ json_database~=0.5 neon-utils~=1.2 combo_lock~=0.2 -ovos-utils~=0.0.32 +ovos-utils~=0.0, >=0.0.32 ovos-bus-client~=0.0.3 \ No newline at end of file diff --git a/skill.json b/skill.json index cca76ff6..038acc7c 100644 --- a/skill.json +++ b/skill.json @@ -34,7 +34,7 @@ "json_database~=0.5", "neon-utils~=1.2", "ovos-bus-client~=0.0.3", - "ovos-utils~=0.0.32" + "ovos-utils~=0.0, >=0.0.32" ], "system": {}, "skill": [] From fdd0f75a46d354d0d41a82894e813c97da8a6724 Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Mon, 5 Feb 2024 22:29:48 +0000 Subject: [PATCH 06/12] Increment Version to 2.0.1a3 --- version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.py b/version.py index 5eddbc66..c0c1ff32 100644 --- a/version.py +++ b/version.py @@ -26,4 +26,4 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -__version__ = "2.0.1a2" +__version__ = "2.0.1a3" From f4d011a1c37c930e6f114d1c49e934641726684d Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Thu, 22 Feb 2024 12:30:09 -0800 Subject: [PATCH 07/12] Implement support for remote client alerts (#136) * 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 Co-authored-by: NeonDaniel --- README.md | 11 +++++++- __init__.py | 72 +++++++++++++++++++++++++++++++++-------------------- skill.json | 6 ++--- 3 files changed, 58 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index a0339aae..18bab274 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Summary -A skill to schedule alarms, timers, and reminders +A skill to schedule alarms, timers, and reminders. ## Description @@ -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": , "missed": False}, ) +``` ## Examples diff --git a/__init__.py b/__init__.py index b74a34f9..ddf4001f 100644 --- a/__init__.py +++ b/__init__.py @@ -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 @@ -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) @@ -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) @@ -1035,12 +1034,13 @@ 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') @@ -1048,28 +1048,29 @@ def _play_notify_expired(self, alert: Alert): 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 @@ -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) @@ -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): """ diff --git a/skill.json b/skill.json index 038acc7c..2325bed0 100644 --- a/skill.json +++ b/skill.json @@ -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\": , \"missed\": False}, ) ```", "examples": [ "Set an alarm for 8 AM.", "When is my next alarm?", From 156b930ddb71103b6e0160002726afe6b1de7972 Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Thu, 22 Feb 2024 20:30:29 +0000 Subject: [PATCH 08/12] Increment Version to 2.0.1a4 --- version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.py b/version.py index c0c1ff32..ecbe7e53 100644 --- a/version.py +++ b/version.py @@ -26,4 +26,4 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -__version__ = "2.0.1a3" +__version__ = "2.0.1a4" From 839fc2f906aebfea7439dea9357e64bad21b1c48 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Tue, 2 Apr 2024 10:55:44 -0700 Subject: [PATCH 09/12] Update log and test requirements to prep release (#141) Co-authored-by: Daniel McKnight --- __init__.py | 2 +- requirements/test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index ddf4001f..7748d0d0 100644 --- a/__init__.py +++ b/__init__.py @@ -1050,7 +1050,7 @@ def _play_notify_expired(self, alert: Alert, message: Message): AlertState.ACTIVE and time.time() < timeout: if message.context.get("klat_data"): log_deprecation("`klat.response` emit will be removed. Listen " - "for `neon.alert_expired", "3.0.0") + "for `neon.alert_expired", "4.0.0") self.send_with_audio(self.dialog_renderer.render( "expired_alert", {'name': alert.alert_name}), to_play, message, private=True) diff --git a/requirements/test.txt b/requirements/test.txt index 55bb597d..19363c17 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1 +1 @@ -neon-minerva[padatious]~=0.1,>=0.1.1a5 +neon-minerva[padatious]~=0.2 From 88198335149782a2fc56a3000619d6f65bac3513 Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Tue, 2 Apr 2024 17:56:05 +0000 Subject: [PATCH 10/12] Increment Version to 2.0.1a5 --- version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.py b/version.py index ecbe7e53..b5a6caf8 100644 --- a/version.py +++ b/version.py @@ -26,4 +26,4 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -__version__ = "2.0.1a4" +__version__ = "2.0.1a5" From 8c0bff3e7ba0cb28298023d226b73a97fd84dead Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Tue, 2 Apr 2024 18:06:29 +0000 Subject: [PATCH 11/12] Increment Version to 3.0.0 --- version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.py b/version.py index b5a6caf8..790b70b9 100644 --- a/version.py +++ b/version.py @@ -26,4 +26,4 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -__version__ = "2.0.1a5" +__version__ = "3.0.0" From 5dccfddb2bbdcf389045f1d26ca321038c1bccce Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Tue, 2 Apr 2024 18:06:53 +0000 Subject: [PATCH 12/12] Update Changelog --- CHANGELOG.md | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9324ce9..551479f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,48 +1,54 @@ # Changelog -## [1.5.2a5](https://github.com/NeonGeckoCom/skill-alerts/tree/1.5.2a5) (2023-06-28) +## [2.0.1a5](https://github.com/NeonGeckoCom/skill-alerts/tree/2.0.1a5) (2024-04-02) -[Full Changelog](https://github.com/NeonGeckoCom/skill-alerts/compare/1.5.2a4...1.5.2a5) +[Full Changelog](https://github.com/NeonGeckoCom/skill-alerts/compare/2.0.1a4...2.0.1a5) -**Fixed bugs:** +**Merged pull requests:** -- \[BUG\] No alarm sound on expiration [\#125](https://github.com/NeonGeckoCom/skill-alerts/issues/125) +- Update log and test requirements to prep release [\#141](https://github.com/NeonGeckoCom/skill-alerts/pull/141) ([NeonDaniel](https://github.com/NeonDaniel)) -**Merged pull requests:** +## [2.0.1a4](https://github.com/NeonGeckoCom/skill-alerts/tree/2.0.1a4) (2024-02-22) -- Refactor resource resolution to resolve default sound errors [\#126](https://github.com/NeonGeckoCom/skill-alerts/pull/126) ([NeonDaniel](https://github.com/NeonDaniel)) +[Full Changelog](https://github.com/NeonGeckoCom/skill-alerts/compare/2.0.1a3...2.0.1a4) -## [1.5.2a4](https://github.com/NeonGeckoCom/skill-alerts/tree/1.5.2a4) (2023-06-26) +**Implemented enhancements:** -[Full Changelog](https://github.com/NeonGeckoCom/skill-alerts/compare/1.5.2a3...1.5.2a4) +- \[FEAT\] Support neon-iris [\#133](https://github.com/NeonGeckoCom/skill-alerts/issues/133) **Merged pull requests:** -- Update event handling [\#124](https://github.com/NeonGeckoCom/skill-alerts/pull/124) ([NeonDaniel](https://github.com/NeonDaniel)) +- Implement support for remote client alerts [\#136](https://github.com/NeonGeckoCom/skill-alerts/pull/136) ([NeonDaniel](https://github.com/NeonDaniel)) -## [1.5.2a3](https://github.com/NeonGeckoCom/skill-alerts/tree/1.5.2a3) (2023-06-20) +## [2.0.1a3](https://github.com/NeonGeckoCom/skill-alerts/tree/2.0.1a3) (2024-02-05) -[Full Changelog](https://github.com/NeonGeckoCom/skill-alerts/compare/1.5.2a2...1.5.2a3) +[Full Changelog](https://github.com/NeonGeckoCom/skill-alerts/compare/2.0.1a2...2.0.1a3) **Merged pull requests:** -- Reafctor skill init/initialize [\#123](https://github.com/NeonGeckoCom/skill-alerts/pull/123) ([NeonDaniel](https://github.com/NeonDaniel)) +- Support ovos-utils 0.1 [\#137](https://github.com/NeonGeckoCom/skill-alerts/pull/137) ([NeonDaniel](https://github.com/NeonDaniel)) -## [1.5.2a2](https://github.com/NeonGeckoCom/skill-alerts/tree/1.5.2a2) (2023-06-15) +## [2.0.1a2](https://github.com/NeonGeckoCom/skill-alerts/tree/2.0.1a2) (2024-01-29) -[Full Changelog](https://github.com/NeonGeckoCom/skill-alerts/compare/1.5.2a1...1.5.2a2) +[Full Changelog](https://github.com/NeonGeckoCom/skill-alerts/compare/2.0.1a1...2.0.1a2) **Merged pull requests:** -- Update for best practices [\#121](https://github.com/NeonGeckoCom/skill-alerts/pull/121) ([NeonDaniel](https://github.com/NeonDaniel)) +- Update to default escalating volume True for alarms to be audible [\#134](https://github.com/NeonGeckoCom/skill-alerts/pull/134) ([NeonDaniel](https://github.com/NeonDaniel)) -## [1.5.2a1](https://github.com/NeonGeckoCom/skill-alerts/tree/1.5.2a1) (2023-06-12) +## [2.0.1a1](https://github.com/NeonGeckoCom/skill-alerts/tree/2.0.1a1) (2024-01-29) + +[Full Changelog](https://github.com/NeonGeckoCom/skill-alerts/compare/2.0.0...2.0.1a1) + +**Fixed bugs:** -[Full Changelog](https://github.com/NeonGeckoCom/skill-alerts/compare/1.5.1...1.5.2a1) +- transcribed: "Set a timer for five minutes." evokes: Speak: "I'm sorry, I don't understand." [\#132](https://github.com/NeonGeckoCom/skill-alerts/issues/132) +- \[BUG\] No module named 'mycroft\_bus\_client' [\#131](https://github.com/NeonGeckoCom/skill-alerts/issues/131) +- \[BUG\] RuntimeError: dictionary changed size during iteratio [\#130](https://github.com/NeonGeckoCom/skill-alerts/issues/130) **Merged pull requests:** -- Refactor mycroft-messagebus-client to ovos-bus-client [\#120](https://github.com/NeonGeckoCom/skill-alerts/pull/120) ([NeonDaniel](https://github.com/NeonDaniel)) +- Update tests to latest automation/shared classes [\#135](https://github.com/NeonGeckoCom/skill-alerts/pull/135) ([NeonDaniel](https://github.com/NeonDaniel))