Exemplo n.º 1
0
def common_tests(class_, xml_, dict_, parent_id):
    """Common tests for the MS classes."""
    xml_content = XML.fromstring(xml_.encode('utf8'))

    # MusicServiceItem.from_xml and MusicServiceItem.to_dict
    item_from_xml = class_.from_xml(xml_content, FAKE_MUSIC_SERVICE, parent_id)
    assert item_from_xml.to_dict == dict_

    # MusicServiceItem.from_dict and MusicServiceItem.to_dict
    item_from_dict = class_.from_dict(dict_)
    assert item_from_dict.to_dict == dict_

    # MusicServiceItem.didl_metadata
    # NOTE! These tests is reliant on the attributes being put in the same
    # order by ElementTree and so it might fail if that changes
    if item_from_xml.can_play:
        dict_encoded = {}
        for key, value in dict_.items():
            try:
                is_str = isinstance(value, unicode)
            except NameError:
                is_str = isinstance(value, str)
            if is_str:
                dict_encoded[key] = escape(value).\
                    encode('ascii', 'xmlcharrefreplace').decode('ascii')

            else:
                dict_encoded[key] = value
        didl = DIDL_TEMPLATE.format(item_class=class_.item_class,
                                    **dict_encoded)
        assert XML.tostring(item_from_xml.didl_metadata).decode('ascii') == \
            didl
        assert XML.tostring(item_from_dict.didl_metadata).decode('ascii') == \
            didl
    else:
        with pytest.raises(DIDLMetadataError):
            # pylint: disable=pointless-statement
            item_from_xml.didl_metadata

    # Text attributes with mandatory content
    for name in ['item_id', 'extended_id', 'title', 'service_id']:
        getter_attributes_test(name, item_from_xml, item_from_dict,
                               dict_[name])
    # Text attributes with voluntary content
    for name in ['parent_id', 'album_art_uri']:
        getter_attributes_test(name, item_from_xml, item_from_dict,
                               dict_.get(name))
    # Boolean attribute
    getter_attributes_test('can_play', item_from_xml, item_from_dict,
                           bool(dict_.get('can_play')))
    return item_from_xml, item_from_dict
Exemplo n.º 2
0
def common_tests(class_, xml_, dict_, parent_id):
    """Common tests for the MS classes."""
    xml_content = XML.fromstring(xml_.encode('utf8'))

    # MusicServiceItem.from_xml and MusicServiceItem.to_dict
    item_from_xml = class_.from_xml(xml_content, FAKE_MUSIC_SERVICE, parent_id)
    assert item_from_xml.to_dict == dict_

    # MusicServiceItem.from_dict and MusicServiceItem.to_dict
    item_from_dict = class_.from_dict(dict_)
    assert item_from_dict.to_dict == dict_

    # MusicServiceItem.didl_metadata
    # NOTE! These tests is reliant on the attributes being put in the same
    # order by ElementTree and so it might fail if that changes
    if item_from_xml.can_play:
        dict_encoded = {}
        for key, value in dict_.items():
            try:
                is_str = isinstance(value, unicode)
            except NameError:
                is_str = isinstance(value, str)
            if is_str:
                dict_encoded[key] = escape(value).\
                    encode('ascii', 'xmlcharrefreplace').decode('ascii')

            else:
                dict_encoded[key] = value
        didl = DIDL_TEMPLATE.format(item_class=class_.item_class,
                                    **dict_encoded)
        assert XML.tostring(item_from_xml.didl_metadata).decode('ascii') == \
            didl
        assert XML.tostring(item_from_dict.didl_metadata).decode('ascii') == \
            didl
    else:
        with pytest.raises(DIDLMetadataError):
            # pylint: disable=pointless-statement
            item_from_xml.didl_metadata

    # Text attributes with mandatory content
    for name in ['item_id', 'extended_id', 'title', 'service_id']:
        getter_attributes_test(name, item_from_xml, item_from_dict,
                               dict_[name])
    # Text attributes with voluntary content
    for name in ['parent_id', 'album_art_uri']:
        getter_attributes_test(name, item_from_xml, item_from_dict,
                               dict_.get(name))
    # Boolean attribute
    getter_attributes_test('can_play', item_from_xml, item_from_dict,
                           bool(dict_.get('can_play')))
    return item_from_xml, item_from_dict
Exemplo n.º 3
0
    def test_add_item_to_sonos_playlist(self, moco):
        moco.contentDirectory.reset_mock()

        playlist = mock.Mock()
        playlist.item_id = 7

        track = mock.Mock()
        track.uri = 'fake_uri'
        track.didl_metadata = XML.Element('a')

        update_id = 100

        moco.contentDirectory.Browse.return_value = {
            'NumberReturned': '0',
            'Result':
            '<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"></DIDL-Lite>',
            'TotalMatches': '0',
            'UpdateID': update_id
        }

        moco.add_item_to_sonos_playlist(track, playlist)
        moco.contentDirectory.Browse.assert_called_once_with([
            ('ObjectID', playlist.item_id),
            ('BrowseFlag', 'BrowseDirectChildren'), ('Filter', '*'),
            ('StartingIndex', 0), ('RequestedCount', 1), ('SortCriteria', '')
        ])
        moco.avTransport.AddURIToSavedQueue.assert_called_once_with([
            ('InstanceID', 0), ('UpdateID', update_id),
            ('ObjectID', playlist.item_id), ('EnqueuedURI', track.uri),
            ('EnqueuedURIMetaData', XML.tostring(track.didl_metadata)),
            ('AddAtIndex', 4294967295)
        ])
Exemplo n.º 4
0
def test_call():
    """Calling a command should result in an http request."""

    s = SoapMessage(
        endpoint="http://endpoint.example.com",
        method="getData",
        parameters=[("one", "1")],
        http_headers={"user-agent": "sonos"},
        soap_action="ACTION",
        soap_header="<a_header>data</a_header>",
        namespace="http://namespace.com",
        other_arg=4,
    )

    response = mock.MagicMock()
    response.headers = {}
    response.status_code = 200
    response.content = DUMMY_VALID_RESPONSE
    with mock.patch("requests.post", return_value=response) as fake_post:
        result = s.call()
        assert XML.tostring(result)
        fake_post.assert_called_once_with(
            "http://endpoint.example.com",
            headers={"SOAPACTION": '"ACTION"', "Content-Type": 'text/xml; charset="utf-8"', "user-agent": "sonos"},
            data=mock.ANY,
            other_arg=4,
        )
Exemplo n.º 5
0
 def test_create_didl_resource_to_from_element(self):
     res = data_structures.DidlResource('a%20uri', 'a:protocol:info:xx',
         bitrate=3)
     elt = res.to_element()
     assert XML.tostring(elt) == (
         b'<res bitrate="3" protocolInfo="a:protocol:info:xx">a%20uri</res>')
     assert data_structures.DidlResource.from_element(elt) == res
Exemplo n.º 6
0
def test_call():
    """ Calling a command should result in an http request"""

    s = SoapMessage(
        endpoint='http://endpoint.example.com',
        method='getData',
        parameters=[('one', '1')],
        http_headers={'user-agent': 'sonos'},
        soap_action='ACTION',
        soap_header="<a_header>data</a_header>",
        namespace="http://namespace.com",
        other_arg=4)

    response = mock.MagicMock()
    response.headers = {}
    response.status_code = 200
    response.content = DUMMY_VALID_RESPONSE
    with mock.patch('requests.post', return_value=response) as fake_post:
        result = s.call()
        assert XML.tostring(result)
        fake_post.assert_called_once_with(
            'http://endpoint.example.com',
            headers={'SOAPACTION': '"ACTION"',
                     'Content-Type': 'text/xml; charset="utf-8"', 'user-agent':
                         'sonos'},
            data=mock.ANY, other_arg=4)
Exemplo n.º 7
0
def test_call():
    """Calling a command should result in an http request."""

    s = SoapMessage(
        endpoint="http://endpoint.example.com",
        method="getData",
        parameters=[("one", "1")],
        http_headers={"user-agent": "sonos"},
        soap_action="ACTION",
        soap_header="<a_header>data</a_header>",
        namespace="http://namespace.com",
        other_arg=4,
    )

    response = mock.MagicMock()
    response.headers = {}
    response.status_code = 200
    response.content = DUMMY_VALID_RESPONSE
    with mock.patch("requests.post", return_value=response) as fake_post:
        result = s.call()
        assert XML.tostring(result)
        fake_post.assert_called_once_with(
            "http://endpoint.example.com",
            headers={
                "SOAPACTION": '"ACTION"',
                "Content-Type": 'text/xml; charset="utf-8"',
                "user-agent": "sonos",
            },
            data=mock.ANY,
            other_arg=4,
        )
Exemplo n.º 8
0
 def test_create_didl_resource_to_from_element(self):
     res = data_structures.DidlResource('a%20uri', 'a:protocol:info:xx',
                                        bitrate=3)
     elt = res.to_element()
     assert XML.tostring(elt) == (
         b'<res bitrate="3" protocolInfo="a:protocol:info:xx">a%20uri</res>')
     assert data_structures.DidlResource.from_element(elt) == res
Exemplo n.º 9
0
def test_call():
    """Calling a command should result in an http request."""

    s = SoapMessage(
        endpoint='http://endpoint.example.com',
        method='getData',
        parameters=[('one', '1')],
        http_headers={'user-agent': 'sonos'},
        soap_action='ACTION',
        soap_header="<a_header>data</a_header>",
        namespace="http://namespace.com",
        other_arg=4)

    response = mock.MagicMock()
    response.headers = {}
    response.status_code = 200
    response.content = DUMMY_VALID_RESPONSE
    with mock.patch('requests.post', return_value=response) as fake_post:
        result = s.call()
        assert XML.tostring(result)
        fake_post.assert_called_once_with(
            'http://endpoint.example.com',
            headers={'SOAPACTION': '"ACTION"',
                     'Content-Type': 'text/xml; charset="utf-8"', 'user-agent':
                         'sonos'},
            data=mock.ANY, other_arg=4)
Exemplo n.º 10
0
    def call(self, method, args=None):
        http_headers = {
            'Accept-Encoding': 'gzip, deflate',
            'User-Agent': 'Linux UPnP/1.0 Sonos/26.99-12345'
        }

        message = SoapMessage(endpoint=self.endpoint,
                              method=method,
                              parameters=[] if args is None else args,
                              http_headers=http_headers,
                              soap_action="http://www.sonos.com/Services/1"
                              ".1#{0}".format(method),
                              soap_header=self.get_soap_header(),
                              namespace='http://www.sonos.com/Services/1.1',
                              timeout=self.timeout)

        try:
            result_elt = message.call()
        except SoapFault as exc:
            raise MusicServiceException(exc.faultstring, exc.faultcode)

        result = list(
            parse(XML.tostring(result_elt),
                  process_namespaces=True,
                  namespaces={
                      'http://www.sonos.com/Services/1.1': None
                  }).values())[0]

        return result if result is not None else {}
Exemplo n.º 11
0
    def __init__(self, faultcode, faultstring, detail=None):
        """ Args:
                faultcode (str): The SOAP faultcode
                faultstring (str): The SOAP faultstring
                detail (Element): The SOAP fault detail, as an ElementTree
                     Element. Default, None

        """
        self.faultcode = faultcode
        self.faultstring = faultstring
        self.detail = detail
        self.detail_string = XML.tostring(detail) if detail is not None else ''
        super(SoapFault, self).__init__(faultcode, faultstring)
Exemplo n.º 12
0
    def __init__(self, faultcode, faultstring, detail=None):
        """ Args:
                faultcode (str): The SOAP faultcode
                faultstring (str): The SOAP faultstring
                detail (Element): The SOAP fault detail, as an ElementTree
                     Element. Default, None

        """
        self.faultcode = faultcode
        self.faultstring = faultstring
        self.detail = detail
        self.detail_string = XML.tostring(detail) if detail is not None else ''
        super(SoapFault, self).__init__(faultcode, faultstring)
Exemplo n.º 13
0
    def get_soap_header(self):
        """ Generate the SOAP authentication header for the related service.

        This header contains all the necessary authentication details.

        Returns:
            (str): A string representation of the XML content of the SOAP
                header

        """

        # According to the SONOS SMAPI, this header must be sent with all
        # SOAP requests. Building this is an expensive operation (though
        # occasionally necessary), so f we have a cached value, return it
        if self._cached_soap_header is not None:
            return self._cached_soap_header
        music_service = self.music_service
        credentials_header = XML.Element(
            "credentials", {'xmlns': "http://www.sonos.com/Services/1.1"})
        device_id = XML.SubElement(credentials_header, 'deviceId')
        device_id.text = self._device_id
        device_provider = XML.SubElement(credentials_header, 'deviceProvider')
        device_provider.text = 'Sonos'
        if music_service.account.oa_device_id:
            # OAuth account credentials are present. We must use them to
            # authenticate.
            login_token = XML.Element('loginToken')
            token = XML.SubElement(login_token, 'token')
            token.text = music_service.account.oa_device_id
            key = XML.SubElement(login_token, 'key')
            key.text = music_service.account.key
            household_id = XML.SubElement(login_token, 'householdId')
            household_id.text = self._device.household_id
            credentials_header.append(login_token)

        # otherwise, perhaps use DeviceLink or UserId auth
        elif music_service.auth_type in ['DeviceLink', 'UserId']:
            # We need a session ID from Sonos
            session_id = self._device.musicServices.GetSessionId([
                ('ServiceId', music_service.service_id),
                ('Username', music_service.account.username)
            ])['SessionId']
            session_elt = XML.Element('sessionId')
            session_elt.text = session_id
            credentials_header.append(session_elt)

        # Anonymous auth. No need for anything further.
        self._cached_soap_header = XML.tostring(credentials_header)
        return self._cached_soap_header
Exemplo n.º 14
0
    def get_soap_header(self):
        """ Generate the SOAP authentication header for the related service.

        This header contains all the necessary authentication details.

        Returns:
            (str): A string representation of the XML content of the SOAP
                header

        """

        # According to the SONOS SMAPI, this header must be sent with all
        # SOAP requests. Building this is an expensive operation (though
        # occasionally necessary), so f we have a cached value, return it
        if self._cached_soap_header is not None:
            return self._cached_soap_header
        music_service = self.music_service
        credentials_header = XML.Element(
            "credentials", {'xmlns': "http://www.sonos.com/Services/1.1"})
        device_id = XML.SubElement(credentials_header, 'deviceId')
        device_id.text = self._device_id
        device_provider = XML.SubElement(credentials_header, 'deviceProvider')
        device_provider.text = 'Sonos'
        if music_service.account.oa_device_id:
            # OAuth account credentials are present. We must use them to
            # authenticate.
            login_token = XML.Element('loginToken')
            token = XML.SubElement(login_token, 'token')
            token.text = music_service.account.oa_device_id
            key = XML.SubElement(login_token, 'key')
            key.text = music_service.account.key
            household_id = XML.SubElement(login_token, 'householdId')
            household_id.text = self._device.household_id
            credentials_header.append(login_token)

        # otherwise, perhaps use DeviceLink or UserId auth
        elif music_service.auth_type in ['DeviceLink', 'UserId']:
            # We need a session ID from Sonos
            session_id = self._device.musicServices.GetSessionId([
                ('ServiceId', music_service.service_id),
                ('Username', music_service.account.username)
            ])['SessionId']
            session_elt = XML.Element('sessionId')
            session_elt.text = session_id
            credentials_header.append(session_elt)

        # Anonymous auth. No need for anything further.
        self._cached_soap_header = XML.tostring(credentials_header)
        return self._cached_soap_header
Exemplo n.º 15
0
 def test_didl_object_to_element(self):
     didl_object = data_structures.DidlObject(title='a_title',
         parent_id='pid', item_id='iid', creator='a_creator')
     # we seem to have to go through this to get ElementTree to deal
     # with namespaces properly!
     elt = XML.fromstring(XML.tostring(didl_object.to_element(True)))
     elt2 = XML.fromstring('<dummy xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" ' +
                 'xmlns:dc="http://purl.org/dc/elements/1.1/" ' +
                 'xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">' +
                 '<item id="iid" parentID="pid" restricted="true">' +
                 '<dc:title>a_title</dc:title>' +
                 '<dc:creator>a_creator</dc:creator>' +
                 '<upnp:class>object</upnp:class><desc id="cdudn" '+
                 'nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">' +
                 'RINCON_AssociatedZPUDN</desc></item></dummy>')[0]
     assert_xml_equal(elt2, elt)
Exemplo n.º 16
0
 def test_didl_object_to_element(self):
     didl_object = data_structures.DidlObject(
         title='a_title', parent_id='pid', item_id='iid', creator='a_creator')
     # we seem to have to go through this to get ElementTree to deal
     # with namespaces properly!
     elt = XML.fromstring(XML.tostring(didl_object.to_element(True)))
     elt2 = XML.fromstring(
         '<dummy xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" ' +
         'xmlns:dc="http://purl.org/dc/elements/1.1/" ' +
         'xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">' +
         '<item id="iid" parentID="pid" restricted="true">' +
         '<dc:title>a_title</dc:title>' +
         '<dc:creator>a_creator</dc:creator>' +
         '<upnp:class>object</upnp:class><desc id="cdudn" ' +
         'nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">' +
         'RINCON_AssociatedZPUDN</desc></item></dummy>')[0]
     assert_xml_equal(elt2, elt)
Exemplo n.º 17
0
    def get_soap_header(self):
        if self._session_id is None:
            return ''

        if self._cached_soap_header is not None:
            return self._cached_soap_header

        credentials_header = XML.Element(
            "credentials", {'xmlns': "http://www.sonos.com/Services/1.1"})

        if self.music_service.auth_type in ['UserId']:
            session_elt = XML.Element('sessionId')
            session_elt.text = self._session_id
            credentials_header.append(session_elt)
        else:
            raise Exception('unknown auth_type = ' +
                            self.music_service.auth_type)

        self._cached_soap_header = XML.tostring(
            credentials_header, encoding='utf-8').decode(encoding='utf-8')
        return self._cached_soap_header
Exemplo n.º 18
0
    def test_add_item_to_sonos_playlist(self, moco):
        playlist = mock.Mock()
        playlist.item_id = 7

        track = mock.Mock()
        track.uri = 'fake_uri'
        track.didl_metadata = XML.Element('a')

        update_id = 100
        moco._music_lib_search = mock.Mock(return_value=(
            {'UpdateID': update_id},
            None))

        moco.add_item_to_sonos_playlist(track, playlist)
        moco._music_lib_search.assert_called_once_with(playlist.item_id, 0, 1)
        moco.avTransport.AddURIToSavedQueue.assert_called_once_with(
            [('InstanceID', 0),
             ('UpdateID', update_id),
             ('ObjectID', playlist.item_id),
             ('EnqueuedURI', track.uri),
             ('EnqueuedURIMetaData', XML.tostring(track.didl_metadata)),
             ('AddAtIndex', 4294967295)]
        )
Exemplo n.º 19
0
    def test_add_item_to_sonos_playlist(self, moco):
        playlist = mock.Mock()
        playlist.item_id = 7

        track = mock.Mock()
        track.uri = 'fake_uri'
        track.didl_metadata = XML.Element('a')

        update_id = 100
        moco._music_lib_search = mock.Mock(return_value=(
            {'UpdateID': update_id},
            None))

        moco.add_item_to_sonos_playlist(track, playlist)
        moco._music_lib_search.assert_called_once_with(playlist.item_id, 0, 1)
        moco.avTransport.AddURIToSavedQueue.assert_called_once_with(
            [('InstanceID', 0),
             ('UpdateID', update_id),
             ('ObjectID', playlist.item_id),
             ('EnqueuedURI', track.uri),
             ('EnqueuedURIMetaData', XML.tostring(track.didl_metadata)),
             ('AddAtIndex', 4294967295)]
        )
Exemplo n.º 20
0
    def test_add_item_to_sonos_playlist(self, moco):
        moco.contentDirectory.reset_mock()

        playlist = mock.Mock()
        playlist.item_id = 7

        track = mock.Mock()
        track.uri = 'fake_uri'
        track.didl_metadata = XML.Element('a')

        update_id = 100

        moco.contentDirectory.Browse.return_value = {
            'NumberReturned': '0',
            'Result': '<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"></DIDL-Lite>',
            'TotalMatches': '0',
            'UpdateID': update_id
        }

        moco.add_item_to_sonos_playlist(track, playlist)
        moco.contentDirectory.Browse.assert_called_once_with([
            ('ObjectID', playlist.item_id),
            ('BrowseFlag', 'BrowseDirectChildren'), 
            ('Filter', '*'),
            ('StartingIndex', 0),
            ('RequestedCount', 1),
            ('SortCriteria', '')
        ])
        moco.avTransport.AddURIToSavedQueue.assert_called_once_with(
            [('InstanceID', 0),
             ('UpdateID', update_id),
             ('ObjectID', playlist.item_id),
             ('EnqueuedURI', track.uri),
             ('EnqueuedURIMetaData', XML.tostring(track.didl_metadata)),
             ('AddAtIndex', 4294967295)]
        )
Exemplo n.º 21
0
    def call(self, method, args=None):
        """ Call a method on the server

        Args:
            method (str): The name of the method to call.
             args (list): A list of (parameter, value) pairs representing
                 the parameters of the method. Default None.

        Returns:
            (OrderedDict): An OrderedDict representing the response

        Raises:
            MusicServiceException

        """
        message = SoapMessage(endpoint=self.endpoint,
                              method=method,
                              parameters=[] if args is None else args,
                              http_headers=self.http_headers,
                              soap_action="http://www.sonos.com/Services/1"
                              ".1#{0}".format(method),
                              soap_header=self.get_soap_header(),
                              namespace=self.namespace,
                              timeout=self.timeout)

        try:
            result_elt = message.call()
        except SoapFault as exc:
            if 'Client.TokenRefreshRequired' in exc.faultcode:
                log.debug('Token refresh required. Trying again')
                # Remove any cached value for the SOAP header
                self._cached_soap_header = None

                # <detail>
                #   <refreshAuthTokenResult>
                #       <authToken>xxxxxxx</authToken>
                #       <privateKey>zzzzzz</privateKey>
                #   </refreshAuthTokenResult>
                # </detail>
                auth_token = exc.detail.findtext('.//authToken')
                private_key = exc.detail.findtext('.//privateKey')
                # We have new details - update the account
                self.music_service.account.oa_device_id = auth_token
                self.music_service.account.key = private_key
                message = SoapMessage(
                    endpoint=self.endpoint,
                    method=method,
                    parameters=args,
                    http_headers=self.http_headers,
                    soap_action="http://www.sonos.com/Services/1"
                    ".1#{0}".format(method),
                    soap_header=self.get_soap_header(),
                    namespace=self.namespace,
                    timeout=self.timeout)
                result_elt = message.call()

            else:
                raise MusicServiceException(exc.faultstring, exc.faultcode)

        # The top key in the OrderedDict will be the methodResult. Its
        # value may be None if no results were returned.
        result = parse(XML.tostring(result_elt),
                       process_namespaces=True,
                       namespaces={
                           'http://www.sonos.com/Services/1.1': None
                       }).values()[0]

        return result if result is not None else {}
Exemplo n.º 22
0
    def call(self, method, args=None):
        """ Call a method on the server

        Args:
            method (str): The name of the method to call.
             args (list): A list of (parameter, value) pairs representing
                 the parameters of the method. Default None.

        Returns:
            (OrderedDict): An OrderedDict representing the response

        Raises:
            MusicServiceException

        """
        message = SoapMessage(
            endpoint=self.endpoint,
            method=method,
            parameters=[] if args is None else args,
            http_headers=self.http_headers,
            soap_action="http://www.sonos.com/Services/1"
                        ".1#{0}".format(method),
            soap_header=self.get_soap_header(),
            namespace=self.namespace,
            timeout=self.timeout)

        try:
            result_elt = message.call()
        except SoapFault as exc:
            if 'Client.TokenRefreshRequired' in exc.faultcode:
                log.debug('Token refresh required. Trying again')
                # Remove any cached value for the SOAP header
                self._cached_soap_header = None

                # <detail>
                #   <refreshAuthTokenResult>
                #       <authToken>xxxxxxx</authToken>
                #       <privateKey>zzzzzz</privateKey>
                #   </refreshAuthTokenResult>
                # </detail>
                auth_token = exc.detail.findtext('.//authToken')
                private_key = exc.detail.findtext('.//privateKey')
                # We have new details - update the account
                self.music_service.account.oa_device_id = auth_token
                self.music_service.account.key = private_key
                message = SoapMessage(
                    endpoint=self.endpoint,
                    method=method,
                    parameters=args,
                    http_headers=self.http_headers,
                    soap_action="http://www.sonos.com/Services/1"
                                ".1#{0}".format(method),
                    soap_header=self.get_soap_header(),
                    namespace=self.namespace,
                    timeout=self.timeout)
                result_elt = message.call()

            else:
                raise MusicServiceException(exc.faultstring, exc.faultcode)

        # The top key in the OrderedDict will be the methodResult. Its
        # value may be None if no results were returned.
        result = parse(
            XML.tostring(result_elt), process_namespaces=True,
            namespaces={'http://www.sonos.com/Services/1.1': None}
        ).values()[0]

        return result if result is not None else {}