diff --git a/config/configuration.yaml.example b/config/configuration.yaml.example index abf4bd5b035..73c03f94144 100644 --- a/config/configuration.yaml.example +++ b/config/configuration.yaml.example @@ -10,8 +10,8 @@ homeassistant: # Impacts weather/sunrise data elevation: 665 - # C for Celsius, F for Fahrenheit - temperature_unit: C + # 'metric' for Metric System, 'imperial' for imperial system + unit_system: metric # Pick yours from here: # http://en.wikipedia.org/wiki/List_of_tz_database_time_zones diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ac5a5b50599..b0d765be361 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -419,8 +419,9 @@ definitions: description: Longitude of Home Assistant server location_name: type: string - temperature_unit: + unit_system: type: string + description: The system for measurement units time_zone: type: string version: diff --git a/homeassistant/components/hvac/__init__.py b/homeassistant/components/hvac/__init__.py index 02a387bc70b..abd40a3ac93 100644 --- a/homeassistant/components/hvac/__init__.py +++ b/homeassistant/components/hvac/__init__.py @@ -6,13 +6,14 @@ https://home-assistant.io/components/hvac/ """ import logging import os +from numbers import Number from homeassistant.helpers.entity_component import EntityComponent from homeassistant.config import load_yaml_config_file import homeassistant.util as util +from homeassistant.util.temperature import convert as convert_temperature from homeassistant.helpers.entity import Entity -from homeassistant.helpers.temperature import convert from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN, @@ -204,8 +205,8 @@ def setup(hass, config): return for hvac in target_hvacs: - hvac.set_temperature(convert( - temperature, hass.config.temperature_unit, + hvac.set_temperature(convert_temperature( + temperature, hass.config.units.temperature_unit, hvac.unit_of_measurement)) if hvac.should_poll: @@ -462,12 +463,12 @@ class HvacDevice(Entity): @property def min_temp(self): """Return the minimum temperature.""" - return convert(19, TEMP_CELSIUS, self.unit_of_measurement) + return convert_temperature(19, TEMP_CELSIUS, self.unit_of_measurement) @property def max_temp(self): """Return the maximum temperature.""" - return convert(30, TEMP_CELSIUS, self.unit_of_measurement) + return convert_temperature(30, TEMP_CELSIUS, self.unit_of_measurement) @property def min_humidity(self): @@ -481,13 +482,13 @@ class HvacDevice(Entity): def _convert_for_display(self, temp): """Convert temperature into preferred units for display purposes.""" - if temp is None: - return None + if temp is None or not isinstance(temp, Number): + return temp - value = convert(temp, self.unit_of_measurement, - self.hass.config.temperature_unit) + value = convert_temperature(temp, self.unit_of_measurement, + self.hass.config.units.temperature_unit) - if self.hass.config.temperature_unit is TEMP_CELSIUS: + if self.hass.config.units.temperature_unit is TEMP_CELSIUS: decimal_count = 1 else: # Users of fahrenheit generally expect integer units. diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 767c2b966a6..b950ec39dd8 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -10,7 +10,7 @@ import socket from homeassistant.const import (ATTR_BATTERY_LEVEL, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - STATE_OFF, STATE_ON, TEMP_CELSIUS) + STATE_OFF, STATE_ON) from homeassistant.helpers import validate_config, discovery CONF_GATEWAYS = 'gateways' @@ -53,7 +53,7 @@ def setup(hass, config): # pylint: disable=too-many-locals import mysensors.mysensors as mysensors version = str(config[DOMAIN].get(CONF_VERSION, DEFAULT_VERSION)) - is_metric = (hass.config.temperature_unit == TEMP_CELSIUS) + is_metric = hass.config.units.is_metric persistence = config[DOMAIN].get(CONF_PERSISTENCE, True) def setup_gateway(device, persistence_file, baud_rate, tcp_port): diff --git a/homeassistant/components/proximity.py b/homeassistant/components/proximity.py index ced3c9a947e..ba0a192398f 100644 --- a/homeassistant/components/proximity.py +++ b/homeassistant/components/proximity.py @@ -27,9 +27,6 @@ DEFAULT_TOLERANCE = 1 # Default zone DEFAULT_PROXIMITY_ZONE = 'home' -# Default unit of measure -DEFAULT_UNIT_OF_MEASUREMENT = 'km' - # Default distance to zone DEFAULT_DIST_TO_ZONE = NOT_SET @@ -71,7 +68,7 @@ def setup_proximity_component(hass, config): # Get the unit of measurement from configuration.yaml. unit_of_measure = config.get(ATTR_UNIT_OF_MEASUREMENT, - DEFAULT_UNIT_OF_MEASUREMENT) + hass.config.units.length_unit) zone_id = 'zone.{}'.format(proximity_zone) state = hass.states.get(zone_id) @@ -216,11 +213,11 @@ class Proximity(Entity): # pylint: disable=too-many-instance-attributes # Loop through each of the distances collected and work out the # closest. - closest_device = '' - dist_to_zone = 1000000 + closest_device = None # type: str + dist_to_zone = None # type: float for device in distances_to_zone: - if distances_to_zone[device] < dist_to_zone: + if not dist_to_zone or distances_to_zone[device] < dist_to_zone: closest_device = device dist_to_zone = distances_to_zone[device] diff --git a/homeassistant/components/sensor/dht.py b/homeassistant/components/sensor/dht.py index a17f952aaf7..ec33b1e4042 100644 --- a/homeassistant/components/sensor/dht.py +++ b/homeassistant/components/sensor/dht.py @@ -10,6 +10,7 @@ from datetime import timedelta from homeassistant.const import TEMP_FAHRENHEIT from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +from homeassistant.util.temperature import celsius_to_fahrenheit # Update this requirement to upstream as soon as it supports Python 3. REQUIREMENTS = ['http://github.com/mala-zaba/Adafruit_Python_DHT/archive/' @@ -32,8 +33,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=import-error import Adafruit_DHT - SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit - unit = hass.config.temperature_unit + SENSOR_TYPES['temperature'][1] = hass.config.units.temperature_unit available_sensors = { "DHT11": Adafruit_DHT.DHT11, "DHT22": Adafruit_DHT.DHT22, @@ -58,7 +58,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if variable not in SENSOR_TYPES: _LOGGER.error('Sensor type: "%s" does not exist', variable) else: - dev.append(DHTSensor(data, variable, unit, name)) + dev.append( + DHTSensor(data, variable, SENSOR_TYPES[variable][1], name)) except KeyError: pass @@ -103,7 +104,8 @@ class DHTSensor(Entity): if self.type == 'temperature': self._state = round(data['temperature'], 1) if self.temp_unit == TEMP_FAHRENHEIT: - self._state = round(data['temperature'] * 1.8 + 32, 1) + self._state = round(celsius_to_fahrenheit(data['temperature']), + 1) elif self.type == 'humidity': self._state = round(data['humidity'], 1) diff --git a/homeassistant/components/sensor/fitbit.py b/homeassistant/components/sensor/fitbit.py index 8bbedf4dd2f..01c067a19f2 100644 --- a/homeassistant/components/sensor/fitbit.py +++ b/homeassistant/components/sensor/fitbit.py @@ -10,7 +10,6 @@ import logging import datetime import time -from homeassistant.const import TEMP_CELSIUS from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity from homeassistant.loader import get_component @@ -238,8 +237,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for resource in config.get("monitored_resources", FITBIT_DEFAULT_RESOURCE_LIST): dev.append(FitbitSensor(authd_client, config_path, resource, - hass.config.temperature_unit == - TEMP_CELSIUS)) + hass.config.units.is_metric)) add_devices(dev) else: diff --git a/homeassistant/components/sensor/forecast.py b/homeassistant/components/sensor/forecast.py index cca1b7d52d7..1a569d3d4c3 100644 --- a/homeassistant/components/sensor/forecast.py +++ b/homeassistant/components/sensor/forecast.py @@ -10,7 +10,7 @@ from requests.exceptions import ConnectionError as ConnectError, \ HTTPError, Timeout from homeassistant.components.sensor import DOMAIN -from homeassistant.const import CONF_API_KEY, TEMP_CELSIUS +from homeassistant.const import CONF_API_KEY from homeassistant.helpers import validate_config from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -62,7 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if 'units' in config: units = config['units'] - elif hass.config.temperature_unit == TEMP_CELSIUS: + elif hass.config.units.is_metric: units = 'si' else: units = 'us' diff --git a/homeassistant/components/sensor/google_travel_time.py b/homeassistant/components/sensor/google_travel_time.py index be42ddf8382..378e9c9c124 100644 --- a/homeassistant/components/sensor/google_travel_time.py +++ b/homeassistant/components/sensor/google_travel_time.py @@ -11,8 +11,7 @@ import voluptuous as vol from homeassistant.helpers.entity import Entity from homeassistant.const import ( - CONF_API_KEY, TEMP_CELSIUS, TEMP_FAHRENHEIT, - EVENT_HOMEASSISTANT_START, ATTR_LATITUDE, ATTR_LONGITUDE) + CONF_API_KEY, EVENT_HOMEASSISTANT_START, ATTR_LATITUDE, ATTR_LONGITUDE) from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -92,10 +91,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): options = config.get(CONF_OPTIONS) if options.get('units') is None: - if hass.config.temperature_unit is TEMP_CELSIUS: - options['units'] = 'metric' - elif hass.config.temperature_unit is TEMP_FAHRENHEIT: - options['units'] = 'imperial' + options['units'] = hass.config.units.name travel_mode = config.get(CONF_TRAVEL_MODE) mode = options.get(CONF_MODE) diff --git a/homeassistant/components/sensor/mold_indicator.py b/homeassistant/components/sensor/mold_indicator.py index 89db341b14d..4e59cd2cd62 100644 --- a/homeassistant/components/sensor/mold_indicator.py +++ b/homeassistant/components/sensor/mold_indicator.py @@ -65,7 +65,7 @@ class MoldIndicator(Entity): self._indoor_humidity_sensor = indoor_humidity_sensor self._outdoor_temp_sensor = outdoor_temp_sensor self._calib_factor = calib_factor - self._is_metric = (hass.config.temperature_unit == TEMP_CELSIUS) + self._is_metric = hass.config.units.is_metric self._dewpoint = None self._indoor_temp = None diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index 060f3bd57e8..efaa8d450b4 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -48,8 +48,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): from pyowm import OWM - SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit - unit = hass.config.temperature_unit + SENSOR_TYPES['temperature'][1] = hass.config.units.temperature_unit forecast = config.get('forecast') owm = OWM(config.get(CONF_API_KEY, None)) @@ -67,13 +66,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if variable not in SENSOR_TYPES: _LOGGER.error('Sensor type: "%s" does not exist', variable) else: - dev.append(OpenWeatherMapSensor(data, variable, unit)) + dev.append(OpenWeatherMapSensor(data, variable, + SENSOR_TYPES[variable][1])) except KeyError: pass if forecast: SENSOR_TYPES['forecast'] = ['Forecast', None] - dev.append(OpenWeatherMapSensor(data, 'forecast', unit)) + dev.append(OpenWeatherMapSensor(data, 'forecast', + SENSOR_TYPES['temperature'][1])) add_devices(dev) diff --git a/homeassistant/components/sensor/temper.py b/homeassistant/components/sensor/temper.py index 5beb49d64e0..fe5ebb17982 100644 --- a/homeassistant/components/sensor/temper.py +++ b/homeassistant/components/sensor/temper.py @@ -21,7 +21,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Setup the Temper sensors.""" from temperusb.temper import TemperHandler - temp_unit = hass.config.temperature_unit + temp_unit = hass.config.units.temperature_unit name = config.get(CONF_NAME, DEVICE_DEFAULT_NAME) temper_devices = TemperHandler().get_devices() add_devices_callback([TemperSensor(dev, temp_unit, name + '_' + str(idx)) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 537b7847b7e..d7b642fb2fc 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -61,11 +61,11 @@ class VeraSensor(VeraDevice, Entity): self._temperature_units = TEMP_CELSIUS if self.hass: - temp = self.hass.config.temperature( + temp = self.hass.config.units.temperature( current_temp, self._temperature_units) - current_temp, self._temperature_units = temp + current_temp = temp self.current_value = current_temp elif self.vera_device.category == "Light Sensor": diff --git a/homeassistant/components/sensor/yweather.py b/homeassistant/components/sensor/yweather.py index ffeb09dc92d..9011ae24b29 100644 --- a/homeassistant/components/sensor/yweather.py +++ b/homeassistant/components/sensor/yweather.py @@ -45,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Yahoo! weather sensor.""" from yahooweather import get_woeid, UNIT_C, UNIT_F - unit = hass.config.temperature_unit + unit = hass.config.units.temperature_unit woeid = config.get("woeid", None) forecast = config.get("forecast", 0) diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index 535a98567d8..b8a3b113671 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/thermostat/ """ import logging import os +from numbers import Number import voluptuous as vol @@ -13,9 +14,9 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.config import load_yaml_config_file from homeassistant.helpers.entity import Entity -from homeassistant.helpers.temperature import convert from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.helpers.config_validation as cv +from homeassistant.util.temperature import convert from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS) @@ -146,9 +147,13 @@ def setup(hass, config): temperature = service.data[ATTR_TEMPERATURE] for thermostat in target_thermostats: - thermostat.set_temperature(convert( - temperature, hass.config.temperature_unit, - thermostat.unit_of_measurement)) + if thermostat.unit_of_measurement is not None: + converted_temperature = convert( + temperature, hass.config.units.temperature_unit, + thermostat.unit_of_measurement) + else: + converted_temperature = temperature + thermostat.set_temperature(converted_temperature) thermostat.update_ha_state(True) @@ -301,22 +306,30 @@ class ThermostatDevice(Entity): @property def min_temp(self): """Return the minimum temperature.""" - return convert(7, TEMP_CELSIUS, self.unit_of_measurement) + try: + unit = self.unit_of_measurement + return convert(7, TEMP_CELSIUS, unit) + except ValueError: + return STATE_UNKNOWN @property def max_temp(self): """Return the maximum temperature.""" - return convert(35, TEMP_CELSIUS, self.unit_of_measurement) + try: + unit = self.unit_of_measurement + return convert(35, TEMP_CELSIUS, unit) + except ValueError: + return STATE_UNKNOWN def _convert_for_display(self, temp): """Convert temperature into preferred units for display purposes.""" - if temp is None: - return None + if temp is None or not isinstance(temp, Number): + return temp - value = convert(temp, self.unit_of_measurement, - self.hass.config.temperature_unit) + value = self.hass.config.units.temperature(temp, + self.unit_of_measurement) - if self.hass.config.temperature_unit is TEMP_CELSIUS: + if self.hass.config.units.is_metric: decimal_count = 1 else: # Users of fahrenheit generally expect integer units. diff --git a/homeassistant/components/thermostat/eq3btsmart.py b/homeassistant/components/thermostat/eq3btsmart.py index 04ca9802af3..a2aec1b8f60 100644 --- a/homeassistant/components/thermostat/eq3btsmart.py +++ b/homeassistant/components/thermostat/eq3btsmart.py @@ -8,7 +8,7 @@ import logging from homeassistant.components.thermostat import ThermostatDevice from homeassistant.const import TEMP_CELSIUS -from homeassistant.helpers.temperature import convert +from homeassistant.util.temperature import convert REQUIREMENTS = ['bluepy_devices==0.2.0'] diff --git a/homeassistant/components/thermostat/heat_control.py b/homeassistant/components/thermostat/heat_control.py index 831f1dd41b9..0f2dbce6040 100644 --- a/homeassistant/components/thermostat/heat_control.py +++ b/homeassistant/components/thermostat/heat_control.py @@ -75,7 +75,7 @@ class HeatControl(ThermostatDevice): self._min_temp = min_temp self._max_temp = max_temp self._target_temp = target_temp - self._unit = None + self._unit = hass.config.units.temperature_unit track_state_change(hass, sensor_entity_id, self._sensor_changed) diff --git a/homeassistant/components/thermostat/homematic.py b/homeassistant/components/thermostat/homematic.py index 345b8785b42..73901ab61df 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/thermostat.homematic/ import logging import homeassistant.components.homematic as homematic from homeassistant.components.thermostat import ThermostatDevice -from homeassistant.helpers.temperature import convert +from homeassistant.util.temperature import convert from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN DEPENDENCIES = ['homematic'] diff --git a/homeassistant/config.py b/homeassistant/config.py index 1061bd4f5ec..53c43be7c17 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -7,12 +7,14 @@ from types import MappingProxyType import voluptuous as vol from homeassistant.const import ( - CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_TEMPERATURE_UNIT, - CONF_TIME_ZONE, CONF_CUSTOMIZE, CONF_ELEVATION, TEMP_FAHRENHEIT, - TEMP_CELSIUS, __version__) + 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 @@ -30,7 +32,9 @@ DEFAULT_CORE_CONFIG = ( ' the sun rises and sets'), (CONF_LONGITUDE, 0, 'longitude', None), (CONF_ELEVATION, 0, None, 'Impacts weather/sunrise data'), - (CONF_TEMPERATURE_UNIT, 'C', None, 'C for Celsius, F for Fahrenheit'), + (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'), ) @@ -88,7 +92,8 @@ CORE_CONFIG_SCHEMA = vol.Schema({ CONF_LATITUDE: cv.latitude, CONF_LONGITUDE: cv.longitude, CONF_ELEVATION: vol.Coerce(int), - CONF_TEMPERATURE_UNIT: cv.temperature_unit, + 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, @@ -131,8 +136,10 @@ def create_default_config(config_dir, detect_location=True): location_info = detect_location and loc_util.detect_location_info() if location_info: - if location_info.use_fahrenheit: - info[CONF_TEMPERATURE_UNIT] = 'F' + 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: @@ -244,18 +251,30 @@ def process_ha_core_config(hass, config): set_customize(config.get(CONF_CUSTOMIZE) or {}) - if CONF_TEMPERATURE_UNIT in config: - hac.temperature_unit = config[CONF_TEMPERATURE_UNIT] + 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.temperature_unit, + 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.temperature_unit, + if None in (hac.latitude, hac.longitude, hac.units, hac.time_zone): info = loc_util.detect_location_info() @@ -264,18 +283,13 @@ def process_ha_core_config(hass, config): return if hac.latitude is None and hac.longitude is None: - hac.latitude = info.latitude - hac.longitude = info.longitude + hac.latitude, hac.longitude = (info.latitude, info.longitude) discovered.append(('latitude', hac.latitude)) discovered.append(('longitude', hac.longitude)) - if hac.temperature_unit is None: - if info.use_fahrenheit: - hac.temperature_unit = TEMP_FAHRENHEIT - discovered.append(('temperature_unit', 'F')) - else: - hac.temperature_unit = TEMP_CELSIUS - discovered.append(('temperature_unit', 'C')) + 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 diff --git a/homeassistant/const.py b/homeassistant/const.py index 16fd9866058..6dac4acbaf0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -47,6 +47,7 @@ CONF_PORT = 'port' CONF_SCAN_INTERVAL = 'scan_interval' CONF_STATE = 'state' CONF_TEMPERATURE_UNIT = 'temperature_unit' +CONF_UNIT_SYSTEM = 'unit_system' CONF_TIME_ZONE = 'time_zone' CONF_USERNAME = 'username' CONF_VALUE_TEMPLATE = 'value_template' @@ -112,11 +113,38 @@ ATTR_ICON = "icon" # The unit of measurement if applicable ATTR_UNIT_OF_MEASUREMENT = "unit_of_measurement" +CONF_UNIT_SYSTEM_METRIC = 'metric' # type: str +CONF_UNIT_SYSTEM_IMPERIAL = 'imperial' # type: str + # Temperature attribute ATTR_TEMPERATURE = "temperature" TEMP_CELSIUS = "°C" TEMP_FAHRENHEIT = "°F" +# Length units +LENGTH_CENTIMETERS = "cm" # type: str +LENGTH_METERS = "m" # type: str +LENGTH_KILOMETERS = "km" # type: str + +LENGTH_INCHES = "in" # type: str +LENGTH_FEET = "ft" # type: str +LENGTH_YARD = "yd" # type: str +LENGTH_MILES = "mi" # type: str + +# Volume units +VOLUME_LITERS = "L" # type: str +VOLUME_MILLILITERS = "mL" # type: str + +VOLUME_GALLONS = "gal" # type: str +VOLUME_FLUID_OUNCE = "fl. oz." # type: str + +# Mass units +MASS_GRAMS = "g" # type: str +MASS_KILOGRAMS = "kg" # type: str + +MASS_OUNCES = "oz" # type: str +MASS_POUNDS = "lb" # type: str + # Contains the information that is discovered ATTR_DISCOVERED = "discovered" @@ -243,3 +271,10 @@ CONTENT_TYPE_TEXT_PLAIN = 'text/plain' # The exit code to send to request a restart RESTART_EXIT_CODE = 100 + +UNIT_NOT_RECOGNIZED_TEMPLATE = '{} is not a recognized {} unit.' # type: str + +LENGTH = 'length' # type: str +MASS = 'mass' # type: str +VOLUME = 'volume' # type: str +TEMPERATURE = 'temperature' # type: str diff --git a/homeassistant/core.py b/homeassistant/core.py index 7ddf5a6c10f..8ce381b072f 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -17,7 +17,6 @@ from types import MappingProxyType from typing import Any, Callable import voluptuous as vol -import homeassistant.helpers.temperature as temp_helper import homeassistant.util as util import homeassistant.util.dt as dt_util import homeassistant.util.location as location @@ -28,11 +27,13 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_SERVICE_EXECUTED, EVENT_SERVICE_REGISTERED, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, RESTART_EXIT_CODE, - SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, TEMP_CELSIUS, - TEMP_FAHRENHEIT, __version__) + SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, __version__) from homeassistant.exceptions import ( HomeAssistantError, InvalidEntityFormatError) from homeassistant.helpers.entity import split_entity_id, valid_entity_id +from homeassistant.helpers.unit_system import ( + METRIC_SYSTEM, +) DOMAIN = "homeassistant" @@ -95,7 +96,7 @@ class HomeAssistant(object): self.bus = EventBus(pool) self.services = ServiceRegistry(self.bus, self.add_job) self.states = StateMachine(self.bus) - self.config = Config() + self.config = Config() # type: Config self.state = CoreState.not_running @property @@ -715,9 +716,9 @@ class Config(object): self.latitude = None self.longitude = None self.elevation = None - self.temperature_unit = None self.location_name = None self.time_zone = None + self.units = METRIC_SYSTEM # If True, pip install is skipped for requirements on startup self.skip_pip = False @@ -731,29 +732,15 @@ class Config(object): # Directory that holds the configuration self.config_dir = get_default_config_dir() - def distance(self, lat, lon): - """Calculate distance from Home Assistant in meters.""" - return location.distance(self.latitude, self.longitude, lat, lon) + def distance(self: object, lat: float, lon: float) -> float: + """Calculate distance from Home Assistant.""" + return self.units.length( + location.distance(self.latitude, self.longitude, lat, lon), 'm') def path(self, *path): """Generate path to the file within the config dir.""" return os.path.join(self.config_dir, *path) - def temperature(self, value, unit): - """Convert temperature to user preferred unit if set.""" - if not (unit in (TEMP_CELSIUS, TEMP_FAHRENHEIT) and - self.temperature_unit and unit != self.temperature_unit): - return value, unit - - try: - temp = float(value) - except ValueError: # Could not convert value to float - return value, unit - - return ( - round(temp_helper.convert(temp, unit, self.temperature_unit), 1), - self.temperature_unit) - def as_dict(self): """Create a dict representation of this dict.""" time_zone = self.time_zone or dt_util.UTC @@ -761,7 +748,7 @@ class Config(object): return { 'latitude': self.latitude, 'longitude': self.longitude, - 'temperature_unit': self.temperature_unit, + 'unit_system': self.units.as_dict(), 'location_name': self.location_name, 'time_zone': time_zone.zone, 'components': self.components, diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 1dc0cc26b5c..314d886db70 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -9,7 +9,7 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_SCAN_INTERVAL, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_ALIAS, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, WEEKDAYS, CONF_CONDITION, CONF_BELOW, CONF_ABOVE, SUN_EVENT_SUNSET, - SUN_EVENT_SUNRISE) + SUN_EVENT_SUNRISE, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC) from homeassistant.helpers.entity import valid_entity_id import homeassistant.util.dt as dt_util from homeassistant.util import slugify @@ -226,6 +226,10 @@ def temperature_unit(value): raise vol.Invalid('invalid temperature unit (expected C or F)') +unit_system = vol.All(vol.Lower, vol.Any(CONF_UNIT_SYSTEM_METRIC, + CONF_UNIT_SYSTEM_IMPERIAL)) + + def template(value): """Validate a jinja2 template.""" if value is None: diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 9968ad3df4a..61f1c53f2a2 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -199,13 +199,15 @@ class Entity(object): attr.pop(ATTR_HIDDEN) # Convert temperature if we detect one - if attr.get(ATTR_UNIT_OF_MEASUREMENT) in (TEMP_CELSIUS, - TEMP_FAHRENHEIT): - - state, attr[ATTR_UNIT_OF_MEASUREMENT] = \ - self.hass.config.temperature( - state, attr[ATTR_UNIT_OF_MEASUREMENT]) - state = str(state) + try: + unit_of_measure = attr.get(ATTR_UNIT_OF_MEASUREMENT) + if unit_of_measure in (TEMP_CELSIUS, TEMP_FAHRENHEIT): + state = \ + str(self.hass.config.units.temperature(float(state), + unit_of_measure)) + except ValueError: + # Could not convert state to float + pass return self.hass.states.set( self.entity_id, state, attr, self.force_update) diff --git a/homeassistant/helpers/temperature.py b/homeassistant/helpers/temperature.py deleted file mode 100644 index 6155c08d8f0..00000000000 --- a/homeassistant/helpers/temperature.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Methods to help handle temperature in Home Assistant.""" -import homeassistant.util.temperature as temp_util -from homeassistant.const import TEMP_CELSIUS - - -def convert(temperature, unit, to_unit): - """Convert temperature to correct unit.""" - if unit == to_unit or unit is None or to_unit is None: - return temperature - elif unit == TEMP_CELSIUS: - return temp_util.celsius_to_fahrenheit(temperature) - - return temp_util.fahrenheit_to_celsius(temperature) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 8ccfd1e0bf5..fab081cc5c5 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -219,7 +219,8 @@ class LocationMethods(object): if len(locations) == 1: return self._hass.config.distance(*locations[0]) - return loc_util.distance(*locations[0] + locations[1]) + return self._hass.config.units.length( + loc_util.distance(*locations[0] + locations[1]), 'm') def _resolve_state(self, entity_id_or_state): """Return state or entity_id if given.""" diff --git a/homeassistant/helpers/unit_system.py b/homeassistant/helpers/unit_system.py new file mode 100644 index 00000000000..e5d1481d979 --- /dev/null +++ b/homeassistant/helpers/unit_system.py @@ -0,0 +1,126 @@ +"""Unit system helper class and methods.""" + +import logging +from numbers import Number +from homeassistant.const import ( + TEMP_CELSIUS, TEMP_FAHRENHEIT, LENGTH_CENTIMETERS, LENGTH_METERS, + LENGTH_KILOMETERS, LENGTH_INCHES, LENGTH_FEET, LENGTH_YARD, LENGTH_MILES, + VOLUME_LITERS, VOLUME_MILLILITERS, VOLUME_GALLONS, VOLUME_FLUID_OUNCE, + MASS_GRAMS, MASS_KILOGRAMS, MASS_OUNCES, MASS_POUNDS, + CONF_UNIT_SYSTEM_METRIC, + CONF_UNIT_SYSTEM_IMPERIAL, LENGTH, MASS, VOLUME, TEMPERATURE, + UNIT_NOT_RECOGNIZED_TEMPLATE) +from homeassistant.util import temperature as temperature_util +from homeassistant.util import distance as distance_util + +_LOGGER = logging.getLogger(__name__) + +LENGTH_UNITS = [ + LENGTH_MILES, + LENGTH_YARD, + LENGTH_FEET, + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_CENTIMETERS, +] + +MASS_UNITS = [ + MASS_POUNDS, + MASS_OUNCES, + MASS_KILOGRAMS, + MASS_GRAMS, +] + +VOLUME_UNITS = [ + VOLUME_GALLONS, + VOLUME_FLUID_OUNCE, + VOLUME_LITERS, + VOLUME_MILLILITERS, +] + +TEMPERATURE_UNITS = [ + TEMP_FAHRENHEIT, + TEMP_CELSIUS, +] + + +def is_valid_unit(unit: str, unit_type: str) -> bool: + """Check if the unit is valid for it's type.""" + if unit_type == LENGTH: + units = LENGTH_UNITS + elif unit_type == TEMPERATURE: + units = TEMPERATURE_UNITS + elif unit_type == MASS: + units = MASS_UNITS + elif unit_type == VOLUME: + units = VOLUME_UNITS + else: + return False + + return unit in units + + +class UnitSystem(object): + """A container for units of measure.""" + + # pylint: disable=too-many-arguments + def __init__(self: object, name: str, temperature: str, length: str, + volume: str, mass: str) -> None: + """Initialize the unit system object.""" + errors = \ + ', '.join(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit, unit_type) + for unit, unit_type in [ + (temperature, TEMPERATURE), + (length, LENGTH), + (volume, VOLUME), + (mass, MASS), ] + if not is_valid_unit(unit, unit_type)) # type: str + + if errors: + raise ValueError(errors) + + self.name = name + self.temperature_unit = temperature + self.length_unit = length + self.mass_unit = mass + self.volume_unit = volume + + @property + def is_metric(self: object) -> bool: + """Determine if this is the metric unit system.""" + return self.name == CONF_UNIT_SYSTEM_METRIC + + def temperature(self: object, temperature: float, from_unit: str) -> ( + float, str): + """Convert the given temperature to this unit system.""" + if not isinstance(temperature, Number): + raise TypeError( + '{} is not a numeric value.'.format(str(temperature))) + + return temperature_util.convert(temperature, + from_unit, self.temperature_unit) + + def length(self: object, length: float, from_unit: str) -> float: + """Convert the given length to this unit system.""" + if not isinstance(length, Number): + raise TypeError('{} is not a numeric value.'.format(str(length))) + + return distance_util.convert(length, from_unit, + self.length_unit) # type: float + + def as_dict(self) -> dict: + """Convert the unit system to a dictionary.""" + return { + LENGTH: self.length_unit, + MASS: self.mass_unit, + TEMPERATURE: self.temperature_unit, + VOLUME: self.volume_unit + } + + +METRIC_SYSTEM = UnitSystem(CONF_UNIT_SYSTEM_METRIC, TEMP_CELSIUS, + LENGTH_KILOMETERS, VOLUME_LITERS, MASS_GRAMS) + +IMPERIAL_SYSTEM = UnitSystem(CONF_UNIT_SYSTEM_IMPERIAL, TEMP_FAHRENHEIT, + LENGTH_MILES, VOLUME_GALLONS, MASS_POUNDS) diff --git a/homeassistant/util/distance.py b/homeassistant/util/distance.py index 69478313df1..ef4e6c02b1a 100644 --- a/homeassistant/util/distance.py +++ b/homeassistant/util/distance.py @@ -3,82 +3,86 @@ import logging from numbers import Number +from homeassistant.const import ( + LENGTH_KILOMETERS, + LENGTH_MILES, + LENGTH_FEET, + LENGTH_METERS, + UNIT_NOT_RECOGNIZED_TEMPLATE, + LENGTH, +) + _LOGGER = logging.getLogger(__name__) -KILOMETERS_SYMBOL = 'km' -METERS_SYMBOL = 'm' -FEET_SYMBOL = 'ft' -MILES_SYMBOL = 'mi' - VALID_UNITS = [ - KILOMETERS_SYMBOL, - METERS_SYMBOL, - FEET_SYMBOL, - MILES_SYMBOL, + LENGTH_KILOMETERS, + LENGTH_MILES, + LENGTH_FEET, + LENGTH_METERS, ] -def convert(value, unit_1, unit_2): +def convert(value: float, unit_1: str, unit_2: str) -> float: """Convert one unit of measurement to another.""" - if not isinstance(value, Number): - raise TypeError(str(value) + ' is not of numeric type') - - if unit_1 == unit_2: - return value - if unit_1 not in VALID_UNITS: - _LOGGER.error('Unknown unit of measure: ' + str(unit_1)) - raise ValueError('Unknown unit of measure: ' + str(unit_1)) - elif unit_2 not in VALID_UNITS: - _LOGGER.error('Unknown unit of measure: ' + str(unit_2)) - raise ValueError('Unknown unit of measure: ' + str(unit_2)) + raise ValueError( + UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, LENGTH)) + if unit_2 not in VALID_UNITS: + raise ValueError( + UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_2, LENGTH)) + + if not isinstance(value, Number): + raise TypeError('{} is not of numeric type'.format(value)) + + if unit_1 == unit_2 or unit_1 not in VALID_UNITS: + return value meters = value - if unit_1 == MILES_SYMBOL: + if unit_1 == LENGTH_MILES: meters = __miles_to_meters(value) - elif unit_1 == FEET_SYMBOL: + elif unit_1 == LENGTH_FEET: meters = __feet_to_meters(value) - elif unit_1 == KILOMETERS_SYMBOL: + elif unit_1 == LENGTH_KILOMETERS: meters = __kilometers_to_meters(value) result = meters - if unit_2 == MILES_SYMBOL: + if unit_2 == LENGTH_MILES: result = __meters_to_miles(meters) - elif unit_2 == FEET_SYMBOL: + elif unit_2 == LENGTH_FEET: result = __meters_to_feet(meters) - elif unit_2 == KILOMETERS_SYMBOL: + elif unit_2 == LENGTH_KILOMETERS: result = __meters_to_kilometers(meters) return result -def __miles_to_meters(miles): +def __miles_to_meters(miles: float) -> float: """Convert miles to meters.""" return miles * 1609.344 -def __feet_to_meters(feet): +def __feet_to_meters(feet: float) -> float: """Convert feet to meters.""" return feet * 0.3048 -def __kilometers_to_meters(kilometers): +def __kilometers_to_meters(kilometers: float) -> float: """Convert kilometers to meters.""" return kilometers * 1000 -def __meters_to_miles(meters): +def __meters_to_miles(meters: float) -> float: """Convert meters to miles.""" return meters * 0.000621371 -def __meters_to_feet(meters): +def __meters_to_feet(meters: float) -> float: """Convert meters to feet.""" return meters * 3.28084 -def __meters_to_kilometers(meters): +def __meters_to_kilometers(meters: float) -> float: """Convert meters to kilometers.""" return meters * 0.001 diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 1cc8ffe0b9f..1fb1c22c2cd 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -31,7 +31,7 @@ LocationInfo = collections.namedtuple( "LocationInfo", ['ip', 'country_code', 'country_name', 'region_code', 'region_name', 'city', 'zip_code', 'time_zone', 'latitude', 'longitude', - 'use_fahrenheit']) + 'use_metric']) def detect_location_info(): @@ -44,11 +44,8 @@ def detect_location_info(): if data is None: return 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 - data['use_fahrenheit'] = data['country_code'] in ( - 'BS', 'BZ', 'KY', 'PW', 'US', 'AS', 'VI') + data['use_metric'] = data['country_code'] not in ( + 'US', 'MM', 'LR') return LocationInfo(**data) diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index a3799ad12a2..d6e245de04f 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -1,4 +1,10 @@ """Temperature util functions.""" +from homeassistant.const import ( + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + UNIT_NOT_RECOGNIZED_TEMPLATE, + TEMPERATURE +) def fahrenheit_to_celsius(fahrenheit: float) -> float: @@ -9,3 +15,20 @@ def fahrenheit_to_celsius(fahrenheit: float) -> float: def celsius_to_fahrenheit(celsius: float) -> float: """Convert a Celsius temperature to Fahrenheit.""" return celsius * 1.8 + 32.0 + + +def convert(temperature: float, from_unit: str, to_unit: str) -> float: + """Convert a temperature from one unit to another.""" + if from_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT): + raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, + TEMPERATURE)) + if to_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT): + raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, + TEMPERATURE)) + + if from_unit == to_unit: + return temperature + elif from_unit == TEMP_CELSIUS: + return celsius_to_fahrenheit(temperature) + else: + return round(fahrenheit_to_celsius(temperature), 1) diff --git a/tests/common.py b/tests/common.py index 4fd9da96ae3..e3ca00c8cbb 100644 --- a/tests/common.py +++ b/tests/common.py @@ -6,11 +6,12 @@ from unittest import mock from homeassistant import core as ha, loader from homeassistant.bootstrap import _setup_component from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.unit_system import METRIC_SYSTEM import homeassistant.util.dt as date_util from homeassistant.const import ( STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, - ATTR_DISCOVERED, SERVER_PORT, TEMP_CELSIUS) + ATTR_DISCOVERED, SERVER_PORT) from homeassistant.components import sun, mqtt _TEST_INSTANCE_PORT = SERVER_PORT @@ -37,7 +38,7 @@ def get_test_home_assistant(num_threads=None): hass.config.longitude = -117.22743 hass.config.elevation = 0 hass.config.time_zone = date_util.get_time_zone('US/Pacific') - hass.config.temperature_unit = TEMP_CELSIUS + hass.config.units = METRIC_SYSTEM if 'custom_components.test' not in loader.AVAILABLE_COMPONENTS: loader.prepare(hass) diff --git a/tests/components/hvac/test_demo.py b/tests/components/hvac/test_demo.py index bdb155e43b4..fb1abd90c65 100644 --- a/tests/components/hvac/test_demo.py +++ b/tests/components/hvac/test_demo.py @@ -1,8 +1,8 @@ """The tests for the demo hvac.""" import unittest -from homeassistant.const import ( - TEMP_CELSIUS, +from homeassistant.helpers.unit_system import ( + METRIC_SYSTEM, ) from homeassistant.components import hvac @@ -18,7 +18,7 @@ class TestDemoHvac(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.hass.config.temperature_unit = TEMP_CELSIUS + self.hass.config.units = METRIC_SYSTEM self.assertTrue(hvac.setup(self.hass, {'hvac': { 'platform': 'demo', }})) diff --git a/tests/components/thermostat/test_demo.py b/tests/components/thermostat/test_demo.py index c4c6a52a20f..ba85c393f63 100644 --- a/tests/components/thermostat/test_demo.py +++ b/tests/components/thermostat/test_demo.py @@ -1,8 +1,8 @@ """The tests for the demo thermostat.""" import unittest -from homeassistant.const import ( - TEMP_CELSIUS, +from homeassistant.helpers.unit_system import ( + METRIC_SYSTEM, ) from homeassistant.components import thermostat @@ -18,7 +18,7 @@ class TestDemoThermostat(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.hass.config.temperature_unit = TEMP_CELSIUS + self.hass.config.units = METRIC_SYSTEM self.assertTrue(thermostat.setup(self.hass, {'thermostat': { 'platform': 'demo', }})) @@ -43,10 +43,10 @@ class TestDemoThermostat(unittest.TestCase): def test_set_target_temp_bad_attr(self): """Test setting the target temperature without required attribute.""" - self.assertEqual('21', self.hass.states.get(ENTITY_NEST).state) + self.assertEqual('21.0', self.hass.states.get(ENTITY_NEST).state) thermostat.set_temperature(self.hass, None, ENTITY_NEST) self.hass.pool.block_till_done() - self.assertEqual('21', self.hass.states.get(ENTITY_NEST).state) + self.assertEqual('21.0', self.hass.states.get(ENTITY_NEST).state) def test_set_target_temp(self): """Test the setting of the target temperature.""" diff --git a/tests/components/thermostat/test_heat_control.py b/tests/components/thermostat/test_heat_control.py index 8ab571ce56b..f7f786adbbb 100644 --- a/tests/components/thermostat/test_heat_control.py +++ b/tests/components/thermostat/test_heat_control.py @@ -10,6 +10,7 @@ from homeassistant.const import ( STATE_OFF, TEMP_CELSIUS, ) +from homeassistant.helpers.unit_system import METRIC_SYSTEM from homeassistant.components import thermostat from tests.common import get_test_home_assistant @@ -75,7 +76,7 @@ class TestThermostatHeatControl(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.hass.config.temperature_unit = TEMP_CELSIUS + self.hass.config.units = METRIC_SYSTEM thermostat.setup(self.hass, {'thermostat': { 'platform': 'heat_control', 'name': 'test', diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 1529c879aab..4d62d328825 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -6,6 +6,13 @@ from unittest.mock import patch from homeassistant.components import group from homeassistant.exceptions import TemplateError from homeassistant.helpers import template +from homeassistant.helpers.unit_system import UnitSystem +from homeassistant.const import ( + LENGTH_METERS, + TEMP_CELSIUS, + MASS_GRAMS, + VOLUME_LITERS, +) import homeassistant.util.dt as dt_util from tests.common import get_test_home_assistant @@ -17,6 +24,9 @@ class TestUtilTemplate(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Setup the tests.""" self.hass = get_test_home_assistant() + self.hass.config.units = UnitSystem('custom', TEMP_CELSIUS, + LENGTH_METERS, VOLUME_LITERS, + MASS_GRAMS) def tearDown(self): # pylint: disable=invalid-name """Stop down stuff we started.""" diff --git a/tests/helpers/test_unit_system.py b/tests/helpers/test_unit_system.py new file mode 100644 index 00000000000..a31b3bfc53a --- /dev/null +++ b/tests/helpers/test_unit_system.py @@ -0,0 +1,132 @@ +"""Test the unit system helper.""" +import unittest + +from homeassistant.helpers.unit_system import ( + UnitSystem, + METRIC_SYSTEM, + IMPERIAL_SYSTEM, +) +from homeassistant.const import ( + LENGTH_METERS, + LENGTH_KILOMETERS, + MASS_GRAMS, + VOLUME_LITERS, + TEMP_CELSIUS, + LENGTH, + MASS, + TEMPERATURE, + VOLUME +) + +SYSTEM_NAME = 'TEST' +INVALID_UNIT = 'INVALID' + + +class TestUnitSystem(unittest.TestCase): + """Test the unit system helper.""" + + def test_invalid_units(self): + """Test errors are raised when invalid units are passed in.""" + with self.assertRaises(ValueError): + UnitSystem(SYSTEM_NAME, INVALID_UNIT, LENGTH_METERS, VOLUME_LITERS, + MASS_GRAMS) + + with self.assertRaises(ValueError): + UnitSystem(SYSTEM_NAME, TEMP_CELSIUS, INVALID_UNIT, VOLUME_LITERS, + MASS_GRAMS) + + with self.assertRaises(ValueError): + UnitSystem(SYSTEM_NAME, TEMP_CELSIUS, LENGTH_METERS, INVALID_UNIT, + MASS_GRAMS) + + with self.assertRaises(ValueError): + UnitSystem(SYSTEM_NAME, TEMP_CELSIUS, LENGTH_METERS, VOLUME_LITERS, + INVALID_UNIT) + + def test_invalid_value(self): + """Test no conversion happens if value is non-numeric.""" + with self.assertRaises(TypeError): + METRIC_SYSTEM.length('25a', LENGTH_KILOMETERS) + with self.assertRaises(TypeError): + METRIC_SYSTEM.temperature('50K', TEMP_CELSIUS) + + def test_as_dict(self): + """Test that the as_dict() method returns the expected dictionary.""" + expected = { + LENGTH: LENGTH_KILOMETERS, + TEMPERATURE: TEMP_CELSIUS, + VOLUME: VOLUME_LITERS, + MASS: MASS_GRAMS + } + + self.assertEqual(expected, METRIC_SYSTEM.as_dict()) + + def test_temperature_same_unit(self): + """Test no conversion happens if to unit is same as from unit.""" + self.assertEqual( + 5, + METRIC_SYSTEM.temperature(5, + METRIC_SYSTEM.temperature_unit)) + + def test_temperature_unknown_unit(self): + """Test no conversion happens if unknown unit.""" + with self.assertRaises(ValueError): + METRIC_SYSTEM.temperature(5, 'K') + + def test_temperature_to_metric(self): + """Test temperature conversion to metric system.""" + self.assertEqual( + 25, + METRIC_SYSTEM.temperature(25, METRIC_SYSTEM.temperature_unit)) + self.assertEqual( + 26.7, + METRIC_SYSTEM.temperature(80, IMPERIAL_SYSTEM.temperature_unit)) + + def test_temperature_to_imperial(self): + """Test temperature conversion to imperial system.""" + self.assertEqual( + 77, + IMPERIAL_SYSTEM.temperature(77, IMPERIAL_SYSTEM.temperature_unit)) + self.assertEqual( + 77, + IMPERIAL_SYSTEM.temperature(25, METRIC_SYSTEM.temperature_unit)) + + def test_length_unknown_unit(self): + """Test length conversion with unknown from unit.""" + with self.assertRaises(ValueError): + METRIC_SYSTEM.length(5, 'fr') + + def test_length_to_metric(self): + """Test length conversion to metric system.""" + self.assertEqual( + 100, + METRIC_SYSTEM.length(100, METRIC_SYSTEM.length_unit) + ) + self.assertEqual( + 8.04672, + METRIC_SYSTEM.length(5, IMPERIAL_SYSTEM.length_unit) + ) + + def test_length_to_imperial(self): + """Test length conversion to imperial system.""" + self.assertEqual( + 100, + IMPERIAL_SYSTEM.length(100, + IMPERIAL_SYSTEM.length_unit) + ) + self.assertEqual( + 3.106855, + IMPERIAL_SYSTEM.length(5, METRIC_SYSTEM.length_unit) + ) + + def test_properties(self): + """Test the unit properties are returned as expected.""" + self.assertEqual(LENGTH_KILOMETERS, METRIC_SYSTEM.length_unit) + self.assertEqual(TEMP_CELSIUS, METRIC_SYSTEM.temperature_unit) + self.assertEqual(MASS_GRAMS, METRIC_SYSTEM.mass_unit) + self.assertEqual(VOLUME_LITERS, METRIC_SYSTEM.volume_unit) + + def test_is_metric(self): + """Test the is metric flag.""" + self.assertTrue(METRIC_SYSTEM.is_metric) + self.assertFalse(IMPERIAL_SYSTEM.is_metric) diff --git a/tests/test_config.py b/tests/test_config.py index 0a6321c2240..4a4f1ef9b6f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -11,9 +11,9 @@ from voluptuous import MultipleInvalid from homeassistant.core import DOMAIN, HomeAssistantError, Config import homeassistant.config as config_util from homeassistant.const import ( - CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME, + CONF_LATITUDE, CONF_LONGITUDE, CONF_UNIT_SYSTEM, CONF_NAME, CONF_TIME_ZONE, CONF_ELEVATION, CONF_CUSTOMIZE, __version__, - TEMP_FAHRENHEIT) + CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT) from homeassistant.util import location as location_util, dt as dt_util from homeassistant.helpers.entity import Entity @@ -145,7 +145,7 @@ class TestConfig(unittest.TestCase): CONF_LATITUDE: 32.8594, CONF_LONGITUDE: -117.2073, CONF_ELEVATION: 101, - CONF_TEMPERATURE_UNIT: 'F', + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, CONF_NAME: 'Home', CONF_TIME_ZONE: 'America/Los_Angeles' } @@ -167,7 +167,7 @@ class TestConfig(unittest.TestCase): def test_core_config_schema(self): for value in ( - {'temperature_unit': 'K'}, + {CONF_UNIT_SYSTEM: 'K'}, {'time_zone': 'non-exist'}, {'latitude': '91'}, {'longitude': -181}, @@ -182,7 +182,7 @@ class TestConfig(unittest.TestCase): 'name': 'Test name', 'latitude': '-23.45', 'longitude': '123.45', - 'temperature_unit': 'c', + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, 'customize': { 'sensor.temperature': { 'hidden': True, @@ -264,7 +264,7 @@ class TestConfig(unittest.TestCase): 'longitude': 50, 'elevation': 25, 'name': 'Huis', - 'temperature_unit': 'F', + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, 'time_zone': 'America/New_York', }) @@ -272,7 +272,28 @@ class TestConfig(unittest.TestCase): assert config.longitude == 50 assert config.elevation == 25 assert config.location_name == 'Huis' - assert config.temperature_unit == TEMP_FAHRENHEIT + assert config.units.name == CONF_UNIT_SYSTEM_IMPERIAL + assert config.time_zone.zone == 'America/New_York' + + def test_loading_configuration_temperature_unit(self): + """Test backward compatibility when loading core config.""" + config = Config() + hass = mock.Mock(config=config) + + config_util.process_ha_core_config(hass, { + 'latitude': 60, + 'longitude': 50, + 'elevation': 25, + 'name': 'Huis', + CONF_TEMPERATURE_UNIT: 'C', + 'time_zone': 'America/New_York', + }) + + assert config.latitude == 60 + assert config.longitude == 50 + assert config.elevation == 25 + assert config.location_name == 'Huis' + assert config.units.name == CONF_UNIT_SYSTEM_METRIC assert config.time_zone.zone == 'America/New_York' @mock.patch('homeassistant.util.location.detect_location_info', @@ -292,7 +313,8 @@ class TestConfig(unittest.TestCase): assert config.longitude == -117.2073 assert config.elevation == 101 assert config.location_name == 'San Diego' - assert config.temperature_unit == TEMP_FAHRENHEIT + assert config.units.name == CONF_UNIT_SYSTEM_METRIC + assert config.units.is_metric assert config.time_zone.zone == 'America/Los_Angeles' @mock.patch('homeassistant.util.location.detect_location_info', @@ -311,5 +333,5 @@ class TestConfig(unittest.TestCase): assert config.longitude == blankConfig.longitude assert config.elevation == blankConfig.elevation assert config.location_name == blankConfig.location_name - assert config.temperature_unit == blankConfig.temperature_unit + assert config.units == blankConfig.units assert config.time_zone == blankConfig.time_zone diff --git a/tests/test_core.py b/tests/test_core.py index e9513a2adb8..56a44964e79 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -15,10 +15,10 @@ import homeassistant.core as ha from homeassistant.exceptions import ( HomeAssistantError, InvalidEntityFormatError) import homeassistant.util.dt as dt_util +from homeassistant.helpers.unit_system import (METRIC_SYSTEM) from homeassistant.const import ( __version__, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, TEMP_CELSIUS, - TEMP_FAHRENHEIT) + EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, CONF_UNIT_SYSTEM) from tests.common import get_test_home_assistant @@ -465,56 +465,12 @@ class TestConfig(unittest.TestCase): os.path.join(data_dir, ".homeassistant", "dir", "test.conf"), self.config.path("dir", "test.conf")) - def test_temperature_not_convert_if_no_preference(self): - """No unit conversion to happen if no preference.""" - self.assertEqual( - (25, TEMP_CELSIUS), - self.config.temperature(25, TEMP_CELSIUS)) - self.assertEqual( - (80, TEMP_FAHRENHEIT), - self.config.temperature(80, TEMP_FAHRENHEIT)) - - def test_temperature_not_convert_if_invalid_value(self): - """No unit conversion to happen if no preference.""" - self.config.temperature_unit = TEMP_FAHRENHEIT - self.assertEqual( - ('25a', TEMP_CELSIUS), - self.config.temperature('25a', TEMP_CELSIUS)) - - def test_temperature_not_convert_if_invalid_unit(self): - """No unit conversion to happen if no preference.""" - self.assertEqual( - (25, 'Invalid unit'), - self.config.temperature(25, 'Invalid unit')) - - def test_temperature_to_convert_to_celsius(self): - """Test temperature conversion to celsius.""" - self.config.temperature_unit = TEMP_CELSIUS - - self.assertEqual( - (25, TEMP_CELSIUS), - self.config.temperature(25, TEMP_CELSIUS)) - self.assertEqual( - (26.7, TEMP_CELSIUS), - self.config.temperature(80, TEMP_FAHRENHEIT)) - - def test_temperature_to_convert_to_fahrenheit(self): - """Test temperature conversion to fahrenheit.""" - self.config.temperature_unit = TEMP_FAHRENHEIT - - self.assertEqual( - (77, TEMP_FAHRENHEIT), - self.config.temperature(25, TEMP_CELSIUS)) - self.assertEqual( - (80, TEMP_FAHRENHEIT), - self.config.temperature(80, TEMP_FAHRENHEIT)) - def test_as_dict(self): """Test as dict.""" expected = { 'latitude': None, 'longitude': None, - 'temperature_unit': None, + CONF_UNIT_SYSTEM: METRIC_SYSTEM.as_dict(), 'location_name': None, 'time_zone': 'UTC', 'components': [], diff --git a/tests/util/test_distance.py b/tests/util/test_distance.py index 498416bc8eb..7f04f6f0569 100644 --- a/tests/util/test_distance.py +++ b/tests/util/test_distance.py @@ -2,14 +2,11 @@ import unittest import homeassistant.util.distance as distance_util - -KILOMETERS = distance_util.KILOMETERS_SYMBOL -METERS = distance_util.METERS_SYMBOL -FEET = distance_util.FEET_SYMBOL -MILES = distance_util.MILES_SYMBOL +from homeassistant.const import (LENGTH_KILOMETERS, LENGTH_METERS, LENGTH_FEET, + LENGTH_MILES) INVALID_SYMBOL = 'bob' -VALID_SYMBOL = KILOMETERS +VALID_SYMBOL = LENGTH_KILOMETERS class TestDistanceUtil(unittest.TestCase): @@ -17,52 +14,78 @@ class TestDistanceUtil(unittest.TestCase): def test_convert_same_unit(self): """Test conversion from any unit to same unit.""" - self.assertEqual(5, distance_util.convert(5, KILOMETERS, KILOMETERS)) - self.assertEqual(2, distance_util.convert(2, METERS, METERS)) - self.assertEqual(10, distance_util.convert(10, MILES, MILES)) - self.assertEqual(9, distance_util.convert(9, FEET, FEET)) + self.assertEqual(5, + distance_util.convert(5, LENGTH_KILOMETERS, + LENGTH_KILOMETERS)) + self.assertEqual(2, + distance_util.convert(2, LENGTH_METERS, + LENGTH_METERS)) + self.assertEqual(10, + distance_util.convert(10, LENGTH_MILES, LENGTH_MILES)) + self.assertEqual(9, + distance_util.convert(9, LENGTH_FEET, LENGTH_FEET)) def test_convert_invalid_unit(self): """Test exception is thrown for invalid units.""" with self.assertRaises(ValueError): - distance_util.convert(5, INVALID_SYMBOL, VALID_SYMBOL) + distance_util.convert(5, INVALID_SYMBOL, + VALID_SYMBOL) with self.assertRaises(ValueError): - distance_util.convert(5, VALID_SYMBOL, INVALID_SYMBOL) + distance_util.convert(5, VALID_SYMBOL, + INVALID_SYMBOL) def test_convert_nonnumeric_value(self): """Test exception is thrown for nonnumeric type.""" with self.assertRaises(TypeError): - distance_util.convert('a', KILOMETERS, METERS) + distance_util.convert('a', LENGTH_KILOMETERS, LENGTH_METERS) def test_convert_from_miles(self): """Test conversion from miles to other units.""" miles = 5 - self.assertEqual(distance_util.convert(miles, MILES, KILOMETERS), - 8.04672) - self.assertEqual(distance_util.convert(miles, MILES, METERS), 8046.72) - self.assertEqual(distance_util.convert(miles, MILES, FEET), - 26400.0008448) + self.assertEqual( + distance_util.convert(miles, LENGTH_MILES, LENGTH_KILOMETERS), + 8.04672) + self.assertEqual( + distance_util.convert(miles, LENGTH_MILES, LENGTH_METERS), + 8046.72) + self.assertEqual( + distance_util.convert(miles, LENGTH_MILES, LENGTH_FEET), + 26400.0008448) def test_convert_from_feet(self): """Test conversion from feet to other units.""" feet = 5000 - self.assertEqual(distance_util.convert(feet, FEET, KILOMETERS), 1.524) - self.assertEqual(distance_util.convert(feet, FEET, METERS), 1524) - self.assertEqual(distance_util.convert(feet, FEET, MILES), - 0.9469694040000001) + self.assertEqual( + distance_util.convert(feet, LENGTH_FEET, LENGTH_KILOMETERS), + 1.524) + self.assertEqual( + distance_util.convert(feet, LENGTH_FEET, LENGTH_METERS), + 1524) + self.assertEqual( + distance_util.convert(feet, LENGTH_FEET, LENGTH_MILES), + 0.9469694040000001) def test_convert_from_kilometers(self): """Test conversion from kilometers to other units.""" km = 5 - self.assertEqual(distance_util.convert(km, KILOMETERS, FEET), 16404.2) - self.assertEqual(distance_util.convert(km, KILOMETERS, METERS), 5000) - self.assertEqual(distance_util.convert(km, KILOMETERS, MILES), - 3.106855) + self.assertEqual( + distance_util.convert(km, LENGTH_KILOMETERS, LENGTH_FEET), + 16404.2) + self.assertEqual( + distance_util.convert(km, LENGTH_KILOMETERS, LENGTH_METERS), + 5000) + self.assertEqual( + distance_util.convert(km, LENGTH_KILOMETERS, LENGTH_MILES), + 3.106855) def test_convert_from_meters(self): """Test conversion from meters to other units.""" m = 5000 - self.assertEqual(distance_util.convert(m, METERS, FEET), 16404.2) - self.assertEqual(distance_util.convert(m, METERS, KILOMETERS), 5) - self.assertEqual(distance_util.convert(m, METERS, MILES), 3.106855) + self.assertEqual(distance_util.convert(m, LENGTH_METERS, LENGTH_FEET), + 16404.2) + self.assertEqual( + distance_util.convert(m, LENGTH_METERS, LENGTH_KILOMETERS), + 5) + self.assertEqual(distance_util.convert(m, LENGTH_METERS, LENGTH_MILES), + 3.106855) diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 1dfb71a87bf..6d099ebcfac 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -79,7 +79,7 @@ class TestLocationUtil(TestCase): assert info.time_zone == 'America/Los_Angeles' assert info.latitude == 32.8594 assert info.longitude == -117.2073 - assert info.use_fahrenheit + assert not info.use_metric @requests_mock.Mocker() @patch('homeassistant.util.location._get_freegeoip', return_value=None) @@ -101,7 +101,7 @@ class TestLocationUtil(TestCase): assert info.time_zone == 'America/Los_Angeles' assert info.latitude == 32.8594 assert info.longitude == -117.2073 - assert info.use_fahrenheit + assert not info.use_metric @patch('homeassistant.util.location.elevation', return_value=0) @patch('homeassistant.util.location._get_freegeoip', return_value=None)