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_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_MAX_TEMP = 38
@ -236,7 +250,7 @@ class Thermostat(HomeAccessory):
state = self.hass.states.get(self.entity_id)
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]
if CHAR_TARGET_HEATING_COOLING in char_values:
@ -244,19 +258,37 @@ class Thermostat(HomeAccessory):
# Ignore it if its the same mode
if char_values[CHAR_TARGET_HEATING_COOLING] != homekit_hvac_mode:
target_hc = char_values[CHAR_TARGET_HEATING_COOLING]
if target_hc in self.hc_homekit_to_hass:
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]}"
)
else:
_LOGGER.warning(
"The entity: %s does not have a %s mode",
self.entity_id,
target_hc,
)
if target_hc not in self.hc_homekit_to_hass:
# If the target heating cooling state we want does not
# exist on the device, we have to sort it out
# based on the the current and target temperature since
# siri will always send HC_HEAT_COOL_AUTO in this case
# and hope for the best.
hc_target_temp = char_values.get(CHAR_TARGET_TEMPERATURE)
hc_current_temp = _get_current_temperature(state, self._unit)
hc_fallback_order = HC_HEAT_COOL_PREFER_HEAT
if (
hc_target_temp is not None
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:
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)
# Update current temperature
current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE)
if isinstance(current_temp, (int, float)):
current_temp = self._temperature_to_homekit(current_temp)
current_temp = _get_current_temperature(new_state, self._unit)
if current_temp is not None:
if self.char_current_temp.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)
# Update target temperature
target_temp = new_state.attributes.get(ATTR_TEMPERATURE)
if isinstance(target_temp, (int, float)):
target_temp = self._temperature_to_homekit(target_temp)
elif features & SUPPORT_TARGET_TEMPERATURE_RANGE:
target_temp = _get_target_temperature(new_state, self._unit)
if target_temp is None and features & SUPPORT_TARGET_TEMPERATURE_RANGE:
# Homekit expects a target temperature
# even if the device does not support it
hc_hvac_mode = self.char_target_heat_cool.value
@ -566,9 +595,8 @@ class WaterHeater(HomeAccessory):
def async_update_state(self, new_state):
"""Update water_heater state after state change."""
# Update current and target temperature
temperature = new_state.attributes.get(ATTR_TEMPERATURE)
if isinstance(temperature, (int, float)):
temperature = temperature_to_homekit(temperature, self._unit)
temperature = _get_target_temperature(new_state, self._unit)
if temperature is not None:
if temperature != self.char_current_temp.value:
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
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."""
from collections import namedtuple
from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE
import pytest
@ -42,6 +40,14 @@ from homeassistant.components.homekit.const import (
PROP_MIN_STEP,
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.const import (
ATTR_ENTITY_ID,
@ -57,24 +63,9 @@ from homeassistant.helpers import entity_registry
from tests.async_mock import patch
from tests.common import async_mock_service
from tests.components.homekit.common import patch_debounce
@pytest.fixture(scope="module")
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):
async def test_thermostat(hass, hk_driver, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = "climate.test"
@ -94,7 +85,7 @@ async def test_thermostat(hass, hk_driver, cls, events):
},
)
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)
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"
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."""
entity_id = "climate.test"
@ -436,7 +427,7 @@ async def test_thermostat_auto(hass, hk_driver, cls, events):
},
)
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)
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."""
entity_id = "climate.test"
# support_auto = True
hass.states.async_set(entity_id, HVAC_MODE_OFF, {ATTR_SUPPORTED_FEATURES: 4})
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)
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%"
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."""
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()
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)
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
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."""
entity_id = "climate.test"
@ -762,7 +753,7 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events):
)
await hass.async_block_till_done()
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)
await acc.run_handler()
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"
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."""
entity_id = "climate.test"
hass.states.async_set(entity_id, HVAC_MODE_OFF)
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(
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)
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."""
entity_id = "climate.test"
hass.states.async_set(entity_id, HVAC_MODE_OFF, {ATTR_TARGET_TEMP_STEP: 1})
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)
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
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."""
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, {})
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.get_temperature_range() == (7, 35)
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",
}
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.get_temperature_range() == (60.0, 70.0)
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."""
entity_id = "climate.test"
@ -947,7 +938,7 @@ async def test_thermostat_hvac_modes(hass, hk_driver, cls):
)
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)
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
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."""
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")
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)
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
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."""
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")
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)
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
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: [
@ -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
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."""
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()
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)
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()
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."""
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()
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)
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
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."""
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},
)
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)
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"
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."""
entity_id = "water_heater.test"
hass.states.async_set(entity_id, HVAC_MODE_HEAT)
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 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
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."""
entity_id = "water_heater.test"
hass.states.async_set(entity_id, HVAC_MODE_HEAT)
await hass.async_block_till_done()
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 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"
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."""
entity_id = "water_heater.test"
hass.states.async_set(entity_id, HVAC_MODE_HEAT)
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(
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)
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."""
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, {})
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.get_temperature_range() == (7, 35)
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",
}
acc = cls.thermostat(
acc = WaterHeater(
hass, hk_driver, "WaterHeater", "water_heater.all_info_set", 2, None
)
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."""
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()
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)
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
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."""
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()
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)
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
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."""
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()
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)
await acc.run_handler()