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
136 changes: 136 additions & 0 deletions homeassistant/components/honeywell/humidifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""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 humidifier 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: Device, description: HoneywellHumidifierEntityDescription
) -> None:
"""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": "[%key:component::humidifier::title%]"
},
"dehumidifier": {
"name": "[%key:component::humidifier::entity_component::dehumidifier::name%]"
}
}
},
"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
23 changes: 23 additions & 0 deletions tests/components/honeywell/snapshots/test_humidity.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# serializer version: 1
# name: test_static_attributes
ReadOnlyDict({
'current_humidity': 50,
'device_class': 'dehumidifier',
'friendly_name': 'device1 Dehumidifier',
'humidity': 30,
'max_humidity': 55,
'min_humidity': 15,
'supported_features': <HumidifierEntityFeature: 0>,
})
# ---
# name: test_static_attributes.1
ReadOnlyDict({
'current_humidity': 50,
'device_class': 'humidifier',
'friendly_name': 'device1 Humidifier',
'humidity': 20,
'max_humidity': 60,
'min_humidity': 10,
'supported_features': <HumidifierEntityFeature: 0>,
})
# ---
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
112 changes: 112 additions & 0 deletions tests/components/honeywell/test_humidity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Test the Honeywell humidity domain."""
mkmer marked this conversation as resolved.
Show resolved Hide resolved

from unittest.mock import MagicMock

from syrupy.assertion import SnapshotAssertion

from homeassistant.components.humidifier import (
ATTR_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 homeassistant.helpers import entity_registry as er

from . import init_integration


async def test_humidifier_service_calls(
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

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_service_calls(
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

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)


async def test_static_attributes(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
device: MagicMock,
config_entry: MagicMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test static humidifier attributes."""
device.has_dehumidifier = True
device.has_humidifier = True
await init_integration(hass, config_entry)

entity_id_dehumidifier = f"humidifier.{device.name}_dehumidifier"
entity_id_humidifier = f"humidifier.{device.name}_humidifier"
entry = entity_registry.async_get(entity_id_dehumidifier)
assert entry

state = hass.states.get(entity_id_dehumidifier)
attributes = state.attributes

assert attributes == snapshot()

state = hass.states.get(entity_id_humidifier)
attributes = state.attributes

assert attributes == snapshot()
mkmer marked this conversation as resolved.
Show resolved Hide resolved