#lang scribble/manual @(require (for-label racket/base racket/contract racket/list racket-sonos racket-upnp)) @title{racket-sonos} @author{Hans Dijkema} @defmodule[racket-sonos] The @racketmodname[racket-sonos] library adds Sonos topology support to @racketmodname[racket-upnp]. It recognises Sonos and IKEA SYMFONISK devices and turns the vendor-specific zone-group topology into logical media renderers. A logical renderer can represent a single speaker, a stereo pair, a bonded home-theatre setup, or a group of rooms. The library does not perform device discovery itself. Use @racket[query-upnp-devices] from @racketmodname[racket-upnp], then pass the complete discovery result to @racket[sonos-groups]. @section{Device helpers} @defproc[(sonos-device? [device any/c]) boolean?]{ Returns a true value when @racket[device] is a UPnP device that advertises a @tt{ZoneGroupTopology} service, identifies its manufacturer as Sonos, or has a model name containing @litchar{SYMFONISK}. The textual comparisons are case-insensitive. Returns @racket[#f] for values that are not UPnP devices. } @defproc[(sonos-device-name [device sonos-device?]) string?]{ Returns the friendly room name of @racket[device]. For the common names @litchar{Living Room - Sonos One} and @litchar{Kitchen - SYMFONISK Bookshelf}, this function returns @litchar{Living Room} and @litchar{Kitchen}, respectively. Other friendly names are returned unchanged. Raises @racket[exn:fail:contract?] when @racket[device] is not recognised by @racket[sonos-device?]. } @section{Reading the Sonos topology} @defproc[(sonos-groups [devices (listof upnp-device?)]) (listof sonos-group?)]{ Finds the first @tt{ZoneGroupTopology} service among @racket[devices], invokes its @tt{GetZoneGroupState} action, and returns the logical Sonos groups in the response. Pass the complete result of @racket[(query-upnp-devices 'all)]. In particular, @racket[devices] must contain the UPnP media-renderer device for every group coordinator that should be returned. A topology group whose coordinator cannot be matched to a media renderer is omitted and a warning is logged. Returns the empty list when none of the supplied devices provides the topology service or when the service returns an empty state. Network and SOAP failures from @racket[upnp-service-call], and malformed topology XML, are reported as exceptions. } @section{Group values} @defstruct[sonos-group ([id (or/c #f string?)] [name string?] [coordinator-id (or/c #f string?)] [member-ids (listof string?)] [bonded? boolean?] [stereo? boolean?] [grouped? boolean?] [renderer media-renderer?]) #:transparent]{ Represents one logical renderer from the Sonos zone-group topology. Values returned by @racket[sonos-groups] have the following fields: @itemlist[ @item{@racket[id] is the topology's group identifier. It falls back to the coordinator identifier when the topology does not supply one.} @item{@racket[name] combines the distinct visible room names with @litchar{ + }. The suffix @litchar{ (stereo)} or @litchar{ (bonded)} describes bonded configurations.} @item{@racket[coordinator-id] is the normalised UDN of the device that coordinates playback. A leading @litchar{uuid:} and any suffix after a Sonos @litchar{RINCON_} identifier are removed.} @item{@racket[member-ids] contains the normalised identifiers of all members, including invisible bonded members.} @item{@racket[bonded?] is true for stereo pairs and other bonded arrangements, such as a soundbar with satellites.} @item{@racket[stereo?] is true when the channel map contains both the left and right front channels.} @item{@racket[grouped?] is true when more than one visible member belongs to the group. A bonded setup with only one visible room is therefore not necessarily grouped.} @item{@racket[renderer] is the coordinator's @racket[media-renderer?] value. Supply this value to the playback operations provided by @racketmodname[racket-upnp].} ] } @section{Example} This example discovers all UPnP devices, reads the Sonos topology, and prints the logical renderers: @racketblock[ (require racket-sonos racket-upnp) (define devices (query-upnp-devices 'all)) (for ([group (in-list (sonos-groups devices))]) (printf "~a: ~a member(s)~a~n" (sonos-group-name group) (length (sonos-group-member-ids group)) (if (sonos-group-grouped? group) " (grouped)" ""))) ] To control a group, use its coordinator renderer. For example: @racketblock[ (define groups (sonos-groups devices)) (unless (null? groups) (media-renderer-play-uri! (sonos-group-renderer (first groups)) "http://192.0.2.10:8080/audio/example.mp3")) ] @index-section[]