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
class SkillSettings(dict): """ A dictionary that can easily be save to a file, serialized as json. It also syncs to the backend for skill settings Args: directory (str): Path to storage directory name (str): user readable name associated with the settings """ def __init__(self, directory, name): super(SkillSettings, self).__init__() # when skills try to instantiate settings # in __init__, it can erase the settings saved # on disk (settings.json). So this prevents that # This is set to true in core.py after skill init self.allow_overwrite = False self.api = DeviceApi() self.config = ConfigurationManager.get() self.name = name self.directory = directory # set file paths self._settings_path = join(directory, 'settings.json') self._meta_path = join(directory, 'settingsmeta.json') self.is_alive = True self.loaded_hash = hash(str(self)) self._complete_intialization = False self._device_identity = None self._api_path = None self._user_identity = None self.changed_callback = None # if settingsmeta exist if isfile(self._meta_path): self._poll_skill_settings() def set_changed_callback(self, callback): """ Set callback to perform when server settings have changed. callback: function/method to call when settings have changed """ self.changed_callback = callback # TODO: break this up into two classes def initialize_remote_settings(self): """ initializes the remote settings to the server """ # if settingsmeta.json exists (and is valid) # this block of code is a control flow for # different scenarios that may arises with settingsmeta self.load_skill_settings_from_file() # loads existing settings.json settings_meta = self._load_settings_meta() if not settings_meta: return self._device_identity = self.api.identity.uuid self._api_path = "/" + self._device_identity + "/skill" self._user_identity = self.api.get()['user']['uuid'] LOG.info("settingsmeta.json exist for {}".format(self.name)) hashed_meta = self._get_meta_hash(str(settings_meta)) skill_settings = self._request_other_settings(hashed_meta) # if hash is new then there is a diff version of settingsmeta if self._is_new_hash(hashed_meta): # first look at all other devices on user account to see # if the settings exist. if it does then sync with device if skill_settings: # not_owner flags that this settings is loaded from # another device. If a skill settings doesn't have # not_owner, then the skill is created from that device self['not_owner'] = True self.save_skill_settings(skill_settings) else: # upload skill settings if uuid = self._load_uuid() if uuid is not None: self._delete_metadata(uuid) self._upload_meta(settings_meta, hashed_meta) else: # hash is not new if skill_settings is not None: self['not_owner'] = True self.save_skill_settings(skill_settings) else: settings = self._request_my_settings(hashed_meta) if settings is None: LOG.info("seems like it got deleted from home... " "sending settingsmeta.json for " "{}".format(self.name)) self._upload_meta(settings_meta, hashed_meta) else: self.save_skill_settings(settings) self._complete_intialization = True @property def _is_stored(self): return hash(str(self)) == self.loaded_hash def __getitem__(self, key): """ Get key """ return super(SkillSettings, self).__getitem__(key) def __setitem__(self, key, value): """ Add/Update key. """ if self.allow_overwrite or key not in self: return super(SkillSettings, self).__setitem__(key, value) def _load_settings_meta(self): """ Loads settings metadata from skills path. """ if isfile(self._meta_path): try: with open(self._meta_path) as f: data = json.load(f) return data except Exception as e: LOG.error("Failed to load setting file: " + self._meta_path) LOG.error(repr(e)) return None else: LOG.info("settingemeta.json does not exist") return None def _send_settings_meta(self, settings_meta): """ Send settingsmeta.json to the server. Args: settings_meta (dict): dictionary of the current settings meta Returns: str: uuid, a unique id for the setting meta data """ try: uuid = self._put_metadata(settings_meta) return uuid except Exception as e: LOG.error(e) return None def save_skill_settings(self, skill_settings): """ takes skill object and save onto self Args: settings (dict): skill """ if self._is_new_hash(skill_settings['identifier']): self._save_uuid(skill_settings['uuid']) self._save_hash(skill_settings['identifier']) sections = skill_settings['skillMetadata']['sections'] for section in sections: for field in section["fields"]: if "name" in field and "value" in field: self[field['name']] = field['value'] self.store() def _load_uuid(self): """ Loads uuid Returns: str: uuid of the previous settingsmeta """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') uuid = None if isfile(uuid_file): with open(uuid_file, 'r') as f: uuid = f.read() return uuid def _save_uuid(self, uuid): """ Saves uuid. Args: str: uuid, unique id of new settingsmeta """ LOG.info("saving uuid {}".format(str(uuid))) directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') with open(uuid_file, 'w') as f: f.write(str(uuid)) def _uuid_exist(self): """ Checks if there is an uuid file. Returns: bool: True if uuid file exist False otherwise """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') return isfile(uuid_file) def _migrate_settings(self, settings_meta): """ sync settings.json and settingsmeta.json in memory """ meta = settings_meta.copy() self.load_skill_settings_from_file() sections = meta['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section['fields']): if 'name' in field: if field["name"] in self: sections[i]['fields'][j]['value'] = \ str(self.__getitem__(field['name'])) meta['skillMetadata']['sections'] = sections return meta def _upload_meta(self, settings_meta, hashed_meta): """ uploads the new meta data to settings with settings migration Args: settings_meta (dict): settingsmeta.json hashed_meta (str): {skill-folder}-settinsmeta.json """ LOG.info("sending settingsmeta.json for {}".format(self.name) + " to servers") meta = self._migrate_settings(settings_meta) meta['identifier'] = str(hashed_meta) response = self._send_settings_meta(meta) if response: self._save_uuid(response['uuid']) if 'not_owner' in self: del self['not_owner'] self._save_hash(hashed_meta) def _delete_old_meta(self): """" Deletes the old meta data """ if self._uuid_exist(): try: LOG.info("a uuid exist for {}".format(self.name) + " deleting old one") old_uuid = self._load_uuid() self._delete_metatdata(old_uuid) except Exception as e: LOG.info(e) def hash(self, str): """ md5 hasher for consistency across cpu architectures """ return hashlib.md5(str).hexdigest() def _get_meta_hash(self, settings_meta): """ Get's the hash of skill Args: settings_meta (str): stringified settingsmeta Returns: _hash (str): hashed to identify skills """ _hash = self.hash(str(settings_meta) + str(self._user_identity)) return "{}--{}".format(basename(self.directory), _hash) def _save_hash(self, hashed_meta): """ Saves hashed_meta to settings directory. Args: hashed_meta (int): hash of new settingsmeta """ LOG.info("saving hash {}".format(str(hashed_meta))) directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) hash_file = join(directory, 'hash') with open(hash_file, 'w') as f: f.write(str(hashed_meta)) def _is_new_hash(self, hashed_meta): """ checks if the stored hash is the same as current. if the hashed file does not exist, usually in the case of first load, then the create it and return True Args: hashed_meta (int): hash of metadata and uuid of device Returns: bool: True if hash is new, otherwise False """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) hash_file = join(directory, 'hash') if isfile(hash_file): with open(hash_file, 'r') as f: current_hash = f.read() return False if current_hash == str(hashed_meta) else True return True def update_remote(self): """ update settings state from server """ skills_settings = None settings_meta = self._load_settings_meta() if settings_meta is None: return hashed_meta = self._get_meta_hash(settings_meta) if self.get('not_owner'): skills_settings = self._request_other_settings(hashed_meta) if not skills_settings: skills_settings = self._request_my_settings(hashed_meta) if skills_settings is not None: self.save_skill_settings(skills_settings) self.store() else: settings_meta = self._load_settings_meta() self._upload_meta(settings_meta, hashed_meta) def _poll_skill_settings(self): """ If identifier exists for this skill poll to backend to request settings and store it if it changes TODO: implement as websocket Args: hashed_meta (int): the hashed identifier """ try: if not self._complete_intialization: self.initialize_remote_settings() if not self._complete_intialization: return # unable to do remote sync else: original = hash(str(self)) self.update_remote() # Call callback for updated settings if self.changed_callback and hash(str(self)) != original: self.changed_callback() except Exception as e: LOG.error(e) LOG.exception("") # this is used in core so do not delete! if self.is_alive: # continues to poll settings every 60 seconds t = Timer(60, self._poll_skill_settings) t.daemon = True t.start() def load_skill_settings_from_file(self): """ If settings.json exist, open and read stored values into self """ if isfile(self._settings_path): with open(self._settings_path) as f: try: json_data = json.load(f) for key in json_data: self[key] = json_data[key] except Exception as e: # TODO: Show error on webUI. Dev will have to fix # metadata to be able to edit later. LOG.error(e) def _request_my_settings(self, identifier): """ Get skill settings for this device associated with the identifier Args: identifier (str): a hashed_meta Returns: skill_settings (dict or None): returns a dict if matches """ LOG.info("getting skill settings from " "server for {}".format(self.name)) settings = self._request_settings() # this loads the settings into memory for use in self.store for skill_settings in settings: if skill_settings['identifier'] == identifier: self._remote_settings = skill_settings return skill_settings return None def _request_settings(self): """ Get all skill settings for this device from server. Returns: dict: dictionary with settings collected from the server. """ settings = self.api.request({"method": "GET", "path": self._api_path}) settings = [skills for skills in settings if skills is not None] return settings def _request_other_settings(self, identifier): """ Retrieves user skill from other devices by identifier (hashed_meta) Args: indentifier (str): identifier for this skill Returns: settings (dict or None): returns the settings if true else None """ LOG.info("syncing settings with other devices " "from server for {}".format(self.name)) path = \ "/" + self._device_identity + "/userSkill?identifier=" + identifier user_skill = self.api.request({"method": "GET", "path": path}) if len(user_skill) == 0: return None else: return user_skill[0] def _put_metadata(self, settings_meta): """ PUT settingsmeta to backend to be configured in server. used in place of POST and PATCH. Args: settings_meta (dict): dictionary of the current settings meta data """ return self.api.request({ "method": "PUT", "path": self._api_path, "json": settings_meta }) def _delete_metadata(self, uuid): """ Deletes the current skill metadata Args: uuid (str): unique id of the skill """ try: LOG.info("deleting metadata") self.api.request({ "method": "DELETE", "path": self._api_path + "/{}".format(uuid) }) except Exception as e: LOG.error(e) LOG.info("cannot delete metadata because this" "device is not original uploader of skill") @property def _should_upload_from_change(self): changed = False if hasattr(self, '_remote_settings'): sections = self._remote_settings['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section['fields']): if 'name' in field: # Ensure that the field exists in settings and that # it has a value to compare if (field["name"] in self and 'value' in sections[i]['fields'][j]): remote_val = sections[i]['fields'][j]["value"] self_val = self.get(field['name']) if str(remote_val) != str(self_val): changed = True if self.get('not_owner'): changed = False return changed def store(self, force=False): """ Store dictionary to file if a change has occured. Args: force: Force write despite no change """ if force or not self._is_stored: with open(self._settings_path, 'w') as f: json.dump(self, f) self.loaded_hash = hash(str(self)) if self._should_upload_from_change: settings_meta = self._load_settings_meta() hashed_meta = self._get_meta_hash(settings_meta) uuid = self._load_uuid() if uuid is not None: LOG.info("deleting meta data for {}".format(self.name)) self._delete_metadata(uuid) self._upload_meta(settings_meta, hashed_meta)
class SkillSettings(dict): """ Dictionary that can easily be saved to a file, serialized as json. It also syncs to the backend for skill settings Args: directory (str): Path to storage directory name (str): user readable name associated with the settings """ def __init__(self, directory, name): super(SkillSettings, self).__init__() # when skills try to instantiate settings # in __init__, it can erase the settings saved # on disk (settings.json). So this prevents that # This is set to true in core.py after skill init self.allow_overwrite = False self.api = DeviceApi() self.config = ConfigurationManager.get() self.name = name # set file paths self._settings_path = join(directory, 'settings.json') self._meta_path = join(directory, 'settingsmeta.json') self.is_alive = True self.loaded_hash = hash(json.dumps(self, sort_keys=True)) self._complete_intialization = False self._device_identity = None self._api_path = None self._user_identity = None self.changed_callback = None self._poll_timer = None self._is_alive = True # if settingsmeta exist if isfile(self._meta_path): self._poll_skill_settings() def __hash__(self): """ Simple object unique hash. """ return hash(str(id(self)) + self.name) def run_poll(self, _=None): """Immediately poll the web for new skill settings""" if self._poll_timer: self._poll_timer.cancel() self._poll_skill_settings() def stop_polling(self): self._is_alive = False if self._poll_timer: self._poll_timer.cancel() def set_changed_callback(self, callback): """ Set callback to perform when server settings have changed. Args: callback: function/method to call when settings have changed """ self.changed_callback = callback # TODO: break this up into two classes def initialize_remote_settings(self): """ initializes the remote settings to the server """ # if settingsmeta.json exists (and is valid) # this block of code is a control flow for # different scenarios that may arises with settingsmeta self.load_skill_settings_from_file() # loads existing settings.json settings_meta = self._load_settings_meta() if not settings_meta: return if not is_paired(): return self._device_identity = self.api.identity.uuid self._api_path = "/" + self._device_identity + "/skill" try: self._user_identity = self.api.get()['user']['uuid'] except RequestException: return hashed_meta = self._get_meta_hash(settings_meta) skill_settings = self._request_other_settings(hashed_meta) # if hash is new then there is a diff version of settingsmeta if self._is_new_hash(hashed_meta): # first look at all other devices on user account to see # if the settings exist. if it does then sync with device if skill_settings: # not_owner flags that this settings is loaded from # another device. If a skill settings doesn't have # not_owner, then the skill is created from that device self['not_owner'] = True self.save_skill_settings(skill_settings) else: # upload skill settings if uuid = self._load_uuid() if uuid is not None: self._delete_metadata(uuid) self._upload_meta(settings_meta, hashed_meta) else: # hash is not new if skill_settings is not None: self['not_owner'] = True self.save_skill_settings(skill_settings) else: settings = self._request_my_settings(hashed_meta) if settings is None: # metadata got deleted from Home, send up self._upload_meta(settings_meta, hashed_meta) else: self.save_skill_settings(settings) self._complete_intialization = True @property def _is_stored(self): return hash(json.dumps(self, sort_keys=True)) == self.loaded_hash def __getitem__(self, key): """ Get key """ return super(SkillSettings, self).__getitem__(key) def __setitem__(self, key, value): """ Add/Update key. """ if self.allow_overwrite or key not in self: return super(SkillSettings, self).__setitem__(key, value) def _load_settings_meta(self): """ Loads settings metadata from skills path. """ if isfile(self._meta_path): try: with open(self._meta_path, encoding='utf-8') as f: data = json.load(f) return data except Exception as e: LOG.error("Failed to load setting file: " + self._meta_path) LOG.error(repr(e)) return None else: return None def _send_settings_meta(self, settings_meta): """ Send settingsmeta.json to the server. Args: settings_meta (dict): dictionary of the current settings meta Returns: dict: uuid, a unique id for the setting meta data """ try: uuid = self._put_metadata(settings_meta) return uuid except HTTPError as e: if e.response.status_code in [422, 500, 501]: raise DelayRequest else: LOG.error(e) return None except Exception as e: LOG.error(e) return None def save_skill_settings(self, skill_settings): """ Takes skill object and save onto self Args: skill_settings (dict): skill """ if self._is_new_hash(skill_settings['identifier']): self._save_uuid(skill_settings['uuid']) self._save_hash(skill_settings['identifier']) sections = skill_settings['skillMetadata']['sections'] for section in sections: for field in section["fields"]: if "name" in field and "value" in field: self[field['name']] = field['value'] self.store() def _load_uuid(self): """ Loads uuid Returns: str: uuid of the previous settingsmeta """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') uuid = None if isfile(uuid_file): with open(uuid_file, 'r') as f: uuid = f.read() return uuid def _save_uuid(self, uuid): """ Saves uuid. Args: uuid (str): uuid, unique id of new settingsmeta """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') os.makedirs(directory, exist_ok=True) with open(uuid_file, 'w') as f: f.write(str(uuid)) def _uuid_exist(self): """ Checks if there is an uuid file. Returns: bool: True if uuid file exist False otherwise """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') return isfile(uuid_file) def _migrate_settings(self, settings_meta): """ sync settings.json and settingsmeta.json in memory """ meta = settings_meta.copy() self.load_skill_settings_from_file() sections = meta['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section['fields']): if 'name' in field: if field["name"] in self: sections[i]['fields'][j]['value'] = \ str(self.__getitem__(field['name'])) meta['skillMetadata']['sections'] = sections return meta def _upload_meta(self, settings_meta, hashed_meta): """ uploads the new meta data to settings with settings migration Args: settings_meta (dict): settingsmeta.json hashed_meta (str): {skill-folder}-settinsmeta.json """ meta = self._migrate_settings(settings_meta) meta['identifier'] = str(hashed_meta) response = self._send_settings_meta(meta) if response and 'uuid' in response: self._save_uuid(response['uuid']) if 'not_owner' in self: del self['not_owner'] self._save_hash(hashed_meta) def hash(self, string): """ md5 hasher for consistency across cpu architectures """ return hashlib.md5(bytes(string, 'utf-8')).hexdigest() def _get_meta_hash(self, settings_meta): """ Gets the hash of skill Args: settings_meta (dict): settingsmeta object Returns: _hash (str): hashed to identify skills """ _hash = self.hash( json.dumps(settings_meta, sort_keys=True) + self._user_identity) return "{}--{}".format(self.name, _hash) def _save_hash(self, hashed_meta): """ Saves hashed_meta to settings directory. Args: hashed_meta (str): hash of new settingsmeta """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) hash_file = join(directory, 'hash') os.makedirs(directory, exist_ok=True) with open(hash_file, 'w') as f: f.write(hashed_meta) def _is_new_hash(self, hashed_meta): """ Check if stored hash is the same as current. If the hashed file does not exist, usually in the case of first load, then the create it and return True Args: hashed_meta (str): hash of metadata and uuid of device Returns: bool: True if hash is new, otherwise False """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) hash_file = join(directory, 'hash') if isfile(hash_file): with open(hash_file, 'r') as f: current_hash = f.read() return False if current_hash == str(hashed_meta) else True return True def update_remote(self): """ update settings state from server """ skills_settings = None settings_meta = self._load_settings_meta() if settings_meta is None: return hashed_meta = self._get_meta_hash(settings_meta) if self.get('not_owner'): skills_settings = self._request_other_settings(hashed_meta) if not skills_settings: skills_settings = self._request_my_settings(hashed_meta) if skills_settings is not None: self.save_skill_settings(skills_settings) self.store() else: settings_meta = self._load_settings_meta() self._upload_meta(settings_meta, hashed_meta) def _poll_skill_settings(self): """ If identifier exists for this skill poll to backend to request settings and store it if it changes TODO: implement as websocket """ delay = 1 original = hash(str(self)) try: if not is_paired(): pass elif not self._complete_intialization: self.initialize_remote_settings() else: self.update_remote() except DelayRequest: LOG.info('{}: Delaying next settings fetch'.format(self.name)) delay = 5 except Exception as e: LOG.exception('Failed to fetch skill settings: {}'.format(repr(e))) finally: # Call callback for updated settings if self._complete_intialization: if self.changed_callback and hash(str(self)) != original: self.changed_callback() if self._poll_timer: self._poll_timer.cancel() if not self._is_alive: return # continues to poll settings every minute self._poll_timer = Timer(delay * 60, self._poll_skill_settings) self._poll_timer.daemon = True self._poll_timer.start() def load_skill_settings_from_file(self): """ If settings.json exist, open and read stored values into self """ if isfile(self._settings_path): with open(self._settings_path) as f: try: json_data = json.load(f) for key in json_data: self[key] = json_data[key] except Exception as e: # TODO: Show error on webUI. Dev will have to fix # metadata to be able to edit later. LOG.error(e) def _type_cast(self, settings_meta, to_platform): """ Tranform data type to be compatible with Home and/or Core. e.g. Web to core "true" => True, "1.4" => 1.4 core to Web False => "false' Args: settings_meta (dict): skills object to_platform (str): platform to convert compatible data types to Returns: dict: skills object """ meta = settings_meta.copy() sections = meta['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section.get('fields', [])): _type = field.get('type') if _type == 'checkbox': value = field.get('value') if to_platform == 'web': if value is True or value == 'True': sections[i]['fields'][j]['value'] = 'true' elif value is False or value == 'False': sections[i]['fields'][j]['value'] = 'false' elif to_platform == 'core': if value == 'true' or value == 'True': sections[i]['fields'][j]['value'] = True elif value == 'false' or value == 'False': sections[i]['fields'][j]['value'] = False elif _type == 'number': value = field.get('value') if to_platform == 'core': if "." in value: sections[i]['fields'][j]['value'] = float(value) else: sections[i]['fields'][j]['value'] = int(value) elif to_platform == 'web': sections[i]['fields'][j]['value'] = str(value) meta['skillMetadata']['sections'] = sections return meta def _request_my_settings(self, identifier): """ Get skill settings for this device associated with the identifier Args: identifier (str): a hashed_meta Returns: skill_settings (dict or None): returns a dict if matches """ settings = self._request_settings() if settings: # this loads the settings into memory for use in self.store for skill_settings in settings: if skill_settings['identifier'] == identifier: skill_settings = \ self._type_cast(skill_settings, to_platform='core') self._remote_settings = skill_settings return skill_settings return None def _request_settings(self): """ Get all skill settings for this device from server. Returns: dict: dictionary with settings collected from the server. """ try: settings = self.api.request({ "method": "GET", "path": self._api_path }) except RequestException: return None settings = [skills for skills in settings if skills is not None] return settings def _request_other_settings(self, identifier): """ Retrieve skill settings from other devices by identifier Args: identifier (str): identifier for this skill Returns: settings (dict or None): the retrieved settings or None """ path = \ "/" + self._device_identity + "/userSkill?identifier=" + identifier try: user_skill = self.api.request({"method": "GET", "path": path}) except RequestException: # Some kind of Timeout, connection HTTPError, etc. user_skill = None if not user_skill: return None else: settings = self._type_cast(user_skill[0], to_platform='core') return settings def _put_metadata(self, settings_meta): """ PUT settingsmeta to backend to be configured in server. used in place of POST and PATCH. Args: settings_meta (dict): dictionary of the current settings meta data """ settings_meta = self._type_cast(settings_meta, to_platform='web') return self.api.request({ "method": "PUT", "path": self._api_path, "json": settings_meta }) def _delete_metadata(self, uuid): """ Delete the current skill metadata Args: uuid (str): unique id of the skill """ try: LOG.debug("deleting metadata") self.api.request({ "method": "DELETE", "path": self._api_path + "/{}".format(uuid) }) except Exception as e: LOG.error(e) LOG.error("cannot delete metadata because this" "device is not original uploader of skill") @property def _should_upload_from_change(self): changed = False if hasattr(self, '_remote_settings'): sections = self._remote_settings['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section['fields']): if 'name' in field: # Ensure that the field exists in settings and that # it has a value to compare if (field["name"] in self and 'value' in sections[i]['fields'][j]): remote_val = sections[i]['fields'][j]["value"] self_val = self.get(field['name']) if str(remote_val) != str(self_val): changed = True if self.get('not_owner'): changed = False return changed def store(self, force=False): """ Store dictionary to file if a change has occured. Args: force: Force write despite no change """ if force or not self._is_stored: with open(self._settings_path, 'w') as f: json.dump(self, f) self.loaded_hash = hash(json.dumps(self, sort_keys=True)) if self._should_upload_from_change: settings_meta = self._load_settings_meta() hashed_meta = self._get_meta_hash(settings_meta) uuid = self._load_uuid() if uuid is not None: self._delete_metadata(uuid) self._upload_meta(settings_meta, hashed_meta)
class SkillSettings(dict): """ 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)
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)
class SkillSettings(dict): """ A dictionary that can easily be save to a file, serialized as json. It also syncs to the backend for skill settings Args: directory (str): Path to storage directory name (str): user readable name associated with the settings """ def __init__(self, directory, name): super(SkillSettings, self).__init__() # when skills try to instantiate settings # in __init__, it can erase the settings saved # on disk (settings.json). So this prevents that # This is set to true in core.py after skill init self.allow_overwrite = False self.api = DeviceApi() self.config = ConfigurationManager.get() self.name = name self.directory = directory # set file paths self._settings_path = join(directory, 'settings.json') self._meta_path = join(directory, 'settingsmeta.json') self.is_alive = True self.loaded_hash = hash(str(self)) self._complete_intialization = False self._device_identity = None self._api_path = None self._user_identity = None self.changed_callback = None # if settingsmeta exist if isfile(self._meta_path): self._poll_skill_settings() def set_changed_callback(self, callback): """ Set callback to perform when server settings have changed. callback: function/method to call when settings have changed """ self.changed_callback = callback # TODO: break this up into two classes def initialize_remote_settings(self): """ initializes the remote settings to the server """ # if settingsmeta.json exists (and is valid) # this block of code is a control flow for # different scenarios that may arises with settingsmeta self.load_skill_settings_from_file() # loads existing settings.json settings_meta = self._load_settings_meta() if not settings_meta: return self._device_identity = self.api.identity.uuid self._api_path = "/" + self._device_identity + "/skill" self._user_identity = self.api.get()['user']['uuid'] LOG.info("settingsmeta.json exist for {}".format(self.name)) hashed_meta = self._get_meta_hash(str(settings_meta)) skill_settings = self._request_other_settings(hashed_meta) # if hash is new then there is a diff version of settingsmeta if self._is_new_hash(hashed_meta): # first look at all other devices on user account to see # if the settings exist. if it does then sync with device if skill_settings: # not_owner flags that this settings is loaded from # another device. If a skill settings doesn't have # not_owner, then the skill is created from that device self['not_owner'] = True self.save_skill_settings(skill_settings) else: # upload skill settings if uuid = self._load_uuid() if uuid is not None: self._delete_metadata(uuid) self._upload_meta(settings_meta, hashed_meta) else: # hash is not new if skill_settings is not None: self['not_owner'] = True self.save_skill_settings(skill_settings) else: settings = self._request_my_settings(hashed_meta) if settings is None: LOG.info("seems like it got deleted from home... " "sending settingsmeta.json for " "{}".format(self.name)) self._upload_meta(settings_meta, hashed_meta) else: self.save_skill_settings(settings) self._complete_intialization = True @property def _is_stored(self): return hash(str(self)) == self.loaded_hash def __getitem__(self, key): """ Get key """ return super(SkillSettings, self).__getitem__(key) def __setitem__(self, key, value): """ Add/Update key. """ if self.allow_overwrite or key not in self: return super(SkillSettings, self).__setitem__(key, value) def _load_settings_meta(self): """ Loads settings metadata from skills path. """ if isfile(self._meta_path): try: with open(self._meta_path) as f: data = json.load(f) return data except Exception as e: LOG.error("Failed to load setting file: "+self._meta_path) LOG.error(repr(e)) return None else: LOG.info("settingemeta.json does not exist") return None def _send_settings_meta(self, settings_meta): """ Send settingsmeta.json to the server. Args: settings_meta (dict): dictionary of the current settings meta Returns: str: uuid, a unique id for the setting meta data """ try: uuid = self._put_metadata(settings_meta) return uuid except Exception as e: LOG.error(e) return None def save_skill_settings(self, skill_settings): """ takes skill object and save onto self Args: settings (dict): skill """ if self._is_new_hash(skill_settings['identifier']): self._save_uuid(skill_settings['uuid']) self._save_hash(skill_settings['identifier']) sections = skill_settings['skillMetadata']['sections'] for section in sections: for field in section["fields"]: if "name" in field and "value" in field: self[field['name']] = field['value'] self.store() def _load_uuid(self): """ Loads uuid Returns: str: uuid of the previous settingsmeta """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') uuid = None if isfile(uuid_file): with open(uuid_file, 'r') as f: uuid = f.read() return uuid def _save_uuid(self, uuid): """ Saves uuid. Args: str: uuid, unique id of new settingsmeta """ LOG.info("saving uuid {}".format(str(uuid))) directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') with open(uuid_file, 'w') as f: f.write(str(uuid)) def _uuid_exist(self): """ Checks if there is an uuid file. Returns: bool: True if uuid file exist False otherwise """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') return isfile(uuid_file) def _migrate_settings(self, settings_meta): """ sync settings.json and settingsmeta.json in memory """ meta = settings_meta.copy() self.load_skill_settings_from_file() sections = meta['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section['fields']): if 'name' in field: if field["name"] in self: sections[i]['fields'][j]['value'] = \ str(self.__getitem__(field['name'])) meta['skillMetadata']['sections'] = sections return meta def _upload_meta(self, settings_meta, hashed_meta): """ uploads the new meta data to settings with settings migration Args: settings_meta (dict): settingsmeta.json hashed_meta (str): {skill-folder}-settinsmeta.json """ LOG.info("sending settingsmeta.json for {}".format(self.name) + " to servers") meta = self._migrate_settings(settings_meta) meta['identifier'] = str(hashed_meta) response = self._send_settings_meta(meta) if response: self._save_uuid(response['uuid']) if 'not_owner' in self: del self['not_owner'] self._save_hash(hashed_meta) def _delete_old_meta(self): """" Deletes the old meta data """ if self._uuid_exist(): try: LOG.info("a uuid exist for {}".format(self.name) + " deleting old one") old_uuid = self._load_uuid() self._delete_metatdata(old_uuid) except Exception as e: LOG.info(e) def hash(self, str): """ md5 hasher for consistency across cpu architectures """ return hashlib.md5(str).hexdigest() def _get_meta_hash(self, settings_meta): """ Get's the hash of skill Args: settings_meta (str): stringified settingsmeta Returns: _hash (str): hashed to identify skills """ _hash = self.hash(str(settings_meta) + str(self._user_identity)) return "{}--{}".format(basename(self.directory), _hash) def _save_hash(self, hashed_meta): """ Saves hashed_meta to settings directory. Args: hashed_meta (int): hash of new settingsmeta """ LOG.info("saving hash {}".format(str(hashed_meta))) directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) hash_file = join(directory, 'hash') with open(hash_file, 'w') as f: f.write(str(hashed_meta)) def _is_new_hash(self, hashed_meta): """ checks if the stored hash is the same as current. if the hashed file does not exist, usually in the case of first load, then the create it and return True Args: hashed_meta (int): hash of metadata and uuid of device Returns: bool: True if hash is new, otherwise False """ directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) hash_file = join(directory, 'hash') if isfile(hash_file): with open(hash_file, 'r') as f: current_hash = f.read() return False if current_hash == str(hashed_meta) else True return True def update_remote(self): """ update settings state from server """ skills_settings = None settings_meta = self._load_settings_meta() if settings_meta is None: return hashed_meta = self._get_meta_hash(settings_meta) if self.get('not_owner'): skills_settings = self._request_other_settings(hashed_meta) if not skills_settings: skills_settings = self._request_my_settings(hashed_meta) if skills_settings is not None: self.save_skill_settings(skills_settings) self.store() else: settings_meta = self._load_settings_meta() self._upload_meta(settings_meta, hashed_meta) def _poll_skill_settings(self): """ If identifier exists for this skill poll to backend to request settings and store it if it changes TODO: implement as websocket Args: hashed_meta (int): the hashed identifier """ try: if not self._complete_intialization: self.initialize_remote_settings() if not self._complete_intialization: return # unable to do remote sync else: original = hash(str(self)) self.update_remote() # Call callback for updated settings if self.changed_callback and hash(str(self)) != original: self.changed_callback() except Exception as e: LOG.error(e) LOG.exception("") # this is used in core so do not delete! if self.is_alive: # continues to poll settings every 60 seconds t = Timer(60, self._poll_skill_settings) t.daemon = True t.start() def load_skill_settings_from_file(self): """ If settings.json exist, open and read stored values into self """ if isfile(self._settings_path): with open(self._settings_path) as f: try: json_data = json.load(f) for key in json_data: self[key] = json_data[key] except Exception as e: # TODO: Show error on webUI. Dev will have to fix # metadata to be able to edit later. LOG.error(e) def _request_my_settings(self, identifier): """ Get skill settings for this device associated with the identifier Args: identifier (str): a hashed_meta Returns: skill_settings (dict or None): returns a dict if matches """ LOG.info("getting skill settings from " "server for {}".format(self.name)) settings = self._request_settings() # this loads the settings into memory for use in self.store for skill_settings in settings: if skill_settings['identifier'] == identifier: self._remote_settings = skill_settings return skill_settings return None def _request_settings(self): """ Get all skill settings for this device from server. Returns: dict: dictionary with settings collected from the server. """ settings = self.api.request({ "method": "GET", "path": self._api_path }) settings = [skills for skills in settings if skills is not None] return settings def _request_other_settings(self, identifier): """ Retrieves user skill from other devices by identifier (hashed_meta) Args: indentifier (str): identifier for this skill Returns: settings (dict or None): returns the settings if true else None """ LOG.info( "syncing settings with other devices " "from server for {}".format(self.name)) path = \ "/" + self._device_identity + "/userSkill?identifier=" + identifier user_skill = self.api.request({ "method": "GET", "path": path }) if len(user_skill) == 0: return None else: return user_skill[0] def _put_metadata(self, settings_meta): """ PUT settingsmeta to backend to be configured in server. used in place of POST and PATCH. Args: settings_meta (dict): dictionary of the current settings meta data """ return self.api.request({ "method": "PUT", "path": self._api_path, "json": settings_meta }) def _delete_metadata(self, uuid): """ Deletes the current skill metadata Args: uuid (str): unique id of the skill """ try: LOG.info("deleting metadata") self.api.request({ "method": "DELETE", "path": self._api_path + "/{}".format(uuid) }) except Exception as e: LOG.error(e) LOG.info( "cannot delete metadata because this" "device is not original uploader of skill") @property def _should_upload_from_change(self): changed = False if hasattr(self, '_remote_settings'): sections = self._remote_settings['skillMetadata']['sections'] for i, section in enumerate(sections): for j, field in enumerate(section['fields']): if 'name' in field: # Ensure that the field exists in settings and that # it has a value to compare if (field["name"] in self and 'value' in sections[i]['fields'][j]): remote_val = sections[i]['fields'][j]["value"] self_val = self.get(field['name']) if str(remote_val) != str(self_val): changed = True if self.get('not_owner'): changed = False return changed def store(self, force=False): """ Store dictionary to file if a change has occured. Args: force: Force write despite no change """ if force or not self._is_stored: with open(self._settings_path, 'w') as f: json.dump(self, f) self.loaded_hash = hash(str(self)) if self._should_upload_from_change: settings_meta = self._load_settings_meta() hashed_meta = self._get_meta_hash(settings_meta) uuid = self._load_uuid() if uuid is not None: LOG.info("deleting meta data for {}".format(self.name)) self._delete_metadata(uuid) self._upload_meta(settings_meta, hashed_meta)
class PairingSkill(MycroftSkill): def __init__(self): super(PairingSkill, self).__init__("PairingSkill") self.api = DeviceApi() self.data = None self.last_request = None self.state = str(uuid4()) self.delay = 10 self.expiration = 72000 # 20 hours self.activator = None self.repeater = None # TODO: Add translation support self.nato_dict = {'A': "'A' as in Apple", 'B': "'B' as in Bravo", 'C': "'C' as in Charlie", 'D': "'D' as in Delta", 'E': "'E' as in Echo", 'F': "'F' as in Fox trot", 'G': "'G' as in Golf", 'H': "'H' as in Hotel", 'I': "'I' as in India", 'J': "'J' as in Juliet", 'K': "'K' as in Kilogram", 'L': "'L' as in London", 'M': "'M' as in Mike", 'N': "'N' as in November", 'O': "'O' as in Oscar", 'P': "'P' as in Paul", 'Q': "'Q' as in Quebec", 'R': "'R' as in Romeo", 'S': "'S' as in Sierra", 'T': "'T' as in Tango", 'U': "'U' as in Uniform", 'V': "'V' as in Victor", 'W': "'W' as in Whiskey", 'X': "'X' as in X-Ray", 'Y': "'Y' as in Yankee", 'Z': "'Z' as in Zebra", '1': 'One', '2': 'Two', '3': 'Three', '4': 'Four', '5': 'Five', '6': 'Six', '7': 'Seven', '8': 'Eight', '9': 'Nine', '0': 'Zero'} def initialize(self): intent = IntentBuilder("PairingIntent") \ .require("PairingKeyword").require("DeviceKeyword").build() self.register_intent(intent, self.handle_pairing) self.emitter.on("mycroft.not.paired", self.not_paired) def not_paired(self, message): self.speak_dialog("pairing.not.paired") self.handle_pairing() def handle_pairing(self, message=None): if self.is_paired(): self.speak_dialog("pairing.paired") elif self.data and self.last_request < time.time(): self.speak_code() else: self.last_request = time.time() + self.expiration self.data = self.api.get_code(self.state) self.enclosure.deactivate_mouth_events() # keeps code on the display self.speak_code() if not self.activator: self.__create_activator() def on_activate(self): try: # wait for a signal from the backend that pairing is complete token = self.data.get("token") login = self.api.activate(self.state, token) # shut down thread that repeats the code to the user if self.repeater: self.repeater.cancel() self.repeater = None # is_speaking() and stop_speaking() support is mycroft-core 0.8.16+ try: if mycroft.util.is_speaking(): # Assume speaking is the pairing code. Stop TTS mycroft.util.stop_speaking() except: pass self.enclosure.activate_mouth_events() # clears the display self.speak_dialog("pairing.paired") # wait_while_speaking() support is mycroft-core 0.8.16+ try: mycroft.util.wait_while_speaking() except: pass IdentityManager.save(login) self.emitter.emit(Message("mycroft.paired", login)) # Un-mute. Would have been muted during onboarding for a new # unit, and not dangerous to do if pairing was started # independently. self.emitter.emit(Message("mycroft.mic.unmute", None)) except: if self.last_request < time.time(): self.data = None self.handle_pairing() else: self.__create_activator() def __create_activator(self): self.activator = Timer(self.delay, self.on_activate) self.activator.daemon = True self.activator.start() def is_paired(self): try: device = self.api.get() except: device = None return device is not None def speak_code(self): """ speak code and start repeating it every 60 second. """ if self.repeater: self.repeater.cancel() self.repeater = None self.__speak_code() self.repeater = Timer(60, self.__repeat_code) self.repeater.daemon = True self.repeater.start() def __speak_code(self): """ Speak code. """ code = self.data.get("code") self.log.info("Pairing code: " + code) data = {"code": '. '.join(map(self.nato_dict.get, code))} self.enclosure.mouth_text(self.data.get("code")) self.speak_dialog("pairing.code", data) def __repeat_code(self): """ Timer function to repeat the code every 60 second. """ # if pairing is complete terminate the thread if self.is_paired(): self.repeater = None return # repeat instructions/code every 60 seconds (start to start) self.__speak_code() self.repeater = Timer(60, self.__repeat_code) self.repeater.daemon = True self.repeater.start() def stop(self): pass def shutdown(self): super(PairingSkill, self).shutdown() if self.activator: self.activator.cancel() if self.repeater: self.repeater.cancel()
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)
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()
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
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