diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 66b04109640..39b04f6d3ea 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -26,6 +26,36 @@ from homeassistant.helpers import ( from homeassistant.helpers.state import HASS_DOMAIN, async_reproduce_state from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, STATES, Scene + +def _convert_states(states): + """Convert state definitions to State objects.""" + result = {} + + for entity_id in states: + entity_id = cv.entity_id(entity_id) + + if isinstance(states[entity_id], dict): + entity_attrs = states[entity_id].copy() + state = entity_attrs.pop(ATTR_STATE, None) + attributes = entity_attrs + else: + state = states[entity_id] + attributes = {} + + # YAML translates 'on' to a boolean + # http://yaml.org/type/bool.html + if isinstance(state, bool): + state = STATE_ON if state else STATE_OFF + elif not isinstance(state, str): + raise vol.Invalid(f"State for {entity_id} should be a string") + + result[entity_id] = State(entity_id, state, attributes) + + return result + + +STATES_SCHEMA = vol.All(dict, _convert_states) + PLATFORM_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): HASS_DOMAIN, @@ -34,9 +64,7 @@ PLATFORM_SCHEMA = vol.Schema( [ { vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ENTITIES): { - cv.entity_id: vol.Any(str, bool, dict) - }, + vol.Required(CONF_ENTITIES): STATES_SCHEMA, } ], ), @@ -44,6 +72,7 @@ PLATFORM_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +SERVICE_APPLY = "apply" SCENECONFIG = namedtuple("SceneConfig", [CONF_NAME, STATES]) _LOGGER = logging.getLogger(__name__) @@ -87,6 +116,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= SCENE_DOMAIN, SERVICE_RELOAD, reload_config ) + async def apply_service(call): + """Apply a scene.""" + await async_reproduce_state( + hass, call.data[CONF_ENTITIES].values(), blocking=True, context=call.context + ) + + hass.services.async_register( + SCENE_DOMAIN, + SERVICE_APPLY, + apply_service, + vol.Schema({vol.Required(CONF_ENTITIES): STATES_SCHEMA}), + ) + def _process_scenes_config(hass, async_add_entities, config): """Process multiple scenes and add them.""" @@ -97,41 +139,11 @@ def _process_scenes_config(hass, async_add_entities, config): return async_add_entities( - HomeAssistantScene(hass, _process_scene_config(scene)) for scene in scene_config + HomeAssistantScene(hass, SCENECONFIG(scene[CONF_NAME], scene[CONF_ENTITIES])) + for scene in scene_config ) -def _process_scene_config(scene_config): - """Process passed in config into a format to work with. - - Async friendly. - """ - name = scene_config.get(CONF_NAME) - - states = {} - c_entities = dict(scene_config.get(CONF_ENTITIES, {})) - - for entity_id in c_entities: - if isinstance(c_entities[entity_id], dict): - entity_attrs = c_entities[entity_id].copy() - state = entity_attrs.pop(ATTR_STATE, None) - attributes = entity_attrs - else: - state = c_entities[entity_id] - attributes = {} - - # YAML translates 'on' to a boolean - # http://yaml.org/type/bool.html - if isinstance(state, bool): - state = STATE_ON if state else STATE_OFF - else: - state = str(state) - - states[entity_id.lower()] = State(entity_id, state, attributes) - - return SCENECONFIG(name, states) - - class HomeAssistantScene(Scene): """A scene is a group of entities and the states we want them to be.""" @@ -148,8 +160,13 @@ class HomeAssistantScene(Scene): @property def device_state_attributes(self): """Return the scene state attributes.""" - return {ATTR_ENTITY_ID: list(self.scene_config.states.keys())} + return {ATTR_ENTITY_ID: list(self.scene_config.states)} async def async_activate(self): """Activate scene. Try to get entities into requested state.""" - await async_reproduce_state(self.hass, self.scene_config.states.values(), True) + await async_reproduce_state( + self.hass, + self.scene_config.states.values(), + blocking=True, + context=self._context, + ) diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 1f71a24c304..ec2dc3118a9 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -1,5 +1,4 @@ """Allow users to set and activate scenes.""" -import asyncio import importlib import logging @@ -7,7 +6,6 @@ import voluptuous as vol from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.state import HASS_DOMAIN @@ -69,20 +67,7 @@ async def async_setup(hass, config): HA_DOMAIN, {"platform": "homeasistant", STATES: []} ) - async def async_handle_scene_service(service): - """Handle calls to the switch services.""" - target_scenes = await component.async_extract_from_service(service) - - tasks = [scene.async_activate() for scene in target_scenes] - if tasks: - await asyncio.wait(tasks) - - hass.services.async_register( - DOMAIN, - SERVICE_TURN_ON, - async_handle_scene_service, - schema=ENTITY_SERVICE_SCHEMA, - ) + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_activate") return True diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml index ee255affe44..0f1e7103aaf 100644 --- a/homeassistant/components/scene/services.yaml +++ b/homeassistant/components/scene/services.yaml @@ -5,4 +5,18 @@ turn_on: fields: entity_id: description: Name(s) of scenes to turn on - example: 'scene.romantic' + example: "scene.romantic" + +reload: + description: Reload the scene configuration + +apply: + description: Activate a scene. Takes same data as the entities field from a single scene in the config. + fields: + entities: + description: The entities and the state that they need to be. + example: + light.kitchen: "on" + light.ceiling: + state: "on" + brightness: 80 diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index 02c018a0b49..c7c3f2bc5d5 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -28,3 +28,26 @@ async def test_reload_config_service(hass): assert hass.states.get("scene.hallo") is None assert hass.states.get("scene.bye") is not None + + +async def test_apply_service(hass): + """Test the apply service.""" + assert await async_setup_component(hass, "scene", {}) + assert await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) + + assert await hass.services.async_call( + "scene", "apply", {"entities": {"light.bed_light": "off"}}, blocking=True + ) + + assert hass.states.get("light.bed_light").state == "off" + + assert await hass.services.async_call( + "scene", + "apply", + {"entities": {"light.bed_light": {"state": "on", "brightness": 50}}}, + blocking=True, + ) + + state = hass.states.get("light.bed_light") + assert state.state == "on" + assert state.attributes["brightness"] == 50