Skip to content
Snippets Groups Projects
Commit eceef82f authored by Paulus Schoutsen's avatar Paulus Schoutsen
Browse files

Add service to reload scenes from configuration.yaml (#25680)


* Allow reloading scenes

* Update requirements

* address comments

* fix typing

* fix tests

* Update homeassistant/components/homeassistant/scene.py

Co-Authored-By: default avatarMartin Hjelmare <marhje52@kth.se>

* Address comments
parent b011dd0b
No related branches found
No related tags found
No related merge requests found
"""Allow users to set and activate scenes.""" """Allow users to set and activate scenes."""
from collections import namedtuple from collections import namedtuple
import logging
import voluptuous as vol import voluptuous as vol
...@@ -11,12 +12,19 @@ from homeassistant.const import ( ...@@ -11,12 +12,19 @@ from homeassistant.const import (
CONF_PLATFORM, CONF_PLATFORM,
STATE_OFF, STATE_OFF,
STATE_ON, 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.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( PLATFORM_SCHEMA = vol.Schema(
{ {
...@@ -37,19 +45,63 @@ PLATFORM_SCHEMA = vol.Schema( ...@@ -37,19 +45,63 @@ PLATFORM_SCHEMA = vol.Schema(
) )
SCENECONFIG = namedtuple("SceneConfig", [CONF_NAME, STATES]) SCENECONFIG = namedtuple("SceneConfig", [CONF_NAME, STATES])
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up home assistant scene entries.""" """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( 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. """Process passed in config into a format to work with.
Async friendly. Async friendly.
......
...@@ -5,6 +5,7 @@ import logging ...@@ -5,6 +5,7 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON
from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
...@@ -60,6 +61,10 @@ async def async_setup(hass, config): ...@@ -60,6 +61,10 @@ async def async_setup(hass, config):
component = hass.data[DOMAIN] = EntityComponent(logger, DOMAIN, hass) component = hass.data[DOMAIN] = EntityComponent(logger, DOMAIN, hass)
await component.async_setup(config) 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): async def async_handle_scene_service(service):
"""Handle calls to the switch services.""" """Handle calls to the switch services."""
......
...@@ -114,7 +114,7 @@ class EntityComponent: ...@@ -114,7 +114,7 @@ class EntityComponent:
# Look in config for Domain, Domain 2, Domain 3 etc and load them # Look in config for Domain, Domain 2, Domain 3 etc and load them
tasks = [] tasks = []
for p_type, p_config in config_per_platform(config, self.domain): 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: if tasks:
await asyncio.wait(tasks) await asyncio.wait(tasks)
...@@ -123,7 +123,7 @@ class EntityComponent: ...@@ -123,7 +123,7 @@ class EntityComponent:
# Refer to: homeassistant.components.discovery.load_platform() # Refer to: homeassistant.components.discovery.load_platform()
async def component_platform_discovered(platform, info): async def component_platform_discovered(platform, info):
"""Handle the loading of a platform.""" """Handle the loading of a platform."""
await self._async_setup_platform(platform, {}, info) await self.async_setup_platform(platform, {}, info)
discovery.async_listen_platform( discovery.async_listen_platform(
self.hass, self.domain, component_platform_discovered self.hass, self.domain, component_platform_discovered
...@@ -212,10 +212,13 @@ class EntityComponent: ...@@ -212,10 +212,13 @@ class EntityComponent:
self.hass.services.async_register(self.domain, name, handle_service, schema) 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 self, platform_type, platform_config, discovery_info=None
): ):
"""Set up a platform for this component.""" """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( platform = await async_prepare_setup_platform(
self.hass, self.config, self.domain, platform_type self.hass, self.config, self.domain, platform_type
) )
......
"""Class to manage the entities for a single platform.""" """Class to manage the entities for a single platform."""
import asyncio import asyncio
from contextvars import ContextVar
from typing import Optional
from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.const import DEVICE_DEFAULT_NAME
from homeassistant.core import callback, valid_entity_id, split_entity_id from homeassistant.core import callback, valid_entity_id, split_entity_id
...@@ -127,6 +129,7 @@ class EntityPlatform: ...@@ -127,6 +129,7 @@ class EntityPlatform:
async_create_setup_task creates a coroutine that sets up platform. async_create_setup_task creates a coroutine that sets up platform.
""" """
current_platform.set(self)
logger = self.logger logger = self.logger
hass = self.hass hass = self.hass
full_name = "{}.{}".format(self.domain, self.platform_name) full_name = "{}.{}".format(self.domain, self.platform_name)
...@@ -457,3 +460,8 @@ class EntityPlatform: ...@@ -457,3 +460,8 @@ class EntityPlatform:
if tasks: if tasks:
await asyncio.wait(tasks) await asyncio.wait(tasks)
current_platform: ContextVar[Optional[EntityPlatform]] = ContextVar(
"current_platform", default=None
)
...@@ -7,6 +7,7 @@ async_timeout==3.0.1 ...@@ -7,6 +7,7 @@ async_timeout==3.0.1
attrs==19.1.0 attrs==19.1.0
bcrypt==3.1.7 bcrypt==3.1.7
certifi>=2019.6.16 certifi>=2019.6.16
contextvars==2.4;python_version<"3.7"
cryptography==2.7 cryptography==2.7
distro==1.4.0 distro==1.4.0
hass-nabucasa==0.16 hass-nabucasa==0.16
......
...@@ -5,6 +5,7 @@ async_timeout==3.0.1 ...@@ -5,6 +5,7 @@ async_timeout==3.0.1
attrs==19.1.0 attrs==19.1.0
bcrypt==3.1.7 bcrypt==3.1.7
certifi>=2019.6.16 certifi>=2019.6.16
contextvars==2.4;python_version<"3.7"
importlib-metadata==0.18 importlib-metadata==0.18
jinja2>=2.10.1 jinja2>=2.10.1
PyJWT==1.7.1 PyJWT==1.7.1
......
...@@ -5,55 +5,55 @@ from setuptools import setup, find_packages ...@@ -5,55 +5,55 @@ from setuptools import setup, find_packages
import homeassistant.const as hass_const import homeassistant.const as hass_const
PROJECT_NAME = 'Home Assistant' PROJECT_NAME = "Home Assistant"
PROJECT_PACKAGE_NAME = 'homeassistant' PROJECT_PACKAGE_NAME = "homeassistant"
PROJECT_LICENSE = 'Apache License 2.0' PROJECT_LICENSE = "Apache License 2.0"
PROJECT_AUTHOR = 'The Home Assistant Authors' PROJECT_AUTHOR = "The Home Assistant Authors"
PROJECT_COPYRIGHT = ' 2013-{}, {}'.format(dt.now().year, PROJECT_AUTHOR) PROJECT_COPYRIGHT = " 2013-{}, {}".format(dt.now().year, PROJECT_AUTHOR)
PROJECT_URL = 'https://home-assistant.io/' PROJECT_URL = "https://home-assistant.io/"
PROJECT_EMAIL = 'hello@home-assistant.io' PROJECT_EMAIL = "hello@home-assistant.io"
PROJECT_GITHUB_USERNAME = 'home-assistant' PROJECT_GITHUB_USERNAME = "home-assistant"
PROJECT_GITHUB_REPOSITORY = 'home-assistant' PROJECT_GITHUB_REPOSITORY = "home-assistant"
PYPI_URL = 'https://pypi.python.org/pypi/{}'.format(PROJECT_PACKAGE_NAME) PYPI_URL = "https://pypi.python.org/pypi/{}".format(PROJECT_PACKAGE_NAME)
GITHUB_PATH = '{}/{}'.format( GITHUB_PATH = "{}/{}".format(PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY)
PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY) GITHUB_URL = "https://github.com/{}".format(GITHUB_PATH)
GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH)
DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, hass_const.__version__) DOWNLOAD_URL = "{}/archive/{}.zip".format(GITHUB_URL, hass_const.__version__)
PROJECT_URLS = { PROJECT_URLS = {
'Bug Reports': '{}/issues'.format(GITHUB_URL), "Bug Reports": "{}/issues".format(GITHUB_URL),
'Dev Docs': 'https://developers.home-assistant.io/', "Dev Docs": "https://developers.home-assistant.io/",
'Discord': 'https://discordapp.com/invite/c5DvZ4e', "Discord": "https://discordapp.com/invite/c5DvZ4e",
'Forum': 'https://community.home-assistant.io/', "Forum": "https://community.home-assistant.io/",
} }
PACKAGES = find_packages(exclude=['tests', 'tests.*']) PACKAGES = find_packages(exclude=["tests", "tests.*"])
REQUIRES = [ REQUIRES = [
'aiohttp==3.5.4', "aiohttp==3.5.4",
'astral==1.10.1', "astral==1.10.1",
'async_timeout==3.0.1', "async_timeout==3.0.1",
'attrs==19.1.0', "attrs==19.1.0",
'bcrypt==3.1.7', "bcrypt==3.1.7",
'certifi>=2019.6.16', "certifi>=2019.6.16",
'importlib-metadata==0.18', 'contextvars==2.4;python_version<"3.7"',
'jinja2>=2.10.1', "importlib-metadata==0.18",
'PyJWT==1.7.1', "jinja2>=2.10.1",
"PyJWT==1.7.1",
# PyJWT has loose dependency. We want the latest one. # PyJWT has loose dependency. We want the latest one.
'cryptography==2.7', "cryptography==2.7",
'pip>=8.0.3', "pip>=8.0.3",
'python-slugify==3.0.2', "python-slugify==3.0.2",
'pytz>=2019.01', "pytz>=2019.01",
'pyyaml==5.1.1', "pyyaml==5.1.1",
'requests==2.22.0', "requests==2.22.0",
'ruamel.yaml==0.15.99', "ruamel.yaml==0.15.99",
'voluptuous==0.11.5', "voluptuous==0.11.5",
'voluptuous-serialize==2.1.0', "voluptuous-serialize==2.1.0",
] ]
MIN_PY_VERSION = '.'.join(map(str, hass_const.REQUIRED_PYTHON_VER)) MIN_PY_VERSION = ".".join(map(str, hass_const.REQUIRED_PYTHON_VER))
setup( setup(
name=PROJECT_PACKAGE_NAME, name=PROJECT_PACKAGE_NAME,
...@@ -67,11 +67,7 @@ setup( ...@@ -67,11 +67,7 @@ setup(
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,
install_requires=REQUIRES, install_requires=REQUIRES,
python_requires='>={}'.format(MIN_PY_VERSION), python_requires=">={}".format(MIN_PY_VERSION),
test_suite='tests', test_suite="tests",
entry_points={ entry_points={"console_scripts": ["hass = homeassistant.__main__:main"]},
'console_scripts': [
'hass = homeassistant.__main__:main'
]
},
) )
"""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
...@@ -116,7 +116,7 @@ async def test_setup_recovers_when_setup_raises(hass): ...@@ -116,7 +116,7 @@ async def test_setup_recovers_when_setup_raises(hass):
@asynctest.patch( @asynctest.patch(
"homeassistant.helpers.entity_component.EntityComponent" "._async_setup_platform", "homeassistant.helpers.entity_component.EntityComponent" ".async_setup_platform",
return_value=mock_coro(), return_value=mock_coro(),
) )
@asynctest.patch( @asynctest.patch(
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment