Remove support for deprecated Sonos configuration (#23385)

pull/23482/head
Anders Melchiorsen 2019-04-26 08:56:43 +02:00 committed by GitHub
parent d038d2426b
commit 5dbf58d67f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 148 additions and 419 deletions

View File

@ -1,10 +1,26 @@
"""Support to embed Sonos."""
from homeassistant import config_entries
from homeassistant.helpers import config_entry_flow
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.const import CONF_HOSTS
from homeassistant.helpers import config_entry_flow, config_validation as cv
DOMAIN = 'sonos'
CONF_ADVERTISE_ADDR = 'advertise_addr'
CONF_INTERFACE_ADDR = 'interface_addr'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
MP_DOMAIN: vol.Schema({
vol.Optional(CONF_ADVERTISE_ADDR): cv.string,
vol.Optional(CONF_INTERFACE_ADDR): cv.string,
vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list_csv, [cv.string]),
}),
}),
}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass, config):
"""Set up the Sonos component."""
@ -22,7 +38,7 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, entry):
"""Set up Sonos from a config entry."""
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
entry, 'media_player'))
entry, MP_DOMAIN))
return True

View File

@ -10,20 +10,21 @@ import async_timeout
import requests
import voluptuous as vol
from homeassistant.components.media_player import (
PLATFORM_SCHEMA, MediaPlayerDevice)
from homeassistant.components.media_player import MediaPlayerDevice
from homeassistant.components.media_player.const import (
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST,
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOURCE,
SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TIME, CONF_HOSTS, STATE_IDLE, STATE_OFF, STATE_PAUSED,
ATTR_ENTITY_ID, ATTR_TIME, STATE_IDLE, STATE_OFF, STATE_PAUSED,
STATE_PLAYING)
import homeassistant.helpers.config_validation as cv
from homeassistant.util.dt import utcnow
from . import DOMAIN as SONOS_DOMAIN
from . import (
CONF_ADVERTISE_ADDR, CONF_HOSTS, CONF_INTERFACE_ADDR,
DOMAIN as SONOS_DOMAIN)
DEPENDENCIES = ('sonos',)
@ -54,9 +55,6 @@ DATA_SONOS = 'sonos_media_player'
SOURCE_LINEIN = 'Line-in'
SOURCE_TV = 'TV'
CONF_ADVERTISE_ADDR = 'advertise_addr'
CONF_INTERFACE_ADDR = 'interface_addr'
# Service call validation schemas
ATTR_SLEEP_TIME = 'sleep_time'
ATTR_ALARM_ID = 'alarm_id'
@ -72,12 +70,6 @@ ATTR_SONOS_GROUP = 'sonos_group'
UPNP_ERRORS_TO_IGNORE = ['701', '711', '712']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_ADVERTISE_ADDR): cv.string,
vol.Optional(CONF_INTERFACE_ADDR): cv.string,
vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list, [cv.string]),
})
SONOS_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
@ -119,57 +111,34 @@ class SonosData:
self.topology_condition = asyncio.Condition(loop=hass.loop)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Sonos platform.
Deprecated.
"""
_LOGGER.warning('Loading Sonos via platform config is deprecated.')
_setup_platform(hass, config, add_entities, discovery_info)
async def async_setup_platform(hass,
config,
async_add_entities,
discovery_info=None):
"""Set up the Sonos platform. Obsolete."""
_LOGGER.error(
'Loading Sonos by media_player platform config is no longer supported')
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Sonos from a config entry."""
def add_entities(entities, update_before_add=False):
"""Sync version of async add entities."""
hass.add_job(async_add_entities, entities, update_before_add)
hass.async_add_executor_job(
_setup_platform, hass, hass.data[SONOS_DOMAIN].get('media_player', {}),
add_entities, None)
def _setup_platform(hass, config, add_entities, discovery_info):
"""Set up the Sonos platform."""
import pysonos
if DATA_SONOS not in hass.data:
hass.data[DATA_SONOS] = SonosData(hass)
config = hass.data[SONOS_DOMAIN].get('media_player', {})
advertise_addr = config.get(CONF_ADVERTISE_ADDR)
if advertise_addr:
pysonos.config.EVENT_ADVERTISE_IP = advertise_addr
players = []
if discovery_info:
player = pysonos.SoCo(discovery_info.get('host'))
# If host already exists by config
if player.uid in hass.data[DATA_SONOS].uids:
return
# If invisible, such as a stereo slave
if not player.is_visible:
return
players.append(player)
else:
def _create_sonos_entities():
"""Discover players and return a list of SonosEntity objects."""
players = []
hosts = config.get(CONF_HOSTS)
if hosts:
# Support retro compatibility with comma separated list of hosts
# from config
hosts = hosts[0] if len(hosts) == 1 else hosts
hosts = hosts.split(',') if isinstance(hosts, str) else hosts
for host in hosts:
try:
players.append(pysonos.SoCo(socket.gethostbyname(host)))
@ -182,11 +151,14 @@ def _setup_platform(hass, config, add_entities, discovery_info):
if not players:
_LOGGER.warning("No Sonos speakers found")
return
hass.data[DATA_SONOS].uids.update(p.uid for p in players)
add_entities(SonosEntity(p) for p in players)
_LOGGER.debug("Added %s Sonos speakers", len(players))
return [SonosEntity(p) for p in players]
entities = await hass.async_add_executor_job(_create_sonos_entities)
hass.data[DATA_SONOS].uids.update(e.unique_id for e in entities)
async_add_entities(entities)
_LOGGER.debug("Added %s Sonos speakers", len(entities))
def _service_to_entities(service):
"""Extract and return entities from service call."""
@ -216,19 +188,19 @@ def _setup_platform(hass, config, add_entities, discovery_info):
await SonosEntity.restore_multi(
hass, entities, service.data[ATTR_WITH_GROUP])
hass.services.register(
hass.services.async_register(
DOMAIN, SERVICE_JOIN, async_service_handle,
schema=SONOS_JOIN_SCHEMA)
hass.services.register(
hass.services.async_register(
DOMAIN, SERVICE_UNJOIN, async_service_handle,
schema=SONOS_SCHEMA)
hass.services.register(
hass.services.async_register(
DOMAIN, SERVICE_SNAPSHOT, async_service_handle,
schema=SONOS_STATES_SCHEMA)
hass.services.register(
hass.services.async_register(
DOMAIN, SERVICE_RESTORE, async_service_handle,
schema=SONOS_STATES_SCHEMA)
@ -244,19 +216,19 @@ def _setup_platform(hass, config, add_entities, discovery_info):
elif service.service == SERVICE_SET_OPTION:
entity.set_option(**service.data)
hass.services.register(
hass.services.async_register(
DOMAIN, SERVICE_SET_TIMER, service_handle,
schema=SONOS_SET_TIMER_SCHEMA)
hass.services.register(
hass.services.async_register(
DOMAIN, SERVICE_CLEAR_TIMER, service_handle,
schema=SONOS_SCHEMA)
hass.services.register(
hass.services.async_register(
DOMAIN, SERVICE_UPDATE_ALARM, service_handle,
schema=SONOS_UPDATE_ALARM_SCHEMA)
hass.services.register(
hass.services.async_register(
DOMAIN, SERVICE_SET_OPTION, service_handle,
schema=SONOS_SET_OPTION_SCHEMA)

View File

@ -0,0 +1,77 @@
"""Configuration for Sonos tests."""
from asynctest.mock import Mock, patch as patch
import pytest
from homeassistant.components.sonos import DOMAIN
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.const import CONF_HOSTS
from tests.common import MockConfigEntry
@pytest.fixture(name="config_entry")
def config_entry_fixture():
"""Create a mock Sonos config entry."""
return MockConfigEntry(domain=DOMAIN, title='Sonos')
@pytest.fixture(name="soco")
def soco_fixture(music_library, speaker_info, dummy_soco_service):
"""Create a mock pysonos SoCo fixture."""
with patch('pysonos.SoCo', autospec=True) as mock, \
patch('socket.gethostbyname', return_value='192.168.42.2'):
mock_soco = mock.return_value
mock_soco.uid = 'RINCON_test'
mock_soco.music_library = music_library
mock_soco.get_speaker_info.return_value = speaker_info
mock_soco.avTransport = dummy_soco_service
mock_soco.renderingControl = dummy_soco_service
mock_soco.zoneGroupTopology = dummy_soco_service
mock_soco.contentDirectory = dummy_soco_service
yield mock_soco
@pytest.fixture(name="discover")
def discover_fixture(soco):
"""Create a mock pysonos discover fixture."""
with patch('pysonos.discover') as mock:
mock.return_value = {soco}
yield mock
@pytest.fixture(name="config")
def config_fixture():
"""Create hass config fixture."""
return {
DOMAIN: {
MP_DOMAIN: {
CONF_HOSTS: ['192.168.42.1']
}
}
}
@pytest.fixture(name="dummy_soco_service")
def dummy_soco_service_fixture():
"""Create dummy_soco_service fixture."""
service = Mock()
service.subscribe = Mock()
return service
@pytest.fixture(name="music_library")
def music_library_fixture():
"""Create music_library fixture."""
music_library = Mock()
music_library.get_sonos_favorites.return_value = []
return music_library
@pytest.fixture(name="speaker_info")
def speaker_info_fixture():
"""Create speaker_info fixture."""
return {
'zone_name': 'Zone A',
'model_name': 'Model Name',
}

View File

@ -35,7 +35,9 @@ async def test_configuring_sonos_creates_entry(hass):
patch('pysonos.discover', return_value=True):
await async_setup_component(hass, sonos.DOMAIN, {
'sonos': {
'some_config': 'to_trigger_import'
'media_player': {
'interface_addr': '127.0.0.1',
}
}
})
await hass.async_block_till_done()

View File

@ -1,360 +1,22 @@
"""The tests for the Demo Media player platform."""
import datetime
import socket
import unittest
import pysonos.snapshot
from unittest import mock
import pysonos
from pysonos import alarms
from homeassistant.setup import setup_component
from homeassistant.components.sonos import media_player as sonos
from homeassistant.components.media_player.const import DOMAIN
from homeassistant.components.sonos.media_player import CONF_INTERFACE_ADDR
from homeassistant.const import CONF_HOSTS, CONF_PLATFORM
from homeassistant.util.async_ import run_coroutine_threadsafe
from tests.common import get_test_home_assistant
ENTITY_ID = 'media_player.kitchen'
"""Tests for the Sonos Media Player platform."""
from homeassistant.components.sonos import media_player, DOMAIN
from homeassistant.setup import async_setup_component
class pysonosDiscoverMock():
"""Mock class for the pysonos.discover method."""
def discover(interface_addr, all_households=False):
"""Return tuple of pysonos.SoCo objects representing found speakers."""
return {SoCoMock('192.0.2.1')}
async def setup_platform(hass, config_entry, config):
"""Set up the media player platform for testing."""
config_entry.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
class AvTransportMock():
"""Mock class for the avTransport property on pysonos.SoCo object."""
def __init__(self):
"""Initialize ethe Transport mock."""
pass
def GetMediaInfo(self, _):
"""Get the media details."""
return {
'CurrentURI': '',
'CurrentURIMetaData': ''
}
async def test_async_setup_entry_hosts(hass, config_entry, config, soco):
"""Test static setup."""
await setup_platform(hass, config_entry, config)
assert hass.data[media_player.DATA_SONOS].entities[0].soco == soco
class MusicLibraryMock():
"""Mock class for the music_library property on pysonos.SoCo object."""
def get_sonos_favorites(self):
"""Return favorites."""
return []
class CacheMock():
"""Mock class for the _zgs_cache property on pysonos.SoCo object."""
def clear(self):
"""Clear cache."""
pass
class SoCoMock():
"""Mock class for the pysonos.SoCo object."""
def __init__(self, ip):
"""Initialize SoCo object."""
self.ip_address = ip
self.is_visible = True
self.volume = 50
self.mute = False
self.shuffle = False
self.night_mode = False
self.dialog_mode = False
self.music_library = MusicLibraryMock()
self.avTransport = AvTransportMock()
self._zgs_cache = CacheMock()
def get_sonos_favorites(self):
"""Get favorites list from sonos."""
return {'favorites': []}
def get_speaker_info(self, force):
"""Return a dict with various data points about the speaker."""
return {'serial_number': 'B8-E9-37-BO-OC-BA:2',
'software_version': '32.11-30071',
'uid': 'RINCON_B8E937BOOCBA02500',
'zone_icon': 'x-rincon-roomicon:kitchen',
'mac_address': 'B8:E9:37:BO:OC:BA',
'zone_name': 'Kitchen',
'model_name': 'Sonos PLAY:1',
'hardware_version': '1.8.1.2-1'}
def get_current_transport_info(self):
"""Return a dict with the current state of the speaker."""
return {'current_transport_speed': '1',
'current_transport_state': 'STOPPED',
'current_transport_status': 'OK'}
def get_current_track_info(self):
"""Return a dict with the current track information."""
return {'album': '',
'uri': '',
'title': '',
'artist': '',
'duration': '0:00:00',
'album_art': '',
'position': '0:00:00',
'playlist_position': '0',
'metadata': ''}
def is_coordinator(self):
"""Return true if coordinator."""
return True
def join(self, master):
"""Join speaker to a group."""
return
def set_sleep_timer(self, sleep_time_seconds):
"""Set the sleep timer."""
return
def unjoin(self):
"""Cause the speaker to separate itself from other speakers."""
return
def uid(self):
"""Return a player uid."""
return "RINCON_XXXXXXXXXXXXXXXXX"
def group(self):
"""Return all group data of this player."""
return
def add_entities_factory(hass):
"""Add entities factory."""
def add_entities(entities, update_befor_add=False):
"""Fake add entity."""
hass.data[sonos.DATA_SONOS].entities = list(entities)
return add_entities
class TestSonosMediaPlayer(unittest.TestCase):
"""Test the media_player module."""
# pylint: disable=invalid-name
def setUp(self):
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
def monkey_available(self):
"""Make a monkey available."""
return True
# Monkey patches
self.real_available = sonos.SonosEntity.available
sonos.SonosEntity.available = monkey_available
# pylint: disable=invalid-name
def tearDown(self):
"""Stop everything that was started."""
# Monkey patches
sonos.SonosEntity.available = self.real_available
self.hass.stop()
@mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
def test_ensure_setup_discovery(self, *args):
"""Test a single device using the autodiscovery provided by HASS."""
sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), {
'host': '192.0.2.1'
})
entities = self.hass.data[sonos.DATA_SONOS].entities
assert len(entities) == 1
assert entities[0].name == 'Kitchen'
@mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
@mock.patch('pysonos.discover')
def test_ensure_setup_config_interface_addr(self, discover_mock, *args):
"""Test an interface address config'd by the HASS config file."""
discover_mock.return_value = {SoCoMock('192.0.2.1')}
config = {
DOMAIN: {
CONF_PLATFORM: 'sonos',
CONF_INTERFACE_ADDR: '192.0.1.1',
}
}
assert setup_component(self.hass, DOMAIN, config)
assert len(self.hass.data[sonos.DATA_SONOS].entities) == 1
assert discover_mock.call_count == 1
@mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
def test_ensure_setup_config_hosts_string_single(self, *args):
"""Test a single address config'd by the HASS config file."""
config = {
DOMAIN: {
CONF_PLATFORM: 'sonos',
CONF_HOSTS: ['192.0.2.1'],
}
}
assert setup_component(self.hass, DOMAIN, config)
entities = self.hass.data[sonos.DATA_SONOS].entities
assert len(entities) == 1
assert entities[0].name == 'Kitchen'
@mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
def test_ensure_setup_config_hosts_string_multiple(self, *args):
"""Test multiple address string config'd by the HASS config file."""
config = {
DOMAIN: {
CONF_PLATFORM: 'sonos',
CONF_HOSTS: ['192.0.2.1,192.168.2.2'],
}
}
assert setup_component(self.hass, DOMAIN, config)
entities = self.hass.data[sonos.DATA_SONOS].entities
assert len(entities) == 2
assert entities[0].name == 'Kitchen'
@mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
def test_ensure_setup_config_hosts_list(self, *args):
"""Test a multiple address list config'd by the HASS config file."""
config = {
DOMAIN: {
CONF_PLATFORM: 'sonos',
CONF_HOSTS: ['192.0.2.1', '192.168.2.2'],
}
}
assert setup_component(self.hass, DOMAIN, config)
entities = self.hass.data[sonos.DATA_SONOS].entities
assert len(entities) == 2
assert entities[0].name == 'Kitchen'
@mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch.object(pysonos, 'discover', new=pysonosDiscoverMock.discover)
@mock.patch('socket.create_connection', side_effect=socket.error())
def test_ensure_setup_sonos_discovery(self, *args):
"""Test a single device using the autodiscovery provided by Sonos."""
sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass))
entities = self.hass.data[sonos.DATA_SONOS].entities
assert len(entities) == 1
assert entities[0].name == 'Kitchen'
@mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
@mock.patch.object(SoCoMock, 'set_sleep_timer')
def test_sonos_set_sleep_timer(self, set_sleep_timerMock, *args):
"""Ensure pysonos methods called for sonos_set_sleep_timer service."""
sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), {
'host': '192.0.2.1'
})
entity = self.hass.data[sonos.DATA_SONOS].entities[-1]
entity.hass = self.hass
entity.set_sleep_timer(30)
set_sleep_timerMock.assert_called_once_with(30)
@mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
@mock.patch.object(SoCoMock, 'set_sleep_timer')
def test_sonos_clear_sleep_timer(self, set_sleep_timerMock, *args):
"""Ensure pysonos method called for sonos_clear_sleep_timer service."""
sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), {
'host': '192.0.2.1'
})
entity = self.hass.data[sonos.DATA_SONOS].entities[-1]
entity.hass = self.hass
entity.set_sleep_timer(None)
set_sleep_timerMock.assert_called_once_with(None)
@mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('pysonos.alarms.Alarm')
@mock.patch('socket.create_connection', side_effect=socket.error())
def test_set_alarm(self, pysonos_mock, alarm_mock, *args):
"""Ensure pysonos methods called for sonos_set_sleep_timer service."""
sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), {
'host': '192.0.2.1'
})
entity = self.hass.data[sonos.DATA_SONOS].entities[-1]
entity.hass = self.hass
alarm1 = alarms.Alarm(pysonos_mock)
alarm1.configure_mock(_alarm_id="1", start_time=None, enabled=False,
include_linked_zones=False, volume=100)
with mock.patch('pysonos.alarms.get_alarms', return_value=[alarm1]):
attrs = {
'time': datetime.time(12, 00),
'enabled': True,
'include_linked_zones': True,
'volume': 0.30,
}
entity.set_alarm(alarm_id=2)
alarm1.save.assert_not_called()
entity.set_alarm(alarm_id=1, **attrs)
assert alarm1.enabled == attrs['enabled']
assert alarm1.start_time == attrs['time']
assert alarm1.include_linked_zones == \
attrs['include_linked_zones']
assert alarm1.volume == 30
alarm1.save.assert_called_once_with()
@mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
@mock.patch.object(pysonos.snapshot.Snapshot, 'snapshot')
def test_sonos_snapshot(self, snapshotMock, *args):
"""Ensure pysonos methods called for sonos_snapshot service."""
sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), {
'host': '192.0.2.1'
})
entities = self.hass.data[sonos.DATA_SONOS].entities
entity = entities[-1]
entity.hass = self.hass
snapshotMock.return_value = True
entity.soco.group = mock.MagicMock()
entity.soco.group.members = [e.soco for e in entities]
run_coroutine_threadsafe(
sonos.SonosEntity.snapshot_multi(self.hass, entities, True),
self.hass.loop).result()
assert snapshotMock.call_count == 1
assert snapshotMock.call_args == mock.call()
@mock.patch('pysonos.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
@mock.patch.object(pysonos.snapshot.Snapshot, 'restore')
def test_sonos_restore(self, restoreMock, *args):
"""Ensure pysonos methods called for sonos_restore service."""
from pysonos.snapshot import Snapshot
sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), {
'host': '192.0.2.1'
})
entities = self.hass.data[sonos.DATA_SONOS].entities
entity = entities[-1]
entity.hass = self.hass
restoreMock.return_value = True
entity._snapshot_group = mock.MagicMock()
entity._snapshot_group.members = [e.soco for e in entities]
entity._soco_snapshot = Snapshot(entity.soco)
run_coroutine_threadsafe(
sonos.SonosEntity.restore_multi(self.hass, entities, True),
self.hass.loop).result()
assert restoreMock.call_count == 1
assert restoreMock.call_args == mock.call()
async def test_async_setup_entry_discover(hass, config_entry, discover):
"""Test discovery setup."""
await setup_platform(hass, config_entry, {})
assert hass.data[media_player.DATA_SONOS].uids == {'RINCON_test'}