Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 49 additions & 10 deletions homeassistant/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,19 @@
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):
Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand Down Expand Up @@ -122,18 +142,37 @@ def handle_turn_service(service):
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:
Expand All @@ -144,6 +183,6 @@ def handle_reload_config(call):
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
29 changes: 29 additions & 0 deletions homeassistant/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
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

Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think such a dependency is a hack.
homeassistant.config shouldn't depend on homeassistant.components.*

Copy link
Copy Markdown
Member Author

@pvizeli pvizeli Feb 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This depense will exists without I Import this or not... Bootstrap work in same way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could call persistence notification service then there will be no dependency - i.e. hass would still come up if homeassistant.components.persistent_notification was removed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LIke in homeassistant.core you call a service and not call homeassistant.components.stop()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will become the same if I import this or I need to do: from ...persistent_notification Import DOMAIN, SERVICE_XY for calling the service later.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, so I propose not to import anything and just use strings to call the service.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a hack but one that we use in other places in this file too, see line 31. However, line 31 does execute it a bit more classy, as it allows for people to overwrite that component.

I prefer to use the function over spelling out the service, because that's the suggested way for Python code.


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")
64 changes: 10 additions & 54 deletions homeassistant/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@
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 (
Expand Down Expand Up @@ -137,7 +136,7 @@ def start(self) -> None:
_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()
Expand All @@ -149,26 +148,23 @@ def async_start(self):
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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that we should also extract the signal handling of the core.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, make a new PR for that

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

Expand Down Expand Up @@ -283,7 +279,7 @@ def stop(self) -> None:
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.
Expand All @@ -306,6 +302,7 @@ def async_stop(self) -> None:
logging.getLogger('').removeHandler(handler)
yield from handler.async_close(blocking=True)

self.exit_code = exit_code
self.loop.stop()

# pylint: disable=no-self-use
Expand All @@ -324,47 +321,6 @@ def _async_exception_handler(self, loop, context):

_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."""
Expand Down
44 changes: 43 additions & 1 deletion tests/components/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
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):
Expand Down Expand Up @@ -150,3 +151,44 @@ def test_reload_core_with_wrong_conf(self, mock_process, mock_error):

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
33 changes: 32 additions & 1 deletion tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
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)
Expand Down Expand Up @@ -376,6 +376,37 @@ def test_discovering_configuration_auto_detect_fails(self, mock_detect,
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
Expand Down
Loading