Merge pull request #1151 from MycroftAI/feature/skill-settings

improved skill settings
pull/1156/head
Åke 2017-10-13 23:16:00 +02:00 committed by GitHub
commit 3e8b366642
3 changed files with 219 additions and 88 deletions

View File

@ -254,7 +254,7 @@ class MycroftSkill(object):
try:
return self._settings
except:
self._settings = SkillSettings(self._dir)
self._settings = SkillSettings(self._dir, self.name)
return self._settings
def bind(self, emitter):
@ -603,7 +603,7 @@ class MycroftSkill(object):
"""
# Store settings
self.settings.store()
self.settings.is_alive = False
# removing events
for e, f in self.events:
self.emitter.remove(e, f)

View File

@ -27,20 +27,15 @@
import json
from threading import Timer
from os.path import isfile, join
from os.path import isfile, join, expanduser
from mycroft.api import DeviceApi
from mycroft.util.log import LOG
from mycroft.configuration import ConfigurationManager
SKILLS_DIR = "/opt/mycroft/skills"
# TODO: allow deleting skill when skill is deleted
class SkillSettings(dict):
"""
SkillSettings creates a dictionary that can easily be stored
""" SkillSettings creates a dictionary that can easily be stored
to file, serialized as json. It also syncs to the backend for
skill settings
@ -48,24 +43,67 @@ class SkillSettings(dict):
settings_file (str): Path to storage file
"""
def __init__(self, directory):
def __init__(self, directory, name):
super(SkillSettings, self).__init__()
self.api = DeviceApi()
self._device_identity = self.api.identity.uuid
self.config = ConfigurationManager.get()
self.name = name
# set file paths
self._settings_path = join(directory, 'settings.json')
self._meta_path = join(directory, 'settingsmeta.json')
self._api_path = "/" + self._device_identity + "/skill"
self.is_alive = True
self.loaded_hash = hash(str(self))
# if settingsmeta.json exists
# this block of code is a control flow for
# different scenarios that may arises with settingsmeta
if isfile(self._meta_path):
self.settings_meta = self._load_settings_meta()
self.settings = self._get_settings()
self._send_settings_meta()
# start polling timer
Timer(60, self._poll_skill_settings).start()
LOG.info("settingsmeta.json exist for {}".format(self.name))
settings_meta = self._load_settings_meta()
hashed_meta = hash(str(settings_meta)+str(self._device_identity))
# check if hash is different from the saved hashed
if self._is_new_hash(hashed_meta):
LOG.info("looks like settingsmeta.json" +
"has changed for {}".format(self.name))
# TODO: once the delete api for device is created uncomment
if self._uuid_exist():
try:
LOG.info("a uuid exist for {}".format(self.name) +
" deleting old one")
old_uuid = self._load_uuid()
LOG.info(old_uuid+self.name)
self._delete_metatdata(old_uuid)
except Exception as e:
LOG.info(e)
LOG.info("sending settingsmeta.json for {}".format(self.name) +
"to home.mycroft.ai")
new_uuid = self._send_settings_meta(settings_meta, hashed_meta)
self._save_uuid(new_uuid)
self._save_hash(hashed_meta)
else: # if hash is old
found_in_backend = False
settings = self._get_remote_settings()
# checks backend if th settings have been deleted
# through web ui
for skill in settings:
if skill["identifier"] == str(hashed_meta):
found_in_backend = True
# if it's been deleted from web ui
# resend the settingsmeta.json
if found_in_backend is False:
LOG.info("seems like it got deleted from home... " +
"sending settingsmeta.json for " +
"{}".format(self.name))
new_uuid = self._send_settings_meta(
settings_meta, hashed_meta)
self._save_uuid(new_uuid)
self._save_hash(hashed_meta)
t = Timer(60, self._poll_skill_settings, [hashed_meta])
t.daemon = True
t.start()
self.load_skill_settings()
@ -74,81 +112,154 @@ class SkillSettings(dict):
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.
"""
""" Add/Update key. """
return super(SkillSettings, self).__setitem__(key, value)
def _load_settings_meta(self):
""" loads settings metadata from skills path """
with open(self._meta_path) as f:
data = json.load(f)
return data
def _skill_exist_in_backend(self):
"""
see if skill settings already exist in the backend
"""
skill_identity = self._get_skill_identity()
for skill_setting in self.settings:
if skill_identity == skill_setting["identifier"]:
return True
return False
def _send_settings_meta(self, settings_meta, hashed_meta):
""" send settingsmeta.json to the backend
def _send_settings_meta(self):
"""
send settingsmeta.json to the backend if skill doesn't
already exist
Args:
param1 (dict): dictionary of the current settings meta data
param1 (int): hashed settings meta data
Returns:
uuid (str): a unique id for the setting meta data
"""
try:
if self._skill_exist_in_backend() is False:
response = self._put_metadata(self.settings_meta)
settings_meta["identifier"] = str(hashed_meta)
self._put_metadata(settings_meta)
settings = self._get_remote_settings()
skill_identity = str(hashed_meta)
uuid = None
# TODO: note uuid should be returned from the put request
for skill_setting in settings:
if skill_setting['identifier'] == skill_identity:
uuid = skill_setting["uuid"]
return uuid
except Exception as e:
LOG.error(e)
def _poll_skill_settings(self):
def _load_uuid(self):
""" loads uuid
Returns:
uuid (str): uuid of the previous settingsmeta
"""
If identifier exists for this skill poll to backend to
directory = self.config.get("skills")["directory"]
directory = join(directory, self.name)
directory = expanduser(directory)
uuid_file = join(directory, 'uuid')
if isfile(uuid_file):
with open(uuid_file, 'r') as f:
uuid = f.read()
return uuid
def _save_uuid(self, uuid):
""" saves uuid to path
Args:
param1 (str): uuid of new seetingsmeta
"""
LOG.info("saving uuid {}".format(str(uuid)))
directory = self.config.get("skills")["directory"]
directory = join(directory, self.name)
directory = expanduser(directory)
uuid_file = join(directory, 'uuid')
with open(uuid_file, 'w') as f:
f.write(str(uuid))
def _save_hash(self, hashed_meta):
""" saves hashed_meta to path
Args:
param1 (int): hashed of new seetingsmeta
"""
LOG.info("saving hash {}".format(str(hashed_meta)))
directory = self.config.get("skills")["directory"]
directory = join(directory, self.name)
directory = expanduser(directory)
hash_file = join(directory, 'hash')
with open(hash_file, 'w') as f:
f.write(str(hashed_meta))
def _uuid_exist(self):
""" checks if there is a uuid file
Returns:
bool: True if uuid file exist False otherwise
"""
directory = self.config.get("skills")["directory"]
directory = join(directory, self.name)
directory = expanduser(directory)
uuid_file = join(directory, 'uuid')
return isfile(uuid_file)
def _is_new_hash(self, hashed_meta):
""" checks if the stored hash is the same as current.
if the hashed file does not exist, usually in the
case of first load, then the create it and return True
Args:
param1 (int): hash of metadata and uuid of device
Returns:
bool: True if hash is new False otherwise
"""
directory = self.config.get("skills")["directory"]
directory = join(directory, self.name)
directory = expanduser(directory)
hash_file = join(directory, 'hash')
if isfile(hash_file):
with open(hash_file, 'r') as f:
current_hash = f.read()
return False if current_hash == str(hashed_meta) else True
return True
def _poll_skill_settings(self, hashed_meta):
""" If identifier exists for this skill poll to backend to
request settings and store it if it changes
TODO: implement as websocket
Args:
param1 (int): the hashed identifier
"""
if self._skill_exist_in_backend():
try:
# update settings
self.settings = self._get_settings()
skill_identity = self._get_skill_identity()
for skill_setting in self.settings:
if skill_setting['identifier'] == skill_identity:
sections = skill_setting['skillMetadata']['sections']
for section in sections:
for field in section["fields"]:
self.__setitem__(field["name"], field["value"])
# store value if settings has changed from backend
self.store()
except Exception as e:
LOG.error(e)
# poll backend every 60 seconds for new settings
Timer(60, self._poll_skill_settings).start()
def _get_skill_identity(self):
"""
returns the skill identifier
"""
LOG.info("getting settings from home.mycroft.ai")
try:
return self.settings_meta["identifier"]
# update settings
settings = self._get_remote_settings()
skill_identity = str(hashed_meta)
for skill_setting in settings:
if skill_setting['identifier'] == skill_identity:
sections = skill_setting['skillMetadata']['sections']
for section in sections:
for field in section["fields"]:
self.__setitem__(field["name"], field["value"])
# store value if settings has changed from backend
self.store()
except Exception as e:
LOG.error(e)
return None
if self.is_alive:
# continues to poll settings every 60 seconds
t = Timer(60, self._poll_skill_settings, [hashed_meta])
t.daemon = True
t.start()
def load_skill_settings(self):
"""
If settings.json exist, open and read stored values into self
"""
""" If settings.json exist, open and read stored values into self """
if isfile(self._settings_path):
with open(self._settings_path) as f:
try:
@ -160,18 +271,15 @@ class SkillSettings(dict):
# metadata to be able to edit later.
LOG.error(e)
def _get_settings(self):
"""
Get skill settings for this device from backend
"""
def _get_remote_settings(self):
""" Get skill settings for this device from backend """
return self.api.request({
"method": "GET",
"path": self._api_path
})
def _put_metadata(self, settings_meta):
"""
PUT settingsmeta to backend to be configured in home.mycroft.ai.
""" PUT settingsmeta to backend to be configured in home.mycroft.ai.
used in plcae of POST and PATCH
"""
return self.api.request({
@ -180,9 +288,19 @@ class SkillSettings(dict):
"json": settings_meta
})
def store(self, force=False):
def _delete_metatdata(self, uuid):
""" Deletes the current skill metadata
Args:
param1 (str): unique id of the skill
"""
Store dictionary to file if a change has occured.
return self.api.request({
"method": "DELETE",
"path": "/skill/{}".format(uuid)
})
def store(self, force=False):
""" Store dictionary to file if a change has occured.
Args:
force: Force write despite no change

View File

@ -29,16 +29,19 @@ class SkillSettingsTest(unittest.TestCase):
pass
def test_new(self):
s = SkillSettings(join(dirname(__file__), 'settings'))
s = SkillSettings(join(dirname(__file__), 'settings'),
"test-skill-settings")
self.assertEqual(len(s), 0)
def test_add_value(self):
s = SkillSettings(join(dirname(__file__), 'settings'))
s = SkillSettings(join(dirname(__file__), 'settings'),
"test-skill-settings")
s['test_val'] = 1
self.assertEqual(s['test_val'], 1)
def test_store(self):
s = SkillSettings(join(dirname(__file__), 'settings'))
s = SkillSettings(join(dirname(__file__), 'settings'),
"test-skill-settings")
s['bool'] = True
s['int'] = 42
s['float'] = 4.2
@ -46,49 +49,59 @@ class SkillSettingsTest(unittest.TestCase):
s['list'] = ['batman', 2, True, 'superman']
s.store()
s2 = SkillSettings(join(dirname(__file__), 'settings'))
s2 = SkillSettings(join(dirname(__file__), 'settings'),
"test-skill-settings")
for key in s:
self.assertEqual(s[key], s2[key])
def test_update_list(self):
s = SkillSettings(join(dirname(__file__), 'settings'))
s = SkillSettings(join(dirname(__file__), 'settings'),
"test-skill-settings")
s['l'] = ['a', 'b', 'c']
s.store()
s2 = SkillSettings(join(dirname(__file__), 'settings'))
s2 = SkillSettings(join(dirname(__file__), 'settings'),
"test-skill-settings")
self.assertEqual(s['l'], s2['l'])
# Update list
s2['l'].append('d')
s2.store()
s3 = SkillSettings(join(dirname(__file__), 'settings'))
s3 = SkillSettings(join(dirname(__file__), 'settings'),
"test-skill-settings")
self.assertEqual(s2['l'], s3['l'])
def test_update_dict(self):
s = SkillSettings(join(dirname(__file__), 'settings'))
s = SkillSettings(join(dirname(__file__), 'settings'),
"test-skill-settings")
s['d'] = {'a': 1, 'b': 2}
s.store()
s2 = SkillSettings(join(dirname(__file__), 'settings'))
s2 = SkillSettings(join(dirname(__file__), 'settings'),
"test-skill-settings")
self.assertEqual(s['d'], s2['d'])
# Update dict
s2['d']['c'] = 3
s2.store()
s3 = SkillSettings(join(dirname(__file__), 'settings'))
s3 = SkillSettings(join(dirname(__file__), 'settings'),
"test-skill-settings")
self.assertEqual(s2['d'], s3['d'])
def test_no_change(self):
s = SkillSettings(join(dirname(__file__), 'settings'))
s = SkillSettings(join(dirname(__file__), 'settings'),
"test-skill-settings")
s['d'] = {'a': 1, 'b': 2}
s.store()
s2 = SkillSettings(join(dirname(__file__), 'settings'))
s2 = SkillSettings(join(dirname(__file__), 'settings'),
"test-skill-settings")
self.assertTrue(len(s) == len(s2))
def test_load_existing(self):
directory = join(dirname(__file__), 'settings', 'settings.json')
with open(directory, 'w') as f:
json.dump({"test": "1"}, f)
s = SkillSettings(join(dirname(__file__), 'settings'))
s = SkillSettings(join(dirname(__file__), 'settings'),
"test-skill-settings")
self.assertEqual(len(s), 1)