diff --git a/homeassistant/components/automation/blueprints/motion_light.yaml b/homeassistant/components/automation/blueprints/motion_light.yaml index aa787e3b2b5..c923778156d 100644 --- a/homeassistant/components/automation/blueprints/motion_light.yaml +++ b/homeassistant/components/automation/blueprints/motion_light.yaml @@ -9,11 +9,12 @@ blueprint: entity: domain: binary_sensor device_class: motion - light_entity: + light_target: name: Light selector: - entity: - domain: light + target: + entity: + domain: light # If motion is detected within the 120s delay, # we restart the script. @@ -28,7 +29,7 @@ trigger: action: - service: homeassistant.turn_on - entity_id: !placeholder light_entity + target: !placeholder light_target - wait_for_trigger: platform: state entity_id: !placeholder motion_entity @@ -36,4 +37,4 @@ action: to: "off" - delay: 120 - service: homeassistant.turn_off - entity_id: !placeholder light_entity + target: !placeholder light_target diff --git a/homeassistant/const.py b/homeassistant/const.py index 8917aa003a4..469c0fa7fbb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -170,6 +170,7 @@ CONF_STATE = "state" CONF_STATE_TEMPLATE = "state_template" CONF_STRUCTURE = "structure" CONF_SWITCHES = "switches" +CONF_TARGET = "target" CONF_TEMPERATURE_UNIT = "temperature_unit" CONF_TIMEOUT = "timeout" CONF_TIME_ZONE = "time_zone" diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index af42b6373ba..37d7f312d94 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -62,6 +62,7 @@ from homeassistant.const import ( CONF_SERVICE, CONF_SERVICE_TEMPLATE, CONF_STATE, + CONF_TARGET, CONF_TIMEOUT, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, @@ -881,7 +882,10 @@ PLATFORM_SCHEMA = vol.Schema( PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) -ENTITY_SERVICE_FIELDS = (ATTR_ENTITY_ID, ATTR_AREA_ID) +ENTITY_SERVICE_FIELDS = { + vol.Optional(ATTR_ENTITY_ID): comp_entity_ids, + vol.Optional(ATTR_AREA_ID): vol.Any(ENTITY_MATCH_NONE, vol.All(ensure_list, [str])), +} def make_entity_service_schema( @@ -892,10 +896,7 @@ def make_entity_service_schema( vol.Schema( { **schema, - vol.Optional(ATTR_ENTITY_ID): comp_entity_ids, - vol.Optional(ATTR_AREA_ID): vol.Any( - ENTITY_MATCH_NONE, vol.All(ensure_list, [str]) - ), + **ENTITY_SERVICE_FIELDS, }, extra=extra, ), @@ -942,6 +943,7 @@ SERVICE_SCHEMA = vol.All( vol.Optional("data"): vol.All(dict, template_complex), vol.Optional("data_template"): vol.All(dict, template_complex), vol.Optional(CONF_ENTITY_ID): comp_entity_ids, + vol.Optional(CONF_TARGET): ENTITY_SERVICE_FIELDS, } ), has_at_least_one_key(CONF_SERVICE, CONF_SERVICE_TEMPLATE), diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 7ddd751753e..d81fa396c0b 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -111,3 +111,13 @@ class TimeSelector(Selector): """Selector of a time value.""" CONFIG_SCHEMA = vol.Schema({}) + + +@SELECTORS.register("target") +class TargetSelector(Selector): + """Selector of a target value (area ID, device ID, entity ID etc). + + Value should follow cv.ENTITY_SERVICE_FIELDS format. + """ + + CONFIG_SCHEMA = vol.Schema({"entity": {"domain": str}}) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index c3700581b40..7aa4ac8b013 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -24,6 +24,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_SERVICE, CONF_SERVICE_TEMPLATE, + CONF_TARGET, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE, ) @@ -136,6 +137,10 @@ def async_prepare_call_from_config( domain, service = domain_service.split(".", 1) service_data = {} + + if CONF_TARGET in config: + service_data.update(config[CONF_TARGET]) + for conf in [CONF_SERVICE_DATA, CONF_SERVICE_DATA_TEMPLATE]: if conf not in config: continue diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py index a601998a00d..a455d1de5b5 100644 --- a/tests/components/automation/test_blueprint.py +++ b/tests/components/automation/test_blueprint.py @@ -123,7 +123,7 @@ async def test_motion_light(hass): "use_blueprint": { "path": "motion_light.yaml", "input": { - "light_entity": "light.kitchen", + "light_target": {"entity_id": "light.kitchen"}, "motion_entity": "binary_sensor.kitchen", }, } diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 9d2d57cd6d0..2c2034147a1 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -117,3 +117,16 @@ def test_boolean_selector_schema(schema): def test_time_selector_schema(schema): """Test time selector.""" selector.validate_selector({"time": schema}) + + +@pytest.mark.parametrize( + "schema", + ( + {}, + {"entity": {}}, + {"entity": {"domain": "light"}}, + ), +) +def test_target_selector_schema(schema): + """Test entity selector.""" + selector.validate_selector({"target": schema}) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 6f2cd4ba130..93125fba96d 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -175,18 +175,25 @@ class TestServiceHelpers(unittest.TestCase): "entity_id": "hello.world", "data": { "hello": "{{ 'goodbye' }}", - "data": {"value": "{{ 'complex' }}", "simple": "simple"}, + "effect": {"value": "{{ 'complex' }}", "simple": "simple"}, }, "data_template": {"list": ["{{ 'list' }}", "2"]}, + "target": {"area_id": "test-area-id", "entity_id": "will.be_overridden"}, } service.call_from_config(self.hass, config) self.hass.block_till_done() - assert self.calls[0].data["hello"] == "goodbye" - assert self.calls[0].data["data"]["value"] == "complex" - assert self.calls[0].data["data"]["simple"] == "simple" - assert self.calls[0].data["list"][0] == "list" + assert dict(self.calls[0].data) == { + "hello": "goodbye", + "effect": { + "value": "complex", + "simple": "simple", + }, + "list": ["list", "2"], + "entity_id": ["hello.world"], + "area_id": ["test-area-id"], + } def test_service_template_service_call(self): """Test legacy service_template call with templating."""