Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

async HTTP component #3914

Merged
merged 12 commits into from Oct 24, 2016
Merged

async HTTP component #3914

merged 12 commits into from Oct 24, 2016

Conversation

balloob
Copy link
Member

@balloob balloob commented Oct 17, 2016

I have been working on porting over the HTTP component to use aiohttp instead of CherryPy WSGI server + Werkzeug. Once this is done, adding a websocket server is pretty much free too 👍

I had to enhance some internal aiohttp classes to get gzip / md5 fingerprinting / caching working. I am planning to see if I can get support for this merged upstream.

Features to add back:

Components to fix:

  • API
  • Frontend
  • logbook
  • history
  • camera
  • emulated_hue
  • Alexa
  • device tracker - locative
  • foursquare
  • media player
  • sensor - torque
  • sensor - fitbit
  • sensor - netio

@balloob balloob added the async label Oct 17, 2016
@mention-bot
Copy link

@balloob, thanks for your PR! By analyzing the history of the files in this pull request, we identified @fabaff, @robbiet480 and @JshWright to be potential reviewers.



class APIEntityStateView(HomeAssistantView):
"""View to handle EntityState requests."""

url = "/api/states/<entity(exist=False):entity_id>"
url = "/api/states/{entity_id}" # TODO validation <entity(exist=False):entity_id>

Choose a reason for hiding this comment

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

farcy v1.1

  • 80: E501 line too long (86 > 79 characters)

def stop_wsgi_server(event):
"""Stop the WSGI server."""
server.stop()
def start_server(event):

Choose a reason for hiding this comment

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

farcy v1.1

  • 5: F811 redefinition of unused 'start_server' from line 138

# if cors_check:
# response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN] = \
# environ.get('HTTP_ORIGIN')
# response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS] = \

Choose a reason for hiding this comment

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

farcy v1.1

  • 80: E501 line too long (80 > 79 characters)

# TODO CORS ?
return web.Response('', status=200)

def __call__(self, request):

Choose a reason for hiding this comment

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

farcy v1.1

  • D102: Missing docstring in public method

@balloob
Copy link
Member Author

balloob commented Oct 17, 2016

I am planning on adding aiohttp to the core dependencies, just like requests is.

def stop_wsgi_server(event):
"""Stop the WSGI server."""
server.stop()
def start_server(event):

Choose a reason for hiding this comment

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

farcy v1.1

  • 5: F811 redefinition of unused 'start_server' from line 137


@asyncio.coroutine

Choose a reason for hiding this comment

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

farcy v1.1

  • 5: E303 too many blank lines (2)


from homeassistant.core import callback, is_callback

Choose a reason for hiding this comment

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

farcy v1.1

  • 1: F401 'callback' imported but unused


if FFMPEG_CONFIG.get(CONF_RUN_TEST):
# if in cache
if input_source in FFMPEG_TEST_CACHE:
return FFMPEG_TEST_CACHE[input_source]

# run test
test = Test(get_binary())
if not test.run_test(input_source):
ffmpeg_test = Test(get_binary())

Choose a reason for hiding this comment

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

farcy v1.1

  • 23: F821 undefined name 'Test'

@balloob balloob changed the title WIP: async HTTP component async HTTP component Oct 19, 2016
raise BadRequest('Unable to read JSON request')

return Request
class GzipFileSender(FileSender):

Choose a reason for hiding this comment

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

farcy v1.1

  • D204: 1 blank line required after class docstring (found 0)

@balloob
Copy link
Member Author

balloob commented Oct 19, 2016

Besides the camera view, this is ready for review.

@bbangert, when you have time, could you take a look. Thanks.

break
response.write(data)
# pylint: disable=broad-except
except Exception as err:

Choose a reason for hiding this comment

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

farcy v1.1

  • 13: F841 local variable 'err' is assigned to but never used

response.write(data)
# pylint: disable=broad-except
except Exception as err:
_LOGGER.debug("Exception on stream sending.", exc_info=True)

Choose a reason for hiding this comment

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

farcy v1.1

  • 17: F821 undefined name '_LOGGER'

"""Stream images as mjpeg stream."""
img_bytes = yield from self.entity.async_camera_image()

if img_bytes is not None and img_bytes != last_image:

Choose a reason for hiding this comment

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

farcy v1.1

  • 51: F821 undefined name 'last_image'

img_bytes = yield from self.entity.async_camera_image()

if img_bytes is not None and img_bytes != last_image:
img_bytes = bytes(

Choose a reason for hiding this comment

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

farcy v1.1

  • 24: E222 multiple spaces after operator

response.write(data)
# pylint: disable=broad-except
except Exception:
_LOGGER.debug("Exception on stream sending.", exc_info=True)

Choose a reason for hiding this comment

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

farcy v1.1

  • 17: F821 undefined name '_LOGGER'

# connect to stream
try:
with async_timeout.timeout(10, loop=self.hass.loop):
stream = yield from session.get(self._mjpeg_url)

Choose a reason for hiding this comment

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

farcy v1.1

  • 37: F821 undefined name 'session'

else:
self._auth = None

self._last_url = None
self._last_image = None
self._session = aiohttp.ClientSession(loop=hass.loop, auth=auth)

Choose a reason for hiding this comment

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

farcy v1.1

  • 68: F821 undefined name 'auth'


def camera_image(self):
"""Return bytes of camera image."""
return run_coroutine_threadsafe(

Choose a reason for hiding this comment

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

farcy v1.1

  • 16: F821 undefined name 'run_coroutine_threadsafe'

@@ -4,10 +4,14 @@
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.generic/
"""
import asyncio
import functools as ft

Choose a reason for hiding this comment

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

farcy v1.1

  • 1: F401 'ft' imported but unused

@pvizeli
Copy link
Member

pvizeli commented Oct 19, 2016

So my work on camera component and his platform is done 💯

return self._requests[key]


class AiohttpClientMockResponse:

Choose a reason for hiding this comment

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

farcy v1.1

  • D204: 1 blank line required after class docstring (found 0)


class AiohttpClientMockResponse:
"""Mock Aiohttp client response."""
def __init__(self, method, url, status, response):

Choose a reason for hiding this comment

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

farcy v1.1

  • D102: Missing docstring in public method



@pytest.fixture
def aioclient_mock():

Choose a reason for hiding this comment

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

farcy v1.1

  • D202: No blank lines allowed after function docstring (found 1)

Copy link
Member

@bbangert bbangert left a comment

Choose a reason for hiding this comment

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

Looks good overall. Left a few questions that may or may not be actual issues, hard to hold all the context at once. It might be nice to clarify whether async_ means its async safe, or is actually a @callback.


return self.json_message("Event forwarding setup.")

@asyncio.coroutine
def delete(self, request):
"""Remove event forwarer."""
Copy link
Member

Choose a reason for hiding this comment

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

forwarder perhaps?

@@ -364,6 +398,7 @@ class APIErrorLogView(HomeAssistantView):
url = URL_API_ERROR_LOG
name = "api:error-log"

@asyncio.coroutine
def get(self, request):
"""Serve error log."""
return self.file(request, self.hass.config.path(ERROR_LOG_FILENAME))
Copy link
Member

Choose a reason for hiding this comment

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

A file request that doesn't block?


def write(img_bytes):
"""Write image to stream."""
response.write(bytes(
Copy link
Member

Choose a reason for hiding this comment

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

Nonblocking call?

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Just read 2 comments below 👍 we're all good

break

if img_bytes is not None and img_bytes != last_image:
write(img_bytes)
Copy link
Member

Choose a reason for hiding this comment

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

I would've expected a yield to be able to write to a socket under async.

Copy link
Member Author

Choose a reason for hiding this comment

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

See above.

write(img_bytes)

last_image = img_bytes
yield from response.drain()
Copy link
Member

Choose a reason for hiding this comment

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

If this is the call that waits for a write buffer to empty, ignore the above comments about waiting on the write.

Copy link
Member Author

Choose a reason for hiding this comment

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

I should have read all the comments before commenting 🎉

@@ -193,9 +201,10 @@ def __init__(self, hass, extra_urls):
)
)

@asyncio.coroutine
Copy link
Member

Choose a reason for hiding this comment

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

A coroutine but nothing needs to yield?

@@ -230,7 +239,7 @@ def get(self, request, entity_id=None):
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
Copy link
Member

Choose a reason for hiding this comment

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

async_render?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a jinja2 template instance, not a HASS instance.


return parsed
finally:
resp.set_tcp_nodelay(True)
Copy link
Member

Choose a reason for hiding this comment

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

Where is it set to False?

Copy link
Member Author

Choose a reason for hiding this comment

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

This code is a direct copy of the aiohttp code. I am planning on contributing it back to them. For now this is a direct copy of their code.

set_tcp_nodelay will reverse the set_tcp_cork call a few lines above. Docs.

@@ -100,14 +102,14 @@ def get(self, request):
elif is_value:
pid = convert_pid(is_value.group(1))
if pid in self.sensors:
self.sensors[pid].on_update(data[key])
self.sensors[pid].async_on_update(data[key])
Copy link
Member

Choose a reason for hiding this comment

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

Guaranteed to not be a coroutine?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes

Copy link
Member Author

Choose a reason for hiding this comment

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

I've added a callback annotation to the method for clarity


def disconnect(self, api):
def async_disconnect(self, api):
Copy link
Member

Choose a reason for hiding this comment

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

Are these callbacks or safe from to call from any thread?

Copy link
Member Author

Choose a reason for hiding this comment

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

Callbacks. Added annotation.

They are only called from the APi when setting up/deleting event forwarding.

@balloob
Copy link
Member Author

balloob commented Oct 24, 2016

@bbangert you found more than I would have expected. It's all fixed now 👯‍♂️

@balloob balloob merged commit 519d9f2 into dev Oct 24, 2016
@balloob balloob deleted the aiohttp branch October 24, 2016 06:48
@home-assistant home-assistant locked and limited conversation to collaborators Mar 17, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants