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/hive/* @Rendili @KJonline
homeassistant/components/homeassistant/* @home-assistant/core
homeassistant/components/homekit/* @bdraco
homeassistant/components/homekit_controller/* @Jc2k
homeassistant/components/homematic/* @pvizeli @danielperna84
homeassistant/components/homematicip_cloud/* @SukramJ

View File

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

View File

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

View File

@ -35,7 +35,7 @@ class Lock(HomeAccessory):
"""Initialize a Lock accessory object."""
super().__init__(*args, category=CATEGORY_DOOR_LOCK)
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)
self.char_current_state = serv_lock_mechanism.configure_char(
@ -46,15 +46,18 @@ class Lock(HomeAccessory):
value=HASS_TO_HOMEKIT[STATE_LOCKED],
setter_callback=self.set_state,
)
self.update_state(state)
def set_state(self, value):
"""Set lock state to value if call came from HomeKit."""
_LOGGER.debug("%s: Set state to %d", self.entity_id, value)
self._flag_state = True
hass_value = HOMEKIT_TO_HASS.get(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}
if self._code:
params[ATTR_CODE] = self._code
@ -65,16 +68,21 @@ class Lock(HomeAccessory):
hass_state = new_state.state
if hass_state in HASS_TO_HOMEKIT:
current_lock_state = HASS_TO_HOMEKIT[hass_state]
self.char_current_state.set_value(current_lock_state)
_LOGGER.debug(
"%s: Updated current state to %s (%d)",
self.entity_id,
hass_state,
current_lock_state,
)
# 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 not self._flag_state:
if self.char_target_state.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_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT,
SUPPORT_TARGET_HUMIDITY,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
)
from homeassistant.components.water_heater import (
@ -52,7 +53,7 @@ from homeassistant.const import (
)
from . import TYPES
from .accessories import HomeAccessory, debounce
from .accessories import HomeAccessory
from .const import (
CHAR_COOLING_THRESHOLD_TEMPERATURE,
CHAR_CURRENT_HEATING_COOLING,
@ -76,26 +77,37 @@ _LOGGER = logging.getLogger(__name__)
HC_HOMEKIT_VALID_MODES_WATER_HEATER = {"Heat": 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()}
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,
HVAC_MODE_OFF: HC_HEAT_COOL_OFF,
HVAC_MODE_HEAT: HC_HEAT_COOL_HEAT,
HVAC_MODE_COOL: HC_HEAT_COOL_COOL,
HVAC_MODE_AUTO: HC_HEAT_COOL_AUTO,
HVAC_MODE_HEAT_COOL: HC_HEAT_COOL_AUTO,
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_HASS_TO_HOMEKIT_ACTION = {
CURRENT_HVAC_OFF: 0,
CURRENT_HVAC_IDLE: 0,
CURRENT_HVAC_HEAT: 1,
CURRENT_HVAC_COOL: 2,
CURRENT_HVAC_DRY: 2,
CURRENT_HVAC_FAN: 2,
CURRENT_HVAC_OFF: HC_HEAT_COOL_OFF,
CURRENT_HVAC_IDLE: HC_HEAT_COOL_OFF,
CURRENT_HVAC_HEAT: HC_HEAT_COOL_HEAT,
CURRENT_HVAC_COOL: HC_HEAT_COOL_COOL,
CURRENT_HVAC_DRY: HC_HEAT_COOL_COOL,
CURRENT_HVAC_FAN: HC_HEAT_COOL_COOL,
}
HEAT_COOL_DEADBAND = 5
@TYPES.register("Thermostat")
class Thermostat(HomeAccessory):
@ -105,12 +117,12 @@ class Thermostat(HomeAccessory):
"""Initialize a Thermostat accessory object."""
super().__init__(*args, category=CATEGORY_THERMOSTAT)
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()
# 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(
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()}
self.char_target_heat_cool = serv_thermostat.configure_char(
CHAR_TARGET_HEATING_COOLING,
value=list(hc_valid_values.values())[0],
setter_callback=self.set_heat_cool,
valid_values=hc_valid_values,
CHAR_TARGET_HEATING_COOLING, valid_values=hc_valid_values,
)
# Current and target temperature characteristics
self.char_current_temp = serv_thermostat.configure_char(
CHAR_CURRENT_TEMPERATURE, value=21.0
)
self.char_target_temp = serv_thermostat.configure_char(
CHAR_TARGET_TEMPERATURE,
value=21.0,
# 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
# temperature units and avoid setting to 73F will result in 74F
properties={PROP_MIN_VALUE: min_temp, PROP_MAX_VALUE: max_temp},
setter_callback=self.set_target_temperature,
properties={PROP_MIN_VALUE: hc_min_temp, PROP_MAX_VALUE: hc_max_temp},
)
# Display units characteristic
@ -209,8 +219,7 @@ class Thermostat(HomeAccessory):
# 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
# temperature units and avoid setting to 73F will result in 74F
properties={PROP_MIN_VALUE: min_temp, PROP_MAX_VALUE: max_temp},
setter_callback=self.set_cooling_threshold,
properties={PROP_MIN_VALUE: hc_min_temp, PROP_MAX_VALUE: hc_max_temp},
)
if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars:
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
# default of 0.1 in order to have enough precision to convert
# temperature units and avoid setting to 73F will result in 74F
properties={PROP_MIN_VALUE: min_temp, PROP_MAX_VALUE: max_temp},
setter_callback=self.set_heating_threshold,
properties={PROP_MIN_VALUE: hc_min_temp, PROP_MAX_VALUE: hc_max_temp},
)
self.char_target_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 0-80%
properties={PROP_MIN_VALUE: min_humidity},
setter_callback=self.set_target_humidity,
)
self.char_current_humidity = serv_thermostat.configure_char(
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):
"""Return min and max temperature range."""
max_temp = self.hass.states.get(self.entity_id).attributes.get(ATTR_MAX_TEMP)
max_temp = (
temperature_to_homekit(max_temp, self._unit)
if max_temp
else DEFAULT_MAX_TEMP
self._temperature_to_homekit(max_temp) if max_temp else DEFAULT_MAX_TEMP
)
max_temp = round(max_temp * 2) / 2
min_temp = self.hass.states.get(self.entity_id).attributes.get(ATTR_MIN_TEMP)
min_temp = (
temperature_to_homekit(min_temp, self._unit)
if min_temp
else DEFAULT_MIN_TEMP
self._temperature_to_homekit(min_temp) if min_temp else DEFAULT_MIN_TEMP
)
min_temp = round(min_temp * 2) / 2
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):
"""Set target humidity to value if call came from HomeKit."""
_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}"
)
@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):
"""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
current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE)
if isinstance(current_temp, (int, float)):
current_temp = temperature_to_homekit(current_temp, self._unit)
self.char_current_temp.set_value(current_temp)
current_temp = self._temperature_to_homekit(current_temp)
if self.char_current_temp.value != current_temp:
self.char_current_temp.set_value(current_temp)
# Update current humidity
if CHAR_CURRENT_HUMIDITY in self.chars:
current_humdity = new_state.attributes.get(ATTR_CURRENT_HUMIDITY)
if isinstance(current_humdity, (int, float)):
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
if self.char_current_humidity.value != current_humdity:
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)
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
if self.char_cooling_thresh_temp:
cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH)
if isinstance(cooling_thresh, (int, float)):
cooling_thresh = temperature_to_homekit(cooling_thresh, self._unit)
if not self._flag_coolingthresh:
cooling_thresh = self._temperature_to_homekit(cooling_thresh)
if self.char_heating_thresh_temp.value != cooling_thresh:
self.char_cooling_thresh_temp.set_value(cooling_thresh)
self._flag_coolingthresh = False
# Update heating threshold temperature if characteristic exists
if self.char_heating_thresh_temp:
heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW)
if isinstance(heating_thresh, (int, float)):
heating_thresh = temperature_to_homekit(heating_thresh, self._unit)
if not self._flag_heatingthresh:
heating_thresh = self._temperature_to_homekit(heating_thresh)
if self.char_heating_thresh_temp.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
if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT:
self.char_display_units.set_value(UNIT_HASS_TO_HOMEKIT[self._unit])
# Update target operation mode
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]
)
unit = UNIT_HASS_TO_HOMEKIT[self._unit]
if self.char_display_units.value != unit:
self.char_display_units.set_value(unit)
@TYPES.register("WaterHeater")
@ -409,8 +467,6 @@ class WaterHeater(HomeAccessory):
"""Initialize a WaterHeater accessory object."""
super().__init__(*args, category=CATEGORY_THERMOSTAT)
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()
serv_thermostat = self.add_preload_service(SERV_THERMOSTAT)
@ -442,6 +498,9 @@ class WaterHeater(HomeAccessory):
CHAR_TEMP_DISPLAY_UNITS, value=0
)
state = self.hass.states.get(self.entity_id)
self.update_state(state)
def get_temperature_range(self):
"""Return min and max temperature range."""
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):
"""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]
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):
"""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(
@ -490,17 +547,16 @@ class WaterHeater(HomeAccessory):
temperature = new_state.attributes.get(ATTR_TEMPERATURE)
if isinstance(temperature, (int, float)):
temperature = temperature_to_homekit(temperature, self._unit)
self.char_current_temp.set_value(temperature)
if not self._flag_temperature:
if temperature != self.char_current_temp.value:
self.char_target_temp.set_value(temperature)
self._flag_temperature = False
# Update display units
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
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._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.category == 5 # Lightbulb
assert acc.char_on.value == 0
assert acc.char_on.value
await acc.run_handler()
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)
driver.add_accessory(acc)
assert acc.char_color_temperature.value == 153
assert acc.char_color_temperature.value == 190
await acc.run_handler()
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)
driver.add_accessory(acc)
assert acc.char_hue.value == 0
assert acc.char_saturation.value == 75
assert acc.char_hue.value == 260
assert acc.char_saturation.value == 90
await acc.run_handler()
await hass.async_block_till_done()

View File

@ -2,6 +2,8 @@
from collections import namedtuple
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
from homeassistant.components.climate.const import (
@ -23,7 +25,6 @@ from homeassistant.components.climate.const import (
CURRENT_HVAC_IDLE,
DEFAULT_MAX_TEMP,
DEFAULT_MIN_HUMIDITY,
DEFAULT_MIN_TEMP,
DOMAIN as DOMAIN_CLIMATE,
HVAC_MODE_AUTO,
HVAC_MODE_COOL,
@ -32,6 +33,8 @@ from homeassistant.components.climate.const import (
HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
)
from homeassistant.components.homekit.const import (
ATTR_VALUE,
@ -41,6 +44,7 @@ from homeassistant.components.homekit.const import (
PROP_MIN_STEP,
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.const import (
ATTR_ENTITY_ID,
@ -58,6 +62,15 @@ from tests.common import async_mock_service
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")
def cls():
"""Patch debounce decorator during import of type_thermostats."""
@ -65,14 +78,14 @@ def cls():
patcher.start()
_import = __import__(
"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)
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."""
entity_id = "climate.test"
@ -80,6 +93,7 @@ async def test_thermostat(hass, hk_driver, cls, events):
entity_id,
HVAC_MODE_OFF,
{
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE,
ATTR_HVAC_MODES: [
HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
@ -87,15 +101,17 @@ async def test_thermostat(hass, hk_driver, cls, events):
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)
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.aid == 2
assert acc.aid == 1
assert acc.category == 9 # Thermostat
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_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
hass.states.async_set(
@ -257,6 +273,7 @@ async def test_thermostat(hass, hk_driver, cls, events):
entity_id,
HVAC_MODE_DRY,
{
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE,
ATTR_TEMPERATURE: 22.0,
ATTR_CURRENT_TEMPERATURE: 22.0,
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_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()
assert call_set_temperature
assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_temperature[0].data[ATTR_TEMPERATURE] == 19.0
assert acc.char_target_temp.value == 19.0
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()
assert call_set_hvac_mode
assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_COOL
assert acc.char_target_heat_cool.value == 2
assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT
assert acc.char_target_heat_cool.value == 1
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()
assert call_set_hvac_mode
assert call_set_hvac_mode[1].data[ATTR_ENTITY_ID] == entity_id
assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT_COOL
assert acc.char_target_heat_cool.value == 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."""
entity_id = "climate.test"
# 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()
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 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_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_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
hass.states.async_set(
@ -379,37 +456,51 @@ async def test_thermostat_auto(hass, hk_driver, cls, events):
# Set from HomeKit
call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, "set_temperature")
await hass.async_add_executor_job(
acc.char_heating_thresh_temp.client_update_value, 20.0
char_heating_thresh_temp_iid = acc.char_heating_thresh_temp.to_HAP()[HAP_REPR_IID]
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()
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] == 20.0
assert call_set_temperature[0].data[ATTR_TARGET_TEMP_HIGH] == 25.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 len(events) == 2
assert events[-1].data[ATTR_VALUE] == "cooling threshold 25.0°C"
assert len(events) == 1
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."""
entity_id = "climate.test"
# support_auto = True
hass.states.async_set(entity_id, HVAC_MODE_OFF, {ATTR_SUPPORTED_FEATURES: 4})
await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None)
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()
@ -435,7 +526,21 @@ async def test_thermostat_humidity(hass, hk_driver, cls, events):
# Set from HomeKit
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()
assert call_set_humidity[0]
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%"
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."""
entity_id = "climate.test"
@ -458,10 +563,19 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events):
ATTR_TEMPERATURE: 23.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
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()
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 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_CURRENT_TEMPERATURE: 18.0,
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()
@ -488,6 +609,13 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events):
ATTR_TEMPERATURE: 23.0,
ATTR_CURRENT_TEMPERATURE: 18.0,
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()
@ -497,31 +625,68 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events):
# Set from HomeKit
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()
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 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()
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 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."""
entity_id = "climate.test"
# 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()
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 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_TEMPERATURE: 71.6,
ATTR_CURRENT_TEMPERATURE: 73.4,
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE
| SUPPORT_TARGET_TEMPERATURE_RANGE,
},
)
await hass.async_block_till_done()
@ -546,38 +713,73 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events):
# Set from HomeKit
call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, "set_temperature")
await hass.async_add_executor_job(
acc.char_cooling_thresh_temp.client_update_value, 23
char_cooling_thresh_temp_iid = acc.char_cooling_thresh_temp.to_HAP()[HAP_REPR_IID]
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()
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_HIGH] == 73.5
assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 68
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(
acc.char_heating_thresh_temp.client_update_value, 22
driver.set_characteristics(
{
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()
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] == 73.5
assert call_set_temperature[1].data[ATTR_TARGET_TEMP_LOW] == 71.5
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()
assert call_set_temperature[2]
assert call_set_temperature[2].data[ATTR_ENTITY_ID] == entity_id
assert call_set_temperature[2].data[ATTR_TEMPERATURE] == 75.0
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."""
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)
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."""
entity_id = "climate.test"
hass.states.async_set(entity_id, HVAC_MODE_OFF, {ATTR_TARGET_TEMP_STEP: 1})
await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None)
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()
@ -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."""
entity_id = "climate.test"
@ -666,7 +870,9 @@ async def test_thermostat_hvac_modes(hass, hk_driver, cls):
)
await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None)
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()
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
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."""
entity_id = "climate.test"
hass.states.async_set(
entity_id,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
{
ATTR_HVAC_MODES: [
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")
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 hass.async_block_till_done()
hap = acc.char_target_heat_cool.to_HAP()
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_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()
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()
assert call_set_hvac_mode
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
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."""
entity_id = "climate.test"
hass.states.async_set(
entity_id,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_HEAT,
{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")
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 hass.async_block_till_done()
hap = acc.char_target_heat_cool.to_HAP()
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_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()
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()
assert call_set_hvac_mode
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
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."""
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()
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 hass.async_block_till_done()
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
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."""
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()
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 hass.async_block_till_done()
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
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):
"""Test if accessory and HA are updated accordingly."""
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()
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_current_heat_cool.value == 1
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})
await hass.async_block_till_done()
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
# Set from HomeKit