diff --git a/README.md b/README.md index e32a2b4..cfcf4c6 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Set named timers for cooking, watering plants, brewing tea and more. ## About -Use Mycroft when your hands are messy or you need more that the one timer in your kitchen. Multiple timers are easy to set and track with conversational interactions. +Use Mycroft when your hands are messy or you need more that the one timer in your kitchen. Multiple timers are easy to set and track with conversational interactions. Interval timers will play a single beep before restarting the timer again. On a Mark 1 you'll see visual feedback as the timer runs, and you can use the top button to stop the beeping once a timer expires. @@ -17,6 +17,10 @@ the top button to stop the beeping once a timer expires. * "How long is left on the turkey timer?" * "Mute the timer" (once triggered) +* "Start an interval timer for every 30 seconds" +* "Set a 10 minute interval timer" +* "Ping me every minute" + ## Credits Mycroft AI (@MycroftAI) diff --git a/__init__.py b/__init__.py index 1fdf7da..df67ed5 100644 --- a/__init__.py +++ b/__init__.py @@ -412,6 +412,14 @@ def update_display(self, message): # Timer still running remaining = (timer["expires"] - now).seconds self.render_timer(idx, remaining) + elif timer.get("is_interval"): + # Timer has expired but it is an interval + self._play_beep() + # reset the timer expiration + remaining = timer["duration"] + time_expires = datetime.now() + timedelta(seconds=remaining) + timer["expires"] = time_expires + self.render_timer(idx, remaining) else: # Timer has expired but not been cleared, flash eyes overtime = (now - timer["expires"]).seconds @@ -667,6 +675,111 @@ def _check_duplicate_timer_name(self, name): return True return False + @intent_handler('start.interval.timer.intent') + def handle_start_interval_timer(self, message): + """ Common handler for start_interval_timer intents + """ + def validate_duration(string): + """Check that extract_duration returns a valid duration.""" + res = extract_duration(string, self.lang) + return res and res[0] + + utt = message.data["utterance"] + #~~ GET TIMER DURATION + secs, utt_remaining = self._extract_duration(utt) + if secs and secs == 1: # prevent "set one timer" doing 1 sec timer + utt_remaining = message.data["utterance"] + + if secs == None: # no duration found, request from user + req_duration = self.get_response('ask.how.long.interval', + validator=validate_duration) + secs, _ = self._extract_duration(req_duration) + if secs is None: + return # user cancelled + + #~~ GET TIMER NAME + # START WIP - Not worried about timer names for now + #if utt_remaining is not None and len(utt_remaining) > 0: + # timer_name = self._get_timer_name(utt_remaining) + # if timer_name: + # if self._check_duplicate_timer_name(timer_name): + # return # make another timer with a different name + #else: + # timer_name = None + timer_name = None + # END WIP + + #~~ SHOULD IT BE AN ALARM? + # TODO: add name of alarm if available? + if secs >= 60*60*24: # 24 hours in seconds + if self.ask_yesno("timer.too.long.alarm.instead") == 'yes': + alarm_time = now_local() + timedelta(seconds=secs) + phrase = self.translate('set.alarm', + {'date': alarm_time.strftime('%B %d %Y'), + 'time': alarm_time.strftime('%I:%M%p')}) + self.bus.emit(Message("recognizer_loop:utterance", + {"utterances": [phrase], "lang": "en-us"})) + return + + #~~ CREATE TIMER + self.timer_index += 1 + time_expires = datetime.now() + timedelta(seconds=secs) + timer = {"name": timer_name, + "index": self.timer_index, + # keep track of ordinal until all timers of that name expire + "ordinal": self._get_ordinal_of_new_timer(secs), + "duration": secs, + "expires": time_expires, + "announced": False, + "is_interval":True} + self.active_timers.append(timer) + self.log.debug("-------------TIMER-CREATED-------------") + for key in timer: + self.log.debug('creating inverval timer: {}: {}'.format(key, timer[key])) + self.log.debug("---------------------------------------") + #~~ INFORM USER + if timer['ordinal'] > 1: + dialog = 'started.ordinal.interval.timer' + else: + dialog = 'started.interval.timer' + # if timer['name'] is not None: + # dialog += '.with.name' + + self.speak_dialog(dialog, + data={"duration": nice_duration(timer["duration"]), + "name": timer["name"], + "ordinal": self._get_speakable_ordinal(timer)}) + + #~~ CLEANUP + self.pickle() + wait_while_speaking() + self.enable_intent("handle_mute_timer") + # Start showing the remaining time on the faceplate + self.update_display(None) + # reset the mute flag with a new timer + self.mute = False + + # Handles custom start phrases eg "ping me in 5 minutes" + # Also over matches Common Play for "start timer" utterances + @intent_file_handler('start.interval.timer.intent') + def handle_start_interval_timer_padatious(self, message): + self.handle_start_interval_timer(message) + + @intent_file_handler('stop.interval.timer.intent') + def handle_stop_interval_timer(self, message): + timer = self._get_next_timer() + if timer and timer["expires"] < datetime.now(): + # Timer is beeping requiring no confirmation reaction, + # treat it like a stop button press + self.stop() + elif message and message.data.get('utterance') == "cancel": + # No expired timers to clear + # Don't cancel active timers with only "cancel" as utterance + return + else: + self.handle_cancel_timer(message) + + # Handles custom start phrases eg "ping me in 5 minutes" # Also over matches Common Play for "start timer" utterances @intent_file_handler('start.timer.intent') diff --git a/dialog/en-us/ask.how.long.interval.dialog b/dialog/en-us/ask.how.long.interval.dialog new file mode 100644 index 0000000..2979807 --- /dev/null +++ b/dialog/en-us/ask.how.long.interval.dialog @@ -0,0 +1 @@ +How long of an interval timer? diff --git a/dialog/en-us/started.interval.timer.dialog b/dialog/en-us/started.interval.timer.dialog new file mode 100644 index 0000000..bd9607a --- /dev/null +++ b/dialog/en-us/started.interval.timer.dialog @@ -0,0 +1,3 @@ +An interval timer has started for every {duration} +Alright, I've set an interval timer for every {duration} +I'm starting an interval timer for every {duration} diff --git a/dialog/en-us/started.ordinal.interval.timer.dialog b/dialog/en-us/started.ordinal.interval.timer.dialog new file mode 100644 index 0000000..7b74569 --- /dev/null +++ b/dialog/en-us/started.ordinal.interval.timer.dialog @@ -0,0 +1,3 @@ +{ordinal} interval timer started for {duration} +Alright, I've set a {ordinal} interval timer for {duration} +I'm starting a {ordinal} interval timer for {duration} diff --git a/vocab/en-us/interval.timer.status.intent b/vocab/en-us/interval.timer.status.intent new file mode 100644 index 0000000..bd9607a --- /dev/null +++ b/vocab/en-us/interval.timer.status.intent @@ -0,0 +1,3 @@ +An interval timer has started for every {duration} +Alright, I've set an interval timer for every {duration} +I'm starting an interval timer for every {duration} diff --git a/vocab/en-us/start.interval.timer.intent b/vocab/en-us/start.interval.timer.intent new file mode 100644 index 0000000..fd6d818 --- /dev/null +++ b/vocab/en-us/start.interval.timer.intent @@ -0,0 +1,5 @@ +(another|one more|second|third|fourth|fifth|) interval timer ((for every |){duration}|) +ping me every {duration} +(set|start|create) (an|) interval timer +(set|start|create) (an|) interval timer for every {duration} +(set|start|create) (a|) {duration} interval timer diff --git a/vocab/en-us/stop.interval.timer.intent b/vocab/en-us/stop.interval.timer.intent new file mode 100644 index 0000000..e14b07e --- /dev/null +++ b/vocab/en-us/stop.interval.timer.intent @@ -0,0 +1,7 @@ +(stop|end|cancel|abort|delete|kill|turn off) (all|the|) (interval timer|interval timers) +turn it off +silence +shutup +shut up +I got it +end timer