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: Martin Hjelmare <marhje52@kth.se>

* Address comments
This commit is contained in:
Paulus Schoutsen 2019-08-05 14:04:20 -07:00
parent b011dd0b02
commit eceef82ffa
9 changed files with 153 additions and 57 deletions

View File

@ -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.

View File

@ -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."""

View File

@ -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
)

View File

@ -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
)

View File

@ -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

View File

@ -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

View File

@ -5,55 +5,55 @@ from setuptools import setup, find_packages
import homeassistant.const as hass_const
PROJECT_NAME = 'Home Assistant'
PROJECT_PACKAGE_NAME = 'homeassistant'
PROJECT_LICENSE = 'Apache License 2.0'
PROJECT_AUTHOR = 'The Home Assistant Authors'
PROJECT_COPYRIGHT = ' 2013-{}, {}'.format(dt.now().year, PROJECT_AUTHOR)
PROJECT_URL = 'https://home-assistant.io/'
PROJECT_EMAIL = 'hello@home-assistant.io'
PROJECT_NAME = "Home Assistant"
PROJECT_PACKAGE_NAME = "homeassistant"
PROJECT_LICENSE = "Apache License 2.0"
PROJECT_AUTHOR = "The Home Assistant Authors"
PROJECT_COPYRIGHT = " 2013-{}, {}".format(dt.now().year, PROJECT_AUTHOR)
PROJECT_URL = "https://home-assistant.io/"
PROJECT_EMAIL = "hello@home-assistant.io"
PROJECT_GITHUB_USERNAME = 'home-assistant'
PROJECT_GITHUB_REPOSITORY = 'home-assistant'
PROJECT_GITHUB_USERNAME = "home-assistant"
PROJECT_GITHUB_REPOSITORY = "home-assistant"
PYPI_URL = 'https://pypi.python.org/pypi/{}'.format(PROJECT_PACKAGE_NAME)
GITHUB_PATH = '{}/{}'.format(
PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY)
GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH)
PYPI_URL = "https://pypi.python.org/pypi/{}".format(PROJECT_PACKAGE_NAME)
GITHUB_PATH = "{}/{}".format(PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY)
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 = {
'Bug Reports': '{}/issues'.format(GITHUB_URL),
'Dev Docs': 'https://developers.home-assistant.io/',
'Discord': 'https://discordapp.com/invite/c5DvZ4e',
'Forum': 'https://community.home-assistant.io/',
"Bug Reports": "{}/issues".format(GITHUB_URL),
"Dev Docs": "https://developers.home-assistant.io/",
"Discord": "https://discordapp.com/invite/c5DvZ4e",
"Forum": "https://community.home-assistant.io/",
}
PACKAGES = find_packages(exclude=['tests', 'tests.*'])
PACKAGES = find_packages(exclude=["tests", "tests.*"])
REQUIRES = [
'aiohttp==3.5.4',
'astral==1.10.1',
'async_timeout==3.0.1',
'attrs==19.1.0',
'bcrypt==3.1.7',
'certifi>=2019.6.16',
'importlib-metadata==0.18',
'jinja2>=2.10.1',
'PyJWT==1.7.1',
"aiohttp==3.5.4",
"astral==1.10.1",
"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",
# PyJWT has loose dependency. We want the latest one.
'cryptography==2.7',
'pip>=8.0.3',
'python-slugify==3.0.2',
'pytz>=2019.01',
'pyyaml==5.1.1',
'requests==2.22.0',
'ruamel.yaml==0.15.99',
'voluptuous==0.11.5',
'voluptuous-serialize==2.1.0',
"cryptography==2.7",
"pip>=8.0.3",
"python-slugify==3.0.2",
"pytz>=2019.01",
"pyyaml==5.1.1",
"requests==2.22.0",
"ruamel.yaml==0.15.99",
"voluptuous==0.11.5",
"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(
name=PROJECT_PACKAGE_NAME,
@ -67,11 +67,7 @@ setup(
include_package_data=True,
zip_safe=False,
install_requires=REQUIRES,
python_requires='>={}'.format(MIN_PY_VERSION),
test_suite='tests',
entry_points={
'console_scripts': [
'hass = homeassistant.__main__:main'
]
},
python_requires=">={}".format(MIN_PY_VERSION),
test_suite="tests",
entry_points={"console_scripts": ["hass = homeassistant.__main__:main"]},
)

View File

@ -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

View File

@ -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(