core.py
Go to the documentation of this file.
1 # Copyright 2017 Mycroft AI Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 #
15 import imp
16 import collections
17 import operator
18 import sys
19 import time
20 import csv
21 import inspect
22 import os
23 import traceback
24 from inspect import signature
25 from datetime import datetime, timedelta
26 
27 import re
28 from itertools import chain
29 from adapt.intent import Intent, IntentBuilder
30 from os.path import join, abspath, dirname, basename, exists
31 from threading import Event, Timer
32 
33 from mycroft import dialog
34 from mycroft.api import DeviceApi
35 from mycroft.audio import wait_while_speaking
36 from mycroft.enclosure.api import EnclosureAPI
37 from mycroft.configuration import Configuration
38 from mycroft.dialog import DialogLoader
39 from mycroft.filesystem import FileSystemAccess
40 from mycroft.messagebus.message import Message
41 from mycroft.metrics import report_metric, report_timing, Stopwatch
42 from mycroft.skills.settings import SkillSettings
43 from mycroft.skills.skill_data import (load_vocabulary, load_regex, to_alnum,
44  munge_regex, munge_intent_parser,
45  read_vocab_file)
46 from mycroft.util import (camel_case_split,
47  resolve_resource_file,
48  play_audio_file)
49 from mycroft.util.log import LOG
50 
51 MainModule = '__init__'
52 
53 
54 def simple_trace(stack_trace):
55  stack_trace = stack_trace[:-1]
56  tb = "Traceback:\n"
57  for line in stack_trace:
58  if line.strip():
59  tb += line
60  return tb
61 
62 
64  """
65  Dig Through the stack for message.
66  """
67  stack = inspect.stack()
68  # Limit search to 10 frames back
69  stack = stack if len(stack) < 10 else stack[:10]
70  local_vars = [frame[0].f_locals for frame in stack]
71  for l in local_vars:
72  if 'message' in l and isinstance(l['message'], Message):
73  return l['message']
74 
75 
76 def unmunge_message(message, skill_id):
77  """ Restore message keywords by removing the Letterified skill ID.
78 
79  Args:
80  message (Message): Intent result message
81  skill_id (str): skill identifier
82 
83  Returns:
84  Message without clear keywords
85  """
86  if isinstance(message, Message) and isinstance(message.data, dict):
87  skill_id = to_alnum(skill_id)
88  for key in list(message.data.keys()):
89  if key.startswith(skill_id):
90  # replace the munged key with the real one
91  new_key = key[len(skill_id):]
92  message.data[new_key] = message.data.pop(key)
93 
94  return message
95 
96 
97 def open_intent_envelope(message):
98  """ Convert dictionary received over messagebus to Intent. """
99  intent_dict = message.data
100  return Intent(intent_dict.get('name'),
101  intent_dict.get('requires'),
102  intent_dict.get('at_least_one'),
103  intent_dict.get('optional'))
104 
105 
106 def load_skill(skill_descriptor, bus, skill_id, BLACKLISTED_SKILLS=None):
107  """ Load skill from skill descriptor.
108 
109  Args:
110  skill_descriptor: descriptor of skill to load
111  bus: Mycroft messagebus connection
112  skill_id: id number for skill
113  use_settings: (default True) selects if the skill should create
114  a settings object.
115 
116  Returns:
117  MycroftSkill: the loaded skill or None on failure
118  """
119  BLACKLISTED_SKILLS = BLACKLISTED_SKILLS or []
120  path = skill_descriptor["path"]
121  name = basename(path)
122  LOG.info("ATTEMPTING TO LOAD SKILL: {} with ID {}".format(name, skill_id))
123  if name in BLACKLISTED_SKILLS:
124  LOG.info("SKILL IS BLACKLISTED " + name)
125  return None
126  main_file = join(path, MainModule + '.py')
127  try:
128  with open(main_file, 'rb') as fp:
129  skill_module = imp.load_module(name.replace('.', '_'), fp,
130  main_file, ('.py', 'rb',
131  imp.PY_SOURCE))
132  if (hasattr(skill_module, 'create_skill') and
133  callable(skill_module.create_skill)):
134  # v2 skills framework
135  skill = skill_module.create_skill()
136  skill.skill_id = skill_id
137  skill.settings.allow_overwrite = True
138  skill.settings.load_skill_settings_from_file()
139  skill.bind(bus)
140  try:
141  skill.load_data_files(path)
142  # Set up intent handlers
143  skill._register_decorated()
144  skill.register_resting_screen()
145  skill.initialize()
146  except Exception as e:
147  # If an exception occurs, make sure to clean up the skill
148  skill.default_shutdown()
149  raise e
150 
151  LOG.info("Loaded " + name)
152  # The very first time a skill is run, speak the intro
153  first_run = skill.settings.get("__mycroft_skill_firstrun", True)
154  if first_run:
155  LOG.info("First run of " + name)
156  skill.settings["__mycroft_skill_firstrun"] = False
157  skill.settings.store()
158  intro = skill.get_intro_message()
159  if intro:
160  skill.speak(intro)
161  return skill
162  else:
163  LOG.warning("Module {} does not appear to be skill".format(name))
164  except FileNotFoundError as e:
165  LOG.error(
166  'Failed to load {} due to a missing file: {}'.format(name, str(e))
167  )
168  except Exception:
169  LOG.exception("Failed to load skill: " + name)
170  return None
171 
172 
173 def create_skill_descriptor(skill_path):
174  return {"path": skill_path}
175 
176 
177 def get_handler_name(handler):
178  """ Name (including class if available) of handler function.
179 
180  Args:
181  handler (function): Function to be named
182 
183  Returns:
184  string: handler name as string
185  """
186  if '__self__' in dir(handler) and 'name' in dir(handler.__self__):
187  return handler.__self__.name + '.' + handler.__name__
188  else:
189  return handler.__name__
190 
191 
192 def intent_handler(intent_parser):
193  """ Decorator for adding a method as an intent handler. """
194 
195  def real_decorator(func):
196  # Store the intent_parser inside the function
197  # This will be used later to call register_intent
198  if not hasattr(func, 'intents'):
199  func.intents = []
200  func.intents.append(intent_parser)
201  return func
202 
203  return real_decorator
204 
205 
206 def intent_file_handler(intent_file):
207  """ Decorator for adding a method as an intent file handler. """
208 
209  def real_decorator(func):
210  # Store the intent_file inside the function
211  # This will be used later to call register_intent_file
212  if not hasattr(func, 'intent_files'):
213  func.intent_files = []
214  func.intent_files.append(intent_file)
215  return func
216 
217  return real_decorator
218 
219 
220 class SkillGUI:
221  """
222  SkillGUI - Interface to the Graphical User Interface
223 
224  Values set in this class are synced to the GUI, accessible within QML
225  via the built-in sessionData mechanism. For example, in Python you can
226  write in a skill:
227  self.gui['temp'] = 33
228  self.gui.show_page('Weather.qml')
229  Then in the Weather.qml you'd access the temp via code such as:
230  text: sessionData.time
231  """
232 
233  def __init__(self, skill):
234  self.__session_data = {} # synced to GUI for use by this skill's pages
235  self.page = None # the active GUI page (e.g. QML template) to show
236  self.skill = skill
238  self.config = Configuration.get()
239 
240  @property
241  def remote_url(self):
242  """ Returns configuration value for url of remote-server. """
243  return self.config.get('remote-server')
244 
245  def build_message_type(self, event):
246  """ Builds a message matching the output from the enclosure. """
247  return '{}.{}'.format(self.skill.skill_id, event)
248 
250  """ Sets the handlers for the default messages. """
251  msg_type = self.build_message_type('set')
252  self.skill.add_event(msg_type, self.gui_set)
253 
254  def register_handler(self, event, handler):
255  """ Register a handler for GUI events.
256 
257  when using the triggerEvent method from Qt
258  triggerEvent("event", {"data": "cool"})
259 
260  Arguments:
261  event (str): event to catch
262  handler: function to handle the event
263  """
264  msg_type = self.build_message_type(event)
265  self.skill.add_event(msg_type, handler)
266 
267  def set_on_gui_changed(self, callback):
268  """ Registers a callback function to run when a value is
269  changed from the GUI.
270 
271  Arguments:
272  callback: Function to call when a value is changed
273  """
274  self.on_gui_changed_callback = callback
275 
276  def gui_set(self, message):
277  for key in message.data:
278  self[key] = message.data[key]
279  if self.on_gui_changed_callback:
281 
282  def __setitem__(self, key, value):
283  self.__session_data[key] = value
284 
285  if self.page:
286  # emit notification (but not needed if page has not been shown yet)
287  data = self.__session_data.copy()
288  data.update({'__from': self.skill.skill_id})
289  self.skill.bus.emit(Message("gui.value.set", data))
290 
291  def __getitem__(self, key):
292  return self.__session_data[key]
293 
294  def __contains__(self, key):
295  return self.__session_data.__contains__(key)
296 
297  def clear(self):
298  """ Reset the value dictionary, and remove namespace from GUI """
299  self.__session_data = {}
300  self.page = None
301  self.skill.bus.emit(Message("gui.clear.namespace",
302  {"__from": self.skill.skill_id}))
303 
304  def send_event(self, event_name, params={}):
305  """ Trigger a gui event.
306 
307  Arguments:
308  event_name (str): name of event to be triggered
309  params: json serializable object containing any parameters that
310  should be sent along with the request.
311  """
312  self.skill.bus.emit(Message("gui.event.send",
313  {"__from": self.skill.skill_id,
314  "event_name": event_name,
315  "params": params}))
316 
317  def show_page(self, name, override_idle=None):
318  """
319  Begin showing the page in the GUI
320 
321  Args:
322  name (str): Name of page (e.g "mypage.qml") to display
323  override_idle (boolean, int):
324  True: Takes over the resting page indefinitely
325  (int): Delays resting page for the specified number of
326  seconds.
327  """
328  self.show_pages([name], 0, override_idle)
329 
330  def show_pages(self, page_names, index=0, override_idle=None):
331  """
332  Begin showing the list of pages in the GUI
333 
334  Args:
335  page_names (list): List of page names (str) to display, such as
336  ["Weather.qml", "Forecast.qml", "Details.qml"]
337  index (int): Page number (0-based) to show initially. For the
338  above list a value of 1 would start on "Forecast.qml"
339  override_idle (boolean, int):
340  True: Takes over the resting page indefinitely
341  (int): Delays resting page for the specified number of
342  seconds.
343  """
344  if not isinstance(page_names, list):
345  raise ValueError('page_names must be a list')
346 
347  if index > len(page_names):
348  raise ValueError('Default index is larger than page list length')
349 
350  self.page = page_names[index]
351 
352  # First sync any data...
353  data = self.__session_data.copy()
354  data.update({'__from': self.skill.skill_id})
355  self.skill.bus.emit(Message("gui.value.set", data))
356 
357  # Convert pages to full reference
358  page_urls = []
359  for name in page_names:
360  if name.startswith("SYSTEM"):
361  page = resolve_resource_file(join('ui', name))
362  else:
363  page = self.skill.find_resource(name, 'ui')
364  if page:
365  if self.config.get('remote') is True:
366  page_urls.append(self.remote_url + "/" + page)
367  else:
368  page_urls.append("file://" + page)
369  else:
370  raise FileNotFoundError("Unable to find page: {}".format(name))
371 
372  self.skill.bus.emit(Message("gui.page.show",
373  {"page": page_urls,
374  "index": index,
375  "__from": self.skill.skill_id,
376  "__idle": override_idle}))
377 
378  def remove_page(self, page):
379  """ Remove a single page from the GUI.
380 
381  Args:
382  page (str): Page to remove from the GUI
383  """
384  return self.remove_pages([page])
385 
386  def remove_pages(self, page_names):
387  """ Remove a list of pages in the GUI.
388 
389  Args:
390  page_names (list): List of page names (str) to display, such as
391  ["Weather.qml", "Forecast.qml", "Other.qml"]
392  """
393  if not isinstance(page_names, list):
394  raise ValueError('page_names must be a list')
395 
396  # Convert pages to full reference
397  page_urls = []
398  for name in page_names:
399  page = self.skill.find_resource(name, 'ui')
400  if page:
401  page_urls.append("file://" + page)
402  else:
403  raise FileNotFoundError("Unable to find page: {}".format(name))
404 
405  self.skill.bus.emit(Message("gui.page.delete",
406  {"page": page_urls,
407  "__from": self.skill.skill_id}))
408 
409  def show_text(self, text, title=None, override_idle=None):
410  """ Display a GUI page for viewing simple text
411 
412  Arguments:
413  text (str): Main text content. It will auto-paginate
414  title (str): A title to display above the text content.
415  """
416  self.clear()
417  self["text"] = text
418  self["title"] = title
419  self.show_page("SYSTEM_TextFrame.qml", override_idle)
420 
421  def show_image(self, url, caption=None,
422  title=None, fill=None,
423  override_idle=None):
424  """ Display a GUI page for viewing an image
425 
426  Arguments:
427  url (str): Pointer to the image
428  caption (str): A caption to show under the image
429  title (str): A title to display above the image content
430  fill (str): Fill type supports 'PreserveAspectFit',
431  'PreserveAspectCrop', 'Stretch'
432  """
433  self.clear()
434  self["image"] = url
435  self["title"] = title
436  self["caption"] = caption
437  self["fill"] = fill
438  self.show_page("SYSTEM_ImageFrame.qml", override_idle)
439 
440  def show_html(self, html, resource_url=None, override_idle=None):
441  """ Display an HTML page in the GUI
442 
443  Arguments:
444  html (str): HTML text to display
445  resource_url (str): Pointer to HTML resources
446  """
447  self.clear()
448  self["html"] = html
449  self["resourceLocation"] = resource_url
450  self.show_page("SYSTEM_HtmlFrame.qml", override_idle)
451 
452  def show_url(self, url, override_idle=None):
453  """ Display an HTML page in the GUI
454 
455  Arguments:
456  url (str): URL to render
457  """
458  self.clear()
459  self["url"] = url
460  self.show_page("SYSTEM_UrlFrame.qml", override_idle)
461 
462 
463 def resting_screen_handler(name=None):
464  """ Decorator for adding a method as an resting screen handler.
465 
466  If selected will be shown on screen when device enters idle mode
467  """
468  name = name or func.__self__.name
469 
470  def real_decorator(func):
471  # Store the resting information inside the function
472  # This will be used later in register_resting_screen
473  if not hasattr(func, 'resting_handler'):
474  func.resting_handler = name
475  return func
476 
477  return real_decorator
478 
479 
480 #######################################################################
481 # MycroftSkill base class
482 #######################################################################
484  """
485  Abstract base class which provides common behaviour and parameters to all
486  Skills implementation.
487  """
488 
489  def __init__(self, name=None, bus=None, use_settings=True):
490  self.name = name or self.__class__.__name__
491  self.resting_name = None
492  # Get directory of skill
493  self._dir = dirname(abspath(sys.modules[self.__module__].__file__))
494  if use_settings:
495  self.settings = SkillSettings(self._dir, self.name)
496  else:
497  self.settings = None
498 
499  self.gui = SkillGUI(self)
500 
501  self._bus = None
502  self._enclosure = None
503  self.bind(bus)
504  #: Mycroft global configuration. (dict)
505  self.config_core = Configuration.get()
506  # TODO: 19.08 - Remove
507  self._config = self.config_core.get(self.name) or {}
508  self.dialog_renderer = None
509  self.root_dir = None #: skill root directory
510 
511  #: Filesystem access to skill specific folder.
512  #: See mycroft.filesystem for details.
513  self.file_system = FileSystemAccess(join('skills', self.name))
515  self.log = LOG.create_logger(self.name) #: Skill logger instance
516  self.reload_skill = True #: allow reloading (default True)
517  self.events = []
519  self.skill_id = '' # will be set from the path, so guaranteed unique
520  self.voc_match_cache = {}
521 
522  @property
523  def enclosure(self):
524  if self._enclosure:
525  return self._enclosure
526  else:
527  LOG.error("Skill not fully initialized. Move code " +
528  "from __init__() to initialize() to correct this.")
529  LOG.error(simple_trace(traceback.format_stack()))
530  raise Exception("Accessed MycroftSkill.enclosure in __init__")
531 
532  @property
533  def bus(self):
534  if self._bus:
535  return self._bus
536  else:
537  LOG.error("Skill not fully initialized. Move code " +
538  "from __init__() to initialize() to correct this.")
539  LOG.error(simple_trace(traceback.format_stack()))
540  raise Exception("Accessed MycroftSkill.bus in __init__")
541 
542  @property
543  def config(self):
544  """ Provide deprecation warning when accessing config.
545  TODO: Remove in 19.08
546  """
547  stack = simple_trace(traceback.format_stack())
548  if (" _register_decorated" not in stack and
549  "register_resting_screen" not in stack):
550  LOG.warning('self.config is deprecated. Switch to using '
551  'self.setting["whatever"] within your skill.')
552  LOG.warning(stack)
553  return self._config
554 
555  @property
556  def location(self):
557  """ Get the JSON data struction holding location information. """
558  # TODO: Allow Enclosure to override this for devices that
559  # contain a GPS.
560  return self.config_core.get('location')
561 
562  @property
563  def location_pretty(self):
564  """ Get a more 'human' version of the location as a string. """
565  loc = self.location
566  if type(loc) is dict and loc["city"]:
567  return loc["city"]["name"]
568  return None
569 
570  @property
571  def location_timezone(self):
572  """ Get the timezone code, such as 'America/Los_Angeles' """
573  loc = self.location
574  if type(loc) is dict and loc["timezone"]:
575  return loc["timezone"]["code"]
576  return None
577 
578  @property
579  def lang(self):
580  return self.config_core.get('lang')
581 
582  def bind(self, bus):
583  """ Register messagebus emitter with skill.
584 
585  Arguments:
586  bus: Mycroft messagebus connection
587  """
588  if bus:
589  self._bus = bus
590  self._enclosure = EnclosureAPI(bus, self.name)
591  self.add_event('mycroft.stop', self.__handle_stop)
592  self.add_event('mycroft.skill.enable_intent',
594  self.add_event('mycroft.skill.disable_intent',
596  self.add_event("mycroft.skill.set_cross_context",
598  self.add_event("mycroft.skill.remove_cross_context",
600  name = 'mycroft.skills.settings.update'
601  func = self.settings.run_poll
602  bus.on(name, func)
603  self.events.append((name, func))
604 
605  # Intialize the SkillGui
606  self.gui.setup_default_handlers()
607 
608  def detach(self):
609  for (name, intent) in self.registered_intents:
610  name = str(self.skill_id) + ':' + name
611  self.bus.emit(Message("detach_intent", {"intent_name": name}))
612 
613  def initialize(self):
614  """ Perform any final setup needed for the skill.
615 
616  Invoked after the skill is fully constructed and registered with the
617  system.
618  """
619  pass
620 
621  def get_intro_message(self):
622  """ Get a message to speak on first load of the skill.
623 
624  Useful for post-install setup instructions.
625 
626  Returns:
627  str: message that will be spoken to the user
628  """
629  return None
630 
631  def converse(self, utterances, lang=None):
632  """ Handle conversation.
633 
634  This method gets a peek at utterances before the normal intent
635  handling process after a skill has been invoked once.
636 
637  To use, override the converse() method and return True to
638  indicate that the utterance has been handled.
639 
640  Args:
641  utterances (list): The utterances from the user. If there are
642  multiple utterances, consider them all to be
643  transcription possibilities. Commonly, the
644  first entry is the user utt and the second
645  is normalized() version of the first utterance
646  lang: language the utterance is in, None for default
647 
648  Returns:
649  bool: True if an utterance was handled, otherwise False
650  """
651  return False
652 
653  def __get_response(self):
654  """ Helper to get a reponse from the user
655 
656  Returns:
657  str: user's response or None on a timeout
658  """
659  event = Event()
660 
661  def converse(utterances, lang=None):
662  converse.response = utterances[0] if utterances else None
663  event.set()
664  return True
665 
666  # install a temporary conversation handler
667  self.make_active()
668  converse.response = None
669  default_converse = self.converse
670  self.converse = converse
671  event.wait(15) # 10 for listener, 5 for SST, then timeout
672  self.converse = default_converse
673  return converse.response
674 
675  def get_response(self, dialog='', data=None, validator=None,
676  on_fail=None, num_retries=-1):
677  """
678  Prompt user and wait for response
679 
680  The given dialog is spoken, followed immediately by listening
681  for a user response. The response can optionally be
682  validated before returning.
683 
684  Example:
685  color = self.get_response('ask.favorite.color')
686 
687  Args:
688  dialog (str): Announcement dialog to speak to the user
689  data (dict): Data used to render the dialog
690  validator (any): Function with following signature
691  def validator(utterance):
692  return utterance != "red"
693  on_fail (any): Dialog or function returning literal string
694  to speak on invalid input. For example:
695  def on_fail(utterance):
696  return "nobody likes the color red, pick another"
697  num_retries (int): Times to ask user for input, -1 for infinite
698  NOTE: User can not respond and timeout or say "cancel" to stop
699 
700  Returns:
701  str: User's reply or None if timed out or canceled
702  """
703  data = data or {}
704 
705  def get_announcement():
706  return self.dialog_renderer.render(dialog, data)
707 
708  if not get_announcement():
709  raise ValueError('dialog message required')
710 
711  def on_fail_default(utterance):
712  fail_data = data.copy()
713  fail_data['utterance'] = utterance
714  if on_fail:
715  return self.dialog_renderer.render(on_fail, fail_data)
716  else:
717  return get_announcement()
718 
719  def is_cancel(utterance):
720  return self.voc_match(utterance, 'cancel')
721 
722  def validator_default(utterance):
723  # accept anything except 'cancel'
724  return not is_cancel(utterance)
725 
726  validator = validator or validator_default
727  on_fail_fn = on_fail if callable(on_fail) else on_fail_default
728 
729  self.speak(get_announcement(), expect_response=True, wait=True)
730  num_fails = 0
731  while True:
732  response = self.__get_response()
733 
734  if response is None:
735  # if nothing said, prompt one more time
736  num_none_fails = 1 if num_retries < 0 else num_retries
737  if num_fails >= num_none_fails:
738  return None
739  else:
740  if validator(response):
741  return response
742 
743  # catch user saying 'cancel'
744  if is_cancel(response):
745  return None
746 
747  num_fails += 1
748  if 0 < num_retries < num_fails:
749  return None
750 
751  line = on_fail_fn(response)
752  self.speak(line, expect_response=True)
753 
754  def ask_yesno(self, prompt, data=None):
755  """ Read prompt and wait for a yes/no answer
756 
757  This automatically deals with translation and common variants,
758  such as 'yeah', 'sure', etc.
759 
760  Args:
761  prompt (str): a dialog id or string to read
762  Returns:
763  string: 'yes', 'no' or whatever the user response if not
764  one of those, including None
765  """
766  resp = self.get_response(dialog=prompt, data=data)
767 
768  if self.voc_match(resp, 'yes'):
769  return 'yes'
770  elif self.voc_match(resp, 'no'):
771  return 'no'
772  else:
773  return resp
774 
775  def voc_match(self, utt, voc_filename, lang=None):
776  """ Determine if the given utterance contains the vocabulary provided
777 
778  Checks for vocabulary match in the utterance instead of the other
779  way around to allow the user to say things like "yes, please" and
780  still match against "Yes.voc" containing only "yes". The method first
781  checks in the current skill's .voc files and secondly the "res/text"
782  folder of mycroft-core. The result is cached to avoid hitting the
783  disk each time the method is called.
784 
785  Args:
786  utt (str): Utterance to be tested
787  voc_filename (str): Name of vocabulary file (e.g. 'yes' for
788  'res/text/en-us/yes.voc')
789  lang (str): Language code, defaults to self.long
790 
791  Returns:
792  bool: True if the utterance has the given vocabulary it
793  """
794  lang = lang or self.lang
795  cache_key = lang + voc_filename
796  if cache_key not in self.voc_match_cache:
797  # Check for both skill resources and mycroft-core resources
798  voc = self.find_resource(voc_filename + '.voc', 'vocab')
799  if not voc:
800  voc = resolve_resource_file(join('text', lang,
801  voc_filename + '.voc'))
802 
803  if not voc or not exists(voc):
804  raise FileNotFoundError(
805  'Could not find {}.voc file'.format(voc_filename))
806  # load vocab and flatten into a simple list
807  vocab = list(chain(*read_vocab_file(voc)))
808  self.voc_match_cache[cache_key] = vocab
809  if utt:
810  # Check for matches against complete words
811  return any([re.match(r'.*\b' + i + r'\b.*', utt)
812  for i in self.voc_match_cache[cache_key]])
813  else:
814  return False
815 
816  def report_metric(self, name, data):
817  """ Report a skill metric to the Mycroft servers
818 
819  Args:
820  name (str): Name of metric. Must use only letters and hyphens
821  data (dict): JSON dictionary to report. Must be valid JSON
822  """
823  report_metric(basename(self.root_dir) + ':' + name, data)
824 
825  def send_email(self, title, body):
826  """ Send an email to the registered user's email
827 
828  Args:
829  title (str): Title of email
830  body (str): HTML body of email. This supports
831  simple HTML like bold and italics
832  """
833  DeviceApi().send_email(title, body, basename(self.root_dir))
834 
835  def make_active(self):
836  """ Bump skill to active_skill list in intent_service
837 
838  This enables converse method to be called even without skill being
839  used in last 5 minutes.
840  """
841  self.bus.emit(Message('active_skill_request',
842  {"skill_id": self.skill_id}))
843 
844  def _handle_collect_resting(self, message=None):
845  """ Handler for collect resting screen messages.
846 
847  Sends info on how to trigger this skills resting page.
848  """
849  self.log.info('Registering resting screen')
850  self.bus.emit(Message('mycroft.mark2.register_idle',
851  data={'name': self.resting_name,
852  'id': self.skill_id}))
853 
855  """ Registers resting screen from the resting_screen_handler decorator.
856 
857  This only allows one screen and if two is registered only one
858  will be used.
859  """
860  attributes = [a for a in dir(self)]
861  for attr_name in attributes:
862  method = getattr(self, attr_name)
863 
864  if hasattr(method, 'resting_handler'):
865  self.resting_name = method.resting_handler
866  self.log.info('Registering resting screen {} for {}.'.format(
867  method, self.resting_name))
868 
869  # Register for handling resting screen
870  msg_type = '{}.{}'.format(self.skill_id, 'idle')
871  self.add_event(msg_type, method)
872  # Register handler for resting screen collect message
873  self.add_event('mycroft.mark2.collect_idle',
875 
876  # Do a send at load to make sure the skill is registered
877  # if reloaded
879  return
880 
882  """ Register all intent handlers that are decorated with an intent.
883 
884  Looks for all functions that have been marked by a decorator
885  and read the intent data from them
886  """
887  attributes = [a for a in dir(self)]
888  for attr_name in attributes:
889  method = getattr(self, attr_name)
890 
891  if hasattr(method, 'intents'):
892  for intent in getattr(method, 'intents'):
893  self.register_intent(intent, method)
894 
895  if hasattr(method, 'intent_files'):
896  for intent_file in getattr(method, 'intent_files'):
897  self.register_intent_file(intent_file, method)
898 
899  def translate(self, text, data=None):
900  """ Load a translatable single string resource
901 
902  The string is loaded from a file in the skill's dialog subdirectory
903  'dialog/<lang>/<text>.dialog'
904  The string is randomly chosen from the file and rendered, replacing
905  mustache placeholders with values found in the data dictionary.
906 
907  Args:
908  text (str): The base filename (no extension needed)
909  data (dict, optional): a JSON dictionary
910 
911  Returns:
912  str: A randomly chosen string from the file
913  """
914  return self.dialog_renderer.render(text, data or {})
915 
916  def find_resource(self, res_name, res_dirname=None):
917  """ Find a resource file
918 
919  Searches for the given filename using this scheme:
920  1) Search the resource lang directory:
921  <skill>/<res_dirname>/<lang>/<res_name>
922  2) Search the resource directory:
923  <skill>/<res_dirname>/<res_name>
924  3) Search the locale lang directory or other subdirectory:
925  <skill>/locale/<lang>/<res_name> or
926  <skill>/locale/<lang>/.../<res_name>
927 
928  Args:
929  res_name (string): The resource name to be found
930  res_dirname (string, optional): A skill resource directory, such
931  'dialog', 'vocab', 'regex' or 'ui'.
932  Defaults to None.
933 
934  Returns:
935  string: The full path to the resource file or None if not found
936  """
937  if res_dirname:
938  # Try the old translated directory (dialog/vocab/regex)
939  path = join(self.root_dir, res_dirname, self.lang, res_name)
940  if exists(path):
941  return path
942 
943  # Try old-style non-translated resource
944  path = join(self.root_dir, res_dirname, res_name)
945  if exists(path):
946  return path
947 
948  # New scheme: search for res_name under the 'locale' folder
949  root_path = join(self.root_dir, 'locale', self.lang)
950  for path, _, files in os.walk(root_path):
951  if res_name in files:
952  return join(path, res_name)
953 
954  # Not found
955  return None
956 
957  def translate_namedvalues(self, name, delim=None):
958  """ Load translation dict containing names and values.
959 
960  This loads a simple CSV from the 'dialog' folders.
961  The name is the first list item, the value is the
962  second. Lines prefixed with # or // get ignored
963 
964  Args:
965  name (str): name of the .value file, no extension needed
966  delim (char): delimiter character used, default is ','
967 
968  Returns:
969  dict: name and value dictionary, or empty dict if load fails
970  """
971 
972  delim = delim or ','
973  result = collections.OrderedDict()
974  if not name.endswith(".value"):
975  name += ".value"
976 
977  try:
978  filename = self.find_resource(name, 'dialog')
979  if filename:
980  with open(filename) as f:
981  reader = csv.reader(f, delimiter=delim)
982  for row in reader:
983  # skip blank or comment lines
984  if not row or row[0].startswith("#"):
985  continue
986  if len(row) != 2:
987  continue
988 
989  result[row[0]] = row[1]
990 
991  return result
992  except Exception:
993  return {}
994 
995  def translate_template(self, template_name, data=None):
996  """ Load a translatable template
997 
998  The strings are loaded from a template file in the skill's dialog
999  subdirectory.
1000  'dialog/<lang>/<template_name>.template'
1001  The strings are loaded and rendered, replacing mustache placeholders
1002  with values found in the data dictionary.
1003 
1004  Args:
1005  template_name (str): The base filename (no extension needed)
1006  data (dict, optional): a JSON dictionary
1007 
1008  Returns:
1009  list of str: The loaded template file
1010  """
1011  return self.__translate_file(template_name + '.template', data)
1012 
1013  def translate_list(self, list_name, data=None):
1014  """ Load a list of translatable string resources
1015 
1016  The strings are loaded from a list file in the skill's dialog
1017  subdirectory.
1018  'dialog/<lang>/<list_name>.list'
1019  The strings are loaded and rendered, replacing mustache placeholders
1020  with values found in the data dictionary.
1021 
1022  Args:
1023  list_name (str): The base filename (no extension needed)
1024  data (dict, optional): a JSON dictionary
1025 
1026  Returns:
1027  list of str: The loaded list of strings with items in consistent
1028  positions regardless of the language.
1029  """
1030  return self.__translate_file(list_name + '.list', data)
1031 
1032  def __translate_file(self, name, data):
1033  """Load and render lines from dialog/<lang>/<name>"""
1034  filename = self.find_resource(name, 'dialog')
1035  if filename:
1036  with open(filename) as f:
1037  text = f.read().replace('{{', '{').replace('}}', '}')
1038  return text.format(**data or {}).rstrip('\n').split('\n')
1039  else:
1040  return None
1041 
1042  def add_event(self, name, handler, handler_info=None, once=False):
1043  """ Create event handler for executing intent
1044 
1045  Args:
1046  name (string): IntentParser name
1047  handler (func): Method to call
1048  handler_info (string): Base message when reporting skill event
1049  handler status on messagebus.
1050  once (bool, optional): Event handler will be removed after it has
1051  been run once.
1052  """
1053 
1054  def wrapper(message):
1055  skill_data = {'name': get_handler_name(handler)}
1056  stopwatch = Stopwatch()
1057  try:
1058  message = unmunge_message(message, self.skill_id)
1059  # Indicate that the skill handler is starting
1060  if handler_info:
1061  # Indicate that the skill handler is starting if requested
1062  msg_type = handler_info + '.start'
1063  self.bus.emit(message.reply(msg_type, skill_data))
1064 
1065  if once:
1066  # Remove registered one-time handler before invoking,
1067  # allowing them to re-schedule themselves.
1068  self.remove_event(name)
1069 
1070  with stopwatch:
1071  if len(signature(handler).parameters) == 0:
1072  handler()
1073  else:
1074  handler(message)
1075  self.settings.store() # Store settings if they've changed
1076 
1077  except Exception as e:
1078  # Convert "MyFancySkill" to "My Fancy Skill" for speaking
1079  handler_name = camel_case_split(self.name)
1080  msg_data = {'skill': handler_name}
1081  msg = dialog.get('skill.error', self.lang, msg_data)
1082  self.speak(msg)
1083  LOG.exception(msg)
1084  # append exception information in message
1085  skill_data['exception'] = repr(e)
1086  finally:
1087  # Indicate that the skill handler has completed
1088  if handler_info:
1089  msg_type = handler_info + '.complete'
1090  self.bus.emit(message.reply(msg_type, skill_data))
1091 
1092  # Send timing metrics
1093  context = message.context
1094  if context and 'ident' in context:
1095  report_timing(context['ident'], 'skill_handler', stopwatch,
1096  {'handler': handler.__name__})
1097 
1098  if handler:
1099  if once:
1100  self.bus.once(name, wrapper)
1101  else:
1102  self.bus.on(name, wrapper)
1103  self.events.append((name, wrapper))
1104 
1105  def remove_event(self, name):
1106  """ Removes an event from bus emitter and events list
1107 
1108  Args:
1109  name (string): Name of Intent or Scheduler Event
1110  Returns:
1111  bool: True if found and removed, False if not found
1112  """
1113  removed = False
1114  for _name, _handler in list(self.events):
1115  if name == _name:
1116  try:
1117  self.events.remove((_name, _handler))
1118  except ValueError:
1119  pass
1120  removed = True
1121 
1122  # Because of function wrappers, the emitter doesn't always directly
1123  # hold the _handler function, it sometimes holds something like
1124  # 'wrapper(_handler)'. So a call like:
1125  # self.bus.remove(_name, _handler)
1126  # will not find it, leaving an event handler with that name left behind
1127  # waiting to fire if it is ever re-installed and triggered.
1128  # Remove all handlers with the given name, regardless of handler.
1129  if removed:
1130  self.bus.remove_all_listeners(name)
1131  return removed
1132 
1133  def register_intent(self, intent_parser, handler):
1134  """ Register an Intent with the intent service.
1135 
1136  Args:
1137  intent_parser: Intent or IntentBuilder object to parse
1138  utterance for the handler.
1139  handler (func): function to register with intent
1140  """
1141  if isinstance(intent_parser, IntentBuilder):
1142  intent_parser = intent_parser.build()
1143  elif not isinstance(intent_parser, Intent):
1144  raise ValueError('"' + str(intent_parser) + '" is not an Intent')
1145 
1146  # Default to the handler's function name if none given
1147  name = intent_parser.name or handler.__name__
1148  munge_intent_parser(intent_parser, name, self.skill_id)
1149  self.bus.emit(Message("register_intent", intent_parser.__dict__))
1150  self.registered_intents.append((name, intent_parser))
1151  self.add_event(intent_parser.name, handler, 'mycroft.skill.handler')
1152 
1153  def register_intent_file(self, intent_file, handler):
1154  """
1155  Register an Intent file with the intent service.
1156  For example:
1157 
1158  === food.order.intent ===
1159  Order some {food}.
1160  Order some {food} from {place}.
1161  I'm hungry.
1162  Grab some {food} from {place}.
1163 
1164  Optionally, you can also use <register_entity_file>
1165  to specify some examples of {food} and {place}
1166 
1167  In addition, instead of writing out multiple variations
1168  of the same sentence you can write:
1169 
1170  === food.order.intent ===
1171  (Order | Grab) some {food} (from {place} | ).
1172  I'm hungry.
1173 
1174  Args:
1175  intent_file: name of file that contains example queries
1176  that should activate the intent. Must end with
1177  '.intent'
1178  handler: function to register with intent
1179  """
1180  name = str(self.skill_id) + ':' + intent_file
1181 
1182  filename = self.find_resource(intent_file, 'vocab')
1183  if not filename:
1184  raise FileNotFoundError(
1185  'Unable to find "' + str(intent_file) + '"'
1186  )
1187 
1188  data = {
1189  "file_name": filename,
1190  "name": name
1191  }
1192  self.bus.emit(Message("padatious:register_intent", data))
1193  self.registered_intents.append((intent_file, data))
1194  self.add_event(name, handler, 'mycroft.skill.handler')
1195 
1196  def register_entity_file(self, entity_file):
1197  """ Register an Entity file with the intent service.
1198 
1199  An Entity file lists the exact values that an entity can hold.
1200  For example:
1201 
1202  === ask.day.intent ===
1203  Is it {weekend}?
1204 
1205  === weekend.entity ===
1206  Saturday
1207  Sunday
1208 
1209  Args:
1210  entity_file (string): name of file that contains examples of an
1211  entity. Must end with '.entity'
1212  """
1213  if entity_file.endswith('.entity'):
1214  entity_file = entity_file.replace('.entity', '')
1215 
1216  filename = self.find_resource(entity_file + ".entity", 'vocab')
1217  if not filename:
1218  raise FileNotFoundError(
1219  'Unable to find "' + entity_file + '.entity"'
1220  )
1221  name = str(self.skill_id) + ':' + entity_file
1222 
1223  self.bus.emit(Message("padatious:register_entity", {
1224  "file_name": filename,
1225  "name": name
1226  }))
1227 
1228  def handle_enable_intent(self, message):
1229  """
1230  Listener to enable a registered intent if it belongs to this skill
1231  """
1232  intent_name = message.data["intent_name"]
1233  for (name, intent) in self.registered_intents:
1234  if name == intent_name:
1235  return self.enable_intent(intent_name)
1236 
1237  def handle_disable_intent(self, message):
1238  """
1239  Listener to disable a registered intent if it belongs to this skill
1240  """
1241  intent_name = message.data["intent_name"]
1242  for (name, intent) in self.registered_intents:
1243  if name == intent_name:
1244  return self.disable_intent(intent_name)
1245 
1246  def disable_intent(self, intent_name):
1247  """
1248  Disable a registered intent if it belongs to this skill
1249 
1250  Args:
1251  intent_name (string): name of the intent to be disabled
1252 
1253  Returns:
1254  bool: True if disabled, False if it wasn't registered
1255  """
1256  names = [intent_tuple[0] for intent_tuple in self.registered_intents]
1257  if intent_name in names:
1258  LOG.debug('Disabling intent ' + intent_name)
1259  name = str(self.skill_id) + ':' + intent_name
1260  self.bus.emit(Message("detach_intent", {"intent_name": name}))
1261  return True
1262 
1263  LOG.error('Could not disable ' + intent_name +
1264  ', it hasn\'t been registered.')
1265  return False
1266 
1267  def enable_intent(self, intent_name):
1268  """
1269  (Re)Enable a registered intent if it belongs to this skill
1270 
1271  Args:
1272  intent_name: name of the intent to be enabled
1273 
1274  Returns:
1275  bool: True if enabled, False if it wasn't registered
1276  """
1277  names = [intent[0] for intent in self.registered_intents]
1278  intents = [intent[1] for intent in self.registered_intents]
1279  if intent_name in names:
1280  intent = intents[names.index(intent_name)]
1281  self.registered_intents.remove((intent_name, intent))
1282  if ".intent" in intent_name:
1283  self.register_intent_file(intent_name, None)
1284  else:
1285  intent.name = intent_name
1286  self.register_intent(intent, None)
1287  LOG.debug('Enabling intent ' + intent_name)
1288  return True
1289 
1290  LOG.error('Could not enable ' + intent_name + ', it hasn\'t been '
1291  'registered.')
1292  return False
1293 
1294  def set_context(self, context, word='', origin=None):
1295  """
1296  Add context to intent service
1297 
1298  Args:
1299  context: Keyword
1300  word: word connected to keyword
1301  """
1302  if not isinstance(context, str):
1303  raise ValueError('context should be a string')
1304  if not isinstance(word, str):
1305  raise ValueError('word should be a string')
1306 
1307  origin = origin or ''
1308  context = to_alnum(self.skill_id) + context
1309  self.bus.emit(Message('add_context',
1310  {'context': context, 'word': word,
1311  'origin': origin}))
1312 
1313  def handle_set_cross_context(self, message):
1314  """
1315  Add global context to intent service
1316 
1317  """
1318  context = message.data.get("context")
1319  word = message.data.get("word")
1320  origin = message.data.get("origin")
1321 
1322  self.set_context(context, word, origin)
1323 
1324  def handle_remove_cross_context(self, message):
1325  """
1326  Remove global context from intent service
1327 
1328  """
1329  context = message.data.get("context")
1330  self.remove_context(context)
1331 
1332  def set_cross_skill_context(self, context, word=''):
1333  """
1334  Tell all skills to add a context to intent service
1335 
1336  Args:
1337  context: Keyword
1338  word: word connected to keyword
1339  """
1340  self.bus.emit(Message("mycroft.skill.set_cross_context",
1341  {"context": context, "word": word,
1342  "origin": self.skill_id}))
1343 
1344  def remove_cross_skill_context(self, context):
1345  """
1346  tell all skills to remove a keyword from the context manager.
1347  """
1348  if not isinstance(context, str):
1349  raise ValueError('context should be a string')
1350  self.bus.emit(Message("mycroft.skill.remove_cross_context",
1351  {"context": context}))
1352 
1353  def remove_context(self, context):
1354  """
1355  remove a keyword from the context manager.
1356  """
1357  if not isinstance(context, str):
1358  raise ValueError('context should be a string')
1359  context = to_alnum(self.skill_id) + context
1360  self.bus.emit(Message('remove_context', {'context': context}))
1361 
1362  def register_vocabulary(self, entity, entity_type):
1363  """ Register a word to a keyword
1364 
1365  Args:
1366  entity: word to register
1367  entity_type: Intent handler entity to tie the word to
1368  """
1369  self.bus.emit(Message('register_vocab', {
1370  'start': entity, 'end': to_alnum(self.skill_id) + entity_type
1371  }))
1372 
1373  def register_regex(self, regex_str):
1374  """ Register a new regex.
1375  Args:
1376  regex_str: Regex string
1377  """
1378  regex = munge_regex(regex_str, self.skill_id)
1379  re.compile(regex) # validate regex
1380  self.bus.emit(Message('register_vocab', {'regex': regex}))
1381 
1382  def speak(self, utterance, expect_response=False, wait=False):
1383  """ Speak a sentence.
1384 
1385  Args:
1386  utterance (str): sentence mycroft should speak
1387  expect_response (bool): set to True if Mycroft should listen
1388  for a response immediately after
1389  speaking the utterance.
1390  wait (bool): set to True to block while the text
1391  is being spoken.
1392  """
1393  # registers the skill as being active
1394  self.enclosure.register(self.name)
1395  data = {'utterance': utterance,
1396  'expect_response': expect_response}
1397  message = dig_for_message()
1398  if message:
1399  self.bus.emit(message.reply("speak", data))
1400  else:
1401  self.bus.emit(Message("speak", data))
1402  if wait:
1404 
1405  def speak_dialog(self, key, data=None, expect_response=False, wait=False):
1406  """ Speak a random sentence from a dialog file.
1407 
1408  Args:
1409  key (str): dialog file key (e.g. "hello" to speak from the file
1410  "locale/en-us/hello.dialog")
1411  data (dict): information used to populate sentence
1412  expect_response (bool): set to True if Mycroft should listen
1413  for a response immediately after
1414  speaking the utterance.
1415  wait (bool): set to True to block while the text
1416  is being spoken.
1417  """
1418  data = data or {}
1419  self.speak(self.dialog_renderer.render(key, data),
1420  expect_response, wait)
1421 
1422  def init_dialog(self, root_directory):
1423  # If "<skill>/dialog/<lang>" exists, load from there. Otherwise
1424  # load dialog from "<skill>/locale/<lang>"
1425  dialog_dir = join(root_directory, 'dialog', self.lang)
1426  if exists(dialog_dir):
1427  self.dialog_renderer = DialogLoader().load(dialog_dir)
1428  elif exists(join(root_directory, 'locale', self.lang)):
1429  locale_path = join(root_directory, 'locale', self.lang)
1430  self.dialog_renderer = DialogLoader().load(locale_path)
1431  else:
1432  LOG.debug('No dialog loaded')
1433 
1434  def load_data_files(self, root_directory):
1435  self.root_dir = root_directory
1436  self.init_dialog(root_directory)
1437  self.load_vocab_files(root_directory)
1438  self.load_regex_files(root_directory)
1439 
1440  def load_vocab_files(self, root_directory):
1441  vocab_dir = join(root_directory, 'vocab', self.lang)
1442  if exists(vocab_dir):
1443  load_vocabulary(vocab_dir, self.bus, self.skill_id)
1444  elif exists(join(root_directory, 'locale', self.lang)):
1445  load_vocabulary(join(root_directory, 'locale', self.lang),
1446  self.bus, self.skill_id)
1447  else:
1448  LOG.debug('No vocab loaded')
1449 
1450  def load_regex_files(self, root_directory):
1451  regex_dir = join(root_directory, 'regex', self.lang)
1452  if exists(regex_dir):
1453  load_regex(regex_dir, self.bus, self.skill_id)
1454  elif exists(join(root_directory, 'locale', self.lang)):
1455  load_regex(join(root_directory, 'locale', self.lang),
1456  self.bus, self.skill_id)
1457 
1458  def __handle_stop(self, event):
1459  """
1460  Handler for the "mycroft.stop" signal. Runs the user defined
1461  `stop()` method.
1462  """
1463 
1464  def __stop_timeout():
1465  # The self.stop() call took more than 100ms, assume it handled Stop
1466  self.bus.emit(Message("mycroft.stop.handled",
1467  {"skill_id": str(self.skill_id) + ":"}))
1468 
1469  timer = Timer(0.1, __stop_timeout) # set timer for 100ms
1470  try:
1471  if self.stop():
1472  self.bus.emit(Message("mycroft.stop.handled",
1473  {"by": "skill:"+str(self.skill_id)}))
1474  timer.cancel()
1475  except Exception:
1476  timer.cancel()
1477  LOG.error("Failed to stop skill: {}".format(self.name),
1478  exc_info=True)
1479 
1480  def stop(self):
1481  pass
1482 
1483  def shutdown(self):
1484  """
1485  This method is intended to be called during the skill
1486  process termination. The skill implementation must
1487  shutdown all processes and operations in execution.
1488  """
1489  pass
1490 
1491  def default_shutdown(self):
1492  """Parent function called internally to shut down everything.
1493 
1494  Shuts down known entities and calls skill specific shutdown method.
1495  """
1496  try:
1497  self.shutdown()
1498  except Exception as e:
1499  LOG.error('Skill specific shutdown function encountered '
1500  'an error: {}'.format(repr(e)))
1501  # Store settings
1502  if exists(self._dir):
1503  self.settings.store()
1504  self.settings.stop_polling()
1505 
1506  # Clear skill from gui
1507  self.gui.clear()
1508 
1509  # removing events
1510  self.cancel_all_repeating_events()
1511  for e, f in self.events:
1512  self.bus.remove(e, f)
1513  self.events = [] # Remove reference to wrappers
1514 
1515  self.bus.emit(
1516  Message("detach_skill", {"skill_id": str(self.skill_id) + ":"}))
1517  try:
1518  self.stop()
1519  except Exception:
1520  LOG.error("Failed to stop skill: {}".format(self.name),
1521  exc_info=True)
1522 
1523  def _unique_name(self, name):
1524  """
1525  Return a name unique to this skill using the format
1526  [skill_id]:[name].
1527 
1528  Args:
1529  name: Name to use internally
1530 
1531  Returns:
1532  str: name unique to this skill
1533  """
1534  return str(self.skill_id) + ':' + (name or '')
1535 
1536  def _schedule_event(self, handler, when, data=None, name=None,
1537  repeat=None):
1538  """
1539  Underlying method for schedule_event and schedule_repeating_event.
1540  Takes scheduling information and sends it off on the message bus.
1541  """
1542  if not name:
1543  name = self.name + handler.__name__
1544  unique_name = self._unique_name(name)
1545  if repeat:
1546  self.scheduled_repeats.append(name) # store "friendly name"
1547 
1548  data = data or {}
1549  self.add_event(unique_name, handler, once=not repeat)
1550  event_data = {}
1551  event_data['time'] = time.mktime(when.timetuple())
1552  event_data['event'] = unique_name
1553  event_data['repeat'] = repeat
1554  event_data['data'] = data
1555  self.bus.emit(Message('mycroft.scheduler.schedule_event',
1556  data=event_data))
1557 
1558  def schedule_event(self, handler, when, data=None, name=None):
1559  """
1560  Schedule a single-shot event.
1561 
1562  Args:
1563  handler: method to be called
1564  when (datetime/int/float): datetime (in system timezone) or
1565  number of seconds in the future when the
1566  handler should be called
1567  data (dict, optional): data to send when the handler is called
1568  name (str, optional): reference name
1569  NOTE: This will not warn or replace a
1570  previously scheduled event of the same
1571  name.
1572  """
1573  data = data or {}
1574  if isinstance(when, (int, float)):
1575  when = datetime.now() + timedelta(seconds=when)
1576  self._schedule_event(handler, when, data, name)
1577 
1578  def schedule_repeating_event(self, handler, when, frequency,
1579  data=None, name=None):
1580  """
1581  Schedule a repeating event.
1582 
1583  Args:
1584  handler: method to be called
1585  when (datetime): time (in system timezone) for first
1586  calling the handler, or None to
1587  initially trigger <frequency> seconds
1588  from now
1589  frequency (float/int): time in seconds between calls
1590  data (dict, optional): data to send when the handler is called
1591  name (str, optional): reference name, must be unique
1592  """
1593  # Do not schedule if this event is already scheduled by the skill
1594  if name not in self.scheduled_repeats:
1595  data = data or {}
1596  if not when:
1597  when = datetime.now() + timedelta(seconds=frequency)
1598  self._schedule_event(handler, when, data, name, frequency)
1599  else:
1600  LOG.debug('The event is already scheduled, cancel previous '
1601  'event if this scheduling should replace the last.')
1602 
1603  def update_scheduled_event(self, name, data=None):
1604  """
1605  Change data of event.
1606 
1607  Args:
1608  name (str): reference name of event (from original scheduling)
1609  """
1610  data = data or {}
1611  data = {
1612  'event': self._unique_name(name),
1613  'data': data
1614  }
1615  self.bus.emit(Message('mycroft.schedule.update_event', data=data))
1616 
1617  def cancel_scheduled_event(self, name):
1618  """
1619  Cancel a pending event. The event will no longer be scheduled
1620  to be executed
1621 
1622  Args:
1623  name (str): reference name of event (from original scheduling)
1624  """
1625  unique_name = self._unique_name(name)
1626  data = {'event': unique_name}
1627  if name in self.scheduled_repeats:
1628  self.scheduled_repeats.remove(name)
1629  if self.remove_event(unique_name):
1630  self.bus.emit(Message('mycroft.scheduler.remove_event',
1631  data=data))
1632 
1634  """
1635  Get scheduled event data and return the amount of time left
1636 
1637  Args:
1638  name (str): reference name of event (from original scheduling)
1639 
1640  Return:
1641  int: the time left in seconds
1642 
1643  Raises:
1644  Exception: Raised if event is not found
1645  """
1646  event_name = self._unique_name(name)
1647  data = {'name': event_name}
1648 
1649  # making event_status an object so it's refrence can be changed
1650  event_status = None
1651  finished_callback = False
1652 
1653  def callback(message):
1654  nonlocal event_status
1655  nonlocal finished_callback
1656  if message.data is not None:
1657  event_time = int(message.data[0][0])
1658  current_time = int(time.time())
1659  time_left_in_seconds = event_time - current_time
1660  event_status = time_left_in_seconds
1661  finished_callback = True
1662 
1663  emitter_name = 'mycroft.event_status.callback.{}'.format(event_name)
1664  self.bus.once(emitter_name, callback)
1665  self.bus.emit(Message('mycroft.scheduler.get_event', data=data))
1666 
1667  start_wait = time.time()
1668  while finished_callback is False and time.time() - start_wait < 3.0:
1669  time.sleep(0.1)
1670  if time.time() - start_wait > 3.0:
1671  raise Exception("Event Status Messagebus Timeout")
1672  return event_status
1673 
1675  """ Cancel any repeating events started by the skill. """
1676  # NOTE: Gotta make a copy of the list due to the removes that happen
1677  # in cancel_scheduled_event().
1678  for e in list(self.scheduled_repeats):
1679  self.cancel_scheduled_event(e)
1680 
1681  def acknowledge(self):
1682  """ Acknowledge a successful request.
1683 
1684  This method plays a sound to acknowledge a request that does not
1685  require a verbal response. This is intended to provide simple feedback
1686  to the user that their request was handled successfully.
1687  """
1688  audio_file = resolve_resource_file(
1689  self.config_core.get('sounds').get('acknowledge'))
1690 
1691  if not audio_file:
1692  LOG.warning("Could not find 'acknowledge' audio file!")
1693  return
1694 
1695  process = play_audio_file(audio_file)
1696  if not process:
1697  LOG.warning("Unable to play 'acknowledge' audio file!")
1698 
1699 
1700 #######################################################################
1701 # FallbackSkill base class
1702 #######################################################################
1704  """
1705  Fallbacks come into play when no skill matches an Adapt or closely with
1706  a Padatious intent. All Fallback skills work together to give them a
1707  view of the user's utterance. Fallback handlers are called in an order
1708  determined the priority provided when the the handler is registered.
1709 
1710  ======== ======== ================================================
1711  Priority Who? Purpose
1712  ======== ======== ================================================
1713  1-4 RESERVED Unused for now, slot for pre-Padatious if needed
1714  5 MYCROFT Padatious near match (conf > 0.8)
1715  6-88 USER General
1716  89 MYCROFT Padatious loose match (conf > 0.5)
1717  90-99 USER Uncaught intents
1718  100+ MYCROFT Fallback Unknown or other future use
1719  ======== ======== ================================================
1720 
1721  Handlers with the numerically lowest priority are invoked first.
1722  Multiple fallbacks can exist at the same priority, but no order is
1723  guaranteed.
1724 
1725  A Fallback can either observe or consume an utterance. A consumed
1726  utterance will not be see by any other Fallback handlers.
1727  """
1728  fallback_handlers = {}
1729 
1730  def __init__(self, name=None, bus=None, use_settings=True):
1731  MycroftSkill.__init__(self, name, bus, use_settings)
1732 
1733  # list of fallback handlers registered by this instance
1735 
1736  @classmethod
1738  """Goes through all fallback handlers until one returns True"""
1739 
1740  def handler(message):
1741  # indicate fallback handling start
1742  bus.emit(message.reply("mycroft.skill.handler.start",
1743  data={'handler': "fallback"}))
1744 
1745  stopwatch = Stopwatch()
1746  handler_name = None
1747  with stopwatch:
1748  for _, handler in sorted(cls.fallback_handlers.items(),
1749  key=operator.itemgetter(0)):
1750  try:
1751  if handler(message):
1752  # indicate completion
1753  handler_name = get_handler_name(handler)
1754  bus.emit(message.reply(
1755  'mycroft.skill.handler.complete',
1756  data={'handler': "fallback",
1757  "fallback_handler": handler_name}))
1758  break
1759  except Exception:
1760  LOG.exception('Exception in fallback.')
1761  else: # No fallback could handle the utterance
1762  bus.emit(message.reply('complete_intent_failure'))
1763  warning = "No fallback could handle intent."
1764  LOG.warning(warning)
1765  # indicate completion with exception
1766  bus.emit(message.reply('mycroft.skill.handler.complete',
1767  data={'handler': "fallback",
1768  'exception': warning}))
1769 
1770  # Send timing metric
1771  if message.context.get('ident'):
1772  ident = message.context['ident']
1773  report_timing(ident, 'fallback_handler', stopwatch,
1774  {'handler': handler_name})
1775 
1776  return handler
1777 
1778  @classmethod
1779  def _register_fallback(cls, handler, priority):
1780  """
1781  Register a function to be called as a general info fallback
1782  Fallback should receive message and return
1783  a boolean (True if succeeded or False if failed)
1784 
1785  Lower priority gets run first
1786  0 for high priority 100 for low priority
1787  """
1788  while priority in cls.fallback_handlers:
1789  priority += 1
1790 
1791  cls.fallback_handlers[priority] = handler
1792 
1793  def register_fallback(self, handler, priority):
1794  """
1795  register a fallback with the list of fallback handlers
1796  and with the list of handlers registered by this instance
1797  """
1798 
1799  def wrapper(*args, **kwargs):
1800  if handler(*args, **kwargs):
1801  self.make_active()
1802  return True
1803  return False
1804 
1805  self.instance_fallback_handlers.append(wrapper)
1806  self._register_fallback(wrapper, priority)
1807 
1808  @classmethod
1809  def remove_fallback(cls, handler_to_del):
1810  """
1811  Remove a fallback handler
1812 
1813  Args:
1814  handler_to_del: reference to handler
1815  """
1816  for priority, handler in cls.fallback_handlers.items():
1817  if handler == handler_to_del:
1818  del cls.fallback_handlers[priority]
1819  return
1820  LOG.warning('Could not remove fallback!')
1821 
1823  """
1824  Remove all fallback handlers registered by the fallback skill.
1825  """
1826  while len(self.instance_fallback_handlers):
1827  handler = self.instance_fallback_handlers.pop()
1828  self.remove_fallback(handler)
1829 
1830  def default_shutdown(self):
1831  """
1832  Remove all registered handlers and perform skill shutdown.
1833  """
1835  super(FallbackSkill, self).default_shutdown()
def register_handler(self, event, handler)
Definition: core.py:254
def load_regex(basedir, bus, skill_id)
Definition: skill_data.py:107
def report_timing(ident, system, timing, additional_data=None)
def remove_pages(self, page_names)
Definition: core.py:386
def register_fallback(self, handler, priority)
Definition: core.py:1793
def handle_disable_intent(self, message)
Definition: core.py:1237
def dig_for_message()
Definition: core.py:63
def resolve_resource_file(res_name)
def disable_intent(self, intent_name)
Definition: core.py:1246
def translate_list(self, list_name, data=None)
Definition: core.py:1013
def load_vocabulary(basedir, bus, skill_id)
Definition: skill_data.py:91
def schedule_event(self, handler, when, data=None, name=None)
Definition: core.py:1558
def register_entity_file(self, entity_file)
Definition: core.py:1196
def set_context(self, context, word='', origin=None)
Definition: core.py:1294
def __handle_stop(self, event)
Definition: core.py:1458
def show_text(self, text, title=None, override_idle=None)
Definition: core.py:409
def show_page(self, name, override_idle=None)
Definition: core.py:317
def set_cross_skill_context(self, context, word='')
Definition: core.py:1332
def send_event(self, event_name, params={})
Definition: core.py:304
def get_handler_name(handler)
Definition: core.py:177
def munge_intent_parser(intent_parser, name, skill_id)
Definition: skill_data.py:148
def __contains__(self, key)
Definition: core.py:294
def init_dialog(self, root_directory)
Definition: core.py:1422
def open_intent_envelope(message)
Definition: core.py:97
def resting_screen_handler(name=None)
Definition: core.py:463
def __setitem__(self, key, value)
Definition: core.py:282
def handle_enable_intent(self, message)
Definition: core.py:1228
def remove_fallback(cls, handler_to_del)
Definition: core.py:1809
def get_scheduled_event_status(self, name)
Definition: core.py:1633
def load_skill(skill_descriptor, bus, skill_id, BLACKLISTED_SKILLS=None)
Definition: core.py:106
def set_on_gui_changed(self, callback)
Definition: core.py:267
def register_intent(self, intent_parser, handler)
Definition: core.py:1133
def unmunge_message(message, skill_id)
Definition: core.py:76
def __init__(self, name=None, bus=None, use_settings=True)
Definition: core.py:1730
def handle_remove_cross_context(self, message)
Definition: core.py:1324
def load_data_files(self, root_directory)
Definition: core.py:1434
def register_resting_screen(self)
Definition: core.py:854
def remove_context(self, context)
Definition: core.py:1353
def __init__(self, name=None, bus=None, use_settings=True)
Definition: core.py:489
def remove_cross_skill_context(self, context)
Definition: core.py:1344
def ask_yesno(self, prompt, data=None)
Definition: core.py:754
def register_vocabulary(self, entity, entity_type)
Definition: core.py:1362
def gui_set(self, message)
Definition: core.py:276
def speak(self, utterance, expect_response=False, wait=False)
Definition: core.py:1382
def build_message_type(self, event)
Definition: core.py:245
def setup_default_handlers(self)
Definition: core.py:249
def translate(self, text, data=None)
Definition: core.py:899
def load_regex_files(self, root_directory)
Definition: core.py:1450
def handle_set_cross_context(self, message)
Definition: core.py:1313
def speak_dialog(self, key, data=None, expect_response=False, wait=False)
Definition: core.py:1405
def send_email(self, title, body)
Definition: core.py:825
def munge_regex(regex, skill_id)
Definition: skill_data.py:135
def voc_match(self, utt, voc_filename, lang=None)
Definition: core.py:775
def register_regex(self, regex_str)
Definition: core.py:1373
def __translate_file(self, name, data)
Definition: core.py:1032
def remove_page(self, page)
Definition: core.py:378
def translate_template(self, template_name, data=None)
Definition: core.py:995
def get_response(self, dialog='', data=None, validator=None, on_fail=None, num_retries=-1)
Definition: core.py:676
def _schedule_event(self, handler, when, data=None, name=None, repeat=None)
Definition: core.py:1537
def to_alnum(skill_id)
Definition: skill_data.py:122
def simple_trace(stack_trace)
Definition: core.py:54
def _register_fallback(cls, handler, priority)
Definition: core.py:1779
def add_event(self, name, handler, handler_info=None, once=False)
Definition: core.py:1042
def _unique_name(self, name)
Definition: core.py:1523
def remove_event(self, name)
Definition: core.py:1105
def _handle_collect_resting(self, message=None)
Definition: core.py:844
def __getitem__(self, key)
Definition: core.py:291
def show_pages(self, page_names, index=0, override_idle=None)
Definition: core.py:330
def create_skill_descriptor(skill_path)
Definition: core.py:173
def __init__(self, skill)
Definition: core.py:233
def find_resource(self, res_name, res_dirname=None)
Definition: core.py:916
def enable_intent(self, intent_name)
Definition: core.py:1267
def intent_file_handler(intent_file)
Definition: core.py:206
def cancel_scheduled_event(self, name)
Definition: core.py:1617
def load_vocab_files(self, root_directory)
Definition: core.py:1440
def report_metric(self, name, data)
Definition: core.py:816
def register_intent_file(self, intent_file, handler)
Definition: core.py:1153
def intent_handler(intent_parser)
Definition: core.py:192
def translate_namedvalues(self, name, delim=None)
Definition: core.py:957
def schedule_repeating_event(self, handler, when, frequency, data=None, name=None)
Definition: core.py:1579
def show_html(self, html, resource_url=None, override_idle=None)
Definition: core.py:440
def get(phrase, lang=None, context=None)
def update_scheduled_event(self, name, data=None)
Definition: core.py:1603
MycroftSkill base class.
Definition: core.py:483
def make_intent_failure_handler(cls, bus)
Definition: core.py:1737
def show_url(self, url, override_idle=None)
Definition: core.py:452
def show_image(self, url, caption=None, title=None, fill=None, override_idle=None)
Definition: core.py:423
def cancel_all_repeating_events(self)
Definition: core.py:1674


mycroft_ros
Author(s):
autogenerated on Mon Apr 26 2021 02:35:40