Saturday, June 19, 2010

Exploring Sonos via UPnP

We’ve had the Sonos system for a while now and we decided that it would be “fun” to create a web application that would talk to the Sonos, get information about what’s playing, and most importantly, match additional album art imagery from a place like http://www.lostvibe.com/ to what’s playing. The Sonos system is great, but the album artwork situation is lacking. You only get the front cover artwork. Well, with this as our goal, we set out to figure how to talk to our Sonos. It took endless hours of trying this and that. Hopefully, in this post we’ll simplify the approach and present a better way to attack the problem (not in the order we did). Others have tackled this problem and created products you could download and use like this or this or this (for the HomeSeer system). These applications assume you know little to nothing about how the system is put together and you just want the functionality. We wanted to strike a balance somewhere in the middle, knowing how Sonos works, getting familiar with UPnP, and having some fun with programming. This post just deals with the first part of the investigation and not building of the actual web application. Stay tuned for that. Update 2010-06-30: here it is, the follow up post.

Sonos is UPnP Compatible…What’s That?
Universal Plug and Play (UPnP) is a set of network protocols whose goal is allowing devices to seamlessly discover and connect to a network and thereafter be monitored and controlled. The UPnP protocols simplify your life as a user because routers, printers, phones, and other devices can be plugged in and ready to go without much intervention by you. (Well, that’s the idea and it usually works.)

How Can I See What UPnP Devices I Have?
So you have devices that are UPnP, but how do you see them if you are running Windows? Well you can use the Univeral Plug-and-Play Tester from noeld.com which is where we first started or you can use the Device Spy tool that was once part of Intel’s Tools for UPnP Technologies, but is now open-sourced and can be found here: http://opentools.homeip.net/dev-tools-for-upnp. The Device Spy tool is a bit easier to work with because its two panes make it easier to see what’s going on and overall it's more user-friendly. Here is an example of what you get when you run it each of the tools.

Left: UPnP Tester: Right: Device Spy
UPnP TesterDevice Spy
As you can see from the screenshot, there are five Sonos devices in our house. The highlighted item is an CONNECT:AMP. Expanding any item, we see there is a lot more going on. To understand what's being shown you need to know that there are two generic objects of interest: a device and service. A device is a container for other devices and services. A service exposes actions and models its state with state variables. So, in the expanded screenshot of a PLAY:5 you can see an example of devices and services:




Device (e.g. ZPS5) (device XML http://192.168.2.10:1400/xml/zone_player.xml)
--Service: AlarmClock
--Service: AudioIn
--Service: DeviceProperties
--Service: GroupManagement
--Service: MusicServices
--Service: SystemProperties
--Service: ZoneGroup Topology
----Device: Media Renderer
------Service: Queue
------Service: AVTransport
------Service: Connection Manager
------Service: GroupRenderingControl (device XML http://192.168.2.225:1400/xml/AVTransport1.xml)
------Service: RenderingControl
----Device: Media Server
------Service: ConnectionManager
------Service: ContentDirectory

We only show two device/service XML URLs above. Once you use the Device Spy tool, you can easily get the other XML URLs and they follow the same pattern as shown above. The service XML is useful because it tells you what actions you can take with the service. So for the AVTransport service you probably can guess there are actions of some sort to start and stop the music. The device/service topology presented by the Device Spy is (probably) built from looking at device and service XML files. The tool doesn't know anything about the devices and services and so has to discover them. To be exact, the Device Spy discovers what’s on the network, then it looks at the device XML which refers to all the sub-devices and services and their XML files and so on. From all the device and service XML, the topology can be rendered.

You can see from the screenshot that we are dealing with a zone called “Office”.

Can I Control My Sonos From Here?
Yes, with the Device Spy tool (or similar) you can control Sonos devices. It’s not convenient for lots of actions so people write programs to abstract the process, naturally. But, using this tool you can get an idea of what’s available and help you understand the system.

Expand the AVTransport service, right click the GetTransportInfo action, and select Invoke Action which brings you to the window shown. When you click the Invoke button it queries the zone player and returns information. You see in this example that the zone is PLAYING. You can see possible values for the TransportState by navigating to the State Variables folder icon, expanding and clicking on TransportState.





To stop the current zone player go the Stop action, right click and select Invoke Action (or just double-click Stop), and click the Invoke button.

Can I Figure Out What’s Playing Using Device Spy?
Yes, but it’s a bit tricky. Because zone players can be grouped into zone groups, there is the concept of a group coordinator. The group coordinator is what you have to “ask” for the metadata on what’s playing. So, to continue with this example, we are looking at the “Office” zone player. Going to the GetPositionInfo action and invoking it you see that the TrackURI value has “x-rincon” in it and that TrackMetaData has NOT_IMPLEMENTED. Cutting to the chase, this means that this zone is a slave to the master or group coordinator with the given id RINCON_xxxxxxxxx.



If you go to the group coordinator specified in the slave zone's TrackURI field and invoke the GetPositionInfo action for that group coordinator zone player then you will get metadata for what's playing.




In this example, a track is being played from a local server so the TrackMetaData gives us information to this effect: the artist, the album, the track, and a link to the album art imagery.

<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/">
  <item id="-1" parentID="-1" restricted="true">
   <res protocolInfo="x-file-cifs:*:audio/flac:*" duration="0:06:40">x-file-cifs://mediamarx/music/Master/Beth%20Orton/Daybreaker/Beth%20Orton%20-%2010%20-%20Thinking%20About%20Tomorrow.flac</res>
   <r:streamContent></r:streamContent>
   <upnp:albumArtURI>/getaa?u=x-file-cifs%3a%2f%2fmediamarx%2fmusic%2fMaster%2fBeth%2520Orton%2fDaybreaker%2fBeth%2520Orton%2520-%252010%2520-%2520Thinking%2520About%2520Tomorrow.flac&amp;v=353</upnp:albumArtURI>
   <dc:title>Thinking About Tomorrow</dc:title>
   <upnp:class>object.item.audioItem.musicTrack</upnp:class>
   <dc:creator>Beth Orton</dc:creator>
   <upnp:album>Daybreaker</upnp:album>
   <upnp:originalTrackNumber>10</upnp:originalTrackNumber>
   <r:albumArtist>Beth Orton</r:albumArtist>
  </item>
</DIDL-Lite>


Okay, So How Would I Control the Sonos via UPnP Without Device Spy?

So far we know that the UPnP Sonos device has certain properties that can be viewed via HTTP, like http://192.168.2.225:1400/xml/AVTransport1.xml, so it looks like we are going to talk using HTTP. To that end, we will use the Fiddler Tool to construct simple HTTP requests to start with. Let’s see if we can duplicate the GetTransportInfo action that we performed with Device Spy above. To piece together what’s needed:

1) View the device_description.xml (right click on zone player and select Get Device XML)  for any zone player and look for AVTransport service.





You should find something like this in the devcie_description.xml:

<serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
<serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
<controlURL>/MediaRenderer/AVTransport/Control</controlURL>
<eventSubURL>/MediaRenderer/AVTransport/Event</eventSubURL>
<SCPDURL>/xml/AVTransport1.xml</SCPDURL>


2) Refer back to the reference on UPnP given at the start of this article and see that we are going to be sending some kind of SOAP message to the device.

3) Run the Device Validator (Device Validator.exe in the download from http://opentools.homeip.net/dev-tools-for-upnp) run some Control tests against the PLAY:5 device to get a sense of what kind of actions we can take.





Here's an information about an example test. It's a POST to the device with a SOAP action.

POST /MediaRenderer/AVTransport/Control HTTP/1.1
HOST: 192.168.2.225:1400
SOAPACTION: "urn:schemas-upnp-org:service:AVTransport:1#GetTransportInfo"
CONTENT-TYPE: text/xml; charset="utf-8"
Content-Length: 353

The body of the request will look something like this:

<?xml version="1.0" encoding="utf-8"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
   <s:Body>
      <u:ACTION  xmlns:u="serviceType">
      <!-- input arguments here -->
      </u:ACTION>
   </s:Body>
</s:Envelope>


Let's take a step back for a second and review HTTP requests.  HTTP requests have three parts: a method like POST or GET, headers, and a body. The body in our work here will contain the SOAP envelope with an ACTION. ACTION in the example above is GetTransportInfo, and in general, is any other applicable action for the device. serviceType is the type from the zone_player.xml file, urn:schemas-upnp-org:service:AVTransport:1 in this example.. Putting it all together, we can construct an HTTP request. The two screen shots below are from Fiddler, but it will be similar in any tool you use

Left: Fiddler Compose Request: Right: Fiddler Response Inspector

A successful request will return the following SOAP message in the body of the response.


<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
  <u:GetTransportInfoResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
    <CurrentTransportState>PLAYING</CurrentTransportState>       <CurrentTransportStatus>OK</CurrentTransportStatus>
    <CurrentSpeed>1</CurrentSpeed>
  </u:GetTransportInfoResponse>
</s:Body>
</s:Envelope>



Can I Search The Sonos Music Index?
Yes, you can browse the Sonos index using a SOAP request over HTTP to the zone player. You can construct a Fiddler HTTP request with the following components:

METHOD: POST http://192.168.2.6:1400/MediaServer/ContentDirectory/Control
HEADER: SOAPACTION: "urn:schemas-upnp-org:service:ContentDirectory:1#Browse"
BODY:

<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
    <u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
         <ObjectID>A:ARTIST</ObjectID>
          <BrowseFlag>BrowseDirectChildren</BrowseFlag>
          <Filter>*</Filter>
         <StartingIndex>0</StartingIndex>
         <RequestedCount>10</RequestedCount>
         <SortCriteria>*</SortCriteria>
     </u:Browse>
</s:Body>
</s:Envelope>


This gets all Artists (since we are using * for a filter) and shows the first 10 results. The ObjectID can be strings like A:ALBUM (to search for albums), A:TRACKS (to search for tracks), or A:PLAYLISTS (to search playlists). The range of ObjectID values can be found from one of the other actions in the Media Server/ContentDirectory service.

Next Steps?
Create a web page that issues HTTP / SOAP requests to your devices. We have something running now, but need to work out the kinks and distill out the basics before showing it. Stay tuned. Here’s a screenshot for now of the prototype:



Update: 01/16/2011. Be sure to check out the followup post on queries here: http://travelmarx.blogspot.com/2011/01/extracting-sonos-playlist-simple-sonos.html.
Update: 06/18/2014. Review instructions. Update with new version of DeviceSpy (downloadable from new location). Try steps in Fiddler on Windows 8.

19 comments:

  1. Did you have an issue browsing the "Internet Radio" section through the Media Server? I sent the "R:" as the objectID and got an Error 720.

    ReplyDelete
  2. Using the GetPositionInfo action when a radio station is playing seems to work - for the three stations I tried. I did not get an Error 720. I used Device Spy and I also tried it out in JavaScript (see info on follow-up post to this one).

    I did notice that of the three stations I tried there was variation in different kinds of data returned. The returned XML looks like this for Radio Alto Adige (an Italian radio station):
    <DIDL-Lite>
    <item id="-1" parentID="-1" restricted="true">
    <res protocolInfo="mms:*:*:*">mms://onair2.xdevel.com/radige</res>
    <r:streamContent></r:streamContent>
    <dc:title>radige</dc:title>
    <upnp:class>object.item</upnp:class>
    </item>
    </DIDL-Lite>
    For another radio station I tried KROQ2, <r:streamContent/> had values.

    ReplyDelete
  3. Hmm, I guess I was trying to use Browse call on the Content Directory service and used "R:" as the ObjectID parameter. This "R:" comes from calling Browse with "0" as the ObjectId and then seeing that the internet radio section is found using the "R:" as the parameter. Calling Browse with this gave me the error...

    ReplyDelete
  4. Okay I see what is going on. If you are using the Browse call (invoking it) on the Content Directory use something like this for ObjectID "A:ARTIST" and set BrowseFlag. Some other values The values for ObjectID are given above. I think they are case sensitive.

    If you are playing the radio, using the Browse actions on the Content Directory is probably not interesting because the Content Directory is your collection, not radio stations. So use the GetPositionInfo command on AVTransport service.

    ReplyDelete
  5. Yea, I can see the Metadata after calling "GetPositionInfo".

    My end goal was to navigate through the radio station listings just like the Sonos iphone app does. But it appears like they don't expose that.

    ReplyDelete
  6. Ah, I see now. You are doing stuff I hadn't thought to try. Yes, when I put the R: as ObjectID I get this:

    <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/">
    <container id="R:" parentID="0" restricted="true">
    <dc:title>Internet Radio</dc:title>
    <upnp:class>object.container</upnp:class>
    </container>
    </DIDL-Lite>

    And that's all I can do. I can't see anything more about the object.container. Compare that to A: as ObjectID and you can see that inside of A: there is A:ARTIST or A:GENRE that you can look at. So I would guess you are correct, they don't expose radio listings in the same way as what's in your media library.

    ReplyDelete
  7. Did a little bit more work on this. I used NetMon on my Vista box that has the Sonos Desktop Controller on it and took a look at the traffic going back and forth. The controller, when you navigate to radio listing and search it, is making web calls back to a web service (at Sonos I think). So, using the Browse function of the Content Directory I don't think is the way to go.

    ReplyDelete
  8. TravelMarx, Hi I have been following much of what you do, but I am having trouble grouping zones, I see a schema "Group Management" and see a "AddMembers" but do not know what the string or MemberID it is looking for.

    Any help on this?

    ReplyDelete
  9. Interesting question. I tried some obvious things but nothing worked.

    I think it is more correct to use Media Renderer/AV Transport/*Coordinator actions. For example, if you use
    Media Renderer / AVTransport / BecomeCoordinatorOfStandaoneGroup of a zone that is in a group, then it pulls the zone out of the group, as expected. This worked for me in Device Spy.

    However, trying to use Media Renderer/AV Transport/ChangeCoordinator to put the zone in a group, didn't work in Device Spy. It requires the MemberID values and it isn't obvious what they are. I tried obvious things like "RINCON_XXXXXXXXXXXXXXX" values, but no luck.

    Next, I turned on "Microsoft Network Monitor" (3.4) for Windows 7 and started the Sonos Controller for Windows. In the controller, I removed a zone from a group and added it back. In the network monitor "capture" I saw the BecomeCoordinatorOfStandaloneGroup action, but never saw any action that was the "adding" back for the zone to the group. What I did see though is a HTTP NOTIFY actions which tells the Sonos controller about the group change by sending some XML about the ZoneGroupState / ZoneGroups / ZoneGroupMember. The XML is the notification for the controller to update itself/UI. So controllers subscribe to notifications and when they receive notification about ZoneGroupState, change UI. But, alas, I didn't not answer your original question on how to make the change happen: as simple as moving a zone form one group to another.

    ReplyDelete
  10. Well, after a little more time with Network Monitor I figured something out that may work. It wasn't what I thought it would be.

    In Device Spy, go to
    Zone Player
    - Media Renderer
    - - urn:upnp-org:serviceId:AVTransport
    - - - SetAVTransportURI
    [InstanceID] - leave as 0
    [CurrentURI] - x-rincon:RINCON_000XXXXXXXXXX1400
    [CurrentURIMetaData] - leave blank

    If you are moving the Zone Player to another group you must know the RINCON_XXX value of the Group Coordinator and put that in the [CurrentURI] field.

    ReplyDelete
  11. Just wanted to say thanks for all the detailed steps. Was able to cobble together an app to turn the volume down automatically when I get phone calls.

    Thanks again,
    Bill.

    ReplyDelete
  12. Wow, that sounds cool. What did you use for a programming language/platform? Is it when your cell phone rings or our land line?

    ReplyDelete
  13. Hi - I just want to say thanks for your great write up. I used your work extensively in my own project to turn an old jukebox into a SONOS controller using a Raspberry Pi. You can read the project here http://wallbox.weebly.com. Cheers Steve

    ReplyDelete
  14. I have not worked with the Denon device. What I can think of to try, based on your error messages:

    1. Try specifying the "Content-length" header (lowercase on "l") as they give in the error.

    2. Specify the date as given in the error. I did not put the Date in my examples because it wasn't needed. Perhaps it is for the Denon device. For example, "Date: Tue, 19 Aug 2014 08:12:31 GMT".

    ReplyDelete
  15. Hi TravelMarx,
    I need to build a remote control using an of the shelf Wi-Fi module. The goal is to press a button and send a command to a Sonos, such as Volume Up, via Wi-Fi. What is an easy way to do this? Can I find somewhere the binary code I need to send to the Sonos in order to send a command? I would really appreciate some help.

    ReplyDelete
    Replies
    1. Hi Lucian
      I did got this to work using a spark core an IR decoder. it decodes a sony remote to change volume,mute,pause,play and change track ( fwd,rev)

      Delete
  16. Could you please explain this part

    urn:schemas-upnp-org:service:AVTransport:1
    urn:upnp-org:serviceId:AVTransport
    /MediaRenderer/AVTransport/Control
    /MediaRenderer/AVTransport/Event
    /xml/AVTransport1.xml

    ReplyDelete
  17. Here is great little article about mspy review. See for yourself.

    ReplyDelete
  18. Thank you for this great article an your work! This helped me very much! :)

    ReplyDelete

All comments go through a moderation process. Even though it may not look like the comment was accepted, it probably was. Check back in a day if you asked a question. Thanks!