From 81eebe41420994178e14f81080fec8a86c48927c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Apr 2017 09:07:17 -0700 Subject: [PATCH 01/14] Fire EVENT_HOMEASSISTANT_START automations off right away while starting --- homeassistant/components/automation/event.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index 21bf243e34fcf6..382a2956f05943 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -9,8 +9,8 @@ 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" @@ -31,6 +31,16 @@ 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): + 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.""" From 99a6b7a2d2d248cbe5b92e998d865a22fa1600fb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Apr 2017 09:34:13 -0700 Subject: [PATCH 02/14] Actually have core state be set to 'starting' during boot --- homeassistant/bootstrap.py | 6 ++---- homeassistant/core.py | 5 ++++- tests/common.py | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index e05fff988659b2..4c586d12ccd2c2 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -74,8 +74,6 @@ def async_from_config_dict(config: Dict[str, Any], This method is a coroutine. """ start = time() - hass.async_track_tasks() - core_config = config.get(core.DOMAIN, {}) try: @@ -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 diff --git a/homeassistant/core.py b/homeassistant/core.py index 1aba2db56f5361..2f18c140bafc82 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -123,7 +123,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) @@ -167,6 +167,7 @@ def async_start(self): 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() self.state = CoreState.running def add_job(self, target: Callable[..., None], *args: Any) -> None: @@ -238,6 +239,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, 'We are not tracking tasks' + # To flush out any call_soon_threadsafe yield from asyncio.sleep(0, loop=self.loop) diff --git a/tests/common.py b/tests/common.py index 01889af1bf1ff0..29de87be8334b1 100644 --- a/tests/common.py +++ b/tests/common.py @@ -101,7 +101,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() @@ -123,7 +122,8 @@ def async_add_job(target, *args): @asyncio.coroutine def mock_async_start(): """Start the mocking.""" - with patch('homeassistant.core._async_create_timer'): + with patch('homeassistant.core._async_create_timer'), \ + patch.object(hass, 'async_stop_track_tasks', mock_coro): yield from orig_start() hass.async_start = mock_async_start From 3fe28bc3ea09d5c0642ae659b0019fc957565b59 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Apr 2017 20:48:18 -0700 Subject: [PATCH 03/14] Fix correct start implementation --- homeassistant/core.py | 4 ++-- tests/common.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 2f18c140bafc82..d6c73037948295 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -165,10 +165,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() self.state = CoreState.running + _async_create_timer(self) def add_job(self, target: Callable[..., None], *args: Any) -> None: """Add job to the executor pool. @@ -239,7 +239,7 @@ 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, 'We are not tracking tasks' + assert self._track_task, 'Not tracking tasks' # To flush out any call_soon_threadsafe yield from asyncio.sleep(0, loop=self.loop) diff --git a/tests/common.py b/tests/common.py index 29de87be8334b1..8a941f2ce42c1a 100644 --- a/tests/common.py +++ b/tests/common.py @@ -122,8 +122,11 @@ def async_add_job(target, *args): @asyncio.coroutine def mock_async_start(): """Start the mocking.""" + # 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', mock_coro): + patch.object(hass, 'async_stop_track_tasks', + hass.async_block_till_done): yield from orig_start() hass.async_start = mock_async_start From 94ef18244245b8c8e5fdb324339e20b356d582f0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Apr 2017 20:48:33 -0700 Subject: [PATCH 04/14] Test and deprecate event automation platform on start --- homeassistant/components/automation/event.py | 3 ++ tests/components/automation/test_event.py | 35 ++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index 382a2956f05943..363dd64b33942d 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -33,6 +33,9 @@ def async_trigger(hass, config, action): 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', diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index b4686650057921..a056520a5c933b 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -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 @@ -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 From 604ceb02a9f15e6095fb973535a350f4c6db084a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Apr 2017 20:52:19 -0700 Subject: [PATCH 05/14] Fix doc strings --- homeassistant/components/automation/event.py | 2 +- homeassistant/components/automation/mqtt.py | 2 +- homeassistant/components/automation/numeric_state.py | 2 +- homeassistant/components/automation/state.py | 2 +- homeassistant/components/automation/sun.py | 2 +- homeassistant/components/automation/template.py | 2 +- homeassistant/components/automation/time.py | 2 +- homeassistant/components/automation/zone.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index 363dd64b33942d..0ff10665eb3989 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -2,7 +2,7 @@ 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 diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index fbea2cede380dd..172a368225d696 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -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 diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 8b3c3e576706f5..3657724f6791dd 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -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 diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index fdc460792635a0..1f55ef67f253fe 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -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 diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 4529b5a8b60c54..3ce84d60a9135c 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -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 diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index a83671d5fa8d73..0fcdeaae5e09b2 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -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 diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index e33fd0f6ba92db..0adcd5f8272a16 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -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 diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 8ffc0498317e13..c2a0e4d094d185 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -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 From 48e275080c278442adab5a4b3869b9fbc5c161c8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Apr 2017 21:51:57 -0700 Subject: [PATCH 06/14] Remove shutting down exception --- homeassistant/core.py | 10 +--------- homeassistant/exceptions.py | 6 ------ 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index d6c73037948295..48e3a33bdaf00a 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -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 @@ -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__) @@ -371,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 \ - 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 diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index f1ed646b02ddaf..f45fd3c38414b8 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -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.""" From deee087b07a941e5890f3377eb136e1c5b0085f7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Apr 2017 21:52:16 -0700 Subject: [PATCH 07/14] More strict when to mark an instance as finished --- tests/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/common.py b/tests/common.py index 8a941f2ce42c1a..d94bae331f526c 100644 --- a/tests/common.py +++ b/tests/common.py @@ -23,7 +23,7 @@ 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 ( @@ -137,7 +137,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 From 3b46e4490714ab22b9e45a1f18a1dbdaaef7479f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Apr 2017 21:52:45 -0700 Subject: [PATCH 08/14] Add automation platform to listen for start/shutdown --- .../components/automation/homeassistant.py | 55 ++++++++++++ .../automation/test_homeassistant.py | 84 +++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 homeassistant/components/automation/homeassistant.py create mode 100644 tests/components/automation/test_homeassistant.py diff --git a/homeassistant/components/automation/homeassistant.py b/homeassistant/components/automation/homeassistant.py new file mode 100644 index 00000000000000..0222ef02c263c0 --- /dev/null +++ b/homeassistant/components/automation/homeassistant.py @@ -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 diff --git a/tests/components/automation/test_homeassistant.py b/tests/components/automation/test_homeassistant.py new file mode 100644 index 00000000000000..89e7ab3220b6ef --- /dev/null +++ b/tests/components/automation/test_homeassistant.py @@ -0,0 +1,84 @@ +"""The tests for the Event automation.""" +import asyncio +from unittest.mock import patch, Mock + +from homeassistant.core import CoreState +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation + +from tests.common import mock_service, mock_coro + + +@asyncio.coroutine +def test_if_fires_on_hass_start(hass): + """Test the firing when HASS starts.""" + calls = mock_service(hass, 'test', 'automation') + hass.state = CoreState.not_running + config = { + automation.DOMAIN: { + 'alias': 'hello', + 'trigger': { + 'platform': 'homeassistant', + 'event': 'start', + }, + 'action': { + 'service': 'test.automation', + } + } + } + + res = yield from async_setup_component(hass, automation.DOMAIN, config) + 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 + + with patch('homeassistant.config.async_hass_config_yaml', + Mock(return_value=mock_coro(config))): + yield from hass.services.async_call( + automation.DOMAIN, automation.SERVICE_RELOAD, blocking=True) + + assert automation.is_on(hass, 'automation.hello') + assert len(calls) == 1 + + +@asyncio.coroutine +def test_if_fires_on_hass_shutdown(hass): + """Test the firing when HASS starts.""" + 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': 'homeassistant', + 'event': 'shutdown', + }, + '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) == 0 + + with patch.object(hass.loop, 'stop'), patch.object(hass.executor, 'shutdown'): + yield from hass.async_stop() + assert len(calls) == 1 + + # with patch('homeassistant.config.async_hass_config_yaml', + # Mock(return_value=mock_coro(config))): + # yield from hass.services.async_call( + # automation.DOMAIN, automation.SERVICE_RELOAD, blocking=True) + + # assert automation.is_on(hass, 'automation.hello') + # assert len(calls) == 1 From 22ec6df9829bd801e8c7fc8e88ea9c023788078c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Apr 2017 23:17:51 -0700 Subject: [PATCH 09/14] When we stop we should wait till it's all done --- homeassistant/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 48e3a33bdaf00a..c8e13afde27475 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -251,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: From bbe33a306c287a491117db373e26c5ab71b01f0d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Apr 2017 23:19:15 -0700 Subject: [PATCH 10/14] Fix testing --- tests/common.py | 10 ++++------ tests/components/automation/test_homeassistant.py | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/common.py b/tests/common.py index d94bae331f526c..471626a8ff5c53 100644 --- a/tests/common.py +++ b/tests/common.py @@ -28,7 +28,8 @@ 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__) @@ -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() diff --git a/tests/components/automation/test_homeassistant.py b/tests/components/automation/test_homeassistant.py index 89e7ab3220b6ef..d63e59b3f5453e 100644 --- a/tests/components/automation/test_homeassistant.py +++ b/tests/components/automation/test_homeassistant.py @@ -71,7 +71,7 @@ def test_if_fires_on_hass_shutdown(hass): assert automation.is_on(hass, 'automation.hello') assert len(calls) == 0 - with patch.object(hass.loop, 'stop'), patch.object(hass.executor, 'shutdown'): + with patch.object(hass.loop, 'stop'): yield from hass.async_stop() assert len(calls) == 1 From aeda4fe365a18972ff620f9a5027a763327960e3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 5 Apr 2017 00:24:03 -0700 Subject: [PATCH 11/14] Fix async bugs in tests --- homeassistant/components/automation/litejet.py | 4 ++-- homeassistant/components/light/litejet.py | 2 +- homeassistant/components/sensor/mqtt_room.py | 1 + homeassistant/components/switch/litejet.py | 4 ++-- tests/components/binary_sensor/test_ffmpeg.py | 4 ++-- tests/components/test_alert.py | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py index 56109e27f1b9ec..c827fe8f7a4f6d 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -70,7 +70,7 @@ def pressed(): nonlocal held_less_than, held_more_than pressed_time = dt_util.utcnow() if held_more_than is None and held_less_than is None: - call_action() + hass.add_job(call_action) if held_more_than is not None and held_less_than is None: cancel_pressed_more_than = track_point_in_utc_time( hass, @@ -88,7 +88,7 @@ def released(): held_time = dt_util.utcnow() - pressed_time if held_less_than is not None and held_time < held_less_than: if held_more_than is None or held_time > held_more_than: - call_action() + hass.add_job(call_action) hass.data['litejet_system'].on_switch_pressed(number, pressed) hass.data['litejet_system'].on_switch_released(number, released) diff --git a/homeassistant/components/light/litejet.py b/homeassistant/components/light/litejet.py index 6534d180262acf..907d7f27fb9224 100644 --- a/homeassistant/components/light/litejet.py +++ b/homeassistant/components/light/litejet.py @@ -48,7 +48,7 @@ def __init__(self, hass, lj, i, name): def _on_load_changed(self): """Called on a LiteJet thread when a load's state changes.""" _LOGGER.debug("Updating due to notification for %s", self._name) - self._hass.async_add_job(self.async_update_ha_state(True)) + self.schedule_update_ha_state(True) @property def supported_features(self): diff --git a/homeassistant/components/sensor/mqtt_room.py b/homeassistant/components/sensor/mqtt_room.py index 432fff6780224b..427daa1a8a209f 100644 --- a/homeassistant/components/sensor/mqtt_room.py +++ b/homeassistant/components/sensor/mqtt_room.py @@ -98,6 +98,7 @@ def update_state(device_id, room, distance): self.hass.async_add_job(self.async_update_ha_state()) + @callback def message_received(topic, payload, qos): """A new MQTT message has been received.""" try: diff --git a/homeassistant/components/switch/litejet.py b/homeassistant/components/switch/litejet.py index 4ea565ff62ff90..87a82ed67f2a2a 100644 --- a/homeassistant/components/switch/litejet.py +++ b/homeassistant/components/switch/litejet.py @@ -47,12 +47,12 @@ def __init__(self, hass, lj, i, name): def _on_switch_pressed(self): _LOGGER.debug("Updating pressed for %s", self._name) self._state = True - self._hass.async_add_job(self.async_update_ha_state()) + self.schedule_update_ha_state() def _on_switch_released(self): _LOGGER.debug("Updating released for %s", self._name) self._state = False - self._hass.async_add_job(self.async_update_ha_state()) + self.schedule_update_ha_state() @property def name(self): diff --git a/tests/components/binary_sensor/test_ffmpeg.py b/tests/components/binary_sensor/test_ffmpeg.py index 64c540f439896e..aadafadd4a65aa 100644 --- a/tests/components/binary_sensor/test_ffmpeg.py +++ b/tests/components/binary_sensor/test_ffmpeg.py @@ -65,7 +65,7 @@ def test_setup_component_start_callback(self, mock_ffmpeg): entity = self.hass.states.get('binary_sensor.ffmpeg_noise') assert entity.state == 'off' - mock_ffmpeg.call_args[0][2](True) + self.hass.add_job(mock_ffmpeg.call_args[0][2], True) self.hass.block_till_done() entity = self.hass.states.get('binary_sensor.ffmpeg_noise') @@ -130,7 +130,7 @@ def test_setup_component_start_callback(self, mock_ffmpeg): entity = self.hass.states.get('binary_sensor.ffmpeg_motion') assert entity.state == 'off' - mock_ffmpeg.call_args[0][2](True) + self.hass.add_job(mock_ffmpeg.call_args[0][2], True) self.hass.block_till_done() entity = self.hass.states.get('binary_sensor.ffmpeg_motion') diff --git a/tests/components/test_alert.py b/tests/components/test_alert.py index 7fc25068732a2f..8150d08ff723d3 100644 --- a/tests/components/test_alert.py +++ b/tests/components/test_alert.py @@ -166,7 +166,7 @@ def record_event(event): def test_noack(self): """Test no ack feature.""" entity = alert.Alert(self.hass, *TEST_NOACK) - self.hass.async_add_job(entity.begin_alerting) + self.hass.add_job(entity.begin_alerting) self.hass.block_till_done() self.assertEqual(True, entity.hidden) From 650dfa6a339b9d53df1b043040396ac18a719596 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 5 Apr 2017 00:24:36 -0700 Subject: [PATCH 12/14] Only set UVLOOP when hass starts from CLI --- homeassistant/__main__.py | 20 ++++++++++++++++---- homeassistant/core.py | 7 +------ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 1d5da4e798f346..7035b26f670177 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -20,6 +20,17 @@ from homeassistant.util.async import run_callback_threadsafe +def attempt_use_uvloop(): + """Attempt to use uvloop.""" + import asyncio + + try: + import uvloop + asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + except ImportError: + pass + + def monkey_patch_asyncio(): """Replace weakref.WeakSet to address Python 3 bug. @@ -311,8 +322,7 @@ def open_browser(event): EVENT_HOMEASSISTANT_START, open_browser ) - hass.start() - return hass.exit_code + return hass.start() def try_to_restart() -> None: @@ -359,11 +369,13 @@ def try_to_restart() -> None: def main() -> int: """Start Home Assistant.""" + validate_python() + + attempt_use_uvloop() + if sys.version_info[:3] < (3, 5, 3): monkey_patch_asyncio() - validate_python() - args = get_arguments() if args.script is not None: diff --git a/homeassistant/core.py b/homeassistant/core.py index c8e13afde27475..284126a520baee 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -37,12 +37,6 @@ import homeassistant.util.location as location from homeassistant.util.unit_system import UnitSystem, METRIC_SYSTEM # NOQA -try: - import uvloop - asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -except ImportError: - pass - DOMAIN = 'homeassistant' # How long we wait for the result of a service call @@ -144,6 +138,7 @@ def start(self) -> None: # Block until stopped _LOGGER.info("Starting Home Assistant core loop") self.loop.run_forever() + return self.exit_code except KeyboardInterrupt: self.loop.create_task(self.async_stop()) self.loop.run_forever() From d758871a7dd9e16db0d6f0bbbbe3416363967ac3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 5 Apr 2017 00:25:02 -0700 Subject: [PATCH 13/14] This hangs normal asyncio event loop --- homeassistant/core.py | 3 ++- tests/common.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 284126a520baee..7c4ff43bd7aa2a 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -246,7 +246,8 @@ 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).result() + self.loop.call_soon_threadsafe( + self.loop.create_task, self.async_stop()) @asyncio.coroutine def async_stop(self, exit_code=0) -> None: diff --git a/tests/common.py b/tests/common.py index 471626a8ff5c53..9dc077dc3f7767 100644 --- a/tests/common.py +++ b/tests/common.py @@ -68,7 +68,6 @@ def start_hass(*mocks): def stop_hass(): """Stop hass.""" orig_stop() - loop.stop() stop_event.wait() loop.close() From 293d5750cb368ea777d40c5e4d7c5b9ded4a7451 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 5 Apr 2017 00:39:02 -0700 Subject: [PATCH 14/14] Clean up Z-Wave node entity test --- tests/components/zwave/test_node_entity.py | 60 ++++++++-------------- 1 file changed, 22 insertions(+), 38 deletions(-) diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index 385677b6d97ab3..c171155404f996 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -1,49 +1,33 @@ """Test Z-Wave node entity.""" +import asyncio import unittest -from unittest.mock import patch, Mock -from tests.common import get_test_home_assistant +from unittest.mock import patch import tests.mock.zwave as mock_zwave import pytest from homeassistant.components.zwave import node_entity -@pytest.mark.usefixtures('mock_openzwave') -class TestZWaveBaseEntity(unittest.TestCase): - """Class to test ZWaveBaseEntity.""" +@asyncio.coroutine +def test_maybe_schedule_update(hass, mock_openzwave): + """Test maybe schedule update.""" + base_entity = node_entity.ZWaveBaseEntity() + base_entity.hass = hass - def setUp(self): - """Initialize values for this testcase class.""" - self.hass = get_test_home_assistant() - - def call_soon(time, func, *args): - """Replace call_later by call_soon.""" - return self.hass.loop.call_soon(func, *args) - - self.hass.loop.call_later = call_soon - self.base_entity = node_entity.ZWaveBaseEntity() - self.base_entity.hass = self.hass - self.hass.start() - - def tearDown(self): # pylint: disable=invalid-name - """Stop everything that was started.""" - self.hass.stop() - - def test_maybe_schedule_update(self): - """Test maybe_schedule_update.""" - with patch.object(self.base_entity, 'async_update_ha_state', - Mock()) as mock_update: - self.base_entity.maybe_schedule_update() - self.hass.block_till_done() - mock_update.assert_called_once_with() - - def test_maybe_schedule_update_called_twice(self): - """Test maybe_schedule_update called twice.""" - with patch.object(self.base_entity, 'async_update_ha_state', - Mock()) as mock_update: - self.base_entity.maybe_schedule_update() - self.base_entity.maybe_schedule_update() - self.hass.block_till_done() - mock_update.assert_called_once_with() + with patch.object(hass.loop, 'call_later') as mock_call_later: + base_entity._schedule_update() + assert mock_call_later.called + + base_entity._schedule_update() + assert len(mock_call_later.mock_calls) == 1 + + do_update = mock_call_later.mock_calls[0][1][1] + + with patch.object(hass, 'async_add_job') as mock_add_job: + do_update() + assert mock_add_job.called + + base_entity._schedule_update() + assert len(mock_call_later.mock_calls) == 2 @pytest.mark.usefixtures('mock_openzwave')