Automation device/entity extraction to include triggers + conditions (#31474)

* Add support for extracting triggers

* Add support for extracting triggers

* Fix test
This commit is contained in:
Paulus Schoutsen 2020-02-05 07:52:21 -08:00 committed by GitHub
parent 431a3a6b44
commit 67680bcfa8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 143 additions and 57 deletions

View file

@ -1,16 +1,19 @@
"""Allow to set up simple automation rules via the config file.""" """Allow to set up simple automation rules via the config file."""
from functools import partial
import importlib import importlib
import logging import logging
from typing import Any, Awaitable, Callable, List from typing import Any, Awaitable, Callable, List, Optional, Set
import voluptuous as vol import voluptuous as vol
from homeassistant.components import sun
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_NAME, ATTR_NAME,
CONF_DEVICE_ID,
CONF_ENTITY_ID,
CONF_ID, CONF_ID,
CONF_PLATFORM, CONF_PLATFORM,
CONF_ZONE,
EVENT_AUTOMATION_TRIGGERED, EVENT_AUTOMATION_TRIGGERED,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_START,
SERVICE_RELOAD, SERVICE_RELOAD,
@ -130,7 +133,7 @@ def automations_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]:
results = [] results = []
for automation_entity in component.entities: for automation_entity in component.entities:
if entity_id in automation_entity.action_script.referenced_entities: if entity_id in automation_entity.referenced_entities:
results.append(automation_entity.entity_id) results.append(automation_entity.entity_id)
return results return results
@ -149,7 +152,7 @@ def entities_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]:
if automation_entity is None: if automation_entity is None:
return [] return []
return list(automation_entity.action_script.referenced_entities) return list(automation_entity.referenced_entities)
@callback @callback
@ -163,7 +166,7 @@ def automations_with_device(hass: HomeAssistant, device_id: str) -> List[str]:
results = [] results = []
for automation_entity in component.entities: for automation_entity in component.entities:
if device_id in automation_entity.action_script.referenced_devices: if device_id in automation_entity.referenced_devices:
results.append(automation_entity.entity_id) results.append(automation_entity.entity_id)
return results return results
@ -182,7 +185,7 @@ def devices_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]:
if automation_entity is None: if automation_entity is None:
return [] return []
return list(automation_entity.action_script.referenced_devices) return list(automation_entity.referenced_devices)
async def async_setup(hass, config): async def async_setup(hass, config):
@ -232,7 +235,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
self, self,
automation_id, automation_id,
name, name,
async_attach_triggers, trigger_config,
cond_func, cond_func,
action_script, action_script,
hidden, hidden,
@ -241,7 +244,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
"""Initialize an automation entity.""" """Initialize an automation entity."""
self._id = automation_id self._id = automation_id
self._name = name self._name = name
self._async_attach_triggers = async_attach_triggers self._trigger_config = trigger_config
self._async_detach_triggers = None self._async_detach_triggers = None
self._cond_func = cond_func self._cond_func = cond_func
self.action_script = action_script self.action_script = action_script
@ -249,6 +252,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
self._hidden = hidden self._hidden = hidden
self._initial_state = initial_state self._initial_state = initial_state
self._is_enabled = False self._is_enabled = False
self._referenced_entities: Optional[Set[str]] = None
self._referenced_devices: Optional[Set[str]] = None
@property @property
def name(self): def name(self):
@ -280,6 +285,45 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
"""Return True if entity is on.""" """Return True if entity is on."""
return self._async_detach_triggers is not None or self._is_enabled return self._async_detach_triggers is not None or self._is_enabled
@property
def referenced_devices(self):
"""Return a set of referenced devices."""
if self._referenced_devices is not None:
return self._referenced_devices
referenced = self.action_script.referenced_devices
if self._cond_func is not None:
for conf in self._cond_func.config:
referenced |= condition.async_extract_devices(conf)
for conf in self._trigger_config:
device = _trigger_extract_device(conf)
if device is not None:
referenced.add(device)
self._referenced_devices = referenced
return referenced
@property
def referenced_entities(self):
"""Return a set of referenced entities."""
if self._referenced_entities is not None:
return self._referenced_entities
referenced = self.action_script.referenced_entities
if self._cond_func is not None:
for conf in self._cond_func.config:
referenced |= condition.async_extract_entities(conf)
for conf in self._trigger_config:
for entity_id in _trigger_extract_entities(conf):
referenced.add(entity_id)
self._referenced_entities = referenced
return referenced
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Startup with initial state or previous state.""" """Startup with initial state or previous state."""
await super().async_added_to_hass() await super().async_added_to_hass()
@ -330,7 +374,11 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
This method is a coroutine. This method is a coroutine.
""" """
if not skip_condition and not self._cond_func(variables): if (
not skip_condition
and self._cond_func is not None
and not self._cond_func(variables)
):
return return
# Create a new context referring to the old context. # Create a new context referring to the old context.
@ -373,9 +421,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
# HomeAssistant is starting up # HomeAssistant is starting up
if self.hass.state != CoreState.not_running: if self.hass.state != CoreState.not_running:
self._async_detach_triggers = await self._async_attach_triggers( self._async_detach_triggers = await self._async_attach_triggers()
self.async_trigger
)
self.async_write_ha_state() self.async_write_ha_state()
return return
@ -385,9 +431,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
if not self._is_enabled or self._async_detach_triggers is not None: if not self._is_enabled or self._async_detach_triggers is not None:
return return
self._async_detach_triggers = await self._async_attach_triggers( self._async_detach_triggers = await self._async_attach_triggers()
self.async_trigger
)
self.hass.bus.async_listen_once( self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, async_enable_automation EVENT_HOMEASSISTANT_START, async_enable_automation
@ -407,6 +451,38 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
self.async_write_ha_state() self.async_write_ha_state()
async def _async_attach_triggers(self):
"""Set up the triggers."""
removes = []
info = {"name": self._name}
for conf in self._trigger_config:
platform = importlib.import_module(
".{}".format(conf[CONF_PLATFORM]), __name__
)
remove = await platform.async_attach_trigger(
self.hass, conf, self.async_trigger, info
)
if not remove:
_LOGGER.error("Error setting up trigger %s", self._name)
continue
_LOGGER.info("Initialized trigger %s", self._name)
removes.append(remove)
if not removes:
return None
@callback
def remove_triggers():
"""Remove attached triggers."""
for remove in removes:
remove()
return remove_triggers
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return automation attributes.""" """Return automation attributes."""
@ -441,22 +517,12 @@ async def _async_process_config(hass, config, component):
if cond_func is None: if cond_func is None:
continue continue
else: else:
cond_func = None
def cond_func(variables):
"""Condition will always pass."""
return True
async_attach_triggers = partial(
_async_process_trigger,
hass,
config,
config_block.get(CONF_TRIGGER, []),
name,
)
entity = AutomationEntity( entity = AutomationEntity(
automation_id, automation_id,
name, name,
async_attach_triggers, config_block[CONF_TRIGGER],
cond_func, cond_func,
action_script, action_script,
hidden, hidden,
@ -471,7 +537,7 @@ async def _async_process_config(hass, config, component):
async 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[CONF_CONDITION]
checks = [] checks = []
for if_config in if_configs: for if_config in if_configs:
@ -485,35 +551,33 @@ async def _async_process_if(hass, config, p_config):
"""AND all conditions.""" """AND all conditions."""
return all(check(hass, variables) for check in checks) return all(check(hass, variables) for check in checks)
if_action.config = if_configs
return if_action return if_action
async def _async_process_trigger(hass, config, trigger_configs, name, action): @callback
"""Set up the triggers. def _trigger_extract_device(trigger_conf: dict) -> Optional[str]:
"""Extract devices from a trigger config."""
This method is a coroutine. if trigger_conf[CONF_PLATFORM] != "device":
"""
removes = []
info = {"name": name}
for conf in trigger_configs:
platform = importlib.import_module(".{}".format(conf[CONF_PLATFORM]), __name__)
remove = await platform.async_attach_trigger(hass, conf, action, info)
if not remove:
_LOGGER.error("Error setting up trigger %s", name)
continue
_LOGGER.info("Initialized trigger %s", name)
removes.append(remove)
if not removes:
return None return None
def remove_triggers(): return trigger_conf[CONF_DEVICE_ID]
"""Remove attached triggers."""
for remove in removes:
remove()
return remove_triggers
@callback
def _trigger_extract_entities(trigger_conf: dict) -> List[str]:
"""Extract entities from a trigger config."""
if trigger_conf[CONF_PLATFORM] in ("state", "numeric_state"):
return trigger_conf[CONF_ENTITY_ID]
if trigger_conf[CONF_PLATFORM] == "zone":
return trigger_conf[CONF_ENTITY_ID] + [trigger_conf[CONF_ZONE]]
if trigger_conf[CONF_PLATFORM] == "geo_location":
return [trigger_conf[CONF_ZONE]]
if trigger_conf[CONF_PLATFORM] == "sun":
return [sun.ENTITY_ID]
return []

View file

@ -935,6 +935,11 @@ async def test_extraction_functions(hass):
{ {
"alias": "test1", "alias": "test1",
"trigger": {"platform": "state", "entity_id": "sensor.trigger_1"}, "trigger": {"platform": "state", "entity_id": "sensor.trigger_1"},
"condition": {
"condition": "state",
"entity_id": "light.condition_state",
"state": "on",
},
"action": [ "action": [
{ {
"service": "test.script", "service": "test.script",
@ -954,7 +959,20 @@ async def test_extraction_functions(hass):
}, },
{ {
"alias": "test2", "alias": "test2",
"trigger": {"platform": "state", "entity_id": "sensor.trigger_2"}, "trigger": {
"platform": "device",
"domain": "light",
"type": "turned_on",
"entity_id": "light.trigger_2",
"device_id": "trigger-device-2",
},
"condition": {
"condition": "device",
"device_id": "condition-device",
"domain": "light",
"type": "is_on",
"entity_id": "light.bla",
},
"action": [ "action": [
{ {
"service": "test.script", "service": "test.script",
@ -989,6 +1007,8 @@ async def test_extraction_functions(hass):
"automation.test2", "automation.test2",
} }
assert set(automation.entities_in_automation(hass, "automation.test1")) == { assert set(automation.entities_in_automation(hass, "automation.test1")) == {
"sensor.trigger_1",
"light.condition_state",
"light.in_both", "light.in_both",
"light.in_first", "light.in_first",
} }
@ -997,6 +1017,8 @@ async def test_extraction_functions(hass):
"automation.test2", "automation.test2",
} }
assert set(automation.devices_in_automation(hass, "automation.test2")) == { assert set(automation.devices_in_automation(hass, "automation.test2")) == {
"trigger-device-2",
"condition-device",
"device-in-both", "device-in-both",
"device-in-last", "device-in-last",
} }

View file

@ -163,7 +163,7 @@ async def test_search(hass):
"automation": [ "automation": [
{ {
"alias": "wled_entity", "alias": "wled_entity",
"trigger": {"platform": "state", "entity_id": "sensor.trigger_1"}, "trigger": {"platform": "template", "value_template": "true"},
"action": [ "action": [
{ {
"service": "test.script", "service": "test.script",
@ -173,7 +173,7 @@ async def test_search(hass):
}, },
{ {
"alias": "wled_device", "alias": "wled_device",
"trigger": {"platform": "state", "entity_id": "sensor.trigger_1"}, "trigger": {"platform": "template", "value_template": "true"},
"action": [ "action": [
{ {
"domain": "light", "domain": "light",