├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── TODO ├── browse.go ├── config ├── config.go └── config_test.go ├── coverage.go ├── cscl └── cscl.go ├── csweb └── csweb.go ├── didl └── didl.go ├── examples ├── browse │ └── browse.go ├── composers │ └── composers.go ├── devices │ └── devices.go ├── discovery │ └── discovery.go └── googletv │ └── googletv.go ├── linn-co-uk └── Playlist.go ├── model ├── message.go └── model.go ├── reciva-com ├── RecivaRadio.go └── RecivaSimpleRemote.go ├── reciva.go ├── sonos.go ├── sonos_test.go ├── ssdp └── ssdp.go └── upnp ├── AVTransport.go ├── AlarmClock.go ├── ConnectionManager.go ├── ContentDirectory.go ├── DeviceProperties.go ├── GroupManagement.go ├── MusicServices.go ├── RenderingControl.go ├── SystemProperties.go ├── ZoneGroupTopology.go ├── device.go ├── event.go ├── service.go ├── soap.go └── upnp.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | old 25 | *.swp 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | go-sonos 2 | ======== 3 | 4 | Copyright (c) 2012, Ian T. Richards 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions 9 | are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, 12 | this list of conditions and the following disclaimer. 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 23 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 24 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # go-sonos 3 | # ======== 4 | # 5 | # Copyright (c) 2012, Ian T. Richards 6 | # All rights reserved. 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions 10 | # are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, 13 | # this list of conditions and the following disclaimer. 14 | # * Redistributions in binary form must reproduce the above copyright 15 | # notice, this list of conditions and the following disclaimer in the 16 | # documentation and/or other materials provided with the distribution. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | # 30 | 31 | GO = go 32 | PACKAGE = github.com/ianr0bkny/go-sonos 33 | GOOS = $(shell go env GOOS) 34 | GOARCH = $(shell go env GOARCH) 35 | 36 | PACKAGE_LIST = \ 37 | $(PACKAGE) \ 38 | $(PACKAGE)/config \ 39 | $(PACKAGE)/cscl \ 40 | $(PACKAGE)/csweb \ 41 | $(PACKAGE)/didl \ 42 | $(PACKAGE)/examples/browse \ 43 | $(PACKAGE)/examples/composers \ 44 | $(PACKAGE)/examples/devices \ 45 | $(PACKAGE)/examples/discovery \ 46 | $(PACKAGE)/examples/googletv \ 47 | $(PACKAGE)/linn-co-uk \ 48 | $(PACKAGE)/model \ 49 | $(PACKAGE)/reciva-com \ 50 | $(PACKAGE)/ssdp \ 51 | $(PACKAGE)/upnp 52 | 53 | all :: 54 | $(GO) install -v $(PACKAGE_LIST) 55 | 56 | clean :: 57 | $(GO) clean -i -x $(PACKAGE_LIST) 58 | rm -rf $(GOPATH)/pkg/$(GOOS)_$(GOARCH)/$(PACKAGE) 59 | 60 | wc :: 61 | wc -l *.go */*.go examples/*/*.go 62 | 63 | longlines :: 64 | egrep '.{120,}' *.go */*.go examples/*/*.go 65 | 66 | coverage :: 67 | $(GO) test -test.run Coverage 68 | 69 | discovery :: 70 | $(GO) test -test.run Discovery 71 | 72 | fmt :: 73 | $(GO) fmt -x $(PACKAGE_LIST) 74 | 75 | vet :: 76 | $(GO) vet -x $(PACKAGE_LIST) 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-sonos 2 | ======== 3 | 4 | A Go-language library for accessing UPnP AV devices 5 | 6 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | go-sonos 2 | ======== 3 | 4 | 1. Service descriptions should be used to validate SOAP method calls 5 | 2. Should have a version of upnp.Call that uses reflection to extract arguments from a struct 6 | 3. TestDicovery cannot be run alongside other tests, since it will try to make duplicate Handle() requests in net/http 7 | 4. sonos.MakeSonos should take an argument to indicate which services should be described 8 | 9 | -------------------------------------------------------------------------------- /browse.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package sonos 32 | 33 | import ( 34 | "github.com/ianr0bkny/go-sonos/model" 35 | "github.com/ianr0bkny/go-sonos/upnp" 36 | "log" 37 | "strings" 38 | ) 39 | 40 | const ( 41 | ObjectID_Attributes = "A:" 42 | ObjectID_MusicShares = "S:" 43 | ObjectID_Queues = "Q:" 44 | ObjectID_SavedQueues = "SQ:" 45 | ObjectID_InternetRadio = "R:" 46 | ObjectID_EntireNetwork = "EN:" 47 | // 48 | ObjectID_Queue_AVT_Instance_0 = "Q:0" 49 | // 50 | ObjectID_Attribute_Genres = "A:GENRE" 51 | ObjectID_Attribute_Album = "A:ALBUM" 52 | ObjectID_Attribute_Artist = "A:ARTIST" 53 | ObjectID_Attribute_Composers = "A:COMPOSER" 54 | ) 55 | 56 | func (this *Sonos) GetRootLevelChildren() (objects []model.Object, err error) { 57 | var result *upnp.BrowseResult 58 | req := &upnp.BrowseRequest{ 59 | ObjectID: upnp.BrowseObjectID_Root, 60 | BrowseFlag: upnp.BrowseFlag_BrowseDirectChildren, 61 | Filter: upnp.BrowseFilter_All, 62 | StartingIndex: 0, 63 | RequestCount: 0, 64 | SortCriteria: upnp.BrowseSortCriteria_None, 65 | } 66 | if result, err = this.Browse(req); nil != err { 67 | return 68 | } else { 69 | objects = model.ObjectStream(result.Doc) 70 | } 71 | return 72 | } 73 | 74 | func (this *Sonos) ListQueues() (objects []model.Object, err error) { 75 | var result *upnp.BrowseResult 76 | req := &upnp.BrowseRequest{ 77 | ObjectID: ObjectID_Queues, 78 | BrowseFlag: upnp.BrowseFlag_BrowseDirectChildren, 79 | Filter: upnp.BrowseFilter_All, 80 | StartingIndex: 0, 81 | RequestCount: 0, 82 | SortCriteria: upnp.BrowseSortCriteria_None, 83 | } 84 | if result, err = this.Browse(req); nil != err { 85 | return 86 | } else { 87 | objects = model.ObjectStream(result.Doc) 88 | } 89 | return 90 | } 91 | 92 | func (this *Sonos) ListSavedQueues() (objects []model.Object, err error) { 93 | var result *upnp.BrowseResult 94 | req := &upnp.BrowseRequest{ 95 | ObjectID: ObjectID_SavedQueues, 96 | BrowseFlag: upnp.BrowseFlag_BrowseDirectChildren, 97 | Filter: upnp.BrowseFilter_All, 98 | StartingIndex: 0, 99 | RequestCount: 0, 100 | SortCriteria: upnp.BrowseSortCriteria_None, 101 | } 102 | if result, err = this.Browse(req); nil != err { 103 | return 104 | } else { 105 | objects = model.ObjectStream(result.Doc) 106 | } 107 | return 108 | } 109 | 110 | func (this *Sonos) ListInternetRadio() (objects []model.Object, err error) { 111 | var result *upnp.BrowseResult 112 | req := &upnp.BrowseRequest{ 113 | ObjectID: ObjectID_InternetRadio, 114 | BrowseFlag: upnp.BrowseFlag_BrowseDirectChildren, 115 | Filter: upnp.BrowseFilter_All, 116 | StartingIndex: 0, 117 | RequestCount: 0, 118 | SortCriteria: upnp.BrowseSortCriteria_None, 119 | } 120 | if result, err = this.Browse(req); nil != err { 121 | return 122 | } else { 123 | objects = model.ObjectStream(result.Doc) 124 | } 125 | return 126 | } 127 | 128 | func (this *Sonos) ListAttributes() (objects []model.Object, err error) { 129 | var result *upnp.BrowseResult 130 | req := &upnp.BrowseRequest{ 131 | ObjectID: ObjectID_Attributes, 132 | BrowseFlag: upnp.BrowseFlag_BrowseDirectChildren, 133 | Filter: upnp.BrowseFilter_All, 134 | StartingIndex: 0, 135 | RequestCount: 0, 136 | SortCriteria: upnp.BrowseSortCriteria_None, 137 | } 138 | if result, err = this.Browse(req); nil != err { 139 | return 140 | } else { 141 | objects = model.ObjectStream(result.Doc) 142 | } 143 | return 144 | } 145 | 146 | func (this *Sonos) ListMusicShares() (objects []model.Object, err error) { 147 | var result *upnp.BrowseResult 148 | req := &upnp.BrowseRequest{ 149 | ObjectID: ObjectID_MusicShares, 150 | BrowseFlag: upnp.BrowseFlag_BrowseDirectChildren, 151 | Filter: upnp.BrowseFilter_All, 152 | StartingIndex: 0, 153 | RequestCount: 0, 154 | SortCriteria: upnp.BrowseSortCriteria_None, 155 | } 156 | if result, err = this.Browse(req); nil != err { 157 | return 158 | } else { 159 | objects = model.ObjectStream(result.Doc) 160 | } 161 | return 162 | } 163 | 164 | func (this *Sonos) GetAllGenres() (objects []model.Object, err error) { 165 | var result *upnp.BrowseResult 166 | req := &upnp.BrowseRequest{ 167 | ObjectID: ObjectID_Attribute_Genres, 168 | BrowseFlag: upnp.BrowseFlag_BrowseDirectChildren, 169 | Filter: upnp.BrowseFilter_All, 170 | StartingIndex: 0, 171 | RequestCount: 0, 172 | SortCriteria: upnp.BrowseSortCriteria_None, 173 | } 174 | if result, err = this.Browse(req); nil != err { 175 | return 176 | } else { 177 | objects = model.ObjectStream(result.Doc) 178 | } 179 | return 180 | } 181 | 182 | func (this *Sonos) GetAllComposers() (objects []model.Object, err error) { 183 | var result *upnp.BrowseResult 184 | req := &upnp.BrowseRequest{ 185 | ObjectID: ObjectID_Attribute_Composers, 186 | BrowseFlag: upnp.BrowseFlag_BrowseDirectChildren, 187 | Filter: upnp.BrowseFilter_All, 188 | StartingIndex: 0, 189 | RequestCount: 0, 190 | SortCriteria: upnp.BrowseSortCriteria_None, 191 | } 192 | if result, err = this.Browse(req); nil != err { 193 | return 194 | } else { 195 | objects = model.ObjectStream(result.Doc) 196 | } 197 | return 198 | } 199 | 200 | func objectIDForGenre(genre string) string { 201 | return strings.Join([]string{ObjectID_Attribute_Genres, genre}, "/") 202 | } 203 | 204 | func objectIDForAlbum(album string) string { 205 | return strings.Join([]string{ObjectID_Attribute_Album, album}, "/") 206 | } 207 | 208 | func objectIDForArtist(artist string) string { 209 | return strings.Join([]string{ObjectID_Attribute_Artist, artist}, "/") 210 | } 211 | 212 | func (this *Sonos) GetGenreArtists(genre string) ([]model.Object, error) { 213 | req := &upnp.BrowseRequest{ 214 | ObjectID: objectIDForGenre(genre), 215 | BrowseFlag: upnp.BrowseFlag_BrowseDirectChildren, 216 | Filter: upnp.BrowseFilter_All, 217 | StartingIndex: 0, 218 | RequestCount: 0, 219 | SortCriteria: upnp.BrowseSortCriteria_None, 220 | } 221 | if result, err := this.Browse(req); nil != err { 222 | log.Printf("Could not browse artists for genre `%s': %v", genre, err) 223 | return nil, err 224 | } else { 225 | return model.ObjectStream(result.Doc), nil 226 | } 227 | } 228 | 229 | func (this *Sonos) ListChildren(objectId string) (objects []model.Object, err error) { 230 | var result *upnp.BrowseResult 231 | req := &upnp.BrowseRequest{ 232 | ObjectID: objectId, 233 | BrowseFlag: upnp.BrowseFlag_BrowseDirectChildren, 234 | Filter: upnp.BrowseFilter_All, 235 | StartingIndex: 0, 236 | RequestCount: 0, 237 | SortCriteria: upnp.BrowseSortCriteria_None, 238 | } 239 | if result, err = this.Browse(req); nil != err { 240 | return 241 | } else { 242 | objects = model.ObjectStream(result.Doc) 243 | } 244 | return 245 | } 246 | 247 | func (this *Sonos) GetMetadata(objectId string) (objects []model.Object, err error) { 248 | var result *upnp.BrowseResult 249 | req := &upnp.BrowseRequest{ 250 | ObjectID: objectId, 251 | BrowseFlag: upnp.BrowseFlag_BrowseMetadata, 252 | Filter: upnp.BrowseFilter_All, 253 | StartingIndex: 0, 254 | RequestCount: 0, 255 | SortCriteria: upnp.BrowseSortCriteria_None, 256 | } 257 | if result, err = this.Browse(req); nil != err { 258 | return 259 | } else { 260 | objects = model.ObjectStream(result.Doc) 261 | } 262 | return 263 | } 264 | 265 | func (this *Sonos) GetDirectChildren(objectId string) (objects []model.Object, err error) { 266 | var result *upnp.BrowseResult 267 | req := &upnp.BrowseRequest{ 268 | ObjectID: objectId, 269 | BrowseFlag: upnp.BrowseFlag_BrowseDirectChildren, 270 | Filter: upnp.BrowseFilter_All, 271 | StartingIndex: 0, 272 | RequestCount: 0, 273 | SortCriteria: upnp.BrowseSortCriteria_None, 274 | } 275 | if result, err = this.Browse(req); nil != err { 276 | return 277 | } else { 278 | objects = model.ObjectStream(result.Doc) 279 | } 280 | return 281 | } 282 | 283 | func (this *Sonos) GetQueueContents() (objects []model.Object, err error) { 284 | var result *upnp.BrowseResult 285 | req := &upnp.BrowseRequest{ 286 | ObjectID: ObjectID_Queue_AVT_Instance_0, 287 | BrowseFlag: upnp.BrowseFlag_BrowseDirectChildren, 288 | Filter: upnp.BrowseFilter_All, 289 | StartingIndex: 0, 290 | RequestCount: 0, 291 | SortCriteria: upnp.BrowseSortCriteria_None, 292 | } 293 | if result, err = this.Browse(req); nil != err { 294 | return 295 | } else { 296 | objects = model.ObjectStream(result.Doc) 297 | } 298 | return 299 | } 300 | 301 | func (this *Sonos) GetAlbumTracks(album string) ([]model.Object, error) { 302 | req := &upnp.BrowseRequest{ 303 | ObjectID: objectIDForAlbum(album), 304 | BrowseFlag: upnp.BrowseFlag_BrowseDirectChildren, 305 | Filter: upnp.BrowseFilter_All, 306 | StartingIndex: 0, 307 | RequestCount: 0, 308 | SortCriteria: upnp.BrowseSortCriteria_None, 309 | } 310 | log.Printf("Browsing tracks for album `%s'", album) 311 | if result, err := this.Browse(req); nil != err { 312 | log.Printf("Could not browse tracks for album `%s': %v", album, err) 313 | return nil, err 314 | } else { 315 | return model.ObjectStream(result.Doc), nil 316 | } 317 | } 318 | 319 | func (this *Sonos) GetTrackFromAlbum(album, track string) ([]model.Object, error) { 320 | if tracks, err := this.GetAlbumTracks(album); nil != err { 321 | return nil, err 322 | } else { 323 | var track_objs []model.Object 324 | for _, track_obj := range tracks { 325 | if track_obj.Title() == track { 326 | track_objs = append(track_objs, track_obj) 327 | } 328 | } 329 | return track_objs, nil 330 | } 331 | } 332 | 333 | func (this *Sonos) GetArtistAlbums(artist string) (objects []model.Object, err error) { 334 | var result *upnp.BrowseResult 335 | req := &upnp.BrowseRequest{ 336 | ObjectID: objectIDForArtist(artist), 337 | BrowseFlag: upnp.BrowseFlag_BrowseDirectChildren, 338 | Filter: upnp.BrowseFilter_All, 339 | StartingIndex: 0, 340 | RequestCount: 0, 341 | SortCriteria: upnp.BrowseSortCriteria_None, 342 | } 343 | if result, err = this.Browse(req); nil != err { 344 | return 345 | } else { 346 | objects = model.ObjectStream(result.Doc) 347 | } 348 | return 349 | } 350 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | // 32 | // A module to support bookmarking discovered UPnP devices. 33 | // 34 | // This module is intended to solve the problem of addressing commands to 35 | // a specific UPnP device, where each device is known by a user-unfriendly 36 | // UUID, where network addresses are subject to change, ala DHCP, and where 37 | // it is not desirable to repeat device discovery each time a command is 38 | // to be sent. 39 | // 40 | // Since device discovery is slow, it should only be run once in a while 41 | // when DHCP leases would have expired. The discovery process writes the 42 | // list of discovered devices to the list of bookmarks. 43 | // 44 | // Once, when the device is installed, an association is added to the 45 | // list of bookmarks, making a memorable alias, i.e. 'kitchen', point to 46 | // a particular UUID. 47 | // 48 | // Now whenever network addresses change disovery can be rerun, with 49 | // aliases automatically pointing to the new address associated with the 50 | // static UUID. 51 | // 52 | // Look at sonos_test.go for examples of how this class is used. 53 | // 54 | package config 55 | 56 | import ( 57 | "encoding/json" 58 | "github.com/ianr0bkny/go-sonos/ssdp" 59 | "io" 60 | "log" 61 | "os" 62 | "path" 63 | ) 64 | 65 | // 66 | // A container for the runtime configuration used by go-sonos application. 67 | // 68 | type Config struct { 69 | // The path to the configuration directory 70 | dirname string 71 | // A handle to the configuration directory 72 | dir *os.File 73 | // A set of discovered devices 74 | Bookmarks Bookmarks 75 | } 76 | 77 | // 78 | // A strucutre that holds all of the fields required to build a UPnP 79 | // device without first trying to discover it. 80 | // 81 | type Bookmark struct { 82 | // A memorable string standing in for a UUID 83 | Alias string `json:"alias,omitempty"` 84 | // The name of the device's product, e.g. 'Sonos' 85 | Product string `json:"product,omitempty"` 86 | // The product version (e.g. "28.1-83040 (BR100)") 87 | ProductVersion string `json:"productVersion,omitempty"` 88 | // The last know location of the device 89 | Location ssdp.Location `json:"location,omitempty"` 90 | // The device's UUID 91 | UUID ssdp.UUID `json:"uuid"` 92 | } 93 | 94 | // 95 | // A map holding a set of bookmarks, where the key is alternately the 96 | // UUID of the device, or an alias to a device. 97 | // 98 | type Bookmarks map[string]Bookmark 99 | 100 | type configDevice struct { 101 | product string 102 | productVersion string 103 | location ssdp.Location 104 | uuid ssdp.UUID 105 | } 106 | 107 | func (this *configDevice) Product() string { 108 | return this.product 109 | } 110 | 111 | func (this *configDevice) ProductVersion() string { 112 | return this.productVersion 113 | } 114 | 115 | func (this *configDevice) Name() string { 116 | panic("Not implemented") 117 | } 118 | 119 | func (this *configDevice) Location() ssdp.Location { 120 | return this.location 121 | } 122 | 123 | func (this *configDevice) UUID() ssdp.UUID { 124 | return this.uuid 125 | } 126 | 127 | func (this *configDevice) Service(key ssdp.ServiceKey) (service ssdp.Service, has bool) { 128 | return 129 | } 130 | 131 | func (this *configDevice) Services() (keys []ssdp.ServiceKey) { 132 | return 133 | } 134 | 135 | // 136 | // Create a configuration object where @dir is the path to the 137 | // configuration directory. Note that Init() must be called in order to 138 | // use the newly created object. 139 | // 140 | func MakeConfig(dir string) *Config { 141 | return &Config{dir, nil, Bookmarks{}} 142 | } 143 | 144 | // 145 | // Initialize the configuration object by loading any existing 146 | // configuration from disk. This method creates the configuration directory, 147 | // if needed. 148 | // 149 | func (this *Config) Init() { 150 | var err error 151 | if this.dir, err = os.Open(this.dirname); nil != err { 152 | if err = os.Mkdir(this.dirname, 0755); nil != err { 153 | log.Printf("Config: %s", err.(*os.PathError).Error()) 154 | return 155 | } else if this.dir, err = os.Open(this.dirname); nil != err { 156 | log.Printf("Config: %s", err.(*os.PathError).Error()) 157 | return 158 | } 159 | } 160 | if fi, err := this.dir.Stat(); nil != err { 161 | log.Printf("Config: %s", err.(*os.PathError).Error()) 162 | return 163 | } else if !fi.IsDir() { 164 | log.Printf("Config: %s: Not a directory", this.dirname) 165 | return 166 | } 167 | this.loadFromDisk() 168 | } 169 | 170 | // 171 | // Write the current configuration to disk. 172 | // 173 | func (this *Config) Save() { 174 | if nil == this.dir { 175 | return 176 | } 177 | this.saveBookmarks() 178 | } 179 | 180 | func (this *Config) saveBookmarks() { 181 | path := path.Join(this.dirname, "bookmarks") 182 | if fd, err := os.Create(path); nil != err { 183 | panic(err) 184 | } else if err := json.NewEncoder(fd).Encode(this.Bookmarks); err != nil { 185 | if io.EOF != err { 186 | panic(err) 187 | } 188 | fd.Close() 189 | } 190 | } 191 | 192 | // 193 | // Add a bookmark to the bookmark set where @ident is either the uuid 194 | // or the alias to add; @product is the product string, such as 'Sonos'; 195 | // @localtion is the network location of the resource; and @uuid is the 196 | // device's UUID. When adding a device @ident and @uuid should be the same; 197 | // when adding an alias @ident and @uuid will be different. 198 | // 199 | func (this *Config) AddBookmark(ident, product, productVersion string, location ssdp.Location, uuid ssdp.UUID) { 200 | if ident != string(uuid) { 201 | this.Bookmarks[ident] = Bookmark{ident, product, productVersion, location, uuid} 202 | } else { 203 | this.Bookmarks[ident] = Bookmark{"", product, productVersion, location, uuid} 204 | } 205 | } 206 | 207 | // 208 | // Add @alias as an alias for @uuid. 209 | // 210 | func (this *Config) AddAlias(uuid ssdp.UUID, alias string) { 211 | old := this.Bookmarks[string(uuid)] 212 | this.AddBookmark(alias, "", "", ssdp.Location(""), old.UUID) 213 | } 214 | 215 | // 216 | // Remove all aliases from the set of bookmarks. 217 | // 218 | func (this *Config) ClearAliases() { 219 | for key, rec := range this.Bookmarks { 220 | if 0 < len(rec.Alias) { 221 | delete(this.Bookmarks, key) 222 | } 223 | } 224 | } 225 | 226 | // 227 | // Remove any association of a device to the alias @alias. If @alias 228 | // is a UUID this method is a noop. 229 | // 230 | func (this *Config) ClearAlias(alias string) { 231 | if rec, has := this.Bookmarks[alias]; has { 232 | if 0 < len(rec.Alias) { 233 | delete(this.Bookmarks, alias) 234 | } 235 | } 236 | } 237 | 238 | func (this *Config) loadFromDisk() { 239 | for { 240 | if files, err := this.dir.Readdir(16); nil != err { 241 | if io.EOF != err { 242 | panic(err) 243 | } else { 244 | break 245 | } 246 | } else { 247 | for _, file := range files { 248 | this.maybeLoadFile(file) 249 | } 250 | } 251 | } 252 | } 253 | 254 | func (this *Config) maybeLoadFile(f os.FileInfo) { 255 | if "bookmarks" == f.Name() { 256 | this.maybeLoadBookmarks(f) 257 | } 258 | } 259 | 260 | func (this *Config) maybeLoadBookmarks(f os.FileInfo) { 261 | if f.IsDir() { 262 | log.Printf("%s/%s: Expected regular file", this.dirname, f.Name()) 263 | return 264 | } else { 265 | path := path.Join(this.dirname, f.Name()) 266 | if fd, err := os.Open(path); nil != err { 267 | panic(err) 268 | } else if err := json.NewDecoder(fd).Decode(&this.Bookmarks); err != nil { 269 | if io.EOF != err { 270 | panic(err) 271 | } 272 | fd.Close() 273 | } 274 | } 275 | } 276 | 277 | func (this *Config) lookupImpl(ident string, history map[string]bool) (dev ssdp.Device) { 278 | if _, has := history[ident]; !has { 279 | history[ident] = true 280 | if bookmark, has := this.Bookmarks[ident]; has { 281 | if 0 < len(bookmark.Alias) { 282 | dev = this.lookupImpl(string(bookmark.UUID), history) 283 | } else { 284 | dev = &configDevice{bookmark.Product, bookmark.ProductVersion, bookmark.Location, bookmark.UUID} 285 | } 286 | } 287 | } 288 | return 289 | } 290 | 291 | // 292 | // Try to find the device associated with the UUID or alias 293 | // @ident. Returns nil if there is no device associated with @ident. 294 | // 295 | func (this *Config) Lookup(ident string) ssdp.Device { 296 | return this.lookupImpl(ident, map[string]bool{}) 297 | } 298 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package config_test 32 | 33 | import ( 34 | "github.com/ianr0bkny/go-sonos" 35 | "github.com/ianr0bkny/go-sonos/config" 36 | "log" 37 | "os" 38 | "testing" 39 | ) 40 | 41 | const ( 42 | alias = "kitchen" 43 | configdir = "dot_go-sonos" 44 | location = "http://192.168.1.44:1400/xml/device_description.xml" 45 | uuid = "RINCON_000E58741A8401400" 46 | ) 47 | 48 | func TestConfig(t *testing.T) { 49 | log.SetFlags(log.Ltime | log.Lshortfile) 50 | 51 | if err := os.RemoveAll(configdir); nil != err { 52 | panic(err) 53 | } 54 | 55 | c := config.MakeConfig(configdir) 56 | c.Init() 57 | c.AddBookmark(uuid, sonos.SONOS, location, uuid) 58 | c.AddAlias(uuid, alias) 59 | c.Save() 60 | c = nil 61 | 62 | d := config.MakeConfig(configdir) 63 | d.Init() 64 | 65 | bookmark := d.Bookmarks[uuid] 66 | if location != bookmark.Location { 67 | panic("failed") 68 | } 69 | 70 | if dev := d.Lookup(alias); nil == dev { 71 | panic("failed") 72 | } 73 | 74 | os.RemoveAll(configdir) 75 | } 76 | -------------------------------------------------------------------------------- /coverage.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package sonos 32 | 33 | import ( 34 | "fmt" 35 | "github.com/ianr0bkny/go-sonos/upnp" 36 | "log" 37 | "reflect" 38 | ) 39 | 40 | type coverageData struct { 41 | total int 42 | missing []string 43 | implemented int 44 | } 45 | 46 | func (this *coverageData) pct() float32 { 47 | return 100. * float32(this.implemented) / float32(this.total) 48 | } 49 | 50 | func (this *coverageData) add(other *coverageData) { 51 | this.total += other.total 52 | this.missing = append(this.missing, other.missing...) 53 | this.implemented += other.implemented 54 | } 55 | 56 | func (this *coverageData) log(name string, missing bool) { 57 | log.Printf("%20s %8.2f%% %3d/%-3d", name, this.pct(), this.implemented, this.total) 58 | if missing { 59 | for _, action := range this.missing { 60 | log.Printf("%20s * %s", "", action) 61 | } 62 | } 63 | } 64 | 65 | func Coverage(s interface{}) { 66 | sv := reflect.Indirect(reflect.ValueOf(s)) 67 | st := sv.Type() 68 | total_cd := coverageData{} 69 | for i := 0; i < st.NumField(); i++ { 70 | superclass := sv.Field(i) 71 | svc := superclass.FieldByName("Svc").Interface().(*upnp.Service) 72 | if nil == svc { 73 | cd := coverageData{} 74 | cd.log(fmt.Sprintf("Service %s not implemented", superclass.Type().Name()), true) 75 | total_cd.add(&cd) 76 | continue 77 | } 78 | actions := svc.Actions() 79 | superclass_type := reflect.PtrTo(superclass.Type()) 80 | cd := coverageData{total: len(actions)} 81 | for _, action := range actions { 82 | if _, has := superclass_type.MethodByName(action); has { 83 | cd.implemented++ 84 | } else { 85 | cd.missing = append(cd.missing, action) 86 | } 87 | } 88 | cd.log(superclass.Type().Name(), true) 89 | total_cd.add(&cd) 90 | } 91 | total_cd.log("TOTAL", false) 92 | } 93 | -------------------------------------------------------------------------------- /cscl/cscl.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | // 32 | // A client to demonstrate controlling Sonos from the command line. 33 | // 34 | // cscl := (c)ontrol (s)onos from the (c)ommand (l)ine 35 | // 36 | package main 37 | 38 | import ( 39 | "errors" 40 | "flag" 41 | "fmt" 42 | "github.com/ianr0bkny/go-sonos" 43 | "github.com/ianr0bkny/go-sonos/config" 44 | "github.com/ianr0bkny/go-sonos/ssdp" 45 | "log" 46 | "os" 47 | "path" 48 | ) 49 | 50 | var CONFIG *config.Config 51 | 52 | func initConfig(dir string) { 53 | if "" == dir { 54 | dir = path.Join(os.Getenv("HOME"), ".go-sonos") 55 | } 56 | CONFIG = config.MakeConfig(dir) 57 | CONFIG.Init() 58 | } 59 | 60 | func cleanup() { 61 | CONFIG.Save() 62 | } 63 | 64 | func alias(flags *Args, args []string) (err error) { 65 | switch len(args) { 66 | case 0: 67 | for key, rec := range CONFIG.Bookmarks { 68 | if 0 < len(rec.Alias) { 69 | fmt.Printf("%s is an alias for %s\n", key, rec.UUID) 70 | } 71 | } 72 | case 1: 73 | key := args[0] 74 | if rec, has := CONFIG.Bookmarks[key]; has { 75 | if 0 < len(rec.Alias) { 76 | fmt.Printf("%s is an alias for %s\n", key, rec.UUID) 77 | } else { 78 | fmt.Printf("%s is not an alias\n", key) 79 | } 80 | } else { 81 | fmt.Printf("%s is not an alias\n", key) 82 | } 83 | case 2: 84 | CONFIG.AddAlias(ssdp.UUID(args[0]), args[1]) 85 | default: 86 | err = errors.New("usage: alias [alias | {uuid alias}]") 87 | } 88 | return 89 | } 90 | 91 | func discover(flags *Args) { 92 | port := fmt.Sprintf("%d", *flags.discoveryPort) 93 | if mgr, err := sonos.Discover(*flags.discoveryDevice, port); nil != err { 94 | panic(err) 95 | } else { 96 | query := ssdp.ServiceQueryTerms{ 97 | ssdp.ServiceKey(sonos.MUSIC_SERVICES): -1, 98 | ssdp.ServiceKey(sonos.RECIVA_RADIO): -1, 99 | } 100 | res := mgr.QueryServices(query) 101 | if dev_list, has := res[sonos.MUSIC_SERVICES]; has { 102 | for _, dev := range dev_list { 103 | if sonos.SONOS == dev.Product() { 104 | fmt.Printf("%s %s\n", string(dev.UUID()), dev.Location()) 105 | CONFIG.AddBookmark(string(dev.UUID()), dev.Product(), dev.ProductVersion(), dev.Location(), dev.UUID()) 106 | } 107 | } 108 | } 109 | if dev_list, has := res[sonos.RECIVA_RADIO]; has { 110 | for _, dev := range dev_list { 111 | if sonos.RADIO == dev.Product() { 112 | fmt.Printf("%s %s\n", string(dev.UUID()), dev.Location()) 113 | CONFIG.AddBookmark(string(dev.UUID()), dev.Product(), dev.ProductVersion(), dev.Location(), dev.UUID()) 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | func devices(flags *Args, args []string) (err error) { 121 | port := fmt.Sprintf("%d", *flags.discoveryPort) 122 | if mgr, err := sonos.Discover(*flags.discoveryDevice, port); nil != err { 123 | panic(err) 124 | } else { 125 | dm := mgr.Devices() 126 | for uuid, dev := range dm { 127 | fmt.Printf("%s {\n\tProduct = %s\n\tName = %s\n\tLocation = %s\n}\n", 128 | uuid, dev.Product(), dev.Name(), dev.Location()) 129 | } 130 | } 131 | return 132 | } 133 | 134 | func queue(flags *Args, args []string) (err error) { 135 | if 1 != len(args) { 136 | log.Fatal("usage: cscl queue alias") 137 | } 138 | if dev := CONFIG.Lookup(args[0]); nil != dev { 139 | s := sonos.Connect(dev, nil, sonos.SVC_CONTENT_DIRECTORY) 140 | if q, err := s.GetQueueContents(); nil != err { 141 | log.Fatalf("GetQueueContents: %#v", err) 142 | } else { 143 | for _, track := range q { 144 | log.Printf("%s\n", track.Title()) 145 | } 146 | } 147 | } else { 148 | log.Fatal("unknown device") 149 | } 150 | return 151 | } 152 | 153 | func unalias(flags *Args, args []string) (err error) { 154 | switch len(args) { 155 | case 0: 156 | CONFIG.ClearAliases() 157 | case 1: 158 | CONFIG.ClearAlias(args[0]) 159 | default: 160 | err = errors.New("usage: unalias [alias]") 161 | } 162 | return 163 | } 164 | 165 | type Args struct { 166 | alias *string 167 | help, usage *bool 168 | configDir *string 169 | discoveryDevice *string 170 | discoveryPort *int 171 | } 172 | 173 | func Usage() { 174 | fmt.Fprintf(os.Stderr, "usage: cscl [-S ] [-C ] [-D ]\n") 175 | fmt.Fprintf(os.Stderr, " [-P ]\n") 176 | fmt.Fprintf(os.Stderr, " [--help|--usage]\n") 177 | fmt.Fprintf(os.Stderr, " [args ...]\n\n") 178 | fmt.Fprintf(os.Stderr, "The available commands are:\n") 179 | fmt.Fprintf(os.Stderr, " alias Add an alias binding\n") 180 | fmt.Fprintf(os.Stderr, " devices Report devices found during discovery\n") 181 | fmt.Fprintf(os.Stderr, " discover Start SSDP device discovery\n") 182 | fmt.Fprintf(os.Stderr, " unalias Remove an alias binding\n") 183 | fmt.Fprintf(os.Stderr, "\n") 184 | } 185 | 186 | func main() { 187 | log.SetFlags(log.Ltime | log.Lshortfile) 188 | 189 | args := Args{} 190 | args.alias = flag.String("S", "", "device alias name") 191 | args.configDir = flag.String("C", "", "go-sonos configuration directory") 192 | args.discoveryDevice = flag.String("D", "eth0", "discovery device") 193 | args.discoveryPort = flag.Int("P", 13104, "discovery response port") 194 | args.help = flag.Bool("help", false, "show the usage message") 195 | args.usage = flag.Bool("usage", false, "show the usage message") 196 | flag.Usage = Usage 197 | flag.Parse() 198 | 199 | if 0 == len(flag.Args()) { 200 | flag.Usage() 201 | os.Exit(0) 202 | } 203 | 204 | initConfig(*args.configDir) 205 | 206 | for _, cmd := range flag.Args() { 207 | switch cmd { 208 | case "alias": 209 | alias(&args, flag.Args()[1:]) 210 | case "devices": 211 | devices(&args, flag.Args()[1:]) 212 | case "discover": 213 | discover(&args) 214 | case "queue": 215 | queue(&args, flag.Args()[1:]) 216 | case "unalias": 217 | unalias(&args, flag.Args()[1:]) 218 | default: 219 | flag.Usage() 220 | } 221 | break 222 | } 223 | 224 | cleanup() 225 | } 226 | -------------------------------------------------------------------------------- /csweb/csweb.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | // 32 | // A server to demonstrate controlling Sonos from a web browser. 33 | // 34 | // csweb := (c)ontrol (s)onos from a (web) browser 35 | // 36 | package main 37 | 38 | import ( 39 | "encoding/json" 40 | "errors" 41 | "fmt" 42 | "github.com/ianr0bkny/go-sonos" 43 | "github.com/ianr0bkny/go-sonos/config" 44 | "github.com/ianr0bkny/go-sonos/model" 45 | "github.com/ianr0bkny/go-sonos/upnp" 46 | "log" 47 | "net/http" 48 | "os" 49 | "strconv" 50 | "strings" 51 | ) 52 | 53 | const ( 54 | CSWEB_CONFIG = "/home/ianr/.go-sonos" 55 | CSWEB_DEVICE = "kitchen" 56 | CSWEB_DISCOVER_PORT = "13104" 57 | CSWEB_EVENTING_PORT = "13105" 58 | CSWEB_NETWORK = "eth0" 59 | CSWEB_HTTP_PORT = 8080 60 | ) 61 | 62 | func initSonos(config *config.Config) *sonos.Sonos { 63 | var s *sonos.Sonos 64 | if dev := config.Lookup(CSWEB_DEVICE); nil != dev { 65 | s = sonos.Connect(dev, nil, sonos.SVC_CONTENT_DIRECTORY|sonos.SVC_AV_TRANSPORT|sonos.SVC_RENDERING_CONTROL) 66 | } else { 67 | log.Fatal("Could not create Sonos instance") 68 | } 69 | return s 70 | } 71 | 72 | func replyOk(w http.ResponseWriter, value interface{}) { 73 | reply(w, nil, value) 74 | } 75 | 76 | func replyError(w http.ResponseWriter, msg string) { 77 | reply(w, errors.New(msg), nil) 78 | } 79 | 80 | type Reply struct { 81 | Error string `json:",omitempty"` 82 | Value interface{} `json:",omitempty"` 83 | } 84 | 85 | func reply(w http.ResponseWriter, err error, value interface{}) { 86 | r := Reply{} 87 | if nil != err { 88 | r.Error = fmt.Sprintf("%v", err) 89 | } 90 | if nil != value { 91 | r.Value = value 92 | } 93 | encoder := json.NewEncoder(os.Stdout) 94 | if false { 95 | encoder.Encode(r) 96 | } 97 | encoder = json.NewEncoder(w) 98 | encoder.Encode(r) 99 | } 100 | 101 | type handlerFunc func(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error 102 | 103 | // 104 | // get-position-info 105 | // 106 | func handle_GetPositionInfo(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error { 107 | if info, err := s.GetPositionInfo(0); nil != err { 108 | return err 109 | } else { 110 | replyOk(w, model.GetPositionInfoMessage(info)) 111 | } 112 | return nil 113 | } 114 | 115 | // 116 | // get-transport-info 117 | // 118 | func handle_GetTransportInfo(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error { 119 | if info, err := s.GetTransportInfo(0); nil != err { 120 | return err 121 | } else { 122 | replyOk(w, info) 123 | } 124 | return nil 125 | } 126 | 127 | // 128 | // get-volume 129 | // 130 | func handle_GetVolume(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error { 131 | if volume, err := s.GetVolume(0, upnp.Channel_Master); nil != err { 132 | return err 133 | } else { 134 | replyOk(w, volume) 135 | } 136 | return nil 137 | } 138 | 139 | // 140 | // next 141 | // 142 | func handle_Next(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error { 143 | if err := s.Next(0); nil != err { 144 | return err 145 | } 146 | replyOk(w, true) 147 | return nil 148 | } 149 | 150 | // 151 | // next-section 152 | // 153 | func handle_NextSection(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error { 154 | if err := s.NextSection(0); nil != err { 155 | return err 156 | } 157 | replyOk(w, true) 158 | return nil 159 | } 160 | 161 | // 162 | // pause 163 | // 164 | func handle_Pause(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error { 165 | if err := s.Pause(0); nil != err { 166 | return err 167 | } 168 | replyOk(w, true) 169 | return nil 170 | } 171 | 172 | // 173 | // play 174 | // 175 | func handle_Play(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error { 176 | if err := s.Play(0, "1"); nil != err { 177 | return err 178 | } 179 | replyOk(w, true) 180 | return nil 181 | } 182 | 183 | // 184 | // previous 185 | // 186 | func handle_Previous(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error { 187 | if err := s.Previous(0); nil != err { 188 | return err 189 | } 190 | replyOk(w, true) 191 | return nil 192 | } 193 | 194 | // 195 | // previous-section 196 | // 197 | func handle_PreviousSection(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error { 198 | if err := s.PreviousSection(0); nil != err { 199 | return err 200 | } 201 | replyOk(w, true) 202 | return nil 203 | } 204 | 205 | // 206 | // remove-track-from-queue 207 | // 208 | func handle_RemoveTrackFromQueue(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error { 209 | track_s := r.FormValue("track") 210 | if err := s.RemoveTrackFromQueue(0, fmt.Sprintf("Q:0/%s", track_s), 0); nil != err { 211 | return err 212 | } 213 | replyOk(w, true) 214 | return nil 215 | } 216 | 217 | // 218 | // seek 219 | // 220 | func handle_Seek(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error { 221 | unit := r.FormValue("unit") 222 | target := r.FormValue("target") 223 | if err := s.Seek(0, unit, target); nil != err { 224 | return err 225 | } 226 | replyOk(w, true) 227 | return nil 228 | } 229 | 230 | // 231 | // set-volume 232 | // 233 | func handle_SetVolume(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error { 234 | volume_s := r.FormValue("value") 235 | if volume, err := strconv.ParseInt(volume_s, 10, 16); nil != err { 236 | return errors.New(fmt.Sprintf("Invalid volume `%s' specified", volume_s)) 237 | } else { 238 | if err := s.SetVolume(0, upnp.Channel_Master, uint16(volume)); nil != err { 239 | return err 240 | } 241 | } 242 | replyOk(w, true) 243 | return nil 244 | } 245 | 246 | // 247 | // stop 248 | // 249 | func handle_Stop(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error { 250 | if err := s.Stop(0); nil != err { 251 | return err 252 | } 253 | replyOk(w, true) 254 | return nil 255 | } 256 | 257 | var controlHandlerMap = map[string]handlerFunc{ 258 | "get-position-info": handle_GetPositionInfo, 259 | "get-transport-info": handle_GetTransportInfo, 260 | "get-volume": handle_GetVolume, 261 | "next": handle_Next, 262 | "next-section": handle_NextSection, 263 | "pause": handle_Pause, 264 | "play": handle_Play, 265 | "previous": handle_Previous, 266 | "previous-section": handle_PreviousSection, 267 | "remove-track-from-queue": handle_RemoveTrackFromQueue, 268 | "seek": handle_Seek, 269 | "set-volume": handle_SetVolume, 270 | "stop": handle_Stop, 271 | } 272 | 273 | func handleControl(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) { 274 | f := r.FormValue("method") 275 | if handler, has := controlHandlerMap[f]; has { 276 | if err := handler(s, w, r); nil != err { 277 | replyError(w, fmt.Sprintf("Error in call to %s: %v", f, err)) 278 | } 279 | return 280 | } else { 281 | replyError(w, fmt.Sprintf("No such method control::%s", f)) 282 | } 283 | } 284 | 285 | // 286 | // get-album-tracks 287 | // 288 | func handle_GetAlbumTracks(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error { 289 | if tracks, err := s.GetAlbumTracks(r.FormValue("album")); nil != err { 290 | return err 291 | } else { 292 | replyOk(w, model.GetQueueContentsMessage(tracks)) 293 | } 294 | return nil 295 | } 296 | 297 | // 298 | // get-all-genres 299 | // 300 | func handle_GetAllGenres(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error { 301 | if list, err := s.GetAllGenres(); nil != err { 302 | return err 303 | } else { 304 | replyOk(w, model.GetQueueContentsMessage(list)) 305 | } 306 | return nil 307 | } 308 | 309 | // 310 | // get-artist-albums 311 | // 312 | func handle_GetArtistAlbums(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error { 313 | if list, err := s.GetArtistAlbums(r.FormValue("artist")); nil != err { 314 | return err 315 | } else { 316 | replyOk(w, model.GetQueueContentsMessage(list)) 317 | } 318 | return nil 319 | } 320 | 321 | // 322 | // get-direct-children 323 | // 324 | func handle_GetDirectChildren(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error { 325 | if list, err := s.GetDirectChildren(r.FormValue("root")); nil != err { 326 | return err 327 | } else { 328 | replyOk(w, model.GetQueueContentsMessage(list)) 329 | } 330 | return nil 331 | } 332 | 333 | // 334 | // get-genre-artists 335 | // 336 | func handle_GetGenreArtists(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error { 337 | if artists, err := s.GetGenreArtists(r.FormValue("genre")); nil != err { 338 | return err 339 | } else { 340 | replyOk(w, model.GetQueueContentsMessage(artists)) 341 | } 342 | return nil 343 | } 344 | 345 | // 346 | // get-queue-contents 347 | // 348 | func handle_GetQueueContents(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) error { 349 | if queue, err := s.GetQueueContents(); nil != err { 350 | return err 351 | } else { 352 | replyOk(w, model.GetQueueContentsMessage(queue)) 353 | } 354 | return nil 355 | } 356 | 357 | var browseHandlerMap = map[string]handlerFunc{ 358 | "get-album-tracks": handle_GetAlbumTracks, 359 | "get-all-genres": handle_GetAllGenres, 360 | "get-artist-albums": handle_GetArtistAlbums, 361 | "get-direct-children": handle_GetDirectChildren, 362 | "get-genre-artists": handle_GetGenreArtists, 363 | "get-queue-contents": handle_GetQueueContents, 364 | } 365 | 366 | func handleBrowse(s *sonos.Sonos, w http.ResponseWriter, r *http.Request) { 367 | f := r.FormValue("method") 368 | if handler, has := browseHandlerMap[f]; has { 369 | if err := handler(s, w, r); nil != err { 370 | replyError(w, fmt.Sprintf("Error in call to %s: %v", f, err)) 371 | } 372 | return 373 | } else { 374 | replyError(w, fmt.Sprintf("No such method browse::%s", f)) 375 | } 376 | } 377 | 378 | func setupHttp(s *sonos.Sonos) { 379 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 380 | http.ServeFile(w, r, strings.Join([]string{"web", r.RequestURI}, "/")) 381 | }) 382 | 383 | http.HandleFunc("/control", func(w http.ResponseWriter, r *http.Request) { 384 | handleControl(s, w, r) 385 | }) 386 | 387 | http.HandleFunc("/browse", func(w http.ResponseWriter, r *http.Request) { 388 | handleBrowse(s, w, r) 389 | }) 390 | } 391 | 392 | func main() { 393 | log.SetFlags(log.Ltime | log.Lshortfile) 394 | config := config.MakeConfig(CSWEB_CONFIG) 395 | config.Init() 396 | 397 | s := initSonos(config) 398 | if nil != s { 399 | setupHttp(s) 400 | } 401 | 402 | log.Printf("Starting server loop ...") 403 | log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", CSWEB_HTTP_PORT), nil)) 404 | } 405 | -------------------------------------------------------------------------------- /didl/didl.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | // 32 | // A minimal implementation of the Digital Item Declaration Language (DIDL). 33 | // 34 | package didl 35 | 36 | import ( 37 | "encoding/xml" 38 | "log" 39 | "strings" 40 | ) 41 | 42 | type didlValidated struct { 43 | Extra []xml.Name `xml:",any"` 44 | } 45 | 46 | func (this *didlValidated) Validate() { 47 | if 0 < len(this.Extra) { 48 | for _, extra := range this.Extra { 49 | log.Printf("Missing <%s/>", extra.Local) 50 | } 51 | } 52 | } 53 | 54 | type Album struct { 55 | XMLName xml.Name 56 | Value string `xml:",chardata"` 57 | } 58 | 59 | type AlbumArtURI struct { 60 | XMLName xml.Name 61 | Value string `xml:",chardata"` 62 | } 63 | 64 | type Class struct { 65 | XMLName xml.Name 66 | Value string `xml:",chardata"` 67 | } 68 | 69 | type Creator struct { 70 | XMLName xml.Name 71 | Value string `xml:",chardata"` 72 | } 73 | 74 | type OriginalTrackNumber struct { 75 | XMLName xml.Name 76 | Value string `xml:",chardata"` 77 | } 78 | 79 | type Res struct { 80 | XMLName xml.Name 81 | ProtocolInfo string `xml:"protocolInfo,attr"` 82 | Value string `xml:",chardata"` 83 | } 84 | type Title struct { 85 | XMLName xml.Name 86 | Value string `xml:",chardata"` 87 | } 88 | 89 | type Container struct { 90 | XMLName xml.Name 91 | ID string `xml:"id,attr"` 92 | ParentID string `xml:"parentID,attr"` 93 | Restricted bool `xml:"restricted,attr"` 94 | Res []Res `xml:"res"` 95 | Title []Title `xml:"title"` 96 | Class []Class `xml:"class"` 97 | AlbumArtURI []AlbumArtURI `xml:"albumArtURI"` 98 | Creator []Creator `xml:"creator"` 99 | didlValidated 100 | } 101 | 102 | type Item struct { 103 | XMLName xml.Name 104 | ID string `xml:"id,attr"` 105 | ParentID string `xml:"parentID,attr"` 106 | Restricted bool `xml:"restricted,attr"` 107 | Res []Res `xml:"res"` 108 | Title []Title `xml:"title"` 109 | Class []Class `xml:"class"` 110 | AlbumArtURI []AlbumArtURI `xml:"albumArtURI"` 111 | Creator []Creator `xml:"creator"` 112 | Album []Album `xml:"album"` 113 | OriginalTrackNumber []OriginalTrackNumber `xml:"originalTrackNumber"` 114 | didlValidated 115 | } 116 | 117 | type Lite struct { 118 | XMLName xml.Name 119 | Container []Container `xml:"container"` 120 | Item []Item `xml:"item"` 121 | didlValidated 122 | } 123 | 124 | const emptyDocument = "" 125 | 126 | func EmptyDocument() string { 127 | return emptyDocument 128 | } 129 | 130 | func EmptyDocuments(num int) string { 131 | var docs string 132 | for i := 0; i < num; i++ { 133 | docs = strings.Join([]string{docs, emptyDocument}, " ") 134 | } 135 | return docs 136 | } 137 | -------------------------------------------------------------------------------- /examples/browse/browse.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012-2015, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | package main 31 | 32 | import ( 33 | "github.com/ianr0bkny/go-sonos" 34 | "github.com/ianr0bkny/go-sonos/ssdp" 35 | "log" 36 | ) 37 | 38 | const ( 39 | instanceId = 0 40 | ) 41 | 42 | // This code locates a content directory device on the network 43 | func main() { 44 | log.Print("go-sonos example discovery\n") 45 | 46 | mgr := ssdp.MakeManager() 47 | mgr.Discover("eth0", "11209", false) 48 | qry := ssdp.ServiceQueryTerms{ 49 | ssdp.ServiceKey("schemas-upnp-org-ContentDirectory"): -1, 50 | } 51 | 52 | result := mgr.QueryServices(qry) 53 | if dev_list, has := result["schemas-upnp-org-ContentDirectory"]; has { 54 | for _, dev := range dev_list { 55 | log.Printf("%s %s %s %s %s\n", dev.Product(), dev.ProductVersion(), dev.Name(), dev.Location(), dev.UUID()) 56 | s := sonos.Connect(dev, nil, sonos.SVC_CONTENT_DIRECTORY) 57 | 58 | //Method 1 59 | if tracks, err := s.GetAlbumTracks("The Beatles"); nil != err { 60 | panic(err) 61 | } else { 62 | for _, track := range tracks { 63 | if "Long, Long, Long" == track.Title() { 64 | log.Printf("%#v", track) 65 | log.Printf("%#v", track.Res()) 66 | if objects, err := s.GetMetadata(track.ID()); nil != err { 67 | panic(err) 68 | } else { 69 | for _, object := range objects { 70 | log.Printf("--> %#v", object) 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | //Method 2 78 | if tracks, err := s.GetTrackFromAlbum("The Beatles", "Long, Long, Long"); nil != err { 79 | panic(err) 80 | } else { 81 | for _, track := range tracks { 82 | log.Printf("%#v", track) 83 | log.Printf("%#v", track.Res()) 84 | if objects, err := s.GetMetadata(track.ID()); nil != err { 85 | panic(err) 86 | } else { 87 | for _, object := range objects { 88 | log.Printf("--> %#v", object) 89 | } 90 | } 91 | } 92 | } 93 | break 94 | } 95 | } 96 | mgr.Close() 97 | } 98 | -------------------------------------------------------------------------------- /examples/composers/composers.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012-2015, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | package main 31 | 32 | import ( 33 | "github.com/ianr0bkny/go-sonos" 34 | "github.com/ianr0bkny/go-sonos/ssdp" 35 | "log" 36 | ) 37 | 38 | const ( 39 | instanceId = 0 40 | ) 41 | 42 | // This code locates a content directory device on the network 43 | func main() { 44 | log.Print("go-sonos example discovery\n") 45 | 46 | mgr := ssdp.MakeManager() 47 | mgr.Discover("eth0", "11209", false) 48 | qry := ssdp.ServiceQueryTerms{ 49 | ssdp.ServiceKey("schemas-upnp-org-ContentDirectory"): -1, 50 | } 51 | 52 | result := mgr.QueryServices(qry) 53 | if dev_list, has := result["schemas-upnp-org-ContentDirectory"]; has { 54 | for _, dev := range dev_list { 55 | log.Printf("%s %s %s %s %s\n", dev.Product(), dev.ProductVersion(), dev.Name(), dev.Location(), dev.UUID()) 56 | s := sonos.Connect(dev, nil, sonos.SVC_CONTENT_DIRECTORY) 57 | if data, err := s.GetAllComposers(); nil != err { 58 | panic(err) 59 | } else { 60 | for i, rec := range data { 61 | log.Printf("[%03d] %s", i, rec.Title()) 62 | } 63 | } 64 | break 65 | } 66 | } 67 | mgr.Close() 68 | } 69 | -------------------------------------------------------------------------------- /examples/devices/devices.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012-2015, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | package main 31 | 32 | import ( 33 | "github.com/ianr0bkny/go-sonos/ssdp" 34 | "log" 35 | ) 36 | 37 | // This code identifies lists all UPnP devices discovered 38 | func main() { 39 | log.Print("go-sonos example discovery\n") 40 | 41 | mgr := ssdp.MakeManager() 42 | mgr.Discover("eth0", "11209", false) 43 | i := 0 44 | dev_map := mgr.Devices() 45 | for _, dev := range dev_map { 46 | log.Printf("[%02d] %s %s %s %s %s\n", i, dev.Product(), dev.ProductVersion(), dev.Name(), dev.Location(), dev.UUID()) 47 | i++ 48 | } 49 | 50 | mgr.Close() 51 | } 52 | -------------------------------------------------------------------------------- /examples/discovery/discovery.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012-2015, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | package main 31 | 32 | import ( 33 | "github.com/ianr0bkny/go-sonos/ssdp" 34 | "log" 35 | ) 36 | 37 | // This code identifies UPnP devices on the netork that support the 38 | // MusicServices API. 39 | func main() { 40 | log.Print("go-sonos example discovery\n") 41 | 42 | mgr := ssdp.MakeManager() 43 | 44 | // Discover() 45 | // eth0 := Network device to query for UPnP devices 46 | // 11209 := Free local port for discovery replies 47 | // false := Do not subscribe for asynchronous updates 48 | mgr.Discover("eth0", "11209", false) 49 | 50 | // SericeQueryTerms 51 | // A map of service keys to minimum required version 52 | qry := ssdp.ServiceQueryTerms{ 53 | ssdp.ServiceKey("schemas-upnp-org-MusicServices"): -1, 54 | } 55 | 56 | // Look for the service keys in qry in the database of discovered devices 57 | result := mgr.QueryServices(qry) 58 | if dev_list, has := result["schemas-upnp-org-MusicServices"]; has { 59 | for _, dev := range dev_list { 60 | log.Printf("%s %s %s %s %s\n", dev.Product(), dev.ProductVersion(), dev.Name(), dev.Location(), dev.UUID()) 61 | } 62 | } 63 | mgr.Close() 64 | } 65 | -------------------------------------------------------------------------------- /examples/googletv/googletv.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012-2015, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | package main 31 | 32 | import ( 33 | "github.com/ianr0bkny/go-sonos" 34 | "github.com/ianr0bkny/go-sonos/ssdp" 35 | "log" 36 | "strings" 37 | ) 38 | 39 | const ( 40 | instanceId = 0 41 | ) 42 | 43 | // This code locates a GoogleTV device on the network 44 | func main() { 45 | log.Print("go-sonos example discovery\n") 46 | 47 | mgr := ssdp.MakeManager() 48 | mgr.Discover("eth0", "11209", false) 49 | dev_map := mgr.Devices() 50 | for _, dev := range dev_map { 51 | if "NSZ-GS7" == dev.Product() { 52 | log.Printf("%s %s %s %s %s\n", dev.Product(), dev.ProductVersion(), dev.Name(), dev.Location(), dev.UUID()) 53 | keys := dev.Services() 54 | for _, key := range keys { 55 | log.Printf("\t%s\n", key) 56 | } 57 | 58 | s := sonos.Connect(dev, nil, sonos.SVC_CONNECTION_MANAGER|sonos.SVC_RENDERING_CONTROL|sonos.SVC_AV_TRANSPORT) 59 | 60 | if source, sink, err := s.GetProtocolInfo(); nil != err { 61 | panic(err) 62 | } else { 63 | log.Printf("Source: %v", source) 64 | for _, sink := range strings.Split(sink, ",") { 65 | log.Printf("Sink: %v", sink) 66 | } 67 | } 68 | 69 | if connection_ids, err := s.GetCurrentConnectionIDs(); nil != err { 70 | panic(err) 71 | } else { 72 | log.Printf("ConnectionIDs: %v", connection_ids) 73 | } 74 | 75 | if presets, err := s.ListPresets(instanceId); nil != err { 76 | panic(err) 77 | } else { 78 | log.Printf("Preset: %v", presets) 79 | } 80 | 81 | if media_info, err := s.GetMediaInfo(instanceId); nil != err { 82 | panic(err) 83 | } else { 84 | log.Printf("%v", media_info) 85 | } 86 | 87 | if transport_info, err := s.GetTransportInfo(instanceId); nil != err { 88 | panic(err) 89 | } else { 90 | log.Printf("%v", transport_info) 91 | } 92 | 93 | if position_info, err := s.GetPositionInfo(instanceId); nil != err { 94 | panic(err) 95 | } else { 96 | log.Printf("%v", position_info) 97 | } 98 | 99 | if device_capabilities, err := s.GetDeviceCapabilities(instanceId); nil != err { 100 | panic(err) 101 | } else { 102 | log.Printf("%v", device_capabilities) 103 | } 104 | 105 | if transport_settings, err := s.GetTransportSettings(instanceId); nil != err { 106 | panic(err) 107 | } else { 108 | log.Printf("%v", transport_settings) 109 | } 110 | 111 | if actions, err := s.GetCurrentTransportActions(instanceId); nil != err { 112 | panic(err) 113 | } else { 114 | log.Printf("%v", actions) 115 | } 116 | 117 | /*TODO*/ 118 | /* 119 | if err := s.SetAVTransportURI(instanceId, uri, metadata); nil != err { 120 | panic(err) 121 | } 122 | */ 123 | } 124 | } 125 | mgr.Close() 126 | } 127 | -------------------------------------------------------------------------------- /linn-co-uk/Playlist.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package linn 32 | 33 | import ( 34 | "encoding/xml" 35 | "github.com/ianr0bkny/go-sonos/upnp" 36 | ) 37 | 38 | type Playlist struct { 39 | Svc *upnp.Service 40 | } 41 | 42 | func (this *Playlist) BeginSet(svc *upnp.Service, channel chan upnp.Event) { 43 | } 44 | 45 | func (this *Playlist) HandleProperty(svc *upnp.Service, value string, channel chan upnp.Event) error { 46 | return nil 47 | } 48 | 49 | func (this *Playlist) EndSet(svc *upnp.Service, channel chan upnp.Event) { 50 | } 51 | 52 | func (this *Playlist) IdArray() (aIdArrayToken uint32, aIdArray string, err error) { 53 | type Response struct { 54 | XMLName xml.Name 55 | AIdArrayToken uint32 `xml:"aIdArrayToken"` 56 | AIdArray string `xml:"aIdArray"` 57 | upnp.ErrorResponse 58 | } 59 | response := this.Svc.CallVa("IdArray") 60 | doc := Response{} 61 | xml.Unmarshal([]byte(response), &doc) 62 | return doc.AIdArrayToken, doc.AIdArray, doc.Error() 63 | } 64 | 65 | func (this *Playlist) TracksMax() (aTracksMax uint32, err error) { 66 | type Response struct { 67 | XMLName xml.Name 68 | ATracksMax uint32 `xml:"aTracksMax"` 69 | upnp.ErrorResponse 70 | } 71 | response := this.Svc.CallVa("IdArray") 72 | doc := Response{} 73 | xml.Unmarshal([]byte(response), &doc) 74 | return doc.ATracksMax, doc.Error() 75 | } 76 | -------------------------------------------------------------------------------- /model/message.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package model 32 | 33 | import ( 34 | "encoding/xml" 35 | "github.com/ianr0bkny/go-sonos/didl" 36 | "github.com/ianr0bkny/go-sonos/upnp" 37 | "strings" 38 | "time" 39 | ) 40 | 41 | type PositionInfo struct { 42 | Track uint32 43 | TrackDuration time.Duration 44 | TrackURI string 45 | RelTime time.Duration 46 | ProtocolInfo string 47 | Title string 48 | Class string 49 | Creator string 50 | Album string 51 | OriginalTrackNumber string 52 | } 53 | 54 | func getDuration(in string) (d time.Duration, err error) { 55 | in = strings.Replace(in, ":", "h", 1) 56 | in = strings.Replace(in, ":", "m", 1) 57 | in += "s" 58 | return time.ParseDuration(in) 59 | } 60 | 61 | func GetPositionInfoMessage(in *upnp.PositionInfo) *PositionInfo { 62 | var trackDuration, relTime time.Duration 63 | trackDuration, err := getDuration(in.TrackDuration) 64 | if nil == err { 65 | trackDuration /= time.Second 66 | } 67 | 68 | relTime, err = getDuration(in.RelTime) 69 | if nil == err { 70 | relTime /= time.Second 71 | } 72 | 73 | out := &PositionInfo{ 74 | Track: in.Track, 75 | TrackDuration: trackDuration, 76 | TrackURI: in.TrackURI, 77 | RelTime: relTime, 78 | } 79 | 80 | metadata := &didl.Lite{} 81 | xml.Unmarshal([]byte(in.TrackMetaData), metadata) 82 | metadata.Validate() 83 | 84 | for _, item := range metadata.Item { 85 | for _, res := range item.Res { 86 | out.ProtocolInfo = res.ProtocolInfo 87 | break 88 | } 89 | for _, title := range item.Title { 90 | out.Title = title.Value 91 | break 92 | } 93 | for _, class := range item.Class { 94 | out.Class = class.Value 95 | break 96 | } 97 | for _, creator := range item.Creator { 98 | out.Creator = creator.Value 99 | break 100 | } 101 | for _, album := range item.Album { 102 | out.Album = album.Value 103 | break 104 | } 105 | for _, originalTrackNumber := range item.OriginalTrackNumber { 106 | out.OriginalTrackNumber = originalTrackNumber.Value 107 | break 108 | } 109 | break 110 | } 111 | return out 112 | } 113 | 114 | type QueueElement struct { 115 | ID string 116 | ParentID string 117 | TrackURI string 118 | Title string 119 | Class string 120 | AlbumArtURI string 121 | Creator string 122 | Album string 123 | OriginalTrackNumber string 124 | } 125 | 126 | func protectEncoding(s string) string { 127 | return strings.Replace(s, "%", "%25", -1) 128 | } 129 | 130 | func GetQueueContentsMessage(in []Object) []QueueElement { 131 | var out []QueueElement 132 | for _, obj := range in { 133 | out = append(out, QueueElement{ 134 | ID: protectEncoding(obj.ID()), 135 | ParentID: obj.ParentID(), 136 | TrackURI: obj.Res(), 137 | Title: obj.Title(), 138 | Class: obj.Class(), 139 | AlbumArtURI: obj.AlbumArtURI(), 140 | Creator: obj.Creator(), 141 | Album: obj.Album(), 142 | OriginalTrackNumber: obj.OriginalTrackNumber(), 143 | }) 144 | } 145 | return out 146 | } 147 | 148 | type TransportInfo *upnp.TransportInfo 149 | -------------------------------------------------------------------------------- /model/model.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | // 32 | // A collection of object classes used in message passing in go-sonos. 33 | // 34 | package model 35 | 36 | import ( 37 | "github.com/ianr0bkny/go-sonos/didl" 38 | _ "log" 39 | ) 40 | 41 | // An abstraction of a DIDL-Lite or block. 42 | type Object interface { 43 | // The ObjectID of this item or container 44 | ID() string 45 | 46 | // The ObjectID of the parent container of this item or container 47 | ParentID() string 48 | 49 | // When true, the ability to change or delete this item or container is restricted 50 | Restricted() bool 51 | 52 | // The URI of the resource described by this item or container. 53 | // For a music track this could be the URI of the disk file on 54 | // the storage share. For a playlist the URI may refer to the 55 | // queue's ObjectID. 56 | Res() string 57 | 58 | // The display name of the container or item. 59 | Title() string 60 | 61 | // A string giving the type of resource described by this Object, e.g.: 62 | // 63 | // Containers: 64 | // * object.container 65 | // * object.container.albumlist 66 | // * object.container.album.musicAlbum 67 | // * object.container.genre.musicGenre 68 | // * object.container.person.musicArtist 69 | // * object.container.playlistContainer 70 | // * object.container.playlistContainer.sameArtist 71 | // 72 | // Items: 73 | // * object.item.audioItem.musicTrack 74 | Class() string 75 | 76 | // The URI to use to access the artwork for this container or item. 77 | AlbumArtURI() string 78 | 79 | // The display name of the Artist or Album Artist. 80 | Creator() string 81 | 82 | // The display name of the containing album. This field is Valid 83 | // for Items only, not Containers. 84 | Album() string 85 | 86 | // The track number of this item in original album sort 87 | // order. This field is Valid for Items only, not Containers. 88 | OriginalTrackNumber() string 89 | 90 | // True, if this Object represents a container; false otherwise. 91 | IsContainer() bool 92 | } 93 | 94 | // 95 | // A flattened structure of exported fields to allow Objects to be passed 96 | // via XML, JSON, or other encoding relying on reflection. Fields in this 97 | // struct mirror the usage of like-named methods in the Object interface. 98 | // 99 | type ObjectMessage struct { 100 | ID string 101 | ParentID string 102 | URI string 103 | Title string 104 | Class string 105 | AlbumArtURI string 106 | Creator string 107 | Album string 108 | } 109 | 110 | func makeObjectMessage(obj Object) *ObjectMessage { 111 | return &ObjectMessage{ 112 | ID: obj.ID(), 113 | ParentID: obj.ParentID(), 114 | URI: obj.Res(), 115 | Title: obj.Title(), 116 | Class: obj.Class(), 117 | AlbumArtURI: obj.AlbumArtURI(), 118 | Creator: obj.Creator(), 119 | Album: obj.Album(), 120 | } 121 | } 122 | 123 | type modelObjectImpl struct { 124 | id string 125 | parentId string 126 | restricted bool 127 | res string 128 | title string 129 | class string 130 | albumArtURI string 131 | creator string 132 | album string 133 | originalTrackNumber string 134 | isContainer bool 135 | } 136 | 137 | func (this modelObjectImpl) ID() string { 138 | return this.id 139 | } 140 | 141 | func (this modelObjectImpl) ParentID() string { 142 | return this.parentId 143 | } 144 | 145 | func (this modelObjectImpl) Restricted() bool { 146 | return this.restricted 147 | } 148 | 149 | func (this modelObjectImpl) Res() string { 150 | return this.res 151 | } 152 | 153 | func (this modelObjectImpl) Title() string { 154 | return this.title 155 | } 156 | 157 | func (this modelObjectImpl) Class() string { 158 | return this.class 159 | } 160 | 161 | func (this modelObjectImpl) AlbumArtURI() string { 162 | return this.albumArtURI 163 | } 164 | 165 | func (this modelObjectImpl) Creator() string { 166 | return this.creator 167 | } 168 | 169 | func (this modelObjectImpl) Album() string { 170 | return this.album 171 | } 172 | 173 | func (this modelObjectImpl) OriginalTrackNumber() string { 174 | return this.originalTrackNumber 175 | } 176 | 177 | func (this modelObjectImpl) IsContainer() bool { 178 | return this.isContainer 179 | } 180 | 181 | func makeContainer(in *didl.Container) Object { 182 | obj := modelObjectImpl{} 183 | obj.id = in.ID 184 | obj.parentId = in.ParentID 185 | obj.restricted = in.Restricted 186 | if 0 < len(in.Res) { 187 | obj.res = in.Res[0].Value 188 | } 189 | if 0 < len(in.Title) { 190 | obj.title = in.Title[0].Value 191 | } 192 | if 0 < len(in.Class) { 193 | obj.class = in.Class[0].Value 194 | } 195 | if 0 < len(in.AlbumArtURI) { 196 | obj.albumArtURI = in.AlbumArtURI[0].Value 197 | } 198 | if 0 < len(in.Creator) { 199 | obj.creator = in.Creator[0].Value 200 | } 201 | obj.isContainer = true 202 | return obj 203 | } 204 | 205 | func makeItem(in *didl.Item) Object { 206 | obj := modelObjectImpl{} 207 | obj.id = in.ID 208 | obj.parentId = in.ParentID 209 | obj.restricted = in.Restricted 210 | if 0 < len(in.Res) { 211 | obj.res = in.Res[0].Value 212 | } 213 | if 0 < len(in.Title) { 214 | obj.title = in.Title[0].Value 215 | } 216 | if 0 < len(in.Class) { 217 | obj.class = in.Class[0].Value 218 | } 219 | if 0 < len(in.AlbumArtURI) { 220 | obj.albumArtURI = in.AlbumArtURI[0].Value 221 | } 222 | if 0 < len(in.Creator) { 223 | obj.creator = in.Creator[0].Value 224 | } 225 | if 0 < len(in.Album) { 226 | obj.album = in.Album[0].Value 227 | } 228 | if 0 < len(in.OriginalTrackNumber) { 229 | obj.originalTrackNumber = in.OriginalTrackNumber[0].Value 230 | } 231 | return obj 232 | } 233 | 234 | // Create a list of Objects from a didl.Lite document. 235 | func ObjectStream(in *didl.Lite) (objects []Object) { 236 | for _, container := range in.Container { 237 | objects = append(objects, makeContainer(&container)) 238 | } 239 | for _, item := range in.Item { 240 | objects = append(objects, makeItem(&item)) 241 | } 242 | return 243 | } 244 | 245 | // Create a list of ObjectMessages from a list of Objects. 246 | func ObjectMessageStream(objs []Object) []*ObjectMessage { 247 | var out []*ObjectMessage 248 | for _, obj := range objs { 249 | out = append(out, makeObjectMessage(obj)) 250 | } 251 | return out 252 | } 253 | -------------------------------------------------------------------------------- /reciva-com/RecivaRadio.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package reciva 32 | 33 | import ( 34 | "encoding/xml" 35 | "github.com/ianr0bkny/go-sonos/upnp" 36 | ) 37 | 38 | type RecivaRadio struct { 39 | Svc *upnp.Service 40 | } 41 | 42 | func (this *RecivaRadio) BeginSet(svc *upnp.Service, channel chan upnp.Event) { 43 | } 44 | 45 | func (this *RecivaRadio) HandleProperty(svc *upnp.Service, value string, channel chan upnp.Event) error { 46 | return nil 47 | } 48 | 49 | func (this *RecivaRadio) EndSet(svc *upnp.Service, channel chan upnp.Event) { 50 | } 51 | 52 | func (this *RecivaRadio) GetDateTime() (retDataTimeValue string, err error) { 53 | type Response struct { 54 | XMLName xml.Name 55 | RetDateTimeValue string 56 | upnp.ErrorResponse 57 | } 58 | response := this.Svc.CallVa("GetDateTime") 59 | doc := Response{} 60 | xml.Unmarshal([]byte(response), &doc) 61 | return doc.RetDateTimeValue, doc.Error() 62 | } 63 | 64 | func (this *RecivaRadio) GetTimeZone() (retTimeZoneValue string, err error) { 65 | type Response struct { 66 | XMLName xml.Name 67 | RetTimeZoneValue string 68 | upnp.ErrorResponse 69 | } 70 | response := this.Svc.CallVa("GetTimeZone") 71 | doc := Response{} 72 | xml.Unmarshal([]byte(response), &doc) 73 | return doc.RetTimeZoneValue, doc.Error() 74 | } 75 | 76 | func (this *RecivaRadio) GetNumberOfPresets() (retNumberOfPresetsValue uint32, err error) { 77 | type Response struct { 78 | XMLName xml.Name 79 | RetNumberOfPresetsValue uint32 80 | upnp.ErrorResponse 81 | } 82 | response := this.Svc.CallVa("GetNumberOfPresets") 83 | doc := Response{} 84 | xml.Unmarshal([]byte(response), &doc) 85 | return doc.RetNumberOfPresetsValue, doc.Error() 86 | } 87 | 88 | func (this *RecivaRadio) GetDisplayLanguages() (retLanguageListValue, retIsoCodeListValue string, err error) { 89 | type Response struct { 90 | XMLName xml.Name 91 | RetLanguageListValue string 92 | RetIsoCodeListValue string 93 | upnp.ErrorResponse 94 | } 95 | response := this.Svc.CallVa("GetDisplayLanguages") 96 | doc := Response{} 97 | xml.Unmarshal([]byte(response), &doc) 98 | return doc.RetLanguageListValue, doc.RetIsoCodeListValue, doc.Error() 99 | } 100 | 101 | func (this *RecivaRadio) GetCurrentDisplayLanguage() (retLanguageValue, retIsoCodeValue string, err error) { 102 | type Response struct { 103 | XMLName xml.Name 104 | RetLanguageValue string 105 | RetIsoCodeValue string 106 | upnp.ErrorResponse 107 | } 108 | response := this.Svc.CallVa("GetCurrentDisplayLanguage") 109 | doc := Response{} 110 | xml.Unmarshal([]byte(response), &doc) 111 | return doc.RetLanguageValue, doc.RetIsoCodeValue, doc.Error() 112 | } 113 | 114 | func (this *RecivaRadio) GetPowerState() (retPowerStateValue string, err error) { 115 | type Response struct { 116 | XMLName xml.Name 117 | RetPowerStateValue string 118 | upnp.ErrorResponse 119 | } 120 | response := this.Svc.CallVa("GetPowerState") 121 | doc := Response{} 122 | xml.Unmarshal([]byte(response), &doc) 123 | return doc.RetPowerStateValue, doc.Error() 124 | } 125 | 126 | func (this *RecivaRadio) SetPowerState(newPowerStateValue string) (retPowerStateValue string, err error) { 127 | type Response struct { 128 | XMLName xml.Name 129 | RetPowerStateValue string 130 | upnp.ErrorResponse 131 | } 132 | args := []upnp.Arg{ 133 | {"NewPowerStateValue", newPowerStateValue}, 134 | } 135 | response := this.Svc.Call("SetPowerState", args) 136 | doc := Response{} 137 | xml.Unmarshal([]byte(response), &doc) 138 | return doc.RetPowerStateValue, doc.Error() 139 | } 140 | -------------------------------------------------------------------------------- /reciva-com/RecivaSimpleRemote.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package reciva 32 | 33 | import ( 34 | "encoding/xml" 35 | "github.com/ianr0bkny/go-sonos/upnp" 36 | ) 37 | 38 | type RecivaSimpleRemote struct { 39 | Svc *upnp.Service 40 | } 41 | 42 | func (this *RecivaSimpleRemote) BeginSet(svc *upnp.Service, channel chan upnp.Event) { 43 | } 44 | 45 | func (this *RecivaSimpleRemote) HandleProperty(svc *upnp.Service, value string, channel chan upnp.Event) error { 46 | return nil 47 | } 48 | 49 | func (this *RecivaSimpleRemote) EndSet(svc *upnp.Service, channel chan upnp.Event) { 50 | } 51 | 52 | func (this *RecivaSimpleRemote) KeyPressed(key, duration string) error { 53 | type Response struct { 54 | XMLName xml.Name 55 | upnp.ErrorResponse 56 | } 57 | args := []upnp.Arg{ 58 | {"Key", key}, 59 | {"Duration", duration}, 60 | } 61 | response := this.Svc.Call("KeyPressed", args) 62 | doc := Response{} 63 | xml.Unmarshal([]byte(response), &doc) 64 | return doc.Error() 65 | } 66 | -------------------------------------------------------------------------------- /reciva.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package sonos 32 | 33 | import ( 34 | "github.com/ianr0bkny/go-sonos/linn-co-uk" 35 | "github.com/ianr0bkny/go-sonos/reciva-com" 36 | "github.com/ianr0bkny/go-sonos/ssdp" 37 | "github.com/ianr0bkny/go-sonos/upnp" 38 | _ "log" 39 | ) 40 | 41 | const RECIVA_RADIO = "reciva-com-RecivaRadio" 42 | const RADIO = "Radio" 43 | 44 | type Reciva struct { 45 | upnp.AVTransport 46 | upnp.ConnectionManager 47 | upnp.RenderingControl 48 | reciva.RecivaSimpleRemote 49 | reciva.RecivaRadio 50 | linn.Playlist 51 | } 52 | 53 | func MakeReciva(svc_map upnp.ServiceMap, reactor upnp.Reactor, flags int) (reciva *Reciva) { 54 | reciva = &Reciva{} 55 | for svc_type, svc_list := range svc_map { 56 | switch svc_type { 57 | case "AVTransport": 58 | for _, svc := range svc_list { 59 | reciva.AVTransport.Svc = svc 60 | svc.Describe() 61 | if nil != reactor { 62 | reactor.Subscribe(svc, &reciva.AVTransport) 63 | } 64 | break 65 | } 66 | case "ConnectionManager": 67 | for _, svc := range svc_list { 68 | reciva.ConnectionManager.Svc = svc 69 | svc.Describe() 70 | if nil != reactor { 71 | reactor.Subscribe(svc, &reciva.ConnectionManager) 72 | } 73 | break 74 | } 75 | case "Playlist": 76 | for _, svc := range svc_list { 77 | reciva.Playlist.Svc = svc 78 | svc.Describe() 79 | if nil != reactor { 80 | reactor.Subscribe(svc, &reciva.Playlist) 81 | } 82 | break 83 | } 84 | case "RecivaRadio": 85 | for _, svc := range svc_list { 86 | reciva.RecivaRadio.Svc = svc 87 | svc.Describe() 88 | if nil != reactor { 89 | reactor.Subscribe(svc, &reciva.RecivaRadio) 90 | } 91 | break 92 | } 93 | case "RecivaSimpleRemote": 94 | for _, svc := range svc_list { 95 | reciva.RecivaSimpleRemote.Svc = svc 96 | svc.Describe() 97 | if nil != reactor { 98 | reactor.Subscribe(svc, &reciva.RecivaSimpleRemote) 99 | } 100 | break 101 | } 102 | case "RenderingControl": 103 | for _, svc := range svc_list { 104 | reciva.RenderingControl.Svc = svc 105 | svc.Describe() 106 | if nil != reactor { 107 | reactor.Subscribe(svc, &reciva.RenderingControl) 108 | } 109 | break 110 | } 111 | } 112 | } 113 | return 114 | } 115 | 116 | func ConnectAnyReciva(mgr ssdp.Manager, reactor upnp.Reactor, flags int) (reciva []*Reciva) { 117 | qry := ssdp.ServiceQueryTerms{ 118 | ssdp.ServiceKey(RECIVA_RADIO): -1, 119 | } 120 | res := mgr.QueryServices(qry) 121 | if dev_list, has := res[RECIVA_RADIO]; has { 122 | for _, dev := range dev_list { 123 | if RADIO == dev.Product() { 124 | if svc_map, err := upnp.Describe(dev.Location()); nil != err { 125 | panic(err) 126 | } else { 127 | reciva = append(reciva, MakeReciva(svc_map, reactor, flags)) 128 | } 129 | break 130 | } 131 | } 132 | } 133 | return 134 | } 135 | 136 | func ConnectReciva(dev ssdp.Device, reactor upnp.Reactor, flags int) (reciva *Reciva) { 137 | if svc_map, err := upnp.Describe(dev.Location()); nil != err { 138 | panic(err) 139 | } else { 140 | reciva = MakeReciva(svc_map, reactor, flags) 141 | } 142 | return 143 | } 144 | -------------------------------------------------------------------------------- /sonos.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | // 32 | // A go-language implementation of the Sonos UPnP API. 33 | // 34 | package sonos 35 | 36 | import ( 37 | "github.com/ianr0bkny/go-sonos/ssdp" 38 | "github.com/ianr0bkny/go-sonos/upnp" 39 | _ "log" 40 | ) 41 | 42 | const MUSIC_SERVICES = "schemas-upnp-org-MusicServices" 43 | const SONOS = "Sonos" 44 | 45 | type Sonos struct { 46 | upnp.AlarmClock 47 | upnp.AVTransport 48 | upnp.ConnectionManager 49 | upnp.ContentDirectory 50 | upnp.DeviceProperties 51 | upnp.GroupManagement 52 | upnp.MusicServices 53 | upnp.RenderingControl 54 | upnp.SystemProperties 55 | upnp.ZoneGroupTopology 56 | } 57 | 58 | const ( 59 | SVC_ALARM_CLOCK = 1 60 | SVC_AV_TRANSPORT = SVC_ALARM_CLOCK << 1 61 | SVC_CONNECTION_MANAGER = SVC_AV_TRANSPORT << 1 62 | SVC_CONTENT_DIRECTORY = SVC_CONNECTION_MANAGER << 1 63 | SVC_DEVICE_PROPERTIES = SVC_CONTENT_DIRECTORY << 1 64 | SVC_GROUP_MANAGEMENT = SVC_DEVICE_PROPERTIES << 1 65 | SVC_MUSIC_SERVICES = SVC_GROUP_MANAGEMENT << 1 66 | SVC_RENDERING_CONTROL = SVC_MUSIC_SERVICES << 1 67 | SVC_SYSTEM_PROPERTIES = SVC_RENDERING_CONTROL << 1 68 | SVC_ZONE_GROUP_TOPOLOGY = SVC_SYSTEM_PROPERTIES << 1 69 | // 70 | SVC_ALL = SVC_ALARM_CLOCK | 71 | SVC_AV_TRANSPORT | 72 | SVC_CONNECTION_MANAGER | 73 | SVC_CONTENT_DIRECTORY | 74 | SVC_DEVICE_PROPERTIES | 75 | SVC_GROUP_MANAGEMENT | 76 | SVC_MUSIC_SERVICES | 77 | SVC_RENDERING_CONTROL | 78 | SVC_SYSTEM_PROPERTIES | 79 | SVC_ZONE_GROUP_TOPOLOGY 80 | ) 81 | 82 | func sonosCheckServiceFlags(svc_type string, flags int) bool { 83 | switch svc_type { 84 | case "AlarmClock": 85 | return flags&SVC_ALARM_CLOCK > 0 86 | case "AVTransport": 87 | return flags&SVC_AV_TRANSPORT > 0 88 | case "ConnectionManager": 89 | return flags&SVC_CONNECTION_MANAGER > 0 90 | case "ContentDirectory": 91 | return flags&SVC_CONTENT_DIRECTORY > 0 92 | case "DeviceProperties": 93 | return flags&SVC_DEVICE_PROPERTIES > 0 94 | case "GroupManagement": 95 | return flags&SVC_GROUP_MANAGEMENT > 0 96 | case "MusicServices": 97 | return flags&SVC_MUSIC_SERVICES > 0 98 | case "RenderingControl": 99 | return flags&SVC_RENDERING_CONTROL > 0 100 | case "SystemProperties": 101 | return flags&SVC_SYSTEM_PROPERTIES > 0 102 | case "ZoneGroupTopology": 103 | return flags&SVC_ZONE_GROUP_TOPOLOGY > 0 104 | } 105 | return false 106 | } 107 | 108 | func MakeSonos(svc_map upnp.ServiceMap, reactor upnp.Reactor, flags int) (sonos *Sonos) { 109 | sonos = &Sonos{} 110 | for svc_type, svc_list := range svc_map { 111 | if !sonosCheckServiceFlags(svc_type, flags) { 112 | continue 113 | } 114 | switch svc_type { 115 | case "AlarmClock": 116 | for _, svc := range svc_list { 117 | sonos.AlarmClock.Svc = svc 118 | svc.Describe() 119 | if nil != reactor { 120 | reactor.Subscribe(svc, &sonos.AlarmClock) 121 | } 122 | break 123 | } 124 | case "AVTransport": 125 | for _, svc := range svc_list { 126 | sonos.AVTransport.Svc = svc 127 | svc.Describe() 128 | if nil != reactor { 129 | reactor.Subscribe(svc, &sonos.AVTransport) 130 | } 131 | break 132 | } 133 | case "ConnectionManager": 134 | for _, svc := range svc_list { 135 | sonos.ConnectionManager.Svc = svc 136 | svc.Describe() 137 | if nil != reactor { 138 | reactor.Subscribe(svc, &sonos.ConnectionManager) 139 | } 140 | break 141 | } 142 | case "ContentDirectory": 143 | for _, svc := range svc_list { 144 | sonos.ContentDirectory.Svc = svc 145 | svc.Describe() 146 | if nil != reactor { 147 | reactor.Subscribe(svc, &sonos.ContentDirectory) 148 | } 149 | break 150 | } 151 | case "DeviceProperties": 152 | for _, svc := range svc_list { 153 | sonos.DeviceProperties.Svc = svc 154 | svc.Describe() 155 | if nil != reactor { 156 | reactor.Subscribe(svc, &sonos.DeviceProperties) 157 | } 158 | break 159 | } 160 | case "GroupManagement": 161 | for _, svc := range svc_list { 162 | sonos.GroupManagement.Svc = svc 163 | svc.Describe() 164 | if nil != reactor { 165 | reactor.Subscribe(svc, &sonos.GroupManagement) 166 | } 167 | break 168 | } 169 | case "MusicServices": 170 | for _, svc := range svc_list { 171 | sonos.MusicServices.Svc = svc 172 | svc.Describe() 173 | if nil != reactor { 174 | reactor.Subscribe(svc, &sonos.MusicServices) 175 | } 176 | break 177 | } 178 | case "RenderingControl": 179 | for _, svc := range svc_list { 180 | sonos.RenderingControl.Svc = svc 181 | svc.Describe() 182 | if nil != reactor { 183 | reactor.Subscribe(svc, &sonos.RenderingControl) 184 | } 185 | break 186 | } 187 | case "SystemProperties": 188 | for _, svc := range svc_list { 189 | sonos.SystemProperties.Svc = svc 190 | svc.Describe() 191 | if nil != reactor { 192 | reactor.Subscribe(svc, &sonos.SystemProperties) 193 | } 194 | break 195 | } 196 | case "ZoneGroupTopology": 197 | for _, svc := range svc_list { 198 | sonos.ZoneGroupTopology.Svc = svc 199 | svc.Describe() 200 | if nil != reactor { 201 | reactor.Subscribe(svc, &sonos.ZoneGroupTopology) 202 | } 203 | break 204 | } 205 | } 206 | } 207 | return 208 | } 209 | 210 | func ConnectAny(mgr ssdp.Manager, reactor upnp.Reactor, flags int) (sonos []*Sonos) { 211 | qry := ssdp.ServiceQueryTerms{ 212 | ssdp.ServiceKey(MUSIC_SERVICES): -1, 213 | } 214 | res := mgr.QueryServices(qry) 215 | if dev_list, has := res[MUSIC_SERVICES]; has { 216 | for _, dev := range dev_list { 217 | if SONOS == dev.Product() { 218 | if svc_map, err := upnp.Describe(dev.Location()); nil != err { 219 | panic(err) 220 | } else { 221 | sonos = append(sonos, MakeSonos(svc_map, reactor, flags)) 222 | } 223 | break 224 | } 225 | } 226 | } 227 | return 228 | } 229 | 230 | func Connect(dev ssdp.Device, reactor upnp.Reactor, flags int) (sonos *Sonos) { 231 | if svc_map, err := upnp.Describe(dev.Location()); nil != err { 232 | panic(err) 233 | } else { 234 | sonos = MakeSonos(svc_map, reactor, flags) 235 | } 236 | return 237 | } 238 | 239 | func MakeReactor(ifiname, port string) upnp.Reactor { 240 | reactor := upnp.MakeReactor() 241 | reactor.Init(ifiname, port) 242 | return reactor 243 | } 244 | 245 | func Discover(ifiname, port string) (mgr ssdp.Manager, err error) { 246 | mgr = ssdp.MakeManager() 247 | mgr.Discover(ifiname, port, false) 248 | return 249 | } 250 | -------------------------------------------------------------------------------- /upnp/AlarmClock.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package upnp 32 | 33 | import ( 34 | "encoding/xml" 35 | _ "log" 36 | ) 37 | 38 | var ( 39 | AlarmClock_EventType = registerEventType("AlarmClock") 40 | ) 41 | 42 | type AlarmClockState struct { 43 | TimeZone string 44 | TimeServer string 45 | TimeGeneration uint32 46 | AlarmListVersion string 47 | DailyIndexRefreshTime string 48 | TimeFormat string 49 | DateFormat string 50 | } 51 | 52 | type AlarmClockEvent struct { 53 | AlarmClockState 54 | Svc *Service 55 | } 56 | 57 | func (this AlarmClockEvent) Service() *Service { 58 | return this.Svc 59 | } 60 | 61 | func (this AlarmClockEvent) Type() int { 62 | return AlarmClock_EventType 63 | } 64 | 65 | type AlarmClock struct { 66 | AlarmClockState 67 | Svc *Service 68 | } 69 | 70 | func (this *AlarmClock) BeginSet(svc *Service, channel chan Event) { 71 | } 72 | 73 | type alarmClockUpdate_XML struct { 74 | XMLName xml.Name `xml:"AlarmClockState"` 75 | Value string `xml:",innerxml"` 76 | } 77 | 78 | func (this *AlarmClock) HandleProperty(svc *Service, value string, channel chan Event) error { 79 | update := alarmClockUpdate_XML{ 80 | Value: value, 81 | } 82 | if bytes, err := xml.Marshal(update); nil != err { 83 | return err 84 | } else { 85 | xml.Unmarshal(bytes, &this.AlarmClockState) 86 | } 87 | return nil 88 | } 89 | 90 | func (this *AlarmClock) EndSet(svc *Service, channel chan Event) { 91 | evt := AlarmClockEvent{AlarmClockState: this.AlarmClockState, Svc: svc} 92 | channel <- evt 93 | } 94 | 95 | func (this *AlarmClock) SetFormat(desiredTimeFormat, desiredDateFormat string) (err error) { 96 | type Response struct { 97 | XMLName xml.Name 98 | ErrorResponse 99 | } 100 | args := []Arg{ 101 | {"DesiredTimeFormat", desiredTimeFormat}, 102 | {"DesiredDateFormat", desiredDateFormat}, 103 | } 104 | response := this.Svc.Call("SetFormat", args) 105 | doc := Response{} 106 | xml.Unmarshal([]byte(response), &doc) 107 | err = doc.Error() 108 | return 109 | } 110 | 111 | func (this *AlarmClock) GetFormat() (currentTimeFormat, currentDateFormat string, err error) { 112 | type Response struct { 113 | XMLName xml.Name 114 | CurrentTimeFormat string 115 | CurrentDateFormat string 116 | ErrorResponse 117 | } 118 | response := this.Svc.CallVa("GetFormat") 119 | doc := Response{} 120 | xml.Unmarshal([]byte(response), &doc) 121 | currentTimeFormat = doc.CurrentTimeFormat 122 | currentDateFormat = doc.CurrentDateFormat 123 | err = doc.Error() 124 | return 125 | } 126 | 127 | func (this *AlarmClock) SetTimeZone(index int32, autoAdjustDst bool) (err error) { 128 | type Response struct { 129 | XMLName xml.Name 130 | ErrorResponse 131 | } 132 | args := []Arg{ 133 | {"Index", index}, 134 | {"AutoAdjustDst", autoAdjustDst}, 135 | } 136 | response := this.Svc.Call("SetTimeZone", args) 137 | doc := Response{} 138 | xml.Unmarshal([]byte(response), &doc) 139 | err = doc.Error() 140 | return 141 | } 142 | 143 | func (this *AlarmClock) GetTimeZone() (index int32, autoAdjustDst bool, err error) { 144 | type Response struct { 145 | XMLName xml.Name 146 | Index int32 147 | AutoAdjustDst bool 148 | ErrorResponse 149 | } 150 | response := this.Svc.CallVa("GetTimeZone") 151 | doc := Response{} 152 | xml.Unmarshal([]byte(response), &doc) 153 | index = doc.Index 154 | autoAdjustDst = doc.AutoAdjustDst 155 | err = doc.Error() 156 | return 157 | } 158 | 159 | func (this *AlarmClock) GetTimeZoneAndRule() (index int32, autoAdjustDst bool, timeZone string, err error) { 160 | type Response struct { 161 | XMLName xml.Name 162 | Index int32 163 | AutoAdjustDst bool 164 | TimeZone string 165 | ErrorResponse 166 | } 167 | response := this.Svc.CallVa("GetTimeZoneAndRule") 168 | doc := Response{} 169 | xml.Unmarshal([]byte(response), &doc) 170 | index = doc.Index 171 | autoAdjustDst = doc.AutoAdjustDst 172 | timeZone = doc.TimeZone 173 | err = doc.Error() 174 | return 175 | } 176 | 177 | func (this *AlarmClock) GetTimeZoneRule(index int32) (timeZone string, err error) { 178 | type Response struct { 179 | XMLName xml.Name 180 | TimeZone string 181 | ErrorResponse 182 | } 183 | args := []Arg{ 184 | {"Index", index}, 185 | } 186 | response := this.Svc.Call("GetTimeZoneRule", args) 187 | doc := Response{} 188 | xml.Unmarshal([]byte(response), &doc) 189 | timeZone = doc.TimeZone 190 | err = doc.Error() 191 | return 192 | } 193 | 194 | func (this *AlarmClock) SetTimeServer(desiredTimeServer string) (err error) { 195 | type Response struct { 196 | XMLName xml.Name 197 | ErrorResponse 198 | } 199 | args := []Arg{ 200 | {"DesiredTimeServer", desiredTimeServer}, 201 | } 202 | response := this.Svc.Call("SetTimeServer", args) 203 | doc := Response{} 204 | xml.Unmarshal([]byte(response), &doc) 205 | err = doc.Error() 206 | return 207 | } 208 | 209 | func (this *AlarmClock) GetTimeServer() (currentTimeServer string, err error) { 210 | type Response struct { 211 | XMLName xml.Name 212 | CurrentTimeServer string 213 | ErrorResponse 214 | } 215 | response := this.Svc.CallVa("GetTimeServer") 216 | doc := Response{} 217 | xml.Unmarshal([]byte(response), &doc) 218 | currentTimeServer = doc.CurrentTimeServer 219 | err = doc.Error() 220 | return 221 | } 222 | 223 | func (this *AlarmClock) SetTimeNow(desiredTime, timeZoneForDesiredTime string) (err error) { 224 | type Response struct { 225 | XMLName xml.Name 226 | ErrorResponse 227 | } 228 | args := []Arg{ 229 | {"DesiredTime", desiredTime}, 230 | {"TimeZoneForDesiredTime", timeZoneForDesiredTime}, 231 | } 232 | response := this.Svc.Call("SetTimeNow", args) 233 | doc := Response{} 234 | xml.Unmarshal([]byte(response), &doc) 235 | err = doc.Error() 236 | return 237 | } 238 | 239 | func (this *AlarmClock) GetHouseholdTimeAtStamp(timeStamp string) (householdUTCTime string, err error) { 240 | type Response struct { 241 | XMLName xml.Name 242 | HouseholdUTCTime string 243 | ErrorResponse 244 | } 245 | args := []Arg{ 246 | {"TimeStamp", timeStamp}, 247 | } 248 | response := this.Svc.Call("GetHouseholdTimeAtStamp", args) 249 | doc := Response{} 250 | xml.Unmarshal([]byte(response), &doc) 251 | householdUTCTime = doc.HouseholdUTCTime 252 | err = doc.Error() 253 | return 254 | } 255 | 256 | type GetTimeNowResponse struct { 257 | CurrentUTCTime string 258 | CurrentLocalTime string 259 | CurrentTimeZone string 260 | CurrentTimeGeneration uint32 261 | } 262 | 263 | func (this *AlarmClock) GetTimeNow() (getTimeNowResponse *GetTimeNowResponse, err error) { 264 | type Response struct { 265 | XMLName xml.Name 266 | GetTimeNowResponse 267 | ErrorResponse 268 | } 269 | response := this.Svc.CallVa("GetTimeNow") 270 | doc := Response{} 271 | xml.Unmarshal([]byte(response), &doc) 272 | getTimeNowResponse = &doc.GetTimeNowResponse 273 | err = doc.Error() 274 | return 275 | } 276 | 277 | const ( 278 | Recurrence_ONCE = "ONCE" 279 | Recurrence_WEEKDAYS = "WEEKDAYS" 280 | Recurrence_WEEKENDS = "WEEKENDS" 281 | Recurrence_DAILY = "DAILY" 282 | ) 283 | 284 | const ( 285 | AlarmPlayMode_NORMAL = "NORMAL" 286 | AlarmPlayMode_REPEAT_ALL = "REPEAT_ALL" 287 | AlarmPlayMode_SHUFFLE_NOREPEAT = "SHUFFLE_NOREPEAT" 288 | AlarmPlayMode_SHUFFLE = "SHUFFLE" 289 | ) 290 | 291 | type CreateAlarmRequest struct { 292 | StartLocalTime string 293 | Duration string 294 | Recurrence string 295 | Enabled bool 296 | RoomUUID string 297 | ProgramURI string 298 | ProgramMetaData string 299 | PlayMode string 300 | Volume uint16 301 | IncludeLinkedZones bool 302 | } 303 | 304 | func (this *AlarmClock) CreateAlarm(req *CreateAlarmRequest) (assignedId uint32, err error) { 305 | type Response struct { 306 | XMLName xml.Name 307 | AssignedID uint32 308 | ErrorResponse 309 | } 310 | args := []Arg{ 311 | {"StartLocalTime", req.StartLocalTime}, 312 | {"Duration", req.Duration}, 313 | {"Recurrence", req.Recurrence}, 314 | {"Enabled", req.Enabled}, 315 | {"RoomUUID", req.RoomUUID}, 316 | {"ProgramURI", req.ProgramURI}, 317 | {"ProgramMetaData", req.ProgramMetaData}, 318 | {"PlayMode", req.PlayMode}, 319 | {"Volume", req.Volume}, 320 | {"IncludeLinkedZones", req.IncludeLinkedZones}, 321 | } 322 | response := this.Svc.Call("CreateAlarm", args) 323 | doc := Response{} 324 | xml.Unmarshal([]byte(response), &doc) 325 | assignedId = doc.AssignedID 326 | err = doc.Error() 327 | return 328 | } 329 | 330 | type UpdateAlarmRequest CreateAlarmRequest 331 | 332 | func (this *AlarmClock) UpdateAlarm(id uint32, req *UpdateAlarmRequest) (err error) { 333 | type Response struct { 334 | XMLName xml.Name 335 | ErrorResponse 336 | } 337 | args := []Arg{ 338 | {"ID", id}, 339 | {"StartLocalTime", req.StartLocalTime}, 340 | {"Duration", req.Duration}, 341 | {"Recurrence", req.Recurrence}, 342 | {"Enabled", req.Enabled}, 343 | {"RoomUUID", req.RoomUUID}, 344 | {"ProgramURI", req.ProgramURI}, 345 | {"ProgramMetaData", req.ProgramMetaData}, 346 | {"PlayMode", req.PlayMode}, 347 | {"Volume", req.Volume}, 348 | {"IncludeLinkedZones", req.IncludeLinkedZones}, 349 | } 350 | response := this.Svc.Call("UpdateAlarm", args) 351 | doc := Response{} 352 | xml.Unmarshal([]byte(response), &doc) 353 | err = doc.Error() 354 | return 355 | } 356 | 357 | func (this *AlarmClock) DestroyAlarm(id uint32) (err error) { 358 | type Response struct { 359 | XMLName xml.Name 360 | ErrorResponse 361 | } 362 | args := []Arg{ 363 | {"ID", id}, 364 | } 365 | response := this.Svc.Call("DestroyAlarm", args) 366 | doc := Response{} 367 | xml.Unmarshal([]byte(response), &doc) 368 | err = doc.Error() 369 | return 370 | } 371 | 372 | func (this *AlarmClock) ListAlarms() (currentAlarmList, currentAlarmListVersion string, err error) { 373 | type Response struct { 374 | XMLName xml.Name 375 | CurrentAlarmList string 376 | CurrentAlarmListVersion string 377 | ErrorResponse 378 | } 379 | response := this.Svc.CallVa("ListAlarms") 380 | doc := Response{} 381 | xml.Unmarshal([]byte(response), &doc) 382 | currentAlarmList = doc.CurrentAlarmList 383 | currentAlarmListVersion = doc.CurrentAlarmListVersion 384 | err = doc.Error() 385 | return 386 | } 387 | 388 | func (this *AlarmClock) SetDailyIndexRefreshTime(desiredDailyIndexRefreshTime string) (err error) { 389 | type Response struct { 390 | XMLName xml.Name 391 | ErrorResponse 392 | } 393 | args := []Arg{ 394 | {"DesiredDailyIndexRefreshTime", desiredDailyIndexRefreshTime}, 395 | } 396 | response := this.Svc.Call("SetDailyIndexRefreshTime", args) 397 | doc := Response{} 398 | xml.Unmarshal([]byte(response), &doc) 399 | err = doc.Error() 400 | return 401 | } 402 | 403 | func (this *AlarmClock) GetDailyIndexRefreshTime() (currentDailyIndexRefreshTime string, err error) { 404 | type Response struct { 405 | XMLName xml.Name 406 | CurrentDailyIndexRefreshTime string 407 | ErrorResponse 408 | } 409 | response := this.Svc.CallVa("GetDailyIndexRefreshTime") 410 | doc := Response{} 411 | xml.Unmarshal([]byte(response), &doc) 412 | currentDailyIndexRefreshTime = doc.CurrentDailyIndexRefreshTime 413 | err = doc.Error() 414 | return 415 | } 416 | -------------------------------------------------------------------------------- /upnp/ConnectionManager.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package upnp 32 | 33 | import ( 34 | "encoding/xml" 35 | _ "log" 36 | ) 37 | 38 | var ( 39 | ConnectionManager_EventType = registerEventType("ConnectionManager") 40 | ) 41 | 42 | type ConnectionManagerState struct { 43 | SourceProtocolInfo string 44 | SinkProtocolInfo string 45 | CurrentConnectionIDs string 46 | } 47 | 48 | type ConnectionManagerEvent struct { 49 | ConnectionManagerState 50 | Svc *Service 51 | } 52 | 53 | func (this ConnectionManagerEvent) Service() *Service { 54 | return this.Svc 55 | } 56 | 57 | func (this ConnectionManagerEvent) Type() int { 58 | return ConnectionManager_EventType 59 | } 60 | 61 | type ConnectionManager struct { 62 | ConnectionManagerState 63 | Svc *Service 64 | } 65 | 66 | func (this *ConnectionManager) BeginSet(svc *Service, channel chan Event) { 67 | } 68 | 69 | type connectionManagerUpdate_XML struct { 70 | XMLName xml.Name `xml:"ConnectionManagerState"` 71 | Value string `xml:",innerxml"` 72 | } 73 | 74 | func (this *ConnectionManager) HandleProperty(svc *Service, value string, channel chan Event) error { 75 | update := connectionManagerUpdate_XML{ 76 | Value: value, 77 | } 78 | if bytes, err := xml.Marshal(update); nil != err { 79 | return err 80 | } else { 81 | xml.Unmarshal(bytes, &this.ConnectionManagerState) 82 | } 83 | return nil 84 | } 85 | 86 | func (this *ConnectionManager) EndSet(svc *Service, channel chan Event) { 87 | evt := ConnectionManagerEvent{ConnectionManagerState: this.ConnectionManagerState, Svc: svc} 88 | channel <- evt 89 | } 90 | 91 | func (this *ConnectionManager) GetProtocolInfo() (source, sink string, err error) { 92 | type Response struct { 93 | XMLName xml.Name 94 | Source string 95 | Sink string 96 | ErrorResponse 97 | } 98 | response := this.Svc.CallVa("GetProtocolInfo") 99 | doc := Response{} 100 | xml.Unmarshal([]byte(response), &doc) 101 | source = doc.Source 102 | sink = doc.Sink 103 | err = doc.Error() 104 | return 105 | } 106 | 107 | func (this *ConnectionManager) GetCurrentConnectionIDs() (connectionIds string, err error) { 108 | type Response struct { 109 | XMLName xml.Name 110 | ConnectionIDs string 111 | ErrorResponse 112 | } 113 | response := this.Svc.CallVa("GetCurrentConnectionIDs") 114 | doc := Response{} 115 | xml.Unmarshal([]byte(response), &doc) 116 | connectionIds = doc.ConnectionIDs 117 | err = doc.Error() 118 | return 119 | } 120 | 121 | const ( 122 | Direction_Input = "Input" 123 | Direction_Output = "Output" 124 | ) 125 | 126 | const ( 127 | Status_OK = "OK" 128 | Status_ContentFormatMismatch = "ContentFormatMismatch" 129 | Status_InsufficientBandwidth = "InsufficientBandwidth" 130 | Status_UnreliableChannel = "UnreliableChannel" 131 | Status_Unknown = "Unknown" 132 | ) 133 | 134 | type ConnectionInfo struct { 135 | RcsID int32 136 | AVTransportID int32 137 | ProtocolInfo string 138 | PeerConnectionManager string 139 | PeerConnectionID int32 140 | Direction string 141 | Status string 142 | } 143 | 144 | func (this *ConnectionManager) GetCurrentConnectionInfo(connectionId int32) (connectionInfo *ConnectionInfo, err error) { 145 | type Response struct { 146 | XMLName xml.Name 147 | ConnectionInfo 148 | ErrorResponse 149 | } 150 | args := []Arg{ 151 | {"ConnectionID", connectionId}, 152 | } 153 | response := this.Svc.Call("GetCurrentConnectionInfo", args) 154 | doc := Response{} 155 | xml.Unmarshal([]byte(response), &doc) 156 | connectionInfo = &doc.ConnectionInfo 157 | err = doc.Error() 158 | return 159 | } 160 | -------------------------------------------------------------------------------- /upnp/ContentDirectory.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package upnp 32 | 33 | import ( 34 | "encoding/xml" 35 | "github.com/ianr0bkny/go-sonos/didl" 36 | _ "log" 37 | ) 38 | 39 | var ( 40 | ContentDirectory_EventType = registerEventType("ContentDirectory") 41 | ) 42 | 43 | type ContentDirectoryState struct { 44 | SystemUpdateID uint32 45 | ContainerUpdateIDs string 46 | ShareListRefreshState string 47 | ShareIndexInProgress bool 48 | ShareIndexLastError string 49 | UserRadioUpdateID string 50 | SavedQueuesUpdateID string 51 | ShareListUpdateID string 52 | RecentlyPlayedUpdateID string 53 | Browseable bool 54 | RadioFavoritesUpdateID uint32 55 | RadioLocationUpdateID uint32 56 | FavoritesUpdateID string 57 | FavoritePresetsUpdateID string 58 | } 59 | 60 | type ContentDirectoryEvent struct { 61 | ContentDirectoryState 62 | Svc *Service 63 | } 64 | 65 | func (this ContentDirectoryEvent) Service() *Service { 66 | return this.Svc 67 | } 68 | 69 | func (this ContentDirectoryEvent) Type() int { 70 | return ContentDirectory_EventType 71 | } 72 | 73 | type ContentDirectory struct { 74 | ContentDirectoryState 75 | Svc *Service 76 | } 77 | 78 | func (this *ContentDirectory) BeginSet(svc *Service, channel chan Event) { 79 | } 80 | 81 | type contentDirectoryUpdate_XML struct { 82 | XMLName xml.Name `xml:"ContentDirectoryState"` 83 | Value string `xml:",innerxml"` 84 | } 85 | 86 | func (this *ContentDirectory) HandleProperty(svc *Service, value string, channel chan Event) error { 87 | update := contentDirectoryUpdate_XML{ 88 | Value: value, 89 | } 90 | if bytes, err := xml.Marshal(update); nil != err { 91 | return err 92 | } else { 93 | xml.Unmarshal(bytes, &this.ContentDirectoryState) 94 | } 95 | return nil 96 | } 97 | 98 | func (this *ContentDirectory) EndSet(svc *Service, channel chan Event) { 99 | evt := ContentDirectoryEvent{ContentDirectoryState: this.ContentDirectoryState, Svc: svc} 100 | channel <- evt 101 | } 102 | 103 | func (this *ContentDirectory) GetSearchCapabilities() (searchCaps string, err error) { 104 | type Response struct { 105 | XMLName xml.Name 106 | SearchCaps string 107 | ErrorResponse 108 | } 109 | response := this.Svc.CallVa("GetSearchCapabilities") 110 | doc := Response{} 111 | xml.Unmarshal([]byte(response), &doc) 112 | searchCaps = doc.SearchCaps 113 | err = doc.Error() 114 | return 115 | } 116 | 117 | func (this *ContentDirectory) GetSortCapabilities() (sortCaps string, err error) { 118 | type Response struct { 119 | XMLName xml.Name 120 | SortCaps string 121 | ErrorResponse 122 | } 123 | response := this.Svc.CallVa("GetSortCapabilities") 124 | doc := Response{} 125 | xml.Unmarshal([]byte(response), &doc) 126 | sortCaps = doc.SortCaps 127 | err = doc.Error() 128 | return 129 | } 130 | 131 | func (this *ContentDirectory) GetSystemUpdateID() (id uint32, err error) { 132 | type Response struct { 133 | XMLName xml.Name 134 | Id uint32 135 | ErrorResponse 136 | } 137 | response := this.Svc.CallVa("GetSystemUpdateID") 138 | doc := Response{} 139 | xml.Unmarshal([]byte(response), &doc) 140 | id = doc.Id 141 | err = doc.Error() 142 | return 143 | } 144 | 145 | func (this *ContentDirectory) GetAlbumArtistDisplayOption() (albumArtistDisplayOption string, err error) { 146 | type Response struct { 147 | XMLName xml.Name 148 | AlbumArtistDisplayOption string 149 | ErrorResponse 150 | } 151 | response := this.Svc.CallVa("GetAlbumArtistDisplayOption") 152 | doc := Response{} 153 | xml.Unmarshal([]byte(response), &doc) 154 | albumArtistDisplayOption = doc.AlbumArtistDisplayOption 155 | err = doc.Error() 156 | return 157 | } 158 | 159 | func (this *ContentDirectory) GetLastIndexChange() (lastIndexChange string, err error) { 160 | type Response struct { 161 | XMLName xml.Name 162 | LastIndexChange string 163 | ErrorResponse 164 | } 165 | response := this.Svc.CallVa("GetLastIndexChange") 166 | doc := Response{} 167 | xml.Unmarshal([]byte(response), &doc) 168 | lastIndexChange = doc.LastIndexChange 169 | err = doc.Error() 170 | return 171 | } 172 | 173 | const ( 174 | BrowseObjectID_Root = "0" 175 | ) 176 | 177 | const ( 178 | BrowseFlag_BrowseMetadata = "BrowseMetadata" 179 | BrowseFlag_BrowseDirectChildren = "BrowseDirectChildren" 180 | ) 181 | 182 | const ( 183 | BrowseFilter_All = "*" 184 | ) 185 | 186 | const ( 187 | BrowseSortCriteria_None = "" 188 | ) 189 | 190 | type BrowseRequest struct { 191 | ObjectID string 192 | BrowseFlag string 193 | Filter string 194 | StartingIndex uint32 195 | RequestCount uint32 196 | SortCriteria string 197 | } 198 | 199 | type BrowseResult struct { 200 | NumberReturned int32 201 | TotalMatches int32 202 | UpdateID int32 203 | Doc *didl.Lite 204 | } 205 | 206 | func (this *ContentDirectory) Browse(req *BrowseRequest) (browseResult *BrowseResult, err error) { 207 | type Response struct { 208 | XMLName xml.Name 209 | Result string 210 | BrowseResult 211 | ErrorResponse 212 | } 213 | args := []Arg{ 214 | {"ObjectID", req.ObjectID}, 215 | {"BrowseFlag", req.BrowseFlag}, 216 | {"Filter", req.Filter}, 217 | {"StartingIndex", req.StartingIndex}, 218 | {"RequestedCount", req.RequestCount}, 219 | {"SortCriteria", req.SortCriteria}, 220 | } 221 | response := this.Svc.Call("Browse", args) 222 | doc := Response{} 223 | xml.Unmarshal([]byte(response), &doc) 224 | doc.Doc = &didl.Lite{} 225 | // log.Printf("%s", doc.Result) 226 | xml.Unmarshal([]byte(doc.Result), doc.Doc) 227 | browseResult = &doc.BrowseResult 228 | err = doc.Error() 229 | return 230 | } 231 | 232 | func (this *ContentDirectory) FindPrefix(objectId, prefix string) (startingIndex, updateId uint32, err error) { 233 | type Response struct { 234 | XMLName xml.Name 235 | StartingIndex uint32 236 | UpdateID uint32 237 | ErrorResponse 238 | } 239 | args := []Arg{ 240 | {"ObjectID", objectId}, 241 | {"StartingIndex", startingIndex}, 242 | {"UpdateID", updateId}, 243 | } 244 | response := this.Svc.Call("FindPrefix", args) 245 | doc := Response{} 246 | xml.Unmarshal([]byte(response), &doc) 247 | startingIndex = doc.StartingIndex 248 | updateId = doc.UpdateID 249 | err = doc.Error() 250 | return 251 | } 252 | 253 | type PrefixLocations struct { 254 | TotalPrefixes uint32 255 | PrefixAndIndexCSV string 256 | UpdateID uint32 257 | } 258 | 259 | func (this *ContentDirectory) GetAllPrefixLocations(objectId string) (prefixLocations *PrefixLocations, err error) { 260 | type Response struct { 261 | XMLName xml.Name 262 | PrefixLocations 263 | ErrorResponse 264 | } 265 | args := []Arg{ 266 | {"ObjectID", objectId}, 267 | } 268 | response := this.Svc.Call("GetAllPrefixLocations", args) 269 | doc := Response{} 270 | xml.Unmarshal([]byte(response), &doc) 271 | prefixLocations = &doc.PrefixLocations 272 | err = doc.Error() 273 | return 274 | } 275 | 276 | func (this *ContentDirectory) CreateObject(container, elements string) (objectId, result string, err error) { 277 | type Response struct { 278 | XMLName xml.Name 279 | ObjectID string 280 | Result string 281 | ErrorResponse 282 | } 283 | args := []Arg{ 284 | {"Container", container}, 285 | {"Elements", elements}, 286 | } 287 | response := this.Svc.Call("CreateObject", args) 288 | doc := Response{} 289 | xml.Unmarshal([]byte(response), &doc) 290 | objectId = doc.ObjectID 291 | result = doc.Result 292 | err = doc.Error() 293 | return 294 | } 295 | 296 | func (this *ContentDirectory) UpdateObject(objectId, currentTagValue, newTagValue string) (err error) { 297 | type Response struct { 298 | XMLName xml.Name 299 | ErrorResponse 300 | } 301 | args := []Arg{ 302 | {"ObjectID", objectId}, 303 | {"CurrentTagValue", currentTagValue}, 304 | {"NewTagValue", newTagValue}, 305 | } 306 | response := this.Svc.Call("UpdateObject", args) 307 | doc := Response{} 308 | xml.Unmarshal([]byte(response), &doc) 309 | err = doc.Error() 310 | return 311 | } 312 | 313 | // 314 | // Remove the directory object given by @objectId (e.g. "SQ:11", to 315 | // remove a saved queue). A 701 error is returned if an invalid @objectId 316 | // is specified. 317 | // 318 | func (this *ContentDirectory) DestroyObject(objectId string) error { 319 | type Response struct { 320 | XMLName xml.Name 321 | ErrorResponse 322 | } 323 | args := []Arg{ 324 | {"ObjectID", objectId}, 325 | } 326 | response := this.Svc.Call("DestroyObject", args) 327 | doc := Response{} 328 | xml.Unmarshal([]byte(response), &doc) 329 | return doc.Error() 330 | } 331 | 332 | func (this *ContentDirectory) RefreshShareList() (err error) { 333 | type Response struct { 334 | XMLName xml.Name 335 | ErrorResponse 336 | } 337 | response := this.Svc.CallVa("RefreshShareList") 338 | doc := Response{} 339 | xml.Unmarshal([]byte(response), &doc) 340 | err = doc.Error() 341 | return 342 | } 343 | 344 | func (this *ContentDirectory) RefreshShareIndex(albumArtistDisplayOption string) (err error) { 345 | type Response struct { 346 | XMLName xml.Name 347 | ErrorResponse 348 | } 349 | args := []Arg{ 350 | {"AlbumArtistDisplayOption", albumArtistDisplayOption}, 351 | } 352 | response := this.Svc.Call("RefreshShareIndex", args) 353 | doc := Response{} 354 | xml.Unmarshal([]byte(response), &doc) 355 | err = doc.Error() 356 | return 357 | } 358 | 359 | func (this *ContentDirectory) RequestResort(sortOrder string) (err error) { 360 | type Response struct { 361 | XMLName xml.Name 362 | ErrorResponse 363 | } 364 | args := []Arg{ 365 | {"SortOrder", sortOrder}, 366 | } 367 | response := this.Svc.Call("RequestResort", args) 368 | doc := Response{} 369 | xml.Unmarshal([]byte(response), &doc) 370 | err = doc.Error() 371 | return 372 | } 373 | 374 | func (this *ContentDirectory) GetShareIndexInProgress() (isIndexing bool, err error) { 375 | type Response struct { 376 | XMLName xml.Name 377 | IsIndexing bool 378 | ErrorResponse 379 | } 380 | response := this.Svc.CallVa("GetShareIndexInProgress") 381 | doc := Response{} 382 | xml.Unmarshal([]byte(response), &doc) 383 | isIndexing = doc.IsIndexing 384 | err = doc.Error() 385 | return 386 | } 387 | 388 | func (this *ContentDirectory) GetBrowseable() (isBrowseable bool, err error) { 389 | type Response struct { 390 | XMLName xml.Name 391 | IsBrowseable bool 392 | ErrorResponse 393 | } 394 | response := this.Svc.CallVa("GetBrowseable") 395 | doc := Response{} 396 | xml.Unmarshal([]byte(response), &doc) 397 | isBrowseable = doc.IsBrowseable 398 | err = doc.Error() 399 | return 400 | } 401 | 402 | func (this *ContentDirectory) SetBrowseable(browseable bool) (err error) { 403 | type Response struct { 404 | XMLName xml.Name 405 | ErrorResponse 406 | } 407 | args := []Arg{ 408 | {"Browseable", browseable}, 409 | } 410 | response := this.Svc.Call("SetBrowseable", args) 411 | doc := Response{} 412 | xml.Unmarshal([]byte(response), &doc) 413 | err = doc.Error() 414 | return 415 | } 416 | -------------------------------------------------------------------------------- /upnp/DeviceProperties.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package upnp 32 | 33 | import ( 34 | "encoding/xml" 35 | _ "log" 36 | ) 37 | 38 | var ( 39 | DeviceProperties_EventType = registerEventType("DeviceProperties") 40 | ) 41 | 42 | type DevicePropertiesState struct { 43 | SettingsReplicationState string 44 | ZoneName string 45 | Icon string 46 | Configuration string 47 | Invisible bool 48 | IsZoneBridge bool 49 | ChannelMapSet string 50 | HTSatChanMapSet string 51 | HTFreq uint32 52 | } 53 | 54 | type DevicePropertiesEvent struct { 55 | DevicePropertiesState 56 | Svc *Service 57 | } 58 | 59 | func (this DevicePropertiesEvent) Service() *Service { 60 | return this.Svc 61 | } 62 | 63 | func (this DevicePropertiesEvent) Type() int { 64 | return DeviceProperties_EventType 65 | } 66 | 67 | type DeviceProperties struct { 68 | DevicePropertiesState 69 | Svc *Service 70 | } 71 | 72 | func (this *DeviceProperties) BeginSet(svc *Service, channel chan Event) { 73 | } 74 | 75 | type devicePropertiesUpdate_XML struct { 76 | XMLName xml.Name `xml:"DevicePropertiesState"` 77 | Value string `xml:",innerxml"` 78 | } 79 | 80 | func (this *DeviceProperties) HandleProperty(svc *Service, value string, channel chan Event) error { 81 | update := devicePropertiesUpdate_XML{ 82 | Value: value, 83 | } 84 | if bytes, err := xml.Marshal(update); nil != err { 85 | return err 86 | } else { 87 | xml.Unmarshal(bytes, &this.DevicePropertiesState) 88 | } 89 | return nil 90 | } 91 | 92 | func (this *DeviceProperties) EndSet(svc *Service, channel chan Event) { 93 | evt := DevicePropertiesEvent{DevicePropertiesState: this.DevicePropertiesState, Svc: svc} 94 | channel <- evt 95 | } 96 | 97 | const ( 98 | LEDState_On = "On" 99 | LEDState_Off = "Off" 100 | ) 101 | 102 | func (this *DeviceProperties) SetLEDState(desiredLEDState string) (err error) { 103 | type Response struct { 104 | XMLName xml.Name 105 | ErrorResponse 106 | } 107 | args := []Arg{ 108 | {"DesiredLEDState", desiredLEDState}, 109 | } 110 | response := this.Svc.Call("SetLEDState", args) 111 | doc := Response{} 112 | xml.Unmarshal([]byte(response), &doc) 113 | err = doc.Error() 114 | return 115 | } 116 | 117 | func (this *DeviceProperties) GetLEDState() (currentLEDState string, err error) { 118 | type Response struct { 119 | XMLName xml.Name 120 | CurrentLEDState string 121 | ErrorResponse 122 | } 123 | response := this.Svc.CallVa("GetLEDState") 124 | doc := Response{} 125 | xml.Unmarshal([]byte(response), &doc) 126 | currentLEDState = doc.CurrentLEDState 127 | err = doc.Error() 128 | return 129 | } 130 | 131 | func (this *DeviceProperties) SetInvisible(desiredInvisible bool) (err error) { 132 | type Response struct { 133 | XMLName xml.Name 134 | ErrorResponse 135 | } 136 | args := []Arg{ 137 | {"DesiredInvisible", desiredInvisible}, 138 | } 139 | response := this.Svc.Call("SetInvisible", args) 140 | doc := Response{} 141 | xml.Unmarshal([]byte(response), &doc) 142 | err = doc.Error() 143 | return 144 | } 145 | 146 | func (this *DeviceProperties) GetInvisible() (currentInvisible bool, err error) { 147 | type Response struct { 148 | XMLName xml.Name 149 | CurrentInvisible bool 150 | ErrorResponse 151 | } 152 | response := this.Svc.CallVa("GetInvisible") 153 | doc := Response{} 154 | xml.Unmarshal([]byte(response), &doc) 155 | currentInvisible = doc.CurrentInvisible 156 | err = doc.Error() 157 | return 158 | } 159 | 160 | func (this *DeviceProperties) AddBondedZones(channelMapSet string) (err error) { 161 | type Response struct { 162 | XMLName xml.Name 163 | ErrorResponse 164 | } 165 | args := []Arg{ 166 | {"ChannelMapSet", channelMapSet}, 167 | } 168 | response := this.Svc.Call("AddBondedZones", args) 169 | doc := Response{} 170 | xml.Unmarshal([]byte(response), &doc) 171 | err = doc.Error() 172 | return 173 | } 174 | 175 | func (this *DeviceProperties) RemoveBondedZones(channelMapSet string) (err error) { 176 | type Response struct { 177 | XMLName xml.Name 178 | ErrorResponse 179 | } 180 | args := []Arg{ 181 | {"ChannelMapSet", channelMapSet}, 182 | } 183 | response := this.Svc.Call("RemoveBondedZones", args) 184 | doc := Response{} 185 | xml.Unmarshal([]byte(response), &doc) 186 | err = doc.Error() 187 | return 188 | } 189 | 190 | func (this *DeviceProperties) CreateStereoPair(channelMapSet string) (err error) { 191 | type Response struct { 192 | XMLName xml.Name 193 | ErrorResponse 194 | } 195 | args := []Arg{ 196 | {"ChannelMapSet", channelMapSet}, 197 | } 198 | response := this.Svc.Call("CreateStereoPair", args) 199 | doc := Response{} 200 | xml.Unmarshal([]byte(response), &doc) 201 | err = doc.Error() 202 | return 203 | } 204 | 205 | func (this *DeviceProperties) SeparateStereoPair(channelMapSet string) (err error) { 206 | type Response struct { 207 | XMLName xml.Name 208 | ErrorResponse 209 | } 210 | args := []Arg{ 211 | {"ChannelMapSet", channelMapSet}, 212 | } 213 | response := this.Svc.Call("SeparateStereoPair", args) 214 | doc := Response{} 215 | xml.Unmarshal([]byte(response), &doc) 216 | err = doc.Error() 217 | return 218 | } 219 | 220 | func (this *DeviceProperties) SetZoneAttributes(desiredZoneName, desiredIcon string) (err error) { 221 | type Response struct { 222 | XMLName xml.Name 223 | ErrorResponse 224 | } 225 | args := []Arg{ 226 | {"DesiredZoneName,", desiredZoneName}, 227 | {"DesiredIcon,", desiredIcon}, 228 | } 229 | response := this.Svc.Call("SetZoneAttributes", args) 230 | doc := Response{} 231 | xml.Unmarshal([]byte(response), &doc) 232 | err = doc.Error() 233 | return 234 | } 235 | 236 | func (this *DeviceProperties) GetZoneAttributes() (currentZoneName, currentIcon string, err error) { 237 | type Response struct { 238 | XMLName xml.Name 239 | CurrentZoneName string 240 | CurrentIcon string 241 | ErrorResponse 242 | } 243 | response := this.Svc.CallVa("GetZoneAttributes") 244 | doc := Response{} 245 | xml.Unmarshal([]byte(response), &doc) 246 | currentZoneName = doc.CurrentZoneName 247 | currentIcon = doc.CurrentIcon 248 | err = doc.Error() 249 | return 250 | } 251 | 252 | func (this *DeviceProperties) GetHouseholdID() (currentHouseholdId string, err error) { 253 | type Response struct { 254 | XMLName xml.Name 255 | CurrentHouseholdID string 256 | ErrorResponse 257 | } 258 | response := this.Svc.CallVa("GetHouseholdID") 259 | doc := Response{} 260 | xml.Unmarshal([]byte(response), &doc) 261 | currentHouseholdId = doc.CurrentHouseholdID 262 | err = doc.Error() 263 | return 264 | } 265 | 266 | // 267 | // The return value for the GetZoneInfo method 268 | // 269 | type ZoneInfo struct { 270 | // Appliance serial number 271 | SerialNumber string 272 | // Software version string 273 | SoftwareVersion string 274 | // Display software version string 275 | DisplaySoftwareVersion string 276 | // Hardware version 277 | HardwareVersion string 278 | // the IP address of the appliance 279 | IPAddress string 280 | // The hardware MAC address of the appliance 281 | MACAddress string 282 | // The Sonos Copyright statement 283 | CopyrightInfo string 284 | // ??? 285 | ExtraInfo string 286 | } 287 | 288 | // 289 | // Fetches basic properties of the appliance including IP address, 290 | // MAC address, and relevant hardware and software version. 291 | // 292 | func (this *DeviceProperties) GetZoneInfo() (*ZoneInfo, error) { 293 | type Response struct { 294 | XMLName xml.Name 295 | ZoneInfo 296 | ErrorResponse 297 | } 298 | response := this.Svc.CallVa("GetZoneInfo") 299 | doc := Response{} 300 | xml.Unmarshal([]byte(response), &doc) 301 | return &doc.ZoneInfo, doc.Error() 302 | } 303 | 304 | func (this *DeviceProperties) SetAutoplayLinkedZones(includeLinkedZones bool) (err error) { 305 | type Response struct { 306 | XMLName xml.Name 307 | ErrorResponse 308 | } 309 | args := []Arg{ 310 | {"IncludeLinkedZones", includeLinkedZones}, 311 | } 312 | response := this.Svc.Call("SetAutoplayLinkedZones", args) 313 | doc := Response{} 314 | xml.Unmarshal([]byte(response), &doc) 315 | err = doc.Error() 316 | return 317 | } 318 | 319 | func (this *DeviceProperties) GetAutoplayLinkedZones() (includeLinkedZones bool, err error) { 320 | type Response struct { 321 | XMLName xml.Name 322 | IncludeLinkedZones bool 323 | ErrorResponse 324 | } 325 | response := this.Svc.CallVa("GetAutoplayLinkedZones") 326 | doc := Response{} 327 | xml.Unmarshal([]byte(response), &doc) 328 | includeLinkedZones = doc.IncludeLinkedZones 329 | err = doc.Error() 330 | return 331 | } 332 | 333 | func (this *DeviceProperties) SetAutoplayRoomUUID(roomUUID string) (err error) { 334 | type Response struct { 335 | XMLName xml.Name 336 | ErrorResponse 337 | } 338 | args := []Arg{ 339 | {"RoomUUID", roomUUID}, 340 | } 341 | response := this.Svc.Call("SetAutoplayRoomUUID", args) 342 | doc := Response{} 343 | xml.Unmarshal([]byte(response), &doc) 344 | err = doc.Error() 345 | return 346 | } 347 | 348 | func (this *DeviceProperties) GetAutoplayRoomUUID() (roomUUID string, err error) { 349 | type Response struct { 350 | XMLName xml.Name 351 | RoomUUID string 352 | ErrorResponse 353 | } 354 | response := this.Svc.CallVa("GetAutoplayRoomUUID") 355 | doc := Response{} 356 | xml.Unmarshal([]byte(response), &doc) 357 | roomUUID = doc.RoomUUID 358 | err = doc.Error() 359 | return 360 | } 361 | 362 | func (this *DeviceProperties) SetAutoplayVolume(volume uint16) (err error) { 363 | type Response struct { 364 | XMLName xml.Name 365 | ErrorResponse 366 | } 367 | args := []Arg{ 368 | {"Volume", volume}, 369 | } 370 | response := this.Svc.Call("SetAutoplayVolume", args) 371 | doc := Response{} 372 | xml.Unmarshal([]byte(response), &doc) 373 | err = doc.Error() 374 | return 375 | } 376 | 377 | func (this *DeviceProperties) GetAutoplayVolume() (currentVolume uint16, err error) { 378 | type Response struct { 379 | XMLName xml.Name 380 | CurrentVolume uint16 381 | ErrorResponse 382 | } 383 | response := this.Svc.CallVa("GetAutoplayVolume") 384 | doc := Response{} 385 | xml.Unmarshal([]byte(response), &doc) 386 | currentVolume = doc.CurrentVolume 387 | err = doc.Error() 388 | return 389 | } 390 | 391 | func (this *DeviceProperties) ImportSetting(settingID uint32, settingURI string) (err error) { 392 | type Response struct { 393 | XMLName xml.Name 394 | ErrorResponse 395 | } 396 | args := []Arg{ 397 | {"SettingID", settingID}, 398 | {"SettingURI", settingURI}, 399 | } 400 | response := this.Svc.Call("ImportSettings", args) 401 | doc := Response{} 402 | xml.Unmarshal([]byte(response), &doc) 403 | err = doc.Error() 404 | return 405 | } 406 | 407 | func (this *DeviceProperties) SetUseAutoplayVolume(useVolume bool) (err error) { 408 | type Response struct { 409 | XMLName xml.Name 410 | ErrorResponse 411 | } 412 | args := []Arg{ 413 | {"UseVolume", useVolume}, 414 | } 415 | response := this.Svc.Call("SetUseAutoplayVolume", args) 416 | doc := Response{} 417 | xml.Unmarshal([]byte(response), &doc) 418 | err = doc.Error() 419 | return 420 | } 421 | 422 | func (this *DeviceProperties) GetUseAutoplayVolume() (useVolume bool, err error) { 423 | type Response struct { 424 | XMLName xml.Name 425 | UseVolume bool 426 | ErrorResponse 427 | } 428 | response := this.Svc.CallVa("GetUseAutoplayVolume") 429 | doc := Response{} 430 | xml.Unmarshal([]byte(response), &doc) 431 | useVolume = doc.UseVolume 432 | err = doc.Error() 433 | return 434 | } 435 | 436 | func (this *DeviceProperties) AddHTSatellite(htSatChanMapSet string) error { 437 | type Response struct { 438 | XMLName xml.Name 439 | ErrorResponse 440 | } 441 | args := []Arg{ 442 | {"HTSatChanMapSet", htSatChanMapSet}, 443 | } 444 | response := this.Svc.Call("AddHTSatellite", args) 445 | doc := Response{} 446 | xml.Unmarshal([]byte(response), &doc) 447 | return doc.Error() 448 | } 449 | 450 | func (this *DeviceProperties) RemoveHTSatellite(satRoomUUID string) error { 451 | type Response struct { 452 | XMLName xml.Name 453 | ErrorResponse 454 | } 455 | args := []Arg{ 456 | {"SatRoomUUID", satRoomUUID}, 457 | } 458 | response := this.Svc.Call("RemoveHTSatellite", args) 459 | doc := Response{} 460 | xml.Unmarshal([]byte(response), &doc) 461 | return doc.Error() 462 | } 463 | 464 | func (this *DeviceProperties) EnterConfigMode(mode, options string) (state string, err error) { 465 | type Response struct { 466 | XMLName xml.Name 467 | State string 468 | ErrorResponse 469 | } 470 | args := []Arg{ 471 | {"Mode", mode}, 472 | {"Options", options}, 473 | } 474 | response := this.Svc.Call("EnterConfigMode", args) 475 | doc := Response{} 476 | xml.Unmarshal([]byte(response), &doc) 477 | state = doc.State 478 | err = doc.Error() 479 | return 480 | } 481 | 482 | func (this *DeviceProperties) ExitConfigMode(options string) error { 483 | type Response struct { 484 | XMLName xml.Name 485 | ErrorResponse 486 | } 487 | args := []Arg{ 488 | {"Options", options}, 489 | } 490 | response := this.Svc.Call("ExitConfigMode", args) 491 | doc := Response{} 492 | xml.Unmarshal([]byte(response), &doc) 493 | return doc.Error() 494 | } 495 | 496 | func (this *DeviceProperties) GetButtonState() (state string, err error) { 497 | type Response struct { 498 | XMLName xml.Name 499 | State string 500 | ErrorResponse 501 | } 502 | response := this.Svc.CallVa("GetButtonState") 503 | doc := Response{} 504 | xml.Unmarshal([]byte(response), &doc) 505 | state = doc.State 506 | err = doc.Error() 507 | return 508 | } 509 | -------------------------------------------------------------------------------- /upnp/GroupManagement.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package upnp 32 | 33 | import ( 34 | "encoding/xml" 35 | _ "log" 36 | ) 37 | 38 | var ( 39 | GroupManagement_EventType = registerEventType("GroupManagement") 40 | ) 41 | 42 | type GroupManagementState struct { 43 | GroupCoordinatorIsLocal bool 44 | LocalGroupUUID string 45 | ResetVolumeAfter bool 46 | VolumeAVTransportURI string 47 | } 48 | 49 | type GroupManagementEvent struct { 50 | GroupManagementState 51 | Svc *Service 52 | } 53 | 54 | func (this GroupManagementEvent) Service() *Service { 55 | return this.Svc 56 | } 57 | 58 | func (this GroupManagementEvent) Type() int { 59 | return GroupManagement_EventType 60 | } 61 | 62 | type GroupManagement struct { 63 | GroupManagementState 64 | Svc *Service 65 | } 66 | 67 | func (this *GroupManagement) BeginSet(svc *Service, channel chan Event) { 68 | } 69 | 70 | type groupManagementUpdate_XML struct { 71 | XMLName xml.Name `xml:"GroupManagementState"` 72 | Value string `xml:",innerxml"` 73 | } 74 | 75 | func (this *GroupManagement) HandleProperty(svc *Service, value string, channel chan Event) error { 76 | update := groupManagementUpdate_XML{ 77 | Value: value, 78 | } 79 | if bytes, err := xml.Marshal(update); nil != err { 80 | return err 81 | } else { 82 | xml.Unmarshal(bytes, &this.GroupManagementState) 83 | } 84 | return nil 85 | } 86 | 87 | func (this *GroupManagement) EndSet(svc *Service, channel chan Event) { 88 | evt := GroupManagementEvent{GroupManagementState: this.GroupManagementState, Svc: svc} 89 | channel <- evt 90 | } 91 | 92 | type MemberInfo struct { 93 | CurrentTransportSettings string 94 | GroupUUIDJoined string 95 | ResetVolumeAfter bool 96 | VolumeAVTransportURI string 97 | } 98 | 99 | func (this *GroupManagement) AddMember(memberId string) (memberInfo *MemberInfo, err error) { 100 | type Response struct { 101 | XMLName xml.Name 102 | MemberInfo 103 | ErrorResponse 104 | } 105 | args := []Arg{ 106 | {"MemberID", memberId}, 107 | } 108 | response := this.Svc.Call("AddMember", args) 109 | doc := Response{} 110 | xml.Unmarshal([]byte(response), &doc) 111 | memberInfo = &doc.MemberInfo 112 | err = doc.Error() 113 | return 114 | } 115 | 116 | func (this *GroupManagement) RemoveMember(memberId string) (err error) { 117 | type Response struct { 118 | XMLName xml.Name 119 | ErrorResponse 120 | } 121 | args := []Arg{ 122 | {"MemberID", memberId}, 123 | } 124 | response := this.Svc.Call("RemoveMember", args) 125 | doc := Response{} 126 | xml.Unmarshal([]byte(response), &doc) 127 | err = doc.Error() 128 | return 129 | } 130 | 131 | func (this *GroupManagement) ReportTrackBufferingResult(memberId string, resultCode int32) (err error) { 132 | type Response struct { 133 | XMLName xml.Name 134 | ErrorResponse 135 | } 136 | args := []Arg{ 137 | {"MemberID", memberId}, 138 | {"ResultCode", resultCode}, 139 | } 140 | response := this.Svc.Call("ReportTrackBufferingResult", args) 141 | doc := Response{} 142 | xml.Unmarshal([]byte(response), &doc) 143 | err = doc.Error() 144 | return 145 | } 146 | -------------------------------------------------------------------------------- /upnp/MusicServices.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package upnp 32 | 33 | import ( 34 | "encoding/xml" 35 | _ "log" 36 | ) 37 | 38 | var ( 39 | MusicServices_EventType = registerEventType("MusicServices") 40 | ) 41 | 42 | type MusicServicesState struct { 43 | ServiceListVersion string 44 | } 45 | 46 | type MusicServicesEvent struct { 47 | MusicServicesState 48 | Svc *Service 49 | } 50 | 51 | func (this MusicServicesEvent) Service() *Service { 52 | return this.Svc 53 | } 54 | 55 | func (this MusicServicesEvent) Type() int { 56 | return MusicServices_EventType 57 | } 58 | 59 | type MusicServices struct { 60 | MusicServicesState 61 | Svc *Service 62 | } 63 | 64 | func (this *MusicServices) BeginSet(svc *Service, channel chan Event) { 65 | } 66 | 67 | type musicServicesUpdate_XML struct { 68 | XMLName xml.Name `xml:"MusicServicesState"` 69 | Value string `xml:",innerxml"` 70 | } 71 | 72 | func (this *MusicServices) HandleProperty(svc *Service, value string, channel chan Event) error { 73 | update := musicServicesUpdate_XML{ 74 | Value: value, 75 | } 76 | if bytes, err := xml.Marshal(update); nil != err { 77 | return err 78 | } else { 79 | xml.Unmarshal(bytes, &this.MusicServicesState) 80 | } 81 | return nil 82 | } 83 | 84 | func (this *MusicServices) EndSet(svc *Service, channel chan Event) { 85 | evt := MusicServicesEvent{MusicServicesState: this.MusicServicesState, Svc: svc} 86 | channel <- evt 87 | } 88 | 89 | type msPolicy_XML struct { 90 | XMLName xml.Name 91 | Auth string `xml:"Auth,attr"` 92 | PollInterval string `xml:"PollInterval,attr"` 93 | } 94 | 95 | type msStrings_XML struct { 96 | XMLName xml.Name 97 | Version string `xml:"Version,attr"` 98 | Uri string `xml:"Uri,attr"` 99 | } 100 | 101 | type msPresentationMap_XML struct { 102 | XMLName xml.Name 103 | Version string `xml:"Version,attr"` 104 | Uri string `xml:"Uri,attr"` 105 | } 106 | 107 | type msPresentation_XML struct { 108 | XMLName xml.Name 109 | Strings []msStrings_XML `xml:"Strings"` 110 | PresentationMap []msPresentationMap_XML `xml:"PresentationMap"` 111 | } 112 | 113 | type msService_XML struct { 114 | XMLName xml.Name 115 | Id string `xml:"Id,attr"` 116 | Name string `xml:"Name,attr"` 117 | Version string `xml:"Version,attr"` 118 | Uri string `xml:"Uri,attr"` 119 | SecureUri string `xml:"SecureUri,attr"` 120 | ContainerType string `xml:"ContainerType,attr"` 121 | Capabilities string `xml:"Capabilities,attr"` 122 | MaxMessagingChars string `xml:"MaxMessagingChars,attr"` 123 | Policy []msPolicy_XML `xml:"Policy"` 124 | Presentation []msPresentation_XML `xml:"Presentation"` 125 | } 126 | 127 | type msServices_XML struct { 128 | XMLName xml.Name 129 | Service []msService_XML `xml:"Service"` 130 | } 131 | 132 | func (this *MusicServices) GetSessionId(serviceId int16, username string) (sessionId string, err error) { 133 | type Response struct { 134 | XMLName xml.Name 135 | SessionId string 136 | ErrorResponse 137 | } 138 | args := []Arg{ 139 | {"ServiceId", serviceId}, 140 | {"Username", username}, 141 | } 142 | response := this.Svc.Call("GetSessionId", args) 143 | doc := Response{} 144 | xml.Unmarshal([]byte(response), &doc) 145 | sessionId = doc.SessionId 146 | err = doc.Error() 147 | return 148 | } 149 | 150 | func (this *MusicServices) ListAvailableServices() (err error) { 151 | type Response struct { 152 | XMLName xml.Name 153 | AvailableServiceDescriptorList string 154 | AvailableServiceTypeList string 155 | AvailableServiceListVersion string 156 | ErrorResponse 157 | } 158 | response := this.Svc.CallVa("ListAvailableServices") 159 | doc := Response{} 160 | xml.Unmarshal([]byte(response), &doc) 161 | services := msServices_XML{} 162 | xml.Unmarshal([]byte(doc.AvailableServiceDescriptorList), &services) 163 | err = doc.Error() 164 | // TODO: Return value 165 | return 166 | } 167 | 168 | func (this *MusicServices) UpdateAvailableServices() (err error) { 169 | type Response struct { 170 | XMLName xml.Name 171 | ErrorResponse 172 | } 173 | response := this.Svc.CallVa("UpdateAvailableServices") 174 | doc := Response{} 175 | xml.Unmarshal([]byte(response), &doc) 176 | err = doc.Error() 177 | return 178 | } 179 | -------------------------------------------------------------------------------- /upnp/SystemProperties.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package upnp 32 | 33 | import ( 34 | "encoding/xml" 35 | _ "log" 36 | ) 37 | 38 | var ( 39 | SystemProperties_EventType = registerEventType("SystemProperties") 40 | ) 41 | 42 | type SystemPropertiesState struct { 43 | } 44 | 45 | type SystemPropertiesEvent struct { 46 | SystemPropertiesState 47 | Svc *Service 48 | } 49 | 50 | func (this SystemPropertiesEvent) Service() *Service { 51 | return this.Svc 52 | } 53 | 54 | func (this SystemPropertiesEvent) Type() int { 55 | return SystemProperties_EventType 56 | } 57 | 58 | type SystemProperties struct { 59 | SystemPropertiesState 60 | Svc *Service 61 | } 62 | 63 | func (this *SystemProperties) BeginSet(svc *Service, channel chan Event) { 64 | } 65 | 66 | type systemPropertiesUpdate_XML struct { 67 | XMLName xml.Name `xml:"SystemPropertiesState"` 68 | Value string `xml:",innerxml"` 69 | } 70 | 71 | func (this *SystemProperties) HandleProperty(svc *Service, value string, channel chan Event) error { 72 | update := systemPropertiesUpdate_XML{ 73 | Value: value, 74 | } 75 | if bytes, err := xml.Marshal(update); nil != err { 76 | return err 77 | } else { 78 | xml.Unmarshal(bytes, &this.SystemPropertiesState) 79 | } 80 | return nil 81 | } 82 | 83 | func (this *SystemProperties) EndSet(svc *Service, channel chan Event) { 84 | evt := SystemPropertiesEvent{SystemPropertiesState: this.SystemPropertiesState, Svc: svc} 85 | channel <- evt 86 | } 87 | 88 | func (this *SystemProperties) SetString(variableName, stringValue string) (err error) { 89 | type Response struct { 90 | XMLName xml.Name 91 | ErrorResponse 92 | } 93 | args := []Arg{ 94 | {"VariableName", variableName}, 95 | {"StringValue", stringValue}, 96 | } 97 | response := this.Svc.Call("SetString", args) 98 | doc := Response{} 99 | xml.Unmarshal([]byte(response), &doc) 100 | err = doc.Error() 101 | return 102 | } 103 | 104 | func (this *SystemProperties) SetStringX(variableName, stringValue string) (err error) { 105 | type Response struct { 106 | XMLName xml.Name 107 | ErrorResponse 108 | } 109 | args := []Arg{ 110 | {"VariableName", variableName}, 111 | {"StringValue", stringValue}, 112 | } 113 | response := this.Svc.Call("SetStringX", args) 114 | doc := Response{} 115 | xml.Unmarshal([]byte(response), &doc) 116 | err = doc.Error() 117 | return 118 | } 119 | 120 | func (this *SystemProperties) GetString(variableName string) (stringValue string, err error) { 121 | type Response struct { 122 | XMLName xml.Name 123 | StringValue string 124 | ErrorResponse 125 | } 126 | args := []Arg{ 127 | {"VariableName", variableName}, 128 | } 129 | response := this.Svc.Call("GetString", args) 130 | doc := Response{} 131 | xml.Unmarshal([]byte(response), &doc) 132 | stringValue = doc.StringValue 133 | err = doc.Error() 134 | return 135 | } 136 | 137 | func (this *SystemProperties) GetStringX(variableName string) (stringValue string, err error) { 138 | type Response struct { 139 | XMLName xml.Name 140 | StringValue string 141 | ErrorResponse 142 | } 143 | args := []Arg{ 144 | {"VariableName", variableName}, 145 | } 146 | response := this.Svc.Call("GetStringX", args) 147 | doc := Response{} 148 | xml.Unmarshal([]byte(response), &doc) 149 | stringValue = doc.StringValue 150 | err = doc.Error() 151 | return 152 | } 153 | 154 | func (this *SystemProperties) Remove(variableName string) (err error) { 155 | type Response struct { 156 | XMLName xml.Name 157 | ErrorResponse 158 | } 159 | args := []Arg{ 160 | {"VariableName", variableName}, 161 | } 162 | response := this.Svc.Call("Remove", args) 163 | doc := Response{} 164 | xml.Unmarshal([]byte(response), &doc) 165 | err = doc.Error() 166 | return 167 | } 168 | 169 | func (this *SystemProperties) GetWebCode(accountType uint32) (webCode string, err error) { 170 | type Response struct { 171 | XMLName xml.Name 172 | WebCode string 173 | ErrorResponse 174 | } 175 | args := []Arg{ 176 | {"AccountType", accountType}, 177 | } 178 | response := this.Svc.Call("GetWebCode", args) 179 | doc := Response{} 180 | xml.Unmarshal([]byte(response), &doc) 181 | webCode = doc.WebCode 182 | err = doc.Error() 183 | return 184 | } 185 | 186 | func (this *SystemProperties) ProvisionTrialAccount(accountType uint32) (err error) { 187 | type Response struct { 188 | XMLName xml.Name 189 | ErrorResponse 190 | } 191 | args := []Arg{ 192 | {"AccountType", accountType}, 193 | } 194 | response := this.Svc.Call("ProvisionTrialAccount", args) 195 | doc := Response{} 196 | xml.Unmarshal([]byte(response), &doc) 197 | err = doc.Error() 198 | return 199 | } 200 | 201 | func (this *SystemProperties) ProvisionCredentialedTrialAccountX(accountType uint32, accountId, accountPassword string) (isExpired bool, 202 | err error) { 203 | type Response struct { 204 | XMLName xml.Name 205 | IsExpired bool 206 | ErrorResponse 207 | } 208 | args := []Arg{ 209 | {"AccountType", accountType}, 210 | {"AccountID", accountId}, 211 | {"AccountPassword", accountPassword}, 212 | } 213 | response := this.Svc.Call("ProvisionCredentialedTrialAccountX", args) 214 | doc := Response{} 215 | xml.Unmarshal([]byte(response), &doc) 216 | isExpired = doc.IsExpired 217 | err = doc.Error() 218 | return 219 | } 220 | 221 | func (this *SystemProperties) MigrateTrialAccountX(accountType uint32, accountId, accountPassword string) (err error) { 222 | type Response struct { 223 | XMLName xml.Name 224 | ErrorResponse 225 | } 226 | args := []Arg{ 227 | {"AccountType", accountType}, 228 | {"AccountID", accountId}, 229 | {"AccountPassword", accountPassword}, 230 | } 231 | response := this.Svc.Call("MigrateTrialAccountX", args) 232 | doc := Response{} 233 | xml.Unmarshal([]byte(response), &doc) 234 | err = doc.Error() 235 | return 236 | } 237 | 238 | func (this *SystemProperties) AddAccountX(accountType uint32, accountId, accountPassword string) (err error) { 239 | type Response struct { 240 | XMLName xml.Name 241 | ErrorResponse 242 | } 243 | args := []Arg{ 244 | {"AccountType", accountType}, 245 | {"AccountID", accountId}, 246 | {"AccountPassword", accountPassword}, 247 | } 248 | response := this.Svc.Call("AddAccountX", args) 249 | doc := Response{} 250 | xml.Unmarshal([]byte(response), &doc) 251 | err = doc.Error() 252 | return 253 | } 254 | 255 | func (this *SystemProperties) AddAccountWithCredentialsX(accountType uint32, accountToken, accountKey string) (err error) { 256 | type Response struct { 257 | XMLName xml.Name 258 | ErrorResponse 259 | } 260 | args := []Arg{ 261 | {"AccountType", accountType}, 262 | {"AccountToken", accountToken}, 263 | {"AccountKey", accountKey}, 264 | } 265 | response := this.Svc.Call("AddAccountWithCredentialsX", args) 266 | doc := Response{} 267 | xml.Unmarshal([]byte(response), &doc) 268 | err = doc.Error() 269 | return 270 | } 271 | 272 | func (this *SystemProperties) RemoveAccount(accountType uint32, accountId string) (err error) { 273 | type Response struct { 274 | XMLName xml.Name 275 | ErrorResponse 276 | } 277 | args := []Arg{ 278 | {"AccountType", accountType}, 279 | {"AccountID", accountId}, 280 | } 281 | response := this.Svc.Call("RemoveAccount", args) 282 | doc := Response{} 283 | xml.Unmarshal([]byte(response), &doc) 284 | err = doc.Error() 285 | return 286 | } 287 | 288 | func (this *SystemProperties) EditAccountPasswordX(accountType uint32, accountId, newAccountPassword string) (err error) { 289 | type Response struct { 290 | XMLName xml.Name 291 | ErrorResponse 292 | } 293 | args := []Arg{ 294 | {"AccountType", accountType}, 295 | {"AccountID", accountId}, 296 | {"NewAccountPassword", newAccountPassword}, 297 | } 298 | response := this.Svc.Call("EditAccountPasswordX", args) 299 | doc := Response{} 300 | xml.Unmarshal([]byte(response), &doc) 301 | err = doc.Error() 302 | return 303 | } 304 | 305 | func (this *SystemProperties) EditAccountMd(accountType uint32, accountId, accountMd string) (err error) { 306 | type Response struct { 307 | XMLName xml.Name 308 | ErrorResponse 309 | } 310 | args := []Arg{ 311 | {"AccountType", accountType}, 312 | {"AccountID", accountId}, 313 | {"AccountMD", accountMd}, 314 | } 315 | response := this.Svc.Call("EditAccountMd", args) 316 | doc := Response{} 317 | xml.Unmarshal([]byte(response), &doc) 318 | err = doc.Error() 319 | return 320 | } 321 | 322 | func (this *SystemProperties) DoPostUpdateTasks() (err error) { 323 | type Response struct { 324 | XMLName xml.Name 325 | ErrorResponse 326 | } 327 | response := this.Svc.CallVa("DoPostUpdateTasks") 328 | doc := Response{} 329 | xml.Unmarshal([]byte(response), &doc) 330 | err = doc.Error() 331 | return 332 | } 333 | 334 | func (this *SystemProperties) ResetThirdPartyCredentials() (err error) { 335 | type Response struct { 336 | XMLName xml.Name 337 | ErrorResponse 338 | } 339 | response := this.Svc.CallVa("ResetThirdPartyCredentials") 340 | doc := Response{} 341 | xml.Unmarshal([]byte(response), &doc) 342 | err = doc.Error() 343 | return 344 | } 345 | 346 | func (this *SystemProperties) RemoveX(variableName string) error { 347 | type Response struct { 348 | XMLName xml.Name 349 | ErrorResponse 350 | } 351 | args := []Arg{ 352 | {"VariableName", variableName}, 353 | } 354 | response := this.Svc.Call("RemoveX", args) 355 | doc := Response{} 356 | xml.Unmarshal([]byte(response), &doc) 357 | return doc.Error() 358 | } 359 | 360 | func (this *SystemProperties) EnableRDM(rdmValue bool) error { 361 | type Response struct { 362 | XMLName xml.Name 363 | ErrorResponse 364 | } 365 | args := []Arg{ 366 | {"RDMValue", rdmValue}, 367 | } 368 | response := this.Svc.Call("EnableRDM", args) 369 | doc := Response{} 370 | xml.Unmarshal([]byte(response), &doc) 371 | return doc.Error() 372 | } 373 | 374 | func (this *SystemProperties) GetRDM() (rdmValue bool, err error) { 375 | type Response struct { 376 | XMLName xml.Name 377 | RDMValue bool 378 | ErrorResponse 379 | } 380 | response := this.Svc.CallVa("GetRDM") 381 | doc := Response{} 382 | xml.Unmarshal([]byte(response), &doc) 383 | return doc.RDMValue, doc.Error() 384 | } 385 | 386 | func (this *SystemProperties) ApplyRDMDefaultSettings() error { 387 | type Response struct { 388 | XMLName xml.Name 389 | ErrorResponse 390 | } 391 | response := this.Svc.CallVa("ApplyRDMDefaultSettings") 392 | doc := Response{} 393 | xml.Unmarshal([]byte(response), &doc) 394 | return doc.Error() 395 | } 396 | 397 | func (this *SystemProperties) RefreshAccountCredentialsX(accountType uint32, accountToken, accountKey string) error { 398 | type Response struct { 399 | XMLName xml.Name 400 | ErrorResponse 401 | } 402 | args := []Arg{ 403 | {"AccountType", accountType}, 404 | {"AccountToken,", accountToken}, 405 | {"AccountKey,", accountKey}, 406 | } 407 | response := this.Svc.Call("RefreshAccountCredentialsX", args) 408 | doc := Response{} 409 | xml.Unmarshal([]byte(response), &doc) 410 | return doc.Error() 411 | } 412 | 413 | type A_ARG_TYPE_AccountType uint32 414 | type A_ARG_TYPE_AccountCredential string 415 | type A_ARG_TYPE_OAuthDeviceID string 416 | type A_ARG_TYPE_AccountUDN string 417 | 418 | func (this *SystemProperties) AddOAuthAccountX(accountType A_ARG_TYPE_AccountType, accountToken, accountKey A_ARG_TYPE_AccountCredential, 419 | oauthDeviceID A_ARG_TYPE_OAuthDeviceID) (accountUDN A_ARG_TYPE_AccountUDN, err error) { 420 | type Response struct { 421 | XMLName xml.Name 422 | AccountUDN A_ARG_TYPE_AccountUDN 423 | ErrorResponse 424 | } 425 | args := []Arg{ 426 | {"AccountType", accountType}, 427 | {"AccountToken,", accountToken}, 428 | {"AccountKey,", accountKey}, 429 | {"OAuthDeviceID,", oauthDeviceID}, 430 | } 431 | response := this.Svc.Call("AddOAuthAccountX", args) 432 | doc := Response{} 433 | xml.Unmarshal([]byte(response), &doc) 434 | accountUDN = doc.AccountUDN 435 | err = doc.Error() 436 | return 437 | } 438 | 439 | type A_ARG_TYPE_AccountNickname string 440 | 441 | func (this *SystemProperties) SetAccountNicknameX(accountUDN A_ARG_TYPE_AccountUDN, accountNickname A_ARG_TYPE_AccountNickname) error { 442 | type Response struct { 443 | XMLName xml.Name 444 | ErrorResponse 445 | } 446 | args := []Arg{ 447 | {"AccountUDN,", accountUDN}, 448 | {"AccountNickname,", accountNickname}, 449 | } 450 | response := this.Svc.Call("SetAccountNicknameX", args) 451 | doc := Response{} 452 | xml.Unmarshal([]byte(response), &doc) 453 | return doc.Error() 454 | } 455 | 456 | type A_ARG_TYPE_AccountID string 457 | type A_ARG_TYPE_AccountPassword string 458 | 459 | func (this *SystemProperties) ReplaceAccountX(accountUDN A_ARG_TYPE_AccountUDN, newAccountID A_ARG_TYPE_AccountID, 460 | newAccountPassword A_ARG_TYPE_AccountPassword) (newAccountUDN A_ARG_TYPE_AccountUDN, err error) { 461 | type Response struct { 462 | XMLName xml.Name 463 | NewAccountUDN A_ARG_TYPE_AccountUDN 464 | ErrorResponse 465 | } 466 | args := []Arg{ 467 | {"AccountUDN", accountUDN}, 468 | {"NewAccountID,", newAccountID}, 469 | {"NewAccountPassword,", newAccountPassword}, 470 | } 471 | response := this.Svc.Call("ReplaceAccountX", args) 472 | doc := Response{} 473 | xml.Unmarshal([]byte(response), &doc) 474 | newAccountUDN = doc.NewAccountUDN 475 | err = doc.Error() 476 | return 477 | } 478 | -------------------------------------------------------------------------------- /upnp/ZoneGroupTopology.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package upnp 32 | 33 | import ( 34 | "encoding/xml" 35 | _ "log" 36 | ) 37 | 38 | var ( 39 | ZoneGroupTopology_EventType = registerEventType("ZoneGroupTopology") 40 | ) 41 | 42 | type ZoneGroupTopologyState struct { 43 | ZoneGroupState string 44 | ThirdPartyMediaServersX string 45 | AvailableSoftwareUpdate string // TODO: Unpack 46 | AlarmRunSequence string 47 | ZoneGroupName string 48 | ZoneGroupID string 49 | ZonePlayerUUIDsInGroup string 50 | } 51 | 52 | type ZoneGroupTopologyEvent struct { 53 | ZoneGroupTopologyState 54 | Svc *Service 55 | } 56 | 57 | func (this ZoneGroupTopologyEvent) Service() *Service { 58 | return this.Svc 59 | } 60 | 61 | func (this ZoneGroupTopologyEvent) Type() int { 62 | return ZoneGroupTopology_EventType 63 | } 64 | 65 | type ZoneGroupTopology struct { 66 | ZoneGroupTopologyState 67 | Svc *Service 68 | } 69 | 70 | func (this *ZoneGroupTopology) BeginSet(svc *Service, channel chan Event) { 71 | } 72 | 73 | type zoneGroupTopologyUpdate_XML struct { 74 | XMLName xml.Name `xml:"ZoneGroupTopologyState"` 75 | Value string `xml:",innerxml"` 76 | } 77 | 78 | func (this *ZoneGroupTopology) HandleProperty(svc *Service, value string, channel chan Event) error { 79 | update := zoneGroupTopologyUpdate_XML{ 80 | Value: value, 81 | } 82 | if bytes, err := xml.Marshal(update); nil != err { 83 | return err 84 | } else { 85 | xml.Unmarshal(bytes, &this.ZoneGroupTopologyState) 86 | } 87 | return nil 88 | } 89 | 90 | func (this *ZoneGroupTopology) EndSet(svc *Service, channel chan Event) { 91 | evt := ZoneGroupTopologyEvent{ZoneGroupTopologyState: this.ZoneGroupTopologyState, Svc: svc} 92 | channel <- evt 93 | } 94 | 95 | const ( 96 | ALL = "All" 97 | SOFTWARE = "Software" 98 | ) 99 | 100 | func (this *ZoneGroupTopology) BeginSoftwareUpdate(updateURL string, flags uint32) (err error) { 101 | type Response struct { 102 | XMLName xml.Name 103 | ErrorResponse 104 | } 105 | args := []Arg{ 106 | {"UpdateURL", updateURL}, 107 | {"Flags", flags}, 108 | } 109 | response := this.Svc.Call("BeginSoftwareUpdate", args) 110 | doc := Response{} 111 | xml.Unmarshal([]byte(response), &doc) 112 | err = doc.Error() 113 | return 114 | } 115 | 116 | type UpdateItem struct { 117 | Type string `xml:"Type,attr"` 118 | Version string `xml:"Version,attr"` 119 | UpdateURL string `xml:"UpdateURL,attr"` 120 | DownloadSize string `xml:"DownloadSize,attr"` 121 | ManifestURL string `xml:"ManifestURL,attr"` 122 | } 123 | 124 | type UpdateType string 125 | 126 | func (this *ZoneGroupTopology) CheckForUpdate(updateType UpdateType, cachedOnly bool, version string) (updateItem *UpdateItem, err error) { 127 | type UpdateItemHolder struct { 128 | XMLName xml.Name 129 | UpdateItem 130 | } 131 | type UpdateItemText struct { 132 | XMLName xml.Name 133 | Text string `xml:",chardata"` 134 | } 135 | type Response struct { 136 | XMLName xml.Name 137 | UpdateItem UpdateItemText 138 | ErrorResponse 139 | } 140 | args := []Arg{ 141 | {"UpdateType", updateType}, 142 | {"CachedOnly", cachedOnly}, 143 | {"Version", version}, 144 | } 145 | response := this.Svc.Call("CheckForUpdate", args) 146 | doc := Response{} 147 | xml.Unmarshal([]byte(response), &doc) 148 | rec := UpdateItemHolder{} 149 | xml.Unmarshal([]byte(doc.UpdateItem.Text), &rec) 150 | updateItem = &rec.UpdateItem 151 | err = doc.Error() 152 | return 153 | } 154 | 155 | const ( 156 | REMOVE = "Remove" 157 | VERIFY_THEN_REMOVE_SYSTEMWIDE = "VerifyThenRemoveSystemwide" 158 | ) 159 | 160 | func (this *ZoneGroupTopology) ReportUnresponsiveDevice(deviceUUID string, desiredAction string) (err error) { 161 | type Response struct { 162 | XMLName xml.Name 163 | ErrorResponse 164 | } 165 | args := []Arg{ 166 | {"DeviceUUID", deviceUUID}, 167 | {"DesiredAction", desiredAction}, 168 | } 169 | response := this.Svc.Call("ReportUnresponsiveDevice", args) 170 | doc := Response{} 171 | xml.Unmarshal([]byte(response), &doc) 172 | err = doc.Error() 173 | return 174 | } 175 | 176 | func (this *ZoneGroupTopology) ReportAlarmStartedRunning() (err error) { 177 | type Response struct { 178 | XMLName xml.Name 179 | ErrorResponse 180 | } 181 | response := this.Svc.CallVa("ReportAlarmStartedRunning") 182 | doc := Response{} 183 | xml.Unmarshal([]byte(response), &doc) 184 | err = doc.Error() 185 | return 186 | } 187 | 188 | func (this *ZoneGroupTopology) SubmitDiagnostics() (diagnosticId string, err error) { 189 | type Response struct { 190 | XMLName xml.Name 191 | DiagnosticID string 192 | ErrorResponse 193 | } 194 | response := this.Svc.CallVa("SubmitDiagnostics") 195 | doc := Response{} 196 | xml.Unmarshal([]byte(response), &doc) 197 | diagnosticId = doc.DiagnosticID 198 | err = doc.Error() 199 | return 200 | } 201 | 202 | func (this *ZoneGroupTopology) RegisterMobileDevice(deviceName, deviceUDN, deviceAddress string) (err error) { 203 | type Response struct { 204 | XMLName xml.Name 205 | ErrorResponse 206 | } 207 | args := []Arg{ 208 | {"MobileDeviceName", deviceName}, 209 | {"MobileDeviceUDN", deviceUDN}, 210 | {"MobileIPAndPort", deviceAddress}, 211 | } 212 | response := this.Svc.Call("RegisterMobileDevice", args) 213 | doc := Response{} 214 | xml.Unmarshal([]byte(response), &doc) 215 | err = doc.Error() 216 | return 217 | } 218 | 219 | type ZoneGroupAttributes struct { 220 | CurrentZoneGroupName string 221 | CurrentZoneGroupID string 222 | ZonePlayerUUIDsInGroup string 223 | } 224 | 225 | func (this *ZoneGroupTopology) GetZoneGroupAttributes() (*ZoneGroupAttributes, error) { 226 | type Response struct { 227 | XMLName xml.Name 228 | ZoneGroupAttributes 229 | ErrorResponse 230 | } 231 | response := this.Svc.CallVa("GetZoneGroupAttributes") 232 | doc := Response{} 233 | xml.Unmarshal([]byte(response), &doc) 234 | return &doc.ZoneGroupAttributes, doc.Error() 235 | } 236 | 237 | type ZoneGroupMember struct { 238 | XMLName xml.Name 239 | UUID string `xml:"UUID,attr"` 240 | Location string `xml:"Location,attr"` 241 | ZoneName string `xml:"ZoneName,attr"` 242 | Icon string `xml:"Icon,attr"` 243 | Configuration string `xml:"Configuration,attr"` 244 | Invisible string `xml:"Invisible,attr"` 245 | IsZoneBridge string `xml:"IsZoneBridge,attr"` 246 | SoftwareVersion string `xml:"SoftwareVersion,attr"` 247 | MinCompatibleVersion string `xml:"MinCompatibleVersion,attr"` 248 | BootSeq string `xml:"BootSeq,attr"` 249 | } 250 | 251 | type ZoneGroup struct { 252 | XMLName xml.Name 253 | Coordinator string `xml:"Coordinator,attr"` 254 | ID string `xml:"ID,attr"` 255 | ZoneGroupMember []ZoneGroupMember 256 | } 257 | 258 | type ZoneGroups struct { 259 | XMLName xml.Name 260 | ZoneGroup []ZoneGroup 261 | } 262 | 263 | func (this *ZoneGroupTopology) GetZoneGroupState() (*ZoneGroups, error) { 264 | type Response struct { 265 | XMLName xml.Name 266 | ZoneGroupState string 267 | ErrorResponse 268 | } 269 | response := this.Svc.CallVa("GetZoneGroupState") 270 | doc := Response{} 271 | xml.Unmarshal([]byte(response), &doc) 272 | state := ZoneGroups{} 273 | xml.Unmarshal([]byte(doc.ZoneGroupState), &state) 274 | return &state, doc.Error() 275 | } 276 | -------------------------------------------------------------------------------- /upnp/device.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package upnp 32 | 33 | import ( 34 | "encoding/xml" 35 | "errors" 36 | "fmt" 37 | "github.com/ianr0bkny/go-sonos/ssdp" 38 | "io/ioutil" 39 | "log" 40 | "net/http" 41 | "net/url" 42 | "path" 43 | "regexp" 44 | "time" 45 | ) 46 | 47 | var upnpOtherDeviceRegex *regexp.Regexp 48 | var upnpOtherServiceRegex *regexp.Regexp 49 | 50 | func init() { 51 | upnpOtherDeviceRegex = regexp.MustCompile("^urn:([^:]+):device:([^:]+)(:(.+))?$") 52 | upnpOtherServiceRegex = regexp.MustCompile("^urn:([^:]+):service:([^:]+)(:(.+))?$") 53 | } 54 | 55 | type upnpSpecVersion_XML struct { 56 | Major int `xml:"major"` 57 | Minor int `xml:"minor"` 58 | } 59 | 60 | type upnpDevice_XML struct { 61 | DeviceType string `xml:"deviceType"` 62 | FriendlyName string `xml:"friendlyName"` 63 | Manufacturer string `xml:"manufacturer"` 64 | ManufacturerURL string `xml:"manufacturerURL"` 65 | ModelNumber string `xml:"modelNumber"` 66 | ModelDescription string `xml:"modelDescription"` 67 | ModelName string `xml:"modelName"` 68 | ModelURL string `xml:"modelURL"` 69 | SoftwareVersion string `xml:"softwareVersion"` 70 | HardwareVersion string `xml:"hardwareVersion"` 71 | SerialNum string `xml:"serialNum"` 72 | UDN string `xml:"UDN"` 73 | IconList upnpIconList_XML `xml:"iconList"` 74 | MinCompatibleVersion string `xml:"minCompatibleVersion"` 75 | DisplayVersion string `xml:"displayVersion"` 76 | ExtraVersion string `xml:"extraVersion"` 77 | RoomName string `xml:"roomName"` 78 | DisplayName string `xml:"displayName"` 79 | ZoneType string `xml:"zoneType"` 80 | Feature1 string `xml:"feature1"` 81 | Feature2 string `xml:"feature2"` 82 | Feature3 string `xml:"feature3"` 83 | InternalSpeakerSize string `xml:"internalSpeakerSize"` 84 | ServiceList upnpServiceList_XML `xml:"serviceList"` 85 | DeviceList upnpDeviceList_XML `xml:"deviceList"` 86 | } 87 | 88 | type upnpIcon_XML struct { 89 | Id string `xml:"id"` 90 | Mimetype string `xml:"mimetype"` 91 | Width string `xml:"width"` 92 | Height string `xml:"height"` 93 | Depth string `xml:"depth"` 94 | Url string `xml:"url"` 95 | } 96 | 97 | type upnpIconList_XML struct { 98 | Icon []upnpIcon_XML `xml:"icon"` 99 | } 100 | 101 | type upnpService_XML struct { 102 | ServiceType string `xml:"serviceType"` 103 | ServiceId string `xml:"serviceId"` 104 | ControlURL ssdp.Location `xml:"controlURL"` 105 | EventSubURL ssdp.Location `xml:"eventSubURL"` 106 | SCPDURL ssdp.Location `xml:"SCPDURL"` 107 | } 108 | 109 | type upnpServiceList_XML struct { 110 | Service []upnpService_XML `xml:"service"` 111 | } 112 | 113 | type upnpDeviceList_XML struct { 114 | Device []upnpDevice_XML `xml:"device"` 115 | } 116 | 117 | type upnpDescribeDevice_XML struct { 118 | XMLNamespace string `xml:"xmlns,attr"` 119 | SpecVersion []upnpSpecVersion_XML `xml:"specVersion"` 120 | Device []upnpDevice_XML `xml:"device"` 121 | } 122 | 123 | type upnpDescribeDeviceJob struct { 124 | result chan []*Service 125 | err_result chan error 126 | response *http.Response 127 | doc upnpDescribeDevice_XML 128 | uri ssdp.Location 129 | } 130 | 131 | func upnpMakeDescribeDeviceJob(uri ssdp.Location) (job *upnpDescribeDeviceJob) { 132 | job = &upnpDescribeDeviceJob{} 133 | job.result = make(chan []*Service) 134 | job.err_result = make(chan error) 135 | job.uri = uri 136 | job.doc = upnpDescribeDevice_XML{} 137 | return 138 | } 139 | 140 | func (this *upnpDescribeDeviceJob) BuildURL(servicePath ssdp.Location) (url *url.URL, err error) { 141 | if url, err = url.Parse(string(this.uri)); nil != err { 142 | return 143 | } 144 | if len(servicePath) > 0 && servicePath[0] == '/' { 145 | // We have an absolute path 146 | url.Path = string(servicePath) 147 | } else { 148 | // We have a path relative to the location of the description 149 | basePath, _ := path.Split(url.Path) 150 | url.Path = path.Join(basePath, string(servicePath)) 151 | } 152 | return 153 | } 154 | 155 | func (this *upnpDescribeDeviceJob) UnpackService(dev *upnpDevice_XML, svc_doc *upnpService_XML) (svc *Service) { 156 | svc = upnpMakeService() 157 | if m := upnpOtherDeviceRegex.FindStringSubmatch(dev.DeviceType); 0 < len(m) { 158 | svc.deviceURI = m[1] 159 | svc.deviceType = m[2] 160 | svc.deviceVersion = m[4] 161 | } else { 162 | this.err_result <- errors.New(fmt.Sprintf("Malformed device type string `%s'", dev.DeviceType)) 163 | return 164 | } 165 | svc.udn = dev.UDN 166 | if m := upnpOtherServiceRegex.FindStringSubmatch(svc_doc.ServiceType); 0 < len(m) { 167 | svc.serviceURI = m[1] 168 | svc.serviceType = m[2] 169 | svc.serviceVersion = m[4] 170 | } else { 171 | this.err_result <- errors.New(fmt.Sprintf("Malformed service type string `%s'", svc_doc.ServiceType)) 172 | return 173 | } 174 | svc.serviceId = svc_doc.ServiceId 175 | var err error 176 | if svc.controlURL, err = this.BuildURL(svc_doc.ControlURL); nil != err { 177 | this.err_result <- err 178 | } else if svc.eventSubURL, err = this.BuildURL(svc_doc.EventSubURL); nil != err { 179 | this.err_result <- err 180 | } else if svc.scpdURL, err = this.BuildURL(svc_doc.SCPDURL); nil != err { 181 | this.err_result <- err 182 | } 183 | return 184 | } 185 | 186 | func (this *upnpDescribeDeviceJob) UnpackDevice(dev *upnpDevice_XML) (svc_list []*Service) { 187 | for _, svc := range dev.ServiceList.Service { 188 | svc_list = append(svc_list, this.UnpackService(dev, &svc)) 189 | } 190 | for _, sub_dev := range dev.DeviceList.Device { 191 | svc_list = append(svc_list, this.UnpackDevice(&sub_dev)...) 192 | } 193 | return 194 | } 195 | 196 | func (this *upnpDescribeDeviceJob) Unpack() (svc_list []*Service) { 197 | for _, dev := range this.doc.Device { 198 | svc_list = append(svc_list, this.UnpackDevice(&dev)...) 199 | } 200 | return 201 | } 202 | 203 | func (this *upnpDescribeDeviceJob) Parse() { 204 | defer this.response.Body.Close() 205 | if body, err := ioutil.ReadAll(this.response.Body); nil == err { 206 | xml.Unmarshal(body, &this.doc) 207 | this.result <- this.Unpack() 208 | } else { 209 | this.err_result <- err 210 | } 211 | } 212 | 213 | func (this *upnpDescribeDeviceJob) Describe() { 214 | var err error 215 | log.Printf("Loading %s", string(this.uri)) 216 | if this.response, err = http.Get(string(this.uri)); nil == err { 217 | this.Parse() 218 | } else { 219 | this.err_result <- err 220 | } 221 | } 222 | 223 | type ServiceMap map[string][]*Service 224 | 225 | func Describe(uri ssdp.Location) (svc_map ServiceMap, err error) { 226 | job := upnpMakeDescribeDeviceJob(uri) 227 | go job.Describe() 228 | timeout := time.NewTimer(time.Duration(3) * time.Second) 229 | select { 230 | case svc_list := <-job.result: 231 | svc_map = make(ServiceMap) 232 | for _, svc := range svc_list { 233 | svc_map[svc.serviceType] = append(svc_map[svc.serviceType], svc) 234 | } 235 | case err = <-job.err_result: 236 | case <-timeout.C: 237 | } 238 | return 239 | } 240 | -------------------------------------------------------------------------------- /upnp/event.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package upnp 32 | 33 | import ( 34 | "encoding/xml" 35 | "errors" 36 | "fmt" 37 | "io/ioutil" 38 | "log" 39 | "net" 40 | "net/http" 41 | "time" 42 | ) 43 | 44 | type upnpEventProperty_XML struct { 45 | Content string `xml:",innerxml"` 46 | } 47 | 48 | type upnpEvent_XML struct { 49 | XMLName xml.Name `xml:"urn:schemas-upnp-org:event-1-0 propertyset"` 50 | Properties []upnpEventProperty_XML `xml:"urn:schemas-upnp-org:event-1-0 property"` 51 | } 52 | 53 | type EventFactory interface { 54 | BeginSet(svc *Service, channel chan Event) 55 | HandleProperty(svc *Service, value string, channel chan Event) error 56 | EndSet(svc *Service, channel chan Event) 57 | } 58 | 59 | type Reactor interface { 60 | Init(ifiname, port string) 61 | Subscribe(svc *Service, factory EventFactory) error 62 | Channel() chan Event 63 | } 64 | 65 | var ( 66 | nextEventType = 0 67 | eventTypeMap = make(map[string]int) 68 | ) 69 | 70 | func registerEventType(tag string) int { 71 | if id, has := eventTypeMap[tag]; has { 72 | return id 73 | } else { 74 | eventTypeMap[tag] = nextEventType 75 | defer func() { 76 | nextEventType++ 77 | }() 78 | } 79 | return nextEventType 80 | } 81 | 82 | type upnpEventType int 83 | 84 | const ( 85 | upnpEventTypeBeginSet upnpEventType = iota 86 | upnpEventTypeProperty 87 | upnpEventTypeEndSet 88 | ) 89 | 90 | type upnpEvent struct { 91 | sid string 92 | value string 93 | etype upnpEventType 94 | } 95 | 96 | type upnpEventRecord struct { 97 | svc *Service 98 | factory EventFactory 99 | } 100 | 101 | type upnpEventMap map[string]*upnpEventRecord 102 | 103 | type Event interface { 104 | Service() *Service 105 | Type() int 106 | } 107 | 108 | type upnpDefaultReactor struct { 109 | ifiname string 110 | port string 111 | initialized bool 112 | server *http.Server 113 | localAddr string 114 | eventMap upnpEventMap 115 | subscrChan chan *upnpEventRecord 116 | unpackChan chan *upnpEvent 117 | eventChan chan Event 118 | } 119 | 120 | func (this *upnpDefaultReactor) serve() { 121 | log.Fatal(this.server.ListenAndServe()) 122 | } 123 | 124 | func (this *upnpDefaultReactor) Init(ifiname, port string) { 125 | if this.initialized { 126 | panic("Attempt to reinitialize reactor") 127 | } 128 | 129 | ifi, err := net.InterfaceByName(ifiname) 130 | if err != nil { 131 | panic(err) 132 | } 133 | addrs, err := ifi.Addrs() 134 | if err != nil { 135 | panic(err) 136 | } 137 | 138 | this.initialized = true 139 | this.port = port 140 | this.ifiname = ifiname 141 | this.localAddr = net.JoinHostPort(addrs[0].(*net.IPNet).IP.String(), port) 142 | this.server = &http.Server{ 143 | Addr: ":" + port, 144 | Handler: nil, 145 | ReadTimeout: 10 * time.Second, 146 | WriteTimeout: 10 * time.Second, 147 | MaxHeaderBytes: 1 << 20, 148 | } 149 | http.Handle("/eventSub", this) 150 | log.Printf("Listening for events on %s", this.localAddr) 151 | go this.run() 152 | go this.serve() 153 | } 154 | 155 | func (this *upnpDefaultReactor) handleAck(svc *Service, resp *http.Response) (sid string, err error) { 156 | sid_key := http.CanonicalHeaderKey("sid") 157 | if sid_list, has := resp.Header[sid_key]; has { 158 | sid = sid_list[0] 159 | } else { 160 | err = errors.New("Subscription ack missing sid") 161 | } 162 | return 163 | } 164 | 165 | func (this *upnpDefaultReactor) Subscribe(svc *Service, factory EventFactory) (err error) { 166 | rec := upnpEventRecord{ 167 | svc: svc, 168 | factory: factory, 169 | } 170 | this.subscrChan <- &rec 171 | return 172 | } 173 | 174 | func (this *upnpDefaultReactor) Channel() chan Event { 175 | return this.eventChan 176 | } 177 | 178 | func (this *upnpDefaultReactor) subscribeImpl(rec *upnpEventRecord) (err error) { 179 | client := &http.Client{} 180 | req, err := http.NewRequest("SUBSCRIBE", rec.svc.eventSubURL.String(), nil) 181 | if nil != err { 182 | return 183 | } 184 | req.Header.Add("CALLBACK", fmt.Sprintf("", this.localAddr)) 185 | req.Header.Add("HOST", rec.svc.eventSubURL.Host) 186 | req.Header.Add("USER-AGENT", "unix/5.1 UPnP/1.1 sonos.go/1.0") 187 | req.Header.Add("NT", "upnp:event") 188 | req.Header.Add("TIMEOUT", "900") 189 | var resp *http.Response 190 | if resp, err = client.Do(req); nil == err { 191 | var sid string 192 | if sid, err = this.handleAck(rec.svc, resp); nil == err { 193 | this.eventMap[sid] = rec 194 | } 195 | } 196 | return 197 | } 198 | 199 | func (this *upnpDefaultReactor) maybePostEvent(event *upnpEvent) { 200 | if rec, has := this.eventMap[event.sid]; has { 201 | switch event.etype { 202 | case upnpEventTypeProperty: 203 | rec.factory.HandleProperty(rec.svc, event.value, this.eventChan) 204 | case upnpEventTypeBeginSet: 205 | rec.factory.BeginSet(rec.svc, this.eventChan) 206 | case upnpEventTypeEndSet: 207 | rec.factory.EndSet(rec.svc, this.eventChan) 208 | } 209 | } 210 | } 211 | 212 | func (this *upnpDefaultReactor) run() { 213 | for { 214 | select { 215 | case subscr := <-this.subscrChan: 216 | this.subscribeImpl(subscr) 217 | case event := <-this.unpackChan: 218 | this.maybePostEvent(event) 219 | } 220 | } 221 | } 222 | 223 | func (this *upnpDefaultReactor) sendAck(writer http.ResponseWriter) { 224 | writer.Write(nil) 225 | } 226 | 227 | func (this *upnpDefaultReactor) notifyBegin(sid string) { 228 | event := &upnpEvent{ 229 | sid: sid, 230 | etype: upnpEventTypeBeginSet, 231 | } 232 | this.unpackChan <- event 233 | } 234 | 235 | func (this *upnpDefaultReactor) notify(sid, value string) { 236 | event := &upnpEvent{ 237 | sid: sid, 238 | value: value, 239 | etype: upnpEventTypeProperty, 240 | } 241 | this.unpackChan <- event 242 | } 243 | 244 | func (this *upnpDefaultReactor) notifyEnd(sid string) { 245 | event := &upnpEvent{ 246 | sid: sid, 247 | etype: upnpEventTypeEndSet, 248 | } 249 | this.unpackChan <- event 250 | } 251 | 252 | func (this *upnpDefaultReactor) unpack(sid string, doc *upnpEvent_XML) { 253 | this.notifyBegin(sid) 254 | for _, prop := range doc.Properties { 255 | this.notify(sid, prop.Content) 256 | } 257 | this.notifyEnd(sid) 258 | } 259 | 260 | func (this *upnpDefaultReactor) handle(request *http.Request) { 261 | defer request.Body.Close() 262 | if body, err := ioutil.ReadAll(request.Body); nil != err { 263 | panic(err) 264 | } else { 265 | sid_key := http.CanonicalHeaderKey("sid") 266 | var sid string 267 | if sid_list, has := request.Header[sid_key]; has { 268 | sid = sid_list[0] 269 | doc := &upnpEvent_XML{} 270 | xml.Unmarshal(body, doc) 271 | this.unpack(sid, doc) 272 | } 273 | } 274 | } 275 | 276 | func (this *upnpDefaultReactor) ServeHTTP(writer http.ResponseWriter, request *http.Request) { 277 | defer this.sendAck(writer) 278 | this.handle(request) 279 | } 280 | 281 | func MakeReactor() Reactor { 282 | reactor := &upnpDefaultReactor{} 283 | reactor.eventMap = make(upnpEventMap) 284 | reactor.subscrChan = make(chan *upnpEventRecord) 285 | reactor.unpackChan = make(chan *upnpEvent) 286 | reactor.eventChan = make(chan Event) 287 | return reactor 288 | } 289 | -------------------------------------------------------------------------------- /upnp/service.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package upnp 32 | 33 | import ( 34 | "encoding/xml" 35 | "errors" 36 | "fmt" 37 | "io/ioutil" 38 | "log" 39 | "net/http" 40 | "net/url" 41 | "time" 42 | ) 43 | 44 | type upnpChardataValue_XML struct { 45 | Chardata string `xml:",chardata"` 46 | } 47 | 48 | type upnpAllowedValueList_XML struct { 49 | AllowedValue []upnpChardataValue_XML `xml:"allowedValue"` 50 | } 51 | 52 | type upnpAllowedValueRange_XML struct { 53 | Minimum []upnpChardataValue_XML `xml:"minimum"` 54 | Maximum []upnpChardataValue_XML `xml:"maximum"` 55 | Step []upnpChardataValue_XML `xml:"step"` 56 | } 57 | 58 | type upnpStateVariable_XML struct { 59 | SendEvents string `xml:"sendEvents,attr"` 60 | Name []upnpChardataValue_XML `xml:"name"` 61 | DataType []upnpChardataValue_XML `xml:"dataType"` 62 | AllowedValueList []upnpAllowedValueList_XML `xml:"allowedValueList"` 63 | AllowedValueRange []upnpAllowedValueRange_XML `xml:"allowedValueRange"` 64 | } 65 | 66 | type upnpServiceStateTable_XML struct { 67 | StateVariable []upnpStateVariable_XML `xml:"stateVariable"` 68 | } 69 | 70 | type upnpActionArgument_XML struct { 71 | Name []upnpChardataValue_XML `xml:"name"` 72 | Direction []upnpChardataValue_XML `xml:"direction"` 73 | RelatedStateVariable []upnpChardataValue_XML `xml:"relatedStateVariable"` 74 | } 75 | 76 | type upnpActionArgumentList_XML struct { 77 | Argument []upnpActionArgument_XML `xml:"argument"` 78 | } 79 | 80 | type upnpAction_XML struct { 81 | Name []upnpChardataValue_XML `xml:"name"` 82 | ArgumentList []upnpActionArgumentList_XML `xml:"argumentList"` 83 | } 84 | 85 | type upnpActionList_XML struct { 86 | Action []upnpAction_XML `xml:"action"` 87 | } 88 | 89 | type upnpDescribeService_XML struct { 90 | XMLNamespace string `xml:"xmlns,attr"` 91 | SpecVersion []upnpSpecVersion_XML `xml:"specVersion"` 92 | ServiceStateTable []upnpServiceStateTable_XML `xml:"serviceStateTable"` 93 | ActionList []upnpActionList_XML `xml:"actionList"` 94 | } 95 | 96 | type upnpDescribeServiceJob struct { 97 | result chan *Service 98 | err_result chan error 99 | response *http.Response 100 | doc upnpDescribeService_XML 101 | svc *Service 102 | } 103 | 104 | func upnpMakeDescribeServiceJob(svc *Service) (job *upnpDescribeServiceJob) { 105 | job = &upnpDescribeServiceJob{} 106 | job.result = make(chan *Service) 107 | job.err_result = make(chan error) 108 | job.svc = svc 109 | job.doc = upnpDescribeService_XML{} 110 | return 111 | } 112 | 113 | func (this *upnpDescribeServiceJob) UnpackChardataValue(val *upnpChardataValue_XML) (s string) { 114 | return val.Chardata 115 | } 116 | 117 | func (this *upnpDescribeServiceJob) UnpackAllowedValueList(val_list *upnpAllowedValueList_XML) (allowed_list []string) { 118 | for _, value := range val_list.AllowedValue { 119 | allowed_list = append(allowed_list, this.UnpackChardataValue(&value)) 120 | } 121 | return 122 | } 123 | 124 | type upnpValueRange struct { 125 | min string 126 | max string 127 | step string 128 | } 129 | 130 | func (this *upnpDescribeServiceJob) UnpackAllowedValueRange(val_range *upnpAllowedValueRange_XML) (vrange *upnpValueRange) { 131 | vrange = &upnpValueRange{} 132 | for _, min := range val_range.Minimum { 133 | vrange.min = this.UnpackChardataValue(&min) 134 | } 135 | for _, max := range val_range.Maximum { 136 | vrange.max = this.UnpackChardataValue(&max) 137 | } 138 | for _, step := range val_range.Step { 139 | vrange.step = this.UnpackChardataValue(&step) 140 | } 141 | return 142 | } 143 | 144 | type upnpStateVariable struct { 145 | name string 146 | dataType string 147 | allowedValues []string 148 | allowedRange *upnpValueRange 149 | } 150 | 151 | func (this *upnpDescribeServiceJob) UnpackStateVariable(v *upnpStateVariable_XML) (sv *upnpStateVariable) { 152 | sv = &upnpStateVariable{} 153 | for _, name := range v.Name { 154 | sv.name = this.UnpackChardataValue(&name) 155 | } 156 | for _, datatype := range v.DataType { 157 | sv.dataType = this.UnpackChardataValue(&datatype) 158 | } 159 | for _, val_list := range v.AllowedValueList { 160 | sv.allowedValues = this.UnpackAllowedValueList(&val_list) 161 | } 162 | for _, val_range := range v.AllowedValueRange { 163 | sv.allowedRange = this.UnpackAllowedValueRange(&val_range) 164 | } 165 | return 166 | } 167 | 168 | func (this *upnpDescribeServiceJob) UnpackStateTable(tab *upnpServiceStateTable_XML) (table []*upnpStateVariable) { 169 | for _, v := range tab.StateVariable { 170 | table = append(table, this.UnpackStateVariable(&v)) 171 | } 172 | return 173 | } 174 | 175 | type upnpActionArgument struct { 176 | name string 177 | dir string 178 | variable string 179 | } 180 | 181 | func (this *upnpDescribeServiceJob) UnpackActionArgument(arg *upnpActionArgument_XML) (aarg *upnpActionArgument) { 182 | aarg = &upnpActionArgument{} 183 | for _, name := range arg.Name { 184 | aarg.name = this.UnpackChardataValue(&name) 185 | } 186 | for _, dir := range arg.Direction { 187 | aarg.dir = this.UnpackChardataValue(&dir) 188 | } 189 | for _, related := range arg.RelatedStateVariable { 190 | aarg.variable = this.UnpackChardataValue(&related) 191 | } 192 | return 193 | } 194 | 195 | func (this *upnpDescribeServiceJob) UnpackArgumentList(arg_list *upnpActionArgumentList_XML) (aarg_list []*upnpActionArgument) { 196 | for _, arg := range arg_list.Argument { 197 | aarg_list = append(aarg_list, this.UnpackActionArgument(&arg)) 198 | } 199 | return 200 | } 201 | 202 | type upnpAction struct { 203 | name string 204 | argList []*upnpActionArgument 205 | } 206 | 207 | func (this *upnpDescribeServiceJob) UnpackAction(action *upnpAction_XML) (act *upnpAction) { 208 | act = &upnpAction{} 209 | for _, name := range action.Name { 210 | act.name = this.UnpackChardataValue(&name) 211 | } 212 | for _, arg_list := range action.ArgumentList { 213 | act.argList = this.UnpackArgumentList(&arg_list) 214 | } 215 | return 216 | } 217 | 218 | func (this *upnpDescribeServiceJob) UnpackActionList(act_list *upnpActionList_XML) (action_list []*upnpAction) { 219 | for _, action := range act_list.Action { 220 | action_list = append(action_list, this.UnpackAction(&action)) 221 | } 222 | return 223 | } 224 | 225 | type Service struct { 226 | deviceURI string 227 | deviceType string 228 | deviceVersion string 229 | udn string 230 | serviceURI string 231 | serviceType string 232 | serviceVersion string 233 | serviceId string 234 | controlURL *url.URL 235 | eventSubURL *url.URL 236 | scpdURL *url.URL 237 | described bool 238 | stateTable []*upnpStateVariable 239 | actionList []*upnpAction 240 | } 241 | 242 | func (this *Service) Actions() (actions []string) { 243 | for _, action := range this.actionList { 244 | actions = append(actions, action.name) 245 | } 246 | return 247 | } 248 | 249 | func upnpMakeService() (svc *Service) { 250 | return &Service{} 251 | } 252 | 253 | func (this *upnpDescribeServiceJob) Unpack() { 254 | for _, tab := range this.doc.ServiceStateTable { 255 | this.svc.stateTable = this.UnpackStateTable(&tab) 256 | } 257 | for _, act_list := range this.doc.ActionList { 258 | this.svc.actionList = this.UnpackActionList(&act_list) 259 | } 260 | return 261 | } 262 | 263 | func (this *upnpDescribeServiceJob) Parse() { 264 | defer this.response.Body.Close() 265 | if body, err := ioutil.ReadAll(this.response.Body); nil == err { 266 | xml.Unmarshal(body, &this.doc) 267 | this.Unpack() 268 | this.result <- this.svc 269 | } else { 270 | this.err_result <- err 271 | } 272 | } 273 | 274 | func (this *upnpDescribeServiceJob) Describe() { 275 | var err error 276 | uri := this.svc.scpdURL.String() 277 | log.Printf("Loading %s", string(uri)) 278 | if this.response, err = http.Get(string(uri)); nil == err { 279 | this.Parse() 280 | } else { 281 | this.err_result <- err 282 | } 283 | } 284 | 285 | func (this *Service) Describe() (err error) { 286 | if this.described { 287 | return 288 | } 289 | job := upnpMakeDescribeServiceJob(this) 290 | go job.Describe() 291 | timeout := time.NewTimer(time.Duration(3) * time.Second) 292 | select { 293 | case <-job.result: 294 | this.described = true 295 | case err = <-job.err_result: 296 | case <-timeout.C: 297 | } 298 | return 299 | } 300 | 301 | func (this *Service) findAction(action string) (act *upnpAction, err error) { 302 | if !this.described { 303 | err = errors.New("Service is not described") 304 | } else { 305 | for _, act = range this.actionList { 306 | if action == act.name { 307 | return 308 | } 309 | } 310 | err = errors.New(fmt.Sprintf("No such method %s for service %s", action, this.serviceId)) 311 | } 312 | return 313 | } 314 | -------------------------------------------------------------------------------- /upnp/soap.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | package upnp 32 | 33 | import ( 34 | "encoding/xml" 35 | "errors" 36 | "fmt" 37 | "io/ioutil" 38 | _ "log" 39 | "net/http" 40 | _ "os" 41 | "strings" 42 | ) 43 | 44 | const ( 45 | soapEncodingStyle = "http://schemas.xmlsoap.org/soap/encoding/" 46 | soapEnvelopeSchema = "http://schemas.xmlsoap.org/soap/envelope/" 47 | soapContentType = "text/xml; charset=\"utf-8\"" 48 | soapUserAgent = "Linux UPnP/1.1 crash" 49 | ) 50 | 51 | type Arg struct { 52 | Key string 53 | Value interface{} 54 | } 55 | 56 | type Args []Arg 57 | 58 | type ErrorResponse struct { 59 | FaultCode string `xml:"faultcode"` 60 | FaultString string `xml:"faultstring"` 61 | Detail struct { 62 | XMLName xml.Name 63 | UPnPError struct { 64 | XMLName xml.Name 65 | ErrorCode string `xml:"errorCode"` 66 | } `xml:"urn:schemas-upnp-org:control-1-0 UPnPError"` 67 | } `xml:"detail"` 68 | } 69 | 70 | func (this *ErrorResponse) Error() (err error) { 71 | if 0 < len(this.FaultCode) { 72 | err = errors.New(this.Detail.UPnPError.ErrorCode) 73 | } 74 | return 75 | } 76 | 77 | type soapRequestAction struct { 78 | XMLName xml.Name 79 | XMLNS_u string `xml:"xmlns:u,attr"` 80 | Argument []soapRequestArgument 81 | } 82 | 83 | func soapBuildNamespace(svc *Service) (ns string) { 84 | return fmt.Sprintf("urn:%s:service:%s:%s", svc.serviceURI, svc.serviceType, svc.serviceVersion) 85 | } 86 | 87 | func soapNewRequestAction(action string, svc *Service, args Args) (ra soapRequestAction) { 88 | ns := soapBuildNamespace(svc) 89 | ra = soapRequestAction{} 90 | ra.XMLName.Local = "u:" + action 91 | ra.XMLNS_u = ns 92 | for _, arg := range args { 93 | ra.Argument = append(ra.Argument, soapNewRequestArgument(arg.Key, fmt.Sprintf("%v", arg.Value))) 94 | } 95 | return 96 | } 97 | 98 | type soapRequestArgument struct { 99 | XMLName xml.Name 100 | Value string `xml:",chardata"` 101 | } 102 | 103 | func soapNewRequestArgument(name, value string) (ar soapRequestArgument) { 104 | ar = soapRequestArgument{} 105 | ar.XMLName.Local = name 106 | ar.Value = value 107 | return 108 | } 109 | 110 | type soapRequestBody struct { 111 | XMLName xml.Name `xml:"s:Body"` 112 | Action soapRequestAction 113 | } 114 | 115 | type soapResponseBody struct { 116 | Data string `xml:",innerxml"` 117 | } 118 | 119 | type soapRequestEnvelope struct { 120 | XMLName xml.Name `xml:"s:Envelope"` 121 | Body soapRequestBody 122 | XMLNS_s string `xml:"xmlns:s,attr"` 123 | EncodingStyle string `xml:"s:encodingStyle,attr"` 124 | } 125 | 126 | type soapResponseEnvelope struct { 127 | XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"` 128 | Body soapResponseBody 129 | } 130 | 131 | type soapRequest struct { 132 | Envelope soapRequestEnvelope 133 | } 134 | 135 | func soapNewRequest(action string, svc *Service, args Args) (req *soapRequest) { 136 | req = &soapRequest{} 137 | req.Envelope.XMLNS_s = soapEnvelopeSchema 138 | req.Envelope.EncodingStyle = soapEncodingStyle 139 | req.Envelope.Body.Action = soapNewRequestAction(action, svc, args) 140 | return 141 | } 142 | 143 | func upnpBuildRequest(svc *Service, action string, args Args) (msg []byte) { 144 | if act, err := svc.findAction(action); nil != err { 145 | panic(err) 146 | } else { 147 | req := soapNewRequest(act.name, svc, args) 148 | var err error 149 | if msg, err = xml.MarshalIndent(req.Envelope, "", " "); nil != err { 150 | panic(err) 151 | } 152 | } 153 | return 154 | } 155 | 156 | func (this *Service) Call(action string, args Args) (response string) { 157 | client := &http.Client{} 158 | r := upnpBuildRequest(this, action, args) 159 | body := strings.NewReader(xml.Header + string(r)) 160 | req, err := http.NewRequest("POST", this.controlURL.String(), body) 161 | req.Header.Set("CONTENT-TYPE", soapContentType) 162 | req.Header.Set("USER-AGENT", soapUserAgent) 163 | req.Header.Set("SOAPACTION", fmt.Sprintf("\"%s#%s\"", soapBuildNamespace(this), action)) 164 | req.Header.Set("CONNECTION", "KEEP-ALIVE") 165 | //req.Write(os.Stdout) 166 | //body.Seek(0, 0) 167 | if nil != err { 168 | panic(err) 169 | } 170 | if resp, err := client.Do(req); nil != err { 171 | panic(err) 172 | } else { 173 | defer resp.Body.Close() 174 | var body []byte 175 | if body, err = ioutil.ReadAll(resp.Body); nil != err { 176 | panic(err) 177 | } 178 | doc := soapResponseEnvelope{} 179 | //log.Printf("%v", string(body)) 180 | xml.Unmarshal(body, &doc) 181 | response = doc.Body.Data 182 | } 183 | return 184 | } 185 | 186 | func (this *Service) CallVa(action string, va_list ...interface{}) (response string) { 187 | var args Args 188 | for i := 0; i < len(va_list); i += 2 { 189 | args = append(args, Arg{va_list[i].(string), va_list[i+1]}) 190 | } 191 | return this.Call(action, args) 192 | } 193 | -------------------------------------------------------------------------------- /upnp/upnp.go: -------------------------------------------------------------------------------- 1 | // 2 | // go-sonos 3 | // ======== 4 | // 5 | // Copyright (c) 2012, Ian T. Richards 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions 10 | // are met: 11 | // 12 | // * Redistributions of source code must retain the above copyright notice, 13 | // this list of conditions and the following disclaimer. 14 | // * Redistributions in binary form must reproduce the above copyright 15 | // notice, this list of conditions and the following disclaimer in the 16 | // documentation and/or other materials provided with the distribution. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | // 32 | // An implementation of the Sonos Universal Plug and Play API. 33 | // 34 | package upnp 35 | --------------------------------------------------------------------------------