diff --git a/homeassistant/components/air_quality/conditions.yaml b/homeassistant/components/air_quality/conditions.yaml index d2589bb612a..97b7c1056da 100644 --- a/homeassistant/components/air_quality/conditions.yaml +++ b/homeassistant/components/air_quality/conditions.yaml @@ -10,366 +10,155 @@ - all - any -# --- Number or entity selectors --- +# --- Unit lists for multi-unit pollutants --- -.number_or_entity_co: &number_or_entity_co - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: - - "ppb" - - "ppm" - - "mg/m³" - - "μg/m³" - - domain: sensor - device_class: carbon_monoxide - - domain: number - device_class: carbon_monoxide - translation_key: number_or_entity +.co_units: &co_units + - "ppb" + - "ppm" + - "mg/m³" + - "μg/m³" -.number_or_entity_co2: &number_or_entity_co2 - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - unit_of_measurement: "ppm" - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: "ppm" - - domain: sensor - device_class: carbon_dioxide - - domain: number - device_class: carbon_dioxide - translation_key: number_or_entity +.ozone_units: &ozone_units + - "ppb" + - "ppm" + - "μg/m³" -.number_or_entity_pm1: &number_or_entity_pm1 - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - unit_of_measurement: "μg/m³" - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: "μg/m³" - - domain: sensor - device_class: pm1 - - domain: number - device_class: pm1 - translation_key: number_or_entity +.voc_units: &voc_units + - "μg/m³" + - "mg/m³" -.number_or_entity_pm25: &number_or_entity_pm25 - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - unit_of_measurement: "μg/m³" - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: "μg/m³" - - domain: sensor - device_class: pm25 - - domain: number - device_class: pm25 - translation_key: number_or_entity +.voc_ratio_units: &voc_ratio_units + - "ppb" + - "ppm" -.number_or_entity_pm4: &number_or_entity_pm4 - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - unit_of_measurement: "μg/m³" - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: "μg/m³" - - domain: sensor - device_class: pm4 - - domain: number - device_class: pm4 - translation_key: number_or_entity +.no_units: &no_units + - "ppb" + - "μg/m³" -.number_or_entity_pm10: &number_or_entity_pm10 - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - unit_of_measurement: "μg/m³" - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: "μg/m³" - - domain: sensor - device_class: pm10 - - domain: number - device_class: pm10 - translation_key: number_or_entity +.no2_units: &no2_units + - "ppb" + - "ppm" + - "μg/m³" -.number_or_entity_ozone: &number_or_entity_ozone - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: - - "ppb" - - "ppm" - - "μg/m³" - - domain: sensor - device_class: ozone - - domain: number - device_class: ozone - translation_key: number_or_entity +.so2_units: &so2_units + - "ppb" + - "μg/m³" -.number_or_entity_voc: &number_or_entity_voc - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: - - "μg/m³" - - "mg/m³" - - domain: sensor - device_class: volatile_organic_compounds - - domain: number - device_class: volatile_organic_compounds - translation_key: number_or_entity +# --- Entity filter anchors --- -.number_or_entity_voc_ratio: &number_or_entity_voc_ratio - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: - - "ppb" - - "ppm" - - domain: sensor - device_class: volatile_organic_compounds_parts - - domain: number - device_class: volatile_organic_compounds_parts - translation_key: number_or_entity +.co_threshold_entity: &co_threshold_entity + - domain: input_number + unit_of_measurement: *co_units + - domain: sensor + device_class: carbon_monoxide + - domain: number + device_class: carbon_monoxide -.number_or_entity_no: &number_or_entity_no - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: - - "ppb" - - "μg/m³" - - domain: sensor - device_class: nitrogen_monoxide - - domain: number - device_class: nitrogen_monoxide - translation_key: number_or_entity +.co2_threshold_entity: &co2_threshold_entity + - domain: input_number + unit_of_measurement: "ppm" + - domain: sensor + device_class: carbon_dioxide + - domain: number + device_class: carbon_dioxide -.number_or_entity_no2: &number_or_entity_no2 - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: - - "ppb" - - "ppm" - - "μg/m³" - - domain: sensor - device_class: nitrogen_dioxide - - domain: number - device_class: nitrogen_dioxide - translation_key: number_or_entity +.pm1_threshold_entity: &pm1_threshold_entity + - domain: input_number + unit_of_measurement: "μg/m³" + - domain: sensor + device_class: pm1 + - domain: number + device_class: pm1 -.number_or_entity_n2o: &number_or_entity_n2o - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - unit_of_measurement: "μg/m³" - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: "μg/m³" - - domain: sensor - device_class: nitrous_oxide - - domain: number - device_class: nitrous_oxide - translation_key: number_or_entity +.pm25_threshold_entity: &pm25_threshold_entity + - domain: input_number + unit_of_measurement: "μg/m³" + - domain: sensor + device_class: pm25 + - domain: number + device_class: pm25 -.number_or_entity_so2: &number_or_entity_so2 - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: - - "ppb" - - "μg/m³" - - domain: sensor - device_class: sulphur_dioxide - - domain: number - device_class: sulphur_dioxide - translation_key: number_or_entity +.pm4_threshold_entity: &pm4_threshold_entity + - domain: input_number + unit_of_measurement: "μg/m³" + - domain: sensor + device_class: pm4 + - domain: number + device_class: pm4 -# --- Unit selectors --- +.pm10_threshold_entity: &pm10_threshold_entity + - domain: input_number + unit_of_measurement: "μg/m³" + - domain: sensor + device_class: pm10 + - domain: number + device_class: pm10 -.unit_co: &unit_co - required: false - selector: - select: - options: - - "ppb" - - "ppm" - - "mg/m³" - - "μg/m³" +.ozone_threshold_entity: &ozone_threshold_entity + - domain: input_number + unit_of_measurement: *ozone_units + - domain: sensor + device_class: ozone + - domain: number + device_class: ozone -.unit_ozone: &unit_ozone - required: false - selector: - select: - options: - - "ppb" - - "ppm" - - "μg/m³" +.voc_threshold_entity: &voc_threshold_entity + - domain: input_number + unit_of_measurement: *voc_units + - domain: sensor + device_class: volatile_organic_compounds + - domain: number + device_class: volatile_organic_compounds -.unit_no2: &unit_no2 - required: false - selector: - select: - options: - - "ppb" - - "ppm" - - "μg/m³" +.voc_ratio_threshold_entity: &voc_ratio_threshold_entity + - domain: input_number + unit_of_measurement: *voc_ratio_units + - domain: sensor + device_class: volatile_organic_compounds_parts + - domain: number + device_class: volatile_organic_compounds_parts -.unit_no: &unit_no - required: false - selector: - select: - options: - - "ppb" - - "μg/m³" +.no_threshold_entity: &no_threshold_entity + - domain: input_number + unit_of_measurement: *no_units + - domain: sensor + device_class: nitrogen_monoxide + - domain: number + device_class: nitrogen_monoxide -.unit_so2: &unit_so2 - required: false - selector: - select: - options: - - "ppb" - - "μg/m³" +.no2_threshold_entity: &no2_threshold_entity + - domain: input_number + unit_of_measurement: *no2_units + - domain: sensor + device_class: nitrogen_dioxide + - domain: number + device_class: nitrogen_dioxide -.unit_voc: &unit_voc - required: false - selector: - select: - options: - - "μg/m³" - - "mg/m³" +.n2o_threshold_entity: &n2o_threshold_entity + - domain: input_number + unit_of_measurement: "μg/m³" + - domain: sensor + device_class: nitrous_oxide + - domain: number + device_class: nitrous_oxide -.unit_voc_ratio: &unit_voc_ratio - required: false - selector: - select: - options: - - "ppb" - - "ppm" +.so2_threshold_entity: &so2_threshold_entity + - domain: input_number + unit_of_measurement: *so2_units + - domain: sensor + device_class: sulphur_dioxide + - domain: number + device_class: sulphur_dioxide + +# --- Number anchors for single-unit pollutants --- + +.co2_threshold_number: &co2_threshold_number + mode: box + unit_of_measurement: "ppm" + +.ugm3_threshold_number: &ugm3_threshold_number + mode: box + unit_of_measurement: "μg/m³" # --- Binary sensor targets --- @@ -491,57 +280,99 @@ is_co_value: target: *target_co_sensor fields: behavior: *condition_behavior - above: *number_or_entity_co - below: *number_or_entity_co - unit: *unit_co + threshold: + required: true + selector: + numeric_threshold: + entity: *co_threshold_entity + mode: is + number: + mode: box + unit_of_measurement: *co_units is_ozone_value: target: *target_ozone fields: behavior: *condition_behavior - above: *number_or_entity_ozone - below: *number_or_entity_ozone - unit: *unit_ozone + threshold: + required: true + selector: + numeric_threshold: + entity: *ozone_threshold_entity + mode: is + number: + mode: box + unit_of_measurement: *ozone_units is_voc_value: target: *target_voc fields: behavior: *condition_behavior - above: *number_or_entity_voc - below: *number_or_entity_voc - unit: *unit_voc + threshold: + required: true + selector: + numeric_threshold: + entity: *voc_threshold_entity + mode: is + number: + mode: box + unit_of_measurement: *voc_units is_voc_ratio_value: target: *target_voc_ratio fields: behavior: *condition_behavior - above: *number_or_entity_voc_ratio - below: *number_or_entity_voc_ratio - unit: *unit_voc_ratio + threshold: + required: true + selector: + numeric_threshold: + entity: *voc_ratio_threshold_entity + mode: is + number: + mode: box + unit_of_measurement: *voc_ratio_units is_no_value: target: *target_no fields: behavior: *condition_behavior - above: *number_or_entity_no - below: *number_or_entity_no - unit: *unit_no + threshold: + required: true + selector: + numeric_threshold: + entity: *no_threshold_entity + mode: is + number: + mode: box + unit_of_measurement: *no_units is_no2_value: target: *target_no2 fields: behavior: *condition_behavior - above: *number_or_entity_no2 - below: *number_or_entity_no2 - unit: *unit_no2 + threshold: + required: true + selector: + numeric_threshold: + entity: *no2_threshold_entity + mode: is + number: + mode: box + unit_of_measurement: *no2_units is_so2_value: target: *target_so2 fields: behavior: *condition_behavior - above: *number_or_entity_so2 - below: *number_or_entity_so2 - unit: *unit_so2 + threshold: + required: true + selector: + numeric_threshold: + entity: *so2_threshold_entity + mode: is + number: + mode: box + unit_of_measurement: *so2_units # --- Numerical sensor conditions without unit conversion --- @@ -549,40 +380,70 @@ is_co2_value: target: *target_co2 fields: behavior: *condition_behavior - above: *number_or_entity_co2 - below: *number_or_entity_co2 + threshold: + required: true + selector: + numeric_threshold: + entity: *co2_threshold_entity + mode: is + number: *co2_threshold_number is_pm1_value: target: *target_pm1 fields: behavior: *condition_behavior - above: *number_or_entity_pm1 - below: *number_or_entity_pm1 + threshold: + required: true + selector: + numeric_threshold: + entity: *pm1_threshold_entity + mode: is + number: *ugm3_threshold_number is_pm25_value: target: *target_pm25 fields: behavior: *condition_behavior - above: *number_or_entity_pm25 - below: *number_or_entity_pm25 + threshold: + required: true + selector: + numeric_threshold: + entity: *pm25_threshold_entity + mode: is + number: *ugm3_threshold_number is_pm4_value: target: *target_pm4 fields: behavior: *condition_behavior - above: *number_or_entity_pm4 - below: *number_or_entity_pm4 + threshold: + required: true + selector: + numeric_threshold: + entity: *pm4_threshold_entity + mode: is + number: *ugm3_threshold_number is_pm10_value: target: *target_pm10 fields: behavior: *condition_behavior - above: *number_or_entity_pm10 - below: *number_or_entity_pm10 + threshold: + required: true + selector: + numeric_threshold: + entity: *pm10_threshold_entity + mode: is + number: *ugm3_threshold_number is_n2o_value: target: *target_n2o fields: behavior: *condition_behavior - above: *number_or_entity_n2o - below: *number_or_entity_n2o + threshold: + required: true + selector: + numeric_threshold: + entity: *n2o_threshold_entity + mode: is + number: *ugm3_threshold_number diff --git a/homeassistant/components/air_quality/strings.json b/homeassistant/components/air_quality/strings.json index 4a4d79e5b45..f3369398b34 100644 --- a/homeassistant/components/air_quality/strings.json +++ b/homeassistant/components/air_quality/strings.json @@ -1,13 +1,9 @@ { "common": { - "condition_above_description": "Require the value to be above this value.", - "condition_above_name": "Above", "condition_behavior_description": "How the value should match on the targeted entities.", "condition_behavior_name": "Behavior", - "condition_below_description": "Require the value to be below this value.", - "condition_below_name": "Below", - "condition_unit_description": "All values will be converted to this unit when evaluating the condition.", - "condition_unit_name": "Unit of measurement", + "condition_threshold_description": "What to test for and threshold values.", + "condition_threshold_name": "Threshold configuration", "trigger_behavior_description": "The behavior of the targeted entities to trigger on.", "trigger_behavior_name": "Behavior", "trigger_threshold_changed_description": "Which changes to trigger on and threshold values.", @@ -18,17 +14,13 @@ "is_co2_value": { "description": "Tests the carbon dioxide level of one or more entities.", "fields": { - "above": { - "description": "[%key:component::air_quality::common::condition_above_description%]", - "name": "[%key:component::air_quality::common::condition_above_name%]" - }, "behavior": { "description": "[%key:component::air_quality::common::condition_behavior_description%]", "name": "[%key:component::air_quality::common::condition_behavior_name%]" }, - "below": { - "description": "[%key:component::air_quality::common::condition_below_description%]", - "name": "[%key:component::air_quality::common::condition_below_name%]" + "threshold": { + "description": "[%key:component::air_quality::common::condition_threshold_description%]", + "name": "[%key:component::air_quality::common::condition_threshold_name%]" } }, "name": "Carbon dioxide value" @@ -56,21 +48,13 @@ "is_co_value": { "description": "Tests the carbon monoxide level of one or more entities.", "fields": { - "above": { - "description": "[%key:component::air_quality::common::condition_above_description%]", - "name": "[%key:component::air_quality::common::condition_above_name%]" - }, "behavior": { "description": "[%key:component::air_quality::common::condition_behavior_description%]", "name": "[%key:component::air_quality::common::condition_behavior_name%]" }, - "below": { - "description": "[%key:component::air_quality::common::condition_below_description%]", - "name": "[%key:component::air_quality::common::condition_below_name%]" - }, - "unit": { - "description": "[%key:component::air_quality::common::condition_unit_description%]", - "name": "[%key:component::air_quality::common::condition_unit_name%]" + "threshold": { + "description": "[%key:component::air_quality::common::condition_threshold_description%]", + "name": "[%key:component::air_quality::common::condition_threshold_name%]" } }, "name": "Carbon monoxide value" @@ -98,17 +82,13 @@ "is_n2o_value": { "description": "Tests the nitrous oxide level of one or more entities.", "fields": { - "above": { - "description": "[%key:component::air_quality::common::condition_above_description%]", - "name": "[%key:component::air_quality::common::condition_above_name%]" - }, "behavior": { "description": "[%key:component::air_quality::common::condition_behavior_description%]", "name": "[%key:component::air_quality::common::condition_behavior_name%]" }, - "below": { - "description": "[%key:component::air_quality::common::condition_below_description%]", - "name": "[%key:component::air_quality::common::condition_below_name%]" + "threshold": { + "description": "[%key:component::air_quality::common::condition_threshold_description%]", + "name": "[%key:component::air_quality::common::condition_threshold_name%]" } }, "name": "Nitrous oxide value" @@ -116,21 +96,13 @@ "is_no2_value": { "description": "Tests the nitrogen dioxide level of one or more entities.", "fields": { - "above": { - "description": "[%key:component::air_quality::common::condition_above_description%]", - "name": "[%key:component::air_quality::common::condition_above_name%]" - }, "behavior": { "description": "[%key:component::air_quality::common::condition_behavior_description%]", "name": "[%key:component::air_quality::common::condition_behavior_name%]" }, - "below": { - "description": "[%key:component::air_quality::common::condition_below_description%]", - "name": "[%key:component::air_quality::common::condition_below_name%]" - }, - "unit": { - "description": "[%key:component::air_quality::common::condition_unit_description%]", - "name": "[%key:component::air_quality::common::condition_unit_name%]" + "threshold": { + "description": "[%key:component::air_quality::common::condition_threshold_description%]", + "name": "[%key:component::air_quality::common::condition_threshold_name%]" } }, "name": "Nitrogen dioxide value" @@ -138,21 +110,13 @@ "is_no_value": { "description": "Tests the nitrogen monoxide level of one or more entities.", "fields": { - "above": { - "description": "[%key:component::air_quality::common::condition_above_description%]", - "name": "[%key:component::air_quality::common::condition_above_name%]" - }, "behavior": { "description": "[%key:component::air_quality::common::condition_behavior_description%]", "name": "[%key:component::air_quality::common::condition_behavior_name%]" }, - "below": { - "description": "[%key:component::air_quality::common::condition_below_description%]", - "name": "[%key:component::air_quality::common::condition_below_name%]" - }, - "unit": { - "description": "[%key:component::air_quality::common::condition_unit_description%]", - "name": "[%key:component::air_quality::common::condition_unit_name%]" + "threshold": { + "description": "[%key:component::air_quality::common::condition_threshold_description%]", + "name": "[%key:component::air_quality::common::condition_threshold_name%]" } }, "name": "Nitrogen monoxide value" @@ -160,21 +124,13 @@ "is_ozone_value": { "description": "Tests the ozone level of one or more entities.", "fields": { - "above": { - "description": "[%key:component::air_quality::common::condition_above_description%]", - "name": "[%key:component::air_quality::common::condition_above_name%]" - }, "behavior": { "description": "[%key:component::air_quality::common::condition_behavior_description%]", "name": "[%key:component::air_quality::common::condition_behavior_name%]" }, - "below": { - "description": "[%key:component::air_quality::common::condition_below_description%]", - "name": "[%key:component::air_quality::common::condition_below_name%]" - }, - "unit": { - "description": "[%key:component::air_quality::common::condition_unit_description%]", - "name": "[%key:component::air_quality::common::condition_unit_name%]" + "threshold": { + "description": "[%key:component::air_quality::common::condition_threshold_description%]", + "name": "[%key:component::air_quality::common::condition_threshold_name%]" } }, "name": "Ozone value" @@ -182,17 +138,13 @@ "is_pm10_value": { "description": "Tests the PM10 level of one or more entities.", "fields": { - "above": { - "description": "[%key:component::air_quality::common::condition_above_description%]", - "name": "[%key:component::air_quality::common::condition_above_name%]" - }, "behavior": { "description": "[%key:component::air_quality::common::condition_behavior_description%]", "name": "[%key:component::air_quality::common::condition_behavior_name%]" }, - "below": { - "description": "[%key:component::air_quality::common::condition_below_description%]", - "name": "[%key:component::air_quality::common::condition_below_name%]" + "threshold": { + "description": "[%key:component::air_quality::common::condition_threshold_description%]", + "name": "[%key:component::air_quality::common::condition_threshold_name%]" } }, "name": "PM10 value" @@ -200,17 +152,13 @@ "is_pm1_value": { "description": "Tests the PM1 level of one or more entities.", "fields": { - "above": { - "description": "[%key:component::air_quality::common::condition_above_description%]", - "name": "[%key:component::air_quality::common::condition_above_name%]" - }, "behavior": { "description": "[%key:component::air_quality::common::condition_behavior_description%]", "name": "[%key:component::air_quality::common::condition_behavior_name%]" }, - "below": { - "description": "[%key:component::air_quality::common::condition_below_description%]", - "name": "[%key:component::air_quality::common::condition_below_name%]" + "threshold": { + "description": "[%key:component::air_quality::common::condition_threshold_description%]", + "name": "[%key:component::air_quality::common::condition_threshold_name%]" } }, "name": "PM1 value" @@ -218,17 +166,13 @@ "is_pm25_value": { "description": "Tests the PM2.5 level of one or more entities.", "fields": { - "above": { - "description": "[%key:component::air_quality::common::condition_above_description%]", - "name": "[%key:component::air_quality::common::condition_above_name%]" - }, "behavior": { "description": "[%key:component::air_quality::common::condition_behavior_description%]", "name": "[%key:component::air_quality::common::condition_behavior_name%]" }, - "below": { - "description": "[%key:component::air_quality::common::condition_below_description%]", - "name": "[%key:component::air_quality::common::condition_below_name%]" + "threshold": { + "description": "[%key:component::air_quality::common::condition_threshold_description%]", + "name": "[%key:component::air_quality::common::condition_threshold_name%]" } }, "name": "PM2.5 value" @@ -236,17 +180,13 @@ "is_pm4_value": { "description": "Tests the PM4 level of one or more entities.", "fields": { - "above": { - "description": "[%key:component::air_quality::common::condition_above_description%]", - "name": "[%key:component::air_quality::common::condition_above_name%]" - }, "behavior": { "description": "[%key:component::air_quality::common::condition_behavior_description%]", "name": "[%key:component::air_quality::common::condition_behavior_name%]" }, - "below": { - "description": "[%key:component::air_quality::common::condition_below_description%]", - "name": "[%key:component::air_quality::common::condition_below_name%]" + "threshold": { + "description": "[%key:component::air_quality::common::condition_threshold_description%]", + "name": "[%key:component::air_quality::common::condition_threshold_name%]" } }, "name": "PM4 value" @@ -274,21 +214,13 @@ "is_so2_value": { "description": "Tests the sulphur dioxide level of one or more entities.", "fields": { - "above": { - "description": "[%key:component::air_quality::common::condition_above_description%]", - "name": "[%key:component::air_quality::common::condition_above_name%]" - }, "behavior": { "description": "[%key:component::air_quality::common::condition_behavior_description%]", "name": "[%key:component::air_quality::common::condition_behavior_name%]" }, - "below": { - "description": "[%key:component::air_quality::common::condition_below_description%]", - "name": "[%key:component::air_quality::common::condition_below_name%]" - }, - "unit": { - "description": "[%key:component::air_quality::common::condition_unit_description%]", - "name": "[%key:component::air_quality::common::condition_unit_name%]" + "threshold": { + "description": "[%key:component::air_quality::common::condition_threshold_description%]", + "name": "[%key:component::air_quality::common::condition_threshold_name%]" } }, "name": "Sulphur dioxide value" @@ -296,21 +228,13 @@ "is_voc_ratio_value": { "description": "Tests the volatile organic compounds ratio of one or more entities.", "fields": { - "above": { - "description": "[%key:component::air_quality::common::condition_above_description%]", - "name": "[%key:component::air_quality::common::condition_above_name%]" - }, "behavior": { "description": "[%key:component::air_quality::common::condition_behavior_description%]", "name": "[%key:component::air_quality::common::condition_behavior_name%]" }, - "below": { - "description": "[%key:component::air_quality::common::condition_below_description%]", - "name": "[%key:component::air_quality::common::condition_below_name%]" - }, - "unit": { - "description": "[%key:component::air_quality::common::condition_unit_description%]", - "name": "[%key:component::air_quality::common::condition_unit_name%]" + "threshold": { + "description": "[%key:component::air_quality::common::condition_threshold_description%]", + "name": "[%key:component::air_quality::common::condition_threshold_name%]" } }, "name": "Volatile organic compounds ratio value" @@ -318,21 +242,13 @@ "is_voc_value": { "description": "Tests the volatile organic compounds level of one or more entities.", "fields": { - "above": { - "description": "[%key:component::air_quality::common::condition_above_description%]", - "name": "[%key:component::air_quality::common::condition_above_name%]" - }, "behavior": { "description": "[%key:component::air_quality::common::condition_behavior_description%]", "name": "[%key:component::air_quality::common::condition_behavior_name%]" }, - "below": { - "description": "[%key:component::air_quality::common::condition_below_description%]", - "name": "[%key:component::air_quality::common::condition_below_name%]" - }, - "unit": { - "description": "[%key:component::air_quality::common::condition_unit_description%]", - "name": "[%key:component::air_quality::common::condition_unit_name%]" + "threshold": { + "description": "[%key:component::air_quality::common::condition_threshold_description%]", + "name": "[%key:component::air_quality::common::condition_threshold_name%]" } }, "name": "Volatile organic compounds value" @@ -345,12 +261,6 @@ "any": "Any" } }, - "number_or_entity": { - "choices": { - "entity": "Entity", - "number": "Number" - } - }, "trigger_behavior": { "options": { "any": "Any", diff --git a/homeassistant/components/battery/conditions.yaml b/homeassistant/components/battery/conditions.yaml index fa37c37e6a3..98584b00044 100644 --- a/homeassistant/components/battery/conditions.yaml +++ b/homeassistant/components/battery/conditions.yaml @@ -14,24 +14,19 @@ - all - any -.number_or_entity: &number_or_entity - required: false - selector: - choose: - choices: - number: - selector: - number: - unit_of_measurement: "%" - entity: - selector: - entity: - filter: - domain: - - input_number - - number - - sensor - translation_key: number_or_entity +.battery_threshold_entity: &battery_threshold_entity + - domain: input_number + unit_of_measurement: "%" + - domain: sensor + device_class: battery + - domain: number + device_class: battery + +.battery_threshold_number: &battery_threshold_number + min: 0 + max: 100 + mode: box + unit_of_measurement: "%" is_low: *condition_common @@ -62,5 +57,10 @@ is_level: device_class: battery fields: behavior: *condition_behavior - above: *number_or_entity - below: *number_or_entity + threshold: + required: true + selector: + numeric_threshold: + entity: *battery_threshold_entity + mode: is + number: *battery_threshold_number diff --git a/homeassistant/components/battery/strings.json b/homeassistant/components/battery/strings.json index 1b66656ce29..e0eec43b74e 100644 --- a/homeassistant/components/battery/strings.json +++ b/homeassistant/components/battery/strings.json @@ -1,7 +1,9 @@ { "common": { "condition_behavior_description": "How the state should match on the targeted batteries.", - "condition_behavior_name": "Behavior" + "condition_behavior_name": "Behavior", + "condition_threshold_description": "What to test for and threshold values.", + "condition_threshold_name": "Threshold configuration" }, "conditions": { "is_charging": { @@ -17,17 +19,13 @@ "is_level": { "description": "Tests the battery level of one or more batteries.", "fields": { - "above": { - "description": "Require the battery percentage to be above this value.", - "name": "Above" - }, "behavior": { "description": "[%key:component::battery::common::condition_behavior_description%]", "name": "[%key:component::battery::common::condition_behavior_name%]" }, - "below": { - "description": "Require the battery percentage to be below this value.", - "name": "Below" + "threshold": { + "description": "[%key:component::battery::common::condition_threshold_description%]", + "name": "[%key:component::battery::common::condition_threshold_name%]" } }, "name": "Battery level" @@ -69,12 +67,6 @@ "all": "All", "any": "Any" } - }, - "number_or_entity": { - "choices": { - "entity": "Entity", - "number": "Number" - } } }, "title": "Battery" diff --git a/homeassistant/components/climate/conditions.yaml b/homeassistant/components/climate/conditions.yaml index db40d7e444c..771d5e96332 100644 --- a/homeassistant/components/climate/conditions.yaml +++ b/homeassistant/components/climate/conditions.yaml @@ -13,58 +13,31 @@ - all - any -.number_or_entity_humidity: &number_or_entity_humidity - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - unit_of_measurement: "%" - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: "%" - - domain: sensor - device_class: humidity - - domain: number - device_class: humidity - translation_key: number_or_entity +.humidity_threshold_entity: &humidity_threshold_entity + - domain: input_number + unit_of_measurement: "%" + - domain: sensor + device_class: humidity + - domain: number + device_class: humidity -.number_or_entity_temperature: &number_or_entity_temperature - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: - - "°C" - - "°F" - - domain: sensor - device_class: temperature - - domain: number - device_class: temperature - translation_key: number_or_entity +.humidity_threshold_number: &humidity_threshold_number + min: 0 + max: 100 + mode: box + unit_of_measurement: "%" -.condition_unit_temperature: &condition_unit_temperature - required: false - selector: - select: - options: - - "°C" - - "°F" +.temperature_units: &temperature_units + - "°C" + - "°F" + +.temperature_threshold_entity: &temperature_threshold_entity + - domain: input_number + unit_of_measurement: *temperature_units + - domain: sensor + device_class: temperature + - domain: number + device_class: temperature is_off: *condition_common is_on: *condition_common @@ -76,13 +49,24 @@ target_humidity: target: *condition_climate_target fields: behavior: *condition_behavior - above: *number_or_entity_humidity - below: *number_or_entity_humidity + threshold: + required: true + selector: + numeric_threshold: + entity: *humidity_threshold_entity + mode: is + number: *humidity_threshold_number target_temperature: target: *condition_climate_target fields: behavior: *condition_behavior - above: *number_or_entity_temperature - below: *number_or_entity_temperature - unit: *condition_unit_temperature + threshold: + required: true + selector: + numeric_threshold: + entity: *temperature_threshold_entity + mode: is + number: + mode: box + unit_of_measurement: *temperature_units diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index 8f2e680c0eb..ec6c99e51ab 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -2,6 +2,8 @@ "common": { "condition_behavior_description": "How the state should match on the targeted climate-control devices.", "condition_behavior_name": "Behavior", + "condition_threshold_description": "What to test for and threshold values.", + "condition_threshold_name": "Threshold configuration", "trigger_behavior_description": "The behavior of the targeted climates to trigger on.", "trigger_behavior_name": "Behavior", "trigger_threshold_changed_description": "Which changes to trigger on and threshold values.", @@ -62,17 +64,13 @@ "target_humidity": { "description": "Tests the humidity setpoint of one or more climate-control devices.", "fields": { - "above": { - "description": "Require the target humidity to be above this value.", - "name": "Above" - }, "behavior": { "description": "[%key:component::climate::common::condition_behavior_description%]", "name": "[%key:component::climate::common::condition_behavior_name%]" }, - "below": { - "description": "Require the target humidity to be below this value.", - "name": "Below" + "threshold": { + "description": "[%key:component::climate::common::condition_threshold_description%]", + "name": "[%key:component::climate::common::condition_threshold_name%]" } }, "name": "Climate-control device target humidity" @@ -80,21 +78,13 @@ "target_temperature": { "description": "Tests the temperature setpoint of one or more climate-control devices.", "fields": { - "above": { - "description": "Require the target temperature to be above this value.", - "name": "Above" - }, "behavior": { "description": "[%key:component::climate::common::condition_behavior_description%]", "name": "[%key:component::climate::common::condition_behavior_name%]" }, - "below": { - "description": "Require the target temperature to be below this value.", - "name": "Below" - }, - "unit": { - "description": "All values will be converted to this unit when evaluating the condition.", - "name": "Unit of measurement" + "threshold": { + "description": "[%key:component::climate::common::condition_threshold_description%]", + "name": "[%key:component::climate::common::condition_threshold_name%]" } }, "name": "Climate-control device target temperature" @@ -284,12 +274,6 @@ "any": "Any" } }, - "number_or_entity": { - "choices": { - "entity": "Entity", - "number": "Number" - } - }, "trigger_behavior": { "options": { "any": "Any", diff --git a/homeassistant/components/humidifier/conditions.yaml b/homeassistant/components/humidifier/conditions.yaml index 6ed179e3caa..bc10ab1db65 100644 --- a/homeassistant/components/humidifier/conditions.yaml +++ b/homeassistant/components/humidifier/conditions.yaml @@ -13,27 +13,19 @@ - all - any -.number_or_entity: &number_or_entity - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - unit_of_measurement: "%" - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: "%" - - domain: sensor - device_class: humidity - - domain: number - device_class: humidity - translation_key: number_or_entity +.humidity_threshold_entity: &humidity_threshold_entity + - domain: input_number + unit_of_measurement: "%" + - domain: sensor + device_class: humidity + - domain: number + device_class: humidity + +.humidity_threshold_number: &humidity_threshold_number + min: 0 + max: 100 + mode: box + unit_of_measurement: "%" is_off: *condition_common is_on: *condition_common @@ -44,5 +36,10 @@ is_target_humidity: target: *condition_humidifier_target fields: behavior: *condition_behavior - above: *number_or_entity - below: *number_or_entity + threshold: + required: true + selector: + numeric_threshold: + entity: *humidity_threshold_entity + mode: is + number: *humidity_threshold_number diff --git a/homeassistant/components/humidifier/strings.json b/homeassistant/components/humidifier/strings.json index fc729062c4a..09b01ce14de 100644 --- a/homeassistant/components/humidifier/strings.json +++ b/homeassistant/components/humidifier/strings.json @@ -2,6 +2,8 @@ "common": { "condition_behavior_description": "How the state should match on the targeted humidifiers.", "condition_behavior_name": "Behavior", + "condition_threshold_description": "What to test for and threshold values.", + "condition_threshold_name": "Threshold configuration", "trigger_behavior_description": "The behavior of the targeted humidifiers to trigger on.", "trigger_behavior_name": "Behavior" }, @@ -49,17 +51,13 @@ "is_target_humidity": { "description": "Tests the target humidity of one or more humidifiers.", "fields": { - "above": { - "description": "Require the target humidity to be above this value.", - "name": "Above" - }, "behavior": { "description": "[%key:component::humidifier::common::condition_behavior_description%]", "name": "[%key:component::humidifier::common::condition_behavior_name%]" }, - "below": { - "description": "Require the target humidity to be below this value.", - "name": "Below" + "threshold": { + "description": "[%key:component::humidifier::common::condition_threshold_description%]", + "name": "[%key:component::humidifier::common::condition_threshold_name%]" } }, "name": "Humidifier target humidity" @@ -159,12 +157,6 @@ "any": "Any" } }, - "number_or_entity": { - "choices": { - "entity": "Entity", - "number": "Number" - } - }, "trigger_behavior": { "options": { "any": "Any", diff --git a/homeassistant/components/humidity/conditions.yaml b/homeassistant/components/humidity/conditions.yaml index 4fc6cd34963..733b2452891 100644 --- a/homeassistant/components/humidity/conditions.yaml +++ b/homeassistant/components/humidity/conditions.yaml @@ -1,24 +1,16 @@ -.number_or_entity: &number_or_entity - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - unit_of_measurement: "%" - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: "%" - - domain: number - device_class: humidity - - domain: sensor - device_class: humidity - translation_key: number_or_entity +.humidity_threshold_entity: &humidity_threshold_entity + - domain: input_number + unit_of_measurement: "%" + - domain: sensor + device_class: humidity + - domain: number + device_class: humidity + +.humidity_threshold_number: &humidity_threshold_number + min: 0 + max: 100 + mode: box + unit_of_measurement: "%" is_value: target: @@ -39,5 +31,10 @@ is_value: options: - all - any - above: *number_or_entity - below: *number_or_entity + threshold: + required: true + selector: + numeric_threshold: + entity: *humidity_threshold_entity + mode: is + number: *humidity_threshold_number diff --git a/homeassistant/components/humidity/strings.json b/homeassistant/components/humidity/strings.json index 9327bf89e18..06836f05dce 100644 --- a/homeassistant/components/humidity/strings.json +++ b/homeassistant/components/humidity/strings.json @@ -2,6 +2,8 @@ "common": { "condition_behavior_description": "How the state should match on the targeted entities.", "condition_behavior_name": "Behavior", + "condition_threshold_description": "What to test for and threshold values.", + "condition_threshold_name": "Threshold configuration", "trigger_behavior_description": "The behavior of the targeted entities to trigger on.", "trigger_behavior_name": "Behavior", "trigger_threshold_changed_description": "Which changes to trigger on and threshold values.", @@ -12,17 +14,13 @@ "is_value": { "description": "Tests if a relative humidity value is above a threshold, below a threshold, or in a range of values.", "fields": { - "above": { - "description": "Require the relative humidity to be above this value.", - "name": "Above" - }, "behavior": { "description": "[%key:component::humidity::common::condition_behavior_description%]", "name": "[%key:component::humidity::common::condition_behavior_name%]" }, - "below": { - "description": "Require the relative humidity to be below this value.", - "name": "Below" + "threshold": { + "description": "[%key:component::humidity::common::condition_threshold_description%]", + "name": "[%key:component::humidity::common::condition_threshold_name%]" } }, "name": "Relative humidity" @@ -35,12 +33,6 @@ "any": "Any" } }, - "number_or_entity": { - "choices": { - "entity": "Entity", - "number": "Number" - } - }, "trigger_behavior": { "options": { "any": "Any", diff --git a/homeassistant/components/illuminance/conditions.yaml b/homeassistant/components/illuminance/conditions.yaml index d2e07200c4b..37980efcae4 100644 --- a/homeassistant/components/illuminance/conditions.yaml +++ b/homeassistant/components/illuminance/conditions.yaml @@ -14,27 +14,6 @@ - all - any -.number_or_entity: &number_or_entity - required: false - selector: - choose: - choices: - number: - selector: - number: - unit_of_measurement: "lx" - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: "lx" - - domain: number - device_class: illuminance - - domain: sensor - device_class: illuminance - translation_key: number_or_entity - is_detected: *detected_condition_common is_not_detected: *detected_condition_common @@ -48,5 +27,19 @@ is_value: device_class: illuminance fields: behavior: *condition_behavior - above: *number_or_entity - below: *number_or_entity + threshold: + required: true + selector: + numeric_threshold: + entity: + - domain: input_number + unit_of_measurement: "lx" + - domain: sensor + device_class: illuminance + - domain: number + device_class: illuminance + mode: is + number: + min: 0 + mode: box + unit_of_measurement: "lx" diff --git a/homeassistant/components/illuminance/strings.json b/homeassistant/components/illuminance/strings.json index aa5090a5d35..5ed11170df0 100644 --- a/homeassistant/components/illuminance/strings.json +++ b/homeassistant/components/illuminance/strings.json @@ -2,6 +2,8 @@ "common": { "condition_behavior_description": "How the state should match on the targeted entities.", "condition_behavior_name": "Behavior", + "condition_threshold_description": "What to test for and threshold values.", + "condition_threshold_name": "Threshold configuration", "trigger_behavior_description": "The behavior of the targeted entities to trigger on.", "trigger_behavior_name": "Behavior", "trigger_threshold_changed_description": "Which changes to trigger on and threshold values.", @@ -32,17 +34,13 @@ "is_value": { "description": "Tests the illuminance value.", "fields": { - "above": { - "description": "Require the illuminance to be above this value.", - "name": "Above" - }, "behavior": { "description": "[%key:component::illuminance::common::condition_behavior_description%]", "name": "[%key:component::illuminance::common::condition_behavior_name%]" }, - "below": { - "description": "Require the illuminance to be below this value.", - "name": "Below" + "threshold": { + "description": "[%key:component::illuminance::common::condition_threshold_description%]", + "name": "[%key:component::illuminance::common::condition_threshold_name%]" } }, "name": "Illuminance" @@ -55,12 +53,6 @@ "any": "Any" } }, - "number_or_entity": { - "choices": { - "entity": "Entity", - "number": "Number" - } - }, "trigger_behavior": { "options": { "any": "Any", diff --git a/homeassistant/components/moisture/conditions.yaml b/homeassistant/components/moisture/conditions.yaml index 03818912730..a1e1f9b4bfd 100644 --- a/homeassistant/components/moisture/conditions.yaml +++ b/homeassistant/components/moisture/conditions.yaml @@ -14,26 +14,19 @@ - all - any -.number_or_entity: &number_or_entity - required: false - selector: - choose: - choices: - number: - selector: - number: - unit_of_measurement: "%" - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: "%" - - domain: number - device_class: moisture - - domain: sensor - device_class: moisture - translation_key: number_or_entity +.moisture_threshold_entity: &moisture_threshold_entity + - domain: input_number + unit_of_measurement: "%" + - domain: sensor + device_class: moisture + - domain: number + device_class: moisture + +.moisture_threshold_number: &moisture_threshold_number + min: 0 + max: 100 + mode: box + unit_of_measurement: "%" is_detected: *detected_condition_common @@ -48,5 +41,10 @@ is_value: device_class: moisture fields: behavior: *condition_behavior - above: *number_or_entity - below: *number_or_entity + threshold: + required: true + selector: + numeric_threshold: + entity: *moisture_threshold_entity + mode: is + number: *moisture_threshold_number diff --git a/homeassistant/components/moisture/strings.json b/homeassistant/components/moisture/strings.json index e4e33bbe061..c2f9705bcca 100644 --- a/homeassistant/components/moisture/strings.json +++ b/homeassistant/components/moisture/strings.json @@ -2,6 +2,8 @@ "common": { "condition_behavior_description": "How the state should match on the targeted entities.", "condition_behavior_name": "Behavior", + "condition_threshold_description": "What to test for and threshold values.", + "condition_threshold_name": "Threshold configuration", "trigger_behavior_description": "The behavior of the targeted entities to trigger on.", "trigger_behavior_name": "Behavior", "trigger_threshold_changed_description": "Which changes to trigger on and threshold values.", @@ -32,17 +34,13 @@ "is_value": { "description": "Tests the moisture level of one or more entities.", "fields": { - "above": { - "description": "Require the moisture level to be above this value.", - "name": "Above" - }, "behavior": { "description": "[%key:component::moisture::common::condition_behavior_description%]", "name": "[%key:component::moisture::common::condition_behavior_name%]" }, - "below": { - "description": "Require the moisture level to be below this value.", - "name": "Below" + "threshold": { + "description": "[%key:component::moisture::common::condition_threshold_description%]", + "name": "[%key:component::moisture::common::condition_threshold_name%]" } }, "name": "Moisture level" @@ -55,12 +53,6 @@ "any": "Any" } }, - "number_or_entity": { - "choices": { - "entity": "Entity", - "number": "Number" - } - }, "trigger_behavior": { "options": { "any": "Any", diff --git a/homeassistant/components/power/conditions.yaml b/homeassistant/components/power/conditions.yaml index c9a3498c186..a34beb6d24a 100644 --- a/homeassistant/components/power/conditions.yaml +++ b/homeassistant/components/power/conditions.yaml @@ -1,43 +1,29 @@ -.number_or_entity_power: &number_or_entity_power - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: - - "mW" - - "W" - - "kW" - - "MW" - - "GW" - - "TW" - - "BTU/h" - - domain: sensor - device_class: power - - domain: number - device_class: power - translation_key: number_or_entity - -.condition_unit_power: &condition_unit_power - required: false +.condition_behavior: &condition_behavior + required: true + default: any selector: select: + translation_key: condition_behavior options: - - "mW" - - "W" - - "kW" - - "MW" - - "GW" - - "TW" - - "BTU/h" + - all + - any + +.power_units: &power_units + - "mW" + - "W" + - "kW" + - "MW" + - "GW" + - "TW" + - "BTU/h" + +.power_threshold_entity: &power_threshold_entity + - domain: input_number + unit_of_measurement: *power_units + - domain: sensor + device_class: power + - domain: number + device_class: power is_value: target: @@ -47,15 +33,13 @@ is_value: - domain: sensor device_class: power fields: - behavior: + behavior: *condition_behavior + threshold: required: true - default: any selector: - select: - translation_key: condition_behavior - options: - - all - - any - above: *number_or_entity_power - below: *number_or_entity_power - unit: *condition_unit_power + numeric_threshold: + entity: *power_threshold_entity + mode: is + number: + mode: box + unit_of_measurement: *power_units diff --git a/homeassistant/components/power/strings.json b/homeassistant/components/power/strings.json index 3f7ba415b7f..f4369b0e225 100644 --- a/homeassistant/components/power/strings.json +++ b/homeassistant/components/power/strings.json @@ -2,6 +2,8 @@ "common": { "condition_behavior_description": "How the power value should match on the targeted entities.", "condition_behavior_name": "Behavior", + "condition_threshold_description": "What to test for and threshold values.", + "condition_threshold_name": "Threshold configuration", "trigger_behavior_description": "The behavior of the targeted entities to trigger on.", "trigger_behavior_name": "Behavior", "trigger_threshold_changed_description": "Which changes to trigger on and threshold values.", @@ -12,21 +14,13 @@ "is_value": { "description": "Tests the power value of one or more entities.", "fields": { - "above": { - "description": "Require the power to be above this value.", - "name": "Above" - }, "behavior": { "description": "[%key:component::power::common::condition_behavior_description%]", "name": "[%key:component::power::common::condition_behavior_name%]" }, - "below": { - "description": "Require the power to be below this value.", - "name": "Below" - }, - "unit": { - "description": "All values will be converted to this unit when evaluating the condition.", - "name": "Unit of measurement" + "threshold": { + "description": "[%key:component::power::common::condition_threshold_description%]", + "name": "[%key:component::power::common::condition_threshold_name%]" } }, "name": "Power value" @@ -39,12 +33,6 @@ "any": "Any" } }, - "number_or_entity": { - "choices": { - "entity": "Entity", - "number": "Number" - } - }, "trigger_behavior": { "options": { "any": "Any", diff --git a/homeassistant/components/temperature/conditions.yaml b/homeassistant/components/temperature/conditions.yaml index bb87a665924..a979b371e00 100644 --- a/homeassistant/components/temperature/conditions.yaml +++ b/homeassistant/components/temperature/conditions.yaml @@ -1,33 +1,14 @@ -.number_or_entity_temperature: &number_or_entity_temperature - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: - - "°C" - - "°F" - - domain: sensor - device_class: temperature - - domain: number - device_class: temperature - translation_key: number_or_entity +.temperature_units: &temperature_units + - "°C" + - "°F" -.condition_unit_temperature: &condition_unit_temperature - required: false - selector: - select: - options: - - "°C" - - "°F" +.temperature_threshold_entity: &temperature_threshold_entity + - domain: input_number + unit_of_measurement: *temperature_units + - domain: sensor + device_class: temperature + - domain: number + device_class: temperature is_value: target: @@ -47,6 +28,12 @@ is_value: options: - all - any - above: *number_or_entity_temperature - below: *number_or_entity_temperature - unit: *condition_unit_temperature + threshold: + required: true + selector: + numeric_threshold: + entity: *temperature_threshold_entity + mode: is + number: + mode: box + unit_of_measurement: *temperature_units diff --git a/homeassistant/components/temperature/strings.json b/homeassistant/components/temperature/strings.json index e20fd7b0c7d..e1c74365759 100644 --- a/homeassistant/components/temperature/strings.json +++ b/homeassistant/components/temperature/strings.json @@ -2,6 +2,8 @@ "common": { "condition_behavior_description": "How the temperature should match on the targeted entities.", "condition_behavior_name": "Behavior", + "condition_threshold_description": "What to test for and threshold values.", + "condition_threshold_name": "Threshold configuration", "trigger_behavior_description": "The behavior of the targeted entities to trigger on.", "trigger_behavior_name": "Behavior", "trigger_threshold_changed_description": "Which changes to trigger on and threshold values.", @@ -12,21 +14,13 @@ "is_value": { "description": "Tests the temperature of one or more entities.", "fields": { - "above": { - "description": "Require the temperature to be above this value.", - "name": "Above" - }, "behavior": { "description": "[%key:component::temperature::common::condition_behavior_description%]", "name": "[%key:component::temperature::common::condition_behavior_name%]" }, - "below": { - "description": "Require the temperature to be below this value.", - "name": "Below" - }, - "unit": { - "description": "All values will be converted to this unit when evaluating the condition.", - "name": "Unit of measurement" + "threshold": { + "description": "[%key:component::temperature::common::condition_threshold_description%]", + "name": "[%key:component::temperature::common::condition_threshold_name%]" } }, "name": "Temperature value" @@ -39,12 +33,6 @@ "any": "Any" } }, - "number_or_entity": { - "choices": { - "entity": "Entity", - "number": "Number" - } - }, "trigger_behavior": { "options": { "any": "Any", diff --git a/homeassistant/components/water_heater/conditions.yaml b/homeassistant/components/water_heater/conditions.yaml index 6ce7ec9747e..a200dfcf832 100644 --- a/homeassistant/components/water_heater/conditions.yaml +++ b/homeassistant/components/water_heater/conditions.yaml @@ -13,36 +13,17 @@ - all - any -.number_or_entity_temperature: &number_or_entity_temperature - required: false - selector: - choose: - choices: - number: - selector: - number: - mode: box - entity: - selector: - entity: - filter: - - domain: input_number - unit_of_measurement: - - "°C" - - "°F" - - domain: sensor - device_class: temperature - - domain: number - device_class: temperature - translation_key: number_or_entity +.temperature_units: &temperature_units + - "°C" + - "°F" -.condition_unit_temperature: &condition_unit_temperature - required: false - selector: - select: - options: - - "°C" - - "°F" +.temperature_threshold_entity: &temperature_threshold_entity + - domain: input_number + unit_of_measurement: *temperature_units + - domain: sensor + device_class: temperature + - domain: number + device_class: temperature is_off: *condition_common is_on: *condition_common @@ -67,6 +48,12 @@ is_target_temperature: target: *condition_water_heater_target fields: behavior: *condition_behavior - above: *number_or_entity_temperature - below: *number_or_entity_temperature - unit: *condition_unit_temperature + threshold: + required: true + selector: + numeric_threshold: + entity: *temperature_threshold_entity + mode: is + number: + mode: box + unit_of_measurement: *temperature_units diff --git a/homeassistant/components/water_heater/strings.json b/homeassistant/components/water_heater/strings.json index eaacea425f8..df8ce5a1297 100644 --- a/homeassistant/components/water_heater/strings.json +++ b/homeassistant/components/water_heater/strings.json @@ -2,6 +2,8 @@ "common": { "condition_behavior_description": "How the state should match on the targeted water heaters.", "condition_behavior_name": "Behavior", + "condition_threshold_description": "What to test for and threshold values.", + "condition_threshold_name": "Threshold configuration", "trigger_behavior_description": "The behavior of the targeted water heaters to trigger on.", "trigger_behavior_name": "Behavior", "trigger_threshold_changed_description": "Which changes to trigger on and threshold values.", @@ -46,21 +48,13 @@ "is_target_temperature": { "description": "Tests the temperature setpoint of one or more water heaters.", "fields": { - "above": { - "description": "Require the target temperature to be above this value.", - "name": "Above" - }, "behavior": { "description": "[%key:component::water_heater::common::condition_behavior_description%]", "name": "[%key:component::water_heater::common::condition_behavior_name%]" }, - "below": { - "description": "Require the target temperature to be below this value.", - "name": "Below" - }, - "unit": { - "description": "All values will be converted to this unit when evaluating the condition.", - "name": "Unit of measurement" + "threshold": { + "description": "[%key:component::water_heater::common::condition_threshold_description%]", + "name": "[%key:component::water_heater::common::condition_threshold_name%]" } }, "name": "Water heater target temperature" @@ -140,12 +134,6 @@ "any": "Any" } }, - "number_or_entity": { - "choices": { - "entity": "Entity", - "number": "Number" - } - }, "trigger_behavior": { "options": { "any": "Any", diff --git a/homeassistant/helpers/automation.py b/homeassistant/helpers/automation.py index 318e920fb1c..83f827ad75e 100644 --- a/homeassistant/helpers/automation.py +++ b/homeassistant/helpers/automation.py @@ -3,16 +3,15 @@ from collections.abc import Callable, Mapping from dataclasses import dataclass from enum import Enum -from typing import Any, Final +from typing import Any, Final, Self import voluptuous as vol from homeassistant.const import CONF_OPTIONS from homeassistant.core import HomeAssistant, split_entity_id -from . import config_validation as cv from .entity import get_device_class_or_undefined -from .typing import ConfigType +from .typing import UNDEFINED, ConfigType, UndefinedType CONF_UNIT: Final = "unit" @@ -145,41 +144,29 @@ def move_options_fields_to_top_level( return new_config -_NUMBER_OR_ENTITY_CHOOSE_SCHEMA = vol.Schema( - { - vol.Required("active_choice"): vol.In(["number", "entity"]), - vol.Optional("entity"): cv.entity_id, - vol.Optional("number"): vol.Coerce(float), - } -) +@dataclass(frozen=True, kw_only=True) +class ThresholdConfig: + """Configuration for threshold conditions and triggers.""" + numerical: bool + entity: str | None + number: float | None + unit: str | None | UndefinedType -def _validate_number_or_entity(value: dict | float | str) -> float | str: - """Validate number or entity selector result.""" - if isinstance(value, dict): - _NUMBER_OR_ENTITY_CHOOSE_SCHEMA(value) - return value[value["active_choice"]] # type: ignore[no-any-return] - return value + @classmethod + def from_config(cls, config: dict[str, Any] | None) -> Self | None: + """Create ThresholdConfig from config dict.""" + if config is None: + return None + entity: str | None = None + number: float | None = None + unit: str | None | UndefinedType = UNDEFINED + numerical = "number" in config + if numerical: + number = config["number"] + unit = config.get("unit_of_measurement", UNDEFINED) + else: + entity = config["entity"] -number_or_entity = vol.All( - _validate_number_or_entity, vol.Any(vol.Coerce(float), cv.entity_id) -) - - -def validate_unit_set_if_range_numerical[_T: dict[str, Any]]( - lower_limit: str, upper_limit: str -) -> Callable[[_T], _T]: - """Validate that unit is set if upper or lower limit is numerical.""" - - def _validate_unit_set_if_range_numerical_impl(options: _T) -> _T: - if ( - any( - opt in options and not isinstance(options[opt], str) - for opt in (lower_limit, upper_limit) - ) - ) and CONF_UNIT not in options: - raise vol.Invalid("Unit must be specified when using numerical thresholds.") - return options - - return _validate_unit_set_if_range_numerical_impl + return cls(numerical=numerical, number=number, entity=entity, unit=unit) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 0456629a0b1..967ddefe1b8 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -78,17 +78,21 @@ from homeassistant.util.yaml import load_yaml_dict from . import config_validation as cv, entity_registry as er, selector from .automation import ( - CONF_UNIT, DomainSpec, + ThresholdConfig, filter_by_domain_specs, get_absolute_description_key, get_relative_description_key, move_options_fields_to_top_level, - number_or_entity, - validate_unit_set_if_range_numerical, ) from .integration_platform import async_process_integration_platforms -from .selector import TargetSelector +from .selector import ( + NumericThresholdMode, + NumericThresholdSelector, + NumericThresholdSelectorConfig, + NumericThresholdType, + TargetSelector, +) from .target import TargetSelection, async_extract_referenced_entity_ids from .template import Template, render_complex from .trace import ( @@ -458,22 +462,6 @@ def make_entity_state_condition( return CustomCondition -def _validate_above_below(config: dict[str, Any]) -> dict[str, Any]: - """Validate that above < below when both are set.""" - above = config.get(CONF_ABOVE) - below = config.get(CONF_BELOW) - if above is None or below is None: - return config - if isinstance(above, str) or isinstance(below, str): - return config - if above >= below: - raise vol.Invalid( - f"A value can never be above {above} and below {below} at the same" - " time. You probably want two different conditions." - ) - return config - - NUMERICAL_CONDITION_SCHEMA = vol.Schema( { vol.Required(CONF_TARGET): cv.TARGET_FIELDS, @@ -482,11 +470,10 @@ NUMERICAL_CONDITION_SCHEMA = vol.Schema( vol.Required(ATTR_BEHAVIOR, default=BEHAVIOR_ANY): vol.In( [BEHAVIOR_ANY, BEHAVIOR_ALL] ), - vol.Optional(CONF_ABOVE): number_or_entity, - vol.Optional(CONF_BELOW): number_or_entity, + vol.Required("threshold"): NumericThresholdSelector( + NumericThresholdSelectorConfig(mode=NumericThresholdMode.IS) + ), }, - cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), - _validate_above_below, ), } ) @@ -503,8 +490,15 @@ class EntityNumericalConditionBase(EntityConditionBase): super().__init__(hass, config) if TYPE_CHECKING: assert config.options is not None - self._above: float | str | None = config.options.get(CONF_ABOVE) - self._below: float | str | None = config.options.get(CONF_BELOW) + threshold_options: dict[str, Any] = config.options["threshold"] + self.threshold = ThresholdConfig.from_config(threshold_options.get("value")) + self.lower_threshold = ThresholdConfig.from_config( + threshold_options.get("value_min") + ) + self.upper_threshold = ThresholdConfig.from_config( + threshold_options.get("value_max") + ) + self._threshold_type = threshold_options["type"] def _is_valid_unit(self, unit: str | None) -> bool: """Check if the given unit is valid for this condition.""" @@ -512,20 +506,26 @@ class EntityNumericalConditionBase(EntityConditionBase): return True return unit == self._valid_unit - def _get_numerical_value(self, entity_or_float: float | str) -> float | None: - """Get numerical value from float or entity state.""" - if isinstance(entity_or_float, str): - if not (ref_state := self._hass.states.get(entity_or_float)): - return None - if not self._is_valid_unit( - ref_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - ): - return None - try: - return float(ref_state.state) - except TypeError, ValueError: - return None - return entity_or_float + def _get_threshold_value(self, threshold: ThresholdConfig | None) -> float | None: + """Get threshold value from float or entity state.""" + if threshold is None: + return None + if threshold.numerical: + return threshold.number + + if not (entity_state := self._hass.states.get(threshold.entity)): # type: ignore[arg-type] + # Entity not found + return None + if not self._is_valid_unit( + entity_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + ): + # Entity unit does not match the expected unit + return None + try: + return float(entity_state.state) + except TypeError, ValueError: + # Entity state is not a valid number + return None def _get_tracked_value(self, entity_state: State) -> Any: """Get the tracked value from a state, with unit validation for state-based values.""" @@ -545,17 +545,27 @@ class EntityNumericalConditionBase(EntityConditionBase): except TypeError, ValueError: return False - if self._above is not None: - if (above := self._get_numerical_value(self._above)) is None: + if self._threshold_type == NumericThresholdType.ABOVE: + if (limit := self._get_threshold_value(self.threshold)) is None: + # Entity not found or invalid number, don't trigger return False - if value <= above: + return value > limit + if self._threshold_type == NumericThresholdType.BELOW: + if (limit := self._get_threshold_value(self.threshold)) is None: + # Entity not found or invalid number, don't trigger return False - if self._below is not None: - if (below := self._get_numerical_value(self._below)) is None: - return False - if value >= below: - return False - return True + return value < limit + + # Mode is BETWEEN or OUTSIDE + lower_limit = self._get_threshold_value(self.lower_threshold) + upper_limit = self._get_threshold_value(self.upper_threshold) + if lower_limit is None or upper_limit is None: + # Entity not found or invalid number, don't trigger + return False + between = lower_limit < value < upper_limit + if self._threshold_type == NumericThresholdType.BETWEEN: + return between + return not between def make_entity_numerical_condition( @@ -586,13 +596,13 @@ def _make_numerical_condition_with_unit_schema( vol.Required(ATTR_BEHAVIOR, default=BEHAVIOR_ANY): vol.In( [BEHAVIOR_ANY, BEHAVIOR_ALL] ), - vol.Optional(CONF_ABOVE): number_or_entity, - vol.Optional(CONF_BELOW): number_or_entity, - vol.Optional(CONF_UNIT): vol.In(unit_converter.VALID_UNITS), + vol.Required("threshold"): NumericThresholdSelector( + NumericThresholdSelectorConfig( + mode=NumericThresholdMode.IS, + unit_of_measurement=list(unit_converter.VALID_UNITS), + ) + ), }, - cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), - _validate_above_below, - validate_unit_set_if_range_numerical(CONF_ABOVE, CONF_BELOW), ), } ) @@ -602,16 +612,8 @@ class EntityNumericalConditionWithUnitBase(EntityNumericalConditionBase): """Condition for numerical state comparisons with unit conversion.""" _base_unit: str | None # Base unit for the tracked value - _manual_limit_unit: str | None # Unit of above/below limits when numbers _unit_converter: type[BaseUnitConverter] - def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None: - """Initialize the numerical condition with unit conversion.""" - super().__init__(hass, config) - if TYPE_CHECKING: - assert config.options is not None - self._manual_limit_unit = config.options.get(CONF_UNIT) - def __init_subclass__(cls, **kwargs: Any) -> None: """Create a schema.""" super().__init_subclass__(**kwargs) @@ -621,25 +623,34 @@ class EntityNumericalConditionWithUnitBase(EntityNumericalConditionBase): """Get the unit of an entity from its state.""" return entity_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - def _get_numerical_value(self, entity_or_float: float | str) -> float | None: - """Get numerical value from float or entity state.""" - if isinstance(entity_or_float, (int, float)): + def _get_threshold_value(self, threshold: ThresholdConfig | None) -> float | None: + """Get threshold value from float or entity state.""" + if threshold is None: + return None + if threshold.numerical: return self._unit_converter.convert( - entity_or_float, self._manual_limit_unit, self._base_unit + threshold.number, # type: ignore[arg-type] + threshold.unit, # type: ignore[arg-type] + self._base_unit, ) - if not (_state := self._hass.states.get(entity_or_float)): + if not (entity_state := self._hass.states.get(threshold.entity)): # type: ignore[arg-type] + # Entity not found return None try: - value = float(_state.state) + value = float(entity_state.state) except TypeError, ValueError: + # Entity state is not a valid number return None try: return self._unit_converter.convert( - value, _state.attributes.get(ATTR_UNIT_OF_MEASUREMENT), self._base_unit + value, + entity_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT), + self._base_unit, ) except HomeAssistantError: + # Unit conversion failed (i.e. incompatible units), treat as invalid number return None def _get_tracked_value(self, entity_state: State) -> Any: @@ -663,23 +674,6 @@ class EntityNumericalConditionWithUnitBase(EntityNumericalConditionBase): except HomeAssistantError: return None - def is_valid_state(self, entity_state: State) -> bool: - """Check if the state is within the specified range.""" - if (value := self._get_tracked_value(entity_state)) is None: - return False - - if self._above is not None: - if (above := self._get_numerical_value(self._above)) is None: - return False - if value <= above: - return False - if self._below is not None: - if (below := self._get_numerical_value(self._below)) is None: - return False - if value >= below: - return False - return True - def make_entity_numerical_condition_with_unit( domain_specs: Mapping[str, DomainSpec], diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index 25a0c35b8cb..fefbc416cb1 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -16,7 +16,6 @@ from typing import ( Final, Literal, Protocol, - Self, TypedDict, cast, override, @@ -70,6 +69,7 @@ from . import config_validation as cv, selector from .automation import ( DomainSpec, NumericalDomainSpec, + ThresholdConfig, filter_by_domain_specs, get_absolute_description_key, get_relative_description_key, @@ -535,34 +535,6 @@ NUMERICAL_ATTRIBUTE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA.extend( ) -@dataclass(frozen=True, kw_only=True) -class ThresholdConfig: - """Configuration for threshold triggers.""" - - numerical: bool - entity: str | None - number: float | None - unit: str | None | UndefinedType - - @classmethod - def from_config(cls, config: dict[str, Any] | None) -> Self | None: - """Create ThresholdConfig from config dict.""" - if config is None: - return None - - entity: str | None = None - number: float | None = None - unit: str | None | UndefinedType = UNDEFINED - numerical = "number" in config - if numerical: - number = config["number"] - unit = config.get("unit_of_measurement", UNDEFINED) - else: - entity = config["entity"] - - return cls(numerical=numerical, number=number, entity=entity, unit=unit) - - class EntityNumericalStateTriggerBase(EntityTriggerBase[NumericalDomainSpec]): """Base class for numerical state and state attribute triggers.""" diff --git a/tests/components/air_quality/test_condition.py b/tests/components/air_quality/test_condition.py index 1ba70a346c5..61eda516827 100644 --- a/tests/components/air_quality/test_condition.py +++ b/tests/components/air_quality/test_condition.py @@ -11,8 +11,6 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, - CONF_ABOVE, - CONF_BELOW, STATE_OFF, STATE_ON, ) @@ -32,11 +30,9 @@ from tests.components.common import ( target_entities, ) -_UGM3_CONDITION_OPTIONS = {"unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER} _UGM3_UNIT_ATTRIBUTES = { ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER } -_PPB_CONDITION_OPTIONS = {"unit": CONCENTRATION_PARTS_PER_BILLION} _PPB_UNIT_ATTRIBUTES = {ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION} _PPM_UNIT_ATTRIBUTES = {ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION} @@ -241,43 +237,43 @@ async def test_air_quality_binary_condition_behavior_all( *parametrize_numerical_condition_above_below_any( "air_quality.is_co_value", device_class="carbon_monoxide", - condition_options=_UGM3_CONDITION_OPTIONS, + threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, unit_attributes=_UGM3_UNIT_ATTRIBUTES, ), *parametrize_numerical_condition_above_below_any( "air_quality.is_ozone_value", device_class="ozone", - condition_options=_UGM3_CONDITION_OPTIONS, + threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, unit_attributes=_UGM3_UNIT_ATTRIBUTES, ), *parametrize_numerical_condition_above_below_any( "air_quality.is_voc_value", device_class="volatile_organic_compounds", - condition_options=_UGM3_CONDITION_OPTIONS, + threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, unit_attributes=_UGM3_UNIT_ATTRIBUTES, ), *parametrize_numerical_condition_above_below_any( "air_quality.is_voc_ratio_value", device_class="volatile_organic_compounds_parts", - condition_options=_PPB_CONDITION_OPTIONS, + threshold_unit=CONCENTRATION_PARTS_PER_BILLION, unit_attributes=_PPB_UNIT_ATTRIBUTES, ), *parametrize_numerical_condition_above_below_any( "air_quality.is_no_value", device_class="nitrogen_monoxide", - condition_options=_UGM3_CONDITION_OPTIONS, + threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, unit_attributes=_UGM3_UNIT_ATTRIBUTES, ), *parametrize_numerical_condition_above_below_any( "air_quality.is_no2_value", device_class="nitrogen_dioxide", - condition_options=_UGM3_CONDITION_OPTIONS, + threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, unit_attributes=_UGM3_UNIT_ATTRIBUTES, ), *parametrize_numerical_condition_above_below_any( "air_quality.is_so2_value", device_class="sulphur_dioxide", - condition_options=_UGM3_CONDITION_OPTIONS, + threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, unit_attributes=_UGM3_UNIT_ATTRIBUTES, ), ], @@ -316,43 +312,43 @@ async def test_air_quality_numerical_with_unit_condition_behavior_any( *parametrize_numerical_condition_above_below_all( "air_quality.is_co_value", device_class="carbon_monoxide", - condition_options=_UGM3_CONDITION_OPTIONS, + threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, unit_attributes=_UGM3_UNIT_ATTRIBUTES, ), *parametrize_numerical_condition_above_below_all( "air_quality.is_ozone_value", device_class="ozone", - condition_options=_UGM3_CONDITION_OPTIONS, + threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, unit_attributes=_UGM3_UNIT_ATTRIBUTES, ), *parametrize_numerical_condition_above_below_all( "air_quality.is_voc_value", device_class="volatile_organic_compounds", - condition_options=_UGM3_CONDITION_OPTIONS, + threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, unit_attributes=_UGM3_UNIT_ATTRIBUTES, ), *parametrize_numerical_condition_above_below_all( "air_quality.is_voc_ratio_value", device_class="volatile_organic_compounds_parts", - condition_options=_PPB_CONDITION_OPTIONS, + threshold_unit=CONCENTRATION_PARTS_PER_BILLION, unit_attributes=_PPB_UNIT_ATTRIBUTES, ), *parametrize_numerical_condition_above_below_all( "air_quality.is_no_value", device_class="nitrogen_monoxide", - condition_options=_UGM3_CONDITION_OPTIONS, + threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, unit_attributes=_UGM3_UNIT_ATTRIBUTES, ), *parametrize_numerical_condition_above_below_all( "air_quality.is_no2_value", device_class="nitrogen_dioxide", - condition_options=_UGM3_CONDITION_OPTIONS, + threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, unit_attributes=_UGM3_UNIT_ATTRIBUTES, ), *parametrize_numerical_condition_above_below_all( "air_quality.is_so2_value", device_class="sulphur_dioxide", - condition_options=_UGM3_CONDITION_OPTIONS, + threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, unit_attributes=_UGM3_UNIT_ATTRIBUTES, ), ], @@ -539,19 +535,38 @@ async def test_air_quality_condition_unit_conversion_co( ], numerical_condition_options=[ { - CONF_ABOVE: 0.2, - CONF_BELOW: 0.8, - "unit": CONCENTRATION_PARTS_PER_MILLION, + "threshold": { + "type": "between", + "value_min": { + "number": 0.2, + "unit_of_measurement": CONCENTRATION_PARTS_PER_MILLION, + }, + "value_max": { + "number": 0.8, + "unit_of_measurement": CONCENTRATION_PARTS_PER_MILLION, + }, + } }, { - CONF_ABOVE: 200, - CONF_BELOW: 800, - "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + "threshold": { + "type": "between", + "value_min": { + "number": 200, + "unit_of_measurement": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + }, + "value_max": { + "number": 800, + "unit_of_measurement": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + }, + } }, ], limit_entity_condition_options={ - CONF_ABOVE: "sensor.above", - CONF_BELOW: "sensor.below", + "threshold": { + "type": "between", + "value_min": {"entity": "sensor.above"}, + "value_max": {"entity": "sensor.below"}, + } }, limit_entities=("sensor.above", "sensor.below"), limit_entity_states=[ diff --git a/tests/components/climate/test_condition.py b/tests/components/climate/test_condition.py index 40f8407dfe5..2d1305b4850 100644 --- a/tests/components/climate/test_condition.py +++ b/tests/components/climate/test_condition.py @@ -13,8 +13,6 @@ from homeassistant.components.climate.const import ( from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, - CONF_ABOVE, - CONF_BELOW, UnitOfTemperature, ) from homeassistant.core import HomeAssistant @@ -34,8 +32,6 @@ from tests.components.common import ( target_entities, ) -_TEMPERATURE_CONDITION_OPTIONS = {"unit": UnitOfTemperature.CELSIUS} - @pytest.fixture async def target_climates(hass: HomeAssistant) -> dict[str, list[str]]: @@ -275,7 +271,7 @@ async def test_climate_attribute_condition_behavior_all( "climate.target_temperature", HVACMode.AUTO, ATTR_TEMPERATURE, - condition_options=_TEMPERATURE_CONDITION_OPTIONS, + threshold_unit=UnitOfTemperature.CELSIUS, ), ], ) @@ -319,7 +315,7 @@ async def test_climate_numerical_condition_behavior_any( "climate.target_temperature", HVACMode.AUTO, ATTR_TEMPERATURE, - condition_options=_TEMPERATURE_CONDITION_OPTIONS, + threshold_unit=UnitOfTemperature.CELSIUS, ), ], ) @@ -365,12 +361,39 @@ async def test_climate_numerical_condition_unit_conversion(hass: HomeAssistant) } ], numerical_condition_options=[ - {CONF_ABOVE: 75, CONF_BELOW: 90, "unit": UnitOfTemperature.FAHRENHEIT}, - {CONF_ABOVE: 24, CONF_BELOW: 30, "unit": UnitOfTemperature.CELSIUS}, + { + "threshold": { + "type": "between", + "value_min": { + "number": 75, + "unit_of_measurement": UnitOfTemperature.FAHRENHEIT, + }, + "value_max": { + "number": 90, + "unit_of_measurement": UnitOfTemperature.FAHRENHEIT, + }, + } + }, + { + "threshold": { + "type": "between", + "value_min": { + "number": 24, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + }, + "value_max": { + "number": 30, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + }, + } + }, ], limit_entity_condition_options={ - CONF_ABOVE: "sensor.above", - CONF_BELOW: "sensor.below", + "threshold": { + "type": "between", + "value_min": {"entity": "sensor.above"}, + "value_max": {"entity": "sensor.below"}, + } }, limit_entities=("sensor.above", "sensor.below"), limit_entity_states=[ diff --git a/tests/components/common.py b/tests/components/common.py index f3b0a0a0b7b..8611cd30333 100644 --- a/tests/components/common.py +++ b/tests/components/common.py @@ -14,8 +14,6 @@ from homeassistant.const import ( ATTR_FLOOR_ID, ATTR_LABEL_ID, ATTR_UNIT_OF_MEASUREMENT, - CONF_ABOVE, - CONF_BELOW, CONF_CONDITION, CONF_ENTITY_ID, CONF_OPTIONS, @@ -1300,6 +1298,7 @@ def parametrize_numerical_condition_above_below_any( *, device_class: str, condition_options: dict[str, Any] | None = None, + threshold_unit: str | None | UndefinedType = UNDEFINED, unit_attributes: dict | None = None, ) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]: """Parametrize above/below threshold test cases for numerical conditions. @@ -1315,7 +1314,13 @@ def parametrize_numerical_condition_above_below_any( return [ *parametrize_condition_states_any( condition=condition, - condition_options={CONF_ABOVE: 20, **condition_options}, + condition_options=_add_threshold_unit( + { + "threshold": {"type": "above", "value": {"number": 20}}, + **condition_options, + }, + threshold_unit, + ), target_states=[ ("21", unit_attributes), ("50", unit_attributes), @@ -1330,7 +1335,13 @@ def parametrize_numerical_condition_above_below_any( ), *parametrize_condition_states_any( condition=condition, - condition_options={CONF_BELOW: 80, **condition_options}, + condition_options=_add_threshold_unit( + { + "threshold": {"type": "below", "value": {"number": 80}}, + **condition_options, + }, + threshold_unit, + ), target_states=[ ("0", unit_attributes), ("50", unit_attributes), @@ -1345,7 +1356,17 @@ def parametrize_numerical_condition_above_below_any( ), *parametrize_condition_states_any( condition=condition, - condition_options={CONF_ABOVE: 20, CONF_BELOW: 80, **condition_options}, + condition_options=_add_threshold_unit( + { + "threshold": { + "type": "between", + "value_min": {"number": 20}, + "value_max": {"number": 80}, + }, + **condition_options, + }, + threshold_unit, + ), target_states=[ ("21", unit_attributes), ("50", unit_attributes), @@ -1367,6 +1388,7 @@ def parametrize_numerical_condition_above_below_all( *, device_class: str, condition_options: dict[str, Any] | None = None, + threshold_unit: str | None | UndefinedType = UNDEFINED, unit_attributes: dict | None = None, ) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]: """Parametrize above/below threshold test cases for numerical conditions with 'all' behavior. @@ -1382,7 +1404,13 @@ def parametrize_numerical_condition_above_below_all( return [ *parametrize_condition_states_all( condition=condition, - condition_options={CONF_ABOVE: 20, **condition_options}, + condition_options=_add_threshold_unit( + { + "threshold": {"type": "above", "value": {"number": 20}}, + **condition_options, + }, + threshold_unit, + ), target_states=[ ("21", unit_attributes), ("50", unit_attributes), @@ -1397,7 +1425,13 @@ def parametrize_numerical_condition_above_below_all( ), *parametrize_condition_states_all( condition=condition, - condition_options={CONF_BELOW: 80, **condition_options}, + condition_options=_add_threshold_unit( + { + "threshold": {"type": "below", "value": {"number": 80}}, + **condition_options, + }, + threshold_unit, + ), target_states=[ ("0", unit_attributes), ("50", unit_attributes), @@ -1412,7 +1446,17 @@ def parametrize_numerical_condition_above_below_all( ), *parametrize_condition_states_all( condition=condition, - condition_options={CONF_ABOVE: 20, CONF_BELOW: 80, **condition_options}, + condition_options=_add_threshold_unit( + { + "threshold": { + "type": "between", + "value_min": {"number": 20}, + "value_max": {"number": 80}, + }, + **condition_options, + }, + threshold_unit, + ), target_states=[ ("21", unit_attributes), ("50", unit_attributes), @@ -1436,6 +1480,7 @@ def parametrize_numerical_attribute_condition_above_below_any( *, condition_options: dict[str, Any] | None = None, required_filter_attributes: dict | None = None, + threshold_unit: str | None | UndefinedType = UNDEFINED, unit_attributes: dict | None = None, ) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]: """Parametrize above/below threshold test cases for attribute-based numerical conditions. @@ -1448,7 +1493,13 @@ def parametrize_numerical_attribute_condition_above_below_any( return [ *parametrize_condition_states_any( condition=condition, - condition_options={CONF_ABOVE: 20, **condition_options}, + condition_options=_add_threshold_unit( + { + "threshold": {"type": "above", "value": {"number": 20}}, + **condition_options, + }, + threshold_unit, + ), target_states=[ (state, {attribute: 21} | unit_attributes), (state, {attribute: 50} | unit_attributes), @@ -1463,7 +1514,13 @@ def parametrize_numerical_attribute_condition_above_below_any( ), *parametrize_condition_states_any( condition=condition, - condition_options={CONF_BELOW: 80, **condition_options}, + condition_options=_add_threshold_unit( + { + "threshold": {"type": "below", "value": {"number": 80}}, + **condition_options, + }, + threshold_unit, + ), target_states=[ (state, {attribute: 0} | unit_attributes), (state, {attribute: 50} | unit_attributes), @@ -1478,7 +1535,17 @@ def parametrize_numerical_attribute_condition_above_below_any( ), *parametrize_condition_states_any( condition=condition, - condition_options={CONF_ABOVE: 20, CONF_BELOW: 80, **condition_options}, + condition_options=_add_threshold_unit( + { + "threshold": { + "type": "between", + "value_min": {"number": 20}, + "value_max": {"number": 80}, + }, + **condition_options, + }, + threshold_unit, + ), target_states=[ (state, {attribute: 21} | unit_attributes), (state, {attribute: 50} | unit_attributes), @@ -1502,6 +1569,7 @@ def parametrize_numerical_attribute_condition_above_below_all( *, condition_options: dict[str, Any] | None = None, required_filter_attributes: dict | None = None, + threshold_unit: str | None | UndefinedType = UNDEFINED, unit_attributes: dict | None = None, ) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]: """Parametrize above/below threshold test cases for attribute-based numerical conditions with 'all' behavior. @@ -1514,7 +1582,13 @@ def parametrize_numerical_attribute_condition_above_below_all( return [ *parametrize_condition_states_all( condition=condition, - condition_options={CONF_ABOVE: 20, **condition_options}, + condition_options=_add_threshold_unit( + { + "threshold": {"type": "above", "value": {"number": 20}}, + **condition_options, + }, + threshold_unit, + ), target_states=[ (state, {attribute: 21} | unit_attributes), (state, {attribute: 50} | unit_attributes), @@ -1529,7 +1603,13 @@ def parametrize_numerical_attribute_condition_above_below_all( ), *parametrize_condition_states_all( condition=condition, - condition_options={CONF_BELOW: 80, **condition_options}, + condition_options=_add_threshold_unit( + { + "threshold": {"type": "below", "value": {"number": 80}}, + **condition_options, + }, + threshold_unit, + ), target_states=[ (state, {attribute: 0} | unit_attributes), (state, {attribute: 50} | unit_attributes), @@ -1544,7 +1624,17 @@ def parametrize_numerical_attribute_condition_above_below_all( ), *parametrize_condition_states_all( condition=condition, - condition_options={CONF_ABOVE: 20, CONF_BELOW: 80, **condition_options}, + condition_options=_add_threshold_unit( + { + "threshold": { + "type": "between", + "value_min": {"number": 20}, + "value_max": {"number": 80}, + }, + **condition_options, + }, + threshold_unit, + ), target_states=[ (state, {attribute: 21} | unit_attributes), (state, {attribute: 50} | unit_attributes), diff --git a/tests/components/power/test_condition.py b/tests/components/power/test_condition.py index 6fb9a76a975..b469d7ac2b6 100644 --- a/tests/components/power/test_condition.py +++ b/tests/components/power/test_condition.py @@ -4,12 +4,7 @@ from typing import Any import pytest -from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, - CONF_ABOVE, - CONF_BELOW, - UnitOfPower, -) +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfPower from homeassistant.core import HomeAssistant from tests.components.common import ( @@ -24,8 +19,6 @@ from tests.components.common import ( target_entities, ) -_POWER_CONDITION_OPTIONS = {"unit": UnitOfPower.WATT} - @pytest.fixture async def target_sensors(hass: HomeAssistant) -> dict[str, list[str]]: @@ -60,7 +53,7 @@ async def test_power_conditions_gated_by_labs_flag( parametrize_numerical_condition_above_below_any( "power.is_value", device_class="power", - condition_options=_POWER_CONDITION_OPTIONS, + threshold_unit=UnitOfPower.WATT, unit_attributes={ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.WATT}, ), ) @@ -97,7 +90,7 @@ async def test_power_sensor_condition_behavior_any( parametrize_numerical_condition_above_below_all( "power.is_value", device_class="power", - condition_options=_POWER_CONDITION_OPTIONS, + threshold_unit=UnitOfPower.WATT, unit_attributes={ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.WATT}, ), ) @@ -134,7 +127,7 @@ async def test_power_sensor_condition_behavior_all( parametrize_numerical_condition_above_below_any( "power.is_value", device_class="power", - condition_options=_POWER_CONDITION_OPTIONS, + threshold_unit=UnitOfPower.WATT, unit_attributes={ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.WATT}, ), ) @@ -171,7 +164,7 @@ async def test_power_number_condition_behavior_any( parametrize_numerical_condition_above_below_all( "power.is_value", device_class="power", - condition_options=_POWER_CONDITION_OPTIONS, + threshold_unit=UnitOfPower.WATT, unit_attributes={ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.WATT}, ), ) @@ -230,12 +223,39 @@ async def test_power_condition_unit_conversion_sensor( } ], numerical_condition_options=[ - {CONF_ABOVE: 0.2, CONF_BELOW: 0.8, "unit": UnitOfPower.KILO_WATT}, - {CONF_ABOVE: 200, CONF_BELOW: 800, "unit": UnitOfPower.WATT}, + { + "threshold": { + "type": "between", + "value_min": { + "number": 0.2, + "unit_of_measurement": UnitOfPower.KILO_WATT, + }, + "value_max": { + "number": 0.8, + "unit_of_measurement": UnitOfPower.KILO_WATT, + }, + } + }, + { + "threshold": { + "type": "between", + "value_min": { + "number": 200, + "unit_of_measurement": UnitOfPower.WATT, + }, + "value_max": { + "number": 800, + "unit_of_measurement": UnitOfPower.WATT, + }, + } + }, ], limit_entity_condition_options={ - CONF_ABOVE: "sensor.above", - CONF_BELOW: "sensor.below", + "threshold": { + "type": "between", + "value_min": {"entity": "sensor.above"}, + "value_max": {"entity": "sensor.below"}, + } }, limit_entities=("sensor.above", "sensor.below"), limit_entity_states=[ diff --git a/tests/components/temperature/test_condition.py b/tests/components/temperature/test_condition.py index 9faafd73a83..96199ea2c88 100644 --- a/tests/components/temperature/test_condition.py +++ b/tests/components/temperature/test_condition.py @@ -6,12 +6,7 @@ import pytest from homeassistant.components.climate import HVACMode from homeassistant.components.weather import ATTR_WEATHER_TEMPERATURE_UNIT -from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, - CONF_ABOVE, - CONF_BELOW, - UnitOfTemperature, -) +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfTemperature from homeassistant.core import HomeAssistant from tests.components.common import ( @@ -28,7 +23,6 @@ from tests.components.common import ( target_entities, ) -_TEMPERATURE_CONDITION_OPTIONS = {"unit": UnitOfTemperature.CELSIUS} _WEATHER_UNIT_ATTRIBUTES = {ATTR_WEATHER_TEMPERATURE_UNIT: UnitOfTemperature.CELSIUS} @@ -77,7 +71,7 @@ async def test_temperature_conditions_gated_by_labs_flag( parametrize_numerical_condition_above_below_any( "temperature.is_value", device_class="temperature", - condition_options=_TEMPERATURE_CONDITION_OPTIONS, + threshold_unit=UnitOfTemperature.CELSIUS, unit_attributes={ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ), ) @@ -114,7 +108,7 @@ async def test_temperature_sensor_condition_behavior_any( parametrize_numerical_condition_above_below_all( "temperature.is_value", device_class="temperature", - condition_options=_TEMPERATURE_CONDITION_OPTIONS, + threshold_unit=UnitOfTemperature.CELSIUS, unit_attributes={ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ), ) @@ -152,7 +146,7 @@ async def test_temperature_sensor_condition_behavior_all( "temperature.is_value", HVACMode.AUTO, "current_temperature", - condition_options=_TEMPERATURE_CONDITION_OPTIONS, + threshold_unit=UnitOfTemperature.CELSIUS, ), ) async def test_temperature_climate_condition_behavior_any( @@ -189,7 +183,7 @@ async def test_temperature_climate_condition_behavior_any( "temperature.is_value", HVACMode.AUTO, "current_temperature", - condition_options=_TEMPERATURE_CONDITION_OPTIONS, + threshold_unit=UnitOfTemperature.CELSIUS, ), ) async def test_temperature_climate_condition_behavior_all( @@ -226,7 +220,7 @@ async def test_temperature_climate_condition_behavior_all( "temperature.is_value", "eco", "current_temperature", - condition_options=_TEMPERATURE_CONDITION_OPTIONS, + threshold_unit=UnitOfTemperature.CELSIUS, ), ) async def test_temperature_water_heater_condition_behavior_any( @@ -263,7 +257,7 @@ async def test_temperature_water_heater_condition_behavior_any( "temperature.is_value", "eco", "current_temperature", - condition_options=_TEMPERATURE_CONDITION_OPTIONS, + threshold_unit=UnitOfTemperature.CELSIUS, ), ) async def test_temperature_water_heater_condition_behavior_all( @@ -300,7 +294,7 @@ async def test_temperature_water_heater_condition_behavior_all( "temperature.is_value", "sunny", "temperature", - condition_options=_TEMPERATURE_CONDITION_OPTIONS, + threshold_unit=UnitOfTemperature.CELSIUS, unit_attributes=_WEATHER_UNIT_ATTRIBUTES, ), ) @@ -338,7 +332,7 @@ async def test_temperature_weather_condition_behavior_any( "temperature.is_value", "sunny", "temperature", - condition_options=_TEMPERATURE_CONDITION_OPTIONS, + threshold_unit=UnitOfTemperature.CELSIUS, unit_attributes=_WEATHER_UNIT_ATTRIBUTES, ), ) @@ -397,12 +391,39 @@ async def test_temperature_condition_unit_conversion_sensor( } ], numerical_condition_options=[ - {CONF_ABOVE: 75, CONF_BELOW: 90, "unit": UnitOfTemperature.FAHRENHEIT}, - {CONF_ABOVE: 24, CONF_BELOW: 30, "unit": UnitOfTemperature.CELSIUS}, + { + "threshold": { + "type": "between", + "value_min": { + "number": 75, + "unit_of_measurement": UnitOfTemperature.FAHRENHEIT, + }, + "value_max": { + "number": 90, + "unit_of_measurement": UnitOfTemperature.FAHRENHEIT, + }, + } + }, + { + "threshold": { + "type": "between", + "value_min": { + "number": 24, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + }, + "value_max": { + "number": 30, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + }, + } + }, ], limit_entity_condition_options={ - CONF_ABOVE: "sensor.above", - CONF_BELOW: "sensor.below", + "threshold": { + "type": "between", + "value_min": {"entity": "sensor.above"}, + "value_max": {"entity": "sensor.below"}, + } }, limit_entities=("sensor.above", "sensor.below"), limit_entity_states=[ @@ -448,12 +469,39 @@ async def test_temperature_condition_unit_conversion_climate( {"state": HVACMode.AUTO, "attributes": {"current_temperature": 20}} ], numerical_condition_options=[ - {CONF_ABOVE: 75, CONF_BELOW: 90, "unit": UnitOfTemperature.FAHRENHEIT}, - {CONF_ABOVE: 24, CONF_BELOW: 30, "unit": UnitOfTemperature.CELSIUS}, + { + "threshold": { + "type": "between", + "value_min": { + "number": 75, + "unit_of_measurement": UnitOfTemperature.FAHRENHEIT, + }, + "value_max": { + "number": 90, + "unit_of_measurement": UnitOfTemperature.FAHRENHEIT, + }, + } + }, + { + "threshold": { + "type": "between", + "value_min": { + "number": 24, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + }, + "value_max": { + "number": 30, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + }, + } + }, ], limit_entity_condition_options={ - CONF_ABOVE: "sensor.above", - CONF_BELOW: "sensor.below", + "threshold": { + "type": "between", + "value_min": {"entity": "sensor.above"}, + "value_max": {"entity": "sensor.below"}, + } }, limit_entities=("sensor.above", "sensor.below"), limit_entity_states=[ diff --git a/tests/components/water_heater/test_condition.py b/tests/components/water_heater/test_condition.py index 7c54c039eb3..f4965ec70b3 100644 --- a/tests/components/water_heater/test_condition.py +++ b/tests/components/water_heater/test_condition.py @@ -15,8 +15,6 @@ from homeassistant.components.water_heater import ( from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, - CONF_ABOVE, - CONF_BELOW, STATE_OFF, STATE_ON, UnitOfTemperature, @@ -37,8 +35,6 @@ from tests.components.common import ( target_entities, ) -_TEMPERATURE_CONDITION_OPTIONS = {"unit": UnitOfTemperature.CELSIUS} - _ALL_STATES = [ STATE_ECO, STATE_ELECTRIC, @@ -205,7 +201,7 @@ async def test_water_heater_state_condition_behavior_all( "water_heater.is_target_temperature", "eco", ATTR_TEMPERATURE, - condition_options=_TEMPERATURE_CONDITION_OPTIONS, + threshold_unit=UnitOfTemperature.CELSIUS, ), ], ) @@ -244,7 +240,7 @@ async def test_water_heater_numerical_condition_behavior_any( "water_heater.is_target_temperature", "eco", ATTR_TEMPERATURE, - condition_options=_TEMPERATURE_CONDITION_OPTIONS, + threshold_unit=UnitOfTemperature.CELSIUS, ), ], ) @@ -292,12 +288,39 @@ async def test_water_heater_numerical_condition_unit_conversion( } ], numerical_condition_options=[ - {CONF_ABOVE: 120, CONF_BELOW: 140, "unit": UnitOfTemperature.FAHRENHEIT}, - {CONF_ABOVE: 49, CONF_BELOW: 60, "unit": UnitOfTemperature.CELSIUS}, + { + "threshold": { + "type": "between", + "value_min": { + "number": 120, + "unit_of_measurement": UnitOfTemperature.FAHRENHEIT, + }, + "value_max": { + "number": 140, + "unit_of_measurement": UnitOfTemperature.FAHRENHEIT, + }, + } + }, + { + "threshold": { + "type": "between", + "value_min": { + "number": 49, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + }, + "value_max": { + "number": 60, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + }, + } + }, ], limit_entity_condition_options={ - CONF_ABOVE: "sensor.above", - CONF_BELOW: "sensor.below", + "threshold": { + "type": "between", + "value_min": {"entity": "sensor.above"}, + "value_max": {"entity": "sensor.below"}, + } }, limit_entities=("sensor.above", "sensor.below"), limit_entity_states=[ diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 081a2b4b70b..f454d51c48f 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -1,6 +1,7 @@ """Test the condition helper.""" from collections.abc import Mapping +from contextlib import AbstractContextManager, nullcontext as does_not_raise from datetime import timedelta import io from typing import Any @@ -21,8 +22,6 @@ from homeassistant.components.system_health import DOMAIN as SYSTEM_HEALTH_DOMAI from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, - CONF_ABOVE, - CONF_BELOW, CONF_CONDITION, CONF_DEVICE_ID, CONF_DOMAIN, @@ -50,7 +49,6 @@ from homeassistant.helpers.condition import ( ATTR_BEHAVIOR, BEHAVIOR_ALL, BEHAVIOR_ANY, - CONF_UNIT, Condition, ConditionChecker, EntityNumericalConditionWithUnitBase, @@ -3059,19 +3057,69 @@ async def _setup_numerical_condition( ("condition_options", "state_value", "expected"), [ # above only - ({CONF_ABOVE: 50}, "75", True), - ({CONF_ABOVE: 50}, "50", False), - ({CONF_ABOVE: 50}, "25", False), + ({"threshold": {"type": "above", "value": {"number": 50}}}, "75", True), + ({"threshold": {"type": "above", "value": {"number": 50}}}, "50", False), + ({"threshold": {"type": "above", "value": {"number": 50}}}, "25", False), # below only - ({CONF_BELOW: 50}, "25", True), - ({CONF_BELOW: 50}, "50", False), - ({CONF_BELOW: 50}, "75", False), + ({"threshold": {"type": "below", "value": {"number": 50}}}, "25", True), + ({"threshold": {"type": "below", "value": {"number": 50}}}, "50", False), + ({"threshold": {"type": "below", "value": {"number": 50}}}, "75", False), # above and below (range) - ({CONF_ABOVE: 20, CONF_BELOW: 80}, "50", True), - ({CONF_ABOVE: 20, CONF_BELOW: 80}, "20", False), - ({CONF_ABOVE: 20, CONF_BELOW: 80}, "80", False), - ({CONF_ABOVE: 20, CONF_BELOW: 80}, "10", False), - ({CONF_ABOVE: 20, CONF_BELOW: 80}, "90", False), + ( + { + "threshold": { + "type": "between", + "value_min": {"number": 20}, + "value_max": {"number": 80}, + } + }, + "50", + True, + ), + ( + { + "threshold": { + "type": "between", + "value_min": {"number": 20}, + "value_max": {"number": 80}, + } + }, + "20", + False, + ), + ( + { + "threshold": { + "type": "between", + "value_min": {"number": 20}, + "value_max": {"number": 80}, + } + }, + "80", + False, + ), + ( + { + "threshold": { + "type": "between", + "value_min": {"number": 20}, + "value_max": {"number": 80}, + } + }, + "10", + False, + ), + ( + { + "threshold": { + "type": "between", + "value_min": {"number": 20}, + "value_max": {"number": 80}, + } + }, + "90", + False, + ), ], ) async def test_numerical_condition_thresholds( @@ -3101,7 +3149,7 @@ async def test_numerical_condition_invalid_state( """Test numerical condition with non-numeric or unavailable state values.""" test = await _setup_numerical_condition( hass, - condition_options={CONF_ABOVE: 50}, + condition_options={"threshold": {"type": "above", "value": {"number": 50}}}, entity_ids="test.entity_1", ) @@ -3116,7 +3164,7 @@ async def test_numerical_condition_attribute_value_source( test = await _setup_numerical_condition( hass, domain_specs={"test": DomainSpec(value_source="brightness")}, - condition_options={CONF_ABOVE: 100}, + condition_options={"threshold": {"type": "above", "value": {"number": 100}}}, entity_ids="test.entity_1", ) @@ -3145,7 +3193,7 @@ async def test_numerical_condition_attribute_value_source_skips_unit_check( test = await _setup_numerical_condition( hass, domain_specs={"test": DomainSpec(value_source="humidity")}, - condition_options={CONF_ABOVE: 50}, + condition_options={"threshold": {"type": "above", "value": {"number": 50}}}, entity_ids="test.entity_1", valid_unit="%", ) @@ -3184,7 +3232,7 @@ async def test_numerical_condition_valid_unit( """Test numerical condition valid_unit filtering.""" test = await _setup_numerical_condition( hass, - condition_options={CONF_ABOVE: 50}, + condition_options={"threshold": {"type": "above", "value": {"number": 50}}}, entity_ids="test.entity_1", valid_unit=valid_unit, ) @@ -3209,7 +3257,10 @@ async def test_numerical_condition_behavior( """Test numerical condition with behavior any/all.""" test = await _setup_numerical_condition( hass, - condition_options={CONF_ABOVE: 50, ATTR_BEHAVIOR: behavior}, + condition_options={ + "threshold": {"type": "above", "value": {"number": 50}}, + ATTR_BEHAVIOR: behavior, + }, entity_ids=["test.entity_1", "test.entity_2"], ) @@ -3253,16 +3304,17 @@ async def test_numerical_condition_schema_requires_above_or_below( @pytest.mark.parametrize( - ("above", "below"), + ("above", "below", "expected_result"), [ - (10.0, 10.0), - (20.0, 10.0), + (10.0, 10.0, does_not_raise()), + (20.0, 10.0, pytest.raises(vol.Invalid, match="must not be greater")), ], ) async def test_numerical_condition_schema_above_must_be_less_than_below( hass: HomeAssistant, above: float, below: float, + expected_result: AbstractContextManager, ) -> None: """Test numerical condition schema rejects above >= below.""" condition_cls = make_entity_numerical_condition({"test": DomainSpec()}) @@ -3280,9 +3332,15 @@ async def test_numerical_condition_schema_above_must_be_less_than_below( config: dict[str, Any] = { CONF_CONDITION: "test", CONF_TARGET: {CONF_ENTITY_ID: "test.entity_1"}, - CONF_OPTIONS: {CONF_ABOVE: above, CONF_BELOW: below}, + CONF_OPTIONS: { + "threshold": { + "type": "between", + "value_min": {"number": above}, + "value_max": {"number": below}, + } + }, } - with pytest.raises(vol.Invalid, match="can never be above"): + with expected_result: await async_validate_condition_config(hass, config) @@ -3329,42 +3387,102 @@ async def _setup_numerical_condition_with_unit( [ # above in °F, state in °C (base unit) # 75°F ≈ 23.89°C, so 25°C > 23.89°C → True - ({CONF_ABOVE: 75, CONF_UNIT: UnitOfTemperature.FAHRENHEIT}, "25", True), + ( + { + "threshold": { + "type": "above", + "value": {"number": 75, "unit_of_measurement": "°F"}, + } + }, + "25", + True, + ), # 75°F ≈ 23.89°C, so 20°C < 23.89°C → False - ({CONF_ABOVE: 75, CONF_UNIT: UnitOfTemperature.FAHRENHEIT}, "20", False), + ( + { + "threshold": { + "type": "above", + "value": {"number": 75, "unit_of_measurement": "°F"}, + } + }, + "20", + False, + ), # below in °F, state in °C # 70°F ≈ 21.11°C, so 20°C < 21.11°C → True - ({CONF_BELOW: 70, CONF_UNIT: UnitOfTemperature.FAHRENHEIT}, "20", True), + ( + { + "threshold": { + "type": "below", + "value": {"number": 70, "unit_of_measurement": "°F"}, + } + }, + "20", + True, + ), # 70°F ≈ 21.11°C, so 25°C > 21.11°C → False - ({CONF_BELOW: 70, CONF_UNIT: UnitOfTemperature.FAHRENHEIT}, "25", False), + ( + { + "threshold": { + "type": "below", + "value": {"number": 70, "unit_of_measurement": "°F"}, + } + }, + "25", + False, + ), # above in °C (same as base), state in °C - ({CONF_ABOVE: 20, CONF_UNIT: UnitOfTemperature.CELSIUS}, "25", True), - ({CONF_ABOVE: 20, CONF_UNIT: UnitOfTemperature.CELSIUS}, "15", False), + ( + { + "threshold": { + "type": "above", + "value": {"number": 20, "unit_of_measurement": "°C"}, + } + }, + "25", + True, + ), + ( + { + "threshold": { + "type": "above", + "value": {"number": 20, "unit_of_measurement": "°C"}, + } + }, + "15", + False, + ), # range with unit conversion # 60°F ≈ 15.56°C, 80°F ≈ 26.67°C ( { - CONF_ABOVE: 60, - CONF_BELOW: 80, - CONF_UNIT: UnitOfTemperature.FAHRENHEIT, + "threshold": { + "type": "between", + "value_min": {"number": 60, "unit_of_measurement": "°F"}, + "value_max": {"number": 80, "unit_of_measurement": "°F"}, + } }, "20", True, ), ( { - CONF_ABOVE: 60, - CONF_BELOW: 80, - CONF_UNIT: UnitOfTemperature.FAHRENHEIT, + "threshold": { + "type": "between", + "value_min": {"number": 60, "unit_of_measurement": "°F"}, + "value_max": {"number": 80, "unit_of_measurement": "°F"}, + } }, "10", False, ), ( { - CONF_ABOVE: 60, - CONF_BELOW: 80, - CONF_UNIT: UnitOfTemperature.FAHRENHEIT, + "threshold": { + "type": "between", + "value_min": {"number": 60, "unit_of_measurement": "°F"}, + "value_max": {"number": 80, "unit_of_measurement": "°F"}, + } }, "30", False, @@ -3399,8 +3517,7 @@ async def test_numerical_condition_with_unit_entity_reference( test = await _setup_numerical_condition_with_unit( hass, condition_options={ - CONF_ABOVE: "sensor.temp_limit", - CONF_UNIT: UnitOfTemperature.CELSIUS, + "threshold": {"type": "above", "value": {"entity": "sensor.temp_limit"}}, }, entity_ids="test.entity_1", ) @@ -3435,8 +3552,7 @@ async def test_numerical_condition_with_unit_entity_reference_incompatible_unit( test = await _setup_numerical_condition_with_unit( hass, condition_options={ - CONF_ABOVE: "sensor.bad_limit", - CONF_UNIT: UnitOfTemperature.CELSIUS, + "threshold": {"type": "above", "value": {"entity": "sensor.bad_limit"}}, }, entity_ids="test.entity_1", ) @@ -3462,8 +3578,10 @@ async def test_numerical_condition_with_unit_tracked_value_conversion( test = await _setup_numerical_condition_with_unit( hass, condition_options={ - CONF_ABOVE: 20, - CONF_UNIT: UnitOfTemperature.CELSIUS, + "threshold": { + "type": "above", + "value": {"number": 20, "unit_of_measurement": "°C"}, + } }, entity_ids="test.entity_1", ) @@ -3495,8 +3613,10 @@ async def test_numerical_condition_with_unit_attribute_value_source( "test": NumericalDomainSpec(value_source="temperature"), }, condition_options={ - CONF_ABOVE: 75, - CONF_UNIT: UnitOfTemperature.FAHRENHEIT, + "threshold": { + "type": "above", + "value": {"number": 75, "unit_of_measurement": "°F"}, + }, }, entity_ids="test.entity_1", ) @@ -3557,8 +3677,10 @@ async def test_numerical_condition_with_unit_get_entity_unit_override( CONF_CONDITION: "test", CONF_TARGET: {CONF_ENTITY_ID: ["test.entity_1"]}, CONF_OPTIONS: { - CONF_ABOVE: 20, - CONF_UNIT: UnitOfTemperature.CELSIUS, + "threshold": { + "type": "above", + "value": {"number": 20, "unit_of_measurement": "°C"}, + } }, } config = await async_validate_condition_config(hass, config) @@ -3598,8 +3720,10 @@ async def test_numerical_condition_with_unit_schema_accepts_valid_units( CONF_CONDITION: "test", CONF_TARGET: {CONF_ENTITY_ID: "test.entity_1"}, CONF_OPTIONS: { - CONF_ABOVE: 20, - CONF_UNIT: UnitOfTemperature.FAHRENHEIT, + "threshold": { + "type": "above", + "value": {"number": 20, "unit_of_measurement": "°F"}, + } }, } result = await async_validate_condition_config(hass, config) @@ -3629,8 +3753,10 @@ async def test_numerical_condition_with_unit_schema_rejects_invalid_units( CONF_CONDITION: "test", CONF_TARGET: {CONF_ENTITY_ID: "test.entity_1"}, CONF_OPTIONS: { - CONF_ABOVE: 20, - CONF_UNIT: "%", + "threshold": { + "type": "above", + "value": {"number": 20, "unit_of_measurement": "%"}, + } }, } with pytest.raises(vol.Invalid): @@ -3648,8 +3774,10 @@ async def test_numerical_condition_with_unit_invalid_state( test = await _setup_numerical_condition_with_unit( hass, condition_options={ - CONF_ABOVE: 50, - CONF_UNIT: UnitOfTemperature.CELSIUS, + "threshold": { + "type": "above", + "value": {"number": 50, "unit_of_measurement": "°C"}, + }, }, entity_ids="test.entity_1", ) @@ -3669,8 +3797,7 @@ async def test_numerical_condition_with_unit_missing_entity_reference( test = await _setup_numerical_condition_with_unit( hass, condition_options={ - CONF_ABOVE: "sensor.nonexistent", - CONF_UNIT: UnitOfTemperature.CELSIUS, + "threshold": {"type": "above", "value": {"entity": "sensor.nonexistent"}} }, entity_ids="test.entity_1", ) @@ -3699,9 +3826,11 @@ async def test_numerical_condition_with_unit_behavior( test = await _setup_numerical_condition_with_unit( hass, condition_options={ - CONF_ABOVE: 50, ATTR_BEHAVIOR: behavior, - CONF_UNIT: UnitOfTemperature.CELSIUS, + "threshold": { + "type": "above", + "value": {"number": 50, "unit_of_measurement": "°C"}, + }, }, entity_ids=["test.entity_1", "test.entity_2"], )