diff --git a/__init__.py b/__init__.py index 88bb7bd4..e82b8d5c 100644 --- a/__init__.py +++ b/__init__.py @@ -26,44 +26,20 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import time import os -import sys +import time +from datetime import datetime, timedelta from threading import RLock -from typing import Tuple, List, Optional, Union, Any -from datetime import datetime, timedelta, timezone +from typing import List, Optional from dateutil.relativedelta import relativedelta -# TODO - deprecate LF (ovos-classifiers should be used) -from lingua_franca.format import ( - nice_duration, - nice_time, - nice_date_time, - pronounce_number, - join_list, -) -# TODO - normalization happens in intent stage (transformer) > 0.0.8 -from lingua_franca.parse import normalize from ovos_bus_client.message import Message -from ovos_workshop.skills import OVOSSkill -from ovos_utils.log import LOG -from ovos_workshop.intents import IntentBuilder -from ovos_workshop.decorators import intent_handler, killable_intent -from ovos_utils.sound import play_audio -from ovos_utils import create_daemon, classproperty -from ovos_utils.process_utils import RuntimeRequirements -from ovos_config import get_default_lang - -from ovos_skill_alerts.util import Weekdays, AlertState, MatchLevel, AlertPriority, WEEKDAYS, WEEKENDS, EVERYDAY -from ovos_skill_alerts.util.ui_models import build_gui_data -from ovos_skill_alerts.util.alert_manager import AlertManager, SYNC_LOCK +from ovos_date_parser import nice_date_time, nice_time, nice_duration +from ovos_number_parser import pronounce_number +from ovos_skill_alerts.util import AlertState, MatchLevel, AlertPriority, WEEKDAYS +from ovos_skill_alerts.util.misc import join_list from ovos_skill_alerts.util.alert import Alert, AlertType, DAVType, LOCAL_USER -from ovos_skill_alerts.util.media import ( - get_ocp_media_type, - ocp_query, - ocp_request, - get_media_source_from_intent -) +from ovos_skill_alerts.util.alert_manager import AlertManager, SYNC_LOCK from ovos_skill_alerts.util.config import use_24h_format, get_default_tz, DEFAULT_SETTINGS from ovos_skill_alerts.util.locale import ( translate, @@ -75,6 +51,11 @@ get_alert_type_from_intent, datetime_display ) +from ovos_skill_alerts.util.media import ( + ocp_query, + ocp_request, + get_media_source_from_intent +) from ovos_skill_alerts.util.parse_utils import ( tokenize_utterance, build_alert_from_intent, @@ -94,6 +75,15 @@ fuzzy_match, fuzzy_match_alerts ) +from ovos_skill_alerts.util.ui_models import build_gui_data +from ovos_utils import create_daemon, classproperty +from ovos_utils.log import LOG +from ovos_utils.process_utils import RuntimeRequirements +from ovos_utils.sound import play_audio +from ovos_utterance_normalizer import UtteranceNormalizerPlugin +from ovos_workshop.decorators import intent_handler +from ovos_workshop.intents import IntentBuilder +from ovos_workshop.skills import OVOSSkill class AlertSkill(OVOSSkill): @@ -108,7 +98,7 @@ def __init__(self, *args, **kwargs): self._gui_timer_lock = RLock() super().__init__(*args, **kwargs) - + @classproperty def runtime_requirements(self): return RuntimeRequirements(internet_before_load=False, @@ -177,7 +167,7 @@ def timer_sound_file(self) -> str: if not file: raise FileNotFoundError(f"Could not resolve sound: {filename}") return file - + @property def play_volume(self) -> int: """ @@ -288,8 +278,8 @@ def _init_dav_clients(self): self.dav_services = dav_services self.sync_frequency = sync_frequency -# Intent Handlers - #@killable_intent() + # Intent Handlers + # @killable_intent() @intent_handler(IntentBuilder("CreateAlarm").require("alarm") .require("create").optionally("question") .optionally("playable").optionally("weekdays") @@ -341,7 +331,7 @@ def handle_ocp_alarm(self, message: Message): alert_time = parse_alert_time_from_message(message) # in case no time was specified, use the next upcoming alarm - if not alert_time: + if not alert_time: alarms = self._get_alerts_list(AlertType.ALARM) if not alarms: return self.speak_dialog("list_alert_none_upcoming", @@ -352,13 +342,13 @@ def handle_ocp_alarm(self, message: Message): alarm.ocp_request = ocp_result assert alarm.media_type == "ocp" self.confirm_alert(alarm, message) - + @intent_handler(IntentBuilder("CreateOcpAlarmAlt").require("wake") .require("media").optionally("question")) def handle_ocp_alarm_alt(self, message: Message): return self.handle_ocp_alarm(message) - - #@killable_intent() + + # @killable_intent() @intent_handler(IntentBuilder("CreateTimer").require("create") .require("timer").optionally("question") .optionally("until")) @@ -379,7 +369,7 @@ def handle_create_timer(self, message: Message): self.confirm_alert(alert, message) - #@killable_intent() + # @killable_intent() @intent_handler(IntentBuilder("CreateReminder").require("create") .require("reminder").optionally("question") .optionally("playable").optionally("weekdays") @@ -401,7 +391,7 @@ def handle_create_reminder(self, message: Message): time_ = self.get_response( "reminder_ask_time", validator=validate_dt_or_delta, num_retries=0 ) - #if response and response != "no": + # if response and response != "no": if isinstance(time_, datetime): alert.expiration = time_ elif isinstance(time_, timedelta): @@ -423,8 +413,8 @@ def handle_create_reminder(self, message: Message): for r in overlapping], "and")} end = [a.until for a in overlapping if a.until] if end: - dialog_data["begin"]= nice_time(min([a.expiration - for a in overlapping])) + dialog_data["begin"] = nice_time(min([a.expiration + for a in overlapping])) dialog_data["end"] = nice_time(max(end)) dialog = "alert_overlapping_duration_ask" else: @@ -433,8 +423,8 @@ def handle_create_reminder(self, message: Message): dialog = "alert_overlapping_ask" if self.ask_yesno(dialog, dialog_data) in ( - "no", - None, + "no", + None, ): return @@ -444,7 +434,7 @@ def handle_create_reminder(self, message: Message): self.specify_dav_attributes(alert, spoken_type) self.confirm_alert(alert, message) - #@killable_intent() + # @killable_intent() @intent_handler(IntentBuilder("CreateReminderAlt").require("remind") .optionally("question").optionally("playable") .optionally("weekdays").optionally("weekends") @@ -457,7 +447,7 @@ def handle_create_reminder_alt(self, message: Message): """ self.handle_create_reminder(message) - #@killable_intent() + # @killable_intent() @intent_handler(IntentBuilder("CreateEvent") .require("create").require("event") .optionally("question").optionally("playable") @@ -489,7 +479,7 @@ def handle_reschedule_alert(self, message: Message): if alert is None: return self.speak_dialog("error_no_scheduled_kind", {"kind": spoken_type}, wait=True) - + # ref_day = convenience experimental because of repeating alerts # user only have to state the time instead remembering the day ref_day = alert.expiration.replace(hour=0, minute=0, second=0) @@ -503,24 +493,24 @@ def handle_reschedule_alert(self, message: Message): else: alert.is_all_day = False rescheduled_time = \ - parse_relative_time_from_message(message, - anchor_time=anchor_time) + parse_relative_time_from_message(message, + anchor_time=anchor_time) dialog = "alert_rescheduled" if rescheduled_time is None: return self.speak_dialog("error_no_time", - {"kind": spoken_type}, wait=True) - + {"kind": spoken_type}, wait=True) + once = True if alert.has_repeat and \ self.ask_yesno("reschedule_recurring_ask", {"type": spoken_type}) == "yes": - once = False + once = False rescheduled = \ - self.alert_manager.reschedule_alert(alert, - rescheduled_time, - once) + self.alert_manager.reschedule_alert(alert, + rescheduled_time, + once) self._display_alert(rescheduled) dialog_data = get_alert_dialog_data(rescheduled, self.lang) self.speak_dialog(dialog, dialog_data, wait=True) @@ -529,7 +519,7 @@ def handle_reschedule_alert(self, message: Message): self.ask_for_prenotification(rescheduled, "alert_rescheduled_prenotification", dialog_data) - + @intent_handler(IntentBuilder("RescheduleAlertAlt") .one_of("earlier", "later").optionally("next") .one_of("alarm", "reminder", "event", "timer") @@ -555,7 +545,7 @@ def handle_change_properties(self, message: Message): if alert is None: return self.speak_dialog("error_no_scheduled_kind", {"kind": spoken_type}, wait=True) - + if message.data.get("priority"): old_priority = alert.priority priority = parse_alert_priority_from_message(message) @@ -597,7 +587,7 @@ def handle_change_media_properties(self, message: Message): alert_type, spoken_type = get_alert_type_from_intent(message) alert: Alert = self._resolve_requested_alert(message, alert_type) - + old_media = alert.media_type new_media = get_media_source_from_intent(message) if new_media == "file": @@ -606,7 +596,7 @@ def handle_change_media_properties(self, message: Message): alert.audio_file = f"file:/{audio}" else: return self.speak_dialog("error_no_script", {"kind": spoken_type}) - + if old_media: self.speak_dialog("media_type_changed", {"old": translate(old_media), @@ -629,8 +619,8 @@ def handle_event_timeframe_check(self, message: Message): Are there any events at 8 pm :param message: Message associated with request """ - utterance = normalize(message.data.get("utterance", ""), - lang=self.lang, remove_articles=False).lower() + normalizer = UtteranceNormalizerPlugin.get_normalizer(lang=self.lang) + utterance = normalizer.normalize(message.data.get("utterance", "")).lower() alert_type, spoken_type = get_alert_type_from_intent(message) begin, end = parse_timeframe_from_message(message) @@ -643,7 +633,7 @@ def handle_event_timeframe_check(self, message: Message): if self.voc_match("next", utterance): return self.handle_next_alert(message) return self.handle_list_all_alerts(message) - + overlapping = self.alert_manager.get_alerts_in_timeframe(begin, end, alert_type) @@ -660,7 +650,7 @@ def handle_event_timeframe_check(self, message: Message): self.speak_dialog("list_alert_wo_duration", data) else: self.speak_dialog("list_alert_timeframe_none") - + def handle_list_all_alerts(self, message: Message): """ Handler to handle request for all alerts (kind optional) @@ -677,11 +667,11 @@ def handle_list_all_alerts(self, message: Message): self._display_alerts(alert_type, alerts_list) if message.data.get("alert"): - kinds = {(a.alert_type.value, spoken_alert_type(a.alert_type)) - for a in alerts_list if a.expiration is not None} + kinds = {(a.alert_type.value, spoken_alert_type(a.alert_type)) + for a in alerts_list if a.expiration is not None} else: kinds = {(alert_type.value, spoken_type)} - + # Restrict to max 10(?) alerts if len(alerts_list) > 10: alerts_list = alerts_list[:10] @@ -725,7 +715,7 @@ def handle_next_alert(self, message: Message): data = get_alert_dialog_data(alert, self.lang) self._display_alert(alert) - + if alert.alert_type == AlertType.TIMER: dialog = "next_alert_timer" else: @@ -734,8 +724,8 @@ def handle_next_alert(self, message: Message): self.speak_dialog(dialog, data, wait=True) @intent_handler(IntentBuilder("TimerStatus").one_of("time", "timer") - .require("remaining").optionally("query") - ) + .require("remaining").optionally("query") + ) def handle_timer_status(self, message: Message): """ Intent handler to handle request for timer status (name optional) @@ -749,11 +739,11 @@ def handle_timer_status(self, message: Message): # show timers if not already up self._display_alerts(AlertType.TIMER, user_timers) - + for i, timer in enumerate(user_timers): dialog_data = get_alert_dialog_data(timer, self.lang) if not dialog_data["name"] and len(user_timers) > 1: - dialog_data["name"] = pronounce_number(i+1, ordinals=True) + dialog_data["name"] = pronounce_number(i + 1, ordinals=True) self.speak_dialog("timer_status", dialog_data, wait=True) # TODO - connect this to naptime skill - mycroft.awoken bus message @@ -806,7 +796,7 @@ def handle_cancel_alert(self, message: Message): for alert in alerts: self._dismiss_alert(alert.ident, drop_dav=True) return self.speak_dialog("confirm_cancel_timeframe" if begin else \ - "confirm_cancel_all", + "confirm_cancel_all", {"kind": spoken_type, "num": len(alerts)}, wait=True) # Only one candidate alert @@ -829,7 +819,7 @@ def handle_cancel_alert(self, message: Message): 'name': name}, wait=True) # Todo Lists - #@killable_intent() + # @killable_intent() @intent_handler(IntentBuilder("CreateList").require("create").require("list")) def handle_create_todo(self, message: Message, alert: Optional[Alert] = None): """ @@ -861,7 +851,7 @@ def handle_create_todo(self, message: Message, alert: Optional[Alert] = None): self.speak_dialog("confirm_todo_set", wait=True) - #@killable_intent() + # @killable_intent() @intent_handler( IntentBuilder("AddListSubitems") .require("create").require("list").require("items") @@ -952,7 +942,7 @@ def handle_query_todo_reminder_names(self, message: Message): .require("query").require("list").require("items") ) def handle_todo_list_entries(self, message: Optional[Message] = None, - alert: Optional[Alert] = None): + alert: Optional[Alert] = None): """ Intent to get the items from a specific todo list :param message: Message associated with request @@ -980,7 +970,7 @@ def handle_todo_list_entries(self, message: Optional[Message] = None, # delay if this handler is used in context time.sleep(2) - #@killable_intent() + # @killable_intent() @intent_handler( IntentBuilder("DeleteListEntries") .require("delete").require("items").require("list").optionally("stored") @@ -996,7 +986,7 @@ def handle_delete_todo_list_entries(self, message: Message): if todo is None: return - + if message.data.get("stored"): to_delete = [todo.alert_name for todo in self.alert_manager.get_children(todo.ident)] @@ -1022,8 +1012,8 @@ def handle_delete_todo_list_entries(self, message: Message): deleted.remove(item) self.speak_dialog("list_todo_num_deleted", {"num": pronounce_number(len(deleted))}) - - #@killable_intent() + + # @killable_intent() @intent_handler( IntentBuilder("DeleteList") .require("delete").require("list") @@ -1034,14 +1024,15 @@ def handle_delete_todo_list(self, message: Message): if todo is None: return - + children = self.alert_manager.get_children(todo.ident) for child in children: self.alert_manager.mark_todo_complete(child) self.alert_manager.mark_todo_complete(todo) - self.speak_dialog("list_deleted", {"name": todo.alert_name}) + self.speak_dialog("list_deleted", {"name": todo.alert_name}) + + # @killable_intent() - #@killable_intent() @intent_handler( IntentBuilder("DeleteTodoEntries") .require("delete").require("todo").optionally("items").optionally("stored") @@ -1070,7 +1061,7 @@ def handle_delete_todo_entries(self, message: Message): to_delete = self._get_response_cascade("remove_list_items_ask", {"lang": self.lang}, message=message) - + deleted = to_delete[:] for item in to_delete: todo = fuzzy_match_alerts(todos, item, 90) @@ -1104,7 +1095,7 @@ def handle_speak_calendar_list(self, message: Message): "dav_calendar_list", data={"service": service, "calendars": join_list(calendars, "and")}, ) - + @intent_handler( IntentBuilder("DAVSync") .require("synchronize").one_of("calendar", "event", "reminder") @@ -1118,7 +1109,7 @@ def handle_dav_sync(self, message: Message): return self.alert_manager.sync_dav() - + def confirm_alert(self, alert: Alert, message: Message, anchor_time: Optional[datetime] = None): """ @@ -1151,7 +1142,7 @@ def confirm_alert(self, alert: Alert, message: Message, if alert.alert_type == AlertType.TIMER: self.speak_dialog('confirm_timer_started', - {'remaining': duration}, wait=True) + {'remaining': duration}, wait=True) return # Notify one-time Alert @@ -1200,8 +1191,8 @@ def confirm_alert(self, alert: Alert, message: Message, wait=True) def ask_for_prenotification(self, alert: Alert, - dialog: str = "alert_prenotification_ask", - data: Optional[dict] = None) -> None: + dialog: str = "alert_prenotification_ask", + data: Optional[dict] = None) -> None: """ Asks if a prenotification (short pre notice) should be added :param alert: Alert object built from user request @@ -1256,7 +1247,7 @@ def converse(self, message: Message): If there is an active alert, see if the user is trying to dismiss it """ user_alerts = self.alert_manager.get_alerts() - active : List[Alert] = user_alerts["active"] + active: List[Alert] = user_alerts["active"] if active: LOG.debug(f"User has active alerts: {[a.alert_name for a in active]}") for utterance in message.data.get("utterances"): @@ -1278,17 +1269,17 @@ def converse(self, message: Message): if voc_match(_utterance, "snooze", self.lang, exact=True): for alert in active: self._snooze_alert(alert, snooze_duration) - self.speak_dialog("confirm_snooze_alert", + self.speak_dialog("confirm_snooze_alert", {"duration": nice_duration( - round(duration.total_seconds()))}) + round(duration.total_seconds()))}) return True return False def _get_response_cascade(self, dialog: str = "", - data: Optional[dict] = None, - message: Optional[Message] = None): + data: Optional[dict] = None, + message: Optional[Message] = None): - data = data or dict() + data = data or dict() response = False items = [] self.speak_dialog(dialog, data, wait=True) @@ -1299,13 +1290,13 @@ def _get_response_cascade(self, dialog: str = "", return items -# Search methods + # Search methods def _resolve_requested_alert( - self, message: Message, - alert_type: AlertType, - tokens = None, - disposition: AlertState = AlertState.PENDING, - dialog: str = "pick_multiple_entries" + self, message: Message, + alert_type: AlertType, + tokens=None, + disposition: AlertState = AlertState.PENDING, + dialog: str = "pick_multiple_entries" ) -> Optional[Alert]: """ Resolve a valid requested alert from a user intent @@ -1316,18 +1307,18 @@ def _resolve_requested_alert( """ tokens = tokens or tokenize_utterance(message) requested_time, requested_name = \ - parse_alert_name_and_time_from_message(message, tokens) + parse_alert_name_and_time_from_message(message, tokens) alerts = self._get_alerts_list(alert_type, disposition=disposition) if not alerts: return None - + # List handling (except lists in the process of creation) if alert_type == AlertType.TODO and message.data.get("list") and \ not message.data.get("create"): alerts = list(filter(lambda alert: alert.children, alerts)) - + if message.data.get("next") or len(alerts) == 1: return alerts[0] elif not any((requested_name, requested_time)): @@ -1353,7 +1344,7 @@ def _resolve_requested_alert( # fuzzy_match aggregates ratio, partial_ratio and token_sort_ratio match_acc = fuzzy_match(alert.alert_name, requested_name) if match_acc >= 90: - candidates.append((match_acc,alert,)) + candidates.append((match_acc, alert,)) if not candidates: return None @@ -1400,15 +1391,15 @@ def _pick_one_by_time(self, alerts: List[Alert], dialog: str = ""): if len(alerts) > 1: alerts.sort(key=lambda x: x.expiration) spoken_list = [ - f"{pronounce_number(i+1)}. \ - {nice_date_time(alert.expiration,use_24hour=self.use_24hour,use_ampm=not self.use_24hour)}" + f"{pronounce_number(i + 1)}. \ + {nice_date_time(alert.expiration, use_24hour=self.use_24hour, use_ampm=not self.use_24hour)}" for i, alert in enumerate(alerts) ] self.speak(join_list(spoken_list, "and"), wait=True) idx = self.get_response("alert_list_choose_between", validator=validate_number, data=dict(length=len(spoken_list))) - if idx: + if idx: return alerts[idx - 1] return None return alerts[0] @@ -1436,7 +1427,7 @@ def _event_get_alerts(self, message): return else: disposition = AlertState[disposition] - + matched = self._get_alerts_list(alert_type, user, disposition) @@ -1444,7 +1435,7 @@ def _event_get_alerts(self, message): data = {alert.ident: alert.serialize for alert in matched} self.bus.emit(message.response(data)) - def _get_alerts_list(self, + def _get_alerts_list(self, alert_type: AlertType, user: str = None, disposition: AlertState = AlertState.PENDING, @@ -1469,7 +1460,7 @@ def _get_alerts_list(self, else: LOG.error(f"Invalid alert disposition requested: {disposition}") matched_alerts = alerts_list["pending"] - + # Optionally filter by name if name: matched_alerts = list( @@ -1480,12 +1471,12 @@ def _get_alerts_list(self, ) return matched_alerts -# GUI methods + # GUI methods def _display_alert(self, alert: Alert): return self._display_alerts(alert.alert_type, [alert]) - + def _display_alerts(self, alert_type: AlertType, - alerts: Optional[List[Alert]] = None): + alerts: Optional[List[Alert]] = None): if alerts is None: alerts = self.alert_manager.get_pending_alerts(alert_type=alert_type) @@ -1494,7 +1485,7 @@ def _display_alerts(self, alert_type: AlertType, elif alert_type == AlertType.TIMER: self._display_timers(alerts) elif alert_type != AlertType.ALL: - self._display_list(alerts, + self._display_list(alerts, header=spoken_alert_type(alert_type)) def _display_alarms(self, alarms: List[Alert]): @@ -1505,7 +1496,7 @@ def _display_alarms(self, alarms: List[Alert]): alarms_view = list() for alarm in alarms: alarms_view.append(build_gui_data(alarm)) - + self.gui['activeAlarmCount'] = len(alarms_view) self.gui['activeAlarms'] = alarms_view @@ -1568,7 +1559,7 @@ def _display_list(self, alerts: List[Alert], header: str = "Todo"): self.gui.show_page("ListView") def _update_homescreen(self, alert: Alert = None, - dismiss_notification = False): + dismiss_notification=False): """ Update homescreen widgets with the current alarms and timers counts. @@ -1579,7 +1570,7 @@ def _update_homescreen(self, alert: Alert = None, self._update_homescreen_widgets(alert) if dismiss_notification: self._delete_homescreen_notification(alert) - + def _update_homescreen_widgets(self, alert: Alert = None): # timer widget if alert is None or alert.alert_type == AlertType.TIMER: @@ -1663,7 +1654,7 @@ def _gui_cancel_timer(self, message: Message): alert_id = message.data['timer']['alertId'] self._dismiss_alert(alert_id, speak=True) LOG.debug(("Timers still active on GUI: " - f"{self.alert_manager.active_gui_timers}")) + f"{self.alert_manager.active_gui_timers}")) def _event_cancel_alarm(self, message: Message): """ @@ -1675,7 +1666,7 @@ def _event_cancel_alarm(self, message: Message): elif isinstance(alert_ids, str): alert_ids = [alert_ids] for alert_id in alert_ids: - self._dismiss_alert(alert_id, speak=True) + self._dismiss_alert(alert_id, speak=True) def _release_gui_alarm(self, alert_id: str): alarm = self.alert_manager.get_alert(alert_id) @@ -1690,7 +1681,7 @@ def _release_gui_alarm(self, alert_id: str): if self.gui['activeAlarmCount'] != 0 or \ self.alert_manager.active_gui_timers: return - + if len(self.gui._pages) > 1: self.gui.remove_page("AlarmsOverviewCard") else: @@ -1710,7 +1701,7 @@ def _event_snooze_alarm(self, message): alert_ids = [alert_ids] for alert_id in alert_ids: alert = self.alert_manager.get_alert(alert_id) - LOG.info(f"GUI Snooze alert: {alert_id}") + LOG.info(f"GUI Snooze alert: {alert_id}") self._snooze_alert(alert) self.speak_dialog("confirm_snooze_alert", {"name": alert.alert_name, @@ -1728,7 +1719,7 @@ def _gui_dismiss_notification(self, message): self.speak_dialog("confirm_dismiss_alert", {"kind": spoken_alert_type(alert.alert_type)}) elif alert_id in self.alert_manager.missed_alerts: - #self.alert_manager.dismiss_missed_alert(alert_id) + # self.alert_manager.dismiss_missed_alert(alert_id) self.alert_manager.rm_alert(alert_id) # the notification has to be explicitly removed to not force the user to # additionally push the trashbin button @@ -1755,7 +1746,7 @@ def _alert_prenotification(self, alert: Alert): """ # As with alert expiration, loop the text and send notification # to homescreen if not conversed - + timeout = time.time() + self.alert_timeout_seconds alert_id = alert.ident while self.alert_manager.get_alert_status(alert_id) == \ @@ -1792,7 +1783,7 @@ def _alert_expired(self, alert: Alert): # (or handled per GUI) if alert.ocp_request or alert.alert_type == AlertType.TIMER: return - + if self.alert_manager.get_alert_status(alert.ident) == AlertState.ACTIVE: self.alert_manager.mark_alert_missed(alert.ident) @@ -1829,7 +1820,7 @@ def _play_notify_expired(self, alert: Alert): # send ocp request if ocp_request(alert, self.bus) is None: alert.ocp_request = None - + if not alert.ocp_request: if alert.audio_file: LOG.debug(alert.audio_file) @@ -1846,7 +1837,7 @@ def _play_notify_expired(self, alert: Alert): if not to_play: self._speak_notify_expired(alert) return - + # display card and register with notification system self._display_expiration(alert) @@ -1865,11 +1856,11 @@ def _play_notify_expired(self, alert: Alert): play_audio(to_play).wait(60) # TODO this depends on the length of the audio file time.sleep(1) - + # reset volume if not to_play == "ocp": self.bus.emit(Message("mycroft.volume.set", - {"percent": self.original_volume})) + {"percent": self.original_volume})) def _speak_notify_expired(self, alert: Alert): @@ -1891,7 +1882,7 @@ def _speak_notify_expired(self, alert: Alert): if alert.alert_type in (AlertType.REMINDER, AlertType.EVENT,): self.speak_dialog('expired_reminder', {'name': name}, - wait=True) + wait=True) elif alert.alert_type == AlertType.TIMER: self.speak_dialog('expired_timer', {'name': name}, @@ -1900,7 +1891,7 @@ def _speak_notify_expired(self, alert: Alert): self.speak_dialog('expired_alert', {'name': name}, wait=True) time.sleep(20) - + def _ocp_query(self, message: Message): kind = message.data.get("media", "media") result = None @@ -1914,14 +1905,15 @@ def _ocp_query(self, message: Message): if dialog == "alarm_ocp_request": self.speak_dialog("ocp_searching") - query = normalize(query, lang=self.lang, remove_articles=False) + normalizer = UtteranceNormalizerPlugin.get_normalizer(lang=self.lang) + query = normalizer.normalize(query) result = ocp_query(query, message, self.bus) if result is None: dialog = "ocp_request_retry" return result - + def _snooze_alert(self, alert: Alert, - duration: Optional[timedelta] = None): + duration: Optional[timedelta] = None): """ Helper to snooze an alert for the specified duration calling the alert manager, update widgets and handle media state @@ -1941,7 +1933,7 @@ def _snooze_alert(self, alert: Alert, if not self.speak_timer: self.bus.emit(Message("mycroft.volume.set", {"percent": self.original_volume})) - + snoozed_alert = self.alert_manager.snooze_alert(alert.ident, duration) self._update_homescreen(snoozed_alert) @@ -1950,8 +1942,8 @@ def _snooze_alert(self, alert: Alert, self.deactivate() def _dismiss_alert(self, alert_id: str, - drop_dav: bool = False, - speak: bool = False): + drop_dav: bool = False, + speak: bool = False): """ Handle a request to dismiss an alert. Removes the first valid entry in active, missed, or pending lists. @@ -1967,7 +1959,7 @@ def _dismiss_alert(self, alert_id: str, if disposition == AlertState.REMOVED: LOG.debug(f"{alert_id} already removed") return - + # release from gui if alert.alert_type == AlertType.TIMER: self.alert_manager.dismiss_alert_from_gui(alert_id) @@ -1993,16 +1985,16 @@ def _dismiss_alert(self, alert_id: str, if alert.stopwatch_mode: self.speak_dialog("stopwatch_delta", {"delta": spoken_duration(alert.stopwatch)}) - + # deativate skill if no more active alerts if not self.alert_manager.get_active_alerts(): self.deactivate() - + def _activate(self): """ Activate the skill """ - timeout = self.config_core["skills"].get("converse",{}).get("timeout", 300) - 2 + timeout = self.config_core["skills"].get("converse", {}).get("timeout", 300) - 2 # repeating event to check if a reactivation is needed self.schedule_repeating_event(self.handle_active_state, when=None, @@ -2040,6 +2032,3 @@ def stop(self): for alert in self.alert_manager.get_active_alerts(): self._dismiss_alert(alert.ident, speak=True) - -def create_skill(): - return AlertSkill() diff --git a/requirements.txt b/requirements.txt index 7ed1ea60..67fd8998 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,5 @@ ovos-bus-client>=0.0.3,<2.0.0 ovos-workshop>=0.0.15 caldav~=1.2 icalendar~=5.0 +ovos-utterance-normalizer>=0.0.1,<1.0.0 +ovos-number-parser>=0.0.1,<1.0.0 diff --git a/test/test_skill.py b/test/test_skill.py index 583d4068..c5cb495e 100644 --- a/test/test_skill.py +++ b/test/test_skill.py @@ -36,14 +36,14 @@ from json_database import JsonStorage from mock import Mock, patch from mock.mock import call -from lingua_franca.format import nice_time, pronounce_number +from ovos_date_parser import nice_time, nice_date_time, nice_duration +from ovos_number_parser import pronounce_number from ovos_bus_client.message import Message from ovos_utils.events import EventSchedulerInterface from ovos_utils.messagebus import FakeBus from ovos_workshop.skills import OVOSSkill from ovos_config.locale import load_language, get_default_tz -from lingua_franca.format import nice_date_time, nice_duration from ovos_skill_alerts import AlertSkill from ovos_skill_alerts.util import AlertPriority, AlertState, AlertType, DAVType, Weekdays, EVERYDAY diff --git a/util/locale.py b/util/locale.py index 56ea7e81..4c6e3eb6 100644 --- a/util/locale.py +++ b/util/locale.py @@ -5,15 +5,15 @@ import re from ovos_bus_client.message import Message -from lingua_franca.format import ( +from ovos_date_parser import ( nice_duration, nice_time, nice_date, - nice_date_time, - join_list, + nice_date_time ) from ovos_config.locale import get_default_lang, load_language -from lingua_franca.format import nice_duration, expand_options +from ovos_utils.bracket_expansion import expand_options +from ovos_skill_alerts.util.parse_utils import join_list from ovos_skill_alerts.util import AlertType, Weekdays, WEEKDAYS, WEEKENDS, EVERYDAY from ovos_skill_alerts.util.alert import Alert diff --git a/util/parse_utils.py b/util/parse_utils.py index f0d3f571..8ef9e8a1 100644 --- a/util/parse_utils.py +++ b/util/parse_utils.py @@ -27,7 +27,6 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import datetime as dt -import re from time import time from uuid import uuid4 from typing import Optional, List, Union, Tuple, Any @@ -35,11 +34,12 @@ from dateutil.relativedelta import relativedelta from ovos_utils.log import LOG from ovos_bus_client import Message, MessageBusClient -from lingua_franca.format import nice_time, nice_day -from lingua_franca.parse import extract_datetime, extract_duration, normalize, extract_number +from ovos_date_parser import nice_time, nice_day, extract_datetime, extract_duration +from ovos_number_parser import extract_number from ovos_config.locale import get_default_lang, load_language, get_default_tz from rapidfuzz import fuzz +from ovos_utterance_normalizer import UtteranceNormalizerPlugin from ovos_skill_alerts.util import AlertPriority, Weekdays, AlertType, DAVType, LOCAL_USER from ovos_skill_alerts.util.alert import Alert from ovos_skill_alerts.util.config import use_24h_format, find_resource_file, get_date_format @@ -138,7 +138,9 @@ def tokenize_utterance(message: Message) -> Tokens: parsed = parsed.strip("-") chunks.extend((parsed, word)) chunks.append(utterance) - tokens = Tokens([normalize(chunk, lang=lang, remove_articles=False).lower() + + normalizer = UtteranceNormalizerPlugin.get_normalizer(lang=lang) + tokens = Tokens([normalizer.normalize(chunk).lower() for chunk in chunks if chunk.strip()], message) return tokens @@ -749,3 +751,61 @@ def fuzzy_match_alerts(test: List[Alert], against: str, confidence: int = None) if result: return sorted(result, key=lambda x:x[1])[-1][0] return None + + +def _get_and_word(lang): + """ Helper to get word translations + + Args: + lang (str, optional): an optional BCP-47 language code, if omitted + the default language will be used. + + Returns: + str: translated version of resource name + """ + mapp = { + "az": "və", + "ca": "i", + "cs": "a", + "da": "og", + "de": "und", + "en": "and", + "fa": "و", + "pl": "oraz", + "pt": "e", + "sl": "in", + "uk": "та" + } + return mapp.get(lang.split("-")[0]) or ", " + + +def join_list(items: List[str], connector: str, sep: Optional[str] = None, + lang: str = '') -> str: + """ Join a list into a phrase using the given connector word + + Examples: + join_list([1,2,3], "and") -> "1, 2 and 3" + join_list([1,2,3], "and", ";") -> "1; 2 and 3" + + Args: + items (array): items to be joined + connector (str): connecting word (resource name), like "and" or "or" + sep (str, optional): separator character, default = "," + lang (str, optional): an optional BCP-47 language code, if omitted + the default language will be used. + Returns: + str: the connected list phrase + """ + + if not items: + return "" + if len(items) == 1: + return str(items[0]) + + if not sep: + sep = ", " + else: + sep += " " + return (sep.join(str(item) for item in items[:-1]) + + " " + _get_and_word(lang) + + " " + items[-1]) diff --git a/util/ui_models.py b/util/ui_models.py index 7057cbc5..5d0ac365 100644 --- a/util/ui_models.py +++ b/util/ui_models.py @@ -28,7 +28,7 @@ from datetime import datetime -from lingua_franca.format import nice_duration, nice_time +from ovos_date_parser import nice_duration, nice_time from ovos_utils.log import LOG from ovos_skill_alerts.util.alert import Alert, AlertType