2017-10-04 06:28:44 +00:00
|
|
|
# Copyright 2017 Mycroft AI Inc.
|
2017-04-13 05:26:45 +00:00
|
|
|
#
|
2017-10-04 06:28:44 +00:00
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
2017-04-13 05:26:45 +00:00
|
|
|
#
|
2017-10-04 06:28:44 +00:00
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
2017-04-13 05:26:45 +00:00
|
|
|
#
|
2017-10-04 06:28:44 +00:00
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
2017-04-13 05:26:45 +00:00
|
|
|
#
|
2017-04-24 10:09:23 +00:00
|
|
|
"""
|
2017-11-09 10:34:00 +00:00
|
|
|
SkillSettings is a simple extension of the python dict which enables
|
|
|
|
local storage of settings. Additionally it can interact with a backend
|
|
|
|
system to provide a GUI interface, described by meta-data described in
|
|
|
|
an optional 'settingsmeta.json' file.
|
2017-04-24 10:09:23 +00:00
|
|
|
|
2017-11-09 10:34:00 +00:00
|
|
|
Usage Example:
|
2017-04-24 10:09:23 +00:00
|
|
|
from mycroft.skill.settings import SkillSettings
|
|
|
|
|
|
|
|
s = SkillSettings('./settings.json')
|
|
|
|
s['meaning of life'] = 42
|
|
|
|
s['flower pot sayings'] = 'Not again...'
|
|
|
|
s.store()
|
2017-11-09 10:34:00 +00:00
|
|
|
|
|
|
|
Metadata format:
|
|
|
|
TODO...see https://docs.google.com/document/d/17cToFjYx5NwtGTeX0sVpXdCICGhDzht-yZVyT26yQDQ/edit#
|
2017-04-24 10:09:23 +00:00
|
|
|
"""
|
|
|
|
|
2017-04-13 05:26:45 +00:00
|
|
|
import json
|
2017-06-30 22:36:04 +00:00
|
|
|
from threading import Timer
|
2017-10-12 17:35:50 +00:00
|
|
|
from os.path import isfile, join, expanduser
|
2017-09-18 19:14:21 +00:00
|
|
|
|
2017-06-28 16:32:19 +00:00
|
|
|
from mycroft.api import DeviceApi
|
2017-09-18 19:14:21 +00:00
|
|
|
from mycroft.util.log import LOG
|
2017-10-12 17:35:50 +00:00
|
|
|
from mycroft.configuration import ConfigurationManager
|
2017-06-28 16:32:19 +00:00
|
|
|
|
2017-10-04 06:28:44 +00:00
|
|
|
|
2017-04-13 05:26:45 +00:00
|
|
|
class SkillSettings(dict):
|
2017-11-09 10:34:00 +00:00
|
|
|
""" A dictionary that can easily be save to a file, serialized as json. It
|
|
|
|
also syncs to the backend for skill settings
|
2017-04-24 10:09:23 +00:00
|
|
|
|
|
|
|
Args:
|
|
|
|
settings_file (str): Path to storage file
|
|
|
|
"""
|
2017-09-18 19:14:21 +00:00
|
|
|
|
2017-10-12 17:35:50 +00:00
|
|
|
def __init__(self, directory, name):
|
2017-04-13 05:26:45 +00:00
|
|
|
super(SkillSettings, self).__init__()
|
2017-06-28 16:32:19 +00:00
|
|
|
self.api = DeviceApi()
|
|
|
|
self._device_identity = self.api.identity.uuid
|
2017-10-12 17:35:50 +00:00
|
|
|
self.config = ConfigurationManager.get()
|
|
|
|
self.name = name
|
2017-07-27 21:28:32 +00:00
|
|
|
# set file paths
|
2017-06-28 16:32:19 +00:00
|
|
|
self._settings_path = join(directory, 'settings.json')
|
|
|
|
self._meta_path = join(directory, 'settingsmeta.json')
|
|
|
|
self._api_path = "/" + self._device_identity + "/skill"
|
2017-10-13 21:06:07 +00:00
|
|
|
self.is_alive = True
|
2017-07-27 21:28:32 +00:00
|
|
|
self.loaded_hash = hash(str(self))
|
2017-06-28 22:31:35 +00:00
|
|
|
|
2017-07-27 21:28:32 +00:00
|
|
|
# if settingsmeta.json exists
|
2017-10-12 17:35:50 +00:00
|
|
|
# this block of code is a control flow for
|
|
|
|
# different scenarios that may arises with settingsmeta
|
2017-07-27 21:28:32 +00:00
|
|
|
if isfile(self._meta_path):
|
2017-10-12 17:35:50 +00:00
|
|
|
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):
|
2017-10-13 22:34:22 +00:00
|
|
|
LOG.info("looks like settingsmeta.json " +
|
2017-10-12 17:35:50 +00:00
|
|
|
"has changed for {}".format(self.name))
|
|
|
|
# TODO: once the delete api for device is created uncomment
|
2017-10-13 21:06:07 +00:00
|
|
|
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)
|
2017-10-12 17:35:50 +00:00
|
|
|
LOG.info("sending settingsmeta.json for {}".format(self.name) +
|
2017-10-13 22:34:22 +00:00
|
|
|
" to home.mycroft.ai")
|
2017-10-12 17:35:50 +00:00
|
|
|
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
|
2017-10-12 19:36:23 +00:00
|
|
|
found_in_backend = False
|
|
|
|
settings = self._get_remote_settings()
|
2017-11-09 10:34:00 +00:00
|
|
|
# checks backend if the settings have been deleted via webUI
|
2017-10-12 17:35:50 +00:00
|
|
|
for skill in settings:
|
|
|
|
if skill["identifier"] == str(hashed_meta):
|
2017-10-12 19:36:23 +00:00
|
|
|
found_in_backend = True
|
2017-11-09 10:34:00 +00:00
|
|
|
# if it's been deleted from webUI resend
|
2017-10-12 19:36:23 +00:00
|
|
|
if found_in_backend is False:
|
2017-10-12 17:35:50 +00:00
|
|
|
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)
|
|
|
|
|
2017-10-13 18:43:55 +00:00
|
|
|
t = Timer(60, self._poll_skill_settings, [hashed_meta])
|
|
|
|
t.daemon = True
|
|
|
|
t.start()
|
2017-07-27 21:38:24 +00:00
|
|
|
|
2017-07-27 21:28:32 +00:00
|
|
|
self.load_skill_settings()
|
2017-04-24 10:08:47 +00:00
|
|
|
|
2017-08-01 18:52:51 +00:00
|
|
|
@property
|
|
|
|
def _is_stored(self):
|
|
|
|
return hash(str(self)) == self.loaded_hash
|
|
|
|
|
2017-04-13 05:26:45 +00:00
|
|
|
def __getitem__(self, key):
|
2017-10-12 19:36:23 +00:00
|
|
|
""" Get key """
|
2017-04-13 05:26:45 +00:00
|
|
|
return super(SkillSettings, self).__getitem__(key)
|
|
|
|
|
|
|
|
def __setitem__(self, key, value):
|
2017-10-12 19:36:23 +00:00
|
|
|
""" Add/Update key. """
|
2017-04-13 05:26:45 +00:00
|
|
|
return super(SkillSettings, self).__setitem__(key, value)
|
|
|
|
|
2017-07-27 21:28:32 +00:00
|
|
|
def _load_settings_meta(self):
|
2017-10-12 19:36:23 +00:00
|
|
|
""" loads settings metadata from skills path """
|
2017-07-27 21:28:32 +00:00
|
|
|
with open(self._meta_path) as f:
|
|
|
|
data = json.load(f)
|
|
|
|
return data
|
|
|
|
|
2017-10-12 17:35:50 +00:00
|
|
|
def _send_settings_meta(self, settings_meta, hashed_meta):
|
|
|
|
""" send settingsmeta.json to the backend
|
2017-07-06 17:40:21 +00:00
|
|
|
|
2017-10-12 17:35:50 +00:00
|
|
|
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
|
2017-07-27 21:38:24 +00:00
|
|
|
"""
|
2017-07-11 23:19:21 +00:00
|
|
|
try:
|
2017-10-12 17:35:50 +00:00
|
|
|
settings_meta["identifier"] = str(hashed_meta)
|
|
|
|
self._put_metadata(settings_meta)
|
2017-10-12 19:36:23 +00:00
|
|
|
settings = self._get_remote_settings()
|
2017-10-12 17:35:50 +00:00
|
|
|
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
|
2017-07-11 23:19:21 +00:00
|
|
|
except Exception as e:
|
2017-09-18 18:55:58 +00:00
|
|
|
LOG.error(e)
|
2017-06-28 22:31:35 +00:00
|
|
|
|
2017-10-12 17:35:50 +00:00
|
|
|
def _load_uuid(self):
|
|
|
|
""" loads uuid
|
|
|
|
|
|
|
|
Returns:
|
2017-10-12 18:32:50 +00:00
|
|
|
uuid (str): uuid of the previous settingsmeta
|
2017-06-28 22:31:35 +00:00
|
|
|
"""
|
2017-10-12 17:35:50 +00:00
|
|
|
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:
|
2017-10-12 18:32:50 +00:00
|
|
|
param1 (str): uuid of new seetingsmeta
|
2017-06-28 22:31:35 +00:00
|
|
|
"""
|
2017-10-12 17:35:50 +00:00
|
|
|
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
|
2017-07-11 20:25:43 +00:00
|
|
|
"""
|
2017-10-12 17:35:50 +00:00
|
|
|
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
|
2017-07-11 20:25:43 +00:00
|
|
|
"""
|
2017-10-12 17:35:50 +00:00
|
|
|
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:
|
2017-10-12 18:32:50 +00:00
|
|
|
param1 (int): hash of metadata and uuid of device
|
2017-10-12 17:35:50 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
"""
|
|
|
|
LOG.info("getting settings from home.mycroft.ai")
|
2017-07-11 20:25:43 +00:00
|
|
|
try:
|
2017-10-12 17:35:50 +00:00
|
|
|
# update settings
|
2017-10-12 19:36:23 +00:00
|
|
|
settings = self._get_remote_settings()
|
2017-10-12 17:35:50 +00:00
|
|
|
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()
|
|
|
|
|
2017-07-11 20:25:43 +00:00
|
|
|
except Exception as e:
|
2017-09-18 18:55:58 +00:00
|
|
|
LOG.error(e)
|
2017-10-12 17:35:50 +00:00
|
|
|
|
2017-10-13 21:06:07 +00:00
|
|
|
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()
|
2017-06-28 22:31:35 +00:00
|
|
|
|
2017-07-27 21:28:32 +00:00
|
|
|
def load_skill_settings(self):
|
2017-10-12 19:36:23 +00:00
|
|
|
""" If settings.json exist, open and read stored values into self """
|
2017-06-28 22:31:35 +00:00
|
|
|
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.
|
2017-09-18 18:55:58 +00:00
|
|
|
LOG.error(e)
|
2017-06-28 16:32:19 +00:00
|
|
|
|
2017-10-12 19:36:23 +00:00
|
|
|
def _get_remote_settings(self):
|
|
|
|
""" Get skill settings for this device from backend """
|
2017-10-13 22:34:22 +00:00
|
|
|
settings = self.api.request({
|
2017-06-28 16:32:19 +00:00
|
|
|
"method": "GET",
|
|
|
|
"path": self._api_path
|
|
|
|
})
|
2017-10-13 22:34:22 +00:00
|
|
|
settings = [skills for skills in settings if skills is not None]
|
|
|
|
return settings
|
2017-06-28 16:32:19 +00:00
|
|
|
|
2017-07-11 20:25:43 +00:00
|
|
|
def _put_metadata(self, settings_meta):
|
2017-10-12 17:35:50 +00:00
|
|
|
""" PUT settingsmeta to backend to be configured in home.mycroft.ai.
|
2017-07-11 20:25:43 +00:00
|
|
|
used in plcae of POST and PATCH
|
2017-06-28 22:31:35 +00:00
|
|
|
"""
|
2017-06-28 16:32:19 +00:00
|
|
|
return self.api.request({
|
2017-07-11 20:25:43 +00:00
|
|
|
"method": "PUT",
|
2017-06-28 16:32:19 +00:00
|
|
|
"path": self._api_path,
|
|
|
|
"json": settings_meta
|
|
|
|
})
|
|
|
|
|
2017-10-12 17:35:50 +00:00
|
|
|
def _delete_metatdata(self, uuid):
|
|
|
|
""" Deletes the current skill metadata
|
|
|
|
|
|
|
|
Args:
|
|
|
|
param1 (str): unique id of the skill
|
2017-06-28 22:31:35 +00:00
|
|
|
"""
|
2017-10-12 17:35:50 +00:00
|
|
|
return self.api.request({
|
|
|
|
"method": "DELETE",
|
2017-10-13 22:39:14 +00:00
|
|
|
"path": self._api_path + "/{}".format(uuid)
|
2017-10-12 17:35:50 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
def store(self, force=False):
|
|
|
|
""" Store dictionary to file if a change has occured.
|
2017-09-01 08:07:49 +00:00
|
|
|
|
|
|
|
Args:
|
|
|
|
force: Force write despite no change
|
2017-06-28 22:31:35 +00:00
|
|
|
"""
|
2017-09-01 08:07:49 +00:00
|
|
|
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))
|