šŸ” index : ~doyle/sonos.rs.git

author Jordan Doyle <jordan@doyle.wf> 2017-12-09 22:32:50.0 +00:00:00
committer Jordan Doyle <jordan@doyle.wf> 2017-12-09 22:32:50.0 +00:00:00
commit
9df5e979ab704421da39a2c75283d645892b239e [patch]
tree
eedec1cdb8eb14c3549aa140f77b96545d30fdfd
parent
2b5e6d9fe9a192ac868d830d777644f744ceff80
download
9df5e979ab704421da39a2c75283d645892b239e.tar.gz

Better SOAP error logging



Diff

 src/device.rs             |  56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 src/error.rs              | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 tests/integration_test.rs |   7 +++++++
 3 files changed, 164 insertions(+), 9 deletions(-)

diff --git a/src/device.rs b/src/device.rs
index ef7b8ce..7ef7ff8 100644
--- a/src/device.rs
+++ a/src/device.rs
@@ -38,6 +38,9 @@
    Stopped,
    Playing,
    PausedPlayback,
    PausedRecording,
    Recording,
    NoMediaPresent,
    Transitioning,
}

@@ -164,7 +167,7 @@
    ) -> Result<Element> {
        let mut headers = Headers::new();
        headers.set(ContentType::xml());
        headers.set_raw("SOAPAction", format!("{}#{}", service, action));
        headers.set_raw("SOAPAction", format!("\"{}#{}\"", service, action));

        let client = reqwest::Client::new();
        let coordinator = if coordinator { self.coordinator()? } else { self.ip };
@@ -194,14 +197,28 @@
        let element =
            Element::parse(request).chain_err(|| "Failed to parse XML from Sonos controller")?;

        Ok(
            element
                .get_child("Body")
                .ok_or("Failed to get body element")?
        let body = element.get_child("Body")
            .ok_or("Failed to get body element")?;

        if let Some(fault) = body.get_child("Fault") {
            let error_code = element_to_string(fault.get_child("detail")
                .chain_err(|| ErrorKind::ParseError)?
                .get_child("UPnPError")
                .chain_err(|| ErrorKind::ParseError)?
                .get_child("errorCode")
                .chain_err(|| ErrorKind::ParseError)?)
                .parse::<u64>()
                .chain_err(|| ErrorKind::ParseError)?;

            let state = AVTransportError::from(error_code);
            error!("Got state {:?} from {}#{} call.", state, service, action);
            Err(ErrorKind::from(state).into())
        } else {
            Ok(body
                .get_child(format!("{}Response", action))
                .ok_or("Failed to find response element")?
                .clone(),
        )
                .ok_or(format!("Failed to find response element {:?}", element))?
                .clone())
        }
    }

    /// Play the current track

@@ -340,6 +357,26 @@
                  <EnqueuedURIMetaData></EnqueuedURIMetaData>
                  <DesiredFirstTrackNumberEnqueued>0</DesiredFirstTrackNumberEnqueued>
                  <EnqueueAsNext>0</EnqueueAsNext>"#,
                uri
            ),
            true,
        )?;

        Ok(())
    }

    pub fn queue_next(&self, uri: &str) -> Result<()> {
        self.soap(
            "MediaRenderer/AVTransport/Control",
            "urn:schemas-upnp-org:service:AVTransport:1",
            "AddURIToQueue",
            &format!(
                r#"
                  <InstanceID>0</InstanceID>
                  <EnqueuedURI>{}</EnqueuedURI>
                  <EnqueuedURIMetaData></EnqueuedURIMetaData>
                  <DesiredFirstTrackNumberEnqueued>0</DesiredFirstTrackNumberEnqueued>
                  <EnqueueAsNext>1</EnqueueAsNext>"#,
                uri
            ),
            true,
@@ -487,6 +524,9 @@
                "STOPPED" => TransportState::Stopped,
                "PLAYING" => TransportState::Playing,
                "PAUSED_PLAYBACK" => TransportState::PausedPlayback,
                "PAUSED_RECORDING" => TransportState::PausedRecording,
                "RECORDING" => TransportState::Recording,
                "NO_MEDIA_PRESENT" => TransportState::NoMediaPresent,
                "TRANSITIONING" => TransportState::Transitioning,
                _ => TransportState::Stopped,
            },
diff --git a/src/error.rs b/src/error.rs
index 0c6d43c..5c0b0fa 100644
--- a/src/error.rs
+++ a/src/error.rs
@@ -1,1 +1,109 @@
error_chain!{}
error_chain! {
    errors {
        AVTransportError(error: AVTransportError) {
            description("An error occurred from AVTransport")
            display("Received error {:?} from Sonos speaker", error)
        }

        ParseError {
            description("An error occurred when attempting to parse SOAP XML from Sonos")
            display("Failed to parse Sonos response XML")
        }
    }
}

impl From<AVTransportError> for ErrorKind {
    fn from(error: AVTransportError) -> Self {
        ErrorKind::AVTransportError(error)
    }
}

#[derive(Debug)]
pub enum AVTransportError {
    /// No action by that name at this service.

    InvalidAction = 401,
    /// Could be any of the following: not enough in args, too many in args, no in arg by that name,

    /// one or more in args are of the wrong data type.

    InvalidArgs = 402,
    /// No state variable by that name at this service.

    InvalidVar = 404,
    /// May be returned in current state of service prevents invoking that action.

    ActionFailed = 501,
    /// The immediate transition from current transport state to desired transport state is not

    /// supported by this device.

    TransitionNotAvailable = 701,
    /// The media does not contain any contents that can be played.

    NoContents = 702,
    /// The media cannot be read (e.g., because of dust or a scratch).

    ReadError = 703,
    /// The storage format of the currently loaded media is not supported

    FormatNotSupported = 704,
    /// The transport is ā€œhold lockedā€.

    TransportLocked = 705,
    /// The media cannot be written (e.g., because of dust or a scratch)

    WriteError = 706,
    /// The media is write-protected or is of a not writable type.

    MediaNotWriteable = 707,
    /// The storage format of the currently loaded media is not supported for recording by this

    /// device

    RecordingFormatNotSupported = 708,
    /// There is no free space left on the loaded media

    MediaFull = 709,
    /// The specified seek mode is not supported by the device

    SeekModeNotSupported = 710,
    /// The specified seek target is not specified in terms of the seek mode, or is not present on

    /// the media

    IllegalSeekTarget = 711,
    /// The specified play mode is not supported by the device

    PlayModeNotSupported = 712,
    /// The specified record quality is not supported by the device

    RecordQualityNotSupported = 713,
    /// The resource to be played has a mimetype which is not supported by the AVTransport service

    IllegalMimeType = 714,
    /// This indicates the resource is already being played by other means

    ContentBusy = 715,
    /// The specified playback speed is not supported by the AVTransport service

    PlaySpeedNotSupported = 717,
    /// The specified instanceID is invalid for this AVTransport

    InvalidInstanceId = 718,
    /// The DNS Server is not available (HTTP error 503)

    NoDnsServer = 737,
    /// Unable to resolve the Fully Qualified Domain Name. (HTTP error 502)

    BadDomainName = 738,
    /// The server that hosts the resource is unreachable or unresponsive (HTTP error 404/410).

    ServerError = 739,
    /// Error we've not come across before

    Unknown,
}

impl From<u64> for AVTransportError {
    fn from(code: u64) -> AVTransportError {
        match code {
            401 => AVTransportError::InvalidAction,
            402 => AVTransportError::InvalidArgs,
            404 => AVTransportError::InvalidVar,
            501 => AVTransportError::ActionFailed,
            701 => AVTransportError::TransitionNotAvailable,
            702 => AVTransportError::NoContents,
            703 => AVTransportError::ReadError,
            704 => AVTransportError::FormatNotSupported,
            705 => AVTransportError::TransportLocked,
            706 => AVTransportError::WriteError,
            707 => AVTransportError::MediaNotWriteable,
            708 => AVTransportError::RecordingFormatNotSupported,
            709 => AVTransportError::MediaFull,
            710 => AVTransportError::SeekModeNotSupported,
            711 => AVTransportError::IllegalSeekTarget,
            712 => AVTransportError::PlayModeNotSupported,
            713 => AVTransportError::RecordQualityNotSupported,
            714 => AVTransportError::IllegalMimeType,
            715 => AVTransportError::ContentBusy,
            717 => AVTransportError::PlaySpeedNotSupported,
            718 => AVTransportError::InvalidInstanceId,
            737 => AVTransportError::NoDnsServer,
            738 => AVTransportError::BadDomainName,
            739 => AVTransportError::ServerError,
            _ => AVTransportError::Unknown,
        }
    }
}
diff --git a/tests/integration_test.rs b/tests/integration_test.rs
index db9aff4..39f1c3c 100644
--- a/tests/integration_test.rs
+++ a/tests/integration_test.rs
@@ -74,6 +74,13 @@
}

#[test]
fn seek() {
    let device = get_speaker();
    device.seek(&std::time::Duration::from_secs(30)).expect("Failed to seek to 30 seconds");
    assert_eq!(device.track().expect("Failed to get track info").running_time.as_secs(), 30);
}

#[test]
fn play() {
    let device = get_speaker();
    device.play().expect("Failed to play");