Update HomeKit humidifiers to handle current humidity (#101964)

This commit is contained in:
J. Nick Koston 2023-10-13 14:23:50 -10:00 committed by GitHub
parent 8fd5d89d43
commit f8f39a29de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 94 additions and 27 deletions

View file

@ -5,6 +5,7 @@ from typing import Any
from pyhap.const import CATEGORY_HUMIDIFIER from pyhap.const import CATEGORY_HUMIDIFIER
from homeassistant.components.humidifier import ( from homeassistant.components.humidifier import (
ATTR_CURRENT_HUMIDITY,
ATTR_HUMIDITY, ATTR_HUMIDITY,
ATTR_MAX_HUMIDITY, ATTR_MAX_HUMIDITY,
ATTR_MIN_HUMIDITY, ATTR_MIN_HUMIDITY,
@ -64,11 +65,28 @@ HC_DEVICE_CLASS_TO_TARGET_CHAR = {
HC_DEHUMIDIFIER: CHAR_DEHUMIDIFIER_THRESHOLD_HUMIDITY, HC_DEHUMIDIFIER: CHAR_DEHUMIDIFIER_THRESHOLD_HUMIDITY,
} }
HC_STATE_INACTIVE = 0 HC_STATE_INACTIVE = 0
HC_STATE_IDLE = 1 HC_STATE_IDLE = 1
HC_STATE_HUMIDIFYING = 2 HC_STATE_HUMIDIFYING = 2
HC_STATE_DEHUMIDIFYING = 3 HC_STATE_DEHUMIDIFYING = 3
BASE_VALID_VALUES = {
"Inactive": HC_STATE_INACTIVE,
"Idle": HC_STATE_IDLE,
}
VALID_VALUES_BY_DEVICE_CLASS = {
HumidifierDeviceClass.HUMIDIFIER: {
**BASE_VALID_VALUES,
"Humidifying": HC_STATE_HUMIDIFYING,
},
HumidifierDeviceClass.DEHUMIDIFIER: {
**BASE_VALID_VALUES,
"Dehumidifying": HC_STATE_DEHUMIDIFYING,
},
}
@TYPES.register("HumidifierDehumidifier") @TYPES.register("HumidifierDehumidifier")
class HumidifierDehumidifier(HomeAccessory): class HumidifierDehumidifier(HomeAccessory):
@ -85,7 +103,8 @@ class HumidifierDehumidifier(HomeAccessory):
) )
self.chars: list[str] = [] self.chars: list[str] = []
state = self.hass.states.get(self.entity_id) states = self.hass.states
state = states.get(self.entity_id)
assert state assert state
device_class = state.attributes.get( device_class = state.attributes.get(
ATTR_DEVICE_CLASS, HumidifierDeviceClass.HUMIDIFIER ATTR_DEVICE_CLASS, HumidifierDeviceClass.HUMIDIFIER
@ -104,7 +123,9 @@ class HumidifierDehumidifier(HomeAccessory):
# Current and target mode characteristics # Current and target mode characteristics
self.char_current_humidifier_dehumidifier = ( self.char_current_humidifier_dehumidifier = (
serv_humidifier_dehumidifier.configure_char( serv_humidifier_dehumidifier.configure_char(
CHAR_CURRENT_HUMIDIFIER_DEHUMIDIFIER, value=0 CHAR_CURRENT_HUMIDIFIER_DEHUMIDIFIER,
value=0,
valid_values=VALID_VALUES_BY_DEVICE_CLASS[device_class],
) )
) )
self.char_target_humidifier_dehumidifier = ( self.char_target_humidifier_dehumidifier = (
@ -149,8 +170,7 @@ class HumidifierDehumidifier(HomeAccessory):
self.linked_humidity_sensor = self.config.get(CONF_LINKED_HUMIDITY_SENSOR) self.linked_humidity_sensor = self.config.get(CONF_LINKED_HUMIDITY_SENSOR)
if self.linked_humidity_sensor: if self.linked_humidity_sensor:
humidity_state = self.hass.states.get(self.linked_humidity_sensor) if humidity_state := states.get(self.linked_humidity_sensor):
if humidity_state:
self._async_update_current_humidity(humidity_state) self._async_update_current_humidity(humidity_state)
async def run(self) -> None: async def run(self) -> None:
@ -191,14 +211,6 @@ class HumidifierDehumidifier(HomeAccessory):
return return
try: try:
current_humidity = float(new_state.state) current_humidity = float(new_state.state)
if self.char_current_humidity.value != current_humidity:
_LOGGER.debug(
"%s: Linked humidity sensor %s changed to %d",
self.entity_id,
self.linked_humidity_sensor,
current_humidity,
)
self.char_current_humidity.set_value(current_humidity)
except ValueError as ex: except ValueError as ex:
_LOGGER.debug( _LOGGER.debug(
"%s: Unable to update from linked humidity sensor %s: %s", "%s: Unable to update from linked humidity sensor %s: %s",
@ -206,6 +218,20 @@ class HumidifierDehumidifier(HomeAccessory):
self.linked_humidity_sensor, self.linked_humidity_sensor,
ex, ex,
) )
return
self._async_update_current_humidity_value(current_humidity)
@callback
def _async_update_current_humidity_value(self, current_humidity: float) -> None:
"""Handle linked humidity or built-in humidity."""
if self.char_current_humidity.value != current_humidity:
_LOGGER.debug(
"%s: Linked humidity sensor %s changed to %d",
self.entity_id,
self.linked_humidity_sensor,
current_humidity,
)
self.char_current_humidity.set_value(current_humidity)
def _set_chars(self, char_values: dict[str, Any]) -> None: def _set_chars(self, char_values: dict[str, Any]) -> None:
"""Set characteristics based on the data coming from HomeKit.""" """Set characteristics based on the data coming from HomeKit."""
@ -229,19 +255,7 @@ class HumidifierDehumidifier(HomeAccessory):
if self._target_humidity_char_name in char_values: if self._target_humidity_char_name in char_values:
state = self.hass.states.get(self.entity_id) state = self.hass.states.get(self.entity_id)
assert state assert state
max_humidity = state.attributes.get(ATTR_MAX_HUMIDITY, DEFAULT_MAX_HUMIDITY) min_humidity, max_humidity = self.get_humidity_range(state)
max_humidity = round(max_humidity)
max_humidity = min(max_humidity, 100)
min_humidity = state.attributes.get(ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY)
min_humidity = round(min_humidity)
min_humidity = max(min_humidity, 0)
# The min/max humidity values here should be clamped to the HomeKit
# min/max that was set when the accessory was added to HomeKit so
# that the user cannot set a value outside of the range that was
# originally set as it could cause HomeKit to report the accessory
# as not responding.
humidity = round(char_values[self._target_humidity_char_name]) humidity = round(char_values[self._target_humidity_char_name])
if (humidity < min_humidity) or (humidity > max_humidity): if (humidity < min_humidity) or (humidity > max_humidity):
@ -260,10 +274,22 @@ class HumidifierDehumidifier(HomeAccessory):
), ),
) )
def get_humidity_range(self, state: State) -> tuple[int, int]:
"""Return min and max humidity range."""
attributes = state.attributes
min_humidity = max(
int(round(attributes.get(ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY))), 0
)
max_humidity = min(
int(round(attributes.get(ATTR_MAX_HUMIDITY, DEFAULT_MAX_HUMIDITY))), 100
)
return min_humidity, max_humidity
@callback @callback
def async_update_state(self, new_state: State) -> None: def async_update_state(self, new_state: State) -> None:
"""Update state without rechecking the device features.""" """Update state without rechecking the device features."""
is_active = new_state.state == STATE_ON is_active = new_state.state == STATE_ON
attributes = new_state.attributes
# Update active state # Update active state
self.char_active.set_value(is_active) self.char_active.set_value(is_active)
@ -279,6 +305,9 @@ class HumidifierDehumidifier(HomeAccessory):
self.char_current_humidifier_dehumidifier.set_value(current_state) self.char_current_humidifier_dehumidifier.set_value(current_state)
# Update target humidity # Update target humidity
target_humidity = new_state.attributes.get(ATTR_HUMIDITY) target_humidity = attributes.get(ATTR_HUMIDITY)
if isinstance(target_humidity, (int, float)): if isinstance(target_humidity, (int, float)):
self.char_target_humidity.set_value(target_humidity) self.char_target_humidity.set_value(target_humidity)
current_humidity = attributes.get(ATTR_CURRENT_HUMIDITY)
if isinstance(current_humidity, (int, float)):
self.char_current_humidity.set_value(current_humidity)

View file

@ -1,4 +1,5 @@
"""Test different accessory types: HumidifierDehumidifier.""" """Test different accessory types: HumidifierDehumidifier."""
from pyhap.accessory_driver import AccessoryDriver
from pyhap.const import ( from pyhap.const import (
CATEGORY_HUMIDIFIER, CATEGORY_HUMIDIFIER,
HAP_REPR_AID, HAP_REPR_AID,
@ -18,6 +19,7 @@ from homeassistant.components.homekit.const import (
) )
from homeassistant.components.homekit.type_humidifiers import HumidifierDehumidifier from homeassistant.components.homekit.type_humidifiers import HumidifierDehumidifier
from homeassistant.components.humidifier import ( from homeassistant.components.humidifier import (
ATTR_CURRENT_HUMIDITY,
ATTR_HUMIDITY, ATTR_HUMIDITY,
ATTR_MAX_HUMIDITY, ATTR_MAX_HUMIDITY,
ATTR_MIN_HUMIDITY, ATTR_MIN_HUMIDITY,
@ -75,7 +77,11 @@ async def test_humidifier(hass: HomeAssistant, hk_driver, events) -> None:
assert acc.char_target_humidifier_dehumidifier.properties[PROP_VALID_VALUES] == { assert acc.char_target_humidifier_dehumidifier.properties[PROP_VALID_VALUES] == {
"Humidifier": 1 "Humidifier": 1
} }
assert acc.char_current_humidifier_dehumidifier.properties[PROP_VALID_VALUES] == {
"Humidifying": 2,
"Idle": 1,
"Inactive": 0,
}
hass.states.async_set( hass.states.async_set(
entity_id, entity_id,
STATE_ON, STATE_ON,
@ -156,6 +162,11 @@ async def test_dehumidifier(hass: HomeAssistant, hk_driver, events) -> None:
assert acc.char_target_humidifier_dehumidifier.properties[PROP_VALID_VALUES] == { assert acc.char_target_humidifier_dehumidifier.properties[PROP_VALID_VALUES] == {
"Dehumidifier": 2 "Dehumidifier": 2
} }
assert acc.char_current_humidifier_dehumidifier.properties[PROP_VALID_VALUES] == {
"Dehumidifying": 3,
"Idle": 1,
"Inactive": 0,
}
hass.states.async_set( hass.states.async_set(
entity_id, entity_id,
@ -523,3 +534,30 @@ async def test_dehumidifier_as_humidifier(
await hass.async_block_till_done() await hass.async_block_till_done()
assert "TargetHumidifierDehumidifierState is not supported" in caplog.text assert "TargetHumidifierDehumidifierState is not supported" in caplog.text
assert len(events) == 0 assert len(events) == 0
async def test_humidifier_that_reports_current_humidity(
hass: HomeAssistant, hk_driver: AccessoryDriver
) -> None:
"""Test a humidifier that provides current humidity can update."""
entity_id = "humidifier.test"
hass.states.async_set(entity_id, STATE_OFF, {ATTR_CURRENT_HUMIDITY: 42})
await hass.async_block_till_done()
acc = HumidifierDehumidifier(
hass,
hk_driver,
"HumidifierDehumidifier",
entity_id,
1,
{},
)
hk_driver.add_accessory(acc)
await acc.run()
await hass.async_block_till_done()
assert acc.char_current_humidity.value == 42.0
hass.states.async_set(entity_id, STATE_OFF, {ATTR_CURRENT_HUMIDITY: 43})
await hass.async_block_till_done()
assert acc.char_current_humidity.value == 43.0