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
parent
8bce9be590
commit
4ec71c58bd
|
@ -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, {})
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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", {}, {}),
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue