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:
parent
08efe2bf6d
commit
3f82ef64a1
6 changed files with 165 additions and 102 deletions
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."""
|
||||
|
|
Loading…
Add table
Reference in a new issue