From ab3717af767d3c740b3e9567299afd363ccaf607 Mon Sep 17 00:00:00 2001 From: roiff Date: Fri, 1 Jun 2018 19:49:16 +0800 Subject: [PATCH] Homekit Thermostat: Better support for temperature ranges (#14679) * Support for obtaining temperature range * Fallback to Defaults * Fixed unit conversion * Added test --- .../components/homekit/type_thermostats.py | 39 +++++++++++---- .../homekit/test_type_thermostats.py | 48 +++++++++++++++++-- 2 files changed, 72 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index d6555d5056d..73a29990fba 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -4,15 +4,16 @@ import logging from pyhap.const import CATEGORY_THERMOSTAT from homeassistant.components.climate import ( - ATTR_CURRENT_TEMPERATURE, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, + ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, + ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, - TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES from .accessories import debounce, HomeAccessory @@ -20,7 +21,7 @@ from .const import ( CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING, CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE, - CHAR_TEMP_DISPLAY_UNITS, SERV_THERMOSTAT) + CHAR_TEMP_DISPLAY_UNITS, PROP_MAX_VALUE, PROP_MIN_VALUE, SERV_THERMOSTAT) from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) @@ -42,17 +43,18 @@ class Thermostat(HomeAccessory): def __init__(self, *args): """Initialize a Thermostat accessory object.""" super().__init__(*args, category=CATEGORY_THERMOSTAT) - self._unit = TEMP_CELSIUS + self._unit = self.hass.config.units.temperature_unit self.support_power_state = False self.heat_cool_flag_target_state = False self.temperature_flag_target_state = False self.coolingthresh_flag_target_state = False self.heatingthresh_flag_target_state = False + min_temp, max_temp = self.get_temperature_range() # Add additional characteristics if auto mode is supported self.chars = [] features = self.hass.states.get(self.entity_id) \ - .attributes.get(ATTR_SUPPORTED_FEATURES) + .attributes.get(ATTR_SUPPORTED_FEATURES, 0) if features & SUPPORT_ON_OFF: self.support_power_state = True if features & SUPPORT_TEMP_RANGE: @@ -73,6 +75,8 @@ class Thermostat(HomeAccessory): CHAR_CURRENT_TEMPERATURE, value=21.0) self.char_target_temp = serv_thermostat.configure_char( CHAR_TARGET_TEMPERATURE, value=21.0, + properties={PROP_MIN_VALUE: min_temp, + PROP_MAX_VALUE: max_temp}, setter_callback=self.set_target_temperature) # Display units characteristic @@ -85,12 +89,30 @@ class Thermostat(HomeAccessory): if CHAR_COOLING_THRESHOLD_TEMPERATURE in self.chars: self.char_cooling_thresh_temp = serv_thermostat.configure_char( CHAR_COOLING_THRESHOLD_TEMPERATURE, value=23.0, + properties={PROP_MIN_VALUE: min_temp, + PROP_MAX_VALUE: max_temp}, setter_callback=self.set_cooling_threshold) if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars: self.char_heating_thresh_temp = serv_thermostat.configure_char( CHAR_HEATING_THRESHOLD_TEMPERATURE, value=19.0, + properties={PROP_MIN_VALUE: min_temp, + PROP_MAX_VALUE: max_temp}, setter_callback=self.set_heating_threshold) + def get_temperature_range(self): + """Return min and max temperature range.""" + max_temp = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_MAX_TEMP) + max_temp = temperature_to_homekit(max_temp, self._unit) if max_temp \ + else DEFAULT_MAX_TEMP + + min_temp = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_MIN_TEMP) + min_temp = temperature_to_homekit(min_temp, self._unit) if min_temp \ + else DEFAULT_MIN_TEMP + + return min_temp, max_temp + def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" if value in HC_HOMEKIT_TO_HASS: @@ -147,9 +169,6 @@ class Thermostat(HomeAccessory): def update_state(self, new_state): """Update security state after state changed.""" - self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, - TEMP_CELSIUS) - # Update current temperature current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE) if isinstance(current_temp, (int, float)): diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 1f6554496a9..00e3e2d22fc 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1,12 +1,16 @@ """Test different accessory types: Thermostats.""" from collections import namedtuple +from unittest.mock import patch import pytest from homeassistant.components.climate import ( - ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, - ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, - DOMAIN, STATE_AUTO, STATE_COOL, STATE_HEAT) + ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, ATTR_TEMPERATURE, + ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, + ATTR_OPERATION_LIST, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, + STATE_AUTO, STATE_COOL, STATE_HEAT) +from homeassistant.components.homekit.const import ( + PROP_MAX_VALUE, PROP_MIN_VALUE) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -31,7 +35,7 @@ async def test_default_thermostat(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' - hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) + hass.states.async_set(entity_id, STATE_OFF) await hass.async_block_till_done() acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) @@ -48,6 +52,9 @@ async def test_default_thermostat(hass, hk_driver, cls): assert acc.char_cooling_thresh_temp is None assert acc.char_heating_thresh_temp is None + assert acc.char_target_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP + assert acc.char_target_temp.properties[PROP_MIN_VALUE] == DEFAULT_MIN_TEMP + hass.states.async_set(entity_id, STATE_HEAT, {ATTR_OPERATION_MODE: STATE_HEAT, ATTR_TEMPERATURE: 22.0, @@ -181,6 +188,15 @@ async def test_auto_thermostat(hass, hk_driver, cls): assert acc.char_cooling_thresh_temp.value == 23.0 assert acc.char_heating_thresh_temp.value == 19.0 + assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] \ + == DEFAULT_MAX_TEMP + assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] \ + == DEFAULT_MIN_TEMP + assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] \ + == DEFAULT_MAX_TEMP + assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] \ + == DEFAULT_MIN_TEMP + hass.states.async_set(entity_id, STATE_AUTO, {ATTR_OPERATION_MODE: STATE_AUTO, ATTR_TARGET_TEMP_HIGH: 22.0, @@ -307,7 +323,9 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls): # support_auto = True hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) + with patch.object(hass.config.units, 'temperature_unit', + new=TEMP_FAHRENHEIT): + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) await hass.async_block_till_done() @@ -349,3 +367,23 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls): assert call_set_temperature[2] assert call_set_temperature[2].data[ATTR_ENTITY_ID] == entity_id assert call_set_temperature[2].data[ATTR_TEMPERATURE] == 75.2 + + +async def test_get_temperature_range(hass, hk_driver, cls): + """Test if temperature range is evaluated correctly.""" + entity_id = 'climate.test' + + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) + + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_MIN_TEMP: 20, ATTR_MAX_TEMP: 25}) + await hass.async_block_till_done() + assert acc.get_temperature_range() == (20, 25) + + acc._unit = TEMP_FAHRENHEIT + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70}) + await hass.async_block_till_done() + assert acc.get_temperature_range() == (15.6, 21.1)