From da6213e659674f63331d84dc16b614bf437c1d92 Mon Sep 17 00:00:00 2001 From: Ken Smith Date: Wed, 24 Mar 2021 15:49:45 -0400 Subject: [PATCH 01/11] fix basic converse bug and fix basic snooze functionality --- __init__.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index 55d77cd..cf4843e 100644 --- a/__init__.py +++ b/__init__.py @@ -739,11 +739,14 @@ def snooze_alarm(self, message): if not has_expired_alarm(self.settings["alarm"]): return + self.cancel_scheduled_event("Beep") + self.cancel_scheduled_event("NextAlarm") + self.__end_beep() self.__end_flash() utt = message.data.get("utterance") or "" - snooze_for = extract_number(utt) + snooze_for = extract_number(utt[0]) if not snooze_for or snooze_for < 1: snooze_for = 9 # default to 9 minutes @@ -784,7 +787,16 @@ def converse(self, utterances, lang="en-us"): """While an alarm is expired, check all utterances for Stop vocab.""" if has_expired_alarm(self.settings["alarm"]): if utterances and self.voc_match(utterances[0], "StopBeeping"): - self._stop_expired_alarm() + + if utterances[0].find("snooze") > -1: + message = Message( + "internal.snooze", + data=dict(utterance=utterances) + ) + self.snooze_alarm(message) + else: + self._stop_expired_alarm() # only do if NOT snooze! + return True # and consume this phrase def stop(self, _=None): From 2e4cdf635f7c90afe6dd6f7f0cdef6a015c8c566 Mon Sep 17 00:00:00 2001 From: Ken Smith Date: Wed, 24 Mar 2021 17:55:36 -0400 Subject: [PATCH 02/11] forgot vocab fix --- vocab/en-us/StopBeeping.voc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vocab/en-us/StopBeeping.voc b/vocab/en-us/StopBeeping.voc index 00e4496..1fed50f 100644 --- a/vocab/en-us/StopBeeping.voc +++ b/vocab/en-us/StopBeeping.voc @@ -11,4 +11,5 @@ off disable ok okay -alright \ No newline at end of file +alright +snooze From 38abc2571d7fb470b663b12afb21eb5d057e46ec Mon Sep 17 00:00:00 2001 From: Ken Smith Date: Wed, 31 Mar 2021 12:07:50 -0400 Subject: [PATCH 03/11] hoping the absence of a Snooze.voc file does not crash non US users --- __init__.py | 44 ++++++++++--------------------------- vocab/en-us/Snooze.voc | 2 ++ vocab/en-us/StopBeeping.voc | 1 - 3 files changed, 14 insertions(+), 33 deletions(-) create mode 100644 vocab/en-us/Snooze.voc diff --git a/__init__.py b/__init__.py index cf4843e..8ef5236 100644 --- a/__init__.py +++ b/__init__.py @@ -787,17 +787,18 @@ def converse(self, utterances, lang="en-us"): """While an alarm is expired, check all utterances for Stop vocab.""" if has_expired_alarm(self.settings["alarm"]): if utterances and self.voc_match(utterances[0], "StopBeeping"): + self._stop_expired_alarm() + elif self.voc_match(utterances[0], "Snooze"): + message = Message( + "internal.snooze", + data=dict(utterance=utterances) + ) + self.snooze_alarm(message) + else: + # TODO deal with this + self.log.info("AlarmSkill:Converse confused by %s" % (utterances[0],)) - if utterances[0].find("snooze") > -1: - message = Message( - "internal.snooze", - data=dict(utterance=utterances) - ) - self.snooze_alarm(message) - else: - self._stop_expired_alarm() # only do if NOT snooze! - - return True # and consume this phrase + return True # and consume this phrase def stop(self, _=None): """Respond to system stop commands.""" @@ -880,28 +881,7 @@ def __end_beep(self): pass self.beep_process = None self._restore_volume() - self._restore_listen_beep() - - def _restore_listen_beep(self): - if "user_beep_setting" in self.settings: - # Wipe from local config - new_conf_values = {"confirm_listening": False} - user_config = LocalConf(USER_CONFIG) - - if ( - self.settings["user_beep_setting"] is None - and "confirm_listening" in user_config - ): - del user_config["confirm_listening"] - else: - user_config.merge( - {"confirm_listening": self.settings["user_beep_setting"]} - ) - user_config.store() - - # Notify all processes to update their loaded configs - self.bus.emit(Message("configuration.updated")) - del self.settings["user_beep_setting"] + #self._restore_listen_beep() def _stop_expired_alarm(self): if has_expired_alarm(self.settings["alarm"]): diff --git a/vocab/en-us/Snooze.voc b/vocab/en-us/Snooze.voc new file mode 100644 index 0000000..0952a0e --- /dev/null +++ b/vocab/en-us/Snooze.voc @@ -0,0 +1,2 @@ +snooze + diff --git a/vocab/en-us/StopBeeping.voc b/vocab/en-us/StopBeeping.voc index 1fed50f..f7b89c1 100644 --- a/vocab/en-us/StopBeeping.voc +++ b/vocab/en-us/StopBeeping.voc @@ -12,4 +12,3 @@ disable ok okay alright -snooze From 411455005b814b90a5dc07a1e60370d449ea59c9 Mon Sep 17 00:00:00 2001 From: Ken Smith Date: Fri, 2 Apr 2021 14:04:48 -0400 Subject: [PATCH 04/11] this works better but will give me grief during a PR review --- __init__.py | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 126 insertions(+), 5 deletions(-) diff --git a/__init__.py b/__init__.py index 8ef5236..996e2f9 100644 --- a/__init__.py +++ b/__init__.py @@ -12,16 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pytz from datetime import datetime, timedelta from os.path import join, abspath, dirname, isfile import re import time - from alsaaudio import Mixer - from adapt.intent import IntentBuilder from mycroft import MycroftSkill, intent_handler -from mycroft.configuration.config import LocalConf, USER_CONFIG +from mycroft.configuration.config import LocalConf, DEFAULT_CONFIG from mycroft.messagebus.message import Message from mycroft.util import play_mp3 from mycroft.util.format import nice_date_time, nice_time, nice_date, join_list @@ -46,7 +45,6 @@ describe_repeat_rule, ) - # WORKING PHRASES/SEQUENCES: # Set an alarm # for 9 @@ -70,7 +68,6 @@ # > 7pm tomorrow # Cancel it - class AlarmSkill(MycroftSkill): """The official Alarm Skill for Mycroft AI.""" @@ -163,6 +160,13 @@ def initialize(self): # Support query for active alarms from other skills self.add_event("private.mycroftai.has_alarm", self.on_has_alarm) + # establish local timezone from config + default_config = LocalConf(DEFAULT_CONFIG) + loc = default_config.get("location") + tz = loc.get("timezone") + self.local_tz = tz["code"] + self.log.info("Local timezone configured for %s" % (self.local_tz,)) + def on_has_alarm(self, message): """Reply to requests for alarm on/off status.""" total = len(self.settings["alarm"]) @@ -662,6 +666,117 @@ def _get_alarm_matches( # No matches found return (status[2], None) + def _fallback_get_alarm_matches(self, utt): + # TODO this needs to become a class variable both here and above + status = ["All", "Matched", "No Match Found", "User Cancelled", "Next"] + return_status = status[2] + alarm_list = self.settings["alarm"] + + self.log.debug("utt:%s, alarms:%s" % (utt, self.settings["alarm"])) + + # Lingua-franca workaround-001 - day of week and date confuses LF terribly. + # if a day of the week (monday, friday, etc) is included with a + # valid date like "monday april 5th" LF will return bad data so we make + # sure we never include both. "monday april 5th" becomes "april 5th". + + # I'm sure to get blowback during pr for this ... + months = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'] + days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] + + for month in months: + if utt.find(month) > -1 and re.search(r'\d+', utt): + # if we have a month and a number/day + # whack any days of week terms + for day in days: + utt = utt.replace(day,'') + + self.log.debug("LF scrubbed utt = %s" % (utt,)) + + when, utt_no_datetime = extract_datetime(utt) or (None, utt) + self.log.debug("when: %s (this is in local timezone), what: %s" % (when, utt_no_datetime)) + + when_utc = when.astimezone(pytz.utc) + self.log.debug("when_utc: %s (this is in utc)" % (when_utc,)) + + have_time = False + if when_utc is None: + self.log.warning("Request contains no discernable date or time") + else: + # assumption 1 - if time is midnight we probably don't have a time + # in other words the user probably said something like 'monday the 25th' + # or tomorrow. + # we can look for the term 'midnight' in the utterance and + # if present we can assume we DO have a time (midnight = 00:00:00), + # otherwise we assume we don't have a time + user_time = when.strftime("%H:%M:%S") + if user_time == "00:00:00" and utt.find("midnight") == -1: + self.log.debug("we have no user time ===> 00:00:00") + else: + have_time = True + + self.log.debug("Do we have a user time:%s" % (have_time,)) + + alarm_matches = [] + + user_dow = None # if user said a day of the week, this is it + + result = re.findall('(mon|tues|wed|thurs|fri|sat|sun)day', utt) + if result: + utt_dow = result[0] + 'day' + user_dow = days.index(utt_dow) if utt_dow in days else None + + self.log.debug("user_dow:%s" % (user_dow,)) + + # if the user says a day of the week (mon-sun) we + # keep them in a separate list of day of week matches + dow_matches = [] + + for alm in alarm_list: + self.log.debug(" Loop: %s" % (alm,)) + + # get utc time from alarm timestamp + dt_obj = datetime.fromtimestamp( alm['timestamp'] ) + dt_obj = dt_obj.astimezone(pytz.utc) + + # we also need a local version of our utc time + cfg_tz = pytz.timezone(self.local_tz) + dt_local = dt_obj.astimezone(cfg_tz) + + if user_dow == dt_local.weekday(): + dow_matches.append(alm) + + if have_time: + # unambiguous + if when_utc == dt_obj: + alarm_matches.append(alm) + else: + self.log.debug(" Loop:have date but no time - disambiguate here!") + self.log.debug(" Loop: when:%s, dt_obj:%s" % (when_utc.strftime("%y-%m-%d"), dt_obj.strftime("%y-%m-%d"))) + # do dates match ? + if when_utc.strftime("%y-%m-%d") == dt_obj.strftime("%y-%m-%d"): + self.log.debug("Note, date match with no time") + alarm_matches.append(alm) + + if len(alarm_matches) == 0: + # we can try some heuristics here + self.log.info("Entering heuristics phase, dow_matches:%s" % (dow_matches,)) + alarm_matches = dow_matches + + + if len(alarm_matches) == 1: + return_status = status[1] + else: + if len(alarm_matches) > 1: + self.log.debug("Can't disambiguate, match count %s is > 1" % (len(alarm_matches,))) + self.speak_dialog("alarm.confused") + + # meet previous ret code protocol + if len(alarm_matches) == 0: + alarm_matches = None + + return return_status, alarm_matches + + @intent_handler( IntentBuilder("") .require("Delete") @@ -690,6 +805,12 @@ def handle_delete(self, message): is_response=False, ) + if alarms is None: + # the poor _get_alarm_matches() method is a bit of a dim bulb. + # we'll inject some heuristics into it here + status, alarms = self._fallback_get_alarm_matches(utt) + self.log.info("No alarms was converted to - status:%s, alarms:%s" % (status, alarms)) + if alarms: total = len(alarms) else: From a0e1ee84825d5b9158da2304fb038b8ece208278 Mon Sep 17 00:00:00 2001 From: Ken Smith Date: Mon, 5 Apr 2021 19:43:40 -0400 Subject: [PATCH 05/11] quick fix based on Gez input --- __init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/__init__.py b/__init__.py index 996e2f9..5a767d7 100644 --- a/__init__.py +++ b/__init__.py @@ -161,10 +161,7 @@ def initialize(self): self.add_event("private.mycroftai.has_alarm", self.on_has_alarm) # establish local timezone from config - default_config = LocalConf(DEFAULT_CONFIG) - loc = default_config.get("location") - tz = loc.get("timezone") - self.local_tz = tz["code"] + self.local_tz = self.config_core.get("location").get("timezone").get("code") self.log.info("Local timezone configured for %s" % (self.local_tz,)) def on_has_alarm(self, message): From 498bd86184780f63721cb0786d1c1a4758d70612 Mon Sep 17 00:00:00 2001 From: Ken Smith Date: Fri, 23 Apr 2021 12:33:58 -0400 Subject: [PATCH 06/11] refactor and clean up. may also require changes I made to the vktest framework --- __init__.py | 266 +++++++++++++++++++------------- test/behave/skill-alarm.feature | 98 ++++-------- test/behave/steps/alarms.py | 12 +- 3 files changed, 191 insertions(+), 185 deletions(-) diff --git a/__init__.py b/__init__.py index 5a767d7..100f43a 100644 --- a/__init__.py +++ b/__init__.py @@ -20,7 +20,6 @@ from alsaaudio import Mixer from adapt.intent import IntentBuilder from mycroft import MycroftSkill, intent_handler -from mycroft.configuration.config import LocalConf, DEFAULT_CONFIG from mycroft.messagebus.message import Message from mycroft.util import play_mp3 from mycroft.util.format import nice_date_time, nice_time, nice_date, join_list @@ -46,23 +45,7 @@ ) # WORKING PHRASES/SEQUENCES: -# Set an alarm -# for 9 -# no for 9 am -# Set an alarm for tomorrow evening at 8:20 -# Set an alarm for monday morning at 8 -# create an alarm for monday morning at 8 -# snooze -# stop -# turn off the alarm -# create a repeating alarm for tuesdays at 7 am -# Set a recurring alarm -# Set a recurring alarm for weekdays at 7 -# snooze for 15 minutes -# set an alarm for 20 seconds from now -# Set an alarm every monday at 7 -# "Set a recurring alarm for mondays and wednesdays at 7" -# "Set an alarm for 10 am every weekday" +# See VK Tests for requirements/usage. # TODO: Context - save the alarm found in queries as context # When is the next alarm # > 7pm tomorrow @@ -107,7 +90,7 @@ def __init__(self): try: self.mixer = Mixer() except Exception as err: - self.log.error("Couldn't allocate mixer, {}".format(repr(err))) + self.log.warning("Couldn't allocate mixer, {}".format(repr(err))) self.mixer = None self.saved_volume = None @@ -161,9 +144,19 @@ def initialize(self): self.add_event("private.mycroftai.has_alarm", self.on_has_alarm) # establish local timezone from config - self.local_tz = self.config_core.get("location").get("timezone").get("code") + self.local_tz = self.location_timezone self.log.info("Local timezone configured for %s" % (self.local_tz,)) + self.days = [ + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', + 'sunday' + ] + def on_has_alarm(self, message): """Reply to requests for alarm on/off status.""" total = len(self.settings["alarm"]) @@ -271,7 +264,6 @@ def handle_wake_me(self, message): @intent_handler( IntentBuilder("") - .require("Set") .require("Alarm") .optionally("Recurring") .optionally("Recurrence") @@ -526,7 +518,7 @@ def _get_alarm_matches( # No alarms if alarms is None or len(alarms) == 0: - self.log.error("Cannot get match. No active alarms.") + self.log.info("Cannot get match. No active alarms.") return (status[2], None) # Extract Alarm Time @@ -553,8 +545,29 @@ def _get_alarm_matches( time_alarm = to_utc(when).timestamp() if is_midnight: time_alarm = time_alarm + 86400.0 + time_matches = [a for a in alarms if abs(a["timestamp"] - time_alarm) <= 60] + # add other categories of alarm matches here + utt = self.workaround_lingua_franca(utt) + when, utt_no_datetime = extract_datetime(utt) or (None, utt) + + when_utc = None + if when is not None: + when_utc = when.astimezone(pytz.utc) + + have_time = False + if when_utc: + user_time = when.strftime("%H:%M:%S") + if user_time == "00:00:00" and self.voc_match(utt, "Midnight"): + have_time = True + if user_time != "00:00:00": + have_time = True + + user_dow = self.get_dow_from_utterance(utt) + + advanced_matches = self.get_advanced_matches(utt, have_time, when, when_utc, user_dow, alarms) + # Extract Recurrence recur = None recurrence_matches = None @@ -594,6 +607,8 @@ def _get_alarm_matches( # Find the Intersection of the Alarms list and all the matched alarms orig_count = len(alarms) + if len(advanced_matches) > 0: + alarms = advanced_matches if when and time_matches: alarms = [a for a in alarms if a in time_matches] if recur and recurrence_matches: @@ -608,6 +623,11 @@ def _get_alarm_matches( elif utt and any(fuzzy_match(i, utt, 1) for i in next_words): return (status[4], [alarms[0]]) + if max_results < orig_count and len(alarms) == max_results: + # if we started with more than we have + # and that's how many we asked for + return (status[1], alarms) + # Given something to match but no match found if ( (number and number > len(alarms)) @@ -615,8 +635,8 @@ def _get_alarm_matches( or (when and not time_matches) ): return (status[2], None) - # If number of alarms filtered were the same, assume user asked for - # All alarms + # If number of alarms filtered were the same, + # assume user asked for ALL alarms if ( len(alarms) == orig_count and max_results > 1 @@ -625,6 +645,7 @@ def _get_alarm_matches( and not recur ): return (status[0], alarms) + # Return immediately if there is ordinal if number and number <= len(alarms): return (status[1], [alarms[number - 1]]) @@ -641,6 +662,7 @@ def _get_alarm_matches( if desc: items_string = join_list(desc, self.translate("and")) + self.log.debug("Too many alarms, ask for clarification") reply = self.get_response( dialog, data={ @@ -649,6 +671,7 @@ def _get_alarm_matches( }, num_retries=1, ) + if reply: return self._get_alarm_matches( reply, @@ -663,71 +686,47 @@ def _get_alarm_matches( # No matches found return (status[2], None) - def _fallback_get_alarm_matches(self, utt): - # TODO this needs to become a class variable both here and above - status = ["All", "Matched", "No Match Found", "User Cancelled", "Next"] - return_status = status[2] - alarm_list = self.settings["alarm"] - - self.log.debug("utt:%s, alarms:%s" % (utt, self.settings["alarm"])) - - # Lingua-franca workaround-001 - day of week and date confuses LF terribly. - # if a day of the week (monday, friday, etc) is included with a - # valid date like "monday april 5th" LF will return bad data so we make - # sure we never include both. "monday april 5th" becomes "april 5th". - - # I'm sure to get blowback during pr for this ... - months = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'] - days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] - - for month in months: - if utt.find(month) > -1 and re.search(r'\d+', utt): - # if we have a month and a number/day - # whack any days of week terms - for day in days: - utt = utt.replace(day,'') - - self.log.debug("LF scrubbed utt = %s" % (utt,)) - - when, utt_no_datetime = extract_datetime(utt) or (None, utt) - self.log.debug("when: %s (this is in local timezone), what: %s" % (when, utt_no_datetime)) - - when_utc = when.astimezone(pytz.utc) - self.log.debug("when_utc: %s (this is in utc)" % (when_utc,)) - - have_time = False - if when_utc is None: - self.log.warning("Request contains no discernable date or time") - else: - # assumption 1 - if time is midnight we probably don't have a time - # in other words the user probably said something like 'monday the 25th' - # or tomorrow. - # we can look for the term 'midnight' in the utterance and - # if present we can assume we DO have a time (midnight = 00:00:00), - # otherwise we assume we don't have a time - user_time = when.strftime("%H:%M:%S") - if user_time == "00:00:00" and utt.find("midnight") == -1: - self.log.debug("we have no user time ===> 00:00:00") + # sometimes LF returns a roman numeral for + # a number so the 26th will return xxvi + def roman_to_int(self, s): + rom_val = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000} + int_val = 0 + for i in range(len(s)): + if i > 0 and rom_val[s[i]] > rom_val[s[i - 1]]: + int_val += rom_val[s[i]] - 2 * rom_val[s[i - 1]] else: - have_time = True - - self.log.debug("Do we have a user time:%s" % (have_time,)) - - alarm_matches = [] + int_val += rom_val[s[i]] + return int_val - user_dow = None # if user said a day of the week, this is it - - result = re.findall('(mon|tues|wed|thurs|fri|sat|sun)day', utt) - if result: - utt_dow = result[0] + 'day' - user_dow = days.index(utt_dow) if utt_dow in days else None + def get_advanced_matches(self, utt, have_time, when, when_utc, user_dow, alarm_list): + # holds exact date and time matches + exact_matches = [] + dom = None + try: + dom = self.roman_to_int(utt) + except: + self.log.debug("Not a roman numeral") - self.log.debug("user_dow:%s" % (user_dow,)) + if dom is None: + dom = extract_number(utt) # if the user says a day of the week (mon-sun) we # keep them in a separate list of day of week matches dow_matches = [] + # we also look for specific time matches like 9am + tod_matches = [] + if when_utc: + tod_matches = self.get_tod_matches(when.time(), alarm_list) + + # these alarms match our when_utc date only + date_matches = [] + + # these alarms match any numbers passed in like + # the 25th, 5 etc which we assume are dom + # hopefully this won't muss up ordinals + dom_matches = [] + for alm in alarm_list: self.log.debug(" Loop: %s" % (alm,)) @@ -742,37 +741,94 @@ def _fallback_get_alarm_matches(self, utt): if user_dow == dt_local.weekday(): dow_matches.append(alm) + if dom == dt_local.day: + dom_matches.append(alm) + if have_time: # unambiguous if when_utc == dt_obj: - alarm_matches.append(alm) + exact_matches.append(alm) else: - self.log.debug(" Loop:have date but no time - disambiguate here!") - self.log.debug(" Loop: when:%s, dt_obj:%s" % (when_utc.strftime("%y-%m-%d"), dt_obj.strftime("%y-%m-%d"))) - # do dates match ? - if when_utc.strftime("%y-%m-%d") == dt_obj.strftime("%y-%m-%d"): - self.log.debug("Note, date match with no time") - alarm_matches.append(alm) + self.log.debug(" Loop: when:%s, dt_obj:%s" % (when_utc, dt_obj)) + if when_utc is not None and dt_obj is not None: + # do dates match ? + if when_utc.strftime("%y-%m-%d") == dt_obj.strftime("%y-%m-%d"): + date_matches.append(alm) - if len(alarm_matches) == 0: - # we can try some heuristics here - self.log.info("Entering heuristics phase, dow_matches:%s" % (dow_matches,)) - alarm_matches = dow_matches + if len(exact_matches) > 0: + return exact_matches + if len(date_matches) > 0: + return date_matches - if len(alarm_matches) == 1: - return_status = status[1] - else: - if len(alarm_matches) > 1: - self.log.debug("Can't disambiguate, match count %s is > 1" % (len(alarm_matches,))) - self.speak_dialog("alarm.confused") + if len(tod_matches) > 0: + return tod_matches - # meet previous ret code protocol - if len(alarm_matches) == 0: - alarm_matches = None + if len(dow_matches) > 0: + return dow_matches - return return_status, alarm_matches + return dom_matches + def get_tod_matches(self, alarm_time, alarm_list): + tod_matches = [] + + # find alarms where the time matches + for alarm in alarm_list: + # get utc time from alarm timestamp + dt_obj = datetime.fromtimestamp( alarm['timestamp'] ) + dt_obj = dt_obj.astimezone(pytz.utc) + + # we also need a local version of our utc time + cfg_tz = pytz.timezone(self.local_tz) + dt_local = dt_obj.astimezone(cfg_tz) + + self.log.debug("Check for TOD matches, dt_local.time()=%s" % (dt_local.time(),)) + if dt_local.time() == alarm_time: + tod_matches.append(alarm) + + return tod_matches + + def get_dow_from_utterance(self, utt): + user_dow = None + result = re.findall('(mon|tues|wed|thurs|fri|sat|sun)day', utt) + if result: + utt_dow = result[0] + 'day' + user_dow = self.days.index(utt_dow) if utt_dow in self.days else None + return user_dow + + def workaround_lingua_franca(self, utt): + # Lingua-franca workaround - day of + # week and date confuses LF terribly. + # if a day of the week (monday, friday, + # etc) is included with a valid date + # like "monday april 5th" LF will + # return bad data so we make sure + # we never include both. "monday + # april 5th" becomes "april 5th". + + months = [ + 'january', + 'february', + 'march', + 'april', + 'may', + 'june', + 'july', + 'august', + 'september', + 'october', + 'november', + 'december' + ] + + for month in months: + if utt.find(month) > -1 and re.search(r'\d+', utt): + # if we have a month and a number/day + # whack any days of week terms + for day in self.days: + utt = utt.replace(day,'') + + return utt @intent_handler( IntentBuilder("") @@ -802,12 +858,6 @@ def handle_delete(self, message): is_response=False, ) - if alarms is None: - # the poor _get_alarm_matches() method is a bit of a dim bulb. - # we'll inject some heuristics into it here - status, alarms = self._fallback_get_alarm_matches(utt) - self.log.info("No alarms was converted to - status:%s, alarms:%s" % (status, alarms)) - if alarms: total = len(alarms) else: diff --git a/test/behave/skill-alarm.feature b/test/behave/skill-alarm.feature index e4f6bce..dc51b33 100644 --- a/test/behave/skill-alarm.feature +++ b/test/behave/skill-alarm.feature @@ -10,12 +10,11 @@ Feature: Alarm skill functionality | set alarm for a time | | set alarm for 8 am | | set an alarm for 7:30 am | - | create an alarm for 7:30 am | + | create an alarm for 7:30 pm | | start an alarm for 6:30 am | | let me know when it's 8:30 pm | | wake me up at 7 tomorrow morning | - @xfail # MS-64 https://mycroft.atlassian.net/browse/MS-64 Scenario Outline: user sets an alarm for a time Given an english speaking user @@ -25,8 +24,8 @@ Feature: Alarm skill functionality Examples: user sets an alarm for a time | set alarm for a time | - | alarm 6 pm | - | alarm for 8 tonight | + | set alarm 6:01 pm | + | alarm for 8:02 tonight | Scenario Outline: user sets an alarm without saying a time Given an english speaking user @@ -39,12 +38,11 @@ Feature: Alarm skill functionality Examples: set alarm withot saying a time | set alarm without saying a time | | set alarm | - | set an alarm | + | new alarm | | create an alarm | - @xfail # Jira MS-65 https://mycroft.atlassian.net/browse/MS-65 - Scenario Outline: Failing user sets an alarm without saying a time + Scenario Outline: user sets an alarm without saying a time Given an english speaking user And there are no previous alarms set When the user says "" @@ -54,10 +52,9 @@ Feature: Alarm skill functionality Examples: set alarm withot saying a time | set alarm without saying a time | - | set an alarm for tomorrow morning | + | set an alarm for tomorrow | | wake me up tomorrow | - | alarm tonight | - | alarm | + | create alarm | Scenario Outline: User sets an alarm without saying a time but then cancels Given an english speaking user @@ -71,7 +68,6 @@ Feature: Alarm skill functionality | set an alarm | nevermind | | create an alarm | cancel | - @xfail # Jira MS-109 https://mycroft.atlassian.net/browse/MS-109 Scenario Outline: User sets an alarm without saying a time but then cancels Given an english speaking user @@ -94,10 +90,10 @@ Feature: Alarm skill functionality | set a named alarm for a time | | set an alarm named sandwich for 12 pm | | set an alarm for 10 am for stretching | - | set an alarm for stretching 10 am | + | set an alarm for stretching 10 pm | | set an alarm named brunch for 11 am | - | set an alarm called brunch for 11 am | - | set an alarm named workout for 11 am | + | set an alarm called brunch for 11:30 am | + | set an alarm named workout for 11 pm | Scenario Outline: user sets an alarm without specifiying am or pm Given an english speaking user @@ -109,10 +105,9 @@ Feature: Alarm skill functionality | set an alarm for a time without am or pm | | set an alarm for 6:30 | | set an alarm for 7 | - | wake me up at 6:30 | + | wake me up at 5:30 | | let me know when it's 6 | - @xfail # Jira MS-66 https://mycroft.atlassian.net/browse/MS-66 Scenario Outline: user sets an alarm without specifiying am or pm Given an english speaking user @@ -122,11 +117,10 @@ Feature: Alarm skill functionality Examples: user sets an alarm without specifiying am or pm | set an alarm for a time without am or pm | - | alarm for 12 | + | set alarm for 12 | - @xfail # Jira MS-67 https://mycroft.atlassian.net/browse/MS-67 - Scenario Outline: Failing set an alarm for a duration instead of a time + Scenario Outline: set an alarm for a duration instead of a time Given an english speaking user And there are no previous alarms set When the user says "" @@ -134,9 +128,9 @@ Feature: Alarm skill functionality Examples: | set an alarm for a duration | - | set an alarm for 30 minutes | - | alarm in 5 minutes | - | alarm in 5 minutes and 30 seconds | + | create an alarm for 30 minutes | + | set alarm in 5 minutes | + | new alarm in 5 minutes and 30 seconds | | set an alarm 8 hours from now | | set an alarm 8 hours and 30 minutes from now | @@ -163,24 +157,24 @@ Feature: Alarm skill functionality Examples: set a recurring alarm | set a recurring alarm for a time | | set alarm every weekday at 7:30 am | - | wake me up every weekday at 7:30 am | + | wake me up every weekday at 4:30 am | | set an alarm every wednesday at 11 am | | set an alarm for weekends at 3 pm | - | set an alarm for 3 pm every weekend | + | set an alarm for 1 pm every weekend | - @xfail # Jira 68 https://mycroft.atlassian.net/browse/MS-68 - Scenario Outline: Failing user sets a recurring alarm + Scenario Outline: user sets a recurring alarm Given an english speaking user And there are no previous alarms set When the user says "" Then "mycroft-alarm" should reply with dialog from "recurring.alarm.scheduled.for.time.dialog" + # removed because not currently supported + # | wake me up every day at 5:30 am except on the weekends | Examples: set a recurring alarm | set a recurring alarm for a time | | set alarm every weekday at 7:30 am | - | alarm every weekday at 7:30 am | - | wake me up every day at 7:30 am except on the weekends | + | alarm every weekday at 6:30 am | Scenario Outline: user sets a recurring alarm without saying a time Given an english speaking user @@ -251,9 +245,8 @@ Feature: Alarm skill functionality | when will my alarm go off | | when's my alarm | - @xfail # Jira 69 https://mycroft.atlassian.net/browse/MS-69 - Scenario Outline: Failing user asks for alarm status of a single alarm + Scenario Outline: user asks for alarm status of a single alarm Given an english speaking user And there are no previous alarms set And an alarm is set for 9:00 am on weekdays @@ -285,9 +278,8 @@ Feature: Alarm skill functionality | when will my alarm go off | | when's my alarm | - @xfail # Jira 70 https://mycroft.atlassian.net/browse/MS-70 - Scenario Outline: Failing user asks for alarm status of multiple alarms + Scenario Outline: user asks for alarm status of multiple alarms Given an english speaking user And there are no previous alarms set And an alarm is set for 9:00 am on weekdays @@ -318,9 +310,8 @@ Feature: Alarm skill functionality | when will my alarm go off | | when's my alarm | - @xfail # Jira MS-71 https://mycroft.atlassian.net/browse/MS-71 - Scenario Outline: Failing user asks for alarm status when no alarms are sets + Scenario Outline: user asks for alarm status when no alarms are sets Given an english speaking user And there are no previous alarms set When the user says "" @@ -344,6 +335,7 @@ Feature: Alarm skill functionality Examples: stop beeping | stop | | stop | + | kill | | stop alarm | | disable alarm | | cancel | @@ -355,42 +347,6 @@ Feature: Alarm skill functionality | abort | | kill alarm | - @xfail - # Jira MS-72 https://mycroft.atlassian.net/browse/MS-72 - Scenario Outline: user snoozes a beeping alarm - Given an english speaking user - And there are no previous alarms set - And an alarm is expired and beeping - When the user says "" - Then "mycroft-alarm" should stop beeping and start beeping again 10 minutes - - Examples: snooze a beeping alarm - | snooze | - | snooze | - | snooze alarm | - | not yet | - | 10 more minutes | - | 10 minutes | - | snooze for 10 minutes | - | give me 10 minutes | - | wake me up in 10 minutes | - | remind me in 10 minutes | - | let me sleep | - - @xfail - # Jira MS-73 https://mycroft.atlassian.net/browse/MS-73 - Scenario Outline: user snoozes an beeping alarm for a specific time - Given an english speaking user - And there are no previous alarms set - And an alarm is expired and beeping - When the user says "" - Then "mycroft-alarm" should stop beeping and start beeping again 5 minutes - - Examples: snooze a beeping alarm for a specific time - | snooze for a time | - | snooze for 5 minutes | - | give me 10 minutes | - Scenario Outline: user deletes an alarm when a single alarm is active Given an english speaking user And there are no previous alarms set @@ -427,7 +383,6 @@ Feature: Alarm skill functionality | cancel | - @xfail # Jira MS-74 https://mycroft.atlassian.net/browse/MS-74 Scenario Outline: user deletes an alarm when multiple alarms are active Given an english speaking user @@ -451,7 +406,6 @@ Feature: Alarm skill functionality | abort alarm | | remove alarm | - @xfail # Jira MS-75 https://mycroft.atlassian.net/browse/MS-75 Scenario Outline: user deletes a specific alarm Given an english speaking user diff --git a/test/behave/steps/alarms.py b/test/behave/steps/alarms.py index 44327d1..decc495 100644 --- a/test/behave/steps/alarms.py +++ b/test/behave/steps/alarms.py @@ -10,6 +10,7 @@ @given('an alarm is set for {alarm_time}') def given_set_alarm(context, alarm_time): emit_utterance(context.bus, 'set an alarm for {}'.format(alarm_time)) + time.sleep(3) wait_for_dialog(context.bus, ['alarm.scheduled.for.time']) context.bus.clear_messages() @@ -25,16 +26,17 @@ def given_no_alarms(context): 'alarm.cancelled.multi', 'alarm.cancelled.recurring'] - print('ASKING QUESTION') + print('\nASKING QUESTION') emit_utterance(context.bus, 'cancel all alarms') for i in range(10): + wait_while_speaking() for message in context.bus.get_messages('speak'): if message.data.get('meta', {}).get('dialog') in followups: - print('Answering yes!') - time.sleep(1) - wait_while_speaking() + print("\nWaiting before saying yes ...") + time.sleep(2) emit_utterance(context.bus, 'yes') - wait_for_dialog(context.bus, cancelled) + rc = wait_for_dialog(context.bus, cancelled) + print('\nWere we understood--->rc= %s' % (rc,)) context.bus.clear_messages() return elif message.data.get('meta', {}).get('dialog') in no_alarms: From 67b71d211a91616b761945ace01a88cc977b4433 Mon Sep 17 00:00:00 2001 From: Ken Smith Date: Mon, 26 Apr 2021 16:59:35 -0400 Subject: [PATCH 07/11] corrections per PR and add back in snooze tests --- __init__.py | 57 +++++++++++------------ test/behave/skill-alarm.feature | 37 +++++++++++++++ test/behave/steps/alarms.py | 81 +++++++++++++++++++++++++++++++-- vocab/en-us/Midnight.voc | 1 + vocab/en-us/Query.voc | 6 ++- vocab/en-us/Set.voc | 1 + 6 files changed, 146 insertions(+), 37 deletions(-) create mode 100644 vocab/en-us/Midnight.voc diff --git a/__init__.py b/__init__.py index 100f43a..65dcb90 100644 --- a/__init__.py +++ b/__init__.py @@ -22,7 +22,7 @@ from mycroft import MycroftSkill, intent_handler from mycroft.messagebus.message import Message from mycroft.util import play_mp3 -from mycroft.util.format import nice_date_time, nice_time, nice_date, join_list +from mycroft.util.format import nice_date_time, nice_time, nice_date, join_list, date_time_format from mycroft.util.parse import extract_datetime, extract_number from mycroft.util.time import to_utc, now_local, now_utc @@ -147,15 +147,15 @@ def initialize(self): self.local_tz = self.location_timezone self.log.info("Local timezone configured for %s" % (self.local_tz,)) - self.days = [ - 'monday', - 'tuesday', - 'wednesday', - 'thursday', - 'friday', - 'saturday', - 'sunday' - ] + self.weekdays = None + self.months = None + date_time_format.cache(self.lang) + if self.lang in date_time_format.lang_config.keys(): + self.weekdays = list(date_time_format.lang_config[self.lang]['weekday'].values()) + self.months = list(date_time_format.lang_config[self.lang]['month'].values()) + if self.weekdays is None or self.months is None: + self.log.error("Error! weekdays:%s, months:%s" % (self.weekdays, self.months)) + def on_has_alarm(self, message): """Reply to requests for alarm on/off status.""" @@ -554,7 +554,8 @@ def _get_alarm_matches( when_utc = None if when is not None: - when_utc = when.astimezone(pytz.utc) + #when_utc = when.astimezone(pytz.utc) + when_utc = to_utc(when) have_time = False if when_utc: @@ -699,6 +700,14 @@ def roman_to_int(self, s): return int_val def get_advanced_matches(self, utt, have_time, when, when_utc, user_dow, alarm_list): + '''see if we have any of the following + in order of preference + exact matches + date matches + time of day matches (tod) + day of week matches (dow) + day of month matches (dom) + ''' # holds exact date and time matches exact_matches = [] dom = None @@ -770,9 +779,9 @@ def get_advanced_matches(self, utt, have_time, when, when_utc, user_dow, alarm_l return dom_matches def get_tod_matches(self, alarm_time, alarm_list): + ''' find alarms where the time matches''' tod_matches = [] - # find alarms where the time matches for alarm in alarm_list: # get utc time from alarm timestamp dt_obj = datetime.fromtimestamp( alarm['timestamp'] ) @@ -793,7 +802,7 @@ def get_dow_from_utterance(self, utt): result = re.findall('(mon|tues|wed|thurs|fri|sat|sun)day', utt) if result: utt_dow = result[0] + 'day' - user_dow = self.days.index(utt_dow) if utt_dow in self.days else None + user_dow = self.weekdays.index(utt_dow) if utt_dow in self.weekdays else None return user_dow def workaround_lingua_franca(self, utt): @@ -806,26 +815,11 @@ def workaround_lingua_franca(self, utt): # we never include both. "monday # april 5th" becomes "april 5th". - months = [ - 'january', - 'february', - 'march', - 'april', - 'may', - 'june', - 'july', - 'august', - 'september', - 'october', - 'november', - 'december' - ] - - for month in months: + for month in self.months: if utt.find(month) > -1 and re.search(r'\d+', utt): # if we have a month and a number/day # whack any days of week terms - for day in self.days: + for day in self.weekdays: utt = utt.replace(day,'') return utt @@ -980,6 +974,8 @@ def _play_beep(self, _=None): """ Play alarm sound file """ now = now_local() + self.bus.emit(Message("mycroft.alarm.beeping", data=dict(time=now.strftime("%m/%d/%Y, %H:%M:%S")))) + if not self.beep_start_time: self.beep_start_time = now elif (now - self.beep_start_time).total_seconds() > self.settings[ @@ -1049,7 +1045,6 @@ def __end_beep(self): pass self.beep_process = None self._restore_volume() - #self._restore_listen_beep() def _stop_expired_alarm(self): if has_expired_alarm(self.settings["alarm"]): diff --git a/test/behave/skill-alarm.feature b/test/behave/skill-alarm.feature index dc51b33..a6edc89 100644 --- a/test/behave/skill-alarm.feature +++ b/test/behave/skill-alarm.feature @@ -448,3 +448,40 @@ Feature: Alarm skill functionality | remove all alarms | | remove every alarm | | delete every alarm | + +# Jira MS-72 https://mycroft.atlassian.net/browse/MS-72 + Scenario Outline: user snoozes a beeping alarm + Given an english speaking user + And there are no previous alarms set + And an alarm is expired and beeping + When the user says "" + Then "mycroft-alarm" should stop beeping and start beeping again in 10 minutes + + Examples: snooze a beeping alarm + | snooze | + | snooze | + | snooze alarm | + | not yet | + | 10 more minutes | + | 10 minutes | + | snooze for 10 minutes | + | give me 10 minutes | + | wake me up in 10 minutes | + | remind me in 10 minutes | + | let me sleep | + + # Jira MS-73 https://mycroft.atlassian.net/browse/MS-73 + Scenario Outline: user snoozes an beeping alarm for a specific time + Given an english speaking user + And there are no previous alarms set + And an alarm is expired and beeping + When the user says "" + Then "mycroft-alarm" should stop beeping and start beeping again in 5 minutes + + Examples: snooze a beeping alarm for a specific time + | snooze for a time | + | snooze for 5 minutes | + | give me 5 minutes | + | snooze 5 | + | snooze for 5 | + diff --git a/test/behave/steps/alarms.py b/test/behave/steps/alarms.py index decc495..8866505 100644 --- a/test/behave/steps/alarms.py +++ b/test/behave/steps/alarms.py @@ -26,17 +26,17 @@ def given_no_alarms(context): 'alarm.cancelled.multi', 'alarm.cancelled.recurring'] - print('\nASKING QUESTION') + #print('\nASKING QUESTION') emit_utterance(context.bus, 'cancel all alarms') for i in range(10): wait_while_speaking() for message in context.bus.get_messages('speak'): if message.data.get('meta', {}).get('dialog') in followups: - print("\nWaiting before saying yes ...") + #print("\nWaiting before saying yes ...") time.sleep(2) emit_utterance(context.bus, 'yes') rc = wait_for_dialog(context.bus, cancelled) - print('\nWere we understood--->rc= %s' % (rc,)) + #print('\nWere we understood--->rc= %s' % (rc,)) context.bus.clear_messages() return elif message.data.get('meta', {}).get('dialog') in no_alarms: @@ -48,11 +48,82 @@ def given_no_alarms(context): @given('an alarm is expired and beeping') def given_expired_alarm(context): - emit_utterance(context.bus, 'set an alarm in 30 seconds') - time.sleep(30) + got_msg = False + emit_utterance(context.bus, 'set an alarm for 1 minute from now') + + # drain any existing messages + for message in context.bus.get_messages('mycroft.alarm.beeping'): + pass + + ctr = 0 + while ctr < 60 and not got_msg: + time.sleep(1) + + # wait for msg = beeping + for message in context.bus.get_messages('mycroft.alarm.beeping'): + #print("\n\nDETECT BEEPING MSG !!!\n\n") + got_msg = True + + ctr += 1 + + context.bus.clear_messages() + assert got_msg, "Error, did not get beeping message!" @then('"mycroft-alarm" should stop beeping') def then_stop_beeping(context): # TODO Implement pass + + +@then('"mycroft-alarm" should stop beeping and start beeping again in 10 minutes') +def then_stop_and_start_beeping(context): + start_time = time.time() + got_msg = False + + # drain any existing messages + for message in context.bus.get_messages('mycroft.alarm.beeping'): + pass + + ctr = 0 + while ctr < 3 and not got_msg: + time.sleep(60) + + # wait for msg = beeping + for message in context.bus.get_messages('mycroft.alarm.beeping'): + got_msg = True + + ctr += 1 + + elapsed = time.time() - start_time + context.bus.clear_messages() + #assert got_msg and elapsed > 5*60, "Error, did not get beeping message!" + assert got_msg, "Error, did not get beeping message!" + + +@then('"mycroft-alarm" should stop beeping and start beeping again in 5 minutes') +def then_stop_and_start_beeping(context): + start_time = time.time() + got_msg = False + + # drain any existing messages + for message in context.bus.get_messages('mycroft.alarm.beeping'): + pass + + ctr = 0 + while ctr < 7 and not got_msg: + time.sleep(60) + + # wait for msg = beeping + for message in context.bus.get_messages('mycroft.alarm.beeping'): + got_msg = True + + ctr += 1 + + elapsed = time.time() - start_time + context.bus.clear_messages() + # TODO assert got msg and > 3 minutes! + #assert got_msg and elapsed > 3*60, "Error, did not get beeping message!" + assert got_msg, "Error, did not get beeping message!" + + diff --git a/vocab/en-us/Midnight.voc b/vocab/en-us/Midnight.voc new file mode 100644 index 0000000..4b4143e --- /dev/null +++ b/vocab/en-us/Midnight.voc @@ -0,0 +1 @@ +midnight diff --git a/vocab/en-us/Query.voc b/vocab/en-us/Query.voc index 33355e0..29fe2e8 100644 --- a/vocab/en-us/Query.voc +++ b/vocab/en-us/Query.voc @@ -13,6 +13,10 @@ when will what time what day show +show me +tell +tell me give +give me how is -how are \ No newline at end of file +how are diff --git a/vocab/en-us/Set.voc b/vocab/en-us/Set.voc index c2c20b0..748dc48 100644 --- a/vocab/en-us/Set.voc +++ b/vocab/en-us/Set.voc @@ -2,3 +2,4 @@ add set start create +new From 525686404d71884910dfeb7e57f7351422c73ea4 Mon Sep 17 00:00:00 2001 From: Ken Smith Date: Tue, 27 Apr 2021 20:12:25 -0400 Subject: [PATCH 08/11] fix bug where you cant exit a multi select exchange --- __init__.py | 11 ++++++++++- vocab/en-us/Terminate.voc | 11 +++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 vocab/en-us/Terminate.voc diff --git a/__init__.py b/__init__.py index 65dcb90..4d883fa 100644 --- a/__init__.py +++ b/__init__.py @@ -554,7 +554,6 @@ def _get_alarm_matches( when_utc = None if when is not None: - #when_utc = when.astimezone(pytz.utc) when_utc = to_utc(when) have_time = False @@ -674,6 +673,11 @@ def _get_alarm_matches( ) if reply: + # if user wants to bail, bail! + if self.voc_match(utt, "Terminate"): + self.log.debug("user cancels the select request") + return (status[3], None) + return self._get_alarm_matches( reply, alarm=alarms, @@ -852,6 +856,10 @@ def handle_delete(self, message): is_response=False, ) + if status == "User Cancelled": + self.speak_dialog("alarm.delete.cancelled") + return + if alarms: total = len(alarms) else: @@ -860,6 +868,7 @@ def handle_delete(self, message): if total == 1: desc = self._describe(alarms[0]) recurring = ".recurring" if alarms[0]["repeat_rule"] else "" + if ( self.ask_yesno("ask.cancel.desc.alarm" + recurring, data={"desc": desc}) == "yes" diff --git a/vocab/en-us/Terminate.voc b/vocab/en-us/Terminate.voc new file mode 100644 index 0000000..4d0b551 --- /dev/null +++ b/vocab/en-us/Terminate.voc @@ -0,0 +1,11 @@ +terminate +no +none +neither +never +nevermind +forget +cancel +exit +stop +abort From 599499561fb02d090c859b108648a6b17e140d84 Mon Sep 17 00:00:00 2001 From: Ken Smith Date: Tue, 27 Apr 2021 20:15:01 -0400 Subject: [PATCH 09/11] fix broken snooze vk tests with 10 minute period --- test/behave/alarms.py | 129 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 test/behave/alarms.py diff --git a/test/behave/alarms.py b/test/behave/alarms.py new file mode 100644 index 0000000..382f097 --- /dev/null +++ b/test/behave/alarms.py @@ -0,0 +1,129 @@ +import time + +from behave import given, then + +from mycroft.audio import wait_while_speaking + +from test.integrationtests.voight_kampff import emit_utterance, wait_for_dialog + + +@given('an alarm is set for {alarm_time}') +def given_set_alarm(context, alarm_time): + emit_utterance(context.bus, 'set an alarm for {}'.format(alarm_time)) + time.sleep(3) + wait_for_dialog(context.bus, ['alarm.scheduled.for.time']) + context.bus.clear_messages() + + +@given('there are no previous alarms set') +def given_no_alarms(context): + followups = ['ask.cancel.alarm.plural', + 'ask.cancel.desc.alarm', + 'ask.cancel.desc.alarm.recurring'] + no_alarms = ['alarms.list.empty'] + cancelled = ['alarm.cancelled.desc', + 'alarm.cancelled.desc.recurring', + 'alarm.cancelled.multi', + 'alarm.cancelled.recurring'] + + #print('\nASKING QUESTION') + emit_utterance(context.bus, 'cancel all alarms') + for i in range(10): + wait_while_speaking() + for message in context.bus.get_messages('speak'): + if message.data.get('meta', {}).get('dialog') in followups: + #print("\nWaiting before saying yes ...") + time.sleep(2) + emit_utterance(context.bus, 'yes') + rc = wait_for_dialog(context.bus, cancelled) + #print('\nWere we understood--->rc= %s' % (rc,)) + context.bus.clear_messages() + return + elif message.data.get('meta', {}).get('dialog') in no_alarms: + context.bus.clear_messages() + return + time.sleep(1) + context.bus.clear_messages() + + +@given('an alarm is expired and beeping') +def given_expired_alarm(context): + got_msg = False + emit_utterance(context.bus, 'set an alarm for 1 minute from now') + + # drain any existing messages + for message in context.bus.get_messages('mycroft.alarm.beeping'): + pass + + ctr = 0 + while ctr < 60 and not got_msg: + time.sleep(1) + + # wait for msg = beeping + for message in context.bus.get_messages('mycroft.alarm.beeping'): + #print("\n\nDETECT BEEPING MSG !!!\n\n") + got_msg = True + + ctr += 1 + + context.bus.clear_messages() + assert got_msg, "Error, did not get beeping message!" + + +@then('"mycroft-alarm" should stop beeping') +def then_stop_beeping(context): + # TODO Implement + pass + + +@then('"mycroft-alarm" should stop beeping and start beeping again in 10 minutes') +def then_stop_and_start_beeping(context): + start_time = time.time() + got_msg = False + + # drain any existing messages + for message in context.bus.get_messages('mycroft.alarm.beeping'): + pass + + ctr = 0 + while ctr < 12 and not got_msg: + time.sleep(60) + + # wait for msg = beeping + for message in context.bus.get_messages('mycroft.alarm.beeping'): + got_msg = True + + ctr += 1 + + elapsed = time.time() - start_time + context.bus.clear_messages() + #assert got_msg and elapsed > 5*60, "Error, did not get beeping message!" + assert got_msg, "Error, did not get beeping message!" + + +@then('"mycroft-alarm" should stop beeping and start beeping again in 5 minutes') +def then_stop_and_start_beeping(context): + start_time = time.time() + got_msg = False + + # drain any existing messages + for message in context.bus.get_messages('mycroft.alarm.beeping'): + pass + + ctr = 0 + while ctr < 7 and not got_msg: + time.sleep(60) + + # wait for msg = beeping + for message in context.bus.get_messages('mycroft.alarm.beeping'): + got_msg = True + + ctr += 1 + + elapsed = time.time() - start_time + context.bus.clear_messages() + # TODO assert got msg and > 3 minutes! + #assert got_msg and elapsed > 3*60, "Error, did not get beeping message!" + assert got_msg, "Error, did not get beeping message!" + + From 3fa1bdedced92f16c48c923fbd0f79307caeadf1 Mon Sep 17 00:00:00 2001 From: Ken Smith Date: Wed, 28 Apr 2021 11:42:58 -0400 Subject: [PATCH 10/11] remove file from incorrect location --- test/behave/alarms.py | 129 ------------------------------------------ 1 file changed, 129 deletions(-) delete mode 100644 test/behave/alarms.py diff --git a/test/behave/alarms.py b/test/behave/alarms.py deleted file mode 100644 index 382f097..0000000 --- a/test/behave/alarms.py +++ /dev/null @@ -1,129 +0,0 @@ -import time - -from behave import given, then - -from mycroft.audio import wait_while_speaking - -from test.integrationtests.voight_kampff import emit_utterance, wait_for_dialog - - -@given('an alarm is set for {alarm_time}') -def given_set_alarm(context, alarm_time): - emit_utterance(context.bus, 'set an alarm for {}'.format(alarm_time)) - time.sleep(3) - wait_for_dialog(context.bus, ['alarm.scheduled.for.time']) - context.bus.clear_messages() - - -@given('there are no previous alarms set') -def given_no_alarms(context): - followups = ['ask.cancel.alarm.plural', - 'ask.cancel.desc.alarm', - 'ask.cancel.desc.alarm.recurring'] - no_alarms = ['alarms.list.empty'] - cancelled = ['alarm.cancelled.desc', - 'alarm.cancelled.desc.recurring', - 'alarm.cancelled.multi', - 'alarm.cancelled.recurring'] - - #print('\nASKING QUESTION') - emit_utterance(context.bus, 'cancel all alarms') - for i in range(10): - wait_while_speaking() - for message in context.bus.get_messages('speak'): - if message.data.get('meta', {}).get('dialog') in followups: - #print("\nWaiting before saying yes ...") - time.sleep(2) - emit_utterance(context.bus, 'yes') - rc = wait_for_dialog(context.bus, cancelled) - #print('\nWere we understood--->rc= %s' % (rc,)) - context.bus.clear_messages() - return - elif message.data.get('meta', {}).get('dialog') in no_alarms: - context.bus.clear_messages() - return - time.sleep(1) - context.bus.clear_messages() - - -@given('an alarm is expired and beeping') -def given_expired_alarm(context): - got_msg = False - emit_utterance(context.bus, 'set an alarm for 1 minute from now') - - # drain any existing messages - for message in context.bus.get_messages('mycroft.alarm.beeping'): - pass - - ctr = 0 - while ctr < 60 and not got_msg: - time.sleep(1) - - # wait for msg = beeping - for message in context.bus.get_messages('mycroft.alarm.beeping'): - #print("\n\nDETECT BEEPING MSG !!!\n\n") - got_msg = True - - ctr += 1 - - context.bus.clear_messages() - assert got_msg, "Error, did not get beeping message!" - - -@then('"mycroft-alarm" should stop beeping') -def then_stop_beeping(context): - # TODO Implement - pass - - -@then('"mycroft-alarm" should stop beeping and start beeping again in 10 minutes') -def then_stop_and_start_beeping(context): - start_time = time.time() - got_msg = False - - # drain any existing messages - for message in context.bus.get_messages('mycroft.alarm.beeping'): - pass - - ctr = 0 - while ctr < 12 and not got_msg: - time.sleep(60) - - # wait for msg = beeping - for message in context.bus.get_messages('mycroft.alarm.beeping'): - got_msg = True - - ctr += 1 - - elapsed = time.time() - start_time - context.bus.clear_messages() - #assert got_msg and elapsed > 5*60, "Error, did not get beeping message!" - assert got_msg, "Error, did not get beeping message!" - - -@then('"mycroft-alarm" should stop beeping and start beeping again in 5 minutes') -def then_stop_and_start_beeping(context): - start_time = time.time() - got_msg = False - - # drain any existing messages - for message in context.bus.get_messages('mycroft.alarm.beeping'): - pass - - ctr = 0 - while ctr < 7 and not got_msg: - time.sleep(60) - - # wait for msg = beeping - for message in context.bus.get_messages('mycroft.alarm.beeping'): - got_msg = True - - ctr += 1 - - elapsed = time.time() - start_time - context.bus.clear_messages() - # TODO assert got msg and > 3 minutes! - #assert got_msg and elapsed > 3*60, "Error, did not get beeping message!" - assert got_msg, "Error, did not get beeping message!" - - From e43221bc651dac2944e674b0c4334b94069309e5 Mon Sep 17 00:00:00 2001 From: Kris Gesling Date: Thu, 29 Apr 2021 09:39:19 +0930 Subject: [PATCH 11/11] Add #todo for consistent cancel experience --- __init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/__init__.py b/__init__.py index 4d883fa..b6c50e0 100644 --- a/__init__.py +++ b/__init__.py @@ -674,6 +674,8 @@ def _get_alarm_matches( if reply: # if user wants to bail, bail! + # TODO - consider use of global "cancel.voc" to make + # cancelling experience consistent across Mycroft. if self.voc_match(utt, "Terminate"): self.log.debug("user cancels the select request") return (status[3], None)