mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +00:00
Add device automation condition (#26313)
* Add support for device conditions * Lint * Update test case * Make and+or conditions async, adjust tests * Cleanup tests * Remove non callback versions of conditions, correct typing * Correct typing * Update light/strings.json * Address review comments * Make device automation lists simple lists, not dicts * Add device_automation/const.py * Use IS_ON/IS_OFF everywhere for conditions
This commit is contained in:
parent
c50faaef3c
commit
f7dc537275
@ -386,7 +386,7 @@ async def _async_process_config(hass, config, component):
|
|||||||
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}), name)
|
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}), name)
|
||||||
|
|
||||||
if CONF_CONDITION in config_block:
|
if CONF_CONDITION in config_block:
|
||||||
cond_func = _async_process_if(hass, config, config_block)
|
cond_func = await _async_process_if(hass, config, config_block)
|
||||||
|
|
||||||
if cond_func is None:
|
if cond_func is None:
|
||||||
continue
|
continue
|
||||||
@ -437,14 +437,14 @@ def _async_get_action(hass, config, name):
|
|||||||
return action
|
return action
|
||||||
|
|
||||||
|
|
||||||
def _async_process_if(hass, config, p_config):
|
async def _async_process_if(hass, config, p_config):
|
||||||
"""Process if checks."""
|
"""Process if checks."""
|
||||||
if_configs = p_config.get(CONF_CONDITION)
|
if_configs = p_config.get(CONF_CONDITION)
|
||||||
|
|
||||||
checks = []
|
checks = []
|
||||||
for if_config in if_configs:
|
for if_config in if_configs:
|
||||||
try:
|
try:
|
||||||
checks.append(condition.async_from_config(if_config, False))
|
checks.append(await condition.async_from_config(hass, if_config, False))
|
||||||
except HomeAssistantError as ex:
|
except HomeAssistantError as ex:
|
||||||
_LOGGER.warning("Invalid condition: %s", ex)
|
_LOGGER.warning("Invalid condition: %s", ex)
|
||||||
return None
|
return None
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
"""Helpers for device automations."""
|
"""Helpers for device automations."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Callable, cast
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import websocket_api
|
from homeassistant.components import websocket_api
|
||||||
from homeassistant.core import split_entity_id
|
from homeassistant.const import CONF_DOMAIN
|
||||||
|
from homeassistant.core import split_entity_id, HomeAssistant
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity_registry import async_entries_for_device
|
from homeassistant.helpers.entity_registry import async_entries_for_device
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.loader import async_get_integration, IntegrationNotFound
|
from homeassistant.loader import async_get_integration, IntegrationNotFound
|
||||||
|
|
||||||
DOMAIN = "device_automation"
|
DOMAIN = "device_automation"
|
||||||
@ -16,14 +20,31 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up device automation."""
|
"""Set up device automation."""
|
||||||
|
hass.components.websocket_api.async_register_command(
|
||||||
|
websocket_device_automation_list_conditions
|
||||||
|
)
|
||||||
hass.components.websocket_api.async_register_command(
|
hass.components.websocket_api.async_register_command(
|
||||||
websocket_device_automation_list_triggers
|
websocket_device_automation_list_triggers
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def _async_get_device_automation_triggers(hass, domain, device_id):
|
async def async_device_condition_from_config(
|
||||||
"""List device triggers."""
|
hass: HomeAssistant, config: ConfigType, config_validation: bool = True
|
||||||
|
) -> Callable[..., bool]:
|
||||||
|
"""Wrap action method with state based condition."""
|
||||||
|
if config_validation:
|
||||||
|
config = cv.DEVICE_CONDITION_SCHEMA(config)
|
||||||
|
integration = await async_get_integration(hass, config[CONF_DOMAIN])
|
||||||
|
platform = integration.get_platform("device_automation")
|
||||||
|
return cast(
|
||||||
|
Callable[..., bool],
|
||||||
|
platform.async_condition_from_config(config, config_validation), # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_get_device_automations_from_domain(hass, domain, fname, device_id):
|
||||||
|
"""List device automations."""
|
||||||
integration = None
|
integration = None
|
||||||
try:
|
try:
|
||||||
integration = await async_get_integration(hass, domain)
|
integration = await async_get_integration(hass, domain)
|
||||||
@ -37,19 +58,19 @@ async def _async_get_device_automation_triggers(hass, domain, device_id):
|
|||||||
# The domain does not have device automations
|
# The domain does not have device automations
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if hasattr(platform, "async_get_triggers"):
|
if hasattr(platform, fname):
|
||||||
return await platform.async_get_triggers(hass, device_id)
|
return await getattr(platform, fname)(hass, device_id)
|
||||||
|
|
||||||
|
|
||||||
async def async_get_device_automation_triggers(hass, device_id):
|
async def _async_get_device_automations(hass, fname, device_id):
|
||||||
"""List device triggers."""
|
"""List device automations."""
|
||||||
device_registry, entity_registry = await asyncio.gather(
|
device_registry, entity_registry = await asyncio.gather(
|
||||||
hass.helpers.device_registry.async_get_registry(),
|
hass.helpers.device_registry.async_get_registry(),
|
||||||
hass.helpers.entity_registry.async_get_registry(),
|
hass.helpers.entity_registry.async_get_registry(),
|
||||||
)
|
)
|
||||||
|
|
||||||
domains = set()
|
domains = set()
|
||||||
triggers = []
|
automations = []
|
||||||
device = device_registry.async_get(device_id)
|
device = device_registry.async_get(device_id)
|
||||||
for entry_id in device.config_entries:
|
for entry_id in device.config_entries:
|
||||||
config_entry = hass.config_entries.async_get_entry(entry_id)
|
config_entry = hass.config_entries.async_get_entry(entry_id)
|
||||||
@ -59,17 +80,33 @@ async def async_get_device_automation_triggers(hass, device_id):
|
|||||||
for entity in entities:
|
for entity in entities:
|
||||||
domains.add(split_entity_id(entity.entity_id)[0])
|
domains.add(split_entity_id(entity.entity_id)[0])
|
||||||
|
|
||||||
device_triggers = await asyncio.gather(
|
device_automations = await asyncio.gather(
|
||||||
*(
|
*(
|
||||||
_async_get_device_automation_triggers(hass, domain, device_id)
|
_async_get_device_automations_from_domain(hass, domain, fname, device_id)
|
||||||
for domain in domains
|
for domain in domains
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for device_trigger in device_triggers:
|
for device_automation in device_automations:
|
||||||
if device_trigger is not None:
|
if device_automation is not None:
|
||||||
triggers.extend(device_trigger)
|
automations.extend(device_automation)
|
||||||
|
|
||||||
return triggers
|
return automations
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.async_response
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "device_automation/condition/list",
|
||||||
|
vol.Required("device_id"): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
async def websocket_device_automation_list_conditions(hass, connection, msg):
|
||||||
|
"""Handle request for device conditions."""
|
||||||
|
device_id = msg["device_id"]
|
||||||
|
conditions = await _async_get_device_automations(
|
||||||
|
hass, "async_get_conditions", device_id
|
||||||
|
)
|
||||||
|
connection.send_result(msg["id"], conditions)
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
@ -82,5 +119,7 @@ async def async_get_device_automation_triggers(hass, device_id):
|
|||||||
async def websocket_device_automation_list_triggers(hass, connection, msg):
|
async def websocket_device_automation_list_triggers(hass, connection, msg):
|
||||||
"""Handle request for device triggers."""
|
"""Handle request for device triggers."""
|
||||||
device_id = msg["device_id"]
|
device_id = msg["device_id"]
|
||||||
triggers = await async_get_device_automation_triggers(hass, device_id)
|
triggers = await _async_get_device_automations(
|
||||||
connection.send_result(msg["id"], {"triggers": triggers})
|
hass, "async_get_triggers", device_id
|
||||||
|
)
|
||||||
|
connection.send_result(msg["id"], triggers)
|
||||||
|
5
homeassistant/components/device_automation/const.py
Normal file
5
homeassistant/components/device_automation/const.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"""Constants for device automations."""
|
||||||
|
CONF_IS_OFF = "is_off"
|
||||||
|
CONF_IS_ON = "is_on"
|
||||||
|
CONF_TURN_OFF = "turn_off"
|
||||||
|
CONF_TURN_ON = "turn_on"
|
@ -2,39 +2,70 @@
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.components.automation.state as state
|
import homeassistant.components.automation.state as state
|
||||||
|
from homeassistant.components.device_automation.const import (
|
||||||
|
CONF_IS_OFF,
|
||||||
|
CONF_IS_ON,
|
||||||
|
CONF_TURN_OFF,
|
||||||
|
CONF_TURN_ON,
|
||||||
|
)
|
||||||
from homeassistant.core import split_entity_id
|
from homeassistant.core import split_entity_id
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
CONF_CONDITION,
|
||||||
CONF_DEVICE_ID,
|
CONF_DEVICE_ID,
|
||||||
CONF_DOMAIN,
|
CONF_DOMAIN,
|
||||||
CONF_ENTITY_ID,
|
CONF_ENTITY_ID,
|
||||||
CONF_PLATFORM,
|
CONF_PLATFORM,
|
||||||
CONF_TYPE,
|
CONF_TYPE,
|
||||||
)
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
from homeassistant.helpers import condition, config_validation as cv
|
||||||
from homeassistant.helpers.entity_registry import async_entries_for_device
|
from homeassistant.helpers.entity_registry import async_entries_for_device
|
||||||
from . import DOMAIN
|
from . import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
# mypy: allow-untyped-defs, no-check-untyped-defs
|
# mypy: allow-untyped-defs, no-check-untyped-defs
|
||||||
|
|
||||||
CONF_TURN_OFF = "turn_off"
|
ENTITY_CONDITIONS = [
|
||||||
CONF_TURN_ON = "turn_on"
|
{
|
||||||
|
# True when light is turned off
|
||||||
|
CONF_CONDITION: "device",
|
||||||
|
CONF_DOMAIN: DOMAIN,
|
||||||
|
CONF_TYPE: CONF_IS_OFF,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# True when light is turned on
|
||||||
|
CONF_CONDITION: "device",
|
||||||
|
CONF_DOMAIN: DOMAIN,
|
||||||
|
CONF_TYPE: CONF_IS_ON,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
ENTITY_TRIGGERS = [
|
ENTITY_TRIGGERS = [
|
||||||
{
|
{
|
||||||
# Trigger when light is turned on
|
# Trigger when light is turned off
|
||||||
CONF_PLATFORM: "device",
|
CONF_PLATFORM: "device",
|
||||||
CONF_DOMAIN: DOMAIN,
|
CONF_DOMAIN: DOMAIN,
|
||||||
CONF_TYPE: CONF_TURN_OFF,
|
CONF_TYPE: CONF_TURN_OFF,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
# Trigger when light is turned off
|
# Trigger when light is turned on
|
||||||
CONF_PLATFORM: "device",
|
CONF_PLATFORM: "device",
|
||||||
CONF_DOMAIN: DOMAIN,
|
CONF_DOMAIN: DOMAIN,
|
||||||
CONF_TYPE: CONF_TURN_ON,
|
CONF_TYPE: CONF_TURN_ON,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
CONDITION_SCHEMA = vol.All(
|
||||||
|
vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_CONDITION): "device",
|
||||||
|
vol.Optional(CONF_DEVICE_ID): str,
|
||||||
|
vol.Required(CONF_DOMAIN): DOMAIN,
|
||||||
|
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||||
|
vol.Required(CONF_TYPE): vol.In([CONF_IS_OFF, CONF_IS_ON]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.All(
|
TRIGGER_SCHEMA = vol.All(
|
||||||
vol.Schema(
|
vol.Schema(
|
||||||
{
|
{
|
||||||
@ -42,7 +73,7 @@ TRIGGER_SCHEMA = vol.All(
|
|||||||
vol.Optional(CONF_DEVICE_ID): str,
|
vol.Optional(CONF_DEVICE_ID): str,
|
||||||
vol.Required(CONF_DOMAIN): DOMAIN,
|
vol.Required(CONF_DOMAIN): DOMAIN,
|
||||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||||
vol.Required(CONF_TYPE): str,
|
vol.Required(CONF_TYPE): vol.In([CONF_TURN_OFF, CONF_TURN_ON]),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -52,9 +83,27 @@ def _is_domain(entity, domain):
|
|||||||
return split_entity_id(entity.entity_id)[0] == domain
|
return split_entity_id(entity.entity_id)[0] == domain
|
||||||
|
|
||||||
|
|
||||||
|
def async_condition_from_config(config, config_validation):
|
||||||
|
"""Evaluate state based on configuration."""
|
||||||
|
config = CONDITION_SCHEMA(config)
|
||||||
|
condition_type = config[CONF_TYPE]
|
||||||
|
if condition_type == CONF_IS_ON:
|
||||||
|
stat = "on"
|
||||||
|
else:
|
||||||
|
stat = "off"
|
||||||
|
state_config = {
|
||||||
|
condition.CONF_CONDITION: "state",
|
||||||
|
condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
|
||||||
|
condition.CONF_STATE: stat,
|
||||||
|
}
|
||||||
|
|
||||||
|
return condition.state_from_config(state_config, config_validation)
|
||||||
|
|
||||||
|
|
||||||
async def async_attach_trigger(hass, config, action, automation_info):
|
async def async_attach_trigger(hass, config, action, automation_info):
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
trigger_type = config.get(CONF_TYPE)
|
config = TRIGGER_SCHEMA(config)
|
||||||
|
trigger_type = config[CONF_TYPE]
|
||||||
if trigger_type == CONF_TURN_ON:
|
if trigger_type == CONF_TURN_ON:
|
||||||
from_state = "off"
|
from_state = "off"
|
||||||
to_state = "on"
|
to_state = "on"
|
||||||
@ -75,17 +124,27 @@ async def async_trigger(hass, config, action, automation_info):
|
|||||||
return await async_attach_trigger(hass, config, action, automation_info)
|
return await async_attach_trigger(hass, config, action, automation_info)
|
||||||
|
|
||||||
|
|
||||||
async def async_get_triggers(hass, device_id):
|
async def _async_get_automations(hass, device_id, automation_templates):
|
||||||
"""List device triggers."""
|
"""List device automations."""
|
||||||
triggers = []
|
automations = []
|
||||||
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
||||||
|
|
||||||
entities = async_entries_for_device(entity_registry, device_id)
|
entities = async_entries_for_device(entity_registry, device_id)
|
||||||
domain_entities = [x for x in entities if _is_domain(x, DOMAIN)]
|
domain_entities = [x for x in entities if _is_domain(x, DOMAIN)]
|
||||||
for entity in domain_entities:
|
for entity in domain_entities:
|
||||||
for trigger in ENTITY_TRIGGERS:
|
for automation in automation_templates:
|
||||||
trigger = dict(trigger)
|
automation = dict(automation)
|
||||||
trigger.update(device_id=device_id, entity_id=entity.entity_id)
|
automation.update(device_id=device_id, entity_id=entity.entity_id)
|
||||||
triggers.append(trigger)
|
automations.append(automation)
|
||||||
|
|
||||||
return triggers
|
return automations
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_conditions(hass, device_id):
|
||||||
|
"""List device conditions."""
|
||||||
|
return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_triggers(hass, device_id):
|
||||||
|
"""List device triggers."""
|
||||||
|
return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS)
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
{
|
{
|
||||||
"device_automation": {
|
"device_automation": {
|
||||||
|
"condition_type": {
|
||||||
|
"is_on": "{name} is on",
|
||||||
|
"is_off": "{name} is off"
|
||||||
|
},
|
||||||
"trigger_type": {
|
"trigger_type": {
|
||||||
"turn_on": "{name} turned on",
|
"turn_on": "{name} turned on",
|
||||||
"turn_off": "{name} turned off"
|
"turn_off": "{name} turned off"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"""Offer reusable conditions."""
|
"""Offer reusable conditions."""
|
||||||
|
import asyncio
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import functools as ft
|
import functools as ft
|
||||||
import logging
|
import logging
|
||||||
@ -10,6 +11,9 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
|||||||
|
|
||||||
from homeassistant.core import HomeAssistant, State
|
from homeassistant.core import HomeAssistant, State
|
||||||
from homeassistant.components import zone as zone_cmp
|
from homeassistant.components import zone as zone_cmp
|
||||||
|
from homeassistant.components.device_automation import ( # noqa: F401 pylint: disable=unused-import
|
||||||
|
async_device_condition_from_config as async_device_from_config,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_GPS_ACCURACY,
|
ATTR_GPS_ACCURACY,
|
||||||
ATTR_LATITUDE,
|
ATTR_LATITUDE,
|
||||||
@ -41,40 +45,9 @@ ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config"
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
# PyLint does not like the use of _threaded_factory
|
|
||||||
# pylint: disable=invalid-name
|
|
||||||
|
|
||||||
|
async def async_from_config(
|
||||||
def _threaded_factory(
|
hass: HomeAssistant, config: ConfigType, config_validation: bool = True
|
||||||
async_factory: Callable[[ConfigType, bool], Callable[..., bool]]
|
|
||||||
) -> Callable[[ConfigType, bool], Callable[..., bool]]:
|
|
||||||
"""Create threaded versions of async factories."""
|
|
||||||
|
|
||||||
@ft.wraps(async_factory)
|
|
||||||
def factory(
|
|
||||||
config: ConfigType, config_validation: bool = True
|
|
||||||
) -> Callable[..., bool]:
|
|
||||||
"""Threaded factory."""
|
|
||||||
async_check = async_factory(config, config_validation)
|
|
||||||
|
|
||||||
def condition_if(
|
|
||||||
hass: HomeAssistant, variables: TemplateVarsType = None
|
|
||||||
) -> bool:
|
|
||||||
"""Validate condition."""
|
|
||||||
return cast(
|
|
||||||
bool,
|
|
||||||
run_callback_threadsafe(
|
|
||||||
hass.loop, async_check, hass, variables
|
|
||||||
).result(),
|
|
||||||
)
|
|
||||||
|
|
||||||
return condition_if
|
|
||||||
|
|
||||||
return factory
|
|
||||||
|
|
||||||
|
|
||||||
def async_from_config(
|
|
||||||
config: ConfigType, config_validation: bool = True
|
|
||||||
) -> Callable[..., bool]:
|
) -> Callable[..., bool]:
|
||||||
"""Turn a condition configuration into a method.
|
"""Turn a condition configuration into a method.
|
||||||
|
|
||||||
@ -95,29 +68,30 @@ def async_from_config(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check for partials to properly determine if coroutine function
|
||||||
|
check_factory = factory
|
||||||
|
while isinstance(check_factory, ft.partial):
|
||||||
|
check_factory = check_factory.func
|
||||||
|
|
||||||
|
if asyncio.iscoroutinefunction(check_factory):
|
||||||
|
return cast(Callable[..., bool], await factory(hass, config, config_validation))
|
||||||
return cast(Callable[..., bool], factory(config, config_validation))
|
return cast(Callable[..., bool], factory(config, config_validation))
|
||||||
|
|
||||||
|
|
||||||
from_config = _threaded_factory(async_from_config)
|
async def async_and_from_config(
|
||||||
|
hass: HomeAssistant, config: ConfigType, config_validation: bool = True
|
||||||
|
|
||||||
def async_and_from_config(
|
|
||||||
config: ConfigType, config_validation: bool = True
|
|
||||||
) -> Callable[..., bool]:
|
) -> Callable[..., bool]:
|
||||||
"""Create multi condition matcher using 'AND'."""
|
"""Create multi condition matcher using 'AND'."""
|
||||||
if config_validation:
|
if config_validation:
|
||||||
config = cv.AND_CONDITION_SCHEMA(config)
|
config = cv.AND_CONDITION_SCHEMA(config)
|
||||||
checks = None
|
checks = [
|
||||||
|
await async_from_config(hass, entry, False) for entry in config["conditions"]
|
||||||
|
]
|
||||||
|
|
||||||
def if_and_condition(
|
def if_and_condition(
|
||||||
hass: HomeAssistant, variables: TemplateVarsType = None
|
hass: HomeAssistant, variables: TemplateVarsType = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Test and condition."""
|
"""Test and condition."""
|
||||||
nonlocal checks
|
|
||||||
|
|
||||||
if checks is None:
|
|
||||||
checks = [async_from_config(entry, False) for entry in config["conditions"]]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for check in checks:
|
for check in checks:
|
||||||
if not check(hass, variables):
|
if not check(hass, variables):
|
||||||
@ -131,26 +105,20 @@ def async_and_from_config(
|
|||||||
return if_and_condition
|
return if_and_condition
|
||||||
|
|
||||||
|
|
||||||
and_from_config = _threaded_factory(async_and_from_config)
|
async def async_or_from_config(
|
||||||
|
hass: HomeAssistant, config: ConfigType, config_validation: bool = True
|
||||||
|
|
||||||
def async_or_from_config(
|
|
||||||
config: ConfigType, config_validation: bool = True
|
|
||||||
) -> Callable[..., bool]:
|
) -> Callable[..., bool]:
|
||||||
"""Create multi condition matcher using 'OR'."""
|
"""Create multi condition matcher using 'OR'."""
|
||||||
if config_validation:
|
if config_validation:
|
||||||
config = cv.OR_CONDITION_SCHEMA(config)
|
config = cv.OR_CONDITION_SCHEMA(config)
|
||||||
checks = None
|
checks = [
|
||||||
|
await async_from_config(hass, entry, False) for entry in config["conditions"]
|
||||||
|
]
|
||||||
|
|
||||||
def if_or_condition(
|
def if_or_condition(
|
||||||
hass: HomeAssistant, variables: TemplateVarsType = None
|
hass: HomeAssistant, variables: TemplateVarsType = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Test and condition."""
|
"""Test and condition."""
|
||||||
nonlocal checks
|
|
||||||
|
|
||||||
if checks is None:
|
|
||||||
checks = [async_from_config(entry, False) for entry in config["conditions"]]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for check in checks:
|
for check in checks:
|
||||||
if check(hass, variables):
|
if check(hass, variables):
|
||||||
@ -163,9 +131,6 @@ def async_or_from_config(
|
|||||||
return if_or_condition
|
return if_or_condition
|
||||||
|
|
||||||
|
|
||||||
or_from_config = _threaded_factory(async_or_from_config)
|
|
||||||
|
|
||||||
|
|
||||||
def numeric_state(
|
def numeric_state(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entity: Union[None, str, State],
|
entity: Union[None, str, State],
|
||||||
@ -263,9 +228,6 @@ def async_numeric_state_from_config(
|
|||||||
return if_numeric_state
|
return if_numeric_state
|
||||||
|
|
||||||
|
|
||||||
numeric_state_from_config = _threaded_factory(async_numeric_state_from_config)
|
|
||||||
|
|
||||||
|
|
||||||
def state(
|
def state(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entity: Union[None, str, State],
|
entity: Union[None, str, State],
|
||||||
@ -423,9 +385,6 @@ def async_template_from_config(
|
|||||||
return template_if
|
return template_if
|
||||||
|
|
||||||
|
|
||||||
template_from_config = _threaded_factory(async_template_from_config)
|
|
||||||
|
|
||||||
|
|
||||||
def time(
|
def time(
|
||||||
before: Optional[dt_util.dt.time] = None,
|
before: Optional[dt_util.dt.time] = None,
|
||||||
after: Optional[dt_util.dt.time] = None,
|
after: Optional[dt_util.dt.time] = None,
|
||||||
|
@ -24,10 +24,13 @@ from homeassistant.const import (
|
|||||||
CONF_ALIAS,
|
CONF_ALIAS,
|
||||||
CONF_BELOW,
|
CONF_BELOW,
|
||||||
CONF_CONDITION,
|
CONF_CONDITION,
|
||||||
|
CONF_DOMAIN,
|
||||||
CONF_ENTITY_ID,
|
CONF_ENTITY_ID,
|
||||||
CONF_ENTITY_NAMESPACE,
|
CONF_ENTITY_NAMESPACE,
|
||||||
|
CONF_FOR,
|
||||||
CONF_PLATFORM,
|
CONF_PLATFORM,
|
||||||
CONF_SCAN_INTERVAL,
|
CONF_SCAN_INTERVAL,
|
||||||
|
CONF_STATE,
|
||||||
CONF_UNIT_SYSTEM_IMPERIAL,
|
CONF_UNIT_SYSTEM_IMPERIAL,
|
||||||
CONF_UNIT_SYSTEM_METRIC,
|
CONF_UNIT_SYSTEM_METRIC,
|
||||||
CONF_VALUE_TEMPLATE,
|
CONF_VALUE_TEMPLATE,
|
||||||
@ -746,8 +749,8 @@ STATE_CONDITION_SCHEMA = vol.All(
|
|||||||
{
|
{
|
||||||
vol.Required(CONF_CONDITION): "state",
|
vol.Required(CONF_CONDITION): "state",
|
||||||
vol.Required(CONF_ENTITY_ID): entity_id,
|
vol.Required(CONF_ENTITY_ID): entity_id,
|
||||||
vol.Required("state"): str,
|
vol.Required(CONF_STATE): str,
|
||||||
vol.Optional("for"): vol.All(time_period, positive_timedelta),
|
vol.Optional(CONF_FOR): vol.All(time_period, positive_timedelta),
|
||||||
# To support use_trigger_value in automation
|
# To support use_trigger_value in automation
|
||||||
# Deprecated 2016/04/25
|
# Deprecated 2016/04/25
|
||||||
vol.Optional("from"): str,
|
vol.Optional("from"): str,
|
||||||
@ -823,6 +826,11 @@ OR_CONDITION_SCHEMA = vol.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DEVICE_CONDITION_SCHEMA = vol.Schema(
|
||||||
|
{vol.Required(CONF_CONDITION): "device", vol.Required(CONF_DOMAIN): str},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
CONDITION_SCHEMA: vol.Schema = vol.Any(
|
CONDITION_SCHEMA: vol.Schema = vol.Any(
|
||||||
NUMERIC_STATE_CONDITION_SCHEMA,
|
NUMERIC_STATE_CONDITION_SCHEMA,
|
||||||
STATE_CONDITION_SCHEMA,
|
STATE_CONDITION_SCHEMA,
|
||||||
@ -832,6 +840,7 @@ CONDITION_SCHEMA: vol.Schema = vol.Any(
|
|||||||
ZONE_CONDITION_SCHEMA,
|
ZONE_CONDITION_SCHEMA,
|
||||||
AND_CONDITION_SCHEMA,
|
AND_CONDITION_SCHEMA,
|
||||||
OR_CONDITION_SCHEMA,
|
OR_CONDITION_SCHEMA,
|
||||||
|
DEVICE_CONDITION_SCHEMA,
|
||||||
)
|
)
|
||||||
|
|
||||||
_SCRIPT_DELAY_SCHEMA = vol.Schema(
|
_SCRIPT_DELAY_SCHEMA = vol.Schema(
|
||||||
|
@ -338,7 +338,7 @@ class Script:
|
|||||||
config_cache_key = frozenset((k, str(v)) for k, v in action.items())
|
config_cache_key = frozenset((k, str(v)) for k, v in action.items())
|
||||||
config = self._config_cache.get(config_cache_key)
|
config = self._config_cache.get(config_cache_key)
|
||||||
if not config:
|
if not config:
|
||||||
config = condition.async_from_config(action, False)
|
config = await condition.async_from_config(self.hass, action, False)
|
||||||
self._config_cache[config_cache_key] = config
|
self._config_cache[config_cache_key] = config
|
||||||
|
|
||||||
self.last_action = action.get(CONF_ALIAS, action[CONF_CONDITION])
|
self.last_action = action.get(CONF_ALIAS, action[CONF_CONDITION])
|
||||||
|
@ -21,7 +21,7 @@ def entity_reg(hass):
|
|||||||
return mock_registry(hass)
|
return mock_registry(hass)
|
||||||
|
|
||||||
|
|
||||||
def _same_triggers(a, b):
|
def _same_lists(a, b):
|
||||||
if len(a) != len(b):
|
if len(a) != len(b):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -31,6 +31,50 @@ def _same_triggers(a, b):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_websocket_get_conditions(hass, hass_ws_client, device_reg, entity_reg):
|
||||||
|
"""Test we get the expected conditions from a light through websocket."""
|
||||||
|
await async_setup_component(hass, "device_automation", {})
|
||||||
|
config_entry = MockConfigEntry(domain="test", data={})
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
device_entry = device_reg.async_get_or_create(
|
||||||
|
config_entry_id=config_entry.entry_id,
|
||||||
|
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
|
||||||
|
)
|
||||||
|
entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id)
|
||||||
|
expected_conditions = [
|
||||||
|
{
|
||||||
|
"condition": "device",
|
||||||
|
"domain": "light",
|
||||||
|
"type": "is_off",
|
||||||
|
"device_id": device_entry.id,
|
||||||
|
"entity_id": "light.test_5678",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": "device",
|
||||||
|
"domain": "light",
|
||||||
|
"type": "is_on",
|
||||||
|
"device_id": device_entry.id,
|
||||||
|
"entity_id": "light.test_5678",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
await client.send_json(
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "device_automation/condition/list",
|
||||||
|
"device_id": device_entry.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg["id"] == 1
|
||||||
|
assert msg["type"] == TYPE_RESULT
|
||||||
|
assert msg["success"]
|
||||||
|
conditions = msg["result"]
|
||||||
|
assert _same_lists(conditions, expected_conditions)
|
||||||
|
|
||||||
|
|
||||||
async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_reg):
|
async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_reg):
|
||||||
"""Test we get the expected triggers from a light through websocket."""
|
"""Test we get the expected triggers from a light through websocket."""
|
||||||
await async_setup_component(hass, "device_automation", {})
|
await async_setup_component(hass, "device_automation", {})
|
||||||
@ -71,5 +115,5 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r
|
|||||||
assert msg["id"] == 1
|
assert msg["id"] == 1
|
||||||
assert msg["type"] == TYPE_RESULT
|
assert msg["type"] == TYPE_RESULT
|
||||||
assert msg["success"]
|
assert msg["success"]
|
||||||
triggers = msg["result"]["triggers"]
|
triggers = msg["result"]
|
||||||
assert _same_triggers(triggers, expected_triggers)
|
assert _same_lists(triggers, expected_triggers)
|
||||||
|
@ -6,11 +6,10 @@ from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM
|
|||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
import homeassistant.components.automation as automation
|
import homeassistant.components.automation as automation
|
||||||
from homeassistant.components.device_automation import (
|
from homeassistant.components.device_automation import (
|
||||||
async_get_device_automation_triggers,
|
_async_get_device_automations as async_get_device_automations,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers import device_registry
|
from homeassistant.helpers import device_registry
|
||||||
|
|
||||||
|
|
||||||
from tests.common import (
|
from tests.common import (
|
||||||
MockConfigEntry,
|
MockConfigEntry,
|
||||||
async_mock_service,
|
async_mock_service,
|
||||||
@ -37,7 +36,7 @@ def calls(hass):
|
|||||||
return async_mock_service(hass, "test", "automation")
|
return async_mock_service(hass, "test", "automation")
|
||||||
|
|
||||||
|
|
||||||
def _same_triggers(a, b):
|
def _same_lists(a, b):
|
||||||
if len(a) != len(b):
|
if len(a) != len(b):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -47,6 +46,37 @@ def _same_triggers(a, b):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_conditions(hass, device_reg, entity_reg):
|
||||||
|
"""Test we get the expected conditions from a light."""
|
||||||
|
config_entry = MockConfigEntry(domain="test", data={})
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
device_entry = device_reg.async_get_or_create(
|
||||||
|
config_entry_id=config_entry.entry_id,
|
||||||
|
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
|
||||||
|
)
|
||||||
|
entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id)
|
||||||
|
expected_conditions = [
|
||||||
|
{
|
||||||
|
"condition": "device",
|
||||||
|
"domain": "light",
|
||||||
|
"type": "is_off",
|
||||||
|
"device_id": device_entry.id,
|
||||||
|
"entity_id": "light.test_5678",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": "device",
|
||||||
|
"domain": "light",
|
||||||
|
"type": "is_on",
|
||||||
|
"device_id": device_entry.id,
|
||||||
|
"entity_id": "light.test_5678",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
conditions = await async_get_device_automations(
|
||||||
|
hass, "async_get_conditions", device_entry.id
|
||||||
|
)
|
||||||
|
assert _same_lists(conditions, expected_conditions)
|
||||||
|
|
||||||
|
|
||||||
async def test_get_triggers(hass, device_reg, entity_reg):
|
async def test_get_triggers(hass, device_reg, entity_reg):
|
||||||
"""Test we get the expected triggers from a light."""
|
"""Test we get the expected triggers from a light."""
|
||||||
config_entry = MockConfigEntry(domain="test", data={})
|
config_entry = MockConfigEntry(domain="test", data={})
|
||||||
@ -72,8 +102,10 @@ async def test_get_triggers(hass, device_reg, entity_reg):
|
|||||||
"entity_id": "light.test_5678",
|
"entity_id": "light.test_5678",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
triggers = await async_get_device_automation_triggers(hass, device_entry.id)
|
triggers = await async_get_device_automations(
|
||||||
assert _same_triggers(triggers, expected_triggers)
|
hass, "async_get_triggers", device_entry.id
|
||||||
|
)
|
||||||
|
assert _same_lists(triggers, expected_triggers)
|
||||||
|
|
||||||
|
|
||||||
async def test_if_fires_on_state_change(hass, calls):
|
async def test_if_fires_on_state_change(hass, calls):
|
||||||
@ -158,3 +190,76 @@ async def test_if_fires_on_state_change(hass, calls):
|
|||||||
assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format(
|
assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format(
|
||||||
dev1.entity_id
|
dev1.entity_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_if_state(hass, calls):
|
||||||
|
"""Test for turn_on and turn_off conditions."""
|
||||||
|
platform = getattr(hass.components, "test.light")
|
||||||
|
|
||||||
|
platform.init()
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
|
||||||
|
)
|
||||||
|
|
||||||
|
dev1, dev2, dev3 = platform.DEVICES
|
||||||
|
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: [
|
||||||
|
{
|
||||||
|
"trigger": {"platform": "event", "event_type": "test_event1"},
|
||||||
|
"condition": [
|
||||||
|
{
|
||||||
|
"condition": "device",
|
||||||
|
"domain": "light",
|
||||||
|
"entity_id": dev1.entity_id,
|
||||||
|
"type": "is_on",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"action": {
|
||||||
|
"service": "test.automation",
|
||||||
|
"data_template": {
|
||||||
|
"some": "is_on {{ trigger.%s }}"
|
||||||
|
% "}} - {{ trigger.".join(("platform", "event.event_type"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"trigger": {"platform": "event", "event_type": "test_event2"},
|
||||||
|
"condition": [
|
||||||
|
{
|
||||||
|
"condition": "device",
|
||||||
|
"domain": "light",
|
||||||
|
"entity_id": dev1.entity_id,
|
||||||
|
"type": "is_off",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"action": {
|
||||||
|
"service": "test.automation",
|
||||||
|
"data_template": {
|
||||||
|
"some": "is_off {{ trigger.%s }}"
|
||||||
|
% "}} - {{ trigger.".join(("platform", "event.event_type"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert hass.states.get(dev1.entity_id).state == STATE_ON
|
||||||
|
assert len(calls) == 0
|
||||||
|
|
||||||
|
hass.bus.async_fire("test_event1")
|
||||||
|
hass.bus.async_fire("test_event2")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert calls[0].data["some"] == "is_on event - test_event1"
|
||||||
|
|
||||||
|
hass.states.async_set(dev1.entity_id, STATE_OFF)
|
||||||
|
hass.bus.async_fire("test_event1")
|
||||||
|
hass.bus.async_fire("test_event2")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 2
|
||||||
|
assert calls[1].data["some"] == "is_off event - test_event2"
|
||||||
|
@ -4,182 +4,175 @@ from unittest.mock import patch
|
|||||||
from homeassistant.helpers import condition
|
from homeassistant.helpers import condition
|
||||||
from homeassistant.util import dt
|
from homeassistant.util import dt
|
||||||
|
|
||||||
from tests.common import get_test_home_assistant
|
|
||||||
|
async def test_and_condition(hass):
|
||||||
|
"""Test the 'and' condition."""
|
||||||
|
test = await condition.async_from_config(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"condition": "and",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"condition": "state",
|
||||||
|
"entity_id": "sensor.temperature",
|
||||||
|
"state": "100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": "numeric_state",
|
||||||
|
"entity_id": "sensor.temperature",
|
||||||
|
"below": 110,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.temperature", 120)
|
||||||
|
assert not test(hass)
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.temperature", 105)
|
||||||
|
assert not test(hass)
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.temperature", 100)
|
||||||
|
assert test(hass)
|
||||||
|
|
||||||
|
|
||||||
class TestConditionHelper:
|
async def test_and_condition_with_template(hass):
|
||||||
"""Test condition helpers."""
|
"""Test the 'and' condition."""
|
||||||
|
test = await condition.async_from_config(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"condition": "and",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"condition": "template",
|
||||||
|
"value_template": '{{ states.sensor.temperature.state == "100" }}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": "numeric_state",
|
||||||
|
"entity_id": "sensor.temperature",
|
||||||
|
"below": 110,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def setup_method(self, method):
|
hass.states.async_set("sensor.temperature", 120)
|
||||||
"""Set up things to be run when tests are started."""
|
assert not test(hass)
|
||||||
self.hass = get_test_home_assistant()
|
|
||||||
|
|
||||||
def teardown_method(self, method):
|
hass.states.async_set("sensor.temperature", 105)
|
||||||
"""Stop everything that was started."""
|
assert not test(hass)
|
||||||
self.hass.stop()
|
|
||||||
|
|
||||||
def test_and_condition(self):
|
hass.states.async_set("sensor.temperature", 100)
|
||||||
"""Test the 'and' condition."""
|
assert test(hass)
|
||||||
test = condition.from_config(
|
|
||||||
{
|
|
||||||
"condition": "and",
|
|
||||||
"conditions": [
|
|
||||||
{
|
|
||||||
"condition": "state",
|
|
||||||
"entity_id": "sensor.temperature",
|
|
||||||
"state": "100",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"condition": "numeric_state",
|
|
||||||
"entity_id": "sensor.temperature",
|
|
||||||
"below": 110,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.hass.states.set("sensor.temperature", 120)
|
|
||||||
assert not test(self.hass)
|
|
||||||
|
|
||||||
self.hass.states.set("sensor.temperature", 105)
|
async def test_or_condition(hass):
|
||||||
assert not test(self.hass)
|
"""Test the 'or' condition."""
|
||||||
|
test = await condition.async_from_config(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"condition": "or",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"condition": "state",
|
||||||
|
"entity_id": "sensor.temperature",
|
||||||
|
"state": "100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": "numeric_state",
|
||||||
|
"entity_id": "sensor.temperature",
|
||||||
|
"below": 110,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
self.hass.states.set("sensor.temperature", 100)
|
hass.states.async_set("sensor.temperature", 120)
|
||||||
assert test(self.hass)
|
assert not test(hass)
|
||||||
|
|
||||||
def test_and_condition_with_template(self):
|
hass.states.async_set("sensor.temperature", 105)
|
||||||
"""Test the 'and' condition."""
|
assert test(hass)
|
||||||
test = condition.from_config(
|
|
||||||
{
|
|
||||||
"condition": "and",
|
|
||||||
"conditions": [
|
|
||||||
{
|
|
||||||
"condition": "template",
|
|
||||||
"value_template": '{{ states.sensor.temperature.state == "100" }}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"condition": "numeric_state",
|
|
||||||
"entity_id": "sensor.temperature",
|
|
||||||
"below": 110,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.hass.states.set("sensor.temperature", 120)
|
hass.states.async_set("sensor.temperature", 100)
|
||||||
assert not test(self.hass)
|
assert test(hass)
|
||||||
|
|
||||||
self.hass.states.set("sensor.temperature", 105)
|
|
||||||
assert not test(self.hass)
|
|
||||||
|
|
||||||
self.hass.states.set("sensor.temperature", 100)
|
async def test_or_condition_with_template(hass):
|
||||||
assert test(self.hass)
|
"""Test the 'or' condition."""
|
||||||
|
test = await condition.async_from_config(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"condition": "or",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"condition": "template",
|
||||||
|
"value_template": '{{ states.sensor.temperature.state == "100" }}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": "numeric_state",
|
||||||
|
"entity_id": "sensor.temperature",
|
||||||
|
"below": 110,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def test_or_condition(self):
|
hass.states.async_set("sensor.temperature", 120)
|
||||||
"""Test the 'or' condition."""
|
assert not test(hass)
|
||||||
test = condition.from_config(
|
|
||||||
{
|
|
||||||
"condition": "or",
|
|
||||||
"conditions": [
|
|
||||||
{
|
|
||||||
"condition": "state",
|
|
||||||
"entity_id": "sensor.temperature",
|
|
||||||
"state": "100",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"condition": "numeric_state",
|
|
||||||
"entity_id": "sensor.temperature",
|
|
||||||
"below": 110,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.hass.states.set("sensor.temperature", 120)
|
hass.states.async_set("sensor.temperature", 105)
|
||||||
assert not test(self.hass)
|
assert test(hass)
|
||||||
|
|
||||||
self.hass.states.set("sensor.temperature", 105)
|
hass.states.async_set("sensor.temperature", 100)
|
||||||
assert test(self.hass)
|
assert test(hass)
|
||||||
|
|
||||||
self.hass.states.set("sensor.temperature", 100)
|
|
||||||
assert test(self.hass)
|
|
||||||
|
|
||||||
def test_or_condition_with_template(self):
|
async def test_time_window(hass):
|
||||||
"""Test the 'or' condition."""
|
"""Test time condition windows."""
|
||||||
test = condition.from_config(
|
sixam = dt.parse_time("06:00:00")
|
||||||
{
|
sixpm = dt.parse_time("18:00:00")
|
||||||
"condition": "or",
|
|
||||||
"conditions": [
|
|
||||||
{
|
|
||||||
"condition": "template",
|
|
||||||
"value_template": '{{ states.sensor.temperature.state == "100" }}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"condition": "numeric_state",
|
|
||||||
"entity_id": "sensor.temperature",
|
|
||||||
"below": 110,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.hass.states.set("sensor.temperature", 120)
|
with patch(
|
||||||
assert not test(self.hass)
|
"homeassistant.helpers.condition.dt_util.now",
|
||||||
|
return_value=dt.now().replace(hour=3),
|
||||||
|
):
|
||||||
|
assert not condition.time(after=sixam, before=sixpm)
|
||||||
|
assert condition.time(after=sixpm, before=sixam)
|
||||||
|
|
||||||
self.hass.states.set("sensor.temperature", 105)
|
with patch(
|
||||||
assert test(self.hass)
|
"homeassistant.helpers.condition.dt_util.now",
|
||||||
|
return_value=dt.now().replace(hour=9),
|
||||||
|
):
|
||||||
|
assert condition.time(after=sixam, before=sixpm)
|
||||||
|
assert not condition.time(after=sixpm, before=sixam)
|
||||||
|
|
||||||
self.hass.states.set("sensor.temperature", 100)
|
with patch(
|
||||||
assert test(self.hass)
|
"homeassistant.helpers.condition.dt_util.now",
|
||||||
|
return_value=dt.now().replace(hour=15),
|
||||||
|
):
|
||||||
|
assert condition.time(after=sixam, before=sixpm)
|
||||||
|
assert not condition.time(after=sixpm, before=sixam)
|
||||||
|
|
||||||
def test_time_window(self):
|
with patch(
|
||||||
"""Test time condition windows."""
|
"homeassistant.helpers.condition.dt_util.now",
|
||||||
sixam = dt.parse_time("06:00:00")
|
return_value=dt.now().replace(hour=21),
|
||||||
sixpm = dt.parse_time("18:00:00")
|
):
|
||||||
|
assert not condition.time(after=sixam, before=sixpm)
|
||||||
|
assert condition.time(after=sixpm, before=sixam)
|
||||||
|
|
||||||
with patch(
|
|
||||||
"homeassistant.helpers.condition.dt_util.now",
|
|
||||||
return_value=dt.now().replace(hour=3),
|
|
||||||
):
|
|
||||||
assert not condition.time(after=sixam, before=sixpm)
|
|
||||||
assert condition.time(after=sixpm, before=sixam)
|
|
||||||
|
|
||||||
with patch(
|
async def test_if_numeric_state_not_raise_on_unavailable(hass):
|
||||||
"homeassistant.helpers.condition.dt_util.now",
|
"""Test numeric_state doesn't raise on unavailable/unknown state."""
|
||||||
return_value=dt.now().replace(hour=9),
|
test = await condition.async_from_config(
|
||||||
):
|
hass,
|
||||||
assert condition.time(after=sixam, before=sixpm)
|
{"condition": "numeric_state", "entity_id": "sensor.temperature", "below": 42},
|
||||||
assert not condition.time(after=sixpm, before=sixam)
|
)
|
||||||
|
|
||||||
with patch(
|
with patch("homeassistant.helpers.condition._LOGGER.warning") as logwarn:
|
||||||
"homeassistant.helpers.condition.dt_util.now",
|
hass.states.async_set("sensor.temperature", "unavailable")
|
||||||
return_value=dt.now().replace(hour=15),
|
assert not test(hass)
|
||||||
):
|
assert len(logwarn.mock_calls) == 0
|
||||||
assert condition.time(after=sixam, before=sixpm)
|
|
||||||
assert not condition.time(after=sixpm, before=sixam)
|
|
||||||
|
|
||||||
with patch(
|
hass.states.async_set("sensor.temperature", "unknown")
|
||||||
"homeassistant.helpers.condition.dt_util.now",
|
assert not test(hass)
|
||||||
return_value=dt.now().replace(hour=21),
|
assert len(logwarn.mock_calls) == 0
|
||||||
):
|
|
||||||
assert not condition.time(after=sixam, before=sixpm)
|
|
||||||
assert condition.time(after=sixpm, before=sixam)
|
|
||||||
|
|
||||||
def test_if_numeric_state_not_raise_on_unavailable(self):
|
|
||||||
"""Test numeric_state doesn't raise on unavailable/unknown state."""
|
|
||||||
test = condition.from_config(
|
|
||||||
{
|
|
||||||
"condition": "numeric_state",
|
|
||||||
"entity_id": "sensor.temperature",
|
|
||||||
"below": 42,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch("homeassistant.helpers.condition._LOGGER.warning") as logwarn:
|
|
||||||
self.hass.states.set("sensor.temperature", "unavailable")
|
|
||||||
assert not test(self.hass)
|
|
||||||
assert len(logwarn.mock_calls) == 0
|
|
||||||
|
|
||||||
self.hass.states.set("sensor.temperature", "unknown")
|
|
||||||
assert not test(self.hass)
|
|
||||||
assert len(logwarn.mock_calls) == 0
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user