def __init__(self, ws): super(SkillManager, self).__init__() self._stop_event = Event() self._loaded_priority = Event() self.loaded_skills = {} self.msm_blocked = False self.ws = ws self.enclosure = EnclosureAPI(ws) # Schedule install/update of default skill self.next_download = None # Conversation management ws.on('skill.converse.request', self.handle_converse_request) # Update on initial connection ws.on('mycroft.internet.connected', self.schedule_update_skills) # Update upon request ws.on('skillmanager.update', self.schedule_now) ws.on('skillmanager.list', self.send_skill_list) # Register handlers for external MSM signals ws.on('msm.updating', self.block_msm) ws.on('msm.removing', self.block_msm) ws.on('msm.installing', self.block_msm) ws.on('msm.updated', self.restore_msm) ws.on('msm.removed', self.restore_msm) ws.on('msm.installed', self.restore_msm) # when locked, MSM is active or intentionally blocked self.__msm_lock = Lock() self.__ext_lock = Lock()
class TTS(object): """ TTS abstract class to be implemented by all TTS engines. It aggregates the minimum required parameters and exposes ``execute(sentence)`` function. """ __metaclass__ = ABCMeta def __init__(self, lang, voice, validator): super(TTS, self).__init__() self.lang = lang self.voice = voice self.filename = '/tmp/tts.wav' self.validator = validator random.seed() def init(self, ws): self.ws = ws self.enclosure = EnclosureAPI(self.ws) @abstractmethod def execute(self, sentence): pass def blink(self, rate=1.0): if random.random() < rate: self.enclosure.eyes_blink("b")
class TTS(object): """ TTS abstract class to be implemented by all TTS engines. It aggregates the minimum required parameters and exposes ``execute(sentence)`` function. """ __metaclass__ = ABCMeta def __init__(self, lang, voice, validator): super(TTS, self).__init__() self.lang = lang or 'en-us' self.voice = voice self.filename = '/tmp/tts.wav' self.validator = validator random.seed() def init(self, ws): self.ws = ws self.enclosure = EnclosureAPI(self.ws) @abstractmethod def execute(self, sentence): pass def blink(self, rate=1.0): if random.random() < rate: self.enclosure.eyes_blink("b")
def handle_open(): # The websocket is up and ready for business. This is a reasonable time # to declare the system is ready for normal operations. Send the # enclosure a message to reset itself to let the user know the system # is ready to receive input, such as stopping the rolling eyes shown # at boot on a Mycroft Mark 1 unit. enclosure = EnclosureAPI(client) enclosure.reset()
def check_connection(): """ Check for network connection. If not paired trigger pairing. Runs as a Timer every second until connection is detected. """ if connected(): enclosure = EnclosureAPI(ws) if is_paired(): # Skip the sync message when unpaired because the prompt to go to # home.mycrof.ai will be displayed by the pairing skill enclosure.mouth_text(mycroft.dialog.get("message_synching.clock")) # Force a sync of the local clock with the internet ws.emit(Message("system.ntp.sync")) time.sleep(15) # TODO: Generate/listen for a message response... # Check if the time skewed significantly. If so, reboot skew = abs((monotonic.monotonic() - start_ticks) - (time.time() - start_clock)) if skew > 60 * 60: # Time moved by over an hour in the NTP sync. Force a reboot to # prevent weird things from occcurring due to the 'time warp'. # ws.emit( Message( "speak", {'utterance': mycroft.dialog.get("time.changed.reboot")})) wait_while_speaking() # provide visual indicators of the reboot enclosure.mouth_text(mycroft.dialog.get("message_rebooting")) enclosure.eyes_color(70, 65, 69) # soft gray enclosure.eyes_spin() # give the system time to finish processing enclosure messages time.sleep(1.0) # reboot ws.emit(Message("system.reboot")) return else: ws.emit(Message("enclosure.mouth.reset")) time.sleep(0.5) ws.emit(Message('mycroft.internet.connected')) # check for pairing, if not automatically start pairing if not is_paired(): # begin the process payload = {'utterances': ["pair my device"], 'lang': "en-us"} ws.emit(Message("recognizer_loop:utterance", payload)) else: from mycroft.api import DeviceApi api = DeviceApi() api.update_version() else: thread = Timer(1, check_connection) thread.daemon = True thread.start()
def __init__(self): self.iface = pyw.winterfaces()[0] self.ap = AccessPoint(self.iface) self.server = None self.client = WebsocketClient() self.enclosure = EnclosureAPI(self.client) self.init_events() self.conn_monitor = None self.conn_monitor_stop = threading.Event()
def __init__(self): self.iface = pyw.winterfaces()[0] self.ap = AccessPoint(self.iface) self.server = None self.client = WebsocketClient() self.enclosure = EnclosureAPI(self.client) self.config = ConfigurationManager.get().get(self.NAME) self.init_events() self.first_setup()
def __init__(self, lang, voice, validator): super(TTS, self).__init__() self.lang = lang self.voice = voice self.filename = '/tmp/tts.wav' self.validator = validator self.ws = WebsocketClient() self.enclosure = EnclosureAPI(self.ws) random.seed()
def bind(self, emitter): """ Register emitter with skill. """ if emitter: self.emitter = emitter # TODO:18.08 - move to self.messagbus name self.enclosure = EnclosureAPI(emitter, self.name) self.add_event('mycroft.stop', self.__handle_stop) self.add_event('mycroft.skill.enable_intent', self.handle_enable_intent) self.add_event('mycroft.skill.disable_intent', self.handle_disable_intent) name = 'mycroft.skills.settings.update' func = self.settings.run_poll emitter.on(name, func) self.events.append((name, func))
def __init__(self, ws): super(SkillManager, self).__init__() self._stop_event = Event() self._connected_event = Event() self.loaded_skills = {} self.ws = ws self.enclosure = EnclosureAPI(ws) # Schedule install/update of default skill self.update_interval = Configuration.get()['skills']['update_interval'] self.update_interval = int(self.update_interval * 60 * MINUTES) self.dot_msm = join(SKILLS_DIR, '.msm') if exists(self.dot_msm): self.next_download = os.path.getmtime(self.dot_msm) + \ self.update_interval else: self.next_download = time.time() - 1 # Conversation management ws.on('skill.converse.request', self.handle_converse_request) # Update on initial connection ws.on('mycroft.internet.connected', lambda x: self._connected_event.set()) # Update upon request ws.on('skillmanager.update', self.schedule_now) ws.on('skillmanager.list', self.send_skill_list) self.msm = self.create_msm()
def __init__(self): self.iface = pyw.winterfaces()[0] self.ap = AccessPoint(self.iface) self.server = None self.ws = WebsocketClient() self.enclosure = EnclosureAPI(self.ws) self.init_events() self.conn_monitor = None self.conn_monitor_stop = threading.Event() self.starting = False
def mute_and_speak(utterance): EnclosureAPI(ws).eyes_stop_flash() lock.acquire() ws.emit(Message("recognizer_loop:audio_output_start")) try: logger.info("Speak: " + utterance) loop.mute() tts.execute(utterance) finally: loop.unmute() lock.release() ws.emit(Message("recognizer_loop:audio_output_end"))
class TTS(object): """ TTS abstract class to be implemented by all TTS engines. It aggregates the minimum required parameters and exposes ``execute(sentence)`` function. """ __metaclass__ = ABCMeta def __init__(self, lang, voice, validator): super(TTS, self).__init__() self.lang = lang or 'en-us' self.voice = voice self.filename = '/tmp/tts.wav' self.validator = validator self.enclosure = None random.seed() def init(self, ws): self.ws = ws self.enclosure = EnclosureAPI(self.ws) @abstractmethod def execute(self, sentence): ''' This performs TTS, blocking until audio completes This performs the TTS sequence. Upon completion, the sentence will have been spoken. Optionally, the TTS engine may have sent visemes to the enclosure by the TTS engine. Args: sentence (str): Words to be spoken ''' # TODO: Move caching support from mimic_tts to here for all TTS pass def blink(self, rate=1.0): if self.enclosure and random.random() < rate: self.enclosure.eyes_blink("b")
def handle_record_begin(): logger.info("Begin Recording...") EnclosureAPI(ws).eyes_brightness(12) # If enabled, play a wave file with a short sound to audibly # indicate recording has begun. if config.get('confirm_listening'): file = resolve_resource_file( config.get('sounds').get('start_listening')) if file: play_wav(file) ws.emit(Message('recognizer_loop:record_begin'))
def handle_speak(event): utterance = event.data['utterance'] print "calling api eyes_flash_stop" EnclosureAPI(ws).eyes_stop_flash() EnclosureAPI(ws).eyes_brightness(1) # This is a bit of a hack for Picroft. The analog audio on a Pi blocks # for 30 seconds fairly often, so we don't want to break on periods # (decreasing the chance of encountering the block). But we will # keep the split for non-Picroft installs since it give user feedback # faster on longer phrases. # # TODO: Remove or make an option? This is really a hack, anyway, # so we likely will want to get rid of this when not running on Mimic if not config.get('enclosure', {}).get('platform') == "picroft": chunks = re.split(r'(?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=\.|\?)\s', utterance) for chunk in chunks: mute_and_speak(chunk) else: mute_and_speak(utterance) EnclosureAPI(ws).eyes_brightness(0)
def execute(self, sentence, client): enclosure = EnclosureAPI(client) random.seed() # blink 50% of the time before speaking (only shows up if the # mimic TTS generation takes fairly long) if (random.random() < 0.5): enclosure.eyes_blink("b") # invoke mimic, creating WAV and outputting phoneme:duration pairs outMimic = subprocess.check_output([ BIN, '-voice', self.voice, '-t', sentence, '-psdur', "-o", "/tmp/mimic.wav" ]) # split into parts lisPairs = outMimic.split(" ") # covert phonemes to visemes visCodes = '' for pair in lisPairs: pho_dur = pair.split(":") if len(pho_dur) != 2: continue visCodes += self.PhonemeToViseme(pho_dur[0]) + ":" visCodes += pho_dur[1] + "," # play WAV and walk thru visemes while it plays enclosure.mouth_viseme(visCodes) subprocess.call(['aplay', '/tmp/mimic.wav']) # after speaking, blink 20% of the time if (random.random() < 0.2): enclosure.eyes_blink("b") # delete WAV os.remove("/tmp/mimic.wav")
def bind(self, emitter): """ Register emitter with skill. """ if emitter: self.emitter = emitter self.enclosure = EnclosureAPI(emitter, self.name) self.__register_stop()
class MycroftSkill(object): """ Abstract base class which provides common behaviour and parameters to all Skills implementation. """ def __init__(self, name=None, emitter=None): self.name = name or self.__class__.__name__ # Get directory of skill self._dir = dirname(abspath(sys.modules[self.__module__].__file__)) self.bind(emitter) self.config_core = Configuration.get() self.config = self.config_core.get(self.name) self.dialog_renderer = None self.vocab_dir = None self.root_dir = None self.file_system = FileSystemAccess(join('skills', self.name)) self.registered_intents = [] self.log = LOG.create_logger(self.name) self.reload_skill = True self.events = [] self.skill_id = 0 @property def location(self): """ Get the JSON data struction holding location information. """ # TODO: Allow Enclosure to override this for devices that # contain a GPS. return self.config_core.get('location') @property def location_pretty(self): """ Get a more 'human' version of the location as a string. """ loc = self.location if type(loc) is dict and loc["city"]: return loc["city"]["name"] return None @property def location_timezone(self): """ Get the timezone code, such as 'America/Los_Angeles' """ loc = self.location if type(loc) is dict and loc["timezone"]: return loc["timezone"]["code"] return None @property def lang(self): return self.config_core.get('lang') @property def settings(self): """ Load settings if not already loaded. """ try: return self._settings except: self._settings = SkillSettings(self._dir, self.name) return self._settings def bind(self, emitter): """ Register emitter with skill. """ if emitter: self.emitter = emitter self.enclosure = EnclosureAPI(emitter, self.name) self.__register_stop() def __register_stop(self): self.stop_time = time.time() self.stop_threshold = self.config_core.get("skills").get( 'stop_threshold') self.add_event('mycroft.stop', self.__handle_stop, False) def detach(self): for (name, intent) in self.registered_intents: name = str(self.skill_id) + ':' + name self.emitter.emit(Message("detach_intent", {"intent_name": name})) def initialize(self): """ Invoked after the skill is fully constructed and registered with the system. Use to perform any final setup needed for the skill. """ pass def get_intro_message(self): """ Get a message to speak on first load of the skill. Useful for post-install setup instructions. Returns: str: message that will be spoken to the user """ return None def converse(self, utterances, lang="en-us"): """ Handle conversation. This method can be used to override the normal intent handler after the skill has been invoked once. To enable this override thise converse method and return True to indicate that the utterance has been handled. Args: utterances (list): The utterances from the user lang: language the utterance is in Returns: True if an utterance was handled, otherwise False """ return False def __get_response(self): """ Helper to get a reponse from the user Returns: str: user's response or None on a timeout """ event = Event() def converse(utterances, lang="en-us"): converse.response = utterances[0] if utterances else None event.set() return True # install a temporary conversation handler self.make_active() converse.response = None default_converse = self.converse self.converse = converse event.wait(15) # 10 for listener, 5 for SST, then timeout self.converse = default_converse return converse.response def get_response(self, dialog='', data=None, announcement='', validator=None, on_fail=None, num_retries=-1): """ Prompt user and wait for response The given dialog or announcement will be spoken, the immediately listen and return user response. The response can optionally be validated. Example: color = self.get_response('ask.favorite.color') Args: dialog (str): Announcement dialog to read to the user data (dict): Data used to render the dialog announcement (str): Literal string (overrides dialog) validator (any): Function with following signature def validator(utterance): return utterance != "red" on_fail (any): Dialog or function returning literal string to speak on invalid input. For example: def on_fail(utterance): return "nobody likes the color red, pick another" num_retries (int): Times to ask user for input, -1 for infinite NOTE: User can not respond and timeout or say "cancel" to stop Returns: str: User's reply or None if timed out or canceled """ data = data or {} def get_announcement(): return announcement or self.dialog_renderer.render(dialog, data) if not get_announcement(): raise ValueError('announcement or dialog message required') def on_fail_default(utterance): fail_data = data.copy() fail_data['utterance'] = utterance if on_fail: return self.dialog_renderer.render(on_fail, fail_data) else: return get_announcement() # TODO: Load with something like mycroft.dialog.get_all() cancel_voc = 'text/' + self.lang + '/cancel.voc' with open(resolve_resource_file(cancel_voc)) as f: cancel_words = list(filter(bool, f.read().split('\n'))) def is_cancel(utterance): return utterance in cancel_words def validator_default(utterance): # accept anything except 'cancel' return not is_cancel(utterance) validator = validator or validator_default on_fail_fn = on_fail if callable(on_fail) else on_fail_default self.speak(get_announcement(), expect_response=True) num_fails = 0 while True: response = self.__get_response() if response is None: # if nothing said, prompt one more time num_none_fails = 1 if num_retries < 0 else num_retries if num_fails >= num_none_fails: return None else: if validator(response): return response # catch user saying 'cancel' if is_cancel(response): return None num_fails += 1 if 0 < num_retries < num_fails: return None line = on_fail_fn(response) self.speak(line, expect_response=True) def report_metric(self, name, data): """ Report a skill metric to the Mycroft servers Args: name (str): Name of metric. Must use only letters and hyphens data (dict): JSON dictionary to report. Must be valid JSON """ report_metric(basename(self.root_dir) + ':' + name, data) def send_email(self, title, body): """ Send an email to the registered user's email Args: title (str): Title of email body (str): HTML body of email. This supports simple HTML like bold and italics """ DeviceApi().send_email(title, body, basename(self.root_dir)) def make_active(self): """ Bump skill to active_skill list in intent_service this enables converse method to be called even without skill being used in last 5 minutes """ self.emitter.emit(Message('active_skill_request', {"skill_id": self.skill_id})) def _register_decorated(self): """ Register all intent handlers that have been decorated with an intent. """ global _intent_list, _intent_file_list for intent_parser, handler in _intent_list: self.register_intent(intent_parser, handler, need_self=True) for intent_file, handler in _intent_file_list: self.register_intent_file(intent_file, handler, need_self=True) _intent_list = [] _intent_file_list = [] def translate(self, text, data=None): """ Load a translatable single string resource The string is loaded from a file in the skill's dialog subdirectory 'dialog/<lang>/<text>.dialog' The string is randomly chosen from the file and rendered, replacing mustache placeholders with values found in the data dictionary. Args: text (str): The base filename (no extension needed) data (dict, optional): a JSON dictionary Returns: str: A randomly chosen string from the file """ return self.dialog_renderer.render(text, data or {}) def translate_namedvalues(self, name, delim=None): """ Load translation dict containing names and values. This loads a simple CSV from the 'dialog' folders. The name is the first list item, the value is the second. Lines prefixed with # or // get ignored Args: name (str): name of the .value file, no extension needed delim (char): delimiter character used, default is ',' Returns: dict: name and value dictionary, or [] if load fails """ delim = delim or ',' result = {} if not name.endswith(".value"): name += ".value" try: with open(join(self.root_dir, 'dialog', self.lang, name)) as f: reader = csv.reader(f, delimiter=delim) for row in reader: # skip blank or comment lines if not row or row[0].startswith("#"): continue if len(row) != 2: continue result[row[0]] = row[1] return result except Exception: return {} def translate_template(self, template_name, data=None): """ Load a translatable template The strings are loaded from a template file in the skill's dialog subdirectory. 'dialog/<lang>/<template_name>.template' The strings are loaded and rendered, replacing mustache placeholders with values found in the data dictionary. Args: template_name (str): The base filename (no extension needed) data (dict, optional): a JSON dictionary Returns: list of str: The loaded template file """ return self.__translate_file(template_name + '.template', data) def translate_list(self, list_name, data=None): """ Load a list of translatable string resources The strings are loaded from a list file in the skill's dialog subdirectory. 'dialog/<lang>/<list_name>.list' The strings are loaded and rendered, replacing mustache placeholders with values found in the data dictionary. Args: list_name (str): The base filename (no extension needed) data (dict, optional): a JSON dictionary Returns: list of str: The loaded list of strings with items in consistent positions regardless of the language. """ return self.__translate_file(list_name + '.list', data) def __translate_file(self, name, data): """Load and render lines from dialog/<lang>/<name>""" with open(join(self.root_dir, 'dialog', self.lang, name)) as f: text = f.read().replace('{{', '{').replace('}}', '}') return text.format(**data or {}).split('\n') def add_event(self, name, handler, need_self=False): """ Create event handler for executing intent Args: name: IntentParser name handler: method to call need_self: optional parameter, when called from a decorated intent handler the function will need the self variable passed as well. """ def wrapper(message): try: # Indicate that the skill handler is starting name = get_handler_name(handler) self.emitter.emit(Message("mycroft.skill.handler.start", data={'handler': name})) stopwatch = Stopwatch() with stopwatch: if need_self: # When registring from decorator self is required if len(getargspec(handler).args) == 2: handler(self, message) elif len(getargspec(handler).args) == 1: handler(self) elif len(getargspec(handler).args) == 0: # Zero may indicate multiple decorators, trying the # usual call signatures try: handler(self, message) except TypeError: handler(self) else: LOG.error("Unexpected argument count:" + str(len(getargspec(handler).args))) raise TypeError else: if len(getargspec(handler).args) == 2: handler(message) elif len(getargspec(handler).args) == 1: handler() else: LOG.error("Unexpected argument count:" + str(len(getargspec(handler).args))) raise TypeError self.settings.store() # Store settings if they've changed # Send timing metrics context = message.context if context and 'ident' in context: report_timing(context['ident'], 'skill_handler', stopwatch, {'handler': handler.__name__}) except Exception as e: # Convert "MyFancySkill" to "My Fancy Skill" for speaking name = re.sub("([a-z])([A-Z])", "\g<1> \g<2>", self.name) # TODO: Localize self.speak( "An error occurred while processing a request in " + name) LOG.error( "An error occurred while processing a request in " + self.name, exc_info=True) # indicate completion with exception self.emitter.emit(Message('mycroft.skill.handler.complete', data={'handler': name, 'exception': e.message})) # Indicate that the skill handler has completed self.emitter.emit(Message('mycroft.skill.handler.complete', data={'handler': name})) if handler: self.emitter.on(name, wrapper) self.events.append((name, wrapper)) def remove_event(self, name): """ Removes an event from emitter and events list Args: name: Name of Intent or Scheduler Event """ for _name, _handler in self.events: if name == _name: self.events.remove((_name, _handler)) self.emitter.remove(_name, _handler) def register_intent(self, intent_parser, handler, need_self=False): """ Register an Intent with the intent service. Args: intent_parser: Intent or IntentBuilder object to parse utterance for the handler. handler: function to register with intent need_self: optional parameter, when called from a decorated intent handler the function will need the self variable passed as well. """ if type(intent_parser) == IntentBuilder: intent_parser = intent_parser.build() elif type(intent_parser) != Intent: raise ValueError('intent_parser is not an Intent') # Default to the handler's function name if none given name = intent_parser.name or handler.__name__ intent_parser.name = str(self.skill_id) + ':' + name self.emitter.emit(Message("register_intent", intent_parser.__dict__)) self.registered_intents.append((name, intent_parser)) self.add_event(intent_parser.name, handler, need_self) def register_intent_file(self, intent_file, handler, need_self=False): """ Register an Intent file with the intent service. For example: === food.order.intent === Order some {food}. Order some {food} from {place}. I'm hungry. Grab some {food} from {place}. Optionally, you can also use <register_entity_file> to specify some examples of {food} and {place} In addition, instead of writing out multiple variations of the same sentence you can write: === food.order.intent === (Order | Grab) some {food} (from {place} | ). I'm hungry. Args: intent_file: name of file that contains example queries that should activate the intent handler: function to register with intent need_self: use for decorator. See <register_intent> """ name = str(self.skill_id) + ':' + intent_file self.emitter.emit(Message("padatious:register_intent", { "file_name": join(self.vocab_dir, intent_file), "name": name })) self.add_event(name, handler, need_self) def register_entity_file(self, entity_file): """ Register an Entity file with the intent service. And Entity file lists the exact values that an entity can hold. For example: === ask.day.intent === Is it {weekday}? === weekday.entity === Monday Tuesday ... Args: entity_file: name of file that contains examples of an entity. Must end with .entity """ if '.entity' not in entity_file: raise ValueError('Invalid entity filename: ' + entity_file) name = str(self.skill_id) + ':' + entity_file.replace('.entity', '') self.emitter.emit(Message("padatious:register_entity", { "file_name": join(self.vocab_dir, entity_file), "name": name })) def disable_intent(self, intent_name): """Disable a registered intent""" LOG.debug('Disabling intent ' + intent_name) name = str(self.skill_id) + ':' + intent_name self.emitter.emit(Message("detach_intent", {"intent_name": name})) def enable_intent(self, intent_name): """Reenable a registered intent""" for (name, intent) in self.registered_intents: if name == intent_name: self.registered_intents.remove((name, intent)) intent.name = name self.register_intent(intent, None) LOG.debug('Enabling intent ' + intent_name) break else: LOG.error('Could not enable ' + intent_name + ', it hasn\'t been registered.') def set_context(self, context, word=''): """ Add context to intent service Args: context: Keyword word: word connected to keyword """ if not isinstance(context, basestring): raise ValueError('context should be a string') if not isinstance(word, basestring): raise ValueError('word should be a string') self.emitter.emit(Message('add_context', {'context': context, 'word': word})) def remove_context(self, context): """ remove_context removes a keyword from from the context manager. """ if not isinstance(context, basestring): raise ValueError('context should be a string') self.emitter.emit(Message('remove_context', {'context': context})) def register_vocabulary(self, entity, entity_type): """ Register a word to an keyword Args: entity: word to register entity_type: Intent handler entity to tie the word to """ self.emitter.emit(Message('register_vocab', { 'start': entity, 'end': entity_type })) def register_regex(self, regex_str): re.compile(regex_str) # validate regex self.emitter.emit(Message('register_vocab', {'regex': regex_str})) def speak(self, utterance, expect_response=False): """ Speak a sentence. Args: utterance (str): sentence mycroft should speak expect_response (bool): set to True if Mycroft should listen for a response immediately after speaking the utterance. """ # registers the skill as being active self.enclosure.register(self.name) data = {'utterance': utterance, 'expect_response': expect_response} message = dig_for_message() if message: self.emitter.emit(message.reply("speak", data)) else: self.emitter.emit(Message("speak", data)) def speak_dialog(self, key, data=None, expect_response=False): """ Speak a random sentence from a dialog file. Args key (str): dialog file key (filename without extension) data (dict): information used to populate sentence expect_response (bool): set to True if Mycroft should listen for a response immediately after speaking the utterance. """ data = data or {} self.speak(self.dialog_renderer.render(key, data), expect_response) def init_dialog(self, root_directory): dialog_dir = join(root_directory, 'dialog', self.lang) if exists(dialog_dir): self.dialog_renderer = DialogLoader().load(dialog_dir) else: LOG.debug('No dialog loaded, ' + dialog_dir + ' does not exist') def load_data_files(self, root_directory): self.init_dialog(root_directory) self.load_vocab_files(join(root_directory, 'vocab', self.lang)) regex_path = join(root_directory, 'regex', self.lang) self.root_dir = root_directory if exists(regex_path): self.load_regex_files(regex_path) def load_vocab_files(self, vocab_dir): self.vocab_dir = vocab_dir if exists(vocab_dir): load_vocabulary(vocab_dir, self.emitter) else: LOG.debug('No vocab loaded, ' + vocab_dir + ' does not exist') def load_regex_files(self, regex_dir): load_regex(regex_dir, self.emitter) def __handle_stop(self, event): """ Handler for the "mycroft.stop" signal. Runs the user defined `stop()` method. """ self.stop_time = time.time() try: self.stop() except: LOG.error("Failed to stop skill: {}".format(self.name), exc_info=True) @abc.abstractmethod def stop(self): pass def is_stop(self): passed_time = time.time() - self.stop_time return passed_time < self.stop_threshold def shutdown(self): """ This method is intended to be called during the skill process termination. The skill implementation must shutdown all processes and operations in execution. """ # Store settings self.settings.store() self.settings.is_alive = False # removing events for e, f in self.events: self.emitter.remove(e, f) self.events = None # Remove reference to wrappers self.emitter.emit( Message("detach_skill", {"skill_id": str(self.skill_id) + ":"})) try: self.stop() except: LOG.error("Failed to stop skill: {}".format(self.name), exc_info=True) def _unique_name(self, name): """ Return a name unique to this skill using the format [skill_id]:[name]. Args: name: Name to use internally Returns: str: name unique to this skill """ return str(self.skill_id) + ':' + name def _schedule_event(self, handler, when, data=None, name=None, repeat=None): """ Underlying method for schedle_event and schedule_repeating_event. Takes scheduling information and sends it of on the message bus. """ data = data or {} if not name: name = self.name + handler.__name__ name = self._unique_name(name) self.add_event(name, handler, False) event_data = {} event_data['time'] = time.mktime(when.timetuple()) event_data['event'] = name event_data['repeat'] = repeat event_data['data'] = data self.emitter.emit(Message('mycroft.scheduler.schedule_event', data=event_data)) def schedule_event(self, handler, when, data=None, name=None): """ Schedule a single event. Args: handler: method to be called when (datetime): when the handler should be called data (dict, optional): data to send when the handler is called name (str, optional): friendly name parameter """ data = data or {} self._schedule_event(handler, when, data, name) def schedule_repeating_event(self, handler, when, frequency, data=None, name=None): """ Schedule a repeating event. Args: handler: method to be called when (datetime): time for calling the handler frequency (float/int): time in seconds between calls data (dict, optional): data to send along to the handler name (str, optional): friendly name parameter """ data = data or {} self._schedule_event(handler, when, data, name, frequency) def update_scheduled_event(self, name, data=None): """ Change data of event. Args: name (str): Name of event """ data = data or {} data = { 'event': self._unique_name(name), 'data': data } self.emitter.emit(Message('mycroft.schedule.update_event', data=data)) def cancel_scheduled_event(self, name): """ Cancel a pending event. The event will no longer be scheduled to be executed Args: name (str): Name of event """ unique_name = self._unique_name(name) data = {'event': unique_name} self.remove_event(unique_name) self.emitter.emit(Message('mycroft.scheduler.remove_event', data=data)) def get_scheduled_event_status(self, name): """ Get scheduled event data and return the amount of time left Args: name (str): Name of event Return: int: the time left in seconds """ event_name = self._unique_name(name) data = {'name': event_name} # making event_status an object so it's refrence can be changed event_status = [None] finished_callback = [False] def callback(message): if message.data is not None: event_time = int(message.data[0][0]) current_time = int(time.time()) time_left_in_seconds = event_time - current_time event_status[0] = time_left_in_seconds finished_callback[0] = True emitter_name = 'mycroft.event_status.callback.{}'.format(event_name) self.emitter.once(emitter_name, callback) self.emitter.emit(Message('mycroft.scheduler.get_event', data=data)) start_wait = time.time() while finished_callback[0] is False and time.time() - start_wait < 3.0: time.sleep(0.1) if time.time() - start_wait > 3.0: raise Exception("Event Status Messagebus Timeout") return event_status[0]
def init(self, ws): self.ws = ws self.enclosure = EnclosureAPI(self.ws)
class WiFi: NAME = "WiFiClient" def __init__(self): self.iface = pyw.winterfaces()[0] self.ap = AccessPoint(self.iface) self.server = None self.client = WebsocketClient() self.enclosure = EnclosureAPI(self.client) self.config = ConfigurationManager.get().get(self.NAME) self.init_events() self.first_setup() def init_events(self): self.client.on('mycroft.wifi.start', self.start) self.client.on('mycroft.wifi.stop', self.stop) self.client.on('mycroft.wifi.scan', self.scan) self.client.on('mycroft.wifi.connect', self.connect) def first_setup(self): if str2bool(self.config.get('setup')): self.start() def start(self, event=None): LOG.info("Starting access point...") self.client.emit( Message( "speak", metadata={'utterance': "Initializing wireless setup mode."})) self.ap.up() if not self.server: self.server = WebServer(self.ap.ip, 80) self.server.start() self.enclosure.mouth_text(self.ap.password) LOG.info("Access point started!\n%s" % self.ap.__dict__) def scan(self, event=None): LOG.info("Scanning wifi connections...") networks = {} status = self.get_status() for cell in Cell.all(self.iface): update = True ssid = cell.ssid quality = self.get_quality(cell.quality) if networks.__contains__(ssid): update = networks.get(ssid).get("quality") < quality if update and ssid: networks[ssid] = { 'quality': quality, 'encrypted': cell.encrypted, 'connected': self.is_connected(ssid, status) } self.client.emit( Message("mycroft.wifi.scanned", {'networks': networks})) LOG.info("Wifi connections scanned!\n%s" % networks) @staticmethod def get_quality(quality): values = quality.split("/") return float(values[0]) / float(values[1]) def connect(self, event=None): if event and event.metadata: ssid = event.metadata.get("ssid") connected = self.is_connected(ssid) if connected: LOG.warn("Mycroft is already connected to %s" % ssid) else: self.disconnect() LOG.info("Connecting to: %s" % ssid) nid = wpa(self.iface, 'add_network') wpa(self.iface, 'set_network', nid, 'ssid', '"' + ssid + '"') if event.metadata.__contains__("pass"): psk = '"' + event.metadata.get("pass") + '"' wpa(self.iface, 'set_network', nid, 'psk', psk) else: wpa(self.iface, 'set_network', nid, 'key_mgmt', 'NONE') wpa(self.iface, 'enable', nid) connected = self.get_connected(ssid) if connected: wpa(self.iface, 'save_config') ConfigurationManager.set(self.NAME, 'setup', False, True) self.client.emit( Message("mycroft.wifi.connected", {'connected': connected})) LOG.info("Connection status for %s = %s" % (ssid, connected)) def disconnect(self): status = self.get_status() nid = status.get("id") if nid: ssid = status.get("ssid") wpa(self.iface, 'disable', nid) LOG.info("Disconnecting %s id: %s" % (ssid, nid)) def get_status(self): res = cli('wpa_cli', '-i', self.iface, 'status') out = str(res.get("stdout")) if out: return dict(o.split("=") for o in out.split("\n")[:-1]) return {} def get_connected(self, ssid, retry=5): connected = self.is_connected(ssid) while not connected and retry > 0: sleep(2) retry -= 1 connected = self.is_connected(ssid) return connected def is_connected(self, ssid, status=None): status = status or self.get_status() state = status.get("wpa_state") return status.get("ssid") == ssid and state == "COMPLETED" def stop(self, event=None): LOG.info("Stopping access point...") self.ap.down() if self.server: self.server.server.shutdown() self.server.server.server_close() self.server.join() self.server = None LOG.info("Access point stopped!") def run(self): try: self.client.run_forever() except Exception as e: LOG.error("Error: {0}".format(e)) self.stop()
def check_connection(): """ Check for network connection. If not paired trigger pairing. Runs as a Timer every second until connection is detected. """ if connected(): enclosure = EnclosureAPI(ws) if is_paired(): # Skip the sync message when unpaired because the prompt to go to # home.mycrof.ai will be displayed by the pairing skill enclosure.mouth_text(mycroft.dialog.get("message_synching.clock")) # Force a sync of the local clock with the internet ws.emit(Message("system.ntp.sync")) time.sleep(15) # TODO: Generate/listen for a message response... # Check if the time skewed significantly. If so, reboot skew = abs((monotonic.monotonic() - start_ticks) - (time.time() - start_clock)) if skew > 60*60: # Time moved by over an hour in the NTP sync. Force a reboot to # prevent weird things from occcurring due to the 'time warp'. # ws.emit(Message("speak", {'utterance': mycroft.dialog.get("time.changed.reboot")})) wait_while_speaking() # provide visual indicators of the reboot enclosure.mouth_text(mycroft.dialog.get("message_rebooting")) enclosure.eyes_color(70, 65, 69) # soft gray enclosure.eyes_spin() # give the system time to finish processing enclosure messages time.sleep(1.0) # reboot ws.emit(Message("system.reboot")) return ws.emit(Message('mycroft.internet.connected')) # check for pairing, if not automatically start pairing if not is_paired(): # begin the process payload = { 'utterances': ["pair my device"], 'lang': "en-us" } ws.emit(Message("recognizer_loop:utterance", payload)) else: if is_paired(): # Skip the message when unpaired because the prompt to go # to home.mycrof.ai will be displayed by the pairing skill enclosure.mouth_text(mycroft.dialog.get("message_updating")) from mycroft.api import DeviceApi api = DeviceApi() api.update_version() else: thread = Timer(1, check_connection) thread.daemon = True thread.start()
class MycroftSkill(object): """ Abstract base class which provides common behaviour and parameters to all Skills implementation. """ def __init__(self, name=None, emitter=None): self.name = name or self.__class__.__name__ # Get directory of skill self._dir = dirname(abspath(sys.modules[self.__module__].__file__)) self.settings = SkillSettings(self._dir, self.name) self.bind(emitter) self.config_core = Configuration.get() self.config = self.config_core.get(self.name) or {} self.dialog_renderer = None self.vocab_dir = None self.root_dir = None self.file_system = FileSystemAccess(join('skills', self.name)) self.registered_intents = [] self.log = LOG.create_logger(self.name) self.reload_skill = True # allow reloading self.events = [] self.scheduled_repeats = [] self.skill_id = '' # will be set from the path, so guaranteed unique @property def location(self): """ Get the JSON data struction holding location information. """ # TODO: Allow Enclosure to override this for devices that # contain a GPS. return self.config_core.get('location') @property def location_pretty(self): """ Get a more 'human' version of the location as a string. """ loc = self.location if type(loc) is dict and loc["city"]: return loc["city"]["name"] return None @property def location_timezone(self): """ Get the timezone code, such as 'America/Los_Angeles' """ loc = self.location if type(loc) is dict and loc["timezone"]: return loc["timezone"]["code"] return None @property def lang(self): return self.config_core.get('lang') def bind(self, emitter): """ Register emitter with skill. """ if emitter: self.emitter = emitter # TODO:18.08 - move to self.messagbus name self.enclosure = EnclosureAPI(emitter, self.name) self.add_event('mycroft.stop', self.__handle_stop) self.add_event('mycroft.skill.enable_intent', self.handle_enable_intent) self.add_event('mycroft.skill.disable_intent', self.handle_disable_intent) name = 'mycroft.skills.settings.update' func = self.settings.run_poll emitter.on(name, func) self.events.append((name, func)) def detach(self): for (name, intent) in self.registered_intents: name = str(self.skill_id) + ':' + name self.emitter.emit(Message("detach_intent", {"intent_name": name})) def initialize(self): """ Invoked after the skill is fully constructed and registered with the system. Use to perform any final setup needed for the skill. """ pass def get_intro_message(self): """ Get a message to speak on first load of the skill. Useful for post-install setup instructions. Returns: str: message that will be spoken to the user """ return None def converse(self, utterances, lang="en-us"): """ Handle conversation. This method can be used to override the normal intent handler after the skill has been invoked once. To enable this override thise converse method and return True to indicate that the utterance has been handled. Args: utterances (list): The utterances from the user lang: language the utterance is in Returns: True if an utterance was handled, otherwise False """ return False def __get_response(self): """ Helper to get a reponse from the user Returns: str: user's response or None on a timeout """ event = Event() def converse(utterances, lang="en-us"): converse.response = utterances[0] if utterances else None event.set() return True # install a temporary conversation handler self.make_active() converse.response = None default_converse = self.converse self.converse = converse event.wait(15) # 10 for listener, 5 for SST, then timeout self.converse = default_converse return converse.response def get_response(self, dialog='', data=None, announcement='', validator=None, on_fail=None, num_retries=-1): """ Prompt user and wait for response The given dialog or announcement will be spoken, the immediately listen and return user response. The response can optionally be validated. Example: color = self.get_response('ask.favorite.color') Args: dialog (str): Announcement dialog to read to the user data (dict): Data used to render the dialog announcement (str): Literal string (overrides dialog) validator (any): Function with following signature def validator(utterance): return utterance != "red" on_fail (any): Dialog or function returning literal string to speak on invalid input. For example: def on_fail(utterance): return "nobody likes the color red, pick another" num_retries (int): Times to ask user for input, -1 for infinite NOTE: User can not respond and timeout or say "cancel" to stop Returns: str: User's reply or None if timed out or canceled """ data = data or {} def get_announcement(): nonlocal announcement # The dialog param can be either a spoken string or a dialog file # TODO: 18.08 merge dialog/announcement if not exists( join(self.root_dir, 'dialog', self.lang, dialog + '.dialog')) and not announcement: announcement = dialog return announcement or self.dialog_renderer.render(dialog, data) if not get_announcement(): raise ValueError('announcement or dialog message required') def on_fail_default(utterance): fail_data = data.copy() fail_data['utterance'] = utterance if on_fail: return self.dialog_renderer.render(on_fail, fail_data) else: return get_announcement() def is_cancel(utterance): return self.is_match(utterance, 'cancel') def validator_default(utterance): # accept anything except 'cancel' return not is_cancel(utterance) validator = validator or validator_default on_fail_fn = on_fail if callable(on_fail) else on_fail_default self.speak(get_announcement(), expect_response=True) wait_while_speaking() num_fails = 0 while True: response = self.__get_response() if response is None: # if nothing said, prompt one more time num_none_fails = 1 if num_retries < 0 else num_retries if num_fails >= num_none_fails: return None else: if validator(response): return response # catch user saying 'cancel' if is_cancel(response): return None num_fails += 1 if 0 < num_retries < num_fails: return None line = on_fail_fn(response) self.speak(line, expect_response=True) def ask_yesno(self, prompt, data=None): """ Read prompt and wait for a yes/no answer This automatically deals with translation and common variants, such as 'yeah', 'sure', etc. Args: prompt (str): a dialog id or string to read Returns: string: 'yes', 'no' or whatever the user response if not one of those, including None """ resp = self.get_response(dialog=prompt, data=data) if self.is_match(resp, 'yes'): return 'yes' if self.is_match(resp, 'no'): return 'no' return resp def is_match(self, utt, voc_filename, lang=None): """ Determine if the given utterance contains the vocabular proviced This checks for vocabulary match in the utternce instead of the other way around to allow the user to say things like "yes, please" and still match against voc files with only "yes" in it. Args: utt (str): Utterance to be tested voc_filename (str): Name of vocabulary file (e.g. 'yes' for 'res/text/en-us/yes.voc') lang (str): Language code, defaults to self.long Returns: bool: True if the utterance has the given vocabulary it """ lang = lang or self.lang voc = join('text', self.lang, voc_filename + ".voc") with open(resolve_resource_file(voc)) as f: words = list(filter(bool, f.read().split('\n'))) if (utt and any(i.strip() in utt for i in words)): return True return False def report_metric(self, name, data): """ Report a skill metric to the Mycroft servers Args: name (str): Name of metric. Must use only letters and hyphens data (dict): JSON dictionary to report. Must be valid JSON """ report_metric(basename(self.root_dir) + ':' + name, data) def send_email(self, title, body): """ Send an email to the registered user's email Args: title (str): Title of email body (str): HTML body of email. This supports simple HTML like bold and italics """ DeviceApi().send_email(title, body, basename(self.root_dir)) def make_active(self): """ Bump skill to active_skill list in intent_service this enables converse method to be called even without skill being used in last 5 minutes """ self.emitter.emit( Message('active_skill_request', {"skill_id": self.skill_id})) def _register_decorated(self): """ Register all intent handlers that have been decorated with an intent. Looks for all functions that have been marked by a decorator and read the intent data from them """ for attr_name in dir(self): method = getattr(self, attr_name) if hasattr(method, 'intents'): for intent in getattr(method, 'intents'): self.register_intent(intent, method) if hasattr(method, 'intent_files'): for intent_file in getattr(method, 'intent_files'): self.register_intent_file(intent_file, method) def translate(self, text, data=None): """ Load a translatable single string resource The string is loaded from a file in the skill's dialog subdirectory 'dialog/<lang>/<text>.dialog' The string is randomly chosen from the file and rendered, replacing mustache placeholders with values found in the data dictionary. Args: text (str): The base filename (no extension needed) data (dict, optional): a JSON dictionary Returns: str: A randomly chosen string from the file """ return self.dialog_renderer.render(text, data or {}) def translate_namedvalues(self, name, delim=None): """ Load translation dict containing names and values. This loads a simple CSV from the 'dialog' folders. The name is the first list item, the value is the second. Lines prefixed with # or // get ignored Args: name (str): name of the .value file, no extension needed delim (char): delimiter character used, default is ',' Returns: dict: name and value dictionary, or [] if load fails """ delim = delim or ',' result = {} if not name.endswith(".value"): name += ".value" try: with open(join(self.root_dir, 'dialog', self.lang, name)) as f: reader = csv.reader(f, delimiter=delim) for row in reader: # skip blank or comment lines if not row or row[0].startswith("#"): continue if len(row) != 2: continue result[row[0]] = row[1] return result except Exception: return {} def translate_template(self, template_name, data=None): """ Load a translatable template The strings are loaded from a template file in the skill's dialog subdirectory. 'dialog/<lang>/<template_name>.template' The strings are loaded and rendered, replacing mustache placeholders with values found in the data dictionary. Args: template_name (str): The base filename (no extension needed) data (dict, optional): a JSON dictionary Returns: list of str: The loaded template file """ return self.__translate_file(template_name + '.template', data) def translate_list(self, list_name, data=None): """ Load a list of translatable string resources The strings are loaded from a list file in the skill's dialog subdirectory. 'dialog/<lang>/<list_name>.list' The strings are loaded and rendered, replacing mustache placeholders with values found in the data dictionary. Args: list_name (str): The base filename (no extension needed) data (dict, optional): a JSON dictionary Returns: list of str: The loaded list of strings with items in consistent positions regardless of the language. """ return self.__translate_file(list_name + '.list', data) def __translate_file(self, name, data): """Load and render lines from dialog/<lang>/<name>""" with open(join(self.root_dir, 'dialog', self.lang, name)) as f: text = f.read().replace('{{', '{').replace('}}', '}') return text.format(**data or {}).split('\n') def add_event(self, name, handler, handler_info=None, once=False): """ Create event handler for executing intent Args: name: IntentParser name handler: method to call handler_info: base message when reporting skill event handler status on messagebus. once: optional parameter, Event handler will be removed after it has been run once. """ def wrapper(message): skill_data = {'name': get_handler_name(handler)} stopwatch = Stopwatch() try: message = unmunge_message(message, self.skill_id) # Indicate that the skill handler is starting if handler_info: # Indicate that the skill handler is starting if requested msg_type = handler_info + '.start' self.emitter.emit(message.reply(msg_type, skill_data)) if once: # Remove registered one-time handler before invoking, # allowing them to re-schedule themselves. self.remove_event(name) with stopwatch: if len(signature(handler).parameters) == 0: handler() else: handler(message) self.settings.store() # Store settings if they've changed except Exception as e: # Convert "MyFancySkill" to "My Fancy Skill" for speaking handler_name = camel_case_split(self.name) msg_data = {'skill': handler_name} msg = dialog.get('skill.error', self.lang, msg_data) self.speak(msg) LOG.exception(msg) # append exception information in message skill_data['exception'] = repr(e) finally: # Indicate that the skill handler has completed if handler_info: msg_type = handler_info + '.complete' self.emitter.emit(message.reply(msg_type, skill_data)) # Send timing metrics context = message.context if context and 'ident' in context: report_timing(context['ident'], 'skill_handler', stopwatch, {'handler': handler.__name__}) if handler: if once: self.emitter.once(name, wrapper) else: self.emitter.on(name, wrapper) self.events.append((name, wrapper)) def remove_event(self, name): """ Removes an event from emitter and events list Args: name: Name of Intent or Scheduler Event Returns: bool: True if found and removed, False if not found """ removed = False for _name, _handler in self.events: if name == _name: try: self.events.remove((_name, _handler)) except ValueError: pass removed = True # Because of function wrappers, the emitter doesn't always directly # hold the _handler function, it sometimes holds something like # 'wrapper(_handler)'. So a call like: # self.emitter.remove(_name, _handler) # will not find it, leaving an event handler with that name left behind # waiting to fire if it is ever re-installed and triggered. # Remove all handlers with the given name, regardless of handler. if removed: self.emitter.remove_all_listeners(name) return removed def register_intent(self, intent_parser, handler): """ Register an Intent with the intent service. Args: intent_parser: Intent or IntentBuilder object to parse utterance for the handler. handler: function to register with intent """ if isinstance(intent_parser, IntentBuilder): intent_parser = intent_parser.build() elif not isinstance(intent_parser, Intent): raise ValueError('intent_parser is not an Intent') # Default to the handler's function name if none given name = intent_parser.name or handler.__name__ munge_intent_parser(intent_parser, name, self.skill_id) self.emitter.emit(Message("register_intent", intent_parser.__dict__)) self.registered_intents.append((name, intent_parser)) self.add_event(intent_parser.name, handler, 'mycroft.skill.handler') def register_intent_file(self, intent_file, handler): """ Register an Intent file with the intent service. For example: === food.order.intent === Order some {food}. Order some {food} from {place}. I'm hungry. Grab some {food} from {place}. Optionally, you can also use <register_entity_file> to specify some examples of {food} and {place} In addition, instead of writing out multiple variations of the same sentence you can write: === food.order.intent === (Order | Grab) some {food} (from {place} | ). I'm hungry. Args: intent_file: name of file that contains example queries that should activate the intent handler: function to register with intent """ name = str(self.skill_id) + ':' + intent_file data = {"file_name": join(self.vocab_dir, intent_file), "name": name} self.emitter.emit(Message("padatious:register_intent", data)) self.registered_intents.append((intent_file, data)) self.add_event(name, handler, 'mycroft.skill.handler') def register_entity_file(self, entity_file): """ Register an Entity file with the intent service. And Entity file lists the exact values that an entity can hold. For example: === ask.day.intent === Is it {weekday}? === weekday.entity === Monday Tuesday ... Args: entity_file: name of file that contains examples of an entity. Must end with .entity """ if '.entity' not in entity_file: raise ValueError('Invalid entity filename: ' + entity_file) name = str(self.skill_id) + ':' + entity_file.replace('.entity', '') self.emitter.emit( Message("padatious:register_entity", { "file_name": join(self.vocab_dir, entity_file), "name": name })) def handle_enable_intent(self, message): """ Listener to enable a registered intent if it belongs to this skill """ intent_name = message.data["intent_name"] for (name, intent) in self.registered_intents: if name == intent_name: return self.enable_intent(intent_name) def handle_disable_intent(self, message): """ Listener to disable a registered intent if it belongs to this skill """ intent_name = message.data["intent_name"] for (name, intent) in self.registered_intents: if name == intent_name: return self.disable_intent(intent_name) def disable_intent(self, intent_name): """ Disable a registered intent if it belongs to this skill Args: intent_name: name of the intent to be disabled Returns: bool: True if disabled, False if it wasn't registered """ names = [intent_tuple[0] for intent_tuple in self.registered_intents] if intent_name in names: LOG.debug('Disabling intent ' + intent_name) name = str(self.skill_id) + ':' + intent_name self.emitter.emit(Message("detach_intent", {"intent_name": name})) return True LOG.error('Could not disable ' + intent_name + ', it hasn\'t been registered.') return False def enable_intent(self, intent_name): """ (Re)Enable a registered intentif it belongs to this skill Args: intent_name: name of the intent to be enabled Returns: bool: True if enabled, False if it wasn't registered """ names = [intent[0] for intent in self.registered_intents] intents = [intent[1] for intent in self.registered_intents] if intent_name in names: intent = intents[names.index(intent_name)] self.registered_intents.remove((intent_name, intent)) if ".intent" in intent_name: self.register_intent_file(intent_name, None) else: intent.name = intent_name self.register_intent(intent, None) LOG.debug('Enabling intent ' + intent_name) return True LOG.error('Could not enable ' + intent_name + ', it hasn\'t been ' 'registered.') return False def set_context(self, context, word=''): """ Add context to intent service Args: context: Keyword word: word connected to keyword """ if not isinstance(context, str): raise ValueError('context should be a string') if not isinstance(word, str): raise ValueError('word should be a string') context = to_alnum(self.skill_id) + context self.emitter.emit( Message('add_context', { 'context': context, 'word': word })) def remove_context(self, context): """ remove_context removes a keyword from from the context manager. """ if not isinstance(context, str): raise ValueError('context should be a string') self.emitter.emit(Message('remove_context', {'context': context})) def register_vocabulary(self, entity, entity_type): """ Register a word to an keyword Args: entity: word to register entity_type: Intent handler entity to tie the word to """ self.emitter.emit( Message('register_vocab', { 'start': entity, 'end': to_alnum(self.skill_id) + entity_type })) def register_regex(self, regex_str): """ Register a new regex. Args: regex_str: Regex string """ regex = munge_regex(regex_str, self.skill_id) re.compile(regex) # validate regex self.emitter.emit(Message('register_vocab', {'regex': regex})) def speak(self, utterance, expect_response=False): """ Speak a sentence. Args: utterance (str): sentence mycroft should speak expect_response (bool): set to True if Mycroft should listen for a response immediately after speaking the utterance. """ # registers the skill as being active self.enclosure.register(self.name) data = {'utterance': utterance, 'expect_response': expect_response} message = dig_for_message() if message: self.emitter.emit(message.reply("speak", data)) else: self.emitter.emit(Message("speak", data)) def speak_dialog(self, key, data=None, expect_response=False): """ Speak a random sentence from a dialog file. Args key (str): dialog file key (filename without extension) data (dict): information used to populate sentence expect_response (bool): set to True if Mycroft should listen for a response immediately after speaking the utterance. """ data = data or {} self.speak(self.dialog_renderer.render(key, data), expect_response) def init_dialog(self, root_directory): dialog_dir = join(root_directory, 'dialog', self.lang) if exists(dialog_dir): self.dialog_renderer = DialogLoader().load(dialog_dir) else: LOG.debug('No dialog loaded, ' + dialog_dir + ' does not exist') def load_data_files(self, root_directory): self.init_dialog(root_directory) self.load_vocab_files(join(root_directory, 'vocab', self.lang)) regex_path = join(root_directory, 'regex', self.lang) self.root_dir = root_directory if exists(regex_path): self.load_regex_files(regex_path) def load_vocab_files(self, vocab_dir): self.vocab_dir = vocab_dir if exists(vocab_dir): load_vocabulary(vocab_dir, self.emitter, self.skill_id) else: LOG.debug('No vocab loaded, ' + vocab_dir + ' does not exist') def load_regex_files(self, regex_dir): load_regex(regex_dir, self.emitter, self.skill_id) def __handle_stop(self, event): """ Handler for the "mycroft.stop" signal. Runs the user defined `stop()` method. """ def __stop_timeout(): # The self.stop() call took more than 100ms, assume it handled Stop self.emitter.emit( Message("mycroft.stop.handled", {"skill_id": str(self.skill_id) + ":"})) timer = Timer(0.1, __stop_timeout) # set timer for 100ms try: if self.stop(): self.emitter.emit( Message("mycroft.stop.handled", {"by": "skill:" + str(self.skill_id)})) timer.cancel() except: timer.cancel() LOG.error("Failed to stop skill: {}".format(self.name), exc_info=True) @abc.abstractmethod def stop(self): pass def shutdown(self): """ This method is intended to be called during the skill process termination. The skill implementation must shutdown all processes and operations in execution. """ pass def default_shutdown(self): """Parent function called internally to shut down everything. Shuts down known entities and calls skill specific shutdown method. """ try: self.shutdown() except Exception as e: LOG.error('Skill specific shutdown function encountered ' 'an error: {}'.format(repr(e))) # Store settings if exists(self._dir): self.settings.store() self.settings.stop_polling() # removing events self.cancel_all_repeating_events() for e, f in self.events: self.emitter.remove(e, f) self.events = [] # Remove reference to wrappers self.emitter.emit( Message("detach_skill", {"skill_id": str(self.skill_id) + ":"})) try: self.stop() except: LOG.error("Failed to stop skill: {}".format(self.name), exc_info=True) def _unique_name(self, name): """ Return a name unique to this skill using the format [skill_id]:[name]. Args: name: Name to use internally Returns: str: name unique to this skill """ return str(self.skill_id) + ':' + (name or '') def _schedule_event(self, handler, when, data=None, name=None, repeat=None): """ Underlying method for schedule_event and schedule_repeating_event. Takes scheduling information and sends it of on the message bus. """ if not name: name = self.name + handler.__name__ name = self._unique_name(name) if repeat: self.scheduled_repeats.append(name) data = data or {} self.add_event(name, handler, once=not repeat) event_data = {} event_data['time'] = time.mktime(when.timetuple()) event_data['event'] = name event_data['repeat'] = repeat event_data['data'] = data self.emitter.emit( Message('mycroft.scheduler.schedule_event', data=event_data)) def schedule_event(self, handler, when, data=None, name=None): """ Schedule a single event. Args: handler: method to be called when (datetime): when the handler should be called (local time) data (dict, optional): data to send when the handler is called name (str, optional): friendly name parameter """ data = data or {} self._schedule_event(handler, when, data, name) def schedule_repeating_event(self, handler, when, frequency, data=None, name=None): """ Schedule a repeating event. Args: handler: method to be called when (datetime): time for calling the handler or None to initially trigger <frequency> seconds from now frequency (float/int): time in seconds between calls data (dict, optional): data to send along to the handler name (str, optional): friendly name parameter """ # Do not schedule if this event is already scheduled by the skill if name not in self.scheduled_repeats: data = data or {} if not when: when = datetime.now() + timedelta(seconds=frequency) self._schedule_event(handler, when, data, name, frequency) else: LOG.debug('The event is already scheduled, cancel previous ' 'event if this scheduling should replace the last.') def update_scheduled_event(self, name, data=None): """ Change data of event. Args: name (str): Name of event """ data = data or {} data = {'event': self._unique_name(name), 'data': data} self.emitter.emit(Message('mycroft.schedule.update_event', data=data)) def cancel_scheduled_event(self, name): """ Cancel a pending event. The event will no longer be scheduled to be executed Args: name (str): Name of event """ unique_name = self._unique_name(name) data = {'event': unique_name} if name in self.scheduled_repeats: self.scheduled_repeats.remove(name) if self.remove_event(unique_name): self.emitter.emit( Message('mycroft.scheduler.remove_event', data=data)) def get_scheduled_event_status(self, name): """ Get scheduled event data and return the amount of time left Args: name (str): Name of event Return: int: the time left in seconds """ event_name = self._unique_name(name) data = {'name': event_name} # making event_status an object so it's refrence can be changed event_status = [None] finished_callback = [False] def callback(message): if message.data is not None: event_time = int(message.data[0][0]) current_time = int(time.time()) time_left_in_seconds = event_time - current_time event_status[0] = time_left_in_seconds finished_callback[0] = True emitter_name = 'mycroft.event_status.callback.{}'.format(event_name) self.emitter.once(emitter_name, callback) self.emitter.emit(Message('mycroft.scheduler.get_event', data=data)) start_wait = time.time() while finished_callback[0] is False and time.time() - start_wait < 3.0: time.sleep(0.1) if time.time() - start_wait > 3.0: raise Exception("Event Status Messagebus Timeout") return event_status[0] def cancel_all_repeating_events(self): """ Cancel any repeating events started by the skill. """ for e in self.scheduled_repeats: self.cancel_scheduled_event(e)
class WiFi: def __init__(self): self.iface = pyw.winterfaces()[0] self.ap = AccessPoint(self.iface) self.server = None self.ws = WebsocketClient() self.enclosure = EnclosureAPI(self.ws) self.init_events() self.conn_monitor = None self.conn_monitor_stop = threading.Event() self.starting = False def init_events(self): ''' Register handlers for various websocket events used to communicate with outside systems. ''' # This event is generated by an outside mechanism. On a # Mark 1 unit this comes from the Enclosure's WIFI menu # item being selected. self.ws.on('mycroft.wifi.start', self.start) # Similar to the above. Resets to factory defaults self.ws.on('mycroft.wifi.reset', self.reset) # Similar to the above. Enable/disable SSH self.ws.on('mycroft.enable.ssh', self.ssh_enable) self.ws.on('mycroft.disable.ssh', self.ssh_disable) # These events are generated by Javascript in the captive # portal. self.ws.on('mycroft.wifi.stop', self.stop) self.ws.on('mycroft.wifi.scan', self.scan) self.ws.on('mycroft.wifi.connect', self.connect) def start(self, event=None): ''' Fire up the MYCROFT access point for the user to connect to with a phone or computer. ''' if self.starting: return self.starting = True LOG.info("Starting access point...") self.intro_msg = "" if event and event.data.get("msg"): self.intro_msg = event.data.get("msg") self.allow_timeout = True if event and event.data.get("allow_timeout"): self.allow_timeout = event.data.get("allow_timeout") # Fire up our access point self.ap.up() if not self.server: LOG.info("Creating web server...") self.server = WebServer(self.ap.ip, 80) LOG.info("Starting web server...") self.server.start() LOG.info("Created web server.") LOG.info("Access point started!\n%s" % self.ap.__dict__) self._start_connection_monitor() def _connection_prompt(self, intro): while self.ap.password is None or self.ap.password == "": sleep(1) # give it time to load # Speak the connection instructions and show the password on screen passwordSpelled = ", ".join(self.ap.password) self._speak_and_show(intro + " Use your mobile device or computer to connect " "to the wifi network 'MYCROFT'. Then enter the " "password " + passwordSpelled, self.ap.password) def _speak_and_show(self, speak, show): ''' Communicate with the user throughout the process ''' self.ws.emit(Message("speak", {'utterance': speak})) if show is None: return wait_while_speaking() self.enclosure.mouth_text(show) def _start_connection_monitor(self): LOG.info("Starting monitor thread...\n") if self.conn_monitor is not None: LOG.info("Killing old thread...\n") self.conn_monitor_stop.set() self.conn_monitor_stop.wait() self.conn_monitor = threading.Thread( target=self._do_connection_monitor, args={}) self.conn_monitor.daemon = True self.conn_monitor.start() LOG.info("Monitor thread setup complete.\n") def _stop_connection_monitor(self): ''' Set flag that will let monitoring thread close ''' self.conn_monitor_stop.set() def _do_connection_monitor(self): LOG.info("Invoked monitor thread...\n") mtimeLast = os.path.getmtime('/var/lib/misc/dnsmasq.leases') bHasConnected = False cARPFailures = 0 timeStarted = time.time() timeLastAnnounced = timeStarted - 45 # first reminder in 90 secs self.conn_monitor_stop.clear() while not self.conn_monitor_stop.isSet(): # do our monitoring... mtime = os.path.getmtime('/var/lib/misc/dnsmasq.leases') if mtimeLast != mtime: # Something changed in the dnsmasq lease file - # presumably a (re)new lease bHasConnected = True cARPFailures = 0 mtimeLast = mtime timeStarted = time.time() # reset start time after connection timeLastAnnounced = time.time() - 45 # announce how to connect if time.time() - timeStarted > 60 * 5 and self.allow_timeout: # After 5 minutes, shut down the access point (unless the # system has never been setup, in which case we stay up # indefinitely) LOG.info("Auto-shutdown of access point after 5 minutes") self.stop() continue if time.time() - timeLastAnnounced >= 45: if bHasConnected: self._speak_and_show( "Follow the prompt on your mobile device or computer " "and choose a wifi network. If you don't get a " "prompt, open your browser and go to start dot " "mycroft dot A I.", "start.mycroft.ai") else: if self.intro_msg: self._connection_prompt(self.intro_msg) self.intro_msg = None # only speak the intro once else: self._connection_prompt("Allow me to walk you through " "the wifi setup process.") timeLastAnnounced = time.time() if bHasConnected: # Flush the ARP entries associated with our access point # This will require all network hardware to re-register # with the ARP tables if still present. if cARPFailures == 0: res = cli_no_output('ip', '-s', '-s', 'neigh', 'flush', self.ap.subnet + '.0/24') # Give ARP system time to re-register hardware sleep(5) # now look at the hardware that has responded, if no entry # shows up on our access point after 2*5=10 seconds, the user # has disconnected if not self._is_ARP_filled(): cARPFailures += 1 if cARPFailures > 2: self._connection_prompt("Connection lost.") bHasConnected = False else: cARPFailures = 0 sleep(5) # wait a bit to prevent thread from hogging CPU LOG.info("Exiting monitor thread...\n") self.conn_monitor_stop.clear() def _is_ARP_filled(self): res = cli_no_output('/usr/sbin/arp', '-n') out = str(res.get("stdout")) if out: # Parse output, skipping header for o in out.split("\n")[1:]: if o[0:len(self.ap.subnet)] == self.ap.subnet: if "(incomplete)" in o: # ping the IP to get the ARP table entry reloaded ip_disconnected = o.split(" ")[0] cli_no_output('/bin/ping', '-c', '1', '-W', '3', ip_disconnected) else: return True # something on subnet is connected! return False def scan(self, event=None): LOG.info("Scanning wifi connections...") networks = {} status = self.get_status() for cell in Cell.all(self.iface): if "x00" in cell.ssid: continue # ignore hidden networks update = True ssid = cell.ssid quality = self.get_quality(cell.quality) # If there are duplicate network IDs (e.g. repeaters) only # report the strongest signal if networks.__contains__(ssid): update = networks.get(ssid).get("quality") < quality if update and ssid: networks[ssid] = { 'quality': quality, 'encrypted': cell.encrypted, 'connected': self.is_connected(ssid, status) } self.ws.emit(Message("mycroft.wifi.scanned", {'networks': networks})) LOG.info("Wifi connections scanned!\n%s" % networks) @staticmethod def get_quality(quality): values = quality.split("/") return float(values[0]) / float(values[1]) def connect(self, event=None): if event and event.data: ssid = event.data.get("ssid") connected = self.is_connected(ssid) if connected: LOG.warn("Mycroft is already connected to %s" % ssid) else: self.disconnect() LOG.info("Connecting to: %s" % ssid) nid = wpa(self.iface, 'add_network') wpa(self.iface, 'set_network', nid, 'ssid', '"' + ssid + '"') if event.data.__contains__("pass"): psk = '"' + event.data.get("pass") + '"' wpa(self.iface, 'set_network', nid, 'psk', psk) else: wpa(self.iface, 'set_network', nid, 'key_mgmt', 'NONE') wpa(self.iface, 'enable', nid) connected = self.get_connected(ssid) if connected: wpa(self.iface, 'save_config') self.ws.emit(Message("mycroft.wifi.connected", {'connected': connected})) LOG.info("Connection status for %s = %s" % (ssid, connected)) def disconnect(self): status = self.get_status() nid = status.get("id") if nid: ssid = status.get("ssid") wpa(self.iface, 'disable', nid) LOG.info("Disconnecting %s id: %s" % (ssid, nid)) def get_status(self): res = cli('wpa_cli', '-i', self.iface, 'status') out = str(res.get("stdout")) if out: return dict(o.split("=") for o in out.split("\n")[:-1]) return {} def get_connected(self, ssid, retry=5): connected = self.is_connected(ssid) while not connected and retry > 0: sleep(2) retry -= 1 connected = self.is_connected(ssid) return connected def is_connected(self, ssid, status=None): status = status or self.get_status() state = status.get("wpa_state") return status.get("ssid") == ssid and state == "COMPLETED" def stop(self, event=None): LOG.info("Stopping access point...") if is_speaking(): stop_speaking() # stop any assistance being spoken self._stop_connection_monitor() self.ap.down() self.enclosure.mouth_reset() # remove "start.mycroft.ai" self.starting = False if self.server: self.server.server.shutdown() self.server.server.server_close() self.server.join() self.server = None LOG.info("Access point stopped!") def run(self): try: self.ws.run_forever() except Exception as e: LOG.error("Error: {0}".format(e)) self.stop() def reset(self, event=None): """Reset the unit to the factory defaults """ LOG.info("Resetting the WPA_SUPPLICANT File") try: call( "echo '" + WPA_SUPPLICANT + "'> /etc/wpa_supplicant/wpa_supplicant.conf", shell=True) # UGLY BUT WORKS call(RM_SKILLS) except Exception as e: LOG.error("Error: {0}".format(e)) def ssh_enable(self, event=None): LOG.info("Enabling SSH") try: call('systemctl enable ssh.service', shell=True) call('systemctl start ssh.service', shell=True) except Exception as e: LOG.error("Error: {0}".format(e)) def ssh_disable(self, event=None): LOG.info("Disabling SSH") try: call('systemctl stop ssh.service', shell=True) call('systemctl disable ssh.service', shell=True) except Exception as e: LOG.error("Error: {0}".format(e))
class WiFi: def __init__(self): self.iface = pyw.winterfaces()[0] self.ap = AccessPoint(self.iface) self.server = None self.ws = WebsocketClient() ConfigurationManager.init(self.ws) self.enclosure = EnclosureAPI(self.ws) self.init_events() self.conn_monitor = None self.conn_monitor_stop = threading.Event() def init_events(self): ''' Register handlers for various websocket events used to communicate with outside systems. ''' # This event is generated by an outside mechanism. On a # Holmes unit this comes from the Enclosure's menu item # being selected. self.ws.on('mycroft.wifi.start', self.start) # These events are generated by Javascript in the captive # portal. self.ws.on('mycroft.wifi.stop', self.stop) self.ws.on('mycroft.wifi.scan', self.scan) self.ws.on('mycroft.wifi.connect', self.connect) def start(self, event=None): ''' Fire up the MYCROFT access point for the user to connect to with a phone or computer. ''' LOG.info("Starting access point...") # Fire up our access point self.ap.up() if not self.server: LOG.info("Creating web server...") self.server = WebServer(self.ap.ip, 80) LOG.info("Starting web server...") self.server.start() LOG.info("Created web server.") LOG.info("Access point started!\n%s" % self.ap.__dict__) self._start_connection_monitor() def _connection_prompt(self, prefix): # let the user know to connect to it... passwordSpelled = ", ".join(self.ap.password) self._speak_and_show( prefix + " Use your mobile device or computer to " "connect to the wifi network " "'MYCROFT'; Then enter the uppercase " "password " + passwordSpelled, self.ap.password) def _speak_and_show(self, speak, show): ''' Communicate with the user throughout the process ''' self.ws.emit(Message("speak", {'utterance': speak})) if show is None: return # TODO: This sleep should not be necessary, but without it the # text to be displayed by enclosure.mouth_text() gets # wiped out immediately when the utterance above is # begins processing. # Remove the sleep once this behavior is corrected. sleep(0.25) self.enclosure.mouth_text(show) def _start_connection_monitor(self): LOG.info("Starting monitor thread...\n") if self.conn_monitor is not None: LOG.info("Killing old thread...\n") self.conn_monitor_stop.set() self.conn_monitor_stop.wait() self.conn_monitor = threading.Thread( target=self._do_connection_monitor, args={}) self.conn_monitor.daemon = True self.conn_monitor.start() LOG.info("Monitor thread setup complete.\n") def _stop_connection_monitor(self): ''' Set flag that will let monitoring thread close ''' self.conn_monitor_stop.set() def _do_connection_monitor(self): LOG.info("Invoked monitor thread...\n") mtimeLast = os.path.getmtime('/var/lib/misc/dnsmasq.leases') bHasConnected = False cARPFailures = 0 timeStarted = time.time() timeLastAnnounced = 0 # force first announcement to now self.conn_monitor_stop.clear() while not self.conn_monitor_stop.isSet(): # do our monitoring... mtime = os.path.getmtime('/var/lib/misc/dnsmasq.leases') if mtimeLast != mtime: # Something changed in the dnsmasq lease file - # presumably a (re)new lease bHasConnected = True cARPFailures = 0 mtimeLast = mtime timeStarted = time.time() # reset start time after connection timeLastAnnounced = time.time() - 45 # announce how to connect if time.time() - timeStarted > 60 * 5: # After 5 minutes, shut down the access point LOG.info("Auto-shutdown of access point after 5 minutes") self.stop() continue if time.time() - timeLastAnnounced >= 45: if bHasConnected: self._speak_and_show( "Now you can open your browser and go to start dot " "mycroft dot A I, then follow the instructions given " " there", "start.mycroft.ai") else: self._connection_prompt("Allow me to walk you through the " " wifi setup process; ") timeLastAnnounced = time.time() if bHasConnected: # Flush the ARP entries associated with our access point # This will require all network hardware to re-register # with the ARP tables if still present. if cARPFailures == 0: res = cli_no_output('ip', '-s', '-s', 'neigh', 'flush', self.ap.subnet + '.0/24') # Give ARP system time to re-register hardware sleep(5) # now look at the hardware that has responded, if no entry # shows up on our access point after 2*5=10 seconds, the user # has disconnected if not self._is_ARP_filled(): cARPFailures += 1 if cARPFailures > 2: self._connection_prompt("Connection lost,") bHasConnected = False else: cARPFailures = 0 sleep(5) # wait a bit to prevent thread from hogging CPU LOG.info("Exiting monitor thread...\n") self.conn_monitor_stop.clear() def _is_ARP_filled(self): res = cli_no_output('/usr/sbin/arp', '-n') out = str(res.get("stdout")) if out: # Parse output, skipping header for o in out.split("\n")[1:]: if o[0:len(self.ap.subnet)] == self.ap.subnet: if "(incomplete)" in o: # ping the IP to get the ARP table entry reloaded ip_disconnected = o.split(" ")[0] cli_no_output('/bin/ping', '-c', '1', '-W', '3', ip_disconnected) else: return True # something on subnet is connected! return False def scan(self, event=None): LOG.info("Scanning wifi connections...") networks = {} status = self.get_status() for cell in Cell.all(self.iface): update = True ssid = cell.ssid quality = self.get_quality(cell.quality) # If there are duplicate network IDs (e.g. repeaters) only # report the strongest signal if networks.__contains__(ssid): update = networks.get(ssid).get("quality") < quality if update and ssid: networks[ssid] = { 'quality': quality, 'encrypted': cell.encrypted, 'connected': self.is_connected(ssid, status) } self.ws.emit(Message("mycroft.wifi.scanned", {'networks': networks})) LOG.info("Wifi connections scanned!\n%s" % networks) @staticmethod def get_quality(quality): values = quality.split("/") return float(values[0]) / float(values[1]) def connect(self, event=None): if event and event.data: ssid = event.data.get("ssid") connected = self.is_connected(ssid) if connected: LOG.warn("Mycroft is already connected to %s" % ssid) else: self.disconnect() LOG.info("Connecting to: %s" % ssid) nid = wpa(self.iface, 'add_network') wpa(self.iface, 'set_network', nid, 'ssid', '"' + ssid + '"') if event.data.__contains__("pass"): psk = '"' + event.data.get("pass") + '"' wpa(self.iface, 'set_network', nid, 'psk', psk) else: wpa(self.iface, 'set_network', nid, 'key_mgmt', 'NONE') wpa(self.iface, 'enable', nid) connected = self.get_connected(ssid) if connected: wpa(self.iface, 'save_config') self.ws.emit(Message("mycroft.wifi.connected", {'connected': connected})) LOG.info("Connection status for %s = %s" % (ssid, connected)) if connected: self.ws.emit(Message("speak", { 'utterance': "Thank you, I'm now connected to the " "internet and ready for use"})) # TODO: emit something that triggers a pairing check def disconnect(self): status = self.get_status() nid = status.get("id") if nid: ssid = status.get("ssid") wpa(self.iface, 'disable', nid) LOG.info("Disconnecting %s id: %s" % (ssid, nid)) def get_status(self): res = cli('wpa_cli', '-i', self.iface, 'status') out = str(res.get("stdout")) if out: return dict(o.split("=") for o in out.split("\n")[:-1]) return {} def get_connected(self, ssid, retry=5): connected = self.is_connected(ssid) while not connected and retry > 0: sleep(2) retry -= 1 connected = self.is_connected(ssid) return connected def is_connected(self, ssid, status=None): status = status or self.get_status() state = status.get("wpa_state") return status.get("ssid") == ssid and state == "COMPLETED" def stop(self, event=None): LOG.info("Stopping access point...") self._stop_connection_monitor() self.ap.down() if self.server: self.server.server.shutdown() self.server.server.server_close() self.server.join() self.server = None LOG.info("Access point stopped!") def _do_net_check(self): # give system 5 seconds to resolve network or get plugged in sleep(5) LOG.info("Checking internet connection again") if not connected() and self.conn_monitor is None: # TODO: Enclosure/localization self._speak_and_show( "This device is not connected to the Internet. Either plug " "in a network cable or hold the button on top for two " "seconds, then select wifi from the menu", None) def run(self): try: # When the system first boots up, check for a valid internet # connection. LOG.info("Checking internet connection") if not connected(): LOG.info("No connection initially, waiting 20...") self.net_check = threading.Thread( target=self._do_net_check, args={}) self.net_check.daemon = True self.net_check.start() else: LOG.info("Connection found!") self.ws.run_forever() except Exception as e: LOG.error("Error: {0}".format(e)) self.stop()
class MycroftSkill(object): """ Abstract base class which provides common behaviour and parameters to all Skills implementation. """ def __init__(self, name=None, emitter=None): self.name = name or self.__class__.__name__ # Get directory of skill self._dir = dirname(abspath(sys.modules[self.__module__].__file__)) self.bind(emitter) self.config_core = Configuration.get() self.config = self.config_core.get(self.name) self.dialog_renderer = None self.vocab_dir = None self.root_dir = None self.file_system = FileSystemAccess(join('skills', self.name)) self.registered_intents = [] self.log = LOG.create_logger(self.name) self.reload_skill = True self.events = [] self.skill_id = 0 @property def location(self): """ Get the JSON data struction holding location information. """ # TODO: Allow Enclosure to override this for devices that # contain a GPS. return self.config_core.get('location') @property def location_pretty(self): """ Get a more 'human' version of the location as a string. """ loc = self.location if type(loc) is dict and loc["city"]: return loc["city"]["name"] return None @property def location_timezone(self): """ Get the timezone code, such as 'America/Los_Angeles' """ loc = self.location if type(loc) is dict and loc["timezone"]: return loc["timezone"]["code"] return None @property def lang(self): return self.config_core.get('lang') @property def settings(self): """ Load settings if not already loaded. """ try: return self._settings except: self._settings = SkillSettings(self._dir, self.name) return self._settings def bind(self, emitter): """ Register emitter with skill. """ if emitter: self.emitter = emitter self.enclosure = EnclosureAPI(emitter, self.name) self.__register_stop() def __register_stop(self): self.stop_time = time.time() self.stop_threshold = self.config_core.get("skills").get( 'stop_threshold') self.add_event('mycroft.stop', self.__handle_stop, False) def detach(self): for (name, intent) in self.registered_intents: name = str(self.skill_id) + ':' + name self.emitter.emit(Message("detach_intent", {"intent_name": name})) def initialize(self): """ Initialization function to be implemented by all Skills. Usually used to create intents rules and register them. """ LOG.debug("No initialize function implemented") def get_intro_message(self): """ Get a message to speak on first load of the skill. Useful for post-install setup instructions. Returns: str: message that will be spoken to the user """ return None def converse(self, utterances, lang="en-us"): """ Handle conversation. This method can be used to override the normal intent handler after the skill has been invoked once. To enable this override thise converse method and return True to indicate that the utterance has been handled. Args: utterances: The utterances from the user lang: language the utterance is in Returns: True if an utterance was handled, otherwise False """ return False def report_metric(self, name, data): """ Report a skill metric to the Mycroft servers Args: name (str): Name of metric data (dict): JSON dictionary to report. Must be valid JSON """ report_metric(basename(self.root_dir) + '/' + name, data) def send_email(self, title, body): """ Send an email to the registered user's email Args: title (str): Title of email body (str): HTML body of email. This supports simple HTML like bold and italics """ DeviceApi().send_email(title, body, basename(self.root_dir)) def make_active(self): """ Bump skill to active_skill list in intent_service this enables converse method to be called even without skill being used in last 5 minutes """ self.emitter.emit(Message('active_skill_request', {"skill_id": self.skill_id})) def _register_decorated(self): """ Register all intent handlers that have been decorated with an intent. """ global _intent_list, _intent_file_list for intent_parser, handler in _intent_list: self.register_intent(intent_parser, handler, need_self=True) for intent_file, handler in _intent_file_list: self.register_intent_file(intent_file, handler, need_self=True) _intent_list = [] _intent_file_list = [] def add_event(self, name, handler, need_self=False): """ Create event handler for executing intent Args: name: IntentParser name handler: method to call need_self: optional parameter, when called from a decorated intent handler the function will need the self variable passed as well. """ def wrapper(message): try: # Indicate that the skill handler is starting name = get_handler_name(handler) self.emitter.emit(Message("mycroft.skill.handler.start", data={'handler': name})) if need_self: # When registring from decorator self is required if len(getargspec(handler).args) == 2: handler(self, message) elif len(getargspec(handler).args) == 1: handler(self) elif len(getargspec(handler).args) == 0: # Zero may indicate multiple decorators, trying the # usual call signatures try: handler(self, message) except TypeError: handler(self) else: raise TypeError else: if len(getargspec(handler).args) == 2: handler(message) elif len(getargspec(handler).args) == 1: handler() else: raise TypeError self.settings.store() # Store settings if they've changed except Exception as e: # TODO: Localize self.speak( "An error occurred while processing a request in " + self.name) LOG.error( "An error occurred while processing a request in " + self.name, exc_info=True) # indicate completion with exception self.emitter.emit(Message('mycroft.skill.handler.complete', data={'handler': name, 'exception': e.message})) # Indicate that the skill handler has completed self.emitter.emit(Message('mycroft.skill.handler.complete', data={'handler': name})) if handler: self.emitter.on(name, wrapper) self.events.append((name, wrapper)) def remove_event(self, name): """ Removes an event from emitter and events list Args: name: Name of Intent or Scheduler Event """ for _name, _handler in self.events: if name == _name: self.events.remove((_name, _handler)) self.emitter.remove(_name, _handler) def register_intent(self, intent_parser, handler, need_self=False): """ Register an Intent with the intent service. Args: intent_parser: Intent or IntentBuilder object to parse utterance for the handler. handler: function to register with intent need_self: optional parameter, when called from a decorated intent handler the function will need the self variable passed as well. """ if type(intent_parser) == IntentBuilder: intent_parser = intent_parser.build() elif type(intent_parser) != Intent: raise ValueError('intent_parser is not an Intent') name = intent_parser.name intent_parser.name = str(self.skill_id) + ':' + intent_parser.name self.emitter.emit(Message("register_intent", intent_parser.__dict__)) self.registered_intents.append((name, intent_parser)) self.add_event(intent_parser.name, handler, need_self) def register_intent_file(self, intent_file, handler, need_self=False): """ Register an Intent file with the intent service. For example: === food.order.intent === Order some {food}. Order some {food} from {place}. I'm hungry. Grab some {food} from {place}. Optionally, you can also use <register_entity_file> to specify some examples of {food} and {place} In addition, instead of writing out multiple variations of the same sentence you can write: === food.order.intent === (Order | Grab) some {food} (from {place} | ). I'm hungry. Args: intent_file: name of file that contains example queries that should activate the intent handler: function to register with intent need_self: use for decorator. See <register_intent> """ name = str(self.skill_id) + ':' + intent_file self.emitter.emit(Message("padatious:register_intent", { "file_name": join(self.vocab_dir, intent_file), "name": name })) self.add_event(name, handler, need_self) def register_entity_file(self, entity_file): """ Register an Entity file with the intent service. And Entity file lists the exact values that an entity can hold. For example: === ask.day.intent === Is it {weekday}? === weekday.entity === Monday Tuesday ... Args: entity_file: name of file that contains examples of an entity. Must end with .entity """ if '.entity' not in entity_file: raise ValueError('Invalid entity filename: ' + entity_file) name = str(self.skill_id) + ':' + entity_file.replace('.entity', '') self.emitter.emit(Message("padatious:register_entity", { "file_name": join(self.vocab_dir, entity_file), "name": name })) def disable_intent(self, intent_name): """Disable a registered intent""" LOG.debug('Disabling intent ' + intent_name) name = str(self.skill_id) + ':' + intent_name self.emitter.emit(Message("detach_intent", {"intent_name": name})) def enable_intent(self, intent_name): """Reenable a registered intent""" for (name, intent) in self.registered_intents: if name == intent_name: self.registered_intents.remove((name, intent)) intent.name = name self.register_intent(intent, None) LOG.debug('Enabling intent ' + intent_name) break else: LOG.error('Could not enable ' + intent_name + ', it hasn\'t been registered.') def set_context(self, context, word=''): """ Add context to intent service Args: context: Keyword word: word connected to keyword """ if not isinstance(context, basestring): raise ValueError('context should be a string') if not isinstance(word, basestring): raise ValueError('word should be a string') self.emitter.emit(Message('add_context', {'context': context, 'word': word})) def remove_context(self, context): """ remove_context removes a keyword from from the context manager. """ if not isinstance(context, basestring): raise ValueError('context should be a string') self.emitter.emit(Message('remove_context', {'context': context})) def register_vocabulary(self, entity, entity_type): """ Register a word to an keyword Args: entity: word to register entity_type: Intent handler entity to tie the word to """ self.emitter.emit(Message('register_vocab', { 'start': entity, 'end': entity_type })) def register_regex(self, regex_str): re.compile(regex_str) # validate regex self.emitter.emit(Message('register_vocab', {'regex': regex_str})) def speak(self, utterance, expect_response=False): """ Speak a sentence. Args: utterance: sentence mycroft should speak expect_response: set to True if Mycroft should expect a response from the user and start listening for response. """ # registers the skill as being active self.enclosure.register(self.name) data = {'utterance': utterance, 'expect_response': expect_response} self.emitter.emit(Message("speak", data)) def speak_dialog(self, key, data=None, expect_response=False): """ Speak sentance based of dialog file. Args key: dialog file key (filname without extension) data: information to populate sentence with expect_response: set to True if Mycroft should expect a response from the user and start listening for response. """ data = data or {} self.speak(self.dialog_renderer.render(key, data), expect_response) def init_dialog(self, root_directory): dialog_dir = join(root_directory, 'dialog', self.lang) if exists(dialog_dir): self.dialog_renderer = DialogLoader().load(dialog_dir) else: LOG.debug('No dialog loaded, ' + dialog_dir + ' does not exist') def load_data_files(self, root_directory): self.init_dialog(root_directory) self.load_vocab_files(join(root_directory, 'vocab', self.lang)) regex_path = join(root_directory, 'regex', self.lang) self.root_dir = root_directory if exists(regex_path): self.load_regex_files(regex_path) def load_vocab_files(self, vocab_dir): self.vocab_dir = vocab_dir if exists(vocab_dir): load_vocabulary(vocab_dir, self.emitter) else: LOG.debug('No vocab loaded, ' + vocab_dir + ' does not exist') def load_regex_files(self, regex_dir): load_regex(regex_dir, self.emitter) def __handle_stop(self, event): """ Handler for the "mycroft.stop" signal. Runs the user defined `stop()` method. """ self.stop_time = time.time() try: self.stop() except: LOG.error("Failed to stop skill: {}".format(self.name), exc_info=True) @abc.abstractmethod def stop(self): pass def is_stop(self): passed_time = time.time() - self.stop_time return passed_time < self.stop_threshold def shutdown(self): """ This method is intended to be called during the skill process termination. The skill implementation must shutdown all processes and operations in execution. """ # Store settings self.settings.store() self.settings.is_alive = False # removing events for e, f in self.events: self.emitter.remove(e, f) self.events = None # Remove reference to wrappers self.emitter.emit( Message("detach_skill", {"skill_id": str(self.skill_id) + ":"})) try: self.stop() except: LOG.error("Failed to stop skill: {}".format(self.name), exc_info=True) def _unique_name(self, name): """ Return a name unique to this skill using the format [skill_id]:[name]. Args: name: Name to use internally Returns: str: name unique to this skill """ return str(self.skill_id) + ':' + name def _schedule_event(self, handler, when, data=None, name=None, repeat=None): """ Underlying method for schedle_event and schedule_repeating_event. Takes scheduling information and sends it of on the message bus. """ data = data or {} if not name: name = self.name + handler.__name__ name = self._unique_name(name) self.add_event(name, handler, False) event_data = {} event_data['time'] = time.mktime(when.timetuple()) event_data['event'] = name event_data['repeat'] = repeat event_data['data'] = data self.emitter.emit(Message('mycroft.scheduler.schedule_event', data=event_data)) def schedule_event(self, handler, when, data=None, name=None): """ Schedule a single event. Args: handler: method to be called when (datetime): when the handler should be called data (dict, optional): data to send when the handler is called name (str, optional): friendly name parameter """ data = data or {} self._schedule_event(handler, when, data, name) def schedule_repeating_event(self, handler, when, frequency, data=None, name=None): """ Schedule a repeating event. Args: handler: method to be called when (datetime): time for calling the handler frequency (float/int): time in seconds between calls data (dict, optional): data to send along to the handler name (str, optional): friendly name parameter """ data = data or {} self._schedule_event(handler, when, data, name, frequency) def update_scheduled_event(self, name, data=None): """ Change data of event. Args: name (str): Name of event """ data = data or {} data = { 'event': self._unique_name(name), 'data': data } self.emitter.emit(Message('mycroft.schedule.update_event', data=data)) def cancel_scheduled_event(self, name): """ Cancel a pending event. The event will no longer be scheduled to be executed Args: name (str): Name of event """ unique_name = self._unique_name(name) data = {'event': unique_name} self.remove_event(unique_name) self.emitter.emit(Message('mycroft.scheduler.remove_event', data=data)) def get_scheduled_event_status(self, name): """ Get scheduled event data and return the amount of time left Args: name (str): Name of event Return: int: the time left in seconds """ event_name = self._unique_name(name) data = {'name': event_name} # making event_status an object so it's refrence can be changed event_status = [None] finished_callback = [False] def callback(message): if message.data is not None: event_time = int(message.data[0][0]) current_time = int(time.time()) time_left_in_seconds = event_time - current_time event_status[0] = time_left_in_seconds finished_callback[0] = True emitter_name = 'mycroft.event_status.callback.{}'.format(event_name) self.emitter.once(emitter_name, callback) self.emitter.emit(Message('mycroft.scheduler.get_event', data=data)) start_wait = time.time() while finished_callback[0] is False and time.time() - start_wait < 3.0: time.sleep(0.1) if time.time() - start_wait > 3.0: raise Exception("Event Status Messagebus Timeout") return event_status[0]
def handle_open(): # TODO: Move this into the Enclosure (not speech client) # Reset the UI to indicate ready for speech processing EnclosureAPI(ws).reset()
class MycroftSkill(object): """ Abstract base class which provides common behaviour and parameters to all Skills implementation. """ def __init__(self, name=None, emitter=None): self.name = name or self.__class__.__name__ # Get directory of skill self._dir = dirname(abspath(sys.modules[self.__module__].__file__)) self.bind(emitter) self.config_core = ConfigurationManager.get() self.config = self.config_core.get(self.name) self.dialog_renderer = None self.vocab_dir = None self.file_system = FileSystemAccess(join('skills', self.name)) self.registered_intents = [] self.log = LOG.create_logger(self.name) self.reload_skill = True self.events = [] self.skill_id = 0 @property def location(self): """ Get the JSON data struction holding location information. """ # TODO: Allow Enclosure to override this for devices that # contain a GPS. return self.config_core.get('location') @property def location_pretty(self): """ Get a more 'human' version of the location as a string. """ loc = self.location if type(loc) is dict and loc["city"]: return loc["city"]["name"] return None @property def location_timezone(self): """ Get the timezone code, such as 'America/Los_Angeles' """ loc = self.location if type(loc) is dict and loc["timezone"]: return loc["timezone"]["code"] return None @property def lang(self): return self.config_core.get('lang') @property def settings(self): """ Load settings if not already loaded. """ try: return self._settings except: self._settings = SkillSettings(self._dir) return self._settings def bind(self, emitter): """ Register emitter with skill. """ if emitter: self.emitter = emitter self.enclosure = EnclosureAPI(emitter, self.name) self.__register_stop() def __register_stop(self): self.stop_time = time.time() self.stop_threshold = self.config_core.get("skills").get( 'stop_threshold') self.add_event('mycroft.stop', self.__handle_stop, False) def detach(self): for (name, intent) in self.registered_intents: name = str(self.skill_id) + ':' + name self.emitter.emit(Message("detach_intent", {"intent_name": name})) def initialize(self): """ Initialization function to be implemented by all Skills. Usually used to create intents rules and register them. """ LOG.debug("No initialize function implemented") def converse(self, utterances, lang="en-us"): """ Handle conversation. This method can be used to override the normal intent handler after the skill has been invoked once. To enable this override thise converse method and return True to indicate that the utterance has been handled. Args: utterances: The utterances from the user lang: language the utterance is in Returns: True if an utterance was handled, otherwise False """ return False def make_active(self): """ Bump skill to active_skill list in intent_service this enables converse method to be called even without skill being used in last 5 minutes """ self.emitter.emit( Message('active_skill_request', {"skill_id": self.skill_id})) def _register_decorated(self): """ Register all intent handlers that has been decorated with an intent. """ global _intent_list, _intent_file_list for intent_parser, handler in _intent_list: self.register_intent(intent_parser, handler, need_self=True) for intent_file, handler in _intent_file_list: self.register_intent_file(intent_file, handler, need_self=True) _intent_list = [] _intent_file_list = [] def add_event(self, name, handler, need_self=False): """ Create event handler for executing intent Args: name: IntentParser name handler: method to call need_self: optional parameter, when called from a decorated intent handler the function will need the self variable passed as well. """ def wrapper(message): try: # Indicate that the skill handler is starting name = get_handler_name(handler) self.emitter.emit( Message("mycroft.skill.handler.start", data={'handler': name})) if need_self: # When registring from decorator self is required if len(getargspec(handler).args) == 2: handler(self, message) elif len(getargspec(handler).args) == 1: handler(self) else: raise TypeError else: if len(getargspec(handler).args) == 2: handler(message) elif len(getargspec(handler).args) == 1: handler() else: raise TypeError self.settings.store() # Store settings if they've changed except Exception as e: # TODO: Localize self.speak("An error occurred while processing a request in " + self.name) LOG.error("An error occurred while processing a request in " + self.name, exc_info=True) # indicate completion with exception self.emitter.emit( Message('mycroft.skill.handler.complete', data={ 'handler': name, 'exception': e.message })) # Indicate that the skill handler has completed self.emitter.emit( Message('mycroft.skill.handler.complete', data={'handler': name})) if handler: self.emitter.on(name, wrapper) self.events.append((name, wrapper)) def register_intent(self, intent_parser, handler, need_self=False): """ Register an Intent with the intent service. Args: intent_parser: Intent or IntentBuilder object to parse utterance for the handler. handler: function to register with intent need_self: optional parameter, when called from a decorated intent handler the function will need the self variable passed as well. """ if type(intent_parser) == IntentBuilder: intent_parser = intent_parser.build() elif type(intent_parser) != Intent: raise ValueError('intent_parser is not an Intent') name = intent_parser.name intent_parser.name = str(self.skill_id) + ':' + intent_parser.name self.emitter.emit(Message("register_intent", intent_parser.__dict__)) self.registered_intents.append((name, intent_parser)) self.add_event(intent_parser.name, handler, need_self) def register_intent_file(self, intent_file, handler, need_self=False): """ Register an Intent file with the intent service. Args: intent_file: name of file that contains example queries that should activate the intent handler: function to register with intent need_self: use for decorator. See register_intent """ intent_name = str(self.skill_id) + ':' + intent_file self.emitter.emit( Message( "padatious:register_intent", { "file_name": join(self.vocab_dir, intent_file), "intent_name": intent_name })) self.add_event(intent_name, handler, need_self) def disable_intent(self, intent_name): """Disable a registered intent""" LOG.debug('Disabling intent ' + intent_name) name = str(self.skill_id) + ':' + intent_name self.emitter.emit(Message("detach_intent", {"intent_name": name})) def enable_intent(self, intent_name): """Reenable a registered intent""" for (name, intent) in self.registered_intents: if name == intent_name: self.registered_intents.remove((name, intent)) intent.name = name self.register_intent(intent, None) LOG.debug('Enabling intent ' + intent_name) break else: LOG.error('Could not enable ' + intent_name + ', it hasn\'t been registered.') def set_context(self, context, word=''): """ Add context to intent service Args: context: Keyword word: word connected to keyword """ if not isinstance(context, basestring): raise ValueError('context should be a string') if not isinstance(word, basestring): raise ValueError('word should be a string') self.emitter.emit( Message('add_context', { 'context': context, 'word': word })) def remove_context(self, context): """ remove_context removes a keyword from from the context manager. """ if not isinstance(context, basestring): raise ValueError('context should be a string') self.emitter.emit(Message('remove_context', {'context': context})) def register_vocabulary(self, entity, entity_type): """ Register a word to an keyword Args: entity: word to register entity_type: Intent handler entity to tie the word to """ self.emitter.emit( Message('register_vocab', { 'start': entity, 'end': entity_type })) def register_regex(self, regex_str): re.compile(regex_str) # validate regex self.emitter.emit(Message('register_vocab', {'regex': regex_str})) def speak(self, utterance, expect_response=False): """ Speak a sentence. Args: utterance: sentence mycroft should speak expect_response: set to True if Mycroft should expect a response from the user and start listening for response. """ # registers the skill as being active self.enclosure.register(self.name) data = {'utterance': utterance, 'expect_response': expect_response} self.emitter.emit(Message("speak", data)) def speak_dialog(self, key, data=None, expect_response=False): """ Speak sentance based of dialog file. Args key: dialog file key (filname without extension) data: information to populate sentence with expect_response: set to True if Mycroft should expect a response from the user and start listening for response. """ data = data or {} self.speak(self.dialog_renderer.render(key, data), expect_response) def init_dialog(self, root_directory): dialog_dir = join(root_directory, 'dialog', self.lang) if exists(dialog_dir): self.dialog_renderer = DialogLoader().load(dialog_dir) else: LOG.debug('No dialog loaded, ' + dialog_dir + ' does not exist') def load_data_files(self, root_directory): self.init_dialog(root_directory) self.load_vocab_files(join(root_directory, 'vocab', self.lang)) regex_path = join(root_directory, 'regex', self.lang) if exists(regex_path): self.load_regex_files(regex_path) def load_vocab_files(self, vocab_dir): self.vocab_dir = vocab_dir if exists(vocab_dir): load_vocabulary(vocab_dir, self.emitter) else: LOG.debug('No vocab loaded, ' + vocab_dir + ' does not exist') def load_regex_files(self, regex_dir): load_regex(regex_dir, self.emitter) def __handle_stop(self, event): """ Handler for the "mycroft.stop" signal. Runs the user defined `stop()` method. """ self.stop_time = time.time() try: self.stop() except: LOG.error("Failed to stop skill: {}".format(self.name), exc_info=True) @abc.abstractmethod def stop(self): pass def is_stop(self): passed_time = time.time() - self.stop_time return passed_time < self.stop_threshold def shutdown(self): """ This method is intended to be called during the skill process termination. The skill implementation must shutdown all processes and operations in execution. """ # Store settings self.settings.store() # removing events for e, f in self.events: self.emitter.remove(e, f) self.events = None # Remove reference to wrappers self.emitter.emit( Message("detach_skill", {"skill_id": str(self.skill_id) + ":"})) try: self.stop() except: LOG.error("Failed to stop skill: {}".format(self.name), exc_info=True) def _unique_name(self, name): """ Return a name unique to this skill using the format [skill_id]:[name]. Args: name: Name to use internally returns: name unique to this skill """ return str(self.skill_id) + ':' + name def _schedule_event(self, handler, when, data=None, name=None, repeat=None): """ Underlying method for schedle_event and schedule_repeating_event. Takes scheduling information and sends it of on the message bus. """ data = data or {} if not name: name = self.name + handler.__name__ name = self._unique_name(name) self.add_event(name, handler, False) event_data = {} event_data['time'] = time.mktime(when.timetuple()) event_data['event'] = name event_data['repeat'] = repeat event_data['data'] = data self.emitter.emit( Message('mycroft.scheduler.schedule_event', data=event_data)) def schedule_event(self, handler, when, data=None, name=None): """ Schedule a single event. Args: handler: method to be called when (datetime): when the handler should be called data (dict, optional): data to send when the handler is called name (str, optional): friendly name parameter """ data = data or {} self._schedule_event(handler, when, data, name) def schedule_repeating_event(self, handler, when, frequency, data=None, name=None): """ Schedule a repeating event. Args: handler: method to be called when (datetime): time for calling the handler frequency (float/int): time in seconds between calls data (dict, optional): data to send along to the handler name (str, optional): friendly name parameter """ data = data or {} self._schedule_event(handler, when, data, name, frequency) def update_event(self, name, data=None): """ Change data of event. Args: name (str): Name of event """ data = data or {} data = {'event': self._unique_name(name), 'data': data} self.emitter.emit(Message('mycroft.schedule.update_event', data=data)) def cancel_event(self, name): """ Cancel a pending event. The event will no longer be scheduled to be executed Args: name (str): Name of event """ data = {'event': self._unique_name(name)} self.emitter.emit(Message('mycroft.scheduler.remove_event', data=data))
class MycroftSkill(object): """ Abstract base class which provides common behaviour and parameters to all Skills implementation. """ def __init__(self, name=None, emitter=None): self.name = name or self.__class__.__name__ # Get directory of skill self._dir = dirname(abspath(sys.modules[self.__module__].__file__)) self.bind(emitter) self.config_core = Configuration.get() self.config = self.config_core.get(self.name) self.dialog_renderer = None self.vocab_dir = None self.root_dir = None self.file_system = FileSystemAccess(join('skills', self.name)) self.registered_intents = [] self.log = LOG.create_logger(self.name) self.reload_skill = True self.events = [] self.skill_id = 0 @property def location(self): """ Get the JSON data struction holding location information. """ # TODO: Allow Enclosure to override this for devices that # contain a GPS. return self.config_core.get('location') @property def location_pretty(self): """ Get a more 'human' version of the location as a string. """ loc = self.location if type(loc) is dict and loc["city"]: return loc["city"]["name"] return None @property def location_timezone(self): """ Get the timezone code, such as 'America/Los_Angeles' """ loc = self.location if type(loc) is dict and loc["timezone"]: return loc["timezone"]["code"] return None @property def lang(self): return self.config_core.get('lang') @property def settings(self): """ Load settings if not already loaded. """ try: return self._settings except: self._settings = SkillSettings(self._dir, self.name) return self._settings def bind(self, emitter): """ Register emitter with skill. """ if emitter: self.emitter = emitter self.enclosure = EnclosureAPI(emitter, self.name) self.__register_stop() def __register_stop(self): self.stop_time = time.time() self.stop_threshold = self.config_core.get("skills").get( 'stop_threshold') self.add_event('mycroft.stop', self.__handle_stop, False) def detach(self): for (name, intent) in self.registered_intents: name = str(self.skill_id) + ':' + name self.emitter.emit(Message("detach_intent", {"intent_name": name})) def initialize(self): """ Invoked after the skill is fully constructed and registered with the system. Use to perform any final setup needed for the skill. """ pass def get_intro_message(self): """ Get a message to speak on first load of the skill. Useful for post-install setup instructions. Returns: str: message that will be spoken to the user """ return None def converse(self, utterances, lang="en-us"): """ Handle conversation. This method can be used to override the normal intent handler after the skill has been invoked once. To enable this override thise converse method and return True to indicate that the utterance has been handled. Args: utterances (list): The utterances from the user lang: language the utterance is in Returns: True if an utterance was handled, otherwise False """ return False def __get_response(self): """ Helper to get a reponse from the user Returns: str: user's response or None on a timeout """ event = Event() def converse(utterances, lang="en-us"): converse.response = utterances[0] if utterances else None event.set() return True # install a temporary conversation handler self.make_active() converse.response = None default_converse = self.converse self.converse = converse event.wait(15) # 10 for listener, 5 for SST, then timeout self.converse = default_converse return converse.response def get_response(self, dialog='', data=None, announcement='', validator=None, on_fail=None, num_retries=-1): """ Prompt user and wait for response The given dialog or announcement will be spoken, the immediately listen and return user response. The response can optionally be validated. Example: color = self.get_response('ask.favorite.color') Args: dialog (str): Announcement dialog to read to the user data (dict): Data used to render the dialog announcement (str): Literal string (overrides dialog) validator (any): Function with following signature def validator(utterance): return utterance != "red" on_fail (any): Dialog or function returning literal string to speak on invalid input. For example: def on_fail(utterance): return "nobody likes the color red, pick another" num_retries (int): Times to ask user for input, -1 for infinite NOTE: User can not respond and timeout or say "cancel" to stop Returns: str: User's reply or None if timed out or canceled """ data = data or {} def get_announcement(): return announcement or self.dialog_renderer.render(dialog, data) if not get_announcement(): raise ValueError('announcement or dialog message required') def on_fail_default(utterance): fail_data = data.copy() fail_data['utterance'] = utterance if on_fail: return self.dialog_renderer.render(on_fail, fail_data) else: return get_announcement() # TODO: Load with something like mycroft.dialog.get_all() cancel_voc = 'text/' + self.lang + '/cancel.voc' with open(resolve_resource_file(cancel_voc)) as f: cancel_words = list(filter(bool, f.read().split('\n'))) def is_cancel(utterance): return utterance in cancel_words def validator_default(utterance): # accept anything except 'cancel' return not is_cancel(utterance) validator = validator or validator_default on_fail_fn = on_fail if callable(on_fail) else on_fail_default self.speak(get_announcement(), expect_response=True) num_fails = 0 while True: response = self.__get_response() if response is None: # if nothing said, prompt one more time num_none_fails = 1 if num_retries < 0 else num_retries if num_fails >= num_none_fails: return None else: if validator(response): return response # catch user saying 'cancel' if is_cancel(response): return None num_fails += 1 if 0 < num_retries < num_fails: return None line = on_fail_fn(response) self.speak(line, expect_response=True) def report_metric(self, name, data): """ Report a skill metric to the Mycroft servers Args: name (str): Name of metric. Must use only letters and hyphens data (dict): JSON dictionary to report. Must be valid JSON """ report_metric(basename(self.root_dir) + ':' + name, data) def send_email(self, title, body): """ Send an email to the registered user's email Args: title (str): Title of email body (str): HTML body of email. This supports simple HTML like bold and italics """ DeviceApi().send_email(title, body, basename(self.root_dir)) def make_active(self): """ Bump skill to active_skill list in intent_service this enables converse method to be called even without skill being used in last 5 minutes """ self.emitter.emit( Message('active_skill_request', {"skill_id": self.skill_id})) def _register_decorated(self): """ Register all intent handlers that have been decorated with an intent. """ global _intent_list, _intent_file_list for intent_parser, handler in _intent_list: self.register_intent(intent_parser, handler, need_self=True) for intent_file, handler in _intent_file_list: self.register_intent_file(intent_file, handler, need_self=True) _intent_list = [] _intent_file_list = [] def translate(self, text, data=None): """ Load a translatable single string resource The string is loaded from a file in the skill's dialog subdirectory 'dialog/<lang>/<text>.dialog' The string is randomly chosen from the file and rendered, replacing mustache placeholders with values found in the data dictionary. Args: text (str): The base filename (no extension needed) data (dict, optional): a JSON dictionary Returns: str: A randomly chosen string from the file """ return self.dialog_renderer.render(text, data or {}) def translate_namedvalues(self, name, delim=None): """ Load translation dict containing names and values. This loads a simple CSV from the 'dialog' folders. The name is the first list item, the value is the second. Lines prefixed with # or // get ignored Args: name (str): name of the .value file, no extension needed delim (char): delimiter character used, default is ',' Returns: dict: name and value dictionary, or [] if load fails """ delim = delim or ',' result = {} if not name.endswith(".value"): name += ".value" try: with open(join(self.root_dir, 'dialog', self.lang, name)) as f: reader = csv.reader(f, delimiter=delim) for row in reader: # skip blank or comment lines if not row or row[0].startswith("#"): continue if len(row) != 2: continue result[row[0]] = row[1] return result except Exception: return {} def translate_template(self, template_name, data=None): """ Load a translatable template The strings are loaded from a template file in the skill's dialog subdirectory. 'dialog/<lang>/<template_name>.template' The strings are loaded and rendered, replacing mustache placeholders with values found in the data dictionary. Args: template_name (str): The base filename (no extension needed) data (dict, optional): a JSON dictionary Returns: list of str: The loaded template file """ return self.__translate_file(template_name + '.template', data) def translate_list(self, list_name, data=None): """ Load a list of translatable string resources The strings are loaded from a list file in the skill's dialog subdirectory. 'dialog/<lang>/<list_name>.list' The strings are loaded and rendered, replacing mustache placeholders with values found in the data dictionary. Args: list_name (str): The base filename (no extension needed) data (dict, optional): a JSON dictionary Returns: list of str: The loaded list of strings with items in consistent positions regardless of the language. """ return self.__translate_file(list_name + '.list', data) def __translate_file(self, name, data): """Load and render lines from dialog/<lang>/<name>""" with open(join(self.root_dir, 'dialog', self.lang, name)) as f: text = f.read().replace('{{', '{').replace('}}', '}') return text.format(**data or {}).split('\n') def add_event(self, name, handler, need_self=False): """ Create event handler for executing intent Args: name: IntentParser name handler: method to call need_self: optional parameter, when called from a decorated intent handler the function will need the self variable passed as well. """ def wrapper(message): try: # Indicate that the skill handler is starting name = get_handler_name(handler) self.emitter.emit( Message("mycroft.skill.handler.start", data={'handler': name})) if need_self: # When registring from decorator self is required if len(getargspec(handler).args) == 2: handler(self, message) elif len(getargspec(handler).args) == 1: handler(self) elif len(getargspec(handler).args) == 0: # Zero may indicate multiple decorators, trying the # usual call signatures try: handler(self, message) except TypeError: handler(self) else: LOG.error("Unexpected argument count:" + str(len(getargspec(handler).args))) raise TypeError else: if len(getargspec(handler).args) == 2: handler(message) elif len(getargspec(handler).args) == 1: handler() else: LOG.error("Unexpected argument count:" + str(len(getargspec(handler).args))) raise TypeError self.settings.store() # Store settings if they've changed except Exception as e: # Convert "MyFancySkill" to "My Fancy Skill" for speaking name = re.sub("([a-z])([A-Z])", "\g<1> \g<2>", self.name) # TODO: Localize self.speak("An error occurred while processing a request in " + name) LOG.error("An error occurred while processing a request in " + self.name, exc_info=True) # indicate completion with exception self.emitter.emit( Message('mycroft.skill.handler.complete', data={ 'handler': name, 'exception': e.message })) # Indicate that the skill handler has completed self.emitter.emit( Message('mycroft.skill.handler.complete', data={'handler': name})) if handler: self.emitter.on(name, wrapper) self.events.append((name, wrapper)) def remove_event(self, name): """ Removes an event from emitter and events list Args: name: Name of Intent or Scheduler Event """ for _name, _handler in self.events: if name == _name: self.events.remove((_name, _handler)) self.emitter.remove(_name, _handler) def register_intent(self, intent_parser, handler, need_self=False): """ Register an Intent with the intent service. Args: intent_parser: Intent or IntentBuilder object to parse utterance for the handler. handler: function to register with intent need_self: optional parameter, when called from a decorated intent handler the function will need the self variable passed as well. """ if type(intent_parser) == IntentBuilder: intent_parser = intent_parser.build() elif type(intent_parser) != Intent: raise ValueError('intent_parser is not an Intent') # Default to the handler's function name if none given name = intent_parser.name or handler.__name__ intent_parser.name = str(self.skill_id) + ':' + name self.emitter.emit(Message("register_intent", intent_parser.__dict__)) self.registered_intents.append((name, intent_parser)) self.add_event(intent_parser.name, handler, need_self) def register_intent_file(self, intent_file, handler, need_self=False): """ Register an Intent file with the intent service. For example: === food.order.intent === Order some {food}. Order some {food} from {place}. I'm hungry. Grab some {food} from {place}. Optionally, you can also use <register_entity_file> to specify some examples of {food} and {place} In addition, instead of writing out multiple variations of the same sentence you can write: === food.order.intent === (Order | Grab) some {food} (from {place} | ). I'm hungry. Args: intent_file: name of file that contains example queries that should activate the intent handler: function to register with intent need_self: use for decorator. See <register_intent> """ name = str(self.skill_id) + ':' + intent_file self.emitter.emit( Message("padatious:register_intent", { "file_name": join(self.vocab_dir, intent_file), "name": name })) self.add_event(name, handler, need_self) def register_entity_file(self, entity_file): """ Register an Entity file with the intent service. And Entity file lists the exact values that an entity can hold. For example: === ask.day.intent === Is it {weekday}? === weekday.entity === Monday Tuesday ... Args: entity_file: name of file that contains examples of an entity. Must end with .entity """ if '.entity' not in entity_file: raise ValueError('Invalid entity filename: ' + entity_file) name = str(self.skill_id) + ':' + entity_file.replace('.entity', '') self.emitter.emit( Message("padatious:register_entity", { "file_name": join(self.vocab_dir, entity_file), "name": name })) def disable_intent(self, intent_name): """Disable a registered intent""" LOG.debug('Disabling intent ' + intent_name) name = str(self.skill_id) + ':' + intent_name self.emitter.emit(Message("detach_intent", {"intent_name": name})) def enable_intent(self, intent_name): """Reenable a registered intent""" for (name, intent) in self.registered_intents: if name == intent_name: self.registered_intents.remove((name, intent)) intent.name = name self.register_intent(intent, None) LOG.debug('Enabling intent ' + intent_name) break else: LOG.error('Could not enable ' + intent_name + ', it hasn\'t been registered.') def set_context(self, context, word=''): """ Add context to intent service Args: context: Keyword word: word connected to keyword """ if not isinstance(context, basestring): raise ValueError('context should be a string') if not isinstance(word, basestring): raise ValueError('word should be a string') self.emitter.emit( Message('add_context', { 'context': context, 'word': word })) def remove_context(self, context): """ remove_context removes a keyword from from the context manager. """ if not isinstance(context, basestring): raise ValueError('context should be a string') self.emitter.emit(Message('remove_context', {'context': context})) def register_vocabulary(self, entity, entity_type): """ Register a word to an keyword Args: entity: word to register entity_type: Intent handler entity to tie the word to """ self.emitter.emit( Message('register_vocab', { 'start': entity, 'end': entity_type })) def register_regex(self, regex_str): re.compile(regex_str) # validate regex self.emitter.emit(Message('register_vocab', {'regex': regex_str})) def speak(self, utterance, expect_response=False): """ Speak a sentence. Args: utterance (str): sentence mycroft should speak expect_response (bool): set to True if Mycroft should listen for a response immediately after speaking the utterance. """ # registers the skill as being active self.enclosure.register(self.name) data = {'utterance': utterance, 'expect_response': expect_response} self.emitter.emit(Message("speak", data)) def speak_dialog(self, key, data=None, expect_response=False): """ Speak a random sentence from a dialog file. Args key (str): dialog file key (filename without extension) data (dict): information used to populate sentence expect_response (bool): set to True if Mycroft should listen for a response immediately after speaking the utterance. """ data = data or {} self.speak(self.dialog_renderer.render(key, data), expect_response) def init_dialog(self, root_directory): dialog_dir = join(root_directory, 'dialog', self.lang) if exists(dialog_dir): self.dialog_renderer = DialogLoader().load(dialog_dir) else: LOG.debug('No dialog loaded, ' + dialog_dir + ' does not exist') def load_data_files(self, root_directory): self.init_dialog(root_directory) self.load_vocab_files(join(root_directory, 'vocab', self.lang)) regex_path = join(root_directory, 'regex', self.lang) self.root_dir = root_directory if exists(regex_path): self.load_regex_files(regex_path) def load_vocab_files(self, vocab_dir): self.vocab_dir = vocab_dir if exists(vocab_dir): load_vocabulary(vocab_dir, self.emitter) else: LOG.debug('No vocab loaded, ' + vocab_dir + ' does not exist') def load_regex_files(self, regex_dir): load_regex(regex_dir, self.emitter) def __handle_stop(self, event): """ Handler for the "mycroft.stop" signal. Runs the user defined `stop()` method. """ self.stop_time = time.time() try: self.stop() except: LOG.error("Failed to stop skill: {}".format(self.name), exc_info=True) @abc.abstractmethod def stop(self): pass def is_stop(self): passed_time = time.time() - self.stop_time return passed_time < self.stop_threshold def shutdown(self): """ This method is intended to be called during the skill process termination. The skill implementation must shutdown all processes and operations in execution. """ # Store settings self.settings.store() self.settings.is_alive = False # removing events for e, f in self.events: self.emitter.remove(e, f) self.events = None # Remove reference to wrappers self.emitter.emit( Message("detach_skill", {"skill_id": str(self.skill_id) + ":"})) try: self.stop() except: LOG.error("Failed to stop skill: {}".format(self.name), exc_info=True) def _unique_name(self, name): """ Return a name unique to this skill using the format [skill_id]:[name]. Args: name: Name to use internally Returns: str: name unique to this skill """ return str(self.skill_id) + ':' + name def _schedule_event(self, handler, when, data=None, name=None, repeat=None): """ Underlying method for schedle_event and schedule_repeating_event. Takes scheduling information and sends it of on the message bus. """ data = data or {} if not name: name = self.name + handler.__name__ name = self._unique_name(name) self.add_event(name, handler, False) event_data = {} event_data['time'] = time.mktime(when.timetuple()) event_data['event'] = name event_data['repeat'] = repeat event_data['data'] = data self.emitter.emit( Message('mycroft.scheduler.schedule_event', data=event_data)) def schedule_event(self, handler, when, data=None, name=None): """ Schedule a single event. Args: handler: method to be called when (datetime): when the handler should be called data (dict, optional): data to send when the handler is called name (str, optional): friendly name parameter """ data = data or {} self._schedule_event(handler, when, data, name) def schedule_repeating_event(self, handler, when, frequency, data=None, name=None): """ Schedule a repeating event. Args: handler: method to be called when (datetime): time for calling the handler frequency (float/int): time in seconds between calls data (dict, optional): data to send along to the handler name (str, optional): friendly name parameter """ data = data or {} self._schedule_event(handler, when, data, name, frequency) def update_scheduled_event(self, name, data=None): """ Change data of event. Args: name (str): Name of event """ data = data or {} data = {'event': self._unique_name(name), 'data': data} self.emitter.emit(Message('mycroft.schedule.update_event', data=data)) def cancel_scheduled_event(self, name): """ Cancel a pending event. The event will no longer be scheduled to be executed Args: name (str): Name of event """ unique_name = self._unique_name(name) data = {'event': unique_name} self.remove_event(unique_name) self.emitter.emit(Message('mycroft.scheduler.remove_event', data=data)) def get_scheduled_event_status(self, name): """ Get scheduled event data and return the amount of time left Args: name (str): Name of event Return: int: the time left in seconds """ event_name = self._unique_name(name) data = {'name': event_name} # making event_status an object so it's refrence can be changed event_status = [None] finished_callback = [False] def callback(message): if message.data is not None: event_time = int(message.data[0][0]) current_time = int(time.time()) time_left_in_seconds = event_time - current_time event_status[0] = time_left_in_seconds finished_callback[0] = True emitter_name = 'mycroft.event_status.callback.{}'.format(event_name) self.emitter.once(emitter_name, callback) self.emitter.emit(Message('mycroft.scheduler.get_event', data=data)) start_wait = time.time() while finished_callback[0] is False and time.time() - start_wait < 3.0: time.sleep(0.1) if time.time() - start_wait > 3.0: raise Exception("Event Status Messagebus Timeout") return event_status[0]
class MycroftSkill(object): """ Abstract base class which provides common behaviour and parameters to all Skills implementation. """ def __init__(self, name=None, emitter=None): self.name = name or self.__class__.__name__ self.bind(emitter) self.config_core = ConfigurationManager.get() self.config = self.config_core.get(self.name) self.dialog_renderer = None self.file_system = FileSystemAccess(join('skills', self.name)) self.registered_intents = [] self.log = getLogger(self.name) self.reload_skill = True self.events = [] @property def location(self): """ Get the JSON data struction holding location information. """ # TODO: Allow Enclosure to override this for devices that # contain a GPS. return self.config_core.get('location') @property def location_pretty(self): """ Get a more 'human' version of the location as a string. """ loc = self.location if type(loc) is dict and loc["city"]: return loc["city"]["name"] return None @property def location_timezone(self): """ Get the timezone code, such as 'America/Los_Angeles' """ loc = self.location if type(loc) is dict and loc["timezone"]: return loc["timezone"]["code"] return None @property def lang(self): return self.config_core.get('lang') @property def settings(self): """ Load settings if not already loaded. """ try: return self._settings except: self._settings = SkillSettings(self._dir) return self._settings def bind(self, emitter): if emitter: self.emitter = emitter self.enclosure = EnclosureAPI(emitter, self.name) self.__register_stop() def __register_stop(self): self.stop_time = time.time() self.stop_threshold = self.config_core.get("skills").get( 'stop_threshold') self.emitter.on('mycroft.stop', self.__handle_stop) def detach(self): for (name, intent) in self.registered_intents: name = self.name + ':' + name self.emitter.emit(Message("detach_intent", {"intent_name": name})) def initialize(self): """ Initialization function to be implemented by all Skills. Usually used to create intents rules and register them. """ logger.debug("No initialize function implemented") def _register_decorated(self): """ Register all intent handlers that has been decorated with an intent. """ global _intent_list for intent_parser, handler in _intent_list: self.register_intent(intent_parser, handler, need_self=True) _intent_list = [] def register_intent(self, intent_parser, handler, need_self=False): name = intent_parser.name intent_parser.name = self.name + ':' + intent_parser.name self.emitter.emit(Message("register_intent", intent_parser.__dict__)) self.registered_intents.append((name, intent_parser)) def receive_handler(message): try: if need_self: # When registring from decorator self is required handler(self, message) else: handler(message) except: # TODO: Localize self.speak( "An error occurred while processing a request in " + self.name) logger.error( "An error occurred while processing a request in " + self.name, exc_info=True) if handler: self.emitter.on(intent_parser.name, receive_handler) self.events.append((intent_parser.name, receive_handler)) def disable_intent(self, intent_name): """Disable a registered intent""" logger.debug('Disabling intent ' + intent_name) name = self.name + ':' + intent_name self.emitter.emit(Message("detach_intent", {"intent_name": name})) def enable_intent(self, intent_name): """Reenable a registered intent""" for (name, intent) in self.registered_intents: if name == intent_name: self.registered_intents.remove((name, intent)) intent.name = name self.register_intent(intent, None) logger.debug('Enabling intent ' + intent_name) break else: logger.error('Could not enable ' + intent_name + ', it hasn\'t been registered.') def register_vocabulary(self, entity, entity_type): self.emitter.emit(Message('register_vocab', { 'start': entity, 'end': entity_type })) def register_regex(self, regex_str): re.compile(regex_str) # validate regex self.emitter.emit(Message('register_vocab', {'regex': regex_str})) def speak(self, utterance, expect_response=False): # registers the skill as being active self.enclosure.register(self.name) data = {'utterance': utterance, 'expect_response': expect_response} self.emitter.emit(Message("speak", data)) def speak_dialog(self, key, data={}, expect_response=False): data['expect_response'] = expect_response self.speak(self.dialog_renderer.render(key, data)) def init_dialog(self, root_directory): dialog_dir = join(root_directory, 'dialog', self.lang) if os.path.exists(dialog_dir): self.dialog_renderer = DialogLoader().load(dialog_dir) else: logger.debug('No dialog loaded, ' + dialog_dir + ' does not exist') def load_data_files(self, root_directory): self.init_dialog(root_directory) self.load_vocab_files(join(root_directory, 'vocab', self.lang)) regex_path = join(root_directory, 'regex', self.lang) if os.path.exists(regex_path): self.load_regex_files(regex_path) def load_vocab_files(self, vocab_dir): if os.path.exists(vocab_dir): load_vocabulary(vocab_dir, self.emitter) else: logger.debug('No vocab loaded, ' + vocab_dir + ' does not exist') def load_regex_files(self, regex_dir): load_regex(regex_dir, self.emitter) def __handle_stop(self, event): """ Handler for the "mycroft.stop" signal. Runs the user defined `stop()` method. """ self.stop_time = time.time() try: self.stop() except: logger.error("Failed to stop skill: {}".format(self.name), exc_info=True) @abc.abstractmethod def stop(self): pass def is_stop(self): passed_time = time.time() - self.stop_time return passed_time < self.stop_threshold def shutdown(self): """ This method is intended to be called during the skill process termination. The skill implementation must shutdown all processes and operations in execution. """ # Store settings self.settings.store() # removing events for e, f in self.events: self.emitter.remove(e, f) self.emitter.emit( Message("detach_skill", {"skill_name": self.name + ":"})) try: self.stop() except: logger.error("Failed to stop skill: {}".format(self.name), exc_info=True)
def bind(self, emitter): if emitter: self.emitter = emitter self.enclosure = EnclosureAPI(emitter) self.__register_stop()
class WiFi: def __init__(self): self.iface = pyw.winterfaces()[0] self.ap = AccessPoint(self.iface) self.server = None self.ws = WebsocketClient() ConfigurationManager.init(self.ws) self.enclosure = EnclosureAPI(self.ws) self.init_events() self.conn_monitor = None self.conn_monitor_stop = threading.Event() def init_events(self): ''' Register handlers for various websocket events used to communicate with outside systems. ''' # This event is generated by an outside mechanism. On a # Holmes unit this comes from the Enclosure's menu item # being selected. self.ws.on('mycroft.wifi.start', self.start) # These events are generated by Javascript in the captive # portal. self.ws.on('mycroft.wifi.stop', self.stop) self.ws.on('mycroft.wifi.scan', self.scan) self.ws.on('mycroft.wifi.connect', self.connect) def start(self, event=None): ''' Fire up the MYCROFT access point for the user to connect to with a phone or computer. ''' LOG.info("Starting access point...") # Fire up our access point self.ap.up() LOG.info("Done putting ap up...") if not self.server: LOG.info("Creating web server...") self.server = WebServer(self.ap.ip, 80) LOG.info("Starting web server...") self.server.start() LOG.info("Created web server.") LOG.info("Access point started!\n%s" % self.ap.__dict__) self._start_connection_monitor() def _connection_prompt(self, prefix): # let the user know to connect to it... passwordSpelled = ", ".join(self.ap.password) self._speak_and_show( prefix + " Use your mobile device or computer to " "connect to the wifi network " "'MYCROFT'; Then enter the uppercase " "password " + passwordSpelled, self.ap.password) def _speak_and_show(self, speak, show): ''' Communicate with the user throughout the process ''' self.ws.emit(Message("speak", {'utterance': speak})) if show is None: return # TODO: This sleep should not be necessary, but without it the # text to be displayed by enclosure.mouth_text() gets # wiped out immediately when the utterance above is # begins processing. # Remove the sleep once this behavior is corrected. sleep(0.25) self.enclosure.mouth_text(show) def _start_connection_monitor(self): LOG.info("Starting monitor thread...\n") if self.conn_monitor is not None: LOG.info("Killing old thread...\n") self.conn_monitor_stop.set() self.conn_monitor_stop.wait() self.conn_monitor = threading.Thread( target=self._do_connection_monitor, args={}) self.conn_monitor.daemon = True self.conn_monitor.start() LOG.info("Monitor thread setup complete.\n") def _stop_connection_monitor(self): ''' Set flag that will let monitoring thread close ''' self.conn_monitor_stop.set() def _do_connection_monitor(self): LOG.info("Invoked monitor thread...\n") mtimeLast = os.path.getmtime('/var/lib/misc/dnsmasq.leases') bHasConnected = False cARPFailures = 0 timeStarted = time.time() timeLastAnnounced = 0 # force first announcement to now self.conn_monitor_stop.clear() while not self.conn_monitor_stop.isSet(): # do our monitoring... mtime = os.path.getmtime('/var/lib/misc/dnsmasq.leases') if mtimeLast != mtime: # Something changed in the dnsmasq lease file - # presumably a (re)new lease bHasConnected = True cARPFailures = 0 mtimeLast = mtime timeStarted = time.time() # reset start time after connection timeLastAnnounced = time.time() - 45 # announce how to connect if time.time() - timeStarted > 60 * 5: # After 5 minutes, shut down the access point LOG.info("Auto-shutdown of access point after 5 minutes") self.stop() continue if time.time() - timeLastAnnounced >= 45: if bHasConnected: self._speak_and_show( "Now you can open your browser and go to start dot " "mycroft dot A I, then follow the instructions given " " there", "start.mycroft.ai") else: self._connection_prompt("Allow me to walk you through the " " wifi setup process; ") timeLastAnnounced = time.time() if bHasConnected: # Flush the ARP entries associated with our access point # This will require all network hardware to re-register # with the ARP tables if still present. if cARPFailures == 0: res = cli_no_output('ip', '-s', '-s', 'neigh', 'flush', self.ap.subnet + '.0/24') # Give ARP system time to re-register hardware sleep(5) # now look at the hardware that has responded, if no entry # shows up on our access point after 2*5=10 seconds, the user # has disconnected if not self._is_ARP_filled(): cARPFailures += 1 if cARPFailures > 2: self._connection_prompt("Connection lost,") bHasConnected = False else: cARPFailures = 0 sleep(5) # wait a bit to prevent thread from hogging CPU LOG.info("Exiting monitor thread...\n") self.conn_monitor_stop.clear() def _is_ARP_filled(self): res = cli_no_output('/usr/sbin/arp', '-n') out = str(res.get("stdout")) if out: # Parse output, skipping header for o in out.split("\n")[1:]: if o[0:len(self.ap.subnet)] == self.ap.subnet: if "(incomplete)" in o: # ping the IP to get the ARP table entry reloaded ip_disconnected = o.split(" ")[0] cli_no_output('/bin/ping', '-c', '1', '-W', '3', ip_disconnected) else: return True # something on subnet is connected! return False def scan(self, event=None): LOG.info("Scanning wifi connections...") networks = {} status = self.get_status() for cell in Cell.all(self.iface): update = True ssid = cell.ssid quality = self.get_quality(cell.quality) # If there are duplicate network IDs (e.g. repeaters) only # report the strongest signal if networks.__contains__(ssid): update = networks.get(ssid).get("quality") < quality if update and ssid: networks[ssid] = { 'quality': quality, 'encrypted': cell.encrypted, 'connected': self.is_connected(ssid, status) } self.ws.emit(Message("mycroft.wifi.scanned", {'networks': networks})) LOG.info("Wifi connections scanned!\n%s" % networks) @staticmethod def get_quality(quality): values = quality.split("/") return float(values[0]) / float(values[1]) def connect(self, event=None): if event and event.data: ssid = event.data.get("ssid") connected = self.is_connected(ssid) if connected: LOG.warn("Mycroft is already connected to %s" % ssid) else: self.disconnect() LOG.info("Connecting to: %s" % ssid) nid = wpa(self.iface, 'add_network') wpa(self.iface, 'set_network', nid, 'ssid', '"' + ssid + '"') if event.data.__contains__("pass"): psk = '"' + event.data.get("pass") + '"' wpa(self.iface, 'set_network', nid, 'psk', psk) else: wpa(self.iface, 'set_network', nid, 'key_mgmt', 'NONE') wpa(self.iface, 'enable', nid) connected = self.get_connected(ssid) if connected: wpa(self.iface, 'save_config') self.ws.emit(Message("mycroft.wifi.connected", {'connected': connected})) LOG.info("Connection status for %s = %s" % (ssid, connected)) if connected: self.ws.emit(Message("speak", { 'utterance': "Thank you, I'm now connected to the " "internet and ready for use"})) # TODO: emit something that triggers a pairing check def disconnect(self): status = self.get_status() nid = status.get("id") if nid: ssid = status.get("ssid") wpa(self.iface, 'disable', nid) LOG.info("Disconnecting %s id: %s" % (ssid, nid)) def get_status(self): res = cli('wpa_cli', '-i', self.iface, 'status') out = str(res.get("stdout")) if out: return dict(o.split("=") for o in out.split("\n")[:-1]) return {} def get_connected(self, ssid, retry=5): connected = self.is_connected(ssid) while not connected and retry > 0: sleep(2) retry -= 1 connected = self.is_connected(ssid) return connected def is_connected(self, ssid, status=None): status = status or self.get_status() state = status.get("wpa_state") return status.get("ssid") == ssid and state == "COMPLETED" def stop(self, event=None): LOG.info("Stopping access point...") self._stop_connection_monitor() self.ap.down() if self.server: self.server.server.shutdown() self.server.server.server_close() self.server.join() self.server = None LOG.info("Access point stopped!") def _do_net_check(self): # give system 5 seconds to resolve network or get plugged in sleep(5) LOG.info("Checking internet connection again") if not connected() and self.conn_monitor is None: # TODO: Enclosure/localization self._speak_and_show( "This device is not connected to the Internet. Either plug " "in a network cable or hold the button on top for two " "seconds, then select wifi from the menu", None) def run(self): try: # When the system first boots up, check for a valid internet # connection. LOG.info("Checking internet connection") if not connected(): LOG.info("No connection initially, waiting 20...") self.net_check = threading.Thread( target=self._do_net_check, args={}) self.net_check.daemon = True self.net_check.start() else: LOG.info("Connection found!") self.ws.run_forever() except Exception as e: LOG.error("Error: {0}".format(e)) self.stop()
def init(self, ws): self.ws = ws self.playback.init(self) self.enclosure = EnclosureAPI(self.ws) self.playback.enclosure = self.enclosure
def handle_open(): EnclosureAPI(ws).system_reset()
class MycroftSkill(object): """ Abstract base class which provides common behaviour and parameters to all Skills implementation. """ def __init__(self, name=None, emitter=None): self.name = name or self.__class__.__name__ # Get directory of skill self._dir = dirname(abspath(sys.modules[self.__module__].__file__)) self.bind(emitter) self.config_core = Configuration.get() self.config = self.config_core.get(self.name) self.dialog_renderer = None self.vocab_dir = None self.file_system = FileSystemAccess(join('skills', self.name)) self.registered_intents = [] self.log = LOG.create_logger(self.name) self.reload_skill = True self.events = [] self.skill_id = 0 @property def location(self): """ Get the JSON data struction holding location information. """ # TODO: Allow Enclosure to override this for devices that # contain a GPS. return self.config_core.get('location') @property def location_pretty(self): """ Get a more 'human' version of the location as a string. """ loc = self.location if type(loc) is dict and loc["city"]: return loc["city"]["name"] return None @property def location_timezone(self): """ Get the timezone code, such as 'America/Los_Angeles' """ loc = self.location if type(loc) is dict and loc["timezone"]: return loc["timezone"]["code"] return None @property def lang(self): return self.config_core.get('lang') @property def settings(self): """ Load settings if not already loaded. """ try: return self._settings except: self._settings = SkillSettings(self._dir, self.name) return self._settings def bind(self, emitter): """ Register emitter with skill. """ if emitter: self.emitter = emitter self.enclosure = EnclosureAPI(emitter, self.name) self.__register_stop() def __register_stop(self): self.stop_time = time.time() self.stop_threshold = self.config_core.get("skills").get( 'stop_threshold') self.add_event('mycroft.stop', self.__handle_stop, False) def detach(self): for (name, intent) in self.registered_intents: name = str(self.skill_id) + ':' + name self.emitter.emit(Message("detach_intent", {"intent_name": name})) def initialize(self): """ Initialization function to be implemented by all Skills. Usually used to create intents rules and register them. """ LOG.debug("No initialize function implemented") def get_intro_message(self): """ Get a message to speak on first load of the skill. Useful for post-install setup instructions. Returns: str: message that will be spoken to the user """ return None def converse(self, utterances, lang="en-us"): """ Handle conversation. This method can be used to override the normal intent handler after the skill has been invoked once. To enable this override thise converse method and return True to indicate that the utterance has been handled. Args: utterances: The utterances from the user lang: language the utterance is in Returns: True if an utterance was handled, otherwise False """ return False def make_active(self): """ Bump skill to active_skill list in intent_service this enables converse method to be called even without skill being used in last 5 minutes """ self.emitter.emit(Message('active_skill_request', {"skill_id": self.skill_id})) def _register_decorated(self): """ Register all intent handlers that have been decorated with an intent. """ global _intent_list, _intent_file_list for intent_parser, handler in _intent_list: self.register_intent(intent_parser, handler, need_self=True) for intent_file, handler in _intent_file_list: self.register_intent_file(intent_file, handler, need_self=True) _intent_list = [] _intent_file_list = [] def add_event(self, name, handler, need_self=False): """ Create event handler for executing intent Args: name: IntentParser name handler: method to call need_self: optional parameter, when called from a decorated intent handler the function will need the self variable passed as well. """ def wrapper(message): try: # Indicate that the skill handler is starting name = get_handler_name(handler) self.emitter.emit(Message("mycroft.skill.handler.start", data={'handler': name})) if need_self: # When registring from decorator self is required if len(getargspec(handler).args) == 2: handler(self, message) elif len(getargspec(handler).args) == 1: handler(self) elif len(getargspec(handler).args) == 0: # Zero may indicate multiple decorators, trying the # usual call signatures try: handler(self, message) except TypeError: handler(self) else: raise TypeError else: if len(getargspec(handler).args) == 2: handler(message) elif len(getargspec(handler).args) == 1: handler() else: raise TypeError self.settings.store() # Store settings if they've changed except Exception as e: # TODO: Localize self.speak( "An error occurred while processing a request in " + self.name) LOG.error( "An error occurred while processing a request in " + self.name, exc_info=True) # indicate completion with exception self.emitter.emit(Message('mycroft.skill.handler.complete', data={'handler': name, 'exception': e.message})) # Indicate that the skill handler has completed self.emitter.emit(Message('mycroft.skill.handler.complete', data={'handler': name})) if handler: self.emitter.on(name, wrapper) self.events.append((name, wrapper)) def remove_event(self, name): """ Removes an event from emitter and events list Args: name: Name of Intent or Scheduler Event """ for _name, _handler in self.events: if name == _name: self.events.remove((_name, _handler)) self.emitter.remove(_name, _handler) def register_intent(self, intent_parser, handler, need_self=False): """ Register an Intent with the intent service. Args: intent_parser: Intent or IntentBuilder object to parse utterance for the handler. handler: function to register with intent need_self: optional parameter, when called from a decorated intent handler the function will need the self variable passed as well. """ if type(intent_parser) == IntentBuilder: intent_parser = intent_parser.build() elif type(intent_parser) != Intent: raise ValueError('intent_parser is not an Intent') name = intent_parser.name intent_parser.name = str(self.skill_id) + ':' + intent_parser.name self.emitter.emit(Message("register_intent", intent_parser.__dict__)) self.registered_intents.append((name, intent_parser)) self.add_event(intent_parser.name, handler, need_self) def register_intent_file(self, intent_file, handler, need_self=False): """ Register an Intent file with the intent service. For example: === food.order.intent === Order some {food}. Order some {food} from {place}. I'm hungry. Grab some {food} from {place}. Optionally, you can also use <register_entity_file> to specify some examples of {food} and {place} In addition, instead of writing out multiple variations of the same sentence you can write: === food.order.intent === (Order | Grab) some {food} (from {place} | ). I'm hungry. Args: intent_file: name of file that contains example queries that should activate the intent handler: function to register with intent need_self: use for decorator. See <register_intent> """ name = str(self.skill_id) + ':' + intent_file self.emitter.emit(Message("padatious:register_intent", { "file_name": join(self.vocab_dir, intent_file), "name": name })) self.add_event(name, handler, need_self) def register_entity_file(self, entity_file): """ Register an Entity file with the intent service. And Entity file lists the exact values that an entity can hold. For example: === ask.day.intent === Is it {weekday}? === weekday.entity === Monday Tuesday ... Args: entity_file: name of file that contains examples of an entity. Must end with .entity """ if '.entity' not in entity_file: raise ValueError('Invalid entity filename: ' + entity_file) name = str(self.skill_id) + ':' + entity_file.replace('.entity', '') self.emitter.emit(Message("padatious:register_entity", { "file_name": join(self.vocab_dir, entity_file), "name": name })) def disable_intent(self, intent_name): """Disable a registered intent""" LOG.debug('Disabling intent ' + intent_name) name = str(self.skill_id) + ':' + intent_name self.emitter.emit(Message("detach_intent", {"intent_name": name})) def enable_intent(self, intent_name): """Reenable a registered intent""" for (name, intent) in self.registered_intents: if name == intent_name: self.registered_intents.remove((name, intent)) intent.name = name self.register_intent(intent, None) LOG.debug('Enabling intent ' + intent_name) break else: LOG.error('Could not enable ' + intent_name + ', it hasn\'t been registered.') def set_context(self, context, word=''): """ Add context to intent service Args: context: Keyword word: word connected to keyword """ if not isinstance(context, basestring): raise ValueError('context should be a string') if not isinstance(word, basestring): raise ValueError('word should be a string') self.emitter.emit(Message('add_context', {'context': context, 'word': word})) def remove_context(self, context): """ remove_context removes a keyword from from the context manager. """ if not isinstance(context, basestring): raise ValueError('context should be a string') self.emitter.emit(Message('remove_context', {'context': context})) def register_vocabulary(self, entity, entity_type): """ Register a word to an keyword Args: entity: word to register entity_type: Intent handler entity to tie the word to """ self.emitter.emit(Message('register_vocab', { 'start': entity, 'end': entity_type })) def register_regex(self, regex_str): re.compile(regex_str) # validate regex self.emitter.emit(Message('register_vocab', {'regex': regex_str})) def speak(self, utterance, expect_response=False): """ Speak a sentence. Args: utterance: sentence mycroft should speak expect_response: set to True if Mycroft should expect a response from the user and start listening for response. """ # registers the skill as being active self.enclosure.register(self.name) data = {'utterance': utterance, 'expect_response': expect_response} self.emitter.emit(Message("speak", data)) def speak_dialog(self, key, data=None, expect_response=False): """ Speak sentance based of dialog file. Args key: dialog file key (filname without extension) data: information to populate sentence with expect_response: set to True if Mycroft should expect a response from the user and start listening for response. """ data = data or {} self.speak(self.dialog_renderer.render(key, data), expect_response) def init_dialog(self, root_directory): dialog_dir = join(root_directory, 'dialog', self.lang) if exists(dialog_dir): self.dialog_renderer = DialogLoader().load(dialog_dir) else: LOG.debug('No dialog loaded, ' + dialog_dir + ' does not exist') def load_data_files(self, root_directory): self.init_dialog(root_directory) self.load_vocab_files(join(root_directory, 'vocab', self.lang)) regex_path = join(root_directory, 'regex', self.lang) if exists(regex_path): self.load_regex_files(regex_path) def load_vocab_files(self, vocab_dir): self.vocab_dir = vocab_dir if exists(vocab_dir): load_vocabulary(vocab_dir, self.emitter) else: LOG.debug('No vocab loaded, ' + vocab_dir + ' does not exist') def load_regex_files(self, regex_dir): load_regex(regex_dir, self.emitter) def __handle_stop(self, event): """ Handler for the "mycroft.stop" signal. Runs the user defined `stop()` method. """ self.stop_time = time.time() try: self.stop() except: LOG.error("Failed to stop skill: {}".format(self.name), exc_info=True) @abc.abstractmethod def stop(self): pass def is_stop(self): passed_time = time.time() - self.stop_time return passed_time < self.stop_threshold def shutdown(self): """ This method is intended to be called during the skill process termination. The skill implementation must shutdown all processes and operations in execution. """ # Store settings self.settings.store() self.settings.is_alive = False # removing events for e, f in self.events: self.emitter.remove(e, f) self.events = None # Remove reference to wrappers self.emitter.emit( Message("detach_skill", {"skill_id": str(self.skill_id) + ":"})) try: self.stop() except: LOG.error("Failed to stop skill: {}".format(self.name), exc_info=True) def _unique_name(self, name): """ Return a name unique to this skill using the format [skill_id]:[name]. Args: name: Name to use internally Returns: str: name unique to this skill """ return str(self.skill_id) + ':' + name def _schedule_event(self, handler, when, data=None, name=None, repeat=None): """ Underlying method for schedle_event and schedule_repeating_event. Takes scheduling information and sends it of on the message bus. """ data = data or {} if not name: name = self.name + handler.__name__ name = self._unique_name(name) self.add_event(name, handler, False) event_data = {} event_data['time'] = time.mktime(when.timetuple()) event_data['event'] = name event_data['repeat'] = repeat event_data['data'] = data self.emitter.emit(Message('mycroft.scheduler.schedule_event', data=event_data)) def schedule_event(self, handler, when, data=None, name=None): """ Schedule a single event. Args: handler: method to be called when (datetime): when the handler should be called data (dict, optional): data to send when the handler is called name (str, optional): friendly name parameter """ data = data or {} self._schedule_event(handler, when, data, name) def schedule_repeating_event(self, handler, when, frequency, data=None, name=None): """ Schedule a repeating event. Args: handler: method to be called when (datetime): time for calling the handler frequency (float/int): time in seconds between calls data (dict, optional): data to send along to the handler name (str, optional): friendly name parameter """ data = data or {} self._schedule_event(handler, when, data, name, frequency) def update_scheduled_event(self, name, data=None): """ Change data of event. Args: name (str): Name of event """ data = data or {} data = { 'event': self._unique_name(name), 'data': data } self.emitter.emit(Message('mycroft.schedule.update_event', data=data)) def cancel_scheduled_event(self, name): """ Cancel a pending event. The event will no longer be scheduled to be executed Args: name (str): Name of event """ unique_name = self._unique_name(name) data = {'event': unique_name} self.remove_event(unique_name) self.emitter.emit(Message('mycroft.scheduler.remove_event', data=data)) def get_scheduled_event_status(self, name): """ Get scheduled event data and return the amount of time left Args: name (str): Name of event Return: int: the time left in seconds """ event_name = self._unique_name(name) data = {'name': event_name} # making event_status an object so it's refrence can be changed event_status = [None] finished_callback = [False] def callback(message): if message.data is not None: event_time = int(message.data[0][0]) current_time = int(time.time()) time_left_in_seconds = event_time - current_time event_status[0] = time_left_in_seconds finished_callback[0] = True emitter_name = 'mycroft.event_status.callback.{}'.format(event_name) self.emitter.once(emitter_name, callback) self.emitter.emit(Message('mycroft.scheduler.get_event', data=data)) start_wait = time.time() while finished_callback[0] is False and time.time() - start_wait < 3.0: time.sleep(0.1) if time.time() - start_wait > 3.0: raise Exception("Event Status Messagebus Timeout") return event_status[0]
def handle_open(): # Reset the UI to indicate ready for speech processing EnclosureAPI(ws).reset()
class SkillManager(Thread): """ Load, update and manage instances of Skill on this system. """ def __init__(self, ws): super(SkillManager, self).__init__() self._stop_event = Event() self._loaded_priority = Event() self.loaded_skills = {} self.msm_blocked = False self.ws = ws self.enclosure = EnclosureAPI(ws) # Schedule install/update of default skill self.next_download = None # Conversation management ws.on('skill.converse.request', self.handle_converse_request) # Update on initial connection ws.on('mycroft.internet.connected', self.schedule_update_skills) # Update upon request ws.on('skillmanager.update', self.schedule_now) ws.on('skillmanager.list', self.send_skill_list) # Register handlers for external MSM signals ws.on('msm.updating', self.block_msm) ws.on('msm.removing', self.block_msm) ws.on('msm.installing', self.block_msm) ws.on('msm.updated', self.restore_msm) ws.on('msm.removed', self.restore_msm) ws.on('msm.installed', self.restore_msm) # when locked, MSM is active or intentionally blocked self.__msm_lock = Lock() self.__ext_lock = Lock() def schedule_update_skills(self, message=None): """ Schedule a skill update to take place directly. """ if direct_update_needed(): # Update skills at next opportunity LOG.info('Skills will be updated directly') self.schedule_now() # Skip the message when unpaired because the prompt to go # to home.mycrof.ai will be displayed by the pairing skill if not is_paired(): self.enclosure.mouth_text(dialog.get("message_updating")) else: LOG.info('Skills will be updated at a later time') self.next_download = time.time() + 60 * MINUTES def schedule_now(self, message=None): self.next_download = time.time() - 1 def block_msm(self, message=None): """ Disallow start of msm. """ # Make sure the external locking of __msm_lock is done in correct order with self.__ext_lock: if not self.msm_blocked: self.__msm_lock.acquire() self.msm_blocked = True def restore_msm(self, message=None): """ Allow start of msm if not allowed. """ # Make sure the external locking of __msm_lock is done in correct order with self.__ext_lock: if self.msm_blocked: self.__msm_lock.release() self.msm_blocked = False def download_skills(self, speak=False): """ Invoke MSM to install default skills and/or update installed skills Args: speak (bool, optional): Speak the result? Defaults to False """ # Don't invoke msm if already running if exists(MSM_BIN) and self.__msm_lock.acquire(): try: # Invoke the MSM script to do the hard work. LOG.debug("==== Invoking Mycroft Skill Manager: " + MSM_BIN) p = subprocess.Popen(MSM_BIN + " default", stderr=subprocess.STDOUT, stdout=subprocess.PIPE, shell=True) (output, err) = p.communicate() res = p.returncode # Always set next update to an hour from now if successful if res == 0: self.next_download = time.time() + 60 * MINUTES if res == 0 and speak: data = {'utterance': dialog.get("skills updated")} self.ws.emit(Message("speak", data)) return True elif not connected(): LOG.error('msm failed, network connection not available') if speak: self.ws.emit( Message( "speak", { 'utterance': dialog.get("not connected to the internet") })) self.next_download = time.time() + 5 * MINUTES return False elif res != 0: LOG.error('msm failed with error {}: {}'.format( res, output)) if speak: self.ws.emit( Message( "speak", { 'utterance': dialog.get( "sorry I couldn't install default skills" ) })) self.next_download = time.time() + 5 * MINUTES return False finally: self.__msm_lock.release() else: LOG.error("Unable to invoke Mycroft Skill Manager: " + MSM_BIN) def _load_or_reload_skill(self, skill_folder): """ Check if unloaded skill or changed skill needs reloading and perform loading if necessary. Returns True if the skill was loaded/reloaded """ if skill_folder not in self.loaded_skills: self.loaded_skills[skill_folder] = { "id": hash(os.path.join(SKILLS_DIR, skill_folder)) } skill = self.loaded_skills.get(skill_folder) skill["path"] = os.path.join(SKILLS_DIR, skill_folder) # check if folder is a skill (must have __init__.py) if not MainModule + ".py" in os.listdir(skill["path"]): return False # getting the newest modified date of skill modified = _get_last_modified_date(skill["path"]) last_mod = skill.get("last_modified", 0) # checking if skill is loaded and hasn't been modified on disk if skill.get("loaded") and modified <= last_mod: return False # Nothing to do! # check if skill was modified elif skill.get("instance") and modified > last_mod: # check if skill has been blocked from reloading if not skill["instance"].reload_skill: return False LOG.debug("Reloading Skill: " + skill_folder) # removing listeners and stopping threads try: skill["instance"]._shutdown() except Exception: LOG.exception("An error occured while shutting down {}".format( skill["instance"].name)) if DEBUG: gc.collect() # Collect garbage to remove false references # Remove two local references that are known refs = sys.getrefcount(skill["instance"]) - 2 if refs > 0: msg = ("After shutdown of {} there are still " "{} references remaining. The skill " "won't be cleaned from memory.") LOG.warning(msg.format(skill['instance'].name, refs)) del skill["instance"] self.ws.emit( Message("mycroft.skills.shutdown", { "folder": skill_folder, "id": skill["id"] })) # (Re)load the skill from disk with self.__msm_lock: # Make sure msm isn't running skill["loaded"] = True desc = create_skill_descriptor(skill["path"]) skill["instance"] = load_skill(desc, self.ws, skill["id"], BLACKLISTED_SKILLS) skill["last_modified"] = modified if skill['instance'] is not None: self.ws.emit( Message( 'mycroft.skills.loaded', { 'folder': skill_folder, 'id': skill['id'], 'name': skill['instance'].name, 'modified': modified })) return True else: self.ws.emit( Message('mycroft.skills.loading_failure', { 'folder': skill_folder, 'id': skill['id'] })) return False def load_skill_list(self, skills_to_load): """ Load the specified list of skills from disk Args: skills_to_load (list): list of skill directory names to load """ if exists(SKILLS_DIR): # checking skills dir and getting all priority skills there skill_list = [ folder for folder in filter( lambda x: os.path.isdir(os.path.join(SKILLS_DIR, x)), os.listdir(SKILLS_DIR)) if folder in skills_to_load ] for skill_folder in skill_list: self._load_or_reload_skill(skill_folder) def run(self): """ Load skills and update periodically from disk and internet """ # Load priority skills first, in order (very first time this will # occur before MSM has run) self.load_skill_list(PRIORITY_SKILLS) self._loaded_priority.set() has_loaded = False # Scan the file folder that contains Skills. If a Skill is updated, # unload the existing version from memory and reload from the disk. while not self._stop_event.is_set(): # check if skill updates are enabled update = Configuration.get().get("skills", {}).get("auto_update", True) # Update skills once an hour if update is enabled if (self.next_download and time.time() >= self.next_download and update): self.download_skills() # Look for recently changed skill(s) needing a reload if (exists(SKILLS_DIR) and (self.next_download or not update)): # checking skills dir and getting all skills there list = filter( lambda x: os.path.isdir(os.path.join(SKILLS_DIR, x)), os.listdir(SKILLS_DIR)) still_loading = False for skill_folder in list: still_loading = (self._load_or_reload_skill(skill_folder) or still_loading) if not has_loaded and not still_loading: has_loaded = True self.ws.emit(Message('mycroft.skills.initialized')) # remember the date of the last modified skill modified_dates = map(lambda x: x.get("last_modified"), self.loaded_skills.values()) # Pause briefly before beginning next scan time.sleep(2) def send_skill_list(self, message=None): """ Send list of loaded skills. """ try: self.ws.emit( Message('mycroft.skills.list', data={'skills': self.loaded_skills.keys()})) except Exception as e: LOG.exception(e) def wait_loaded_priority(self): """ Block until all priority skills have loaded """ while not self._loaded_priority.is_set(): time.sleep(1) def stop(self): """ Tell the manager to shutdown """ self._stop_event.set() # Do a clean shutdown of all skills for name, skill_info in self.loaded_skills.items(): instance = skill_info.get('instance') if instance: try: instance._shutdown() except Exception: LOG.exception('Shutting down skill: ' + name) def handle_converse_request(self, message): """ Check if the targeted skill id can handle conversation If supported, the conversation is invoked. """ skill_id = int(message.data["skill_id"]) utterances = message.data["utterances"] lang = message.data["lang"] # loop trough skills list and call converse for skill with skill_id for skill in self.loaded_skills: if self.loaded_skills[skill]["id"] == skill_id: try: instance = self.loaded_skills[skill]["instance"] except BaseException: LOG.error("converse requested but skill not loaded") self.ws.emit( Message("skill.converse.response", { "skill_id": 0, "result": False })) return try: result = instance.converse(utterances, lang) self.ws.emit( Message("skill.converse.response", { "skill_id": skill_id, "result": result })) return except BaseException: LOG.exception("Error in converse method for skill " + str(skill_id)) self.ws.emit( Message("skill.converse.response", { "skill_id": 0, "result": False }))