From d1f4901d537e85a5efa68800016e83402b147724 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen <paulus@paulusschoutsen.nl> Date: Thu, 30 Jun 2016 09:02:12 -0700 Subject: [PATCH] Migrate to cherrypy wsgi from eventlet (#2387) --- homeassistant/components/api.py | 37 ++--- homeassistant/components/camera/__init__.py | 5 +- homeassistant/components/http.py | 54 ++++-- homeassistant/core.py | 24 +++ homeassistant/remote.py | 15 +- requirements_all.txt | 7 +- setup.py | 1 - .../device_tracker/test_locative.py | 9 +- tests/components/test_alexa.py | 7 +- tests/components/test_api.py | 154 +++++++++--------- tests/components/test_frontend.py | 7 +- tests/components/test_http.py | 9 +- tests/test_remote.py | 18 +- 13 files changed, 181 insertions(+), 166 deletions(-) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index b538a62d008..f0073bad838 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -6,7 +6,7 @@ https://home-assistant.io/developers/api/ """ import json import logging -from time import time +import queue import homeassistant.core as ha import homeassistant.remote as rem @@ -72,19 +72,14 @@ class APIEventStream(HomeAssistantView): def get(self, request): """Provide a streaming interface for the event bus.""" - from eventlet.queue import LightQueue, Empty - import eventlet - - cur_hub = eventlet.hubs.get_hub() - request.environ['eventlet.minimum_write_chunk_size'] = 0 - to_write = LightQueue() stop_obj = object() + to_write = queue.Queue() restrict = request.args.get('restrict') if restrict: - restrict = restrict.split(',') + restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP] - def thread_forward_events(event): + def forward_events(event): """Forward events to the open request.""" if event.event_type == EVENT_TIME_CHANGED: return @@ -99,28 +94,20 @@ class APIEventStream(HomeAssistantView): else: data = json.dumps(event, cls=rem.JSONEncoder) - cur_hub.schedule_call_global(0, lambda: to_write.put(data)) + to_write.put(data) def stream(): """Stream events to response.""" - self.hass.bus.listen(MATCH_ALL, thread_forward_events) + self.hass.bus.listen(MATCH_ALL, forward_events) _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) - last_msg = time() # Fire off one message right away to have browsers fire open event to_write.put(STREAM_PING_PAYLOAD) while True: try: - # Somehow our queue.get sometimes takes too long to - # be notified of arrival of data. Probably - # because of our spawning on hub in other thread - # hack. Because current goal is to get this out, - # We just timeout every second because it will - # return right away if qsize() > 0. - # So yes, we're basically polling :( - payload = to_write.get(timeout=1) + payload = to_write.get(timeout=STREAM_PING_INTERVAL) if payload is stop_obj: break @@ -129,15 +116,13 @@ class APIEventStream(HomeAssistantView): _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), msg.strip()) yield msg.encode("UTF-8") - last_msg = time() - except Empty: - if time() - last_msg > 50: - to_write.put(STREAM_PING_PAYLOAD) + except queue.Empty: + to_write.put(STREAM_PING_PAYLOAD) except GeneratorExit: - _LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) break - self.hass.bus.remove_listener(MATCH_ALL, thread_forward_events) + _LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) + self.hass.bus.remove_listener(MATCH_ALL, forward_events) return self.Response(stream(), mimetype='text/event-stream') diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 87342528987..2f23118a1c3 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -6,6 +6,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/camera/ """ import logging +import time from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -81,8 +82,6 @@ class Camera(Entity): def mjpeg_stream(self, response): """Generate an HTTP MJPEG stream from camera images.""" - import eventlet - def stream(): """Stream images as mjpeg stream.""" try: @@ -99,7 +98,7 @@ class Camera(Entity): last_image = img_bytes - eventlet.sleep(0.5) + time.sleep(0.5) except GeneratorExit: pass diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index d170f2a713e..11aa18cad5c 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -13,19 +13,19 @@ import re import ssl import voluptuous as vol -import homeassistant.core as ha import homeassistant.remote as rem from homeassistant import util from homeassistant.const import ( SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, - HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS) + HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS, + EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) from homeassistant.helpers.entity import split_entity_id import homeassistant.util.dt as dt_util import homeassistant.helpers.config_validation as cv DOMAIN = "http" -REQUIREMENTS = ("eventlet==0.19.0", "static3==0.7.0", "Werkzeug==0.11.10") +REQUIREMENTS = ("cherrypy==6.0.2", "static3==0.7.0", "Werkzeug==0.11.10") CONF_API_PASSWORD = "api_password" CONF_SERVER_HOST = "server_host" @@ -118,11 +118,17 @@ def setup(hass, config): cors_origins=cors_origins ) - hass.bus.listen_once( - ha.EVENT_HOMEASSISTANT_START, - lambda event: - threading.Thread(target=server.start, daemon=True, - name='WSGI-server').start()) + def start_wsgi_server(event): + """Start the WSGI server.""" + server.start() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_wsgi_server) + + def stop_wsgi_server(event): + """Stop the WSGI server.""" + server.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_wsgi_server) hass.wsgi = server hass.config.api = rem.API(server_host if server_host != '0.0.0.0' @@ -241,6 +247,7 @@ class HomeAssistantWSGI(object): self.server_port = server_port self.cors_origins = cors_origins self.event_forwarder = None + self.server = None def register_view(self, view): """Register a view with the WSGI server. @@ -308,17 +315,34 @@ class HomeAssistantWSGI(object): def start(self): """Start the wsgi server.""" - from eventlet import wsgi - import eventlet + from cherrypy import wsgiserver + from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter + + # pylint: disable=too-few-public-methods,super-init-not-called + class ContextSSLAdapter(BuiltinSSLAdapter): + """SSL Adapter that takes in an SSL context.""" + + def __init__(self, context): + self.context = context + + # pylint: disable=no-member + self.server = wsgiserver.CherryPyWSGIServer( + (self.server_host, self.server_port), self, + server_name='Home Assistant') - sock = eventlet.listen((self.server_host, self.server_port)) if self.ssl_certificate: context = ssl.SSLContext(SSL_VERSION) context.options |= SSL_OPTS context.set_ciphers(CIPHERS) context.load_cert_chain(self.ssl_certificate, self.ssl_key) - sock = context.wrap_socket(sock, server_side=True) - wsgi.server(sock, self, log=_LOGGER) + self.server.ssl_adapter = ContextSSLAdapter(context) + + threading.Thread(target=self.server.start, daemon=True, + name='WSGI-server').start() + + def stop(self): + """Stop the wsgi server.""" + self.server.stop() def dispatch_request(self, request): """Handle incoming request.""" @@ -365,6 +389,10 @@ class HomeAssistantWSGI(object): """Handle a request for base app + extra apps.""" from werkzeug.wsgi import DispatcherMiddleware + if not self.hass.is_running: + from werkzeug.exceptions import BadRequest + return BadRequest()(environ, start_response) + app = DispatcherMiddleware(self.base_app, self.extra_apps) # Strip out any cachebusting MD5 fingerprints fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', '')) diff --git a/homeassistant/core.py b/homeassistant/core.py index cbf02ea587f..82ec20c82f9 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -49,6 +49,19 @@ MIN_WORKER_THREAD = 2 _LOGGER = logging.getLogger(__name__) +class CoreState(enum.Enum): + """Represent the current state of Home Assistant.""" + + not_running = "NOT_RUNNING" + starting = "STARTING" + running = "RUNNING" + stopping = "STOPPING" + + def __str__(self): + """Return the event.""" + return self.value + + class HomeAssistant(object): """Root object of the Home Assistant home automation.""" @@ -59,14 +72,23 @@ class HomeAssistant(object): self.services = ServiceRegistry(self.bus, pool) self.states = StateMachine(self.bus) self.config = Config() + self.state = CoreState.not_running + + @property + def is_running(self): + """Return if Home Assistant is running.""" + return self.state == CoreState.running def start(self): """Start home assistant.""" _LOGGER.info( "Starting Home Assistant (%d threads)", self.pool.worker_count) + self.state = CoreState.starting create_timer(self) self.bus.fire(EVENT_HOMEASSISTANT_START) + self.pool.block_till_done() + self.state = CoreState.running def block_till_stopped(self): """Register service homeassistant/stop and will block until called.""" @@ -113,8 +135,10 @@ class HomeAssistant(object): def stop(self): """Stop Home Assistant and shuts down all threads.""" _LOGGER.info("Stopping") + self.state = CoreState.stopping self.bus.fire(EVENT_HOMEASSISTANT_STOP) self.pool.stop() + self.state = CoreState.not_running class JobPriority(util.OrderedEnum): diff --git a/homeassistant/remote.py b/homeassistant/remote.py index b2dfc3ae18f..6c49decdff2 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -11,6 +11,7 @@ from datetime import datetime import enum import json import logging +import time import threading import urllib.parse @@ -123,6 +124,7 @@ class HomeAssistant(ha.HomeAssistant): self.services = ha.ServiceRegistry(self.bus, pool) self.states = StateMachine(self.bus, self.remote_api) self.config = ha.Config() + self.state = ha.CoreState.not_running self.config.api = local_api @@ -134,17 +136,20 @@ class HomeAssistant(ha.HomeAssistant): raise HomeAssistantError( 'Unable to setup local API to receive events') + self.state = ha.CoreState.starting ha.create_timer(self) self.bus.fire(ha.EVENT_HOMEASSISTANT_START, origin=ha.EventOrigin.remote) - # Give eventlet time to startup - import eventlet - eventlet.sleep(0.1) + # Ensure local HTTP is started + self.pool.block_till_done() + self.state = ha.CoreState.running + time.sleep(0.05) # Setup that events from remote_api get forwarded to local_api - # Do this after we fire START, otherwise HTTP is not started + # Do this after we are running, otherwise HTTP is not started + # or requests are blocked if not connect_remote_events(self.remote_api, self.config.api): raise HomeAssistantError(( 'Could not setup event forwarding from api {} to ' @@ -153,6 +158,7 @@ class HomeAssistant(ha.HomeAssistant): def stop(self): """Stop Home Assistant and shuts down all threads.""" _LOGGER.info("Stopping") + self.state = ha.CoreState.stopping self.bus.fire(ha.EVENT_HOMEASSISTANT_STOP, origin=ha.EventOrigin.remote) @@ -161,6 +167,7 @@ class HomeAssistant(ha.HomeAssistant): # Disconnect master event forwarding disconnect_remote_events(self.remote_api, self.config.api) + self.state = ha.CoreState.not_running class EventBus(ha.EventBus): diff --git a/requirements_all.txt b/requirements_all.txt index 7ee914b2487..2dc4da44710 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,6 @@ pytz>=2016.4 pip>=7.0.0 jinja2>=2.8 voluptuous==0.8.9 -eventlet==0.19.0 # homeassistant.components.isy994 PyISY==1.0.6 @@ -48,6 +47,9 @@ blockchain==1.3.3 # homeassistant.components.notify.aws_sqs boto3==1.3.1 +# homeassistant.components.http +cherrypy==6.0.2 + # homeassistant.components.notify.xmpp dnspython3==1.12.0 @@ -61,9 +63,6 @@ eliqonline==1.0.12 # homeassistant.components.enocean enocean==0.31 -# homeassistant.components.http -eventlet==0.19.0 - # homeassistant.components.thermostat.honeywell evohomeclient==0.2.5 diff --git a/setup.py b/setup.py index b574e156931..fbce912c3d6 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,6 @@ REQUIRES = [ 'pip>=7.0.0', 'jinja2>=2.8', 'voluptuous==0.8.9', - 'eventlet==0.19.0', ] setup( diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/device_tracker/test_locative.py index 7445b5daf8c..427980be5f1 100644 --- a/tests/components/device_tracker/test_locative.py +++ b/tests/components/device_tracker/test_locative.py @@ -1,8 +1,8 @@ """The tests the for Locative device tracker platform.""" +import time import unittest from unittest.mock import patch -import eventlet import requests from homeassistant import bootstrap, const @@ -32,12 +32,9 @@ def setUpModule(): # pylint: disable=invalid-name bootstrap.setup_component(hass, http.DOMAIN, { http.DOMAIN: { http.CONF_SERVER_PORT: SERVER_PORT - } + }, }) - # Set up API - bootstrap.setup_component(hass, 'api') - # Set up device tracker bootstrap.setup_component(hass, device_tracker.DOMAIN, { device_tracker.DOMAIN: { @@ -46,7 +43,7 @@ def setUpModule(): # pylint: disable=invalid-name }) hass.start() - eventlet.sleep(0.05) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name diff --git a/tests/components/test_alexa.py b/tests/components/test_alexa.py index e1eb257577c..97d73b8b49d 100644 --- a/tests/components/test_alexa.py +++ b/tests/components/test_alexa.py @@ -1,9 +1,9 @@ """The tests for the Alexa component.""" # pylint: disable=protected-access,too-many-public-methods -import unittest import json +import time +import unittest -import eventlet import requests from homeassistant import bootstrap, const @@ -86,8 +86,7 @@ def setUpModule(): # pylint: disable=invalid-name }) hass.start() - - eventlet.sleep(0.1) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 8d1ee1c4ad5..752980e65c8 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -1,12 +1,12 @@ """The tests for the Home Assistant API component.""" # pylint: disable=protected-access,too-many-public-methods -# from contextlib import closing +from contextlib import closing import json import tempfile +import time import unittest from unittest.mock import patch -import eventlet import requests from homeassistant import bootstrap, const @@ -48,10 +48,7 @@ def setUpModule(): # pylint: disable=invalid-name bootstrap.setup_component(hass, 'api') hass.start() - - # To start HTTP - # TODO fix this - eventlet.sleep(0.05) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name @@ -387,25 +384,23 @@ class TestAPI(unittest.TestCase): headers=HA_HEADERS) self.assertEqual(422, req.status_code) - # TODO disabled because eventlet cannot validate - # a connection to itself, need a second instance - # # Setup a real one - # req = requests.post( - # _url(const.URL_API_EVENT_FORWARD), - # data=json.dumps({ - # 'api_password': API_PASSWORD, - # 'host': '127.0.0.1', - # 'port': SERVER_PORT - # }), - # headers=HA_HEADERS) - # self.assertEqual(200, req.status_code) - - # # Delete it again.. - # req = requests.delete( - # _url(const.URL_API_EVENT_FORWARD), - # data=json.dumps({}), - # headers=HA_HEADERS) - # self.assertEqual(400, req.status_code) + # Setup a real one + req = requests.post( + _url(const.URL_API_EVENT_FORWARD), + data=json.dumps({ + 'api_password': API_PASSWORD, + 'host': '127.0.0.1', + 'port': SERVER_PORT + }), + headers=HA_HEADERS) + self.assertEqual(200, req.status_code) + + # Delete it again.. + req = requests.delete( + _url(const.URL_API_EVENT_FORWARD), + data=json.dumps({}), + headers=HA_HEADERS) + self.assertEqual(400, req.status_code) req = requests.delete( _url(const.URL_API_EVENT_FORWARD), @@ -425,57 +420,58 @@ class TestAPI(unittest.TestCase): headers=HA_HEADERS) self.assertEqual(200, req.status_code) - # def test_stream(self): - # """Test the stream.""" - # listen_count = self._listen_count() - # with closing(requests.get(_url(const.URL_API_STREAM), timeout=3, - # stream=True, headers=HA_HEADERS)) as req: - - # self.assertEqual(listen_count + 1, self._listen_count()) - - # hass.bus.fire('test_event') - - # data = self._stream_next_event(req) - - # self.assertEqual('test_event', data['event_type']) - - # def test_stream_with_restricted(self): - # """Test the stream with restrictions.""" - # listen_count = self._listen_count() - # url = _url('{}?restrict=test_event1,test_event3'.format( - # const.URL_API_STREAM)) - # with closing(requests.get(url, stream=True, timeout=3, - # headers=HA_HEADERS)) as req: - # self.assertEqual(listen_count + 1, self._listen_count()) - - # hass.bus.fire('test_event1') - # data = self._stream_next_event(req) - # self.assertEqual('test_event1', data['event_type']) - - # hass.bus.fire('test_event2') - # hass.bus.fire('test_event3') - - # data = self._stream_next_event(req) - # self.assertEqual('test_event3', data['event_type']) - - # def _stream_next_event(self, stream): - # """Read the stream for next event while ignoring ping.""" - # while True: - # data = b'' - # last_new_line = False - # for dat in stream.iter_content(1): - # if dat == b'\n' and last_new_line: - # break - # data += dat - # last_new_line = dat == b'\n' - - # conv = data.decode('utf-8').strip()[6:] - - # if conv != 'ping': - # break - - # return json.loads(conv) - - # def _listen_count(self): - # """Return number of event listeners.""" - # return sum(hass.bus.listeners.values()) + def test_stream(self): + """Test the stream.""" + listen_count = self._listen_count() + with closing(requests.get(_url(const.URL_API_STREAM), timeout=3, + stream=True, headers=HA_HEADERS)) as req: + stream = req.iter_content(1) + self.assertEqual(listen_count + 1, self._listen_count()) + + hass.bus.fire('test_event') + + data = self._stream_next_event(stream) + + self.assertEqual('test_event', data['event_type']) + + def test_stream_with_restricted(self): + """Test the stream with restrictions.""" + listen_count = self._listen_count() + url = _url('{}?restrict=test_event1,test_event3'.format( + const.URL_API_STREAM)) + with closing(requests.get(url, stream=True, timeout=3, + headers=HA_HEADERS)) as req: + stream = req.iter_content(1) + self.assertEqual(listen_count + 1, self._listen_count()) + + hass.bus.fire('test_event1') + data = self._stream_next_event(stream) + self.assertEqual('test_event1', data['event_type']) + + hass.bus.fire('test_event2') + hass.bus.fire('test_event3') + + data = self._stream_next_event(stream) + self.assertEqual('test_event3', data['event_type']) + + def _stream_next_event(self, stream): + """Read the stream for next event while ignoring ping.""" + while True: + data = b'' + last_new_line = False + for dat in stream: + if dat == b'\n' and last_new_line: + break + data += dat + last_new_line = dat == b'\n' + + conv = data.decode('utf-8').strip()[6:] + + if conv != 'ping': + break + + return json.loads(conv) + + def _listen_count(self): + """Return number of event listeners.""" + return sum(hass.bus.listeners.values()) diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 61e33931c24..083ebd2eb0c 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -1,9 +1,9 @@ """The tests for Home Assistant frontend.""" # pylint: disable=protected-access,too-many-public-methods import re +import time import unittest -import eventlet import requests import homeassistant.bootstrap as bootstrap @@ -42,10 +42,7 @@ def setUpModule(): # pylint: disable=invalid-name bootstrap.setup_component(hass, 'frontend') hass.start() - - # Give eventlet time to start - # TODO fix this - eventlet.sleep(0.05) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name diff --git a/tests/components/test_http.py b/tests/components/test_http.py index f665a9530c8..6ab79f3e0cc 100644 --- a/tests/components/test_http.py +++ b/tests/components/test_http.py @@ -1,8 +1,8 @@ """The tests for the Home Assistant HTTP component.""" # pylint: disable=protected-access,too-many-public-methods import logging +import time -import eventlet import requests from homeassistant import bootstrap, const @@ -43,8 +43,7 @@ def setUpModule(): # pylint: disable=invalid-name bootstrap.setup_component(hass, 'api') hass.start() - - eventlet.sleep(0.05) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name @@ -83,7 +82,7 @@ class TestHttp: logs = caplog.text() - assert const.URL_API in logs + # assert const.URL_API in logs assert API_PASSWORD not in logs def test_access_denied_with_wrong_password_in_url(self): @@ -106,5 +105,5 @@ class TestHttp: logs = caplog.text() - assert const.URL_API in logs + # assert const.URL_API in logs assert API_PASSWORD not in logs diff --git a/tests/test_remote.py b/tests/test_remote.py index 893f02bea31..f3ec35daee5 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -1,9 +1,8 @@ """Test Home Assistant remote methods and classes.""" # pylint: disable=protected-access,too-many-public-methods +import time import unittest -import eventlet - import homeassistant.core as ha import homeassistant.bootstrap as bootstrap import homeassistant.remote as remote @@ -47,10 +46,7 @@ def setUpModule(): # pylint: disable=invalid-name bootstrap.setup_component(hass, 'api') hass.start() - - # Give eventlet time to start - # TODO fix this - eventlet.sleep(0.05) + time.sleep(0.05) master_api = remote.API("127.0.0.1", API_PASSWORD, MASTER_PORT) @@ -63,10 +59,6 @@ def setUpModule(): # pylint: disable=invalid-name slave.start() - # Give eventlet time to start - # TODO fix this - eventlet.sleep(0.05) - def tearDownModule(): # pylint: disable=invalid-name """Stop the Home Assistant server and slave.""" @@ -257,7 +249,6 @@ class TestRemoteClasses(unittest.TestCase): slave.pool.block_till_done() # Wait till master gives updated state hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertEqual("remote.statemachine test", slave.states.get("remote.test").state) @@ -266,13 +257,11 @@ class TestRemoteClasses(unittest.TestCase): """Remove statemachine from master.""" hass.states.set("remote.master_remove", "remove me!") hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertIn('remote.master_remove', slave.states.entity_ids()) hass.states.remove("remote.master_remove") hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertNotIn('remote.master_remove', slave.states.entity_ids()) @@ -280,14 +269,12 @@ class TestRemoteClasses(unittest.TestCase): """Remove statemachine from slave.""" hass.states.set("remote.slave_remove", "remove me!") hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertIn('remote.slave_remove', slave.states.entity_ids()) self.assertTrue(slave.states.remove("remote.slave_remove")) slave.pool.block_till_done() hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertNotIn('remote.slave_remove', slave.states.entity_ids()) @@ -306,6 +293,5 @@ class TestRemoteClasses(unittest.TestCase): slave.pool.block_till_done() # Wait till master gives updated event hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertEqual(1, len(test_value)) -- GitLab