From 35a922b3a8d00821a00d153997b7be57117de328 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 15 Jun 2017 22:36:04 +0200 Subject: [PATCH] fix tests --- .../components/automation/__init__.py | 486 ------------------ tests/components/automation/test_init.py | 53 +- tests/components/switch/test_template.py | 20 +- 3 files changed, 36 insertions(+), 523 deletions(-) delete mode 100644 homeassistant/components/automation/__init__.py diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py deleted file mode 100644 index 09f0e286755..00000000000 --- a/homeassistant/components/automation/__init__.py +++ /dev/null @@ -1,486 +0,0 @@ -""" -Allow to setup simple automation rules via the config file. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/automation/ -""" -import asyncio -from functools import partial -import logging -import os - -import voluptuous as vol - -from homeassistant.setup import async_prepare_setup_platform -from homeassistant.core import CoreState -from homeassistant import config as conf_util -from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, - SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID) -from homeassistant.components import logbook -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import extract_domain_configs, script, condition -from homeassistant.helpers.entity import ToggleEntity -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.restore_state import async_get_last_state -from homeassistant.loader import get_platform -from homeassistant.util.dt import utcnow -import homeassistant.helpers.config_validation as cv -from homeassistant.components.frontend import register_built_in_panel - -DOMAIN = 'automation' -DEPENDENCIES = ['group'] -ENTITY_ID_FORMAT = DOMAIN + '.{}' - -GROUP_NAME_ALL_AUTOMATIONS = 'all automations' - -CONF_ALIAS = 'alias' -CONF_HIDE_ENTITY = 'hide_entity' - -CONF_CONDITION = 'condition' -CONF_ACTION = 'action' -CONF_TRIGGER = 'trigger' -CONF_CONDITION_TYPE = 'condition_type' -CONF_INITIAL_STATE = 'initial_state' - -CONDITION_USE_TRIGGER_VALUES = 'use_trigger_values' -CONDITION_TYPE_AND = 'and' -CONDITION_TYPE_OR = 'or' - -DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND -DEFAULT_HIDE_ENTITY = False -DEFAULT_INITIAL_STATE = True - -ATTR_LAST_TRIGGERED = 'last_triggered' -ATTR_VARIABLES = 'variables' -SERVICE_TRIGGER = 'trigger' - -_LOGGER = logging.getLogger(__name__) - - -def _platform_validator(config): - """Validate it is a valid platform.""" - platform = get_platform(DOMAIN, config[CONF_PLATFORM]) - - if not hasattr(platform, 'TRIGGER_SCHEMA'): - return config - - return getattr(platform, 'TRIGGER_SCHEMA')(config) - - -_TRIGGER_SCHEMA = vol.All( - cv.ensure_list, - [ - vol.All( - vol.Schema({ - vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN) - }, extra=vol.ALLOW_EXTRA), - _platform_validator - ), - ] -) - -_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA]) - -PLATFORM_SCHEMA = vol.Schema({ - # str on purpose - CONF_ID: str, - CONF_ALIAS: cv.string, - vol.Optional(CONF_INITIAL_STATE): cv.boolean, - vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean, - vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA, - vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA, - vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, -}) - -SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) - -TRIGGER_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Optional(ATTR_VARIABLES, default={}): dict, -}) - -RELOAD_SERVICE_SCHEMA = vol.Schema({}) - - -def is_on(hass, entity_id): - """ - Return true if specified automation entity_id is on. - - Async friendly. - """ - return hass.states.is_state(entity_id, STATE_ON) - - -def turn_on(hass, entity_id=None): - """Turn on specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) - - -def turn_off(hass, entity_id=None): - """Turn off specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) - - -def toggle(hass, entity_id=None): - """Toggle specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) - - -def trigger(hass, entity_id=None): - """Trigger specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TRIGGER, data) - - -def reload(hass): - """Reload the automation from config.""" - hass.services.call(DOMAIN, SERVICE_RELOAD) - - -def async_reload(hass): - """Reload the automation from config. - - Returns a coroutine object. - """ - return hass.services.async_call(DOMAIN, SERVICE_RELOAD) - - -@asyncio.coroutine -def async_setup(hass, config): - """Set up the automation.""" - component = EntityComponent(_LOGGER, DOMAIN, hass, - group_name=GROUP_NAME_ALL_AUTOMATIONS) - - yield from _async_process_config(hass, config, component) - - descriptions = yield from hass.async_add_job( - conf_util.load_yaml_config_file, os.path.join( - os.path.dirname(__file__), 'services.yaml') - ) - - @asyncio.coroutine - def trigger_service_handler(service_call): - """Handle automation triggers.""" - tasks = [] - for entity in component.async_extract_from_service(service_call): - tasks.append(entity.async_trigger( - service_call.data.get(ATTR_VARIABLES), True)) - - if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) - - @asyncio.coroutine - def turn_onoff_service_handler(service_call): - """Handle automation turn on/off service calls.""" - tasks = [] - method = 'async_{}'.format(service_call.service) - for entity in component.async_extract_from_service(service_call): - tasks.append(getattr(entity, method)()) - - if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) - - @asyncio.coroutine - def toggle_service_handler(service_call): - """Handle automation toggle service calls.""" - tasks = [] - for entity in component.async_extract_from_service(service_call): - if entity.is_on: - tasks.append(entity.async_turn_off()) - else: - tasks.append(entity.async_turn_on()) - - if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) - - @asyncio.coroutine - def reload_service_handler(service_call): - """Remove all automations and load new ones from config.""" - conf = yield from component.async_prepare_reload() - if conf is None: - return - yield from _async_process_config(hass, conf, component) - - hass.services.async_register( - DOMAIN, SERVICE_TRIGGER, trigger_service_handler, - descriptions.get(SERVICE_TRIGGER), schema=TRIGGER_SERVICE_SCHEMA) - - hass.services.async_register( - DOMAIN, SERVICE_RELOAD, reload_service_handler, - descriptions.get(SERVICE_RELOAD), schema=RELOAD_SERVICE_SCHEMA) - - hass.services.async_register( - DOMAIN, SERVICE_TOGGLE, toggle_service_handler, - descriptions.get(SERVICE_TOGGLE), schema=SERVICE_SCHEMA) - - for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF): - hass.services.async_register( - DOMAIN, service, turn_onoff_service_handler, - descriptions.get(service), schema=SERVICE_SCHEMA) - - if 'frontend' in hass.config.components: - register_built_in_panel(hass, 'automation', 'Automations', - 'mdi:playlist-play') - - return True - - -class AutomationEntity(ToggleEntity): - """Entity to show status of entity.""" - - def __init__(self, automation_id, name, async_attach_triggers, cond_func, - async_action, hidden, initial_state): - """Initialize an automation entity.""" - self._id = automation_id - self._name = name - self._async_attach_triggers = async_attach_triggers - self._async_detach_triggers = None - self._cond_func = cond_func - self._async_action = async_action - self._last_triggered = None - self._hidden = hidden - self._initial_state = initial_state - - @property - def name(self): - """Name of the automation.""" - return self._name - - @property - def should_poll(self): - """No polling needed for automation entities.""" - return False - - @property - def state_attributes(self): - """Return the entity state attributes.""" - return { - ATTR_LAST_TRIGGERED: self._last_triggered - } - - @property - def hidden(self) -> bool: - """Return True if the automation entity should be hidden from UIs.""" - return self._hidden - - @property - def is_on(self) -> bool: - """Return True if entity is on.""" - return self._async_detach_triggers is not None - - @asyncio.coroutine - def async_added_to_hass(self) -> None: - """Startup with initial state or previous state.""" - if self._initial_state is not None: - enable_automation = self._initial_state - _LOGGER.debug("Automation %s initial state %s from config " - "initial_state", self.entity_id, enable_automation) - else: - state = yield from async_get_last_state(self.hass, self.entity_id) - if state: - enable_automation = state.state == STATE_ON - self._last_triggered = state.attributes.get('last_triggered') - _LOGGER.debug("Automation %s initial state %s from recorder " - "last state %s", self.entity_id, - enable_automation, state) - else: - enable_automation = DEFAULT_INITIAL_STATE - _LOGGER.debug("Automation %s initial state %s from default " - "initial state", self.entity_id, - enable_automation) - - if not enable_automation: - return - - # HomeAssistant is starting up - elif self.hass.state == CoreState.not_running: - @asyncio.coroutine - def async_enable_automation(event): - """Start automation on startup.""" - yield from self.async_enable() - - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, async_enable_automation) - - # HomeAssistant is running - else: - yield from self.async_enable() - - @asyncio.coroutine - def async_turn_on(self, **kwargs) -> None: - """Turn the entity on and update the state.""" - if self.is_on: - return - - yield from self.async_enable() - - @asyncio.coroutine - def async_turn_off(self, **kwargs) -> None: - """Turn the entity off.""" - if not self.is_on: - return - - self._async_detach_triggers() - self._async_detach_triggers = None - yield from self.async_update_ha_state() - - @asyncio.coroutine - def async_trigger(self, variables, skip_condition=False): - """Trigger automation. - - This method is a coroutine. - """ - if skip_condition or self._cond_func(variables): - yield from self._async_action(self.entity_id, variables) - self._last_triggered = utcnow() - yield from self.async_update_ha_state() - - @asyncio.coroutine - def async_remove(self): - """Remove automation from HASS.""" - yield from self.async_turn_off() - yield from super().async_remove() - - @asyncio.coroutine - def async_enable(self): - """Enable this automation entity. - - This method is a coroutine. - """ - if self.is_on: - return - - self._async_detach_triggers = yield from self._async_attach_triggers( - self.async_trigger) - yield from self.async_update_ha_state() - - @property - def device_state_attributes(self): - """Return automation attributes.""" - if self._id is None: - return None - - return { - CONF_ID: self._id - } - - -@asyncio.coroutine -def _async_process_config(hass, config, component): - """Process config and add automations. - - This method is a coroutine. - """ - entities = [] - - for config_key in extract_domain_configs(config, DOMAIN): - conf = config[config_key] - - for list_no, config_block in enumerate(conf): - automation_id = config_block.get(CONF_ID) - name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key, - list_no) - - hidden = config_block[CONF_HIDE_ENTITY] - initial_state = config_block.get(CONF_INITIAL_STATE) - - action = _async_get_action(hass, config_block.get(CONF_ACTION, {}), - name) - - if CONF_CONDITION in config_block: - cond_func = _async_process_if(hass, config, config_block) - - if cond_func is None: - continue - else: - def cond_func(variables): - """Condition will always pass.""" - return True - - async_attach_triggers = partial( - _async_process_trigger, hass, config, - config_block.get(CONF_TRIGGER, []), name - ) - entity = AutomationEntity( - automation_id, name, async_attach_triggers, cond_func, action, - hidden, initial_state) - - entities.append(entity) - - if entities: - yield from component.async_add_entities(entities) - - -def _async_get_action(hass, config, name): - """Return an action based on a configuration.""" - script_obj = script.Script(hass, config, name) - - @asyncio.coroutine - def action(entity_id, variables): - """Execute an action.""" - _LOGGER.info('Executing %s', name) - logbook.async_log_entry( - hass, name, 'has been triggered', DOMAIN, entity_id) - yield from script_obj.async_run(variables) - - return action - - -def _async_process_if(hass, config, p_config): - """Process if checks.""" - if_configs = p_config.get(CONF_CONDITION) - - checks = [] - for if_config in if_configs: - try: - checks.append(condition.async_from_config(if_config, False)) - except HomeAssistantError as ex: - _LOGGER.warning('Invalid condition: %s', ex) - return None - - def if_action(variables=None): - """AND all conditions.""" - return all(check(hass, variables) for check in checks) - - return if_action - - -@asyncio.coroutine -def _async_process_trigger(hass, config, trigger_configs, name, action): - """Set up the triggers. - - This method is a coroutine. - """ - removes = [] - - for conf in trigger_configs: - platform = yield from async_prepare_setup_platform( - hass, config, DOMAIN, conf.get(CONF_PLATFORM)) - - if platform is None: - return None - - remove = yield from platform.async_trigger(hass, conf, action) - - if not remove: - _LOGGER.error("Error setting up trigger %s", name) - continue - - _LOGGER.info("Initialized trigger %s", name) - removes.append(remove) - - if not removes: - return None - - def remove_triggers(): - """Remove attached triggers.""" - for remove in removes: - remove() - - return remove_triggers diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index f67c572ae75..30c4adf565e 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -32,7 +32,7 @@ class TestAutomation(unittest.TestCase): def test_service_data_not_a_dict(self): """Test service data not dict.""" - with assert_setup_component(0): + with assert_setup_component(0, automation.DOMAIN): assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -390,23 +390,7 @@ class TestAutomation(unittest.TestCase): self.hass.block_till_done() assert automation.is_on(self.hass, entity_id) - @patch('homeassistant.config.load_yaml_config_file', autospec=True, - return_value={ - automation.DOMAIN: { - 'alias': 'bye', - 'trigger': { - 'platform': 'event', - 'event_type': 'test_event2', - }, - 'action': { - 'service': 'test.automation', - 'data_template': { - 'event': '{{ trigger.event.event_type }}' - } - } - } - }) - def test_reload_config_service(self, mock_load_yaml): + def test_reload_config_service(self): """Test the reload config service.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { @@ -435,10 +419,25 @@ class TestAutomation(unittest.TestCase): assert len(self.calls) == 1 assert self.calls[0].data.get('event') == 'test_event' - automation.reload(self.hass) - self.hass.block_till_done() - # De-flake ?! - self.hass.block_till_done() + with patch('homeassistant.config.load_yaml_config_file', autospec=True, + return_value={ + automation.DOMAIN: { + 'alias': 'bye', + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event2', + }, + 'action': { + 'service': 'test.automation', + 'data_template': { + 'event': '{{ trigger.event.event_type }}' + } + } + }}): + automation.reload(self.hass) + self.hass.block_till_done() + # De-flake ?! + self.hass.block_till_done() assert self.hass.states.get('automation.hello') is None assert self.hass.states.get('automation.bye') is not None @@ -455,9 +454,7 @@ class TestAutomation(unittest.TestCase): assert len(self.calls) == 2 assert self.calls[1].data.get('event') == 'test_event2' - @patch('homeassistant.config.load_yaml_config_file', autospec=True, - return_value={automation.DOMAIN: 'not valid'}) - def test_reload_config_when_invalid_config(self, mock_load_yaml): + def test_reload_config_when_invalid_config(self): """Test the reload config service handling invalid config.""" with assert_setup_component(1): assert setup_component(self.hass, automation.DOMAIN, { @@ -483,8 +480,10 @@ class TestAutomation(unittest.TestCase): assert len(self.calls) == 1 assert self.calls[0].data.get('event') == 'test_event' - automation.reload(self.hass) - self.hass.block_till_done() + with patch('homeassistant.config.load_yaml_config_file', autospec=True, + return_value={automation.DOMAIN: 'not valid'}): + automation.reload(self.hass) + self.hass.block_till_done() assert self.hass.states.get('automation.hello') is None diff --git a/tests/components/switch/test_template.py b/tests/components/switch/test_template.py index e14eea01189..218a6596899 100644 --- a/tests/components/switch/test_template.py +++ b/tests/components/switch/test_template.py @@ -36,7 +36,7 @@ class TestTemplateSwitch: def test_template_state_text(self): """"Test the state text of a template.""" - with assert_setup_component(1): + with assert_setup_component(1, 'switch'): assert setup.setup_component(self.hass, 'switch', { 'switch': { 'platform': 'template', @@ -74,7 +74,7 @@ class TestTemplateSwitch: def test_template_state_boolean_on(self): """Test the setting of the state with boolean on.""" - with assert_setup_component(1): + with assert_setup_component(1, 'switch'): assert setup.setup_component(self.hass, 'switch', { 'switch': { 'platform': 'template', @@ -103,7 +103,7 @@ class TestTemplateSwitch: def test_template_state_boolean_off(self): """Test the setting of the state with off.""" - with assert_setup_component(1): + with assert_setup_component(1, 'switch'): assert setup.setup_component(self.hass, 'switch', { 'switch': { 'platform': 'template', @@ -132,7 +132,7 @@ class TestTemplateSwitch: def test_icon_template(self): """Test icon template.""" - with assert_setup_component(1): + with assert_setup_component(1, 'switch'): assert setup.setup_component(self.hass, 'switch', { 'switch': { 'platform': 'template', @@ -171,7 +171,7 @@ class TestTemplateSwitch: def test_template_syntax_error(self): """Test templating syntax error.""" - with assert_setup_component(0): + with assert_setup_component(0, 'switch'): assert setup.setup_component(self.hass, 'switch', { 'switch': { 'platform': 'template', @@ -199,7 +199,7 @@ class TestTemplateSwitch: def test_invalid_name_does_not_create(self): """Test invalid name.""" - with assert_setup_component(0): + with assert_setup_component(0, 'switch'): assert setup.setup_component(self.hass, 'switch', { 'switch': { 'platform': 'template', @@ -227,7 +227,7 @@ class TestTemplateSwitch: def test_invalid_switch_does_not_create(self): """Test invalid switch.""" - with assert_setup_component(0): + with assert_setup_component(0, 'switch'): assert setup.setup_component(self.hass, 'switch', { 'switch': { 'platform': 'template', @@ -258,7 +258,7 @@ class TestTemplateSwitch: def test_missing_template_does_not_create(self): """Test missing template.""" - with assert_setup_component(0): + with assert_setup_component(0, 'switch'): assert setup.setup_component(self.hass, 'switch', { 'switch': { 'platform': 'template', @@ -286,7 +286,7 @@ class TestTemplateSwitch: def test_missing_on_does_not_create(self): """Test missing on.""" - with assert_setup_component(0): + with assert_setup_component(0, 'switch'): assert setup.setup_component(self.hass, 'switch', { 'switch': { 'platform': 'template', @@ -314,7 +314,7 @@ class TestTemplateSwitch: def test_missing_off_does_not_create(self): """Test missing off.""" - with assert_setup_component(0): + with assert_setup_component(0, 'switch'): assert setup.setup_component(self.hass, 'switch', { 'switch': { 'platform': 'template',