Add humidity (steamer) control to Huum (#150330)

Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Vincent Wolsink
2025-08-10 21:55:20 +02:00
committed by GitHub
parent b760bf342a
commit c2b284de2d
6 changed files with 218 additions and 1 deletions

View File

@@ -4,7 +4,7 @@ from homeassistant.const import Platform
DOMAIN = "huum"
PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.LIGHT]
PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.LIGHT, Platform.NUMBER]
CONFIG_STEAMER = 1
CONFIG_LIGHT = 2

View File

@@ -0,0 +1,13 @@
{
"entity": {
"number": {
"humidity": {
"default": "mdi:water",
"range": {
"0": "mdi:water-off",
"1": "mdi:water"
}
}
}
}
}

View File

@@ -0,0 +1,64 @@
"""Control for steamer."""
from __future__ import annotations
import logging
from huum.const import SaunaStatus
from homeassistant.components.number import NumberEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import CONFIG_STEAMER, CONFIG_STEAMER_AND_LIGHT
from .coordinator import HuumConfigEntry, HuumDataUpdateCoordinator
from .entity import HuumBaseEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HuumConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up steamer if applicable."""
coordinator = config_entry.runtime_data
# Light is configured for this sauna.
if coordinator.data.config in [CONFIG_STEAMER, CONFIG_STEAMER_AND_LIGHT]:
async_add_entities([HuumSteamer(coordinator)])
class HuumSteamer(HuumBaseEntity, NumberEntity):
"""Representation of a steamer."""
_attr_translation_key = "humidity"
_attr_native_max_value = 10
_attr_native_min_value = 0
_attr_native_step = 1
def __init__(self, coordinator: HuumDataUpdateCoordinator) -> None:
"""Initialize the steamer."""
super().__init__(coordinator)
self._attr_unique_id = coordinator.config_entry.entry_id
@property
def native_value(self) -> float:
"""Return the current value."""
return self.coordinator.data.humidity
async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""
target_temperature = self.coordinator.data.target_temperature
if (
not target_temperature
or self.coordinator.data.status != SaunaStatus.ONLINE_HEATING
):
return
await self.coordinator.huum.turn_on(
temperature=target_temperature, humidity=int(value)
)
await self.coordinator.async_refresh()

View File

@@ -24,6 +24,11 @@
"light": {
"name": "[%key:component::light::title%]"
}
},
"number": {
"humidity": {
"name": "[%key:component::sensor::entity_component::humidity::name%]"
}
}
}
}

View File

@@ -0,0 +1,58 @@
# serializer version: 1
# name: test_number_entity[number.huum_sauna_humidity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 10,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.huum_sauna_humidity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Humidity',
'platform': 'huum',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'humidity',
'unique_id': 'AABBCC112233',
'unit_of_measurement': None,
})
# ---
# name: test_number_entity[number.huum_sauna_humidity-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Huum sauna Humidity',
'max': 10,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'context': <ANY>,
'entity_id': 'number.huum_sauna_humidity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '5',
})
# ---

View File

@@ -0,0 +1,77 @@
"""Tests for the Huum number entity."""
from unittest.mock import AsyncMock
from huum.const import SaunaStatus
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.number import (
ATTR_VALUE,
DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_with_selected_platforms
from tests.common import MockConfigEntry, snapshot_platform
ENTITY_ID = "number.huum_sauna_humidity"
async def test_number_entity(
hass: HomeAssistant,
mock_huum: AsyncMock,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Test the initial parameters."""
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.NUMBER])
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_set_humidity(
hass: HomeAssistant,
mock_huum: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test setting the humidity."""
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.NUMBER])
mock_huum.status = SaunaStatus.ONLINE_HEATING
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: ENTITY_ID,
ATTR_VALUE: 5,
},
blocking=True,
)
mock_huum.turn_on.assert_called_once_with(temperature=80, humidity=5)
async def test_dont_set_humidity_when_sauna_not_heating(
hass: HomeAssistant,
mock_huum: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test setting the humidity."""
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.NUMBER])
mock_huum.status = SaunaStatus.ONLINE_NOT_HEATING
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: ENTITY_ID,
ATTR_VALUE: 5,
},
blocking=True,
)
mock_huum.turn_on.assert_not_called()