Update HomeKit humidifiers to handle current humidity (#101964)
This commit is contained in:
parent
8fd5d89d43
commit
f8f39a29de
2 changed files with 94 additions and 27 deletions
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue