Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
6 changes: 2 additions & 4 deletions homeassistant/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,6 @@ def async_from_config_dict(config: Dict[str, Any],
This method is a coroutine.
"""
start = time()
hass.async_track_tasks()

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.

Track tasks is now enabled by default. It's what we want during startup.


core_config = config.get(core.DOMAIN, {})

try:
Expand Down Expand Up @@ -140,10 +138,10 @@ def async_from_config_dict(config: Dict[str, Any],
continue
hass.async_add_job(async_setup_component(hass, component, config))

yield from hass.async_stop_track_tasks()
yield from hass.async_block_till_done()

stop = time()
_LOGGER.info('Home Assistant initialized in %ss', round(stop-start, 2))
_LOGGER.info('Home Assistant initialized in %.2fs', stop-start)

async_register_signal_handling(hass)
return hass
Expand Down
19 changes: 16 additions & 3 deletions homeassistant/components/automation/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
Offer event listening automation rules.

For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#event-trigger
at https://home-assistant.io/docs/automation/trigger/#event-trigger
"""
import asyncio
import logging

import voluptuous as vol

from homeassistant.core import callback
from homeassistant.const import CONF_PLATFORM
from homeassistant.core import callback, CoreState
from homeassistant.const import CONF_PLATFORM, EVENT_HOMEASSISTANT_START
from homeassistant.helpers import config_validation as cv

CONF_EVENT_TYPE = "event_type"
Expand All @@ -31,6 +31,19 @@ def async_trigger(hass, config, action):
event_type = config.get(CONF_EVENT_TYPE)
event_data = config.get(CONF_EVENT_DATA)

if (event_type == EVENT_HOMEASSISTANT_START and
hass.state == CoreState.starting):
_LOGGER.warning('Deprecation: Automations should not listen to event '
"'homeassistant_start'. Use platform 'homeassistant' "
'instead. Feature will be removed in 0.45')
hass.async_run_job(action, {
'trigger': {
'platform': 'event',
'event': None,
},
})
return lambda: None

@callback
def handle_event(event):
"""Listen for events and calls the action when data matches."""
Expand Down
55 changes: 55 additions & 0 deletions homeassistant/components/automation/homeassistant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""
Offer Home Assistant core automation rules.

For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#homeassistant-trigger
"""
import asyncio
import logging

import voluptuous as vol

from homeassistant.core import callback, CoreState
from homeassistant.const import (
CONF_PLATFORM, CONF_EVENT, EVENT_HOMEASSISTANT_STOP)

EVENT_START = 'start'
EVENT_SHUTDOWN = 'shutdown'
_LOGGER = logging.getLogger(__name__)

TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'homeassistant',
vol.Required(CONF_EVENT): vol.Any(EVENT_START, EVENT_SHUTDOWN),
})


@asyncio.coroutine
def async_trigger(hass, config, action):
"""Listen for events based on configuration."""
event = config.get(CONF_EVENT)

if event == EVENT_SHUTDOWN:
@callback
def hass_shutdown(event):
"""Called when Home Assistant is shutting down."""
hass.async_run_job(action, {
'trigger': {
'platform': 'homeassistant',
'event': event,
},
})

return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
hass_shutdown)

# Automation are enabled while hass is starting up, fire right away
# Check state because a config reload shouldn't trigger it.
elif hass.state == CoreState.starting:
hass.async_run_job(action, {
'trigger': {
'platform': 'homeassistant',
'event': event,
},
})

return lambda: None
2 changes: 1 addition & 1 deletion homeassistant/components/automation/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Offer MQTT listening automation rules.

For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#mqtt-trigger
at https://home-assistant.io/docs/automation/trigger/#mqtt-trigger
"""
import asyncio
import json
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/automation/numeric_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Offer numeric state listening automation rules.

For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#numeric-state-trigger
at https://home-assistant.io/docs/automation/trigger/#numeric-state-trigger
"""
import asyncio
import logging
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/automation/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Offer state listening automation rules.

For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#state-trigger
at https://home-assistant.io/docs/automation/trigger/#state-trigger
"""
import asyncio
import voluptuous as vol
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/automation/sun.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Offer sun based automation rules.

For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#sun-trigger
at https://home-assistant.io/docs/automation/trigger/#sun-trigger
"""
import asyncio
from datetime import timedelta
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/automation/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Offer template automation rules.

For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#template-trigger
at https://home-assistant.io/docs/automation/trigger/#template-trigger
"""
import asyncio
import logging
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/automation/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Offer time listening automation rules.

For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#time-trigger
at https://home-assistant.io/docs/automation/trigger/#time-trigger
"""
import asyncio
import logging
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/automation/zone.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Offer zone automation rules.

For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#zone-trigger
at https://home-assistant.io/docs/automation/trigger/#zone-trigger
"""
import asyncio
import voluptuous as vol
Expand Down
19 changes: 7 additions & 12 deletions homeassistant/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
EVENT_TIME_CHANGED, MATCH_ALL, EVENT_HOMEASSISTANT_CLOSE,
EVENT_SERVICE_REMOVED, __version__)
from homeassistant.exceptions import (
HomeAssistantError, InvalidEntityFormatError, ShuttingDown)
HomeAssistantError, InvalidEntityFormatError)
from homeassistant.util.async import (
run_coroutine_threadsafe, run_callback_threadsafe)
import homeassistant.util as util
Expand Down Expand Up @@ -86,10 +86,6 @@ def async_loop_exception_handler(loop, context):
kwargs = {}
exception = context.get('exception')
if exception:
# Do not report on shutting down exceptions.
if isinstance(exception, ShuttingDown):
return

kwargs['exc_info'] = (type(exception), exception,
exception.__traceback__)

Expand Down Expand Up @@ -123,7 +119,7 @@ def __init__(self, loop=None):
self.loop.set_default_executor(self.executor)
self.loop.set_exception_handler(async_loop_exception_handler)
self._pending_tasks = []
self._track_task = False
self._track_task = True
self.bus = EventBus(self)
self.services = ServiceRegistry(self)
self.states = StateMachine(self.bus, self.loop)
Expand Down Expand Up @@ -165,9 +161,10 @@ def async_start(self):

# pylint: disable=protected-access
self.loop._thread_ident = threading.get_ident()
_async_create_timer(self)
self.bus.async_fire(EVENT_HOMEASSISTANT_START)
yield from self.async_stop_track_tasks()

@balloob balloob Apr 5, 2017

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.

So this fixes the fact that CoreState.starting was never detectable before, as we would immediately overwrite it.

However, this has the downside that it is impossible for components/platforms to start a permanent async job as a result from EVENT_HOMEASSISTANT_START.

(note that things like MQTT server rely on loop.create_server which will work. Z-Wave depends on a thread and that will work)

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.

Maybe add a timeout to async_stop_track_tasks after which we will no longer stop tracking tasks?

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 should not be needed to add a timeout

self.state = CoreState.running
_async_create_timer(self)

def add_job(self, target: Callable[..., None], *args: Any) -> None:
"""Add job to the executor pool.
Expand Down Expand Up @@ -238,6 +235,8 @@ def block_till_done(self) -> None:
@asyncio.coroutine
def async_block_till_done(self):
"""Block till all pending work is done."""
assert self._track_task, 'Not tracking tasks'

# To flush out any call_soon_threadsafe
yield from asyncio.sleep(0, loop=self.loop)

Expand All @@ -252,7 +251,7 @@ def async_block_till_done(self):

def stop(self) -> None:
"""Stop Home Assistant and shuts down all threads."""
run_coroutine_threadsafe(self.async_stop(), self.loop)
run_coroutine_threadsafe(self.async_stop(), self.loop).result()

@asyncio.coroutine
def async_stop(self, exit_code=0) -> None:
Expand Down Expand Up @@ -368,10 +367,6 @@ def async_fire(self, event_type: str, event_data=None,

This method must be run in the event loop.
"""
if event_type != EVENT_HOMEASSISTANT_STOP and \

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.

This was silently (because exception suppressed) killing shutdown tasks (noticed it when adding shutdown event in automation). It should be fine to just wait till all is done. Timer is already stopped so nothing new should come in.

self._hass.state == CoreState.stopping:
raise ShuttingDown("Home Assistant is shutting down")

listeners = self._listeners.get(event_type, [])

# EVENT_HOMEASSISTANT_CLOSE should go only to his listeners
Expand Down
6 changes: 0 additions & 6 deletions homeassistant/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ class HomeAssistantError(Exception):
pass


class ShuttingDown(HomeAssistantError):
"""When trying to change something during shutdown."""

pass


class InvalidEntityFormatError(HomeAssistantError):
"""When an invalid formatted entity is encountered."""

Expand Down
21 changes: 11 additions & 10 deletions tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@
from homeassistant.const import (
STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, EVENT_TIME_CHANGED,
EVENT_STATE_CHANGED, EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE,
ATTR_DISCOVERED, SERVER_PORT, EVENT_HOMEASSISTANT_STOP)
ATTR_DISCOVERED, SERVER_PORT, EVENT_HOMEASSISTANT_CLOSE)
from homeassistant.components import sun, mqtt, recorder
from homeassistant.components.http.auth import auth_middleware
from homeassistant.components.http.const import (
KEY_USE_X_FORWARDED_FOR, KEY_BANS_ENABLED, KEY_TRUSTED_NETWORKS)
from homeassistant.util.async import run_callback_threadsafe
from homeassistant.util.async import (
run_callback_threadsafe, run_coroutine_threadsafe)

_TEST_INSTANCE_PORT = SERVER_PORT
_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -58,19 +59,16 @@ def run_loop():
loop.run_forever()
stop_event.set()

orig_start = hass.start
orig_stop = hass.stop

@patch.object(hass.loop, 'run_forever')
@patch.object(hass.loop, 'close')
def start_hass(*mocks):
"""Helper to start hass."""
orig_start()
hass.block_till_done()
run_coroutine_threadsafe(hass.async_start(), loop=hass.loop).result()

def stop_hass():
"""Stop hass."""
orig_stop()
loop.stop()
stop_event.wait()
loop.close()

Expand Down Expand Up @@ -101,7 +99,6 @@ def async_add_job(target, *args):
return orig_async_add_job(target, *args)

hass.async_add_job = async_add_job
hass.async_track_tasks()

hass.config.location_name = 'test home'
hass.config.config_dir = get_test_config_dir()
Expand All @@ -123,7 +120,11 @@ def async_add_job(target, *args):
@asyncio.coroutine
def mock_async_start():
"""Start the mocking."""
with patch('homeassistant.core._async_create_timer'):
# 1. We only mock time during tests
# 2. We want block_till_done that is called inside stop_track_tasks
with patch('homeassistant.core._async_create_timer'), \
patch.object(hass, 'async_stop_track_tasks',
hass.async_block_till_done):
yield from orig_start()

hass.async_start = mock_async_start
Expand All @@ -134,7 +135,7 @@ def clear_instance(event):
global INST_COUNT
INST_COUNT -= 1

hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, clear_instance)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, clear_instance)

return hass

Expand Down
35 changes: 32 additions & 3 deletions tests/components/automation/test_event.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"""The tests for the Event automation."""
import asyncio
import unittest

from homeassistant.core import callback
from homeassistant.setup import setup_component
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.core import callback, CoreState
from homeassistant.setup import setup_component, async_setup_component
import homeassistant.components.automation as automation

from tests.common import get_test_home_assistant, mock_component
from tests.common import get_test_home_assistant, mock_component, mock_service


# pylint: disable=invalid-name
Expand Down Expand Up @@ -92,3 +94,30 @@ def test_if_not_fires_if_event_data_not_matches(self):
self.hass.bus.fire('test_event', {'some_attr': 'some_other_value'})
self.hass.block_till_done()
self.assertEqual(0, len(self.calls))


@asyncio.coroutine
def test_if_fires_on_event_with_data(hass):
"""Test the firing of events with data."""
calls = mock_service(hass, 'test', 'automation')
hass.state = CoreState.not_running

res = yield from async_setup_component(hass, automation.DOMAIN, {
automation.DOMAIN: {
'alias': 'hello',
'trigger': {
'platform': 'event',
'event_type': EVENT_HOMEASSISTANT_START,
},
'action': {
'service': 'test.automation',
}
}
})
assert res
assert not automation.is_on(hass, 'automation.hello')
assert len(calls) == 0

yield from hass.async_start()
assert automation.is_on(hass, 'automation.hello')
assert len(calls) == 1
Loading