Sonos responsiveness improvements + enhancements (#4063)

* Sonos responsiveness improvements (async_ coroutines, event based updating, album art caching) + Better radio station information

* Docstring fixes.

* Docstring fixes.

* Updated SoCo dependency + fixed file permissions.

* Only fetch speaker info if needed.

* PEP8 fixes

* Fixed SoCoMock.get_speaker_info to get test to pass.

* Regenerated requirements_all.txt + async fetching of album art with caching + added http_session to HomeAssistant object.

* Unit test fixed.

* Add blank line as per flake8

* Fixed media image proxy unit test.

* Removed async stuff.

* Removed last remnants of async stuff.
This commit is contained in:
Bjarni Ivarsson 2016-11-01 18:42:38 +01:00 committed by Robbie Trencheny
parent dad54bb993
commit c549ea115d
5 changed files with 390 additions and 99 deletions

View file

@ -8,9 +8,9 @@ import asyncio
import hashlib import hashlib
import logging import logging
import os import os
import requests
from aiohttp import web from aiohttp import web
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
@ -19,6 +19,7 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
from homeassistant.const import ( from homeassistant.const import (
STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE, STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE,
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON,
@ -36,6 +37,16 @@ SCAN_INTERVAL = 10
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
ENTITY_IMAGE_URL = '/api/media_player_proxy/{0}?token={1}&cache={2}' ENTITY_IMAGE_URL = '/api/media_player_proxy/{0}?token={1}&cache={2}'
ATTR_CACHE_IMAGES = 'images'
ATTR_CACHE_URLS = 'urls'
ATTR_CACHE_MAXSIZE = 'maxsize'
ENTITY_IMAGE_CACHE = {
ATTR_CACHE_IMAGES: {},
ATTR_CACHE_URLS: [],
ATTR_CACHE_MAXSIZE: 16
}
CONTENT_TYPE_HEADER = 'Content-Type'
SERVICE_PLAY_MEDIA = 'play_media' SERVICE_PLAY_MEDIA = 'play_media'
SERVICE_SELECT_SOURCE = 'select_source' SERVICE_SELECT_SOURCE = 'select_source'
@ -672,6 +683,51 @@ class MediaPlayerDevice(Entity):
return state_attr return state_attr
def preload_media_image_url(self, url):
"""Preload and cache a media image for future use."""
run_coroutine_threadsafe(
_async_fetch_image(self.hass, url), self.hass.loop
).result()
@asyncio.coroutine
def _async_fetch_image(hass, url):
"""Helper method to fetch image.
Images are cached in memory (the images are typically 10-100kB in size).
"""
cache_images = ENTITY_IMAGE_CACHE[ATTR_CACHE_IMAGES]
cache_urls = ENTITY_IMAGE_CACHE[ATTR_CACHE_URLS]
cache_maxsize = ENTITY_IMAGE_CACHE[ATTR_CACHE_MAXSIZE]
if url in cache_images:
return cache_images[url]
content, content_type = (None, None)
try:
with async_timeout.timeout(10, loop=hass.loop):
response = yield from hass.websession.get(url)
if response.status == 200:
content = yield from response.read()
content_type = response.headers.get(CONTENT_TYPE_HEADER)
hass.loop.create_task(response.release())
except asyncio.TimeoutError:
pass
if content:
cache_images[url] = (content, content_type)
cache_urls.append(url)
while len(cache_urls) > cache_maxsize:
# remove oldest item from cache
oldest_url = cache_urls[0]
if oldest_url in cache_images:
del cache_images[oldest_url]
cache_urls = cache_urls[1:]
return content, content_type
class MediaPlayerImageView(HomeAssistantView): class MediaPlayerImageView(HomeAssistantView):
"""Media player view to serve an image.""" """Media player view to serve an image."""
@ -698,21 +754,10 @@ class MediaPlayerImageView(HomeAssistantView):
if not authenticated: if not authenticated:
return web.Response(status=401) return web.Response(status=401)
image_url = player.media_image_url data, content_type = yield from _async_fetch_image(
self.hass, player.media_image_url)
if image_url is None: if data 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 web.Response(status=500) return web.Response(status=500)
return web.Response(body=response) return web.Response(body=data, content_type=content_type)

371
homeassistant/components/media_player/sonos.py Executable file → Normal file
View file

@ -17,12 +17,14 @@ from homeassistant.components.media_player import (
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice) SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
from homeassistant.const import ( from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF, STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID)
ATTR_ENTITY_ID)
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['SoCo==0.12'] REQUIREMENTS = ['https://github.com/SoCo/SoCo/archive/'
'cf8c2701165562eccbf1ecc879bf7060ceb0993e.zip#'
'SoCo==0.12']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -223,6 +225,29 @@ def only_if_coordinator(func):
return wrapper return wrapper
def _parse_timespan(timespan):
"""Parse a time-span into number of seconds."""
if timespan in ('', 'NOT_IMPLEMENTED', None):
return None
else:
return sum(60 ** x[0] * int(x[1]) for x in enumerate(
reversed(timespan.split(':'))))
# pylint: disable=too-few-public-methods
class _ProcessSonosEventQueue():
"""Queue like object for dispatching sonos events."""
def __init__(self, sonos_device):
self._sonos_device = sonos_device
def put(self, item, block=True, timeout=None):
"""Queue up event for processing."""
# Instead of putting events on a queue, dispatch them to the event
# processing method.
self._sonos_device.process_sonos_event(item)
# pylint: disable=abstract-method # pylint: disable=abstract-method
class SonosDevice(MediaPlayerDevice): class SonosDevice(MediaPlayerDevice):
"""Representation of a Sonos device.""" """Representation of a Sonos device."""
@ -234,8 +259,11 @@ class SonosDevice(MediaPlayerDevice):
self.hass = hass self.hass = hass
self.volume_increment = 5 self.volume_increment = 5
self._player = player self._player = player
self._player_volume = None
self._player_volume_muted = None
self._speaker_info = None self._speaker_info = None
self._name = None self._name = None
self._status = None
self._coordinator = None self._coordinator = None
self._media_content_id = None self._media_content_id = None
self._media_duration = None self._media_duration = None
@ -243,6 +271,15 @@ class SonosDevice(MediaPlayerDevice):
self._media_artist = None self._media_artist = None
self._media_album_name = None self._media_album_name = None
self._media_title = None self._media_title = None
self._media_radio_show = None
self._media_next_title = None
self._support_previous_track = False
self._support_next_track = False
self._support_pause = False
self._current_track_uri = None
self._current_track_is_radio_stream = False
self._queue = None
self._last_avtransport_event = None
self.update() self.update()
self.soco_snapshot = Snapshot(self._player) self.soco_snapshot = Snapshot(self._player)
@ -268,36 +305,95 @@ class SonosDevice(MediaPlayerDevice):
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
if self._status == 'PAUSED_PLAYBACK': if self._coordinator:
return self._coordinator.state
if self._status in ('PAUSED_PLAYBACK', 'STOPPED'):
return STATE_PAUSED return STATE_PAUSED
if self._status == 'PLAYING': if self._status in ('PLAYING', 'TRANSITIONING'):
return STATE_PLAYING return STATE_PLAYING
if self._status == 'STOPPED':
return STATE_IDLE
if self._status == 'OFF': if self._status == 'OFF':
return STATE_OFF return STATE_OFF
return STATE_UNKNOWN return STATE_IDLE
@property @property
def is_coordinator(self): def is_coordinator(self):
"""Return true if player is a coordinator.""" """Return true if player is a coordinator."""
return self._player.is_coordinator return self._coordinator is None
def _is_available(self):
try:
sock = socket.create_connection(
address=(self._player.ip_address, 1443),
timeout=3)
sock.close()
return True
except socket.error:
return False
# pylint: disable=invalid-name
def _subscribe_to_player_events(self):
if self._queue is None:
self._queue = _ProcessSonosEventQueue(self)
self._player.avTransport.subscribe(
auto_renew=True,
event_queue=self._queue)
self._player.renderingControl.subscribe(
auto_renew=True,
event_queue=self._queue)
# pylint: disable=too-many-branches, too-many-statements
def update(self): def update(self):
"""Retrieve latest state.""" """Retrieve latest state."""
self._speaker_info = self._player.get_speaker_info() if self._speaker_info is None:
self._name = self._speaker_info['zone_name'].replace( self._speaker_info = self._player.get_speaker_info(True)
' (R)', '').replace(' (L)', '') self._name = self._speaker_info['zone_name'].replace(
' (R)', '').replace(' (L)', '')
if self.available: if self._last_avtransport_event:
self._status = self._player.get_current_transport_info().get( is_available = True
'current_transport_state') else:
trackinfo = self._player.get_current_track_info() is_available = self._is_available()
if trackinfo['uri'].startswith('x-rincon:'): if is_available:
if self._queue is None or self._player_volume is None:
self._player_volume = self._player.volume
if self._queue is None or self._player_volume_muted is None:
self._player_volume_muted = self._player.mute
track_info = None
if self._last_avtransport_event:
variables = self._last_avtransport_event.variables
current_track_metadata = variables.get(
'current_track_meta_data', {}
)
self._status = variables.get('transport_state')
if current_track_metadata:
# no need to ask speaker for information we already have
current_track_metadata = current_track_metadata.__dict__
track_info = {
'uri': variables.get('current_track_uri'),
'artist': current_track_metadata.get('creator'),
'album': current_track_metadata.get('album'),
'title': current_track_metadata.get('title'),
'playlist_position': variables.get('current_track'),
'duration': variables.get('current_track_duration')
}
else:
transport_info = self._player.get_current_transport_info()
self._status = transport_info.get('current_transport_state')
if not track_info:
track_info = self._player.get_current_track_info()
if track_info['uri'].startswith('x-rincon:'):
# this speaker is a slave, find the coordinator # this speaker is a slave, find the coordinator
# the uri of the track is 'x-rincon:{coordinator-id}' # the uri of the track is 'x-rincon:{coordinator-id}'
coordinator_id = trackinfo['uri'][9:] coordinator_id = track_info['uri'][9:]
coordinators = [device for device in DEVICES coordinators = [device for device in DEVICES
if device.unique_id == coordinator_id] if device.unique_id == coordinator_id]
self._coordinator = coordinators[0] if coordinators else None self._coordinator = coordinators[0] if coordinators else None
@ -305,39 +401,44 @@ class SonosDevice(MediaPlayerDevice):
self._coordinator = None self._coordinator = None
if not self._coordinator: if not self._coordinator:
mediainfo = self._player.avTransport.GetMediaInfo([ media_info = self._player.avTransport.GetMediaInfo(
('InstanceID', 0) [('InstanceID', 0)]
]) )
duration = trackinfo.get('duration', '0:00') current_media_uri = media_info['CurrentURI']
# if the speaker is playing from the "line-in" source, getting media_artist = track_info.get('artist')
# track metadata can return NOT_IMPLEMENTED, which breaks the media_album_name = track_info.get('album')
# volume logic below media_title = track_info.get('title')
if duration == 'NOT_IMPLEMENTED':
duration = None
else:
duration = sum(60 ** x[0] * int(x[1]) for x in enumerate(
reversed(duration.split(':'))))
media_image_url = trackinfo.get('album_art', None) is_radio_stream = \
media_artist = trackinfo.get('artist', None) current_media_uri.startswith('x-sonosapi-stream:') or \
media_album_name = trackinfo.get('album', None) current_media_uri.startswith('x-rincon-mp3radio:')
media_title = trackinfo.get('title', None)
if media_image_url in ('', 'NOT_IMPLEMENTED', None): if is_radio_stream:
# fallback to asking the speaker directly is_radio_stream = True
media_image_url = \ media_image_url = self._format_media_image_url(
'http://{host}:{port}/getaa?s=1&u={uri}'.format( current_media_uri
host=self._player.ip_address, )
port=1400, support_previous_track = False
uri=urllib.parse.quote(mediainfo['CurrentURI']) support_next_track = False
support_pause = False
# for radio streams we set the radio station name as the
# title.
if media_artist and media_title:
# artist and album name are in the data, concatenate
# that do display as artist.
# "Information" field in the sonos pc app
media_artist = '{artist} - {title}'.format(
artist=media_artist,
title=media_title
) )
else:
# "On Now" field in the sonos pc app
media_artist = self._media_radio_show
if media_artist in ('', 'NOT_IMPLEMENTED', None): current_uri_metadata = media_info["CurrentURIMetaData"]
# if listening to a radio stream the media_artist field
# will be empty and the title field will contain the
# filename that is being streamed
current_uri_metadata = mediainfo["CurrentURIMetaData"]
if current_uri_metadata not in \ if current_uri_metadata not in \
('', 'NOT_IMPLEMENTED', None): ('', 'NOT_IMPLEMENTED', None):
@ -350,16 +451,80 @@ class SonosDevice(MediaPlayerDevice):
'.//{http://purl.org/dc/elements/1.1/}title') './/{http://purl.org/dc/elements/1.1/}title')
if md_title not in ('', 'NOT_IMPLEMENTED', None): if md_title not in ('', 'NOT_IMPLEMENTED', None):
media_artist = ''
media_title = md_title media_title = md_title
self._media_content_id = trackinfo.get('title', None) if media_artist and media_title:
self._media_duration = duration # some radio stations put their name into the artist
# name, e.g.:
# media_title = "Station"
# media_artist = "Station - Artist - Title"
# detect this case and trim from the front of
# media_artist for cosmetics
str_to_trim = '{title} - '.format(
title=media_title
)
chars = min(len(media_artist), len(str_to_trim))
if media_artist[:chars].upper() == \
str_to_trim[:chars].upper():
media_artist = media_artist[chars:]
else:
# not a radio stream
media_image_url = self._format_media_image_url(
track_info['uri']
)
support_previous_track = True
support_next_track = True
support_pause = True
playlist_position = track_info.get('playlist_position')
if playlist_position in ('', 'NOT_IMPLEMENTED', None):
playlist_position = None
else:
playlist_position = int(playlist_position)
playlist_size = media_info.get('NrTracks')
if playlist_size in ('', 'NOT_IMPLEMENTED', None):
playlist_size = None
else:
playlist_size = int(playlist_size)
if playlist_position is not None and \
playlist_size is not None:
if playlist_position == 1:
support_previous_track = False
if playlist_position == playlist_size:
support_next_track = False
self._media_content_id = track_info.get('title')
self._media_duration = _parse_timespan(
track_info.get('duration')
)
self._media_image_url = media_image_url self._media_image_url = media_image_url
self._media_artist = media_artist self._media_artist = media_artist
self._media_album_name = media_album_name self._media_album_name = media_album_name
self._media_title = media_title self._media_title = media_title
self._current_track_uri = track_info['uri']
self._current_track_is_radio_stream = is_radio_stream
self._support_previous_track = support_previous_track
self._support_next_track = support_next_track
self._support_pause = support_pause
# update state of the whole group
# pylint: disable=protected-access
for device in [x for x in DEVICES if x._coordinator == self]:
if device.entity_id:
device.update_ha_state(False)
if self._queue is None and self.entity_id:
self._subscribe_to_player_events()
else: else:
self._player_volume = None
self._player_volume_muted = None
self._status = 'OFF' self._status = 'OFF'
self._coordinator = None self._coordinator = None
self._media_content_id = None self._media_content_id = None
@ -368,16 +533,79 @@ class SonosDevice(MediaPlayerDevice):
self._media_artist = None self._media_artist = None
self._media_album_name = None self._media_album_name = None
self._media_title = None self._media_title = None
self._media_radio_show = None
self._media_next_title = None
self._current_track_uri = None
self._current_track_is_radio_stream = False
self._support_previous_track = False
self._support_next_track = False
self._support_pause = False
self._last_avtransport_event = None
def _format_media_image_url(self, uri):
return 'http://{host}:{port}/getaa?s=1&u={uri}'.format(
host=self._player.ip_address,
port=1400,
uri=urllib.parse.quote(uri)
)
def process_sonos_event(self, event):
"""Process a service event coming from the speaker."""
next_track_image_url = None
if event.service == self._player.avTransport:
self._last_avtransport_event = event
self._media_radio_show = None
if self._current_track_is_radio_stream:
current_track_metadata = event.variables.get(
'current_track_meta_data'
)
if current_track_metadata:
self._media_radio_show = \
current_track_metadata.radio_show.split(',')[0]
next_track_uri = event.variables.get('next_track_uri')
if next_track_uri:
next_track_image_url = self._format_media_image_url(
next_track_uri
)
next_track_metadata = event.variables.get('next_track_meta_data')
if next_track_metadata:
next_track = '{title} - {creator}'.format(
title=next_track_metadata.title,
creator=next_track_metadata.creator
)
if next_track != self._media_next_title:
self._media_next_title = next_track
else:
self._media_next_title = None
elif event.service == self._player.renderingControl:
if 'volume' in event.variables:
self._player_volume = int(
event.variables['volume'].get('Master')
)
if 'mute' in event.variables:
self._player_volume_muted = \
event.variables['mute'].get('Master') == '1'
self.update_ha_state(True)
if next_track_image_url:
self.preload_media_image_url(next_track_image_url)
@property @property
def volume_level(self): def volume_level(self):
"""Volume level of the media player (0..1).""" """Volume level of the media player (0..1)."""
return self._player.volume / 100.0 return self._player_volume / 100.0
@property @property
def is_volume_muted(self): def is_volume_muted(self):
"""Return true if volume is muted.""" """Return true if volume is muted."""
return self._player.mute return self._player_volume_muted
@property @property
def media_content_id(self): def media_content_id(self):
@ -427,11 +655,6 @@ class SonosDevice(MediaPlayerDevice):
@property @property
def media_title(self): def media_title(self):
"""Title of current playing media.""" """Title of current playing media."""
if self._player.is_playing_line_in:
return SUPPORT_SOURCE_LINEIN
if self._player.is_playing_tv:
return SUPPORT_SOURCE_TV
if self._coordinator: if self._coordinator:
return self._coordinator.media_title return self._coordinator.media_title
else: else:
@ -440,11 +663,25 @@ class SonosDevice(MediaPlayerDevice):
@property @property
def supported_media_commands(self): def supported_media_commands(self):
"""Flag of media commands that are supported.""" """Flag of media commands that are supported."""
if self._coordinator:
return self._coordinator.supported_media_commands
supported = SUPPORT_SONOS
if not self.source_list: if not self.source_list:
# some devices do not allow source selection # some devices do not allow source selection
return SUPPORT_SONOS ^ SUPPORT_SELECT_SOURCE supported = supported ^ SUPPORT_SELECT_SOURCE
return SUPPORT_SONOS if not self._support_previous_track:
supported = supported ^ SUPPORT_PREVIOUS_TRACK
if not self._support_next_track:
supported = supported ^ SUPPORT_NEXT_TRACK
if not self._support_pause:
supported = supported ^ SUPPORT_PAUSE
return supported
def volume_up(self): def volume_up(self):
"""Volume up media player.""" """Volume up media player."""
@ -489,10 +726,9 @@ class SonosDevice(MediaPlayerDevice):
return None return None
@only_if_coordinator
def turn_off(self): def turn_off(self):
"""Turn off media player.""" """Turn off media player."""
self._player.pause() self.media_pause()
def media_play(self): def media_play(self):
"""Send play command.""" """Send play command."""
@ -536,10 +772,9 @@ class SonosDevice(MediaPlayerDevice):
else: else:
self._player.clear_queue() self._player.clear_queue()
@only_if_coordinator
def turn_on(self): def turn_on(self):
"""Turn the media player on.""" """Turn the media player on."""
self._player.play() self.media_play()
def play_media(self, media_type, media_id, **kwargs): def play_media(self, media_type, media_id, **kwargs):
""" """
@ -592,15 +827,3 @@ class SonosDevice(MediaPlayerDevice):
def clear_sleep_timer(self): def clear_sleep_timer(self):
"""Clear the timer on the player.""" """Clear the timer on the player."""
self._player.set_sleep_timer(None) self._player.set_sleep_timer(None)
@property
def available(self):
"""Return True if player is reachable, False otherwise."""
try:
sock = socket.create_connection(
address=(self._player.ip_address, 1443),
timeout=3)
sock.close()
return True
except socket.error:
return False

View file

@ -24,9 +24,6 @@ PyMata==2.13
# homeassistant.components.rpi_gpio # homeassistant.components.rpi_gpio
# RPi.GPIO==0.6.1 # RPi.GPIO==0.6.1
# homeassistant.components.media_player.sonos
SoCo==0.12
# homeassistant.components.notify.twitter # homeassistant.components.notify.twitter
TwitterAPI==2.4.2 TwitterAPI==2.4.2
@ -154,6 +151,9 @@ https://github.com/GadgetReactor/pyHS100/archive/1f771b7d8090a91c6a58931532e4273
# homeassistant.components.switch.dlink # homeassistant.components.switch.dlink
https://github.com/LinuxChristian/pyW215/archive/v0.3.5.zip#pyW215==0.3.5 https://github.com/LinuxChristian/pyW215/archive/v0.3.5.zip#pyW215==0.3.5
# homeassistant.components.media_player.sonos
https://github.com/SoCo/SoCo/archive/cf8c2701165562eccbf1ecc879bf7060ceb0993e.zip#SoCo==0.12
# homeassistant.components.media_player.webostv # homeassistant.components.media_player.webostv
# homeassistant.components.notify.webostv # homeassistant.components.notify.webostv
https://github.com/TheRealLink/pylgtv/archive/v0.1.2.zip#pylgtv==0.1.2 https://github.com/TheRealLink/pylgtv/archive/v0.1.2.zip#pylgtv==0.1.2

View file

@ -1,6 +1,7 @@
"""The tests for the Demo Media player platform.""" """The tests for the Demo Media player platform."""
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
import asyncio
from homeassistant.bootstrap import setup_component from homeassistant.bootstrap import setup_component
from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.const import HTTP_HEADER_HA_AUTH
@ -8,7 +9,6 @@ import homeassistant.components.media_player as mp
import homeassistant.components.http as http import homeassistant.components.http as http
import requests import requests
import requests_mock
import time import time
from tests.common import get_test_home_assistant, get_test_instance_port from tests.common import get_test_home_assistant, get_test_instance_port
@ -260,12 +260,35 @@ class TestMediaPlayerWeb(unittest.TestCase):
"""Stop everything that was started.""" """Stop everything that was started."""
self.hass.stop() self.hass.stop()
@requests_mock.Mocker(real_http=True) def test_media_image_proxy(self):
def test_media_image_proxy(self, m):
"""Test the media server image proxy server .""" """Test the media server image proxy server ."""
fake_picture_data = 'test.test' fake_picture_data = 'test.test'
m.get('https://graph.facebook.com/v2.5/107771475912710/'
'picture?type=large', text=fake_picture_data) class MockResponse():
def __init__(self):
self.status = 200
self.headers = {'Content-Type': 'sometype'}
@asyncio.coroutine
def read(self):
return fake_picture_data.encode('ascii')
@asyncio.coroutine
def release(self):
pass
class MockWebsession():
@asyncio.coroutine
def get(self, url):
return MockResponse()
@asyncio.coroutine
def close(self):
pass
self.hass._websession = MockWebsession()
self.hass.block_till_done() self.hass.block_till_done()
assert setup_component( assert setup_component(
self.hass, mp.DOMAIN, self.hass, mp.DOMAIN,

2
tests/components/media_player/test_sonos.py Executable file → Normal file
View file

@ -42,7 +42,7 @@ class SoCoMock():
"""Clear the sleep timer.""" """Clear the sleep timer."""
return return
def get_speaker_info(self): def get_speaker_info(self, force):
"""Return a dict with various data points about the speaker.""" """Return a dict with various data points about the speaker."""
return {'serial_number': 'B8-E9-37-BO-OC-BA:2', return {'serial_number': 'B8-E9-37-BO-OC-BA:2',
'software_version': '32.11-30071', 'software_version': '32.11-30071',