Merge pull request #654 from MycroftAI/feature/issue-653

The configuration values were poorly documented.  See issue #653
pull/660/head
kfezer 2017-04-13 20:26:54 +00:00 committed by GitHub
commit 0036f404b5
6 changed files with 534 additions and 13 deletions

View File

@ -14,14 +14,15 @@
#
# You should have received a copy of the GNU General Public License
# along with Mycroft Core. If not, see <http://www.gnu.org/licenses/>.
import json
import json
import inflection
import re
from genericpath import exists, isfile
from os.path import join, dirname, expanduser
from mycroft.util.log import getLogger
from mycroft.util.json_helper import load_commented_json
__author__ = 'seanfitz, jdorleans'
@ -38,6 +39,25 @@ load_order = [DEFAULT_CONFIG, REMOTE_CONFIG, SYSTEM_CONFIG, USER_CONFIG]
class ConfigurationLoader(object):
"""
A utility for loading Mycroft configuration files.
Mycroft configuration comes from four potential locations:
* Defaults found in 'mycroft.conf' in the code
* Remote settings (coming from home.mycroft.ai)
* System settings (typically found at /etc/mycroft/mycroft.conf
* User settings (typically found at /home/<user>/.mycroft/mycroft.conf
These get loaded in that order on top of each other. So a value specified
in the Default would be overridden by a value with the same name found
in the Remote. And a value in the Remote would be overridden by a value
set in the User settings. Not all values exist at all levels.
See comments in the 'mycroft.conf' for more information about specific
settings and where they reside.
Note:
Values are overridden by name. This includes all data under that name,
so you if a value contains a complex structure, you cannot specify
only a single component of that structure -- you have to override the
entire structure.
"""
@staticmethod
@ -81,9 +101,8 @@ class ConfigurationLoader(object):
def __load(config, location):
if exists(location) and isfile(location):
try:
with open(location) as f:
config.update(json.load(f))
LOG.debug("Configuration '%s' loaded" % location)
config.update(load_commented_json(location))
LOG.debug("Configuration '%s' loaded" % location)
except Exception, e:
LOG.error("Error loading configuration '%s'" % location)
LOG.error(repr(e))
@ -129,6 +148,8 @@ class RemoteConfiguration(object):
def __load(config, setting):
for k, v in setting.iteritems():
if k not in RemoteConfiguration.IGNORED_SETTINGS:
# Translate the CamelCase values stored remotely into the
# Python-style names used within mycroft-core.
key = inflection.underscore(re.sub(r"Setting(s)?", "", k))
if isinstance(v, dict):
config[key] = config.get(key, {})
@ -150,14 +171,28 @@ class RemoteConfiguration(object):
class ConfigurationManager(object):
"""
Static management utility for calling up cached configuration.
Static management utility for accessing the cached configuration.
This configuration is periodically updated from the remote server
to keep in sync.
"""
__config = None
__listener = None
@staticmethod
def instance():
"""
The cached configuration.
Returns:
dict: A dictionary representing the Mycroft configuration
"""
return ConfigurationManager.get()
@staticmethod
def init(ws):
ConfigurationManager.__listener = ConfigurationListener(ws)
# Start listening for configuration update events on the messagebus
ConfigurationManager.__listener = _ConfigurationListener(ws)
@staticmethod
def load_defaults():
@ -186,7 +221,8 @@ class ConfigurationManager(object):
"""
Get cached configuration.
:return: A dictionary representing Mycroft configuration.
Returns:
dict: A dictionary representing the Mycroft configuration
"""
if not ConfigurationManager.__config:
ConfigurationManager.load_defaults()
@ -214,14 +250,21 @@ class ConfigurationManager(object):
"""
ConfigurationManager.update(config)
location = SYSTEM_CONFIG if is_system else USER_CONFIG
with open(location, 'rw') as f:
config = json.load(f).update(config)
loc_config = load_commented_json(location)
with open(location, 'w') as f:
config = loc_config.update(config)
json.dump(config, f)
class ConfigurationListener(object):
class _ConfigurationListener(object):
""" Utility to synchronize remote configuration changes locally
This listens to the messagebus for 'configuration.updated', and
refreshes the cached configuration when this is encountered.
"""
def __init__(self, ws):
super(ConfigurationListener, self).__init__()
super(_ConfigurationListener, self).__init__()
ws.on("configuration.updated", self.updated)
@staticmethod

View File

@ -1,15 +1,56 @@
{
// Definition and documentation of all variables used by mycroft-core.
//
// Settings seen here are considered DEFAULT. Settings can also be
// overridden at the REMOTE level (set by the user via
// https://home.mycroft.ai), at the SYSTEM level (typically in the file
// '/etc/mycroft/mycroft.conf'), or at the USER level (typically in the
// file '~/.mycroft/mycroft.conf').
//
// The Override: comment indicates at what level (if any) this is
// overridden by the system to a value besides the default shown here.
// Language used for speech-to-text and text-to-speech.
// Code is a BCP-47 identifier (https://tools.ietf.org/html/bcp47), lowercased
// TODO: save unmodified, lowercase upon demand
// Override: none
"lang": "en-us",
// Measurement units, either 'metric' or 'english'
// Override: REMOTE
"system_unit": "metric",
// Time format, either 'half' (e.g. "11:37 pm") or 'full' (e.g. "23:37")
// Override: REMOTE
"time_format": "half",
// Date format, either 'MDY' (e.g. "11-29-1978") or 'DMY' (e.g. "29-11-1978")
// Override: REMOTE
"date_format": "MDY",
// Play a beep when system begins to listen?
// Override: none
"confirm_listening": true,
// File locations of sounds to play for system events
// Override: none
"sounds": {
"start_listening": "snd/start_listening.wav",
"end_listening": "snd/end_listening.wav"
},
// Mechanism used to play WAV audio files
// Override: SYSTEM
"play_wav_cmdline": "aplay %1",
// Mechanism used to play MP3 audio files
// Override: SYSTEM
"play_mp3_cmdline": "mpg123 %1",
// Location where the system resides
// NOTE: Although this is set here, an Enclosure can override the value.
// For example a mycroft-core running in a car could use the GPS.
// Override: REMOTE
"location": {
"city": {
"code": "Lawrence",
@ -34,22 +75,36 @@
"offset": -21600000
}
},
// General skill values
// Override: none
"skills": {
// Directory to look for user skills
"directory": "~/.mycroft/skills",
// TODO: Old unused kludge, remove from code
"stop_threshold": 2.0
},
// Address of the REMOTE server
// Override: none
"server": {
"url": "https://api.mycroft.ai",
"version": "v1",
"update": true,
"metrics": false
},
// The mycroft-core messagebus' websocket
// Override: none
"websocket": {
"host": "0.0.0.0",
"port": 8181,
"route": "/core",
"ssl": false
},
// Settings used by the wake-up-word listener
// Override: REMOTE
"listener": {
"sample_rate": 16000,
"channels": 1,
@ -59,22 +114,42 @@
"multiplier": 1.0,
"energy_ratio": 1.5
},
// Mark 1 enclosure settings
// Override: SYSTEM (e.g. Picroft)
"enclosure": {
// Platform name (e.g. 'Picroft', 'Mark_1'
// Override: SYSTEM
# "platform": "picroft",
// COMM params to the Arduino/faceplate
"port": "/dev/ttyAMA0",
"rate": 9600,
"timeout": 5.0,
// ??
"update": true,
// Run a self test at bootup?
"test": false
},
"log_level": "DEBUG",
"ignore_logs": ["enclosure.mouth.viseme"],
"session": {
"ttl": 180
},
// Speech to Text parameters
// Override: REMOTE
"stt": {
// Engine. Options: "mycroft", "google", "wit", "ibm"
"module": "mycroft"
},
// Text to Speech parameters
// Override: REMOTE
"tts": {
// Engine. Options: "mimic", "google", "marytts", "fatts", "espeak", "spdsay"
"module": "mimic",
"mimic": {
"voice": "ap"
@ -84,6 +159,12 @@
"voice": "m1"
}
},
// =================================================================
// All of the follow are specific to particular skills and will soon
// be removed from this file.
// =================================================================
"wifi": {
"setup": false
},

View File

@ -0,0 +1,73 @@
# Copyright (c) 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/>.
import json
def load_commented_json(filename):
""" Loads an JSON file, ignoring comments
Supports a trivial extension to the JSON file format. Allow comments
to be embedded within the JSON, requiring that a comment be on an
independent line starting with '//' or '#'.
NOTE: A file created with these style comments will break strict JSON
parsers. This is similar to but lighter-weight than "human json"
proposed at https://hjson.org
Args:
filename (str): path to the commented JSON file
Returns:
obj: decoded Python object
"""
with open(filename) as f:
contents = f.read()
return json.loads(uncomment_json(contents))
def uncomment_json(commented_json_str):
""" Removes comments from a JSON string.
Supporting a trivial extension to the JSON format. Allow comments
to be embedded within the JSON, requiring that a comment be on an
independent line starting with '//' or '#'.
Example...
{
// comment
'name' : 'value'
}
Args:
commented_json_str (str): a JSON string
Returns:
str: uncommented, legal JSON
"""
lines = commented_json_str.splitlines()
# remove all comment lines, starting with // or #
nocomment = []
for line in lines:
stripped = line.lstrip()
if stripped.startswith("//") or stripped.startswith("#"):
continue
nocomment.append(line)
return " ".join(nocomment)

168
test/util/commented.json Normal file
View File

@ -0,0 +1,168 @@
// Leading comment
{
// C-style comment
"lang": "en-us",
// Comment spaced out
"system_unit": "metric",
// Comment tabbed out
"time_format": "half",
// comment and // another inside
"date_format": "MDY",
// Comment with " inside it
"confirm_listening": true,
# Python-style comment
"sounds": {
# Commment with no space
"start_listening": "snd/start_listening.wav",
# Comment inside with a tab
# and multiple lines
"end_listening": "snd/end_listening.wav"
// and a final thought
},
"play_wav_cmdline": "aplay %1",
"play_mp3_cmdline": "mpg123 %1",
"location": {
"city": {
"code": "Lawrence",
"name": "Lawrence",
"state": {
"code": "KS",
"name": "Kansas",
"country": {
"code": "US",
"name": "United States"
}
}
},
"coordinate": {
"latitude": 38.971669,
"longitude": -95.23525
},
"timezone": {
"code": "America/Chicago",
"name": "Central Standard Time",
"dstOffset": 3600000,
"offset": -21600000
}
},
"skills": {
"directory": "~/.mycroft/skills",
"stop_threshold": 2.0
},
"server": {
"url": "https://api.mycroft.ai",
"version": "v1",
"update": true,
"metrics": false
},
"websocket": {
"host": "0.0.0.0",
"port": 8181,
"route": "/core",
"ssl": false
},
"listener": {
"sample_rate": 16000,
"channels": 1,
"wake_word": "hey mycroft",
"phonemes": "HH EY . M AY K R AO F T",
"threshold": 1e-90,
"multiplier": 1.0,
"energy_ratio": 1.5
},
"enclosure": {
"port": "/dev/ttyAMA0",
"rate": 9600,
"timeout": 5.0,
"update": true,
"test": false
},
"log_level": "DEBUG",
"ignore_logs": ["enclosure.mouth.viseme"],
"session": {
"ttl": 180
},
"stt": {
"module": "mycroft"
},
"tts": {
"module": "mimic",
"mimic": {
"voice": "ap"
},
"espeak": {
"lang": "english-us",
"voice": "m1"
}
},
// Mixture
# of types
// of comments
"wifi": {
"setup": false
},
"ConfigurationSkill": {
"max_delay": 60
},
"WikipediaSkill": {
"max_results": 5,
"max_phrases": 2
},
"WolframAlphaSkill": {
"api_key": "",
"proxy": true
},
"WeatherSkill": {
"api_key": "",
"proxy": true,
"temperature": "fahrenheit"
},
"NPRNewsSkill": {
"url_rss": "http://www.npr.org/rss/podcast.php?id=500005"
},
"AlarmSkill": {
"filename": "alarm.mp3",
"max_delay": 600,
"repeat_time": 20,
"extended_delay": 60
},
"ReminderSkill": {
"max_delay": 600,
"repeat_time": 60,
"extended_delay": 60
},
"VolumeSkill": {
"default_level": 6,
"min_volume": 0,
"max_volume": 100
},
"AudioRecordSkill": {
"filename": "/tmp/mycroft-recording.wav",
"free_disk": 100,
"max_time": 600,
"notify_delay": 5,
"rate": 16000,
"channels": 1
},
"SkillInstallerSkill": {
}
}
# Trailing comments
# These go on
# and on
// and on

135
test/util/plain.json Normal file
View File

@ -0,0 +1,135 @@
{
"lang": "en-us",
"system_unit": "metric",
"time_format": "half",
"date_format": "MDY",
"confirm_listening": true,
"sounds": {
"start_listening": "snd/start_listening.wav",
"end_listening": "snd/end_listening.wav"
},
"play_wav_cmdline": "aplay %1",
"play_mp3_cmdline": "mpg123 %1",
"location": {
"city": {
"code": "Lawrence",
"name": "Lawrence",
"state": {
"code": "KS",
"name": "Kansas",
"country": {
"code": "US",
"name": "United States"
}
}
},
"coordinate": {
"latitude": 38.971669,
"longitude": -95.23525
},
"timezone": {
"code": "America/Chicago",
"name": "Central Standard Time",
"dstOffset": 3600000,
"offset": -21600000
}
},
"skills": {
"directory": "~/.mycroft/skills",
"stop_threshold": 2.0
},
"server": {
"url": "https://api.mycroft.ai",
"version": "v1",
"update": true,
"metrics": false
},
"websocket": {
"host": "0.0.0.0",
"port": 8181,
"route": "/core",
"ssl": false
},
"listener": {
"sample_rate": 16000,
"channels": 1,
"wake_word": "hey mycroft",
"phonemes": "HH EY . M AY K R AO F T",
"threshold": 1e-90,
"multiplier": 1.0,
"energy_ratio": 1.5
},
"enclosure": {
"port": "/dev/ttyAMA0",
"rate": 9600,
"timeout": 5.0,
"update": true,
"test": false
},
"log_level": "DEBUG",
"ignore_logs": ["enclosure.mouth.viseme"],
"session": {
"ttl": 180
},
"stt": {
"module": "mycroft"
},
"tts": {
"module": "mimic",
"mimic": {
"voice": "ap"
},
"espeak": {
"lang": "english-us",
"voice": "m1"
}
},
"wifi": {
"setup": false
},
"ConfigurationSkill": {
"max_delay": 60
},
"WikipediaSkill": {
"max_results": 5,
"max_phrases": 2
},
"WolframAlphaSkill": {
"api_key": "",
"proxy": true
},
"WeatherSkill": {
"api_key": "",
"proxy": true,
"temperature": "fahrenheit"
},
"NPRNewsSkill": {
"url_rss": "http://www.npr.org/rss/podcast.php?id=500005"
},
"AlarmSkill": {
"filename": "alarm.mp3",
"max_delay": 600,
"repeat_time": 20,
"extended_delay": 60
},
"ReminderSkill": {
"max_delay": 600,
"repeat_time": 60,
"extended_delay": 60
},
"VolumeSkill": {
"default_level": 6,
"min_volume": 0,
"max_volume": 100
},
"AudioRecordSkill": {
"filename": "/tmp/mycroft-recording.wav",
"free_disk": 100,
"max_time": 600,
"notify_delay": 5,
"rate": 16000,
"channels": 1
},
"SkillInstallerSkill": {
}
}

View File

@ -0,0 +1,21 @@
import unittest
import json
from mycroft.util.json_helper import load_commented_json, uncomment_json
class TestFileLoad(unittest.TestCase):
def test_load(self):
# Load normal JSON file
with open('plain.json', 'rw') as f:
data_from_plain = json.load(f)
# Load commented JSON file
data_from_commented = load_commented_json('commented.json')
# Should be the same...
self.assertEqual(data_from_commented, data_from_plain)
if __name__ == "__main__":
unittest.main()