16 SkillSettings is a simple extension of a Python dict which enables 17 simplified storage of settings. Values stored into the dict will 18 automatically persist locally. Additionally, it can interact with 19 a backend system to provide a GUI interface for some or all of the 22 The GUI for the setting is described by a file in the skill's root 23 directory called settingsmeta.json (or settingsmeta.yaml, if you 24 prefer working with yaml). The "name" associates the user-interface 25 field with the setting name in the dictionary. For example, you 26 might have a setting['username']. In the settingsmeta you can 27 describe the interface you want to edit that value with: 33 "label": "Email address to associate", 34 "placeholder": "example@mail.com", 38 When the user changes the setting via the web UI, it will be sent 39 down to all the devices and automatically placed into the 40 settings['username']. Any local changes made to the value (e.g. 41 via a verbal interaction) will also be synched to the server to show 44 NOTE: As it stands today, this functions seamlessly with a single 45 device. With multiple devices there are a few hitches that are being 46 worked out. The first device where a skill is installed creates the 47 setting and values are sent down to any other devices that install the 48 same skill. However only the original device can make changes locally 49 for synching to the web. This limitation is temporary and will be 54 from mycroft.skill.settings import SkillSettings 56 s = SkillSettings('./settings.json', 'ImportantSettings') 57 s['meaning of life'] = 42 58 s['flower pot sayings'] = 'Not again...' 59 s.store() # This happens automagically in a MycroftSkill 69 from threading
import Timer
70 from os.path
import isfile, join, expanduser
71 from requests.exceptions
import RequestException, HTTPError
72 from msm
import SkillEntry
79 from .msm_wrapper
import create_msm
87 """ Create global id for the skill. 89 TODO: Handle dirty skill 92 directory: skill directory 93 config: config for the device to fetch msm setup 97 global msm_creation_time
98 if msm
is None or time.time() - msm_creation_time > 60 * 60:
99 msm_creation_time = time.time()
102 s = SkillEntry.from_folder(directory, msm)
104 return s.skill_gid, s.meta_info.get(
'display_name')
108 """ Splits camelcase and removes leading/trailing Skill. """ 109 name = re.sub(
r'(^[Ss]kill|[Ss]kill$)',
'', name)
114 """ Indicate that the next request should be delayed. """ 118 class SkillSettings(dict):
119 """ Dictionary that can easily be saved to a file, serialized as json. It 120 also syncs to the backend for skill settings 123 directory (str): Path to storage directory 124 name (str): user readable name associated with the settings 125 no_upload (bool): True if the upload to mycroft servers should be 130 super(SkillSettings, self).
__init__()
167 elif self.
config[
'skills'][
'upload_skill_manifest']:
169 self._blank_poll_timer.daemon =
True 170 self._blank_poll_timer.start()
174 """ Finalizes the skill gid to include device uuid if needed. """ 176 return self.__skill_gid.replace(
'@|',
'@{}|'.format(
182 """ Simple object unique hash. """ 183 return hash(str(id(self)) + self.
name)
186 """Immediately poll the web for new skill settings""" 188 self._poll_timer.cancel()
194 self._poll_timer.cancel()
196 self._blank_poll_timer.cancel()
199 """ Set callback to perform when server settings have changed. 202 callback: function/method to call when settings have changed 208 """ initializes the remote settings to the server """ 214 if not settings_meta:
224 except RequestException:
248 """ Add/Update key. """ 250 return super(SkillSettings, self).
__setitem__(key, value)
253 """ Load settings metadata from the skill folder. 255 If no settingsmeta exists a basic settingsmeta will be created 256 containing a basic identifier. 263 json_file =
True if ext.lower() ==
".json" else False 266 with open(self.
_meta_path, encoding=
'utf-8')
as f:
270 data = yaml.safe_load(f)
271 except Exception
as e:
272 LOG.error(
"Failed to load setting file: " + self.
_meta_path)
280 data[
'display_name'] = (self.
display_name or data.get(
'name')
or 284 if 'name' not in data:
285 data[
'name'] = data[
'display_name']
290 """ Send settingsmeta to the server. 293 settings_meta (dict): dictionary of the current settings meta 295 dict: uuid, a unique id for the setting meta data 299 uuid = self.api.upload_skill_metadata(
300 self.
_type_cast(settings_meta, to_platform=
'web'))
302 except HTTPError
as e:
303 if e.response.status_code
in [422, 500, 501]:
310 except Exception
as e:
315 """ Takes skill object and save onto self 318 skill_settings (dict): skill 320 if 'skillMetadata' in skill_settings:
321 sections = skill_settings[
'skillMetadata'][
'sections']
322 for section
in sections:
323 for field
in section[
"fields"]:
324 if "name" in field
and "value" in field:
327 super(SkillSettings, self).
__setitem__(field[
'name'],
332 """ sync settings.json and settingsmeta in memory """ 333 meta = settings_meta.copy()
334 if 'skillMetadata' not in meta:
337 sections = meta[
'skillMetadata'][
'sections']
338 for i, section
in enumerate(sections):
339 for j, field
in enumerate(section[
'fields']):
341 if field[
"name"]
in self:
342 sections[i][
'fields'][j][
'value'] = \
344 meta[
'skillMetadata'][
'sections'] = sections
348 """ uploads the new meta data to settings with settings migration 351 settings_meta (dict): settingsmeta.json or settingsmeta.yaml 352 identifier (str): identifier for skills meta data 354 LOG.debug(
'Uploading settings meta for {}'.format(identifier))
356 meta[
'identifier'] = identifier
360 """ md5 hasher for consistency across cpu architectures """ 361 return hashlib.md5(bytes(string,
'utf-8')).hexdigest()
364 """ update settings state from server """ 366 if settings_meta
is None:
371 if skills_settings
is not None:
374 LOG.debug(
"No Settings on server for {}".format(self.
skill_gid))
380 """ Send blank settingsmeta to remote. """ 384 self._blank_poll_timer.daemon =
True 385 self._blank_poll_timer.start()
392 self._blank_poll_timer.daemon =
True 393 self._blank_poll_timer.start()
394 except Exception
as e:
395 LOG.exception(
'Failed to send blank meta: {}'.format(repr(e)))
398 """ If identifier exists for this skill poll to backend to 399 request settings and store it if it changes 400 TODO: implement as websocket 403 original =
hash(str(self))
412 LOG.info(
'{}: Delaying next settings fetch'.format(self.
name))
414 except Exception
as e:
415 LOG.exception(
'Failed to fetch skill settings: {}'.format(repr(e)))
423 self._poll_timer.cancel()
430 self._poll_timer.daemon =
True 431 self._poll_timer.start()
434 """ If settings.json exist, open and read stored values into self """ 438 json_data = json.load(f)
439 for key
in json_data:
440 self[key] = json_data[key]
441 except Exception
as e:
447 """ Tranform data type to be compatible with Home and/or Core. 451 "true" => True, "1.4" => 1.4 457 settings_meta (dict): skills object 458 to_platform (str): platform to convert 459 compatible data types to 463 meta = settings_meta.copy()
464 if 'skillMetadata' not in settings_meta:
467 sections = meta[
'skillMetadata'][
'sections']
469 for i, section
in enumerate(sections):
470 for j, field
in enumerate(section.get(
'fields', [])):
471 _type = field.get(
'type')
472 if _type ==
'checkbox':
473 value = field.get(
'value')
475 if to_platform ==
'web':
476 if value
is True or value ==
'True':
477 sections[i][
'fields'][j][
'value'] =
'true' 478 elif value
is False or value ==
'False':
479 sections[i][
'fields'][j][
'value'] =
'false' 481 elif to_platform ==
'core':
482 if value ==
'true' or value ==
'True':
483 sections[i][
'fields'][j][
'value'] =
True 484 elif value ==
'false' or value ==
'False':
485 sections[i][
'fields'][j][
'value'] =
False 487 elif _type ==
'number':
488 value = field.get(
'value')
490 if to_platform ==
'core':
492 sections[i][
'fields'][j][
'value'] = float(value)
494 sections[i][
'fields'][j][
'value'] = int(value)
496 elif to_platform ==
'web':
497 sections[i][
'fields'][j][
'value'] = str(value)
499 meta[
'skillMetadata'][
'sections'] = sections
503 """ Get skill settings for this device associated 506 identifier (str): a hashed_meta 508 skill_settings (dict or None): returns a dict if matches 513 for skill_settings
in settings:
514 if skill_settings[
'identifier'] == identifier:
515 LOG.debug(
"Fetched settings for {}".format(identifier))
517 self.
_type_cast(skill_settings, to_platform=
'core')
519 return skill_settings
523 """ Get all skill settings for this device from server. 526 dict: dictionary with settings collected from the server. 529 settings = self.api.get_skill_settings()
530 except RequestException:
533 settings = [skills
for skills
in settings
if skills
is not None]
539 if (hasattr(self,
'_remote_settings')
and 542 for i, section
in enumerate(sections):
543 for j, field
in enumerate(section[
'fields']):
547 if (field[
"name"]
in self
and 548 'value' in sections[i][
'fields'][j]):
549 remote_val = sections[i][
'fields'][j][
"value"]
550 self_val = self.get(field[
'name'])
551 if str(remote_val) != str(self_val):
553 if self.get(
'not_owner'):
558 """ Store dictionary to file if a change has occured. 561 force: Force write despite no change 574 json_path = join(base_directory,
'settingsmeta.json')
575 yaml_path = join(base_directory,
'settingsmeta.yaml')
576 if isfile(json_path):
578 if isfile(yaml_path):
def _migrate_settings(self, settings_meta)
def save_skill_settings(self, skill_settings)
def _should_upload_from_change(self)
def _request_settings(self)
def _send_settings_meta(self, settings_meta)
def _init_blank_meta(self)
def initialize_remote_settings(self)
def store(self, force=False)
def _request_my_settings(self, identifier)
def is_paired(ignore_errors=True)
def _get_meta_path(base_directory)
def __setitem__(self, key, value)
def __getitem__(self, key)
def build_global_id(directory, config)
def _poll_skill_settings(self)
def _load_settings_meta(self)
def run_poll(self, _=None)
def set_changed_callback(self, callback)
def load_skill_settings_from_file(self)
def _upload_meta(self, settings_meta, identifier)
def _type_cast(self, settings_meta, to_platform)
def __init__(self, directory, name)