From c2b284de2df830698bbf027334c32f36b85e3c14 Mon Sep 17 00:00:00 2001 From: Vincent Wolsink Date: Sun, 10 Aug 2025 21:55:20 +0200 Subject: [PATCH] Add humidity (steamer) control to Huum (#150330) Co-authored-by: Norbert Rittel Co-authored-by: Joost Lekkerkerker --- homeassistant/components/huum/const.py | 2 +- homeassistant/components/huum/icons.json | 13 ++++ homeassistant/components/huum/number.py | 64 +++++++++++++++ homeassistant/components/huum/strings.json | 5 ++ .../huum/snapshots/test_number.ambr | 58 ++++++++++++++ tests/components/huum/test_number.py | 77 +++++++++++++++++++ 6 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/huum/icons.json create mode 100644 homeassistant/components/huum/number.py create mode 100644 tests/components/huum/snapshots/test_number.ambr create mode 100644 tests/components/huum/test_number.py diff --git a/homeassistant/components/huum/const.py b/homeassistant/components/huum/const.py index 13663d31cd0..177c035f041 100644 --- a/homeassistant/components/huum/const.py +++ b/homeassistant/components/huum/const.py @@ -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 diff --git a/homeassistant/components/huum/icons.json b/homeassistant/components/huum/icons.json new file mode 100644 index 00000000000..4281cdbde2a --- /dev/null +++ b/homeassistant/components/huum/icons.json @@ -0,0 +1,13 @@ +{ + "entity": { + "number": { + "humidity": { + "default": "mdi:water", + "range": { + "0": "mdi:water-off", + "1": "mdi:water" + } + } + } + } +} diff --git a/homeassistant/components/huum/number.py b/homeassistant/components/huum/number.py new file mode 100644 index 00000000000..daaf348c029 --- /dev/null +++ b/homeassistant/components/huum/number.py @@ -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() diff --git a/homeassistant/components/huum/strings.json b/homeassistant/components/huum/strings.json index 55ccf0fdd81..13c2e5c85f6 100644 --- a/homeassistant/components/huum/strings.json +++ b/homeassistant/components/huum/strings.json @@ -24,6 +24,11 @@ "light": { "name": "[%key:component::light::title%]" } + }, + "number": { + "humidity": { + "name": "[%key:component::sensor::entity_component::humidity::name%]" + } } } } diff --git a/tests/components/huum/snapshots/test_number.ambr b/tests/components/huum/snapshots/test_number.ambr new file mode 100644 index 00000000000..19c0642f007 --- /dev/null +++ b/tests/components/huum/snapshots/test_number.ambr @@ -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': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.huum_sauna_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + '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': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.huum_sauna_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5', + }) +# --- diff --git a/tests/components/huum/test_number.py b/tests/components/huum/test_number.py new file mode 100644 index 00000000000..3d7a74bfce3 --- /dev/null +++ b/tests/components/huum/test_number.py @@ -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()