From 3f82ef64a1bf23c989405b647eeb40050e8ca0ea Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 8 Feb 2017 18:17:52 +0100 Subject: [PATCH] Move core service from core to components (#5787) * Move core servcie from core to components * add new handler for signals/exception * Static persistent id * Move unittest * fix coro/callback * Add more unittest for new services * Address comments * Update __init__.py --- homeassistant/components/__init__.py | 59 ++++++++++++++++++++----- homeassistant/config.py | 29 +++++++++++++ homeassistant/core.py | 64 +++++----------------------- tests/components/test_init.py | 44 ++++++++++++++++++- tests/test_config.py | 33 +++++++++++++- tests/test_core.py | 38 +---------------- 6 files changed, 165 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index a4f18250d17..0e9d554a579 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -12,14 +12,19 @@ import itertools as it import logging import homeassistant.core as ha +import homeassistant.config as conf_util +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.service import extract_entity_ids from homeassistant.loader import get_component from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE) + ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, + SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART, + RESTART_EXIT_CODE) _LOGGER = logging.getLogger(__name__) SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config' +SERVICE_CHECK_CONFIG = 'check_config' def is_on(hass, entity_id=None): @@ -75,6 +80,21 @@ def toggle(hass, entity_id=None, **service_data): hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data) +def stop(hass): + """Stop Home Assistant.""" + hass.services.call(ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP) + + +def restart(hass): + """Stop Home Assistant.""" + hass.services.call(ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART) + + +def check_config(hass): + """Check the config files.""" + hass.services.call(ha.DOMAIN, SERVICE_CHECK_CONFIG) + + def reload_core_config(hass): """Reload the core config.""" hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG) @@ -84,7 +104,7 @@ def reload_core_config(hass): def async_setup(hass, config): """Setup general services related to Home Assistant.""" @asyncio.coroutine - def handle_turn_service(service): + def async_handle_turn_service(service): """Method to handle calls to homeassistant.turn_on/off.""" entity_ids = extract_entity_ids(hass, service) @@ -122,18 +142,37 @@ def async_setup(hass, config): yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( - ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service) + ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service) hass.services.async_register( - ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service) + ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service) hass.services.async_register( - ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service) + ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service) @asyncio.coroutine - def handle_reload_config(call): - """Service handler for reloading core config.""" - from homeassistant.exceptions import HomeAssistantError - from homeassistant import config as conf_util + def async_handle_core_service(call): + """Service handler for handling core services.""" + if call.service == SERVICE_HOMEASSISTANT_STOP: + hass.async_add_job(hass.async_stop()) + return + try: + yield from conf_util.async_check_ha_config_file(hass) + except HomeAssistantError: + return + + if call.service == SERVICE_HOMEASSISTANT_RESTART: + hass.async_add_job(hass.async_stop(RESTART_EXIT_CODE)) + + hass.services.async_register( + ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service) + hass.services.async_register( + ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service) + hass.services.async_register( + ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service) + + @asyncio.coroutine + def async_handle_reload_config(call): + """Service handler for reloading core config.""" try: conf = yield from conf_util.async_hass_config_yaml(hass) except HomeAssistantError as err: @@ -144,6 +183,6 @@ def async_setup(hass, config): hass, conf.get(ha.DOMAIN) or {}) hass.services.async_register( - ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, handle_reload_config) + ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config) return True diff --git a/homeassistant/config.py b/homeassistant/config.py index 23d52f6bc66..101c1a1dc87 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -3,7 +3,9 @@ import asyncio from collections import OrderedDict import logging import os +import re import shutil +import sys # pylint: disable=unused-import from typing import Any, List, Tuple # NOQA @@ -453,3 +455,30 @@ def merge_packages_customize(core_customize, packages): conf = schema(pkg) cust.extend(conf.get(CONF_CORE, {}).get(CONF_CUSTOMIZE, [])) return cust + + +@asyncio.coroutine +def async_check_ha_config_file(hass): + """Check if HA config file valid. + + This method is a coroutine. + """ + import homeassistant.components.persistent_notification as pn + + proc = yield from asyncio.create_subprocess_exec( + sys.argv[0], + '--script', + 'check_config', + stdout=asyncio.subprocess.PIPE) + # Wait for the subprocess exit + (stdout_data, dummy) = yield from proc.communicate() + result = yield from proc.wait() + if result: + content = re.sub(r'\033\[[^m]*m', '', str(stdout_data, 'utf-8')) + # Put error cleaned from color codes in the error log so it + # will be visible at the UI. + _LOGGER.error(content) + pn.async_create( + hass, "Config error. See dev-info panel for details.", + "Config validating", "{0}.check_config".format(CONF_CORE)) + raise HomeAssistantError("Invalid config") diff --git a/homeassistant/core.py b/homeassistant/core.py index 2cf2e3c85e4..db186fb1b1c 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -26,8 +26,7 @@ from homeassistant.const import ( ATTR_SERVICE_CALL_ID, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_SERVICE_EXECUTED, EVENT_SERVICE_REGISTERED, EVENT_STATE_CHANGED, - EVENT_TIME_CHANGED, MATCH_ALL, RESTART_EXIT_CODE, - SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, __version__) + EVENT_TIME_CHANGED, MATCH_ALL, RESTART_EXIT_CODE, __version__) from homeassistant.exceptions import ( HomeAssistantError, InvalidEntityFormatError, ShuttingDown) from homeassistant.util.async import ( @@ -137,7 +136,7 @@ class HomeAssistant(object): _LOGGER.info("Starting Home Assistant core loop") self.loop.run_forever() except KeyboardInterrupt: - self.loop.call_soon(self._async_stop_handler) + self.loop.create_task(self.async_stop()) self.loop.run_forever() finally: self.loop.close() @@ -149,26 +148,23 @@ class HomeAssistant(object): This method is a coroutine. """ _LOGGER.info("Starting Home Assistant") - self.state = CoreState.starting - # Register the restart/stop event - self.services.async_register( - DOMAIN, SERVICE_HOMEASSISTANT_STOP, self._async_stop_handler) - self.services.async_register( - DOMAIN, SERVICE_HOMEASSISTANT_RESTART, self._async_restart_handler) - # Setup signal handling if sys.platform != 'win32': + def _async_signal_handle(exit_code): + """Wrap signal handling.""" + self.async_add_job(self.async_stop(exit_code)) + try: self.loop.add_signal_handler( - signal.SIGTERM, self._async_stop_handler) + signal.SIGTERM, _async_signal_handle, 0) except ValueError: _LOGGER.warning("Could not bind to SIGTERM") try: self.loop.add_signal_handler( - signal.SIGHUP, self._async_restart_handler) + signal.SIGHUP, _async_signal_handle, RESTART_EXIT_CODE) except ValueError: _LOGGER.warning("Could not bind to SIGHUP") @@ -283,7 +279,7 @@ class HomeAssistant(object): run_coroutine_threadsafe(self.async_stop(), self.loop) @asyncio.coroutine - def async_stop(self) -> None: + def async_stop(self, exit_code=0) -> None: """Stop Home Assistant and shuts down all threads. This method is a coroutine. @@ -306,6 +302,7 @@ class HomeAssistant(object): logging.getLogger('').removeHandler(handler) yield from handler.async_close(blocking=True) + self.exit_code = exit_code self.loop.stop() # pylint: disable=no-self-use @@ -324,47 +321,6 @@ class HomeAssistant(object): _LOGGER.error("Error doing job: %s", context['message'], **kwargs) - @callback - def _async_stop_handler(self, *args): - """Stop Home Assistant.""" - self.exit_code = 0 - self.loop.create_task(self.async_stop()) - - @asyncio.coroutine - def _async_check_config_and_restart(self): - """Restart Home Assistant if config is valid. - - This method is a coroutine. - """ - proc = yield from asyncio.create_subprocess_exec( - sys.argv[0], - '--script', - 'check_config', - stdout=asyncio.subprocess.PIPE) - # Wait for the subprocess exit - (stdout_data, dummy) = yield from proc.communicate() - result = yield from proc.wait() - if result: - _LOGGER.error("check_config failed. Not restarting.") - content = re.sub(r'\033\[[^m]*m', '', str(stdout_data, 'utf-8')) - # Put error cleaned from color codes in the error log so it - # will be visible at the UI. - _LOGGER.error(content) - yield from self.services.async_call( - 'persistent_notification', 'create', { - 'message': 'Config error. See dev-info panel for details.', - 'title': 'Restarting', - 'notification_id': '{}.restart'.format(DOMAIN)}) - return - - self.exit_code = RESTART_EXIT_CODE - yield from self.async_stop() - - @callback - def _async_restart_handler(self, *args): - """Restart Home Assistant.""" - self.loop.create_task(self._async_check_config_and_restart()) - class EventOrigin(enum.Enum): """Represent the origin of an event.""" diff --git a/tests/components/test_init.py b/tests/components/test_init.py index 833319646a2..14066d65f2a 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -11,11 +11,12 @@ from homeassistant import config from homeassistant.const import ( STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE) import homeassistant.components as comps +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity from homeassistant.util.async import run_coroutine_threadsafe from tests.common import ( - get_test_home_assistant, mock_service, patch_yaml_files) + get_test_home_assistant, mock_service, patch_yaml_files, mock_coro) class TestComponentsCore(unittest.TestCase): @@ -150,3 +151,44 @@ class TestComponentsCore(unittest.TestCase): assert mock_error.called assert mock_process.called is False + + @patch('homeassistant.core.HomeAssistant.async_stop', + return_value=mock_coro()()) + def test_stop_homeassistant(self, mock_stop): + """Test stop service.""" + comps.stop(self.hass) + self.hass.block_till_done() + assert mock_stop.called + + @patch('homeassistant.core.HomeAssistant.async_stop', + return_value=mock_coro()()) + @patch('homeassistant.config.async_check_ha_config_file', + return_value=mock_coro()()) + def test_restart_homeassistant(self, mock_check, mock_restart): + """Test stop service.""" + comps.restart(self.hass) + self.hass.block_till_done() + assert mock_restart.called + assert mock_check.called + + @patch('homeassistant.core.HomeAssistant.async_stop', + return_value=mock_coro()()) + @patch('homeassistant.config.async_check_ha_config_file', + side_effect=HomeAssistantError("Test error")) + def test_restart_homeassistant_wrong_conf(self, mock_check, mock_restart): + """Test stop service.""" + comps.restart(self.hass) + self.hass.block_till_done() + assert mock_check.called + assert not mock_restart.called + + @patch('homeassistant.core.HomeAssistant.async_stop', + return_value=mock_coro()()) + @patch('homeassistant.config.async_check_ha_config_file', + return_value=mock_coro()()) + def test_check_config(self, mock_check, mock_stop): + """Test stop service.""" + comps.check_config(self.hass) + self.hass.block_till_done() + assert mock_check.called + assert not mock_stop.called diff --git a/tests/test_config.py b/tests/test_config.py index 1197a0130d8..2ad25dece11 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -18,7 +18,7 @@ from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.helpers.entity import Entity from tests.common import ( - get_test_config_dir, get_test_home_assistant) + get_test_config_dir, get_test_home_assistant, mock_generator) CONFIG_DIR = get_test_config_dir() YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) @@ -376,6 +376,37 @@ class TestConfig(unittest.TestCase): assert self.hass.config.units == blankConfig.units assert self.hass.config.time_zone == blankConfig.time_zone + @mock.patch('asyncio.create_subprocess_exec') + def test_check_ha_config_file_correct(self, mock_create): + """Check that restart propagates to stop.""" + process_mock = mock.MagicMock() + attrs = { + 'communicate.return_value': mock_generator(('output', 'error')), + 'wait.return_value': mock_generator(0)} + process_mock.configure_mock(**attrs) + mock_create.return_value = mock_generator(process_mock) + + assert run_coroutine_threadsafe( + config_util.async_check_ha_config_file(self.hass), self.hass.loop + ).result() is None + + @mock.patch('asyncio.create_subprocess_exec') + def test_check_ha_config_file_wrong(self, mock_create): + """Check that restart with a bad config doesn't propagate to stop.""" + process_mock = mock.MagicMock() + attrs = { + 'communicate.return_value': + mock_generator((r'\033[hellom'.encode('utf-8'), 'error')), + 'wait.return_value': mock_generator(1)} + process_mock.configure_mock(**attrs) + mock_create.return_value = mock_generator(process_mock) + + with self.assertRaises(HomeAssistantError): + run_coroutine_threadsafe( + config_util.async_check_ha_config_file(self.hass), + self.hass.loop + ).result() + # pylint: disable=redefined-outer-name @pytest.fixture diff --git a/tests/test_core.py b/tests/test_core.py index 72c459a4eac..14276584ae2 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -14,10 +14,9 @@ from homeassistant.util.async import run_coroutine_threadsafe import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import (METRIC_SYSTEM) from homeassistant.const import ( - __version__, EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, CONF_UNIT_SYSTEM, - SERVICE_HOMEASSISTANT_RESTART, RESTART_EXIT_CODE) + __version__, EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, CONF_UNIT_SYSTEM) -from tests.common import get_test_home_assistant, mock_generator +from tests.common import get_test_home_assistant PST = pytz.timezone('America/Los_Angeles') @@ -221,39 +220,6 @@ class TestHomeAssistant(unittest.TestCase): with pytest.raises(ValueError): self.hass.add_job(None, 'test_arg') - @patch('asyncio.create_subprocess_exec') - def test_restart(self, mock_create): - """Check that restart propagates to stop.""" - process_mock = MagicMock() - attrs = { - 'communicate.return_value': mock_generator(('output', 'error')), - 'wait.return_value': mock_generator(0)} - process_mock.configure_mock(**attrs) - mock_create.return_value = mock_generator(process_mock) - - self.hass.start() - with patch.object(self.hass, 'async_stop') as mock_stop: - self.hass.services.call(ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART) - mock_stop.assert_called_once_with() - self.assertEqual(RESTART_EXIT_CODE, self.hass.exit_code) - - @patch('asyncio.create_subprocess_exec') - def test_restart_bad_config(self, mock_create): - """Check that restart with a bad config doesn't propagate to stop.""" - process_mock = MagicMock() - attrs = { - 'communicate.return_value': - mock_generator((r'\033[hellom'.encode('utf-8'), 'error')), - 'wait.return_value': mock_generator(1)} - process_mock.configure_mock(**attrs) - mock_create.return_value = mock_generator(process_mock) - - self.hass.start() - with patch.object(self.hass, 'async_stop') as mock_stop: - self.hass.services.call(ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART) - mock_stop.assert_not_called() - self.assertEqual(None, self.hass.exit_code) - class TestEvent(unittest.TestCase): """A Test Event class."""