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 committed by Pascal Vizeli
parent 0449132c35
commit 7a90808e52
9 changed files with 113 additions and 12 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

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

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(