Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for for to binary_sensor, light and switch device conditions #27153

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion homeassistant/components/binary_sensor/device_condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from homeassistant.core import HomeAssistant
from homeassistant.components.device_automation.const import CONF_IS_OFF, CONF_IS_ON
from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_TYPE
from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE
from homeassistant.helpers import condition, config_validation as cv
from homeassistant.helpers.entity_registry import (
async_entries_for_device,
Expand Down Expand Up @@ -188,6 +188,7 @@
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(IS_OFF + IS_ON),
vol.Optional(CONF_FOR): cv.positive_time_period_dict,
}
)

Expand Down Expand Up @@ -244,5 +245,16 @@ def async_condition_from_config(
condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
condition.CONF_STATE: stat,
}
if CONF_FOR in config:
state_config[CONF_FOR] = config[CONF_FOR]

return condition.state_from_config(state_config, config_validation)


async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict:
"""List condition capabilities."""
return {
"extra_fields": vol.Schema(
{vol.Optional(CONF_FOR): cv.positive_time_period_dict}
)
}
2 changes: 1 addition & 1 deletion homeassistant/components/binary_sensor/device_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ async def async_get_triggers(hass, device_id):
return triggers


async def async_get_trigger_capabilities(hass, trigger):
async def async_get_trigger_capabilities(hass, config):
"""List trigger capabilities."""
return {
"extra_fields": vol.Schema(
Expand Down
19 changes: 19 additions & 0 deletions homeassistant/components/device_automation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ async def async_setup(hass, config):
hass.components.websocket_api.async_register_command(
websocket_device_automation_list_triggers
)
hass.components.websocket_api.async_register_command(
websocket_device_automation_get_condition_capabilities
)
hass.components.websocket_api.async_register_command(
websocket_device_automation_get_trigger_capabilities
)
Expand Down Expand Up @@ -206,6 +209,22 @@ async def websocket_device_automation_list_triggers(hass, connection, msg):
connection.send_result(msg["id"], triggers)


@websocket_api.async_response
@websocket_api.websocket_command(
{
vol.Required("type"): "device_automation/condition/capabilities",
vol.Required("condition"): dict,
}
)
async def websocket_device_automation_get_condition_capabilities(hass, connection, msg):
"""Handle request for device condition capabilities."""
condition = msg["condition"]
capabilities = await _async_get_device_automation_capabilities(
hass, "condition", condition
)
connection.send_result(msg["id"], capabilities)


@websocket_api.async_response
@websocket_api.websocket_command(
{
Expand Down
14 changes: 13 additions & 1 deletion homeassistant/components/device_automation/toggle_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In([CONF_IS_OFF, CONF_IS_ON]),
vol.Optional(CONF_FOR): cv.positive_time_period_dict,
}
)

Expand Down Expand Up @@ -132,6 +133,8 @@ def async_condition_from_config(
condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
condition.CONF_STATE: stat,
}
if CONF_FOR in config:
state_config[CONF_FOR] = config[CONF_FOR]

return condition.state_from_config(state_config, config_validation)

Expand Down Expand Up @@ -213,7 +216,16 @@ async def async_get_triggers(
return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain)


async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict:
async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict:
"""List condition capabilities."""
return {
"extra_fields": vol.Schema(
{vol.Optional(CONF_FOR): cv.positive_time_period_dict}
)
}


async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict:
"""List trigger capabilities."""
return {
"extra_fields": vol.Schema(
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/light/device_condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ def async_condition_from_config(
async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]:
"""List device conditions."""
return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN)


async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict:
"""List condition capabilities."""
return await toggle_entity.async_get_condition_capabilities(hass, config)
4 changes: 2 additions & 2 deletions homeassistant/components/light/device_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN)


async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict:
async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict:
"""List trigger capabilities."""
return await toggle_entity.async_get_trigger_capabilities(hass, trigger)
return await toggle_entity.async_get_trigger_capabilities(hass, config)
2 changes: 1 addition & 1 deletion homeassistant/components/sensor/device_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ async def async_get_triggers(hass, device_id):
return triggers


async def async_get_trigger_capabilities(hass, trigger):
async def async_get_trigger_capabilities(hass, config):
"""List trigger capabilities."""
return {
"extra_fields": vol.Schema(
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/switch/device_condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ def async_condition_from_config(
async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]:
"""List device conditions."""
return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN)


async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict:
"""List condition capabilities."""
return await toggle_entity.async_get_condition_capabilities(hass, config)
4 changes: 2 additions & 2 deletions homeassistant/components/switch/device_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN)


async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict:
async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict:
"""List trigger capabilities."""
return await toggle_entity.async_get_trigger_capabilities(hass, trigger)
return await toggle_entity.async_get_trigger_capabilities(hass, config)
97 changes: 96 additions & 1 deletion tests/components/binary_sensor/test_device_condition.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
"""The test for binary_sensor device automation."""
from datetime import timedelta
import pytest
from unittest.mock import patch

from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES
from homeassistant.components.binary_sensor.device_condition import ENTITY_CONDITIONS
from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM
from homeassistant.setup import async_setup_component
import homeassistant.components.automation as automation
from homeassistant.helpers import device_registry
import homeassistant.util.dt as dt_util

from tests.common import (
MockConfigEntry,
async_mock_service,
mock_device_registry,
mock_registry,
async_get_device_automations,
async_get_device_automation_capabilities,
)


Expand Down Expand Up @@ -71,6 +75,28 @@ async def test_get_conditions(hass, device_reg, entity_reg):
assert conditions == expected_conditions


async def test_get_condition_capabilities(hass, device_reg, entity_reg):
"""Test we get the expected capabilities from a binary_sensor condition."""
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(DOMAIN, "test", "5678", device_id=device_entry.id)
expected_capabilities = {
"extra_fields": [
{"name": "for", "optional": True, "type": "positive_time_period_dict"}
]
}
conditions = await async_get_device_automations(hass, "condition", device_entry.id)
for condition in conditions:
capabilities = await async_get_device_automation_capabilities(
hass, "condition", condition
)
assert capabilities == expected_capabilities


async def test_if_state(hass, calls):
"""Test for turn_on and turn_off conditions."""
platform = getattr(hass.components, f"test.{DOMAIN}")
Expand Down Expand Up @@ -131,7 +157,6 @@ async def test_if_state(hass, calls):
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"
Expand All @@ -142,3 +167,73 @@ async def test_if_state(hass, calls):
await hass.async_block_till_done()
assert len(calls) == 2
assert calls[1].data["some"] == "is_off event - test_event2"


async def test_if_fires_on_for_condition(hass, calls):
"""Test for firing if condition is on with delay."""
point1 = dt_util.utcnow()
point2 = point1 + timedelta(seconds=10)
point3 = point2 + timedelta(seconds=10)

platform = getattr(hass.components, f"test.{DOMAIN}")

platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})

sensor1 = platform.ENTITIES["battery"]

with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow:
mock_utcnow.return_value = point1
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: [
{
"trigger": {"platform": "event", "event_type": "test_event1"},
"condition": {
"condition": "device",
"domain": DOMAIN,
"device_id": "",
"entity_id": sensor1.entity_id,
"type": "is_not_bat_low",
"for": {"seconds": 5},
},
"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(sensor1.entity_id).state == STATE_ON
assert len(calls) == 0

hass.bus.async_fire("test_event1")
await hass.async_block_till_done()
assert len(calls) == 0

# Time travel 10 secs into the future
mock_utcnow.return_value = point2
hass.bus.async_fire("test_event1")
await hass.async_block_till_done()
assert len(calls) == 0

hass.states.async_set(sensor1.entity_id, STATE_OFF)
hass.bus.async_fire("test_event1")
await hass.async_block_till_done()
assert len(calls) == 0

# Time travel 20 secs into the future
mock_utcnow.return_value = point3
hass.bus.async_fire("test_event1")
await hass.async_block_till_done()
assert len(calls) == 1
assert calls[0].data["some"] == "is_off event - test_event1"
97 changes: 97 additions & 0 deletions tests/components/device_automation/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,103 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r
assert _same_lists(triggers, expected_triggers)


async def test_websocket_get_condition_capabilities(
hass, hass_ws_client, device_reg, entity_reg
):
"""Test we get the expected condition capabilities for 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_capabilities = {
"extra_fields": [
{"name": "for", "optional": True, "type": "positive_time_period_dict"}
]
}

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"]

id = 2
for condition in conditions:
await client.send_json(
{
"id": id,
"type": "device_automation/condition/capabilities",
"condition": condition,
}
)
msg = await client.receive_json()
assert msg["id"] == id
assert msg["type"] == TYPE_RESULT
assert msg["success"]
capabilities = msg["result"]
assert capabilities == expected_capabilities
id = id + 1


async def test_websocket_get_bad_condition_capabilities(
hass, hass_ws_client, device_reg, entity_reg
):
"""Test we get no condition capabilities for a non existing domain."""
await async_setup_component(hass, "device_automation", {})
expected_capabilities = {}

client = await hass_ws_client(hass)
await client.send_json(
{
"id": 1,
"type": "device_automation/condition/capabilities",
"condition": {"domain": "beer"},
}
)
msg = await client.receive_json()
assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"]
capabilities = msg["result"]
assert capabilities == expected_capabilities


async def test_websocket_get_no_condition_capabilities(
hass, hass_ws_client, device_reg, entity_reg
):
"""Test we get no condition capabilities for a domain with no device condition capabilities."""
await async_setup_component(hass, "device_automation", {})
expected_capabilities = {}

client = await hass_ws_client(hass)
await client.send_json(
{
"id": 1,
"type": "device_automation/condition/capabilities",
"condition": {"domain": "deconz"},
}
)
msg = await client.receive_json()
assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"]
capabilities = msg["result"]
assert capabilities == expected_capabilities


async def test_websocket_get_trigger_capabilities(
hass, hass_ws_client, device_reg, entity_reg
):
Expand Down
Loading