Porting VLCthing

Have no clue why, but out of no where VLCThing lives. It started passing URLs to the VLCserver. So now when a contact opens, it sends a link to my server and it plays the link through VLC. Tts don’t work, wasn’t expecting it too. But now I have sound for events.

1 Like

Hmm seems vlcthing still fails on tts even with new update. Does anyone have any suggestions on changes to the code to be compatible with hubitat?

and vlc speaks.... Hubitat Dave may take some getting used to....lol

Is your ported VLC Thing App code posted somewhere? I’d like to take a look at it. Thanks.

1 Like

@cwwilson08 I would as well.
I have a version that I ported a while back that isn't working with the new textToSpeech(phrase) function added. If yours is working, I would like to look at the code if you do not mind.

I'm currently getting the following error when calling playTrackAndRestore() for a VLCThing device.

No signature of method: java.lang.String.call() is applicable for argument types: (java.lang.String, java.lang.String, java.lang.Long, java.util.LinkedHashMap) values: [http://10.x.x.x:8080/tts/7d3c8f5e7e5494bea7aa3b9987ef7f42.mp3, ...] Possible solutions: wait(), any(), trim(), dump(), split(), grep() on line null

I do not remember doing anything wild to the code. (if anything at all)

I can say lots of things do not work. I have not tried play track and restore.

Basic play tts works.

/**
 *  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())
    //log.debug "body: ${body}"
    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 = 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 "speak(${text})"
    def sound = myTextToSpeech(text)
    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) / 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")}"
}
1 Like

Ahh. Your version is based on VLCThing 2.0.0 (12/22/2016). Mine was based on 1.2.2 (8/25/2015).
I imported the code that you posted. Working now, Thanks!

1 Like

FYI. I commented out a line tonight that was giving an error. It looked like it was something important.

Just (hopefully) fixed an issue with poll/refresh on line 320 in the device code.

Modified:

def elapsed = (now() - state.lastPoll) / 1000

To:

def elapsed = (now() - (state?.lastPoll ? state.lastPoll : (now()-301))) / 1000
1 Like

Thank you! As I have no idea what Im doing it will take a group effort if we want a fully working port

I am curious if a there is a port of bigtalker working??

BigTalker2 works in speechSynthesis mode (LanNouncer, etc) with a few modifications from the ST app.
Hubitat just added the textToSpeech function needed for BigTalker to work in musicPlayer mode.
I have it in my environment to work with.
I use VLCThing to test musicPlayer mode with BigTalker as that's the only musicPlayer "device" I have.

Getting VLCThing working (Thanks for your help with that) along with the textToSpeech function were both steps that I needed to get resolved so that I could further develop/test BigTalker2 for Hubitat release. All that to say..... It's in the works, now.

1 Like

Thanks for pointing me here. Now, how do I get this to work? :stuck_out_tongue_winking_eye:

Followed the ST thread (a bit. Hard to follow TBH). Read the instructions on the Github Repo, but I'm not getting output. I've misconfigured clearly, and this is my first experience with VLC Thing. I setup the HTTP input and password in VLC player as instructed. Varies a bit in the latest version, but I assume it's still usable with VLC Thing.

I set the IP of my laptop I'm on right now, for testing. No firewall enabled and on. I entered Port 80 (also tried 8080). Entered the password I set in VLC player HTTP settings.

I set the volume in the VLC driver in Hubitat (I've tried 10, 100 and 50), but I'm not cetain what the range it. I confirmed that I can hear audio file in VLC player. When I click on Test TTS, I don't hear anything. When I click on "Speak" and enter Hello for the string, I don't hear anything.

What am I not doing or have configured wrong? No rush. This is a side project.

What do your logs show?

if you go to http://localhost:8080 on the hp running vlc what do you see?

OK. I got it working. Thanks for pointing me in the right direction. What happens to those .mp3 files that are generated? Can I set them to purge automatically?

Also, can I create customer commands in RM to play sounds? Would love for have it play a tone and then announce the door that is open. I get the tone from my Alarm system now, but this is the same lame sounding tone, day after day and I can't set it not to sound in the middle of the night.

I can get a track to play with TTS in a rule. It's nice to have a tone go with "door (x) open" and "laundry machine (x) is finished", but I can't seem to get any of the choices in RM to play only the track I specify. If I have two tracks in my VLC player media library, both are played, even though I specify a specific track in RM.

If I ask for TTS only, then that works, I won't get the track, but if I have more than one track in VLC Player, it seems to be an all or nothing deal. Any ideas how to resolve this?

[Edit] Plays the MP3 no matter whether to not it is selected in the Rule. If it's in VLC Player media library, it plays, even if the RM rule does not have it selected. :rat: :rat: !

I've been using Big Talker with it. Set up commands based on your device. So far, it's been working really good. Go through the steps, pick vlcthing as your audio device. Been really happy

@rayzurbock Hasn't released for Hubitat yet. Are you using it on ST?

No ported over an older version. Seems to work good.

Original BigTalker author here. Good job porting if it’s working for you.
I have a 2.0 port in beta that I was ready to release until it hit a brick wall with the Sonos driver. Hopefully that will be addressed. I’m contemplating releasing without Sonos support until (and if) the driver supports the needed commands.

What speaker drivers are you using? Just VlcThing?

Yes, I just use Vlcthing. Bluetooth that over to my Alexa. I can't say that Big Talker is fully working. But it seams to be for what I am doing with it for now. It won't let me post the code, to many lines. But I included some ss of it. Been using Big Talker for about a year between here and st. Great job on it and thank you for your work.

1 Like