Compare commits

...

2 Commits

Author SHA1 Message Date
hans 81cde742cc Merge branch 'main' of https://git.dijkewijk.nl/hans/racket-sonos 2026-08-29 21:40:17 +02:00
hans 1ad75d20c4 Added scribbles 2026-08-29 21:40:04 +02:00
2 changed files with 149 additions and 1 deletions
+6 -1
View File
@@ -12,4 +12,9 @@
"simple-log" "simple-log"
)) ))
(define build-deps (define build-deps
'("rackunit-lib")) '("racket-doc"
"rackunit-lib"
"scribble-lib"))
(define scribblings
'(("scribblings/racket-sonos.scrbl" () (library))))
+143
View File
@@ -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[]