Google Home voice options

Sorry to start another thread on the subject, but with as many of the threads as I've read on Google Home and Google Assistant, I just couldn't find what I was looking for. The annoying thing being that I know it has been asked before, but I couldn't find it.

There are so many options for making Google Home speak, but I prefer the official Google voices that are available. I have Google Assistant Relay, but the Broadcast from [username] is a non-starter for me. What other options, aside from a Google Routine, are available for TTS in the official Google voices?

I only have a handful of phrases, so creating a few Google Assistant Routines wouldn't be that big of a deal, however I would like to have it include the current time, as is possible with the built-in Chromecast Integration (beta), but not in the official Google TTS voices. Anyone know if it's possible to make a Google Assistant Routine include the current time in the phrase you enter? Is there an option here available that gives you the ability to include current time in the phrase, but do it in the Google Assistant official voice?

I believe the cast web api will allow you to use native Google voices.

1 Like

No, it doesn't. It is not supported on Hubitat. You have to use the Amazon Poly TTS voice.

CORRECTION: It appears that the google voice settings do work now within the newest version of Cast-Web-API. I currently have v1.2. I am not sure it it is also working in previous versions.

1 Like

Thanks. I was just trying to get cast web working. Saves me the trouble.

It has worked for me before in the past... Just been awhile since I tried...

1 Like

OK. I'm game to try again. How do I configure it? I get no explicit errors that I see in the node server, but it's defaulting to local machine 127.0.0.1, which as I understand it is an error state. Do I need to install in unsafe ?

I think it uses the same default port as assistant relay. You may have to change one or the other.

Are you installing on a pi? I do not remember the steps exactly.

I actually think I followed a guide written b @Ryan780 last time I set it up.

1 Like

The latest version is cast-web-api-cli. It includes its own built in PM2 process manager. I would recommend using that one. And no, 127.0.0.1 is Local Host... But as long as you are using the correct URL to reach it from Hubitat you should be fine.

Also, I believe both Cast-web and Relay use the devices Mac as the device ID. That may be a problem as well. You may need to deploy one using Docker and a custom Mac address (how you do that, I have no idea. I just know other people have.)

I tried unsafe, no change. Shut everything else down (although none of them are on port 3000, I've already changed them), but that didn't help. Just checked the log and it shows...

GoogleAssistant require error: Error: Cannot find module 'google-assistant'
cast-web-api v1.2.1

Is there a config.json file or do I have to mess with a yaml file?

Nope. All you have to do is install and run and then go to http://IP-ADDRESS:3000/assistant/setup and it will guide you through authorizing Google Assistant API in order to use Broadcasts. It will then create a parent device called cast-web-api and it will have child devices for all of your google home devices you selected within the app. I would advise using the following modified drivers. They allow for the Device Notification capability which for the Cast-web device results in a broadcast or from the individual devices results in a cast to the individual device.

The device also allows for the music player capability to play tracks of mp3 files from accessible website or online storage (github).

Cast-web-API

/**
 *  cast-web-api
 *
 *  Copyright 2018 Tobias Haerke
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 */
 preferences {
    input("configLoglevel", "enum", title: "Log level?",
        required: false, multiple:false, value: "nothing", options: ["0","1","2","3","4"])
    input("syncNamesFromAPI", "bool", title: "Don't sync cast-device names from API?", required: false)
}
 
metadata {
    definition (name: "cast-web-api", namespace: "vervallsweg", author: "Tobias Haerke") {
        capability "Actuator"
        capability "Audio Notification"
        capability "Refresh"
        capability "Speech Synthesis"
		capability "Notification"
        
        command "checkAssistant"
        command "checkVersion"
        command "setApiHost", ["string"]
		command "delayedNotification", ["string","number"]
    }

    simulator {
        // TODO: define status and reply messages here
    }


}

String mphrase = 'phrase'

// parse events into attributes
def parse(String description) {
    //er('debug', "Parsing: "+description)
    def message = parseLanMessage(description)
    def children = getChildDevices()
    logger('debug', "Parsing json: "+message.json + ", status"+message.status)
    
    if(message.json) {
        if(message.json.id) {
            def foundTarget = false;
            children.each { child ->
                if ( child.deviceNetworkId == message.json.id ){
                    foundTarget = true;
                    logger('debug', "Parsing found target name: "+child.displayName+", dni: "+child.deviceNetworkId)
                    if(message.json.name) {
                        if(settings.syncNamesFromAPI != true) {
                            child.displayName = message.json.name
                        }
                    }
                    //if(message.json.connection) { //WTF: cannot be set by dev?!
                    //    if( message.json.connection.equals("connected") ) {
                    //      child.status = "ONLINE"
                    //    }
                    //    if( message.json.connection.equals("disconnected") ) {
                    //      child.status = "OFFLINE"
                    //    }
                    //}
                    child.parse(description)
                }
            }
            if(!foundTarget) {
                logger('error', "Parsing found no target for: "+message.json.id)
            }
        }
        if(message.json.api) {
            if(message.json.api.version) {
                if (message.json.api.version.this && message.json.api.version.latest) {
                    sendEvent(name: "updateStatus", value: ("Current: "+ message.json.api.version.this + "\nLatest: " + message.json.api.version.latest), displayed: false)
                }
            }
        }
        if(message.json.containsKey("assistant") && message.json.containsKey("ready")) {
            logger('debug', "containsKey")
            sendEvent(name: "assistantStatus", value: ("Assistant: "+ message.json.assistant + "\nready: " + message.json.ready + "\nIf you never used Google Assistant with cast-web go to service manager > Setup Google Assistant"), displayed: false)   
        }
    }
}

def installed() {
    installDevices( parseListFromString( getDataValue("devices") ) )
    sendEvent(name: "updateStatus", value: "Click to check for updates", displayed: false)
}

def updated() {
    installDevices( parseListFromString( getDataValue("devices") ) )
}

def refresh() {
    unschedule(refreshAll)
    refreshAll()
    runEvery5Minutes(refreshAll)
}

def refreshAll() {
    def hub = location.hubs[0];
    //sendHttpRequest("GET", getDataValue("apiHost"), "/device", '');
    sendHttpRequest("POST", getDataValue("apiHost"), "/callback", '[{"url":"'+hub.localIP+':'+hub.localSrvPortTCP+'", "settings":""}]');
    getChildDevices().each{child -> child.refresh()}
}

def installDevices(ids) {
    def devicesToCreate = []
    def children = getChildDevices()
    logger('debug', "installDevices() children: " + children.size() + ", ids: " + ids)
    
    ids.each { id ->
        
        def exists = false
        
        children.each { child ->
            if( id.equals( child.deviceNetworkId ) ) { //id has a nbsp added to the front
                exists = true
            }
        }
        
        if (exists) {
            logger('debug', "installDevices() id: "+id+", exists!")
        } else {
            logger('warn', "installDevices() id: "+id+", doesn't exist")
            devicesToCreate.add(id)
            logger('debug', "installDevices() devicesToCreate: "+devicesToCreate)
        }
    }
    
    createDevices(devicesToCreate)
}

def createDevices(ids) {
    logger('debug', "createDevices() ids: " + ids)
    
    ids.each {
        if(it!=null&&it!="") {
            logger('debug', "createDevices() adding id: " + it)
            addChildDevice("vervallsweg", "cast-web-device", it, ["apiHost": getDataValue("apiHost")])
		}
    }
}

def parseListFromString(string) {
    string = string.replace("[", "").replace("]", "").replace(" ", "")
    def list = string.split(',').collect{it as String}
    logger('debug', 'parseListFromString(), string: '+string+', list: ' + list)
    return list
}

def checkVersion() {
    sendHttpRequest("GET", getDataValue("apiHost"), "/config", "");
    //sendHttpTest(getDataValue("apiHost"), "/config")
}

def checkAssistant() {
    sendHttpRequest("GET", getDataValue("apiHost"), "/assistant", "");
}

def speak(phrase) {
    logger('info', "speak(), phrase: " + phrase)
    sendHttpRequest("POST", getDataValue("apiHost"), "/assistant/broadcast/", '{"message":"'+phrase+'"}');
}

def playText(message, level = 0, resume = false) {
    logger('info', "playText, message: " + message + " level: " + level)
    return speak(message)
}

def playTextAndResume(message, level = 0, thirdValue = 0) {
    logger('info', "playTextAndResume, message: " + message + " level: " + level)
    return speak(message)
}

def playTextAndRestore(message, level = 0, thirdValue = 0) {
    logger('info', "playTextAndRestore, message: " + message + " level: " + level)
    return speak(message)
}

def setApiHost(apiHost) {
    log.warn "apiHost: "+apiHost
    logger('info', 'setApiHost(), to: '+apiHost)
    updateDataValue("apiHost", apiHost)
    
    def children = getChildDevices()
    children.each { child ->
        child.updateDataValue("apiHost", apiHost)
    }
}

def sendHttpRequest(String method, String host, String path, def body="") {
    logger('debug', "Executing 'sendHttpRequest' method: "+method+" host: "+host+" path: "+path+" body: "+body)
    def bodyHead = "";
    if(!body.equals("")) {
        bodyHead = "Content-Type: application/json\r\nContent-Length:${body.length()+1}\r\n";
        body = " "+body;
    }
    sendHubCommand(new hubitat.device.HubAction("""${method} ${path} HTTP/1.1\r\nHOST: $host\r\n${bodyHead}\r\n${body}""", hubitat.device.Protocol.LAN, host))
}

//DEBUGGING
def logger(level, message) {
    def logLevel=1
    if(settings.configLoglevel) {
        logLevel = settings.configLoglevel.toInteger() ?: 0
    }
    if(level=="error"&&logLevel>0) {
        log.error message
    }
    if(level=="warn"&&logLevel>1) {
        log.warn message
    }
    if(level=="info"&&logLevel>2) {
        log.info message
    }
    if(level=="debug"&&logLevel>3) {
        log.debug message
    }
}

def deviceNotification(phrase){
	speak(phrase)
}

def delayedNotification(phrase,delay){
	delay = (delay.toInteger())*60
	mphrase = phrase
	runIn (delay, delayedSpeak)
}

def delayedSpeak(){
	speak(mphrase)
}

Cast-web-Device

/**
 *  cast-web-device
 *
 *  Copyright 2017 Tobias Haerke
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 */
import org.json.JSONObject

preferences {
    input("configOn", "enum", title: "Switch on does?",
        required: false, multiple:false, value: "nothing", options: ["Play","Pause","Stop","Play preset 1","Play preset 2","Play preset 3","Play preset 4","Play preset 5","Play preset 6"])
    input("configOff", "enum", title: "Switch off does?",
        required: false, multiple:false, value: "nothing", options: ["Play","Pause","Stop","Play preset 1","Play preset 2","Play preset 3","Play preset 4","Play preset 5","Play preset 6"])
    input("configNext", "enum", title: "Next song does?",
        required: false, multiple:false, value: "nothing", options: ["Play","Pause","Stop","Next preset","Previous preset","Play preset 1","Play preset 2","Play preset 3","Play preset 4","Play preset 5","Play preset 6"])
    input("configPrev", "enum", title: "Previous song does?",
        required: false, multiple:false, value: "nothing", options: ["Play","Pause","Stop","Next preset","Previous preset","Play preset 1","Play preset 2","Play preset 3","Play preset 4","Play preset 5","Play preset 6"])
    input("configResume", "enum", title: "Resume/restore (if nothing was playing before) plays preset?",
        required: false, multiple:false, value: "nothing", options: ["1","2","3","4","5","6"])
    input("configLoglevel", "enum", title: "Log level?",
        required: false, multiple:false, value: "0", options: ["0","1","2","3","4"])
    input("halt", "bool", title: "Stop after TTS?", required: false)
    input("googleTTS", "bool", title: "Use Google's TTS voice?", required: false)
    input("googleTTSLanguage", "enum", title: "Google TTS language?",
        required: false, multiple:false, value: "nothing", options: ["cs-CZ","da-DK","de-DE","en-AU","en-CA","en-GH","en-GB","en-IN","en-IE","en-KE","en-NZ","en-NG","en-PH","en-ZA","en-TZ","en-US","es-AR","es-BO","es-CL","es-CO","es-CR","es-EC","es-SV","es-ES","es-US","es-GT","es-HN","es-MX","es-PA","es-PY","es-PE","es-PR","es-DO","es-UY","es-VE","eu-ES","fr-CA","fr-FR","it-IT","lt-LT","hu-HU","nl-NL","nb-NO","pl-P","pt-BR","pt-PT","ro-RO","sk-SK","sl-SI","fi-FI","sv-SE","ta-IN","vi-VN","tr-TR","el-GR","bg-BG","ru-RU","sr-RS","he-IL","ar-AE","fa-IR","hi-IN","th-TH","ko-KR","cmn-Hant-TW","yue-Hant-HK","ja-JP","cmn-Hans-HK","cmn-Hans-CN"])
}
 
metadata {
    definition (name: "cast-web-device", namespace: "vervallsweg", author: "Tobias Haerke") {
        capability "Actuator"
        capability "Audio Notification"
        capability "Music Player"
        capability "Polling"
        capability "Refresh"
        capability "Speech Synthesis"
        capability "Switch"
		capability "Notification"
        capability "Switch Level"
        //capability "Health Check" //TODO: Implement health check

        command "checkForUpdate"
    }

    simulator {
        // TODO: define status and reply messages here
    }
}

// Device handler states
def installed() {
    logger('debug', "Executing 'installed'")
    log.debug "installed"
    
    //Preset, update-status tiles
    refresh()
    sendEvent(name: "updateStatus", value: ("Version "+getThisVersion() + "\nClick to check for updates"), displayed: false)
    parsePresets()
    refresh() //If callback exists already
}

def updated() {
    logger('debug', "Executing 'updated'")
    log.debug "updated"
    
    //Preset, update-status tiles
    refresh()
    sendEvent(name: "updateStatus", value: ("Version "+getThisVersion() + "\nClick to check for updates"), displayed: false)
    parsePresets()
}

def refresh() {
    apiCall('/', true)
}

// parse events into attributes
def parse(String description) {
    try {
        logger('debug', "'parse', parsing: '${description}'")
        def msg = parseLanMessage(description)
        logger('debug', 'parse, msg.json: ' + msg.json)
        
        if(msg.json!=null){
            if(!msg.json.response) {
                if(msg.json.status) {
                    setTrackData(msg.json.status)
                    generateTrackDescription()
                }
                if(msg.json.connection) {
                    logger('debug', "msg.json.connection: "+msg.json.connection)
                    sendEvent(name: "connection", value: msg.json.connection, displayed:false)
                }
            } else if( !msg.json.response.equals('ok') ) {
                logger('error', "json response not ok: " + msg.json)
            }
        }
    } catch (e) {
        logger('error', "Exception caught while parsing data: "+e) //TODO: Health check
    }
}

// handle commands
def play() {
    logger('debug', "Executing 'play'")
    apiCall('/play', true);
}

def pause() {
    logger('debug', "Executing 'pause'")
    apiCall('/pause', true);
}

def stop() {
    logger('debug', "Executing 'stop'")
    apiCall('/stop', true);
}

def nextTrack() {
    logger('debug', "Executing 'nextTrack' encode: ")
    selectableAction(settings.configNext)
}

def previousTrack() {
    logger('debug', "Executing 'previousTrack'")
    selectableAction(settings.configPrev)
}

def setLevel(level,duration=null) {
    logger('debug', "Executing 'setLevel', level: " + level)
    double lvl
    try { lvl = (double) level; } catch (e) {
        lvl = Double.parseDouble(level)
    }
    if( lvl == device.currentValue("level") ){
        logger('debug', "Setting group level: " + level)
        apiCall('/volume/'+lvl+'/group', true)
        return
    }
    apiCall('/volume/'+lvl, true)
	runIn(2, refresh)
}

def mute() {
    logger('debug', "Executing 'mute'")
    apiCall('/muted/true', true)
}

def unmute() {
    logger('debug', "Executing 'unmute'")
    apiCall('/muted/false', true)
}

def setTrack(trackToSet) {
    logger('debug', "Executing 'setTrack'")
    return playTrack(trackToSet)
}

def resumeTrack(trackToSet) {
    logger('debug', "Executing 'resumeTrack'")
    return playTrack(trackToSet)
}

def restoreTrack(trackToSet) {
    logger('debug', "Executing 'restoreTrack'")
    return playTrack(trackToSet)
}

def on() {
    logger('debug', "Executing 'on'")
    selectableAction(settings.configOn)
}

def off() {
    logger('debug', "Executing 'off'")
    selectableAction(settings.configOff ?: 'Stop')
}


def speak(phrase, resume = false) {
    if(settings.googleTTS && settings.googleTTSLanguage){
        if(settings.googleTTS==true) {
            return playTrack( phrase, 0, 0, true, settings.googleTTSLanguage )
        }
    }
    //return playTrack( textToSpeech(phrase, true).uri, 0, 0, true )
	return playTrack( textToSpeech(phrase.replaceAll("%20"," ")).uri, 0, 0, true )
}
//AUDIO NOTIFICATION, TEXT
def playText(message, level, resume = false) {
    logger('info', "playText, message: " + message + " level: " + level)
    
    if (level!=0&&level!=null) { setLevel(level) }
    return speak(message, true)
}

def playTextAndResume(message, level = 0, thirdValue = 0) {
    logger('info', "playTextAndResume, message: " + message + " level: " + level)
    playText(message, level, true)
}

def playTextAndRestore(phrase, volume) {
    logger('info', "playTextAndRestore, message: " + message + " level: " + level)
    def oldlevel = level
	setLevel(volume)
	//TODO: Reset level to level before the message was played
	runIn (10,setLevel (oldlevel))
	return playTrack( textToSpeech(phrase.replaceAll("%20"," ")).uri, 0, 0, true )
	
}

def playTrackAtVolume(trackToPlay, level = 0) {
    logger('info', "playTrackAtVolume" + trackToPlay)
    
    def url = "" + trackToPlay;
    return playTrack(url, level)
}
//AUDIO NOTIFICATION, TRACK
def playTrack(uri, level = 0, thirdValue = 0, resume = false, googleTTS = false) {
	if (halt){
		runIn(20,stop)
	}
	logger('info', "Executing 'playTrack', uri: " + uri + " level: " + level + " resume: " + resume)

    if (level!=0&&level!=null) { setLevel(level) }
    
    def data = '{ "mediaType":"audio/mp3", "mediaUrl":"'+uri+'", "mediaStreamType":"BUFFERED", "mediaTitle":"Hubitat", "mediaSubtitle":"Hubitat playback", "mediaImageUrl":"https://raw.githubusercontent.com/ryancasler/Hubitat_Ryan/master/Icons/Icon4.png"}'
    if(googleTTS) {
        data = '{ "mediaType":"audio/mp3", "mediaUrl":"", "mediaStreamType":"BUFFERED", "mediaTitle":"'+uri+'", "mediaSubtitle":"Hubitat notification", "mediaImageUrl":"https://raw.githubusercontent.com/ryancasler/Hubitat_Ryan/master/Icons/Icon4.png", "googleTTS":"'+googleTTS+'"}'
    }
    if(resume) {
        def number = 0
        JSONObject preset = null
        if(settings.configResume) { number = settings.configResume }
        if(getTrackData(['preset'])[0]) { number = getTrackData(['preset'])[0] }
        log.warn 'number: '+number
        if(number > 0) {
            preset = getPresetObject(number)
        }
    
        if (preset) {
            preset.each {
                data = data + ', '+it.toString()
            }
            //data = data + ', '+preset.toString()
        }
    }
    data = "["+data+"]"
    log.warn 'playTrack() data: '+data
    return setMediaPlaybacks(data)
}

def playTrackAndResume(uri, level = 0) {
    logger('info', "Executing 'playTrackAndResume', uri: " + uri + " level: " + level)
    return playTrack(uri, level, 0, true)
}


def playTrackAndRestore(uri, level = 0) {
    logger('info', "Executing 'playTrackAndRestore', uri: " + uri + " level: " + level)
    //TODO: Reset level to level before the track was played
    return playTrack(uri, level, 0, true)
}

def playTrackAndRestore(String uri, String duration, level = 0) {
    logger('info', "Executing 'playTrackAndRestore', uri: " + uri + " duration: " + duration + " level: " + level)
    //TODO: Reset level to level before the track was played
    return playTrack(uri, level, 0, true)
}

def generateTrackDescription() {
    def trackDescription = getTrackData(["title"])[0] + "\n" + getTrackData(["application"])[0] + "\n" + removePresetMediaSubtitle(getTrackData(["subtitle"])[0])
    
    logger('debug', "Executing 'generateTrackDescription', trackDescription: "+ trackDescription)
    sendEvent(name: "trackDescription", value: trackDescription, displayed:false)
}

def setTrackData(newTrackData) {
    JSONObject currentTrackData = new JSONObject( device.currentValue("trackData") ?: "{}" )
    logger('debug', "setTrackData() currentTrackData: "+currentTrackData+", newTrackData: "+newTrackData)
    def changed = false
    
    newTrackData.each { key, value ->
        if(key=='connection'||key=='volume'||key=='muted'||key=='application'||key=='status'||key=='title'||key=='subtitle'||key=='image'||key=='preset'||key=='groupPlayback') {
            if(currentTrackData.has(key)) {
                if(currentTrackData.get(key)==value) { return }
            }
            currentTrackData.put(key, value); changed=true;
            if(currentTrackData.has('volume')) {
                sendEvent(name: "level", value: currentTrackData.get('volume'), unit: "%", changed: true)
            }
            if(currentTrackData.has('muted')) {
                if(currentTrackData.get('muted')) {
                    sendEvent(name: "mute", value: "muted", changed: true)
                } else {
                    sendEvent(name: "mute", value: "unmuted", changed: true)
                }
            }
            if(currentTrackData.has('status')) {
                if( currentTrackData.get('status').equals("PLAYING") || currentTrackData.get('status').equals("PAUSED") ) {
                    if( currentTrackData.has('groupPlayback') ) {
                        if( currentTrackData.get('groupPlayback') ) {
                            sendEvent(name: "status", value: 'group', changed: true)
                            sendEvent(name: "switch", value: on, displayed: false)
                        } else {
                            sendEvent(name: "status", value: currentTrackData.get('status').toLowerCase(), changed: true)
                            sendEvent(name: "switch", value: on, displayed: false)
                        }
                    } else {
                        sendEvent(name: "status", value: currentTrackData.get('status').toLowerCase(), changed: true)
                        sendEvent(name: "switch", value: on, displayed: false)
                    }
                } else if( currentTrackData.get('application').equals("") || currentTrackData.get('application').equals("Backdrop") ) {
                    sendEvent(name: "status", value: "ready", changed: true)
                    sendEvent(name: "switch", value: off, displayed: false)
                }
            }
            if(currentTrackData.has('preset')) {
                logger( 'debug', "setTrackData() sendEvent presetName playing for: "+ currentTrackData.get('preset') )
                sendEvent(name: "preset"+currentTrackData.get('preset')+"Name", value: "Playing", displayed: false, changed: true)
                parsePresets( currentTrackData.get('preset') )
            }
        }
    }
    
    if(changed){
        logger('debug', "sendEvent trackdata, currentTrackData: "+currentTrackData)
        sendEvent(name: "trackData", value: currentTrackData, displayed:false)
    }
}

def getTrackData(keys) {
    def returnValues = []
    logger('debug', "getTrackData, keys: "+keys)
    JSONObject trackData = new JSONObject( device.currentValue("trackData") ?: "{}" )
    
    keys.each {
        def defaultValue = null
        if( it.equals('title') || it.equals('subtitle') ) { defaultValue="--" }
        if( it.equals('application') ) { defaultValue="Ready to cast" }
        
        returnValues.add( trackData.optString(it, defaultValue) ?: defaultValue )
    }
    
    return returnValues
}

def removeTrackData(keys) {
    JSONObject trackData = new JSONObject( device.currentValue("trackData") ?: "{}" )
    keys.each{
        if( trackData.has( it ) ) {
            if( it.equals('preset') ) { 
                logger('debug', 'removeTrackData, resetPresetName for: ' + getTrackData(['preset'])[0])
                parsePresets()
            }
            logger('debug', "removeTrackData, removing key: "+it+", value: "+trackData.get(it))
            trackData.remove(it)
        }
    }
    sendEvent(name: "trackData", value: trackData, displayed:false)
}

// GOOGLE CAST
def setMediaPlayback(mediaType, mediaUrl, mediaStreamType, mediaTitle, mediaSubtitle, mediaImageUrl) {
    apiCall("/playMedia", true, '[ { "contentType":"'+mediaType+'", "mediaUrl":"'+mediaUrl+'", "mediaStreamType":"'+mediaStreamType+'", "mediaTitle":"'+mediaTitle+'", "mediaSubtitle":"'+mediaSubtitle+'", "mediaImageUrl":"'+mediaImageUrl+'" } ]')
}

def setMediaPlaybacks(def data) {
    apiCall("/playMedia", true, data)
}

// NETWORKING STUFF
def apiCall(String path, def dev, def media=null) {
    if (dev) {
        path = '/device/' + device.deviceNetworkId + path
    }
    if ( path.contains('subscribe') ) {
        def hub = location.hubs[0]
        path = path + '/' + hub.localIP + ':' + hub.localSrvPortTCP
    }
    if (media) {
        sendHttpPost(getDataValue('apiHost'), path, media)
        return
    }
    sendHttpRequest(getDataValue('apiHost'), path)  
}

def sendHttpRequest(String host, String path, def defaultCallback=hubResponseReceived) {
    logger('debug', "Executing 'sendHttpRequest' host: "+host+" path: "+path)
    sendHubCommand(new hubitat.device.HubAction("""GET ${path} HTTP/1.1\r\nHOST: $host\r\n\r\n""", hubitat.device.Protocol.LAN, host, [callback: defaultCallback]))
}

def sendHttpPost(String host, String path, def data) {
    logger('debug', "Executing 'sendHttpPost' host: "+host+" path: "+path+" data: "+data+" data.length():"+data.length()+1)
    def ha = new hubitat.device.HubAction("""POST ${path} HTTP/1.1\r\nHOST: $host\r\nContent-Type: application/json\r\nContent-Length:${data.length()+1}\r\n\r\n ${data}""", hubitat.device.Protocol.LAN, host, [callback: hubResponseReceived])
    logger('debug', "HubAction: "+ha)
    sendHubCommand(ha)
}

void hubResponseReceived(hubitat.device.HubResponse hubResponse) {
    parse(hubResponse.description)
}

// HELPERS
def getTimeStamp() {
    Date now = new Date(); 
    def timeStamp = (long)(now.getTime()/1000)
    logger('info', "Timestamp generated: "+timeStamp)
    return timeStamp;
}

def urlEncode(String) {
    return java.net.URLEncoder.encode(String, "UTF-8")
}

def selectableAction(action) {
    if( action.equals("Play") ) { play() }
    if( action.equals("Pause") ) { pause() }
    if( action.equals("Stop") ) { stop() }
    if( action.equals("Play preset 1") ) { playPreset(1) }
    if( action.equals("Play preset 2") ) { playPreset(2) }
    if( action.equals("Play preset 3") ) { playPreset(3) }
    if( action.equals("Play preset 4") ) { playPreset(4) }
    if( action.equals("Play preset 5") ) { playPreset(5) }
    if( action.equals("Play preset 6") ) { playPreset(6) }
    if( action.equals("Next preset") ) { nextPreset() }
    if( action.equals("Previous preset") ) { previousPreset() }
}

//UPDATE
def getThisVersion() {
    return "1.0.0"
}

def getLatestVersion() {
    try {
        httpGet([uri: "https://raw.githubusercontent.com/vervallsweg/smartthings/master/devicetypes/vervallsweg/cast-web.src/version.json"]) { resp ->
            logger('debug', "response status: ${resp.status}")
            String data = "${resp.getData()}"
            logger('debug', "data: ${data}")
            
            if(resp.status==200 && data!=null) {
                return parseJson(data)
            } else {
                return null
            }
        }
    } catch (e) {
        logger('error', "something went wrong: $e")
        return null
    }
}

def checkForUpdate() {
    def latestVersion = getLatestVersion()
    if (latestVersion == null) {
        logger('error', "Couldn't check for new version, thisVersion: " + getThisVersion())
        sendEvent(name: "updateStatus", value: ("Version "+getThisVersion() + "\n Error getting latest version \n"), displayed: false)
        return null
    } else {
        logger('info', "checkForUpdate thisVersion: " + getThisVersion() + ", latestVersion: " + getLatestVersion().version)
        sendEvent(name: "updateStatus", value: ("Current: "+getThisVersion() + "\nLatest: " + getLatestVersion().version), displayed: false)
    }
}

//DEBUGGING
def logger(level, message) {
    def logLevel=1
    if(settings.configLoglevel) {
        logLevel = settings.configLoglevel.toInteger() ?: 0
    }
    if(level=="error"&&logLevel>0) {
        log.error message
    }
    if(level=="warn"&&logLevel>1) {
        log.warn message
    }
    if(level=="info"&&logLevel>2) {
        log.info message
    }
    if(level=="debug"&&logLevel>3) {
        log.debug message
    }
}

There is an issue with the STOP command that I've reported to vervallsweg (the author).

Thanks. I'm stuck at just trying to get the damn node instance to run. I'll have to address this later. Short on patience right now. Appreciate the pointers.

Make sure you are using v8 of NodeJS. Everything else won't work. If you are looking for an easy way to modify your node version on your RPi, try this:

I'm running on MacOS 10.13.6 with Node 8.11.3

I have a pi. Maybe I'll try that later.

OK, I get it. Gives errors, but still it's running. Weird

I already have Google Assistant Relay setup and running. Do I have to do this again for Cast web?

Part of it yet. You don't have to set up a different project in Google Console but you will have to access the console to get your ID and secret and then authorize CAST-web-API. You should be able to just copy/paste everything you've already created in Google Console into the setup page in Cast-Web.

1 Like

I would do this.

sudo npm install google-assistant -g --unsafe-perm

sudo npm install cast-web-api-cli -g --unsafe-perm

If you are using the regular version, it would be:

sudo npm install cast-web-api -g --unsafe-perm

You have to run them both in that order. I recommend the CLI version as it has PM2 built in and the commands are much easier.

Yes tried that. It actually seems to be working, but doesn't show the Mac IP. Instead shows local machine. Yet I can get to the setup page. Just looking for my client ID and Secret from the Assistant Relay setup. It's been so long. Forgot where to get those from.

console.cloud.google.com