Again with VLC Thing and Hubitat

Hi all...
I'm here trying to play an specific track on VLC running on a Windows 10 PC but so far no luck.
Let me explain:

  • I had been a week reading several times the article " Porting VLCthing" and others as well but in any my doubt is fully explained.
  • I got connection with VLC using port 8080 (for me is ok) and I can Play the last track, go to Next track, and others simple actions from the created Device using the driver published on this community but I can't figure out how to play an specific track residing on a local web server or a local file (any option will be valid for me).

Do anyone was able to do it?
My VLC is v3.0.7.1 and Windows 10 last version, no problem with Firewall.... looks is a problem of the driver. ¿?¿?

Try your url in a web browser. If it works there it should work in vlcthing.

It works on any web browser 100% sure....
The driver is returning this error on the logger: groovy.lang.MissingPropertyException: No such property: uri for class: java.lang.String on line 210 (playTrack)

and on that line on the Driver Code says:

def command = "command=in_play&input=" + URLEncoder.encode(uri.uri, "UTF-8")

This is the whole function:
// MusicPlayer.playTrack
def playTrack(uri) {
//log.debug "playTrack(${uri.uri})"
def command = "command=in_play&input=" + URLEncoder.encode(uri.uri, "UTF-8")
return apiCommand(command, 500)
}

Here is a view of a rule I created only to learn how to use this feature:


Any idea?

Open the device detail page and use the speak button.

Done...
I tried to play text like "Hi" or similar not just in Speak but in "Play Text"
but no TTS sound... Play, Mute, Unmute... working fine.
I tried to load the track on "Play Track" but the same result.... silence.

I checked and in my W10 the TTS is working perfect.

Can you please publish here the Driver Code you are using?

I will when I get to my PC.

As far as I know a proper "port" was never released. And that was some time ago. I do not recall exactly what it took get working.

Ok thanks for your time on this one... I will keep eyes open on this threat looking for a solution.

One last question.... I'm trying to do this because here internet is a nightmare and very expensive so I would like to use a device not depending on Internet.

Do you have any experience using Google Home Mini offline with Hubitat? As far as I checked on the web it only can be used as a Bluetooth speaker but nothing else.

Here is what I use. Fair warning all functions may not work properly.

I can confirm speak and play track to work here. This has been in my hub for some time.

    /**
     *  VLC Things. A SmartThings device handler for the VLC media player.
     *
     *  For more information, please visit
     *  <https://github.com/statusbits/smartthings/tree/master/VlcThing.md/>
     *
     *  --------------------------------------------------------------------------
     *
     *  Copyright © 2014 Statusbits.com
     *
     *  This program is free software: you can redistribute it and/or modify it
     *  under the terms of the GNU General Public License as published by the Free
     *  Software Foundation, either version 3 of the License, or (at your option)
     *  any later version.
     *
     *  This program is distributed in the hope that it will be useful, but
     *  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     *  or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
     *  for more details.
     *
     *  You should have received a copy of the GNU General Public License along
     *  with this program.  If not, see <http://www.gnu.org/licenses/>.
     *
     *  --------------------------------------------------------------------------
     *
     *  Version 2.0.0 (12/22/2016)
     */

    import groovy.json.JsonSlurper

    preferences {
        // NOTE: Android client does not accept "defaultValue" attribute!
        input("confIpAddr", "string", title:"VLC IP Address",
            required:false, displayDuringSetup:true)
        input("confTcpPort", "number", title:"VLC TCP Port",
            required:false, displayDuringSetup:true)
        input("confPassword", "password", title:"VLC Password",
            required:false, displayDuringSetup:true)
    }

    metadata {
        definition (name:"VLC Thing", namespace:"statusbits", author:"geko@statusbits.com") {
            capability "Actuator"
            capability "Switch"
            capability "Music Player"
            capability "Speech Synthesis"
            capability "Refresh"
            capability "Polling"

            // Custom attributes
            attribute "connection", "string"    // Connection status string

            // Custom commands
            command "enqueue", ["string"]
            command "seek", ["number"]
            command "playTrackAndResume", ["string","number","number"]
            command "playTrackAndRestore", ["string","number","number"]
            command "playTextAndResume", ["string","number"]
            command "playTextAndRestore", ["string","number"]
            command "playSoundAndTrack", ["string","number","json_object","number"]
            command "testTTS"
        }

    }  
    def installed() {
        //log.debug "installed()"
        log.info title()

        // Initialize attributes to default values (Issue #18)
        sendEvent([name:'status', value:'stopped', displayed:false])
        sendEvent([name:'level', value:'0', displayed:false])
        sendEvent([name:'mute', value:'unmuted', displayed:false])
        sendEvent([name:'trackDescription', value:'', displayed:false])
        sendEvent([name:'connection', value:'disconnected', displayed:false])
    }

    def updated() {
    	//log.debug "updated with settings: ${settings}"
        log.info title()

        unschedule()

        if (!settings.confIpAddr) {
    	    log.warn "IP address is not set!"
            return
        }

        def port = settings.confTcpPort
        if (!port) {
    	    log.warn "Using default TCP port 8080!"
            port = 8080
        }

        def dni = createDNI(settings.confIpAddr, port)
        device.deviceNetworkId = dni
        state.dni = dni
        state.hostAddress = "${settings.confIpAddr}:${settings.confTcpPort}"
        state.requestTime = 0
        state.responseTime = 0
        state.updatedTime = 0
        state.lastPoll = 0

        if (settings.confPassword) {
            state.userAuth = ":${settings.confPassword}".bytes.encodeBase64() as String
        } else {
            state.userAuth = null
        }

        startPollingTask()
        //STATE()
    }

    def pollingTask() {
        //log.debug "pollingTask()"

        state.lastPoll = now()

        // Check connection status
        def requestTime = state.requestTime ?: 0
        def responseTime = state.responseTime ?: 0
        if (requestTime && (requestTime - responseTime) > 10000) {
            log.warn "No connection!"
            sendEvent([
                name:           'connection',
                value:          'disconnected',
                isStateChange:  true,
                displayed:      true
            ])
        }

        def updated = state.updatedTime ?: 0
        if ((now() - updated) > 10000) {
            return apiGetStatus()
        }
    }

    def parse(String message) {
        def msg = stringToMap(message)
        if (msg.containsKey("simulator")) {
            // simulator input
            return parseHttpResponse(msg)
        }

        if (!msg.containsKey("headers")) {
            log.error "No HTTP headers found in '${message}'"
            return null
        }

        // parse HTTP response headers
        def headers = new String(msg.headers.decodeBase64())
        def parsedHeaders = parseHttpHeaders(headers)
       // log.debug "parsedHeaders: ${parsedHeaders}"
        if (parsedHeaders.status != 200) {
            log.error "Server error: ${parsedHeaders.reason}"
            return null
        }

        // parse HTTP response body
        if (!msg.body) {
            log.error "No HTTP body found in '${message}'"
            return null
        }

        def body = new String(msg.body.decodeBase64())
        
        def slurper = new JsonSlurper()
        return parseHttpResponse(slurper.parseText(body))
    }

    // switch.on
    def on() {
        play()
    }

    // switch.off
    def off() {
        stop()
    }

    // MusicPlayer.play
    def play() {
        //log.debug "play()"

        def command
        if (device.currentValue('status') == 'paused') {
            command = 'command=pl_forceresume'
        } else {
            command = 'command=pl_play'
        }

        return apiCommand(command, 500)
    }

    // MusicPlayer.stop
    def stop() {
        //log.debug "stop()"
        return apiCommand("command=pl_stop", 500)
    }

    // MusicPlayer.pause
    def pause() {
        //log.debug "pause()"
        return apiCommand("command=pl_forcepause")
    }

    // MusicPlayer.playTrack
    def playTrack(uri) {
        //log.debug "playTrack(${uri})"
        def command = "command=in_play&input=" + URLEncoder.encode(uri, "UTF-8")
        return apiCommand(command, 500)
    }

    // MusicPlayer.playText
    def playText(text) {
        log.debug "playText(${text})"
        def sound = textToSpeech(text, "Brian")
        //def sound = myTextToSpeech(text)
        log.debug "line 278 ${sound}"
        return playTrack(sound.uri)
    }

    // MusicPlayer.setTrack
    def setTrack(name) {
        log.warn "setTrack(${name}) not implemented"
        return null
    }

    // MusicPlayer.resumeTrack
    def resumeTrack(name) {
        log.warn "resumeTrack(${name}) not implemented"
        return null
    }

    // MusicPlayer.restoreTrack
    def restoreTrack(name) {
        log.warn "restoreTrack(${name}) not implemented"
        return null
    }

    // MusicPlayer.nextTrack
    def nextTrack() {
        //log.debug "nextTrack()"
        return apiCommand("command=pl_next", 500)
    }

    // MusicPlayer.previousTrack
    def previousTrack() {
        //log.debug "previousTrack()"
        return apiCommand("command=pl_previous", 500)
    }

    // MusicPlayer.setLevel
    def setLevel(number) {
        //log.debug "setLevel(${number})"

        if (device.currentValue('mute') == 'muted') {
            sendEvent(name:'mute', value:'unmuted')
        }

        sendEvent(name:"level", value:number)
        def volume = ((number * 512) / 100) as int
        return apiCommand("command=volume&val=${volume}")
    }

    // MusicPlayer.mute
    def mute() {
        //log.debug "mute()"

        if (device.currentValue('mute') == 'muted') {
            return null
        }

        state.savedVolume = device.currentValue('level')
        sendEvent(name:'mute', value:'muted')
        sendEvent(name:'level', value:0)

        return apiCommand("command=volume&val=0")
    }

    // MusicPlayer.unmute
    def unmute() {
        //log.debug "unmute()"

        if (device.currentValue('mute') == 'muted') {
            return setLevel(state.savedVolume.toInteger())
        }

        return null
    }

    // SpeechSynthesis.speak
    def speak(text) {
        log.debug "line 292 speak(${text})"
        def sound = myTextToSpeech(text)
        //def sound = textToSpeech(text, "Brian")
        return playTrack(sound.uri)
    }

    // polling.poll 
    def poll() {
        //log.debug "poll()"
        return refresh()
    }

    // refresh.refresh
    def refresh() {
        //log.debug "refresh()"
        //STATE()

        if (!updateDNI()) {
            sendEvent([
                name:           'connection',
                value:          'disconnected',
                isStateChange:  true,
                displayed:      false
            ])

            return null
        }

        // Restart polling task if it's not run for 5 minutes
        def elapsed = (now() - (state?.lastPoll ? state.lastPoll : (now()-301))) / 1000
        if (elapsed > 300) {
            log.warn "Restarting polling task..."
            unschedule()
            startPollingTask()
        }

        return apiGetStatus()
    }

    def enqueue(uri) {
        //log.debug "enqueue(${uri})"
        def command = "command=in_enqueue&input=" + URLEncoder.encode(uri, "UTF-8")
        return apiCommand(command)
    }

    def seek(trackNumber) {
        //log.debug "seek(${trackNumber})"
        def command = "command=pl_play&id=${trackNumber}"
        return apiCommand(command, 500)
    }

    def playTrackAndResume(uri, duration, volume = null) {
        //log.debug "playTrackAndResume(${uri}, ${duration}, ${volume})"

        // FIXME
        return playTrackAndRestore(uri, duration, volume)
    }

    def playTrackAndRestore(uri, duration, volume = null) {
        //log.debug "playTrackAndRestore(${uri}, ${duration}, ${volume})"

        def currentStatus = device.currentValue('status')
        def currentVolume = device.currentValue('level')
        def currentMute = device.currentValue('mute')
        def actions = []
        if (currentStatus == 'playing') {
            actions << apiCommand("command=pl_stop")
            actions << delayHubAction(500)
        }

        if (volume) {
            actions << setLevel(volume)
            actions << delayHubAction(500)
        } else if (currentMute == 'muted') {
            actions << unmute()
            actions << delayHubAction(500)
        }

        def delay = (duration.toInteger() + 1) * 1000
        //log.debug "delay = ${delay}"

        actions << playTrack(uri)
        actions << delayHubAction(delay)
        actions << apiCommand("command=pl_stop")
        actions << delayHubAction(500)

        if (currentMute == 'muted') {
            actions << mute()
        } else if (volume) {
            actions << setLevel(currentVolume)
        }

        actions << apiGetStatus()
        actions = actions.flatten()
        //log.debug "actions: ${actions}"

        return actions
    }

    def playTextAndResume(text) {
        log.debug "playTextAndResume(${text}, ${volume})"
        def sound = myTextToSpeech(text)
        //log.debug "line 393 sound = ${sound}"
        return playTrackAndResume(sound.uri, (sound.duration as Integer) + 1, volume)
    }

    def playTextAndRestore(text, volume = null) {
        //log.debug "playTextAndRestore(${text}, ${volume})"
        def sound = myTextToSpeech(text)
        return playTrackAndRestore(sound.uri, (sound.duration as Integer) + 1, volume)
    }

    def playSoundAndTrack(uri, duration, trackData, volume = null) {
        //log.debug "playSoundAndTrack(${uri}, ${duration}, ${trackData}, ${volume})"

        // FIXME
        return playTrackAndRestore(uri, duration, volume)
    }

    def testTTS() {
        //log.debug "testTTS()"
        def text = "VLC for Smart Things is brought to you by Statusbits.com"
        return playTextAndResume(text)
    }

    private startPollingTask() {
        //log.debug "startPollingTask()"

        pollingTask()

        Random rand = new Random(now())
        def seconds = rand.nextInt(60)
        def sched = "${seconds} 0/1 * * * ?"

        //log.debug "Scheduling polling task with \"${sched}\""
        schedule(sched, pollingTask)
    }

    def apiGet(String path) {
        //log.debug "apiGet(${path})"

        if (!updateDNI()) {
            return null
        }

        state.requestTime = now()
        state.responseTime = 0

        def headers = [
            HOST:       state.hostAddress,
            Accept:     "*/*"
        ]
        
        if (state.userAuth != null) {
            headers['Authorization'] = "Basic ${state.userAuth}"
        }

        def httpRequest = [
            method:     'GET',
            path:       path,
            headers:    headers
        ]

        //log.debug "httpRequest: ${httpRequest}"
        return new hubitat.device.HubAction(httpRequest)
    }

    private def delayHubAction(ms) {
        return new hubitat.device.HubAction("delay ${ms}")
    }

    private apiGetStatus() {
        return apiGet("/requests/status.json")
    }

    private apiCommand(command, refresh = 0) {
        //log.debug "apiCommand(${command})"

        def actions = [
            apiGet("/requests/status.json?${command}")
        ]

        if (refresh) {
            actions << delayHubAction(refresh)
            actions << apiGetStatus()
        }

        return actions
    }

    private def apiGetPlaylists() {
        //log.debug "getPlaylists()"
        return apiGet("/requests/playlist.json")
    }

    private parseHttpHeaders(String headers) {
        def lines = headers.readLines()
        def status = lines.remove(0).split()

        def result = [
            protocol:   status[0],
            status:     status[1].toInteger(),
            reason:     status[2]
        ]

        return result
    }

    private def parseHttpResponse(Map data) {
        //log.debug "parseHttpResponse(${data})"

        state.updatedTime = now()
        if (!state.responseTime) {
            state.responseTime = now()
        }

        def events = []

        if (data.containsKey('state')) {
            def vlcState = data.state
            //log.debug "VLC state: ${vlcState})"
            events << createEvent(name:"status", value:vlcState)
            if (vlcState == 'stopped') {
                events << createEvent([name:'trackDescription', value:''])
            }
        }

        if (data.containsKey('volume')) {
            //log.debug "VLC volume: ${data.volume})"
            def volume = ((data.volume.toInteger() * 100) / 512) as int
            events << createEvent(name:'level', value:volume)
        }

        if (data.containsKey('information')) {
            parseTrackInfo(events, data.information)
        }

        events << createEvent([
            name:           'connection',
            value:          'connected',
            isStateChange:  true,
            displayed:      false
        ])

        //log.debug "events: ${events}"
        return events
    }

    private def parseTrackInfo(events, Map info) {
        //log.debug "parseTrackInfo(${events}, ${info})"

        if (info.containsKey('category') && info.category.containsKey('meta')) {
            def meta = info.category.meta
            //log.debug "Track info: ${meta})"
            if (meta.containsKey('filename')) {
                if (meta.filename.contains("//s3.amazonaws.com/smartapp-")) {
                    log.trace "Skipping event generation for sound file ${meta.filename}"
                    return
                }
            }

            def track = ""
            if (meta.containsKey('artist')) {
                track = "${meta.artist} - "
            }
            if (meta.containsKey('title')) {
                track += meta.title
            } else if (meta.containsKey('filename')) {
                def parts = meta.filename.tokenize('/');
                track += parts.last()
            } else {
                track += '<untitled>'
            }

            if (track != device.currentState('trackDescription')) {
                meta.station = track
                events << createEvent(name:'trackDescription', value:track, displayed:false)
               // events << createEvent(name:'trackData', value:meta.encodeAsJSON(), displayed:false)
            }
        }
    }

    private def myTextToSpeech(text) {
       
        def sound = textToSpeech(text)
        log.debug "${text}"
        log.debug "${sound.uri}"
        sound.uri = sound.uri.replace('https:', 'http:')
        return sound
    }

    private String createDNI(ipaddr, port) {
        //log.debug "createDNI(${ipaddr}, ${port})"

        def hexIp = ipaddr.tokenize('.').collect {
            String.format('%02X', it.toInteger())
        }.join()

        def hexPort = String.format('%04X', port.toInteger())
     
        return "${hexIp}:${hexPort}"
    }

    private updateDNI() {
        if (!state.dni) {
    	    log.warn "DNI is not set! Please enter IP address and port in settings."
            return false
        }
     
        if (state.dni != device.deviceNetworkId) {
    	    log.warn "Invalid DNI: ${device.deviceNetworkId}!"
            device.deviceNetworkId = state.dni
        }

        return true
    }

    private def title() {
        return "VLC Thing. Version 2.0.0 (12/22/2016). Copyright © 2014 Statusbits.com"
    }

    private def STATE() {
        log.trace "state: ${state}"
        log.trace "deviceNetworkId: ${device.deviceNetworkId}"
        log.trace "status: ${device.currentValue('status')}"
        log.trace "level: ${device.currentValue('level')}"
        log.trace "mute: ${device.currentValue('mute')}"
        log.trace "trackDescription: ${device.currentValue('trackDescription')}"
        log.trace "connection: ${device.currentValue("connection")}"
    }
3 Likes

I do not think these will function without internet.

You are correct. Any google home device (mini, home, max, hub or hub max) will not work at all without an internet connection. You cannot even play locally streamed music.

Are you trying to cast to a Google Home Mini with VLC Thing?

Assuming you have it all working, but if you don't...

If you are trying to cast to a Google Home device, there's a trick. At least on the Mac version there is. Perhaps the Windows version doesn't have this issue. But if it does, maybe this will help you.

1 Like

VLC Thing is to play to VLC. :slight_smile: he then asked a question about google home.

2 Likes

Understood. But he didn't mention how he was playing the sound from VLC Player. When I saw the question about Google Home Mini, I thought that perhaps this was how he was trying to play the sound, and possibly why it may not be working for him. So I offered what I know works on the Mac version.

Perhaps he also was not aware that you could cast to a Google Home Mini from VLC Player? I might have accidentally helped :wink:

Yes but i don't think you'd want to (or even could) cast to Google home through VLC through VLC thing. That's 2 more hops than necessary.

:thinking: Huh? I don't follow.

Nevermind...not important.

Thanks a lot cwwilson08 for the Code!! :+1:,
I will try it for sure right now and I will let you know the results.

For the rest of collegues .. I am not trying to cast to google home through VLC... I'm just researching what options do I have to have TTS or tracks playback on my home but NOT DEPENDING ON WWW. As you said Google Home (and similars) depends on www according my readings, VLC do not and that's why for now is my choice even when I have to use a computer turned on all the time to use it (I own an old small laptop I think could be the one).

But of course it would be nice to have a dedicated device for this. And I know about PI option but in my country I can't buy it so at the end it will cost almost the same compared with a Google mini and thats why I was asking for it.

Thanks for the help anyway...now I will try the provided Code

Yeap!!! cwwilson08 you nail it!.
This Code is working 100%..:sunglasses:
I suggest to the rest to save it because in this whole forum I couldn't find a post with a working code except this one.
Thanks a lot!

2 Likes

Advice for all!
The above driver code for VLC Thing is working fine with RM but in HSM and Notification app do not work well due an issue with the PlayTrack function.

bravenel help me to fix it and I just want to share this code with you.
I'm planing to keep fixing it as possible:

Thanks again to cwwilson08 for sharing the initial code:

/**
     *  VLC Things. A SmartThings device handler for the VLC media player.
     *
     *  For more information, please visit
     *  <https://github.com/statusbits/smartthings/tree/master/VlcThing.md/>
     *
     *  --------------------------------------------------------------------------
     *
     *  Copyright © 2014 Statusbits.com
     *
     *  This program is free software: you can redistribute it and/or modify it
     *  under the terms of the GNU General Public License as published by the Free
     *  Software Foundation, either version 3 of the License, or (at your option)
     *  any later version.
     *
     *  This program is distributed in the hope that it will be useful, but
     *  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     *  or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
     *  for more details.
     *
     *  You should have received a copy of the GNU General Public License along
     *  with this program.  If not, see <http://www.gnu.org/licenses/>.
     *
     *  --------------------------------------------------------------------------
     *
     *  Version 2.0.0 (12/22/2016)
     */

    import groovy.json.JsonSlurper

    preferences {
        // NOTE: Android client does not accept "defaultValue" attribute!
        input("confIpAddr", "string", title:"VLC IP Address",
            required:false, displayDuringSetup:true)
        input("confTcpPort", "number", title:"VLC TCP Port",
            required:false, displayDuringSetup:true)
        input("confPassword", "password", title:"VLC Password",
            required:false, displayDuringSetup:true)
    }

    metadata {
        definition (name:"VLC Thing", namespace:"statusbits", author:"geko@statusbits.com") {
            capability "Actuator"
            capability "Switch"
            capability "Music Player"
            capability "Speech Synthesis"
            capability "Refresh"
            capability "Polling"

            // Custom attributes
            attribute "connection", "string"    // Connection status string

            // Custom commands
            command "enqueue", ["string"]
            command "seek", ["number"]
            command "playTrackAndResume", ["string","number","number"]
            command "playTrackAndRestore", ["string","number","number"]
            command "playTextAndResume", ["string","number"]
            command "playTextAndRestore", ["string","number"]
            command "playSoundAndTrack", ["string","number","json_object","number"]
            command "testTTS"
        }

    }  
    def installed() {
        //log.debug "installed()"
        log.info title()

        // Initialize attributes to default values (Issue #18)
        sendEvent([name:'status', value:'stopped', displayed:false])
        sendEvent([name:'level', value:'0', displayed:false])
        sendEvent([name:'mute', value:'unmuted', displayed:false])
        sendEvent([name:'trackDescription', value:'', displayed:false])
        sendEvent([name:'connection', value:'disconnected', displayed:false])
    }

    def updated() {
    	//log.debug "updated with settings: ${settings}"
        log.info title()

        unschedule()

        if (!settings.confIpAddr) {
    	    log.warn "IP address is not set!"
            return
        }

        def port = settings.confTcpPort
        if (!port) {
    	    log.warn "Using default TCP port 8080!"
            port = 8080
        }

        def dni = createDNI(settings.confIpAddr, port)
        device.deviceNetworkId = dni
        state.dni = dni
        state.hostAddress = "${settings.confIpAddr}:${settings.confTcpPort}"
        state.requestTime = 0
        state.responseTime = 0
        state.updatedTime = 0
        state.lastPoll = 0

        if (settings.confPassword) {
            state.userAuth = ":${settings.confPassword}".bytes.encodeBase64() as String
        } else {
            state.userAuth = null
        }

        startPollingTask()
        //STATE()
    }

    def pollingTask() {
        //log.debug "pollingTask()"

        state.lastPoll = now()

        // Check connection status
        def requestTime = state.requestTime ?: 0
        def responseTime = state.responseTime ?: 0
        if (requestTime && (requestTime - responseTime) > 10000) {
            log.warn "No connection!"
            sendEvent([
                name:           'connection',
                value:          'disconnected',
                isStateChange:  true,
                displayed:      true
            ])
        }

        def updated = state.updatedTime ?: 0
        if ((now() - updated) > 10000) {
            return apiGetStatus()
        }
    }

    def parse(String message) {
        def msg = stringToMap(message)
        if (msg.containsKey("simulator")) {
            // simulator input
            return parseHttpResponse(msg)
        }

        if (!msg.containsKey("headers")) {
            log.error "No HTTP headers found in '${message}'"
            return null
        }

        // parse HTTP response headers
        def headers = new String(msg.headers.decodeBase64())
        def parsedHeaders = parseHttpHeaders(headers)
       // log.debug "parsedHeaders: ${parsedHeaders}"
        if (parsedHeaders.status != 200) {
            log.error "Server error: ${parsedHeaders.reason}"
            return null
        }

        // parse HTTP response body
        if (!msg.body) {
            log.error "No HTTP body found in '${message}'"
            return null
        }

        def body = new String(msg.body.decodeBase64())
        
        def slurper = new JsonSlurper()
        return parseHttpResponse(slurper.parseText(body))
    }

    // switch.on
    def on() {
        play()
    }

    // switch.off
    def off() {
        stop()
    }

    // MusicPlayer.play
    def play() {
        //log.debug "play()"

        def command
        if (device.currentValue('status') == 'paused') {
            command = 'command=pl_forceresume'
        } else {
            command = 'command=pl_play'
        }

        return apiCommand(command, 500)
    }

    // MusicPlayer.stop
    def stop() {
        //log.debug "stop()"
        return apiCommand("command=pl_stop", 500)
    }

    // MusicPlayer.pause
    def pause() {
        //log.debug "pause()"
        return apiCommand("command=pl_forcepause")
    }

    // MusicPlayer.setLevel
    def setLevel(number) {
        //log.debug "setLevel(${number})"

        if (device.currentValue('mute') == 'muted') {
            sendEvent(name:'mute', value:'unmuted')
        }

        sendEvent(name:"level", value:number)
        def volume = ((number * 512) / 100) as int
        return apiCommand("command=volume&val=${volume}")
    }

    // MusicPlayer.playTrack
    def playTrack(uri) {
        //log.debug "playTrack(${uri})"
        def command = "command=in_play&input=" + URLEncoder.encode(uri, "UTF-8")
        return apiCommand(command, 500)
    }

    def playTrack(track, level) {
       setLevel(level)
       playTrack(track)
    }

    // MusicPlayer.playText
    def playText(text) {
        log.debug "playText(${text})"
        def sound = textToSpeech(text, "Brian")
        //def sound = myTextToSpeech(text)
        log.debug "line 278 ${sound}"
        return playTrack(sound.uri)
    }
    // MusicPlayer.setTrack
    def setTrack(name) {
        log.warn "setTrack(${name}) not implemented"
        return null
    }

    // MusicPlayer.resumeTrack
    def resumeTrack(name) {
        log.warn "resumeTrack(${name}) not implemented"
        return null
    }

    // MusicPlayer.restoreTrack
    def restoreTrack(name) {
        log.warn "restoreTrack(${name}) not implemented"
        return null
    }

    // MusicPlayer.nextTrack
    def nextTrack() {
        //log.debug "nextTrack()"
        return apiCommand("command=pl_next", 500)
    }

    // MusicPlayer.previousTrack
    def previousTrack() {
        //log.debug "previousTrack()"
        return apiCommand("command=pl_previous", 500)
    }

    // MusicPlayer.mute
    def mute() {
        //log.debug "mute()"

        if (device.currentValue('mute') == 'muted') {
            return null
        }

        state.savedVolume = device.currentValue('level')
        sendEvent(name:'mute', value:'muted')
        sendEvent(name:'level', value:0)

        return apiCommand("command=volume&val=0")
    }

    // MusicPlayer.unmute
    def unmute() {
        //log.debug "unmute()"

        if (device.currentValue('mute') == 'muted') {
            return setLevel(state.savedVolume.toInteger())
        }

        return null
    }

    // SpeechSynthesis.speak
    def speak(text) {
        log.debug "line 292 speak(${text})"
        def sound = myTextToSpeech(text)
        //def sound = textToSpeech(text, "Brian")
        return playTrack(sound.uri)
    }

    // polling.poll 
    def poll() {
        //log.debug "poll()"
        return refresh()
    }

    // refresh.refresh
    def refresh() {
        //log.debug "refresh()"
        //STATE()

        if (!updateDNI()) {
            sendEvent([
                name:           'connection',
                value:          'disconnected',
                isStateChange:  true,
                displayed:      false
            ])

            return null
        }

        // Restart polling task if it's not run for 5 minutes
        def elapsed = (now() - (state?.lastPoll ? state.lastPoll : (now()-301))) / 1000
        if (elapsed > 300) {
            log.warn "Restarting polling task..."
            unschedule()
            startPollingTask()
        }

        return apiGetStatus()
    }

    def enqueue(uri) {
        //log.debug "enqueue(${uri})"
        def command = "command=in_enqueue&input=" + URLEncoder.encode(uri, "UTF-8")
        return apiCommand(command)
    }

    def seek(trackNumber) {
        //log.debug "seek(${trackNumber})"
        def command = "command=pl_play&id=${trackNumber}"
        return apiCommand(command, 500)
    }

    def playTrackAndResume(uri, duration, volume = null) {
        //log.debug "playTrackAndResume(${uri}, ${duration}, ${volume})"

        // FIXME
        return playTrackAndRestore(uri, duration, volume)
    }

    def playTrackAndRestore(uri, duration, volume = null) {
        //log.debug "playTrackAndRestore(${uri}, ${duration}, ${volume})"

        def currentStatus = device.currentValue('status')
        def currentVolume = device.currentValue('level')
        def currentMute = device.currentValue('mute')
        def actions = []
        if (currentStatus == 'playing') {
            actions << apiCommand("command=pl_stop")
            actions << delayHubAction(500)
        }

        if (volume) {
            actions << setLevel(volume)
            actions << delayHubAction(500)
        } else if (currentMute == 'muted') {
            actions << unmute()
            actions << delayHubAction(500)
        }

        def delay = (duration.toInteger() + 1) * 1000
        //log.debug "delay = ${delay}"

        actions << playTrack(uri)
        actions << delayHubAction(delay)
        actions << apiCommand("command=pl_stop")
        actions << delayHubAction(500)

        if (currentMute == 'muted') {
            actions << mute()
        } else if (volume) {
            actions << setLevel(currentVolume)
        }

        actions << apiGetStatus()
        actions = actions.flatten()
        //log.debug "actions: ${actions}"

        return actions
    }

    def playTextAndResume(text) {
        log.debug "playTextAndResume(${text}, ${volume})"
        def sound = myTextToSpeech(text)
        //log.debug "line 393 sound = ${sound}"
        return playTrackAndResume(sound.uri, (sound.duration as Integer) + 1, volume)
    }

    def playTextAndRestore(text, volume = null) {
        //log.debug "playTextAndRestore(${text}, ${volume})"
        def sound = myTextToSpeech(text)
        return playTrackAndRestore(sound.uri, (sound.duration as Integer) + 1, volume)
    }

    def playSoundAndTrack(uri, duration, trackData, volume = null) {
        //log.debug "playSoundAndTrack(${uri}, ${duration}, ${trackData}, ${volume})"

        // FIXME
        return playTrackAndRestore(uri, duration, volume)
    }

    def testTTS() {
        //log.debug "testTTS()"
        def text = "VLC for Smart Things is brought to you by Statusbits.com"
        return playTextAndResume(text)
    }

    private startPollingTask() {
        //log.debug "startPollingTask()"

        pollingTask()

        Random rand = new Random(now())
        def seconds = rand.nextInt(60)
        def sched = "${seconds} 0/1 * * * ?"

        //log.debug "Scheduling polling task with \"${sched}\""
        schedule(sched, pollingTask)
    }

    def apiGet(String path) {
        //log.debug "apiGet(${path})"

        if (!updateDNI()) {
            return null
        }

        state.requestTime = now()
        state.responseTime = 0

        def headers = [
            HOST:       state.hostAddress,
            Accept:     "*/*"
        ]
        
        if (state.userAuth != null) {
            headers['Authorization'] = "Basic ${state.userAuth}"
        }

        def httpRequest = [
            method:     'GET',
            path:       path,
            headers:    headers
        ]

        //log.debug "httpRequest: ${httpRequest}"
        return new hubitat.device.HubAction(httpRequest)
    }

    private def delayHubAction(ms) {
        return new hubitat.device.HubAction("delay ${ms}")
    }

    private apiGetStatus() {
        return apiGet("/requests/status.json")
    }

    private apiCommand(command, refresh = 0) {
        //log.debug "apiCommand(${command})"

        def actions = [
            apiGet("/requests/status.json?${command}")
        ]

        if (refresh) {
            actions << delayHubAction(refresh)
            actions << apiGetStatus()
        }

        return actions
    }

    private def apiGetPlaylists() {
        //log.debug "getPlaylists()"
        return apiGet("/requests/playlist.json")
    }

    private parseHttpHeaders(String headers) {
        def lines = headers.readLines()
        def status = lines.remove(0).split()

        def result = [
            protocol:   status[0],
            status:     status[1].toInteger(),
            reason:     status[2]
        ]

        return result
    }

    private def parseHttpResponse(Map data) {
        //log.debug "parseHttpResponse(${data})"

        state.updatedTime = now()
        if (!state.responseTime) {
            state.responseTime = now()
        }

        def events = []

        if (data.containsKey('state')) {
            def vlcState = data.state
            //log.debug "VLC state: ${vlcState})"
            events << createEvent(name:"status", value:vlcState)
            if (vlcState == 'stopped') {
                events << createEvent([name:'trackDescription', value:''])
            }
        }

        if (data.containsKey('volume')) {
            //log.debug "VLC volume: ${data.volume})"
            def volume = ((data.volume.toInteger() * 100) / 512) as int
            events << createEvent(name:'level', value:volume)
        }

        if (data.containsKey('information')) {
            parseTrackInfo(events, data.information)
        }

        events << createEvent([
            name:           'connection',
            value:          'connected',
            isStateChange:  true,
            displayed:      false
        ])

        //log.debug "events: ${events}"
        return events
    }

    private def parseTrackInfo(events, Map info) {
        //log.debug "parseTrackInfo(${events}, ${info})"

        if (info.containsKey('category') && info.category.containsKey('meta')) {
            def meta = info.category.meta
            //log.debug "Track info: ${meta})"
            if (meta.containsKey('filename')) {
                if (meta.filename.contains("//s3.amazonaws.com/smartapp-")) {
                    log.trace "Skipping event generation for sound file ${meta.filename}"
                    return
                }
            }

            def track = ""
            if (meta.containsKey('artist')) {
                track = "${meta.artist} - "
            }
            if (meta.containsKey('title')) {
                track += meta.title
            } else if (meta.containsKey('filename')) {
                def parts = meta.filename.tokenize('/');
                track += parts.last()
            } else {
                track += '<untitled>'
            }

            if (track != device.currentState('trackDescription')) {
                meta.station = track
                events << createEvent(name:'trackDescription', value:track, displayed:false)
               // events << createEvent(name:'trackData', value:meta.encodeAsJSON(), displayed:false)
            }
        }
    }

    private def myTextToSpeech(text) {
       
        def sound = textToSpeech(text)
        log.debug "${text}"
        log.debug "${sound.uri}"
        sound.uri = sound.uri.replace('https:', 'http:')
        return sound
    }

    private String createDNI(ipaddr, port) {
        //log.debug "createDNI(${ipaddr}, ${port})"

        def hexIp = ipaddr.tokenize('.').collect {
            String.format('%02X', it.toInteger())
        }.join()

        def hexPort = String.format('%04X', port.toInteger())
     
        return "${hexIp}:${hexPort}"
    }

    private updateDNI() {
        if (!state.dni) {
    	    log.warn "DNI is not set! Please enter IP address and port in settings."
            return false
        }
     
        if (state.dni != device.deviceNetworkId) {
    	    log.warn "Invalid DNI: ${device.deviceNetworkId}!"
            device.deviceNetworkId = state.dni
        }

        return true
    }

    private def title() {
        return "VLC Thing. Version 2.0.0 (12/22/2016). Copyright © 2014 Statusbits.com"
    }

    private def STATE() {
        log.trace "state: ${state}"
        log.trace "deviceNetworkId: ${device.deviceNetworkId}"
        log.trace "status: ${device.currentValue('status')}"
        log.trace "level: ${device.currentValue('level')}"
        log.trace "mute: ${device.currentValue('mute')}"
        log.trace "trackDescription: ${device.currentValue('trackDescription')}"
        log.trace "connection: ${device.currentValue("connection")}"
    }
3 Likes

How does one use this code? When I tried to paste it into the Apps Code, it gave me an error message:

"Cannot get property 'input' on null object on line 33"

What am I missing?