mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 17:57:55 +00:00

Add unit symbol constants Initial unit system object Import more constants Pydoc for unit system file Import constants for configuration validation Unit system validation method Typing for constants Inches are valid lengths too Typings Change base class to dict - needed for remote api call serialization Validation Use dictionary keys Defined unit systems Update location util to use metric instead of us fahrenheit Update constant imports Import defined unit systems Update configuration to use unit system Update schema to use unit system Update constants Add imports to core for unit system and distance Type for config Default unit system Convert distance from HASS instance Update temperature conversion to use unit system Update temperature conversion Set unit system based on configuration Set info unit system Return unit system dictionary with config dictionary Auto discover unit system Update location test for use metric Update forecast unit system Update mold indicator unit system Update thermostat unit system Update thermostat demo test Unit tests around unit system Update test common hass configuration Update configuration unit tests There should always be a unit system! Update core unit tests Constants typing Linting issues Remove unused import Update fitbit sensor to use application unit system Update google travel time to use application unit system Update configuration example Update dht sensor Update DHT temperature conversion to use the utility function Update swagger config Update my sensors metric flag Update hvac component temperature conversion HVAC conversion for temperature Pull unit from sensor type map Pull unit from sensor type map Update the temper sensor unit Update yWeather sensor unit Update hvac demo unit test Set unit test config unit system to metric Use hass unit system length for default in proximity Use the name of the system instead of temperature Use constants from const Unused import Forecasted temperature Fix calculation in case furthest distance is greater than 1000000 units Remove unneeded constants Set default length to km or miles Use constants Linting doesn't like importing just for typing Fix reference Test is expecting meters - set config to meters Use constant Use constant PyDoc for unit test Should be not in Rename to units Change unit system to be an object - not a dictionary Return tuple in conversion Move convert to temperature util Temperature conversion is now in unit system Update imports Rename to units Units is now an object Use temperature util conversion Unit system is now an object Validate and convert unit system config Return the scalar value in template distance Test is expecting meters Update unit tests around unit system Distance util returns tuple Fix location info test Set units Update unit tests Convert distance DOH Pull out the scalar from the vector Linting I really hate python linting Linting again BLARG Unit test documentation Unit test around is metric flag Break ternary statement into if/else blocks Don't use dictionary - use members is metric flag Rename constants Use is metric flag Move constants to CONST file Move to const file Raise error if unit is not expected Typing No need to return unit since only performing conversion if it can work Use constants Line wrapping Raise error if invalid value Remove subscripts from conversion as they are no longer returned as tuples No longer tuples No longer tuples Check for numeric type Fix string format to use correct variable Typing Assert errors raised Remove subscript Only convert temperature if we know the unit If no unit of measurement set - default to HASS config Convert only if we know the unit Remove subscription Fix not in clause Linting fixes Wants a boolean Clearer if-block Check if the key is in the config first Missed a couple expecting tuples Backwards compatibility No like-y ternary! Error handling around state setting Pretty unit system configuration validation More tuple crap Use is metric flag Error handling around min/max temp Explode if no unit Pull unit from config Celsius has a decimal Unused import Check if it's a temperature before we try to convert it to a temperature Linting says too many statements - combine lat/long in a fairly reasonable manner Backwards compatibility unit test Better doc
312 lines
10 KiB
Python
312 lines
10 KiB
Python
"""Module to help with parsing and generating configuration files."""
|
|
import logging
|
|
import os
|
|
import shutil
|
|
from types import MappingProxyType
|
|
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.const import (
|
|
CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_UNIT_SYSTEM,
|
|
CONF_TIME_ZONE, CONF_CUSTOMIZE, CONF_ELEVATION, CONF_UNIT_SYSTEM_METRIC,
|
|
CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS,
|
|
__version__)
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
from homeassistant.util.yaml import load_yaml
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.unit_system import (IMPERIAL_SYSTEM, METRIC_SYSTEM)
|
|
from homeassistant.helpers.entity import valid_entity_id, set_customize
|
|
from homeassistant.util import dt as date_util, location as loc_util
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
YAML_CONFIG_FILE = 'configuration.yaml'
|
|
VERSION_FILE = '.HA_VERSION'
|
|
CONFIG_DIR_NAME = '.homeassistant'
|
|
|
|
DEFAULT_CORE_CONFIG = (
|
|
# Tuples (attribute, default, auto detect property, description)
|
|
(CONF_NAME, 'Home', None, 'Name of the location where Home Assistant is '
|
|
'running'),
|
|
(CONF_LATITUDE, 0, 'latitude', 'Location required to calculate the time'
|
|
' the sun rises and sets'),
|
|
(CONF_LONGITUDE, 0, 'longitude', None),
|
|
(CONF_ELEVATION, 0, None, 'Impacts weather/sunrise data'),
|
|
(CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_METRIC, None,
|
|
'{} for Metric, {} for Imperial'.format(CONF_UNIT_SYSTEM_METRIC,
|
|
CONF_UNIT_SYSTEM_IMPERIAL)),
|
|
(CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki'
|
|
'pedia.org/wiki/List_of_tz_database_time_zones'),
|
|
)
|
|
DEFAULT_CONFIG = """
|
|
# Show links to resources in log and frontend
|
|
introduction:
|
|
|
|
# Enables the frontend
|
|
frontend:
|
|
|
|
http:
|
|
# Uncomment this to add a password (recommended!)
|
|
# api_password: PASSWORD
|
|
|
|
# Checks for available updates
|
|
updater:
|
|
|
|
# Discover some devices automatically
|
|
discovery:
|
|
|
|
# Allows you to issue voice commands from the frontend in enabled browsers
|
|
conversation:
|
|
|
|
# Enables support for tracking state changes over time.
|
|
history:
|
|
|
|
# View all events in a logbook
|
|
logbook:
|
|
|
|
# Track the sun
|
|
sun:
|
|
|
|
# Weather Prediction
|
|
sensor:
|
|
platform: yr
|
|
"""
|
|
|
|
|
|
def _valid_customize(value):
|
|
"""Config validator for customize."""
|
|
if not isinstance(value, dict):
|
|
raise vol.Invalid('Expected dictionary')
|
|
|
|
for key, val in value.items():
|
|
if not valid_entity_id(key):
|
|
raise vol.Invalid('Invalid entity ID: {}'.format(key))
|
|
|
|
if not isinstance(val, dict):
|
|
raise vol.Invalid('Value of {} is not a dictionary'.format(key))
|
|
|
|
return value
|
|
|
|
CORE_CONFIG_SCHEMA = vol.Schema({
|
|
CONF_NAME: vol.Coerce(str),
|
|
CONF_LATITUDE: cv.latitude,
|
|
CONF_LONGITUDE: cv.longitude,
|
|
CONF_ELEVATION: vol.Coerce(int),
|
|
vol.Optional(CONF_TEMPERATURE_UNIT): cv.temperature_unit,
|
|
CONF_UNIT_SYSTEM: cv.unit_system,
|
|
CONF_TIME_ZONE: cv.time_zone,
|
|
vol.Required(CONF_CUSTOMIZE,
|
|
default=MappingProxyType({})): _valid_customize,
|
|
})
|
|
|
|
|
|
def get_default_config_dir() -> str:
|
|
"""Put together the default configuration directory based on OS."""
|
|
data_dir = os.getenv('APPDATA') if os.name == "nt" \
|
|
else os.path.expanduser('~')
|
|
return os.path.join(data_dir, CONFIG_DIR_NAME)
|
|
|
|
|
|
def ensure_config_exists(config_dir: str, detect_location: bool=True) -> str:
|
|
"""Ensure a config file exists in given configuration directory.
|
|
|
|
Creating a default one if needed.
|
|
Return path to the config file.
|
|
"""
|
|
config_path = find_config_file(config_dir)
|
|
|
|
if config_path is None:
|
|
print("Unable to find configuration. Creating default one in",
|
|
config_dir)
|
|
config_path = create_default_config(config_dir, detect_location)
|
|
|
|
return config_path
|
|
|
|
|
|
def create_default_config(config_dir, detect_location=True):
|
|
"""Create a default configuration file in given configuration directory.
|
|
|
|
Return path to new config file if success, None if failed.
|
|
"""
|
|
config_path = os.path.join(config_dir, YAML_CONFIG_FILE)
|
|
version_path = os.path.join(config_dir, VERSION_FILE)
|
|
|
|
info = {attr: default for attr, default, _, _ in DEFAULT_CORE_CONFIG}
|
|
|
|
location_info = detect_location and loc_util.detect_location_info()
|
|
|
|
if location_info:
|
|
if location_info.use_metric:
|
|
info[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_METRIC
|
|
else:
|
|
info[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_IMPERIAL
|
|
|
|
for attr, default, prop, _ in DEFAULT_CORE_CONFIG:
|
|
if prop is None:
|
|
continue
|
|
info[attr] = getattr(location_info, prop) or default
|
|
|
|
if location_info.latitude and location_info.longitude:
|
|
info[CONF_ELEVATION] = loc_util.elevation(location_info.latitude,
|
|
location_info.longitude)
|
|
|
|
# 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:
|
|
config_file.write("homeassistant:\n")
|
|
|
|
for attr, _, _, description in DEFAULT_CORE_CONFIG:
|
|
if info[attr] is None:
|
|
continue
|
|
elif description:
|
|
config_file.write(" # {}\n".format(description))
|
|
config_file.write(" {}: {}\n".format(attr, info[attr]))
|
|
|
|
config_file.write(DEFAULT_CONFIG)
|
|
|
|
with open(version_path, 'wt') as version_file:
|
|
version_file.write(__version__)
|
|
|
|
return config_path
|
|
|
|
except IOError:
|
|
print('Unable to create default configuration file', config_path)
|
|
return None
|
|
|
|
|
|
def find_config_file(config_dir):
|
|
"""Look in given directory for supported configuration files."""
|
|
config_path = os.path.join(config_dir, YAML_CONFIG_FILE)
|
|
|
|
return config_path if os.path.isfile(config_path) else None
|
|
|
|
|
|
def load_yaml_config_file(config_path):
|
|
"""Parse a YAML configuration file."""
|
|
conf_dict = load_yaml(config_path)
|
|
|
|
if not isinstance(conf_dict, dict):
|
|
msg = 'The configuration file {} does not contain a dictionary'.format(
|
|
os.path.basename(config_path))
|
|
_LOGGER.error(msg)
|
|
raise HomeAssistantError(msg)
|
|
|
|
return conf_dict
|
|
|
|
|
|
def process_ha_config_upgrade(hass):
|
|
"""Upgrade config if necessary."""
|
|
version_path = hass.config.path(VERSION_FILE)
|
|
|
|
try:
|
|
with open(version_path, 'rt') as inp:
|
|
conf_version = inp.readline().strip()
|
|
except FileNotFoundError:
|
|
# Last version to not have this file
|
|
conf_version = '0.7.7'
|
|
|
|
if conf_version == __version__:
|
|
return
|
|
|
|
_LOGGER.info('Upgrading config directory from %s to %s', conf_version,
|
|
__version__)
|
|
|
|
lib_path = hass.config.path('deps')
|
|
if os.path.isdir(lib_path):
|
|
shutil.rmtree(lib_path)
|
|
|
|
with open(version_path, 'wt') as outp:
|
|
outp.write(__version__)
|
|
|
|
|
|
def process_ha_core_config(hass, config):
|
|
"""Process the [homeassistant] section from the config."""
|
|
# pylint: disable=too-many-branches
|
|
config = CORE_CONFIG_SCHEMA(config)
|
|
hac = hass.config
|
|
|
|
def set_time_zone(time_zone_str):
|
|
"""Helper method to set time zone."""
|
|
if time_zone_str is None:
|
|
return
|
|
|
|
time_zone = date_util.get_time_zone(time_zone_str)
|
|
|
|
if time_zone:
|
|
hac.time_zone = time_zone
|
|
date_util.set_default_time_zone(time_zone)
|
|
else:
|
|
_LOGGER.error('Received invalid time zone %s', time_zone_str)
|
|
|
|
for key, attr in ((CONF_LATITUDE, 'latitude'),
|
|
(CONF_LONGITUDE, 'longitude'),
|
|
(CONF_NAME, 'location_name'),
|
|
(CONF_ELEVATION, 'elevation')):
|
|
if key in config:
|
|
setattr(hac, attr, config[key])
|
|
|
|
if CONF_TIME_ZONE in config:
|
|
set_time_zone(config.get(CONF_TIME_ZONE))
|
|
|
|
set_customize(config.get(CONF_CUSTOMIZE) or {})
|
|
|
|
if CONF_UNIT_SYSTEM in config:
|
|
if config[CONF_UNIT_SYSTEM] == CONF_UNIT_SYSTEM_IMPERIAL:
|
|
hac.units = IMPERIAL_SYSTEM
|
|
else:
|
|
hac.units = METRIC_SYSTEM
|
|
elif CONF_TEMPERATURE_UNIT in config:
|
|
unit = config[CONF_TEMPERATURE_UNIT]
|
|
if unit == TEMP_CELSIUS:
|
|
hac.units = METRIC_SYSTEM
|
|
else:
|
|
hac.units = IMPERIAL_SYSTEM
|
|
_LOGGER.warning("Found deprecated temperature unit in core config, "
|
|
"expected unit system. Replace 'temperature: %s' with "
|
|
"'unit_system: %s'", unit, hac.units.name)
|
|
|
|
# Shortcut if no auto-detection necessary
|
|
if None not in (hac.latitude, hac.longitude, hac.units,
|
|
hac.time_zone, hac.elevation):
|
|
return
|
|
|
|
discovered = []
|
|
|
|
# If we miss some of the needed values, auto detect them
|
|
if None in (hac.latitude, hac.longitude, hac.units,
|
|
hac.time_zone):
|
|
info = loc_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, hac.longitude = (info.latitude, info.longitude)
|
|
discovered.append(('latitude', hac.latitude))
|
|
discovered.append(('longitude', hac.longitude))
|
|
|
|
if hac.units is None:
|
|
hac.units = METRIC_SYSTEM if info.use_metric else IMPERIAL_SYSTEM
|
|
discovered.append((CONF_UNIT_SYSTEM, hac.units.name))
|
|
|
|
if hac.location_name is None:
|
|
hac.location_name = info.city
|
|
discovered.append(('name', info.city))
|
|
|
|
if hac.time_zone is None:
|
|
set_time_zone(info.time_zone)
|
|
discovered.append(('time_zone', info.time_zone))
|
|
|
|
if hac.elevation is None and hac.latitude is not None and \
|
|
hac.longitude is not None:
|
|
elevation = loc_util.elevation(hac.latitude, hac.longitude)
|
|
hac.elevation = elevation
|
|
discovered.append(('elevation', elevation))
|
|
|
|
if discovered:
|
|
_LOGGER.warning(
|
|
'Incomplete core config. Auto detected %s',
|
|
', '.join('{}: {}'.format(key, val) for key, val in discovered))
|