Merge pull request #1151 from MycroftAI/feature/skill-settings
improved skill settingspull/1156/head
commit
3e8b366642
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue