From 41bef4b919cc9768b3a027811487cf1719f2ca20 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 18 Dec 2019 15:15:11 -0500 Subject: [PATCH] Add timer reload service. (#30015) --- homeassistant/components/timer/__init__.py | 54 ++++++++--- tests/components/timer/test_init.py | 103 ++++++++++++++++++++- 2 files changed, 140 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index de60752eada..0cc707f5a45 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -4,11 +4,12 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_ICON, CONF_NAME +from homeassistant.const import CONF_ICON, CONF_NAME, SERVICE_RELOAD import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.helpers.service import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -54,26 +55,31 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) + async def async_setup(hass, config): """Set up a timer.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - entities = [] + entities = await _async_process_config(hass, config) - for object_id, cfg in config[DOMAIN].items(): - if not cfg: - cfg = {} - - name = cfg.get(CONF_NAME) - icon = cfg.get(CONF_ICON) - duration = cfg.get(CONF_DURATION) - - entities.append(Timer(hass, object_id, name, icon, duration)) - - if not entities: - return False + async def reload_service_handler(service_call): + """Remove all input booleans and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + new_entities = await _async_process_config(hass, conf) + if new_entities: + await component.async_add_entities(new_entities) + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) component.async_register_entity_service( SERVICE_START, { @@ -87,10 +93,28 @@ async def async_setup(hass, config): component.async_register_entity_service(SERVICE_CANCEL, {}, "async_cancel") component.async_register_entity_service(SERVICE_FINISH, {}, "async_finish") - await component.async_add_entities(entities) + if entities: + await component.async_add_entities(entities) return True +async def _async_process_config(hass, config): + """Process config and create list of entities.""" + entities = [] + + for object_id, cfg in config[DOMAIN].items(): + if not cfg: + cfg = {} + + name = cfg.get(CONF_NAME) + icon = cfg.get(CONF_ICON) + duration = cfg.get(CONF_DURATION) + + entities.append(Timer(hass, object_id, name, icon, duration)) + + return entities + + class Timer(RestoreEntity): """Representation of a timer.""" diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 39648b68fd7..547f7e4ab05 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -3,6 +3,9 @@ import asyncio from datetime import timedelta import logging +from unittest.mock import patch + +import pytest from homeassistant.components.timer import ( ATTR_DURATION, @@ -23,8 +26,14 @@ from homeassistant.components.timer import ( STATUS_IDLE, STATUS_PAUSED, ) -from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON, CONF_ENTITY_ID -from homeassistant.core import CoreState +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + ATTR_ICON, + CONF_ENTITY_ID, + SERVICE_RELOAD, +) +from homeassistant.core import Context, CoreState +from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -195,3 +204,93 @@ def test_no_initial_state_and_no_restore_state(hass): state = hass.states.get("timer.test1") assert state assert state.state == STATUS_IDLE + + +async def test_config_reload(hass, hass_admin_user, hass_read_only_user): + """Test reload service.""" + count_start = len(hass.states.async_entity_ids()) + + _LOGGER.debug("ENTITIES @ start: %s", hass.states.async_entity_ids()) + + config = { + DOMAIN: { + "test_1": {}, + "test_2": { + CONF_NAME: "Hello World", + CONF_ICON: "mdi:work", + CONF_DURATION: 10, + }, + } + } + + assert await async_setup_component(hass, "timer", config) + await hass.async_block_till_done() + + assert count_start + 2 == len(hass.states.async_entity_ids()) + await hass.async_block_till_done() + + state_1 = hass.states.get("timer.test_1") + state_2 = hass.states.get("timer.test_2") + state_3 = hass.states.get("timer.test_3") + + assert state_1 is not None + assert state_2 is not None + assert state_3 is None + + assert STATUS_IDLE == state_1.state + assert ATTR_ICON not in state_1.attributes + assert ATTR_FRIENDLY_NAME not in state_1.attributes + + assert STATUS_IDLE == state_2.state + assert "Hello World" == state_2.attributes.get(ATTR_FRIENDLY_NAME) + assert "mdi:work" == state_2.attributes.get(ATTR_ICON) + assert "0:00:10" == state_2.attributes.get(ATTR_DURATION) + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + DOMAIN: { + "test_2": { + CONF_NAME: "Hello World reloaded", + CONF_ICON: "mdi:work-reloaded", + CONF_DURATION: 20, + }, + "test_3": {}, + } + }, + ): + with patch("homeassistant.config.find_config_file", return_value=""): + with pytest.raises(Unauthorized): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_read_only_user.id), + ) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("timer.test_1") + state_2 = hass.states.get("timer.test_2") + state_3 = hass.states.get("timer.test_3") + + assert state_1 is None + assert state_2 is not None + assert state_3 is not None + + assert STATUS_IDLE == state_2.state + assert "Hello World reloaded" == state_2.attributes.get(ATTR_FRIENDLY_NAME) + assert "mdi:work-reloaded" == state_2.attributes.get(ATTR_ICON) + assert "0:00:20" == state_2.attributes.get(ATTR_DURATION) + + assert STATUS_IDLE == state_3.state + assert ATTR_ICON not in state_3.attributes + assert ATTR_FRIENDLY_NAME not in state_3.attributes