From 4615816b71c2d9eeef0d80f77cb102a0bc3d45fd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 17 Oct 2016 00:42:20 -0700 Subject: [PATCH] Fix stuff for async http --- homeassistant/components/alexa.py | 28 +++--- homeassistant/components/api.py | 18 ++-- .../components/device_tracker/locative.py | 24 +++-- homeassistant/components/foursquare.py | 28 +++--- homeassistant/components/frontend/__init__.py | 18 ++-- homeassistant/components/http.py | 82 ++++++++++------- homeassistant/components/logbook.py | 3 +- .../components/media_player/__init__.py | 32 ++++--- .../components/persistent_notification.py | 3 +- tests/components/media_player/test_demo.py | 87 ++++++++++--------- 10 files changed, 191 insertions(+), 132 deletions(-) diff --git a/homeassistant/components/alexa.py b/homeassistant/components/alexa.py index 64ff50af32372..ee90266d7c7cc 100644 --- a/homeassistant/components/alexa.py +++ b/homeassistant/components/alexa.py @@ -4,6 +4,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/alexa/ """ +import asyncio import copy import enum import logging @@ -12,6 +13,7 @@ import voluptuous as vol +from homeassistant.core import callback from homeassistant.const import HTTP_BAD_REQUEST from homeassistant.helpers import template, script, config_validation as cv from homeassistant.components.http import HomeAssistantView @@ -20,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) INTENTS_API_ENDPOINT = '/api/alexa' -FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/' +FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/{briefing_id}' CONF_ACTION = 'action' CONF_CARD = 'card' @@ -128,9 +130,10 @@ def __init__(self, hass, intents): self.intents = intents + @asyncio.coroutine def post(self, request): """Handle Alexa.""" - data = request.json + data = yield from request.json() _LOGGER.debug('Received Alexa request: %s', data) @@ -176,7 +179,7 @@ def post(self, request): action = config.get(CONF_ACTION) if action is not None: - action.run(response.variables) + yield from action.async_run(response.variables) # pylint: disable=unsubscriptable-object if speech is not None: @@ -218,8 +221,8 @@ def add_card(self, card_type, title, content): self.card = card return - card["title"] = title.render(self.variables) - card["content"] = content.render(self.variables) + card["title"] = title.async_render(self.variables) + card["content"] = content.async_render(self.variables) self.card = card def add_speech(self, speech_type, text): @@ -229,7 +232,7 @@ def add_speech(self, speech_type, text): key = 'ssml' if speech_type == SpeechType.ssml else 'text' if isinstance(text, template.Template): - text = text.render(self.variables) + text = text.async_render(self.variables) self.speech = { 'type': speech_type.value, @@ -244,7 +247,7 @@ def add_reprompt(self, speech_type, text): self.reprompt = { 'type': speech_type.value, - key: text.render(self.variables) + key: text.async_render(self.variables) } def as_dict(self): @@ -284,6 +287,7 @@ def __init__(self, hass, flash_briefings): template.attach(hass, self.flash_briefings) # pylint: disable=too-many-branches + @callback def get(self, request, briefing_id): """Handle Alexa Flash Briefing request.""" _LOGGER.debug('Received Alexa flash briefing request for: %s', @@ -292,7 +296,7 @@ def get(self, request, briefing_id): if self.flash_briefings.get(briefing_id) is None: err = 'No configured Alexa flash briefing was found for: %s' _LOGGER.error(err, briefing_id) - return self.Response(status=404) + return b'', 404 briefing = [] @@ -300,13 +304,13 @@ def get(self, request, briefing_id): output = {} if item.get(CONF_TITLE) is not None: if isinstance(item.get(CONF_TITLE), template.Template): - output[ATTR_TITLE_TEXT] = item[CONF_TITLE].render() + output[ATTR_TITLE_TEXT] = item[CONF_TITLE].async_render() else: output[ATTR_TITLE_TEXT] = item.get(CONF_TITLE) if item.get(CONF_TEXT) is not None: if isinstance(item.get(CONF_TEXT), template.Template): - output[ATTR_MAIN_TEXT] = item[CONF_TEXT].render() + output[ATTR_MAIN_TEXT] = item[CONF_TEXT].async_render() else: output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT) @@ -315,7 +319,7 @@ def get(self, request, briefing_id): if item.get(CONF_AUDIO) is not None: if isinstance(item.get(CONF_AUDIO), template.Template): - output[ATTR_STREAM_URL] = item[CONF_AUDIO].render() + output[ATTR_STREAM_URL] = item[CONF_AUDIO].async_render() else: output[ATTR_STREAM_URL] = item.get(CONF_AUDIO) @@ -323,7 +327,7 @@ def get(self, request, briefing_id): if isinstance(item.get(CONF_DISPLAY_URL), template.Template): output[ATTR_REDIRECTION_URL] = \ - item[CONF_DISPLAY_URL].render() + item[CONF_DISPLAY_URL].async_render() else: output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 9f10a67827403..0266753801885 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -61,7 +61,7 @@ class APIStatusView(HomeAssistantView): url = URL_API name = "api:status" - @asyncio.coroutine + @ha.callback def get(self, request): """Retrieve if API is running.""" return self.json_message('API running.') @@ -141,7 +141,7 @@ class APIConfigView(HomeAssistantView): url = URL_API_CONFIG name = "api:config" - @asyncio.coroutine + @ha.callback def get(self, request): """Get current configuration.""" return self.json(self.hass.config.as_dict()) @@ -154,7 +154,7 @@ class APIDiscoveryView(HomeAssistantView): url = URL_API_DISCOVERY_INFO name = "api:discovery" - @asyncio.coroutine + @ha.callback def get(self, request): """Get discovery info.""" needs_auth = self.hass.config.api.api_password is not None @@ -172,7 +172,7 @@ class APIStatesView(HomeAssistantView): url = URL_API_STATES name = "api:states" - @asyncio.coroutine + @ha.callback def get(self, request): """Get current states.""" return self.json(self.hass.states.async_all()) @@ -184,7 +184,7 @@ class APIEntityStateView(HomeAssistantView): url = "/api/states/{entity_id}" # TODO validation name = "api:entity-state" - @asyncio.coroutine + @ha.callback def get(self, request, entity_id): """Retrieve state of entity.""" state = self.hass.states.get(entity_id) @@ -224,7 +224,7 @@ def post(self, request, entity_id): return resp - @asyncio.coroutine + @ha.callback def delete(self, request, entity_id): """Remove entity.""" if self.hass.states.async_remove(entity_id): @@ -239,7 +239,7 @@ class APIEventListenersView(HomeAssistantView): url = URL_API_EVENTS name = "api:event-listeners" - @asyncio.coroutine + @ha.callback def get(self, request): """Get event listeners.""" return self.json(async_events_json(self.hass)) @@ -281,7 +281,7 @@ class APIServicesView(HomeAssistantView): url = URL_API_SERVICES name = "api:services" - @asyncio.coroutine + @ha.callback def get(self, request): """Get registered services.""" return self.json(async_services_json(self.hass)) @@ -384,7 +384,7 @@ class APIComponentsView(HomeAssistantView): url = URL_API_COMPONENTS name = "api:components" - @asyncio.coroutine + @ha.callback def get(self, request): """Get current loaded components.""" return self.json(self.hass.config.components) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index f3f2c3c94f575..a6a28dce6e350 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -4,6 +4,8 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.locative/ """ +import asyncio +from functools import partial import logging from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME @@ -35,15 +37,23 @@ def __init__(self, hass, see): super().__init__(hass) self.see = see + @asyncio.coroutine def get(self, request): """Locative message received as GET.""" - return self.post(request) + res = yield from self._handle(request.GET) + return res + @asyncio.coroutine def post(self, request): """Locative message received.""" - # pylint: disable=too-many-return-statements - data = request.values + data = yield from request.post() + res = yield from self._handle(data) + return res + @asyncio.coroutine + def _handle(self, data): + """Handle locative request.""" + # pylint: disable=too-many-return-statements if 'latitude' not in data or 'longitude' not in data: return ('Latitude and longitude not specified.', HTTP_UNPROCESSABLE_ENTITY) @@ -68,7 +78,9 @@ def post(self, request): direction = data['trigger'] if direction == 'enter': - self.see(dev_id=device, location_name=location_name) + yield from self.hass.loop.run_in_executor( + None, partial(self.see, dev_id=device, + location_name=location_name)) return 'Setting location to {}'.format(location_name) elif direction == 'exit': @@ -76,7 +88,9 @@ def post(self, request): '{}.{}'.format(DOMAIN, device)) if current_state is None or current_state.state == location_name: - self.see(dev_id=device, location_name=STATE_NOT_HOME) + yield from self.hass.loop.run_in_executor( + None, partial(self.see, dev_id=device, + location_name=STATE_NOT_HOME)) return 'Setting location to not home' else: # Ignore the message if it is telling us to exit a zone that we diff --git a/homeassistant/components/foursquare.py b/homeassistant/components/foursquare.py index b08ba89ca77de..99bfc063bad56 100644 --- a/homeassistant/components/foursquare.py +++ b/homeassistant/components/foursquare.py @@ -4,6 +4,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/foursquare/ """ +import asyncio import logging import os import json @@ -11,7 +12,7 @@ import requests import voluptuous as vol -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_BAD_REQUEST from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv from homeassistant.components.http import HomeAssistantView @@ -93,16 +94,21 @@ def __init__(self, hass, push_secret): super().__init__(hass) self.push_secret = push_secret + @asyncio.coroutine def post(self, request): """Accept the POST from Foursquare.""" - raw_data = request.form - _LOGGER.debug("Received Foursquare push: %s", raw_data) - if self.push_secret != raw_data["secret"]: + try: + data = yield from request.json() + except json.decoder.JSONDecodeError: + return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) + + secret = data.pop('secret', None) + + _LOGGER.debug("Received Foursquare push: %s", data) + + if self.push_secret != secret: _LOGGER.error("Received Foursquare push with invalid" - "push secret! Data: %s", raw_data) - return - parsed_payload = { - key: json.loads(val) for key, val in raw_data.items() - if key != "secret" - } - self.hass.bus.fire(EVENT_PUSH, parsed_payload) + "push secret: %s", secret) + return self.json_message('Incorrect secret', HTTP_BAD_REQUEST) + + self.hass.bus.async_fire(EVENT_PUSH, data) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 2d9abe8fe33a9..f89623ccd00b4 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -1,8 +1,12 @@ """Handle the frontend for Home Assistant.""" +import asyncio import hashlib +import json import logging import os +from aiohttp import web + from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.components import api from homeassistant.components.http import HomeAssistantView @@ -161,13 +165,14 @@ class BootstrapView(HomeAssistantView): url = "/api/bootstrap" name = "api:bootstrap" + @asyncio.coroutine def get(self, request): """Return all data needed to bootstrap Home Assistant.""" return self.json({ 'config': self.hass.config.as_dict(), - 'states': self.hass.states.all(), - 'events': api.events_json(self.hass), - 'services': api.services_json(self.hass), + 'states': self.hass.states.async_all(), + 'events': api.async_events_json(self.hass), + 'services': api.async_services_json(self.hass), 'panels': PANELS, }) @@ -193,6 +198,7 @@ def __init__(self, hass, extra_urls): ) ) + @asyncio.coroutine def get(self, request, entity_id=None): """Serve the index view.""" if self.hass.wsgi.development: @@ -230,7 +236,7 @@ def get(self, request, entity_id=None): icons_url=icons_url, icons=FINGERPRINTS['mdi.html'], panel_url=panel_url, panels=PANELS) - return self.Response(resp, mimetype='text/html') + return web.Response(text=resp, content_type='text/html') class ManifestJSONView(HomeAssistantView): @@ -240,8 +246,8 @@ class ManifestJSONView(HomeAssistantView): url = "/manifest.json" name = "manifestjson" + @asyncio.coroutine def get(self, request): """Return the manifest.json.""" - import json msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8') - return self.Response(msg, mimetype="application/manifest+json") + return web.Response(body=msg, content_type="application/manifest+json") diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index c96e9ba35d7d8..9d471916ff264 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -8,6 +8,7 @@ import hmac import json import logging +import os from pathlib import Path import re import ssl @@ -17,9 +18,9 @@ from aiohttp import web from aiohttp.file_sender import FileSender from aiohttp.errors import HttpMethodNotAllowed -from aiohttp.web_exceptions import HTTPUnauthorized +from aiohttp.web_exceptions import HTTPUnauthorized, HTTPMovedPermanently -from homeassistant.core import callback +from homeassistant.core import callback, is_callback import homeassistant.remote as rem from homeassistant import util from homeassistant.const import ( @@ -245,9 +246,7 @@ def __init__(self, hass, development, api_password, ssl_certificate, trusted_networks): """Initilalize the WSGI Home Assistant server.""" self.app = web.Application(loop=hass.loop) - self.views = {} self.hass = hass - self.extra_apps = {} self.development = development self.api_password = api_password self.ssl_certificate = ssl_certificate @@ -259,6 +258,13 @@ def __init__(self, hass, development, api_password, ssl_certificate, self.event_forwarder = None self.server = None + if development: + try: + import aiohttp_debugtoolbar + aiohttp_debugtoolbar.setup(self.app) + except ImportError: + pass + def register_view(self, view): """Register a view with the WSGI server. @@ -266,14 +272,11 @@ def register_view(self, view): It is optional to instantiate it before registering; this method will handle it either way. """ - if view.name in self.views: - _LOGGER.warning("View '%s' is being overwritten", view.name) if isinstance(view, type): # Instantiate the view, if needed view = view(self.hass) - self.views[view.name] = view - self.app.router.add_route('*', view.url, view) + self.app.router.add_route('*', view.url, view, name=view.name) for url in view.extra_urls: self.app.router.add_route('*', url, view) @@ -287,41 +290,43 @@ def register_redirect(self, url, redirect_to): for the redirect, otherwise it has to be a string with placeholders in rule syntax. """ - return # TODO - from werkzeug.routing import Rule - self.url_map.add(Rule(url, redirect_to=redirect_to)) + def redirect(request): + """Redirect to location.""" + raise HTTPMovedPermanently(redirect_to) + + self.app.router.add_route('GET', url, redirect) def register_static_path(self, url_root, path, cache_length=31): """Register a folder to serve as a static path. Specify optional cache length of asset in days. """ - return # TODO - # http://aiohttp.readthedocs.io/en/stable/web.html#static-file-handling - from static import Cling + # TODO - TEMPORARY WORKAROUND, DOES NOT SUPPORT GZIP + if os.path.isdir(path): + self.app.router.add_static(url_root, path) + return - headers = [] + @asyncio.coroutine + def serve_file(request): + """Redirect to location.""" + return FileSender().send(request, Path(path)) - if cache_length and not self.development: - # 1 year in seconds - cache_time = cache_length * 86400 + self.app.router.add_route('GET', url_root, serve_file) - headers.append({ - 'prefix': '', - HTTP_HEADER_CACHE_CONTROL: - "public, max-age={}".format(cache_time) - }) + # http://aiohttp.readthedocs.io/en/stable/web.html#static-file-handling - self.register_wsgi_app(url_root, Cling(path, headers=headers)) + # Cache static while not in development + # if cache_length and not self.development: + # # 1 year in seconds + # cache_time = cache_length * 86400 - def register_wsgi_app(self, url_root, app): - """Register a path to serve a WSGI app.""" - return # TODO remove - only used by register_static_path - if url_root in self.extra_apps: - _LOGGER.warning("Url root '%s' is being overwritten", url_root) + # headers.append({ + # 'prefix': '', + # HTTP_HEADER_CACHE_CONTROL: + # "public, max-age={}".format(cache_time) + # }) - self.extra_apps[url_root] = app @asyncio.coroutine def start(self): @@ -417,6 +422,7 @@ def options(self, request): return web.Response('', status=200) def __call__(self, request): + """Handle incoming request.""" try: handler = getattr(self, request.method.lower()) except AttributeError: @@ -456,10 +462,13 @@ def __call__(self, request): _LOGGER.info('Serving %s to %s (auth: %s)', request.path, remote_addr, authenticated) - assert asyncio.iscoroutinefunction(handler), \ - 'handler should be a coroutine' + assert asyncio.iscoroutinefunction(handler) or is_callback(handler), \ + "Handler should be a coroutine or a callback" + + result = handler(request, **request.match_info) - result = yield from handler(request, **request.match_info) + if asyncio.iscoroutine(result): + result = yield from result if isinstance(result, web.StreamResponse): # The method handler returned a ready-made Response, how nice of it @@ -470,4 +479,11 @@ def __call__(self, request): if isinstance(result, tuple): result, status_code = result + if isinstance(result, str): + result = result.encode('utf-8') + elif result is None: + result = b'' + elif not isinstance(result, bytes): + assert False, 'Result should be None, string, bytes or Response.' + return web.Response(body=result, status=status_code) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 557c59a33ecb7..e732d5c350d77 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -115,13 +115,14 @@ class LogbookView(HomeAssistantView): url = '/api/logbook' name = 'api:logbook' - extra_urls = ['/api/logbook/'] + extra_urls = ['/api/logbook/{datetime}'] def __init__(self, hass, config): """Initilalize the logbook view.""" super().__init__(hass) self.config = config + @asyncio.coroutine def get(self, request, datetime=None): """Retrieve logbook entries.""" start_day = dt_util.as_utc(datetime or dt_util.start_of_local_day()) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index a3a6274a89eac..ebd3625912f09 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -4,11 +4,13 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/media_player/ """ +import asyncio import hashlib import logging import os import requests +from aiohttp import web import voluptuous as vol from homeassistant.config import load_yaml_config_file @@ -677,7 +679,7 @@ class MediaPlayerImageView(HomeAssistantView): """Media player view to serve an image.""" requires_auth = False - url = "/api/media_player_proxy/" + url = "/api/media_player_proxy/{entity_id}" name = "api:media_player:image" def __init__(self, hass, entities): @@ -685,26 +687,34 @@ def __init__(self, hass, entities): super().__init__(hass) self.entities = entities + @asyncio.coroutine def get(self, request, entity_id): """Start a get request.""" player = self.entities.get(entity_id) - if player is None: - return self.Response(status=404) + return web.Response(status=404) authenticated = (request.authenticated or - request.args.get('token') == player.access_token) + request.GET.get('token') == player.access_token) if not authenticated: - return self.Response(status=401) + return web.Response(status=401) image_url = player.media_image_url - if image_url: - response = requests.get(image_url) - else: - response = None + + if image_url is None: + return web.Response(status=404) + + def fetch_image(): + """Helper method to fetch image.""" + try: + return requests.get(image_url).content + except requests.RequestException: + return None + + response = yield from self.hass.loop.run_in_executor(None, fetch_image) if response is None: - return self.Response(status=500) + return web.Response(status=500) - return self.Response(response) + return web.Response(body=response) diff --git a/homeassistant/components/persistent_notification.py b/homeassistant/components/persistent_notification.py index 8315f9c1194ae..cc1f6a2726035 100644 --- a/homeassistant/components/persistent_notification.py +++ b/homeassistant/components/persistent_notification.py @@ -38,7 +38,8 @@ def create(hass, message, title=None, notification_id=None): """Generate a notification.""" run_callback_threadsafe( - hass.loop, hass, message, title, notification_id).result() + hass.loop, async_create, hass, message, title, notification_id, + ).result() def async_create(hass, message, title=None, notification_id=None): diff --git a/tests/components/media_player/test_demo.py b/tests/components/media_player/test_demo.py index b49502054f169..2bbfaa77b8dfc 100644 --- a/tests/components/media_player/test_demo.py +++ b/tests/components/media_player/test_demo.py @@ -18,42 +18,19 @@ API_PASSWORD = "test1234" HA_HEADERS = {HTTP_HEADER_HA_AUTH: API_PASSWORD} -hass = None - entity_id = 'media_player.walkman' -def setUpModule(): # pylint: disable=invalid-name - """Initalize a Home Assistant server.""" - global hass - - hass = get_test_home_assistant() - setup_component(hass, http.DOMAIN, { - http.DOMAIN: { - http.CONF_SERVER_PORT: SERVER_PORT, - http.CONF_API_PASSWORD: API_PASSWORD, - }, - }) - - hass.start() - time.sleep(0.05) - - -def tearDownModule(): # pylint: disable=invalid-name - """Stop the Home Assistant server.""" - hass.stop() - - class TestDemoMediaPlayer(unittest.TestCase): """Test the media_player module.""" def setUp(self): # pylint: disable=invalid-name """Setup things to be run when tests are started.""" - self.hass = hass - try: - self.hass.config.components.remove(mp.DOMAIN) - except ValueError: - pass + self.hass = get_test_home_assistant() + + def tearDown(self): + """Shut down test instance.""" + self.hass.stop() def test_source_select(self): """Test the input source service.""" @@ -226,21 +203,6 @@ def test_prev_next_track(self): assert 0 == (mp.SUPPORT_PREVIOUS_TRACK & state.attributes.get('supported_media_commands')) - @requests_mock.Mocker(real_http=True) - def test_media_image_proxy(self, m): - """Test the media server image proxy server .""" - fake_picture_data = 'test.test' - m.get('https://graph.facebook.com/v2.5/107771475912710/' - 'picture?type=large', text=fake_picture_data) - assert setup_component( - self.hass, mp.DOMAIN, - {'media_player': {'platform': 'demo'}}) - assert self.hass.states.is_state(entity_id, 'playing') - state = self.hass.states.get(entity_id) - req = requests.get(HTTP_BASE_URL + - state.attributes.get('entity_picture')) - assert req.text == fake_picture_data - @patch('homeassistant.components.media_player.demo.DemoYoutubePlayer.' 'media_seek') def test_play_media(self, mock_seek): @@ -275,3 +237,42 @@ def test_play_media(self, mock_seek): mp.media_seek(self.hass, 100, ent_id) self.hass.block_till_done() assert mock_seek.called + + +class TestMediaPlayerWeb(unittest.TestCase): + """Test the media player web views sensor.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + setup_component(self.hass, http.DOMAIN, { + http.DOMAIN: { + http.CONF_SERVER_PORT: SERVER_PORT, + http.CONF_API_PASSWORD: API_PASSWORD, + }, + }) + + self.hass.start() + time.sleep(0.05) + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + @requests_mock.Mocker(real_http=True) + def test_media_image_proxy(self, m): + """Test the media server image proxy server .""" + fake_picture_data = 'test.test' + m.get('https://graph.facebook.com/v2.5/107771475912710/' + 'picture?type=large', text=fake_picture_data) + self.hass.block_till_done() + assert setup_component( + self.hass, mp.DOMAIN, + {'media_player': {'platform': 'demo'}}) + assert self.hass.states.is_state(entity_id, 'playing') + state = self.hass.states.get(entity_id) + req = requests.get(HTTP_BASE_URL + + state.attributes.get('entity_picture')) + assert req.status_code == 200 + assert req.text == fake_picture_data