Add humidifier support to homekit (#37207)

* Add humidifier support to homekit

* spell

* dependencies

* lint

* add linked humidity sensor for humidifiers

* Apply suggestions from code review

Co-authored-by: J. Nick Koston <nick@koston.org>

* apply suggestions from code review

* pylint

* Fix tests

* Update homeassistant/components/homekit/type_humidifiers.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Update tests/components/homekit/test_homekit.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Apply suggestions from code review

Co-authored-by: J. Nick Koston <nick@koston.org>

* apply suggestions from code review

* lint

* pylint

* push

* test for unavailable linker sensor

* black

* valid values key case

* black

* Update homeassistant/components/homekit/type_humidifiers.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* black

* coverage

* Set current humidity to 0 if linked sensor removed or unavailable

* use last known humidity instead

Co-authored-by: J. Nick Koston <nick@koston.org>
pull/37369/head
Shulyaka 2020-07-02 20:53:11 +03:00 committed by GitHub
parent 8bce9be590
commit 4ec71c58bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 780 additions and 2 deletions

View File

@ -15,6 +15,7 @@ from homeassistant.components.binary_sensor import (
)
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
@ -25,6 +26,7 @@ from homeassistant.const import (
CONF_NAME,
CONF_PORT,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_HUMIDITY,
EVENT_HOMEASSISTANT_STARTED,
EVENT_HOMEASSISTANT_STOP,
)
@ -53,6 +55,7 @@ from .const import (
CONF_FILTER,
CONF_LINKED_BATTERY_CHARGING_SENSOR,
CONF_LINKED_BATTERY_SENSOR,
CONF_LINKED_HUMIDITY_SENSOR,
CONF_LINKED_MOTION_SENSOR,
CONF_SAFE_MODE,
CONF_ZEROCONF_DEFAULT_INTERFACE,
@ -475,6 +478,7 @@ class HomeKit:
(BINARY_SENSOR_DOMAIN, DEVICE_CLASS_BATTERY_CHARGING),
(BINARY_SENSOR_DOMAIN, DEVICE_CLASS_MOTION),
(SENSOR_DOMAIN, DEVICE_CLASS_BATTERY),
(SENSOR_DOMAIN, DEVICE_CLASS_HUMIDITY),
}
)
@ -549,6 +553,7 @@ class HomeKit:
type_sensors,
type_switches,
type_thermostats,
type_humidifiers,
)
for state in bridged_states:
@ -618,6 +623,15 @@ class HomeKit:
CONF_LINKED_MOTION_SENSOR, motion_binary_sensor_entity_id,
)
if state.entity_id.startswith(f"{HUMIDIFIER_DOMAIN}."):
current_humidity_sensor_entity_id = device_lookup[
ent_reg_ent.device_id
].get((SENSOR_DOMAIN, DEVICE_CLASS_HUMIDITY))
if current_humidity_sensor_entity_id:
self._config.setdefault(state.entity_id, {}).setdefault(
CONF_LINKED_HUMIDITY_SENSOR, current_humidity_sensor_entity_id,
)
async def _async_set_device_info_attributes(self, ent_reg_ent, dev_reg, entity_id):
"""Set attributes that will be used for homekit device info."""
ent_cfg = self._config.setdefault(entity_id, {})

View File

@ -162,6 +162,9 @@ def get_accessory(hass, driver, state, aid, config):
elif state.domain == "fan":
a_type = "Fan"
elif state.domain == "humidifier":
a_type = "HumidifierDehumidifier"
elif state.domain == "light":
a_type = "Light"

View File

@ -47,6 +47,7 @@ SUPPORTED_DOMAINS = [
"demo",
"device_tracker",
"fan",
"humidifier",
"input_boolean",
"light",
"lock",
@ -65,6 +66,7 @@ DEFAULT_DOMAINS = [
"alarm_control_panel",
"climate",
"cover",
"humidifier",
"light",
"lock",
"media_player",

View File

@ -42,6 +42,7 @@ CONF_FILTER = "filter"
CONF_LINKED_BATTERY_SENSOR = "linked_battery_sensor"
CONF_LINKED_BATTERY_CHARGING_SENSOR = "linked_battery_charging_sensor"
CONF_LINKED_MOTION_SENSOR = "linked_motion_sensor"
CONF_LINKED_HUMIDITY_SENSOR = "linked_humidity_sensor"
CONF_LOW_BATTERY_THRESHOLD = "low_battery_threshold"
CONF_MAX_FPS = "max_fps"
CONF_MAX_HEIGHT = "max_height"
@ -111,6 +112,7 @@ SERV_CARBON_MONOXIDE_SENSOR = "CarbonMonoxideSensor"
SERV_CONTACT_SENSOR = "ContactSensor"
SERV_FANV2 = "Fanv2"
SERV_GARAGE_DOOR_OPENER = "GarageDoorOpener"
SERV_HUMIDIFIER_DEHUMIDIFIER = "HumidifierDehumidifier"
SERV_HUMIDITY_SENSOR = "HumiditySensor"
SERV_INPUT_SOURCE = "InputSource"
SERV_LEAK_SENSOR = "LeakSensor"
@ -151,15 +153,18 @@ CHAR_COOLING_THRESHOLD_TEMPERATURE = "CoolingThresholdTemperature"
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = "CurrentAmbientLightLevel"
CHAR_CURRENT_DOOR_STATE = "CurrentDoorState"
CHAR_CURRENT_HEATING_COOLING = "CurrentHeatingCoolingState"
CHAR_CURRENT_HUMIDIFIER_DEHUMIDIFIER = "CurrentHumidifierDehumidifierState"
CHAR_CURRENT_POSITION = "CurrentPosition"
CHAR_CURRENT_HUMIDITY = "CurrentRelativeHumidity"
CHAR_CURRENT_SECURITY_STATE = "SecuritySystemCurrentState"
CHAR_CURRENT_TEMPERATURE = "CurrentTemperature"
CHAR_CURRENT_TILT_ANGLE = "CurrentHorizontalTiltAngle"
CHAR_CURRENT_VISIBILITY_STATE = "CurrentVisibilityState"
CHAR_DEHUMIDIFIER_THRESHOLD_HUMIDITY = "RelativeHumidityDehumidifierThreshold"
CHAR_FIRMWARE_REVISION = "FirmwareRevision"
CHAR_HEATING_THRESHOLD_TEMPERATURE = "HeatingThresholdTemperature"
CHAR_HUE = "Hue"
CHAR_HUMIDIFIER_THRESHOLD_HUMIDITY = "RelativeHumidityHumidifierThreshold"
CHAR_IDENTIFIER = "Identifier"
CHAR_IN_USE = "InUse"
CHAR_INPUT_SOURCE_TYPE = "InputSourceType"
@ -190,6 +195,7 @@ CHAR_SWING_MODE = "SwingMode"
CHAR_TARGET_DOOR_STATE = "TargetDoorState"
CHAR_TARGET_HEATING_COOLING = "TargetHeatingCoolingState"
CHAR_TARGET_POSITION = "TargetPosition"
CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER = "TargetHumidifierDehumidifierState"
CHAR_TARGET_HUMIDITY = "TargetRelativeHumidity"
CHAR_TARGET_SECURITY_STATE = "SecuritySystemTargetState"
CHAR_TARGET_TEMPERATURE = "TargetTemperature"
@ -207,6 +213,7 @@ PROP_MAX_VALUE = "maxValue"
PROP_MIN_VALUE = "minValue"
PROP_MIN_STEP = "minStep"
PROP_CELSIUS = {"minValue": -273, "maxValue": 999}
PROP_VALID_VALUES = "ValidValues"
# #### Device Classes ####
DEVICE_CLASS_CO = "co"

View File

@ -0,0 +1,241 @@
"""Class to hold all thermostat accessories."""
import logging
from pyhap.const import CATEGORY_HUMIDIFIER
from homeassistant.components.humidifier.const import (
ATTR_HUMIDITY,
ATTR_MAX_HUMIDITY,
ATTR_MIN_HUMIDITY,
DEFAULT_MAX_HUMIDITY,
DEFAULT_MIN_HUMIDITY,
DEVICE_CLASS_DEHUMIDIFIER,
DEVICE_CLASS_HUMIDIFIER,
DOMAIN,
SERVICE_SET_HUMIDITY,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_ON,
UNIT_PERCENTAGE,
)
from homeassistant.core import callback
from homeassistant.helpers.event import async_track_state_change_event
from .accessories import TYPES, HomeAccessory
from .const import (
CHAR_ACTIVE,
CHAR_CURRENT_HUMIDIFIER_DEHUMIDIFIER,
CHAR_CURRENT_HUMIDITY,
CHAR_DEHUMIDIFIER_THRESHOLD_HUMIDITY,
CHAR_HUMIDIFIER_THRESHOLD_HUMIDITY,
CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER,
CONF_LINKED_HUMIDITY_SENSOR,
PROP_MAX_VALUE,
PROP_MIN_STEP,
PROP_MIN_VALUE,
SERV_HUMIDIFIER_DEHUMIDIFIER,
)
_LOGGER = logging.getLogger(__name__)
HC_HUMIDIFIER = 1
HC_DEHUMIDIFIER = 2
HC_HASS_TO_HOMEKIT_DEVICE_CLASS = {
DEVICE_CLASS_HUMIDIFIER: HC_HUMIDIFIER,
DEVICE_CLASS_DEHUMIDIFIER: HC_DEHUMIDIFIER,
}
HC_HASS_TO_HOMEKIT_DEVICE_CLASS_NAME = {
DEVICE_CLASS_HUMIDIFIER: "Humidifier",
DEVICE_CLASS_DEHUMIDIFIER: "Dehumidifier",
}
HC_DEVICE_CLASS_TO_TARGET_CHAR = {
HC_HUMIDIFIER: CHAR_HUMIDIFIER_THRESHOLD_HUMIDITY,
HC_DEHUMIDIFIER: CHAR_DEHUMIDIFIER_THRESHOLD_HUMIDITY,
}
HC_STATE_INACTIVE = 0
HC_STATE_IDLE = 1
HC_STATE_HUMIDIFYING = 2
HC_STATE_DEHUMIDIFYING = 3
@TYPES.register("HumidifierDehumidifier")
class HumidifierDehumidifier(HomeAccessory):
"""Generate a HumidifierDehumidifier accessory for a humidifier."""
def __init__(self, *args):
"""Initialize a HumidifierDehumidifier accessory object."""
super().__init__(*args, category=CATEGORY_HUMIDIFIER)
self.chars = []
state = self.hass.states.get(self.entity_id)
device_class = state.attributes.get(ATTR_DEVICE_CLASS, DEVICE_CLASS_HUMIDIFIER)
self._hk_device_class = HC_HASS_TO_HOMEKIT_DEVICE_CLASS[device_class]
self._target_humidity_char_name = HC_DEVICE_CLASS_TO_TARGET_CHAR[
self._hk_device_class
]
self.chars.append(self._target_humidity_char_name)
serv_humidifier_dehumidifier = self.add_preload_service(
SERV_HUMIDIFIER_DEHUMIDIFIER, self.chars
)
# Current and target mode characteristics
self.char_current_humidifier_dehumidifier = serv_humidifier_dehumidifier.configure_char(
CHAR_CURRENT_HUMIDIFIER_DEHUMIDIFIER, value=0
)
self.char_target_humidifier_dehumidifier = serv_humidifier_dehumidifier.configure_char(
CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER,
value=self._hk_device_class,
valid_values={
HC_HASS_TO_HOMEKIT_DEVICE_CLASS_NAME[
device_class
]: self._hk_device_class
},
)
# Current and target humidity characteristics
self.char_current_humidity = serv_humidifier_dehumidifier.configure_char(
CHAR_CURRENT_HUMIDITY, value=0
)
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)
self.char_target_humidity = serv_humidifier_dehumidifier.configure_char(
self._target_humidity_char_name,
value=45,
properties={
PROP_MIN_VALUE: min_humidity,
PROP_MAX_VALUE: max_humidity,
PROP_MIN_STEP: 1,
},
)
# Active/inactive characteristics
self.char_active = serv_humidifier_dehumidifier.configure_char(
CHAR_ACTIVE, value=False
)
self.async_update_state(state)
serv_humidifier_dehumidifier.setter_callback = self._set_chars
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:
self._async_update_current_humidity(humidity_state)
async def run_handler(self):
"""Handle accessory driver started event.
Run inside the Home Assistant event loop.
"""
if self.linked_humidity_sensor:
async_track_state_change_event(
self.hass,
[self.linked_humidity_sensor],
self.async_update_current_humidity_event,
)
await super().run_handler()
@callback
def async_update_current_humidity_event(self, event):
"""Handle state change event listener callback."""
self._async_update_current_humidity(event.data.get("new_state"))
@callback
def _async_update_current_humidity(self, new_state):
"""Handle linked humidity sensor state change to update HomeKit value."""
if new_state is None:
_LOGGER.error(
"%s: Unable to update from linked humidity sensor %s: the entity state is None",
self.entity_id,
self.linked_humidity_sensor,
)
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.error(
"%s: Unable to update from linked humidity sensor %s: %s",
self.entity_id,
self.linked_humidity_sensor,
ex,
)
def _set_chars(self, char_values):
_LOGGER.debug("HumidifierDehumidifier _set_chars: %s", char_values)
if CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER in char_values:
hk_value = char_values[CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER]
if self._hk_device_class != hk_value:
_LOGGER.error(
"%s is not supported", CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER
)
if CHAR_ACTIVE in char_values:
self.call_service(
DOMAIN,
SERVICE_TURN_ON if char_values[CHAR_ACTIVE] else SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: self.entity_id},
f"{CHAR_ACTIVE} to {char_values[CHAR_ACTIVE]}",
)
if self._target_humidity_char_name in char_values:
humidity = round(char_values[self._target_humidity_char_name])
self.call_service(
DOMAIN,
SERVICE_SET_HUMIDITY,
{ATTR_ENTITY_ID: self.entity_id, ATTR_HUMIDITY: humidity},
f"{self._target_humidity_char_name} to "
f"{char_values[self._target_humidity_char_name]}{UNIT_PERCENTAGE}",
)
@callback
def async_update_state(self, new_state):
"""Update state without rechecking the device features."""
is_active = new_state.state == STATE_ON
# Update active state
if self.char_active.value != is_active:
self.char_active.set_value(is_active)
# Set current state
if is_active:
if self._hk_device_class == HC_HUMIDIFIER:
current_state = HC_STATE_HUMIDIFYING
else:
current_state = HC_STATE_DEHUMIDIFYING
else:
current_state = HC_STATE_INACTIVE
if self.char_current_humidifier_dehumidifier.value != current_state:
self.char_current_humidifier_dehumidifier.set_value(current_state)
# Update target humidity
target_humidity = new_state.attributes.get(ATTR_HUMIDITY)
if isinstance(target_humidity, (int, float)):
if self.char_target_humidity.value != target_humidity:
self.char_target_humidity.set_value(target_humidity)

View File

@ -34,6 +34,7 @@ from .const import (
CONF_FEATURE_LIST,
CONF_LINKED_BATTERY_CHARGING_SENSOR,
CONF_LINKED_BATTERY_SENSOR,
CONF_LINKED_HUMIDITY_SENSOR,
CONF_LINKED_MOTION_SENSOR,
CONF_LOW_BATTERY_THRESHOLD,
CONF_MAX_FPS,
@ -124,6 +125,10 @@ CAMERA_SCHEMA = BASIC_INFO_SCHEMA.extend(
}
)
HUMIDIFIER_SCHEMA = BASIC_INFO_SCHEMA.extend(
{vol.Optional(CONF_LINKED_HUMIDITY_SENSOR): cv.entity_domain(sensor.DOMAIN)}
)
CODE_SCHEMA = BASIC_INFO_SCHEMA.extend(
{vol.Optional(ATTR_CODE, default=None): vol.Any(None, cv.string)}
)
@ -230,6 +235,9 @@ def validate_entity_config(values):
elif domain == "switch":
config = SWITCH_TYPE_SCHEMA(config)
elif domain == "humidifier":
config = HUMIDIFIER_SCHEMA(config)
else:
config = BASIC_INFO_SCHEMA(config)

View File

@ -18,6 +18,7 @@ def _mock_config_entry_with_options_populated():
"filter": {
"include_domains": [
"fan",
"humidifier",
"vacuum",
"media_player",
"climate",
@ -134,7 +135,8 @@ async def test_options_flow_advanced(hass):
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={"include_domains": ["fan", "vacuum", "climate"]},
result["flow_id"],
user_input={"include_domains": ["fan", "vacuum", "climate", "humidifier"]},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@ -157,7 +159,7 @@ async def test_options_flow_advanced(hass):
"filter": {
"exclude_domains": [],
"exclude_entities": ["climate.old"],
"include_domains": ["fan", "vacuum", "climate"],
"include_domains": ["fan", "vacuum", "climate", "humidifier"],
"include_entities": [],
},
"safe_mode": True,
@ -332,6 +334,7 @@ async def test_options_flow_blocked_when_from_yaml(hass):
"filter": {
"include_domains": [
"fan",
"humidifier",
"vacuum",
"media_player",
"climate",

View File

@ -91,6 +91,7 @@ def test_customize_options(config, name):
{ATTR_SUPPORTED_FEATURES: climate.SUPPORT_TARGET_TEMPERATURE_RANGE},
{},
),
("HumidifierDehumidifier", "humidifier.test", "auto", {}, {}),
("WaterHeater", "water_heater.test", "auto", {}, {}),
],
)

View File

@ -42,13 +42,16 @@ from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_UNIT_OF_MEASUREMENT,
CONF_IP_ADDRESS,
CONF_NAME,
CONF_PORT,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_HUMIDITY,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
STATE_ON,
UNIT_PERCENTAGE,
)
from homeassistant.core import State
from homeassistant.helpers import device_registry
@ -1094,3 +1097,80 @@ async def test_homekit_finds_linked_motion_sensors(
"linked_motion_sensor": "binary_sensor.camera_motion_sensor",
},
)
async def test_homekit_finds_linked_humidity_sensors(
hass, hk_driver, debounce_patcher, device_reg, entity_reg
):
"""Test HomeKit start method."""
entry = await async_init_integration(hass)
homekit = HomeKit(
hass,
None,
None,
None,
{},
{"humidifier.humidifier": {}},
DEFAULT_SAFE_MODE,
advertise_ip=None,
entry_id=entry.entry_id,
)
homekit.driver = hk_driver
homekit._filter = Mock(return_value=True)
homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge")
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
sw_version="0.16.1",
model="Smart Brainy Clever Humidifier",
manufacturer="Home Assistant",
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
humidity_sensor = entity_reg.async_get_or_create(
"sensor",
"humidifier",
"humidity_sensor",
device_id=device_entry.id,
device_class=DEVICE_CLASS_HUMIDITY,
)
humidifier = entity_reg.async_get_or_create(
"humidifier", "humidifier", "demo", device_id=device_entry.id
)
hass.states.async_set(
humidity_sensor.entity_id,
"42",
{
ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE,
},
)
hass.states.async_set(humidifier.entity_id, STATE_ON)
def _mock_get_accessory(*args, **kwargs):
return [None, "acc", None]
with patch.object(homekit.bridge, "add_accessory"), patch(
f"{PATH_HOMEKIT}.show_setup_message"
), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch(
"pyhap.accessory_driver.AccessoryDriver.start"
):
await homekit.async_start()
await hass.async_block_till_done()
mock_get_acc.assert_called_with(
hass,
hk_driver,
ANY,
ANY,
{
"manufacturer": "Home Assistant",
"model": "Smart Brainy Clever Humidifier",
"sw_version": "0.16.1",
"linked_humidity_sensor": "sensor.humidifier_humidity_sensor",
},
)

View File

@ -0,0 +1,419 @@
"""Test different accessory types: HumidifierDehumidifier."""
from pyhap.const import (
CATEGORY_HUMIDIFIER,
HAP_REPR_AID,
HAP_REPR_CHARS,
HAP_REPR_IID,
HAP_REPR_VALUE,
)
from homeassistant.components.homekit.const import (
ATTR_VALUE,
CONF_LINKED_HUMIDITY_SENSOR,
PROP_MAX_VALUE,
PROP_MIN_STEP,
PROP_MIN_VALUE,
PROP_VALID_VALUES,
)
from homeassistant.components.homekit.type_humidifiers import HumidifierDehumidifier
from homeassistant.components.humidifier.const import (
ATTR_HUMIDITY,
ATTR_MAX_HUMIDITY,
ATTR_MIN_HUMIDITY,
DEFAULT_MAX_HUMIDITY,
DEFAULT_MIN_HUMIDITY,
DEVICE_CLASS_DEHUMIDIFIER,
DEVICE_CLASS_HUMIDIFIER,
DOMAIN,
SERVICE_SET_HUMIDITY,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_UNIT_OF_MEASUREMENT,
DEVICE_CLASS_HUMIDITY,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
UNIT_PERCENTAGE,
)
from tests.common import async_mock_service
async def test_humidifier(hass, hk_driver, events):
"""Test if humidifier accessory and HA are updated accordingly."""
entity_id = "humidifier.test"
hass.states.async_set(entity_id, STATE_OFF)
await hass.async_block_till_done()
acc = HumidifierDehumidifier(
hass, hk_driver, "HumidifierDehumidifier", entity_id, 1, None
)
hk_driver.add_accessory(acc)
await acc.run_handler()
await hass.async_block_till_done()
assert acc.aid == 1
assert acc.category == CATEGORY_HUMIDIFIER
assert acc.char_current_humidifier_dehumidifier.value == 0
assert acc.char_target_humidifier_dehumidifier.value == 1
assert acc.char_current_humidity.value == 0
assert acc.char_target_humidity.value == 45.0
assert acc.char_active.value == 0
assert acc.char_target_humidity.properties[PROP_MAX_VALUE] == DEFAULT_MAX_HUMIDITY
assert acc.char_target_humidity.properties[PROP_MIN_VALUE] == DEFAULT_MIN_HUMIDITY
assert acc.char_target_humidity.properties[PROP_MIN_STEP] == 1.0
assert acc.char_target_humidifier_dehumidifier.properties[PROP_VALID_VALUES] == {
"Humidifier": 1
}
hass.states.async_set(
entity_id, STATE_ON, {ATTR_HUMIDITY: 47},
)
await hass.async_block_till_done()
assert acc.char_target_humidity.value == 47.0
assert acc.char_current_humidifier_dehumidifier.value == 2
assert acc.char_target_humidifier_dehumidifier.value == 1
assert acc.char_active.value == 1
hass.states.async_set(
entity_id,
STATE_OFF,
{ATTR_HUMIDITY: 42, ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDIFIER},
)
await hass.async_block_till_done()
assert acc.char_target_humidity.value == 42.0
assert acc.char_current_humidifier_dehumidifier.value == 0
assert acc.char_target_humidifier_dehumidifier.value == 1
assert acc.char_active.value == 0
# Set from HomeKit
call_set_humidity = async_mock_service(hass, DOMAIN, SERVICE_SET_HUMIDITY)
char_target_humidity_iid = acc.char_target_humidity.to_HAP()[HAP_REPR_IID]
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_humidity_iid,
HAP_REPR_VALUE: 39.0,
},
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert len(call_set_humidity) == 1
assert call_set_humidity[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_humidity[0].data[ATTR_HUMIDITY] == 39.0
assert acc.char_target_humidity.value == 39.0
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "RelativeHumidityHumidifierThreshold to 39.0%"
async def test_dehumidifier(hass, hk_driver, events):
"""Test if dehumidifier accessory and HA are updated accordingly."""
entity_id = "humidifier.test"
hass.states.async_set(
entity_id, STATE_OFF, {ATTR_DEVICE_CLASS: DEVICE_CLASS_DEHUMIDIFIER}
)
await hass.async_block_till_done()
acc = HumidifierDehumidifier(
hass, hk_driver, "HumidifierDehumidifier", entity_id, 1, None
)
hk_driver.add_accessory(acc)
await acc.run_handler()
await hass.async_block_till_done()
assert acc.aid == 1
assert acc.category == CATEGORY_HUMIDIFIER
assert acc.char_current_humidifier_dehumidifier.value == 0
assert acc.char_target_humidifier_dehumidifier.value == 2
assert acc.char_current_humidity.value == 0
assert acc.char_target_humidity.value == 45.0
assert acc.char_active.value == 0
assert acc.char_target_humidity.properties[PROP_MAX_VALUE] == DEFAULT_MAX_HUMIDITY
assert acc.char_target_humidity.properties[PROP_MIN_VALUE] == DEFAULT_MIN_HUMIDITY
assert acc.char_target_humidity.properties[PROP_MIN_STEP] == 1.0
assert acc.char_target_humidifier_dehumidifier.properties[PROP_VALID_VALUES] == {
"Dehumidifier": 2
}
hass.states.async_set(
entity_id, STATE_ON, {ATTR_HUMIDITY: 30},
)
await hass.async_block_till_done()
assert acc.char_target_humidity.value == 30.0
assert acc.char_current_humidifier_dehumidifier.value == 3
assert acc.char_target_humidifier_dehumidifier.value == 2
assert acc.char_active.value == 1
hass.states.async_set(
entity_id, STATE_OFF, {ATTR_HUMIDITY: 42},
)
await hass.async_block_till_done()
assert acc.char_target_humidity.value == 42.0
assert acc.char_current_humidifier_dehumidifier.value == 0
assert acc.char_target_humidifier_dehumidifier.value == 2
assert acc.char_active.value == 0
# Set from HomeKit
call_set_humidity = async_mock_service(hass, DOMAIN, SERVICE_SET_HUMIDITY)
char_target_humidity_iid = acc.char_target_humidity.to_HAP()[HAP_REPR_IID]
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_humidity_iid,
HAP_REPR_VALUE: 39.0,
},
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert len(call_set_humidity) == 1
assert call_set_humidity[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_humidity[0].data[ATTR_HUMIDITY] == 39.0
assert acc.char_target_humidity.value == 39.0
assert len(events) == 1
assert (
events[-1].data[ATTR_VALUE] == "RelativeHumidityDehumidifierThreshold to 39.0%"
)
async def test_hygrostat_power_state(hass, hk_driver, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = "humidifier.test"
hass.states.async_set(
entity_id, STATE_ON, {ATTR_HUMIDITY: 43},
)
await hass.async_block_till_done()
acc = HumidifierDehumidifier(
hass, hk_driver, "HumidifierDehumidifier", entity_id, 1, None
)
hk_driver.add_accessory(acc)
await acc.run_handler()
await hass.async_block_till_done()
assert acc.char_current_humidifier_dehumidifier.value == 2
assert acc.char_target_humidifier_dehumidifier.value == 1
assert acc.char_active.value == 1
hass.states.async_set(
entity_id, STATE_OFF, {ATTR_HUMIDITY: 43},
)
await hass.async_block_till_done()
assert acc.char_current_humidifier_dehumidifier.value == 0
assert acc.char_target_humidifier_dehumidifier.value == 1
assert acc.char_active.value == 0
# Set from HomeKit
call_turn_on = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON)
char_active_iid = acc.char_active.to_HAP()[HAP_REPR_IID]
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_active_iid,
HAP_REPR_VALUE: 1,
},
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert len(call_turn_on) == 1
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
assert acc.char_active.value == 1
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "Active to 1"
call_turn_off = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF)
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_active_iid,
HAP_REPR_VALUE: 0,
},
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert len(call_turn_off) == 1
assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
assert acc.char_active.value == 0
assert len(events) == 2
assert events[-1].data[ATTR_VALUE] == "Active to 0"
async def test_hygrostat_get_humidity_range(hass, hk_driver):
"""Test if humidity range is evaluated correctly."""
entity_id = "humidifier.test"
hass.states.async_set(
entity_id, STATE_OFF, {ATTR_MIN_HUMIDITY: 40, ATTR_MAX_HUMIDITY: 45}
)
await hass.async_block_till_done()
acc = HumidifierDehumidifier(
hass, hk_driver, "HumidifierDehumidifier", entity_id, 1, None
)
hk_driver.add_accessory(acc)
await acc.run_handler()
await hass.async_block_till_done()
assert acc.char_target_humidity.properties[PROP_MAX_VALUE] == 45
assert acc.char_target_humidity.properties[PROP_MIN_VALUE] == 40
async def test_humidifier_with_linked_humidity_sensor(hass, hk_driver):
"""Test a humidifier with a linked humidity sensor can update."""
humidity_sensor_entity_id = "sensor.bedroom_humidity"
hass.states.async_set(
humidity_sensor_entity_id,
"42.0",
{
ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE,
},
)
await hass.async_block_till_done()
entity_id = "humidifier.test"
hass.states.async_set(entity_id, STATE_OFF)
await hass.async_block_till_done()
acc = HumidifierDehumidifier(
hass,
hk_driver,
"HumidifierDehumidifier",
entity_id,
1,
{CONF_LINKED_HUMIDITY_SENSOR: humidity_sensor_entity_id},
)
hk_driver.add_accessory(acc)
await acc.run_handler()
await hass.async_block_till_done()
assert acc.char_current_humidity.value == 42.0
hass.states.async_set(
humidity_sensor_entity_id,
"43.0",
{
ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE,
},
)
await hass.async_block_till_done()
assert acc.char_current_humidity.value == 43.0
hass.states.async_set(
humidity_sensor_entity_id,
STATE_UNAVAILABLE,
{
ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE,
},
)
await hass.async_block_till_done()
assert acc.char_current_humidity.value == 43.0
hass.states.async_remove(humidity_sensor_entity_id)
await hass.async_block_till_done()
assert acc.char_current_humidity.value == 43.0
async def test_humidifier_with_a_missing_linked_humidity_sensor(hass, hk_driver):
"""Test a humidifier with a configured linked motion sensor that is missing."""
humidity_sensor_entity_id = "sensor.bedroom_humidity"
entity_id = "humidifier.test"
hass.states.async_set(entity_id, STATE_OFF)
await hass.async_block_till_done()
acc = HumidifierDehumidifier(
hass,
hk_driver,
"HumidifierDehumidifier",
entity_id,
1,
{CONF_LINKED_HUMIDITY_SENSOR: humidity_sensor_entity_id},
)
hk_driver.add_accessory(acc)
await acc.run_handler()
await hass.async_block_till_done()
assert acc.char_current_humidity.value == 0
async def test_humidifier_as_dehumidifier(hass, hk_driver, events, caplog):
"""Test an invalid char_target_humidifier_dehumidifier from HomeKit."""
entity_id = "humidifier.test"
hass.states.async_set(entity_id, STATE_OFF)
await hass.async_block_till_done()
acc = HumidifierDehumidifier(
hass, hk_driver, "HumidifierDehumidifier", entity_id, 1, None
)
hk_driver.add_accessory(acc)
await acc.run_handler()
await hass.async_block_till_done()
assert acc.char_target_humidifier_dehumidifier.value == 1
# Set from HomeKit
char_target_humidifier_dehumidifier_iid = acc.char_target_humidifier_dehumidifier.to_HAP()[
HAP_REPR_IID
]
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_target_humidifier_dehumidifier_iid,
HAP_REPR_VALUE: 0,
},
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert "TargetHumidifierDehumidifierState is not supported" in caplog.text
assert len(events) == 0