mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add humidity support to homekit thermostats (#33367)
This commit is contained in:
parent
f5cbc9d208
commit
6cafc9aaef
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user