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
This commit is contained in:
Pascal Vizeli 2017-02-08 18:17:52 +01:00 committed by Paulus Schoutsen
parent 08efe2bf6d
commit 3f82ef64a1
6 changed files with 165 additions and 102 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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