Skip to content

Commit dfa911b

Browse files
authored
Add tests asserting air_quality trigger features (#168377)
1 parent 6da92a8 commit dfa911b

File tree

2 files changed

+139
-0
lines changed

2 files changed

+139
-0
lines changed

tests/components/air_quality/test_trigger.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
assert_trigger_behavior_first,
2626
assert_trigger_behavior_last,
2727
assert_trigger_gated_by_labs_flag,
28+
assert_trigger_options_supported,
2829
parametrize_numerical_state_value_changed_trigger_states,
2930
parametrize_numerical_state_value_crossed_threshold_trigger_states,
3031
parametrize_target_entities,
@@ -95,6 +96,83 @@ async def test_air_quality_triggers_gated_by_labs_flag(
9596
await assert_trigger_gated_by_labs_flag(hass, caplog, trigger_key)
9697

9798

99+
_CHANGED_THRESHOLD = {"threshold": {"type": "any"}}
100+
_PLAIN_CROSSED_THRESHOLD = {"threshold": {"type": "above", "value": {"number": 50}}}
101+
_PPB_CROSSED_THRESHOLD = {
102+
"threshold": {
103+
"type": "above",
104+
"value": {
105+
"number": 50,
106+
"unit_of_measurement": CONCENTRATION_PARTS_PER_BILLION,
107+
},
108+
}
109+
}
110+
_UGM3_CROSSED_THRESHOLD = {
111+
"threshold": {
112+
"type": "above",
113+
"value": {
114+
"number": 50,
115+
"unit_of_measurement": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
116+
},
117+
}
118+
}
119+
120+
121+
@pytest.mark.usefixtures("enable_labs_preview_features")
122+
@pytest.mark.parametrize(
123+
("trigger_key", "base_options", "supports_behavior", "supports_duration"),
124+
[
125+
("air_quality.gas_detected", {}, True, True),
126+
("air_quality.gas_cleared", {}, True, True),
127+
("air_quality.co_detected", {}, True, True),
128+
("air_quality.co_cleared", {}, True, True),
129+
("air_quality.smoke_detected", {}, True, True),
130+
("air_quality.smoke_cleared", {}, True, True),
131+
("air_quality.co_changed", _CHANGED_THRESHOLD, False, False),
132+
("air_quality.co_crossed_threshold", _UGM3_CROSSED_THRESHOLD, True, True),
133+
("air_quality.co2_changed", _CHANGED_THRESHOLD, False, False),
134+
("air_quality.co2_crossed_threshold", _PLAIN_CROSSED_THRESHOLD, True, True),
135+
("air_quality.pm1_changed", _CHANGED_THRESHOLD, False, False),
136+
("air_quality.pm1_crossed_threshold", _PLAIN_CROSSED_THRESHOLD, True, True),
137+
("air_quality.pm25_changed", _CHANGED_THRESHOLD, False, False),
138+
("air_quality.pm25_crossed_threshold", _PLAIN_CROSSED_THRESHOLD, True, True),
139+
("air_quality.pm4_changed", _CHANGED_THRESHOLD, False, False),
140+
("air_quality.pm4_crossed_threshold", _PLAIN_CROSSED_THRESHOLD, True, True),
141+
("air_quality.pm10_changed", _CHANGED_THRESHOLD, False, False),
142+
("air_quality.pm10_crossed_threshold", _PLAIN_CROSSED_THRESHOLD, True, True),
143+
("air_quality.ozone_changed", _CHANGED_THRESHOLD, False, False),
144+
("air_quality.ozone_crossed_threshold", _UGM3_CROSSED_THRESHOLD, True, True),
145+
("air_quality.voc_changed", _CHANGED_THRESHOLD, False, False),
146+
("air_quality.voc_crossed_threshold", _UGM3_CROSSED_THRESHOLD, True, True),
147+
("air_quality.voc_ratio_changed", _CHANGED_THRESHOLD, False, False),
148+
("air_quality.voc_ratio_crossed_threshold", _PPB_CROSSED_THRESHOLD, True, True),
149+
("air_quality.no_changed", _CHANGED_THRESHOLD, False, False),
150+
("air_quality.no_crossed_threshold", _UGM3_CROSSED_THRESHOLD, True, True),
151+
("air_quality.no2_changed", _CHANGED_THRESHOLD, False, False),
152+
("air_quality.no2_crossed_threshold", _UGM3_CROSSED_THRESHOLD, True, True),
153+
("air_quality.n2o_changed", _CHANGED_THRESHOLD, False, False),
154+
("air_quality.n2o_crossed_threshold", _PLAIN_CROSSED_THRESHOLD, True, True),
155+
("air_quality.so2_changed", _CHANGED_THRESHOLD, False, False),
156+
("air_quality.so2_crossed_threshold", _UGM3_CROSSED_THRESHOLD, True, True),
157+
],
158+
)
159+
async def test_air_quality_trigger_options_validation(
160+
hass: HomeAssistant,
161+
trigger_key: str,
162+
base_options: dict[str, Any] | None,
163+
supports_behavior: bool,
164+
supports_duration: bool,
165+
) -> None:
166+
"""Test that air_quality triggers support the expected options."""
167+
await assert_trigger_options_supported(
168+
hass,
169+
trigger_key,
170+
base_options,
171+
supports_behavior=supports_behavior,
172+
supports_duration=supports_duration,
173+
)
174+
175+
98176
@pytest.mark.usefixtures("enable_labs_preview_features")
99177
@pytest.mark.parametrize(
100178
("trigger_target_config", "entity_id", "entities_in_target"),

tests/components/common.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Any, TypedDict
99

1010
import pytest
11+
import voluptuous as vol
1112

1213
from homeassistant.const import (
1314
ATTR_AREA_ID,
@@ -1095,6 +1096,66 @@ async def assert_trigger_gated_by_labs_flag(
10951096
) in caplog.text
10961097

10971098

1099+
async def _validate_trigger_options(
1100+
hass: HomeAssistant,
1101+
trigger: str,
1102+
options: dict[str, Any] | None,
1103+
*,
1104+
valid: bool,
1105+
) -> None:
1106+
"""Assert that a trigger accepts or rejects the given options during validation."""
1107+
trigger_config: dict[str, Any] = {
1108+
CONF_PLATFORM: trigger,
1109+
CONF_TARGET: {ATTR_LABEL_ID: "test_label"},
1110+
}
1111+
if options is not None:
1112+
trigger_config[CONF_OPTIONS] = options
1113+
if valid:
1114+
await async_validate_trigger_config(hass, [trigger_config])
1115+
else:
1116+
with pytest.raises(vol.Invalid):
1117+
await async_validate_trigger_config(hass, [trigger_config])
1118+
1119+
1120+
async def assert_trigger_options_supported(
1121+
hass: HomeAssistant,
1122+
trigger: str,
1123+
base_options: dict[str, Any] | None,
1124+
*,
1125+
supports_behavior: bool,
1126+
supports_duration: bool,
1127+
) -> None:
1128+
"""Assert which options a trigger supports.
1129+
1130+
Tests that the trigger:
1131+
- Accepts the minimal config (base_options)
1132+
- Accepts/rejects behavior depending on supports_behavior
1133+
- Accepts/rejects duration depending on supports_duration
1134+
- Rejects unknown options
1135+
"""
1136+
# Minimal config should always be valid
1137+
await _validate_trigger_options(hass, trigger, base_options, valid=True)
1138+
1139+
def _merge(extra: dict[str, Any]) -> dict[str, Any]:
1140+
return {**(base_options or {}), **extra}
1141+
1142+
# Behavior
1143+
for behavior in ("any", "first", "last"):
1144+
await _validate_trigger_options(
1145+
hass, trigger, _merge({"behavior": behavior}), valid=supports_behavior
1146+
)
1147+
1148+
# Duration
1149+
await _validate_trigger_options(
1150+
hass, trigger, _merge({"for": {"seconds": 5}}), valid=supports_duration
1151+
)
1152+
1153+
# Unknown option should always be rejected
1154+
await _validate_trigger_options(
1155+
hass, trigger, _merge({"unknown_option": True}), valid=False
1156+
)
1157+
1158+
10981159
async def assert_condition_behavior_any(
10991160
hass: HomeAssistant,
11001161
*,

0 commit comments

Comments
 (0)