mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +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_DOOR_STATE = "TargetDoorState"
|
||||||
CHAR_TARGET_HEATING_COOLING = "TargetHeatingCoolingState"
|
CHAR_TARGET_HEATING_COOLING = "TargetHeatingCoolingState"
|
||||||
CHAR_TARGET_POSITION = "TargetPosition"
|
CHAR_TARGET_POSITION = "TargetPosition"
|
||||||
|
CHAR_TARGET_HUMIDITY = "TargetRelativeHumidity"
|
||||||
CHAR_TARGET_SECURITY_STATE = "SecuritySystemTargetState"
|
CHAR_TARGET_SECURITY_STATE = "SecuritySystemTargetState"
|
||||||
CHAR_TARGET_TEMPERATURE = "TargetTemperature"
|
CHAR_TARGET_TEMPERATURE = "TargetTemperature"
|
||||||
CHAR_TARGET_TILT_ANGLE = "TargetHorizontalTiltAngle"
|
CHAR_TARGET_TILT_ANGLE = "TargetHorizontalTiltAngle"
|
||||||
|
@ -4,11 +4,14 @@ import logging
|
|||||||
from pyhap.const import CATEGORY_THERMOSTAT
|
from pyhap.const import CATEGORY_THERMOSTAT
|
||||||
|
|
||||||
from homeassistant.components.climate.const import (
|
from homeassistant.components.climate.const import (
|
||||||
|
ATTR_CURRENT_HUMIDITY,
|
||||||
ATTR_CURRENT_TEMPERATURE,
|
ATTR_CURRENT_TEMPERATURE,
|
||||||
|
ATTR_HUMIDITY,
|
||||||
ATTR_HVAC_ACTION,
|
ATTR_HVAC_ACTION,
|
||||||
ATTR_HVAC_MODE,
|
ATTR_HVAC_MODE,
|
||||||
ATTR_HVAC_MODES,
|
ATTR_HVAC_MODES,
|
||||||
ATTR_MAX_TEMP,
|
ATTR_MAX_TEMP,
|
||||||
|
ATTR_MIN_HUMIDITY,
|
||||||
ATTR_MIN_TEMP,
|
ATTR_MIN_TEMP,
|
||||||
ATTR_TARGET_TEMP_HIGH,
|
ATTR_TARGET_TEMP_HIGH,
|
||||||
ATTR_TARGET_TEMP_LOW,
|
ATTR_TARGET_TEMP_LOW,
|
||||||
@ -17,6 +20,7 @@ from homeassistant.components.climate.const import (
|
|||||||
CURRENT_HVAC_IDLE,
|
CURRENT_HVAC_IDLE,
|
||||||
CURRENT_HVAC_OFF,
|
CURRENT_HVAC_OFF,
|
||||||
DEFAULT_MAX_TEMP,
|
DEFAULT_MAX_TEMP,
|
||||||
|
DEFAULT_MIN_HUMIDITY,
|
||||||
DEFAULT_MIN_TEMP,
|
DEFAULT_MIN_TEMP,
|
||||||
DOMAIN as DOMAIN_CLIMATE,
|
DOMAIN as DOMAIN_CLIMATE,
|
||||||
HVAC_MODE_AUTO,
|
HVAC_MODE_AUTO,
|
||||||
@ -25,8 +29,10 @@ 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,
|
||||||
|
SERVICE_SET_HUMIDITY,
|
||||||
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_TEMPERATURE_RANGE,
|
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||||
)
|
)
|
||||||
from homeassistant.components.water_heater import (
|
from homeassistant.components.water_heater import (
|
||||||
@ -39,6 +45,7 @@ from homeassistant.const import (
|
|||||||
ATTR_TEMPERATURE,
|
ATTR_TEMPERATURE,
|
||||||
TEMP_CELSIUS,
|
TEMP_CELSIUS,
|
||||||
TEMP_FAHRENHEIT,
|
TEMP_FAHRENHEIT,
|
||||||
|
UNIT_PERCENTAGE,
|
||||||
)
|
)
|
||||||
|
|
||||||
from . import TYPES
|
from . import TYPES
|
||||||
@ -46,9 +53,11 @@ from .accessories import HomeAccessory, debounce
|
|||||||
from .const import (
|
from .const import (
|
||||||
CHAR_COOLING_THRESHOLD_TEMPERATURE,
|
CHAR_COOLING_THRESHOLD_TEMPERATURE,
|
||||||
CHAR_CURRENT_HEATING_COOLING,
|
CHAR_CURRENT_HEATING_COOLING,
|
||||||
|
CHAR_CURRENT_HUMIDITY,
|
||||||
CHAR_CURRENT_TEMPERATURE,
|
CHAR_CURRENT_TEMPERATURE,
|
||||||
CHAR_HEATING_THRESHOLD_TEMPERATURE,
|
CHAR_HEATING_THRESHOLD_TEMPERATURE,
|
||||||
CHAR_TARGET_HEATING_COOLING,
|
CHAR_TARGET_HEATING_COOLING,
|
||||||
|
CHAR_TARGET_HUMIDITY,
|
||||||
CHAR_TARGET_TEMPERATURE,
|
CHAR_TARGET_TEMPERATURE,
|
||||||
CHAR_TEMP_DISPLAY_UNITS,
|
CHAR_TEMP_DISPLAY_UNITS,
|
||||||
DEFAULT_MAX_TEMP_WATER_HEATER,
|
DEFAULT_MAX_TEMP_WATER_HEATER,
|
||||||
@ -99,6 +108,10 @@ class Thermostat(HomeAccessory):
|
|||||||
self._flag_heatingthresh = False
|
self._flag_heatingthresh = False
|
||||||
min_temp, max_temp = self.get_temperature_range()
|
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
|
# Add additional characteristics if auto mode is supported
|
||||||
self.chars = []
|
self.chars = []
|
||||||
state = self.hass.states.get(self.entity_id)
|
state = self.hass.states.get(self.entity_id)
|
||||||
@ -109,6 +122,9 @@ class Thermostat(HomeAccessory):
|
|||||||
(CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
|
(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)
|
serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars)
|
||||||
|
|
||||||
# Current mode characteristics
|
# Current mode characteristics
|
||||||
@ -193,6 +209,23 @@ class Thermostat(HomeAccessory):
|
|||||||
properties={PROP_MIN_VALUE: min_temp, PROP_MAX_VALUE: max_temp},
|
properties={PROP_MIN_VALUE: min_temp, PROP_MAX_VALUE: max_temp},
|
||||||
setter_callback=self.set_heating_threshold,
|
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):
|
def get_temperature_range(self):
|
||||||
"""Return min and max temperature range."""
|
"""Return min and max temperature range."""
|
||||||
@ -224,6 +257,15 @@ class Thermostat(HomeAccessory):
|
|||||||
DOMAIN_CLIMATE, SERVICE_SET_HVAC_MODE_THERMOSTAT, params, hass_value
|
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
|
@debounce
|
||||||
def set_cooling_threshold(self, value):
|
def set_cooling_threshold(self, value):
|
||||||
"""Set cooling threshold temp to value if call came from HomeKit."""
|
"""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)
|
current_temp = temperature_to_homekit(current_temp, self._unit)
|
||||||
self.char_current_temp.set_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
|
# Update target temperature
|
||||||
target_temp = new_state.attributes.get(ATTR_TEMPERATURE)
|
target_temp = new_state.attributes.get(ATTR_TEMPERATURE)
|
||||||
if isinstance(target_temp, (int, float)):
|
if isinstance(target_temp, (int, float)):
|
||||||
@ -296,6 +344,12 @@ class Thermostat(HomeAccessory):
|
|||||||
self.char_target_temp.set_value(target_temp)
|
self.char_target_temp.set_value(target_temp)
|
||||||
self._flag_temperature = False
|
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
|
# 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)
|
||||||
|
@ -5,7 +5,9 @@ from unittest.mock import patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.climate.const import (
|
from homeassistant.components.climate.const import (
|
||||||
|
ATTR_CURRENT_HUMIDITY,
|
||||||
ATTR_CURRENT_TEMPERATURE,
|
ATTR_CURRENT_TEMPERATURE,
|
||||||
|
ATTR_HUMIDITY,
|
||||||
ATTR_HVAC_ACTION,
|
ATTR_HVAC_ACTION,
|
||||||
ATTR_HVAC_MODE,
|
ATTR_HVAC_MODE,
|
||||||
ATTR_HVAC_MODES,
|
ATTR_HVAC_MODES,
|
||||||
@ -18,6 +20,7 @@ from homeassistant.components.climate.const import (
|
|||||||
CURRENT_HVAC_HEAT,
|
CURRENT_HVAC_HEAT,
|
||||||
CURRENT_HVAC_IDLE,
|
CURRENT_HVAC_IDLE,
|
||||||
DEFAULT_MAX_TEMP,
|
DEFAULT_MAX_TEMP,
|
||||||
|
DEFAULT_MIN_HUMIDITY,
|
||||||
DEFAULT_MIN_TEMP,
|
DEFAULT_MIN_TEMP,
|
||||||
DOMAIN as DOMAIN_CLIMATE,
|
DOMAIN as DOMAIN_CLIMATE,
|
||||||
HVAC_MODE_AUTO,
|
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_display_units.value == 0
|
||||||
assert acc.char_cooling_thresh_temp is None
|
assert acc.char_cooling_thresh_temp is None
|
||||||
assert acc.char_heating_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_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] == 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"
|
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):
|
async def test_thermostat_power_state(hass, hk_driver, cls, events):
|
||||||
"""Test if accessory and HA are updated accordingly."""
|
"""Test if accessory and HA are updated accordingly."""
|
||||||
entity_id = "climate.test"
|
entity_id = "climate.test"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user