Porting VLCthing


#1

I am trying to port over VLCthing for notifications. VLCthing can do a lot more, tts and such. Not sure if that will work. I am more trying to get it to work to pass links to VLC on my computer which then I use to play sounds through Alexa. Looks like everything should be working, and I know my VLC server is working as it functions with ST. Looks like it authenticates, but nothing happens on VLC. Logs just show, no connection. I don’t have much knowledge of HTTP headers and I am thinking its something tied with them or parsing the return. Hoping that someone could glance at it and spot what I am missing and provide a little guidance. Thank you.

/**
 *  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:true, displayDuringSetup:true)
    input("confTcpPort", "number", title:"VLC TCP Port",
        required:true, 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"
    }

    tiles(scale:2) {
		multiAttributeTile(name:"mediaplayer", type:"mediaPlayer", width:6, height:4) {
			tileAttribute("device.status", key:"PRIMARY_CONTROL") {
				attributeState("stopped", label:"Stopped", defaultState:true)
				attributeState("playing", label:"Playing")
				attributeState("paused", label:"Paused",)
			}
			tileAttribute("device.status", key:"MEDIA_STATUS") {
				attributeState("stopped", label:"Stopped", action:"music Player.play", nextState:"playing")
				attributeState("playing", label:"Playing", action:"music Player.pause", nextState:"paused")
				attributeState("paused", label:"Paused", action:"music Player.play", nextState:"playing")
			}
			tileAttribute("device.status", key:"PREVIOUS_TRACK") {
				attributeState("status", action:"music Player.previousTrack", defaultState:true)
			}
			tileAttribute("device.status", key:"NEXT_TRACK") {
				attributeState("status", action:"music Player.nextTrack", defaultState:true)
			}
			tileAttribute ("device.level", key:"SLIDER_CONTROL") {
				attributeState("level", action:"music Player.setLevel")
			}
			tileAttribute ("device.mute", key:"MEDIA_MUTED") {
				attributeState("unmuted", action:"music Player.mute", nextState:"muted", defaultState:true)
				attributeState("muted", action:"music Player.unmute", nextState:"unmuted")
			}
			tileAttribute("device.trackDescription", key: "MARQUEE") {
				attributeState("trackDescription", label:"${currentValue}", defaultState:true)
			}
		}

        standardTile("status", "device.status", width:2, height:2, canChangeIcon:true) {
            state "stopped", label:'Stopped', icon:"st.Electronics.electronics16", backgroundColor:"#ffffff", action:"Music Player.play", nextState:"playing"
            state "paused", label:'Paused', icon:"st.Electronics.electronics16", backgroundColor:"#ffffff", action:"Music Player.play", nextState:"playing"
            state "playing", label:'Playing', icon:"st.Electronics.electronics16", backgroundColor:"#79b821", action:"Music Player.pause", nextState:"paused"
        }

        standardTile("refresh", "device.connection", width:2, height:2, inactiveLabel:false, decoration:"flat") {
            state "default", icon:"st.secondary.refresh", backgroundColor:"#FFFFFF", action:"refresh.refresh", defaultState:true
            state "connected", icon:"st.secondary.refresh", backgroundColor:"#44b621", action:"refresh.refresh"
            state "disconnected", icon:"st.secondary.refresh", backgroundColor:"#ea5462", action:"refresh.refresh"
        }

        standardTile("testTTS", "device.status", width:2, height:2, inactiveLabel:false, decoration:"flat") {
            state "default", label:"Test", icon:"http://statusbits.github.io/icons/vlcthing.png", action:"testTTS"
        }

        main("status")
        details(["mediaplayer", "refresh", "testTTS"])
    }

    simulator {
        status "Stoped"         : "simulator:true, state:'stopped'"
        status "Playing"        : "simulator:true, state:'playing'"
        status "Paused"         : "simulator:true, state:'paused'"
        status "Volume 0%"      : "simulator:true, volume:0"
        status "Volume 25%"     : "simulator:true, volume:127"
        status "Volume 50%"     : "simulator:true, volume:255"
        status "Volume 75%"     : "simulator:true, volume:383"
        status "Volume 100%"    : "simulator:true, volume:511"
    }
}

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) {
        //sendHubCommand(apiGetStatus())
        return apiGetStatus()
    }
}

def parse(String message) {
    log.debug "parse description = ${description}"
    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)
    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, volume = null) {
    //log.debug "playTextAndResume(${text}, ${volume})"
    def sound = myTextToSpeech(text)
    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, true)
    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")}"
}

Again with VLC Thing and Hubitat
Chirping (Piezzo Buzzer) when opening a door or window
Another hub lockup
[Release] - Speaker Central
Need something that beeps when contact sensor opens/closes
[Release] Tasmota Sonoff Hubitat Driver & Device Support
#2

I don’t think the texttospeech() function that VLCThing uses, is available on Hubitat yet.
The driver will save and function fine, but no speech would be translated and this should be an error shown in the logs when the command to speak is sent.


#3

That is correct. I’m more looking at the player function. This will also send web URLs of files to vlc to play. So dings, dongs, or even voice files that are out online. It don’t use tts for that. It just passes the URL to vlc on the computer and tells it to play. That function should work right now. Once it connects. You can use apps like bigtalker to pass the sounds.


#4

Has any progress been made on this?


#5

Unfortunately no, I’m not experienced enough with groovy. I’m not sure if its not sending information over to the vlc server, or if the vlc server just isn’t recognizing the info. The logs look like its working correctly, it can’t establish a connection though. No one jumped in to offer any advice with it though and I’m stuck for what I know.


#6

So after banging my head on the wall trying to port apps over to hubitat and not finding any solutions to get tts to work or much documentation to help along the way. (lol and lacking the required knowledge to undertake such task) I decided I had enough.

I realized we could pretty easily setup a virtual switch and react to through webcore - webcore can then send a simple local http get request to any device on your network running autoremote tasker plugin,

There is also a way to get autoremote to connect to a pi. You can send messages with commands that work in a command line. run scripts or whatever you would like. I am currently having difficulty with this though.

In the meantime I figured I would share this for those interested in a quick and dirty way to get tts working. I believe I saw somewhere someone was working a virtual http switches for hubitat. I am guessing these could do the same thing. I cant seem to find them currently though…


#7

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.


#8

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?


#9

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


#10

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


#11

@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


#12

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")}"
}

Best TTS device to use with HE
VLC Thing with multiple instances
New Apartment Setup
#13

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!


#14

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


#15

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

#16

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??


#17

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.


#18

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.


#19

What do your logs show?

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


#20

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.