From 4d63bf473d689fd575365937de9ac63eb0826833 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 19 Sep 2024 09:50:47 +0200 Subject: [PATCH] Add validation to set_humidity action in humidifier (#125863) --- .../components/humidifier/__init__.py | 38 ++++++++- .../components/humidifier/strings.json | 5 ++ tests/components/humidifier/conftest.py | 69 +++++++++++++++ tests/components/humidifier/test_init.py | 83 ++++++++++++++++++- 4 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 tests/components/humidifier/conftest.py diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index 37e2bd3e3ba..605bd4284f8 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -18,7 +18,8 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( all_with_deprecated_constants, @@ -45,7 +46,13 @@ from .const import ( # noqa: F401 DOMAIN, MODE_AUTO, MODE_AWAY, + MODE_BABY, + MODE_BOOST, + MODE_COMFORT, + MODE_ECO, + MODE_HOME, MODE_NORMAL, + MODE_SLEEP, SERVICE_SET_HUMIDITY, SERVICE_SET_MODE, HumidifierAction, @@ -108,7 +115,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: vol.Coerce(int), vol.Range(min=0, max=100) ) }, - "async_set_humidity", + async_service_humidity_set, ) return True @@ -281,6 +288,33 @@ class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_AT return features +async def async_service_humidity_set( + entity: HumidifierEntity, service_call: ServiceCall +) -> None: + """Handle set humidity service.""" + humidity = service_call.data[ATTR_HUMIDITY] + min_humidity = entity.min_humidity + max_humidity = entity.max_humidity + _LOGGER.debug( + "Check valid humidity %d in range %d - %d", + humidity, + min_humidity, + max_humidity, + ) + if humidity < min_humidity or humidity > max_humidity: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="humidity_out_of_range", + translation_placeholders={ + "humidity": str(humidity), + "min_humidity": str(min_humidity), + "max_humidity": str(max_humidity), + }, + ) + + await entity.async_set_humidity(humidity) + + # As we import deprecated constants from the const module, we need to add these two functions # otherwise this module will be logged for using deprecated constants and not the custom component # These can be removed if no deprecated constant are in this module anymore diff --git a/homeassistant/components/humidifier/strings.json b/homeassistant/components/humidifier/strings.json index 0416f4a68a6..753368dc572 100644 --- a/homeassistant/components/humidifier/strings.json +++ b/homeassistant/components/humidifier/strings.json @@ -115,5 +115,10 @@ "name": "[%key:common::action::toggle%]", "description": "Toggles the humidifier on/off." } + }, + "exceptions": { + "humidity_out_of_range": { + "message": "Provided humidity {humidity} is not valid. Accepted range is {min_humidity} to {max_humidity}." + } } } diff --git a/tests/components/humidifier/conftest.py b/tests/components/humidifier/conftest.py new file mode 100644 index 00000000000..9fe1720ffc0 --- /dev/null +++ b/tests/components/humidifier/conftest.py @@ -0,0 +1,69 @@ +"""Fixtures for Humidifier platform tests.""" + +from collections.abc import Generator + +import pytest + +from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from tests.common import ( + MockConfigEntry, + MockModule, + mock_config_flow, + mock_integration, + mock_platform, +) + + +class MockFlow(ConfigFlow): + """Test flow.""" + + +@pytest.fixture +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: + """Mock config flow.""" + mock_platform(hass, "test.config_flow") + + with mock_config_flow("test", MockFlow): + yield + + +@pytest.fixture +def register_test_integration( + hass: HomeAssistant, config_flow_fixture: None +) -> Generator: + """Provide a mocked integration for tests.""" + + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + + async def help_async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups( + config_entry, [HUMIDIFIER_DOMAIN] + ) + return True + + async def help_async_unload_entry( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Unload test config emntry.""" + return await hass.config_entries.async_unload_platforms( + config_entry, [Platform.HUMIDIFIER] + ) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=help_async_setup_entry_init, + async_unload_entry=help_async_unload_entry, + ), + ) + + return config_entry diff --git a/tests/components/humidifier/test_init.py b/tests/components/humidifier/test_init.py index b31750a3a3b..2725f942576 100644 --- a/tests/components/humidifier/test_init.py +++ b/tests/components/humidifier/test_init.py @@ -8,16 +8,28 @@ import pytest from homeassistant.components import humidifier from homeassistant.components.humidifier import ( + ATTR_HUMIDITY, ATTR_MODE, + DOMAIN as HUMIDIFIER_DOMAIN, + MODE_ECO, + MODE_NORMAL, + SERVICE_SET_HUMIDITY, HumidifierEntity, HumidifierEntityFeature, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError -from tests.common import help_test_all, import_and_test_deprecated_constant_enum +from tests.common import ( + MockConfigEntry, + MockEntity, + help_test_all, + import_and_test_deprecated_constant_enum, + setup_test_component_platform, +) -class MockHumidifierEntity(HumidifierEntity): +class MockHumidifierEntity(MockEntity, HumidifierEntity): """Mock Humidifier device to use in tests.""" @property @@ -101,3 +113,70 @@ def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> assert "is using deprecated supported features values" not in caplog.text assert entity.state_attributes[ATTR_MODE] == "mode1" + + +async def test_humidity_validation( + hass: HomeAssistant, + register_test_integration: MockConfigEntry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test validation for humidity.""" + + class MockHumidifierEntityHumidity(MockEntity, HumidifierEntity): + """Mock climate class with mocked aux heater.""" + + _attr_supported_features = HumidifierEntityFeature.MODES + _attr_available_modes = [MODE_NORMAL, MODE_ECO] + _attr_mode = MODE_NORMAL + _attr_target_humidity = 50 + _attr_min_humidity = 50 + _attr_max_humidity = 60 + + def set_humidity(self, humidity: int) -> None: + """Set new target humidity.""" + self._attr_target_humidity = humidity + + test_humidifier = MockHumidifierEntityHumidity( + name="Test", + unique_id="unique_humidifier_test", + ) + + setup_test_component_platform( + hass, HUMIDIFIER_DOMAIN, entities=[test_humidifier], from_config_entry=True + ) + await hass.config_entries.async_setup(register_test_integration.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("humidifier.test") + assert state.attributes.get(ATTR_HUMIDITY) == 50 + + with pytest.raises( + ServiceValidationError, + match="Provided humidity 1 is not valid. Accepted range is 50 to 60", + ) as exc: + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_SET_HUMIDITY, + { + "entity_id": "humidifier.test", + ATTR_HUMIDITY: "1", + }, + blocking=True, + ) + + assert exc.value.translation_key == "humidity_out_of_range" + assert "Check valid humidity 1 in range 50 - 60" in caplog.text + + with pytest.raises( + ServiceValidationError, + match="Provided humidity 70 is not valid. Accepted range is 50 to 60", + ) as exc: + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_SET_HUMIDITY, + { + "entity_id": "humidifier.test", + ATTR_HUMIDITY: "70", + }, + blocking=True, + )