diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index e557e744777..bd04fd1a156 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -128,6 +128,7 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = { "climate", "device_tracker", "fan", + "humidifier", "lawn_mower", "light", "lock", diff --git a/homeassistant/components/humidifier/condition.py b/homeassistant/components/humidifier/condition.py new file mode 100644 index 00000000000..77c108128a2 --- /dev/null +++ b/homeassistant/components/humidifier/condition.py @@ -0,0 +1,27 @@ +"""Provides conditions for humidifiers.""" + +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers.condition import ( + Condition, + make_entity_state_attribute_condition, + make_entity_state_condition, +) + +from .const import ATTR_ACTION, DOMAIN, HumidifierAction + +CONDITIONS: dict[str, type[Condition]] = { + "is_off": make_entity_state_condition(DOMAIN, STATE_OFF), + "is_on": make_entity_state_condition(DOMAIN, STATE_ON), + "is_drying": make_entity_state_attribute_condition( + DOMAIN, ATTR_ACTION, HumidifierAction.DRYING + ), + "is_humidifying": make_entity_state_attribute_condition( + DOMAIN, ATTR_ACTION, HumidifierAction.HUMIDIFYING + ), +} + + +async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]: + """Return the humidifier conditions.""" + return CONDITIONS diff --git a/homeassistant/components/humidifier/conditions.yaml b/homeassistant/components/humidifier/conditions.yaml new file mode 100644 index 00000000000..ca80146d184 --- /dev/null +++ b/homeassistant/components/humidifier/conditions.yaml @@ -0,0 +1,19 @@ +.condition_common: &condition_common + target: + entity: + domain: humidifier + fields: + behavior: + required: true + default: any + selector: + select: + translation_key: condition_behavior + options: + - all + - any + +is_off: *condition_common +is_on: *condition_common +is_drying: *condition_common +is_humidifying: *condition_common diff --git a/homeassistant/components/humidifier/icons.json b/homeassistant/components/humidifier/icons.json index 4544b2bfb68..589759f7123 100644 --- a/homeassistant/components/humidifier/icons.json +++ b/homeassistant/components/humidifier/icons.json @@ -1,4 +1,18 @@ { + "conditions": { + "is_drying": { + "condition": "mdi:arrow-down-bold" + }, + "is_humidifying": { + "condition": "mdi:arrow-up-bold" + }, + "is_off": { + "condition": "mdi:air-humidifier-off" + }, + "is_on": { + "condition": "mdi:air-humidifier" + } + }, "entity_component": { "_": { "default": "mdi:air-humidifier", diff --git a/homeassistant/components/humidifier/strings.json b/homeassistant/components/humidifier/strings.json index 14371037a74..9182354de9a 100644 --- a/homeassistant/components/humidifier/strings.json +++ b/homeassistant/components/humidifier/strings.json @@ -1,8 +1,52 @@ { "common": { + "condition_behavior_description": "How the state should match on the targeted humidifiers.", + "condition_behavior_name": "Behavior", "trigger_behavior_description": "The behavior of the targeted humidifiers to trigger on.", "trigger_behavior_name": "Behavior" }, + "conditions": { + "is_drying": { + "description": "Tests if one or more humidifiers are drying.", + "fields": { + "behavior": { + "description": "[%key:component::humidifier::common::condition_behavior_description%]", + "name": "[%key:component::humidifier::common::condition_behavior_name%]" + } + }, + "name": "Humidifier is drying" + }, + "is_humidifying": { + "description": "Tests if one or more humidifiers are humidifying.", + "fields": { + "behavior": { + "description": "[%key:component::humidifier::common::condition_behavior_description%]", + "name": "[%key:component::humidifier::common::condition_behavior_name%]" + } + }, + "name": "Humidifier is humidifying" + }, + "is_off": { + "description": "Tests if one or more humidifiers are off.", + "fields": { + "behavior": { + "description": "[%key:component::humidifier::common::condition_behavior_description%]", + "name": "[%key:component::humidifier::common::condition_behavior_name%]" + } + }, + "name": "Humidifier is off" + }, + "is_on": { + "description": "Tests if one or more humidifiers are on.", + "fields": { + "behavior": { + "description": "[%key:component::humidifier::common::condition_behavior_description%]", + "name": "[%key:component::humidifier::common::condition_behavior_name%]" + } + }, + "name": "Humidifier is on" + } + }, "device_automation": { "action_type": { "set_humidity": "Set humidity for {entity_name}", @@ -91,6 +135,12 @@ } }, "selector": { + "condition_behavior": { + "options": { + "all": "All", + "any": "Any" + } + }, "number_or_entity": { "choices": { "entity": "Entity", diff --git a/tests/components/humidifier/test_condition.py b/tests/components/humidifier/test_condition.py new file mode 100644 index 00000000000..8355ec787da --- /dev/null +++ b/tests/components/humidifier/test_condition.py @@ -0,0 +1,276 @@ +"""Test humidifier conditions.""" + +from typing import Any + +import pytest + +from homeassistant.components.humidifier.const import ATTR_ACTION, HumidifierAction +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant + +from tests.components import ( + ConditionStateDescription, + assert_condition_gated_by_labs_flag, + create_target_condition, + parametrize_condition_states_all, + parametrize_condition_states_any, + parametrize_target_entities, + set_or_remove_state, + target_entities, +) + + +@pytest.fixture +async def target_humidifiers(hass: HomeAssistant) -> list[str]: + """Create multiple humidifier entities associated with different targets.""" + return (await target_entities(hass, "humidifier"))["included"] + + +@pytest.mark.parametrize( + "condition", + [ + "humidifier.is_off", + "humidifier.is_on", + "humidifier.is_drying", + "humidifier.is_humidifying", + ], +) +async def test_humidifier_conditions_gated_by_labs_flag( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, condition: str +) -> None: + """Test the humidifier conditions are gated by the labs flag.""" + await assert_condition_gated_by_labs_flag(hass, caplog, condition) + + +@pytest.mark.usefixtures("enable_labs_preview_features") +@pytest.mark.parametrize( + ("condition_target_config", "entity_id", "entities_in_target"), + parametrize_target_entities("humidifier"), +) +@pytest.mark.parametrize( + ("condition", "condition_options", "states"), + [ + *parametrize_condition_states_any( + condition="humidifier.is_off", + target_states=[STATE_OFF], + other_states=[STATE_ON], + ), + *parametrize_condition_states_any( + condition="humidifier.is_on", + target_states=[STATE_ON], + other_states=[STATE_OFF], + ), + ], +) +async def test_humidifier_state_condition_behavior_any( + hass: HomeAssistant, + target_humidifiers: list[str], + condition_target_config: dict, + entity_id: str, + entities_in_target: int, + condition: str, + condition_options: dict[str, Any], + states: list[ConditionStateDescription], +) -> None: + """Test the humidifier state condition with the 'any' behavior.""" + other_entity_ids = set(target_humidifiers) - {entity_id} + + # Set all humidifiers, including the tested humidifier, to the initial state + for eid in target_humidifiers: + set_or_remove_state(hass, eid, states[0]["included"]) + await hass.async_block_till_done() + + condition = await create_target_condition( + hass, + condition=condition, + target=condition_target_config, + behavior="any", + ) + + for state in states: + included_state = state["included"] + set_or_remove_state(hass, entity_id, included_state) + await hass.async_block_till_done() + assert condition(hass) == state["condition_true"] + + # Check if changing other humidifiers also passes the condition + for other_entity_id in other_entity_ids: + set_or_remove_state(hass, other_entity_id, included_state) + await hass.async_block_till_done() + assert condition(hass) == state["condition_true"] + + +@pytest.mark.usefixtures("enable_labs_preview_features") +@pytest.mark.parametrize( + ("condition_target_config", "entity_id", "entities_in_target"), + parametrize_target_entities("humidifier"), +) +@pytest.mark.parametrize( + ("condition", "condition_options", "states"), + [ + *parametrize_condition_states_all( + condition="humidifier.is_off", + target_states=[STATE_OFF], + other_states=[STATE_ON], + ), + *parametrize_condition_states_all( + condition="humidifier.is_on", + target_states=[STATE_ON], + other_states=[STATE_OFF], + ), + ], +) +async def test_humidifier_state_condition_behavior_all( + hass: HomeAssistant, + target_humidifiers: list[str], + condition_target_config: dict, + entity_id: str, + entities_in_target: int, + condition: str, + condition_options: dict[str, Any], + states: list[ConditionStateDescription], +) -> None: + """Test the humidifier state condition with the 'all' behavior.""" + other_entity_ids = set(target_humidifiers) - {entity_id} + + # Set all humidifiers, including the tested humidifier, to the initial state + for eid in target_humidifiers: + set_or_remove_state(hass, eid, states[0]["included"]) + await hass.async_block_till_done() + + condition = await create_target_condition( + hass, + condition=condition, + target=condition_target_config, + behavior="all", + ) + + for state in states: + included_state = state["included"] + + set_or_remove_state(hass, entity_id, included_state) + await hass.async_block_till_done() + assert condition(hass) == state["condition_true_first_entity"] + + for other_entity_id in other_entity_ids: + set_or_remove_state(hass, other_entity_id, included_state) + await hass.async_block_till_done() + + assert condition(hass) == state["condition_true"] + + +@pytest.mark.usefixtures("enable_labs_preview_features") +@pytest.mark.parametrize( + ("condition_target_config", "entity_id", "entities_in_target"), + parametrize_target_entities("humidifier"), +) +@pytest.mark.parametrize( + ("condition", "condition_options", "states"), + [ + *parametrize_condition_states_any( + condition="humidifier.is_drying", + target_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.DRYING})], + other_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.IDLE})], + ), + *parametrize_condition_states_any( + condition="humidifier.is_humidifying", + target_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.HUMIDIFYING})], + other_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.IDLE})], + ), + ], +) +async def test_humidifier_attribute_condition_behavior_any( + hass: HomeAssistant, + target_humidifiers: list[str], + condition_target_config: dict, + entity_id: str, + entities_in_target: int, + condition: str, + condition_options: dict[str, Any], + states: list[ConditionStateDescription], +) -> None: + """Test the humidifier attribute condition with the 'any' behavior.""" + other_entity_ids = set(target_humidifiers) - {entity_id} + + # Set all humidifiers, including the tested humidifier, to the initial state + for eid in target_humidifiers: + set_or_remove_state(hass, eid, states[0]["included"]) + await hass.async_block_till_done() + + condition = await create_target_condition( + hass, + condition=condition, + target=condition_target_config, + behavior="any", + ) + + for state in states: + included_state = state["included"] + set_or_remove_state(hass, entity_id, included_state) + await hass.async_block_till_done() + assert condition(hass) == state["condition_true"] + + # Check if changing other humidifiers also passes the condition + for other_entity_id in other_entity_ids: + set_or_remove_state(hass, other_entity_id, included_state) + await hass.async_block_till_done() + assert condition(hass) == state["condition_true"] + + +@pytest.mark.usefixtures("enable_labs_preview_features") +@pytest.mark.parametrize( + ("condition_target_config", "entity_id", "entities_in_target"), + parametrize_target_entities("humidifier"), +) +@pytest.mark.parametrize( + ("condition", "condition_options", "states"), + [ + *parametrize_condition_states_all( + condition="humidifier.is_drying", + target_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.DRYING})], + other_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.IDLE})], + ), + *parametrize_condition_states_all( + condition="humidifier.is_humidifying", + target_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.HUMIDIFYING})], + other_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.IDLE})], + ), + ], +) +async def test_humidifier_attribute_condition_behavior_all( + hass: HomeAssistant, + target_humidifiers: list[str], + condition_target_config: dict, + entity_id: str, + entities_in_target: int, + condition: str, + condition_options: dict[str, Any], + states: list[ConditionStateDescription], +) -> None: + """Test the humidifier attribute condition with the 'all' behavior.""" + other_entity_ids = set(target_humidifiers) - {entity_id} + + # Set all humidifiers, including the tested humidifier, to the initial state + for eid in target_humidifiers: + set_or_remove_state(hass, eid, states[0]["included"]) + await hass.async_block_till_done() + + condition = await create_target_condition( + hass, + condition=condition, + target=condition_target_config, + behavior="all", + ) + + for state in states: + included_state = state["included"] + + set_or_remove_state(hass, entity_id, included_state) + await hass.async_block_till_done() + assert condition(hass) == state["condition_true_first_entity"] + + for other_entity_id in other_entity_ids: + set_or_remove_state(hass, other_entity_id, included_state) + await hass.async_block_till_done() + + assert condition(hass) == state["condition_true"]