Skip to content
This repository was archived by the owner on Sep 8, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mycroft/skills/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ def settings(self):
try:
return self._settings
except:
self._settings = SkillSettings(join(self._dir, 'settings.json'))
self._settings = SkillSettings(self._dir)
return self._settings

def bind(self, emitter):
Expand Down
145 changes: 130 additions & 15 deletions mycroft/skills/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,29 +30,46 @@

import json
import sys
from os.path import isfile
from threading import Timer
from os.path import isfile, join, exists, expanduser
from mycroft.util.log import getLogger
from mycroft.api import DeviceApi

logger = getLogger(__name__)
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
to file, serialized as json.
to file, serialized as json. It also syncs to the backend for
skill settings

Args:
settings_file (str): Path to storage file
"""
def __init__(self, settings_file):
def __init__(self, directory):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to keep the settings_file option to keep this more a general class that can be instantiated multiple times with multiple settings-files. Can you append a .meta to the settings_file path to keep the api?

Makes testing easier as well...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

super(SkillSettings, self).__init__()
self._path = settings_file
# if file exist, open and read stored values into self
if isfile(self._path):
with open(self._path) as f:
json_data = json.load(f)
for key in json_data:
self.__setitem__(key, json_data[key])
self.api = DeviceApi()
self._device_identity = self.api.identity.uuid
# 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.loaded_hash = hash(str(self))

# if settingsmeta.json exists
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()

self.load_skill_settings()

@property
def _is_stored(self):
return hash(str(self)) == self.loaded_hash
Expand All @@ -66,11 +83,109 @@ def __setitem__(self, key, value):
"""
return super(SkillSettings, self).__setitem__(key, value)

def _load_settings_meta(self):
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):
"""
send settingsmeta.json to the backend if skill doesn't
already exist
"""
try:
if self._skill_exist_in_backend() is False:
response = self._put_metadata(self.settings_meta)
except Exception as e:
logger.error(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
"""
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
if not self._is_stored:
self.store()
self.loaded_hash = hash(str(self))

except Exception as e:
logger.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
"""
try:
return self.settings_meta["identifier"]
except Exception as e:
logger.error(e)
return None

def load_skill_settings(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.__setitem__(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.
logger.error(e)

def _get_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.
used in plcae of POST and PATCH
"""
return self.api.request({
"method": "PUT",
"path": self._api_path,
"json": settings_meta
})

def store(self):
"""
Store dictionary to file if it has changed
Store dictionary to file
"""
if not self._is_stored:
with open(self._path, 'w')as f:
json.dump(self, f)
self.loaded_hash = hash(str(self))
with open(self._settings_path, 'w') as f:
json.dump(self, f)
41 changes: 23 additions & 18 deletions test/skills/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,74 +2,79 @@

from os.path import join, dirname, abspath
from os import remove
import json
import unittest


class SkillSettingsTest(unittest.TestCase):
def setUp(self):
try:
remove(join(dirname(__file__), 'settings', 'store.json'))
remove(join(dirname(__file__), 'settings', 'settings.json'))
except OSError:
pass

def test_new(self):
s = SkillSettings(join(dirname(__file__), 'settings.json'))
s = SkillSettings(join(dirname(__file__), 'settings'))
self.assertEqual(len(s), 0)

def test_add_value(self):
s = SkillSettings(join(dirname(__file__), 'settings.json'))
s = SkillSettings(join(dirname(__file__), 'settings'))
s['test_val'] = 1

def test_load_existing(self):
s = SkillSettings(join(dirname(__file__), 'settings', 'existing.json'))
self.assertEqual(len(s), 4)
self.assertEqual(s['test_val'], 1)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a kind of important test case and should be implemented in some way

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved


def test_store(self):
s = SkillSettings(join(dirname(__file__), 'settings', 'store.json'))
s = SkillSettings(join(dirname(__file__), 'settings'))
s['bool'] = True
s['int'] = 42
s['float'] = 4.2
s['string'] = 'Always carry a towel'
s['list'] = ['batman', 2, True, 'superman']
s.store()

s2 = SkillSettings(join(dirname(__file__), 'settings', 'store.json'))
s2 = SkillSettings(join(dirname(__file__), 'settings'))
for key in s:
self.assertEqual(s[key], s2[key])

def test_update_list(self):
s = SkillSettings(join(dirname(__file__), 'settings', 'store.json'))
s = SkillSettings(join(dirname(__file__), 'settings'))
s['l'] = ['a', 'b', 'c']
s.store()
s2 = SkillSettings(join(dirname(__file__), 'settings', 'store.json'))
s2 = SkillSettings(join(dirname(__file__), 'settings'))
self.assertEqual(s['l'], s2['l'])

# Update list
s2['l'].append('d')
s2.store()
s3 = SkillSettings(join(dirname(__file__), 'settings', 'store.json'))
s3 = SkillSettings(join(dirname(__file__), 'settings'))
self.assertEqual(s2['l'], s3['l'])

def test_update_dict(self):
s = SkillSettings(join(dirname(__file__), 'settings', 'store.json'))
s = SkillSettings(join(dirname(__file__), 'settings'))
s['d'] = {'a': 1, 'b': 2}
s.store()
s2 = SkillSettings(join(dirname(__file__), 'settings', 'store.json'))
s2 = SkillSettings(join(dirname(__file__), 'settings'))
self.assertEqual(s['d'], s2['d'])

# Update dict
s2['d']['c'] = 3
s2.store()
s3 = SkillSettings(join(dirname(__file__), 'settings', 'store.json'))
s3 = SkillSettings(join(dirname(__file__), 'settings'))
self.assertEqual(s2['d'], s3['d'])

def test_no_change(self):
s = SkillSettings(join(dirname(__file__), 'settings', 'store.json'))
s = SkillSettings(join(dirname(__file__), 'settings'))
s['d'] = {'a': 1, 'b': 2}
s.store()

s = SkillSettings(join(dirname(__file__), 'settings', 'store.json'))
self.assertTrue(s._is_stored)
s2 = SkillSettings(join(dirname(__file__), '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'))
self.assertEqual(len(s), 1)


if __name__ == '__main__':
Expand Down
7 changes: 0 additions & 7 deletions test/skills/settings/existing.json

This file was deleted.

1 change: 1 addition & 0 deletions test/skills/settings/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"d": {"a": 1, "c": 3, "b": 2}, "int": 42, "float": 4.2, "list": ["batman", 2, true, "superman"], "l": ["a", "b", "c", "d"], "bool": true, "string": "Always carry a towel"}