Add humidity support to homekit thermostats (#33367)

This commit is contained in:
J. Nick Koston 2020-03-31 13:45:33 -05:00 committed by GitHub
parent f5cbc9d208
commit 6cafc9aaef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 103 additions and 0 deletions

View File

@ -142,6 +142,7 @@ CHAR_SWING_MODE = "SwingMode"
CHAR_TARGET_DOOR_STATE = "TargetDoorState"
CHAR_TARGET_HEATING_COOLING = "TargetHeatingCoolingState"
CHAR_TARGET_POSITION = "TargetPosition"
CHAR_TARGET_HUMIDITY = "TargetRelativeHumidity"
CHAR_TARGET_SECURITY_STATE = "SecuritySystemTargetState"
CHAR_TARGET_TEMPERATURE = "TargetTemperature"
CHAR_TARGET_TILT_ANGLE = "TargetHorizontalTiltAngle"

View File

@ -4,11 +4,14 @@ import logging
from pyhap.const import CATEGORY_THERMOSTAT
from homeassistant.components.climate.const import (
ATTR_CURRENT_HUMIDITY,
ATTR_CURRENT_TEMPERATURE,
ATTR_HUMIDITY,
ATTR_HVAC_ACTION,
ATTR_HVAC_MODE,
ATTR_HVAC_MODES,
ATTR_MAX_TEMP,
ATTR_MIN_HUMIDITY,
ATTR_MIN_TEMP,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
@ -17,6 +20,7 @@ from homeassistant.components.climate.const import (
CURRENT_HVAC_IDLE,
CURRENT_HVAC_OFF,
DEFAULT_MAX_TEMP,
DEFAULT_MIN_HUMIDITY,
DEFAULT_MIN_TEMP,
DOMAIN as DOMAIN_CLIMATE,
HVAC_MODE_AUTO,
@ -25,8 +29,10 @@ from homeassistant.components.climate.const import (
HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
SERVICE_SET_HUMIDITY,
SERVICE_SET_HVAC_MODE as SERVICE_SET_HVAC_MODE_THERMOSTAT,
SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT,
SUPPORT_TARGET_HUMIDITY,
SUPPORT_TARGET_TEMPERATURE_RANGE,
)
from homeassistant.components.water_heater import (
@ -39,6 +45,7 @@ from homeassistant.const import (
ATTR_TEMPERATURE,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
UNIT_PERCENTAGE,
)
from . import TYPES
@ -46,9 +53,11 @@ from .accessories import HomeAccessory, debounce
from .const import (
CHAR_COOLING_THRESHOLD_TEMPERATURE,
CHAR_CURRENT_HEATING_COOLING,
CHAR_CURRENT_HUMIDITY,
CHAR_CURRENT_TEMPERATURE,
CHAR_HEATING_THRESHOLD_TEMPERATURE,
CHAR_TARGET_HEATING_COOLING,
CHAR_TARGET_HUMIDITY,
CHAR_TARGET_TEMPERATURE,
CHAR_TEMP_DISPLAY_UNITS,
DEFAULT_MAX_TEMP_WATER_HEATER,
@ -99,6 +108,10 @@ class Thermostat(HomeAccessory):
self._flag_heatingthresh = False
min_temp, max_temp = self.get_temperature_range()
min_humidity = self.hass.states.get(self.entity_id).attributes.get(
ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY
)
# Add additional characteristics if auto mode is supported
self.chars = []
state = self.hass.states.get(self.entity_id)
@ -109,6 +122,9 @@ class Thermostat(HomeAccessory):
(CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
)
if features & SUPPORT_TARGET_HUMIDITY:
self.chars.extend((CHAR_TARGET_HUMIDITY, CHAR_CURRENT_HUMIDITY))
serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars)
# Current mode characteristics
@ -193,6 +209,23 @@ class Thermostat(HomeAccessory):
properties={PROP_MIN_VALUE: min_temp, PROP_MAX_VALUE: max_temp},
setter_callback=self.set_heating_threshold,
)
self.char_target_humidity = None
self.char_current_humidity = None
if CHAR_TARGET_HUMIDITY in self.chars:
self.char_target_humidity = serv_thermostat.configure_char(
CHAR_TARGET_HUMIDITY,
value=50,
# We do not set a max humidity because
# homekit currently has a bug that will show the lower bound
# shifted upwards. For example if you have a max humidity
# 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,
)
def get_temperature_range(self):
"""Return min and max temperature range."""
@ -224,6 +257,15 @@ class Thermostat(HomeAccessory):
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)
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HUMIDITY: value}
self.call_service(
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."""
@ -288,6 +330,12 @@ class Thermostat(HomeAccessory):
current_temp = temperature_to_homekit(current_temp, self._unit)
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)):
@ -296,6 +344,12 @@ class Thermostat(HomeAccessory):
self.char_target_temp.set_value(target_temp)
self._flag_temperature = False
# 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)
# Update cooling threshold temperature if characteristic exists
if self.char_cooling_thresh_temp:
cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH)

View File

@ -5,7 +5,9 @@ from unittest.mock import patch
import pytest
from homeassistant.components.climate.const import (
ATTR_CURRENT_HUMIDITY,
ATTR_CURRENT_TEMPERATURE,
ATTR_HUMIDITY,
ATTR_HVAC_ACTION,
ATTR_HVAC_MODE,
ATTR_HVAC_MODES,
@ -18,6 +20,7 @@ from homeassistant.components.climate.const import (
CURRENT_HVAC_HEAT,
CURRENT_HVAC_IDLE,
DEFAULT_MAX_TEMP,
DEFAULT_MIN_HUMIDITY,
DEFAULT_MIN_TEMP,
DOMAIN as DOMAIN_CLIMATE,
HVAC_MODE_AUTO,
@ -99,6 +102,8 @@ async def test_thermostat(hass, hk_driver, cls, events):
assert acc.char_display_units.value == 0
assert acc.char_cooling_thresh_temp is None
assert acc.char_heating_thresh_temp is None
assert acc.char_target_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_MIN_VALUE] == DEFAULT_MIN_TEMP
@ -357,6 +362,49 @@ async def test_thermostat_auto(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] == "cooling threshold 25.0°C"
async def test_thermostat_humidity(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly with humidity."""
entity_id = "climate.test"
# support_auto = True
hass.states.async_set(entity_id, HVAC_MODE_OFF, {ATTR_SUPPORTED_FEATURES: 4})
await hass.async_block_till_done()
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None)
await hass.async_add_job(acc.run)
await hass.async_block_till_done()
assert acc.char_target_humidity.value == 50
assert acc.char_current_humidity.value == 50
assert acc.char_target_humidity.properties[PROP_MIN_VALUE] == DEFAULT_MIN_HUMIDITY
hass.states.async_set(
entity_id, HVAC_MODE_HEAT_COOL, {ATTR_HUMIDITY: 65, ATTR_CURRENT_HUMIDITY: 40},
)
await hass.async_block_till_done()
assert acc.char_current_humidity.value == 40
assert acc.char_target_humidity.value == 65
hass.states.async_set(
entity_id, HVAC_MODE_COOL, {ATTR_HUMIDITY: 35, ATTR_CURRENT_HUMIDITY: 70},
)
await hass.async_block_till_done()
assert acc.char_current_humidity.value == 70
assert acc.char_target_humidity.value == 35
# Set from HomeKit
call_set_humidity = async_mock_service(hass, DOMAIN_CLIMATE, "set_humidity")
await hass.async_add_job(acc.char_target_humidity.client_update_value, 35)
await hass.async_block_till_done()
assert call_set_humidity[0]
assert call_set_humidity[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_humidity[0].data[ATTR_HUMIDITY] == 35
assert acc.char_target_humidity.value == 35
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "35%"
async def test_thermostat_power_state(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = "climate.test"