diff --git a/info.rkt b/info.rkt index 35fd700..c8f399d 100644 --- a/info.rkt +++ b/info.rkt @@ -12,4 +12,9 @@ "simple-log" )) (define build-deps - '("rackunit-lib")) + '("racket-doc" + "rackunit-lib" + "scribble-lib")) + +(define scribblings + '(("scribblings/racket-sonos.scrbl" () (library)))) diff --git a/scribblings/racket-sonos.scrbl b/scribblings/racket-sonos.scrbl new file mode 100644 index 0000000..b45f06f --- /dev/null +++ b/scribblings/racket-sonos.scrbl @@ -0,0 +1,143 @@ +#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[]