diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index de8a4dc88e7..66b04109640 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -1,5 +1,6 @@ """Allow users to set and activate scenes.""" from collections import namedtuple +import logging import voluptuous as vol @@ -11,12 +12,19 @@ from homeassistant.const import ( CONF_PLATFORM, STATE_OFF, STATE_ON, + SERVICE_RELOAD, +) +from homeassistant.core import State, DOMAIN +from homeassistant import config as conf_util +from homeassistant.exceptions import HomeAssistantError +from homeassistant.loader import async_get_integration +from homeassistant.helpers import ( + config_per_platform, + config_validation as cv, + entity_platform, ) -from homeassistant.core import State -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.state import HASS_DOMAIN, async_reproduce_state -from homeassistant.components.scene import STATES, Scene - +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, STATES, Scene PLATFORM_SCHEMA = vol.Schema( { @@ -37,19 +45,63 @@ PLATFORM_SCHEMA = vol.Schema( ) SCENECONFIG = namedtuple("SceneConfig", [CONF_NAME, STATES]) +_LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up home assistant scene entries.""" - scene_config = config.get(STATES) + _process_scenes_config(hass, async_add_entities, config) + + # This platform can be loaded multiple times. Only first time register the service. + if hass.services.has_service(SCENE_DOMAIN, SERVICE_RELOAD): + return + + # Store platform for later. + platform = entity_platform.current_platform.get() + + async def reload_config(call): + """Reload the scene config.""" + try: + conf = await conf_util.async_hass_config_yaml(hass) + except HomeAssistantError as err: + _LOGGER.error(err) + return + + integration = await async_get_integration(hass, SCENE_DOMAIN) + + conf = await conf_util.async_process_component_config(hass, conf, integration) + + if not conf or not platform: + return + + await platform.async_reset() + + # Extract only the config for the Home Assistant platform, ignore the rest. + for p_type, p_config in config_per_platform(conf, SCENE_DOMAIN): + if p_type != DOMAIN: + continue + + _process_scenes_config(hass, async_add_entities, p_config) + + hass.helpers.service.async_register_admin_service( + SCENE_DOMAIN, SERVICE_RELOAD, reload_config + ) + + +def _process_scenes_config(hass, async_add_entities, config): + """Process multiple scenes and add them.""" + scene_config = config[STATES] + + # Check empty list + if not scene_config: + return async_add_entities( - HomeAssistantScene(hass, _process_config(scene)) for scene in scene_config + HomeAssistantScene(hass, _process_scene_config(scene)) for scene in scene_config ) - return True -def _process_config(scene_config): +def _process_scene_config(scene_config): """Process passed in config into a format to work with. Async friendly. diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 0d00c2c5ea2..5ddb1116d8f 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -5,6 +5,7 @@ import logging 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 @@ -60,6 +61,10 @@ async def async_setup(hass, config): component = hass.data[DOMAIN] = EntityComponent(logger, DOMAIN, hass) await component.async_setup(config) + # Ensure Home Assistant platform always loaded. + await component.async_setup_platform( + HA_DOMAIN, {"platform": "homeasistant", STATES: []} + ) async def async_handle_scene_service(service): """Handle calls to the switch services.""" diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index ed1b41a0abd..b28beeaea72 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -114,7 +114,7 @@ class EntityComponent: # Look in config for Domain, Domain 2, Domain 3 etc and load them tasks = [] for p_type, p_config in config_per_platform(config, self.domain): - tasks.append(self._async_setup_platform(p_type, p_config)) + tasks.append(self.async_setup_platform(p_type, p_config)) if tasks: await asyncio.wait(tasks) @@ -123,7 +123,7 @@ class EntityComponent: # Refer to: homeassistant.components.discovery.load_platform() async def component_platform_discovered(platform, info): """Handle the loading of a platform.""" - await self._async_setup_platform(platform, {}, info) + await self.async_setup_platform(platform, {}, info) discovery.async_listen_platform( self.hass, self.domain, component_platform_discovered @@ -212,10 +212,13 @@ class EntityComponent: self.hass.services.async_register(self.domain, name, handle_service, schema) - async def _async_setup_platform( + async def async_setup_platform( self, platform_type, platform_config, discovery_info=None ): """Set up a platform for this component.""" + if self.config is None: + raise RuntimeError("async_setup needs to be called first") + platform = await async_prepare_setup_platform( self.hass, self.config, self.domain, platform_type ) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 5012f578106..ea71828f21a 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -1,5 +1,7 @@ """Class to manage the entities for a single platform.""" import asyncio +from contextvars import ContextVar +from typing import Optional from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import callback, valid_entity_id, split_entity_id @@ -127,6 +129,7 @@ class EntityPlatform: async_create_setup_task creates a coroutine that sets up platform. """ + current_platform.set(self) logger = self.logger hass = self.hass full_name = "{}.{}".format(self.domain, self.platform_name) @@ -457,3 +460,8 @@ class EntityPlatform: if tasks: await asyncio.wait(tasks) + + +current_platform: ContextVar[Optional[EntityPlatform]] = ContextVar( + "current_platform", default=None +) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 13c5b1a0144..89465568c65 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -7,6 +7,7 @@ async_timeout==3.0.1 attrs==19.1.0 bcrypt==3.1.7 certifi>=2019.6.16 +contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.16 diff --git a/requirements_all.txt b/requirements_all.txt index 4062cdc501f..e3ff35ad977 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,6 +5,7 @@ async_timeout==3.0.1 attrs==19.1.0 bcrypt==3.1.7 certifi>=2019.6.16 +contextvars==2.4;python_version<"3.7" importlib-metadata==0.18 jinja2>=2.10.1 PyJWT==1.7.1 diff --git a/setup.py b/setup.py index 5133ce9c16b..da50b5f988c 100755 --- a/setup.py +++ b/setup.py @@ -37,6 +37,7 @@ REQUIRES = [ "attrs==19.1.0", "bcrypt==3.1.7", "certifi>=2019.6.16", + 'contextvars==2.4;python_version<"3.7"', "importlib-metadata==0.18", "jinja2>=2.10.1", "PyJWT==1.7.1", diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py new file mode 100644 index 00000000000..02c018a0b49 --- /dev/null +++ b/tests/components/homeassistant/test_scene.py @@ -0,0 +1,30 @@ +"""Test Home Assistant scenes.""" +from unittest.mock import patch + +from homeassistant.setup import async_setup_component + + +async def test_reload_config_service(hass): + """Test the reload config service.""" + assert await async_setup_component(hass, "scene", {}) + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={"scene": {"name": "Hallo", "entities": {"light.kitchen": "on"}}}, + ), patch("homeassistant.config.find_config_file", return_value=""): + await hass.services.async_call("scene", "reload", blocking=True) + await hass.async_block_till_done() + + assert hass.states.get("scene.hallo") is not None + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={"scene": {"name": "Bye", "entities": {"light.kitchen": "on"}}}, + ), patch("homeassistant.config.find_config_file", return_value=""): + await hass.services.async_call("scene", "reload", blocking=True) + await hass.async_block_till_done() + + assert hass.states.get("scene.hallo") is None + assert hass.states.get("scene.bye") is not None diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 3dd6ca8b55f..0d52f430ff5 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -116,7 +116,7 @@ async def test_setup_recovers_when_setup_raises(hass): @asynctest.patch( - "homeassistant.helpers.entity_component.EntityComponent" "._async_setup_platform", + "homeassistant.helpers.entity_component.EntityComponent" ".async_setup_platform", return_value=mock_coro(), ) @asynctest.patch(