Add support for climate fan and oscillate mode to HomeKit (#66463)

This commit is contained in:
J. Nick Koston 2022-02-21 22:58:31 -10:00 committed by GitHub
parent 0b813f8600
commit f69571f164
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 541 additions and 12 deletions

View file

@ -6,6 +6,8 @@ from pyhap.const import CATEGORY_THERMOSTAT
from homeassistant.components.climate.const import (
ATTR_CURRENT_HUMIDITY,
ATTR_CURRENT_TEMPERATURE,
ATTR_FAN_MODE,
ATTR_FAN_MODES,
ATTR_HUMIDITY,
ATTR_HVAC_ACTION,
ATTR_HVAC_MODE,
@ -13,6 +15,8 @@ from homeassistant.components.climate.const import (
ATTR_MAX_TEMP,
ATTR_MIN_HUMIDITY,
ATTR_MIN_TEMP,
ATTR_SWING_MODE,
ATTR_SWING_MODES,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
CURRENT_HVAC_COOL,
@ -25,6 +29,13 @@ from homeassistant.components.climate.const import (
DEFAULT_MIN_HUMIDITY,
DEFAULT_MIN_TEMP,
DOMAIN as DOMAIN_CLIMATE,
FAN_AUTO,
FAN_HIGH,
FAN_LOW,
FAN_MEDIUM,
FAN_MIDDLE,
FAN_OFF,
FAN_ON,
HVAC_MODE_AUTO,
HVAC_MODE_COOL,
HVAC_MODE_DRY,
@ -32,12 +43,21 @@ from homeassistant.components.climate.const import (
HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
SERVICE_SET_FAN_MODE,
SERVICE_SET_HUMIDITY,
SERVICE_SET_HVAC_MODE as SERVICE_SET_HVAC_MODE_THERMOSTAT,
SERVICE_SET_SWING_MODE,
SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT,
SUPPORT_FAN_MODE,
SUPPORT_SWING_MODE,
SUPPORT_TARGET_HUMIDITY,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
SWING_BOTH,
SWING_HORIZONTAL,
SWING_OFF,
SWING_ON,
SWING_VERTICAL,
)
from homeassistant.components.water_heater import (
DOMAIN as DOMAIN_WATER_HEATER,
@ -51,15 +71,24 @@ from homeassistant.const import (
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
from homeassistant.core import callback
from homeassistant.core import State, callback
from homeassistant.util.percentage import (
ordered_list_item_to_percentage,
percentage_to_ordered_list_item,
)
from .accessories import TYPES, HomeAccessory
from .const import (
CHAR_ACTIVE,
CHAR_COOLING_THRESHOLD_TEMPERATURE,
CHAR_CURRENT_FAN_STATE,
CHAR_CURRENT_HEATING_COOLING,
CHAR_CURRENT_HUMIDITY,
CHAR_CURRENT_TEMPERATURE,
CHAR_HEATING_THRESHOLD_TEMPERATURE,
CHAR_ROTATION_SPEED,
CHAR_SWING_MODE,
CHAR_TARGET_FAN_STATE,
CHAR_TARGET_HEATING_COOLING,
CHAR_TARGET_HUMIDITY,
CHAR_TARGET_TEMPERATURE,
@ -67,7 +96,9 @@ from .const import (
DEFAULT_MAX_TEMP_WATER_HEATER,
DEFAULT_MIN_TEMP_WATER_HEATER,
PROP_MAX_VALUE,
PROP_MIN_STEP,
PROP_MIN_VALUE,
SERV_FANV2,
SERV_THERMOSTAT,
)
from .util import temperature_to_homekit, temperature_to_states
@ -103,6 +134,11 @@ HC_HEAT_COOL_PREFER_COOL = [
HC_HEAT_COOL_OFF,
]
ORDERED_FAN_SPEEDS = [FAN_LOW, FAN_MIDDLE, FAN_MEDIUM, FAN_HIGH]
PRE_DEFINED_FAN_MODES = set(ORDERED_FAN_SPEEDS)
SWING_MODE_PREFERRED_ORDER = [SWING_ON, SWING_BOTH, SWING_HORIZONTAL, SWING_VERTICAL]
PRE_DEFINED_SWING_MODES = set(SWING_MODE_PREFERRED_ORDER)
HC_MIN_TEMP = 10
HC_MAX_TEMP = 38
@ -127,6 +163,19 @@ HC_HASS_TO_HOMEKIT_ACTION = {
CURRENT_HVAC_FAN: HC_HEAT_COOL_COOL,
}
FAN_STATE_INACTIVE = 0
FAN_STATE_IDLE = 1
FAN_STATE_ACTIVE = 2
HC_HASS_TO_HOMEKIT_FAN_STATE = {
CURRENT_HVAC_OFF: FAN_STATE_INACTIVE,
CURRENT_HVAC_IDLE: FAN_STATE_IDLE,
CURRENT_HVAC_HEAT: FAN_STATE_ACTIVE,
CURRENT_HVAC_COOL: FAN_STATE_ACTIVE,
CURRENT_HVAC_DRY: FAN_STATE_ACTIVE,
CURRENT_HVAC_FAN: FAN_STATE_ACTIVE,
}
HEAT_COOL_DEADBAND = 5
@ -144,9 +193,11 @@ class Thermostat(HomeAccessory):
# 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)
self.fan_chars = []
state: State = self.hass.states.get(self.entity_id)
attributes = state.attributes
min_humidity = attributes.get(ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY)
features = attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if features & SUPPORT_TARGET_TEMPERATURE_RANGE:
self.chars.extend(
@ -157,6 +208,7 @@ class Thermostat(HomeAccessory):
self.chars.extend((CHAR_TARGET_HUMIDITY, CHAR_CURRENT_HUMIDITY))
serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars)
self.set_primary_service(serv_thermostat)
# Current mode characteristics
self.char_current_heat_cool = serv_thermostat.configure_char(
@ -233,10 +285,116 @@ class Thermostat(HomeAccessory):
CHAR_CURRENT_HUMIDITY, value=50
)
fan_modes = self.fan_modes = {
fan_mode.lower(): fan_mode
for fan_mode in attributes.get(ATTR_FAN_MODES, [])
}
self.ordered_fan_speeds = []
if (
features & SUPPORT_FAN_MODE
and fan_modes
and PRE_DEFINED_FAN_MODES.intersection(fan_modes)
):
self.ordered_fan_speeds = [
speed for speed in ORDERED_FAN_SPEEDS if speed in fan_modes
]
self.fan_chars.append(CHAR_ROTATION_SPEED)
if FAN_AUTO in fan_modes and (FAN_ON in fan_modes or self.ordered_fan_speeds):
self.fan_chars.append(CHAR_TARGET_FAN_STATE)
self.fan_modes = fan_modes
if (
features & SUPPORT_SWING_MODE
and (swing_modes := attributes.get(ATTR_SWING_MODES))
and PRE_DEFINED_SWING_MODES.intersection(swing_modes)
):
self.swing_on_mode = next(
iter(
swing_mode
for swing_mode in SWING_MODE_PREFERRED_ORDER
if swing_mode in swing_modes
)
)
self.fan_chars.append(CHAR_SWING_MODE)
if self.fan_chars:
if attributes.get(ATTR_HVAC_ACTION) is not None:
self.fan_chars.append(CHAR_CURRENT_FAN_STATE)
serv_fan = self.add_preload_service(SERV_FANV2, self.fan_chars)
serv_thermostat.add_linked_service(serv_fan)
self.char_active = serv_fan.configure_char(
CHAR_ACTIVE, value=1, setter_callback=self._set_fan_active
)
if CHAR_SWING_MODE in self.fan_chars:
self.char_swing = serv_fan.configure_char(
CHAR_SWING_MODE,
value=0,
setter_callback=self._set_fan_swing_mode,
)
self.char_swing.display_name = "Swing Mode"
if CHAR_ROTATION_SPEED in self.fan_chars:
self.char_speed = serv_fan.configure_char(
CHAR_ROTATION_SPEED,
value=100,
properties={PROP_MIN_STEP: 100 / len(self.ordered_fan_speeds)},
setter_callback=self._set_fan_speed,
)
self.char_speed.display_name = "Fan Mode"
if CHAR_CURRENT_FAN_STATE in self.fan_chars:
self.char_current_fan_state = serv_fan.configure_char(
CHAR_CURRENT_FAN_STATE,
value=0,
)
self.char_current_fan_state.display_name = "Fan State"
if CHAR_TARGET_FAN_STATE in self.fan_chars and FAN_AUTO in self.fan_modes:
self.char_target_fan_state = serv_fan.configure_char(
CHAR_TARGET_FAN_STATE,
value=0,
setter_callback=self._set_fan_auto,
)
self.char_target_fan_state.display_name = "Fan Auto"
self._async_update_state(state)
serv_thermostat.setter_callback = self._set_chars
def _set_fan_swing_mode(self, swing_on) -> None:
_LOGGER.debug("%s: Set swing mode to %s", self.entity_id, swing_on)
mode = self.swing_on_mode if swing_on else SWING_OFF
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_SWING_MODE: mode}
self.async_call_service(DOMAIN_CLIMATE, SERVICE_SET_SWING_MODE, params)
def _set_fan_speed(self, speed) -> None:
_LOGGER.debug("%s: Set fan speed to %s", self.entity_id, speed)
mode = percentage_to_ordered_list_item(self.ordered_fan_speeds, speed - 1)
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_FAN_MODE: mode}
self.async_call_service(DOMAIN_CLIMATE, SERVICE_SET_FAN_MODE, params)
def _get_on_mode(self) -> str:
if self.ordered_fan_speeds:
return percentage_to_ordered_list_item(self.ordered_fan_speeds, 50)
return self.fan_modes[FAN_ON]
def _set_fan_active(self, active) -> None:
_LOGGER.debug("%s: Set fan active to %s", self.entity_id, active)
if FAN_OFF not in self.fan_modes:
_LOGGER.debug(
"%s: Fan does not support off, resetting to on", self.entity_id
)
self.char_active.value = 1
self.char_active.notify()
return
mode = self._get_on_mode() if active else self.fan_modes[FAN_OFF]
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_FAN_MODE: mode}
self.async_call_service(DOMAIN_CLIMATE, SERVICE_SET_FAN_MODE, params)
def _set_fan_auto(self, auto) -> None:
_LOGGER.debug("%s: Set fan auto to %s", self.entity_id, auto)
mode = self.fan_modes[FAN_AUTO] if auto else self._get_on_mode()
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_FAN_MODE: mode}
self.async_call_service(DOMAIN_CLIMATE, SERVICE_SET_FAN_MODE, params)
def _temperature_to_homekit(self, temp):
return temperature_to_homekit(temp, self._unit)
@ -446,7 +604,8 @@ class Thermostat(HomeAccessory):
@callback
def _async_update_state(self, new_state):
"""Update state without rechecking the device features."""
features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
attributes = new_state.attributes
features = attributes.get(ATTR_SUPPORTED_FEATURES, 0)
# Update target operation mode FIRST
hvac_mode = new_state.state
@ -462,7 +621,7 @@ class Thermostat(HomeAccessory):
)
# Set current operation mode for supported thermostats
if hvac_action := new_state.attributes.get(ATTR_HVAC_ACTION):
if hvac_action := attributes.get(ATTR_HVAC_ACTION):
homekit_hvac_action = HC_HASS_TO_HOMEKIT_ACTION[hvac_action]
self.char_current_heat_cool.set_value(homekit_hvac_action)
@ -473,26 +632,26 @@ class Thermostat(HomeAccessory):
# Update current humidity
if CHAR_CURRENT_HUMIDITY in self.chars:
current_humdity = new_state.attributes.get(ATTR_CURRENT_HUMIDITY)
current_humdity = attributes.get(ATTR_CURRENT_HUMIDITY)
if isinstance(current_humdity, (int, float)):
self.char_current_humidity.set_value(current_humdity)
# Update target humidity
if CHAR_TARGET_HUMIDITY in self.chars:
target_humdity = new_state.attributes.get(ATTR_HUMIDITY)
target_humdity = attributes.get(ATTR_HUMIDITY)
if isinstance(target_humdity, (int, float)):
self.char_target_humidity.set_value(target_humdity)
# Update cooling threshold temperature if characteristic exists
if self.char_cooling_thresh_temp:
cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH)
cooling_thresh = attributes.get(ATTR_TARGET_TEMP_HIGH)
if isinstance(cooling_thresh, (int, float)):
cooling_thresh = self._temperature_to_homekit(cooling_thresh)
self.char_cooling_thresh_temp.set_value(cooling_thresh)
# Update heating threshold temperature if characteristic exists
if self.char_heating_thresh_temp:
heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW)
heating_thresh = attributes.get(ATTR_TARGET_TEMP_LOW)
if isinstance(heating_thresh, (int, float)):
heating_thresh = self._temperature_to_homekit(heating_thresh)
self.char_heating_thresh_temp.set_value(heating_thresh)
@ -504,11 +663,11 @@ class Thermostat(HomeAccessory):
# even if the device does not support it
hc_hvac_mode = self.char_target_heat_cool.value
if hc_hvac_mode == HC_HEAT_COOL_HEAT:
temp_low = new_state.attributes.get(ATTR_TARGET_TEMP_LOW)
temp_low = attributes.get(ATTR_TARGET_TEMP_LOW)
if isinstance(temp_low, (int, float)):
target_temp = self._temperature_to_homekit(temp_low)
elif hc_hvac_mode == HC_HEAT_COOL_COOL:
temp_high = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH)
temp_high = attributes.get(ATTR_TARGET_TEMP_HIGH)
if isinstance(temp_high, (int, float)):
target_temp = self._temperature_to_homekit(temp_high)
if target_temp:
@ -519,6 +678,44 @@ class Thermostat(HomeAccessory):
unit = UNIT_HASS_TO_HOMEKIT[self._unit]
self.char_display_units.set_value(unit)
if self.fan_chars:
self._async_update_fan_state(new_state)
@callback
def _async_update_fan_state(self, new_state):
"""Update state without rechecking the device features."""
attributes = new_state.attributes
if CHAR_SWING_MODE in self.fan_chars and (
swing_mode := attributes.get(ATTR_SWING_MODE)
):
swing = 1 if swing_mode in PRE_DEFINED_SWING_MODES else 0
self.char_swing.set_value(swing)
fan_mode = attributes.get(ATTR_FAN_MODE)
fan_mode_lower = fan_mode.lower() if isinstance(fan_mode, str) else None
if (
CHAR_ROTATION_SPEED in self.fan_chars
and fan_mode_lower in self.ordered_fan_speeds
):
self.char_speed.set_value(
ordered_list_item_to_percentage(self.ordered_fan_speeds, fan_mode_lower)
)
if CHAR_TARGET_FAN_STATE in self.fan_chars:
self.char_target_fan_state.set_value(1 if fan_mode_lower == FAN_AUTO else 0)
if CHAR_CURRENT_FAN_STATE in self.fan_chars and (
hvac_action := attributes.get(ATTR_HVAC_ACTION)
):
self.char_current_fan_state.set_value(
HC_HASS_TO_HOMEKIT_FAN_STATE[hvac_action]
)
self.char_active.set_value(
int(new_state.state != HVAC_MODE_OFF and fan_mode_lower != FAN_OFF)
)
@TYPES.register("WaterHeater")
class WaterHeater(HomeAccessory):

View file

@ -7,12 +7,16 @@ import pytest
from homeassistant.components.climate.const import (
ATTR_CURRENT_HUMIDITY,
ATTR_CURRENT_TEMPERATURE,
ATTR_FAN_MODE,
ATTR_FAN_MODES,
ATTR_HUMIDITY,
ATTR_HVAC_ACTION,
ATTR_HVAC_MODE,
ATTR_HVAC_MODES,
ATTR_MAX_TEMP,
ATTR_MIN_TEMP,
ATTR_SWING_MODE,
ATTR_SWING_MODES,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
ATTR_TARGET_TEMP_STEP,
@ -24,6 +28,12 @@ from homeassistant.components.climate.const import (
DEFAULT_MAX_TEMP,
DEFAULT_MIN_HUMIDITY,
DOMAIN as DOMAIN_CLIMATE,
FAN_AUTO,
FAN_HIGH,
FAN_LOW,
FAN_MEDIUM,
FAN_OFF,
FAN_ON,
HVAC_MODE_AUTO,
HVAC_MODE_COOL,
HVAC_MODE_DRY,
@ -31,11 +41,22 @@ from homeassistant.components.climate.const import (
HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
SERVICE_SET_FAN_MODE,
SERVICE_SET_SWING_MODE,
SUPPORT_FAN_MODE,
SUPPORT_SWING_MODE,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
SWING_BOTH,
SWING_HORIZONTAL,
SWING_OFF,
)
from homeassistant.components.homekit.const import (
ATTR_VALUE,
CHAR_CURRENT_FAN_STATE,
CHAR_ROTATION_SPEED,
CHAR_SWING_MODE,
CHAR_TARGET_FAN_STATE,
DEFAULT_MAX_TEMP_WATER_HEATER,
DEFAULT_MIN_TEMP_WATER_HEATER,
PROP_MAX_VALUE,
@ -2017,3 +2038,314 @@ async def test_thermostat_with_temp_clamps(hass, hk_driver, events):
assert acc.char_target_heat_cool.value == 3
assert acc.char_current_temp.value == 1000
assert acc.char_display_units.value == 0
async def test_thermostat_with_fan_modes_with_auto(hass, hk_driver, events):
"""Test a thermostate with fan modes with an auto fan mode."""
entity_id = "climate.test"
hass.states.async_set(
entity_id,
HVAC_MODE_OFF,
{
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE
| SUPPORT_TARGET_TEMPERATURE_RANGE
| SUPPORT_FAN_MODE
| SUPPORT_SWING_MODE,
ATTR_FAN_MODES: [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH],
ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL],
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
ATTR_FAN_MODE: FAN_AUTO,
ATTR_SWING_MODE: SWING_BOTH,
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 = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
await acc.run()
await hass.async_block_till_done()
assert acc.char_cooling_thresh_temp.value == 23.0
assert acc.char_heating_thresh_temp.value == 19.0
assert acc.ordered_fan_speeds == [FAN_LOW, FAN_MEDIUM, FAN_HIGH]
assert CHAR_ROTATION_SPEED in acc.fan_chars
assert CHAR_TARGET_FAN_STATE in acc.fan_chars
assert CHAR_SWING_MODE in acc.fan_chars
assert CHAR_CURRENT_FAN_STATE in acc.fan_chars
assert acc.char_speed.value == 100
hass.states.async_set(
entity_id,
HVAC_MODE_OFF,
{
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE
| SUPPORT_TARGET_TEMPERATURE_RANGE
| SUPPORT_FAN_MODE
| SUPPORT_SWING_MODE,
ATTR_FAN_MODES: [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH],
ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL],
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
ATTR_FAN_MODE: FAN_LOW,
ATTR_SWING_MODE: SWING_BOTH,
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()
assert acc.char_speed.value == pytest.approx(100 / 3)
call_set_swing_mode = async_mock_service(
hass, DOMAIN_CLIMATE, SERVICE_SET_SWING_MODE
)
char_swing_iid = acc.char_swing.to_HAP()[HAP_REPR_IID]
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_swing_iid,
HAP_REPR_VALUE: 0,
}
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert len(call_set_swing_mode) == 1
assert call_set_swing_mode[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_set_swing_mode[-1].data[ATTR_SWING_MODE] == SWING_OFF
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_swing_iid,
HAP_REPR_VALUE: 1,
}
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert len(call_set_swing_mode) == 2
assert call_set_swing_mode[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_set_swing_mode[-1].data[ATTR_SWING_MODE] == SWING_BOTH
call_set_fan_mode = async_mock_service(hass, DOMAIN_CLIMATE, SERVICE_SET_FAN_MODE)
char_rotation_speed_iid = acc.char_speed.to_HAP()[HAP_REPR_IID]
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_rotation_speed_iid,
HAP_REPR_VALUE: 100,
}
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert len(call_set_fan_mode) == 1
assert call_set_fan_mode[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == FAN_HIGH
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_rotation_speed_iid,
HAP_REPR_VALUE: 100 / 3,
}
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert len(call_set_fan_mode) == 2
assert call_set_fan_mode[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == FAN_LOW
char_active_iid = acc.char_active.to_HAP()[HAP_REPR_IID]
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_active_iid,
HAP_REPR_VALUE: 0,
}
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert acc.char_active.value == 1
char_target_fan_state_iid = acc.char_target_fan_state.to_HAP()[HAP_REPR_IID]
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_fan_state_iid,
HAP_REPR_VALUE: 1,
}
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert len(call_set_fan_mode) == 3
assert call_set_fan_mode[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == FAN_AUTO
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_fan_state_iid,
HAP_REPR_VALUE: 0,
}
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert len(call_set_fan_mode) == 4
assert call_set_fan_mode[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == FAN_MEDIUM
async def test_thermostat_with_fan_modes_with_off(hass, hk_driver, events):
"""Test a thermostate with fan modes that can turn off."""
entity_id = "climate.test"
hass.states.async_set(
entity_id,
HVAC_MODE_COOL,
{
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE
| SUPPORT_TARGET_TEMPERATURE_RANGE
| SUPPORT_FAN_MODE
| SUPPORT_SWING_MODE,
ATTR_FAN_MODES: [FAN_ON, FAN_OFF],
ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL],
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
ATTR_FAN_MODE: FAN_ON,
ATTR_SWING_MODE: SWING_BOTH,
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 = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
await acc.run()
await hass.async_block_till_done()
assert acc.char_cooling_thresh_temp.value == 23.0
assert acc.char_heating_thresh_temp.value == 19.0
assert acc.ordered_fan_speeds == []
assert CHAR_ROTATION_SPEED not in acc.fan_chars
assert CHAR_TARGET_FAN_STATE not in acc.fan_chars
assert CHAR_SWING_MODE in acc.fan_chars
assert CHAR_CURRENT_FAN_STATE in acc.fan_chars
assert acc.char_active.value == 1
hass.states.async_set(
entity_id,
HVAC_MODE_COOL,
{
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE
| SUPPORT_TARGET_TEMPERATURE_RANGE
| SUPPORT_FAN_MODE
| SUPPORT_SWING_MODE,
ATTR_FAN_MODES: [FAN_ON, FAN_OFF],
ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL],
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
ATTR_FAN_MODE: FAN_OFF,
ATTR_SWING_MODE: SWING_BOTH,
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()
assert acc.char_active.value == 0
call_set_fan_mode = async_mock_service(hass, DOMAIN_CLIMATE, SERVICE_SET_FAN_MODE)
char_active_iid = acc.char_active.to_HAP()[HAP_REPR_IID]
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_active_iid,
HAP_REPR_VALUE: 1,
}
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert len(call_set_fan_mode) == 1
assert call_set_fan_mode[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == FAN_ON
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_active_iid,
HAP_REPR_VALUE: 0,
}
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert len(call_set_fan_mode) == 2
assert call_set_fan_mode[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == FAN_OFF