From 810df38f0d9b322380e7044e74907477a9d7fc37 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Aug 2020 18:13:43 -0500 Subject: [PATCH] Add the ability to reload light/cover groups from yaml (#39250) * Add the ability to reload light/cover groups from yaml Update previous usage to reduce code duplication. * Fix conflict from rebase --- homeassistant/components/group/__init__.py | 5 ++ .../template/alarm_control_panel.py | 5 +- .../components/template/binary_sensor.py | 6 +-- homeassistant/components/template/cover.py | 6 +-- homeassistant/components/template/fan.py | 6 +-- homeassistant/components/template/light.py | 6 +-- homeassistant/components/template/lock.py | 6 +-- homeassistant/components/template/sensor.py | 6 +-- homeassistant/components/template/switch.py | 6 +-- homeassistant/components/template/vacuum.py | 10 ++-- .../components/universal/media_player.py | 24 +-------- homeassistant/helpers/reload.py | 37 +++++++++++-- tests/components/group/test_light.py | 52 ++++++++++++++++++- .../components/universal/test_media_player.py | 5 +- tests/fixtures/group/configuration.yaml | 11 ++++ tests/helpers/test_reload.py | 40 ++++++++++++++ 16 files changed, 175 insertions(+), 56 deletions(-) create mode 100644 tests/fixtures/group/configuration.yaml diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 67f8096134e4..1dc9a1870309 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -34,6 +34,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.reload import async_reload_integration_platforms from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass @@ -57,6 +58,8 @@ ATTR_ALL = "all" SERVICE_SET = "set" SERVICE_REMOVE = "remove" +PLATFORMS = ["light", "cover"] + _LOGGER = logging.getLogger(__name__) @@ -219,6 +222,8 @@ async def async_setup(hass, config): await component.async_add_entities(auto) + await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS) + hass.services.async_register( DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({}) ) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 292c359d3345..e37c7e2982e9 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -31,9 +31,10 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.script import Script -from . import async_setup_reload_service +from .const import DOMAIN, PLATFORMS from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -112,7 +113,7 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Template Alarm Control Panels.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 5ea04d67207d..a15226e7e642 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -24,10 +24,10 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.template import result_as_boolean -from . import async_setup_reload_service -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -97,7 +97,7 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template binary sensors.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 828d7790ebf8..e26c0fca8f48 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -35,10 +35,10 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.script import Script -from . import async_setup_reload_service -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -152,7 +152,7 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Template cover.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index b71367879486..01cf22c4aabe 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -32,10 +32,10 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.script import Script -from . import async_setup_reload_service -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -129,7 +129,7 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template fans.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 8fa2ae5f632c..a69c148ea8f0 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -31,10 +31,10 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.script import Script -from . import async_setup_reload_service -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -135,7 +135,7 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template lights.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 74197e6eb6d9..b917430a6ffe 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -15,10 +15,10 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.script import Script -from . import async_setup_reload_service -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -65,7 +65,7 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template lock.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index f68443649287..01c13af0ef2b 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -25,9 +25,9 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.helpers.reload import async_setup_reload_service -from . import async_setup_reload_service -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS from .template_entity import TemplateEntity CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" @@ -97,7 +97,7 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template sensors.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index 7ef540144d84..c612d3307daf 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -23,11 +23,11 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.script import Script -from . import async_setup_reload_service -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -90,7 +90,7 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template switches.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index b6b6669f7b9d..6375995ed7de 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.components.vacuum import ( ATTR_FAN_SPEED, - DOMAIN, + DOMAIN as VACUUM_DOMAIN, SERVICE_CLEAN_SPOT, SERVICE_LOCATE, SERVICE_PAUSE, @@ -41,10 +41,10 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.script import Script -from . import async_setup_reload_service -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -55,7 +55,7 @@ CONF_FAN_SPEED_LIST = "fan_speeds" CONF_FAN_SPEED_TEMPLATE = "fan_speed_template" CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" -ENTITY_ID_FORMAT = DOMAIN + ".{}" +ENTITY_ID_FORMAT = VACUUM_DOMAIN + ".{}" _VALID_STATES = [ STATE_CLEANING, STATE_DOCKED, @@ -145,7 +145,7 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template vacuums.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index aaf4464452b7..7d1ac9953b63 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -56,7 +56,6 @@ from homeassistant.const import ( SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK, SERVICE_MEDIA_STOP, - SERVICE_RELOAD, SERVICE_SHUFFLE_SET, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -72,7 +71,7 @@ from homeassistant.const import ( from homeassistant.core import EVENT_HOMEASSISTANT_START, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.reload import async_reload_integration_platforms +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.service import async_call_from_config _LOGGER = logging.getLogger(__name__) @@ -104,30 +103,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( extra=vol.REMOVE_EXTRA, ) -EVENT_UNIVERSAL_RELOADED = "event_universal_reloaded" - - -async def async_setup_reload_service(hass): - """Create the reload service for the universal domain.""" - - if hass.services.has_service("universal", SERVICE_RELOAD): - return - - async def _reload_config(call): - """Reload the template universal config.""" - - await async_reload_integration_platforms(hass, "universal", ["media_player"]) - hass.bus.async_fire(EVENT_UNIVERSAL_RELOADED, context=call.context) - - hass.helpers.service.async_register_admin_service( - "universal", SERVICE_RELOAD, _reload_config - ) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the universal media players.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, "universal", ["media_player"]) player = UniversalMediaPlayer( hass, diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index 3922f3710e23..73d78501578d 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -1,10 +1,12 @@ """Class to reload platforms.""" +import asyncio import logging -from typing import Optional +from typing import Iterable, Optional from homeassistant import config as conf_util -from homeassistant.core import callback +from homeassistant.const import SERVICE_RELOAD +from homeassistant.core import Event, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform from homeassistant.helpers.entity_platform import DATA_ENTITY_PLATFORM, EntityPlatform @@ -15,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) async def async_reload_integration_platforms( - hass: HomeAssistantType, integration_name: str, integration_platforms: str + hass: HomeAssistantType, integration_name: str, integration_platforms: Iterable ) -> None: """Reload an integration's platforms. @@ -68,3 +70,32 @@ def async_get_platform( return platform return None + + +async def async_setup_reload_service( + hass: HomeAssistantType, domain: str, platforms: Iterable +) -> None: + """Create the reload service for the domain.""" + + if hass.services.has_service(domain, SERVICE_RELOAD): + return + + async def _reload_config(call: Event) -> None: + """Reload the platforms.""" + + await async_reload_integration_platforms(hass, domain, platforms) + hass.bus.async_fire(f"event_{domain}_reloaded", context=call.context) + + hass.helpers.service.async_register_admin_service( + domain, SERVICE_RELOAD, _reload_config + ) + + +def setup_reload_service( + hass: HomeAssistantType, domain: str, platforms: Iterable +) -> None: + """Sync version of async_setup_reload_service.""" + + asyncio.run_coroutine_threadsafe( + async_setup_reload_service(hass, domain, platforms), hass.loop, + ).result() diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 8c659a5ebf6a..5ca008ba91fc 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -1,5 +1,10 @@ """The tests for the Group Light platform.""" -from homeassistant.components.group import DOMAIN +from os import path + +from asynctest.mock import patch + +from homeassistant import config as hass_config +from homeassistant.components.group import DOMAIN, SERVICE_RELOAD import homeassistant.components.group.light as group from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -632,3 +637,48 @@ async def test_invalid_service_calls(hass): mock_call.assert_called_once_with( LIGHT_DOMAIN, SERVICE_TURN_ON, data, blocking=True, context=None ) + + +async def test_reload(hass): + """Test the ability to reload lights.""" + await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "demo"}, + { + "platform": DOMAIN, + "entities": [ + "light.bed_light", + "light.ceiling_lights", + "light.kitchen_lights", + ], + }, + ] + }, + ) + await hass.async_block_till_done() + + await hass.async_block_till_done() + await hass.async_start() + + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_ON + + yaml_path = path.join( + _get_fixtures_base_path(), "fixtures", "group/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, SERVICE_RELOAD, {}, blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("light.light_group") is None + assert hass.states.get("light.master_hall_lights_g") is not None + assert hass.states.get("light.outside_patio_lights_g") is not None + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 8f274fcc9c94..52fbab19539c 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -14,6 +14,7 @@ import homeassistant.components.media_player as media_player import homeassistant.components.switch as switch import homeassistant.components.universal.media_player as universal from homeassistant.const import ( + SERVICE_RELOAD, STATE_OFF, STATE_ON, STATE_PAUSED, @@ -818,7 +819,7 @@ async def test_master_state_with_template(hass): async def test_reload(hass): - """Test the state_template option.""" + """Test reloading the media player from yaml.""" hass.states.async_set("input_boolean.test", STATE_OFF) hass.states.async_set("media_player.mock1", STATE_OFF) @@ -863,7 +864,7 @@ async def test_reload(hass): ) with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - "universal", universal.SERVICE_RELOAD, {}, blocking=True, + "universal", SERVICE_RELOAD, {}, blocking=True, ) await hass.async_block_till_done() diff --git a/tests/fixtures/group/configuration.yaml b/tests/fixtures/group/configuration.yaml new file mode 100644 index 000000000000..9047024e3de0 --- /dev/null +++ b/tests/fixtures/group/configuration.yaml @@ -0,0 +1,11 @@ +light: + - platform: group + name: Master Hall Lights G + entities: + - light.master_hall_lights + - light.master_hall_lights_2 + - platform: group + name: Outside Patio Lights G + entities: + - light.outside_patio_lights + - light.outside_patio_lights_2 diff --git a/tests/helpers/test_reload.py b/tests/helpers/test_reload.py index d9067b4b35f7..c1062a488ee9 100644 --- a/tests/helpers/test_reload.py +++ b/tests/helpers/test_reload.py @@ -3,10 +3,12 @@ import logging from os import path from homeassistant import config +from homeassistant.const import SERVICE_RELOAD from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.reload import ( async_get_platform, async_reload_integration_platforms, + async_setup_reload_service, ) from tests.async_mock import Mock, patch @@ -59,5 +61,43 @@ async def test_reload_platform(hass): assert len(setup_called) == 2 +async def test_setup_reload_service(hass): + """Test setting up a reload service.""" + component_setup = Mock(return_value=True) + + setup_called = [] + + async def setup_platform(*args): + setup_called.append(args) + + mock_integration(hass, MockModule(DOMAIN, setup=component_setup)) + mock_integration(hass, MockModule(PLATFORM, dependencies=[DOMAIN])) + + mock_platform = MockPlatform(async_setup_platform=setup_platform) + mock_entity_platform(hass, f"{DOMAIN}.{PLATFORM}", mock_platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + await component.async_setup({DOMAIN: {"platform": PLATFORM, "sensors": None}}) + await hass.async_block_till_done() + assert component_setup.called + + assert f"{DOMAIN}.{PLATFORM}" in hass.config.components + assert len(setup_called) == 1 + + await async_setup_reload_service(hass, PLATFORM, [DOMAIN]) + + yaml_path = path.join( + _get_fixtures_base_path(), "fixtures", "helpers/reload_configuration.yaml", + ) + with patch.object(config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + PLATFORM, SERVICE_RELOAD, {}, blocking=True, + ) + await hass.async_block_till_done() + + assert len(setup_called) == 2 + + def _get_fixtures_base_path(): return path.dirname(path.dirname(__file__))