Add (de)humidifier platform to Honeywell (#132287)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
mkmer 2024-12-18 09:27:40 -05:00 committed by GitHub
parent 1e075cdac7
commit c9f1829c0b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 318 additions and 4 deletions

View File

@ -22,7 +22,7 @@ from .const import (
)
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}

View File

@ -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",
)
@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)

View File

@ -61,6 +61,14 @@
}
}
}
},
"humidifier": {
"humidifier": {
"name": "[%key:component::humidifier::title%]"
},
"dehumidifier": {
"name": "[%key:component::humidifier::entity_component::dehumidifier::name%]"
}
}
},
"exceptions": {

View File

@ -1,4 +1,4 @@
"""Tests for honeywell component."""
"""Tests for Honeywell component."""
from unittest.mock import MagicMock

View File

@ -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
@ -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,
@ -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,

View File

@ -0,0 +1,39 @@
# serializer version: 1
# name: test_static_attributes[dehumidifier]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_humidity': 50,
'device_class': 'dehumidifier',
'friendly_name': 'device1 Dehumidifier',
'humidity': 30,
'max_humidity': 55,
'min_humidity': 15,
'supported_features': <HumidifierEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'humidifier.device1_dehumidifier',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_static_attributes[humidifier]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_humidity': 50,
'device_class': 'humidifier',
'friendly_name': 'device1 Humidifier',
'humidity': 20,
'max_humidity': 60,
'min_humidity': 10,
'supported_features': <HumidifierEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'humidifier.device1_humidifier',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

@ -1,4 +1,4 @@
"""Test the Whirlpool Sixth Sense climate domain."""
"""Test the Honeywell climate domain."""
import datetime
from unittest.mock import MagicMock

View File

@ -0,0 +1,110 @@
"""Test the Honeywell humidity domain."""
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)
assert state == snapshot(name="dehumidifier")
state = hass.states.get(entity_id_humidifier)
assert state == snapshot(name="humidifier")