def test_nice_date(self): for lang in self.test_config: i = 1 while (self.test_config[lang].get('test_nice_date') and self.test_config[lang]['test_nice_date'].get(str(i))): p = self.test_config[lang]['test_nice_date'][str(i)] dp = ast.literal_eval(p['datetime_param']) np = ast.literal_eval(p['now']) dt = datetime.datetime( dp[0], dp[1], dp[2], dp[3], dp[4], dp[5]) now = None if not np else datetime.datetime( np[0], np[1], np[2], np[3], np[4], np[5]) print('Testing for ' + lang + ' that ' + str(dt) + ' is date ' + p['assertEqual']) self.assertEqual(p['assertEqual'], nice_date(dt, lang=lang, now=now)) i = i + 1 # test fall back to english dt = datetime.datetime(2018, 2, 4, 0, 2, 3) self.assertEqual(nice_date( dt, lang='invalid', now=datetime.datetime(2018, 2, 4, 0, 2, 3)), 'today') # test all days in a year for all languages, # that some output is produced for lang in self.test_config: for dt in (datetime.datetime(2017, 12, 30, 0, 2, 3) + datetime.timedelta(n) for n in range(368)): self.assertTrue(len(nice_date(dt, lang=lang)) > 0)
def handle_get_next_appointment(self, message): """ Generates the response for the intent asking for the next appointment in the connected Nextcloud calendar. :param message: The speech input message. Unused in this method. """ del message # unused by handler next_event = self.caldav_interface.get_next_event() output_data = {} dialog_filename = "next.appointment" if next_event is not None: title = next_event["title"] startdate_time = next_event["starttime"] enddate_time = next_event["endtime"] output_data["startdate"] = nice_date(startdate_time) if is_multiple_fulldays_event(startdate_time, enddate_time): output_data["enddate"] = nice_date(enddate_time) else: if not is_fullday_event(startdate_time, enddate_time): output_data["time"] = nice_time(startdate_time) if title is not None: output_data["title"] = title # because we are using Python v3.7.x # the order of the keys of the dictionary is the the same as inserted, # so we can iterate over the keys to generate the correct dialog filenames for key in output_data: dialog_filename += "." + key self.speak_dialog(dialog_filename, output_data)
def dump_alarms(self, tag=""): # Useful when debugging dump = "\n" + "="*30 + " ALARMS " + tag + " " + "="*30 + "\n" dump += "raw = " + str(self.settings["alarm"]) + "\n\n" now_ts = to_utc(now_utc()).timestamp() dt = datetime.fromtimestamp(now_ts) dump += "now = {} ({})\n".format( nice_time(self.get_alarm_local(timestamp=now_ts), speech=False, use_ampm=not self.use_24hour, use_24hour = self.use_24hour), now_ts) dump += " U{} L{}\n".format(to_utc(dt), to_local(dt)) dump += "\n\n" idx = 0 for alarm in self.settings["alarm"]: dt = self.get_alarm_local(alarm) dump += "alarm[{}] - {} \n".format(idx, alarm) dump += " Next: {} {}\n".format( nice_time(dt, speech=False, use_ampm= not self.use_24hour, use_24hour = self.use_24hour), nice_date(dt, now=now_local())) dump += " U{} L{}\n".format(dt, to_local(dt)) if 'snooze' in alarm: dtOrig = self.get_alarm_local(timestamp=alarm['snooze']) dump += " Orig: {} {}\n".format( nice_time(dtOrig, speech=False, use_ampm= not self.use_24hour, use_24hour = self.use_24hour), nice_date(dtOrig, now=now_local())) idx += 1 dump += "="*75 self.log.info(dump)
def handle_read_tasks_intent(self, message): if self._check_for_credentials(): return time = extract_datetime(message.data.get("utterance", ""))[0] project = self._extract_project(message.data.get("utterance", "")) tasks = self.todoist.get_tasks(time=time, project_name=project) if len(tasks) > 0: if not self.todoist.project_exists(project): project = "" self.speak_dialog('TasksOnTodo', data={ 'project': project, 'datetime': nice_date(time, now=datetime.datetime.now()) }) for task in tasks: self.speak(task) else: self.speak_dialog('NoTasks', data={ 'project': project or "", 'datetime': nice_date(time, now=datetime.datetime.now()) })
def handle_get_appointment_date(self, message): """ Handles the intent asking for events on a specific date. Checks the calendar on the specified date first, then responses with the number of planned events. If it's not 0, the user is asked if the events should be listed. If the user responds with 'yes' all events are listed with title, start and end time. :param message: The speech input message. Used to extract the specified date """ self.log.info(f"Intent message: {message.data['utterance']}") extracted_date = extract_datetime(message.data['utterance'], datetime.now(self.timezone))[0] self.log.debug(f"Extracted date(s): {extracted_date}") events = self.caldav_interface.get_events_for_date(extracted_date) self.log.debug(f"Events on {extracted_date}: {events}") if len(events) == 0: self.speak_dialog("no.events.on.date", { "date": nice_date(extracted_date, now=datetime.now(self.timezone)) }) else: self.speak_dialog( "number.events.on.date", { "date": nice_date(extracted_date, now=datetime.now(self.timezone)), "number_of_appointments": len(events), "plural": "s" if len(events) > 1 else "" }) list_events = self.ask_yesno("list.events.of.date", { "date": nice_date(extracted_date, now=datetime.now(self.timezone)) }) if list_events == "yes": self.speak_dialog( "events.on.date", { "date": nice_date(extracted_date, now=datetime.now(self.timezone)) }) for event in events: title = event["title"] startdate_time = event["starttime"] enddate_time = event["endtime"] self.speak_dialog( "event.details", { "title": title, "starttime": nice_time(startdate_time, use_ampm=True), "endtime": nice_time(enddate_time, use_ampm=True) })
def handle_date_last_weekend(self, message): # Strip year off nice_date as request is inherently close # Don't pass `now` to `nice_date` as a # request on Monday will return "yesterday" saturday_date = ', '.join(nice_date(extract_datetime( self.texts.get('previous saturday'))[0]).split(', ')[:2]) sunday_date = ', '.join(nice_date(extract_datetime( self.texts.get('previous sunday'))[0]).split(', ')[:2]) self.speak_dialog('date.last.weekend', { 'direction': self.texts.get('last'), 'saturday_date': saturday_date, 'sunday_date': sunday_date })
def handle_date_future_weekend(self, message): # Strip year off nice_date as request is inherently close # Don't pass `now` to `nice_date` as a # request on Friday will return "tomorrow" saturday_date = ', '.join(nice_date(extract_datetime( self.texts.get('this saturday'))[0]).split(', ')[:2]) sunday_date = ', '.join(nice_date(extract_datetime( self.texts.get('this sunday'))[0]).split(', ')[:2]) self.speak_dialog('date.future.weekend', { 'direction': self.texts.get('next'), 'saturday_date': saturday_date, 'sunday_date': sunday_date })
def select_event_date_not_none(self, title, date): """ If the date of an event that should be deleted is given this method is used to handle the selection of the correct event if there are multiple events for that date. When the event is found the user is asked for confirmation and the event is deleted. :param date: datetime of the event that should be deleted :return: None """ events_on_date = self.caldav_interface.get_events_for_date(date) if title is not None: event = next( (event for event in events_on_date if event["title"] == title), None) if event is not None: return event if len(events_on_date) == 0: self.speak_dialog("no.events.events.on.date", {"date": nice_date(date)}) if len(events_on_date) == 1: return events_on_date[0] if len(events_on_date) > 1: title_of_events = [ event["title"] for event in events_on_date if event["title"] is not None ] self.speak_dialog("multiple.matching.events", {"detail": "date"}) title = self.ask_selection(title_of_events, "event.selection.delete", None, 0.7) event = next( (event for event in events_on_date if event["title"] == title), None) return event return None
def date_str(self, d): if is_today(d): return 'today' elif is_tomorrow(d): return 'tomorrow' else: return nice_date(d.date())
def date_str(self, d): if is_today(d): return self.texts.get('today') elif is_tomorrow(d): return self.texts.get('tomorrow') else: return nice_date(d.date(), lang=self.lang)
def handle_num_appoint(self, message): if self.update_credentials() is False: # No credentials return # clean/get date in utter utter = message.data["utterance"] when = extract_datetime(utter, datetime.datetime.now(), self.lang)[0] if when is None: when = extract_datetime("today", datetime.datetime.now(), self.lang) self.log.info(str(when)) # get events events = self.get_events(when) nice_when = nice_date(when, now=now_local(), lang=self.lang) if events: num_events = len(events) if num_events == 1: self.speak_dialog("num.event", data={"when": nice_when}) else: self.speak_dialog("num.events", data={ "num_events": num_events, "when": nice_when }) elif events is None or events == []: self.speak_dialog("no.events", data={"when": nice_when})
def handle_meetings_at_day(self, message): """Method is called when user speaks an intent in ``meetings.at.day.intent``. Calls the methods login_to_nextcloud() and get_appointment_info() to get the info of the users next appointment and gives the answer, for a specific day. speak() is a build-in MycroftSkill method, to let mycroft speak to the user. """ self.login_to_nextcloud() try: start = get_date(message.data) list_of_events = self.get_appointment_info(from_start=start, days=1, get_next=False) if len(list_of_events) > 0: self.log.info(list_of_events) events_string = ' and '.join(event[1]\ for event in list_of_events) self.speak('On '+nice_date(start)+\ ' you have the following appointments: '+\ events_string) else: self.speak('You Don\'t have any appointments planned') except TypeError: self.speak('Sorry, I need you to tell me a month and day')
def get_nice_event(event, is_on_date=False): """Transforms Events nicely spoken for Mycroft. nice_date() and nice_time() are functions from Mycroft.util.format that uses Lingua Franca to transform numbers and dates etc. to words. see mycroft.ai documentation at: https://mycroft-ai.gitbook.io/docs/mycroft-technologies/lingua-franca Args: event: Event extracted from Nextcloud. Returns: apmnt_date_time (str): The Date and Time of the next appointment nicely spoken String. apmnt_title (str): The Title of the Appointment. """ if type(event[0]) is date: apmnt_date_time = nice_date(event[0]) + ", all day " if is_on_date: apmnt_date_time = ", all day" else: apmnt_date_time = nice_date_time(event[0]) if is_on_date: apmnt_date_time = ", at " + apmnt_date_time apmnt_title = str(event[1]) return apmnt_date_time, apmnt_title
def handle_create_event(self, message): """ Handler to create a new event in the calendar. It checked first if a title or date is given in the intent message. Then the user is asked for missing information as start time, duration or full-day event. If all information are present the event is created using the CalDav interface. :param message: the intent message :return: None """ title, date = get_title_date_from_message(message) fullday = False while title is None: title = self.get_response("ask.for.title", {"action": "create"}) self.log.info(f"set title of new event to: {title}") if date is None: date_response = self.get_response("ask.for.date") date, rest = extract_datetime(date_response) self.log.info(f"set date of new event to: {date}") title = self.remove_time_from_title(title) if date.time() == dt.time(0): time, rest = None, None pattern = re.compile(r"full\s*day") while time is None or time.time() == dt.time(0): rest = self.get_response("new.event.starttime", {"event": title}) if rest is not None: extracted = extract_datetime(rest) if extracted is not None: time, rest = extracted if re.search(pattern, rest): fullday = True break if not fullday: date = datetime.combine(date.date(), time.time()) duration = None while duration is None: if not fullday: duration_utterance = self.get_response( "ask.duration.new.event") duration, rest = extract_duration(duration_utterance) else: duration_utterance = self.get_response("ask.fullday.duration") duration = extract_number(duration_utterance) self.log.info( f"New calendar event will be created. Title: {title}, Date: {str(date)}" ) self.caldav_interface.create_new_event(title, date, duration, fullday) self.speak_dialog("successful.create.event", { "event": title, "date": nice_date(date) })
def delete_event_on_confirmation(self, event): """ Asks the user for confirmation before a event is deleted. If user confirms the event is deleted from the Nextcloud calendar. :param event: dictionary with the details of the event """ if event is not None: confirm = self.ask_yesno('confirm.delete.event', { "title": event["title"], "date": nice_date(event["starttime"]) }) if confirm == "yes": self.caldav_interface.delete_event(event) self.speak_dialog("successful.delete.event", { "title": event["title"], "date": nice_date(event["starttime"]) }) return self.speak_dialog("no.event.changed", {"action": "deleted"})
def handle_movie_year(self, message): """ Gets the year the movie was released. """ movie = message.data.get("movie") try: movieDetails = MOVIE.details(MOVIE.search(movie)[:1][0].id) self.speak_dialog("movie.year", {"movie": movieDetails.title, "year": nice_date(datetime.strptime(movieDetails.release_date.replace("-", " "), "%Y %m %d"))}) ## If the title can not be found, it creates an IndexError except IndexError: self.speak_dialog("no.info", {"movie": movie})
def handle_movie_information(self, message): """ Gets the short version and adds the TagLine for good measure. """ movie = message.data.get("movie") try: movieDetails = MOVIE.details(MOVIE.search(movie)[:1][0].id) self.speak_dialog("movie.info.response", {"movie": movieDetails.title, "year": nice_date(datetime.strptime(movieDetails.release_date.replace("-", " "), "%Y %m %d")), "budget": nice_number(movieDetails.budget)}) self.speak(movieDetails.tagline) # If the title can not be found, it creates an IndexError except IndexError: self.speak_dialog("no.info", {"movie": movie})
def _read_unread_channel(self, chan): if self.state == "stopped": return msg_count = chan['msg_count'] if msg_count: channel_message = self.dialog_renderer.render( "messages.for.channel", {'display_name': chan['display_name']}) LOG.debug(channel_message) self.speak(channel_message) pfc = self.mm.posts.get_posts_for_channel(chan['channel_id']) order = pfc['order'] # in case returned posts are less than number of unread # avoid 'index out of bounds' msg_count = msg_count if msg_count < len(order) else len(order) prev_date = "" for i in range(0, msg_count): if self.state == "stopped": break # order starts with newest to oldest, # start to read the oldest of the unread post = pfc['posts'][order[msg_count - i - 1]] create_at = "" # nice_date does only support en-us yet - bummer! # MM timestamps are in millisecs, python in secs msg_date = nice_date(datetime.fromtimestamp(post['create_at'] / 1000), self.lang, now=datetime.now()) if prev_date != msg_date: create_at = msg_date + " " prev_date = msg_date msg_time = nice_time( datetime.fromtimestamp(post['create_at'] / 1000), self.lang) create_at += msg_time msg = self.dialog_renderer.render( "message", { 'user_name': self._get_user_name(post['user_id']), 'create_at': create_at, 'message': post['message'] }) LOG.debug(msg) self.speak(msg, wait=True) time.sleep(.3) # mark channel as read self.mm.channels.view_channel(self.userid, {'channel_id': chan['channel_id']}) # TODO clarify when to reset prev_unread/prev_mentions self.prev_unread = 0 self.prev_mentions = 0
def __save_reminder_local(self, reminder, reminder_time): """ Speak verification and store the reminder. """ # Choose dialog depending on the date if is_today(reminder_time): self.speak_dialog('SavingReminder', {'timedate': nice_time(reminder_time, lang=self.lang)}) elif is_tomorrow(reminder_time): self.speak_dialog('SavingReminderTomorrow', {'timedate': nice_time(reminder_time, lang=self.lang)}) else: self.speak_dialog('SavingReminderDate', {'time': nice_time(reminder_time, lang=self.lang), 'date': nice_date(reminder_time, lang=self.lang)}) # Store reminder serialized = serialize(reminder_time) if 'reminders' in self.settings: self.settings['reminders'].append((reminder, serialized)) else: self.settings['reminders'] = [(reminder, serialized)]
def get_next_reminder(self, msg=None): """ Get the first upcoming reminder. """ if len(self.settings.get('reminders', [])) > 0: reminders = [(r[0], deserialize(r[1])) for r in self.settings['reminders']] next_reminder = sorted(reminders, key=lambda tup: tup[1])[0] if is_today(next_reminder[1]): self.speak_dialog('NextToday', data={'time': nice_time(next_reminder[1], lang=self.lang), 'reminder': next_reminder[0]}) elif is_tomorrow(next_reminder[1]): self.speak_dialog('NextTomorrow', data={'time': nice_time(next_reminder[1], lang=self.lang), 'reminder': next_reminder[0]}) else: self.speak_dialog('NextOtherDate', data={'time': nice_time(next_reminder[1], lang=self.lang), 'date': nice_date(next_reminder[1], lang=self.lang), 'reminder': next_reminder[0]}) else: self.speak_dialog('NoUpcoming')
def handle_day_appoint(self, message): # clean/get date in utter if self.update_credentials() is False: # No credentials return utter = message.data["utterance"] when = extract_datetime(utter, datetime.datetime.now(), self.lang)[0] if when is None: when = extract_datetime("today", datetime.datetime.now(), self.lang) self.log.info(str(when)) # get events events = self.get_events(when) nice_when = nice_date(when, now=now_local(), lang=self.lang) if events: # say first self.speak_dialog("day", data={ "num_events": len(events), "event": events[0].get("event"), "when": nice_when, "time": nice_time(events[0].get("datetime"), use_ampm=True) }) # Say follow up for x in range(1, len(events)): self.speak_dialog("day.followed", data={ "event": events[x].get("event"), "time": nice_time(events[x].get("datetime"), use_ampm=True) }) elif events is None or events == []: self.speak_dialog("no.events", data={"when": nice_when})
def handle_cinema_deckchair(self, message): try: now_date = datetime.now() # 1. Scrape website for movie on this date try: program_tr_list = self.cinema_program.fetch( cache=[join(self.file_system.path), now_date]) except (exceptions.ConnectionError or exceptions.HTTPError or exceptions.Timeout or exceptions.TooManyRedirects) as e: self.log.error('Error: {0}'.format(e)) self.speak_dialog('error.http') return # 2. Extract date from utterance, or default to today when = extract_datetime(message.data.get('utterance'))[0] if self.testing_date is not None: when = self.testing_date # 3. Test if date is in deckchair program range first_date = self._get_date_from_list(program_tr_list, 'first') last_date = self._get_date_from_list(program_tr_list, 'last') if when < first_date or last_date < when: self.speak_dialog( 'error.datenotfound', { 'when': nice_date(when, now=now_date), 'first_date': nice_date(first_date, now=now_date), 'last_date': nice_date(last_date, now=now_date) }) return False # 4. Find movie on provided date try: date_row = next( (x for x in program_tr_list if x.getchildren()[0].text == when.strftime('%A %-d %B'))) except StopIteration: self.log.info('Date note found: {}'.format( when.strftime('%A %-d %B'))) return self.speak_dialog( 'error.datenotfound', { 'when': nice_date(when, now=now_date), 'first_date': nice_date(first_date, now=now_date), 'last_date': nice_date(last_date, now=now_date) }) movies_on_date = self._add_movie_from_date(date_row, movies_on_date=[]) # 5. Construct message to return movie_details_dialog = [] for movie in movies_on_date: if len(movie_details_dialog) > 0: movie_details_dialog.append(self.translate('and')) movie_title = movie.getchildren()[0].getchildren()[0].text movie_time = nice_time( when.replace(hour=int(movie.getchildren()[1].text[0:-5]), minute=int( movie.getchildren()[1].text[-4:-2]))) movie_details_dialog.append( self.translate('movie.at.time', { 'title': movie_title, 'time': movie_time })) self.speak_dialog( 'whats.on', { 'when': nice_date(when, now=datetime.now()), 'movie_details': ', '.join(movie_details_dialog) }) # 6. Fetch data and set context for follow up questions # Reset data from previous requests. self._movies_on_requested_date = [] self._current_movie_context = '' for movie in movies_on_date: movie_details = self._fetch_movie_details(movie) self._movie_dict[movie_details['title']] = movie_details self._movies_on_requested_date.append( movie.getchildren()[0].getchildren()[0].text) self.set_context('DeckchairContext', 'True') except Exception as e: self.log.exception(format(e)) self.speak_dialog('error')
def _future_time_delta_en(future_date, anchor=None): """ Return a nice string representing a future datetime in english. If you need to explicitly set the reference that the future is relative to, just pass it in as a second datetime object. """ if not anchor: anchor = now_local() delta = future_date - anchor seconds = delta.seconds days = delta.days global_seconds = days * 24 * 60 * 60 + seconds minutes = int(round(seconds / 60.) % 60) day_changes = (to_local(future_date) - to_local(datetime(*anchor.timetuple()[:3]))).days if days < 0: raise AttributeError("Negative timedelta. I can only do futures!") if global_seconds <= 45: if seconds == 1: return "a second" if seconds <= 15: return 'a moment' else: return str(seconds) + " seconds" elif global_seconds < 60 * 59.5: if seconds <= 90: return 'about a minute' elif seconds <= 60 * 4.5: return str(minutes) + " minutes" elif anchor.day == future_date.day: h = seconds // 3600 if h == 1: hour_string = "1 hour" else: hour_string = str(h) + " hours" if minutes > 4: hour_string += " and " + str(minutes) + " minutes" return hour_string elif global_seconds <= 60 * 60 * 24 * 2 and day_changes == 1: if future_date.hour == 0: if future_date.minute == 0: return 'midnight tonight' return 'tomorrow at %s' % nice_time_en(future_date) elif global_seconds <= 60 * 60 * 24 * 8 and day_changes <= 7: if day_changes <= 3 or (future_date.weekday() == 6 and anchor.weekday() != 6): return '%s at %s' % (future_date.strftime('%A'), nice_time_en(future_date)) elif (future_date.weekday() > anchor.weekday() or anchor.weekday() == 6) and day_changes <= 6: return 'this %s at %s' % (future_date.strftime('%A'), nice_time_en(future_date)) else: return 'next %s at %s' % (future_date.strftime('%A'), nice_time_en(future_date)) else: # needs to be here from mycroft.util.format import nice_date return nice_date(future_date)
def test_invalid_lang_code(self): dt = datetime.datetime(2018, 2, 4, 0, 2, 3) with self.assertRaises(UnsupportedLanguageError): nice_date(dt, lang='invalid', now=dt)
def handle_query_date(self, message, response_type="simple"): utt = message.data.get('utterance', "").lower() try: extract = extract_datetime(utt) except Exception: self.speak_dialog('date.not.found') return day = extract[0] if extract else now_local() # check if a Holiday was requested, e.g. "What day is Christmas?" year = extract_number(utt) if not year or year < 1500 or year > 3000: # filter out non-years year = day.year all_holidays = {} # TODO: How to pick a location for holidays? for st in holidays.US.STATES: holiday_dict = holidays.US(years=[year], state=st) for d, name in holiday_dict.items(): if name not in all_holidays: all_holidays[name] = d for name in all_holidays: d = all_holidays[name] # Uncomment to display all holidays in the database # self.log.info("Day, name: " +str(d) + " " + str(name)) if name.replace(" Day", "").lower() in utt: day = d break location = self._extract_location(utt) today = to_local(now_utc()) if location: # TODO: Timezone math! if (day.year == today.year and day.month == today.month and day.day == today.day): day = now_utc() # for questions ~ "what is the day in sydney" day = self.get_local_datetime(location, dtUTC=day) if not day: return # failed in timezone lookup speak_date = nice_date(day, lang=self.lang) # speak it if response_type == "simple": self.speak_dialog("date", {"date": speak_date}) elif response_type == "relative": # remove time data to get clean dates day_date = day.replace(hour=0, minute=0, second=0, microsecond=0) today_date = today.replace(hour=0, minute=0, second=0, microsecond=0) num_days = (day_date - today_date).days if num_days >= 0: speak_num_days = nice_duration(num_days * 86400) self.speak_dialog("date.relative.future", { "date": speak_date, "num_days": speak_num_days }) else: # if in the past, make positive before getting duration speak_num_days = nice_duration(num_days * -86400) self.speak_dialog("date.relative.past", { "date": speak_date, "num_days": speak_num_days }) # and briefly show the date self.answering_query = True self.show_date(location, day=day) time.sleep(10) mycroft.audio.wait_while_speaking() if self.platform == "mycroft_mark_1": self.enclosure.mouth_reset() self.enclosure.activate_mouth_events() self.answering_query = False self.displayed_time = None
def format_date(self, date: datetime.date): return nice_date(datetime.datetime(date.year, date.month, date.day), now=datetime.datetime.now(tz=self.timezone))
def getMovieDate(self): return nice_date( datetime.strptime(self.movieDetails.release_date.replace("-", " "), "%Y %m %d"))