Merge pull request #108 from balloob/chore-improve-config

Refactor config files into separate module
This commit is contained in:
Paulus Schoutsen 2015-04-26 10:31:59 -07:00
commit cb7b0d8a4f
7 changed files with 395 additions and 82 deletions

View File

@ -15,8 +15,6 @@ import re
import datetime as dt import datetime as dt
import functools as ft import functools as ft
import requests
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED,
@ -898,41 +896,6 @@ class Config(object):
# Directory that holds the configuration # Directory that holds the configuration
self.config_dir = os.path.join(os.getcwd(), 'config') self.config_dir = os.path.join(os.getcwd(), 'config')
def auto_detect(self):
""" Will attempt to detect config of Home Assistant. """
# Only detect if location or temp unit missing
if None not in (self.latitude, self.longitude, self.temperature_unit):
return
_LOGGER.info('Auto detecting location and temperature unit')
try:
info = requests.get(
'https://freegeoip.net/json/', timeout=5).json()
except requests.RequestException:
return
if self.latitude is None and self.longitude is None:
self.latitude = info['latitude']
self.longitude = info['longitude']
if self.temperature_unit is None:
# From Wikipedia:
# Fahrenheit is used in the Bahamas, Belize, the Cayman Islands,
# Palau, and the United States and associated territories of
# American Samoa and the U.S. Virgin Islands
if info['country_code'] in ('BS', 'BZ', 'KY', 'PW',
'US', 'AS', 'VI'):
self.temperature_unit = TEMP_FAHRENHEIT
else:
self.temperature_unit = TEMP_CELCIUS
if self.location_name is None:
self.location_name = info['city']
if self.time_zone is None:
self.time_zone = info['time_zone']
def path(self, path): def path(self, path):
""" Returns path to the file within the config dir. """ """ Returns path to the file within the config dir. """
return os.path.join(self.config_dir, path) return os.path.join(self.config_dir, path)

View File

@ -72,24 +72,13 @@ def ensure_config_path(config_dir):
'directory {} ').format(config_dir)) 'directory {} ').format(config_dir))
sys.exit() sys.exit()
# Try to use yaml configuration first import homeassistant.config as config_util
config_path = os.path.join(config_dir, 'configuration.yaml')
if not os.path.isfile(config_path):
config_path = os.path.join(config_dir, 'home-assistant.conf')
# Ensure a config file exists to make first time usage easier config_path = config_util.ensure_config_exists(config_dir)
if not os.path.isfile(config_path):
config_path = os.path.join(config_dir, 'configuration.yaml') if config_path is None:
try: print('Error getting configuration path')
with open(config_path, 'w') as conf: sys.exit()
conf.write("frontend:\n\n")
conf.write("discovery:\n\n")
conf.write("history:\n\n")
conf.write("logbook:\n\n")
except IOError:
print(('Fatal Error: No configuration file found and unable '
'to write a default one to {}').format(config_path))
sys.exit()
return config_path return config_path

View File

@ -10,13 +10,12 @@ start by calling homeassistant.start_home_assistant(bus)
""" """
import os import os
import configparser
import yaml
import io
import logging import logging
from collections import defaultdict from collections import defaultdict
import homeassistant import homeassistant
import homeassistant.util as util
import homeassistant.config as config_util
import homeassistant.loader as loader import homeassistant.loader as loader
import homeassistant.components as core_components import homeassistant.components as core_components
import homeassistant.components.group as group import homeassistant.components.group as group
@ -148,26 +147,7 @@ def from_config_file(config_path, hass=None):
# Set config dir to directory holding config file # Set config dir to directory holding config file
hass.config.config_dir = os.path.abspath(os.path.dirname(config_path)) hass.config.config_dir = os.path.abspath(os.path.dirname(config_path))
config_dict = {} config_dict = config_util.load_config_file(config_path)
# check config file type
if os.path.splitext(config_path)[1] == '.yaml':
# Read yaml
config_dict = yaml.load(io.open(config_path, 'r'))
# If YAML file was empty
if config_dict is None:
config_dict = {}
else:
# Read config
config = configparser.ConfigParser()
config.read(config_path)
for section in config.sections():
config_dict[section] = {}
for key, val in config.items(section):
config_dict[section][key] = val
return from_config_dict(config_dict, hass) return from_config_dict(config_dict, hass)
@ -201,12 +181,14 @@ def enable_logging(hass):
def process_ha_core_config(hass, config): def process_ha_core_config(hass, config):
""" Processes the [homeassistant] section from the config. """ """ Processes the [homeassistant] section from the config. """
hac = hass.config
for key, attr in ((CONF_LATITUDE, 'latitude'), for key, attr in ((CONF_LATITUDE, 'latitude'),
(CONF_LONGITUDE, 'longitude'), (CONF_LONGITUDE, 'longitude'),
(CONF_NAME, 'location_name'), (CONF_NAME, 'location_name'),
(CONF_TIME_ZONE, 'time_zone')): (CONF_TIME_ZONE, 'time_zone')):
if key in config: if key in config:
setattr(hass.config, attr, config[key]) setattr(hac, attr, config[key])
for entity_id, attrs in config.get(CONF_CUSTOMIZE, {}).items(): for entity_id, attrs in config.get(CONF_CUSTOMIZE, {}).items():
Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values()) Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values())
@ -215,11 +197,37 @@ def process_ha_core_config(hass, config):
unit = config[CONF_TEMPERATURE_UNIT] unit = config[CONF_TEMPERATURE_UNIT]
if unit == 'C': if unit == 'C':
hass.config.temperature_unit = TEMP_CELCIUS hac.temperature_unit = TEMP_CELCIUS
elif unit == 'F': elif unit == 'F':
hass.config.temperature_unit = TEMP_FAHRENHEIT hac.temperature_unit = TEMP_FAHRENHEIT
hass.config.auto_detect() # If we miss some of the needed values, auto detect them
if None not in (hac.latitude, hac.longitude, hac.temperature_unit):
return
_LOGGER.info('Auto detecting location and temperature unit')
info = util.detect_location_info()
if info is None:
_LOGGER.error('Could not detect location information')
return
if hac.latitude is None and hac.longitude is None:
hac.latitude = info.latitude
hac.longitude = info.longitude
if hac.temperature_unit is None:
if info.use_fahrenheit:
hac.temperature_unit = TEMP_FAHRENHEIT
else:
hac.temperature_unit = TEMP_CELCIUS
if hac.location_name is None:
hac.location_name = info.city
if hac.time_zone is None:
hac.time_zone = info.time_zone
def _ensure_loader_prepared(hass): def _ensure_loader_prepared(hass):

140
homeassistant/config.py Normal file
View File

@ -0,0 +1,140 @@
"""
homeassistant.config
~~~~~~~~~~~~~~~~~~~~
Module to help with parsing and generating configuration files.
"""
import logging
import os
from homeassistant import HomeAssistantError
from homeassistant.const import (
CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME,
CONF_TIME_ZONE)
import homeassistant.util as util
_LOGGER = logging.getLogger(__name__)
YAML_CONFIG_FILE = 'configuration.yaml'
CONF_CONFIG_FILE = 'home-assistant.conf'
DEFAULT_COMPONENTS = [
'discovery', 'frontend', 'conversation', 'history', 'logbook']
def ensure_config_exists(config_dir, detect_location=True):
""" Ensures a config file exists in given config dir.
Creating a default one if needed.
Returns path to the config file. """
config_path = find_config_file(config_dir)
if config_path is None:
_LOGGER.info("Unable to find configuration. Creating default one")
config_path = create_default_config(config_dir, detect_location)
return config_path
def create_default_config(config_dir, detect_location=True):
""" Creates a default configuration file in given config dir.
Returns path to new config file if success, None if failed. """
config_path = os.path.join(config_dir, YAML_CONFIG_FILE)
# Writing files with YAML does not create the most human readable results
# So we're hard coding a YAML template.
try:
with open(config_path, 'w') as config_file:
location_info = detect_location and util.detect_location_info()
if location_info:
temp_unit = 'F' if location_info.use_fahrenheit else 'C'
auto_config = {
CONF_NAME: 'Home',
CONF_LATITUDE: location_info.latitude,
CONF_LONGITUDE: location_info.longitude,
CONF_TEMPERATURE_UNIT: temp_unit,
CONF_TIME_ZONE: location_info.time_zone,
}
config_file.write("homeassistant:\n")
for key, value in auto_config.items():
config_file.write(" {}: {}\n".format(key, value))
config_file.write("\n")
for component in DEFAULT_COMPONENTS:
config_file.write("{}:\n\n".format(component))
return config_path
except IOError:
_LOGGER.exception(
'Unable to write default configuration file %s', config_path)
return None
def find_config_file(config_dir):
""" Looks in given directory for supported config files. """
for filename in (YAML_CONFIG_FILE, CONF_CONFIG_FILE):
config_path = os.path.join(config_dir, filename)
if os.path.isfile(config_path):
return config_path
return None
def load_config_file(config_path):
""" Loads given config file. """
config_ext = os.path.splitext(config_path)[1]
if config_ext == '.yaml':
return load_yaml_config_file(config_path)
elif config_ext == '.conf':
return load_conf_config_file(config_path)
def load_yaml_config_file(config_path):
""" Parse a YAML configuration file. """
import yaml
try:
with open(config_path) as conf_file:
# If configuration file is empty YAML returns None
# We convert that to an empty dict
conf_dict = yaml.load(conf_file) or {}
except yaml.YAMLError:
_LOGGER.exception('Error reading YAML configuration file')
raise HomeAssistantError()
if not isinstance(conf_dict, dict):
_LOGGER.error(
'The configuration file %s does not contain a dictionary',
os.path.basename(config_path))
raise HomeAssistantError()
return conf_dict
def load_conf_config_file(config_path):
""" Parse the old style conf configuration. """
import configparser
config_dict = {}
config = configparser.ConfigParser()
config.read(config_path)
for section in config.sections():
config_dict[section] = {}
for key, val in config.items(section):
config_dict[section][key] = val
return config_dict

View File

@ -16,6 +16,8 @@ import random
import string import string
from functools import wraps from functools import wraps
import requests
RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)') RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)')
RE_SANITIZE_PATH = re.compile(r'(~|\.(\.)+)') RE_SANITIZE_PATH = re.compile(r'(~|\.(\.)+)')
RE_SLUGIFY = re.compile(r'[^A-Za-z0-9_]+') RE_SLUGIFY = re.compile(r'[^A-Za-z0-9_]+')
@ -174,6 +176,32 @@ def get_random_string(length=10):
return ''.join(generator.choice(source_chars) for _ in range(length)) return ''.join(generator.choice(source_chars) for _ in range(length))
LocationInfo = collections.namedtuple(
"LocationInfo",
['ip', 'country_code', 'country_name', 'region_code', 'region_name',
'city', 'zip_code', 'time_zone', 'latitude', 'longitude',
'use_fahrenheit'])
def detect_location_info():
""" Detect location information. """
try:
raw_info = requests.get(
'https://freegeoip.net/json/', timeout=5).json()
except requests.RequestException:
return
data = {key: raw_info.get(key) for key in LocationInfo._fields}
# From Wikipedia: Fahrenheit is used in the Bahamas, Belize,
# the Cayman Islands, Palau, and the United States and associated
# territories of American Samoa and the U.S. Virgin Islands
data['use_fahrenheit'] = data['country_code'] in (
'BS', 'BZ', 'KY', 'PW', 'US', 'AS', 'VI')
return LocationInfo(**data)
class OrderedEnum(enum.Enum): class OrderedEnum(enum.Enum):
""" Taken from Python 3.4.0 docs. """ """ Taken from Python 3.4.0 docs. """
# pylint: disable=no-init, too-few-public-methods # pylint: disable=no-init, too-few-public-methods

View File

@ -11,10 +11,15 @@ from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
def get_test_config_dir():
""" Returns a path to a test config dir. """
return os.path.join(os.path.dirname(__file__), "config")
def get_test_home_assistant(): def get_test_home_assistant():
""" Returns a Home Assistant object pointing at test config dir. """ """ Returns a Home Assistant object pointing at test config dir. """
hass = ha.HomeAssistant() hass = ha.HomeAssistant()
hass.config.config_dir = os.path.join(os.path.dirname(__file__), "config") hass.config.config_dir = get_test_config_dir()
return hass return hass

180
tests/test_config.py Normal file
View File

@ -0,0 +1,180 @@
"""
tests.test_config
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests config utils.
"""
# pylint: disable=too-many-public-methods,protected-access
import unittest
import unittest.mock as mock
import os
from homeassistant import DOMAIN, HomeAssistantError
import homeassistant.util as util
import homeassistant.config as config_util
from homeassistant.const import (
CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME,
CONF_TIME_ZONE)
from helpers import get_test_config_dir
CONFIG_DIR = get_test_config_dir()
YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE)
CONF_PATH = os.path.join(CONFIG_DIR, config_util.CONF_CONFIG_FILE)
def create_file(path):
""" Creates an empty file. """
with open(path, 'w'):
pass
def mock_detect_location_info():
""" Mock implementation of util.detect_location_info. """
return util.LocationInfo(
ip='1.1.1.1',
country_code='US',
country_name='United States',
region_code='CA',
region_name='California',
city='San Diego',
zip_code='92122',
time_zone='America/Los_Angeles',
latitude='2.0',
longitude='1.0',
use_fahrenheit=True,
)
class TestConfig(unittest.TestCase):
""" Test the config utils. """
def tearDown(self): # pylint: disable=invalid-name
""" Clean up. """
for path in (YAML_PATH, CONF_PATH):
if os.path.isfile(path):
os.remove(path)
def test_create_default_config(self):
""" Test creationg of default config. """
config_util.create_default_config(CONFIG_DIR, False)
self.assertTrue(os.path.isfile(YAML_PATH))
def test_find_config_file_yaml(self):
""" Test if it finds a YAML config file. """
create_file(YAML_PATH)
self.assertEqual(YAML_PATH, config_util.find_config_file(CONFIG_DIR))
def test_find_config_file_conf(self):
""" Test if it finds the old CONF config file. """
create_file(CONF_PATH)
self.assertEqual(CONF_PATH, config_util.find_config_file(CONFIG_DIR))
def test_find_config_file_prefers_yaml_over_conf(self):
""" Test if find config prefers YAML over CONF if both exist. """
create_file(YAML_PATH)
create_file(CONF_PATH)
self.assertEqual(YAML_PATH, config_util.find_config_file(CONFIG_DIR))
def test_ensure_config_exists_creates_config(self):
""" Test that calling ensure_config_exists creates a new config file if
none exists. """
config_util.ensure_config_exists(CONFIG_DIR, False)
self.assertTrue(os.path.isfile(YAML_PATH))
def test_ensure_config_exists_uses_existing_config(self):
""" Test that calling ensure_config_exists uses existing config. """
create_file(YAML_PATH)
config_util.ensure_config_exists(CONFIG_DIR, False)
with open(YAML_PATH) as f:
content = f.read()
# File created with create_file are empty
self.assertEqual('', content)
def test_load_yaml_config_converts_empty_files_to_dict(self):
""" Test that loading an empty file returns an empty dict. """
create_file(YAML_PATH)
self.assertIsInstance(
config_util.load_yaml_config_file(YAML_PATH), dict)
def test_load_yaml_config_raises_error_if_not_dict(self):
""" Test error raised when YAML file is not a dict. """
with open(YAML_PATH, 'w') as f:
f.write('5')
with self.assertRaises(HomeAssistantError):
config_util.load_yaml_config_file(YAML_PATH)
def test_load_yaml_config_raises_error_if_malformed_yaml(self):
""" Test error raised if invalid YAML. """
with open(YAML_PATH, 'w') as f:
f.write(':')
with self.assertRaises(HomeAssistantError):
config_util.load_yaml_config_file(YAML_PATH)
def test_load_config_loads_yaml_config(self):
""" Test correct YAML config loading. """
with open(YAML_PATH, 'w') as f:
f.write('hello: world')
self.assertEqual({'hello': 'world'},
config_util.load_config_file(YAML_PATH))
def test_load_config_loads_conf_config(self):
""" Test correct YAML config loading. """
create_file(CONF_PATH)
self.assertEqual({}, config_util.load_config_file(CONF_PATH))
def test_conf_config_file(self):
""" Test correct CONF config loading. """
with open(CONF_PATH, 'w') as f:
f.write('[ha]\ntime_zone=America/Los_Angeles')
self.assertEqual({'ha': {'time_zone': 'America/Los_Angeles'}},
config_util.load_conf_config_file(CONF_PATH))
def test_create_default_config_detect_location(self):
""" Test that detect location sets the correct config keys. """
with mock.patch('homeassistant.util.detect_location_info',
mock_detect_location_info):
config_util.ensure_config_exists(CONFIG_DIR)
config = config_util.load_config_file(YAML_PATH)
self.assertIn(DOMAIN, config)
ha_conf = config[DOMAIN]
expected_values = {
CONF_LATITUDE: 2.0,
CONF_LONGITUDE: 1.0,
CONF_TEMPERATURE_UNIT: 'F',
CONF_NAME: 'Home',
CONF_TIME_ZONE: 'America/Los_Angeles'
}
self.assertEqual(expected_values, ha_conf)
def test_create_default_config_returns_none_if_write_error(self):
"""
Test that writing default config to non existing folder returns None.
"""
self.assertIsNone(
config_util.create_default_config(
os.path.join(CONFIG_DIR, 'non_existing_dir/'), False))