source: trunk/I_Sonos1.xml @ 218

Revision 218, 132.8 KB checked in by lolodomo, 10 years ago (diff)

Fix group management for Playbar

  • Property svn:keywords set to Id
Line 
1<?xml version="1.0"?>
2<implementation>
3<!-- $Id$ -->
4
5<functions>
6  local url = require("socket.url")
7
8  local MSG_CLASS = "Sonos"
9
10  local PLUGIN_VERSION = "1.4 (dev)"
11
12  local DEBUG_MODE = false
13  local taskHandle = -1
14
15  local TASK_ERROR = 2
16  local TASK_ERROR_PERM = -2
17  local TASK_SUCCESS = 4
18  local TASK_BUSY = 1
19
20  local VERA_LOCAL_IP
21  local VERA_IP
22  local VERA_WEB_PORT
23
24  local UPNP_AVTRANSPORT_SERVICE = 'urn:schemas-upnp-org:service:AVTransport:1'
25  local UPNP_RENDERING_CONTROL_SERVICE = 'urn:schemas-upnp-org:service:RenderingControl:1'
26  local UPNP_GROUP_RENDERING_CONTROL_SERVICE = 'urn:schemas-upnp-org:service:GroupRenderingControl:1'
27  local UPNP_DEVICE_PROPERTIES_SERVICE = 'urn:schemas-upnp-org:service:DeviceProperties:1'
28  local UPNP_CONNECTION_MANAGER_SERVICE = 'urn:schemas-upnp-org:service:ConnectionManager:1'
29  local UPNP_ZONEGROUPTOPOLOGY_SERVICE = 'urn:schemas-upnp-org:service:ZoneGroupTopology:1'
30  local UPNP_MUSICSERVICES_SERVICE = 'urn:schemas-upnp-org:service:MusicServices:1'
31  local UPNP_MR_CONTENT_DIRECTORY_SERVICE = 'urn:schemas-upnp-org:service:ContentDirectory:1'
32
33  local UPNP_AVTRANSPORT_SID = 'urn:upnp-org:serviceId:AVTransport'
34  local UPNP_RENDERING_CONTROL_SID = 'urn:upnp-org:serviceId:RenderingControl'
35  local UPNP_GROUP_RENDERING_CONTROL_SID = 'urn:upnp-org:serviceId:GroupRenderingControl'
36  local UPNP_DEVICE_PROPERTIES_SID = 'urn:upnp-org:serviceId:DeviceProperties'
37  local UPNP_CONNECTION_MANAGER_SID = 'urn:upnp-org:serviceId:ConnectionManager'
38  local UPNP_ZONEGROUPTOPOLOGY_SID = 'urn:upnp-org:serviceId:ZoneGroupTopology'
39  local UPNP_MUSICSERVICES_SID = 'urn:upnp-org:serviceId:MusicServices'
40  local UPNP_MR_CONTENT_DIRECTORY_SID = 'urn:upnp-org:serviceId:ContentDirectory'
41
42  if (package.path:find("/etc/cmh-ludl/?.lua;/etc/cmh-lu/?.lua", 1, true) == nil) then
43      package.path = package.path..";/etc/cmh-ludl/?.lua;/etc/cmh-lu/?.lua"
44  end
45  package.loaded.L_Sonos1 = nil
46
47  local upnp = require("L_Sonos1")
48  local tts = require("L_SonosTTS")
49
50  -- Table of Sonos IP addresses indexed by Vera devices
51  local ip = {}
52  local port
53  local descriptionURL
54  local iconURL
55
56  local HADEVICE_SID = "urn:micasaverde-com:serviceId:HaDevice1"
57  local SONOS_SID = "urn:micasaverde-com:serviceId:Sonos1"
58  local SONOS_DEVICE_TYPE = "urn:schemas-micasaverde-com:device:Sonos:1"
59
60  local EventSubscriptions = {
61      { service = UPNP_AVTRANSPORT_SERVICE,
62        eventVariable = "LastChange",
63        actionName = "NotifyAVTransportChange",
64        id = "",
65        expiry = "" },
66      { service = UPNP_RENDERING_CONTROL_SERVICE,
67        eventVariable = "LastChange",
68        actionName = "NotifyRenderingChange",
69        id = "",
70        expiry = "" },
71      { service = UPNP_ZONEGROUPTOPOLOGY_SERVICE,
72        eventVariable = "ZoneGroupState",
73        actionName = "NotifyZoneGroupTopologyChange",
74        id = "",
75        expiry = "" },
76      { service = UPNP_MR_CONTENT_DIRECTORY_SERVICE,
77        eventVariable = "ContainerUpdateIDs",
78        actionName = "NotifyContentDirectoryChange",
79        id = "",
80        expiry = "" }
81  }
82
83  local PLUGIN_ICON = "http://%s:%d/cmh/skins/default/icons/Sonos.png"
84
85  local LOCAL_BASE_WEB_URL = "http://%s:%d"
86
87  local QUEUE_URI = "x-rincon-queue:%s#0"
88
89  local playbackCxt = {}
90  local sayPlayback = {}
91
92  -- Table of Sonos UUIDs indexed by Vera devices
93  local UUIDs = {}
94
95  local groupsState = ""
96
97  local sonosServices = {}
98
99  -- Tables indexed by Sonos UUIDs
100  local metaDataKeys = {}
101  local dataTable = {}
102
103  local variableSidTable = {
104        TransportState = UPNP_AVTRANSPORT_SID,
105        TransportStatus = UPNP_AVTRANSPORT_SID,
106        TransportPlaySpeed = UPNP_AVTRANSPORT_SID,
107        CurrentPlayMode = UPNP_AVTRANSPORT_SID,
108        CurrentCrossfadeMode = UPNP_AVTRANSPORT_SID,
109        CurrentTransportActions = UPNP_AVTRANSPORT_SID,
110        NumberOfTracks = UPNP_AVTRANSPORT_SID,
111        CurrentMediaDuration = UPNP_AVTRANSPORT_SID,
112        AVTransportURI = UPNP_AVTRANSPORT_SID,
113        AVTransportURIMetaData = UPNP_AVTRANSPORT_SID,
114        CurrentRadio = UPNP_AVTRANSPORT_SID,
115        CurrentService = SONOS_SID,
116        CurrentTrack = UPNP_AVTRANSPORT_SID,
117        CurrentTrackDuration = UPNP_AVTRANSPORT_SID,
118        CurrentTrackURI = UPNP_AVTRANSPORT_SID,
119        CurrentTrackMetaData = UPNP_AVTRANSPORT_SID,
120        CurrentStatus = UPNP_AVTRANSPORT_SID,
121        CurrentTitle = UPNP_AVTRANSPORT_SID,
122        CurrentArtist = UPNP_AVTRANSPORT_SID,
123        CurrentAlbum = UPNP_AVTRANSPORT_SID,
124        CurrentDetails = UPNP_AVTRANSPORT_SID,
125        CurrentAlbumArt = UPNP_AVTRANSPORT_SID,
126        RelativeTimePosition = UPNP_AVTRANSPORT_SID,
127
128        Mute = UPNP_RENDERING_CONTROL_SID,
129        Volume = UPNP_RENDERING_CONTROL_SID,
130
131        SavedQueues = UPNP_MR_CONTENT_DIRECTORY_SID,
132        FavoritesRadios = UPNP_MR_CONTENT_DIRECTORY_SID,
133        Favorites = UPNP_MR_CONTENT_DIRECTORY_SID,
134        Queue = UPNP_MR_CONTENT_DIRECTORY_SID,
135
136        GroupCoordinator = SONOS_SID,
137        ZonePlayerUUIDsInGroup = UPNP_ZONEGROUPTOPOLOGY_SID,
138        ZoneGroupState = UPNP_ZONEGROUPTOPOLOGY_SID,
139
140        SonosOnline = SONOS_SID,
141        ZoneName = UPNP_DEVICE_PROPERTIES_SID,
142        SonosID = UPNP_DEVICE_PROPERTIES_SID,
143        SonosModelName = SONOS_SID,
144        SonosModel = SONOS_SID,
145
146        ProxyUsed = SONOS_SID
147  }
148
149  local BROWSE_TIMEOUT = 5
150  local fetchQueue = true
151
152  local idConfRefresh = 0
153
154  local function log(stuff, level)
155    luup.log(string.format("%s: %s", MSG_CLASS, stuff), (level or 50))
156  end
157
158  local function warning(stuff)
159    log("warning: " .. stuff, 2)
160  end
161
162  local function error(stuff)
163    log("error: " .. stuff, 1)
164  end
165
166  local function debug(stuff)
167    if (DEBUG_MODE) then
168      log("debug: " .. stuff)
169    end
170  end
171
172  local function task(text, mode)
173    luup.log("task " .. text)
174    if (mode == TASK_ERROR_PERM) then
175      taskHandle = luup.task(text, TASK_ERROR, MSG_CLASS, taskHandle)
176    else
177      taskHandle = luup.task(text, mode, MSG_CLASS, taskHandle)
178
179      -- Clear the previous error, since they're all transient
180      if (mode ~= TASK_SUCCESS) then
181        luup.call_delay("clearTask", 30, "", false)
182      end
183    end
184  end
185
186  --
187  -- Has to be "non-local" in order for MiOS to call it :(
188  --
189  function clearTask()
190    task("Clearing...", TASK_SUCCESS)
191  end
192
193  local function defaultValue(arr, val, default)
194    if (arr == nil or arr[val] == nil or arr[val] == "") then
195      return default
196    else
197      return arr[val]
198    end
199  end
200
201  local function setData(name, value, deviceId, default)
202      local uuid = UUIDs[deviceId]
203      if (uuid ~= "" and dataTable[uuid] ~= nil) then
204          dataTable[uuid][name] = value
205      end
206
207      if (deviceId == 0 or variableSidTable[name] == nil) then
208          return (default or false)
209      end
210
211      local curValue = luup.variable_get(variableSidTable[name], name, deviceId)
212
213      if ((value ~= curValue) or (curValue == nil)) then
214          luup.variable_set(variableSidTable[name], name, value, deviceId)
215          return true
216      else
217          return (default or false)
218      end
219  end
220
221  local function setVariableValue(serviceId, name, value, deviceId)
222      if (deviceId ~= 0) then
223          luup.variable_set(serviceId, name, value, deviceId)
224      end
225  end
226
227  local function parseSimple(value, tag)
228      local pos1, pos2, tmp
229      local elts = {}
230      local eltsTable = {}
231
232      for tmp in value:gmatch("(&lt;"..tag.."%s?.-&lt;/"..tag..">)") do
233          local elts0, eltsTable0 = upnp.parseFirstElt(tmp, tag, nil)
234          table.insert(elts, elts0)
235          table.insert(eltsTable, eltsTable0)
236      end
237
238      return elts, eltsTable
239  end
240
241  --
242  -- Put together a rudimentary status string for the Dashboard
243  --
244  local function getSimpleDIDLStatus(meta)
245      local title = ""
246      local artist = ""
247      local album = ""
248      local details = ""
249      local albumArt = ""
250      local desc = ""
251      local didl = nil
252      local didlTable = nil
253      local complement = ""
254      if (meta ~= nil and meta ~= "") then
255          didl, didlTable = upnp.parseDIDLItem(meta)
256
257          desc = didlTable["desc"] or desc
258          if (didlTable["upnp:class"] == "object.item") then
259              title = upnp.decode(didlTable["dc:title"] or title)
260              details = upnp.decode(didlTable["r:streamContent"] or details)
261              if (details ~= "") then
262                  if (string.sub(title, 1, 10) ~= "x-sonosapi") then
263                      complement = ": "
264                  end
265                  complement = complement .. details
266              end
267              if (didlTable["upnp:albumArtURI"] ~= nil) then
268                  albumArt = upnp.decode(didlTable["upnp:albumArtURI"])
269              end
270          elseif ((didlTable["upnp:class"] == "object.item.audioItem.musicTrack")
271                  or (didlTable["upnp:class"] == "object.item.audioItem")) then
272              title = upnp.decode(didlTable["dc:title"] or title)
273              artist = upnp.decode(didlTable["r:albumArtist"] or didlTable["dc:creator"] or artist)
274              album = upnp.decode(didlTable["upnp:album"] or album)
275              details = upnp.decode(didlTable["r:streamContent"] or details)
276              local title2, artist2 = details:match(".*|TITLE ([^|]*)|ARTIST ([^|]*)")
277              if (title2 ~= nil) then
278                  title = title2
279              end
280              if (artist2 ~= nil) then
281                  artist = artist2
282              end
283              if (didlTable["upnp:albumArtURI"] ~= nil) then
284                  albumArt = upnp.decode(didlTable["upnp:albumArtURI"])
285              end
286              if (artist ~= "" and album ~= "") then
287                  complement = string.format(" (%s, %s)", artist, album)
288              elseif (artist ~= "") then
289                  complement = string.format(" (%s)", artist)
290              elseif (album ~= "") then
291                  complement = string.format(" (%s)", album)
292              end
293          elseif (didlTable["upnp:class"] == "object.item.audioItem.audioBroadcast") then
294              title = upnp.decode(didlTable["dc:title"] or title)
295          end
296      end
297      return complement, title, artist, album, details, albumArt, desc, didl, didlTable
298  end
299
300  local function getValueFromXML(xml, tag, subTag, value, tagResult)
301      local result = nil
302      local elts, eltsTable = parseSimple(xml, tag)
303      for i, v in ipairs(eltsTable) do
304          if (v[subTag] == value and v[tagResult] ~= nil) then
305              result = v[tagResult]
306              break
307          end
308      end
309      return result
310  end
311
312  local function getAttribute(xml, tag, attribute)
313      return xml:match("&lt;"..tag.."%s?.-%s"..attribute..'="([^"]+)"[^>]->')
314  end
315
316  local function testParsing()
317      local coordinator, item, item2, id, zoneName, invisible, channelMapSet, isZoneBridge, location, HTSatChanMapSet
318      local tag = "ZoneGroup"
319      local subtag = "ZoneGroupMember"
320      for coordinator, item in groupsState:gmatch("&lt;"..tag..'%sCoordinator="([^"]-)"[^>]->(.-)&lt;/'..tag..'>') do
321          log("testParsing: group coordinator=" .. coordinator)
322          for item2 in item:gmatch("(&lt;"..subtag.."%s?[^>]->)") do
323              id = getAttribute(item2, subtag, "UUID")
324              zoneName = getAttribute(item2, subtag, "ZoneName")
325              invisible = getAttribute(item2, subtag, "Invisible")
326              channelMapSet = getAttribute(item2, subtag, "ChannelMapSet")
327              isZoneBridge = getAttribute(item2, subtag, "IsZoneBridge")
328              location = getAttribute(item2, subtag, "Location")
329              HTSatChanMapSet = getAttribute(item2, subtag, "HTSatChanMapSet")
330              log("testParsing: group member id=" .. id .. " zoneName=" .. zoneName .. " invisible=" .. (invisible or "nil") .. " isZoneBridge=" .. (isZoneBridge or "nil") .. " channelMapSet=" .. (channelMapSet or "nil") .. " HTSatChanMapSet=" .. (HTSatChanMapSet or "nil") .. " location=" .. (location or "nil"))
331          end
332      end
333  end
334
335  local function getZoneNameFromUUID(uuid)
336      local result = nil
337      local coordinator, item, item2, id
338      local tag = "ZoneGroup"
339      local subtag = "ZoneGroupMember"
340      local found = false
341      for coordinator, item in groupsState:gmatch("&lt;"..tag..'%sCoordinator="([^"]-)"[^>]->(.-)&lt;/'..tag..'>') do
342          for item2 in item:gmatch("(&lt;"..subtag.."%s?[^>]->)") do
343              id = getAttribute(item2, subtag, "UUID")
344              if (id == uuid) then
345                  result = getAttribute(item2, subtag, "ZoneName")
346                  found = true
347                  break
348              end
349          end
350          if (found) then
351              break
352          end
353      end
354      return result
355  end
356
357  local function getUUIDFromZoneName(name)
358      local result = nil
359      local coordinator, item, item2, zoneName, invisible, channelMapSet
360      local tag = "ZoneGroup"
361      local subtag = "ZoneGroupMember"
362      local found = false
363      for coordinator, item in groupsState:gmatch("&lt;"..tag..'%sCoordinator="([^"]-)"[^>]->(.-)&lt;/'..tag..'>') do
364          for item2 in item:gmatch("(&lt;"..subtag.."%s?[^>]->)") do
365              zoneName = getAttribute(item2, subtag, "ZoneName")
366              invisible = getAttribute(item2, subtag, "Invisible")
367              channelMapSet = getAttribute(item2, subtag, "ChannelMapSet")
368              if (zoneName == name and (channelMapSet == nil or invisible == "1")) then
369                  result = getAttribute(item2, subtag, "UUID")
370                  found = true
371                  break
372              end
373          end
374          if (found) then
375              break
376          end
377      end
378      return result
379  end
380
381  local function getIPFromUUID(uuid)
382      local result = nil
383      local coordinator, item, item2, id, location
384      local tag = "ZoneGroup"
385      local subtag = "ZoneGroupMember"
386      local found = false
387      for coordinator, item in groupsState:gmatch("&lt;"..tag..'%sCoordinator="([^"]-)"[^>]->(.-)&lt;/'..tag..'>') do
388         for item2 in item:gmatch("(&lt;"..subtag.."%s?[^>]->)") do
389              id = getAttribute(item2, subtag, "UUID")
390              if (id == uuid) then
391                  location = getAttribute(item2, subtag, "Location")
392                  if (location ~= nil) then
393                      result = location:match("http://(.-):.+")
394                  end
395                  found = true
396                  break
397              end
398          end
399          if (found) then
400              break
401          end
402      end
403      return result
404  end
405
406  local function deviceIsOnline(device)
407    local changed = setData("SonosOnline", "1", device, false)
408    if (changed) then
409        setVariableValue(HADEVICE_SID, "LastUpdate", os.time(), device)
410    end
411    return changed
412  end
413
414  local function deviceIsOffline(device)
415    local changed = setData("SonosOnline", "0", device, false)
416    if (changed) then
417        iconURL = PLUGIN_ICON:format(VERA_IP, VERA_WEB_PORT)
418
419        groupsState = "&lt;ZoneGroups>&lt;/ZoneGroups>"
420
421        changed = setData("TransportState", "STOPPED",  device, changed)
422        changed = setData("TransportStatus", "KO",  device, changed)
423        changed = setData("TransportPlaySpeed", "1",  device, changed)
424        changed = setData("CurrentPlayMode", "NORMAL", device, changed)
425        changed = setData("CurrentCrossfadeMode", "0", device, changed)
426        changed = setData("CurrentTransportActions", "", device, changed)
427        changed = setData("NumberOfTracks", "NOT_IMPLEMENTED", device, changed)
428        changed = setData("CurrentMediaDuration", "NOT_IMPLEMENTED", device, changed)
429        changed = setData("AVTransportURI", "", device, changed)
430        changed = setData("AVTransportURIMetaData", "", device, changed)
431        changed = setData("CurrentRadio", "", device, changed)
432        changed = setData("CurrentService", "", device, changed)
433        changed = setData("CurrentTrack", "NOT_IMPLEMENTED", device, changed)
434        changed = setData("CurrentTrackDuration", "NOT_IMPLEMENTED", device, changed)
435        changed = setData("CurrentTrackURI", "", device, changed)
436        changed = setData("CurrentTrackMetaData", "", device, changed)
437        changed = setData("CurrentStatus", "Offline", device, changed)
438        changed = setData("CurrentTitle", "", device, changed)
439        changed = setData("CurrentArtist", "", device, changed)
440        changed = setData("CurrentAlbum", "", device, changed)
441        changed = setData("CurrentDetails", "", device, changed)
442        changed = setData("CurrentAlbumArt", iconURL, device, changed)
443        changed = setData("RelativeTimePosition", "NOT_IMPLEMENTED", device, changed)
444        changed = setData("Volume", "0", device, changed)
445        changed = setData("Mute", "0", device, changed)
446        changed = setData("SavedQueues", "", device, changed)
447        changed = setData("FavoritesRadios", "", device, changed)
448        changed = setData("Favorites", "", device, changed)
449        changed = setData("Queue", "", device, changed)
450        changed = setData("GroupCoordinator", "", device, changed)
451        changed = setData("ZonePlayerUUIDsInGroup", "", device, changed)
452        changed = setData("ZoneGroupState", groupsState, device, changed)
453
454        setVariableValue(HADEVICE_SID, "LastUpdate", os.time(), device)
455
456        if (device ~= 0) then
457            upnp.cancelProxySubscriptions(EventSubscriptions)
458        end
459    end
460  end
461
462  local function commsFailure(device, text)
463    debug("commsFailure: Device offline? status=" .. text)
464
465    deviceIsOffline(device)
466  end
467
468  local function getSonosServiceId(serviceName)
469    local serviceId = nil
470    for k, v in pairs(sonosServices) do
471        if (v == serviceName) then
472            serviceId = k
473            break
474        end
475    end
476    return serviceId
477  end
478
479  local function getServiceFromURI(transportUri, trackUri)
480    local serviceName = ""
481    local serviceId = nil
482    local serviceCmd
483    if (transportUri == nil) then
484    elseif (transportUri:find("pndrradio:") == 1) then
485        serviceName = "Pandora"
486        serviceId = getSonosServiceId(serviceName)
487        if (serviceId == nil) then
488            serviceId = "-1"
489        end
490    else
491        serviceCmd, serviceId = transportUri:match("x%-sonosapi%-stream:([^%?]+%?sid=(%d+).*)")
492        if (serviceCmd == nil) then
493            serviceCmd, serviceId = transportUri:match("x%-sonosapi%-radio:([^%?]+%?sid=(%d+).*)")
494        end
495        if (serviceCmd == nil) then
496            serviceCmd, serviceId = transportUri:match("x%-sonosapi%-hls:([^%?]+%?sid=(%d+).*)")
497        end
498        if (serviceCmd == nil and trackUri ~= nil) then
499            serviceCmd, serviceId = trackUri:match("x%-sonosprog%-http:([^%?]+%?sid=(%d+).*)")
500        end
501        if (serviceCmd == nil and trackUri ~= nil) then
502            serviceCmd, serviceId = trackUri:match("x%-sonos%-http:([^%?]+%?sid=(%d+).*)")
503        end
504        if (serviceCmd ~= nil and serviceId ~= nil) then
505            serviceName = sonosServices[serviceId] or ""
506        end
507    end
508    return serviceName, serviceId
509  end
510
511  local function updateServicesMetaDataKeys(device, id, key)
512    local uuid = UUIDs[device]
513    if (id ~= nil and key ~= "" and metaDataKeys[uuid][id] ~= key) then
514        metaDataKeys[uuid][id] = key
515        local data = ""
516        for k, v in pairs(metaDataKeys[uuid]) do
517            data = data .. string.format('%s=%s\n', k, v)
518        end
519        setVariableValue(SONOS_SID, "SonosServicesKeys", data, device)
520        setVariableValue(HADEVICE_SID, "LastUpdate", os.time(), device)
521    end
522  end
523
524  local function loadServicesMetaDataKeys(device)
525    local keys = {}
526    local elts = luup.variable_get(SONOS_SID, "SonosServicesKeys", device) or ""
527    for token, value in elts:gmatch("([^=]+)=([^\n]+)\n") do
528        keys[token] = value
529    end
530    return keys
531  end
532
533  local function extractDataFromMetaData(device, currentUri, currentUriMetaData, trackUri, trackUriMetaData)
534    local statusString, info, title, title2, artist, album, details, albumArt, desc, desc2
535    local service, serviceId, serviceCmd
536    local uuid = UUIDs[device]
537    info, title, artist, album, details, albumArt, desc = getSimpleDIDLStatus(currentUriMetaData)
538    info, title2, artist, album, details, albumArt, desc2 = getSimpleDIDLStatus(trackUriMetaData)
539    service, serviceId = getServiceFromURI(currentUri, trackUri)
540    updateServicesMetaDataKeys(device, serviceId, desc)
541    statusString = ""
542    if (service ~= "") then
543        statusString = statusString .. service
544    end
545    if (title ~= "") then
546        if (statusString ~= "") then
547            statusString = statusString .. ": "
548        end
549        statusString = statusString .. title
550    end
551    if (currentUri ~= nil and currentUri:find("x%-rincon%-stream:") == 1) then
552        if (title2 == "" or title2 == " ") then
553            title2 = "Line-In"
554        end
555        local zone = getZoneNameFromUUID(currentUri:match(".+:(.+)"))
556        if (zone ~= nil) then
557            title2 = title2 .. " (" .. zone .. ")"
558        end
559    end
560    if (currentUri ~= nil and currentUri:find("x%-rincon:") == 1) then
561        title2 = ""
562        info = "Group"
563        local zone = getZoneNameFromUUID(currentUri:match(".+:(.+)"))
564        if (zone ~= nil) then
565            info = info .. " driven by " .. zone
566        end
567    end
568    if (currentUri == "") then
569        info = "No music"
570    end
571    if (title ~= "" and title ~= title2 and string.sub(title2, 1, 10) == "x-sonosapi") then
572        title2 = title
573    end
574    if (title2 ~= "" and title2 ~= title) then
575        if (statusString ~= "") then
576            statusString = statusString .. ": "
577        end
578        statusString = statusString .. title2
579    end
580    if (info ~= "") then
581        if (statusString ~= "") then
582            statusString = statusString .. ": "
583        end
584        statusString = statusString .. info
585    end
586    if (albumArt ~= "") then
587        albumArt = url.absolute(string.format("http://%s:%s/", ip[uuid], port), albumArt)
588    elseif (serviceId ~= nil) then
589        albumArt = string.format("http://%s:%s/getaa?s=1&amp;u=%s", ip[uuid], port, url.escape(currentUri))
590    else
591        albumArt = iconURL
592    end
593    return service, title, statusString, title2, artist, album, details, albumArt
594  end
595
596  local function getGroupInfos(uuid)
597      local grpMembers = ""
598      local grpCoordinator = ""
599      local coordinator, item, item2, id, members
600      local tag = "ZoneGroup"
601      local subtag = "ZoneGroupMember"
602      local found = false
603      for coordinator, item in groupsState:gmatch("&lt;"..tag..'%sCoordinator="([^"]-)"[^>]->(.-)&lt;/'..tag..'>') do
604          members = ""
605          for item2 in item:gmatch("(&lt;"..subtag.."%s?[^>]->)") do
606              id = getAttribute(item2, subtag, "UUID")
607              if (id ~= nil) then
608                  if (members ~= "") then
609                      members = members .. ","
610                  end
611                  members = members .. id
612              end
613              if (uuid == id) then
614                  found = true
615              end
616          end
617          if (found) then
618              grpCoordinator = coordinator
619              grpMembers = members
620              break
621          end
622      end
623      return grpMembers, grpCoordinator
624  end
625
626  local function getAllUUIDs()
627      local members = ""
628      local coordinator, item, item2, id, invisible, channelMapSet, isZoneBridge
629      local tag = "ZoneGroup"
630      local subtag = "ZoneGroupMember"
631      for coordinator, item in groupsState:gmatch("&lt;"..tag..'%sCoordinator="([^"]-)"[^>]->(.-)&lt;/'..tag..'>') do
632          for item2 in item:gmatch("(&lt;"..subtag.."%s?[^>]->)") do
633              id = getAttribute(item2, subtag, "UUID")
634              invisible = getAttribute(item2, subtag, "Invisible")
635              channelMapSet = getAttribute(item2, subtag, "ChannelMapSet")
636              isZoneBridge = getAttribute(item2, subtag, "IsZoneBridge")
637              if (id ~= nil and isZoneBridge ~= "1" and (channelMapSet == nil or invisible == "1")) then
638                  if (members ~= "") then
639                      members = members .. ","
640                  end
641                  members = members .. id
642              end
643          end
644      end
645      return members
646  end
647
648  local function parseSavedQueues(xml)
649      local result = ""
650      local title, id
651      for id, title in xml:gmatch('&lt;container%s?.-id="([^"]-)"[^>]->.-&lt;dc:title%s?[^>]->(.-)&lt;/dc:title>.-&lt;/container>') do
652          id = upnp.decode(id)
653          title = upnp.decode(title)
654          result = result .. id .. "@" .. title .. "\n"
655      end
656      return result
657  end
658
659  local function parseFavoritesRadios(xml)
660      local result = ""
661      local title, res
662      for title, res in xml:gmatch("&lt;item%s?[^>]->.-&lt;dc:title%s?[^>]->(.-)&lt;/dc:title>.-&lt;res%s?[^>]->(.-)&lt;/res>.-&lt;/item>") do
663          title = upnp.decode(title)
664          result = result .. res .. "@" .. title .. "\n"
665      end
666      return result
667  end
668
669  local function parseIdTitle(xml)
670      local result = ""
671      local id, title
672      for id, title in xml:gmatch('&lt;item%s?.-id="([^"]-)"[^>]->.-&lt;dc:title%s?[^>]->(.-)&lt;/dc:title>.-&lt;/item>') do
673          id = upnp.decode(id)
674          title = upnp.decode(title)
675          result = result .. id .. "@" .. title .. "\n"
676      end
677      return result
678  end
679
680  local function parseQueue(xml)
681      local result = ""
682      local title
683      for title in xml:gmatch("&lt;item%s?[^>]->.-&lt;dc:title%s?[^>]->(.-)&lt;/dc:title>.-&lt;/item>") do
684          title = upnp.decode(title)
685          result = result .. title .. "\n"
686      end
687      return result
688  end
689
690  local function refreshNow(device, force, refreshQueue)
691    debug("refreshNow: device=" .. device)
692
693    if (upnp.proxyVersionAtLeast(1) and force == false) then
694        return ""
695    end
696
697    local uuid = UUIDs[device]
698    if (uuid == nil or uuid == "") then
699        return ""
700    end
701
702    local status, tmp
703    local changed = false
704    local statusString, info, title, title2, artist, album, details, albumArt
705    local currentUri, currentUriMetaData, trackUri, trackUriMetaData, service
706
707    -- Update network and group information
708    local ZoneGroupTopology = upnp.getService(uuid, UPNP_ZONEGROUPTOPOLOGY_SERVICE)
709    if (ZoneGroupTopology ~= nil) then
710        status, tmp = ZoneGroupTopology.GetZoneGroupState({})
711        if (status ~= true) then
712            commsFailure(device, tmp)
713            return ""
714        end
715        if (deviceIsOnline(device)) then
716            setup(device, true)
717        end
718        groupsState = upnp.extractElement("ZoneGroupState", tmp, "")
719        changed = setData("ZoneGroupState", groupsState, device, changed)
720        local members, coordinator = getGroupInfos(uuid)
721        changed = setData("ZonePlayerUUIDsInGroup", members, device, changed)
722        changed = setData("GroupCoordinator", coordinator, device, changed)
723    end
724
725    local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
726    if (AVTransport ~= nil) then
727        -- GetCurrentTransportState  (PLAYING, STOPPED, etc)
728        status, tmp = AVTransport.GetTransportInfo({InstanceID="0"})
729        if (status ~= true) then
730            commsFailure(device, tmp)
731            return ""
732        end
733        changed = setData("TransportState", upnp.extractElement("CurrentTransportState", tmp, ""), device, changed)
734        changed = setData("TransportStatus", upnp.extractElement("CurrentTransportStatus", tmp, ""), device, changed)
735        changed = setData("TransportPlaySpeed", upnp.extractElement("CurrentSpeed", tmp, ""), device, changed)
736
737        -- Get Playmode (NORMAL, REPEAT_ALL, SHUFFLE_NOREPEAT, SHUFFLE)
738        status, tmp = AVTransport.GetTransportSettings({InstanceID="0"})
739        changed = setData("CurrentPlayMode", upnp.extractElement("PlayMode", tmp, ""), device, changed)
740
741        -- Get Crossfademode
742        status, tmp = AVTransport.GetCrossfadeMode({InstanceID="0"})
743        changed = setData("CurrentCrossfadeMode", upnp.extractElement("CrossfadeMode", tmp, ""), device, changed)
744
745        -- Get Current Transport Actions (a CSV of valid Transport Action/Transitions)
746        status, tmp = AVTransport.GetCurrentTransportActions({InstanceID="0"})
747        changed = setData("CurrentTransportActions", upnp.extractElement("Actions", tmp, ""), device, changed)
748
749        -- Get Media Information
750        status, tmp = AVTransport.GetMediaInfo({InstanceID="0"})
751        currentUri = upnp.extractElement("CurrentURI", tmp, "")
752        currentUriMetaData = upnp.extractElement("CurrentURIMetaData", tmp, "")
753        changed = setData("NumberOfTracks", upnp.extractElement("NrTracks", tmp, "NOT_IMPLEMENTED"), device, changed)
754        changed = setData("CurrentMediaDuration", upnp.extractElement("MediaDuration", tmp, "NOT_IMPLEMENTED"), device, changed)
755        changed = setData("AVTransportURI", currentUri, device, changed)
756        changed = setData("AVTransportURIMetaData", currentUriMetaData, device, changed)
757
758        -- Get Current URI - song or radio station etc
759        status, tmp = AVTransport.GetPositionInfo({InstanceID="0"})
760        trackUri = upnp.extractElement("TrackURI", tmp, "")
761        trackUriMetaData = upnp.extractElement("TrackMetaData", tmp, "")
762        changed = setData("CurrentTrack", upnp.extractElement("Track", tmp, "NOT_IMPLEMENTED"), device, changed)
763        changed = setData("CurrentTrackDuration", upnp.extractElement("TrackDuration", tmp, "NOT_IMPLEMENTED"), device, changed)
764        changed = setData("CurrentTrackURI", trackUri, device, changed)
765        changed = setData("CurrentTrackMetaData", trackUriMetaData, device, changed)
766        changed = setData("RelativeTimePosition", upnp.extractElement("RelTime", tmp, "NOT_IMPLEMENTED"), device, changed)
767
768        service, title, statusString, title2, artist, album, details, albumArt =
769            extractDataFromMetaData(device, currentUri, currentUriMetaData, trackUri, trackUriMetaData)
770
771        changed = setData("CurrentService", service, device, changed)
772        changed = setData("CurrentRadio", title, device, changed)
773        changed = setData("CurrentStatus", statusString, device, changed)
774        changed = setData("CurrentTitle", title2, device, changed)
775        changed = setData("CurrentArtist", artist, device, changed)
776        changed = setData("CurrentAlbum", album, device, changed)
777        changed = setData("CurrentDetails", details, device, changed)
778        changed = setData("CurrentAlbumArt", albumArt, device, changed)
779    end
780
781    local Rendering = upnp.getService(uuid, UPNP_RENDERING_CONTROL_SERVICE)
782    if (Rendering ~= nil) then
783        -- Get Mute status
784        status, tmp = Rendering.GetMute({OrderedArgs={"InstanceID=0", "Channel=Master"}})
785        if (status ~= true) then
786            commsFailure(device, tmp)
787            return ""
788        end
789        changed = setData("Mute", upnp.extractElement("CurrentMute", tmp, ""), device, changed)
790
791        -- Get Volume
792        status, tmp = Rendering.GetVolume({OrderedArgs={"InstanceID=0", "Channel=Master"}})
793        changed = setData("Volume", upnp.extractElement("CurrentVolume", tmp, ""), device, changed)
794    end
795
796    -- Sonos queue
797    if (refreshQueue) then
798        if (fetchQueue) then
799            info = upnp.browseContent(uuid, UPNP_MR_CONTENT_DIRECTORY_SERVICE, "Q:0", false, "dc:title", parseQueue, BROWSE_TIMEOUT)
800        else
801            info = ""
802        end
803        changed = setData("Queue", info, device, changed)
804    end
805
806    if (changed) then
807        setVariableValue(HADEVICE_SID, "LastUpdate", os.time(), device)
808    end
809  end
810
811  local function refreshVolumeNow(device)
812    debug("refreshVolumeNow: start")
813
814    if (upnp.proxyVersionAtLeast(1)) then
815        return
816    end
817    local Rendering = upnp.getService(UUIDs[device], UPNP_RENDERING_CONTROL_SERVICE)
818    if (Rendering == nil) then
819        return
820    end
821
822    local status, tmp, changed
823
824    -- Get Volume
825    status, tmp = Rendering.GetVolume({OrderedArgs={"InstanceID=0", "Channel=Master"}})
826
827    if (status ~= true) then
828        commsFailure(device, tmp)
829        return
830    end
831
832    changed = setData("Volume", upnp.extractElement("CurrentVolume", tmp, ""), device, false)
833
834    if (changed) then
835        setVariableValue(HADEVICE_SID, "LastUpdate", os.time(), device)
836    end
837  end
838
839  local function refreshMuteNow(device)
840    debug("refreshMuteNow: start")
841
842    if (upnp.proxyVersionAtLeast(1)) then
843        return
844    end
845    local Rendering = upnp.getService(UUIDs[device], UPNP_RENDERING_CONTROL_SERVICE)
846    if (Rendering == nil) then
847        return
848    end
849
850    local status, tmp, changed
851
852    -- Get Mute status
853    status, tmp = Rendering.GetMute({OrderedArgs={"InstanceID=0", "Channel=Master"}})
854
855    if (status ~= true) then
856        commsFailure(device, tmp)
857        return
858    end
859
860    changed = setData("Mute", upnp.extractElement("CurrentMute", tmp, ""), device, false)
861
862    if (changed) then
863        setVariableValue(HADEVICE_SID, "LastUpdate", os.time(), device)
864    end
865  end
866
867  local function controlAnotherZone(targetUUID, sourceDevice)
868      debug("controlAnotherZone targetUUID=" .. targetUUID .. " sourceDevice=" .. sourceDevice)
869      local device = nil
870      local sourceUUID = UUIDs[sourceDevice]
871      if (targetUUID == sourceUUID) then
872          device = sourceDevice
873      else
874          local targetIP = getIPFromUUID(targetUUID)
875          if (targetIP ~= nil) then
876              device = 0
877              UUIDs[device] = targetUUID
878              ip[device] = targetIP
879              metaDataKeys[targetUUID] = metaDataKeys[sourceUUID]
880              dataTable[targetUUID] = {}
881              if (ip[targetUUID] == nil or ip[targetUUID] ~= targetIP) then
882                  local descrURL = string.format(descriptionURL, targetIP, port)
883                  upnp.resetServices(targetUUID)
884                  local status = upnp.setup(descrURL,
885                                      "urn:schemas-upnp-org:device:ZonePlayer:1",
886                                      { },
887                                      { { "urn:schemas-upnp-org:device:ZonePlayer:1",
888                                          { UPNP_MUSICSERVICES_SERVICE,
889                                            UPNP_ZONEGROUPTOPOLOGY_SERVICE } },
890                                        { "urn:schemas-upnp-org:device:MediaRenderer:1",
891                                         { UPNP_AVTRANSPORT_SERVICE,
892                                            UPNP_RENDERING_CONTROL_SERVICE,
893                                            UPNP_GROUP_RENDERING_CONTROL_SERVICE } },
894                                        { "urn:schemas-upnp-org:device:MediaServer:1",
895                                          { UPNP_MR_CONTENT_DIRECTORY_SERVICE } }})
896                  if (status == true) then
897                      ip[targetUUID] = targetIP
898                  else
899                      ip[targetUUID] = nil
900                      device = nil
901                  end
902              end
903          end
904      end
905      debug("controlAnotherZone result=" .. device)
906      return device
907  end
908
909   local function controlByCoordinator(device)
910      local resDevice = nil
911      local resUUID
912      local coordinator = dataTable[UUIDs[device]].GroupCoordinator or ""
913      if (coordinator ~= "") then
914          resDevice = controlAnotherZone(coordinator, device)
915          resUUID = coordinator
916      end
917      if (resDevice == nil) then
918          resDevice = device
919          resUUID = UUIDs[device]
920      end
921      return resDevice, resUUID
922  end
923
924 local function savePlaybackContexts(device, uuids)
925    debug("savePlaybackContexts: device=" .. device .. " uuids=" .. uuids)
926
927    local cxt = {}
928
929    for uuid in uuids:gmatch("RINCON_%x+") do
930        local device2 = controlAnotherZone(uuid, device)
931        if (device2 ~= nil) then
932            refreshNow(device2, true, false)
933            cxt[uuid] = {}
934            cxt[uuid].TransportState = dataTable[uuid].TransportState
935            cxt[uuid].TransportPlaySpeed = dataTable[uuid].TransportPlaySpeed
936            cxt[uuid].CurrentPlayMode = dataTable[uuid].CurrentPlayMode
937            cxt[uuid].CurrentCrossfadeMode = dataTable[uuid].CurrentCrossfadeMode
938            cxt[uuid].CurrentTransportActions = dataTable[uuid].CurrentTransportActions
939            cxt[uuid].AVTransportURI = dataTable[uuid].AVTransportURI
940            cxt[uuid].AVTransportURIMetaData = dataTable[uuid].AVTransportURIMetaData
941            cxt[uuid].CurrentTrack = dataTable[uuid].CurrentTrack
942            cxt[uuid].CurrentTrackDuration = dataTable[uuid].CurrentTrackDuration
943            cxt[uuid].RelativeTimePosition = dataTable[uuid].RelativeTimePosition
944            cxt[uuid].Mute = dataTable[uuid].Mute
945            cxt[uuid].Volume = dataTable[uuid].Volume
946            cxt[uuid].GroupCoordinator = dataTable[uuid].GroupCoordinator
947        end
948    end
949
950    return { context = cxt, devices = devices }
951  end
952
953  local function restorePlaybackContext(device, uuid, cxt)
954    debug("restorePlaybackContext: device=" .. device .. " uuid=" .. uuid)
955    local instanceId="0"
956    local channel="Master"
957
958    local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
959    local Rendering = upnp.getService(uuid, UPNP_RENDERING_CONTROL_SERVICE)
960
961    if (AVTransport ~= nil) then
962        AVTransport.Stop({InstanceID=instanceId})
963
964        AVTransport.SetAVTransportURI(
965            {OrderedArgs={"InstanceID=" .. instanceId,
966                          "CurrentURI=" .. cxt.AVTransportURI,
967                          "CurrentURIMetaData=" .. cxt.AVTransportURIMetaData}})
968
969        if (cxt.AVTransportURI ~= "") then
970            if (cxt.CurrentTransportActions:find("Seek") ~= nil) then
971                AVTransport.Seek(
972                    {OrderedArgs={"InstanceID=" .. instanceId,
973                                  "Unit=TRACK_NR",
974                                  "Target=" .. cxt.CurrentTrack}})
975
976                if (cxt.CurrentTrackDuration ~= "0:00:00"
977                        and cxt.CurrentTrackDuration ~= "NOT_IMPLEMENTED"
978                        and cxt.RelativeTimePosition ~= "NOT_IMPLEMENTED") then
979                    AVTransport.Seek(
980                        {OrderedArgs={"InstanceID=" .. instanceId,
981                                      "Unit=REL_TIME",
982                                      "Target=" .. cxt.RelativeTimePosition}})
983                end
984            end
985
986            -- Restore repeat, shuffle and cross fade mode only on the group coordinator
987            if (cxt.GroupCoordinator == uuid) then
988                AVTransport.SetPlayMode(
989                    {OrderedArgs={"InstanceID=" .. instanceId,
990                                  "NewPlayMode=" .. cxt.CurrentPlayMode}})
991
992                AVTransport.SetCrossfadeMode(
993                    {OrderedArgs={"InstanceID=" .. instanceId,
994                                  "CrossfadeMode=" .. cxt.CurrentCrossfadeMode}})
995            end
996        end
997    end
998
999    if (Rendering ~= nil) then
1000        Rendering.SetMute(
1001            {OrderedArgs={"InstanceID=" .. instanceId,
1002                          "Channel=" .. channel,
1003                          "DesiredMute=" .. cxt.Mute}})
1004
1005        Rendering.SetVolume(
1006            {OrderedArgs={"InstanceID=" .. instanceId,
1007                          "Channel=" .. channel,
1008                          "DesiredVolume=" .. cxt.Volume}})
1009    end
1010
1011    if (AVTransport ~= nil
1012            and cxt.AVTransportURI ~= ""
1013            and (cxt.TransportState == "PLAYING"
1014                     or cxt.TransportState == "TRANSITIONING")) then
1015        AVTransport.Play(
1016            {OrderedArgs={"InstanceID=" .. instanceId,
1017                          "Speed=" .. cxt.TransportPlaySpeed}})
1018    end
1019
1020    if (device ~= 0) then
1021        refreshNow(device, false, true)
1022    end
1023  end
1024
1025  local function restorePlaybackContexts(device, playCxt)
1026    debug("restorePlaybackContexts: device=" .. device)
1027    local instanceId="0"
1028    local channel="Master"
1029    local localUUID = UUIDs[device]
1030    local device2
1031
1032    if (playCxt == nil) then
1033        warning("Please save the context before restoring it !")
1034        return
1035    end
1036
1037    -- First break the TTS group
1038    for uuid in playCxt.grpMembers:gmatch("RINCON_%x+") do
1039        local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
1040        if (AVTransport ~= nil) then
1041            AVTransport.BecomeCoordinatorOfStandaloneGroup({InstanceID=instanceId})
1042        end
1043    end
1044
1045    -- Then restore context for zone group coordinators
1046    for uuid, cxt in pairs(playCxt.context) do
1047        if (cxt.GroupCoordinator == uuid) then
1048            device2 = 0
1049            if (uuid == localUUID) then
1050                device2 = device
1051            end
1052            restorePlaybackContext(device2, uuid, cxt)
1053        end
1054    end
1055    -- Finally restore context for other zones
1056    for uuid, cxt in pairs(playCxt.context) do
1057        if (cxt.GroupCoordinator ~= uuid) then
1058            device2 = 0
1059            if (uuid == localUUID) then
1060                device2 = device
1061            end
1062            restorePlaybackContext(device2, uuid, cxt)
1063        end
1064    end
1065  end
1066
1067  local function decodeURI(device, coordinator, uri)
1068    local uuid = nil
1069    local track = nil
1070    local uriMetaData = ""
1071    local service, serviceId
1072    local title = nil
1073    local controlByGroup = true
1074    local localUUID = UUIDs[device]
1075    local requireQueuing = false
1076
1077    if (uri:sub(1, 2) == "Q:") then
1078        track = uri:sub(3)
1079        uri = QUEUE_URI:format(coordinator)
1080    elseif (uri:sub(1, 3) == "AI:") then
1081        if (#uri > 3) then
1082            uuid = getUUIDFromZoneName(uri:sub(4))
1083        else
1084            uuid = localUUID
1085        end
1086        if (uuid ~= nil) then
1087            uri = "x-rincon-stream:" .. uuid
1088        else
1089            uri = nil
1090        end
1091    elseif (uri:sub(1, 3) == "SQ:") then
1092        local found = false
1093        if (dataTable[localUUID].SavedQueues ~= nil) then
1094            local line, id, title
1095            for line in dataTable[localUUID].SavedQueues:gmatch("(.-)\n") do
1096                id, title = line:match("^(.+)@(.-)$")
1097                if (id ~= nil and title == uri:sub(4)) then
1098                    found = true
1099                    uri = "ID:" .. id
1100                    break
1101                end
1102            end
1103        end
1104        if (found == false) then
1105            uri = nil
1106        end
1107    elseif (uri:sub(1, 3) == "FR:") then
1108        title = uri:sub(4)
1109        local found = false
1110        if (dataTable[localUUID].FavoritesRadios ~= nil) then
1111            local line, id, title
1112            for line in dataTable[localUUID].FavoritesRadios:gmatch("(.-)\n") do
1113                id, title = line:match("^(.+)@(.-)$")
1114                if (id ~= nil and title == uri:sub(4)) then
1115                    found = true
1116                    uri = "ID:" .. id
1117                    break
1118                end
1119            end
1120        end
1121        if (found == false) then
1122            uri = nil
1123        end
1124    elseif (uri:sub(1, 3) == "SF:") then
1125        title = uri:sub(4)
1126        local found = false
1127        if (dataTable[localUUID].Favorites ~= nil) then
1128            local line, id, title
1129            for line in dataTable[localUUID].Favorites:gmatch("(.-)\n") do
1130                id, title = line:match("^(.+)@(.-)$")
1131                if (id ~= nil and title == uri:sub(4)) then
1132                    found = true
1133                    uri = "ID:" .. id
1134                    break
1135                end
1136            end
1137        end
1138        if (found == false) then
1139            uri = nil
1140        end
1141    elseif (uri:sub(1, 3) == "TR:") then
1142        title = uri:sub(4)
1143        serviceId = getSonosServiceId("TuneIn") or "254"
1144        uri = "x-sonosapi-stream:s" .. uri:sub(4) .. "?sid=" .. serviceId .. "&amp;flags=32"
1145    elseif (uri:sub(1, 3) == "SR:") then
1146        title = uri:sub(4)
1147        serviceId = getSonosServiceId("SiriusXM") or "37"
1148        uri = "x-sonosapi-hls:r%3a" .. title .. "?sid=" .. serviceId .. "&amp;flags=288"
1149    elseif (uri:sub(1, 3) == "GZ:") then
1150        controlByGroup = false
1151        if (#uri > 3) then
1152            uuid = getUUIDFromZoneName(uri:sub(4))
1153        end
1154        if (uuid ~= nil) then
1155            uri = "x-rincon:" .. uuid
1156        else
1157            uri = nil
1158        end
1159    end
1160
1161    if (uri:sub(1, 3) == "ID:") then
1162        local xml = upnp.browseContent(localUUID, UPNP_MR_CONTENT_DIRECTORY_SERVICE, uri:sub(4), true, nil, nil, nil)
1163        debug("data from server: " .. (xml or "nil"))
1164        if (xml == "") then
1165            uri = nil
1166        else
1167            title, uri = xml:match("&lt;DIDL%-Lite%s?[^>]->&lt;item%s?[^>]->.-&lt;dc:title%s?[^>]->(.-)&lt;/dc:title>.-&lt;res%s?[^>]->(.*)&lt;/res>.-&lt;/item>&lt;/DIDL%-Lite>")
1168            if (uri ~= nil) then
1169                uriMetaData = upnp.decode(xml:match("&lt;DIDL%-Lite%s?[^>]->&lt;item%s?[^>]->.-&lt;r:resMD%s?[^>]->(.*)&lt;/r:resMD>.-&lt;/item>&lt;/DIDL%-Lite>") or "")
1170            else
1171                title, uri = xml:match("&lt;DIDL%-Lite%s?[^>]->&lt;container%s?[^>]->.-&lt;dc:title%s?[^>]->(.-)&lt;/dc:title>.-&lt;res%s?[^>]->(.*)&lt;/res>.-&lt;/container>&lt;/DIDL%-Lite>")
1172                if (uri ~= nil) then
1173                    uriMetaData = upnp.decode(xml:match("&lt;DIDL%-Lite%s?[^>]->&lt;container%s?[^>]->.-&lt;r:resMD%s?[^>]->(.*)&lt;/r:resMD>.-&lt;/container>&lt;/DIDL%-Lite>") or "")
1174                end
1175            end
1176        end
1177    end
1178
1179    if (uri ~= nil and
1180           (uri:sub(1, 38) == "file:///jffs/settings/savedqueues.rsq#"
1181               or uri:sub(1, 18) == "x-rincon-playlist:"
1182               or uri:sub(1, 21) == "x-rincon-cpcontainer:")) then
1183        requireQueuing = true
1184    end
1185
1186    if (uri ~= nil and uri ~= "" and uriMetaData == "") then
1187        service, serviceId = getServiceFromURI(uri, nil)
1188        if (serviceId ~= nil and metaDataKeys[localUUID][serviceId] ~= nil) then
1189            if (title == nil) then
1190                uriMetaData = '&lt;DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">'
1191                              .. '&lt;item>&lt;desc>' .. metaDataKeys[localUUID][serviceId] .. '&lt;/desc>'
1192                              .. '&lt;/item>&lt;/DIDL-Lite>'
1193            else
1194                uriMetaData = '&lt;DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">'
1195                              .. '&lt;item>&lt;dc:title>' .. title .. '&lt;/dc:title>'
1196                              .. '&lt;desc>' .. metaDataKeys[localUUID][serviceId] .. '&lt;/desc>'
1197                              .. '&lt;/item>&lt;/DIDL-Lite>'
1198            end
1199        elseif (title ~= nil) then
1200            uriMetaData = '&lt;DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">'
1201                          .. '&lt;item>&lt;dc:title>' .. title .. '&lt;/dc:title>'
1202                          .. '&lt;upnp:class>object.item.audioItem.audioBroadcast&lt;/upnp:class>'
1203                          .. '&lt;/item>&lt;/DIDL-Lite>'
1204        end
1205    end
1206
1207    debug("uri: " .. (uri or "nil"))
1208    debug("uriMetaData: " .. (uriMetaData or "nil"))
1209    return uri, uriMetaData, track, controlByGroup, requireQueuing
1210  end
1211
1212  local function playURI(device, instanceId, uri, speed, volume, uuids, sameVolumeForAll, enqueueMode, newGroup, controlByGroup)
1213    local uriMetaData, track, controlByGroup2, requireQueuing, device2, uuid, status, tmp, position
1214    local channel = "Master"
1215
1216    if (newGroup) then
1217        controlByGroup = false
1218    end
1219
1220    if (controlByGroup) then
1221        device2, uuid = controlByCoordinator(device)
1222    else
1223        uuid = UUIDs[device]
1224    end
1225
1226    uri, uriMetaData, track, controlByGroup2, requireQueuing = decodeURI(device, uuid, uri)
1227    if (controlByGroup and not controlByGroup2) then
1228        controlByGroup = false
1229        uuid = UUIDs[device]
1230    end
1231    if (enqueueMode == nil and requireQueuing) then
1232        enqueueMode = "REPLACE_QUEUE_AND_PLAY"
1233    end
1234
1235    local onlyEnqueue = false
1236    if (enqueueMode == "ENQUEUE" or enqueueMode == "REPLACE_QUEUE"
1237        or enqueueMode == "ENQUEUE_AT_FIRST" or enqueueMode == "ENQUEUE_AT_NEXT_PLAY") then
1238        onlyEnqueue = true
1239    end
1240
1241    local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
1242    local Rendering = upnp.getService(uuid, UPNP_RENDERING_CONTROL_SERVICE)
1243
1244    -- Queue management
1245    if (AVTransport ~= nil and enqueueMode ~= nil
1246           and (uri:sub(1, 12) == "x-file-cifs:"
1247                   or uri:sub(1, 37) == "file:///jffs/settings/savedqueues.rsq"
1248                   or uri:sub(1, 18) == "x-rincon-playlist:"
1249                   or uri:sub(1, 21) == "x-rincon-cpcontainer:")) then
1250        if (enqueueMode == "REPLACE_QUEUE" or enqueueMode == "REPLACE_QUEUE_AND_PLAY") then
1251            AVTransport.RemoveAllTracksFromQueue({InstanceID=instanceId})
1252        end
1253
1254        if (enqueueMode == "ENQUEUE_AT_FIRST" or enqueueMode == "ENQUEUE_AT_FIRST_AND_PLAY") then
1255            status, tmp = AVTransport.AddURIToQueue(
1256               {OrderedArgs={"InstanceID=" .. instanceId,
1257                             "EnqueuedURI=" .. uri,
1258                             "EnqueuedURIMetaData=" .. uriMetaData,
1259                             "DesiredFirstTrackNumberEnqueued=1",
1260                             "EnqueueAsNext=false"}})
1261        elseif (enqueueMode == "ENQUEUE_AT_NEXT_PLAY") then
1262            position = "0"
1263            status, tmp = AVTransport.GetMediaInfo({InstanceID="0"})
1264            if (status == true and upnp.extractElement("CurrentURI", tmp, "") == QUEUE_URI:format(uuid)) then
1265                status, tmp = AVTransport.GetPositionInfo({InstanceID="0"})
1266                if (status == true) then
1267                    position = upnp.extractElement("Track", tmp, "")
1268                    position = tonumber(position)+1
1269                end
1270            end
1271            status, tmp = AVTransport.AddURIToQueue(
1272               {OrderedArgs={"InstanceID=" .. instanceId,
1273                             "EnqueuedURI=" .. uri,
1274                             "EnqueuedURIMetaData=" .. uriMetaData,
1275                             "DesiredFirstTrackNumberEnqueued=" .. position,
1276                             "EnqueueAsNext=false"}})
1277        elseif (enqueueMode == "ENQUEUE" or enqueueMode == "ENQUEUE_AND_PLAY"
1278                or enqueueMode == "REPLACE_QUEUE" or enqueueMode == "REPLACE_QUEUE_AND_PLAY") then
1279            status, tmp = AVTransport.AddURIToQueue(
1280               {OrderedArgs={"InstanceID=" .. instanceId,
1281                             "EnqueuedURI=" .. uri,
1282                             "EnqueuedURIMetaData=" .. uriMetaData,
1283                             "DesiredFirstTrackNumberEnqueued=0",
1284                             "EnqueueAsNext=true"}})
1285        else
1286            status = false
1287        end
1288        if (status == true) then
1289            track = upnp.extractElement("FirstTrackNumberEnqueued", tmp, "")
1290            uri = QUEUE_URI:format(uuid)
1291        else
1292            uri = nil
1293        end
1294    end
1295
1296    if (onlyEnqueue == false and AVTransport ~= nil and uri ~= nil and uri ~= "") then
1297        if (newGroup) then
1298            AVTransport.BecomeCoordinatorOfStandaloneGroup({InstanceID=instanceId})
1299        end
1300
1301        AVTransport.SetAVTransportURI(
1302            {OrderedArgs={"InstanceID=" .. instanceId,
1303                          "CurrentURI=" .. uri,
1304                          "CurrentURIMetaData=" .. uriMetaData}})
1305
1306        local volume2 = volume
1307        if (sameVolumeForAll == false) then
1308            volume2 = nil
1309        end
1310        groupDevices(device, instanceId, uuids, volume2)
1311
1312        if (track ~= nil and track ~= "" and tonumber(track) ~= nil) then
1313            AVTransport.Seek(
1314               {OrderedArgs={"InstanceID=" .. instanceId,
1315                             "Unit=TRACK_NR",
1316                             "Target=" .. track}})
1317        end
1318
1319        if (volume ~= nil and Rendering ~= nil) then
1320            Rendering.SetVolume(
1321                {OrderedArgs={"InstanceID=" .. instanceId,
1322                              "Channel=" .. channel,
1323                              "DesiredVolume=" .. volume}})
1324        end
1325
1326        if (speed ~= nil) then
1327            AVTransport.Play(
1328               {OrderedArgs={"InstanceID=" .. instanceId,
1329                             "Speed=" .. speed}})
1330        end
1331    end
1332  end
1333
1334  local function sayOrAlert(device, parameters, saveAndRestore)
1335    local instanceId = defaultValue(parameters, "InstanceID", "0")
1336    local channel = defaultValue(parameters, "Channel", "Master")
1337    local volume = defaultValue(parameters, "Volume", nil)
1338    local devices = defaultValue(parameters, "GroupDevices", "")
1339    local zones = defaultValue(parameters, "GroupZones", "")
1340    local uri = defaultValue(parameters, "URI", nil)
1341    local duration = defaultValue(parameters, "Duration", "0")
1342    local sameVolume = false
1343    if (parameters.SameVolumeForAll == "true"
1344        or parameters.SameVolumeForAll == "TRUE"
1345        or parameters.SameVolumeForAll == "1") then
1346        sameVolume = true
1347    end
1348
1349    if (uri == nil or uri == "") then
1350        return
1351    end
1352
1353    local uuidListe = ""
1354    local newGroup
1355    local controlByGroup
1356    local localUUID = UUIDs[device]
1357    if (zones:upper() == "CURRENT") then
1358        local coordinator = dataTable[localUUID].GroupCoordinator or ""
1359
1360        if (saveAndRestore == true and sayPlayback[device] == nil) then
1361            sayPlayback[device] = savePlaybackContexts(device, coordinator)
1362            sayPlayback[device].grpMembers = ""
1363        end
1364
1365        newGroup = false
1366        controlByGroup = true
1367    else
1368        local uuid
1369        for id in devices:gmatch("%d+") do
1370            id = tonumber(id)
1371            if (id ~= device) then
1372                uuid = UUIDs[id]
1373                if (uuid == nil) then
1374                    uuid = luup.variable_get(UPNP_DEVICE_PROPERTIES_SID, "SonosID", id)
1375                end
1376                if (uuid ~= nil and uuid ~= "" and uuidListe:find(uuid) == nil) then
1377                    if (uuidListe ~= "") then
1378                        uuidListe = uuidListe .. ","
1379                    end
1380                    uuidListe = uuidListe .. uuid
1381                end
1382            end
1383        end
1384        if (zones:upper() == "ALL") then
1385            local uuids = getAllUUIDs()
1386            for uuid in uuids:gmatch("RINCON_%x+") do
1387                if (uuid ~= localUUID and uuidListe:find(uuid) == nil) then
1388                    if (uuidListe ~= "") then
1389                        uuidListe = uuidListe .. ","
1390                    end
1391                    uuidListe = uuidListe .. uuid
1392                end
1393            end
1394        else
1395            for zone in zones:gmatch("[^,]+") do
1396                uuid = getUUIDFromZoneName(zone)
1397                if (uuid ~= nil and uuid ~= "" and uuid ~= localUUID and uuidListe:find(uuid) == nil) then
1398                    if (uuidListe ~= "") then
1399                        uuidListe = uuidListe .. ","
1400                    end
1401                    uuidListe = uuidListe .. uuid
1402                end
1403            end
1404        end
1405
1406        local members, coordinator
1407
1408        if (saveAndRestore == true and sayPlayback[device] == nil) then
1409            local uuidListe2 = uuidListe
1410            if (uuidListe2:find(localUUID) == nil) then
1411                if (uuidListe2 == "") then
1412                    uuidListe2 = localUUID
1413                else
1414                    uuidListe2 = localUUID .. "," .. uuidListe2
1415                end
1416            end
1417
1418            local uuidListe3 = uuidListe2
1419            for uuid in uuidListe2:gmatch("RINCON_%x+") do
1420                members, coordinator = getGroupInfos(uuid)
1421                if (coordinator == uuid) then
1422                    for uuid2 in members:gmatch("RINCON_%x+") do
1423                        if (uuidListe3:find(uuid2) == nil) then
1424                            uuidListe3 = uuidListe3 .. "," .. uuid2
1425                        end
1426                    end
1427                end
1428            end
1429            sayPlayback[device] = savePlaybackContexts(device, uuidListe3)
1430            sayPlayback[device].grpMembers = uuidListe
1431
1432            -- Break all groups
1433            local cxt
1434            for uuid, cxt in pairs(sayPlayback[device].context) do
1435                if (cxt.GroupCoordinator ~= uuid) then
1436                    local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
1437                    if (AVTransport ~= nil) then
1438                        AVTransport.BecomeCoordinatorOfStandaloneGroup({InstanceID=instanceId})
1439                    end
1440                end
1441            end
1442        end
1443
1444        coordinator = dataTable[localUUID].GroupCoordinator or ""
1445        members = dataTable[localUUID].ZonePlayerUUIDsInGroup or ""
1446        newGroup = false
1447        if (coordinator ~= "" and coordinator == localUUID and members ~= localUUID) then
1448            newGroup = true
1449        end
1450        controlByGroup = false
1451    end
1452
1453    playURI(device, instanceId, uri, "1", volume, uuidListe, sameVolume, nil, newGroup, controlByGroup)
1454
1455    if (saveAndRestore == true) then
1456        luup.call_delay("endSayAlert", duration, device)
1457    end
1458
1459    refreshNow(device, false, false)
1460  end
1461
1462  function endSayAlert(device)
1463    device = tonumber(device)
1464    local finished = tts.endPlayback(device)
1465    if (finished == true) then
1466        restorePlaybackContexts(device, sayPlayback[device])
1467        sayPlayback[device] = nil
1468    end
1469  end
1470
1471  function groupDevices(device, instanceId, uuids, volume)
1472    uuids = uuids or ""
1473    for uuid in uuids:gmatch("RINCON_%x+") do
1474        local device2 = controlAnotherZone(uuid, device)
1475        if (device2 ~= nil) then
1476            playURI(device2, instanceId, "x-rincon:" .. UUIDs[device], "1", volume, nil, false, nil, false, false)
1477        end
1478    end
1479  end
1480
1481  local function joinGroup(device, zone)
1482    local localUUID = UUIDs[device]
1483    local uuid = getUUIDFromZoneName(zone)
1484    if (uuid ~= nil) then
1485        local members, coordinator = getGroupInfos(uuid)
1486        if (members:find(localUUID) == nil) then
1487            playURI(device, "0", "x-rincon:" .. coordinator, "1", nil, nil, false, nil, false, false)
1488        end
1489    end
1490  end
1491
1492  local function leaveGroup(device)
1493    local AVTransport = upnp.getService(UUIDs[device], UPNP_AVTRANSPORT_SERVICE)
1494    if (AVTransport ~= nil) then
1495        AVTransport.BecomeCoordinatorOfStandaloneGroup({InstanceID="0"})
1496    end
1497  end
1498
1499  local function updateGroupMembers(device, members)
1500    local prevMembers, coordinator = getGroupInfos(UUIDs[device])
1501    local uuidListe = ""
1502    local uuid, device2
1503    if (members:upper() == "ALL") then
1504        local uuids = getAllUUIDs()
1505        for uuid in uuids:gmatch("RINCON_%x+") do
1506            if (uuidListe:find(uuid) == nil) then
1507                if (uuidListe ~= "") then
1508                    uuidListe = uuidListe .. ","
1509                end
1510                uuidListe = uuidListe .. uuid
1511            end
1512        end
1513    else
1514        for zone in members:gmatch("[^,]+") do
1515            uuid = getUUIDFromZoneName(zone)
1516            if (uuid ~= nil and uuid ~= "" and uuidListe:find(uuid) == nil) then
1517                if (uuidListe ~= "") then
1518                    uuidListe = uuidListe .. ","
1519                end
1520                uuidListe = uuidListe .. uuid
1521            end
1522        end
1523    end
1524    -- Adding new members
1525    for uuid in uuidListe:gmatch("RINCON_%x+") do
1526        if (prevMembers:find(uuid) == nil) then
1527            device2 = controlAnotherZone(uuid, device)
1528            if (device2 ~= nil) then
1529                playURI(device2, "0", "x-rincon:" .. coordinator, "1", nil, nil, false, nil, false, false)
1530            end
1531        end
1532    end
1533    -- Removing members
1534    for uuid in prevMembers:gmatch("RINCON_%x+") do
1535        if (uuidListe:find(uuid) == nil) then
1536            device2 = controlAnotherZone(uuid, device)
1537            if (device2 ~= nil) then
1538                local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
1539                if (AVTransport ~= nil) then
1540                    AVTransport.BecomeCoordinatorOfStandaloneGroup({InstanceID="0"})
1541                end
1542            end
1543        end
1544    end
1545  end
1546
1547  local function pauseAll(device)
1548    local uuids = getAllUUIDs()
1549    for uuid in uuids:gmatch("RINCON_%x+") do
1550        local device2 = controlAnotherZone(uuid, device)
1551        if (device2 ~= nil) then
1552            local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
1553            if (AVTransport ~= nil) then
1554                AVTransport.Pause({InstanceID="0"})
1555            end
1556        end
1557    end
1558  end
1559
1560  local function setupTTSSettings(device)
1561    local lang = luup.variable_get(SONOS_SID, "DefaultLanguageTTS", device)
1562    if (lang == nil) then
1563        lang = "en"
1564        luup.variable_set(SONOS_SID, "DefaultLanguageTTS", lang, device)
1565    end
1566    local engine = luup.variable_get(SONOS_SID, "DefaultEngineTTS", device)
1567    if (engine == nil) then
1568        engine = "GOOGLE"
1569        luup.variable_set(SONOS_SID, "DefaultEngineTTS", engine, device)
1570    end
1571    local googleURL = luup.variable_get(SONOS_SID, "GoogleTTSServerURL", device)
1572    if (googleURL == nil) then
1573        googleURL = "http://translate.google.com"
1574        luup.variable_set(SONOS_SID, "GoogleTTSServerURL", googleURL, device)
1575    end
1576    local serverURL = luup.variable_get(SONOS_SID, "OSXTTSServerURL", device)
1577    if (serverURL == nil) then
1578        serverURL = ""
1579        luup.variable_set(SONOS_SID, "OSXTTSServerURL", serverURL, device)
1580    end
1581    tts.setup(lang, engine, sayOrAlert, LOCAL_BASE_WEB_URL:format(VERA_IP, VERA_WEB_PORT), googleURL, serverURL)
1582  end
1583
1584  function updateWithoutProxy(device)
1585    if (upnp.proxyVersionAtLeast(1) == false) then
1586        luup.call_delay("updateWithoutProxy", 15, device)
1587    end
1588    device = tonumber(device)
1589
1590    refreshNow(device, true, true)
1591  end
1592
1593  local function getAvailableServices(uuid)
1594    debug("getAvailableServices: start")
1595    local services = {}
1596    local MusicServices = upnp.getService(uuid, UPNP_MUSICSERVICES_SERVICE)
1597    if (MusicServices == nil) then
1598        return services
1599    end
1600    local status, tmp, item, id, name, changed
1601    local tag = "Service"
1602    status, tmp = MusicServices.ListAvailableServices({})
1603    if (status == true) then
1604        tmp = upnp.extractElement("AvailableServiceDescriptorList", tmp, "")
1605        for item in tmp:gmatch("(&lt;"..tag.."%s.-&lt;/"..tag..">)") do
1606            id = item:match("&lt;"..tag..'%s?.-%sId="([^"]+)"[^>]->.-&lt;/'..tag..">")
1607            name = item:match("&lt;"..tag..'%s?.-%sName="([^"]+)"[^>]->.-&lt;/'..tag..">")
1608            if (id ~= nil and name ~= nil) then
1609                debug("getAvailableServices: " .. string.format('%s => %s', id, name))
1610                services[id] = name
1611            end
1612        end
1613    end
1614    return services
1615  end
1616
1617  function setup(device, init)
1618    device = tonumber(device)
1619
1620    local changed = false
1621    local info
1622
1623    local newDevice = false
1624    local newIP
1625     if (device == 0) then
1626        newIP = ip[device]
1627    else
1628        newIP = luup.attr_get("ip", device)
1629    end
1630    if (newIP ~= ip[device]) then
1631        newDevice = true
1632        upnp.resetServices(UUIDs[device])
1633        upnp.cancelProxySubscriptions(EventSubscriptions)
1634        UUIDs[device] = ""
1635        ip[device] = newIP
1636        changed = setData("ZoneName", "", device, changed)
1637        changed = setData("SonosID", "", device, changed)
1638        changed = setData("SonosModelName", "", device, changed)
1639        changed = setData("SonosModel", "", device, changed)
1640        changed = setData("SonosOnline", "0", device, changed)
1641    end
1642
1643    if (ip[device] == nil or ip[device] == "") then
1644        changed = setData("ZoneName", "", device, changed)
1645        changed = setData("SonosID", "", device, changed)
1646        changed = setData("SonosModelName", "", device, changed)
1647        changed = setData("SonosModel", "", device, changed)
1648        changed = setData("ProxyUsed", "", device, changed)
1649        deviceIsOffline(device)
1650        return
1651    end
1652
1653    local descrURL = string.format(descriptionURL, ip[device], port)
1654    local status, servicesLoaded, values, icon =
1655                           upnp.setup(descrURL,
1656                                      "urn:schemas-upnp-org:device:ZonePlayer:1",
1657                                      { "UDN", "roomName", "modelName", "modelNumber" },
1658                                      { { "urn:schemas-upnp-org:device:ZonePlayer:1",
1659                                          { UPNP_MUSICSERVICES_SERVICE,
1660                                            UPNP_ZONEGROUPTOPOLOGY_SERVICE } },
1661                                        { "urn:schemas-upnp-org:device:MediaRenderer:1",
1662                                          { UPNP_AVTRANSPORT_SERVICE,
1663                                            UPNP_RENDERING_CONTROL_SERVICE,
1664                                            UPNP_GROUP_RENDERING_CONTROL_SERVICE } },
1665                                        { "urn:schemas-upnp-org:device:MediaServer:1",
1666                                          { UPNP_MR_CONTENT_DIRECTORY_SERVICE } }})
1667    if (status == false) then
1668        deviceIsOffline(device)
1669        return
1670    end
1671
1672    local newOnline = deviceIsOnline(device)
1673
1674    local uuid = values.UDN:match("uuid:(.+)") or ""
1675    UUIDs[device] = uuid
1676    ip[uuid] = ip[device]
1677    if (dataTable[uuid] == nil) then
1678        dataTable[uuid] = {}
1679    end
1680    changed = setData("ZoneName", values.roomName or "", device, changed)
1681    changed = setData("SonosID", uuid, device, changed)
1682    changed = setData("SonosModelName", values.modelName or "", device, changed)
1683    local model = 0
1684    if (values.modelNumber == "S3") then
1685        model = 1
1686    elseif (values.modelNumber == "S5") then
1687        model = 2
1688    elseif (values.modelNumber == "ZP80") then
1689        model = 3
1690    elseif (values.modelNumber == "ZP90") then
1691        model = 3
1692    elseif (values.modelNumber == "ZP100") then
1693        model = 4
1694    elseif (values.modelNumber == "ZP120") then
1695        model = 4
1696    elseif (values.modelNumber == "S9") then
1697        model = 5
1698    elseif (values.modelNumber == "S1") then
1699        model = 6
1700    end
1701    changed = setData("SonosModel", string.format("%d", model), device, changed)
1702
1703    if (icon ~= nil) then
1704        iconURL = icon
1705    else
1706        iconURL = PLUGIN_ICON:format(VERA_IP, VERA_WEB_PORT)
1707    end
1708
1709    if (device ~= 0 and (init or newDevice or newOnline)) then
1710        upnp.subscribeToEvents(device, VERA_IP, EventSubscriptions, SONOS_SID, uuid)
1711    end
1712
1713    if (upnp.proxyVersionAtLeast(1)) then
1714        changed = setData("ProxyUsed", "proxy is in use", device, changed)
1715        BROWSE_TIMEOUT = 30
1716    else
1717        changed = setData("ProxyUsed", "proxy is not in use", device, changed)
1718        BROWSE_TIMEOUT = 5
1719    end
1720
1721    if (init or newDevice or newOnline) then
1722        sonosServices = getAvailableServices(uuid)
1723        metaDataKeys[uuid] = loadServicesMetaDataKeys(device)
1724    end
1725
1726    if (init or newDevice or newOnline or upnp.proxyVersionAtLeast(1) == false) then
1727        -- Sonos playlists
1728        info = upnp.browseContent(uuid, UPNP_MR_CONTENT_DIRECTORY_SERVICE, "SQ:", false, "dc:title", parseSavedQueues, BROWSE_TIMEOUT)
1729        changed = setData("SavedQueues", info, device, changed)
1730
1731        -- Favorites radio stations
1732        info = upnp.browseContent(uuid, UPNP_MR_CONTENT_DIRECTORY_SERVICE, "R:0/0", false, "dc:title", parseIdTitle, BROWSE_TIMEOUT)
1733        changed = setData("FavoritesRadios", info, device, changed)
1734
1735        -- Sonos favorites
1736        info = upnp.browseContent(uuid, UPNP_MR_CONTENT_DIRECTORY_SERVICE, "FV:2", false, "dc:title", parseIdTitle, BROWSE_TIMEOUT)
1737        changed = setData("Favorites", info, device, changed)
1738    end
1739
1740    if (changed) then
1741        setVariableValue(HADEVICE_SID, "LastUpdate", os.time(), device)
1742    end
1743
1744    if (newDevice or newOnline) then
1745        refreshNow(device, true, true)
1746    end
1747  end
1748
1749  local function getCheckStateRate(device)
1750    local rate = luup.variable_get(SONOS_SID, "CheckStateRate", device)
1751    if (rate == nil or tonumber(rate) == nil) then
1752        rate = "0"
1753        luup.variable_set(SONOS_SID, "CheckStateRate", rate, device)
1754    end
1755    return tonumber(rate) * 60
1756  end
1757
1758  local function setCheckStateRate(device, rate)
1759    debug("setCheckStateRate rate=" .. (rate or "nil"))
1760    if (rate == nil or tonumber(rate) == nil) then
1761        rate = "0"
1762    end
1763    luup.variable_set(SONOS_SID, "CheckStateRate", rate, device)
1764
1765    idConfRefresh = idConfRefresh + 1
1766
1767    checkDeviceState(idConfRefresh .. ":" .. device)
1768  end
1769
1770  function checkDeviceState(data)
1771    debug("checkDeviceState " .. data)
1772    local cpt, device = data:match("(%d+):(%d+)")
1773    if (cpt ~= nil and device ~= nil) then
1774        cpt = tonumber(cpt)
1775        if (cpt == nil or cpt ~= idConfRefresh) then
1776            return
1777        end
1778        device = tonumber(device)
1779        local rate = getCheckStateRate(device)
1780        if (rate > 0) then
1781            luup.call_delay("checkDeviceState", rate, idConfRefresh .. ":" .. device)
1782            setup(device, false)
1783        end
1784    end
1785  end
1786
1787  local function handleRenderingChange(device, event)
1788      debug("handleRenderingChange for device " .. device .. " value " .. event)
1789      local changed = false
1790      local token, attributes, attr, value
1791      local tmp = event:match("&lt;Event%s?[^>]->&lt;InstanceID%s?[^>]->(.+)&lt;/InstanceID>&lt;/Event>")
1792      if (tmp ~= nil) then
1793          for token, attributes in tmp:gmatch('&lt;([a-zA-Z0-9:]+)(%s?.-)/>') do
1794              local attrTable = {}
1795              for attr, value in attributes:gmatch('%s(.-)="(.-)"') do
1796                  attrTable[attr] = value
1797              end
1798              if (attrTable.val ~= nil and
1799                      (attrTable.channel == "Master" or attrTable.channel == nil)) then
1800                  changed = setData(token, upnp.decode(attrTable.val), device, changed)
1801              end
1802          end
1803      end
1804      if (changed) then
1805          setVariableValue(HADEVICE_SID, "LastUpdate", os.time(), device)
1806      end
1807  end
1808
1809  local function handleAVTransportChange(device, uuid, event)
1810      debug("handleAVTransportChange for device " .. device .. " UUID " .. uuid .. " value " .. event)
1811      local statusString, title, title2, artist, album, details, albumArt, info, desc
1812      local currentUri, currentUriMetaData, trackUri, trackUriMetaData, service, serviceId
1813      local changed = false
1814      local token, attributes, attr, value
1815      local tmp = event:match("&lt;Event%s?[^>]->&lt;InstanceID%s?[^>]->(.+)&lt;/InstanceID>&lt;/Event>")
1816      if (tmp ~= nil) then
1817          local found = false
1818          local found2 = false
1819          for token, attributes in tmp:gmatch('&lt;([a-zA-Z0-9:]+)(%s?.-)/>') do
1820              if (token == "RelativeTimePosition") then
1821                  found = true
1822              elseif (token == "r:EnqueuedTransportURIMetaData") then
1823                  found2 = true
1824              end
1825              local attrTable = {}
1826              for attr, value in attributes:gmatch('%s(.-)="(.-)"') do
1827                  attrTable[attr] = value
1828              end
1829              if (attrTable.val ~= nil) then
1830                  changed = setData(token, upnp.decode(attrTable.val), device, changed)
1831              end
1832          end
1833
1834          currentUri = dataTable[uuid].AVTransportURI
1835          currentUriMetaData = dataTable[uuid].AVTransportURIMetaData
1836          trackUri = dataTable[uuid].CurrentTrackURI
1837          trackUriMetaData = dataTable[uuid].CurrentTrackMetaData
1838          service, title, statusString, title2, artist, album, details, albumArt =
1839              extractDataFromMetaData(device, currentUri, currentUriMetaData, trackUri, trackUriMetaData)
1840          changed = setData("CurrentService", service, device, changed)
1841          changed = setData("CurrentRadio", title, device, changed)
1842          changed = setData("CurrentStatus", statusString, device, changed)
1843          changed = setData("CurrentTitle", title2, device, changed)
1844          changed = setData("CurrentArtist", artist, device, changed)
1845          changed = setData("CurrentAlbum", album, device, changed)
1846          changed = setData("CurrentDetails", details, device, changed)
1847          changed = setData("CurrentAlbumArt", albumArt, device, changed)
1848
1849          if (not found) then
1850              local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
1851              if (AVTransport ~= nil) then
1852                  local status, tmp2 = AVTransport.GetPositionInfo({InstanceID="0"})
1853                  changed = setData("RelativeTimePosition", upnp.extractElement("RelTime", tmp2, "NOT_IMPLEMENTED"), device, changed)
1854              end
1855          end
1856
1857          if (found2) then
1858              info, title, artist, album, details, albumArt, desc =
1859                  getSimpleDIDLStatus(dataTable[uuid]["r:EnqueuedTransportURIMetaData"])
1860              service, serviceId = getServiceFromURI(currentUri, trackUri)
1861              updateServicesMetaDataKeys(device, serviceId, desc)
1862          end
1863      end
1864      if (changed) then
1865          setVariableValue(HADEVICE_SID, "LastUpdate", os.time(), device)
1866      end
1867  end
1868
1869  local function handleContentDirectoryChange(device, uuid, id)
1870      debug("handleContentDirectoryChange for device " .. device .. " UUID " .. uuid .. " value " .. id)
1871      local info
1872      local changed = false
1873
1874      if (id:find("SQ:,") == 1) then
1875          -- Sonos playlists
1876          info = upnp.browseContent(uuid, UPNP_MR_CONTENT_DIRECTORY_SERVICE, "SQ:", false, "dc:title", parseSavedQueues, BROWSE_TIMEOUT)
1877          changed = setData("SavedQueues", info, device, changed)
1878      elseif (id:find("R:0,") == 1) then
1879          -- Favorites radio stations
1880          info = upnp.browseContent(uuid, UPNP_MR_CONTENT_DIRECTORY_SERVICE, "R:0/0", false, "dc:title", parseIdTitle, BROWSE_TIMEOUT)
1881          changed = setData("FavoritesRadios", info, device, changed)
1882      elseif (id:find("FV:2,") == 1) then
1883          -- Sonos favorites
1884          info = upnp.browseContent(uuid, UPNP_MR_CONTENT_DIRECTORY_SERVICE, "FV:2", false, "dc:title", parseIdTitle, BROWSE_TIMEOUT)
1885          changed = setData("Favorites", info, device, changed)
1886      elseif (id:find("Q:0,") == 1) then
1887          -- Sonos queue
1888          if (fetchQueue) then
1889              info = upnp.browseContent(uuid, UPNP_MR_CONTENT_DIRECTORY_SERVICE, "Q:0", false, "dc:title", parseQueue, BROWSE_TIMEOUT)
1890          else
1891              info = ""
1892          end
1893          changed = setData("Queue", info, device, changed)
1894      end
1895      if (changed) then
1896          setVariableValue(HADEVICE_SID, "LastUpdate", os.time(), device)
1897      end
1898  end
1899
1900  function renewSubscriptions(data)
1901    local device, uuid = data:match("(%d+):(.*)")
1902    if (device ~= nil and uuid ~= nil) then
1903        device = tonumber(device)
1904        debug("Renewal of all event subscriptions for device " .. device)
1905        if (uuid ~= UUIDs[device]) then
1906            debug("Renewal ignored for uuid " .. uuid)
1907        elseif (upnp.subscribeToEvents(device, VERA_IP, EventSubscriptions, SONOS_SID, UUIDs[device]) == false) then
1908            setup(device, true)
1909        end
1910    end
1911  end
1912
1913  function cancelProxySubscription(sid)
1914    upnp.cancelProxySubscription(sid)
1915  end
1916
1917  function setDebugLogs(device, enable)
1918    debug("setDebugLogs " .. (enable or "nil"))
1919
1920    if ((enable == "true") or (enable == "yes"))
1921    then
1922        enable = "1"
1923    elseif ((enable == "false") or (enable == "no"))
1924    then
1925        enable = "0"
1926    end
1927    if ((enable ~= "0") and (enable ~= "1"))
1928    then
1929        task("SetDebugLogs: invalid argument", TASK_ERROR)
1930        return
1931    end
1932
1933    luup.variable_set(SONOS_SID, "DebugLogs", enable, device)
1934    if (enable == "1")
1935    then
1936        DEBUG_MODE = true
1937    else
1938        DEBUG_MODE = false
1939    end
1940  end
1941
1942  function setReadQueueContent(device, enable)
1943    debug("setReadQueueContent " .. (enable or "nil"))
1944
1945    if ((enable == "true") or (enable == "yes"))
1946    then
1947        enable = "1"
1948    elseif ((enable == "false") or (enable == "no"))
1949    then
1950        enable = "0"
1951    end
1952    if ((enable ~= "0") and (enable ~= "1"))
1953    then
1954        task("SetReadQueueContent: invalid argument", TASK_ERROR)
1955        return
1956    end
1957
1958    luup.variable_set(SONOS_SID, "FetchQueue", enable, device)
1959    if (enable == "1")
1960    then
1961        fetchQueue = true
1962    else
1963        fetchQueue = false
1964    end
1965    handleContentDirectoryChange(device, UUIDs[device], "Q:0,")
1966  end
1967
1968  function SonosReload()
1969    luup.call_action("urn:micasaverde-com:serviceId:HomeAutomationGateway1", "Reload", {}, 0)
1970  end
1971
1972  function deferredSonosStartup(device)
1973    debug("deferredSonosStartup: start " .. device)
1974    device = tonumber(device)
1975
1976    -- Check that the proxy is running.
1977    for retries = 1, 3 do
1978        local version = upnp.getProxyApiVersion()
1979        if (version) then
1980            log("UPnP proxy event identified - API version " .. version)
1981            break
1982        end
1983    end
1984
1985-- the next line has to be uncommented to force the old mode without the UPnP event proxy
1986-- upnp.unuseProxy()
1987
1988    if (luup.devices[device].device_type == SONOS_DEVICE_TYPE) then
1989        UUIDs[device] = ""
1990        ip[device] = luup.attr_get("ip", device)
1991
1992        setupTTSSettings(device)
1993
1994        idConfRefresh = 0
1995        local rate = getCheckStateRate(device)
1996        if (rate > 0) then
1997            luup.call_delay("checkDeviceState", rate, idConfRefresh .. ":" .. device)
1998        end
1999        setup(device, true)
2000        updateWithoutProxy(device)
2001    end
2002  end
2003
2004  function sonosStartup(lul_device)
2005    log("#" .. lul_device .. " starting up with id " .. luup.devices[lul_device].id)
2006
2007    luup.variable_set(SONOS_SID, "PluginVersion", PLUGIN_VERSION, lul_device)
2008
2009    if (luup.variable_get(SONOS_SID, "DiscoveryResult", lul_device) == nil
2010            or luup.variable_get(SONOS_SID, "DiscoveryResult", lul_device) == "scanning") then
2011        luup.variable_set(SONOS_SID, "DiscoveryResult", "", lul_device)
2012    end
2013
2014    local routerIp = luup.variable_get(SONOS_SID, "RouterIp", lul_device)
2015    if (routerIp == nil) then
2016        routerIp = ""
2017        luup.variable_set(SONOS_SID, "RouterIp", routerIp, lul_device)
2018    end
2019    local routerPort = luup.variable_get(SONOS_SID, "RouterPort", lul_device)
2020    if (routerPort == nil or routerPort == "") then
2021        routerPort = "80"
2022        luup.variable_set(SONOS_SID, "RouterPort", routerPort, lul_device)
2023    end
2024    routerPort = tonumber(routerPort)
2025
2026    local rate = luup.variable_get(SONOS_SID, "CheckStateRate", lul_device)
2027    if (rate == nil or tonumber(rate) == nil) then
2028        luup.variable_set(SONOS_SID, "CheckStateRate", "0", lul_device)
2029    end
2030
2031    local debugLogs = luup.variable_get(SONOS_SID, "DebugLogs", lul_device)
2032    if (debugLogs == nil or tonumber(debugLogs) == nil) then
2033        luup.variable_set(SONOS_SID, "DebugLogs", "0", lul_device)
2034    end
2035    if (luup.variable_get(SONOS_SID, "DebugLogs", lul_device) == "1") then
2036        DEBUG_MODE = true
2037    end
2038
2039    local fetch = luup.variable_get(SONOS_SID, "FetchQueue", lul_device)
2040    if (fetch == nil or tonumber(fetch) == nil) then
2041        luup.variable_set(SONOS_SID, "FetchQueue", "1", lul_device)
2042    end
2043    if (luup.variable_get(SONOS_SID, "FetchQueue", lul_device) == "0") then
2044        fetchQueue = false
2045    end
2046
2047    --
2048    -- Acquire the IP Address of Vera itself, needed for the Say method later on.
2049    -- Note: We're assuming Vera is connected via it's WAN Port to the Sonos devices
2050    --
2051    local stdout = io.popen("GetNetworkState.sh ip_wan")
2052    VERA_LOCAL_IP = stdout:read("*a")
2053    stdout:close()
2054    debug("sonosStartup: Vera IP Address=" .. VERA_LOCAL_IP)
2055
2056    if (routerIp == "") then
2057        VERA_IP = VERA_LOCAL_IP
2058        VERA_WEB_PORT = 80
2059    else
2060        VERA_IP = routerIp
2061        VERA_WEB_PORT = routerPort
2062    end
2063    upnp.initialize(debug, warning, error)
2064
2065    tts.initialize(debug, warning, error)
2066
2067    port = 1400
2068    descriptionURL = "http://%s:%s/xml/device_description.xml"
2069    iconURL = PLUGIN_ICON:format(VERA_IP, VERA_WEB_PORT)
2070
2071    if (upnp.isDiscoveryPatchInstalled(VERA_LOCAL_IP)) then
2072        luup.variable_set(SONOS_SID, "DiscoveryPatchInstalled", "1", lul_device)
2073    else
2074        luup.variable_set(SONOS_SID, "DiscoveryPatchInstalled", "0", lul_device)
2075    end
2076
2077    luup.call_delay("deferredSonosStartup", 1, lul_device)
2078  end
2079</functions>
2080<startup>sonosStartup</startup>
2081<actionList>
2082  <action>
2083    <serviceId>urn:micasaverde-com:serviceId:MediaNavigation1</serviceId>
2084    <name>Play</name>
2085    <run>
2086      local device, uuid = controlByCoordinator(lul_device)
2087      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2088      if (AVTransport == nil) then
2089          return
2090      end
2091
2092      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2093      local speed = defaultValue(lul_settings, "Speed", "1")
2094
2095      AVTransport.Play(
2096          {OrderedArgs={"InstanceID=" .. instanceId,
2097                        "Speed=" ..speed}})
2098
2099      -- Force a refresh when current service is Pandora due to a bug (missing notification)
2100      local force = false
2101      local currentUri = dataTable[uuid].AVTransportURI
2102      if (currentUri ~= nil and currentUri:find("pndrradio:") == 1) then
2103          force = true
2104      end
2105      if (device ~= 0) then
2106          refreshNow(device, force, false)
2107      end
2108    </run>
2109  </action>
2110
2111  <action>
2112    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2113    <name>Play</name>
2114    <run>
2115      local device, uuid = controlByCoordinator(lul_device)
2116      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2117      if (AVTransport == nil) then
2118          return
2119      end
2120
2121      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2122      local speed = defaultValue(lul_settings, "Speed", "1")
2123
2124      AVTransport.Play(
2125          {OrderedArgs={"InstanceID=" .. instanceId,
2126                        "Speed=" .. speed}})
2127
2128      -- Force a refresh when current service is Pandora due to a bug (missing notification)
2129      local force = false
2130      local currentUri = dataTable[uuid].AVTransportURI
2131      if (currentUri ~= nil and currentUri:find("pndrradio:") == 1) then
2132          force = true
2133      end
2134      if (device ~= 0) then
2135          refreshNow(device, force, false)
2136      end
2137    </run>
2138  </action>
2139
2140  <action>
2141    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
2142    <name>Say</name>
2143    <run>
2144      debug("Say: " .. (lul_settings.Text or "nil"))
2145      tts.queueAlert(lul_device, lul_settings)
2146    </run>
2147  </action>
2148
2149  <action>
2150    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
2151    <name>SetupTTS</name>
2152    <run>
2153      luup.variable_set(SONOS_SID, "DefaultLanguageTTS", lul_settings.DefaultLanguage or "en", lul_device)
2154      luup.variable_set(SONOS_SID, "DefaultEngineTTS", lul_settings.DefaultEngine or "GOOGLE", lul_device)
2155      luup.variable_set(SONOS_SID, "OSXTTSServerURL", lul_settings.OSXTTSServerURL or "", lul_device)
2156      luup.variable_set(SONOS_SID, "GoogleTTSServerURL", lul_settings.GoogleTTSServerURL or "http://translate.google.com", lul_device)
2157      setupTTSSettings(lul_device)
2158    </run>
2159  </action>
2160
2161  <action>
2162    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2163    <name>Seek</name>
2164    <run>
2165      local device, uuid = controlByCoordinator(lul_device)
2166      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2167      if (AVTransport == nil) then
2168          return
2169      end
2170
2171      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2172      local unit = defaultValue(lul_settings, "Unit", "")
2173      local target = defaultValue(lul_settings, "Target", "")
2174
2175      AVTransport.Seek(
2176          {OrderedArgs={"InstanceID=" ..instanceId,
2177                        "Unit=" .. unit,
2178                        "Target=" .. target}})
2179    </run>
2180  </action>
2181
2182  <action>
2183    <serviceId>urn:micasaverde-com:serviceId:MediaNavigation1</serviceId>
2184    <name>Pause</name>
2185    <run>
2186      local device, uuid = controlByCoordinator(lul_device)
2187      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2188      if (AVTransport == nil) then
2189          return
2190      end
2191
2192      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2193
2194      AVTransport.Pause({InstanceID=instanceId})
2195
2196      -- Force a refresh when current service is Pandora due to a bug (missing notification)
2197      local force = false
2198      local currentUri = dataTable[uuid].AVTransportURI
2199      if (currentUri ~= nil and currentUri:find("pndrradio:") == 1) then
2200          force = true
2201      end
2202      if (device ~= 0) then
2203          refreshNow(device, force, false)
2204      end
2205    </run>
2206  </action>
2207
2208  <action>
2209    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2210    <name>Pause</name>
2211    <run>
2212      local device, uuid = controlByCoordinator(lul_device)
2213      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2214      if (AVTransport == nil) then
2215          return
2216      end
2217
2218      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2219
2220      AVTransport.Pause({InstanceID=instanceId})
2221
2222      -- Force a refresh when current service is Pandora due to a bug (missing notification)
2223      local force = false
2224      local currentUri = dataTable[uuid].AVTransportURI
2225      if (currentUri ~= nil and currentUri:find("pndrradio:") == 1) then
2226          force = true
2227      end
2228      if (device ~= 0) then
2229          refreshNow(device, force, false)
2230      end
2231    </run>
2232  </action>
2233
2234
2235  <action>
2236    <serviceId>urn:micasaverde-com:serviceId:MediaNavigation1</serviceId>
2237    <name>Stop</name>
2238    <run>
2239      local device, uuid = controlByCoordinator(lul_device)
2240      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2241      if (AVTransport == nil) then
2242          return
2243      end
2244
2245      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2246
2247      AVTransport.Stop({InstanceID=instanceId})
2248
2249      if (device ~= 0) then
2250          refreshNow(device, false, false)
2251      end
2252    </run>
2253  </action>
2254
2255  <action>
2256    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2257    <name>Stop</name>
2258    <run>
2259      local device, uuid = controlByCoordinator(lul_device)
2260      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2261      if (AVTransport == nil) then
2262          return
2263      end
2264
2265      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2266
2267      AVTransport.Stop({InstanceID=instanceId})
2268
2269      if (device ~= 0) then
2270          refreshNow(device, false, false)
2271      end
2272    </run>
2273  </action>
2274
2275  <action>
2276    <serviceId>urn:micasaverde-com:serviceId:MediaNavigation1</serviceId>
2277    <name>SkipDown</name>
2278    <run>
2279      local device, uuid = controlByCoordinator(lul_device)
2280      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2281      if (AVTransport == nil) then
2282          return
2283      end
2284
2285      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2286
2287      AVTransport.Next({InstanceID=instanceId})
2288
2289      -- Force a refresh when current service is Pandora due to a bug (missing notification)
2290      local force = false
2291      local currentUri = dataTable[uuid].AVTransportURI
2292      if (currentUri ~= nil and currentUri:find("pndrradio:") == 1) then
2293          force = true
2294      end
2295      if (device ~= 0) then
2296          refreshNow(device, force, false)
2297      end
2298    </run>
2299  </action>
2300
2301  <action>
2302    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2303    <name>Next</name>
2304    <run>
2305      local device, uuid = controlByCoordinator(lul_device)
2306      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2307      if (AVTransport == nil) then
2308          return
2309      end
2310
2311      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2312
2313      AVTransport.Next({InstanceID=instanceId})
2314    </run>
2315  </action>
2316
2317  <action>
2318    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2319    <name>NextProgrammedRadioTracks</name>
2320    <run>
2321      local device, uuid = controlByCoordinator(lul_device)
2322      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2323      if (AVTransport == nil) then
2324          return
2325      end
2326
2327      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2328
2329      AVTransport.NextProgrammedRadioTracks({InstanceID=instanceId})
2330    </run>
2331  </action>
2332
2333  <action>
2334    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2335    <name>NextSection</name>
2336    <run>
2337      local device, uuid = controlByCoordinator(lul_device)
2338      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2339      if (AVTransport == nil) then
2340          return
2341      end
2342
2343      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2344
2345      AVTransport.NextSection({InstanceID=instanceId})
2346    </run>
2347  </action>
2348
2349  <action>
2350    <serviceId>urn:micasaverde-com:serviceId:MediaNavigation1</serviceId>
2351    <name>SkipUp</name>
2352    <run>
2353      local device, uuid = controlByCoordinator(lul_device)
2354      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2355      if (AVTransport == nil) then
2356          return
2357      end
2358
2359      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2360
2361      AVTransport.Previous({InstanceID=instanceId})
2362
2363      if (device ~= 0) then
2364          refreshNow(device, false, false)
2365      end
2366    </run>
2367  </action>
2368
2369  <action>
2370    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2371    <name>Previous</name>
2372    <run>
2373      local device, uuid = controlByCoordinator(lul_device)
2374      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2375      if (AVTransport == nil) then
2376          return
2377      end
2378
2379      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2380
2381      AVTransport.Previous({InstanceID=instanceId})
2382    </run>
2383  </action>
2384
2385  <action>
2386    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2387    <name>PreviousSection</name>
2388    <run>
2389      local device, uuid = controlByCoordinator(lul_device)
2390      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2391      if (AVTransport == nil) then
2392          return
2393      end
2394
2395      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2396
2397      AVTransport.PreviousSection({InstanceID=instanceId})
2398    </run>
2399  </action>
2400
2401  <action>
2402    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2403    <name>GetPositionInfo</name>
2404    <run>
2405      local device, uuid = controlByCoordinator(lul_device)
2406      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2407      if (AVTransport == nil) then
2408          return
2409      end
2410
2411      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2412
2413      local status, tmp = AVTransport.GetPositionInfo({InstanceID=instanceId})
2414      setData("RelativeTimePosition", upnp.extractElement("RelTime", tmp, "NOT_IMPLEMENTED"), device, false)
2415    </run>
2416  </action>
2417
2418  <action>
2419    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2420    <name>SetPlayMode</name>
2421    <run>
2422      local device, uuid = controlByCoordinator(lul_device)
2423      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2424      if (AVTransport == nil) then
2425          return
2426      end
2427
2428      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2429      local newPlayMode = defaultValue(lul_settings, "NewPlayMode", "NORMAL")
2430
2431      -- NORMAL, SHUFFLE, SHUFFLE_NOREPEAT, REPEAT_ONE, REPEAT_ALL, RANDOM, DIRECT_1, INTRO
2432      AVTransport.SetPlayMode(
2433          {OrderedArgs={"InstanceID=" .. instanceId,
2434                        "NewPlayMode=" .. newPlayMode}})
2435    </run>
2436  </action>
2437
2438  <action>
2439    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2440    <name>SetAVTransportURI</name>
2441    <run>
2442      local AVTransport = upnp.getService(UUIDs[lul_device], UPNP_AVTRANSPORT_SERVICE)
2443      if (AVTransport == nil) then
2444          return
2445      end
2446
2447      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2448      local currentURI = defaultValue(lul_settings, "CurrentURI", "")
2449      local currentURIMetaData = defaultValue(lul_settings, "CurrentURIMetaData", "")
2450
2451      AVTransport.SetAVTransportURI(
2452          {OrderedArgs={"InstanceID=" .. instanceId,
2453                        "CurrentURI=" .. currentURI,
2454                        "CurrentURIMetaData=" .. currentURIMetaData}})
2455    </run>
2456  </action>
2457
2458  <action>
2459    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2460    <name>SetNextAVTransportURI</name>
2461    <run>
2462      local AVTransport = upnp.getService(UUIDs[lul_device], UPNP_AVTRANSPORT_SERVICE)
2463      if (AVTransport == nil) then
2464          return
2465      end
2466
2467      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2468      local nextURI = defaultValue(lul_settings, "NextURI", "")
2469      local nextURIMetaData = defaultValue(lul_settings, "NextURIMetaData", "")
2470
2471      AVTransport.SetNextAVTransportURI(
2472          {OrderedArgs={"InstanceID=" .. instanceId,
2473                        "NextURI=" .. nextURI,
2474                        "NextURIMetaData=" .. nextURIMetaData}})
2475    </run>
2476  </action>
2477
2478  <action>
2479    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2480    <name>AddMultipleURIsToQueue</name>
2481    <run>
2482      local device, uuid = controlByCoordinator(lul_device)
2483      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2484      if (AVTransport == nil) then
2485          return
2486      end
2487
2488      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2489      local updateID = defaultValue(lul_settings, "UpdateID", "")
2490      local numberOfURIs = defaultValue(lul_settings, "NumberOfURIs", "")
2491      local enqueuedURIs = defaultValue(lul_settings, "EnqueuedURIs", "")
2492      local enqueuedURIsMetaData = defaultValue(lul_settings, "EnqueuedURIsMetaData", "")
2493      local containerURI = defaultValue(lul_settings, "ContainerURI", "")
2494      local containerMetaData = defaultValue(lul_settings, "ContainerMetaData", "")
2495      local desiredFirstTrackNumberEnqueued = defaultValue(lul_settings, "DesiredFirstTrackNumberEnqueued", 1)
2496      local enqueueAsNext = defaultValue(lul_settings, "EnqueueAsNext", true)
2497
2498      AVTransport.AddMultipleURIsToQueue(
2499          {OrderedArgs={"InstanceID=" .. instanceId,
2500                        "UpdateID=" .. updateID,
2501                        "NumberOfURIs=" .. numberOfURIs,
2502                        "EnqueuedURIs=" .. enqueuedURIs,
2503                        "EnqueuedURIsMetaData=" .. enqueuedURIsMetaData,
2504                        "ContainerURI=" .. containerURI,
2505                        "ContainerMetaData=" .. containerMetaData,
2506                       "DesiredFirstTrackNumberEnqueued=" .. desiredFirstTrackNumberEnqueued,
2507                        "EnqueueAsNext=" .. enqueueAsNext}})
2508    </run>
2509  </action>
2510
2511  <action>
2512    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2513    <name>AddURIToQueue</name>
2514    <run>
2515      local device, uuid = controlByCoordinator(lul_device)
2516      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2517      if (AVTransport == nil) then
2518          return
2519      end
2520
2521      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2522      local enqueuedURI = defaultValue(lul_settings, "EnqueuedURI", "")
2523      local enqueuedURIMetaData = defaultValue(lul_settings, "EnqueuedURIMetaData", "")
2524      local desiredFirstTrackNumberEnqueued = defaultValue(lul_settings, "DesiredFirstTrackNumberEnqueued", 1)
2525      local enqueueAsNext = defaultValue(lul_settings, "EnqueueAsNext", true)
2526
2527      AVTransport.AddURIToQueue(
2528          {OrderedArgs={"InstanceID=" .. instanceId,
2529                        "EnqueuedURI=" .. enqueuedURI,
2530                        "EnqueuedURIMetaData=" .. enqueuedURIMetaData,
2531                        "DesiredFirstTrackNumberEnqueued=" .. desiredFirstTrackNumberEnqueued,
2532                        "EnqueueAsNext=" .. enqueueAsNext}})
2533    </run>
2534  </action>
2535
2536  <action>
2537    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2538    <name>CreateSavedQueue</name>
2539    <run>
2540      local device, uuid = controlByCoordinator(lul_device)
2541      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2542      if (AVTransport == nil) then
2543          return
2544      end
2545
2546      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2547      local title = defaultValue(lul_settings, "Title", "")
2548      local enqueuedURI = defaultValue(lul_settings, "EnqueuedURI", "")
2549      local enqueuedURIMetaData = defaultValue(lul_settings, "EnqueuedURIMetaData", "")
2550
2551      AVTransport.CreateSavedQueue(
2552          {OrderedArgs={"InstanceID=" .. instanceId,
2553                        "Title=" .. title,
2554                        "EnqueuedURI=" .. enqueuedURI,
2555                        "EnqueuedURIMetaData=" .. enqueuedURIMetaData}})
2556    </run>
2557  </action>
2558
2559  <action>
2560    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2561    <name>AddURIToSavedQueue</name>
2562    <run>
2563      local device, uuid = controlByCoordinator(lul_device)
2564      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2565      if (AVTransport == nil) then
2566          return
2567      end
2568
2569      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2570      local objectID = defaultValue(lul_settings, "ObjectID", "")
2571      local updateID = defaultValue(lul_settings, "UpdateID", "")
2572      local enqueuedURI = defaultValue(lul_settings, "EnqueuedURI", "")
2573      local enqueuedURIMetaData = defaultValue(lul_settings, "EnqueuedURIMetaData", "")
2574      local addAtIndex = defaultValue(lul_settings, "AddAtIndex", 1)
2575
2576      AVTransport.AddURIToSavedQueue(
2577          {OrderedArgs={"InstanceID=" .. instanceId,
2578                        "ObjectID=" .. objectID,
2579                        "UpdateID=" .. updateID,
2580                        "EnqueuedURI=" .. enqueuedURI,
2581                        "EnqueuedURIMetaData=" .. enqueuedURIMetaData,
2582                        "AddAtIndex=" .. addAtIndex}})
2583    </run>
2584  </action>
2585
2586  <action>
2587    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2588    <name>ReorderTracksInQueue</name>
2589    <run>
2590      local device, uuid = controlByCoordinator(lul_device)
2591      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2592      if (AVTransport == nil) then
2593          return
2594      end
2595
2596      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2597
2598      AVTransport.ReorderTracksInQueue(
2599          {OrderedArgs={"InstanceID=" .. instanceId,
2600                        "StartingIndex=" .. lul_settings.StartingIndex,
2601                        "NumberOfTracks=" .. lul_settings.NumberOfTracks,
2602                        "InsertBefore=" .. lul_settings.InsertBefore,
2603                        "UpdateID=" .. lul_settings.UpdateID}})
2604    </run>
2605  </action>
2606
2607  <action>
2608    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2609    <name>ReorderTracksInSavedQueue</name>
2610    <run>
2611      local device, uuid = controlByCoordinator(lul_device)
2612      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2613      if (AVTransport == nil) then
2614          return
2615      end
2616
2617      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2618
2619      AVTransport.ReorderTracksInSavedQueue(
2620          {OrderedArgs={"InstanceID=" .. instanceId,
2621                        "ObjectID=" .. lul_settings.ObjectID,
2622                        "UpdateID=" .. lul_settings.UpdateID,
2623                        "TrackList=" .. lul_settings.TrackList,
2624                        "NewPositionList=" .. lul_settings.NewPositionList}})
2625    </run>
2626  </action>
2627
2628  <action>
2629    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2630    <name>RemoveTrackFromQueue</name>
2631    <run>
2632      local device, uuid = controlByCoordinator(lul_device)
2633      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2634      if (AVTransport == nil) then
2635          return
2636      end
2637
2638      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2639
2640      AVTransport.RemoveTrackFromQueue(
2641          {OrderedArgs={"InstanceID=" .. instanceId,
2642                        "ObjectID=" .. lul_settings.ObjectID,
2643                        "UpdateID=" .. lul_settings.UpdateID}})
2644    </run>
2645  </action>
2646
2647  <action>
2648    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2649    <name>RemoveTrackRangeFromQueue</name>
2650    <run>
2651      local device, uuid = controlByCoordinator(lul_device)
2652      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2653      if (AVTransport == nil) then
2654          return
2655      end
2656
2657      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2658
2659      AVTransport.RemoveTrackRangeFromQueue(
2660          {OrderedArgs={"InstanceID=" .. instanceId,
2661                        "UpdateID=" .. lul_settings.UpdateID,
2662                        "StartingIndex=" .. lul_settings.StartingIndex,
2663                        "NumberOfTracks=" .. lul_settings.NumberOfTracks}})
2664    </run>
2665  </action>
2666
2667  <action>
2668    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2669    <name>RemoveAllTracksFromQueue</name>
2670    <run>
2671      local device, uuid = controlByCoordinator(lul_device)
2672      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2673      if (AVTransport == nil) then
2674          return
2675      end
2676
2677      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2678
2679      AVTransport.RemoveAllTracksFromQueue({InstanceID=instanceId})
2680    </run>
2681  </action>
2682
2683  <action>
2684    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2685    <name>SaveQueue</name>
2686    <run>
2687      local device, uuid = controlByCoordinator(lul_device)
2688      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2689      if (AVTransport == nil) then
2690          return
2691      end
2692
2693      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2694
2695      AVTransport.SaveQueue(
2696          {OrderedArgs={"InstanceID=" .. instanceId,
2697                        "Title=" .. lul_settings.Title,
2698                        "ObjectID=" .. lul_settings.ObjectID}})
2699    </run>
2700  </action>
2701
2702  <action>
2703    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2704    <name>BackupQueue</name>
2705    <run>
2706      local device, uuid = controlByCoordinator(lul_device)
2707      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2708      if (AVTransport == nil) then
2709          return
2710      end
2711
2712      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2713
2714      AVTransport.BackupQueue({InstanceID=instanceId})
2715    </run>
2716  </action>
2717
2718  <action>
2719    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2720    <name>ChangeTransportSettings</name>
2721    <run>
2722      local device, uuid = controlByCoordinator(lul_device)
2723      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2724      if (AVTransport == nil) then
2725          return
2726      end
2727
2728      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2729
2730      AVTransport.ChangeTransportSettings(
2731          {OrderedArgs={"InstanceID=" .. instanceId,
2732                        "NewTransportSettings=" .. lul_settings.NewTransportSettings,
2733                        "CurrentAVTransportURI=" .. lul_settings.CurrentAVTransportURI}})
2734    </run>
2735  </action>
2736
2737  <action>
2738    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2739    <name>ConfigureSleepTimer</name>
2740    <run>
2741      local AVTransport = upnp.getService(UUIDs[lul_device], UPNP_AVTRANSPORT_SERVICE)
2742      if (AVTransport == nil) then
2743          return
2744      end
2745
2746      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2747
2748      AVTransport.ConfigureSleepTimer(
2749          {OrderedArgs={"InstanceID=" .. instanceId,
2750                        "NewSleepTimerDuration=" .. lul_settings.NewSleepTimerDuration}})
2751    </run>
2752  </action>
2753
2754  <action>
2755    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2756    <name>RunAlarm</name>
2757    <run>
2758      local AVTransport = upnp.getService(UUIDs[lul_device], UPNP_AVTRANSPORT_SERVICE)
2759      if (AVTransport == nil) then
2760          return
2761      end
2762
2763      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2764
2765      AVTransport.RunAlarm(
2766          {OrderedArgs={"InstanceID=" .. instanceId,
2767                        "AlarmID=" .. lul_settings.AlarmID,
2768                        "LoggedStartTime=" .. lul_settings.LoggedStartTime,
2769                        "Duration=" .. lul_settings.Duration,
2770                        "ProgramURI=" .. lul_settings.ProgramURI,
2771                        "ProgramMetaData=" .. lul_settings.ProgramMetaData,
2772                        "PlayMode=" .. lul_settings.PlayMode,
2773                        "Volume=" .. lul_settings.Volume,
2774                        "IncludeLinkedZones=" .. lul_settings.IncludeLinkedZones}})
2775    </run>
2776  </action>
2777
2778  <action>
2779    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2780    <name>StartAutoplay</name>
2781    <run>
2782      local device, uuid = controlByCoordinator(lul_device)
2783      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2784      if (AVTransport == nil) then
2785          return
2786      end
2787
2788      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2789
2790      AVTransport.StartAutoplay(
2791          {OrderedArgs={"InstanceID=" .. instanceId,
2792                        "ProgramURI=" .. lul_settings.ProgramURI,
2793                        "ProgramMetaData=" .. lul_settings.ProgramMetaData,
2794                        "Volume=" .. lul_settings.Volume,
2795                        "IncludeLinkedZones=" .. lul_settings.IncludeLinkedZones,
2796                        "ResetVolumeAfter=" .. lul_settings.ResetVolumeAfter}})
2797    </run>
2798  </action>
2799
2800  <action>
2801    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2802    <name>SnoozeAlarm</name>
2803    <run>
2804      local AVTransport = upnp.getService(UUIDs[lul_device], UPNP_AVTRANSPORT_SERVICE)
2805      if (AVTransport == nil) then
2806          return
2807      end
2808
2809      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2810
2811      AVTransport.SnoozeAlarm(
2812          {OrderedArgs={"InstanceID=" .. instanceId,
2813                        "Duration=" .. lul_settings.Duration}})
2814    </run>
2815  </action>
2816
2817  <action>
2818    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2819    <name>SetCrossfadeMode</name>
2820    <run>
2821      local device, uuid = controlByCoordinator(lul_device)
2822      local AVTransport = upnp.getService(uuid, UPNP_AVTRANSPORT_SERVICE)
2823      if (AVTransport == nil) then
2824          return
2825      end
2826
2827      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2828      local desiredMode = tonumber(defaultValue(lul_settings, "CrossfadeMode", nil))
2829
2830      -- If parameter is nill, we consider the callback as a toggle
2831      if (desiredMode == nil and device ~= 0) then
2832          local currentMode = luup.variable_get(UPNP_AVTRANSPORT_SID, "CurrentCrossfadeMode", device)
2833          desiredMode = 1 - (tonumber(currentMode) or 0)
2834      end
2835
2836      AVTransport.SetCrossfadeMode(
2837          {OrderedArgs={"InstanceID=" .. instanceId,
2838                        "CrossfadeMode=" .. desiredMode}})
2839
2840      if (device ~= 0) then
2841          refreshNow(device, false, false)
2842      end
2843    </run>
2844  </action>
2845
2846  <action>
2847    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2848    <name>NotifyDeletedURI</name>
2849    <run>
2850      local AVTransport = upnp.getService(UUIDs[lul_device], UPNP_AVTRANSPORT_SERVICE)
2851      if (AVTransport == nil) then
2852          return
2853      end
2854
2855      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2856
2857      AVTransport.NotifyDeletedURI(
2858          {OrderedArgs={"InstanceID=" .. instanceId,
2859                        "DeletedURI=" .. lul_settings.DeletedURI}})
2860    </run>
2861  </action>
2862
2863  <action>
2864    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2865    <name>BecomeCoordinatorOfStandaloneGroup</name>
2866    <run>
2867      local AVTransport = upnp.getService(UUIDs[lul_device], UPNP_AVTRANSPORT_SERVICE)
2868      if (AVTransport == nil) then
2869          return
2870      end
2871
2872      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2873
2874      AVTransport.BecomeCoordinatorOfStandaloneGroup({InstanceID=instanceId})
2875    </run>
2876  </action>
2877
2878  <action>
2879    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2880    <name>BecomeGroupCoordinator</name>
2881    <run>
2882      local AVTransport = upnp.getService(UUIDs[lul_device], UPNP_AVTRANSPORT_SERVICE)
2883      if (AVTransport == nil) then
2884          return
2885      end
2886
2887      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2888
2889      AVTransport.BecomeGroupCoordinator(
2890          {OrderedArgs={"InstanceID=" .. instanceId,
2891                        "CurrentCoordinator=" .. lul_settings.CurrentCoordinator,
2892                        "CurrentGroupID=" .. lul_settings.CurrentGroupID,
2893                        "OtherMembers=" .. lul_settings.OtherMembers,
2894                        "TransportSettings=" .. lul_settings.TransportSettings,
2895                        "CurrentURI=" .. lul_settings.CurrentURI,
2896                        "CurrentURIMetaData=" .. lul_settings.CurrentURIMetaData,
2897                        "SleepTimerState=" .. lul_settings.SleepTimerState,
2898                        "AlarmState=" .. lul_settings.AlarmState,
2899                        "StreamRestartState=" .. lul_settings.StreamRestartState,
2900                        "CurrentQueueTrackList=" .. lul_settings.CurrentQueueTrackList}})
2901    </run>
2902  </action>
2903
2904  <action>
2905    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2906    <name>BecomeGroupCoordinatorAndSource</name>
2907    <run>
2908      local AVTransport = upnp.getService(UUIDs[lul_device], UPNP_AVTRANSPORT_SERVICE)
2909      if (AVTransport == nil) then
2910          return
2911      end
2912
2913      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2914
2915      AVTransport.BecomeGroupCoordinatorAndSource(
2916          {OrderedArgs={"InstanceID=" .. instanceId,
2917                        "CurrentCoordinator=" .. lul_settings.CurrentCoordinator,
2918                        "CurrentGroupID=" .. lul_settings.CurrentGroupID,
2919                        "OtherMembers=" .. lul_settings.OtherMembers,
2920                        "CurrentURI=" .. lul_settings.CurrentURI,
2921                        "CurrentURIMetaData=" .. lul_settings.CurrentURIMetaData,
2922                        "SleepTimerState=" .. lul_settings.SleepTimerState,
2923                        "AlarmState=" .. lul_settings.AlarmState,
2924                        "StreamRestartState=" .. lul_settings.StreamRestartState,
2925                        "CurrentAVTTrackList=" .. lul_settings.CurrentAVTrackList,
2926                        "CurrentQueueTrackList=" .. lul_settings.CurrentAVTTrackList,
2927                        "CurrentSourceState=" .. lul_settings.CurrentSourceState,
2928                        "ResumePlayback=" .. lul_settings.ResumePlayback}})
2929    </run>
2930  </action>
2931
2932  <action>
2933    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2934    <name>ChangeCoordinator</name>
2935    <run>
2936      local AVTransport = upnp.getService(UUIDs[lul_device], UPNP_AVTRANSPORT_SERVICE)
2937      if (AVTransport == nil) then
2938          return
2939      end
2940
2941      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2942
2943      AVTransport.ChangeCoordinator(
2944          {OrderedArgs={"InstanceID=" .. instanceId,
2945                        "CurrentCoordinator=" .. lul_settings.CurrentCoordinator,
2946                        "NewCoordinator=" .. lul_settings.NewCoordinator,
2947                        "NewTransportSettings=" .. lul_settings.NewTransportSettings}})
2948    </run>
2949  </action>
2950
2951  <action>
2952    <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
2953    <name>DelegateGroupCoordinationTo</name>
2954    <run>
2955      local AVTransport = upnp.getService(UUIDs[lul_device], UPNP_AVTRANSPORT_SERVICE)
2956      if (AVTransport == nil) then
2957          return
2958      end
2959
2960      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2961
2962      AVTransport.DelegateGroupCoordinationTo(
2963          {OrderedArgs={"InstanceID=" .. instanceId,
2964                        "NewCoordinator=" .. lul_settings.NewCoordinator,
2965                        "RejoinGroup=" .. lul_settings.RejoinGroup}})
2966    </run>
2967  </action>
2968
2969  <action>
2970    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
2971    <name>SetURIToPlay</name>
2972    <run>
2973      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2974      local uri = defaultValue(lul_settings, "URIToPlay", "")
2975
2976      playURI(lul_device, instanceId, uri, nil, nil, nil, false, nil, false, true)
2977
2978      refreshNow(lul_device, false, true)
2979
2980      -- URI must include protocol as prefix.
2981      -- x-file-cifs:
2982      -- file:
2983      -- x-rincon:
2984      -- x-rincon-mp3radio:
2985      -- x-rincon-playlist:
2986      -- x-rincon-queue:
2987      -- x-rincon-stream:
2988      -- example is DR Jazz Radio: x-rincon-mp3radio://live-icy.gss.dr.dk:8000/Channel22_HQ.mp3
2989    </run>
2990  </action>
2991
2992  <action>
2993    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
2994    <name>PlayURI</name>
2995    <run>
2996      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
2997      local uri = defaultValue(lul_settings, "URIToPlay", "")
2998      local volume = defaultValue(lul_settings, "Volume", nil)
2999      local speed = defaultValue(lul_settings, "Speed", "1")
3000
3001      playURI(lul_device, instanceId, uri, speed, volume, nil, false, nil, false, true)
3002
3003      refreshNow(lul_device, false, true)
3004
3005      -- URI must include protocol as prefix.
3006      -- x-file-cifs:
3007      -- file:
3008      -- x-rincon:
3009      -- x-rincon-mp3radio:
3010      -- x-rincon-playlist:
3011      -- x-rincon-queue:
3012      -- x-rincon-stream:
3013      -- example is DR Jazz Radio: x-rincon-mp3radio://live-icy.gss.dr.dk:8000/Channel22_HQ.mp3
3014    </run>
3015  </action>
3016
3017  <action>
3018    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3019    <name>EnqueueURI</name>
3020    <run>
3021      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3022      local uri = defaultValue(lul_settings, "URIToEnqueue", "")
3023      local enqueueMode = defaultValue(lul_settings, "EnqueueMode", "ENQUEUE_AND_PLAY")
3024
3025      playURI(lul_device, instanceId, uri, "1", nil, nil, false, enqueueMode, false, true)
3026
3027      refreshNow(lul_device, false, true)
3028    </run>
3029  </action>
3030
3031  <action>
3032    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3033    <name>Alert</name>
3034    <run>
3035      local duration = tonumber(defaultValue(lul_settings, "Duration", "0")) or 0
3036      if (duration > 0) then
3037          tts.queueAlert(lul_device, lul_settings)
3038      else
3039          sayOrAlert(lul_device, lul_settings, false)
3040      end
3041    </run>
3042  </action>
3043
3044  <action>
3045    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3046    <name>PauseAll</name>
3047    <run>
3048      pauseAll(lul_device)
3049    </run>
3050  </action>
3051
3052  <action>
3053    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3054    <name>JoinGroup</name>
3055    <run>
3056      local zone = defaultValue(lul_settings, "Zone", "")
3057      joinGroup(lul_device, zone)
3058    </run>
3059  </action>
3060
3061  <action>
3062    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3063    <name>LeaveGroup</name>
3064    <run>
3065      leaveGroup(lul_device)
3066    </run>
3067  </action>
3068
3069  <action>
3070    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3071    <name>UpdateGroupMembers</name>
3072    <run>
3073      local zones = defaultValue(lul_settings, "Zones", "")
3074
3075      updateGroupMembers(lul_device, zones)
3076    </run>
3077  </action>
3078
3079  <action>
3080    <serviceId>urn:micasaverde-com:serviceId:Volume1</serviceId>
3081    <name>Mute</name>
3082    <run>
3083      -- Toggle Mute
3084      local Rendering = upnp.getService(UUIDs[lul_device], UPNP_RENDERING_CONTROL_SERVICE)
3085      if (Rendering == nil) then
3086          return
3087      end
3088
3089      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3090      local channel = defaultValue(lul_settings, "Channel", "Master")
3091      local isMuted = luup.variable_get(UPNP_RENDERING_CONTROL_SID, "Mute", lul_device)
3092      local desiredMute = 1 - (tonumber(isMuted) or 0)
3093
3094      Rendering.SetMute(
3095         {OrderedArgs={"InstanceID=" .. instanceId,
3096                       "Channel=" .. channel,
3097                       "DesiredMute=" .. desiredMute}})
3098
3099      refreshMuteNow(lul_device)
3100    </run>
3101  </action>
3102
3103  <action>
3104    <serviceId>urn:micasaverde-com:serviceId:Volume1</serviceId>
3105    <name>Up</name>
3106    <run>
3107      -- Volume up
3108      local Rendering = upnp.getService(UUIDs[lul_device], UPNP_RENDERING_CONTROL_SERVICE)
3109      if (Rendering == nil) then
3110          return
3111      end
3112
3113      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3114      local channel = defaultValue(lul_settings, "Channel", "Master")
3115
3116      Rendering.SetRelativeVolume(
3117         {OrderedArgs={"InstanceID=" .. instanceId,
3118                       "Channel=" .. channel,
3119                       "Adjustment=3"}})
3120
3121      refreshVolumeNow(lul_device)
3122    </run>
3123  </action>
3124
3125  <action>
3126    <serviceId>urn:micasaverde-com:serviceId:Volume1</serviceId>
3127    <name>Down</name>
3128    <run>
3129      -- Volume down
3130      local Rendering = upnp.getService(UUIDs[lul_device], UPNP_RENDERING_CONTROL_SERVICE)
3131      if (Rendering == nil) then
3132          return
3133      end
3134
3135      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3136      local channel = defaultValue(lul_settings, "Channel", "Master")
3137
3138      Rendering.SetRelativeVolume(
3139         {OrderedArgs={"InstanceID=" .. instanceId,
3140                       "Channel=" .. channel,
3141                       "Adjustment=-3"}})
3142
3143      refreshVolumeNow(lul_device)
3144    </run>
3145  </action>
3146
3147  <action>
3148    <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
3149    <name>SetMute</name>
3150    <run>
3151      local Rendering = upnp.getService(UUIDs[lul_device], UPNP_RENDERING_CONTROL_SERVICE)
3152      if (Rendering == nil) then
3153          return
3154      end
3155
3156      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3157      local desiredMute = defaultValue(lul_settings, "DesiredMute", nil)
3158      local channel = defaultValue(lul_settings, "Channel", "Master")
3159
3160      -- If parameter is nill, we consider the callback as a toggle
3161      if (desiredMute == nil) then
3162          local isMuted = luup.variable_get(UPNP_RENDERING_CONTROL_SID, "Mute", lul_device)
3163          desiredMute = 1 - (tonumber(isMuted) or 0)
3164      end
3165
3166      Rendering.SetMute(
3167         {OrderedArgs={"InstanceID=" .. instanceId,
3168                       "Channel=" .. channel,
3169                       "DesiredMute=" .. desiredMute}})
3170
3171      refreshMuteNow(lul_device)
3172    </run>
3173  </action>
3174
3175  <action>
3176    <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
3177    <name>ResetBasicEQ</name>
3178    <run>
3179      local Rendering = upnp.getService(UUIDs[lul_device], UPNP_RENDERING_CONTROL_SERVICE)
3180      if (Rendering == nil) then
3181          return
3182      end
3183
3184      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3185
3186      Rendering.ResetBasicEQ({InstanceID=instanceId})
3187    </run>
3188  </action>
3189
3190  <action>
3191    <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
3192    <name>ResetExtEQ</name>
3193    <run>
3194      local Rendering = upnp.getService(UUIDs[lul_device], UPNP_RENDERING_CONTROL_SERVICE)
3195      if (Rendering == nil) then
3196          return
3197      end
3198
3199      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3200
3201      Rendering.ResetExtEQ(
3202         {OrderedArgs={"InstanceID=" .. instanceId,
3203                       "EQType=" .. lul_settings.EQType}})
3204    </run>
3205  </action>
3206
3207  <action>
3208    <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
3209    <name>SetVolume</name>
3210    <run>
3211      local Rendering = upnp.getService(UUIDs[lul_device], UPNP_RENDERING_CONTROL_SERVICE)
3212      if (Rendering == nil) then
3213          return
3214      end
3215
3216      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3217      local desiredVolume = tonumber(defaultValue(lul_settings, "DesiredVolume", "5"))
3218      local channel = defaultValue(lul_settings, "Channel", "Master")
3219
3220      Rendering.SetVolume(
3221         {OrderedArgs={"InstanceID=" .. instanceId,
3222                       "Channel=" .. channel,
3223                       "DesiredVolume=" .. desiredVolume}})
3224
3225      refreshVolumeNow(lul_device)
3226    </run>
3227  </action>
3228
3229  <action>
3230    <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
3231    <name>SetRelativeVolume</name>
3232    <run>
3233      local Rendering = upnp.getService(UUIDs[lul_device], UPNP_RENDERING_CONTROL_SERVICE)
3234      if (Rendering == nil) then
3235          return
3236      end
3237
3238      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3239      local channel = defaultValue(lul_settings, "Channel", "Master")
3240
3241      Rendering.SetRelativeVolume(
3242         {OrderedArgs={"InstanceID=" .. instanceId,
3243                       "Channel=" .. channel,
3244                       "Adjustment=" .. lul_settings.Adjustment}})
3245
3246      refreshVolumeNow(lul_device)
3247    </run>
3248  </action>
3249
3250  <action>
3251    <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
3252    <name>SetVolumeDB</name>
3253    <run>
3254      local Rendering = upnp.getService(UUIDs[lul_device], UPNP_RENDERING_CONTROL_SERVICE)
3255      if (Rendering == nil) then
3256          return
3257      end
3258
3259      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3260      local channel = defaultValue(lul_settings, "Channel", "Master")
3261      local desiredVolume = defaultValue(lul_settings, "DesiredVolume", "0")
3262
3263      Rendering.SetVolumeDB(
3264         {OrderedArgs={"InstanceID=" .. instanceId,
3265                       "Channel=" .. channel,
3266                       "DesiredVolume=" .. desiredVolume}})
3267    </run>
3268  </action>
3269
3270  <action>
3271    <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
3272    <name>SetBass</name>
3273    <run>
3274      local Rendering = upnp.getService(UUIDs[lul_device], UPNP_RENDERING_CONTROL_SERVICE)
3275      if (Rendering == nil) then
3276          return
3277      end
3278
3279      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3280
3281      Rendering.SetBass(
3282         {OrderedArgs={"InstanceID=" .. instanceId,
3283                       "DesiredBass=" .. lul_settings.DesiredBass}})
3284    </run>
3285  </action>
3286
3287  <action>
3288    <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
3289    <name>SetTreble</name>
3290    <run>
3291      local Rendering = upnp.getService(UUIDs[lul_device], UPNP_RENDERING_CONTROL_SERVICE)
3292      if (Rendering == nil) then
3293          return
3294      end
3295
3296      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3297
3298      Rendering.SetTreble(
3299         {OrderedArgs={"InstanceID=" .. instanceId,
3300                       "DesiredTreble=" .. lul_settings.DesiredTreble}})
3301    </run>
3302  </action>
3303
3304  <action>
3305    <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
3306    <name>SetEQ</name>
3307    <run>
3308      local Rendering = upnp.getService(UUIDs[lul_device], UPNP_RENDERING_CONTROL_SERVICE)
3309      if (Rendering == nil) then
3310          return
3311      end
3312
3313      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3314
3315      Rendering.SetEQ(
3316         {OrderedArgs={"InstanceID=" .. instanceId,
3317                       "EQType=" .. lul_settings.EQType,
3318                       "DesiredValue=" .. lul_settings.DesiredValue}})
3319    </run>
3320  </action>
3321
3322  <action>
3323    <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
3324    <name>SetLoudness</name>
3325    <run>
3326      local Rendering = upnp.getService(UUIDs[lul_device], UPNP_RENDERING_CONTROL_SERVICE)
3327      if (Rendering == nil) then
3328          return
3329      end
3330
3331      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3332
3333      Rendering.SetLoudness(
3334         {OrderedArgs={"InstanceID=" .. instanceId,
3335                       "Channel=" .. lul_settings.Channel,
3336                       "DesiredLoudness=" .. lul_settings.DesiredLoudness}})
3337    </run>
3338  </action>
3339
3340  <action>
3341    <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
3342    <name>SetOutputFixed</name>
3343    <run>
3344      local Rendering = upnp.getService(UUIDs[lul_device], UPNP_RENDERING_CONTROL_SERVICE)
3345      if (Rendering == nil) then
3346          return
3347      end
3348
3349      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3350
3351      Rendering.SetOutputFixed(
3352         {OrderedArgs={"InstanceID=" .. instanceId,
3353                       "DesiredFixed=" .. lul_settings.DesiredFixed}})
3354    </run>
3355  </action>
3356
3357  <action>
3358    <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
3359    <name>RampToVolume</name>
3360    <run>
3361      local Rendering = upnp.getService(UUIDs[lul_device], UPNP_RENDERING_CONTROL_SERVICE)
3362      if (Rendering == nil) then
3363          return
3364      end
3365
3366      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3367
3368      Rendering.RampToVolume(
3369         {OrderedArgs={"InstanceID=" .. instanceId,
3370                       "Channel=" .. lul_settings.Channel,
3371                       "RampType=" .. lul_settings.RampType,
3372                       "DesiredVolume=" .. lul_settings.DesiredVolume,
3373                       "ResetVolumeAfter=" .. lul_settings.ResetVolumeAfter,
3374                       "ProgramURI=" .. lul_settings.ProgramURI}})
3375    </run>
3376  </action>
3377
3378  <action>
3379    <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
3380    <name>RestoreVolumePriorToRamp</name>
3381    <run>
3382      local Rendering = upnp.getService(UUIDs[lul_device], UPNP_RENDERING_CONTROL_SERVICE)
3383      if (Rendering == nil) then
3384          return
3385      end
3386
3387      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3388
3389      Rendering.RestoreVolumePriorToRamp(
3390         {OrderedArgs={"InstanceID=" .. instanceId,
3391                       "Channel=" .. lul_settings.Channel}})
3392    </run>
3393  </action>
3394
3395  <action>
3396    <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
3397    <name>SetChannelMap</name>
3398    <run>
3399      local Rendering = upnp.getService(UUIDs[lul_device], UPNP_RENDERING_CONTROL_SERVICE)
3400      if (Rendering == nil) then
3401          return
3402      end
3403
3404      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3405
3406      Rendering.SetChannelMap(
3407         {OrderedArgs={"InstanceID=" .. instanceId,
3408                       "ChannelMap=" .. lul_settings.ChannelMap}})
3409    </run>
3410  </action>
3411
3412  <action>
3413    <serviceId>urn:upnp-org:serviceId:GroupRenderingControl</serviceId>
3414    <name>SetGroupMute</name>
3415    <run>
3416      local device, uuid = controlByCoordinator(lul_device)
3417      local GroupRendering = upnp.getService(uuid, UPNP_GROUP_RENDERING_CONTROL_SERVICE)
3418      if (GroupRendering == nil) then
3419          return
3420      end
3421
3422      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3423      local desiredMute = defaultValue(lul_settings, "DesiredMute", "0")
3424
3425      GroupRendering.SetGroupMute(
3426         {OrderedArgs={"InstanceID=" .. instanceId,
3427                       "DesiredMute=" .. desiredMute}})
3428    </run>
3429  </action>
3430
3431  <action>
3432    <serviceId>urn:upnp-org:serviceId:GroupRenderingControl</serviceId>
3433    <name>SetGroupVolume</name>
3434    <run>
3435      local device, uuid = controlByCoordinator(lul_device)
3436      local GroupRendering = upnp.getService(uuid, UPNP_GROUP_RENDERING_CONTROL_SERVICE)
3437      if (GroupRendering == nil) then
3438          return
3439      end
3440
3441      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3442      local desiredVolume = tonumber(defaultValue(lul_settings, "DesiredVolume", "5"))
3443
3444      GroupRendering.SetGroupVolume(
3445         {OrderedArgs={"InstanceID=" .. instanceId,
3446                       "DesiredVolume=" .. desiredVolume}})
3447    </run>
3448  </action>
3449
3450  <action>
3451    <serviceId>urn:upnp-org:serviceId:GroupRenderingControl</serviceId>
3452    <name>SetRelativeGroupVolume</name>
3453    <run>
3454      local device, uuid = controlByCoordinator(lul_device)
3455      local GroupRendering = upnp.getService(uuid, UPNP_GROUP_RENDERING_CONTROL_SERVICE)
3456      if (GroupRendering == nil) then
3457          return
3458      end
3459
3460      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3461
3462      GroupRendering.SetRelativeGroupVolume(
3463         {OrderedArgs={"InstanceID=" .. instanceId,
3464                       "Adjustment=" .. lul_settings.Adjustment}})
3465    </run>
3466  </action>
3467
3468  <action>
3469    <serviceId>urn:upnp-org:serviceId:GroupRenderingControl</serviceId>
3470    <name>SnapshotGroupVolume</name>
3471    <run>
3472      local device, uuid = controlByCoordinator(lul_device)
3473      local GroupRendering = upnp.getService(uuid, UPNP_GROUP_RENDERING_CONTROL_SERVICE)
3474      if (GroupRendering == nil) then
3475          return
3476      end
3477
3478      local instanceId = defaultValue(lul_settings, "InstanceID", "0")
3479
3480      GroupRendering.SnapshotGroupVolume(
3481         {InstanceID=instanceId})
3482    </run>
3483  </action>
3484
3485  <action>
3486    <serviceId>urn:micasaverde-com:serviceId:HaDevice1</serviceId>
3487    <name>Poll</name>
3488    <job>
3489      refreshNow(lul_device, true, true)
3490      return 4,0
3491    </job>
3492  </action>
3493
3494  <action>
3495    <serviceId>urn:micasaverde-com:serviceId:HaDevice1</serviceId>
3496    <name>ToggleState</name>
3497    <run>
3498      -- TODO: This needs to be implemented to make the Icon cause a toggle action
3499    </run>
3500  </action>
3501
3502  <action>
3503    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3504    <name>SavePlaybackContext</name>
3505    <job>
3506      local devices = defaultValue(lul_settings, "GroupDevices", "")
3507      local zones = defaultValue(lul_settings, "GroupZones", "")
3508
3509      local uuid
3510      local uuidListe = UUIDs[lul_device]
3511      for id in devices:gmatch("%d+") do
3512          id = tonumber(id)
3513          if (id ~= lul_device) then
3514              uuid = UUIDs[id]
3515              if (uuid == nil) then
3516                  uuid = luup.variable_get(UPNP_DEVICE_PROPERTIES_SID, "SonosID", id)
3517              end
3518              if (uuid ~= nil and uuid ~= "" and uuidListe:find(uuid) == nil) then
3519                  uuidListe = uuidListe .. ","
3520                  uuidListe = uuidListe .. uuid
3521              end
3522          end
3523      end
3524      if (zones:upper() == "ALL") then
3525          local uuids = getAllUUIDs()
3526          for uuid in uuids:gmatch("RINCON_%x+") do
3527              if (uuid ~= UUIDs[lul_device] and uuidListe:find(uuid) == nil) then
3528                  uuidListe = uuidListe .. ","
3529                  uuidListe = uuidListe .. uuid
3530              end
3531          end
3532      else
3533          for zone in zones:gmatch("[^,]+") do
3534              uuid = getUUIDFromZoneName(zone)
3535              if (uuid ~= nil and uuid ~= "" and uuid ~= UUIDs[lul_device] and uuidListe:find(uuid) == nil) then
3536                  uuidListe = uuidListe .. ","
3537                  uuidListe = uuidListe .. uuid
3538               end
3539          end
3540      end
3541
3542      playbackCxt[lul_device] = savePlaybackContexts(lul_device, uuidListe)
3543      playbackCxt[lul_device].grpMembers = ""
3544      return 4,0
3545    </job>
3546  </action>
3547
3548  <action>
3549    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3550    <name>RestorePlaybackContext</name>
3551    <job>
3552      restorePlaybackContexts(lul_device, playbackCxt[lul_device])
3553      return 4,0
3554    </job>
3555  </action>
3556
3557  <action>
3558    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3559    <name>StartSonosDiscovery</name>
3560    <job>
3561      setVariableValue(SONOS_SID, "DiscoveryResult", "scanning", lul_device)
3562      local xml = upnp.scanUPnPDevices("urn:schemas-upnp-org:device:ZonePlayer:1", { "modelName", "friendlyName", "roomName" })
3563      setVariableValue(SONOS_SID, "DiscoveryResult", xml, lul_device)
3564      return 4,0
3565    </job>
3566  </action>
3567
3568  <action>
3569    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3570    <name>SelectSonosDevice</name>
3571    <run>
3572      local newDescrURL = lul_settings.URL or ""
3573      local newIP, newPort = newDescrURL:match("http://([%d%.]-):(%d+)/.-")
3574      if (newIP ~= nil and newPort ~= nil) then
3575          luup.attr_set("ip", newIP, lul_device)
3576          luup.attr_set("mac", "", lul_device)
3577          setup(lul_device, false)
3578      end
3579    </run>
3580  </action>
3581
3582  <action>
3583    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3584    <name>SearchAndSelectSonosDevice</name>
3585    <job>
3586      if (lul_settings.Name == nil or lul_settings.Name == "") then
3587          return
3588      end
3589
3590      local descrURL = upnp.searchUPnPDevices("urn:schemas-upnp-org:device:ZonePlayer:1",
3591                                              lul_settings.Name,
3592                                              lul_settings.IP)
3593      if (descrURL ~= nil) then
3594          local newIP, newPort = descrURL:match("http://([%d%.]-):(%d+)/.-")
3595          if (newIP ~= nil and newPort ~= nil) then
3596              luup.attr_set("ip", newIP, lul_device)
3597              luup.attr_set("mac", "", lul_device)
3598              setup(lul_device, false)
3599          end
3600      end
3601
3602      return 4,0
3603    </job>
3604  </action>
3605
3606  <action>
3607    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3608    <name>SetCheckStateRate</name>
3609    <run>
3610      setCheckStateRate(lul_device, lul_settings.rate)
3611    </run>
3612  </action>
3613
3614  <action>
3615    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3616    <name>SetDebugLogs</name>
3617    <run>
3618      setDebugLogs(lul_device, lul_settings.enable)
3619    </run>
3620  </action>
3621
3622  <action>
3623    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3624    <name>SetReadQueueContent</name>
3625    <run>
3626      setReadQueueContent(lul_device, lul_settings.enable)
3627    </run>
3628  </action>
3629
3630  <action>
3631    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3632    <name>InstallDiscoveryPatch</name>
3633    <run>
3634      local reload = false
3635      if (upnp.installDiscoveryPatch(VERA_LOCAL_IP)) then
3636          reload = true
3637          log("Discovery patch now installed")
3638      else
3639          log("Discovery patch installation failed")
3640      end
3641      if (upnp.isDiscoveryPatchInstalled(VERA_LOCAL_IP)) then
3642          luup.variable_set(SONOS_SID, "DiscoveryPatchInstalled", "1", lul_device)
3643      else
3644          luup.variable_set(SONOS_SID, "DiscoveryPatchInstalled", "0", lul_device)
3645      end
3646      if (reload) then
3647          luup.call_delay("SonosReload", 2, "")
3648      end
3649    </run>
3650  </action>
3651
3652  <action>
3653    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3654    <name>UninstallDiscoveryPatch</name>
3655    <run>
3656      local reload = false
3657      if (upnp.uninstallDiscoveryPatch(VERA_LOCAL_IP)) then
3658          reload = true
3659          log("Discovery patch now uninstalled")
3660      else
3661          log("Discovery patch uninstallation failed")
3662      end
3663      if (upnp.isDiscoveryPatchInstalled(VERA_LOCAL_IP)) then
3664          luup.variable_set(SONOS_SID, "DiscoveryPatchInstalled", "1", lul_device)
3665      else
3666          luup.variable_set(SONOS_SID, "DiscoveryPatchInstalled", "0", lul_device)
3667      end
3668      if (reload) then
3669          luup.call_delay("SonosReload", 2, "")
3670      end
3671    </run>
3672  </action>
3673
3674  <action>
3675    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3676    <name>NotifyRenderingChange</name>
3677    <job>
3678      if (upnp.isValidNotification("NotifyRenderingChange", lul_settings.sid, EventSubscriptions)) then
3679          handleRenderingChange(lul_device, lul_settings.LastChange or "")
3680      end
3681      return 4,0
3682    </job>
3683  </action>
3684
3685  <action>
3686    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3687    <name>NotifyAVTransportChange</name>
3688    <job>
3689      if (upnp.isValidNotification("NotifyAVTransportChange", lul_settings.sid, EventSubscriptions)) then
3690          handleAVTransportChange(lul_device, UUIDs[lul_device], lul_settings.LastChange or "")
3691      end
3692      return 4,0
3693    </job>
3694  </action>
3695
3696  <action>
3697    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3698    <name>NotifyMusicServicesChange</name>
3699    <job>
3700      if (upnp.isValidNotification("NotifyMusicServicesChange", lul_settings.sid, EventSubscriptions)) then
3701          -- log("NotifyMusicServicesChange for device " .. lul_device .. " SID " .. lul_settings.sid .. " with value " .. (lul_settings.LastChange or "nil"))
3702      end
3703      return 4,0
3704    </job>
3705  </action>
3706
3707  <action>
3708    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3709    <name>NotifyZoneGroupTopologyChange</name>
3710    <job>
3711      if (upnp.isValidNotification("NotifyZoneGroupTopologyChange", lul_settings.sid, EventSubscriptions)) then
3712          groupsState = lul_settings.ZoneGroupState or ""
3713
3714          local changed = setData("ZoneGroupState", groupsState, lul_device, false)
3715
3716          local members, coordinator = getGroupInfos(UUIDs[lul_device])
3717          changed = setData("ZonePlayerUUIDsInGroup", members, lul_device, changed)
3718          changed = setData("GroupCoordinator", coordinator, lul_device, changed)
3719
3720          if (changed) then
3721              setVariableValue(HADEVICE_SID, "LastUpdate", os.time(), lul_device)
3722          end
3723      end
3724      return 4,0
3725    </job>
3726  </action>
3727
3728  <action>
3729    <serviceId>urn:micasaverde-com:serviceId:Sonos1</serviceId>
3730    <name>NotifyContentDirectoryChange</name>
3731    <job>
3732      if (upnp.isValidNotification("NotifyContentDirectoryChange", lul_settings.sid, EventSubscriptions)) then
3733          handleContentDirectoryChange(lul_device, UUIDs[lul_device], lul_settings.ContainerUpdateIDs or "")
3734      end
3735      return 4,0
3736    </job>
3737  </action>
3738</actionList>
3739</implementation>
Note: See TracBrowser for help on using the repository browser.