App idea - Music Manager

It would be great to have an app that saved tracks, albums, stations etc. for quick access. Maybe it shows the currently playing URI and allows you to create a switch associated with it. Just thinking out loud here.

2 Likes

VLC Player can do that. Maybe not exactly in the way you're thinking, but you do have control over it via Hubitat with the VLC Thing driver.

You can do this with RM (at least if you use Sonos).

In the log screen when a track, or station, plays there is a displayed URI. You can then copy that URI

There is a method to create a custom command in RM that allows you to paste the URI as a custom command.

From there you could create a series of virtual switches along the lines of this.

If Switch 1 turns on

THEN

Play URI for Pandora Rock Station (or Spotify Playlist).

I do something like this now for a rule in a room that turns on the speaker to Classical music on motion.

I had an app that did something very similar on Smartthings that I have tried on a few occasions to port over. There were quite a few roadblocks many of which have been fixed by the hubitat team over the update cycle. I haven't had time to revisit recently but if you are familiar with groovy code you might get some inspiration from my unfinished code.

When functional, it searches the event list of your chosen speaker and extract the uri's in a list you can then use to assign to a virtual buttons. HE changed the information stored in the event list making the uri extraction hit or miss depending on the type of station that was recently played.

Don't get caught up with the virtual container portion as it might get confusing. This is simply a way to have all the "virtual stations" organized under one parent device. I hate having virtual devices scattered everywhere so I built a container to keep them grouped...forgive my OCD.

Hopefully this is helpful to you.

Here's a more up to date version that account for some of the HE updates....again...very unfinished:

/**
 *  Sonos Playlist Controller 
 *
 *  Copyright 2017 Stephan Hackett
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 *	 
 * Speacial Thanks to @GuyInATie for allowing me to use snippets from his Sonos Remote Control smartApp. They were used
 * as the foundation of the code for retrieving and storing the recently played station from the Sonos speakers.
 *
 *	Icons by http://www.icons8.com	
 *
 */

def version() {return "0.1.20180530"}

definition(
    name: "Sonos PlayList Control",
    namespace: "stephack",
    author: "Stephan Hackett",
    description: "Autoplay Stations/Playlists on Sonos speakers",
    category: "My Apps",
    //parent: "stephack:Sonos PlayList Control",
    iconUrl: "https://cdn.rawgit.com/stephack/Virtual/master/resources/images/spca.png",
    iconX2Url: "https://cdn.rawgit.com/stephack/Virtual/master/resources/images/spca.png",
    iconX3Url: "https://cdn.rawgit.com/stephack/SPC/Virtual/resources/images/spca.png"
)

preferences {
	//page(name: "startPage")
	//page(name: "parentPage")
	page(name: "mainPage", nextPage: confirmOptions)
	//page(name: "choosePlaylists", title: "Select stations for your Virtual Playlists", nextPage: confirmOptions)
	page(name: "confirmOptions", title: "Confirm All Settings Below")
    page(name: "aboutPage")
}

def startPage() {
    if (parent) {
        mainPage()
    } else {
        parentPage()
    }
}

def parentPage() {
	return dynamicPage(name: "parentPage", title: "", nextPage: "", install: true, uninstall: true) {
        section("Installed PlayList Controls") {
            app(name: "childApps", appName: appName(), namespace: "stephack", title: "Create New Playlist", multiple: true)
        }
        section("Version Info & User's Guide") {
       		href (name: "aboutPage", 
       		title: "Sonos PlayList Control\nver "+version(), 
       		description: "Tap for User's Guide and Info.",
       		image: "https://cdn.rawgit.com/stephack/Virtual/master/resources/images/spca.png",
       		required: false,
       		page: "aboutPage"
 	   		)
      	}
    }
}

private def appName() { return "${parent ? "VC Config" : "Sonos PlayList Control"}" }

def mainPage(){
	dynamicPage(name:"mainPage",uninstall:true){
    	section("Speaker to control with Virtual Playlists") {
        	input "sonos", "capability.musicPlayer", title: "Choose Speaker", submitOnChange: true, multiple:false, required: true, image: "https://cdn.rawgit.com/stephack/Virtual/master/resources/images/speakera.png"            
        }
        
        if(sonos){
        	section{
            	input "vbTotal", "number", title: "# of Presets to create", description:"Enter number: (1-6)", multiple: false, submitOnChange: true, required: true, image: "https://cdn.rawgit.com/stephack/Virtual/master/resources/images/spca.png", range: "1..6"
        	}        
        	if(vbTotal && vbTotal>=1 && vbTotal<=6){
        		for(i in 1..vbTotal) {
         			section("Virtual Preset ${i}"){
        				input "vLabel${i}", "text", title: "Preset Name", description: "Enter Name Here", multiple: false, required: true
            			input "tracks${i}","enum",title:"Playlist/Station to Run", description: "Tap to choose", required:true, multiple: false, options: stationSelections()
       				}
      			}
   			}
        	else if(vbTotal){
        		section{paragraph "Please choose a value between 1 and 6."}
        	}
        }
        
        section("Set Custom App Name") {
			label title: "Assign a Custom App Name", required: false
      	}                  
	}
}

def confirmOptions(){
	dynamicPage(name:"confirmOptions",install:true, uninstall:true){    
		section("Speaker Being Controlled"){
        		paragraph "${sonos}"
		}
        for(i in 1..vbTotal) {
           	def childLabel = app."vLabel${i}"
            def currTrack = app."tracks${i}"
       		section("VPL ${i} - Alexa Commands"){
        		paragraph "'Alexa, turn on ${childLabel}'\n\n"+
               	"This will turn on ${sonos} and start the playlist/station [${currTrack}]"
       		}
       	}    	
        if(state.oldVbTotal>vbTotal){
        	section("The following will be deleted"){
            	for(i in vbTotal+1..state.oldVbTotal) {
            		def childLabel = app."vLabel${i}"
                	def currTrack = app."tracks${i}"
        			paragraph "${childLabel} with [${currTrack}] will be removed."                	
       			}
          	}       	
        }
	}
}

def installed() {
	log.debug "Installed with settings: ${settings}"
    initialize()    
}

def updated() {
	log.debug "Updated with settings: ${settings}"
  	unsubscribe()
    initialize()
}

def initialize() {
    //if(parent) { 
    	initChild() 
   // } else {
    //	initParent() 
   // }  
}

def initChild() {
	app.label==app.name?app.updateLabel(defaultLabel()):app.updateLabel(app.label)	
	    
    createContainer()
    if(state.savedPList==null){state.savedPList=[]} 
    savePlaylistsToState()
    state.oldVbTotal = vbTotal //keep track of number of changes to Virtual Device total to manage confirmOptions display of what will be deleted
    log.debug "Initialization Complete"
    createVB("Momentary Button")    
}

def initParent() {
	log.debug "Parent Initialized"
}

def defaultLabel() {
	return "${sonos}"-"Sonos"+"Presets"
}

def createContainer() {
	log.info "Creating Virtual Container"
    def childDevice = getAllChildDevices()?.find {it.device.deviceNetworkId == "VC_${app.id}"}        
    if (!childDevice) {
    	childDevice = addChildDevice("stephack", "Virtual Container", "VC_${app.id}", null,[completedSetup: true,
        label: app.label]) 
        log.info "Created Container [${childDevice}]"
        childDevice.sendEvent(name:"level", value: "1")
        for(i in 1..6){childDevice.sendEvent(name:"vlabel${i}", value:"--")}	///sets defaults for attributes...needed for inconsistent IOS tile display
	}
    else {
    	childDevice.label = app.label
        childDevice.name = app.label
        log.info "Container renamed to [${app.label}]"
	}
}

def createVB(vType){	//creates Preset Virtual Momentary Switches
    def vbInfo = []
	for(i in 1..vbTotal) {
    	vbInfo << [id:i, name:(app."vLabel${i}")]
    }
    def childDevice = getAllChildDevices()?.find {it.device.deviceNetworkId == "VC_${app.id}" }        
    childDevice.createChildVB(vbTotal,vbInfo,vType)
}

def containerOn(which) {	//sends Child station requests to Sonos speaker
    def currTrack = app."tracks${which}"
    selectPlaylist(currTrack)
    log.info "Child $which requested [$currTrack] playlist on $sonos"
}

def containerOff(which) {
}

def containerLevel(val, which) {
}



//{audioSource=Sonos Q, station=null, name=Lets Be Superheroes, artist=Bounce Patrol, album=Kids Songs, trackNumber=4, status=playing, level=37, mute=unmuted, uri=x-rincon-queue:RINCON_949F3E14DD0401400#0, trackUri=x-sonos-http:A0DvPDnowsKT-ql_Qx247D2ptuyjd2cMMy5gvQ4mccM_FRVVJe2eOQ.mp3?sid=151&flags=32&sn=4, transportUri=x-rincon-queue:RINCON_949F3E14DD0401400#0, enqueuedUri=x-rincon-cpcontainer:1004204cxwT8X2rh3H99IUYJD_vlFBi-kKPq1K-Q5Av-CV86IlVaoyhvOaCsYg?sid=151&flags=8268&sn=4, metaData=<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\/\"><item id=\"1004204cxwT8X2rh3H99IUYJD_vlFBi-kKPq1K-Q5Av-CV86IlVaoyhvOaCsYg\" parentID=\"100520649zyj7a88zQERCtGerSSbntY3TSIXb0wTJuH9zCxru_xuuDja4gDCqw\" restricted=\"true\"><dc:title>Kids Songs<\/dc:title><upnp:class>object.container.album.musicAlbum<\/upnp:class><desc id=\"cdudn\" nameSpace=\"urn:schemas-rinconnetworks-com:metadata-1-0\/\">SA_RINCON38663_X_#Svc38663-64d7a80d-Token<\/desc><\/item><\/DIDL-Lite>, trackMetaData=<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\/\"><item id=\"-1\" parentID=\"-1\" restricted=\"true\"><res protocolInfo=\"sonos.com-http:*:audio\/mpeg:*\" duration=\"0:03:32\">x-sonos-http:A0DvPDnowsKT-ql_Qx247D2ptuyjd2cMMy5gvQ4mccM_FRVVJe2eOQ.mp3?sid=151&amp;flags=32&amp;sn=4<\/res><r:streamContent><\/r:streamContent><r:radioShowMd><\/r:radioShowMd><upnp:albumArtURI>\/getaa?s=1&amp;u=x-sonos-http%3aA0DvPDnowsKT-ql_Qx247D2ptuyjd2cMMy5gvQ4mccM_FRVVJe2eOQ.mp3%3fsid%3d151%26flags%3d32%26sn%3d4<\/upnp:albumArtURI><dc:title>Let&apos;s Be Superheroes<\/dc:title><upnp:class>object.item.audioItem.musicTrack<\/upnp:class><dc:creator>Bounce Patrol<\/dc:creator><upnp:album>Kids Songs<\/upnp:album><\/item><\/DIDL-Lite>}
 

def stationSelections(){	//retrieves recently played stations from sonos speaker and presents as options when choosing station in VPLs
    def newOptions = state.savedPList.findAll()//{it.selected==true}	//ensures previously selected stations are included in the new list
    //log.error newOptions
    def states = sonos.statesSince("trackData", new Date(0), [max:30]) //??gets all previously played tracks from sonos speaker
    //log.info states
    states?.each{	//for each track in the fresh list
    	def stationData = it.jsonValue
        //log.info stationData
        //log.debug stationData.uri
        //log.info stationData.metaData
        //log.error stationData.station
        if(!newOptions.find{fav->fav.station==stationData.station}){ //checks whether previously selected tracks(newOptions) exist in the new list and prevents adding second entry
   			newOptions << [uri:stationData.uri,metaData:stationData.metaData,station:stationData.station]
        }
    }
    def options = newOptions.collect{it.station?:"N/A"}.sort()
    //log.error newOptions
    log.info options
    state.savedPList = newOptions
	return options
}

def savePlaylistsToState(){	//stores all the stations selected by the VPLs in savedPList state
	log.info "Saving Playlist info"
    def newStationList = []
    for(i in 1..vbTotal){
        def placeHold = app."tracks${i}"
    	if(placeHold){
        	newStationList << state.savedPList.find{it.station==placeHold} // add each selected station to savedPList state
        }
   	}
    state.savedPList = newStationList
}

def selectPlaylist(pList){	//receives playlist request from VPL and finds detailed track info stored in savedPList state
  	def myStations = state.savedPList
    if(myStations.size==0){
		log.debug "No Saved Playlists/Stations Found"
    }
    else{ 
    	def stationToPlay = myStations.find{stationToPlay->stationToPlay.station==pList}
    	playStation(stationToPlay)
    }    
}

def isPlaylistOrAlbum(trackData){ //determines type of playlist/station and formats properly for playback
	trackData.uri.startsWith('x-rincon-cpcontainer') ? true : false
}

def playStation(trackData){	//sends formatted play command to Sonos speaker
    if(isPlaylistOrAlbum(trackData)){
		log.debug "Working around some sonos device handler playlist wierdness ${trackData.station}. This seems to work"
        trackData.transportUri=trackData.uri
        trackData.enqueuedUri="savedqueues"
        trackData.trackNumber="1"
        sonos.setTrack(trackData)
        pause(1500)
        sonos.play()
    }
    else{
    	sonos.playTrack(trackData.uri,trackData.metaData)
    }
}

def toJson(str){
    def slurper = new groovy.json.JsonSlurper()
    def json = slurper.parseText(str)
}

def aboutPage() {
	dynamicPage(name: "aboutPage", title: none) {
     	section("User's Guide: Sonos Playlist Control") {
        	paragraph "This smartApp allows you to create voice commands for your integrated Sonos speakers. These commands are available to connect"+
            " with other smartApps like Alexa and Google Home. There are 2 types of 'Voice Commands' you can create."
        }
        section("1. Virtual Playlists"){
        	paragraph "These allow you to turn on the speaker and automatically start playing a station or playlist.\n"+
            "They are also exposed as dimmable switches, they should be used more like station presets buttons. They do NOT process 'OFF' commands.\nSee Best Practices below."
		}
		section("Best Practices:"){
        	paragraph "You should set your Virtual Playlist name to the voice command you would use to start playback of a particular station."+
            " While it can be used for volume control, it would be more practical to use the Virtual Speaker for that instead.\n"+
            "By design, it cannot be used to turn off the speaker. Again, the Virtual Speaker should be used instead.\n\n"+
            " - 'Alexa, turn on [Jazz in the Dining Room]'\n"+
            " Starts playback of the associated Jazz station."
 		}
	}
}/**
 *  Sonos Playlist Controller 
 *
 *  Copyright 2017 Stephan Hackett
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 *	 
 * Speacial Thanks to @GuyInATie for allowing me to use snippets from his Sonos Remote Control smartApp. They were used
 * as the foundation of the code for retrieving and storing the recently played station from the Sonos speakers.
 *
 *	Icons by http://www.icons8.com	
 *
 */

def version() {return "0.1.20180530"}

definition(
    name: "Sonos PlayList Control",
    namespace: "stephack",
    author: "Stephan Hackett",
    description: "Autoplay Stations/Playlists on Sonos speakers",
    category: "My Apps",
    //parent: "stephack:Sonos PlayList Control",
    iconUrl: "https://cdn.rawgit.com/stephack/Virtual/master/resources/images/spca.png",
    iconX2Url: "https://cdn.rawgit.com/stephack/Virtual/master/resources/images/spca.png",
    iconX3Url: "https://cdn.rawgit.com/stephack/SPC/Virtual/resources/images/spca.png"
)

preferences {
	//page(name: "startPage")
	//page(name: "parentPage")
	page(name: "mainPage", nextPage: confirmOptions)
	//page(name: "choosePlaylists", title: "Select stations for your Virtual Playlists", nextPage: confirmOptions)
	page(name: "confirmOptions", title: "Confirm All Settings Below")
    page(name: "aboutPage")
}

def startPage() {
    if (parent) {
        mainPage()
    } else {
        parentPage()
    }
}

def parentPage() {
	return dynamicPage(name: "parentPage", title: "", nextPage: "", install: true, uninstall: true) {
        section("Installed PlayList Controls") {
            app(name: "childApps", appName: appName(), namespace: "stephack", title: "Create New Playlist", multiple: true)
        }
        section("Version Info & User's Guide") {
       		href (name: "aboutPage", 
       		title: "Sonos PlayList Control\nver "+version(), 
       		description: "Tap for User's Guide and Info.",
       		image: "https://cdn.rawgit.com/stephack/Virtual/master/resources/images/spca.png",
       		required: false,
       		page: "aboutPage"
 	   		)
      	}
    }
}

private def appName() { return "${parent ? "VC Config" : "Sonos PlayList Control"}" }

def mainPage(){
	dynamicPage(name:"mainPage",uninstall:true){
    	section("Speaker to control with Virtual Playlists") {
        	input "sonos", "capability.musicPlayer", title: "Choose Speaker", submitOnChange: true, multiple:false, required: true, image: "https://cdn.rawgit.com/stephack/Virtual/master/resources/images/speakera.png"            
        }
        
        if(sonos){
        	section{
            	input "vbTotal", "number", title: "# of Presets to create", description:"Enter number: (1-6)", multiple: false, submitOnChange: true, required: true, image: "https://cdn.rawgit.com/stephack/Virtual/master/resources/images/spca.png", range: "1..6"
        	}        
        	if(vbTotal && vbTotal>=1 && vbTotal<=6){
        		for(i in 1..vbTotal) {
         			section("Virtual Preset ${i}"){
        				input "vLabel${i}", "text", title: "Preset Name", description: "Enter Name Here", multiple: false, required: true
            			input "tracks${i}","enum",title:"Playlist/Station to Run", description: "Tap to choose", required:true, multiple: false, options: stationSelections()
       				}
      			}
   			}
        	else if(vbTotal){
        		section{paragraph "Please choose a value between 1 and 6."}
        	}
        }
        
        section("Set Custom App Name") {
			label title: "Assign a Custom App Name", required: false
      	}                  
	}
}

def confirmOptions(){
	dynamicPage(name:"confirmOptions",install:true, uninstall:true){    
		section("Speaker Being Controlled"){
        		paragraph "${sonos}"
		}
        for(i in 1..vbTotal) {
           	def childLabel = app."vLabel${i}"
            def currTrack = app."tracks${i}"
       		section("VPL ${i} - Alexa Commands"){
        		paragraph "'Alexa, turn on ${childLabel}'\n\n"+
               	"This will turn on ${sonos} and start the playlist/station [${currTrack}]"
       		}
       	}    	
        if(state.oldVbTotal>vbTotal){
        	section("The following will be deleted"){
            	for(i in vbTotal+1..state.oldVbTotal) {
            		def childLabel = app."vLabel${i}"
                	def currTrack = app."tracks${i}"
        			paragraph "${childLabel} with [${currTrack}] will be removed."                	
       			}
          	}       	
        }
	}
}

def installed() {
	log.debug "Installed with settings: ${settings}"
    initialize()    
}

def updated() {
	log.debug "Updated with settings: ${settings}"
  	unsubscribe()
    initialize()
}

def initialize() {
    //if(parent) { 
    	initChild() 
   // } else {
    //	initParent() 
   // }  
}

def initChild() {
	app.label==app.name?app.updateLabel(defaultLabel()):app.updateLabel(app.label)	
	    
    createContainer()
    if(state.savedPList==null){state.savedPList=[]} 
    savePlaylistsToState()
    state.oldVbTotal = vbTotal //keep track of number of changes to Virtual Device total to manage confirmOptions display of what will be deleted
    log.debug "Initialization Complete"
    createVB("Momentary Button")    
}

def initParent() {
	log.debug "Parent Initialized"
}

def defaultLabel() {
	return "${sonos}"-"Sonos"+"Presets"
}

def createContainer() {
	log.info "Creating Virtual Container"
    def childDevice = getAllChildDevices()?.find {it.device.deviceNetworkId == "VC_${app.id}"}        
    if (!childDevice) {
    	childDevice = addChildDevice("stephack", "Virtual Container", "VC_${app.id}", null,[completedSetup: true,
        label: app.label]) 
        log.info "Created Container [${childDevice}]"
        childDevice.sendEvent(name:"level", value: "1")
        for(i in 1..6){childDevice.sendEvent(name:"vlabel${i}", value:"--")}	///sets defaults for attributes...needed for inconsistent IOS tile display
	}
    else {
    	childDevice.label = app.label
        childDevice.name = app.label
        log.info "Container renamed to [${app.label}]"
	}
}

def createVB(vType){	//creates Preset Virtual Momentary Switches
    def vbInfo = []
	for(i in 1..vbTotal) {
    	vbInfo << [id:i, name:(app."vLabel${i}")]
    }
    def childDevice = getAllChildDevices()?.find {it.device.deviceNetworkId == "VC_${app.id}" }        
    childDevice.createChildVB(vbTotal,vbInfo,vType)
}

def containerOn(which) {	//sends Child station requests to Sonos speaker
    def currTrack = app."tracks${which}"
    selectPlaylist(currTrack)
    log.info "Child $which requested [$currTrack] playlist on $sonos"
}

def containerOff(which) {
}

def containerLevel(val, which) {
}



//{audioSource=Sonos Q, station=null, name=Lets Be Superheroes, artist=Bounce Patrol, album=Kids Songs, trackNumber=4, status=playing, level=37, mute=unmuted, uri=x-rincon-queue:RINCON_949F3E14DD0401400#0, trackUri=x-sonos-http:A0DvPDnowsKT-ql_Qx247D2ptuyjd2cMMy5gvQ4mccM_FRVVJe2eOQ.mp3?sid=151&flags=32&sn=4, transportUri=x-rincon-queue:RINCON_949F3E14DD0401400#0, enqueuedUri=x-rincon-cpcontainer:1004204cxwT8X2rh3H99IUYJD_vlFBi-kKPq1K-Q5Av-CV86IlVaoyhvOaCsYg?sid=151&flags=8268&sn=4, metaData=<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\/\"><item id=\"1004204cxwT8X2rh3H99IUYJD_vlFBi-kKPq1K-Q5Av-CV86IlVaoyhvOaCsYg\" parentID=\"100520649zyj7a88zQERCtGerSSbntY3TSIXb0wTJuH9zCxru_xuuDja4gDCqw\" restricted=\"true\"><dc:title>Kids Songs<\/dc:title><upnp:class>object.container.album.musicAlbum<\/upnp:class><desc id=\"cdudn\" nameSpace=\"urn:schemas-rinconnetworks-com:metadata-1-0\/\">SA_RINCON38663_X_#Svc38663-64d7a80d-Token<\/desc><\/item><\/DIDL-Lite>, trackMetaData=<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\/\"><item id=\"-1\" parentID=\"-1\" restricted=\"true\"><res protocolInfo=\"sonos.com-http:*:audio\/mpeg:*\" duration=\"0:03:32\">x-sonos-http:A0DvPDnowsKT-ql_Qx247D2ptuyjd2cMMy5gvQ4mccM_FRVVJe2eOQ.mp3?sid=151&amp;flags=32&amp;sn=4<\/res><r:streamContent><\/r:streamContent><r:radioShowMd><\/r:radioShowMd><upnp:albumArtURI>\/getaa?s=1&amp;u=x-sonos-http%3aA0DvPDnowsKT-ql_Qx247D2ptuyjd2cMMy5gvQ4mccM_FRVVJe2eOQ.mp3%3fsid%3d151%26flags%3d32%26sn%3d4<\/upnp:albumArtURI><dc:title>Let&apos;s Be Superheroes<\/dc:title><upnp:class>object.item.audioItem.musicTrack<\/upnp:class><dc:creator>Bounce Patrol<\/dc:creator><upnp:album>Kids Songs<\/upnp:album><\/item><\/DIDL-Lite>}
 

def stationSelections(){	//retrieves recently played stations from sonos speaker and presents as options when choosing station in VPLs
    def newOptions = state.savedPList.findAll()//{it.selected==true}	//ensures previously selected stations are included in the new list
    //log.error newOptions
    def states = sonos.statesSince("trackData", new Date(0), [max:30]) //??gets all previously played tracks from sonos speaker
    //log.info states
    states?.each{	//for each track in the fresh list
    	def stationData = it.jsonValue
        //log.info stationData
        //log.debug stationData.uri
        //log.info stationData.metaData
        //log.error stationData.station
        if(!newOptions.find{fav->fav.station==stationData.station}){ //checks whether previously selected tracks(newOptions) exist in the new list and prevents adding second entry
   			newOptions << [uri:stationData.uri,metaData:stationData.metaData,station:stationData.station]
        }
    }
    def options = newOptions.collect{it.station?:"N/A"}.sort()
    //log.error newOptions
    log.info options
    state.savedPList = newOptions
	return options
}

def savePlaylistsToState(){	//stores all the stations selected by the VPLs in savedPList state
	log.info "Saving Playlist info"
    def newStationList = []
    for(i in 1..vbTotal){
        def placeHold = app."tracks${i}"
    	if(placeHold){
        	newStationList << state.savedPList.find{it.station==placeHold} // add each selected station to savedPList state
        }
   	}
    state.savedPList = newStationList
}

def selectPlaylist(pList){	//receives playlist request from VPL and finds detailed track info stored in savedPList state
  	def myStations = state.savedPList
    if(myStations.size==0){
		log.debug "No Saved Playlists/Stations Found"
    }
    else{ 
    	def stationToPlay = myStations.find{stationToPlay->stationToPlay.station==pList}
    	playStation(stationToPlay)
    }    
}

def isPlaylistOrAlbum(trackData){ //determines type of playlist/station and formats properly for playback
	trackData.uri.startsWith('x-rincon-cpcontainer') ? true : false
}

def playStation(trackData){	//sends formatted play command to Sonos speaker
    if(isPlaylistOrAlbum(trackData)){
		log.debug "Working around some sonos device handler playlist wierdness ${trackData.station}. This seems to work"
        trackData.transportUri=trackData.uri
        trackData.enqueuedUri="savedqueues"
        trackData.trackNumber="1"
        sonos.setTrack(trackData)
        pause(1500)
        sonos.play()
    }
    else{
    	sonos.playTrack(trackData.uri,trackData.metaData)
    }
}

def toJson(str){
    def slurper = new groovy.json.JsonSlurper()
    def json = slurper.parseText(str)
}

def aboutPage() {
	dynamicPage(name: "aboutPage", title: none) {
     	section("User's Guide: Sonos Playlist Control") {
        	paragraph "This smartApp allows you to create voice commands for your integrated Sonos speakers. These commands are available to connect"+
            " with other smartApps like Alexa and Google Home. There are 2 types of 'Voice Commands' you can create."
        }
        section("1. Virtual Playlists"){
        	paragraph "These allow you to turn on the speaker and automatically start playing a station or playlist.\n"+
            "They are also exposed as dimmable switches, they should be used more like station presets buttons. They do NOT process 'OFF' commands.\nSee Best Practices below."
		}
		section("Best Practices:"){
        	paragraph "You should set your Virtual Playlist name to the voice command you would use to start playback of a particular station."+
            " While it can be used for volume control, it would be more practical to use the Virtual Speaker for that instead.\n"+
            "By design, it cannot be used to turn off the speaker. Again, the Virtual Speaker should be used instead.\n\n"+
            " - 'Alexa, turn on [Jazz in the Dining Room]'\n"+
            " Starts playback of the associated Jazz station."
 		}
	}
}

This is exactly what I had in mind. I tried the updated code, def version() {return "0.1.20180530"}, and it wouldn't load, throwing several errors. The older code on github loaded fine but does not find any stations.

This is really good though, anything I can do to help push this forward let me know.

It won't work as is because it is tied to a couple of custom virtual devices that are not included above (they too are a work in progress :sweat_smile:).

It will require a lot of trial and error to bend this to work with HE. This and many other WAF projects that I had working in ST are on my to do list but don't know when I can jump down this rabbit hole again. I posted the code so you or others can grab snippets to make it work for you. PM me any specific questions or errors you have and I'll do my best to explain what the app is doing.

Looking a little closer, the code was posted twice. The new code loads fine but still doesn't find any playlists, do they need to currently be playing? Also, will these virtual devices be available in dashboards?

They are momentary buttons..so yes.

Any chance you’ve had time to work on your app?

Lol, you must have read my Virtual Container posts and saw references to "Playlists".

Yeah, I've been working on it but as it has always been...it's a time drain. I have a skeleton in place but I'm having trouble getting consistent results from the Sonos speaker event log. There's way more stuff to filter out than ST had. Making it user friendly has been really challenging.

Now that I have the Virtual Container code portion built out, I can refocus on this a bit more.

I hope to have something to test out soon.

You mean the Hubitat Logs, right? I am seeing all the event corresponding to my Sonos speaker, but not a single URI. Any help would be appreciated on how to identify URIs.

dev:22019-06-21 20:01:36.364 infoSPK LR Sonos is playing
dev:22019-06-21 20:01:34.707 infoSPK LR Sonos playing: Los Invisibles (En Directo) by Callejeros from Obras 2004 en Directo
dev:22019-06-21 20:01:34.691 infoSPK LR Sonos is transitioning
dev:22019-06-21 20:01:34.556 infoSPK LR Sonos is stopped
dev:22019-06-21 20:01:30.428 infoSPK LR Sonos is playing

How do you find the URI for Pandora stations? I see you mention this is in the log screen when using Sonos, but I dont have Sonos.

I've been trying to figure out how to start a station and cast it to Chromecast audio group.

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.