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>
This commit is contained in:
parent
8bce9be590
commit
4ec71c58bd
10 changed files with 780 additions and 2 deletions
|
@ -15,6 +15,7 @@ from homeassistant.components.binary_sensor import (
|
||||||
)
|
)
|
||||||
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
|
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
|
||||||
from homeassistant.components.http import HomeAssistantView
|
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.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
@ -25,6 +26,7 @@ from homeassistant.const import (
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
DEVICE_CLASS_BATTERY,
|
DEVICE_CLASS_BATTERY,
|
||||||
|
DEVICE_CLASS_HUMIDITY,
|
||||||
EVENT_HOMEASSISTANT_STARTED,
|
EVENT_HOMEASSISTANT_STARTED,
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
)
|
)
|
||||||
|
@ -53,6 +55,7 @@ from .const import (
|
||||||
CONF_FILTER,
|
CONF_FILTER,
|
||||||
CONF_LINKED_BATTERY_CHARGING_SENSOR,
|
CONF_LINKED_BATTERY_CHARGING_SENSOR,
|
||||||
CONF_LINKED_BATTERY_SENSOR,
|
CONF_LINKED_BATTERY_SENSOR,
|
||||||
|
CONF_LINKED_HUMIDITY_SENSOR,
|
||||||
CONF_LINKED_MOTION_SENSOR,
|
CONF_LINKED_MOTION_SENSOR,
|
||||||
CONF_SAFE_MODE,
|
CONF_SAFE_MODE,
|
||||||
CONF_ZEROCONF_DEFAULT_INTERFACE,
|
CONF_ZEROCONF_DEFAULT_INTERFACE,
|
||||||
|
@ -475,6 +478,7 @@ class HomeKit:
|
||||||
(BINARY_SENSOR_DOMAIN, DEVICE_CLASS_BATTERY_CHARGING),
|
(BINARY_SENSOR_DOMAIN, DEVICE_CLASS_BATTERY_CHARGING),
|
||||||
(BINARY_SENSOR_DOMAIN, DEVICE_CLASS_MOTION),
|
(BINARY_SENSOR_DOMAIN, DEVICE_CLASS_MOTION),
|
||||||
(SENSOR_DOMAIN, DEVICE_CLASS_BATTERY),
|
(SENSOR_DOMAIN, DEVICE_CLASS_BATTERY),
|
||||||
|
(SENSOR_DOMAIN, DEVICE_CLASS_HUMIDITY),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -549,6 +553,7 @@ class HomeKit:
|
||||||
type_sensors,
|
type_sensors,
|
||||||
type_switches,
|
type_switches,
|
||||||
type_thermostats,
|
type_thermostats,
|
||||||
|
type_humidifiers,
|
||||||
)
|
)
|
||||||
|
|
||||||
for state in bridged_states:
|
for state in bridged_states:
|
||||||
|
@ -618,6 +623,15 @@ class HomeKit:
|
||||||
CONF_LINKED_MOTION_SENSOR, motion_binary_sensor_entity_id,
|
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):
|
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."""
|
"""Set attributes that will be used for homekit device info."""
|
||||||
ent_cfg = self._config.setdefault(entity_id, {})
|
ent_cfg = self._config.setdefault(entity_id, {})
|
||||||
|
|
|
@ -162,6 +162,9 @@ def get_accessory(hass, driver, state, aid, config):
|
||||||
elif state.domain == "fan":
|
elif state.domain == "fan":
|
||||||
a_type = "Fan"
|
a_type = "Fan"
|
||||||
|
|
||||||
|
elif state.domain == "humidifier":
|
||||||
|
a_type = "HumidifierDehumidifier"
|
||||||
|
|
||||||
elif state.domain == "light":
|
elif state.domain == "light":
|
||||||
a_type = "Light"
|
a_type = "Light"
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,7 @@ SUPPORTED_DOMAINS = [
|
||||||
"demo",
|
"demo",
|
||||||
"device_tracker",
|
"device_tracker",
|
||||||
"fan",
|
"fan",
|
||||||
|
"humidifier",
|
||||||
"input_boolean",
|
"input_boolean",
|
||||||
"light",
|
"light",
|
||||||
"lock",
|
"lock",
|
||||||
|
@ -65,6 +66,7 @@ DEFAULT_DOMAINS = [
|
||||||
"alarm_control_panel",
|
"alarm_control_panel",
|
||||||
"climate",
|
"climate",
|
||||||
"cover",
|
"cover",
|
||||||
|
"humidifier",
|
||||||
"light",
|
"light",
|
||||||
"lock",
|
"lock",
|
||||||
"media_player",
|
"media_player",
|
||||||
|
|
|
@ -42,6 +42,7 @@ CONF_FILTER = "filter"
|
||||||
CONF_LINKED_BATTERY_SENSOR = "linked_battery_sensor"
|
CONF_LINKED_BATTERY_SENSOR = "linked_battery_sensor"
|
||||||
CONF_LINKED_BATTERY_CHARGING_SENSOR = "linked_battery_charging_sensor"
|
CONF_LINKED_BATTERY_CHARGING_SENSOR = "linked_battery_charging_sensor"
|
||||||
CONF_LINKED_MOTION_SENSOR = "linked_motion_sensor"
|
CONF_LINKED_MOTION_SENSOR = "linked_motion_sensor"
|
||||||
|
CONF_LINKED_HUMIDITY_SENSOR = "linked_humidity_sensor"
|
||||||
CONF_LOW_BATTERY_THRESHOLD = "low_battery_threshold"
|
CONF_LOW_BATTERY_THRESHOLD = "low_battery_threshold"
|
||||||
CONF_MAX_FPS = "max_fps"
|
CONF_MAX_FPS = "max_fps"
|
||||||
CONF_MAX_HEIGHT = "max_height"
|
CONF_MAX_HEIGHT = "max_height"
|
||||||
|
@ -111,6 +112,7 @@ SERV_CARBON_MONOXIDE_SENSOR = "CarbonMonoxideSensor"
|
||||||
SERV_CONTACT_SENSOR = "ContactSensor"
|
SERV_CONTACT_SENSOR = "ContactSensor"
|
||||||
SERV_FANV2 = "Fanv2"
|
SERV_FANV2 = "Fanv2"
|
||||||
SERV_GARAGE_DOOR_OPENER = "GarageDoorOpener"
|
SERV_GARAGE_DOOR_OPENER = "GarageDoorOpener"
|
||||||
|
SERV_HUMIDIFIER_DEHUMIDIFIER = "HumidifierDehumidifier"
|
||||||
SERV_HUMIDITY_SENSOR = "HumiditySensor"
|
SERV_HUMIDITY_SENSOR = "HumiditySensor"
|
||||||
SERV_INPUT_SOURCE = "InputSource"
|
SERV_INPUT_SOURCE = "InputSource"
|
||||||
SERV_LEAK_SENSOR = "LeakSensor"
|
SERV_LEAK_SENSOR = "LeakSensor"
|
||||||
|
@ -151,15 +153,18 @@ CHAR_COOLING_THRESHOLD_TEMPERATURE = "CoolingThresholdTemperature"
|
||||||
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = "CurrentAmbientLightLevel"
|
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = "CurrentAmbientLightLevel"
|
||||||
CHAR_CURRENT_DOOR_STATE = "CurrentDoorState"
|
CHAR_CURRENT_DOOR_STATE = "CurrentDoorState"
|
||||||
CHAR_CURRENT_HEATING_COOLING = "CurrentHeatingCoolingState"
|
CHAR_CURRENT_HEATING_COOLING = "CurrentHeatingCoolingState"
|
||||||
|
CHAR_CURRENT_HUMIDIFIER_DEHUMIDIFIER = "CurrentHumidifierDehumidifierState"
|
||||||
CHAR_CURRENT_POSITION = "CurrentPosition"
|
CHAR_CURRENT_POSITION = "CurrentPosition"
|
||||||
CHAR_CURRENT_HUMIDITY = "CurrentRelativeHumidity"
|
CHAR_CURRENT_HUMIDITY = "CurrentRelativeHumidity"
|
||||||
CHAR_CURRENT_SECURITY_STATE = "SecuritySystemCurrentState"
|
CHAR_CURRENT_SECURITY_STATE = "SecuritySystemCurrentState"
|
||||||
CHAR_CURRENT_TEMPERATURE = "CurrentTemperature"
|
CHAR_CURRENT_TEMPERATURE = "CurrentTemperature"
|
||||||
CHAR_CURRENT_TILT_ANGLE = "CurrentHorizontalTiltAngle"
|
CHAR_CURRENT_TILT_ANGLE = "CurrentHorizontalTiltAngle"
|
||||||
CHAR_CURRENT_VISIBILITY_STATE = "CurrentVisibilityState"
|
CHAR_CURRENT_VISIBILITY_STATE = "CurrentVisibilityState"
|
||||||
|
CHAR_DEHUMIDIFIER_THRESHOLD_HUMIDITY = "RelativeHumidityDehumidifierThreshold"
|
||||||
CHAR_FIRMWARE_REVISION = "FirmwareRevision"
|
CHAR_FIRMWARE_REVISION = "FirmwareRevision"
|
||||||
CHAR_HEATING_THRESHOLD_TEMPERATURE = "HeatingThresholdTemperature"
|
CHAR_HEATING_THRESHOLD_TEMPERATURE = "HeatingThresholdTemperature"
|
||||||
CHAR_HUE = "Hue"
|
CHAR_HUE = "Hue"
|
||||||
|
CHAR_HUMIDIFIER_THRESHOLD_HUMIDITY = "RelativeHumidityHumidifierThreshold"
|
||||||
CHAR_IDENTIFIER = "Identifier"
|
CHAR_IDENTIFIER = "Identifier"
|
||||||
CHAR_IN_USE = "InUse"
|
CHAR_IN_USE = "InUse"
|
||||||
CHAR_INPUT_SOURCE_TYPE = "InputSourceType"
|
CHAR_INPUT_SOURCE_TYPE = "InputSourceType"
|
||||||
|
@ -190,6 +195,7 @@ CHAR_SWING_MODE = "SwingMode"
|
||||||
CHAR_TARGET_DOOR_STATE = "TargetDoorState"
|
CHAR_TARGET_DOOR_STATE = "TargetDoorState"
|
||||||
CHAR_TARGET_HEATING_COOLING = "TargetHeatingCoolingState"
|
CHAR_TARGET_HEATING_COOLING = "TargetHeatingCoolingState"
|
||||||
CHAR_TARGET_POSITION = "TargetPosition"
|
CHAR_TARGET_POSITION = "TargetPosition"
|
||||||
|
CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER = "TargetHumidifierDehumidifierState"
|
||||||
CHAR_TARGET_HUMIDITY = "TargetRelativeHumidity"
|
CHAR_TARGET_HUMIDITY = "TargetRelativeHumidity"
|
||||||
CHAR_TARGET_SECURITY_STATE = "SecuritySystemTargetState"
|
CHAR_TARGET_SECURITY_STATE = "SecuritySystemTargetState"
|
||||||
CHAR_TARGET_TEMPERATURE = "TargetTemperature"
|
CHAR_TARGET_TEMPERATURE = "TargetTemperature"
|
||||||
|
@ -207,6 +213,7 @@ PROP_MAX_VALUE = "maxValue"
|
||||||
PROP_MIN_VALUE = "minValue"
|
PROP_MIN_VALUE = "minValue"
|
||||||
PROP_MIN_STEP = "minStep"
|
PROP_MIN_STEP = "minStep"
|
||||||
PROP_CELSIUS = {"minValue": -273, "maxValue": 999}
|
PROP_CELSIUS = {"minValue": -273, "maxValue": 999}
|
||||||
|
PROP_VALID_VALUES = "ValidValues"
|
||||||
|
|
||||||
# #### Device Classes ####
|
# #### Device Classes ####
|
||||||
DEVICE_CLASS_CO = "co"
|
DEVICE_CLASS_CO = "co"
|
||||||
|
|
241
homeassistant/components/homekit/type_humidifiers.py
Normal file
241
homeassistant/components/homekit/type_humidifiers.py
Normal 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)
|
|
@ -34,6 +34,7 @@ from .const import (
|
||||||
CONF_FEATURE_LIST,
|
CONF_FEATURE_LIST,
|
||||||
CONF_LINKED_BATTERY_CHARGING_SENSOR,
|
CONF_LINKED_BATTERY_CHARGING_SENSOR,
|
||||||
CONF_LINKED_BATTERY_SENSOR,
|
CONF_LINKED_BATTERY_SENSOR,
|
||||||
|
CONF_LINKED_HUMIDITY_SENSOR,
|
||||||
CONF_LINKED_MOTION_SENSOR,
|
CONF_LINKED_MOTION_SENSOR,
|
||||||
CONF_LOW_BATTERY_THRESHOLD,
|
CONF_LOW_BATTERY_THRESHOLD,
|
||||||
CONF_MAX_FPS,
|
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(
|
CODE_SCHEMA = BASIC_INFO_SCHEMA.extend(
|
||||||
{vol.Optional(ATTR_CODE, default=None): vol.Any(None, cv.string)}
|
{vol.Optional(ATTR_CODE, default=None): vol.Any(None, cv.string)}
|
||||||
)
|
)
|
||||||
|
@ -230,6 +235,9 @@ def validate_entity_config(values):
|
||||||
elif domain == "switch":
|
elif domain == "switch":
|
||||||
config = SWITCH_TYPE_SCHEMA(config)
|
config = SWITCH_TYPE_SCHEMA(config)
|
||||||
|
|
||||||
|
elif domain == "humidifier":
|
||||||
|
config = HUMIDIFIER_SCHEMA(config)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
config = BASIC_INFO_SCHEMA(config)
|
config = BASIC_INFO_SCHEMA(config)
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ def _mock_config_entry_with_options_populated():
|
||||||
"filter": {
|
"filter": {
|
||||||
"include_domains": [
|
"include_domains": [
|
||||||
"fan",
|
"fan",
|
||||||
|
"humidifier",
|
||||||
"vacuum",
|
"vacuum",
|
||||||
"media_player",
|
"media_player",
|
||||||
"climate",
|
"climate",
|
||||||
|
@ -134,7 +135,8 @@ async def test_options_flow_advanced(hass):
|
||||||
assert result["step_id"] == "init"
|
assert result["step_id"] == "init"
|
||||||
|
|
||||||
result = await hass.config_entries.options.async_configure(
|
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
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
@ -157,7 +159,7 @@ async def test_options_flow_advanced(hass):
|
||||||
"filter": {
|
"filter": {
|
||||||
"exclude_domains": [],
|
"exclude_domains": [],
|
||||||
"exclude_entities": ["climate.old"],
|
"exclude_entities": ["climate.old"],
|
||||||
"include_domains": ["fan", "vacuum", "climate"],
|
"include_domains": ["fan", "vacuum", "climate", "humidifier"],
|
||||||
"include_entities": [],
|
"include_entities": [],
|
||||||
},
|
},
|
||||||
"safe_mode": True,
|
"safe_mode": True,
|
||||||
|
@ -332,6 +334,7 @@ async def test_options_flow_blocked_when_from_yaml(hass):
|
||||||
"filter": {
|
"filter": {
|
||||||
"include_domains": [
|
"include_domains": [
|
||||||
"fan",
|
"fan",
|
||||||
|
"humidifier",
|
||||||
"vacuum",
|
"vacuum",
|
||||||
"media_player",
|
"media_player",
|
||||||
"climate",
|
"climate",
|
||||||
|
|
|
@ -91,6 +91,7 @@ def test_customize_options(config, name):
|
||||||
{ATTR_SUPPORTED_FEATURES: climate.SUPPORT_TARGET_TEMPERATURE_RANGE},
|
{ATTR_SUPPORTED_FEATURES: climate.SUPPORT_TARGET_TEMPERATURE_RANGE},
|
||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
|
("HumidifierDehumidifier", "humidifier.test", "auto", {}, {}),
|
||||||
("WaterHeater", "water_heater.test", "auto", {}, {}),
|
("WaterHeater", "water_heater.test", "auto", {}, {}),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -42,13 +42,16 @@ from homeassistant.config_entries import SOURCE_IMPORT
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_DEVICE_CLASS,
|
ATTR_DEVICE_CLASS,
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
|
ATTR_UNIT_OF_MEASUREMENT,
|
||||||
CONF_IP_ADDRESS,
|
CONF_IP_ADDRESS,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
DEVICE_CLASS_BATTERY,
|
DEVICE_CLASS_BATTERY,
|
||||||
|
DEVICE_CLASS_HUMIDITY,
|
||||||
EVENT_HOMEASSISTANT_START,
|
EVENT_HOMEASSISTANT_START,
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
|
UNIT_PERCENTAGE,
|
||||||
)
|
)
|
||||||
from homeassistant.core import State
|
from homeassistant.core import State
|
||||||
from homeassistant.helpers import device_registry
|
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",
|
"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",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
419
tests/components/homekit/test_type_humidifiers.py
Normal file
419
tests/components/homekit/test_type_humidifiers.py
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue