Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add (de)humidifier platform to Honeywell #132287

Merged
merged 14 commits into from
Dec 18, 2024
2 changes: 1 addition & 1 deletion homeassistant/components/honeywell/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
)

UPDATE_LOOP_SLEEP_TIME = 5
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.SWITCH]
PLATFORMS = [Platform.CLIMATE, Platform.HUMIDIFIER, Platform.SENSOR, Platform.SWITCH]

MIGRATE_OPTIONS_KEYS = {CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE}

Expand Down
134 changes: 134 additions & 0 deletions homeassistant/components/honeywell/humidifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""Support for Honeywell (de)humidifiers."""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

from aiosomecomfort.device import Device

from homeassistant.components.humidifier import (
HumidifierDeviceClass,
HumidifierEntity,
HumidifierEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import HoneywellConfigEntry
from .const import DOMAIN

HUMIDIFIER_KEY = "humidifier"
DEHUMIDIFIER_KEY = "dehumidifier"


@dataclass(frozen=True, kw_only=True)
class HoneywellHumidifierEntityDescription(HumidifierEntityDescription):
"""Describes a Honeywell sensor entity."""

current_humidity: Callable[[Device], Any]
current_set_humidity: Callable[[Device], Any]
max_humidity: Callable[[Device], Any]
min_humidity: Callable[[Device], Any]
set_humidity: Callable[[Device, Any], Any]
mode: Callable[[Device], Any]
off: Callable[[Device], Any]
on: Callable[[Device], Any]


HUMIDIFIERS: dict[str, HoneywellHumidifierEntityDescription] = {
"Humidifier": HoneywellHumidifierEntityDescription(
key=HUMIDIFIER_KEY,
translation_key=HUMIDIFIER_KEY,
current_humidity=lambda device: device.current_humidity,
set_humidity=lambda device, humidity: device.set_humidifier_setpoint(humidity),
min_humidity=lambda device: device.humidifier_lower_limit,
max_humidity=lambda device: device.humidifier_upper_limit,
current_set_humidity=lambda device: device.humidifier_setpoint,
mode=lambda device: device.humidifier_mode,
off=lambda device: device.set_humidifier_off(),
on=lambda device: device.set_humidifier_auto(),
device_class=HumidifierDeviceClass.HUMIDIFIER,
),
"Dehumidifier": HoneywellHumidifierEntityDescription(
key=DEHUMIDIFIER_KEY,
translation_key=DEHUMIDIFIER_KEY,
current_humidity=lambda device: device.current_humidity,
set_humidity=lambda device, humidity: device.set_dehumidifier_setpoint(
humidity
),
min_humidity=lambda device: device.dehumidifier_lower_limit,
max_humidity=lambda device: device.dehumidifier_upper_limit,
current_set_humidity=lambda device: device.dehumidifier_setpoint,
mode=lambda device: device.dehumidifier_mode,
off=lambda device: device.set_dehumidifier_off(),
on=lambda device: device.set_dehumidifier_auto(),
device_class=HumidifierDeviceClass.DEHUMIDIFIER,
),
}


async def async_setup_entry(
hass: HomeAssistant,
config_entry: HoneywellConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Honeywell (de)humidifier dynamically."""
data = config_entry.runtime_data
entities: list = []
for device in data.devices.values():
if device.has_humidifier:
entities.append(HoneywellHumidifier(device, HUMIDIFIERS["Humidifier"]))
if device.has_dehumidifier:
entities.append(HoneywellHumidifier(device, HUMIDIFIERS["Dehumidifier"]))

async_add_entities(entities)


class HoneywellHumidifier(HumidifierEntity):
"""Representation of a Honeywell US (De)Humidifier."""

entity_description: HoneywellHumidifierEntityDescription
_attr_has_entity_name = True

def __init__(self, device, description) -> None:
mkmer marked this conversation as resolved.
Show resolved Hide resolved
"""Initialize the (De)Humidifier."""
self._device = device
self.entity_description = description
self._attr_unique_id = f"{device.deviceid}_{description.key}"
self._attr_min_humidity = description.min_humidity(device)
self._attr_max_humidity = description.max_humidity(device)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.deviceid)},
name=device.name,
manufacturer="Honeywell",
)
mkmer marked this conversation as resolved.
Show resolved Hide resolved

@property
def is_on(self) -> bool:
"""Return the device is on or off."""
return self.entity_description.mode(self._device) != 0

@property
def target_humidity(self) -> int | None:
"""Return the humidity we try to reach."""
return self.entity_description.current_set_humidity(self._device)

@property
def current_humidity(self) -> int | None:
"""Return the current humidity."""
return self.entity_description.current_humidity(self._device)

async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
await self.entity_description.on(self._device)

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
await self.entity_description.off(self._device)

async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
await self.entity_description.set_humidity(self._device, humidity)
8 changes: 8 additions & 0 deletions homeassistant/components/honeywell/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@
}
}
}
},
"humidifier": {
"humidifier": {
"name": "Humidifier"
},
"dehumidifier": {
"name": "Dehumidifier"
mkmer marked this conversation as resolved.
Show resolved Hide resolved
}
}
},
"exceptions": {
Expand Down
2 changes: 1 addition & 1 deletion tests/components/honeywell/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Tests for honeywell component."""
"""Tests for Honeywell component."""

from unittest.mock import MagicMock

Expand Down
23 changes: 22 additions & 1 deletion tests/components/honeywell/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,16 @@ def device():
mock_device.refresh = AsyncMock()
mock_device.heat_away_temp = HEATAWAY
mock_device.cool_away_temp = COOLAWAY

mock_device.has_humidifier = False
mock_device.has_dehumidifier = False
mock_device.humidifier_upper_limit = 60
mock_device.humidifier_lower_limit = 10
mock_device.humidifier_setpoint = 20
mock_device.dehumidifier_mode = 1
mock_device.dehumidifier_upper_limit = 55
mock_device.dehumidifier_lower_limit = 15
mock_device.dehumidifier_setpoint = 30
mock_device.dehumidifier_mode = 1
mock_device.raw_dr_data = {"CoolSetpLimit": None, "HeatSetpLimit": None}

return mock_device
Expand All @@ -149,6 +158,8 @@ def device_with_outdoor_sensor():
mock_device.temperature_unit = "C"
mock_device.outdoor_temperature = OUTDOORTEMP
mock_device.outdoor_humidity = OUTDOORHUMIDITY
mock_device.has_humidifier = False
mock_device.has_dehumidifier = False
mock_device.raw_ui_data = {
"SwitchOffAllowed": True,
"SwitchAutoAllowed": True,
Expand Down Expand Up @@ -188,6 +199,16 @@ def another_device():
mock_device.mac_address = "macaddress1"
mock_device.outdoor_temperature = None
mock_device.outdoor_humidity = None
mock_device.has_humidifier = False
mock_device.has_dehumidifier = False
mock_device.humidifier_upper_limit = 60
mock_device.humidifier_lower_limit = 10
mock_device.humidifier_setpoint = 20
mock_device.dehumidifier_mode = 1
mock_device.dehumidifier_upper_limit = 55
mock_device.dehumidifier_lower_limit = 15
mock_device.dehumidifier_setpoint = 30
mock_device.dehumidifier_mode = 1
mock_device.raw_ui_data = {
"SwitchOffAllowed": True,
"SwitchAutoAllowed": True,
Expand Down
2 changes: 1 addition & 1 deletion tests/components/honeywell/test_climate.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Test the Whirlpool Sixth Sense climate domain."""
"""Test the Honeywell climate domain."""

import datetime
from unittest.mock import MagicMock
Expand Down
98 changes: 98 additions & 0 deletions tests/components/honeywell/test_humidity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Test the Honeywell humidity domain."""
mkmer marked this conversation as resolved.
Show resolved Hide resolved

from unittest.mock import MagicMock

from homeassistant.components.humidifier import (
ATTR_CURRENT_HUMIDITY,
ATTR_HUMIDITY,
ATTR_MAX_HUMIDITY,
ATTR_MIN_HUMIDITY,
DOMAIN as HUMIDIFIER_DOMAIN,
SERVICE_SET_HUMIDITY,
)
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
from homeassistant.core import HomeAssistant

from . import init_integration


async def test_humidifier(
hass: HomeAssistant, device: MagicMock, config_entry: MagicMock
) -> None:
"""Test the setup of the climate entities when there are no additional options available."""
device.has_humidifier = True
await init_integration(hass, config_entry)
entity_id = f"humidifier.{device.name}_humidifier"
assert hass.states.get(f"humidifier.{device.name}_dehumidifier") is None
state = hass.states.get(entity_id)
assert state
attributes = state.attributes
assert attributes[ATTR_MAX_HUMIDITY] == 60
assert attributes[ATTR_MIN_HUMIDITY] == 10
assert attributes[ATTR_CURRENT_HUMIDITY] == 50
assert attributes[ATTR_HUMIDITY] == 20

await hass.services.async_call(
HUMIDIFIER_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
device.set_humidifier_auto.assert_called_once()

await hass.services.async_call(
HUMIDIFIER_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
device.set_humidifier_off.assert_called_once()

await hass.services.async_call(
HUMIDIFIER_DOMAIN,
SERVICE_SET_HUMIDITY,
{ATTR_ENTITY_ID: entity_id, ATTR_HUMIDITY: 40},
blocking=True,
)
device.set_humidifier_setpoint.assert_called_once_with(40)


async def test_dehumidifier(
hass: HomeAssistant, device: MagicMock, config_entry: MagicMock
) -> None:
"""Test the setup of the climate entities when there are no additional options available."""
device.has_dehumidifier = True
await init_integration(hass, config_entry)
entity_id = f"humidifier.{device.name}_dehumidifier"
assert hass.states.get(f"humidifier.{device.name}_humidifier") is None
state = hass.states.get(entity_id)
assert state
attributes = state.attributes
assert attributes[ATTR_MAX_HUMIDITY] == 55
assert attributes[ATTR_MIN_HUMIDITY] == 15
assert attributes[ATTR_CURRENT_HUMIDITY] == 50
assert attributes[ATTR_HUMIDITY] == 30

await hass.services.async_call(
HUMIDIFIER_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
device.set_dehumidifier_auto.assert_called_once()

await hass.services.async_call(
HUMIDIFIER_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
device.set_dehumidifier_off.assert_called_once()

await hass.services.async_call(
HUMIDIFIER_DOMAIN,
SERVICE_SET_HUMIDITY,
{ATTR_ENTITY_ID: entity_id, ATTR_HUMIDITY: 40},
blocking=True,
)
device.set_dehumidifier_setpoint.assert_called_once_with(40)