Translate siri requests to turn on thermostats to valid targets (#44236)

Siri always requests auto mode even when the device does not
support auto which results in the thermostat failing to turn
on as success is assumed.  We now determine the heat cool
target mode based on the current temp, target temp, and
supported modes to ensure the thermostat will reach the
requested target temp.
This commit is contained in:
J. Nick Koston 2020-12-23 17:23:26 -10:00 committed by GitHub
parent 2d131823ce
commit 677fc6e2bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 356 additions and 86 deletions

View File

@ -89,6 +89,20 @@ HC_HEAT_COOL_HEAT = 1
HC_HEAT_COOL_COOL = 2 HC_HEAT_COOL_COOL = 2
HC_HEAT_COOL_AUTO = 3 HC_HEAT_COOL_AUTO = 3
HC_HEAT_COOL_PREFER_HEAT = [
HC_HEAT_COOL_AUTO,
HC_HEAT_COOL_HEAT,
HC_HEAT_COOL_COOL,
HC_HEAT_COOL_OFF,
]
HC_HEAT_COOL_PREFER_COOL = [
HC_HEAT_COOL_AUTO,
HC_HEAT_COOL_COOL,
HC_HEAT_COOL_HEAT,
HC_HEAT_COOL_OFF,
]
HC_MIN_TEMP = 10 HC_MIN_TEMP = 10
HC_MAX_TEMP = 38 HC_MAX_TEMP = 38
@ -236,7 +250,7 @@ class Thermostat(HomeAccessory):
state = self.hass.states.get(self.entity_id) state = self.hass.states.get(self.entity_id)
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
hvac_mode = self.hass.states.get(self.entity_id).state hvac_mode = state.state
homekit_hvac_mode = HC_HASS_TO_HOMEKIT[hvac_mode] homekit_hvac_mode = HC_HASS_TO_HOMEKIT[hvac_mode]
if CHAR_TARGET_HEATING_COOLING in char_values: if CHAR_TARGET_HEATING_COOLING in char_values:
@ -244,19 +258,37 @@ class Thermostat(HomeAccessory):
# Ignore it if its the same mode # Ignore it if its the same mode
if char_values[CHAR_TARGET_HEATING_COOLING] != homekit_hvac_mode: if char_values[CHAR_TARGET_HEATING_COOLING] != homekit_hvac_mode:
target_hc = char_values[CHAR_TARGET_HEATING_COOLING] target_hc = char_values[CHAR_TARGET_HEATING_COOLING]
if target_hc in self.hc_homekit_to_hass: if target_hc not in self.hc_homekit_to_hass:
service = SERVICE_SET_HVAC_MODE_THERMOSTAT # If the target heating cooling state we want does not
hass_value = self.hc_homekit_to_hass[target_hc] # exist on the device, we have to sort it out
params = {ATTR_HVAC_MODE: hass_value} # based on the the current and target temperature since
events.append( # siri will always send HC_HEAT_COOL_AUTO in this case
f"{CHAR_TARGET_HEATING_COOLING} to {char_values[CHAR_TARGET_HEATING_COOLING]}" # and hope for the best.
) hc_target_temp = char_values.get(CHAR_TARGET_TEMPERATURE)
else: hc_current_temp = _get_current_temperature(state, self._unit)
_LOGGER.warning( hc_fallback_order = HC_HEAT_COOL_PREFER_HEAT
"The entity: %s does not have a %s mode", if (
self.entity_id, hc_target_temp is not None
target_hc, and hc_current_temp is not None
) and hc_target_temp < hc_current_temp
):
hc_fallback_order = HC_HEAT_COOL_PREFER_COOL
for hc_fallback in hc_fallback_order:
if hc_fallback in self.hc_homekit_to_hass:
_LOGGER.debug(
"Siri requested target mode: %s and the device does not support, falling back to %s",
target_hc,
hc_fallback,
)
target_hc = hc_fallback
break
service = SERVICE_SET_HVAC_MODE_THERMOSTAT
hass_value = self.hc_homekit_to_hass[target_hc]
params = {ATTR_HVAC_MODE: hass_value}
events.append(
f"{CHAR_TARGET_HEATING_COOLING} to {char_values[CHAR_TARGET_HEATING_COOLING]}"
)
if CHAR_TARGET_TEMPERATURE in char_values: if CHAR_TARGET_TEMPERATURE in char_values:
hc_target_temp = char_values[CHAR_TARGET_TEMPERATURE] hc_target_temp = char_values[CHAR_TARGET_TEMPERATURE]
@ -429,9 +461,8 @@ class Thermostat(HomeAccessory):
self.char_current_heat_cool.set_value(homekit_hvac_action) self.char_current_heat_cool.set_value(homekit_hvac_action)
# Update current temperature # Update current temperature
current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE) current_temp = _get_current_temperature(new_state, self._unit)
if isinstance(current_temp, (int, float)): if current_temp is not None:
current_temp = self._temperature_to_homekit(current_temp)
if self.char_current_temp.value != current_temp: if self.char_current_temp.value != current_temp:
self.char_current_temp.set_value(current_temp) self.char_current_temp.set_value(current_temp)
@ -466,10 +497,8 @@ class Thermostat(HomeAccessory):
self.char_heating_thresh_temp.set_value(heating_thresh) self.char_heating_thresh_temp.set_value(heating_thresh)
# Update target temperature # Update target temperature
target_temp = new_state.attributes.get(ATTR_TEMPERATURE) target_temp = _get_target_temperature(new_state, self._unit)
if isinstance(target_temp, (int, float)): if target_temp is None and features & SUPPORT_TARGET_TEMPERATURE_RANGE:
target_temp = self._temperature_to_homekit(target_temp)
elif features & SUPPORT_TARGET_TEMPERATURE_RANGE:
# Homekit expects a target temperature # Homekit expects a target temperature
# even if the device does not support it # even if the device does not support it
hc_hvac_mode = self.char_target_heat_cool.value hc_hvac_mode = self.char_target_heat_cool.value
@ -566,9 +595,8 @@ class WaterHeater(HomeAccessory):
def async_update_state(self, new_state): def async_update_state(self, new_state):
"""Update water_heater state after state change.""" """Update water_heater state after state change."""
# Update current and target temperature # Update current and target temperature
temperature = new_state.attributes.get(ATTR_TEMPERATURE) temperature = _get_target_temperature(new_state, self._unit)
if isinstance(temperature, (int, float)): if temperature is not None:
temperature = temperature_to_homekit(temperature, self._unit)
if temperature != self.char_current_temp.value: if temperature != self.char_current_temp.value:
self.char_target_temp.set_value(temperature) self.char_target_temp.set_value(temperature)
@ -606,3 +634,19 @@ def _get_temperature_range_from_state(state, unit, default_min, default_max):
max_temp = min_temp max_temp = min_temp
return min_temp, max_temp return min_temp, max_temp
def _get_target_temperature(state, unit):
"""Calculate the target temperature from a state."""
target_temp = state.attributes.get(ATTR_TEMPERATURE)
if isinstance(target_temp, (int, float)):
return temperature_to_homekit(target_temp, unit)
return None
def _get_current_temperature(state, unit):
"""Calculate the current temperature from a state."""
target_temp = state.attributes.get(ATTR_CURRENT_TEMPERATURE)
if isinstance(target_temp, (int, float)):
return temperature_to_homekit(target_temp, unit)
return None

View File

@ -1,6 +1,4 @@
"""Test different accessory types: Thermostats.""" """Test different accessory types: Thermostats."""
from collections import namedtuple
from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE
import pytest import pytest
@ -42,6 +40,14 @@ from homeassistant.components.homekit.const import (
PROP_MIN_STEP, PROP_MIN_STEP,
PROP_MIN_VALUE, PROP_MIN_VALUE,
) )
from homeassistant.components.homekit.type_thermostats import (
HC_HEAT_COOL_AUTO,
HC_HEAT_COOL_COOL,
HC_HEAT_COOL_HEAT,
HC_HEAT_COOL_OFF,
Thermostat,
WaterHeater,
)
from homeassistant.components.water_heater import DOMAIN as DOMAIN_WATER_HEATER from homeassistant.components.water_heater import DOMAIN as DOMAIN_WATER_HEATER
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
@ -57,24 +63,9 @@ from homeassistant.helpers import entity_registry
from tests.async_mock import patch from tests.async_mock import patch
from tests.common import async_mock_service from tests.common import async_mock_service
from tests.components.homekit.common import patch_debounce
@pytest.fixture(scope="module") async def test_thermostat(hass, hk_driver, events):
def cls():
"""Patch debounce decorator during import of type_thermostats."""
patcher = patch_debounce()
patcher.start()
_import = __import__(
"homeassistant.components.homekit.type_thermostats",
fromlist=["WaterHeater", "Thermostat"],
)
patcher_tuple = namedtuple("Cls", ["water_heater", "thermostat"])
yield patcher_tuple(thermostat=_import.Thermostat, water_heater=_import.WaterHeater)
patcher.stop()
async def test_thermostat(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly.""" """Test if accessory and HA are updated accordingly."""
entity_id = "climate.test" entity_id = "climate.test"
@ -94,7 +85,7 @@ async def test_thermostat(hass, hk_driver, cls, events):
}, },
) )
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc) hk_driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
@ -414,7 +405,7 @@ async def test_thermostat(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] == "TargetHeatingCoolingState to 3" assert events[-1].data[ATTR_VALUE] == "TargetHeatingCoolingState to 3"
async def test_thermostat_auto(hass, hk_driver, cls, events): async def test_thermostat_auto(hass, hk_driver, events):
"""Test if accessory and HA are updated accordingly.""" """Test if accessory and HA are updated accordingly."""
entity_id = "climate.test" entity_id = "climate.test"
@ -436,7 +427,7 @@ async def test_thermostat_auto(hass, hk_driver, cls, events):
}, },
) )
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc) hk_driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
@ -568,14 +559,14 @@ async def test_thermostat_auto(hass, hk_driver, cls, events):
) )
async def test_thermostat_humidity(hass, hk_driver, cls, events): async def test_thermostat_humidity(hass, hk_driver, events):
"""Test if accessory and HA are updated accordingly with humidity.""" """Test if accessory and HA are updated accordingly with humidity."""
entity_id = "climate.test" entity_id = "climate.test"
# support_auto = True # support_auto = True
hass.states.async_set(entity_id, HVAC_MODE_OFF, {ATTR_SUPPORTED_FEATURES: 4}) hass.states.async_set(entity_id, HVAC_MODE_OFF, {ATTR_SUPPORTED_FEATURES: 4})
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc) hk_driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
@ -627,7 +618,7 @@ async def test_thermostat_humidity(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] == "35%" assert events[-1].data[ATTR_VALUE] == "35%"
async def test_thermostat_power_state(hass, hk_driver, cls, events): async def test_thermostat_power_state(hass, hk_driver, events):
"""Test if accessory and HA are updated accordingly.""" """Test if accessory and HA are updated accordingly."""
entity_id = "climate.test" entity_id = "climate.test"
@ -650,7 +641,7 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events):
}, },
) )
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc) hk_driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
@ -747,7 +738,7 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events):
assert acc.char_target_heat_cool.value == 2 assert acc.char_target_heat_cool.value == 2
async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): async def test_thermostat_fahrenheit(hass, hk_driver, events):
"""Test if accessory and HA are updated accordingly.""" """Test if accessory and HA are updated accordingly."""
entity_id = "climate.test" entity_id = "climate.test"
@ -762,7 +753,7 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events):
) )
await hass.async_block_till_done() await hass.async_block_till_done()
with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT, new=TEMP_FAHRENHEIT): with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT, new=TEMP_FAHRENHEIT):
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc) hk_driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
await hass.async_block_till_done() await hass.async_block_till_done()
@ -856,13 +847,13 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] == "TargetTemperature to 24.0°C" assert events[-1].data[ATTR_VALUE] == "TargetTemperature to 24.0°C"
async def test_thermostat_get_temperature_range(hass, hk_driver, cls): async def test_thermostat_get_temperature_range(hass, hk_driver):
"""Test if temperature range is evaluated correctly.""" """Test if temperature range is evaluated correctly."""
entity_id = "climate.test" entity_id = "climate.test"
hass.states.async_set(entity_id, HVAC_MODE_OFF) hass.states.async_set(entity_id, HVAC_MODE_OFF)
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None) acc = Thermostat(hass, hk_driver, "Climate", entity_id, 2, None)
hass.states.async_set( hass.states.async_set(
entity_id, HVAC_MODE_OFF, {ATTR_MIN_TEMP: 20, ATTR_MAX_TEMP: 25} entity_id, HVAC_MODE_OFF, {ATTR_MIN_TEMP: 20, ATTR_MAX_TEMP: 25}
@ -878,13 +869,13 @@ async def test_thermostat_get_temperature_range(hass, hk_driver, cls):
assert acc.get_temperature_range() == (15.5, 21.0) assert acc.get_temperature_range() == (15.5, 21.0)
async def test_thermostat_temperature_step_whole(hass, hk_driver, cls): async def test_thermostat_temperature_step_whole(hass, hk_driver):
"""Test climate device with single digit precision.""" """Test climate device with single digit precision."""
entity_id = "climate.test" entity_id = "climate.test"
hass.states.async_set(entity_id, HVAC_MODE_OFF, {ATTR_TARGET_TEMP_STEP: 1}) hass.states.async_set(entity_id, HVAC_MODE_OFF, {ATTR_TARGET_TEMP_STEP: 1})
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc) hk_driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
@ -893,7 +884,7 @@ async def test_thermostat_temperature_step_whole(hass, hk_driver, cls):
assert acc.char_target_temp.properties[PROP_MIN_STEP] == 0.1 assert acc.char_target_temp.properties[PROP_MIN_STEP] == 0.1
async def test_thermostat_restore(hass, hk_driver, cls, events): async def test_thermostat_restore(hass, hk_driver, events):
"""Test setting up an entity from state in the event registry.""" """Test setting up an entity from state in the event registry."""
hass.state = CoreState.not_running hass.state = CoreState.not_running
@ -919,7 +910,7 @@ async def test_thermostat_restore(hass, hk_driver, cls, events):
hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {})
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", "climate.simple", 2, None) acc = Thermostat(hass, hk_driver, "Climate", "climate.simple", 2, None)
assert acc.category == 9 assert acc.category == 9
assert acc.get_temperature_range() == (7, 35) assert acc.get_temperature_range() == (7, 35)
assert set(acc.char_target_heat_cool.properties["ValidValues"].keys()) == { assert set(acc.char_target_heat_cool.properties["ValidValues"].keys()) == {
@ -929,7 +920,7 @@ async def test_thermostat_restore(hass, hk_driver, cls, events):
"off", "off",
} }
acc = cls.thermostat(hass, hk_driver, "Climate", "climate.all_info_set", 2, None) acc = Thermostat(hass, hk_driver, "Climate", "climate.all_info_set", 2, None)
assert acc.category == 9 assert acc.category == 9
assert acc.get_temperature_range() == (60.0, 70.0) assert acc.get_temperature_range() == (60.0, 70.0)
assert set(acc.char_target_heat_cool.properties["ValidValues"].keys()) == { assert set(acc.char_target_heat_cool.properties["ValidValues"].keys()) == {
@ -938,7 +929,7 @@ async def test_thermostat_restore(hass, hk_driver, cls, events):
} }
async def test_thermostat_hvac_modes(hass, hk_driver, cls): async def test_thermostat_hvac_modes(hass, hk_driver):
"""Test if unsupported HVAC modes are deactivated in HomeKit.""" """Test if unsupported HVAC modes are deactivated in HomeKit."""
entity_id = "climate.test" entity_id = "climate.test"
@ -947,7 +938,7 @@ async def test_thermostat_hvac_modes(hass, hk_driver, cls):
) )
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc) hk_driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
@ -971,7 +962,7 @@ async def test_thermostat_hvac_modes(hass, hk_driver, cls):
assert acc.char_target_heat_cool.value == 1 assert acc.char_target_heat_cool.value == 1
async def test_thermostat_hvac_modes_with_auto_heat_cool(hass, hk_driver, cls): async def test_thermostat_hvac_modes_with_auto_heat_cool(hass, hk_driver):
"""Test we get heat cool over auto.""" """Test we get heat cool over auto."""
entity_id = "climate.test" entity_id = "climate.test"
@ -990,7 +981,7 @@ async def test_thermostat_hvac_modes_with_auto_heat_cool(hass, hk_driver, cls):
call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode")
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc) hk_driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
@ -1034,7 +1025,7 @@ async def test_thermostat_hvac_modes_with_auto_heat_cool(hass, hk_driver, cls):
assert acc.char_target_heat_cool.value == 3 assert acc.char_target_heat_cool.value == 3
async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver, cls): async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver):
"""Test we get auto when there is no heat cool.""" """Test we get auto when there is no heat cool."""
entity_id = "climate.test" entity_id = "climate.test"
@ -1046,7 +1037,7 @@ async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver, cls
call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode")
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc) hk_driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
@ -1069,7 +1060,8 @@ async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver, cls
assert acc.char_target_heat_cool.value == 1 assert acc.char_target_heat_cool.value == 1
char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID] char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID]
call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode")
await hass.async_block_till_done()
hk_driver.set_characteristics( hk_driver.set_characteristics(
{ {
HAP_REPR_CHARS: [ HAP_REPR_CHARS: [
@ -1090,7 +1082,7 @@ async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver, cls
assert acc.char_target_heat_cool.value == 3 assert acc.char_target_heat_cool.value == 3
async def test_thermostat_hvac_modes_with_auto_only(hass, hk_driver, cls): async def test_thermostat_hvac_modes_with_auto_only(hass, hk_driver):
"""Test if unsupported HVAC modes are deactivated in HomeKit.""" """Test if unsupported HVAC modes are deactivated in HomeKit."""
entity_id = "climate.test" entity_id = "climate.test"
@ -1099,7 +1091,7 @@ async def test_thermostat_hvac_modes_with_auto_only(hass, hk_driver, cls):
) )
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc) hk_driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
@ -1122,8 +1114,242 @@ async def test_thermostat_hvac_modes_with_auto_only(hass, hk_driver, cls):
await hass.async_block_till_done() await hass.async_block_till_done()
assert acc.char_target_heat_cool.value == 3 assert acc.char_target_heat_cool.value == 3
char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID]
call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode")
await hass.async_block_till_done()
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_heat_cool_iid,
HAP_REPR_VALUE: HC_HEAT_COOL_HEAT,
},
]
},
"mock_addr",
)
async def test_thermostat_hvac_modes_without_off(hass, hk_driver, cls): await hass.async_block_till_done()
assert call_set_hvac_mode
assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_AUTO
async def test_thermostat_hvac_modes_with_heat_only(hass, hk_driver):
"""Test if unsupported HVAC modes are deactivated in HomeKit and siri calls get converted to heat."""
entity_id = "climate.test"
hass.states.async_set(
entity_id, HVAC_MODE_HEAT, {ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_OFF]}
)
await hass.async_block_till_done()
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
await acc.run_handler()
await hass.async_block_till_done()
hap = acc.char_target_heat_cool.to_HAP()
assert hap["valid-values"] == [HC_HEAT_COOL_OFF, HC_HEAT_COOL_HEAT]
assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT
await hass.async_add_executor_job(
acc.char_target_heat_cool.set_value, HC_HEAT_COOL_HEAT
)
await hass.async_block_till_done()
assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT
with pytest.raises(ValueError):
await hass.async_add_executor_job(
acc.char_target_heat_cool.set_value, HC_HEAT_COOL_COOL
)
await hass.async_block_till_done()
assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT
with pytest.raises(ValueError):
await hass.async_add_executor_job(
acc.char_target_heat_cool.set_value, HC_HEAT_COOL_AUTO
)
await hass.async_block_till_done()
assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT
char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID]
call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode")
await hass.async_block_till_done()
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_heat_cool_iid,
HAP_REPR_VALUE: HC_HEAT_COOL_AUTO,
},
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert call_set_hvac_mode
assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT
async def test_thermostat_hvac_modes_with_cool_only(hass, hk_driver):
"""Test if unsupported HVAC modes are deactivated in HomeKit and siri calls get converted to cool."""
entity_id = "climate.test"
hass.states.async_set(
entity_id, HVAC_MODE_COOL, {ATTR_HVAC_MODES: [HVAC_MODE_COOL, HVAC_MODE_OFF]}
)
await hass.async_block_till_done()
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
await acc.run_handler()
await hass.async_block_till_done()
hap = acc.char_target_heat_cool.to_HAP()
assert hap["valid-values"] == [HC_HEAT_COOL_OFF, HC_HEAT_COOL_COOL]
assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL
await hass.async_add_executor_job(
acc.char_target_heat_cool.set_value, HC_HEAT_COOL_COOL
)
await hass.async_block_till_done()
assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL
with pytest.raises(ValueError):
await hass.async_add_executor_job(
acc.char_target_heat_cool.set_value, HC_HEAT_COOL_AUTO
)
await hass.async_block_till_done()
assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL
with pytest.raises(ValueError):
await hass.async_add_executor_job(
acc.char_target_heat_cool.set_value, HC_HEAT_COOL_HEAT
)
await hass.async_block_till_done()
assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL
char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID]
call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode")
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_heat_cool_iid,
HAP_REPR_VALUE: HC_HEAT_COOL_AUTO,
},
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert call_set_hvac_mode
assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_COOL
async def test_thermostat_hvac_modes_with_heat_cool_only(hass, hk_driver):
"""Test if unsupported HVAC modes are deactivated in HomeKit and siri calls get converted to heat or cool."""
entity_id = "climate.test"
hass.states.async_set(
entity_id,
HVAC_MODE_COOL,
{
ATTR_CURRENT_TEMPERATURE: 30,
ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_OFF],
},
)
await hass.async_block_till_done()
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
await acc.run_handler()
await hass.async_block_till_done()
hap = acc.char_target_heat_cool.to_HAP()
assert hap["valid-values"] == [
HC_HEAT_COOL_OFF,
HC_HEAT_COOL_HEAT,
HC_HEAT_COOL_COOL,
]
assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL
await hass.async_add_executor_job(
acc.char_target_heat_cool.set_value, HC_HEAT_COOL_COOL
)
await hass.async_block_till_done()
assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL
with pytest.raises(ValueError):
await hass.async_add_executor_job(
acc.char_target_heat_cool.set_value, HC_HEAT_COOL_AUTO
)
await hass.async_block_till_done()
assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL
await hass.async_add_executor_job(
acc.char_target_heat_cool.set_value, HC_HEAT_COOL_HEAT
)
await hass.async_block_till_done()
assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT
char_target_temp_iid = acc.char_target_temp.to_HAP()[HAP_REPR_IID]
char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID]
call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode")
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_heat_cool_iid,
HAP_REPR_VALUE: HC_HEAT_COOL_AUTO,
},
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_temp_iid,
HAP_REPR_VALUE: 0,
},
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert call_set_hvac_mode
assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_COOL
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_heat_cool_iid,
HAP_REPR_VALUE: HC_HEAT_COOL_AUTO,
},
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_temp_iid,
HAP_REPR_VALUE: 200,
},
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert call_set_hvac_mode
assert call_set_hvac_mode[1].data[ATTR_ENTITY_ID] == entity_id
assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT
async def test_thermostat_hvac_modes_without_off(hass, hk_driver):
"""Test a thermostat that has no off.""" """Test a thermostat that has no off."""
entity_id = "climate.test" entity_id = "climate.test"
@ -1132,7 +1358,7 @@ async def test_thermostat_hvac_modes_without_off(hass, hk_driver, cls):
) )
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc) hk_driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
@ -1160,7 +1386,7 @@ async def test_thermostat_hvac_modes_without_off(hass, hk_driver, cls):
assert acc.char_target_heat_cool.value == 1 assert acc.char_target_heat_cool.value == 1
async def test_thermostat_without_target_temp_only_range(hass, hk_driver, cls, events): async def test_thermostat_without_target_temp_only_range(hass, hk_driver, events):
"""Test a thermostat that only supports a range.""" """Test a thermostat that only supports a range."""
entity_id = "climate.test" entity_id = "climate.test"
@ -1171,7 +1397,7 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, cls, e
{ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE}, {ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE},
) )
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc) hk_driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
@ -1342,13 +1568,13 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, cls, e
assert events[-1].data[ATTR_VALUE] == "HeatingThresholdTemperature to 27.0°C" assert events[-1].data[ATTR_VALUE] == "HeatingThresholdTemperature to 27.0°C"
async def test_water_heater(hass, hk_driver, cls, events): async def test_water_heater(hass, hk_driver, events):
"""Test if accessory and HA are updated accordingly.""" """Test if accessory and HA are updated accordingly."""
entity_id = "water_heater.test" entity_id = "water_heater.test"
hass.states.async_set(entity_id, HVAC_MODE_HEAT) hass.states.async_set(entity_id, HVAC_MODE_HEAT)
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.water_heater(hass, hk_driver, "WaterHeater", entity_id, 2, None) acc = WaterHeater(hass, hk_driver, "WaterHeater", entity_id, 2, None)
await acc.run_handler() await acc.run_handler()
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1416,14 +1642,14 @@ async def test_water_heater(hass, hk_driver, cls, events):
assert acc.char_target_heat_cool.value == 1 assert acc.char_target_heat_cool.value == 1
async def test_water_heater_fahrenheit(hass, hk_driver, cls, events): async def test_water_heater_fahrenheit(hass, hk_driver, events):
"""Test if accessory and HA are update accordingly.""" """Test if accessory and HA are update accordingly."""
entity_id = "water_heater.test" entity_id = "water_heater.test"
hass.states.async_set(entity_id, HVAC_MODE_HEAT) hass.states.async_set(entity_id, HVAC_MODE_HEAT)
await hass.async_block_till_done() await hass.async_block_till_done()
with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT, new=TEMP_FAHRENHEIT): with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT, new=TEMP_FAHRENHEIT):
acc = cls.water_heater(hass, hk_driver, "WaterHeater", entity_id, 2, None) acc = WaterHeater(hass, hk_driver, "WaterHeater", entity_id, 2, None)
await acc.run_handler() await acc.run_handler()
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1448,13 +1674,13 @@ async def test_water_heater_fahrenheit(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] == "140.0°F" assert events[-1].data[ATTR_VALUE] == "140.0°F"
async def test_water_heater_get_temperature_range(hass, hk_driver, cls): async def test_water_heater_get_temperature_range(hass, hk_driver):
"""Test if temperature range is evaluated correctly.""" """Test if temperature range is evaluated correctly."""
entity_id = "water_heater.test" entity_id = "water_heater.test"
hass.states.async_set(entity_id, HVAC_MODE_HEAT) hass.states.async_set(entity_id, HVAC_MODE_HEAT)
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "WaterHeater", entity_id, 2, None) acc = WaterHeater(hass, hk_driver, "WaterHeater", entity_id, 2, None)
hass.states.async_set( hass.states.async_set(
entity_id, HVAC_MODE_HEAT, {ATTR_MIN_TEMP: 20, ATTR_MAX_TEMP: 25} entity_id, HVAC_MODE_HEAT, {ATTR_MIN_TEMP: 20, ATTR_MAX_TEMP: 25}
@ -1470,7 +1696,7 @@ async def test_water_heater_get_temperature_range(hass, hk_driver, cls):
assert acc.get_temperature_range() == (15.5, 21.0) assert acc.get_temperature_range() == (15.5, 21.0)
async def test_water_heater_restore(hass, hk_driver, cls, events): async def test_water_heater_restore(hass, hk_driver, events):
"""Test setting up an entity from state in the event registry.""" """Test setting up an entity from state in the event registry."""
hass.state = CoreState.not_running hass.state = CoreState.not_running
@ -1492,7 +1718,7 @@ async def test_water_heater_restore(hass, hk_driver, cls, events):
hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {})
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "WaterHeater", "water_heater.simple", 2, None) acc = Thermostat(hass, hk_driver, "WaterHeater", "water_heater.simple", 2, None)
assert acc.category == 9 assert acc.category == 9
assert acc.get_temperature_range() == (7, 35) assert acc.get_temperature_range() == (7, 35)
assert set(acc.char_current_heat_cool.properties["ValidValues"].keys()) == { assert set(acc.char_current_heat_cool.properties["ValidValues"].keys()) == {
@ -1501,7 +1727,7 @@ async def test_water_heater_restore(hass, hk_driver, cls, events):
"Off", "Off",
} }
acc = cls.thermostat( acc = WaterHeater(
hass, hk_driver, "WaterHeater", "water_heater.all_info_set", 2, None hass, hk_driver, "WaterHeater", "water_heater.all_info_set", 2, None
) )
assert acc.category == 9 assert acc.category == 9
@ -1513,7 +1739,7 @@ async def test_water_heater_restore(hass, hk_driver, cls, events):
} }
async def test_thermostat_with_no_modes_when_we_first_see(hass, hk_driver, cls, events): async def test_thermostat_with_no_modes_when_we_first_see(hass, hk_driver, events):
"""Test if a thermostat that is not ready when we first see it.""" """Test if a thermostat that is not ready when we first see it."""
entity_id = "climate.test" entity_id = "climate.test"
@ -1528,7 +1754,7 @@ async def test_thermostat_with_no_modes_when_we_first_see(hass, hk_driver, cls,
}, },
) )
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc) hk_driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
@ -1566,7 +1792,7 @@ async def test_thermostat_with_no_modes_when_we_first_see(hass, hk_driver, cls,
assert acc.char_display_units.value == 0 assert acc.char_display_units.value == 0
async def test_thermostat_with_no_off_after_recheck(hass, hk_driver, cls, events): async def test_thermostat_with_no_off_after_recheck(hass, hk_driver, events):
"""Test if a thermostat that is not ready when we first see it that actually does not have off.""" """Test if a thermostat that is not ready when we first see it that actually does not have off."""
entity_id = "climate.test" entity_id = "climate.test"
@ -1581,7 +1807,7 @@ async def test_thermostat_with_no_off_after_recheck(hass, hk_driver, cls, events
}, },
) )
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc) hk_driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
@ -1619,7 +1845,7 @@ async def test_thermostat_with_no_off_after_recheck(hass, hk_driver, cls, events
assert acc.char_display_units.value == 0 assert acc.char_display_units.value == 0
async def test_thermostat_with_temp_clamps(hass, hk_driver, cls, events): async def test_thermostat_with_temp_clamps(hass, hk_driver, events):
"""Test that tempatures are clamped to valid values to prevent homekit crash.""" """Test that tempatures are clamped to valid values to prevent homekit crash."""
entity_id = "climate.test" entity_id = "climate.test"
@ -1635,7 +1861,7 @@ async def test_thermostat_with_temp_clamps(hass, hk_driver, cls, events):
}, },
) )
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc) hk_driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()