コード例 #1
0
    def load_brain(self):
        LOG.info('Loading Brain')
        if (isfile(self.brain_path) and getsize(self.brain_path) >
            (1024 * 1024)):
            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)
        try:
            device = DeviceApi().get()
        except Exception:
            device = {"name": "Mycroft", "type": "AI"}
        self.kernel.setBotPredicate("name", device.get("name", 'Mycroft'))
        self.kernel.setBotPredicate("species",
                                    device.get("platform", "unknown"))
        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")

        self.brain_loaded = True
        return
コード例 #2
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)
コード例 #3
0
ファイル: settings.py プロジェクト: yazici/mycroft-core
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)
コード例 #4
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)
コード例 #5
0
ファイル: settings.py プロジェクト: MycroftAI/mycroft-core
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)
コード例 #6
0
ファイル: settings.py プロジェクト: Ceda-EI/mycroft-core
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)
コード例 #7
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()
コード例 #8
0
ファイル: __init__.py プロジェクト: domcross/fhem-skill
    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)
コード例 #9
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()
コード例 #10
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
コード例 #11
0
class Mympdplaylist(MycroftSkill):
    def __init__(self):
        super(Mympdplaylist, self).__init__(name="My MPD playlist manager")
        self.settings_change_callback = self.on_settings_changed
        self.on_settings_changed()

    def initialize(self):
        self.host = self.settings.get('radio_1')
        self.port = self.settings.get('port_1')
        self.placements = {self.settings.get('placement_1').lower():[self.settings.get('radio_1'),self.settings.get('port_1')],\
        self.settings.get('placement_2').lower():[self.settings.get('radio_2'),self.settings.get('port_2')],\
        self.settings.get('placement_3').lower():[self.settings.get('radio_3'),self.settings.get('port_3')],\
        self.settings.get('placement_4').lower():[self.settings.get('radio_4'),self.settings.get('port_4')],\
        self.settings.get('placement_5').lower():[self.settings.get('radio_5'),self.settings.get('port_5')]}
        #self.search_fields = [self.settings.get('trans_artist').lower(), self.settings.get('trans_title').lower(),\
        #                     self.settings.get('trans_album').lower(),self.settings.get('trans_genre').lower()]
        self.same_device = DeviceApi()
        info = self.same_device.get()
        self.same_device = info['description'].lower()

    def on_settings_changed(self):
        self.placement_1 = self.settings.get('placement_1', False).lower()
        self.placement_2 = self.settings.get('placement_2', False).lower()
        self.placement_3 = self.settings.get('placement_3', False).lower()
        self.placement_4 = self.settings.get('placement_4', False).lower()
        self.placement_5 = self.settings.get('placement_5', False).lower()
        self.placements = {self.settings.get('placement_1').lower():[self.settings.get('radio_1'),self.settings.get('port_1')],\
        self.settings.get('placement_2').lower():[self.settings.get('radio_2'),self.settings.get('port_2')],\
        self.settings.get('placement_3').lower():[self.settings.get('radio_3'),self.settings.get('port_3')],\
        self.settings.get('placement_4').lower():[self.settings.get('radio_4'),self.settings.get('port_4')],\
        self.settings.get('placement_5').lower():[self.settings.get('radio_5'),self.settings.get('port_5')]}

#Basic MPD functions

    def open_connection(self, radio):
        host = self.placements[radio][0]
        port = self.placements[radio][1]
        try:
            mpcc.connect(host, port, timeout=10)
        except (ConnectionRefusedError):
            self.speak_dialog('mpd_not_running')
        except (OSError):
            self.speak_dialog('device_not_running')
        finally:
            pass

    def close_connection(self):
        mpcc.disconnect()

    def start_mpd(self, placement):
        self.open_connection(placement)
        mpcc.play()
        self.close_connection()

    def stop_mpd(self, placement):
        self.open_connection(placement)
        mpcc.stop()
        self.close_connection()

#Helper functions
#replaces device's IP if no placement has been spoken

    def check_placement(self, placement):
        if (placement == None and self.same_device == ''):
            placement = self.select_location()
        if placement == None: placement = self.same_device
        return placement
#fallback dialog if no device's placement is set (should work ;-))

    def select_location(self):
        self.speak_dialog('where_to_play')
        location = self.ask_selection(list(self.placements.keys()))
        return placement

#transforms complex list/dictionary results to speakable text

    def eval_list(self, liste, query=None):
        answer = 0
        if not len(liste) == 0:
            for key in range(len(liste)):
                keys = liste[key].keys()
                pos = int(liste[key]['pos']) + 1
                if 'name' in keys:
                    name = liste[key]['name']
                    self.speak_dialog('query_found', {
                        'pos': pos,
                        'name': name
                    })
                    answer += 1
                    continue
                if 'title' in keys:
                    name = liste[key]['title']
                    self.speak_dialog('query_found', {
                        'pos': pos,
                        'name': name
                    })
                    answer += 1
                    continue
                else:
                    name = liste[key]['title']
                    self.speak_dialog('query_found', {
                        'pos': pos,
                        'name': name
                    })
                    answer += 1
        else:
            self.speak_dialog('query_not_found', {'query': query})
            answer = 0
        return answer

#adds all playlists to a complex list which will be evaluated by eval_list()

    def merging_stored_lists(self, placement):
        self.open_connection(placement)
        pl = list(mpcc.listplaylists())
        a = []
        pl_dict = {}
        for key in range(len(pl)):
            pl_dict[pl[key]['playlist']] = mpcc.listplaylistinfo(
                pl[key]['playlist'])
            a.append(pl_dict)
        self.close_connection()
        return pl_dict

#creates a speakable answer from simple lists

    def create_answer_from_search_result(self, query, result_dict):
        answer = {'query': query}
        result = {'result': 'dummy'}
        a = ''
        for k1 in result_dict:
            pos = ''
            for k2 in range(len(result_dict[k1])):
                pos = pos + str(result_dict[k1][k2])
                if len(result_dict[k1]) > 1 and k2 < len(result_dict[k1]) - 1:
                    pos = pos + " und "
                if k2 == len(result_dict[k1]) - 1: pos = str(pos) + "; "
            a = a + "in " + k1 + " position " + pos
            result2 = {'result': a}
            result.update(result2)
        answer.update(result)
        return answer

#deletes a given playlist and adds selected titles from database search

    def play_from_database_search(self, placement, playlist, pos):
        self.open_connection(placement)
        mpcc.clear()
        for i in range(len(playlist)):
            mpcc.add(playlist[i]['file'])
        mpcc.play(pos)
        self.close_connection()

#Current playlist functions names a self-explanatory

    def switch_to_next(self, placement):
        self.open_connection(placement)
        mpcc.next()
        self.close_connection()

    def switch_to_previous(self, placement):
        self.open_connection(placement)
        mpcc.previous()
        self.close_connection()

    def switch_to_first(self, placement):
        self.open_connection(placement)
        mpcc.play(0)
        self.close_connection()

    def switch_to_last(self, placement):
        self.open_connection(placement)
        result = mpcc.playlistinfo()
        playlist_length = len(result)
        pos = playlist_length - 1
        mpcc.play(pos)
        self.close_connection()

    def switch_to_pos(self, placement, pos):
        self.open_connection(placement)
        list_pos = int(pos)
        list_pos = list_pos - 1
        mpcc.play(list_pos)
        self.close_connection()

    def speak_current_title(self, placement):
        self.open_connection(placement)
        liste = mpcc.currentsong()
        if not 'artist' in liste:
            title = liste['title']
            pos = str(int(liste['pos']) + 1)
            self.speak_dialog('speak_current_title', {
                'pos': pos,
                'title': title
            })
        else:
            title = liste['title']
            pos = str(int(liste['pos']) + 1)
            artist = liste['artist']
            self.speak_dialog('speak_current_title_artist', {
                'pos': pos,
                'title': title,
                'artist': artist
            })
        self.close_connection()

    def speak_current_list(self, placement):
        self.open_connection(placement)
        liste = mpcc.playlistinfo()
        answer = self.eval_list(liste)
        self.close_connection()
        return answer

#MPD volume functions - names are self-explanatory

    def vol_up(self, placement):
        self.open_connection(placement)
        mpcc.volume(+5)
        self.close_connection()

    def vol_down(self, placement):
        self.open_connection(placement)
        mpcc.volume(-5)
        self.close_connection()

    def set_vol(self, placement, vol):
        self.open_connection(placement)
        mpcc.setvol(vol)
        self.close_connection()

#Searching in current playlist

    def search_in_current_playlist(self, placement, query):
        try:
            self.open_connection(placement)
            tag = ['title', 'name', 'artist']
            needle = query
            result = []
            for key in tag:
                try:
                    result_new = mpcc.playlistsearch(key, str(needle))
                except Exception:
                    continue
                if not len(result_new) == 0:
                    result = result + result_new
            answer = self.eval_list(result, query)
            return answer
        except Exception as e:
            self.speak_dialog('error_in_function_current_playlist')
            return answer
        finally:
            self.close_connection()

#Stored playlists functions

    def list_stored_playlists(self, placement):
        try:
            self.open_connection(placement)
            list_playlist = mpcc.listplaylists()
        except Exception as e:
            LOGGER.info(str(e))
        finally:
            self.close_connection()
        names = ''
        key = 0
        while key < len(list_playlist):
            names = names + list_playlist[key]['playlist'] + ", "
            key += 1
        return names

    def playlist_replace_and_play(self, placement, playlist, pos):
        self.open_connection(placement)
        list_playlist = mpcc.listplaylists()
        for key in range(len(list_playlist)):
            curr_playlist = list_playlist[key]['playlist']
            if curr_playlist == playlist:
                result = list_playlist[key]['playlist']
                try:
                    mpcc.clear()
                    time.sleep(.5)
                    mpcc.load(result)
                    time.sleep(.5)
                    mpcc.play((int(pos) - 1))
                    time.sleep(.5)
                except Exception as e:
                    LOGGER.info("Error: " + str(e))
                finally:
                    self.close_connection()
                break
            else:
                continue
#next function is deprecated but maybe reactivated sometime

    def search_in_stored_playlists(self, placement, query):
        fields = ['title', 'name', 'artist']
        result = ''
        result_raw = {}
        value_result = []
        pl_dict = self.merging_stored_lists(placement)
        pl_names = list(pl_dict)
        for key1 in range(len(pl_names)):
            for key2 in range(len(pl_dict[pl_names[key1]])):
                for key3 in range(len(fields)):
                    if fields[key3] in pl_dict[pl_names[key1]][key2]:
                        if query.lower() in pl_dict[pl_names[key1]][key2][
                                fields[key3]].lower():
                            line = (pl_dict[pl_names[key1]][key2])
                            pos = pl_dict[pl_names[key1]].index(line)
                            pos = pos + 1
                            value_result.append(pos)
                            key_result = pl_names[key1]
                            result_raw.update({key_result: value_result})
                            result = result_raw
                            print(result)
                        else:
                            pass
                    else:
                        pass
            value_result = []
        if len(result) != 0:
            answer = self.create_answer_from_search_result(query, result)
        else:
            self.speak_dialog('query_not_found', {'query': query})
        answer['result'] = answer['result'][:-2]
        answer['result'] = answer['result'] + "."
        return answer

#current function for searching in playlists

    def search_playlists_stored(self, placement, query, merged_playlists):
        results = 0
        keys1 = list(merged_playlists.keys())
        i1 = len(keys1)
        for k in range(i1):
            k_len = (len(merged_playlists[keys1[k]]))
            for k2 in range(k_len):
                keys2 = list(merged_playlists[keys1[k]][k2].keys())
                for k3 in range(len(keys2)):
                    if query.lower() in merged_playlists[keys1[k]][k2][
                            keys2[k3]].lower():
                        result = merged_playlists[keys1[k]][k2][keys2[k3]]
                        if keys1[k]:
                            playlist, query, pos, name = keys1[
                                k], query, k2 + 1, result
                            self.speak_dialog(
                                'search_all_playlists', {
                                    'query': query,
                                    'playlist': playlist,
                                    'pos': pos,
                                    'name': name
                                })
                            results = results + 1
                            time.sleep(1)
                        break
        if results == 0:
            self.speak_dialog('term_not_found', {'query': query})

#Searches in database

    def search_in_database_and_play(self, placement, query, selection, pos):
        try:
            self.open_connection(placement)
            mpcc.clear()
            mpcc.searchadd(selection, query)
            if pos != "": pos = int(pos) - 1
            if pos == None: pos = 0
            mpcc.play(pos)
        except:
            self.close_connection()
        finally:
            self.close_connection()

    def search_only_in_database(self, placement, query, selection):
        try:
            self.open_connection(placement)
            result = mpcc.search(selection, query)
            result_len = len(result)
            return (result, result_len)
        except:
            self.close_connection()
        finally:
            self.close_connection()


#Intent handlers

    @intent_handler('start_mpd.intent')
    def handle_start_mpd(self, message):
        placement = message.data.get('placement')
        placement = self.check_placement(placement)
        self.start_mpd(placement)

    @intent_handler('stop_mpd.intent')
    def handle_stop_mpd(self, message):
        placement = message.data.get('placement')
        placement = self.check_placement(placement)
        self.stop_mpd(placement)

    @intent_handler('pos.intent')
    def handle_switch_to_pos(self, message):
        pos = message.data.get('pos_nr')
        placement = message.data.get('placement')
        placement = self.check_placement(placement)
        pos = extract_number(pos)
        pos = int(pos)
        self.switch_to_pos(placement, pos)

    @intent_handler('pos_next.intent')
    def handle_pos_next(self, message):
        placement = message.data.get('placement')
        placement = self.check_placement(placement)
        self.switch_to_next(placement)

    @intent_handler('pos_previous.intent')
    def handle_pos_previous(self, message):
        placement = message.data.get('placement')
        placement = self.check_placement(placement)
        self.switch_to_previous(placement)

    @intent_handler('pos_first.intent')
    def handle_pos_first(self, message):
        placement = message.data.get('placement')
        placement = self.check_placement(placement)
        self.switch_to_first(placement)

    @intent_handler('pos_last.intent')
    def handle_pos_last(self, message):
        placement = message.data.get('placement')
        placement = self.check_placement(placement)
        self.switch_to_last(placement)

    @intent_handler('vol_down.intent')
    def volume_down(self, message):
        placement = message.data.get('placement')
        placement = self.check_placement(placement)
        self.vol_down(placement)

    @intent_handler('vol_up.intent')
    def volume_up(self, message):
        placement = message.data.get('placement')
        placement = self.check_placement(placement)
        self.vol_up(placement)

    @intent_handler('vol_set_to.intent')
    def volume_set(self, message):
        placement = message.data.get('placement')
        placement = self.check_placement(placement)
        vol = message.data.get('pos_nr')
        vol = extract_number(vol)
        vol = int(vol)
        self.set_vol(placement, vol)

    @intent_handler('info_current_title.intent')
    def handle_speak_title(self, message):
        placement = message.data.get('placement')
        placement = self.check_placement(placement)
        self.speak_current_title(placement)

    @intent_handler('playlist_stored.intent')
    def handle_list_stored_playlists(self, message):
        placement = message.data.get('placement')
        placement = self.check_placement(placement)
        answer = self.list_stored_playlists(placement)
        self.speak(answer)
        playlist = self.get_response('which_playlist_to_play', num_retries=0)
        if playlist == None:
            self.speak_dialog('cancel')
            pass
        else:
            pos_nr = self.get_response('which_position_to_play', num_retries=0)
            if pos_nr == None:
                pos_nr = 1
                self.speak_dialog('starting_with_number_one')
                self.playlist_replace_and_play(placement, playlist, pos_nr)
            else:
                self.playlist_replace_and_play(placement, playlist, pos_nr)

    @intent_handler('playlist_replace_and_play.intent')
    def handle_playlist_replace_and_play(self, message):
        playlist = message.data.get('playlist')
        placement = message.data.get('placement')
        placement = self.check_placement(placement)
        pos = message.data.get('pos_nr')
        pos = extract_number(pos)
        pos = int(pos)
        self.playlist_replace_and_play(placement, playlist, pos)

    @intent_handler('search.intent')
    def handle_search_current_playlist(self, message):
        placement = message.data.get('placement')
        placement = self.check_placement(placement)
        query = message.data.get('query')
        answer = self.search_in_current_playlist(placement, query)
        if answer != 0:
            play_title = self.ask_yesno('to_play')
            if play_title == 'yes':
                pos_nr = self.get_response('which_position_to_play')
                pos_nr = extract_number(pos_nr)
                self.switch_to_pos(placement, pos_nr)
            elif play_title == 'no':
                pass
            else:
                self.speak_dialog('some_error')
        else:
            self.speak(answer)

    @intent_handler('search_all_playlists.intent')
    def handle_search_all_playlists(self, message):
        data = message.data
        data = data.keys()
        placement = message.data.get('placement')
        placement = self.check_placement(placement)
        query = message.data.get('query')
        merged_playlists = self.merging_stored_lists(placement)
        self.search_playlists_stored(placement, query, merged_playlists)

    @intent_handler('info_current_list.intent')
    def handle_speak_current_playlist(self, message):
        placement = message.data.get('placement')
        placement = self.check_placement(placement)
        answer = self.speak_current_list(placement)
        self.speak(answer)

    @intent_handler('search_add_play_database.intent')
    def handle_search_in_database(self, message):
        placement = message.data.get('placement')
        placement = self.check_placement(placement)
        query = message.data.get('query')
        query_dict = {
            'query': query
        }
        pos_nr = message.data.get('pos_nr')
        pos_nr = extract_number(pos_nr)
        query_correct = self.ask_yesno('feedback_query', query_dict)
        if query_correct == 'yes':
            selection = self.get_response('which_data_field')
            if self.voc_match(selection, 'artist'): selection = 'artist'
            elif self.voc_match(selection, 'title'): selection = 'title'
            elif self.voc_match(selection, 'album'): selection = 'album'
            elif self.voc_match(selection, 'genre'): selection = 'genre'
            else: self.speak_dialog('missunderstand_selection')
            if pos_nr == "": pos_nr = '0'
            self.search_in_database_and_play(placement, query, selection,
                                             pos_nr)
        else:
            pass

    @intent_handler('search_in_database.intent')
    def handle_database_dialog(self, message):
        placement = message.data.get('placement')
        placement = self.check_placement(placement)
        query = message.data.get('query')
        query_dict = {
            'query': query
        }
        query_correct = self.ask_yesno('feedback_query', query_dict)
        if query_correct == 'yes':
            selection = self.get_response('which_data_field')
            if self.voc_match(selection, 'artist'): selection = 'artist'
            elif self.voc_match(selection, 'title'): selection = 'title'
            elif self.voc_match(selection, 'album'): selection = 'album'
            elif self.voc_match(selection, 'genre'): selection = 'genre'
            else: self.speak_dialog('missunderstand_selection')
        else:
            self.speak_dialog('missunderstand_query')
        if selection == 'artist' or selection == 'title' or selection == 'album' or selection == 'genre':
            placement = self.check_placement(placement)
            search_result = self.search_only_in_database(
                placement, query, selection)
            numbers_result = search_result[1]
            if numbers_result != 0:
                title = self.get_response('which_title_to_play',
                                          {'numbers': numbers_result})
                if self.voc_match(title, 'nothing'):
                    pass
                elif self.voc_match(title, 'all'):
                    title = 0
                    self.play_from_database_search(placement, search_result[0],
                                                   title)
                else:
                    title = extract_number(title)
                    title = int(title) - 1
                    self.play_from_database_search(placement, search_result[0],
                                                   title)
            else:
                self.speak_dialog('no_result', {
                    'query': query,
                    'selection': selection
                })

    def stop(self):
        pass