Convert homekit thermostats to use service callbacks (#34073)

* Convert homekit thermostats to use service callbacks

Service callbacks allow us to get all the temperature
changes in one request so we can avoid all the
need to store state and debounce.

* remove excess debug

* Fix lock and light tests

* Ensure all code for Thermostats has coverage

* I am answering all the homekit cases anyways so might as well be aware of regressions

* Make lock notifications reliable

* Update homeassistant/components/homekit/type_lights.py

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
J. Nick Koston 2020-04-12 17:38:33 -05:00 committed by GitHub
parent 173d7fa060
commit 5d649b2541
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 710 additions and 239 deletions

View File

@ -162,6 +162,7 @@ homeassistant/components/hisense_aehw4a1/* @bannhead
homeassistant/components/history/* @home-assistant/core homeassistant/components/history/* @home-assistant/core
homeassistant/components/hive/* @Rendili @KJonline homeassistant/components/hive/* @Rendili @KJonline
homeassistant/components/homeassistant/* @home-assistant/core homeassistant/components/homeassistant/* @home-assistant/core
homeassistant/components/homekit/* @bdraco
homeassistant/components/homekit_controller/* @Jc2k homeassistant/components/homekit_controller/* @Jc2k
homeassistant/components/homematic/* @pvizeli @danielperna84 homeassistant/components/homematic/* @pvizeli @danielperna84
homeassistant/components/homematicip_cloud/* @SukramJ homeassistant/components/homematicip_cloud/* @SukramJ

View File

@ -3,5 +3,5 @@
"name": "HomeKit", "name": "HomeKit",
"documentation": "https://www.home-assistant.io/integrations/homekit", "documentation": "https://www.home-assistant.io/integrations/homekit",
"requirements": ["HAP-python==2.8.2"], "requirements": ["HAP-python==2.8.2"],
"codeowners": [] "codeowners": ["@bdraco"]
} }

View File

@ -54,9 +54,9 @@ class Light(HomeAccessory):
super().__init__(*args, category=CATEGORY_LIGHTBULB) super().__init__(*args, category=CATEGORY_LIGHTBULB)
self.chars = [] self.chars = []
self._features = self.hass.states.get(self.entity_id).attributes.get( state = self.hass.states.get(self.entity_id)
ATTR_SUPPORTED_FEATURES
) self._features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if self._features & SUPPORT_BRIGHTNESS: if self._features & SUPPORT_BRIGHTNESS:
self.chars.append(CHAR_BRIGHTNESS) self.chars.append(CHAR_BRIGHTNESS)
@ -101,10 +101,12 @@ class Light(HomeAccessory):
if CHAR_SATURATION in self.chars: if CHAR_SATURATION in self.chars:
self.char_saturation = serv_light.configure_char(CHAR_SATURATION, value=75) self.char_saturation = serv_light.configure_char(CHAR_SATURATION, value=75)
self.update_state(state)
serv_light.setter_callback = self._set_chars serv_light.setter_callback = self._set_chars
def _set_chars(self, char_values): def _set_chars(self, char_values):
_LOGGER.debug("_set_chars: %s", char_values) _LOGGER.debug("Light _set_chars: %s", char_values)
events = [] events = []
service = SERVICE_TURN_ON service = SERVICE_TURN_ON
params = {ATTR_ENTITY_ID: self.entity_id} params = {ATTR_ENTITY_ID: self.entity_id}

View File

@ -35,7 +35,7 @@ class Lock(HomeAccessory):
"""Initialize a Lock accessory object.""" """Initialize a Lock accessory object."""
super().__init__(*args, category=CATEGORY_DOOR_LOCK) super().__init__(*args, category=CATEGORY_DOOR_LOCK)
self._code = self.config.get(ATTR_CODE) self._code = self.config.get(ATTR_CODE)
self._flag_state = False state = self.hass.states.get(self.entity_id)
serv_lock_mechanism = self.add_preload_service(SERV_LOCK) serv_lock_mechanism = self.add_preload_service(SERV_LOCK)
self.char_current_state = serv_lock_mechanism.configure_char( self.char_current_state = serv_lock_mechanism.configure_char(
@ -46,15 +46,18 @@ class Lock(HomeAccessory):
value=HASS_TO_HOMEKIT[STATE_LOCKED], value=HASS_TO_HOMEKIT[STATE_LOCKED],
setter_callback=self.set_state, setter_callback=self.set_state,
) )
self.update_state(state)
def set_state(self, value): def set_state(self, value):
"""Set lock state to value if call came from HomeKit.""" """Set lock state to value if call came from HomeKit."""
_LOGGER.debug("%s: Set state to %d", self.entity_id, value) _LOGGER.debug("%s: Set state to %d", self.entity_id, value)
self._flag_state = True
hass_value = HOMEKIT_TO_HASS.get(value) hass_value = HOMEKIT_TO_HASS.get(value)
service = STATE_TO_SERVICE[hass_value] service = STATE_TO_SERVICE[hass_value]
if self.char_current_state.value != value:
self.char_current_state.set_value(value)
params = {ATTR_ENTITY_ID: self.entity_id} params = {ATTR_ENTITY_ID: self.entity_id}
if self._code: if self._code:
params[ATTR_CODE] = self._code params[ATTR_CODE] = self._code
@ -65,16 +68,21 @@ class Lock(HomeAccessory):
hass_state = new_state.state hass_state = new_state.state
if hass_state in HASS_TO_HOMEKIT: if hass_state in HASS_TO_HOMEKIT:
current_lock_state = HASS_TO_HOMEKIT[hass_state] current_lock_state = HASS_TO_HOMEKIT[hass_state]
self.char_current_state.set_value(current_lock_state)
_LOGGER.debug( _LOGGER.debug(
"%s: Updated current state to %s (%d)", "%s: Updated current state to %s (%d)",
self.entity_id, self.entity_id,
hass_state, hass_state,
current_lock_state, current_lock_state,
) )
# LockTargetState only supports locked and unlocked # LockTargetState only supports locked and unlocked
# Must set lock target state before current state
# or there will be no notification
if hass_state in (STATE_LOCKED, STATE_UNLOCKED): if hass_state in (STATE_LOCKED, STATE_UNLOCKED):
if not self._flag_state: if self.char_target_state.value != current_lock_state:
self.char_target_state.set_value(current_lock_state) self.char_target_state.set_value(current_lock_state)
self._flag_state = False
# Set lock current state ONLY after ensuring that
# target state is correct or there will be no
# notification
if self.char_current_state.value != current_lock_state:
self.char_current_state.set_value(current_lock_state)

View File

@ -36,6 +36,7 @@ from homeassistant.components.climate.const import (
SERVICE_SET_HVAC_MODE as SERVICE_SET_HVAC_MODE_THERMOSTAT, SERVICE_SET_HVAC_MODE as SERVICE_SET_HVAC_MODE_THERMOSTAT,
SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT, SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT,
SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_HUMIDITY,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE, SUPPORT_TARGET_TEMPERATURE_RANGE,
) )
from homeassistant.components.water_heater import ( from homeassistant.components.water_heater import (
@ -52,7 +53,7 @@ from homeassistant.const import (
) )
from . import TYPES from . import TYPES
from .accessories import HomeAccessory, debounce from .accessories import HomeAccessory
from .const import ( from .const import (
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_COOLING_THRESHOLD_TEMPERATURE,
CHAR_CURRENT_HEATING_COOLING, CHAR_CURRENT_HEATING_COOLING,
@ -76,26 +77,37 @@ _LOGGER = logging.getLogger(__name__)
HC_HOMEKIT_VALID_MODES_WATER_HEATER = {"Heat": 1} HC_HOMEKIT_VALID_MODES_WATER_HEATER = {"Heat": 1}
UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1} UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1}
HC_HEAT_COOL_OFF = 0
HC_HEAT_COOL_HEAT = 1
HC_HEAT_COOL_COOL = 2
HC_HEAT_COOL_AUTO = 3
HC_MIN_TEMP = 10
HC_MAX_TEMP = 38
UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()} UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()}
HC_HASS_TO_HOMEKIT = { HC_HASS_TO_HOMEKIT = {
HVAC_MODE_OFF: 0, HVAC_MODE_OFF: HC_HEAT_COOL_OFF,
HVAC_MODE_HEAT: 1, HVAC_MODE_HEAT: HC_HEAT_COOL_HEAT,
HVAC_MODE_COOL: 2, HVAC_MODE_COOL: HC_HEAT_COOL_COOL,
HVAC_MODE_AUTO: 3, HVAC_MODE_AUTO: HC_HEAT_COOL_AUTO,
HVAC_MODE_HEAT_COOL: 3, HVAC_MODE_HEAT_COOL: HC_HEAT_COOL_AUTO,
HVAC_MODE_FAN_ONLY: 2, HVAC_MODE_DRY: HC_HEAT_COOL_COOL,
HVAC_MODE_FAN_ONLY: HC_HEAT_COOL_COOL,
} }
HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()} HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()}
HC_HASS_TO_HOMEKIT_ACTION = { HC_HASS_TO_HOMEKIT_ACTION = {
CURRENT_HVAC_OFF: 0, CURRENT_HVAC_OFF: HC_HEAT_COOL_OFF,
CURRENT_HVAC_IDLE: 0, CURRENT_HVAC_IDLE: HC_HEAT_COOL_OFF,
CURRENT_HVAC_HEAT: 1, CURRENT_HVAC_HEAT: HC_HEAT_COOL_HEAT,
CURRENT_HVAC_COOL: 2, CURRENT_HVAC_COOL: HC_HEAT_COOL_COOL,
CURRENT_HVAC_DRY: 2, CURRENT_HVAC_DRY: HC_HEAT_COOL_COOL,
CURRENT_HVAC_FAN: 2, CURRENT_HVAC_FAN: HC_HEAT_COOL_COOL,
} }
HEAT_COOL_DEADBAND = 5
@TYPES.register("Thermostat") @TYPES.register("Thermostat")
class Thermostat(HomeAccessory): class Thermostat(HomeAccessory):
@ -105,12 +117,12 @@ class Thermostat(HomeAccessory):
"""Initialize a Thermostat accessory object.""" """Initialize a Thermostat accessory object."""
super().__init__(*args, category=CATEGORY_THERMOSTAT) super().__init__(*args, category=CATEGORY_THERMOSTAT)
self._unit = self.hass.config.units.temperature_unit self._unit = self.hass.config.units.temperature_unit
self._flag_heat_cool = False
self._flag_temperature = False
self._flag_coolingthresh = False
self._flag_heatingthresh = False
min_temp, max_temp = self.get_temperature_range() min_temp, max_temp = self.get_temperature_range()
# Homekit only supports 10-38
hc_min_temp = max(min_temp, HC_MIN_TEMP)
hc_max_temp = min(max_temp, HC_MAX_TEMP)
min_humidity = self.hass.states.get(self.entity_id).attributes.get( min_humidity = self.hass.states.get(self.entity_id).attributes.get(
ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY
) )
@ -174,24 +186,22 @@ class Thermostat(HomeAccessory):
hc_valid_values = {k: v for v, k in self.hc_homekit_to_hass.items()} hc_valid_values = {k: v for v, k in self.hc_homekit_to_hass.items()}
self.char_target_heat_cool = serv_thermostat.configure_char( self.char_target_heat_cool = serv_thermostat.configure_char(
CHAR_TARGET_HEATING_COOLING, CHAR_TARGET_HEATING_COOLING, valid_values=hc_valid_values,
value=list(hc_valid_values.values())[0],
setter_callback=self.set_heat_cool,
valid_values=hc_valid_values,
) )
# Current and target temperature characteristics # Current and target temperature characteristics
self.char_current_temp = serv_thermostat.configure_char( self.char_current_temp = serv_thermostat.configure_char(
CHAR_CURRENT_TEMPERATURE, value=21.0 CHAR_CURRENT_TEMPERATURE, value=21.0
) )
self.char_target_temp = serv_thermostat.configure_char( self.char_target_temp = serv_thermostat.configure_char(
CHAR_TARGET_TEMPERATURE, CHAR_TARGET_TEMPERATURE,
value=21.0, value=21.0,
# We do not set PROP_MIN_STEP here and instead use the HomeKit # We do not set PROP_MIN_STEP here and instead use the HomeKit
# default of 0.1 in order to have enough precision to convert # default of 0.1 in order to have enough precision to convert
# temperature units and avoid setting to 73F will result in 74F # temperature units and avoid setting to 73F will result in 74F
properties={PROP_MIN_VALUE: min_temp, PROP_MAX_VALUE: max_temp}, properties={PROP_MIN_VALUE: hc_min_temp, PROP_MAX_VALUE: hc_max_temp},
setter_callback=self.set_target_temperature,
) )
# Display units characteristic # Display units characteristic
@ -209,8 +219,7 @@ class Thermostat(HomeAccessory):
# We do not set PROP_MIN_STEP here and instead use the HomeKit # We do not set PROP_MIN_STEP here and instead use the HomeKit
# default of 0.1 in order to have enough precision to convert # default of 0.1 in order to have enough precision to convert
# temperature units and avoid setting to 73F will result in 74F # temperature units and avoid setting to 73F will result in 74F
properties={PROP_MIN_VALUE: min_temp, PROP_MAX_VALUE: max_temp}, properties={PROP_MIN_VALUE: hc_min_temp, PROP_MAX_VALUE: hc_max_temp},
setter_callback=self.set_cooling_threshold,
) )
if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars: if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars:
self.char_heating_thresh_temp = serv_thermostat.configure_char( self.char_heating_thresh_temp = serv_thermostat.configure_char(
@ -219,8 +228,7 @@ class Thermostat(HomeAccessory):
# We do not set PROP_MIN_STEP here and instead use the HomeKit # We do not set PROP_MIN_STEP here and instead use the HomeKit
# default of 0.1 in order to have enough precision to convert # default of 0.1 in order to have enough precision to convert
# temperature units and avoid setting to 73F will result in 74F # temperature units and avoid setting to 73F will result in 74F
properties={PROP_MIN_VALUE: min_temp, PROP_MAX_VALUE: max_temp}, properties={PROP_MIN_VALUE: hc_min_temp, PROP_MAX_VALUE: hc_max_temp},
setter_callback=self.set_heating_threshold,
) )
self.char_target_humidity = None self.char_target_humidity = None
self.char_current_humidity = None self.char_current_humidity = None
@ -234,43 +242,134 @@ class Thermostat(HomeAccessory):
# of 80% homekit will give you the options 20%-100% instead # of 80% homekit will give you the options 20%-100% instead
# of 0-80% # of 0-80%
properties={PROP_MIN_VALUE: min_humidity}, properties={PROP_MIN_VALUE: min_humidity},
setter_callback=self.set_target_humidity,
) )
self.char_current_humidity = serv_thermostat.configure_char( self.char_current_humidity = serv_thermostat.configure_char(
CHAR_CURRENT_HUMIDITY, value=50 CHAR_CURRENT_HUMIDITY, value=50
) )
self.update_state(state)
serv_thermostat.setter_callback = self._set_chars
def _temperature_to_homekit(self, temp):
return temperature_to_homekit(temp, self._unit)
def _temperature_to_states(self, temp):
return temperature_to_states(temp, self._unit)
def _set_chars(self, char_values):
_LOGGER.debug("Thermostat _set_chars: %s", char_values)
events = []
params = {}
service = None
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
homekit_hvac_mode = HC_HASS_TO_HOMEKIT[hvac_mode]
if CHAR_TARGET_HEATING_COOLING in char_values:
# Homekit will reset the mode when VIEWING the temp
# Ignore it if its the same mode
if char_values[CHAR_TARGET_HEATING_COOLING] != homekit_hvac_mode:
service = SERVICE_SET_HVAC_MODE_THERMOSTAT
hass_value = self.hc_homekit_to_hass[
char_values[CHAR_TARGET_HEATING_COOLING]
]
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]
if features & SUPPORT_TARGET_TEMPERATURE:
service = SERVICE_SET_TEMPERATURE_THERMOSTAT
temperature = self._temperature_to_states(hc_target_temp)
events.append(
f"{CHAR_TARGET_TEMPERATURE} to {char_values[CHAR_TARGET_TEMPERATURE]}°C"
)
params[ATTR_TEMPERATURE] = temperature
elif features & SUPPORT_TARGET_TEMPERATURE_RANGE:
# Homekit will send us a target temperature
# even if the device does not support it
_LOGGER.debug(
"Homekit requested target temp: %s and the device does not support",
hc_target_temp,
)
if (
homekit_hvac_mode == HC_HEAT_COOL_HEAT
and CHAR_HEATING_THRESHOLD_TEMPERATURE not in char_values
):
char_values[CHAR_HEATING_THRESHOLD_TEMPERATURE] = hc_target_temp
if (
homekit_hvac_mode == HC_HEAT_COOL_COOL
and CHAR_COOLING_THRESHOLD_TEMPERATURE not in char_values
):
char_values[CHAR_COOLING_THRESHOLD_TEMPERATURE] = hc_target_temp
if (
CHAR_HEATING_THRESHOLD_TEMPERATURE in char_values
or CHAR_COOLING_THRESHOLD_TEMPERATURE in char_values
):
service = SERVICE_SET_TEMPERATURE_THERMOSTAT
high = self.char_cooling_thresh_temp.value
low = self.char_heating_thresh_temp.value
min_temp, max_temp = self.get_temperature_range()
if CHAR_COOLING_THRESHOLD_TEMPERATURE in char_values:
events.append(
f"{CHAR_COOLING_THRESHOLD_TEMPERATURE} to {char_values[CHAR_COOLING_THRESHOLD_TEMPERATURE]}°C"
)
high = char_values[CHAR_COOLING_THRESHOLD_TEMPERATURE]
# If the device doesn't support TARGET_TEMPATURE
# this can happen
if high < low:
low = high - HEAT_COOL_DEADBAND
if CHAR_HEATING_THRESHOLD_TEMPERATURE in char_values:
events.append(
f"{CHAR_HEATING_THRESHOLD_TEMPERATURE} to {char_values[CHAR_HEATING_THRESHOLD_TEMPERATURE]}°C"
)
low = char_values[CHAR_HEATING_THRESHOLD_TEMPERATURE]
# If the device doesn't support TARGET_TEMPATURE
# this can happen
if low > high:
high = low + HEAT_COOL_DEADBAND
high = min(high, max_temp)
low = max(low, min_temp)
params.update(
{
ATTR_TARGET_TEMP_HIGH: self._temperature_to_states(high),
ATTR_TARGET_TEMP_LOW: self._temperature_to_states(low),
}
)
if service:
params[ATTR_ENTITY_ID] = self.entity_id
self.call_service(
DOMAIN_CLIMATE, service, params, ", ".join(events),
)
if CHAR_TARGET_HUMIDITY in char_values:
self.set_target_humidity(char_values[CHAR_TARGET_HUMIDITY])
def get_temperature_range(self): def get_temperature_range(self):
"""Return min and max temperature range.""" """Return min and max temperature range."""
max_temp = self.hass.states.get(self.entity_id).attributes.get(ATTR_MAX_TEMP) max_temp = self.hass.states.get(self.entity_id).attributes.get(ATTR_MAX_TEMP)
max_temp = ( max_temp = (
temperature_to_homekit(max_temp, self._unit) self._temperature_to_homekit(max_temp) if max_temp else DEFAULT_MAX_TEMP
if max_temp
else DEFAULT_MAX_TEMP
) )
max_temp = round(max_temp * 2) / 2 max_temp = round(max_temp * 2) / 2
min_temp = self.hass.states.get(self.entity_id).attributes.get(ATTR_MIN_TEMP) min_temp = self.hass.states.get(self.entity_id).attributes.get(ATTR_MIN_TEMP)
min_temp = ( min_temp = (
temperature_to_homekit(min_temp, self._unit) self._temperature_to_homekit(min_temp) if min_temp else DEFAULT_MIN_TEMP
if min_temp
else DEFAULT_MIN_TEMP
) )
min_temp = round(min_temp * 2) / 2 min_temp = round(min_temp * 2) / 2
return min_temp, max_temp return min_temp, max_temp
def set_heat_cool(self, value):
"""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 = 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
)
@debounce
def set_target_humidity(self, value): def set_target_humidity(self, value):
"""Set target humidity to value if call came from HomeKit.""" """Set target humidity to value if call came from HomeKit."""
_LOGGER.debug("%s: Set target humidity to %d", self.entity_id, value) _LOGGER.debug("%s: Set target humidity to %d", self.entity_id, value)
@ -279,126 +378,85 @@ class Thermostat(HomeAccessory):
DOMAIN_CLIMATE, SERVICE_SET_HUMIDITY, params, f"{value}{UNIT_PERCENTAGE}" DOMAIN_CLIMATE, SERVICE_SET_HUMIDITY, params, f"{value}{UNIT_PERCENTAGE}"
) )
@debounce
def set_cooling_threshold(self, value):
"""Set cooling threshold temp to value if call came from HomeKit."""
_LOGGER.debug(
"%s: Set cooling threshold temperature to %.1f°C", self.entity_id, value
)
self._flag_coolingthresh = True
low = self.char_heating_thresh_temp.value
temperature = temperature_to_states(value, self._unit)
params = {
ATTR_ENTITY_ID: self.entity_id,
ATTR_TARGET_TEMP_HIGH: temperature,
ATTR_TARGET_TEMP_LOW: temperature_to_states(low, self._unit),
}
self.call_service(
DOMAIN_CLIMATE,
SERVICE_SET_TEMPERATURE_THERMOSTAT,
params,
f"cooling threshold {temperature}{self._unit}",
)
@debounce
def set_heating_threshold(self, value):
"""Set heating threshold temp to value if call came from HomeKit."""
_LOGGER.debug(
"%s: Set heating threshold temperature to %.1f°C", self.entity_id, value
)
self._flag_heatingthresh = True
high = self.char_cooling_thresh_temp.value
temperature = temperature_to_states(value, self._unit)
params = {
ATTR_ENTITY_ID: self.entity_id,
ATTR_TARGET_TEMP_HIGH: temperature_to_states(high, self._unit),
ATTR_TARGET_TEMP_LOW: temperature,
}
self.call_service(
DOMAIN_CLIMATE,
SERVICE_SET_TEMPERATURE_THERMOSTAT,
params,
f"heating threshold {temperature}{self._unit}",
)
@debounce
def set_target_temperature(self, value):
"""Set target temperature to value if call came from HomeKit."""
_LOGGER.debug("%s: Set target temperature to %.1f°C", self.entity_id, value)
self._flag_temperature = True
temperature = temperature_to_states(value, self._unit)
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_TEMPERATURE: temperature}
self.call_service(
DOMAIN_CLIMATE,
SERVICE_SET_TEMPERATURE_THERMOSTAT,
params,
f"{temperature}{self._unit}",
)
def update_state(self, new_state): def update_state(self, new_state):
"""Update thermostat state after state changed.""" """Update thermostat state after state changed."""
features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
# Update target operation mode FIRST
hvac_mode = new_state.state
if hvac_mode and hvac_mode in HC_HASS_TO_HOMEKIT:
homekit_hvac_mode = HC_HASS_TO_HOMEKIT[hvac_mode]
if self.char_target_heat_cool.value != homekit_hvac_mode:
self.char_target_heat_cool.set_value(homekit_hvac_mode)
# Set current operation mode for supported thermostats
hvac_action = new_state.attributes.get(ATTR_HVAC_ACTION)
if hvac_action:
homekit_hvac_action = HC_HASS_TO_HOMEKIT_ACTION[hvac_action]
if self.char_current_heat_cool.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 = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE)
if isinstance(current_temp, (int, float)): if isinstance(current_temp, (int, float)):
current_temp = temperature_to_homekit(current_temp, self._unit) current_temp = self._temperature_to_homekit(current_temp)
self.char_current_temp.set_value(current_temp) if self.char_current_temp.value != current_temp:
self.char_current_temp.set_value(current_temp)
# Update current humidity # Update current humidity
if CHAR_CURRENT_HUMIDITY in self.chars: if CHAR_CURRENT_HUMIDITY in self.chars:
current_humdity = new_state.attributes.get(ATTR_CURRENT_HUMIDITY) current_humdity = new_state.attributes.get(ATTR_CURRENT_HUMIDITY)
if isinstance(current_humdity, (int, float)): if isinstance(current_humdity, (int, float)):
self.char_current_humidity.set_value(current_humdity) if self.char_current_humidity.value != current_humdity:
self.char_current_humidity.set_value(current_humdity)
# Update target temperature
target_temp = new_state.attributes.get(ATTR_TEMPERATURE)
if isinstance(target_temp, (int, float)):
target_temp = temperature_to_homekit(target_temp, self._unit)
if not self._flag_temperature:
self.char_target_temp.set_value(target_temp)
self._flag_temperature = False
# Update target humidity # Update target humidity
if CHAR_TARGET_HUMIDITY in self.chars: if CHAR_TARGET_HUMIDITY in self.chars:
target_humdity = new_state.attributes.get(ATTR_HUMIDITY) target_humdity = new_state.attributes.get(ATTR_HUMIDITY)
if isinstance(target_humdity, (int, float)): if isinstance(target_humdity, (int, float)):
self.char_target_humidity.set_value(target_humdity) if self.char_target_humidity.value != target_humdity:
self.char_target_humidity.set_value(target_humdity)
# Update cooling threshold temperature if characteristic exists # Update cooling threshold temperature if characteristic exists
if self.char_cooling_thresh_temp: if self.char_cooling_thresh_temp:
cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH) cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH)
if isinstance(cooling_thresh, (int, float)): if isinstance(cooling_thresh, (int, float)):
cooling_thresh = temperature_to_homekit(cooling_thresh, self._unit) cooling_thresh = self._temperature_to_homekit(cooling_thresh)
if not self._flag_coolingthresh: if self.char_heating_thresh_temp.value != cooling_thresh:
self.char_cooling_thresh_temp.set_value(cooling_thresh) self.char_cooling_thresh_temp.set_value(cooling_thresh)
self._flag_coolingthresh = False
# Update heating threshold temperature if characteristic exists # Update heating threshold temperature if characteristic exists
if self.char_heating_thresh_temp: if self.char_heating_thresh_temp:
heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW) heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW)
if isinstance(heating_thresh, (int, float)): if isinstance(heating_thresh, (int, float)):
heating_thresh = temperature_to_homekit(heating_thresh, self._unit) heating_thresh = self._temperature_to_homekit(heating_thresh)
if not self._flag_heatingthresh: if self.char_heating_thresh_temp.value != heating_thresh:
self.char_heating_thresh_temp.set_value(heating_thresh) self.char_heating_thresh_temp.set_value(heating_thresh)
self._flag_heatingthresh = False
# 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:
# Homekit expects a target temperature
# 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:
target_temp = self._temperature_to_homekit(
new_state.attributes.get(ATTR_TARGET_TEMP_LOW)
)
elif hc_hvac_mode == HC_HEAT_COOL_COOL:
target_temp = self._temperature_to_homekit(
new_state.attributes.get(ATTR_TARGET_TEMP_HIGH)
)
if target_temp and self.char_target_temp.value != target_temp:
self.char_target_temp.set_value(target_temp)
# Update display units # Update display units
if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT: if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT:
self.char_display_units.set_value(UNIT_HASS_TO_HOMEKIT[self._unit]) unit = UNIT_HASS_TO_HOMEKIT[self._unit]
if self.char_display_units.value != unit:
# Update target operation mode self.char_display_units.set_value(unit)
hvac_mode = new_state.state
if hvac_mode and hvac_mode in HC_HASS_TO_HOMEKIT:
if not self._flag_heat_cool:
self.char_target_heat_cool.set_value(HC_HASS_TO_HOMEKIT[hvac_mode])
self._flag_heat_cool = False
# Set current operation mode for supported thermostats
hvac_action = new_state.attributes.get(ATTR_HVAC_ACTION)
if hvac_action:
self.char_current_heat_cool.set_value(
HC_HASS_TO_HOMEKIT_ACTION[hvac_action]
)
@TYPES.register("WaterHeater") @TYPES.register("WaterHeater")
@ -409,8 +467,6 @@ class WaterHeater(HomeAccessory):
"""Initialize a WaterHeater accessory object.""" """Initialize a WaterHeater accessory object."""
super().__init__(*args, category=CATEGORY_THERMOSTAT) super().__init__(*args, category=CATEGORY_THERMOSTAT)
self._unit = self.hass.config.units.temperature_unit self._unit = self.hass.config.units.temperature_unit
self._flag_heat_cool = False
self._flag_temperature = False
min_temp, max_temp = self.get_temperature_range() min_temp, max_temp = self.get_temperature_range()
serv_thermostat = self.add_preload_service(SERV_THERMOSTAT) serv_thermostat = self.add_preload_service(SERV_THERMOSTAT)
@ -442,6 +498,9 @@ class WaterHeater(HomeAccessory):
CHAR_TEMP_DISPLAY_UNITS, value=0 CHAR_TEMP_DISPLAY_UNITS, value=0
) )
state = self.hass.states.get(self.entity_id)
self.update_state(state)
def get_temperature_range(self): def get_temperature_range(self):
"""Return min and max temperature range.""" """Return min and max temperature range."""
max_temp = self.hass.states.get(self.entity_id).attributes.get(ATTR_MAX_TEMP) max_temp = self.hass.states.get(self.entity_id).attributes.get(ATTR_MAX_TEMP)
@ -465,16 +524,14 @@ class WaterHeater(HomeAccessory):
def set_heat_cool(self, value): def set_heat_cool(self, value):
"""Change operation mode to value if call came from HomeKit.""" """Change operation mode to value if call came from HomeKit."""
_LOGGER.debug("%s: Set heat-cool to %d", self.entity_id, value) _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 = HC_HOMEKIT_TO_HASS[value]
if hass_value != HVAC_MODE_HEAT: if hass_value != HVAC_MODE_HEAT:
self.char_target_heat_cool.set_value(1) # Heat if self.char_target_heat_cool.value != 1:
self.char_target_heat_cool.set_value(1) # Heat
@debounce
def set_target_temperature(self, value): def set_target_temperature(self, value):
"""Set target temperature to value if call came from HomeKit.""" """Set target temperature to value if call came from HomeKit."""
_LOGGER.debug("%s: Set target temperature to %.1f°C", self.entity_id, value) _LOGGER.debug("%s: Set target temperature to %.1f°C", self.entity_id, value)
self._flag_temperature = True
temperature = temperature_to_states(value, self._unit) temperature = temperature_to_states(value, self._unit)
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_TEMPERATURE: temperature} params = {ATTR_ENTITY_ID: self.entity_id, ATTR_TEMPERATURE: temperature}
self.call_service( self.call_service(
@ -490,17 +547,16 @@ class WaterHeater(HomeAccessory):
temperature = new_state.attributes.get(ATTR_TEMPERATURE) temperature = new_state.attributes.get(ATTR_TEMPERATURE)
if isinstance(temperature, (int, float)): if isinstance(temperature, (int, float)):
temperature = temperature_to_homekit(temperature, self._unit) temperature = temperature_to_homekit(temperature, self._unit)
self.char_current_temp.set_value(temperature) if temperature != self.char_current_temp.value:
if not self._flag_temperature:
self.char_target_temp.set_value(temperature) self.char_target_temp.set_value(temperature)
self._flag_temperature = False
# Update display units # Update display units
if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT: if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT:
self.char_display_units.set_value(UNIT_HASS_TO_HOMEKIT[self._unit]) unit = UNIT_HASS_TO_HOMEKIT[self._unit]
if self.char_display_units.value != unit:
self.char_display_units.set_value(unit)
# Update target operation mode # Update target operation mode
operation_mode = new_state.state operation_mode = new_state.state
if operation_mode and not self._flag_heat_cool: if operation_mode and self.char_target_heat_cool.value != 1:
self.char_target_heat_cool.set_value(1) # Heat self.char_target_heat_cool.set_value(1) # Heat
self._flag_heat_cool = False

View File

@ -66,7 +66,7 @@ async def test_light_basic(hass, hk_driver, cls, events, driver):
assert acc.aid == 1 assert acc.aid == 1
assert acc.category == 5 # Lightbulb assert acc.category == 5 # Lightbulb
assert acc.char_on.value == 0 assert acc.char_on.value
await acc.run_handler() await acc.run_handler()
await hass.async_block_till_done() await hass.async_block_till_done()
@ -260,7 +260,7 @@ async def test_light_color_temperature(hass, hk_driver, cls, events, driver):
acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
driver.add_accessory(acc) driver.add_accessory(acc)
assert acc.char_color_temperature.value == 153 assert acc.char_color_temperature.value == 190
await acc.run_handler() await acc.run_handler()
await hass.async_block_till_done() await hass.async_block_till_done()
@ -326,8 +326,8 @@ async def test_light_rgb_color(hass, hk_driver, cls, events, driver):
acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
driver.add_accessory(acc) driver.add_accessory(acc)
assert acc.char_hue.value == 0 assert acc.char_hue.value == 260
assert acc.char_saturation.value == 75 assert acc.char_saturation.value == 90
await acc.run_handler() await acc.run_handler()
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -2,6 +2,8 @@
from collections import namedtuple from collections import namedtuple
from unittest.mock import patch from unittest.mock import patch
from pyhap.accessory_driver import AccessoryDriver
from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE
import pytest import pytest
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
@ -23,7 +25,6 @@ from homeassistant.components.climate.const import (
CURRENT_HVAC_IDLE, CURRENT_HVAC_IDLE,
DEFAULT_MAX_TEMP, DEFAULT_MAX_TEMP,
DEFAULT_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY,
DEFAULT_MIN_TEMP,
DOMAIN as DOMAIN_CLIMATE, DOMAIN as DOMAIN_CLIMATE,
HVAC_MODE_AUTO, HVAC_MODE_AUTO,
HVAC_MODE_COOL, HVAC_MODE_COOL,
@ -32,6 +33,8 @@ from homeassistant.components.climate.const import (
HVAC_MODE_HEAT, HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL, HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF, HVAC_MODE_OFF,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
) )
from homeassistant.components.homekit.const import ( from homeassistant.components.homekit.const import (
ATTR_VALUE, ATTR_VALUE,
@ -41,6 +44,7 @@ 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_MIN_TEMP
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,
@ -58,6 +62,15 @@ from tests.common import async_mock_service
from tests.components.homekit.common import patch_debounce from tests.components.homekit.common import patch_debounce
@pytest.fixture
def driver():
"""Patch AccessoryDriver without zeroconf or HAPServer."""
with patch("pyhap.accessory_driver.HAPServer"), patch(
"pyhap.accessory_driver.Zeroconf"
), patch("pyhap.accessory_driver.AccessoryDriver.persist"):
yield AccessoryDriver()
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def cls(): def cls():
"""Patch debounce decorator during import of type_thermostats.""" """Patch debounce decorator during import of type_thermostats."""
@ -65,14 +78,14 @@ def cls():
patcher.start() patcher.start()
_import = __import__( _import = __import__(
"homeassistant.components.homekit.type_thermostats", "homeassistant.components.homekit.type_thermostats",
fromlist=["Thermostat", "WaterHeater"], fromlist=["WaterHeater", "Thermostat"],
) )
patcher_tuple = namedtuple("Cls", ["thermostat", "water_heater"]) patcher_tuple = namedtuple("Cls", ["water_heater", "thermostat"])
yield patcher_tuple(thermostat=_import.Thermostat, water_heater=_import.WaterHeater) yield patcher_tuple(thermostat=_import.Thermostat, water_heater=_import.WaterHeater)
patcher.stop() patcher.stop()
async def test_thermostat(hass, hk_driver, cls, events): async def test_thermostat(hass, hk_driver, cls, events, driver):
"""Test if accessory and HA are updated accordingly.""" """Test if accessory and HA are updated accordingly."""
entity_id = "climate.test" entity_id = "climate.test"
@ -80,6 +93,7 @@ async def test_thermostat(hass, hk_driver, cls, events):
entity_id, entity_id,
HVAC_MODE_OFF, HVAC_MODE_OFF,
{ {
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE,
ATTR_HVAC_MODES: [ ATTR_HVAC_MODES: [
HVAC_MODE_HEAT, HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL, HVAC_MODE_HEAT_COOL,
@ -87,15 +101,17 @@ async def test_thermostat(hass, hk_driver, cls, events):
HVAC_MODE_COOL, HVAC_MODE_COOL,
HVAC_MODE_OFF, HVAC_MODE_OFF,
HVAC_MODE_AUTO, HVAC_MODE_AUTO,
] ],
}, },
) )
await hass.async_block_till_done() await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None) acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
await hass.async_block_till_done() await hass.async_block_till_done()
assert acc.aid == 2 assert acc.aid == 1
assert acc.category == 9 # Thermostat assert acc.category == 9 # Thermostat
assert acc.get_temperature_range() == (7.0, 35.0) assert acc.get_temperature_range() == (7.0, 35.0)
@ -110,7 +126,7 @@ async def test_thermostat(hass, hk_driver, cls, events):
assert acc.char_current_humidity is None assert acc.char_current_humidity is None
assert acc.char_target_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP assert acc.char_target_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
assert acc.char_target_temp.properties[PROP_MIN_VALUE] == DEFAULT_MIN_TEMP assert acc.char_target_temp.properties[PROP_MIN_VALUE] == HC_MIN_TEMP
assert acc.char_target_temp.properties[PROP_MIN_STEP] == 0.1 assert acc.char_target_temp.properties[PROP_MIN_STEP] == 0.1
hass.states.async_set( hass.states.async_set(
@ -257,6 +273,7 @@ async def test_thermostat(hass, hk_driver, cls, events):
entity_id, entity_id,
HVAC_MODE_DRY, HVAC_MODE_DRY,
{ {
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE,
ATTR_TEMPERATURE: 22.0, ATTR_TEMPERATURE: 22.0,
ATTR_CURRENT_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 22.0,
ATTR_HVAC_ACTION: CURRENT_HVAC_DRY, ATTR_HVAC_ACTION: CURRENT_HVAC_DRY,
@ -273,42 +290,102 @@ async def test_thermostat(hass, hk_driver, cls, events):
call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, "set_temperature") call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, "set_temperature")
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_add_executor_job(acc.char_target_temp.client_update_value, 19.0) char_target_temp_iid = acc.char_target_temp.to_HAP()[HAP_REPR_IID]
char_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID]
driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_temp_iid,
HAP_REPR_VALUE: 19.0,
},
]
},
"mock_addr",
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert call_set_temperature assert call_set_temperature
assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_temperature[0].data[ATTR_TEMPERATURE] == 19.0 assert call_set_temperature[0].data[ATTR_TEMPERATURE] == 19.0
assert acc.char_target_temp.value == 19.0 assert acc.char_target_temp.value == 19.0
assert len(events) == 1 assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "19.0°C" assert events[-1].data[ATTR_VALUE] == "TargetTemperature to 19.0°C"
await hass.async_add_executor_job(acc.char_target_heat_cool.client_update_value, 2) driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_heat_cool_iid,
HAP_REPR_VALUE: 2,
},
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert not call_set_hvac_mode
driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_heat_cool_iid,
HAP_REPR_VALUE: 1,
},
]
},
"mock_addr",
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert call_set_hvac_mode 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_ENTITY_ID] == entity_id
assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_COOL assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT
assert acc.char_target_heat_cool.value == 2 assert acc.char_target_heat_cool.value == 1
assert len(events) == 2 assert len(events) == 2
assert events[-1].data[ATTR_VALUE] == HVAC_MODE_COOL assert events[-1].data[ATTR_VALUE] == "TargetHeatingCoolingState to 1"
await hass.async_add_executor_job(acc.char_target_heat_cool.client_update_value, 3) driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_heat_cool_iid,
HAP_REPR_VALUE: 3,
},
]
},
"mock_addr",
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert call_set_hvac_mode 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_ENTITY_ID] == entity_id
assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT_COOL assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT_COOL
assert acc.char_target_heat_cool.value == 3 assert acc.char_target_heat_cool.value == 3
assert len(events) == 3 assert len(events) == 3
assert events[-1].data[ATTR_VALUE] == HVAC_MODE_HEAT_COOL 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, cls, events, driver):
"""Test if accessory and HA are updated accordingly.""" """Test if accessory and HA are updated accordingly."""
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: 6}) hass.states.async_set(
entity_id,
HVAC_MODE_OFF,
{
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE
| 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, 2, None) acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
await hass.async_block_till_done() await hass.async_block_till_done()
@ -316,10 +393,10 @@ async def test_thermostat_auto(hass, hk_driver, cls, events):
assert acc.char_heating_thresh_temp.value == 19.0 assert acc.char_heating_thresh_temp.value == 19.0
assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] == DEFAULT_MIN_TEMP assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] == HC_MIN_TEMP
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_STEP] == 0.1 assert acc.char_cooling_thresh_temp.properties[PROP_MIN_STEP] == 0.1
assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == DEFAULT_MIN_TEMP assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == HC_MIN_TEMP
assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1 assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1
hass.states.async_set( hass.states.async_set(
@ -379,37 +456,51 @@ async def test_thermostat_auto(hass, hk_driver, cls, events):
# Set from HomeKit # Set from HomeKit
call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, "set_temperature") call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, "set_temperature")
await hass.async_add_executor_job( char_heating_thresh_temp_iid = acc.char_heating_thresh_temp.to_HAP()[HAP_REPR_IID]
acc.char_heating_thresh_temp.client_update_value, 20.0 char_cooling_thresh_temp_iid = acc.char_cooling_thresh_temp.to_HAP()[HAP_REPR_IID]
driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_heating_thresh_temp_iid,
HAP_REPR_VALUE: 20.0,
},
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_cooling_thresh_temp_iid,
HAP_REPR_VALUE: 25.0,
},
]
},
"mock_addr",
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert call_set_temperature[0] assert call_set_temperature[0]
assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 20.0 assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 20.0
assert call_set_temperature[0].data[ATTR_TARGET_TEMP_HIGH] == 25.0
assert acc.char_heating_thresh_temp.value == 20.0 assert acc.char_heating_thresh_temp.value == 20.0
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "heating threshold 20.0°C"
await hass.async_add_executor_job(
acc.char_cooling_thresh_temp.client_update_value, 25.0
)
await hass.async_block_till_done()
assert call_set_temperature[1]
assert call_set_temperature[1].data[ATTR_ENTITY_ID] == entity_id
assert call_set_temperature[1].data[ATTR_TARGET_TEMP_HIGH] == 25.0
assert acc.char_cooling_thresh_temp.value == 25.0 assert acc.char_cooling_thresh_temp.value == 25.0
assert len(events) == 2 assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "cooling threshold 25.0°C" assert (
events[-1].data[ATTR_VALUE]
== "CoolingThresholdTemperature to 25.0°C, HeatingThresholdTemperature to 20.0°C"
)
async def test_thermostat_humidity(hass, hk_driver, cls, events): async def test_thermostat_humidity(hass, hk_driver, cls, events, driver):
"""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, 2, None) acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
await hass.async_block_till_done() await hass.async_block_till_done()
@ -435,7 +526,21 @@ async def test_thermostat_humidity(hass, hk_driver, cls, events):
# Set from HomeKit # Set from HomeKit
call_set_humidity = async_mock_service(hass, DOMAIN_CLIMATE, "set_humidity") call_set_humidity = async_mock_service(hass, DOMAIN_CLIMATE, "set_humidity")
await hass.async_add_executor_job(acc.char_target_humidity.client_update_value, 35) char_target_humidity_iid = acc.char_target_humidity.to_HAP()[HAP_REPR_IID]
driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_humidity_iid,
HAP_REPR_VALUE: 35,
},
]
},
"mock_addr",
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert call_set_humidity[0] assert call_set_humidity[0]
assert call_set_humidity[0].data[ATTR_ENTITY_ID] == entity_id assert call_set_humidity[0].data[ATTR_ENTITY_ID] == entity_id
@ -445,7 +550,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, cls, events, driver):
"""Test if accessory and HA are updated accordingly.""" """Test if accessory and HA are updated accordingly."""
entity_id = "climate.test" entity_id = "climate.test"
@ -458,10 +563,19 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events):
ATTR_TEMPERATURE: 23.0, ATTR_TEMPERATURE: 23.0,
ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT,
ATTR_HVAC_MODES: [
HVAC_MODE_HEAT_COOL,
HVAC_MODE_COOL,
HVAC_MODE_AUTO,
HVAC_MODE_HEAT,
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 = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
await hass.async_block_till_done() await hass.async_block_till_done()
@ -475,6 +589,13 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events):
ATTR_TEMPERATURE: 23.0, ATTR_TEMPERATURE: 23.0,
ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
ATTR_HVAC_MODES: [
HVAC_MODE_HEAT_COOL,
HVAC_MODE_COOL,
HVAC_MODE_AUTO,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
],
}, },
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -488,6 +609,13 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events):
ATTR_TEMPERATURE: 23.0, ATTR_TEMPERATURE: 23.0,
ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
ATTR_HVAC_MODES: [
HVAC_MODE_HEAT_COOL,
HVAC_MODE_COOL,
HVAC_MODE_AUTO,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
],
}, },
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -497,31 +625,68 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events):
# Set from HomeKit # Set from HomeKit
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_add_executor_job(acc.char_target_heat_cool.client_update_value, 1) char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID]
driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_heat_cool_iid,
HAP_REPR_VALUE: 1,
},
]
},
"mock_addr",
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert call_set_hvac_mode 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_ENTITY_ID] == entity_id
assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT
assert acc.char_target_heat_cool.value == 1 assert acc.char_target_heat_cool.value == 1
assert len(events) == 1 assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == HVAC_MODE_HEAT assert events[-1].data[ATTR_VALUE] == "TargetHeatingCoolingState to 1"
driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_heat_cool_iid,
HAP_REPR_VALUE: 2,
},
]
},
"mock_addr",
)
await hass.async_add_executor_job(acc.char_target_heat_cool.client_update_value, 0)
await hass.async_block_till_done() await hass.async_block_till_done()
assert acc.char_target_heat_cool.value == 0 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_COOL
assert len(events) == 2 assert len(events) == 2
assert events[-1].data[ATTR_VALUE] == HVAC_MODE_OFF assert events[-1].data[ATTR_VALUE] == "TargetHeatingCoolingState to 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, cls, events, driver):
"""Test if accessory and HA are updated accordingly.""" """Test if accessory and HA are updated accordingly."""
entity_id = "climate.test" entity_id = "climate.test"
# support_ = True # support_ = True
hass.states.async_set(entity_id, HVAC_MODE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) hass.states.async_set(
entity_id,
HVAC_MODE_OFF,
{
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE
| SUPPORT_TARGET_TEMPERATURE_RANGE
},
)
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, 2, None) acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
await hass.async_block_till_done() await hass.async_block_till_done()
@ -533,6 +698,8 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events):
ATTR_TARGET_TEMP_LOW: 68.1, ATTR_TARGET_TEMP_LOW: 68.1,
ATTR_TEMPERATURE: 71.6, ATTR_TEMPERATURE: 71.6,
ATTR_CURRENT_TEMPERATURE: 73.4, ATTR_CURRENT_TEMPERATURE: 73.4,
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE
| SUPPORT_TARGET_TEMPERATURE_RANGE,
}, },
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -546,38 +713,73 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events):
# Set from HomeKit # Set from HomeKit
call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, "set_temperature") call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, "set_temperature")
await hass.async_add_executor_job( char_cooling_thresh_temp_iid = acc.char_cooling_thresh_temp.to_HAP()[HAP_REPR_IID]
acc.char_cooling_thresh_temp.client_update_value, 23 char_heating_thresh_temp_iid = acc.char_heating_thresh_temp.to_HAP()[HAP_REPR_IID]
char_target_temp_iid = acc.char_target_temp.to_HAP()[HAP_REPR_IID]
driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_cooling_thresh_temp_iid,
HAP_REPR_VALUE: 23,
},
]
},
"mock_addr",
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert call_set_temperature[0] assert call_set_temperature[0]
assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_temperature[0].data[ATTR_TARGET_TEMP_HIGH] == 73.5 assert call_set_temperature[0].data[ATTR_TARGET_TEMP_HIGH] == 73.5
assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 68 assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 68
assert len(events) == 1 assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "cooling threshold 73.5°F" assert events[-1].data[ATTR_VALUE] == "CoolingThresholdTemperature to 23°C"
await hass.async_add_executor_job( driver.set_characteristics(
acc.char_heating_thresh_temp.client_update_value, 22 {
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_heating_thresh_temp_iid,
HAP_REPR_VALUE: 22,
},
]
},
"mock_addr",
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert call_set_temperature[1] assert call_set_temperature[1]
assert call_set_temperature[1].data[ATTR_ENTITY_ID] == entity_id assert call_set_temperature[1].data[ATTR_ENTITY_ID] == entity_id
assert call_set_temperature[1].data[ATTR_TARGET_TEMP_HIGH] == 73.5 assert call_set_temperature[1].data[ATTR_TARGET_TEMP_HIGH] == 73.5
assert call_set_temperature[1].data[ATTR_TARGET_TEMP_LOW] == 71.5 assert call_set_temperature[1].data[ATTR_TARGET_TEMP_LOW] == 71.5
assert len(events) == 2 assert len(events) == 2
assert events[-1].data[ATTR_VALUE] == "heating threshold 71.5°F" assert events[-1].data[ATTR_VALUE] == "HeatingThresholdTemperature to 22°C"
await hass.async_add_executor_job(acc.char_target_temp.client_update_value, 24.0) driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_temp_iid,
HAP_REPR_VALUE: 24.0,
},
]
},
"mock_addr",
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert call_set_temperature[2] assert call_set_temperature[2]
assert call_set_temperature[2].data[ATTR_ENTITY_ID] == entity_id assert call_set_temperature[2].data[ATTR_ENTITY_ID] == entity_id
assert call_set_temperature[2].data[ATTR_TEMPERATURE] == 75.0 assert call_set_temperature[2].data[ATTR_TEMPERATURE] == 75.0
assert len(events) == 3 assert len(events) == 3
assert events[-1].data[ATTR_VALUE] == "75.0°F" 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, cls, driver):
"""Test if temperature range is evaluated correctly.""" """Test if temperature range is evaluated correctly."""
entity_id = "climate.test" entity_id = "climate.test"
@ -599,13 +801,15 @@ 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, cls, 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, 2, None) acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
await hass.async_block_till_done() await hass.async_block_till_done()
@ -657,7 +861,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, cls, 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"
@ -666,7 +870,9 @@ 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, 2, None) acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
await hass.async_block_till_done() await hass.async_block_till_done()
hap = acc.char_target_heat_cool.to_HAP() hap = acc.char_target_heat_cool.to_HAP()
@ -688,13 +894,13 @@ 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, cls, driver):
"""Test we get heat cool over auto.""" """Test we get heat cool over auto."""
entity_id = "climate.test" entity_id = "climate.test"
hass.states.async_set( hass.states.async_set(
entity_id, entity_id,
HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF,
{ {
ATTR_HVAC_MODES: [ ATTR_HVAC_MODES: [
HVAC_MODE_HEAT_COOL, HVAC_MODE_HEAT_COOL,
@ -707,12 +913,14 @@ 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, 2, None) acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
await hass.async_block_till_done() await hass.async_block_till_done()
hap = acc.char_target_heat_cool.to_HAP() hap = acc.char_target_heat_cool.to_HAP()
assert hap["valid-values"] == [0, 1, 3] assert hap["valid-values"] == [0, 1, 3]
assert acc.char_target_heat_cool.value == 3 assert acc.char_target_heat_cool.value == 0
await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 3) await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 3)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -727,7 +935,21 @@ async def test_thermostat_hvac_modes_with_auto_heat_cool(hass, hk_driver, cls):
await hass.async_block_till_done() await hass.async_block_till_done()
assert acc.char_target_heat_cool.value == 1 assert acc.char_target_heat_cool.value == 1
await hass.async_add_executor_job(acc.char_target_heat_cool.client_update_value, 3) char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID]
driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_heat_cool_iid,
HAP_REPR_VALUE: 3,
},
]
},
"mock_addr",
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert call_set_hvac_mode 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_ENTITY_ID] == entity_id
@ -735,24 +957,28 @@ 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, cls, 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"
hass.states.async_set( hass.states.async_set(
entity_id, entity_id,
HVAC_MODE_HEAT_COOL, HVAC_MODE_HEAT,
{ATTR_HVAC_MODES: [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF]}, {ATTR_HVAC_MODES: [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF]},
) )
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, 2, None) acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
await hass.async_block_till_done() await hass.async_block_till_done()
hap = acc.char_target_heat_cool.to_HAP() hap = acc.char_target_heat_cool.to_HAP()
assert hap["valid-values"] == [0, 1, 3] assert hap["valid-values"] == [0, 1, 3]
assert acc.char_target_heat_cool.value == 3 assert acc.char_target_heat_cool.value == 1
await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 3) await hass.async_add_executor_job(acc.char_target_heat_cool.set_value, 3)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -767,7 +993,21 @@ async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver, cls
await hass.async_block_till_done() await hass.async_block_till_done()
assert acc.char_target_heat_cool.value == 1 assert acc.char_target_heat_cool.value == 1
await hass.async_add_executor_job(acc.char_target_heat_cool.client_update_value, 3) char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID]
driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_heat_cool_iid,
HAP_REPR_VALUE: 3,
},
]
},
"mock_addr",
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert call_set_hvac_mode 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_ENTITY_ID] == entity_id
@ -775,7 +1015,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, cls, 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"
@ -784,7 +1024,9 @@ 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, 2, None) acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
await hass.async_block_till_done() await hass.async_block_till_done()
hap = acc.char_target_heat_cool.to_HAP() hap = acc.char_target_heat_cool.to_HAP()
@ -806,7 +1048,7 @@ async def test_thermostat_hvac_modes_with_auto_only(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_without_off(hass, hk_driver, cls): async def test_thermostat_hvac_modes_without_off(hass, hk_driver, cls, driver):
"""Test a thermostat that has no off.""" """Test a thermostat that has no off."""
entity_id = "climate.test" entity_id = "climate.test"
@ -815,7 +1057,9 @@ 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, 2, None) acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
driver.add_accessory(acc)
await acc.run_handler() await acc.run_handler()
await hass.async_block_till_done() await hass.async_block_till_done()
hap = acc.char_target_heat_cool.to_HAP() hap = acc.char_target_heat_cool.to_HAP()
@ -841,6 +1085,166 @@ 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, driver
):
"""Test a thermostat that only supports a range."""
entity_id = "climate.test"
# support_auto = True
hass.states.async_set(
entity_id,
HVAC_MODE_OFF,
{ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE},
)
await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
driver.add_accessory(acc)
await acc.run_handler()
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.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] == HC_MIN_TEMP
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_STEP] == 0.1
assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == HC_MIN_TEMP
assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1
hass.states.async_set(
entity_id,
HVAC_MODE_HEAT_COOL,
{
ATTR_TARGET_TEMP_HIGH: 22.0,
ATTR_TARGET_TEMP_LOW: 20.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT,
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE,
},
)
await hass.async_block_till_done()
assert acc.char_heating_thresh_temp.value == 20.0
assert acc.char_cooling_thresh_temp.value == 22.0
assert acc.char_current_heat_cool.value == 1
assert acc.char_target_heat_cool.value == 3
assert acc.char_current_temp.value == 18.0
assert acc.char_display_units.value == 0
hass.states.async_set(
entity_id,
HVAC_MODE_COOL,
{
ATTR_TARGET_TEMP_HIGH: 23.0,
ATTR_TARGET_TEMP_LOW: 19.0,
ATTR_CURRENT_TEMPERATURE: 24.0,
ATTR_HVAC_ACTION: CURRENT_HVAC_COOL,
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE,
},
)
await hass.async_block_till_done()
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 == 2
assert acc.char_current_temp.value == 24.0
assert acc.char_display_units.value == 0
hass.states.async_set(
entity_id,
HVAC_MODE_COOL,
{
ATTR_TARGET_TEMP_HIGH: 23.0,
ATTR_TARGET_TEMP_LOW: 19.0,
ATTR_CURRENT_TEMPERATURE: 21.0,
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE,
},
)
await hass.async_block_till_done()
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 == 0
assert acc.char_target_heat_cool.value == 2
assert acc.char_current_temp.value == 21.0
assert acc.char_display_units.value == 0
# Set from HomeKit
call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, "set_temperature")
char_target_temp_iid = acc.char_target_temp.to_HAP()[HAP_REPR_IID]
driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_temp_iid,
HAP_REPR_VALUE: 17.0,
}
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert call_set_temperature[0]
assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 12.0
assert call_set_temperature[0].data[ATTR_TARGET_TEMP_HIGH] == 17.0
assert acc.char_target_temp.value == 17.0
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "CoolingThresholdTemperature to 17.0°C"
hass.states.async_set(
entity_id,
HVAC_MODE_HEAT,
{
ATTR_TARGET_TEMP_HIGH: 23.0,
ATTR_TARGET_TEMP_LOW: 19.0,
ATTR_CURRENT_TEMPERATURE: 21.0,
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE,
},
)
await hass.async_block_till_done()
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 == 0
assert acc.char_target_heat_cool.value == 1
assert acc.char_current_temp.value == 21.0
assert acc.char_display_units.value == 0
# Set from HomeKit
call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, "set_temperature")
char_target_temp_iid = acc.char_target_temp.to_HAP()[HAP_REPR_IID]
driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_temp_iid,
HAP_REPR_VALUE: 27.0,
}
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert call_set_temperature[0]
assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 27.0
assert call_set_temperature[0].data[ATTR_TARGET_TEMP_HIGH] == 32.0
assert acc.char_target_temp.value == 27.0
assert len(events) == 2
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, cls, 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"
@ -875,7 +1279,7 @@ async def test_water_heater(hass, hk_driver, cls, events):
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert acc.char_target_temp.value == 56.0 assert acc.char_target_temp.value == 56.0
assert acc.char_current_temp.value == 56.0 assert acc.char_current_temp.value == 50.0
assert acc.char_target_heat_cool.value == 1 assert acc.char_target_heat_cool.value == 1
assert acc.char_current_heat_cool.value == 1 assert acc.char_current_heat_cool.value == 1
assert acc.char_display_units.value == 0 assert acc.char_display_units.value == 0
@ -929,7 +1333,7 @@ async def test_water_heater_fahrenheit(hass, hk_driver, cls, events):
hass.states.async_set(entity_id, HVAC_MODE_HEAT, {ATTR_TEMPERATURE: 131}) hass.states.async_set(entity_id, HVAC_MODE_HEAT, {ATTR_TEMPERATURE: 131})
await hass.async_block_till_done() await hass.async_block_till_done()
assert acc.char_target_temp.value == 55.0 assert acc.char_target_temp.value == 55.0
assert acc.char_current_temp.value == 55.0 assert acc.char_current_temp.value == 50
assert acc.char_display_units.value == 1 assert acc.char_display_units.value == 1
# Set from HomeKit # Set from HomeKit