Limit available heat/cool modes for HomeKit thermostats (#28586)

* Limit available heat/cool modes for HomeKit thermostats.
The Home app only shows appropriate modes (heat/cool/auto) for the device. Depending on the climate integration, disabling the auto start might be needed.

* Include improved mapping for HVAC modes in tests
This commit is contained in:
David K 2019-12-06 14:07:45 +01:00 committed by Pascal Vizeli
parent 27530be46f
commit c5f4872aea
2 changed files with 115 additions and 29 deletions

View file

@ -7,6 +7,7 @@ from homeassistant.components.climate.const import (
ATTR_CURRENT_TEMPERATURE,
ATTR_HVAC_ACTION,
ATTR_HVAC_MODE,
ATTR_HVAC_MODES,
ATTR_MAX_TEMP,
ATTR_MIN_TEMP,
ATTR_TARGET_TEMP_HIGH,
@ -23,6 +24,8 @@ from homeassistant.components.climate.const import (
HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
HVAC_MODE_AUTO,
HVAC_MODE_FAN_ONLY,
SERVICE_SET_HVAC_MODE as SERVICE_SET_HVAC_MODE_THERMOSTAT,
SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT,
SUPPORT_TARGET_TEMPERATURE_RANGE,
@ -60,13 +63,18 @@ from .util import temperature_to_homekit, temperature_to_states
_LOGGER = logging.getLogger(__name__)
HC_HOMEKIT_VALID_MODES_WATER_HEATER = {
"Heat": 1,
}
UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1}
UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()}
HC_HASS_TO_HOMEKIT = {
HVAC_MODE_OFF: 0,
HVAC_MODE_HEAT: 1,
HVAC_MODE_COOL: 2,
HVAC_MODE_AUTO: 3,
HVAC_MODE_HEAT_COOL: 3,
HVAC_MODE_FAN_ONLY: 2,
}
HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()}
@ -97,9 +105,9 @@ class Thermostat(HomeAccessory):
# Add additional characteristics if auto mode is supported
self.chars = []
features = self.hass.states.get(self.entity_id).attributes.get(
ATTR_SUPPORTED_FEATURES, 0
)
state = self.hass.states.get(self.entity_id)
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if features & SUPPORT_TARGET_TEMPERATURE_RANGE:
self.chars.extend(
(CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
@ -107,12 +115,44 @@ class Thermostat(HomeAccessory):
serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars)
# Current and target mode characteristics
# Current mode characteristics
self.char_current_heat_cool = serv_thermostat.configure_char(
CHAR_CURRENT_HEATING_COOLING, value=0
)
# Target mode characteristics
hc_modes = state.attributes.get(ATTR_HVAC_MODES, None)
if hc_modes is None:
_LOGGER.error(
"%s: HVAC modes not yet available. Please disable auto start for homekit.",
self.entity_id,
)
hc_modes = (
HVAC_MODE_HEAT,
HVAC_MODE_COOL,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
)
# determine available modes for this entity, prefer AUTO over HEAT_COOL and COOL over FAN_ONLY
self.hc_homekit_to_hass = {
c: s
for s, c in HC_HASS_TO_HOMEKIT.items()
if (
s in hc_modes
and not (
(s == HVAC_MODE_HEAT_COOL and HVAC_MODE_AUTO in hc_modes)
or (s == HVAC_MODE_FAN_ONLY and HVAC_MODE_COOL in hc_modes)
)
)
}
hc_valid_values = {k: v for v, k in self.hc_homekit_to_hass.items()}
self.char_target_heat_cool = serv_thermostat.configure_char(
CHAR_TARGET_HEATING_COOLING, value=0, setter_callback=self.set_heat_cool
CHAR_TARGET_HEATING_COOLING,
value=0,
setter_callback=self.set_heat_cool,
valid_values=hc_valid_values,
)
# Current and target temperature characteristics
@ -185,7 +225,7 @@ class Thermostat(HomeAccessory):
"""Change operation mode to value if call came from HomeKit."""
_LOGGER.debug("%s: Set heat-cool to %d", self.entity_id, value)
self._flag_heat_cool = True
hass_value = HC_HOMEKIT_TO_HASS[value]
hass_value = self.hc_homekit_to_hass[value]
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HVAC_MODE: hass_value}
self.call_service(
DOMAIN_CLIMATE, SERVICE_SET_HVAC_MODE_THERMOSTAT, params, hass_value
@ -318,7 +358,10 @@ class WaterHeater(HomeAccessory):
CHAR_CURRENT_HEATING_COOLING, value=1
)
self.char_target_heat_cool = serv_thermostat.configure_char(
CHAR_TARGET_HEATING_COOLING, value=1, setter_callback=self.set_heat_cool
CHAR_TARGET_HEATING_COOLING,
value=1,
setter_callback=self.set_heat_cool,
valid_values=HC_HOMEKIT_VALID_MODES_WATER_HEATER,
)
self.char_current_temp = serv_thermostat.configure_char(

View file

@ -24,6 +24,8 @@ from homeassistant.components.climate.const import (
HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_AUTO,
)
from homeassistant.components.homekit.const import (
ATTR_VALUE,
@ -64,7 +66,20 @@ async def test_thermostat(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = "climate.test"
hass.states.async_set(entity_id, HVAC_MODE_OFF)
hass.states.async_set(
entity_id,
HVAC_MODE_OFF,
{
ATTR_HVAC_MODES: [
HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_COOL,
HVAC_MODE_OFF,
HVAC_MODE_AUTO,
],
},
)
await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None)
await hass.async_add_job(acc.run)
@ -120,7 +135,7 @@ async def test_thermostat(hass, hk_driver, cls, events):
hass.states.async_set(
entity_id,
HVAC_MODE_COOL,
HVAC_MODE_FAN_ONLY,
{
ATTR_TEMPERATURE: 20.0,
ATTR_CURRENT_TEMPERATURE: 25.0,
@ -164,9 +179,8 @@ async def test_thermostat(hass, hk_driver, cls, events):
hass.states.async_set(
entity_id,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_AUTO,
{
ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL],
ATTR_TEMPERATURE: 22.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT,
@ -183,7 +197,6 @@ async def test_thermostat(hass, hk_driver, cls, events):
entity_id,
HVAC_MODE_HEAT_COOL,
{
ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL],
ATTR_TEMPERATURE: 22.0,
ATTR_CURRENT_TEMPERATURE: 25.0,
ATTR_HVAC_ACTION: CURRENT_HVAC_COOL,
@ -198,9 +211,8 @@ async def test_thermostat(hass, hk_driver, cls, events):
hass.states.async_set(
entity_id,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_AUTO,
{
ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL],
ATTR_TEMPERATURE: 22.0,
ATTR_CURRENT_TEMPERATURE: 22.0,
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
@ -226,14 +238,23 @@ async def test_thermostat(hass, hk_driver, cls, events):
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "19.0°C"
await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 1)
await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 2)
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
assert acc.char_target_heat_cool.value == 1
assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_COOL
assert acc.char_target_heat_cool.value == 2
assert len(events) == 2
assert events[-1].data[ATTR_VALUE] == HVAC_MODE_HEAT
assert events[-1].data[ATTR_VALUE] == HVAC_MODE_COOL
await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 3)
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_AUTO
assert acc.char_target_heat_cool.value == 3
assert len(events) == 3
assert events[-1].data[ATTR_VALUE] == HVAC_MODE_AUTO
async def test_thermostat_auto(hass, hk_driver, cls, events):
@ -261,7 +282,6 @@ async def test_thermostat_auto(hass, hk_driver, cls, events):
entity_id,
HVAC_MODE_HEAT_COOL,
{
ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL,
ATTR_TARGET_TEMP_HIGH: 22.0,
ATTR_TARGET_TEMP_LOW: 20.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
@ -278,9 +298,8 @@ async def test_thermostat_auto(hass, hk_driver, cls, events):
hass.states.async_set(
entity_id,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_COOL,
{
ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL,
ATTR_TARGET_TEMP_HIGH: 23.0,
ATTR_TARGET_TEMP_LOW: 19.0,
ATTR_CURRENT_TEMPERATURE: 24.0,
@ -291,15 +310,14 @@ async def test_thermostat_auto(hass, hk_driver, cls, events):
assert acc.char_heating_thresh_temp.value == 19.0
assert acc.char_cooling_thresh_temp.value == 23.0
assert acc.char_current_heat_cool.value == 2
assert acc.char_target_heat_cool.value == 3
assert acc.char_target_heat_cool.value == 2
assert acc.char_current_temp.value == 24.0
assert acc.char_display_units.value == 0
hass.states.async_set(
entity_id,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_AUTO,
{
ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL,
ATTR_TARGET_TEMP_HIGH: 23.0,
ATTR_TARGET_TEMP_LOW: 19.0,
ATTR_CURRENT_TEMPERATURE: 21.0,
@ -346,7 +364,6 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events):
HVAC_MODE_HEAT,
{
ATTR_SUPPORTED_FEATURES: 4096,
ATTR_HVAC_MODE: HVAC_MODE_HEAT,
ATTR_TEMPERATURE: 23.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT,
@ -364,7 +381,6 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events):
entity_id,
HVAC_MODE_OFF,
{
ATTR_HVAC_MODE: HVAC_MODE_HEAT,
ATTR_TEMPERATURE: 23.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
@ -378,7 +394,6 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events):
entity_id,
HVAC_MODE_OFF,
{
ATTR_HVAC_MODE: HVAC_MODE_OFF,
ATTR_TEMPERATURE: 23.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
@ -423,7 +438,6 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events):
entity_id,
HVAC_MODE_HEAT_COOL,
{
ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL,
ATTR_TARGET_TEMP_HIGH: 75.2,
ATTR_TARGET_TEMP_LOW: 68.1,
ATTR_TEMPERATURE: 71.6,
@ -503,6 +517,34 @@ async def test_thermostat_temperature_step_whole(hass, hk_driver, cls):
assert acc.char_target_temp.properties[PROP_MIN_STEP] == 1.0
async def test_thermostat_hvac_modes(hass, hk_driver, cls):
"""Test if unsupported HVAC modes are deactivated in HomeKit."""
entity_id = "climate.test"
hass.states.async_set(
entity_id, HVAC_MODE_OFF, {ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_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)
await hass.async_block_till_done()
with pytest.raises(ValueError):
await hass.async_add_job(acc.char_target_heat_cool.set_value, 3)
await hass.async_block_till_done()
assert acc.char_target_heat_cool.value == 0
await hass.async_add_job(acc.char_target_heat_cool.set_value, 1)
await hass.async_block_till_done()
assert acc.char_target_heat_cool.value == 1
with pytest.raises(ValueError):
await hass.async_add_job(acc.char_target_heat_cool.set_value, 2)
await hass.async_block_till_done()
assert acc.char_target_heat_cool.value == 1
async def test_water_heater(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = "water_heater.test"
@ -571,7 +613,8 @@ async def test_water_heater(hass, hk_driver, cls, events):
await hass.async_block_till_done()
assert acc.char_target_heat_cool.value == 1
await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 3)
with pytest.raises(ValueError):
await hass.async_add_job(acc.char_target_heat_cool.set_value, 3)
await hass.async_block_till_done()
assert acc.char_target_heat_cool.value == 1