def __init__(self, cache=None): super(RemoteConf, self).__init__(None) cache = cache or '/opt/mycroft/web_config_cache.json' try: # Here to avoid cyclic import from mycroft.api import DeviceApi api = DeviceApi() setting = api.get_settings() location = api.get_location() if location: setting["location"] = location # Remove server specific entries config = {} translate_remote(config, setting) for key in config: self.__setitem__(key, config[key]) self.store(cache) except HTTPError as e: LOG.error("HTTPError fetching remote configuration: %s" % e.response.status_code) self.load_local(cache) except Exception as e: LOG.error("Failed to fetch remote configuration: %s" % repr(e), exc_info=True) self.load_local(cache)
def __init__(self, directory, name): super(SkillSettings, self).__init__() # when skills try to instantiate settings # in __init__, it can erase the settings saved # on disk (settings.json). So this prevents that # This is set to true in core.py after skill init self.allow_overwrite = False self.api = DeviceApi() self.config = ConfigurationManager.get() self.name = name self.directory = directory # set file paths self._settings_path = join(directory, 'settings.json') self._meta_path = join(directory, 'settingsmeta.json') self.is_alive = True self.loaded_hash = hash(str(self)) self._complete_intialization = False self._device_identity = None self._api_path = None self._user_identity = None self.changed_callback = None # if settingsmeta exist if isfile(self._meta_path): self._poll_skill_settings()
def load(config=None): RemoteConfiguration.validate(config) update = config.get("server", {}).get("update") if update: try: from mycroft.api import DeviceApi api = DeviceApi() setting = api.find_setting() location = api.find_location() if location: setting["location"] = location RemoteConfiguration.__load(config, setting) except Exception as e: LOG.warn("Failed to fetch remote configuration: %s" % repr(e)) else: LOG.debug("Remote configuration not activated.") return config
def __init__(self, cache=None): super(RemoteConf, self).__init__(None) cache = cache or WEB_CONFIG_CACHE from mycroft.api import is_paired if not is_paired(): self.load_local(cache) return try: # Here to avoid cyclic import from mycroft.api import DeviceApi api = DeviceApi() setting = api.get_settings() try: location = api.get_location() except RequestException as e: LOG.error("RequestException fetching remote location: {}" .format(str(e))) if exists(cache) and isfile(cache): location = load_commented_json(cache).get('location') if location: setting["location"] = location # Remove server specific entries config = {} translate_remote(config, setting) for key in config: self.__setitem__(key, config[key]) self.store(cache) except RequestException as e: LOG.error("RequestException fetching remote configuration: {}" .format(str(e))) self.load_local(cache) except Exception as e: LOG.error("Failed to fetch remote configuration: %s" % repr(e), exc_info=True) self.load_local(cache)
def check_connection(): """ Check for network connection. If not paired trigger pairing. Runs as a Timer every second until connection is detected. """ if connected(): 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()
class ConfigurationSkill(ScheduledSkill): def __init__(self): super(ConfigurationSkill, self).__init__("ConfigurationSkill") self.max_delay = self.config.get('max_delay') self.api = DeviceApi() def initialize(self): self.load_data_files(dirname(__file__)) intent = IntentBuilder("UpdateConfigurationIntent") \ .require("ConfigurationSkillKeyword") \ .require("ConfigurationSkillUpdateVerb") \ .build() self.register_intent(intent, self.handle_update_intent) self.schedule() def handle_update_intent(self, message): try: self.update() self.speak_dialog("config.updated") except HTTPError as e: self.__api_error(e) def notify(self, timestamp): try: self.update() except HTTPError as e: if e.response.status_code == 401: self.log.warn("Impossible to update configuration because " "device isn't paired") self.schedule() def update(self): config = self.api.find_setting() self.emitter.emit(Message("configuration.updated", config)) def __api_error(self, e): if e.response.status_code == 401: self.emitter.emit(Message("mycroft.not.paired")) def get_times(self): return [self.get_utc_time() + self.max_delay] def stop(self): pass
def __init__(self, lang, config): super(Mimic, self).__init__(lang, config, MimicValidator(self), 'wav', ssml_tags=[ "speak", "ssml", "phoneme", "voice", "audio", "prosody" ]) self.default_binary = get_mimic_binary() self.clear_cache() # Download subscriber voices if needed self.subscriber_voices = get_subscriber_voices() self.is_subscriber = DeviceApi().is_subscriber if self.is_subscriber: trd = Thread(target=download_subscriber_voices, args=[self.voice]) trd.daemon = True trd.start()
def load_credentials(self): """ Retrieve credentials from the backend and connect to Spotify """ try: creds = MycroftSpotifyCredentials(self.OAUTH_ID) self.spotify = SpotifyConnect(client_credentials_manager=creds) except HTTPError: LOG.info('Couldn\'t fetch credentials') self.spotify = None if self.spotify: # Spotfy connection worked, prepare for usage # TODO: Repeat occasionally on failures? # If not able to authorize, the method will be repeated after 60 # seconds self.create_intents() # Should be safe to set device_name here since home has already # been connected self.device_name = DeviceApi().get().get('name') self.cancel_scheduled_event('SpotifyLogin') self.launch_librespot()
def __init__(self, wake_word_recognizer): self.config = Configuration.get() listener_config = self.config.get('listener') self.upload_url = listener_config['wake_word_upload']['url'] self.upload_disabled = listener_config['wake_word_upload']['disable'] self.wake_word_name = wake_word_recognizer.key_phrase self.overflow_exc = listener_config.get('overflow_exception', False) speech_recognition.Recognizer.__init__(self) self.wake_word_recognizer = wake_word_recognizer self.audio = pyaudio.PyAudio() self.multiplier = listener_config.get('multiplier') self.energy_ratio = listener_config.get('energy_ratio') # check the config for the flag to save wake words. self.save_utterances = listener_config.get('save_utterances', False) self.save_wake_words = listener_config.get('record_wake_words') self.saved_wake_words_dir = join(gettempdir(), 'mycroft_wake_words') self.upload_lock = Lock() self.filenames_to_upload = [] self.mic_level_file = os.path.join(get_ipc_directory(), "mic_level") # Signal statuses self._stop_signaled = False self._listen_triggered = False # The maximum audio in seconds to keep for transcribing a phrase # The wake word must fit in this time num_phonemes = wake_word_recognizer.num_phonemes len_phoneme = listener_config.get('phoneme_duration', 120) / 1000.0 self.TEST_WW_SEC = num_phonemes * len_phoneme self.SAVED_WW_SEC = max(3, self.TEST_WW_SEC) try: self.account_id = DeviceApi().get()['user']['uuid'] except (requests.RequestException, AttributeError): self.account_id = '0'
def upload_settings(self): """ Upload new setting values to the Selene Web UI. """ self.log.debug('upload_settings() started') try: settings_uploader = SettingsMetaUploader(self.root_dir, self.name) settings_uploader.api = DeviceApi() settings_uploader._load_settings_meta_file() settings_uploader._update_settings_meta() settings_uploader.settings_meta['skillMetadata']['sections'][0][ 'fields'][1]['value'] = self.settings.get('api_token', '') settings_uploader.settings_meta['skillMetadata']['sections'][0][ 'fields'][3]['value'] = self.settings.get('root_ca', '') settings_uploader._issue_api_call() self.log.debug( 'Setting values successfully synced to the Selene Web UI.') self.cancel_scheduled_event(name='Sync Settings') except Exception as e: self.log.exception('Unable to sync settings to the Selene Web UI \ due to an exception -\n{}'.format(e)) self.log.debug('upload_settings() completed')
def load_brain(self): if isfile(self.brain_path): self.kernel.bootstrap(brainFile = self.brain_path) else: aimls = listdir(self.aiml_path) for aiml in aimls: self.kernel.learn(os.path.join(self.aiml_path, aiml)) self.kernel.saveBrain(self.brain_path) device = DeviceApi().get() self.kernel.setBotPredicate("name", device["name"]) self.kernel.setBotPredicate("species", device["type"]) self.kernel.setBotPredicate("genus", "Mycroft") self.kernel.setBotPredicate("family", "virtual personal assistant") self.kernel.setBotPredicate("order", "artificial intelligence") self.kernel.setBotPredicate("class", "computer program") self.kernel.setBotPredicate("kingdom", "machine") self.kernel.setBotPredicate("hometown", "127.0.0.1") self.kernel.setBotPredicate("botmaster", "master") self.kernel.setBotPredicate("master", "the community") # IDEA: extract age from https://api.github.com/repos/MycroftAI/mycroft-core created_at date self.kernel.setBotPredicate("age", "2") return
class PairingSkill(MycroftSkill): poll_frequency = 10 # secs between checking server for activation def __init__(self): super(PairingSkill, self).__init__("PairingSkill") self.api = DeviceApi() self.data = None self.time_code_expires = None self.state = str(uuid4()) self.activator = None self.activator_lock = Lock() self.activator_cancelled = False self.counter_lock = Lock() self.count = -1 # for repeating pairing code. -1 = not running self.nato_dict = None self.mycroft_ready = False self.pair_dialog_lock = Lock() self.paired_dialog = 'pairing.paired' self.pairing_performed = False self.num_failed_codes = 0 def initialize(self): self.add_event("mycroft.not.paired", self.not_paired) self.nato_dict = self.translate_namedvalues('codes') # If the device isn't paired catch mycroft.ready to report # that the device is ready for use. # This assumes that the pairing skill is loaded as a priority skill # before the rest of the skills are loaded. if not is_paired(): self.add_event("mycroft.ready", self.handle_mycroft_ready) platform = self.config_core['enclosure'].get('platform', 'unknown') if platform in PLATFORMS_WITH_BUTTON: self.paired_dialog = 'pairing.paired' else: self.paired_dialog = 'pairing.paired.no.button' def handle_mycroft_ready(self, message): """Catch info that skills are loaded and ready.""" with self.pair_dialog_lock: if is_paired() and self.pairing_performed: self.speak_dialog(self.paired_dialog) else: self.mycroft_ready = True def not_paired(self, message): if not message.data.get('quiet', True): self.speak_dialog("pairing.not.paired") self.handle_pairing() @intent_handler(IntentBuilder("PairingIntent") .require("PairingKeyword").require("DeviceKeyword")) def handle_pairing(self, message=None): if check_remote_pairing(ignore_errors=True): # Already paired! Just tell user self.speak_dialog("already.paired") elif not self.data: # Kick off pairing... with self.counter_lock: if self.count > -1: # We snuck in to this handler somehow while the pairing # process is still being setup. Ignore it. self.log.debug("Ignoring call to handle_pairing") return # Not paired or already pairing, so start the process. self.count = 0 self.reload_skill = False # Prevent restart during the process self.log.debug("Kicking off pairing sequence") try: # Obtain a pairing code from the backend self.data = self.api.get_code(self.state) # Keep track of when the code was obtained. The codes expire # after 20 hours. self.time_code_expires = time.monotonic() + 72000 # 20 hours except Exception: time.sleep(10) # Call restart pairing here # Bail out after Five minutes (5 * 6 attempts at 10 seconds # interval) if self.num_failed_codes < 5 * 6: self.num_failed_codes += 1 self.abort_and_restart(quiet=True) else: self.end_pairing('connection.error') self.num_failed_codes = 0 return self.num_failed_codes = 0 # Reset counter on success mycroft.audio.wait_while_speaking() self.gui.show_page("pairing_start.qml", override_idle=True) self.speak_dialog("pairing.intro") self.enclosure.deactivate_mouth_events() self.enclosure.mouth_text("home.mycroft.ai ") # HACK this gives the Mark 1 time to scroll the address and # the user time to browse to the website. # TODO: mouth_text() really should take an optional parameter # to not scroll a second time. time.sleep(7) mycroft.audio.wait_while_speaking() if not self.activator: self.__create_activator() def check_for_activate(self): """Method is called every 10 seconds by Timer. Checks if user has activated the device yet on home.mycroft.ai and if not repeats the pairing code every 60 seconds. """ try: # Attempt to activate. If the user has completed pairing on the, # backend, this will succeed. Otherwise it throws and HTTPError() token = self.data.get("token") login = self.api.activate(self.state, token) # HTTPError() thrown # When we get here, the pairing code has been entered on the # backend and pairing can now be saved. # The following is kinda ugly, but it is really critical that we # get this saved successfully or we need to let the user know that # they have to perform pairing all over again at the website. try: IdentityManager.save(login) except Exception as e: self.log.debug("First save attempt failed: " + repr(e)) time.sleep(2) try: IdentityManager.save(login) except Exception as e2: # Something must be seriously wrong self.log.debug("Second save attempt failed: " + repr(e2)) self.abort_and_restart() if mycroft.audio.is_speaking(): # Assume speaking is the pairing code. Stop TTS of that. mycroft.audio.stop_speaking() self.enclosure.activate_mouth_events() # clears the display # Notify the system it is paired self.gui.show_page("pairing_done.qml", override_idle=False) self.bus.emit(Message("mycroft.paired", login)) self.pairing_performed = True with self.pair_dialog_lock: if self.mycroft_ready: # Tell user they are now paired self.speak_dialog(self.paired_dialog) mycroft.audio.wait_while_speaking() else: self.speak_dialog("wait.for.startup") mycroft.audio.wait_while_speaking() # Un-mute. Would have been muted during onboarding for a new # unit, and not dangerous to do if pairing was started # independently. self.bus.emit(Message("mycroft.mic.unmute", None)) # Send signal to update configuration self.bus.emit(Message("configuration.updated")) # Allow this skill to auto-update again self.reload_skill = True except HTTPError: # speak pairing code every 60th second with self.counter_lock: if self.count == 0: self.speak_code() self.count = (self.count + 1) % 6 if time.monotonic() > self.time_code_expires: # After 20 hours the token times out. Restart # the pairing process. with self.counter_lock: self.count = -1 self.data = None self.handle_pairing() else: # trigger another check in 10 seconds self.__create_activator() except Exception as e: self.log.debug("Unexpected error: " + repr(e)) self.abort_and_restart() def end_pairing(self, error_dialog): """Resets the pairing and don't restart it. Arguments: error_dialog: Reason for the ending of the pairing process. """ self.speak_dialog(error_dialog) self.bus.emit(Message("mycroft.mic.unmute", None)) self.data = None self.count = -1 def abort_and_restart(self, quiet=False): # restart pairing sequence self.log.debug("Aborting Pairing") self.enclosure.activate_mouth_events() if not quiet: self.speak_dialog("unexpected.error.restarting") # Reset state variables for a new pairing session with self.counter_lock: self.count = -1 self.activator = None self.data = None # Clear pairing code info self.log.info("Restarting pairing process") self.bus.emit(Message("mycroft.not.paired", data={'quiet': quiet})) def __create_activator(self): # Create a timer that will poll the backend in 10 seconds to see # if the user has completed the device registration process with self.activator_lock: if not self.activator_cancelled: self.activator = Timer(PairingSkill.poll_frequency, self.check_for_activate) self.activator.daemon = True self.activator.start() def speak_code(self): """Speak pairing code.""" code = self.data.get("code") self.log.info("Pairing code: " + code) data = {"code": '. '.join(map(self.nato_dict.get, code)) + '.'} # Make sure code stays on display self.enclosure.deactivate_mouth_events() self.enclosure.mouth_text(self.data.get("code")) self.gui['code'] = self.data.get("code") self.gui.show_page("pairing.qml", override_idle=True) self.speak_dialog("pairing.code", data) def shutdown(self): with self.activator_lock: self.activator_cancelled = True if self.activator: self.activator.cancel() if self.activator: self.activator.join()
def __init__(self, directory, name): super(SkillSettings, self).__init__() self.api = DeviceApi() self._device_identity = self.api.identity.uuid self.config = ConfigurationManager.get() self.name = name # set file paths self._settings_path = join(directory, 'settings.json') self._meta_path = join(directory, 'settingsmeta.json') self._api_path = "/" + self._device_identity + "/skill" self.is_alive = True self.loaded_hash = hash(str(self)) # if settingsmeta.json exists # this block of code is a control flow for # different scenarios that may arises with settingsmeta if isfile(self._meta_path): LOG.info("settingsmeta.json exist for {}".format(self.name)) settings_meta = self._load_settings_meta() hashed_meta = hash(str(settings_meta) + str(self._device_identity)) # check if hash is different from the saved hashed if self._is_new_hash(hashed_meta): LOG.info("looks like settingsmeta.json " + "has changed for {}".format(self.name)) # TODO: once the delete api for device is created uncomment if self._uuid_exist(): try: LOG.info("a uuid exist for {}".format(self.name) + " deleting old one") old_uuid = self._load_uuid() self._delete_metatdata(old_uuid) except Exception as e: LOG.info(e) LOG.info("sending settingsmeta.json for {}".format(self.name) + " to home.mycroft.ai") new_uuid = self._send_settings_meta(settings_meta, hashed_meta) self._save_uuid(new_uuid) self._save_hash(hashed_meta) else: # if hash is old found_in_backend = False settings = self._get_remote_settings() # checks backend if th settings have been deleted # through web ui for skill in settings: if skill["identifier"] == str(hashed_meta): found_in_backend = True # if it's been deleted from web ui # resend the settingsmeta.json if found_in_backend is False: LOG.info("seems like it got deleted from home... " + "sending settingsmeta.json for " + "{}".format(self.name)) new_uuid = self._send_settings_meta( settings_meta, hashed_meta) self._save_uuid(new_uuid) self._save_hash(hashed_meta) t = Timer(60, self._poll_skill_settings, [hashed_meta]) t.daemon = True t.start() self.load_skill_settings()
class SkillSettingsDownloader: """Manages download of skill settings. Performs settings download on a repeating Timer. If a change is seen the data is sent to the relevant skill. """ def __init__(self, bus): self.bus = bus self.continue_downloading = True self.last_download_result = {} self.remote_settings = None self.api = DeviceApi() self.download_timer = None def stop_downloading(self): """Stop synchronizing backend and core.""" self.continue_downloading = False if self.download_timer: self.download_timer.cancel() # TODO: implement as websocket def download(self): """Download the settings stored on the backend and check for changes""" if is_paired(): remote_settings = self._get_remote_settings() if remote_settings: settings_changed = self.last_download_result != remote_settings if settings_changed: LOG.debug('Skill settings changed since last download') self._emit_settings_change_events(remote_settings) self.last_download_result = remote_settings else: LOG.debug('No skill settings changes since last download') else: LOG.debug('Settings not downloaded - device is not paired') # If this method is called outside of the timer loop, ensure the # existing timer is canceled before starting a new one. if self.download_timer: self.download_timer.cancel() if self.continue_downloading: self.download_timer = Timer(ONE_MINUTE, self.download) self.download_timer.daemon = True self.download_timer.start() def _get_remote_settings(self): """Get the settings for this skill from the server Returns: skill_settings (dict or None): returns a dict on success, else None """ try: remote_settings = self.api.get_skill_settings() except Exception: LOG.exception('Failed to download remote settings from server.') remote_settings = None return remote_settings def _emit_settings_change_events(self, remote_settings): """Emit changed settings events for each affected skill.""" for skill_gid, skill_settings in remote_settings.items(): settings_changed = False try: previous_settings = self.last_download_result.get(skill_gid) except Exception: LOG.exception('error occurred handling setting change events') else: if previous_settings != skill_settings: settings_changed = True if settings_changed: log_msg = 'Emitting skill.settings.change event for skill {} ' LOG.info(log_msg.format(skill_gid)) message = Message('mycroft.skills.settings.changed', data={skill_gid: skill_settings}) self.bus.emit(message)
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(bus) 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(dialog.get("message_synching.clock")) # Force a sync of the local clock with the internet config = Configuration.get() platform = config['enclosure'].get("platform", "unknown") if platform in ['mycroft_mark_1', 'picroft']: bus.wait_for_response(Message('system.ntp.sync'), 'system.ntp.sync.complete', 15) if not is_paired(): try_update_system(platform) # Check if the time skewed significantly. If so, reboot skew = abs((time.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'. # data = {'utterance': dialog.get("time.changed.reboot")} bus.emit(Message("speak", data)) wait_while_speaking() # provide visual indicators of the reboot enclosure.mouth_text(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 bus.emit(Message("system.reboot")) return else: bus.emit(Message("enclosure.mouth.reset")) time.sleep(0.5) enclosure.eyes_color(189, 183, 107) # dark khaki enclosure.mouth_text(dialog.get("message_loading.skills")) bus.emit(Message('mycroft.internet.connected')) # check for pairing, if not automatically start pairing try: if not is_paired(ignore_errors=False): payload = {'utterances': ["pair my device"], 'lang': "en-us"} bus.emit(Message("recognizer_loop:utterance", payload)) else: from mycroft.api import DeviceApi api = DeviceApi() api.update_version() except BackendDown: data = {'utterance': dialog.get("backend.down")} bus.emit(Message("speak", data)) bus.emit(Message("backend.down")) else: thread = Timer(1, check_connection) thread.daemon = True thread.start()
class SkillSettings(dict): """ A dictionary that can easily be save to a file, serialized as json. It also syncs to the backend for skill settings Args: directory (str): Path to storage directory name (str): user readable name associated with the settings """ def __init__(self, directory, name): super(SkillSettings, self).__init__() # when skills try to instantiate settings # in __init__, it can erase the settings saved # on disk (settings.json). So this prevents that # This is set to true in core.py after skill init self.allow_overwrite = False self.api = DeviceApi() self.config = ConfigurationManager.get() self.name = name self.directory = directory # set file paths self._settings_path = join(directory, 'settings.json') self._meta_path = join(directory, 'settingsmeta.json') self.is_alive = True self.loaded_hash = hash(str(self)) self._complete_intialization = False self._device_identity = None self._api_path = None self._user_identity = None self.changed_callback = None # if settingsmeta exist if isfile(self._meta_path): self._poll_skill_settings() def set_changed_callback(self, callback): """ Set callback to perform when server settings have changed. callback: function/method to call when settings have changed """ self.changed_callback = callback # TODO: break this up into two classes def initialize_remote_settings(self): """ initializes the remote settings to the server """ # if settingsmeta.json exists (and is valid) # this block of code is a control flow for # different scenarios that may arises with settingsmeta self.load_skill_settings_from_file() # loads existing settings.json settings_meta = self._load_settings_meta() if not settings_meta: return self._device_identity = self.api.identity.uuid self._api_path = "/" + self._device_identity + "/skill" self._user_identity = self.api.get()['user']['uuid'] LOG.info("settingsmeta.json exist for {}".format(self.name)) hashed_meta = self._get_meta_hash(str(settings_meta)) skill_settings = self._request_other_settings(hashed_meta) # if hash is new then there is a diff version of settingsmeta if self._is_new_hash(hashed_meta): # first look at all other devices on user account to see # if the settings exist. if it does then sync with device if skill_settings: # not_owner flags that this settings is loaded from # another device. If a skill settings doesn't have # not_owner, then the skill is created from that device self['not_owner'] = True self.save_skill_settings(skill_settings) else: # upload skill settings if uuid = self._load_uuid() if uuid is not None: self._delete_metadata(uuid) self._upload_meta(settings_meta, hashed_meta) else: # hash is not new if skill_settings is not None: self['not_owner'] = True self.save_skill_settings(skill_settings) else: settings = self._request_my_settings(hashed_meta) if settings is None: LOG.info("seems like it got deleted from home... " "sending settingsmeta.json for " "{}".format(self.name)) self._upload_meta(settings_meta, hashed_meta) else: self.save_skill_settings(settings) self._complete_intialization = True @property def _is_stored(self): return hash(str(self)) == self.loaded_hash def __getitem__(self, key): """ Get key """ return super(SkillSettings, self).__getitem__(key) def __setitem__(self, key, value): """ Add/Update key. """ if self.allow_overwrite or key not in self: return super(SkillSettings, self).__setitem__(key, value) def _load_settings_meta(self): """ Loads settings metadata from skills path. """ if isfile(self._meta_path): try: with open(self._meta_path) as f: data = json.load(f) return data except Exception as e: LOG.error("Failed to load setting file: " + self._meta_path) LOG.error(repr(e)) return None else: LOG.info("settingemeta.json does not exist") return None def _send_settings_meta(self, settings_meta): """ Send settingsmeta.json to the server. Args: settings_meta (dict): dictionary of the current settings meta Returns: str: uuid, a unique id for the setting meta data """ try: uuid = self._put_metadata(settings_meta) return uuid except Exception as e: LOG.error(e) return None def save_skill_settings(self, skill_settings): """ takes skill object and save onto self Args: settings (dict): skill """ if self._is_new_hash(skill_settings['identifier']): self._save_uuid(skill_settings['uuid']) self._save_hash(skill_settings['identifier']) sections = skill_settings['skillMetadata']['sections'] for section in sections: for field in section["fields"]: if "name" in field and "value" in field: self[field['name']] = field['value'] self.store() def _load_uuid(self): """ Loads uuid Returns: str: uuid of the previous settingsmeta """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') uuid = None if isfile(uuid_file): with open(uuid_file, 'r') as f: uuid = f.read() return uuid def _save_uuid(self, uuid): """ Saves uuid. Args: str: uuid, unique id of new settingsmeta """ LOG.info("saving uuid {}".format(str(uuid))) directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') with open(uuid_file, 'w') as f: f.write(str(uuid)) def _uuid_exist(self): """ Checks if there is an uuid file. Returns: bool: True if uuid file exist False otherwise """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') return isfile(uuid_file) def _migrate_settings(self, settings_meta): """ sync settings.json and settingsmeta.json in memory """ meta = settings_meta.copy() self.load_skill_settings_from_file() sections = meta['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section['fields']): if 'name' in field: if field["name"] in self: sections[i]['fields'][j]['value'] = \ str(self.__getitem__(field['name'])) meta['skillMetadata']['sections'] = sections return meta def _upload_meta(self, settings_meta, hashed_meta): """ uploads the new meta data to settings with settings migration Args: settings_meta (dict): settingsmeta.json hashed_meta (str): {skill-folder}-settinsmeta.json """ LOG.info("sending settingsmeta.json for {}".format(self.name) + " to servers") meta = self._migrate_settings(settings_meta) meta['identifier'] = str(hashed_meta) response = self._send_settings_meta(meta) if response: self._save_uuid(response['uuid']) if 'not_owner' in self: del self['not_owner'] self._save_hash(hashed_meta) def _delete_old_meta(self): """" Deletes the old meta data """ if self._uuid_exist(): try: LOG.info("a uuid exist for {}".format(self.name) + " deleting old one") old_uuid = self._load_uuid() self._delete_metatdata(old_uuid) except Exception as e: LOG.info(e) def hash(self, str): """ md5 hasher for consistency across cpu architectures """ return hashlib.md5(str).hexdigest() def _get_meta_hash(self, settings_meta): """ Get's the hash of skill Args: settings_meta (str): stringified settingsmeta Returns: _hash (str): hashed to identify skills """ _hash = self.hash(str(settings_meta) + str(self._user_identity)) return "{}--{}".format(basename(self.directory), _hash) def _save_hash(self, hashed_meta): """ Saves hashed_meta to settings directory. Args: hashed_meta (int): hash of new settingsmeta """ LOG.info("saving hash {}".format(str(hashed_meta))) directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) hash_file = join(directory, 'hash') with open(hash_file, 'w') as f: f.write(str(hashed_meta)) def _is_new_hash(self, hashed_meta): """ checks if the stored hash is the same as current. if the hashed file does not exist, usually in the case of first load, then the create it and return True Args: hashed_meta (int): hash of metadata and uuid of device Returns: bool: True if hash is new, otherwise False """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) hash_file = join(directory, 'hash') if isfile(hash_file): with open(hash_file, 'r') as f: current_hash = f.read() return False if current_hash == str(hashed_meta) else True return True def update_remote(self): """ update settings state from server """ skills_settings = None settings_meta = self._load_settings_meta() if settings_meta is None: return hashed_meta = self._get_meta_hash(settings_meta) if self.get('not_owner'): skills_settings = self._request_other_settings(hashed_meta) if not skills_settings: skills_settings = self._request_my_settings(hashed_meta) if skills_settings is not None: self.save_skill_settings(skills_settings) self.store() else: settings_meta = self._load_settings_meta() self._upload_meta(settings_meta, hashed_meta) def _poll_skill_settings(self): """ If identifier exists for this skill poll to backend to request settings and store it if it changes TODO: implement as websocket Args: hashed_meta (int): the hashed identifier """ try: if not self._complete_intialization: self.initialize_remote_settings() if not self._complete_intialization: return # unable to do remote sync else: original = hash(str(self)) self.update_remote() # Call callback for updated settings if self.changed_callback and hash(str(self)) != original: self.changed_callback() except Exception as e: LOG.error(e) LOG.exception("") # this is used in core so do not delete! if self.is_alive: # continues to poll settings every 60 seconds t = Timer(60, self._poll_skill_settings) t.daemon = True t.start() def load_skill_settings_from_file(self): """ If settings.json exist, open and read stored values into self """ if isfile(self._settings_path): with open(self._settings_path) as f: try: json_data = json.load(f) for key in json_data: self[key] = json_data[key] except Exception as e: # TODO: Show error on webUI. Dev will have to fix # metadata to be able to edit later. LOG.error(e) def _request_my_settings(self, identifier): """ Get skill settings for this device associated with the identifier Args: identifier (str): a hashed_meta Returns: skill_settings (dict or None): returns a dict if matches """ LOG.info("getting skill settings from " "server for {}".format(self.name)) settings = self._request_settings() # this loads the settings into memory for use in self.store for skill_settings in settings: if skill_settings['identifier'] == identifier: self._remote_settings = skill_settings return skill_settings return None def _request_settings(self): """ Get all skill settings for this device from server. Returns: dict: dictionary with settings collected from the server. """ settings = self.api.request({"method": "GET", "path": self._api_path}) settings = [skills for skills in settings if skills is not None] return settings def _request_other_settings(self, identifier): """ Retrieves user skill from other devices by identifier (hashed_meta) Args: indentifier (str): identifier for this skill Returns: settings (dict or None): returns the settings if true else None """ LOG.info("syncing settings with other devices " "from server for {}".format(self.name)) path = \ "/" + self._device_identity + "/userSkill?identifier=" + identifier user_skill = self.api.request({"method": "GET", "path": path}) if len(user_skill) == 0: return None else: return user_skill[0] def _put_metadata(self, settings_meta): """ PUT settingsmeta to backend to be configured in server. used in place of POST and PATCH. Args: settings_meta (dict): dictionary of the current settings meta data """ return self.api.request({ "method": "PUT", "path": self._api_path, "json": settings_meta }) def _delete_metadata(self, uuid): """ Deletes the current skill metadata Args: uuid (str): unique id of the skill """ try: LOG.info("deleting metadata") self.api.request({ "method": "DELETE", "path": self._api_path + "/{}".format(uuid) }) except Exception as e: LOG.error(e) LOG.info("cannot delete metadata because this" "device is not original uploader of skill") @property def _should_upload_from_change(self): changed = False if hasattr(self, '_remote_settings'): sections = self._remote_settings['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section['fields']): if 'name' in field: # Ensure that the field exists in settings and that # it has a value to compare if (field["name"] in self and 'value' in sections[i]['fields'][j]): remote_val = sections[i]['fields'][j]["value"] self_val = self.get(field['name']) if str(remote_val) != str(self_val): changed = True if self.get('not_owner'): changed = False return changed def store(self, force=False): """ Store dictionary to file if a change has occured. Args: force: Force write despite no change """ if force or not self._is_stored: with open(self._settings_path, 'w') as f: json.dump(self, f) self.loaded_hash = hash(str(self)) if self._should_upload_from_change: settings_meta = self._load_settings_meta() hashed_meta = self._get_meta_hash(settings_meta) uuid = self._load_uuid() if uuid is not None: LOG.info("deleting meta data for {}".format(self.name)) self._delete_metadata(uuid) self._upload_meta(settings_meta, hashed_meta)
class SkillSettings(dict): """ Dictionary that can easily be saved to a file, serialized as json. It also syncs to the backend for skill settings Args: directory (str): Path to storage directory name (str): user readable name associated with the settings no_upload (bool): True if the upload to mycroft servers should be disabled. """ def __init__(self, directory, name): super(SkillSettings, self).__init__() # when skills try to instantiate settings # in __init__, it can erase the settings saved # on disk (settings.json). So this prevents that # This is set to true in core.py after skill init self.allow_overwrite = False self.api = DeviceApi() self.config = ConfigurationManager.get() self.name = name # set file paths self._settings_path = join(directory, 'settings.json') self._meta_path = _get_meta_path(directory) self._directory = directory self.is_alive = True self.loaded_hash = hash(json.dumps(self, sort_keys=True)) self._complete_intialization = False self._device_identity = None self._api_path = None self._user_identity = None self.changed_callback = None self._poll_timer = None self._blank_poll_timer = None self._is_alive = True # Add Information extracted from the skills-meta.json entry for the # skill. skill_gid, disp_name = build_global_id(self._directory, self.config) self.__skill_gid = skill_gid self.display_name = disp_name # if settingsmeta exist if self._meta_path: self._poll_skill_settings() # if not disallowed by user upload an entry for all skills installed elif self.config['skills']['upload_skill_manifest']: self._blank_poll_timer = Timer(1, self._init_blank_meta) self._blank_poll_timer.daemon = True self._blank_poll_timer.start() @property def skill_gid(self): """ Finalizes the skill gid to include device uuid if needed. """ if is_paired(): return self.__skill_gid.replace('@|', '@{}|'.format( DeviceApi().identity.uuid)) else: return self.__skill_gid def __hash__(self): """ Simple object unique hash. """ return hash(str(id(self)) + self.name) def run_poll(self, _=None): """Immediately poll the web for new skill settings""" if self._poll_timer: self._poll_timer.cancel() self._poll_skill_settings() def stop_polling(self): self._is_alive = False if self._poll_timer: self._poll_timer.cancel() if self._blank_poll_timer: self._blank_poll_timer.cancel() def set_changed_callback(self, callback): """ Set callback to perform when server settings have changed. Args: callback: function/method to call when settings have changed """ self.changed_callback = callback # TODO: break this up into two classes def initialize_remote_settings(self): """ initializes the remote settings to the server """ # if the settingsmeta file exists (and is valid) # this block of code is a control flow for # different scenarios that may arises with settingsmeta self.load_skill_settings_from_file() # loads existing settings.json settings_meta = self._load_settings_meta() if not settings_meta: return if not is_paired(): return self._device_identity = self.api.identity.uuid self._api_path = "/" + self._device_identity + "/skill" try: self._user_identity = self.api.get()['user']['uuid'] except RequestException: return settings = self._request_my_settings(self.skill_gid) if settings: self.save_skill_settings(settings) # TODO if this skill_gid is not a modified version check if a modified # version exists on the server and delete it # Always try to upload settingsmeta on startup self._upload_meta(settings_meta, self.skill_gid) self._complete_intialization = True @property def _is_stored(self): return hash(json.dumps(self, sort_keys=True)) == self.loaded_hash def __getitem__(self, key): """ Get key """ return super(SkillSettings, self).__getitem__(key) def __setitem__(self, key, value): """ Add/Update key. """ if self.allow_overwrite or key not in self: return super(SkillSettings, self).__setitem__(key, value) def _load_settings_meta(self): """ Load settings metadata from the skill folder. If no settingsmeta exists a basic settingsmeta will be created containing a basic identifier. Returns: (dict) settings meta """ if self._meta_path and os.path.isfile(self._meta_path): _, ext = os.path.splitext(self._meta_path) json_file = True if ext.lower() == ".json" else False try: with open(self._meta_path, encoding='utf-8') as f: if json_file: data = json.load(f) else: data = yaml.safe_load(f) except Exception as e: LOG.error("Failed to load setting file: " + self._meta_path) LOG.error(repr(e)) data = {} else: data = {} # Insert skill_gid and display_name data['skill_gid'] = self.skill_gid data['display_name'] = (self.display_name or data.get('name') or display_name(self.name)) # Backwards compatibility: if 'name' not in data: data['name'] = data['display_name'] return data def _send_settings_meta(self, settings_meta): """ Send settingsmeta to the server. Args: settings_meta (dict): dictionary of the current settings meta Returns: dict: uuid, a unique id for the setting meta data """ try: uuid = self.api.upload_skill_metadata( self._type_cast(settings_meta, to_platform='web')) return uuid except HTTPError as e: if e.response.status_code in [422, 500, 501]: LOG.info(e.response.status_code) raise DelayRequest else: LOG.error(e) return None except Exception as e: LOG.error(e) return None def save_skill_settings(self, skill_settings): """ Takes skill object and save onto self Args: skill_settings (dict): skill """ if 'skillMetadata' in skill_settings: sections = skill_settings['skillMetadata']['sections'] for section in sections: for field in section["fields"]: if "name" in field and "value" in field: # Bypass the change lock to allow server to update # during skill init super(SkillSettings, self).__setitem__(field['name'], field['value']) self.store() def _migrate_settings(self, settings_meta): """ sync settings.json and settingsmeta in memory """ meta = settings_meta.copy() if 'skillMetadata' not in meta: return meta self.load_skill_settings_from_file() sections = meta['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section['fields']): if 'name' in field: if field["name"] in self: sections[i]['fields'][j]['value'] = \ str(self.__getitem__(field['name'])) meta['skillMetadata']['sections'] = sections return meta def _upload_meta(self, settings_meta, identifier): """ uploads the new meta data to settings with settings migration Args: settings_meta (dict): settingsmeta.json or settingsmeta.yaml identifier (str): identifier for skills meta data """ LOG.debug('Uploading settings meta for {}'.format(identifier)) meta = self._migrate_settings(settings_meta) meta['identifier'] = identifier response = self._send_settings_meta(meta) def hash(self, string): """ md5 hasher for consistency across cpu architectures """ return hashlib.md5(bytes(string, 'utf-8')).hexdigest() def update_remote(self): """ update settings state from server """ settings_meta = self._load_settings_meta() if settings_meta is None: return # Get settings skills_settings = self._request_my_settings(self.skill_gid) if skills_settings is not None: self.save_skill_settings(skills_settings) else: LOG.debug("No Settings on server for {}".format(self.skill_gid)) # Settings meta doesn't exist on server push them settings_meta = self._load_settings_meta() self._upload_meta(settings_meta, self.skill_gid) def _init_blank_meta(self): """ Send blank settingsmeta to remote. """ try: if not is_paired() and self.is_alive: self._blank_poll_timer = Timer(60, self._init_blank_meta) self._blank_poll_timer.daemon = True self._blank_poll_timer.start() else: self.initialize_remote_settings() except DelayRequest: # Delay 5 minutes and retry self._blank_poll_timer = Timer(60 * 5, self._init_blank_meta) self._blank_poll_timer.daemon = True self._blank_poll_timer.start() except Exception as e: LOG.exception('Failed to send blank meta: {}'.format(repr(e))) def _poll_skill_settings(self): """ If identifier exists for this skill poll to backend to request settings and store it if it changes TODO: implement as websocket """ delay = 1 original = hash(str(self)) try: if not is_paired(): pass elif not self._complete_intialization: self.initialize_remote_settings() else: self.update_remote() except DelayRequest: LOG.info('{}: Delaying next settings fetch'.format(self.name)) delay = 5 except Exception as e: LOG.exception('Failed to fetch skill settings: {}'.format(repr(e))) finally: # Call callback for updated settings if self._complete_intialization: if self.changed_callback and hash(str(self)) != original: self.changed_callback() if self._poll_timer: self._poll_timer.cancel() if not self._is_alive: return # continues to poll settings every minute self._poll_timer = Timer(delay * 60, self._poll_skill_settings) self._poll_timer.daemon = True self._poll_timer.start() def load_skill_settings_from_file(self): """ If settings.json exist, open and read stored values into self """ if isfile(self._settings_path): with open(self._settings_path) as f: try: json_data = json.load(f) for key in json_data: self[key] = json_data[key] except Exception as e: # TODO: Show error on webUI. Dev will have to fix # metadata to be able to edit later. LOG.error(e) def _type_cast(self, settings_meta, to_platform): """ Tranform data type to be compatible with Home and/or Core. e.g. Web to core "true" => True, "1.4" => 1.4 core to Web False => "false' Args: settings_meta (dict): skills object to_platform (str): platform to convert compatible data types to Returns: dict: skills object """ meta = settings_meta.copy() if 'skillMetadata' not in settings_meta: return meta sections = meta['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section.get('fields', [])): _type = field.get('type') if _type == 'checkbox': value = field.get('value') if to_platform == 'web': if value is True or value == 'True': sections[i]['fields'][j]['value'] = 'true' elif value is False or value == 'False': sections[i]['fields'][j]['value'] = 'false' elif to_platform == 'core': if value == 'true' or value == 'True': sections[i]['fields'][j]['value'] = True elif value == 'false' or value == 'False': sections[i]['fields'][j]['value'] = False elif _type == 'number': value = field.get('value') if to_platform == 'core': if "." in value: sections[i]['fields'][j]['value'] = float(value) else: sections[i]['fields'][j]['value'] = int(value) elif to_platform == 'web': sections[i]['fields'][j]['value'] = str(value) meta['skillMetadata']['sections'] = sections return meta def _request_my_settings(self, identifier): """ Get skill settings for this device associated with the identifier Args: identifier (str): a hashed_meta Returns: skill_settings (dict or None): returns a dict if matches """ settings = self._request_settings() if settings: # this loads the settings into memory for use in self.store for skill_settings in settings: if skill_settings['identifier'] == identifier: LOG.debug("Fetched settings for {}".format(identifier)) skill_settings = \ self._type_cast(skill_settings, to_platform='core') self._remote_settings = skill_settings return skill_settings return None def _request_settings(self): """ Get all skill settings for this device from server. Returns: dict: dictionary with settings collected from the server. """ try: settings = self.api.get_skill_settings() except RequestException: return None settings = [skills for skills in settings if skills is not None] return settings @property def _should_upload_from_change(self): changed = False if (hasattr(self, '_remote_settings') and 'skillMetadata' in self._remote_settings): sections = self._remote_settings['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section['fields']): if 'name' in field: # Ensure that the field exists in settings and that # it has a value to compare if (field["name"] in self and 'value' in sections[i]['fields'][j]): remote_val = sections[i]['fields'][j]["value"] self_val = self.get(field['name']) if str(remote_val) != str(self_val): changed = True if self.get('not_owner'): changed = False return changed def store(self, force=False): """ Store dictionary to file if a change has occured. Args: force: Force write despite no change """ if force or not self._is_stored: with open(self._settings_path, 'w') as f: json.dump(self, f) self.loaded_hash = hash(json.dumps(self, sort_keys=True)) if self._should_upload_from_change: settings_meta = self._load_settings_meta() self._upload_meta(settings_meta, self.skill_gid)
def initialize(self): self.mute = str(self.settings.get('MuteIt', '')) if (self.mute == 'True') or (self.mute == 'true'): try: self.mixer = Mixer() msg = "Telegram Messages will temporary Mute Mycroft" logger.info(msg) except: msg = "There is a problem with alsa audio, mute is not working!" logger.info( "There is a problem with alsaaudio, mute is not working!") self.sendMycroftSay(msg) self.mute = 'false' else: logger.info("Telegram: Muting is off") self.mute = "false" self.add_event('telegram-skill:response', self.sendHandler) self.add_event('speak', self.responseHandler) user_id1 = self.settings.get('TeleID1', '') user_id2 = self.settings.get('TeleID2', '') #user_id3 = self.settings.get('TeleID3', '') # makes web-settings too crouded #user_id4 = self.settings.get('TeleID4', '') # makes web-settings too crouded self.chat_whitelist = [ user_id1, user_id2 ] #,user_id3,user_id4] # makes web-settings too crouded # Get Bot Token from settings.json UnitName = DeviceApi().get()['name'] MyCroftDevice1 = self.settings.get('MDevice1', '') MyCroftDevice2 = self.settings.get('MDevice2', '') self.bottoken = "" if MyCroftDevice1 == UnitName: logger.debug("Found MyCroft Unit 1: " + UnitName) self.bottoken = self.settings.get('TeleToken1', '') elif MyCroftDevice2 == UnitName: logger.debug("Found MyCroft Unit 2: " + UnitName) self.bottoken = self.settings.get('TeleToken2', '') else: msg = ( "No or incorrect Device Name specified! Your DeviceName is: " + UnitName) logger.info(msg) self.sendMycroftSay(msg) # Connection to Telegram API self.telegram_updater = Updater( token=self.bottoken) # get telegram Updates self.telegram_dispatcher = self.telegram_updater.dispatcher receive_handler = MessageHandler( Filters.text, self.TelegramMessages ) # TODO: Make audio Files as Input possible: Filters.text | Filters.audio self.telegram_dispatcher.add_handler(receive_handler) self.telegram_updater.start_polling( clean=True) # start clean and look for messages wbot = telegram.Bot(token=self.bottoken) global loaded # get global variable if loaded == 0: # check if bot is just started loaded = 1 # make sure that users gets this message only once bot is newly loaded if self.mute == "false": msg = "Telegram Skill is loaded" self.sendMycroftSay(msg) loadedmessage = "Telegram-Skill on Mycroft Unit \"" + UnitName + "\" is loaded and ready to use!" # give User a nice message try: wbot.send_message( chat_id=user_id1, text=loadedmessage) # send welcome message to user 1 except: pass try: wbot.send_message( chat_id=user_id2, text=loadedmessage) # send welcome message to user 2 except: pass
class PairingSkill(MycroftSkill): def __init__(self): super(PairingSkill, self).__init__("PairingSkill") self.api = DeviceApi() self.data = None self.state = str(uuid4()) self.delay = 10 self.activator = None def initialize(self): self.load_data_files(dirname(__file__)) intent = IntentBuilder("PairingIntent") \ .require("PairingKeyword").require("DeviceKeyword").build() self.register_intent(intent, self.handle_pairing) self.emitter.on("mycroft.not.paired", self.not_paired) def not_paired(self, message): self.speak_dialog("pairing.not.paired") self.handle_pairing() def handle_pairing(self, message=None): if self.is_paired(): self.speak_dialog("pairing.paired") elif self.data: self.speak_code() else: self.data = self.api.get_code(self.state) self.enclosure.deactivate_mouth_events() self.enclosure.mouth_text(self.data.get("code")) self.speak_code() self.activator = Timer(self.delay, self.activate) self.activator.start() def activate(self): try: token = self.data.get("token") login = self.api.activate(self.state, token) self.enclosure.activate_mouth_events() self.speak_dialog("pairing.paired") IdentityManager.save(login) self.emitter.emit(Message("mycroft.paired", login)) except: self.data["expiration"] -= self.delay if self.data.get("expiration") <= 0: self.data = None self.handle_pairing() else: self.activator = Timer(self.delay, self.activate) self.activator.start() def is_paired(self): try: device = self.api.find() except: device = None return device is not None def speak_code(self): code = self.data.get("code") self.log.info("Pairing code: " + code) data = {"code": '. '.join(code).replace("0", "zero")} self.speak_dialog("pairing.code", data) def stop(self): pass
class SkillSettings(dict): """ A dictionary that can easily be save to a file, serialized as json. It also syncs to the backend for skill settings Args: directory (str): Path to storage directory name (str): user readable name associated with the settings """ def __init__(self, directory, name): super(SkillSettings, self).__init__() # when skills try to instantiate settings # in __init__, it can erase the settings saved # on disk (settings.json). So this prevents that # This is set to true in core.py after skill init self.allow_overwrite = False self.api = DeviceApi() self.config = ConfigurationManager.get() self.name = name self.directory = directory # set file paths self._settings_path = join(directory, 'settings.json') self._meta_path = join(directory, 'settingsmeta.json') self.is_alive = True self.loaded_hash = hash(str(self)) self._complete_intialization = False self._device_identity = None self._api_path = None self._user_identity = None self.changed_callback = None # if settingsmeta exist if isfile(self._meta_path): self._poll_skill_settings() def set_changed_callback(self, callback): """ Set callback to perform when server settings have changed. callback: function/method to call when settings have changed """ self.changed_callback = callback # TODO: break this up into two classes def initialize_remote_settings(self): """ initializes the remote settings to the server """ # if settingsmeta.json exists (and is valid) # this block of code is a control flow for # different scenarios that may arises with settingsmeta self.load_skill_settings_from_file() # loads existing settings.json settings_meta = self._load_settings_meta() if not settings_meta: return self._device_identity = self.api.identity.uuid self._api_path = "/" + self._device_identity + "/skill" self._user_identity = self.api.get()['user']['uuid'] LOG.info("settingsmeta.json exist for {}".format(self.name)) hashed_meta = self._get_meta_hash(str(settings_meta)) skill_settings = self._request_other_settings(hashed_meta) # if hash is new then there is a diff version of settingsmeta if self._is_new_hash(hashed_meta): # first look at all other devices on user account to see # if the settings exist. if it does then sync with device if skill_settings: # not_owner flags that this settings is loaded from # another device. If a skill settings doesn't have # not_owner, then the skill is created from that device self['not_owner'] = True self.save_skill_settings(skill_settings) else: # upload skill settings if uuid = self._load_uuid() if uuid is not None: self._delete_metadata(uuid) self._upload_meta(settings_meta, hashed_meta) else: # hash is not new if skill_settings is not None: self['not_owner'] = True self.save_skill_settings(skill_settings) else: settings = self._request_my_settings(hashed_meta) if settings is None: LOG.info("seems like it got deleted from home... " "sending settingsmeta.json for " "{}".format(self.name)) self._upload_meta(settings_meta, hashed_meta) else: self.save_skill_settings(settings) self._complete_intialization = True @property def _is_stored(self): return hash(str(self)) == self.loaded_hash def __getitem__(self, key): """ Get key """ return super(SkillSettings, self).__getitem__(key) def __setitem__(self, key, value): """ Add/Update key. """ if self.allow_overwrite or key not in self: return super(SkillSettings, self).__setitem__(key, value) def _load_settings_meta(self): """ Loads settings metadata from skills path. """ if isfile(self._meta_path): try: with open(self._meta_path) as f: data = json.load(f) return data except Exception as e: LOG.error("Failed to load setting file: "+self._meta_path) LOG.error(repr(e)) return None else: LOG.info("settingemeta.json does not exist") return None def _send_settings_meta(self, settings_meta): """ Send settingsmeta.json to the server. Args: settings_meta (dict): dictionary of the current settings meta Returns: str: uuid, a unique id for the setting meta data """ try: uuid = self._put_metadata(settings_meta) return uuid except Exception as e: LOG.error(e) return None def save_skill_settings(self, skill_settings): """ takes skill object and save onto self Args: settings (dict): skill """ if self._is_new_hash(skill_settings['identifier']): self._save_uuid(skill_settings['uuid']) self._save_hash(skill_settings['identifier']) sections = skill_settings['skillMetadata']['sections'] for section in sections: for field in section["fields"]: if "name" in field and "value" in field: self[field['name']] = field['value'] self.store() def _load_uuid(self): """ Loads uuid Returns: str: uuid of the previous settingsmeta """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') uuid = None if isfile(uuid_file): with open(uuid_file, 'r') as f: uuid = f.read() return uuid def _save_uuid(self, uuid): """ Saves uuid. Args: str: uuid, unique id of new settingsmeta """ LOG.info("saving uuid {}".format(str(uuid))) directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') with open(uuid_file, 'w') as f: f.write(str(uuid)) def _uuid_exist(self): """ Checks if there is an uuid file. Returns: bool: True if uuid file exist False otherwise """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') return isfile(uuid_file) def _migrate_settings(self, settings_meta): """ sync settings.json and settingsmeta.json in memory """ meta = settings_meta.copy() self.load_skill_settings_from_file() sections = meta['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section['fields']): if 'name' in field: if field["name"] in self: sections[i]['fields'][j]['value'] = \ str(self.__getitem__(field['name'])) meta['skillMetadata']['sections'] = sections return meta def _upload_meta(self, settings_meta, hashed_meta): """ uploads the new meta data to settings with settings migration Args: settings_meta (dict): settingsmeta.json hashed_meta (str): {skill-folder}-settinsmeta.json """ LOG.info("sending settingsmeta.json for {}".format(self.name) + " to servers") meta = self._migrate_settings(settings_meta) meta['identifier'] = str(hashed_meta) response = self._send_settings_meta(meta) if response: self._save_uuid(response['uuid']) if 'not_owner' in self: del self['not_owner'] self._save_hash(hashed_meta) def _delete_old_meta(self): """" Deletes the old meta data """ if self._uuid_exist(): try: LOG.info("a uuid exist for {}".format(self.name) + " deleting old one") old_uuid = self._load_uuid() self._delete_metatdata(old_uuid) except Exception as e: LOG.info(e) def hash(self, str): """ md5 hasher for consistency across cpu architectures """ return hashlib.md5(str).hexdigest() def _get_meta_hash(self, settings_meta): """ Get's the hash of skill Args: settings_meta (str): stringified settingsmeta Returns: _hash (str): hashed to identify skills """ _hash = self.hash(str(settings_meta) + str(self._user_identity)) return "{}--{}".format(basename(self.directory), _hash) def _save_hash(self, hashed_meta): """ Saves hashed_meta to settings directory. Args: hashed_meta (int): hash of new settingsmeta """ LOG.info("saving hash {}".format(str(hashed_meta))) directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) hash_file = join(directory, 'hash') with open(hash_file, 'w') as f: f.write(str(hashed_meta)) def _is_new_hash(self, hashed_meta): """ checks if the stored hash is the same as current. if the hashed file does not exist, usually in the case of first load, then the create it and return True Args: hashed_meta (int): hash of metadata and uuid of device Returns: bool: True if hash is new, otherwise False """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) hash_file = join(directory, 'hash') if isfile(hash_file): with open(hash_file, 'r') as f: current_hash = f.read() return False if current_hash == str(hashed_meta) else True return True def update_remote(self): """ update settings state from server """ skills_settings = None settings_meta = self._load_settings_meta() if settings_meta is None: return hashed_meta = self._get_meta_hash(settings_meta) if self.get('not_owner'): skills_settings = self._request_other_settings(hashed_meta) if not skills_settings: skills_settings = self._request_my_settings(hashed_meta) if skills_settings is not None: self.save_skill_settings(skills_settings) self.store() else: settings_meta = self._load_settings_meta() self._upload_meta(settings_meta, hashed_meta) def _poll_skill_settings(self): """ If identifier exists for this skill poll to backend to request settings and store it if it changes TODO: implement as websocket Args: hashed_meta (int): the hashed identifier """ try: if not self._complete_intialization: self.initialize_remote_settings() if not self._complete_intialization: return # unable to do remote sync else: original = hash(str(self)) self.update_remote() # Call callback for updated settings if self.changed_callback and hash(str(self)) != original: self.changed_callback() except Exception as e: LOG.error(e) LOG.exception("") # this is used in core so do not delete! if self.is_alive: # continues to poll settings every 60 seconds t = Timer(60, self._poll_skill_settings) t.daemon = True t.start() def load_skill_settings_from_file(self): """ If settings.json exist, open and read stored values into self """ if isfile(self._settings_path): with open(self._settings_path) as f: try: json_data = json.load(f) for key in json_data: self[key] = json_data[key] except Exception as e: # TODO: Show error on webUI. Dev will have to fix # metadata to be able to edit later. LOG.error(e) def _request_my_settings(self, identifier): """ Get skill settings for this device associated with the identifier Args: identifier (str): a hashed_meta Returns: skill_settings (dict or None): returns a dict if matches """ LOG.info("getting skill settings from " "server for {}".format(self.name)) settings = self._request_settings() # this loads the settings into memory for use in self.store for skill_settings in settings: if skill_settings['identifier'] == identifier: self._remote_settings = skill_settings return skill_settings return None def _request_settings(self): """ Get all skill settings for this device from server. Returns: dict: dictionary with settings collected from the server. """ settings = self.api.request({ "method": "GET", "path": self._api_path }) settings = [skills for skills in settings if skills is not None] return settings def _request_other_settings(self, identifier): """ Retrieves user skill from other devices by identifier (hashed_meta) Args: indentifier (str): identifier for this skill Returns: settings (dict or None): returns the settings if true else None """ LOG.info( "syncing settings with other devices " "from server for {}".format(self.name)) path = \ "/" + self._device_identity + "/userSkill?identifier=" + identifier user_skill = self.api.request({ "method": "GET", "path": path }) if len(user_skill) == 0: return None else: return user_skill[0] def _put_metadata(self, settings_meta): """ PUT settingsmeta to backend to be configured in server. used in place of POST and PATCH. Args: settings_meta (dict): dictionary of the current settings meta data """ return self.api.request({ "method": "PUT", "path": self._api_path, "json": settings_meta }) def _delete_metadata(self, uuid): """ Deletes the current skill metadata Args: uuid (str): unique id of the skill """ try: LOG.info("deleting metadata") self.api.request({ "method": "DELETE", "path": self._api_path + "/{}".format(uuid) }) except Exception as e: LOG.error(e) LOG.info( "cannot delete metadata because this" "device is not original uploader of skill") @property def _should_upload_from_change(self): changed = False if hasattr(self, '_remote_settings'): sections = self._remote_settings['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section['fields']): if 'name' in field: # Ensure that the field exists in settings and that # it has a value to compare if (field["name"] in self and 'value' in sections[i]['fields'][j]): remote_val = sections[i]['fields'][j]["value"] self_val = self.get(field['name']) if str(remote_val) != str(self_val): changed = True if self.get('not_owner'): changed = False return changed def store(self, force=False): """ Store dictionary to file if a change has occured. Args: force: Force write despite no change """ if force or not self._is_stored: with open(self._settings_path, 'w') as f: json.dump(self, f) self.loaded_hash = hash(str(self)) if self._should_upload_from_change: settings_meta = self._load_settings_meta() hashed_meta = self._get_meta_hash(settings_meta) uuid = self._load_uuid() if uuid is not None: LOG.info("deleting meta data for {}".format(self.name)) self._delete_metadata(uuid) self._upload_meta(settings_meta, hashed_meta)
class PairingSkill(MycroftSkill): def __init__(self): super(PairingSkill, self).__init__("PairingSkill") self.api = DeviceApi() self.data = None self.last_request = None self.state = str(uuid4()) self.delay = 10 self.expiration = 72000 # 20 hours self.activator = None self.repeater = None # TODO: Add translation support self.nato_dict = {'A': "'A' as in Apple", 'B': "'B' as in Bravo", 'C': "'C' as in Charlie", 'D': "'D' as in Delta", 'E': "'E' as in Echo", 'F': "'F' as in Fox trot", 'G': "'G' as in Golf", 'H': "'H' as in Hotel", 'I': "'I' as in India", 'J': "'J' as in Juliet", 'K': "'K' as in Kilogram", 'L': "'L' as in London", 'M': "'M' as in Mike", 'N': "'N' as in November", 'O': "'O' as in Oscar", 'P': "'P' as in Paul", 'Q': "'Q' as in Quebec", 'R': "'R' as in Romeo", 'S': "'S' as in Sierra", 'T': "'T' as in Tango", 'U': "'U' as in Uniform", 'V': "'V' as in Victor", 'W': "'W' as in Whiskey", 'X': "'X' as in X-Ray", 'Y': "'Y' as in Yankee", 'Z': "'Z' as in Zebra", '1': 'One', '2': 'Two', '3': 'Three', '4': 'Four', '5': 'Five', '6': 'Six', '7': 'Seven', '8': 'Eight', '9': 'Nine', '0': 'Zero'} def initialize(self): intent = IntentBuilder("PairingIntent") \ .require("PairingKeyword").require("DeviceKeyword").build() self.register_intent(intent, self.handle_pairing) self.emitter.on("mycroft.not.paired", self.not_paired) def not_paired(self, message): self.speak_dialog("pairing.not.paired") self.handle_pairing() def handle_pairing(self, message=None): if self.is_paired(): self.speak_dialog("pairing.paired") elif self.data and self.last_request < time.time(): self.speak_code() else: self.last_request = time.time() + self.expiration self.data = self.api.get_code(self.state) self.enclosure.deactivate_mouth_events() # keeps code on the display self.speak_code() if not self.activator: self.__create_activator() def on_activate(self): try: # wait for a signal from the backend that pairing is complete token = self.data.get("token") login = self.api.activate(self.state, token) # shut down thread that repeats the code to the user if self.repeater: self.repeater.cancel() self.repeater = None # is_speaking() and stop_speaking() support is mycroft-core 0.8.16+ try: if mycroft.util.is_speaking(): # Assume speaking is the pairing code. Stop TTS mycroft.util.stop_speaking() except: pass self.enclosure.activate_mouth_events() # clears the display self.speak_dialog("pairing.paired") # wait_while_speaking() support is mycroft-core 0.8.16+ try: mycroft.util.wait_while_speaking() except: pass IdentityManager.save(login) self.emitter.emit(Message("mycroft.paired", login)) # Un-mute. Would have been muted during onboarding for a new # unit, and not dangerous to do if pairing was started # independently. self.emitter.emit(Message("mycroft.mic.unmute", None)) except: if self.last_request < time.time(): self.data = None self.handle_pairing() else: self.__create_activator() def __create_activator(self): self.activator = Timer(self.delay, self.on_activate) self.activator.daemon = True self.activator.start() def is_paired(self): try: device = self.api.get() except: device = None return device is not None def speak_code(self): """ speak code and start repeating it every 60 second. """ if self.repeater: self.repeater.cancel() self.repeater = None self.__speak_code() self.repeater = Timer(60, self.__repeat_code) self.repeater.daemon = True self.repeater.start() def __speak_code(self): """ Speak code. """ code = self.data.get("code") self.log.info("Pairing code: " + code) data = {"code": '. '.join(map(self.nato_dict.get, code))} self.enclosure.mouth_text(self.data.get("code")) self.speak_dialog("pairing.code", data) def __repeat_code(self): """ Timer function to repeat the code every 60 second. """ # if pairing is complete terminate the thread if self.is_paired(): self.repeater = None return # repeat instructions/code every 60 seconds (start to start) self.__speak_code() self.repeater = Timer(60, self.__repeat_code) self.repeater.daemon = True self.repeater.start() def stop(self): pass def shutdown(self): super(PairingSkill, self).shutdown() if self.activator: self.activator.cancel() if self.repeater: self.repeater.cancel()
class SkillSettings(dict): """ Dictionary that can easily be saved to a file, serialized as json. It also syncs to the backend for skill settings Args: directory (str): Path to storage directory name (str): user readable name associated with the settings """ def __init__(self, directory, name): super(SkillSettings, self).__init__() # when skills try to instantiate settings # in __init__, it can erase the settings saved # on disk (settings.json). So this prevents that # This is set to true in core.py after skill init self.allow_overwrite = False self.api = DeviceApi() self.config = ConfigurationManager.get() self.name = name # set file paths self._settings_path = join(directory, 'settings.json') self._meta_path = join(directory, 'settingsmeta.json') self.is_alive = True self.loaded_hash = hash(json.dumps(self, sort_keys=True)) self._complete_intialization = False self._device_identity = None self._api_path = None self._user_identity = None self.changed_callback = None self._poll_timer = None self._is_alive = True # if settingsmeta exist if isfile(self._meta_path): self._poll_skill_settings() def __hash__(self): """ Simple object unique hash. """ return hash(str(id(self)) + self.name) def run_poll(self, _=None): """Immediately poll the web for new skill settings""" if self._poll_timer: self._poll_timer.cancel() self._poll_skill_settings() def stop_polling(self): self._is_alive = False if self._poll_timer: self._poll_timer.cancel() def set_changed_callback(self, callback): """ Set callback to perform when server settings have changed. Args: callback: function/method to call when settings have changed """ self.changed_callback = callback # TODO: break this up into two classes def initialize_remote_settings(self): """ initializes the remote settings to the server """ # if settingsmeta.json exists (and is valid) # this block of code is a control flow for # different scenarios that may arises with settingsmeta self.load_skill_settings_from_file() # loads existing settings.json settings_meta = self._load_settings_meta() if not settings_meta: return if not is_paired(): return self._device_identity = self.api.identity.uuid self._api_path = "/" + self._device_identity + "/skill" try: self._user_identity = self.api.get()['user']['uuid'] except RequestException: return hashed_meta = self._get_meta_hash(settings_meta) skill_settings = self._request_other_settings(hashed_meta) # if hash is new then there is a diff version of settingsmeta if self._is_new_hash(hashed_meta): # first look at all other devices on user account to see # if the settings exist. if it does then sync with device if skill_settings: # not_owner flags that this settings is loaded from # another device. If a skill settings doesn't have # not_owner, then the skill is created from that device self['not_owner'] = True self.save_skill_settings(skill_settings) else: # upload skill settings if uuid = self._load_uuid() if uuid is not None: self._delete_metadata(uuid) self._upload_meta(settings_meta, hashed_meta) else: # hash is not new if skill_settings is not None: self['not_owner'] = True self.save_skill_settings(skill_settings) else: settings = self._request_my_settings(hashed_meta) if settings is None: # metadata got deleted from Home, send up self._upload_meta(settings_meta, hashed_meta) else: self.save_skill_settings(settings) self._complete_intialization = True @property def _is_stored(self): return hash(json.dumps(self, sort_keys=True)) == self.loaded_hash def __getitem__(self, key): """ Get key """ return super(SkillSettings, self).__getitem__(key) def __setitem__(self, key, value): """ Add/Update key. """ if self.allow_overwrite or key not in self: return super(SkillSettings, self).__setitem__(key, value) def _load_settings_meta(self): """ Loads settings metadata from skills path. """ if isfile(self._meta_path): try: with open(self._meta_path, encoding='utf-8') as f: data = json.load(f) return data except Exception as e: LOG.error("Failed to load setting file: "+self._meta_path) LOG.error(repr(e)) return None else: return None def _send_settings_meta(self, settings_meta): """ Send settingsmeta.json to the server. Args: settings_meta (dict): dictionary of the current settings meta Returns: dict: uuid, a unique id for the setting meta data """ try: uuid = self._put_metadata(settings_meta) return uuid except Exception as e: LOG.error(e) return None def save_skill_settings(self, skill_settings): """ Takes skill object and save onto self Args: skill_settings (dict): skill """ if self._is_new_hash(skill_settings['identifier']): self._save_uuid(skill_settings['uuid']) self._save_hash(skill_settings['identifier']) sections = skill_settings['skillMetadata']['sections'] for section in sections: for field in section["fields"]: if "name" in field and "value" in field: self[field['name']] = field['value'] self.store() def _load_uuid(self): """ Loads uuid Returns: str: uuid of the previous settingsmeta """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') uuid = None if isfile(uuid_file): with open(uuid_file, 'r') as f: uuid = f.read() return uuid def _save_uuid(self, uuid): """ Saves uuid. Args: uuid (str): uuid, unique id of new settingsmeta """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') os.makedirs(directory, exist_ok=True) with open(uuid_file, 'w') as f: f.write(str(uuid)) def _uuid_exist(self): """ Checks if there is an uuid file. Returns: bool: True if uuid file exist False otherwise """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') return isfile(uuid_file) def _migrate_settings(self, settings_meta): """ sync settings.json and settingsmeta.json in memory """ meta = settings_meta.copy() self.load_skill_settings_from_file() sections = meta['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section['fields']): if 'name' in field: if field["name"] in self: sections[i]['fields'][j]['value'] = \ str(self.__getitem__(field['name'])) meta['skillMetadata']['sections'] = sections return meta def _upload_meta(self, settings_meta, hashed_meta): """ uploads the new meta data to settings with settings migration Args: settings_meta (dict): settingsmeta.json hashed_meta (str): {skill-folder}-settinsmeta.json """ meta = self._migrate_settings(settings_meta) meta['identifier'] = str(hashed_meta) response = self._send_settings_meta(meta) if response and 'uuid' in response: self._save_uuid(response['uuid']) if 'not_owner' in self: del self['not_owner'] self._save_hash(hashed_meta) def hash(self, string): """ md5 hasher for consistency across cpu architectures """ return hashlib.md5(bytes(string, 'utf-8')).hexdigest() def _get_meta_hash(self, settings_meta): """ Gets the hash of skill Args: settings_meta (dict): settingsmeta object Returns: _hash (str): hashed to identify skills """ _hash = self.hash(json.dumps(settings_meta, sort_keys=True) + self._user_identity) return "{}--{}".format(self.name, _hash) def _save_hash(self, hashed_meta): """ Saves hashed_meta to settings directory. Args: hashed_meta (str): hash of new settingsmeta """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) hash_file = join(directory, 'hash') os.makedirs(directory, exist_ok=True) with open(hash_file, 'w') as f: f.write(hashed_meta) def _is_new_hash(self, hashed_meta): """ Check if stored hash is the same as current. If the hashed file does not exist, usually in the case of first load, then the create it and return True Args: hashed_meta (str): hash of metadata and uuid of device Returns: bool: True if hash is new, otherwise False """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) hash_file = join(directory, 'hash') if isfile(hash_file): with open(hash_file, 'r') as f: current_hash = f.read() return False if current_hash == str(hashed_meta) else True return True def update_remote(self): """ update settings state from server """ skills_settings = None settings_meta = self._load_settings_meta() if settings_meta is None: return hashed_meta = self._get_meta_hash(settings_meta) if self.get('not_owner'): skills_settings = self._request_other_settings(hashed_meta) if not skills_settings: skills_settings = self._request_my_settings(hashed_meta) if skills_settings is not None: self.save_skill_settings(skills_settings) self.store() else: settings_meta = self._load_settings_meta() self._upload_meta(settings_meta, hashed_meta) def _poll_skill_settings(self): """ If identifier exists for this skill poll to backend to request settings and store it if it changes TODO: implement as websocket """ original = hash(str(self)) try: if not is_paired(): pass elif not self._complete_intialization: self.initialize_remote_settings() else: self.update_remote() except Exception as e: LOG.exception('Failed to fetch skill settings: {}'.format(repr(e))) finally: # Call callback for updated settings if self._complete_intialization: if self.changed_callback and hash(str(self)) != original: self.changed_callback() if self._poll_timer: self._poll_timer.cancel() if not self._is_alive: return # continues to poll settings every minute self._poll_timer = Timer(1 * 60, self._poll_skill_settings) self._poll_timer.daemon = True self._poll_timer.start() def load_skill_settings_from_file(self): """ If settings.json exist, open and read stored values into self """ if isfile(self._settings_path): with open(self._settings_path) as f: try: json_data = json.load(f) for key in json_data: self[key] = json_data[key] except Exception as e: # TODO: Show error on webUI. Dev will have to fix # metadata to be able to edit later. LOG.error(e) def _type_cast(self, settings_meta, to_platform): """ Tranform data type to be compatible with Home and/or Core. e.g. Web to core "true" => True, "1.4" => 1.4 core to Web False => "false' Args: settings_meta (dict): skills object to_platform (str): platform to convert compatible data types to Returns: dict: skills object """ meta = settings_meta.copy() sections = meta['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section.get('fields', [])): _type = field.get('type') if _type == 'checkbox': value = field.get('value') if to_platform == 'web': if value is True or value == 'True': sections[i]['fields'][j]['value'] = 'true' elif value is False or value == 'False': sections[i]['fields'][j]['value'] = 'false' elif to_platform == 'core': if value == 'true' or value == 'True': sections[i]['fields'][j]['value'] = True elif value == 'false' or value == 'False': sections[i]['fields'][j]['value'] = False elif _type == 'number': value = field.get('value') if to_platform == 'core': if "." in value: sections[i]['fields'][j]['value'] = float(value) else: sections[i]['fields'][j]['value'] = int(value) elif to_platform == 'web': sections[i]['fields'][j]['value'] = str(value) meta['skillMetadata']['sections'] = sections return meta def _request_my_settings(self, identifier): """ Get skill settings for this device associated with the identifier Args: identifier (str): a hashed_meta Returns: skill_settings (dict or None): returns a dict if matches """ settings = self._request_settings() if settings: # this loads the settings into memory for use in self.store for skill_settings in settings: if skill_settings['identifier'] == identifier: skill_settings = \ self._type_cast(skill_settings, to_platform='core') self._remote_settings = skill_settings return skill_settings return None def _request_settings(self): """ Get all skill settings for this device from server. Returns: dict: dictionary with settings collected from the server. """ try: settings = self.api.request({ "method": "GET", "path": self._api_path }) except RequestException: return None settings = [skills for skills in settings if skills is not None] return settings def _request_other_settings(self, identifier): """ Retrieve skill settings from other devices by identifier Args: identifier (str): identifier for this skill Returns: settings (dict or None): the retrieved settings or None """ path = \ "/" + self._device_identity + "/userSkill?identifier=" + identifier try: user_skill = self.api.request({"method": "GET", "path": path}) except RequestException: # Some kind of Timeout, connection HTTPError, etc. user_skill = None if not user_skill: return None else: settings = self._type_cast(user_skill[0], to_platform='core') return settings def _put_metadata(self, settings_meta): """ PUT settingsmeta to backend to be configured in server. used in place of POST and PATCH. Args: settings_meta (dict): dictionary of the current settings meta data """ settings_meta = self._type_cast(settings_meta, to_platform='web') return self.api.request({ "method": "PUT", "path": self._api_path, "json": settings_meta }) def _delete_metadata(self, uuid): """ Delete the current skill metadata Args: uuid (str): unique id of the skill """ try: LOG.debug("deleting metadata") self.api.request({ "method": "DELETE", "path": self._api_path + "/{}".format(uuid) }) except Exception as e: LOG.error(e) LOG.error( "cannot delete metadata because this" "device is not original uploader of skill") @property def _should_upload_from_change(self): changed = False if hasattr(self, '_remote_settings'): sections = self._remote_settings['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section['fields']): if 'name' in field: # Ensure that the field exists in settings and that # it has a value to compare if (field["name"] in self and 'value' in sections[i]['fields'][j]): remote_val = sections[i]['fields'][j]["value"] self_val = self.get(field['name']) if str(remote_val) != str(self_val): changed = True if self.get('not_owner'): changed = False return changed def store(self, force=False): """ Store dictionary to file if a change has occured. Args: force: Force write despite no change """ if force or not self._is_stored: with open(self._settings_path, 'w') as f: json.dump(self, f) self.loaded_hash = hash(json.dumps(self, sort_keys=True)) if self._should_upload_from_change: settings_meta = self._load_settings_meta() hashed_meta = self._get_meta_hash(settings_meta) uuid = self._load_uuid() if uuid is not None: self._delete_metadata(uuid) self._upload_meta(settings_meta, hashed_meta)
def __init__(self): super(ConfigurationSkill, self).__init__("ConfigurationSkill") self.max_delay = self.config.get('max_delay') self.api = DeviceApi()
class PairingSkill(OVOSSkill): poll_frequency = 5 # secs between checking server for activation def __init__(self): super(PairingSkill, self).__init__("PairingSkill") self.reload_skill = False self.api = DeviceApi() self.data = None self.time_code_expires = None self.state = str(uuid4()) self.activator = None self.activator_lock = Lock() self.activator_cancelled = False self.counter_lock = Lock() self.count = -1 # for repeating pairing code. -1 = not running self.nato_dict = None self.mycroft_ready = False self.num_failed_codes = 0 self.in_pairing = False self.initial_stt = self.config_core["stt"]["module"] self.using_mock = self.config_core["server"][ "url"] != "https://api.mycroft.ai" # startup def initialize(self): # specific distros can override this if "pairing_url" not in self.settings: self.settings["pairing_url"] = "home.mycroft.ai" if "color" not in self.settings: self.settings["color"] = "#FF0000" if not is_paired(): # If the device isn't paired catch mycroft.ready to report # that the device is ready for use. # This assumes that the pairing skill is loaded as a priority skill # before the rest of the skills are loaded. self.add_event("mycroft.ready", self.handle_mycroft_ready) self.in_pairing = True self.make_active() # to enable converse # show loading screen once wifi setup ends if not connected(): self.bus.once("ovos.wifi.setup.completed", self.show_loading_screen) else: # this is usually the first skill to load # ASSUMPTION: is the first skill in priority list self.show_loading_screen() self.add_event("mycroft.not.paired", self.not_paired) # events for GUI interaction self.gui.register_handler("mycroft.device.set.backend", self.handle_backend_selected_event) self.gui.register_handler("mycroft.device.confirm.backend", self.handle_backend_confirmation_event) self.gui.register_handler("mycroft.return.select.backend", self.handle_return_event) self.gui.register_handler("mycroft.device.confirm.stt", self.select_stt) self.gui.register_handler("mycroft.device.confirm.tts", self.select_tts) self.nato_dict = self.translate_namedvalues('codes') def show_loading_screen(self, message=None): self.handle_display_manager("LoadingScreen") def send_stop_signal(self, stop_event=None, should_sleep=True): # TODO move this one into default OVOSkill class # stop the previous event execution if stop_event: self.bus.emit(Message(stop_event)) # stop TTS self.bus.emit(Message("mycroft.audio.speech.stop")) if should_sleep: # STT might continue recording and screw up the next get_response # TODO make mycroft-core allow aborting recording in a sane way self.bus.emit(Message('mycroft.mic.mute')) sleep(0.5) # if TTS had not yet started self.bus.emit(Message("mycroft.audio.speech.stop")) sleep( 1.5) # the silence from muting should make STT stop recording self.bus.emit(Message('mycroft.mic.unmute')) def handle_intent_aborted(self): self.log.info("killing all dialogs") def not_paired(self, message): if not message.data.get('quiet', True): self.speak_dialog("pairing.not.paired") self.handle_pairing() def handle_mycroft_ready(self, message): """Catch info that skills are loaded and ready.""" self.mycroft_ready = True self.gui.remove_page("ProcessLoader.qml") self.bus.emit(Message("mycroft.gui.screen.close", {"skill_id": self.skill_id})) # Tell OVOS-GUI to finally collect resting screens self.bus.emit(Message("ovos.pairing.process.completed")) # voice events def converse(self, message): if self.in_pairing: # capture all utterances until paired # prompts from this skill are handled with get_response return True return False @intent_handler(IntentBuilder("PairingIntent") .require("PairingKeyword").require("DeviceKeyword")) def handle_pairing(self, message=None): self.in_pairing = True if self.using_mock: # user triggered intent, wants to enable pairing self.select_selene() elif check_remote_pairing(ignore_errors=True): # Already paired! Just tell user self.speak_dialog("already.paired") elif not self.data: self.handle_backend_menu() # config handling def change_to_mimic(self): conf = LocalConf(USER_CONFIG) conf["tts"] = { "module": "mimic", "mimic": { "voice": "ap", } } conf.store() self.bus.emit(Message("configuration.patch", {"config": conf})) def change_to_mimic2(self): conf = LocalConf(USER_CONFIG) conf["tts"] = { "module": "mimic2" } conf.store() self.bus.emit(Message("configuration.patch", {"config": conf})) def change_to_larynx(self): conf = LocalConf(USER_CONFIG) conf["tts"] = { "module": "neon-tts-plugin-larynx-server", "neon-tts-plugin-larynx-server": { "host": "http://tts.neon.ai", "voice": "mary_ann", "vocoder": "hifi_gan/vctk_small" } } conf.store() self.bus.emit(Message("configuration.patch", {"config": conf})) def change_to_pico(self): conf = LocalConf(USER_CONFIG) conf["tts"] = { "module": "ovos-tts-plugin-pico" } conf.store() self.bus.emit(Message("configuration.patch", {"config": conf})) def change_to_chromium(self): conf = LocalConf(USER_CONFIG) conf["stt"] = { "module": "ovos-stt-plugin-chromium" } conf.store() self.bus.emit(Message("configuration.patch", {"config": conf})) def change_to_kaldi(self): conf = LocalConf(USER_CONFIG) conf["stt"] = { "module": "ovos-stt-plugin-vosk-streaming", "ovos-stt-plugin-vosk-streaming": { "model": expanduser( "~/.local/share/vosk/vosk-model-small-en-us-0.15") } } conf.store() self.bus.emit(Message("configuration.patch", {"config": conf})) def enable_selene(self): config = { "stt": {"module": "mycroft"}, "server": { "url": "https://api.mycroft.ai", "version": "v1" }, "listener": { "wake_word_upload": { "url": "https://training.mycroft.ai/precise/upload" } } } conf = LocalConf(USER_CONFIG) conf.update(config) conf.store() self.using_mock = False self.bus.emit(Message("configuration.patch", {"config": config})) def enable_mock(self): url = "http://0.0.0.0:{p}".format(p=CONFIGURATION["backend_port"]) version = CONFIGURATION["api_version"] config = { "server": { "url": url, "version": version }, # no web ui to set location, best guess from ip address # should get at least timezone right "location": ip_geolocate("0.0.0.0"), "listener": { "wake_word_upload": { "url": "http://0.0.0.0:{p}/precise/upload".format( p=CONFIGURATION["backend_port"]) } } } conf = LocalConf(USER_CONFIG) conf.update(config) conf.store() self.using_mock = True self.bus.emit(Message("configuration.patch", {"config": config})) # Pairing GUI events #### Backend selection menu @killable_event(msg="pairing.backend.menu.stop") def handle_backend_menu(self): self.send_stop_signal("pairing.confirmation.stop") self.handle_display_manager("BackendSelect") self.speak_dialog("select_backend_gui") def handle_backend_selected_event(self, message): self.send_stop_signal("pairing.backend.menu.stop", should_sleep=False) self.handle_backend_confirmation(message.data["backend"]) def handle_return_event(self, message): self.send_stop_signal("pairing.confirmation.stop", should_sleep=False) page = message.data.get("page", "") self.handle_backend_menu() ### Backend confirmation @killable_event(msg="pairing.confirmation.stop", callback=handle_intent_aborted) def handle_backend_confirmation(self, selection): if selection == "selene": self.handle_display_manager("BackendMycroft") self.speak_dialog("selected_mycroft_backend_gui") elif selection == "local": self.handle_display_manager("BackendLocal") self.speak_dialog("selected_local_backend_gui") def handle_backend_confirmation_event(self, message): self.send_stop_signal("pairing.confirmation.stop") if message.data["backend"] == "local": self.select_local() else: self.select_selene() def select_selene(self): # selene selected if self.using_mock: self.enable_selene() self.data = None # TODO needs to restart, user wants to change back to selene # eg, local was selected and at some point user said # "pair my device" if check_remote_pairing(ignore_errors=True): # Already paired! Just tell user self.speak_dialog("already.paired") self.in_pairing = False elif not self.data: # continue to normal pairing process self.kickoff_pairing() def select_local(self, message=None): # mock backend selected self.data = None self.handle_stt_menu() ### STT selection @killable_event(msg="pairing.stt.menu.stop", callback=handle_intent_aborted) def handle_stt_menu(self): self.handle_display_manager("BackendLocalSTT") self.send_stop_signal("pairing.confirmation.stop") self.speak_dialog("select_mycroft_stt_gui") def select_stt(self, message): selection = message.data["engine"] self.send_stop_signal("pairing.stt.menu.stop") if selection == "google": self.change_to_chromium() elif selection == "kaldi": self.change_to_kaldi() if not self.using_mock: self.enable_mock() # create pairing file with dummy data login = {"uuid": self.state, "access": "OVOSdbF1wJ4jA5lN6x6qmVk_QvJPqBQZTUJQm7fYzkDyY_Y=", "refresh": "OVOS66c5SpAiSpXbpHlq9HNGl1vsw_srX49t5tCv88JkhuE=", "expires_at": time.time() + 999999} IdentityManager.save(login) self.handle_tts_menu() ### TTS selection @killable_event(msg="pairing.tts.menu.stop", callback=handle_intent_aborted) def handle_tts_menu(self): self.handle_display_manager("BackendLocalTTS") self.send_stop_signal("pairing.stt.menu.stop") self.speak_dialog("select_mycroft_tts_gui") def select_tts(self, message): selection = message.data["engine"] self.send_stop_signal() if selection == "mimic": self.change_to_mimic() elif selection == "mimic2": self.change_to_mimic2() elif selection == "pico": self.change_to_pico() elif selection == "larynx": self.change_to_larynx() self.handle_display_manager("BackendLocalRestart") self.in_pairing = False time.sleep(5) system_reboot() # TODO no need for full restart #subprocess.call("sudo systemctl restart mycroft-audio", shell=True) #subprocess.call("sudo systemctl restart mycroft-voice", shell=True) #subprocess.call("sudo systemctl restart mycroft-skills", shell=True) # pairing def kickoff_pairing(self): # Kick off pairing... with self.counter_lock: if self.count > -1: # We snuck in to this handler somehow while the pairing # process is still being setup. Ignore it. self.log.debug("Ignoring call to handle_pairing") return # Not paired or already pairing, so start the process. self.count = 0 self.log.debug("Kicking off pairing sequence") try: # Obtain a pairing code from the backend self.data = self.api.get_code(self.state) # Keep track of when the code was obtained. The codes expire # after 20 hours. self.time_code_expires = time.monotonic() + 72000 # 20 hours except Exception: time.sleep(10) # Call restart pairing here # Bail out after Five minutes (5 * 6 attempts at 10 seconds # interval) if self.num_failed_codes < 5 * 6: self.num_failed_codes += 1 self.abort_and_restart(quiet=True) else: self.end_pairing('connection.error') self.num_failed_codes = 0 return self.num_failed_codes = 0 # Reset counter on success mycroft.audio.wait_while_speaking() self.show_pairing_start() self.speak_dialog("pairing.intro") # HACK this gives the Mark 1 time to scroll the address and # the user time to browse to the website. # TODO: mouth_text() really should take an optional parameter # to not scroll a second time. time.sleep(7) mycroft.audio.wait_while_speaking() if not self.activator: self.__create_activator() def check_for_activate(self): """Method is called every 10 seconds by Timer. Checks if user has activated the device yet on home.mycroft.ai and if not repeats the pairing code every 60 seconds. """ try: # Attempt to activate. If the user has completed pairing on the, # backend, this will succeed. Otherwise it throws and HTTPError() token = self.data.get("token") login = self.api.activate(self.state, token) # HTTPError() thrown # When we get here, the pairing code has been entered on the # backend and pairing can now be saved. # The following is kinda ugly, but it is really critical that we # get this saved successfully or we need to let the user know that # they have to perform pairing all over again at the website. try: IdentityManager.save(login) except Exception as e: self.log.debug("First save attempt failed: " + repr(e)) time.sleep(2) try: IdentityManager.save(login) except Exception as e2: # Something must be seriously wrong self.log.debug("Second save attempt failed: " + repr(e2)) self.abort_and_restart() if mycroft.audio.is_speaking(): # Assume speaking is the pairing code. Stop TTS of that. mycroft.audio.stop_speaking() self.show_pairing_success() self.bus.emit(Message("mycroft.paired", login)) if self.mycroft_ready: # Tell user they are now paired self.speak_dialog("pairing.paired", wait=True) # Un-mute. Would have been muted during onboarding for a new # unit, and not dangerous to do if pairing was started # independently. self.bus.emit(Message("mycroft.mic.unmute", None)) # Send signal to update configuration self.bus.emit(Message("configuration.updated")) except HTTPError: # speak pairing code every 60th second with self.counter_lock: if self.count == 0: self.speak_code() self.count = (self.count + 1) % 6 if time.monotonic() > self.time_code_expires: # After 20 hours the token times out. Restart # the pairing process. with self.counter_lock: self.count = -1 self.data = None self.handle_pairing() else: # trigger another check in 10 seconds self.__create_activator() except Exception as e: self.log.debug("Unexpected error: " + repr(e)) self.abort_and_restart() def end_pairing(self, error_dialog): """Resets the pairing and don't restart it. Arguments: error_dialog: Reason for the ending of the pairing process. """ self.speak_dialog(error_dialog) self.bus.emit(Message("mycroft.mic.unmute", None)) self.data = None self.count = -1 self.in_pairing = False def abort_and_restart(self, quiet=False): # restart pairing sequence self.log.debug("Aborting Pairing") self.enclosure.activate_mouth_events() if not quiet: self.speak_dialog("unexpected.error.restarting") # Reset state variables for a new pairing session with self.counter_lock: self.count = -1 self.activator = None self.data = None # Clear pairing code info self.log.info("Restarting pairing process") self.show_pairing_fail() self.bus.emit(Message("mycroft.not.paired", data={'quiet': quiet})) def __create_activator(self): # Create a timer that will poll the backend in 10 seconds to see # if the user has completed the device registration process with self.activator_lock: if not self.activator_cancelled: self.activator = Timer(PairingSkill.poll_frequency, self.check_for_activate) self.activator.daemon = True self.activator.start() def speak_code(self): """Speak pairing code.""" code = self.data.get("code") self.log.info("Pairing code: " + code) data = {"code": '. '.join(map(self.nato_dict.get, code)) + '.'} self.show_pairing(self.data.get("code")) self.speak_dialog("pairing.code", data) # GUI def handle_display_manager(self, state): self.gui["state"] = state self.gui.show_page( "ProcessLoader.qml", override_idle=True, override_animations=True) def show_pairing_start(self): # Make sure code stays on display self.enclosure.deactivate_mouth_events() self.enclosure.mouth_text(self.settings["pairing_url"] + " ") self.handle_display_manager("PairingStart") # self.gui.show_page("pairing_start.qml", override_idle=True, # override_animations=True) def show_pairing(self, code): # self.gui.remove_page("pairing_start.qml") self.enclosure.deactivate_mouth_events() self.enclosure.mouth_text(code) self.gui["txtcolor"] = self.settings["color"] self.gui["backendurl"] = self.settings["pairing_url"] self.gui["code"] = code self.handle_display_manager("Pairing") # self.gui.show_page("pairing.qml", override_idle=True, # override_animations=True) def show_pairing_success(self): self.enclosure.activate_mouth_events() # clears the display # self.gui.remove_page("pairing.qml") self.gui["status"] = "Success" self.gui["label"] = "Device Paired" self.gui["bgColor"] = "#40DBB0" # self.gui.show_page("status.qml", override_idle=True, # override_animations=True) self.handle_display_manager("Status") # allow GUI to linger around for a bit sleep(5) # self.gui.remove_page("status.qml") self.handle_display_manager("InstallingSkills") def show_pairing_fail(self): self.gui.release() self.gui["status"] = "Failed" self.gui["label"] = "Pairing Failed" self.gui["bgColor"] = "#FF0000" self.handle_display_manager("Status") sleep(5) def shutdown(self): with self.activator_lock: self.activator_cancelled = True if self.activator: self.activator.cancel() if self.activator: self.activator.join()
def _setup(self, force=False): # when description of home.mycroft.ai > Devices > [this mycroft device] # is filled, use this the name of the room where mycroft is located if self.settings.get('device_location', False): dev = DeviceApi() info = dev.get() if 'description' in info: self.device_location = info['description'] LOG.debug("mycroft device location: {}".format(self.device_location)) if self.settings and (force or self.fhem is None): LOG.debug("_setup") portnumber = self.settings.get('portnum') try: portnumber = int(portnumber) except TypeError: portnumber = 8083 except ValueError: # String might be some rubbish (like '') portnumber = 0 self.fhem = \ python_fhem.Fhem(self.settings.get('host'), port=portnumber, csrf=True, protocol=self.settings.get('protocol', 'http').lower(), use_ssl=self.settings.get('ssl', False), username=self.settings.get('username'), password=self.settings.get('password') ) self.fhem.connect() LOG.debug("connect: {}".format(self.fhem.connected())) if self.fhem.connected(): self.allowed_devices_room = self.settings.get( 'room', 'Homebridge') self.ignore_rooms = self.settings.get('ignore_rooms', '') # Check if natural language control is loaded at fhem-server # and activate fallback accordingly LOG.debug("fallback_device_name %s" % self.settings.get('fallback_device_name')) LOG.debug("enable_fallback %s" % self.settings.get('enable_fallback')) if self.settings.get('enable_fallback') and \ self.settings.get('fallback_device_name', ""): fallback_device = \ self.fhem.get_device(self.settings.get( 'fallback_device_name', "")) if fallback_device: # LOG.debug("fallback device {}".format(fallback_device)) self.fallback_device_name = self.settings.get( 'fallback_device_name', "") self.fallback_device_type = self.fhem.get_internals( "TYPE", name=self.fallback_device_name)[ self.fallback_device_name] LOG.debug("fallback_device_type is %s" % self.fallback_device_type) if self.fallback_device_type in [ "Talk2Fhem", "TEERKO", "Babble" ]: self.enable_fallback = True else: self.enable_fallback = False else: self.enable_fallback = False LOG.debug('fhem-fallback enabled: %s' % self.enable_fallback)
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 """ if not connected(): LOG.error('msm failed, network connection not available') if speak: self.bus.emit( Message("speak", { 'utterance': dialog.get("not connected to the internet") })) self.next_download = time.time() + 5 * MINUTES return False installed_skills = self.load_installed_skills() msm = SkillManager.create_msm() with msm.lock, self.thread_lock: default_groups = dict(msm.repo.get_default_skill_names()) if msm.platform in default_groups: platform_groups = default_groups[msm.platform] else: LOG.info('Platform defaults not found, using DEFAULT ' 'skills only') platform_groups = [] default_names = set( chain(default_groups['default'], platform_groups)) default_skill_errored = False def get_skill_data(skill_name): """ Get skill data structure from name. """ for e in msm.skills_data.get('skills', []): if e.get('name') == skill_name: return e # if skill isn't in the list return empty structure return {} def install_or_update(skill): """Install missing defaults and update existing skills""" if get_skill_data(skill.name).get('beta'): skill.sha = None # Will update to latest head if skill.is_local: skill.update() if skill.name not in installed_skills: skill.update_deps() elif skill.name in default_names: try: msm.install(skill, origin='default') except Exception: if skill.name in default_names: LOG.warning('Failed to install default skill: ' + skill.name) nonlocal default_skill_errored default_skill_errored = True raise installed_skills.add(skill.name) try: msm.apply(install_or_update, msm.list()) if SkillManager.manifest_upload_allowed and is_paired(): try: DeviceApi().upload_skills_data(msm.skills_data) except Exception: LOG.exception('Could not upload skill manifest') except MsmException as e: LOG.error('Failed to update skills: {}'.format(repr(e))) self.save_installed_skills(installed_skills) if speak: data = {'utterance': dialog.get("skills updated")} self.bus.emit(Message("speak", data)) if default_skill_errored and self.num_install_retries < 10: self.num_install_retries += 1 self.next_download = time.time() + 5 * MINUTES return False self.num_install_retries = 0 with open(self.dot_msm, 'a'): os.utime(self.dot_msm, None) self.next_download = time.time() + self.update_interval return True
def initialize(self): self.settings_change_callback = self.on_settings_changed self.on_settings_changed() self.same_device = DeviceApi() info = self.same_device.get() self.same_device = info['description'].lower()
def room_name(self): # Assume the "name" of the device is the "room name" device = DeviceApi().get() return device["name"]
class MySamsungTvRc(MycroftSkill): def __init__(self): super(MySamsungTvRc, self).__init__(name="MySamsungTV") def initialize(self): self.settings_change_callback = self.on_settings_changed self.on_settings_changed() self.same_device = DeviceApi() info = self.same_device.get() self.same_device = info['description'].lower() #self.trans = {"nach links": "LEFT", "nach rechts": "RIGHT", "nach oben": "UP", "nach unten": "DOWN", "nehmen": "ENTER", "verlassen": "EXIT"} def on_settings_changed(self): self.host = self.settings.get('tv') self.port = self.settings.get('port') self.placement = self.settings.get('placement') self.name_rc = self.settings.get('rc_name') self.method = self.settings.get('method') self.description_rc = self.settings.get('description_rc') self.translations = self.settings.get('translations') self.trans = self.translations.split(',') self.curs_move_dict = {self.trans[0]: 'LEFT', self.trans[1]: 'RIGHT', \ self.trans[2]: 'UP', self.trans[3]: 'DOWN', \ self.trans[4]: 'ENTER', self.trans[5]: 'EXIT'} #LOGGER.info(self.curs_move_dict) self.config = {"name": self.name_rc, "description": self.description_rc,\ "id": "", "host": self.host, "port": self.port, "method": self.method,\ "timeout": 0} #Main functions def send_keycode(self, keycode): '''Standard function for sending keycodes''' keycode = "KEY_" + keycode.upper() try: with samsungctl.Remote(self.config) as remote: remote.control(keycode) except Exception as e: LOGGER.info(str(e)) finally: pass #Helper functions def send_channel_pos(self, pos): '''Function for sending channel number; with multi-digit numbers \ the values are transmitted number by number. Therefore there is a \ small pause to consider the latency time of the LAN/WLAN or web server.''' if len(pos) > 1: i = 0 while i < len(pos): self.send_keycode(pos[i]) time.sleep(.5) i += 1 else: self.send_keycode(pos) def explain_cursor_moves(self, translations): '''Usage of cursor based selections''' self.speak_dialog('cursor_moves') self.speak(translations) move = "" return move def explain_cursor_moves_source(self): '''Usage of cursor based selections''' self.speak_dialog('cursor_moves_source') move = "" return move def cursor_recursion(self, move): '''Recursive function to handle cursor movements''' move = self.get_response('cursor_dummy', 0) if move == None: keycode = "EXIT" self.send_keycode(keycode) return if move == self.trans[4]: keycode = "ENTER" self.send_keycode(keycode) return if move == self.trans[5]: keycode = "EXIT" self.send_keycode(keycode) return keycode = self.curs_move_dict[move] self.send_keycode(keycode) move = "" self.cursor_recursion(move) ##Handlers #basic handlers @intent_handler('next_channel.intent') def handle_next_channel(self): keycode = "CHUP" self.send_keycode(keycode) @intent_handler('prev_channel.intent') def handle_prev_channel(self): keycode = "CHDOWN" self.send_keycode(keycode) @intent_handler('pos.intent') def handle_switch_to_pos(self, message): pos = message.data.get('pos_nr') pos = extract_number(pos) pos = str(int(pos)) self.send_channel_pos(pos) @intent_handler('vol_up.intent') def handle_vol_up(self): keycode = "VOLUP" self.send_keycode(keycode) @intent_handler('vol_down.intent') def handle_vol_down(self): keycode = "VOLDOWN" self.send_keycode(keycode) @intent_handler('menu_leave.intent') def handle_menu_leave(self): keycode = "EXIT" self.send_keycode(keycode) @intent_handler('info.intent') def handle_info(self): keycode = "INFO" self.send_keycode(keycode) @intent_handler('poweroff.intent') def handle_poweroff(self): keycode = "POWEROFF" self.send_keycode(keycode) #dialog handlers @intent_handler('channel_by_dialog.intent') def handle_channel_by_dialog(self, message): keycode = "CH_LIST" self.send_keycode(keycode) move = self.explain_cursor_moves(self.translations) self.cursor_recursion(move) @intent_handler('program_guide_dialog.intent') def handle_program_guide(self): keycode = "GUIDE" self.send_keycode(keycode) move = self.explain_cursor_moves(self.translations) self.cursor_recursion(move) @intent_handler('source_dialog.intent') def handle_source(self): keycode = "SOURCE" self.send_keycode(keycode) move = self.explain_cursor_moves_source() self.cursor_recursion(move) @intent_handler('smarthub_dialog.intent') def handle_smarthub(self): keycode = "CONTENTS" self.send_keycode(keycode) move = self.explain_cursor_moves() self.cursor_recursion(move) @intent_handler('tools.intent') def handle_tools(self): keycode = "TOOLS" self.send_keycode(keycode) move = self.explain_cursor_moves() self.cursor_recursion(move) #recording and playback handlers @intent_handler('pause.intent') def handle_timeshift_or_pause(self): keycode = "PAUSE" self.send_keycode(keycode) @intent_handler('play.intent') def handle_playing(self): keycode = "PLAY" self.send_keycode(keycode) @intent_handler('stop.intent') def handle_stop(self): keycode = "STOP" self.send_keycode(keycode) @intent_handler('record.intent') def handle_recording(self): keycode = "REC" self.send_keycode(keycode) @intent_handler('rewind.intent') def handle_recording(self): keycode = "REWIND" self.send_keycode(keycode) @intent_handler('fastforward.intent') def handle_recording(self): keycode = "FF" self.send_keycode(keycode) #source handlers @intent_handler('hdmi.intent') def handle_recording(self): keycode = "HDMI" self.send_keycode(keycode) @intent_handler('dtv.intent') def handle_recording(self): keycode = "DTV" self.send_keycode(keycode) def stop(self): pass
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()
def get_device_name(self): try: return DeviceApi().get()['name'] except: self.log.exception('API Error') return ':error:'
class PairingSkill(MycroftSkill): def __init__(self): super(PairingSkill, self).__init__("PairingSkill") self.api = DeviceApi() self.data = None self.state = str(uuid4()) self.delay = 10 self.activator = None # TODO: Add translation support self.nato_dict = { 'A': "'A' as in Apple", 'B': "'B' as in Bravo", 'C': "'C' as in Charlie", 'D': "'D' as in Delta", 'E': "'E' as in Echo", 'F': "'F' as in Fox trot", 'G': "'G' as in Golf", 'H': "'H' as in Hotel", 'I': "'I' as in India", 'J': "'J' as in Juliet", 'K': "'K' as in Kilogram", 'L': "'L' as in London", 'M': "'M' as in Mike", 'N': "'N' as in November", 'O': "'O' as in Oscar", 'P': "'P' as in Paul", 'Q': "'Q' as in Quebec", 'R': "'R' as in Romeo", 'S': "'S' as in Sierra", 'T': "'T' as in Tango", 'U': "'U' as in Uniform", 'V': "'V' as in Victor", 'W': "'W' as in Whiskey", 'X': "'X' as in X-Ray", 'Y': "'Y' as in Yankee", 'Z': "'Z' as in Zebra", '1': 'One', '2': 'Two', '3': 'Three', '4': 'Four', '5': 'Five', '6': 'Six', '7': 'Seven', '8': 'Eight', '9': 'Nine', '0': 'Zero' } def initialize(self): intent = IntentBuilder("PairingIntent") \ .require("PairingKeyword").require("DeviceKeyword").build() self.register_intent(intent, self.handle_pairing) self.emitter.on("mycroft.not.paired", self.not_paired) def not_paired(self, message): self.speak_dialog("pairing.not.paired") self.handle_pairing() def handle_pairing(self, message=None): if self.is_paired(): self.speak_dialog("pairing.paired") elif self.data: self.speak_code() else: self.data = self.api.get_code(self.state) self.enclosure.deactivate_mouth_events() self.enclosure.mouth_text(self.data.get("code")) self.speak_code() self.__create_activator() def activate(self): try: token = self.data.get("token") login = self.api.activate(self.state, token) self.enclosure.activate_mouth_events() self.speak_dialog("pairing.paired") IdentityManager.save(login) self.emitter.emit(Message("mycroft.paired", login)) except: self.data["expiration"] -= self.delay if self.data.get("expiration") <= 0: self.data = None self.handle_pairing() else: self.__create_activator() def __create_activator(self): self.activator = Timer(self.delay, self.activate) self.activator.daemon = True self.activator.start() def is_paired(self): try: device = self.api.find() except: device = None return device is not None def speak_code(self): code = self.data.get("code") self.log.info("Pairing code: " + code) data = {"code": '. '.join(map(self.nato_dict.get, code))} self.speak_dialog("pairing.code", data) def stop(self): pass def shutdown(self): super(PairingSkill, self).shutdown() if self.activator: self.activator.cancel()
class SettingsMetaUploader: """Synchronize the contents of the settingsmeta.json file with the backend. The settingsmeta.json (or settingsmeta.yaml) file is defined by the skill author. It defines the user-configurable settings for a skill and contains instructions for how to display the skill's settings in the Selene web application (https://account.mycroft.ai). """ _msm_skill_display_name = None _settings_meta_path = None def __init__(self, skill_directory: str, skill_name: str): self.skill_directory = Path(skill_directory) self.skill_name = skill_name self.json_path = self.skill_directory.joinpath('settingsmeta.json') self.yaml_path = self.skill_directory.joinpath('settingsmeta.yaml') self.config = Configuration.get() self.settings_meta = {} self.api = None self.upload_timer = None self._stopped = None # Property placeholders self._msm = None self._skill_gid = None @property def msm(self): """Instance of the Mycroft Skills Manager""" if self._msm is None: msm_config = build_msm_config(self.config) self._msm = create_msm(msm_config) return self._msm def get_local_skills(self): return {skill.path: skill for skill in self.msm.local_skills.values()} @property def skill_gid(self): """Skill identifier recognized by backend and core. The skill_gid contains the device ID if the skill has been modified on that device. MSM does not know the ID of the device. So, if it finds a modified skill, it prepends the skill name portion of the ID with "@|". The device ID is known to this class. To "finalize" the skill_gid, insert the device ID between the "@" and the "|" """ api = self.api or DeviceApi() if api.identity.uuid: skills = self.get_local_skills() skill_dir = str(self.skill_directory) if skill_dir not in skills: self.msm.clear_cache() skills = self.get_local_skills() skill = skills[skill_dir] # If modified prepend the device uuid self._skill_gid = skill.skill_gid.replace( '@|', '@{}|'.format(api.identity.uuid)) return self._skill_gid else: return None @property def msm_skill_display_name(self): """Display name defined in MSM for use in settings meta.""" if self._msm_skill_display_name is None: skills = { skill.path: skill for skill in self.msm.local_skills.values() } skill = skills[str(self.skill_directory)] self._msm_skill_display_name = skill.meta_info.get('display_name') return self._msm_skill_display_name @property def settings_meta_path(self): """Fully qualified path to the settingsmeta file.""" if self._settings_meta_path is None: if self.yaml_path.is_file(): self._settings_meta_path = self.yaml_path else: self._settings_meta_path = self.json_path return self._settings_meta_path def upload(self): """Upload the contents of the settingsmeta file to Mycroft servers. The settingsmeta file does not change often, if at all. Only perform the upload if a change in the file is detected. """ synced = False if is_paired(): self.api = DeviceApi() if self.api.identity.uuid: settings_meta_file_exists = (self.json_path.is_file() or self.yaml_path.is_file()) if settings_meta_file_exists: self._load_settings_meta_file() self._update_settings_meta() LOG.debug('Uploading settings meta for ' + self.skill_gid) synced = self._issue_api_call() else: LOG.debug('settingsmeta.json not uploaded - no identity') else: LOG.debug('settingsmeta.json not uploaded - device is not paired') if not synced and not self._stopped: self.upload_timer = Timer(ONE_MINUTE, self.upload) self.upload_timer.daemon = True self.upload_timer.start() def stop(self): """ Stop upload attempts if Timer is running.""" if self.upload_timer: self.upload_timer.cancel() # Set stopped flag if upload is running when stop is called. self._stopped = True def _load_settings_meta_file(self): """Read the contents of the settingsmeta file into memory.""" # Imported here do handle issue with readthedocs build import yaml _, ext = os.path.splitext(str(self.settings_meta_path)) is_json_file = self.settings_meta_path.suffix == ".json" try: with open(str(self.settings_meta_path)) as meta_file: if is_json_file: self.settings_meta = json.load(meta_file) else: self.settings_meta = yaml.safe_load(meta_file) except Exception: log_msg = "Failed to load settingsmeta file: " LOG.exception(log_msg + str(self.settings_meta_path)) def _update_settings_meta(self): """Make sure the skill gid and name are included in settings meta. Even if a skill does not have a settingsmeta file, we will upload settings meta JSON containing a skill gid and name """ # Insert skill_gid and display_name self.settings_meta.update( skill_gid=self.skill_gid, display_name=(self.msm_skill_display_name or self.settings_meta.get('name') or get_display_name(self.skill_name))) for deprecated in ('color', 'identifier', 'name'): if deprecated in self.settings_meta: log_msg = ('DEPRECATION WARNING: The "{}" attribute in the ' 'settingsmeta file is no longer supported.') LOG.warning(log_msg.format(deprecated)) del (self.settings_meta[deprecated]) def _issue_api_call(self): """Use the API to send the settings meta to the server.""" try: self.api.upload_skill_metadata(self.settings_meta) except Exception: LOG.exception('Failed to upload skill settings meta ' 'for {}'.format(self.skill_gid)) success = False else: success = True return success
class SkillSettingsDownloader: """Manages the contents of the settings.json file. The settings.json file contains a set of name/value pairs representing the values of the settings defined in settingsmeta.json """ def __init__(self, bus): self.bus = bus self.continue_downloading = True self.changed_callback = None self.settings_meta_fields = None self.last_download_result = {} self.remote_settings = None self.settings_changed = False self.api = DeviceApi() self.download_timer = None def stop_downloading(self): """Stop synchronizing backend and core.""" self.continue_downloading = False if self.download_timer: self.download_timer.cancel() # TODO: implement as websocket def download(self): """Download the settings stored on the backend and check for changes""" if is_paired(): download_success = self._get_remote_settings() if download_success: self.settings_changed = (self.last_download_result != self.remote_settings) if self.settings_changed: LOG.debug('Skill settings changed since last download') self._emit_settings_change_events() self.last_download_result = self.remote_settings else: LOG.debug('No skill settings changes since last download') else: LOG.debug('Settings not downloaded - device is not paired') # If this method is called outside of the timer loop, ensure the # existing timer is canceled before starting a new one. if self.download_timer: self.download_timer.cancel() if self.continue_downloading: self.download_timer = Timer(ONE_MINUTE, self.download) self.download_timer.daemon = True self.download_timer.start() def _get_remote_settings(self): """Get the settings for this skill from the server Returns: skill_settings (dict or None): returns a dict if matches """ try: remote_settings = self.api.get_skill_settings() except Exception: LOG.exception('Failed to download remote settings from server.') success = False else: self.remote_settings = remote_settings success = True return success def _emit_settings_change_events(self): for skill_gid, remote_settings in self.remote_settings.items(): settings_changed = False try: previous_settings = self.last_download_result[skill_gid] except KeyError: if remote_settings: settings_changed = True except Exception: LOG.exception('error occurred handling setting change events') else: if previous_settings != remote_settings: settings_changed = True if settings_changed: log_msg = 'Emitting skill.settings.change event for skill {} ' LOG.info(log_msg.format(skill_gid)) message = Message('mycroft.skills.settings.changed', data={skill_gid: remote_settings}) self.bus.emit(message)
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(bus) 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(dialog.get("message_synching.clock")) # Force a sync of the local clock with the internet config = Configuration.get() platform = config['enclosure'].get("platform", "unknown") if platform in ['mycroft_mark_1', 'picroft']: bus.wait_for_response(Message('system.ntp.sync'), 'system.ntp.sync.complete', 15) if not is_paired(): try_update_system(platform) # Check if the time skewed significantly. If so, reboot skew = abs((time.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'. # data = {'utterance': dialog.get("time.changed.reboot")} bus.emit(Message("speak", data)) wait_while_speaking() # provide visual indicators of the reboot enclosure.mouth_text(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 bus.emit(Message("system.reboot")) return else: bus.emit(Message("enclosure.mouth.reset")) time.sleep(0.5) enclosure.eyes_color(189, 183, 107) # dark khaki enclosure.mouth_text(dialog.get("message_loading.skills")) bus.emit(Message('mycroft.internet.connected')) # check for pairing, if not automatically start pairing try: if not is_paired(ignore_errors=False): payload = { 'utterances': ["pair my device"], 'lang': "en-us" } bus.emit(Message("recognizer_loop:utterance", payload)) else: from mycroft.api import DeviceApi api = DeviceApi() api.update_version() except BackendDown: data = {'utterance': dialog.get("backend.down")} bus.emit(Message("speak", data)) bus.emit(Message("backend.down")) else: thread = Timer(1, check_connection) thread.daemon = True thread.start()
class SkillSettings(dict): """ Dictionary that can easily be saved to a file, serialized as json. It also syncs to the backend for skill settings Args: directory (str): Path to storage directory name (str): user readable name associated with the settings """ def __init__(self, directory, name): super(SkillSettings, self).__init__() # when skills try to instantiate settings # in __init__, it can erase the settings saved # on disk (settings.json). So this prevents that # This is set to true in core.py after skill init self.allow_overwrite = False self.api = DeviceApi() self.config = ConfigurationManager.get() self.name = name # set file paths self._settings_path = join(directory, 'settings.json') self._meta_path = join(directory, 'settingsmeta.json') self.is_alive = True self.loaded_hash = hash(json.dumps(self, sort_keys=True)) self._complete_intialization = False self._device_identity = None self._api_path = None self._user_identity = None self.changed_callback = None self._poll_timer = None self._is_alive = True # if settingsmeta exist if isfile(self._meta_path): self._poll_skill_settings() def __hash__(self): """ Simple object unique hash. """ return hash(str(id(self)) + self.name) def run_poll(self, _=None): """Immediately poll the web for new skill settings""" if self._poll_timer: self._poll_timer.cancel() self._poll_skill_settings() def stop_polling(self): self._is_alive = False if self._poll_timer: self._poll_timer.cancel() def set_changed_callback(self, callback): """ Set callback to perform when server settings have changed. Args: callback: function/method to call when settings have changed """ self.changed_callback = callback # TODO: break this up into two classes def initialize_remote_settings(self): """ initializes the remote settings to the server """ # if settingsmeta.json exists (and is valid) # this block of code is a control flow for # different scenarios that may arises with settingsmeta self.load_skill_settings_from_file() # loads existing settings.json settings_meta = self._load_settings_meta() if not settings_meta: return if not is_paired(): return self._device_identity = self.api.identity.uuid self._api_path = "/" + self._device_identity + "/skill" try: self._user_identity = self.api.get()['user']['uuid'] except RequestException: return hashed_meta = self._get_meta_hash(settings_meta) skill_settings = self._request_other_settings(hashed_meta) # if hash is new then there is a diff version of settingsmeta if self._is_new_hash(hashed_meta): # first look at all other devices on user account to see # if the settings exist. if it does then sync with device if skill_settings: # not_owner flags that this settings is loaded from # another device. If a skill settings doesn't have # not_owner, then the skill is created from that device self['not_owner'] = True self.save_skill_settings(skill_settings) else: # upload skill settings if uuid = self._load_uuid() if uuid is not None: self._delete_metadata(uuid) self._upload_meta(settings_meta, hashed_meta) else: # hash is not new if skill_settings is not None: self['not_owner'] = True self.save_skill_settings(skill_settings) else: settings = self._request_my_settings(hashed_meta) if settings is None: # metadata got deleted from Home, send up self._upload_meta(settings_meta, hashed_meta) else: self.save_skill_settings(settings) self._complete_intialization = True @property def _is_stored(self): return hash(json.dumps(self, sort_keys=True)) == self.loaded_hash def __getitem__(self, key): """ Get key """ return super(SkillSettings, self).__getitem__(key) def __setitem__(self, key, value): """ Add/Update key. """ if self.allow_overwrite or key not in self: return super(SkillSettings, self).__setitem__(key, value) def _load_settings_meta(self): """ Loads settings metadata from skills path. """ if isfile(self._meta_path): try: with open(self._meta_path, encoding='utf-8') as f: data = json.load(f) return data except Exception as e: LOG.error("Failed to load setting file: " + self._meta_path) LOG.error(repr(e)) return None else: return None def _send_settings_meta(self, settings_meta): """ Send settingsmeta.json to the server. Args: settings_meta (dict): dictionary of the current settings meta Returns: dict: uuid, a unique id for the setting meta data """ try: uuid = self._put_metadata(settings_meta) return uuid except HTTPError as e: if e.response.status_code in [422, 500, 501]: raise DelayRequest else: LOG.error(e) return None except Exception as e: LOG.error(e) return None def save_skill_settings(self, skill_settings): """ Takes skill object and save onto self Args: skill_settings (dict): skill """ if self._is_new_hash(skill_settings['identifier']): self._save_uuid(skill_settings['uuid']) self._save_hash(skill_settings['identifier']) sections = skill_settings['skillMetadata']['sections'] for section in sections: for field in section["fields"]: if "name" in field and "value" in field: self[field['name']] = field['value'] self.store() def _load_uuid(self): """ Loads uuid Returns: str: uuid of the previous settingsmeta """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') uuid = None if isfile(uuid_file): with open(uuid_file, 'r') as f: uuid = f.read() return uuid def _save_uuid(self, uuid): """ Saves uuid. Args: uuid (str): uuid, unique id of new settingsmeta """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') os.makedirs(directory, exist_ok=True) with open(uuid_file, 'w') as f: f.write(str(uuid)) def _uuid_exist(self): """ Checks if there is an uuid file. Returns: bool: True if uuid file exist False otherwise """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') return isfile(uuid_file) def _migrate_settings(self, settings_meta): """ sync settings.json and settingsmeta.json in memory """ meta = settings_meta.copy() self.load_skill_settings_from_file() sections = meta['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section['fields']): if 'name' in field: if field["name"] in self: sections[i]['fields'][j]['value'] = \ str(self.__getitem__(field['name'])) meta['skillMetadata']['sections'] = sections return meta def _upload_meta(self, settings_meta, hashed_meta): """ uploads the new meta data to settings with settings migration Args: settings_meta (dict): settingsmeta.json hashed_meta (str): {skill-folder}-settinsmeta.json """ meta = self._migrate_settings(settings_meta) meta['identifier'] = str(hashed_meta) response = self._send_settings_meta(meta) if response and 'uuid' in response: self._save_uuid(response['uuid']) if 'not_owner' in self: del self['not_owner'] self._save_hash(hashed_meta) def hash(self, string): """ md5 hasher for consistency across cpu architectures """ return hashlib.md5(bytes(string, 'utf-8')).hexdigest() def _get_meta_hash(self, settings_meta): """ Gets the hash of skill Args: settings_meta (dict): settingsmeta object Returns: _hash (str): hashed to identify skills """ _hash = self.hash( json.dumps(settings_meta, sort_keys=True) + self._user_identity) return "{}--{}".format(self.name, _hash) def _save_hash(self, hashed_meta): """ Saves hashed_meta to settings directory. Args: hashed_meta (str): hash of new settingsmeta """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) hash_file = join(directory, 'hash') os.makedirs(directory, exist_ok=True) with open(hash_file, 'w') as f: f.write(hashed_meta) def _is_new_hash(self, hashed_meta): """ Check if stored hash is the same as current. If the hashed file does not exist, usually in the case of first load, then the create it and return True Args: hashed_meta (str): hash of metadata and uuid of device Returns: bool: True if hash is new, otherwise False """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) hash_file = join(directory, 'hash') if isfile(hash_file): with open(hash_file, 'r') as f: current_hash = f.read() return False if current_hash == str(hashed_meta) else True return True def update_remote(self): """ update settings state from server """ skills_settings = None settings_meta = self._load_settings_meta() if settings_meta is None: return hashed_meta = self._get_meta_hash(settings_meta) if self.get('not_owner'): skills_settings = self._request_other_settings(hashed_meta) if not skills_settings: skills_settings = self._request_my_settings(hashed_meta) if skills_settings is not None: self.save_skill_settings(skills_settings) self.store() else: settings_meta = self._load_settings_meta() self._upload_meta(settings_meta, hashed_meta) def _poll_skill_settings(self): """ If identifier exists for this skill poll to backend to request settings and store it if it changes TODO: implement as websocket """ delay = 1 original = hash(str(self)) try: if not is_paired(): pass elif not self._complete_intialization: self.initialize_remote_settings() else: self.update_remote() except DelayRequest: LOG.info('{}: Delaying next settings fetch'.format(self.name)) delay = 5 except Exception as e: LOG.exception('Failed to fetch skill settings: {}'.format(repr(e))) finally: # Call callback for updated settings if self._complete_intialization: if self.changed_callback and hash(str(self)) != original: self.changed_callback() if self._poll_timer: self._poll_timer.cancel() if not self._is_alive: return # continues to poll settings every minute self._poll_timer = Timer(delay * 60, self._poll_skill_settings) self._poll_timer.daemon = True self._poll_timer.start() def load_skill_settings_from_file(self): """ If settings.json exist, open and read stored values into self """ if isfile(self._settings_path): with open(self._settings_path) as f: try: json_data = json.load(f) for key in json_data: self[key] = json_data[key] except Exception as e: # TODO: Show error on webUI. Dev will have to fix # metadata to be able to edit later. LOG.error(e) def _type_cast(self, settings_meta, to_platform): """ Tranform data type to be compatible with Home and/or Core. e.g. Web to core "true" => True, "1.4" => 1.4 core to Web False => "false' Args: settings_meta (dict): skills object to_platform (str): platform to convert compatible data types to Returns: dict: skills object """ meta = settings_meta.copy() sections = meta['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section.get('fields', [])): _type = field.get('type') if _type == 'checkbox': value = field.get('value') if to_platform == 'web': if value is True or value == 'True': sections[i]['fields'][j]['value'] = 'true' elif value is False or value == 'False': sections[i]['fields'][j]['value'] = 'false' elif to_platform == 'core': if value == 'true' or value == 'True': sections[i]['fields'][j]['value'] = True elif value == 'false' or value == 'False': sections[i]['fields'][j]['value'] = False elif _type == 'number': value = field.get('value') if to_platform == 'core': if "." in value: sections[i]['fields'][j]['value'] = float(value) else: sections[i]['fields'][j]['value'] = int(value) elif to_platform == 'web': sections[i]['fields'][j]['value'] = str(value) meta['skillMetadata']['sections'] = sections return meta def _request_my_settings(self, identifier): """ Get skill settings for this device associated with the identifier Args: identifier (str): a hashed_meta Returns: skill_settings (dict or None): returns a dict if matches """ settings = self._request_settings() if settings: # this loads the settings into memory for use in self.store for skill_settings in settings: if skill_settings['identifier'] == identifier: skill_settings = \ self._type_cast(skill_settings, to_platform='core') self._remote_settings = skill_settings return skill_settings return None def _request_settings(self): """ Get all skill settings for this device from server. Returns: dict: dictionary with settings collected from the server. """ try: settings = self.api.request({ "method": "GET", "path": self._api_path }) except RequestException: return None settings = [skills for skills in settings if skills is not None] return settings def _request_other_settings(self, identifier): """ Retrieve skill settings from other devices by identifier Args: identifier (str): identifier for this skill Returns: settings (dict or None): the retrieved settings or None """ path = \ "/" + self._device_identity + "/userSkill?identifier=" + identifier try: user_skill = self.api.request({"method": "GET", "path": path}) except RequestException: # Some kind of Timeout, connection HTTPError, etc. user_skill = None if not user_skill: return None else: settings = self._type_cast(user_skill[0], to_platform='core') return settings def _put_metadata(self, settings_meta): """ PUT settingsmeta to backend to be configured in server. used in place of POST and PATCH. Args: settings_meta (dict): dictionary of the current settings meta data """ settings_meta = self._type_cast(settings_meta, to_platform='web') return self.api.request({ "method": "PUT", "path": self._api_path, "json": settings_meta }) def _delete_metadata(self, uuid): """ Delete the current skill metadata Args: uuid (str): unique id of the skill """ try: LOG.debug("deleting metadata") self.api.request({ "method": "DELETE", "path": self._api_path + "/{}".format(uuid) }) except Exception as e: LOG.error(e) LOG.error("cannot delete metadata because this" "device is not original uploader of skill") @property def _should_upload_from_change(self): changed = False if hasattr(self, '_remote_settings'): sections = self._remote_settings['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section['fields']): if 'name' in field: # Ensure that the field exists in settings and that # it has a value to compare if (field["name"] in self and 'value' in sections[i]['fields'][j]): remote_val = sections[i]['fields'][j]["value"] self_val = self.get(field['name']) if str(remote_val) != str(self_val): changed = True if self.get('not_owner'): changed = False return changed def store(self, force=False): """ Store dictionary to file if a change has occured. Args: force: Force write despite no change """ if force or not self._is_stored: with open(self._settings_path, 'w') as f: json.dump(self, f) self.loaded_hash = hash(json.dumps(self, sort_keys=True)) if self._should_upload_from_change: settings_meta = self._load_settings_meta() hashed_meta = self._get_meta_hash(settings_meta) uuid = self._load_uuid() if uuid is not None: self._delete_metadata(uuid) self._upload_meta(settings_meta, hashed_meta)
class SkillSettings(dict): """ SkillSettings creates a dictionary that can easily be stored to file, serialized as json. It also syncs to the backend for skill settings Args: settings_file (str): Path to storage file """ def __init__(self, directory, name): super(SkillSettings, self).__init__() self.api = DeviceApi() self._device_identity = self.api.identity.uuid self.config = ConfigurationManager.get() self.name = name # set file paths self._settings_path = join(directory, 'settings.json') self._meta_path = join(directory, 'settingsmeta.json') self._api_path = "/" + self._device_identity + "/skill" self.is_alive = True self.loaded_hash = hash(str(self)) # if settingsmeta.json exists # this block of code is a control flow for # different scenarios that may arises with settingsmeta if isfile(self._meta_path): LOG.info("settingsmeta.json exist for {}".format(self.name)) settings_meta = self._load_settings_meta() hashed_meta = hash(str(settings_meta) + str(self._device_identity)) # check if hash is different from the saved hashed if self._is_new_hash(hashed_meta): LOG.info("looks like settingsmeta.json " + "has changed for {}".format(self.name)) # TODO: once the delete api for device is created uncomment if self._uuid_exist(): try: LOG.info("a uuid exist for {}".format(self.name) + " deleting old one") old_uuid = self._load_uuid() self._delete_metatdata(old_uuid) except Exception as e: LOG.info(e) LOG.info("sending settingsmeta.json for {}".format(self.name) + " to home.mycroft.ai") new_uuid = self._send_settings_meta(settings_meta, hashed_meta) self._save_uuid(new_uuid) self._save_hash(hashed_meta) else: # if hash is old found_in_backend = False settings = self._get_remote_settings() # checks backend if th settings have been deleted # through web ui for skill in settings: if skill["identifier"] == str(hashed_meta): found_in_backend = True # if it's been deleted from web ui # resend the settingsmeta.json if found_in_backend is False: LOG.info("seems like it got deleted from home... " + "sending settingsmeta.json for " + "{}".format(self.name)) new_uuid = self._send_settings_meta( settings_meta, hashed_meta) self._save_uuid(new_uuid) self._save_hash(hashed_meta) t = Timer(60, self._poll_skill_settings, [hashed_meta]) t.daemon = True t.start() self.load_skill_settings() @property def _is_stored(self): return hash(str(self)) == self.loaded_hash def __getitem__(self, key): """ Get key """ return super(SkillSettings, self).__getitem__(key) def __setitem__(self, key, value): """ Add/Update key. """ return super(SkillSettings, self).__setitem__(key, value) def _load_settings_meta(self): """ loads settings metadata from skills path """ with open(self._meta_path) as f: data = json.load(f) return data def _send_settings_meta(self, settings_meta, hashed_meta): """ send settingsmeta.json to the backend Args: param1 (dict): dictionary of the current settings meta data param1 (int): hashed settings meta data Returns: uuid (str): a unique id for the setting meta data """ try: settings_meta["identifier"] = str(hashed_meta) self._put_metadata(settings_meta) settings = self._get_remote_settings() skill_identity = str(hashed_meta) uuid = None # TODO: note uuid should be returned from the put request for skill_setting in settings: if skill_setting['identifier'] == skill_identity: uuid = skill_setting["uuid"] return uuid except Exception as e: LOG.error(e) def _load_uuid(self): """ loads uuid Returns: uuid (str): uuid of the previous settingsmeta """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') if isfile(uuid_file): with open(uuid_file, 'r') as f: uuid = f.read() return uuid def _save_uuid(self, uuid): """ saves uuid to path Args: param1 (str): uuid of new seetingsmeta """ LOG.info("saving uuid {}".format(str(uuid))) directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') with open(uuid_file, 'w') as f: f.write(str(uuid)) def _save_hash(self, hashed_meta): """ saves hashed_meta to path Args: param1 (int): hashed of new seetingsmeta """ LOG.info("saving hash {}".format(str(hashed_meta))) directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) hash_file = join(directory, 'hash') with open(hash_file, 'w') as f: f.write(str(hashed_meta)) def _uuid_exist(self): """ checks if there is a uuid file Returns: bool: True if uuid file exist False otherwise """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') return isfile(uuid_file) def _is_new_hash(self, hashed_meta): """ checks if the stored hash is the same as current. if the hashed file does not exist, usually in the case of first load, then the create it and return True Args: param1 (int): hash of metadata and uuid of device Returns: bool: True if hash is new False otherwise """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) hash_file = join(directory, 'hash') if isfile(hash_file): with open(hash_file, 'r') as f: current_hash = f.read() return False if current_hash == str(hashed_meta) else True return True def _poll_skill_settings(self, hashed_meta): """ If identifier exists for this skill poll to backend to request settings and store it if it changes TODO: implement as websocket Args: param1 (int): the hashed identifier """ LOG.info("getting settings from home.mycroft.ai") try: # update settings settings = self._get_remote_settings() skill_identity = str(hashed_meta) for skill_setting in settings: if skill_setting['identifier'] == skill_identity: sections = skill_setting['skillMetadata']['sections'] for section in sections: for field in section["fields"]: self.__setitem__(field["name"], field["value"]) # store value if settings has changed from backend self.store() except Exception as e: LOG.error(e) if self.is_alive: # continues to poll settings every 60 seconds t = Timer(60, self._poll_skill_settings, [hashed_meta]) t.daemon = True t.start() def load_skill_settings(self): """ If settings.json exist, open and read stored values into self """ if isfile(self._settings_path): with open(self._settings_path) as f: try: json_data = json.load(f) for key in json_data: self.__setitem__(key, json_data[key]) except Exception as e: # TODO: Show error on webUI. Dev will have to fix # metadata to be able to edit later. LOG.error(e) def _get_remote_settings(self): """ Get skill settings for this device from backend """ settings = self.api.request({"method": "GET", "path": self._api_path}) settings = [skills for skills in settings if skills is not None] return settings def _put_metadata(self, settings_meta): """ PUT settingsmeta to backend to be configured in home.mycroft.ai. used in plcae of POST and PATCH """ return self.api.request({ "method": "PUT", "path": self._api_path, "json": settings_meta }) def _delete_metatdata(self, uuid): """ Deletes the current skill metadata Args: param1 (str): unique id of the skill """ return self.api.request({ "method": "DELETE", "path": self._api_path + "/{}".format(uuid) }) def store(self, force=False): """ Store dictionary to file if a change has occured. Args: force: Force write despite no change """ if force or not self._is_stored: with open(self._settings_path, 'w') as f: json.dump(self, f) self.loaded_hash = hash(str(self))
class SkillSettings(dict): """ SkillSettings creates a dictionary that can easily be stored to file, serialized as json. It also syncs to the backend for skill settings Args: settings_file (str): Path to storage file """ def __init__(self, directory): super(SkillSettings, self).__init__() self.api = DeviceApi() self._device_identity = self.api.identity.uuid # set file paths self._settings_path = join(directory, 'settings.json') self._meta_path = join(directory, 'settingsmeta.json') self._api_path = "/" + self._device_identity + "/skill" self.loaded_hash = hash(str(self)) # if settingsmeta.json exists if isfile(self._meta_path): self.settings_meta = self._load_settings_meta() self.settings = self._get_settings() self._send_settings_meta() # start polling timer Timer(60, self._poll_skill_settings).start() self.load_skill_settings() @property def _is_stored(self): return hash(str(self)) == self.loaded_hash def __getitem__(self, key): return super(SkillSettings, self).__getitem__(key) def __setitem__(self, key, value): """ Add/Update key. """ return super(SkillSettings, self).__setitem__(key, value) def _load_settings_meta(self): with open(self._meta_path) as f: data = json.load(f) return data def _skill_exist_in_backend(self): """ see if skill settings already exist in the backend """ skill_identity = self._get_skill_identity() for skill_setting in self.settings: if skill_identity == skill_setting["identifier"]: return True return False def _send_settings_meta(self): """ send settingsmeta.json to the backend if skill doesn't already exist """ try: if self._skill_exist_in_backend() is False: response = self._put_metadata(self.settings_meta) except Exception as e: LOG.error(e) def _poll_skill_settings(self): """ If identifier exists for this skill poll to backend to request settings and store it if it changes TODO: implement as websocket """ if self._skill_exist_in_backend(): try: # update settings self.settings = self._get_settings() skill_identity = self._get_skill_identity() for skill_setting in self.settings: if skill_setting['identifier'] == skill_identity: sections = skill_setting['skillMetadata']['sections'] for section in sections: for field in section["fields"]: self.__setitem__(field["name"], field["value"]) # store value if settings has changed from backend self.store() except Exception as e: LOG.error(e) # poll backend every 60 seconds for new settings Timer(60, self._poll_skill_settings).start() def _get_skill_identity(self): """ returns the skill identifier """ try: return self.settings_meta["identifier"] except Exception as e: LOG.error(e) return None def load_skill_settings(self): """ If settings.json exist, open and read stored values into self """ if isfile(self._settings_path): with open(self._settings_path) as f: try: json_data = json.load(f) for key in json_data: self.__setitem__(key, json_data[key]) except Exception as e: # TODO: Show error on webUI. Dev will have to fix # metadata to be able to edit later. LOG.error(e) def _get_settings(self): """ Get skill settings for this device from backend """ return self.api.request({ "method": "GET", "path": self._api_path }) def _put_metadata(self, settings_meta): """ PUT settingsmeta to backend to be configured in home.mycroft.ai. used in plcae of POST and PATCH """ return self.api.request({ "method": "PUT", "path": self._api_path, "json": settings_meta }) def store(self, force=False): """ Store dictionary to file if a change has occured. Args: force: Force write despite no change """ if force or not self._is_stored: with open(self._settings_path, 'w') as f: json.dump(self, f) self.loaded_hash = hash(str(self))
def get_oauth_token(self): try: token = DeviceApi().get_oauth_token(1172752248686736379) except requests.HTTPError: return None return token['access_token']
class PairingSkill(MycroftSkill): poll_frequency = 10 # secs between checking server for activation def __init__(self): super(PairingSkill, self).__init__("PairingSkill") self.api = DeviceApi() self.data = None self.time_code_expires = None self.state = str(uuid4()) self.activator = None self.activator_lock = Lock() self.activator_cancelled = False self.counter_lock = Lock() self.count = -1 # for repeating pairing code. -1 = not running # TODO:18.02 Add translation support # Can't change before then for fear of breaking really old mycroft-core # instances that just came up on wifi and haven't upgraded code yet. self.nato_dict = { 'A': "'A' as in Apple", 'B': "'B' as in Bravo", 'C': "'C' as in Charlie", 'D': "'D' as in Delta", 'E': "'E' as in Echo", 'F': "'F' as in Fox trot", 'G': "'G' as in Golf", 'H': "'H' as in Hotel", 'I': "'I' as in India", 'J': "'J' as in Juliet", 'K': "'K' as in Kilogram", 'L': "'L' as in London", 'M': "'M' as in Mike", 'N': "'N' as in November", 'O': "'O' as in Oscar", 'P': "'P' as in Paul", 'Q': "'Q' as in Quebec", 'R': "'R' as in Romeo", 'S': "'S' as in Sierra", 'T': "'T' as in Tango", 'U': "'U' as in Uniform", 'V': "'V' as in Victor", 'W': "'W' as in Whiskey", 'X': "'X' as in X-Ray", 'Y': "'Y' as in Yankee", 'Z': "'Z' as in Zebra", '1': 'One', '2': 'Two', '3': 'Three', '4': 'Four', '5': 'Five', '6': 'Six', '7': 'Seven', '8': 'Eight', '9': 'Nine', '0': 'Zero' } def initialize(self): # TODO:18.02 - use decorator intent = IntentBuilder("PairingIntent") \ .require("PairingKeyword").require("DeviceKeyword").build() self.register_intent(intent, self.handle_pairing) self.add_event("mycroft.not.paired", self.not_paired) def not_paired(self, message): self.speak_dialog("pairing.not.paired") self.handle_pairing() def handle_pairing(self, message=None): if self.is_paired(): # Already paired! Just tell user self.speak_dialog("pairing.paired") elif not self.data: # Kick off pairing... with self.counter_lock: if self.count > -1: # We snuck in to this handler somehow while the pairing process # is still being setup. Ignore it. self.log.debug("Ignoring call to handle_pairing") return # Not paired or already pairing, so start the process. self.count = 0 self.reload_skill = False # Prevent restart during the process self.log.debug("Kicking off pairing sequence") try: # Obtain a pairing code from the backend self.data = self.api.get_code(self.state) # Keep track of when the code was obtained. The codes expire # after 20 hours. self.time_code_expires = time.time() + 72000 # 20 hours except Exception as e: self.log.debug("Failed to get pairing code: " + repr(e)) self.speak_dialog('connection.error') self.emitter.emit(Message("mycroft.mic.unmute", None)) with self.counter_lock: self.count = -1 return # wait_while_speaking() support is mycroft-core 0.8.16+ # TODO:18.02 - Remove this try/catch and migrate to the preferred # mycroft.audio.wait_while_speaking try: # This will make sure the user is in 0.8.16+ before continuing # so a < 0.8.16 system will skip writing the URL to the mouth mycroft.util.wait_while_speaking() self.speak_dialog("pairing.intro") self.enclosure.deactivate_mouth_events() self.enclosure.mouth_text("home.mycroft.ai ") # HACK this gives the Mark 1 time to scroll the address and # the user time to browse to the website. # TODO: mouth_text() really should take an optional parameter # to not scroll a second time. time.sleep(7) mycroft.util.wait_while_speaking() except: pass if not self.activator: self.__create_activator() def check_for_activate(self): """ Function called ever 10 seconds by Timer. Checks if user has activated the device yet on home.mycroft.ai and if not repeats the pairing code every 60 seconds. """ try: # Attempt to activate. If the user has completed pairing on the, # backend, this will succeed. Otherwise it throws and HTTPError() token = self.data.get("token") login = self.api.activate(self.state, token) # HTTPError() thrown # When we get here, the pairing code has been entered on the # backend and pairing can now be saved. # The following is kinda ugly, but it is really critical that we get # this saved successfully or we need to let the user know that they # have to perform pairing all over again at the website. try: IdentityManager.save(login) except Exception as e: self.log.debug("First save attempt failed: " + repr(e)) time.sleep(2) try: IdentityManager.save(login) except Exception as e2: # Something must be seriously wrong self.log.debug("Second save attempt failed: " + repr(e2)) self.abort_and_restart() # is_speaking() and stop_speaking() support is mycroft-core 0.8.16+ try: if mycroft.util.is_speaking(): # Assume speaking is the pairing code. Stop TTS of that. mycroft.util.stop_speaking() except: pass self.enclosure.activate_mouth_events() # clears the display # Tell user they are now paired self.speak_dialog("pairing.paired") try: mycroft.util.wait_while_speaking() except: pass # Notify the system it is paired and ready self.emitter.emit(Message("mycroft.paired", login)) # Un-mute. Would have been muted during onboarding for a new # unit, and not dangerous to do if pairing was started # independently. self.emitter.emit(Message("mycroft.mic.unmute", None)) # Send signal to update configuration self.emitter.emit(Message("configuration.updated")) # Allow this skill to auto-update again self.reload_skill = True except HTTPError: # speak pairing code every 60th second with self.counter_lock: if self.count == 0: self.speak_code() self.count = (self.count + 1) % 6 if time.time() > self.time_code_expires: # After 20 hours the token times out. Restart # the pairing process. with self.counter_lock: self.count = -1 self.data = None self.handle_pairing() else: # trigger another check in 10 seconds self.__create_activator() except Exception as e: self.log.debug("Unexpected error: " + repr(e)) self.abort_and_restart() def abort_and_restart(self): # restart pairing sequence self.enclosure.activate_mouth_events() self.speak_dialog("unexpected.error.restarting") self.emitter.emit(Message("mycroft.not.paired")) with self.counter_lock: self.count = -1 self.activator = None def __create_activator(self): # Create a timer that will poll the backend in 10 seconds to see # if the user has completed the device registration process with self.activator_lock: if not self.activator_cancelled: self.activator = Timer(PairingSkill.poll_frequency, self.check_for_activate) self.activator.daemon = True self.activator.start() def is_paired(self): """ Determine if pairing process has completed. """ try: device = self.api.get() except: device = None return device is not None def speak_code(self): """ Speak pairing code. """ code = self.data.get("code") self.log.info("Pairing code: " + code) data = {"code": '. '.join(map(self.nato_dict.get, code))} # Make sure code stays on display self.enclosure.deactivate_mouth_events() self.enclosure.mouth_text(self.data.get("code")) self.speak_dialog("pairing.code", data) def shutdown(self): super(PairingSkill, self).shutdown() with self.activator_lock: self.activator_cancelled = True if self.activator: self.activator.cancel() if self.activator: self.activator.join()