diff --git a/homeassistant/components/homekit/type_humidifiers.py b/homeassistant/components/homekit/type_humidifiers.py index d296b293820..939c1bf37ae 100644 --- a/homeassistant/components/homekit/type_humidifiers.py +++ b/homeassistant/components/homekit/type_humidifiers.py @@ -5,6 +5,7 @@ from typing import Any from pyhap.const import CATEGORY_HUMIDIFIER from homeassistant.components.humidifier import ( + ATTR_CURRENT_HUMIDITY, ATTR_HUMIDITY, ATTR_MAX_HUMIDITY, ATTR_MIN_HUMIDITY, @@ -64,11 +65,28 @@ HC_DEVICE_CLASS_TO_TARGET_CHAR = { HC_DEHUMIDIFIER: CHAR_DEHUMIDIFIER_THRESHOLD_HUMIDITY, } + HC_STATE_INACTIVE = 0 HC_STATE_IDLE = 1 HC_STATE_HUMIDIFYING = 2 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") class HumidifierDehumidifier(HomeAccessory): @@ -85,7 +103,8 @@ class HumidifierDehumidifier(HomeAccessory): ) self.chars: list[str] = [] - state = self.hass.states.get(self.entity_id) + states = self.hass.states + state = states.get(self.entity_id) assert state device_class = state.attributes.get( ATTR_DEVICE_CLASS, HumidifierDeviceClass.HUMIDIFIER @@ -104,7 +123,9 @@ class HumidifierDehumidifier(HomeAccessory): # Current and target mode characteristics self.char_current_humidifier_dehumidifier = ( 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 = ( @@ -149,8 +170,7 @@ class HumidifierDehumidifier(HomeAccessory): self.linked_humidity_sensor = self.config.get(CONF_LINKED_HUMIDITY_SENSOR) if self.linked_humidity_sensor: - humidity_state = self.hass.states.get(self.linked_humidity_sensor) - if humidity_state: + if humidity_state := states.get(self.linked_humidity_sensor): self._async_update_current_humidity(humidity_state) async def run(self) -> None: @@ -191,14 +211,6 @@ class HumidifierDehumidifier(HomeAccessory): return try: 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: _LOGGER.debug( "%s: Unable to update from linked humidity sensor %s: %s", @@ -206,6 +218,20 @@ class HumidifierDehumidifier(HomeAccessory): self.linked_humidity_sensor, 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: """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: state = self.hass.states.get(self.entity_id) assert state - max_humidity = state.attributes.get(ATTR_MAX_HUMIDITY, DEFAULT_MAX_HUMIDITY) - 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. - + min_humidity, max_humidity = self.get_humidity_range(state) humidity = round(char_values[self._target_humidity_char_name]) 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 def async_update_state(self, new_state: State) -> None: """Update state without rechecking the device features.""" is_active = new_state.state == STATE_ON + attributes = new_state.attributes # Update active state self.char_active.set_value(is_active) @@ -279,6 +305,9 @@ class HumidifierDehumidifier(HomeAccessory): self.char_current_humidifier_dehumidifier.set_value(current_state) # Update target humidity - target_humidity = new_state.attributes.get(ATTR_HUMIDITY) + target_humidity = attributes.get(ATTR_HUMIDITY) if isinstance(target_humidity, (int, float)): 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) diff --git a/tests/components/homekit/test_type_humidifiers.py b/tests/components/homekit/test_type_humidifiers.py index e0b3e40967f..c8c4f398375 100644 --- a/tests/components/homekit/test_type_humidifiers.py +++ b/tests/components/homekit/test_type_humidifiers.py @@ -1,4 +1,5 @@ """Test different accessory types: HumidifierDehumidifier.""" +from pyhap.accessory_driver import AccessoryDriver from pyhap.const import ( CATEGORY_HUMIDIFIER, HAP_REPR_AID, @@ -18,6 +19,7 @@ from homeassistant.components.homekit.const import ( ) from homeassistant.components.homekit.type_humidifiers import HumidifierDehumidifier from homeassistant.components.humidifier import ( + ATTR_CURRENT_HUMIDITY, ATTR_HUMIDITY, ATTR_MAX_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] == { "Humidifier": 1 } - + assert acc.char_current_humidifier_dehumidifier.properties[PROP_VALID_VALUES] == { + "Humidifying": 2, + "Idle": 1, + "Inactive": 0, + } hass.states.async_set( entity_id, 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] == { "Dehumidifier": 2 } + assert acc.char_current_humidifier_dehumidifier.properties[PROP_VALID_VALUES] == { + "Dehumidifying": 3, + "Idle": 1, + "Inactive": 0, + } hass.states.async_set( entity_id, @@ -523,3 +534,30 @@ async def test_dehumidifier_as_humidifier( await hass.async_block_till_done() assert "TargetHumidifierDehumidifierState is not supported" in caplog.text 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