From c93c6a66e865d68698dd9c6bd0bddd42038cb0bd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 24 Apr 2020 18:40:23 +0200 Subject: [PATCH] Add NOT condition helper (#34624) --- .../components/automation/__init__.py | 1 + homeassistant/helpers/condition.py | 26 +++++++ homeassistant/helpers/config_validation.py | 12 ++++ tests/helpers/test_condition.py | 67 +++++++++++++++++++ 4 files changed, 106 insertions(+) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 76fe619cc1c..34a83f3ed59 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -54,6 +54,7 @@ CONF_SKIP_CONDITION = "skip_condition" CONDITION_USE_TRIGGER_VALUES = "use_trigger_values" CONDITION_TYPE_AND = "and" +CONDITION_TYPE_NOT = "not" CONDITION_TYPE_OR = "or" DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 363d33b14ea..61704e2d23a 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -137,6 +137,32 @@ async def async_or_from_config( return if_or_condition +async def async_not_from_config( + hass: HomeAssistant, config: ConfigType, config_validation: bool = True +) -> ConditionCheckerType: + """Create multi condition matcher using 'NOT'.""" + if config_validation: + config = cv.NOT_CONDITION_SCHEMA(config) + checks = [ + await async_from_config(hass, entry, False) for entry in config["conditions"] + ] + + def if_not_condition( + hass: HomeAssistant, variables: TemplateVarsType = None + ) -> bool: + """Test not condition.""" + try: + for check in checks: + if check(hass, variables): + return False + except Exception as ex: # pylint: disable=broad-except + _LOGGER.warning("Error during not-condition: %s", ex) + + return True + + return if_not_condition + + def numeric_state( hass: HomeAssistant, entity: Union[None, str, State], diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 7bb3223f3d7..01f737b47da 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -924,6 +924,17 @@ OR_CONDITION_SCHEMA = vol.Schema( } ) +NOT_CONDITION_SCHEMA = vol.Schema( + { + vol.Required(CONF_CONDITION): "not", + vol.Required("conditions"): vol.All( + ensure_list, + # pylint: disable=unnecessary-lambda + [lambda value: CONDITION_SCHEMA(value)], + ), + } +) + DEVICE_CONDITION_BASE_SCHEMA = vol.Schema( { vol.Required(CONF_CONDITION): "device", @@ -945,6 +956,7 @@ CONDITION_SCHEMA: vol.Schema = key_value_schemas( "zone": ZONE_CONDITION_SCHEMA, "and": AND_CONDITION_SCHEMA, "or": OR_CONDITION_SCHEMA, + "not": NOT_CONDITION_SCHEMA, "device": DEVICE_CONDITION_SCHEMA, }, ) diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 4e8e59aad78..cadce628c10 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -127,6 +127,73 @@ async def test_or_condition_with_template(hass): assert test(hass) +async def test_not_condition(hass): + """Test the 'not' condition.""" + test = await condition.async_from_config( + hass, + { + "condition": "not", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.temperature", + "state": "100", + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": 50, + }, + ], + }, + ) + + hass.states.async_set("sensor.temperature", 101) + assert test(hass) + + hass.states.async_set("sensor.temperature", 50) + assert test(hass) + + hass.states.async_set("sensor.temperature", 49) + assert not test(hass) + + hass.states.async_set("sensor.temperature", 100) + assert not test(hass) + + +async def test_not_condition_with_template(hass): + """Test the 'or' condition.""" + test = await condition.async_from_config( + hass, + { + "condition": "not", + "conditions": [ + { + "condition": "template", + "value_template": '{{ states.sensor.temperature.state == "100" }}', + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": 50, + }, + ], + }, + ) + + hass.states.async_set("sensor.temperature", 101) + assert test(hass) + + hass.states.async_set("sensor.temperature", 50) + assert test(hass) + + hass.states.async_set("sensor.temperature", 49) + assert not test(hass) + + hass.states.async_set("sensor.temperature", 100) + assert not test(hass) + + async def test_time_window(hass): """Test time condition windows.""" sixam = dt.parse_time("06:00:00")