mycroft-core/mycroft/skills/settings.py

195 lines
6.3 KiB
Python

# Copyright 2017 Mycroft AI, Inc.
#
# This file is part of Mycroft Core.
#
# Mycroft Core is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Mycroft Core is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Mycroft Core. If not, see <http://www.gnu.org/licenses/>.
"""
This module provides the SkillSettings dictionary, which is a simple
extension of the python dict to enable storing.
Example:
from mycroft.skill.settings import SkillSettings
s = SkillSettings('./settings.json')
s['meaning of life'] = 42
s['flower pot sayings'] = 'Not again...'
s.store()
"""
import json
import sys
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. It also syncs to the backend for
skill settings
Args:
settings_file (str): Path to storage file
"""
def __init__(self, directory):
super(SkillSettings, self).__init__()
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
def __getitem__(self, key):
return super(SkillSettings, self).__getitem__(key)
def __setitem__(self, key, value):
"""
Add/Update key.
"""
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
self.store()
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, force=False):
"""
Store dictionary to file if a change has occured.
Args:
force: Force write despite no change
"""
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))