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