diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 61f5773356f..81a7adca1b7 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -9,12 +9,12 @@ from datetime import timedelta import logging import os import functools as ft -from numbers import Number import voluptuous as vol from homeassistant.config import load_yaml_config_file from homeassistant.loader import bind_hass +from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.util.temperature import convert as convert_temperature from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity @@ -22,7 +22,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN, - TEMP_CELSIUS) + TEMP_CELSIUS, PRECISION_WHOLE, PRECISION_TENTHS) DOMAIN = 'climate' @@ -71,11 +71,6 @@ ATTR_OPERATION_LIST = 'operation_list' ATTR_SWING_MODE = 'swing_mode' ATTR_SWING_LIST = 'swing_list' -# The degree of precision for each platform -PRECISION_WHOLE = 1 -PRECISION_HALVES = 0.5 -PRECISION_TENTHS = 0.1 - CONVERTIBLE_ATTRIBUTE = [ ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, @@ -456,12 +451,18 @@ class ClimateDevice(Entity): def state_attributes(self): """Return the optional state attributes.""" data = { - ATTR_CURRENT_TEMPERATURE: - self._convert_for_display(self.current_temperature), - ATTR_MIN_TEMP: self._convert_for_display(self.min_temp), - ATTR_MAX_TEMP: self._convert_for_display(self.max_temp), - ATTR_TEMPERATURE: - self._convert_for_display(self.target_temperature), + ATTR_CURRENT_TEMPERATURE: show_temp( + self.hass, self.current_temperature, self.temperature_unit, + self.precision), + ATTR_MIN_TEMP: show_temp( + self.hass, self.min_temp, self.temperature_unit, + self.precision), + ATTR_MAX_TEMP: show_temp( + self.hass, self.max_temp, self.temperature_unit, + self.precision), + ATTR_TEMPERATURE: show_temp( + self.hass, self.target_temperature, self.temperature_unit, + self.precision), } if self.target_temperature_step is not None: @@ -469,10 +470,12 @@ class ClimateDevice(Entity): target_temp_high = self.target_temperature_high if target_temp_high is not None: - data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display( - self.target_temperature_high) - data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display( - self.target_temperature_low) + data[ATTR_TARGET_TEMP_HIGH] = show_temp( + self.hass, self.target_temperature_high, self.temperature_unit, + self.precision) + data[ATTR_TARGET_TEMP_LOW] = show_temp( + self.hass, self.target_temperature_low, self.temperature_unit, + self.precision) humidity = self.target_humidity if humidity is not None: @@ -733,24 +736,3 @@ class ClimateDevice(Entity): def max_humidity(self): """Return the maximum humidity.""" return 99 - - def _convert_for_display(self, temp): - """Convert temperature into preferred units for display purposes.""" - if temp is None: - return temp - - # if the temperature is not a number this can cause issues - # with polymer components, so bail early there. - if not isinstance(temp, Number): - raise TypeError("Temperature is not a number: %s" % temp) - - if self.temperature_unit != self.unit_of_measurement: - temp = convert_temperature( - temp, self.temperature_unit, self.unit_of_measurement) - # Round in the units appropriate - if self.precision == PRECISION_HALVES: - return round(temp * 2) / 2.0 - elif self.precision == PRECISION_TENTHS: - return round(temp, 1) - # PRECISION_WHOLE as a fall back - return round(temp) diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index d70890317fd..dba096bb632 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -9,12 +9,9 @@ import logging import voluptuous as vol from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, PRECISION_HALVES, - STATE_AUTO, STATE_ON, STATE_OFF, -) + STATE_ON, STATE_OFF, STATE_AUTO, PLATFORM_SCHEMA, ClimateDevice) from homeassistant.const import ( - CONF_MAC, TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE) - + CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['python-eq3bt==0.1.6'] @@ -58,15 +55,17 @@ class EQ3BTSmartThermostat(ClimateDevice): def __init__(self, _mac, _name): """Initialize the thermostat.""" - # we want to avoid name clash with this module.. + # We want to avoid name clash with this module. import eq3bt as eq3 - self.modes = {eq3.Mode.Open: STATE_ON, - eq3.Mode.Closed: STATE_OFF, - eq3.Mode.Auto: STATE_AUTO, - eq3.Mode.Manual: STATE_MANUAL, - eq3.Mode.Boost: STATE_BOOST, - eq3.Mode.Away: STATE_AWAY} + self.modes = { + eq3.Mode.Open: STATE_ON, + eq3.Mode.Closed: STATE_OFF, + eq3.Mode.Auto: STATE_AUTO, + eq3.Mode.Manual: STATE_MANUAL, + eq3.Mode.Boost: STATE_BOOST, + eq3.Mode.Away: STATE_AWAY, + } self.reverse_modes = {v: k for k, v in self.modes.items()} @@ -153,11 +152,11 @@ class EQ3BTSmartThermostat(ClimateDevice): def device_state_attributes(self): """Return the device specific state attributes.""" dev_specific = { + ATTR_STATE_AWAY_END: self._thermostat.away_end, ATTR_STATE_LOCKED: self._thermostat.locked, ATTR_STATE_LOW_BAT: self._thermostat.low_battery, ATTR_STATE_VALVE: self._thermostat.valve_state, ATTR_STATE_WINDOW_OPEN: self._thermostat.window_open, - ATTR_STATE_AWAY_END: self._thermostat.away_end, } return dev_specific diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py index 75627f11a71..54d8d8617c7 100644 --- a/homeassistant/components/climate/wink.py +++ b/homeassistant/components/climate/wink.py @@ -4,46 +4,51 @@ Support for Wink thermostats, Air Conditioners, and Water Heaters. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.wink/ """ -import logging import asyncio +import logging -from homeassistant.components.wink import WinkDevice, DOMAIN from homeassistant.components.climate import ( - STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice, - ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - ATTR_TEMPERATURE, STATE_FAN_ONLY, - ATTR_CURRENT_HUMIDITY, STATE_ECO, STATE_ELECTRIC, - STATE_PERFORMANCE, STATE_HIGH_DEMAND, - STATE_HEAT_PUMP, STATE_GAS) + STATE_ECO, STATE_GAS, STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ELECTRIC, + STATE_FAN_ONLY, STATE_HEAT_PUMP, ATTR_TEMPERATURE, STATE_HIGH_DEMAND, + STATE_PERFORMANCE, ATTR_TARGET_TEMP_LOW, ATTR_CURRENT_HUMIDITY, + ATTR_TARGET_TEMP_HIGH, ClimateDevice) +from homeassistant.components.wink import DOMAIN, WinkDevice from homeassistant.const import ( - TEMP_CELSIUS, STATE_ON, - STATE_OFF, STATE_UNKNOWN) + STATE_ON, STATE_OFF, TEMP_CELSIUS, STATE_UNKNOWN, PRECISION_TENTHS) +from homeassistant.helpers.temperature import display_temp as show_temp _LOGGER = logging.getLogger(__name__) +ATTR_ECO_TARGET = 'eco_target' +ATTR_EXTERNAL_TEMPERATURE = 'external_temperature' +ATTR_OCCUPIED = 'occupied' +ATTR_RHEEM_TYPE = 'rheem_type' +ATTR_SCHEDULE_ENABLED = 'schedule_enabled' +ATTR_SMART_TEMPERATURE = 'smart_temperature' +ATTR_TOTAL_CONSUMPTION = 'total_consumption' +ATTR_VACATION_MODE = 'vacation_mode' + DEPENDENCIES = ['wink'] SPEED_LOW = 'low' SPEED_MEDIUM = 'medium' SPEED_HIGH = 'high' -HA_STATE_TO_WINK = {STATE_AUTO: 'auto', - STATE_ECO: 'eco', - STATE_FAN_ONLY: 'fan_only', - STATE_HEAT: 'heat_only', - STATE_COOL: 'cool_only', - STATE_PERFORMANCE: 'performance', - STATE_HIGH_DEMAND: 'high_demand', - STATE_HEAT_PUMP: 'heat_pump', - STATE_ELECTRIC: 'electric_only', - STATE_GAS: 'gas', - STATE_OFF: 'off'} -WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()} +HA_STATE_TO_WINK = { + STATE_AUTO: 'auto', + STATE_COOL: 'cool_only', + STATE_ECO: 'eco', + STATE_ELECTRIC: 'electric_only', + STATE_FAN_ONLY: 'fan_only', + STATE_GAS: 'gas', + STATE_HEAT: 'heat_only', + STATE_HEAT_PUMP: 'heat_pump', + STATE_HIGH_DEMAND: 'high_demand', + STATE_OFF: 'off', + STATE_PERFORMANCE: 'performance', +} -ATTR_EXTERNAL_TEMPERATURE = "external_temperature" -ATTR_SMART_TEMPERATURE = "smart_temperature" -ATTR_ECO_TARGET = "eco_target" -ATTR_OCCUPIED = "occupied" +WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()} def setup_platform(hass, config, add_devices, discovery_info=None): @@ -85,15 +90,18 @@ class WinkThermostat(WinkDevice, ClimateDevice): target_temp_high = self.target_temperature_high target_temp_low = self.target_temperature_low if target_temp_high is not None: - data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display( - self.target_temperature_high) + data[ATTR_TARGET_TEMP_HIGH] = show_temp( + self.hass, self.target_temperature_high, self.temperature_unit, + PRECISION_TENTHS) if target_temp_low is not None: - data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display( - self.target_temperature_low) + data[ATTR_TARGET_TEMP_LOW] = show_temp( + self.hass, self.target_temperature_low, self.temperature_unit, + PRECISION_TENTHS) if self.external_temperature: - data[ATTR_EXTERNAL_TEMPERATURE] = self._convert_for_display( - self.external_temperature) + data[ATTR_EXTERNAL_TEMPERATURE] = show_temp( + self.hass, self.external_temperature, self.temperature_unit, + PRECISION_TENTHS) if self.smart_temperature: data[ATTR_SMART_TEMPERATURE] = self.smart_temperature @@ -358,13 +366,15 @@ class WinkAC(WinkDevice, ClimateDevice): target_temp_high = self.target_temperature_high target_temp_low = self.target_temperature_low if target_temp_high is not None: - data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display( - self.target_temperature_high) + data[ATTR_TARGET_TEMP_HIGH] = show_temp( + self.hass, self.target_temperature_high, self.temperature_unit, + PRECISION_TENTHS) if target_temp_low is not None: - data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display( - self.target_temperature_low) - data["total_consumption"] = self.wink.total_consumption() - data["schedule_enabled"] = self.wink.schedule_enabled() + data[ATTR_TARGET_TEMP_LOW] = show_temp( + self.hass, self.target_temperature_low, self.temperature_unit, + PRECISION_TENTHS) + data[ATTR_TOTAL_CONSUMPTION] = self.wink.total_consumption() + data[ATTR_SCHEDULE_ENABLED] = self.wink.schedule_enabled() return data @@ -471,8 +481,8 @@ class WinkWaterHeater(WinkDevice, ClimateDevice): def device_state_attributes(self): """Return the optional state attributes.""" data = {} - data["vacation_mode"] = self.wink.vacation_mode_enabled() - data["rheem_type"] = self.wink.rheem_type() + data[ATTR_VACATION_MODE] = self.wink.vacation_mode_enabled() + data[ATTR_RHEEM_TYPE] = self.wink.rheem_type() return data diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 9e927da893e..acb95c17814 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -6,11 +6,10 @@ https://home-assistant.io/components/weather/ """ import asyncio import logging -from numbers import Number -from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.helpers.temperature import display_temp as show_temp +from homeassistant.const import PRECISION_WHOLE, PRECISION_TENTHS, TEMP_CELSIUS from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.entity import Entity @@ -98,11 +97,19 @@ class WeatherEntity(Entity): """Return the forecast.""" return None + @property + def precision(self): + """Return the forecast.""" + return PRECISION_TENTHS if self.temperature_unit == TEMP_CELSIUS \ + else PRECISION_WHOLE + @property def state_attributes(self): """Return the state attributes.""" data = { - ATTR_WEATHER_TEMPERATURE: self._temp_for_display(self.temperature), + ATTR_WEATHER_TEMPERATURE: show_temp( + self.hass, self.temperature, self.temperature_unit, + self.precision), ATTR_WEATHER_HUMIDITY: self.humidity, } @@ -134,8 +141,9 @@ class WeatherEntity(Entity): forecast = [] for forecast_entry in self.forecast: forecast_entry = dict(forecast_entry) - forecast_entry[ATTR_FORECAST_TEMP] = self._temp_for_display( - forecast_entry[ATTR_FORECAST_TEMP]) + forecast_entry[ATTR_FORECAST_TEMP] = show_temp( + self.hass, forecast_entry[ATTR_FORECAST_TEMP], + self.temperature_unit, self.precision) forecast.append(forecast_entry) data[ATTR_FORECAST] = forecast @@ -151,19 +159,3 @@ class WeatherEntity(Entity): def condition(self): """Return the current condition.""" raise NotImplementedError() - - def _temp_for_display(self, temp): - """Convert temperature into preferred units for display purposes.""" - unit = self.temperature_unit - hass_unit = self.hass.config.units.temperature_unit - - if (temp is None or not isinstance(temp, Number) or - unit == hass_unit): - return temp - - value = convert_temperature(temp, unit, hass_unit) - - if hass_unit == TEMP_CELSIUS: - return round(value, 1) - # Users of fahrenheit generally expect integer units. - return round(value) diff --git a/homeassistant/components/weather/demo.py b/homeassistant/components/weather/demo.py index 0a404447346..02e07996213 100644 --- a/homeassistant/components/weather/demo.py +++ b/homeassistant/components/weather/demo.py @@ -31,7 +31,7 @@ CONDITION_CLASSES = { def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo weather.""" add_devices([ - DemoWeather('South', 'Sunshine', 21, 92, 1099, 0.5, TEMP_CELSIUS, + DemoWeather('South', 'Sunshine', 21.6414, 92, 1099, 0.5, TEMP_CELSIUS, [22, 19, 15, 12, 14, 18, 21]), DemoWeather('North', 'Shower rain', -12, 54, 987, 4.8, TEMP_FAHRENHEIT, [-10, -13, -18, -23, -19, -14, -9]) diff --git a/homeassistant/const.py b/homeassistant/const.py index de3f60e825f..90aa2c52483 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -417,3 +417,8 @@ SPEED_MS = 'speed_ms' # type: str ILLUMINANCE = 'illuminance' # type: str WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] + +# The degree of precision for platforms +PRECISION_WHOLE = 1 +PRECISION_HALVES = 0.5 +PRECISION_TENTHS = 0.1 diff --git a/homeassistant/helpers/temperature.py b/homeassistant/helpers/temperature.py new file mode 100644 index 00000000000..a4626c33210 --- /dev/null +++ b/homeassistant/helpers/temperature.py @@ -0,0 +1,33 @@ +"""Temperature helpers for Home Assistant.""" +from numbers import Number + +from homeassistant.core import HomeAssistant +from homeassistant.util.temperature import convert as convert_temperature + + +def display_temp(hass: HomeAssistant, temperature: float, unit: str, + precision: float) -> float: + """Convert temperature into preferred units for display purposes.""" + temperature_unit = unit + ha_unit = hass.config.units.temperature_unit + + if temperature is None: + return temperature + + # If the temperature is not a number this can cause issues + # with Polymer components, so bail early there. + if not isinstance(temperature, Number): + raise TypeError( + "Temperature is not a number: {}".format(temperature)) + + if temperature_unit != ha_unit: + temperature = convert_temperature( + temperature, temperature_unit, ha_unit) + + # Round in the units appropriate + if precision == 0.5: + return round(temperature * 2) / 2.0 + elif precision == 0.1: + return round(temperature, 1) + # Integer as a fall back (PRECISION_WHOLE) + return round(temperature) diff --git a/tests/components/weather/test_weather.py b/tests/components/weather/test_weather.py index 1563dd377c4..9d22b1ad0ae 100644 --- a/tests/components/weather/test_weather.py +++ b/tests/components/weather/test_weather.py @@ -37,7 +37,7 @@ class TestWeather(unittest.TestCase): assert state.state == 'sunny' data = state.attributes - assert data.get(ATTR_WEATHER_TEMPERATURE) == 21 + assert data.get(ATTR_WEATHER_TEMPERATURE) == 21.6 assert data.get(ATTR_WEATHER_HUMIDITY) == 92 assert data.get(ATTR_WEATHER_PRESSURE) == 1099 assert data.get(ATTR_WEATHER_WIND_SPEED) == 0.5 @@ -57,4 +57,4 @@ class TestWeather(unittest.TestCase): assert state.state == 'rainy' data = state.attributes - assert data.get(ATTR_WEATHER_TEMPERATURE) == -24.4 + assert data.get(ATTR_WEATHER_TEMPERATURE) == -24 diff --git a/tests/helpers/test_temperature.py b/tests/helpers/test_temperature.py new file mode 100644 index 00000000000..96e7bd6c74f --- /dev/null +++ b/tests/helpers/test_temperature.py @@ -0,0 +1,49 @@ +"""Tests Home Assistant temperature helpers.""" +import unittest + +from tests.common import get_test_home_assistant + +from homeassistant.const import ( + TEMP_CELSIUS, PRECISION_WHOLE, TEMP_FAHRENHEIT, PRECISION_HALVES, + PRECISION_TENTHS) +from homeassistant.helpers.temperature import display_temp +from homeassistant.util.unit_system import METRIC_SYSTEM + +TEMP = 24.636626 + + +class TestHelpersTemperature(unittest.TestCase): + """Setup the temperature tests.""" + + def setUp(self): + """Setup the tests.""" + self.hass = get_test_home_assistant() + self.hass.config.unit_system = METRIC_SYSTEM + + def tearDown(self): + """Stop down stuff we started.""" + self.hass.stop() + + def test_temperature_not_a_number(self): + """Test that temperature is a number.""" + temp = "Temperature" + with self.assertRaises(Exception) as context: + display_temp(self.hass, temp, TEMP_CELSIUS, PRECISION_HALVES) + + self.assertTrue("Temperature is not a number: {}".format(temp) + in str(context.exception)) + + def test_celsius_halves(self): + """Test temperature to celsius rounding to halves.""" + self.assertEqual(24.5, display_temp( + self.hass, TEMP, TEMP_CELSIUS, PRECISION_HALVES)) + + def test_celsius_tenths(self): + """Test temperature to celsius rounding to tenths.""" + self.assertEqual(24.6, display_temp( + self.hass, TEMP, TEMP_CELSIUS, PRECISION_TENTHS)) + + def test_fahrenheit_wholes(self): + """Test temperature to fahrenheit rounding to wholes.""" + self.assertEqual(-4, display_temp( + self.hass, TEMP, TEMP_FAHRENHEIT, PRECISION_WHOLE))