Exemplo n.º 1
0
    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)
Exemplo n.º 2
0
    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()
Exemplo n.º 3
0
    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
Exemplo n.º 4
0
    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)
Exemplo n.º 5
0
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()
Exemplo n.º 6
0
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
Exemplo n.º 7
0
    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()
Exemplo n.º 8
0
    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()
Exemplo n.º 9
0
    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'
Exemplo n.º 10
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')
Exemplo n.º 11
0
 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
Exemplo n.º 12
0
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()
Exemplo n.º 13
0
    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()
Exemplo n.º 14
0
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)
Exemplo n.º 15
0
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()
Exemplo n.º 16
0
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)
Exemplo n.º 17
0
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)
Exemplo n.º 18
0
    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
Exemplo n.º 19
0
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
Exemplo n.º 20
0
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)
Exemplo n.º 21
0
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()
Exemplo n.º 22
0
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)
Exemplo n.º 23
0
 def __init__(self):
     super(ConfigurationSkill, self).__init__("ConfigurationSkill")
     self.max_delay = self.config.get('max_delay')
     self.api = DeviceApi()
Exemplo n.º 24
0
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()
Exemplo n.º 25
0
    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)
Exemplo n.º 26
0
    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
Exemplo n.º 27
0
 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()
Exemplo n.º 28
0
 def room_name(self):
     # Assume the "name" of the device is the "room name"
     device = DeviceApi().get()
     return device["name"]
Exemplo n.º 29
0
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
Exemplo n.º 30
0
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()
Exemplo n.º 31
0
 def get_device_name(self):
     try:
         return DeviceApi().get()['name']
     except:
         self.log.exception('API Error')
         return ':error:'
Exemplo n.º 32
0
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()
Exemplo n.º 33
0
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
Exemplo n.º 34
0
 def __init__(self):
     super(ConfigurationSkill, self).__init__("ConfigurationSkill")
     self.max_delay = self.config.get('max_delay')
     self.api = DeviceApi()
Exemplo n.º 35
0
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)
Exemplo n.º 36
0
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()
Exemplo n.º 37
0
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)
Exemplo n.º 38
0
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))
Exemplo n.º 39
0
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))
Exemplo n.º 40
0
 def get_oauth_token(self):
     try:
         token = DeviceApi().get_oauth_token(1172752248686736379)
     except requests.HTTPError:
         return None
     return token['access_token']
Exemplo n.º 41
0
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()