From f5cbae0cd50d3e7cb7506db026ea0f31b24d5aab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 14 Jul 2020 10:21:10 -1000 Subject: [PATCH] Avoid homekit crash when temperature is clamped above max value (#37746) --- .../components/homekit/type_thermostats.py | 75 +++++++++---------- .../homekit/test_type_thermostats.py | 54 +++++++++++++ 2 files changed, 90 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 9ef5ae7fa0a..10e4e956328 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -120,21 +120,12 @@ class Thermostat(HomeAccessory): self._state_updates = 0 self.hc_homekit_to_hass = None self.hc_hass_to_homekit = None - min_temp, max_temp = self.get_temperature_range() - - # Homekit only supports 10-38, overwriting - # the max to appears to work, but less than 0 causes - # a crash on the home app - hc_min_temp = max(min_temp, 0) - hc_max_temp = max_temp - - min_humidity = self.hass.states.get(self.entity_id).attributes.get( - ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY - ) + hc_min_temp, hc_max_temp = self.get_temperature_range() # Add additional characteristics if auto mode is supported self.chars = [] state = self.hass.states.get(self.entity_id) + min_humidity = state.attributes.get(ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY) features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if features & SUPPORT_TARGET_TEMPERATURE_RANGE: @@ -370,19 +361,12 @@ class Thermostat(HomeAccessory): 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 = ( - self._temperature_to_homekit(max_temp) if max_temp else DEFAULT_MAX_TEMP + return _get_temperature_range_from_state( + self.hass.states.get(self.entity_id), + self._unit, + DEFAULT_MIN_TEMP, + DEFAULT_MAX_TEMP, ) - max_temp = round(max_temp * 2) / 2 - - min_temp = self.hass.states.get(self.entity_id).attributes.get(ATTR_MIN_TEMP) - min_temp = ( - self._temperature_to_homekit(min_temp) if min_temp else DEFAULT_MIN_TEMP - ) - min_temp = round(min_temp * 2) / 2 - - return min_temp, max_temp def set_target_humidity(self, value): """Set target humidity to value if call came from HomeKit.""" @@ -551,23 +535,12 @@ class WaterHeater(HomeAccessory): 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_WATER_HEATER + return _get_temperature_range_from_state( + self.hass.states.get(self.entity_id), + self._unit, + DEFAULT_MIN_TEMP_WATER_HEATER, + DEFAULT_MAX_TEMP_WATER_HEATER, ) - max_temp = round(max_temp * 2) / 2 - - 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_WATER_HEATER - ) - min_temp = round(min_temp * 2) / 2 - - return min_temp, max_temp def set_heat_cool(self, value): """Change operation mode to value if call came from HomeKit.""" @@ -609,3 +582,27 @@ class WaterHeater(HomeAccessory): operation_mode = new_state.state if operation_mode and self.char_target_heat_cool.value != 1: self.char_target_heat_cool.set_value(1) # Heat + + +def _get_temperature_range_from_state(state, unit, default_min, default_max): + """Calculate the temperature range from a state.""" + min_temp = state.attributes.get(ATTR_MIN_TEMP) + if min_temp: + min_temp = round(temperature_to_homekit(min_temp, unit) * 2) / 2 + else: + min_temp = default_min + + max_temp = state.attributes.get(ATTR_MAX_TEMP) + if max_temp: + max_temp = round(temperature_to_homekit(max_temp, unit) * 2) / 2 + else: + max_temp = default_max + + # Homekit only supports 10-38, overwriting + # the max to appears to work, but less than 0 causes + # a crash on the home app + min_temp = max(min_temp, 0) + if min_temp > max_temp: + max_temp = min_temp + + return min_temp, max_temp diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 8a303ede876..e371fa6fe25 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1617,3 +1617,57 @@ async def test_thermostat_with_no_off_after_recheck(hass, hk_driver, cls, events assert acc.char_target_heat_cool.value == 3 assert acc.char_current_temp.value == 18.0 assert acc.char_display_units.value == 0 + + +async def test_thermostat_with_temp_clamps(hass, hk_driver, cls, events): + """Test that tempatures are clamped to valid values to prevent homekit crash.""" + entity_id = "climate.test" + + hass.states.async_set( + entity_id, + HVAC_MODE_COOL, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE, + ATTR_HVAC_MODES: [], + ATTR_MAX_TEMP: 50, + ATTR_MIN_TEMP: 100, + }, + ) + await hass.async_block_till_done() + acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run_handler() + await hass.async_block_till_done() + + assert acc.char_cooling_thresh_temp.value == 100 + assert acc.char_heating_thresh_temp.value == 100 + + assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] == 100 + assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] == 100 + assert acc.char_cooling_thresh_temp.properties[PROP_MIN_STEP] == 0.1 + assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] == 100 + assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == 100 + assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1 + + assert acc.char_target_heat_cool.value == 2 + + hass.states.async_set( + entity_id, + HVAC_MODE_HEAT_COOL, + { + ATTR_TARGET_TEMP_HIGH: 822.0, + ATTR_TARGET_TEMP_LOW: 20.0, + ATTR_CURRENT_TEMPERATURE: 9918.0, + ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, + ATTR_HVAC_MODES: [HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO], + }, + ) + await hass.async_block_till_done() + assert acc.char_heating_thresh_temp.value == 100.0 + assert acc.char_cooling_thresh_temp.value == 100.0 + assert acc.char_current_heat_cool.value == 1 + assert acc.char_target_heat_cool.value == 3 + assert acc.char_current_temp.value == 1000 + assert acc.char_display_units.value == 0