[RELEASE] cast-web v1.2.1 - Chromecast Device Handler & SmartApp

How did you configure the web API?

Have a look at 1456 in this thread.

1 Like

It turns out that I had a dual NAT issue and the ST hub didn’t see the API on a different router. All is working now after I moved the networks around… however, the problem im having here is the API goes offline every 1 or 2 days. I have the GUI version running autostart but I still have to come by to manually restart it all the time. The other issue I am having is for some weird reason, randomly the Google mini makes ding noises just like it does right before it speaks but nothing happens afterwards. I think it is all related and this has something to do with network hiccups and momentary dropped connections.

So how do I get Windows version 1.2.1 GUI to restart itself on dropped connections? Autostart just does not work and I just have to manually come by to clear the error message and click start ?

Thanks

I managed to edit the smartapp code below to allow for me to input the API Host Address; however, something is still not working as it doesn’t show connect and can’t discover devices.

/**
 *  Cast web - service manager
 *
 *  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.
 *
 */
definition(
    name: "Cast web - service manager",
    namespace: "vervallsweg",
    author: "Tobias Haerke",
    description: "Connect your Cast devices through the Cast web API to SmartThings.",
    category: "SmartThings Labs",
    iconUrl: "https://github.com/vervallsweg/smartthings/raw/master/icn/ic_cast_grey_24dp.png",
    iconX2Url: "https://github.com/vervallsweg/smartthings/raw/master/icn/ic_cast_grey_24dp.png",
    iconX3Url: "https://github.com/vervallsweg/smartthings/raw/master/icn/ic_cast_grey_24dp.png") {
    appSetting "api"
}


preferences {
    page(name: "mainPage")
    page(name: "apiHostAddressPage")
    page(name: "checkApiConnectionPage")
    page(name: "discoveryPage")
    page(name: "addDevicesPage")
    page(name: "configureDevicePage")
    page(name: "saveDeviceConfigurationPage")
    page(name: "updateServiceManagerPage")
}

def mainPage() {
    if(state.latestHttpResponse){state.latestHttpResponse = null;}
    dynamicPage(name: "mainPage", title: "Manage your Cast devices", nextPage: null, uninstall: true, install: true) {
        section("Configure web API"){
            href "apiHostAddressPage", title: "API host address", description:""
            href "updateServiceManagerPage", title: "Check for updates", description:""
            href "checkApiConnectionPage", title: "Test API connection", description:""
            href "setupGoogleAssistant",title: "Setup the Google Assistant with cast-web to broadcast messages", required: false, style: "external", url: "http://"+apiHostAddress+"/assistant/setup/", description: ""
        }
        section("Configure Cast devices"){
            input(name: "settingsLogLevel", type: "enum", title: "Service manager log level", options: [0, 1, 2, 3, 4])
            href "discoveryPage", title:"Discover Devices", description:""//, params: [pbutton: i]
        }
        section("Installed Devices"){
            def dMap = [:]
            getChildDevices().sort({ a, b -> a["label"] <=> b["label"] }).each {
                it.getChildDevices().sort({ a, b -> a["label"] <=> b["label"] }).each {
                    logger('debug', "mainPage(), it.label: "+it.label+", it.deviceNetworkId: "+it.deviceNetworkId)
                    href "configureDevicePage", title:"$it.label", description:"", params: [dni: it.deviceNetworkId]
                }
            }
        }
    }
}

def apiHostAddressPage() {
    dynamicPage(name:"apiHostAddressPage", title:"Configure Api", nextPage: "mainPage", refreshInterval:10) {
        logger('debug', "apiHostAddressPage(), refresh")
        
        section("Please input API.") {
            input(name: "apiHostAddress", "string", title: "API host address", required:true)
        }
    }
}

def checkApiConnectionPage() {
    dynamicPage(name:"checkApiConnectionPage", title:"Test API connection", nextPage: "mainPage", refreshInterval:10) {
        getDevices() //TODO: get root and only check status
        logger('debug', "checkApiConnectionPage(), refresh")
        
        section("Please wait for the API to answer, this might take a couple of seconds.") {
            if(state.latestHttpResponse) {
                if(state.latestHttpResponse==200) {
                    paragraph "Connected \nOK: 200"
                } else {
                    paragraph "Connection error \nHTTP response code: " + state.latestHttpResponse
                }
            }
        }
    }
}

def discoveryPage() {
    dynamicPage(name:"discoveryPage", title:"Discovery Started!", nextPage: "addDevicesPage", refreshInterval:10) {
        getDevices()
        logger('debug', "discoveryPage(), refresh")
        
        section("Please wait while we discover your Cast devices. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
            if(state.devicesMap!=null && state.devicesMap.size()>0) {
                input "selectedDevices", "enum", required:false, title:"Select Cast Device ("+ state.devicesMap.size() +" found)", multiple:true, options: state.devicesMap
                //state.selectedDevicesMap = state.devicesMap
            } else {
                input "selectedDevices", "enum", required:false, title:"Select Cast Device (0 found)", multiple:true, options: [:]
                //state.selectedDevicesMap = null
            }
        }
        
        //state.latestDeviceMap = null
    }
}

def addDevicesPage() {
    def addedDevices = addDevices(selectedDevices)
    
    dynamicPage(name:"addDevicesPage", title:"Done", nextPage: null, uninstall: false, install: true) {
        section("Devices added") {
            if( !addedDevices.equals("0") ) {
                addedDevices.each{ key, value ->
                    paragraph title: value, ""+key
                }
            } else {
                paragraph "No devices added."
            }
        }
    }
}

def addDevices(selectedDevices) {
    def addedDevices = [:]
    logger('debug', "selectedDevices: "+selectedDevices+" childDevices: " + getChildDevices().size() )
        
    if(selectedDevices && selectedDevices!=null) {
        
        if(getChildDevices().size()<1) {
            logger('debug', "No cast-web-api installed" )
            
            if(state.latestHttpMac) {
                addChildDevice("vervallsweg", "cast-web-api", ""+state.latestHttpMac, location.hubs[0].id, [
                    "label": "cast-web-api",
                    "data": [
                        "apiHost": apiHostAddress,
                        "devices": "[]"
                    ]
                ])
            } else {
                addedDevices.put('Error', "The cast-web-api doesn't retun it's MAC address. No devices were added.")
            }
        }
        
        selectedDevices.each { key ->
            logger('debug', "Selected device id: " + key + ", name: " + state.devicesMap[key] )
            addedDevices.put(key, state.devicesMap[key])
        }
        
        getChildDevices().each {
            it.updateDataValue("devices", ""+selectedDevices);
            it.updated()
        }
    }
    
    if(addedDevices==[:]) {
        return "0"
    } else {
        return addedDevices
    }
}

def configureDevicePage(dni) {
    def d
    getChildDevices().each{ api ->
        api.getChildDevices().each { device ->
            if( device.deviceNetworkId.equals(dni["dni"]) ) {
                d = device
            }
        }
    }
    logger('debug', "configureDevicePage() selected device d: " + d)
    state.configCurrentDevice = d.deviceNetworkId
    
    if(d){
        resetFormVar(d)
    
        dynamicPage(name: "configureDevicePage", title: "Configure "+d.displayName+" ("+d.deviceNetworkId+")", nextPage: "saveDeviceConfigurationPage") {
            section("Connection settings") {
                input(name: "api_host_address", type: "text", title: "cast-web-api address", defaultValue: [d.getDataValue("apiHost")], required: true)
            }
            section("Presets") {
                input(name: "presetObject", type: "text", title: "Preset object", defaultValue: [d.getDataValue("presetObject")], required: true)
                href(name: "presetGenerator",title: "Edit this preset in your browser",required: false,style: "external",url: "https://vervallsweg.github.io/smartthings/cast-web-preset-generator/preset-generator.html?"+d.getDataValue("presetObject"),description: "")
            }
        }
    } else {
        dynamicPage(name: "configureDevicePage", title: "Error", nextPage: "mainPage") {
            section("Something went wrong"){ 
                paragraph "Cannot access the device"
            }
        }
    }
}

def saveDeviceConfigurationPage() {
    def d
    getChildDevices().each{ api ->
        api.getChildDevices().each { device ->
            if( device.deviceNetworkId.equals( state.configCurrentDevice ) ) {
                d = device
            }
        }
    }
    logger('debug', "saveDeviceConfigurationPage() writing configuration for d: " + d)
    
    //d.displayName = label
    //d.updateDataValue("deviceType", device_type)
    //d.updateDataValue("pollMinutes", ""+poll_minutes)
    //d.updateDataValue("pollSecond", ""+poll_seconds)
    //d.updateDataValue("deviceAddress", device_address)
    d.updateDataValue("apiHost", api_host_address)
    d.updateDataValue("presetObject", presetObject)
    //d.updateDataValue("logLevel", ""+log_level)
    d.updated()
    
    dynamicPage(name: "saveDeviceConfigurationPage", title: "Configuration updated for: "+d.deviceNetworkId, nextPage: "mainPage") {
        section("Device name"){ paragraph ""+d.displayName }
        //section("Device type"){ paragraph ""+d.getDataValue("deviceType") }
        //section("Refresh every x minute"){ paragraph ""+d.getDataValue("pollMinutes") }
        //section("Refresh on every x second"){ paragraph ""+d.getDataValue("pollSecond") }
        //section("Cast device IP address"){ paragraph ""+d.getDataValue("deviceAddress") }
        section("Web API host address"){ paragraph ""+d.getDataValue("apiHost") }
        section("Presets"){ paragraph ""+d.getDataValue("presetObject") }
        //section("Log level"){ paragraph ""+d.getDataValue("logLevel") }
    }
}

def resetFormVar(device) {
    //if(label){ app.updateSetting("label", device.label) }
    //if(device_type){ app.updateSetting("device_type", [device.getDataValue("deviceType")]) }
    //if(poll_minutes){ app.updateSetting("poll_minutes", [device.getDataValue("pollMinutes")]) }
    //if(poll_seconds){ app.updateSetting("poll_seconds", [device.getDataValue("pollSecond")]) }
    //if(device_address){ app.updateSetting("device_address", [device.getDataValue("deviceAddress")]) }
    if(api_host_address){ app.updateSetting("api_host_address", [device.getDataValue("apiHost")]) }
    if(presetObject){ app.updateSetting("presetObject", [device.getDataValue("presetObject")]) }
    //if(log_level){ app.updateSetting("log_level", [device.getDataValue("logLevel")]) }
}

def installed() {
    logger('debug', "Installed with settings: ${settings}")

    initialize()
}

def updated() {
    logger('debug', "Updated with settings: ${settings}")

    unsubscribe()
    initialize()
}

def initialize() {
    // TODO: subscribe to attributes, devices, locations, etc.
    getChildDevices().each {
        if(it) {
            log.info "it: "+it
            try {
                it.setApiHost(apiHostAddress);
            } catch(e) {
                log.info "Yeah, probably double exec error: "+e
            }
        }
    }
}

def getDevices() {
    logger('debug', "Executing 'getDevices'")
    sendHttpRequest(apiHostAddress, '/device')
}

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

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

def parse(description) {
    logger('debug', "Parsing '${description}'")
    
    def msg, json, status, mac
    try {
        msg = parseLanMessage(description)
        status = msg.status
        json = msg.json
        mac = msg.mac
    } catch (e) {
        logger("error", "Exception caught while parsing data: "+e)
        return null;
    }
  
    state.latestHttpResponse = status
    state.latestHttpMac = mac
    if(status==200){
        def length = 0
        logger('debug', "JSON rcvd: "+json+", JSON.size: "+json.size)
        
        def devices = [:]
        for(int i=0; i<json.size; i++) {
            logger('debug', "index "+ i +": "+json[i]['name']+", "+ json[i]['id'])
            devices.put(json[i]['id'], json[i]['name'])
        }
       
        logger('debug', "devices: " + devices)
        state.devicesMap = devices
    } else {
        state.devicesMap = [:]
    }
}

//UPDATE
def getThisVersion() {
    return '1.2.0'
}

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

def checkForUpdate() {
    if(getThisVersion() != getLatestVersion().version) {
        return "Update available from: " + getThisVersion() + " to: " + getLatestVersion().version
    } else {
        logger('debug', "Up to date, " + "thisVersion: " + getThisVersion() + ", latestVersion: " + getLatestVersion().version)
        return "Up to date: " + getThisVersion()
    }
}

def updateServiceManagerPage() {
    dynamicPage(name:"updateServiceManagerPage", title:"Check for updates", nextPage: nextPage) {
        section("Checked for updates") {
            paragraph "" + checkForUpdate()
        }
        section("Latest version") {
            def latestVersion = getLatestVersion()
            paragraph "Version: " + latestVersion.version
            paragraph "Type: " + latestVersion.type
            paragraph "Release date: " + latestVersion.date
            href(name: "Changelog",title: "Changelog",required: false, url: latestVersion.changelog, description: "")
        }
    }
}

//DEBUGGING
def logger(level, message) {
    def smLogLevel = 0
    if(settingsLogLevel) {
        smLogLevel = settingsLogLevel.toInteger()
    }
    if(level=="error"&&smLogLevel>0) {
        log.error message
    }
    if(level=="warn"&&smLogLevel>1) {
        log.warn message
    }
    if(level=="info"&&smLogLevel>2) {
        log.info message
    }
    if(level=="debug"&&smLogLevel>3) {
        log.debug message
    }
}

Why do you say it only has 6 months back to live?

@SmartThings don’t want to say exactly when, just 2021, so it could be in 1, 6 or 11 months.

That happened with me. I had to wait a few hours and, like magic, the devices appeared

Were you using this modified smartapp to input the API address? If not, is your API address from your PC/RPi running the node use the default address or a custom one?

Yes, i changed the DH on line 43. Replaced “String” with “Text” and after that the “new” smartthings app allowed to insert the IP address

1 Like

Reading through these comments and I’m thinking this question may be moot at this point. But after having problems with the cast-web-api, I deleted every thing and started from scratch. I’ve limped back through the process (the line 43 code change being the main culpriate)… and it seems to be running-ish… but all of my devices were added as “cast-web-device”. I have no way that I can tell of to figure out which is which. Also, they are all grayed out and say “checking”.

Is this integration just toast now?

You did everything right. I ended up adding my devices one by one to manually change the name, but can also decode which ones by changing the volume slider and listening for which device changes.

As far as other issues, it’s due to it being built for Classic, and is not being maintained/updated. It would be great if someone could modify it for the new SmartThings app, but people may be waiting for more Nest integration to include cast devices.

Do you have a guide to change the depencdency?

Will you update if you get the Nest Hub to work or did you give up? I still have problems with my Nest hub not working with Cast-web. I’ve tried different things, but dosent seem to get anything out on the Nest hub- I can add it but thats about it

See the updated instruction in the original post.

Hubitat has a chromecast integration BUILT IN.
Just saying.

I just updated the node version to the last one (10.15.1) using the steps mention in “Dory - node.js” Play Store app.

“Upgrade node.js : ‘download file’ menu → check ‘appfiles’ → check ‘executable’
v10.15.1 arm(only for android 5.0 above)”

Then, install the package again and it will work without problems.

3 Likes

I have two questions.

Is there an update on the device handler so it works correctly in the new ST app?

For some reason I can’t get TTS voice to work (e.g. in Danish)


When this TTS switch is on the speaker only beeps and no voice but when I switch is off the voice is back (in English)
What should I do to make this work?

3 Likes

I do just that but within an LXC :ok_hand:

Hi, new to smartthings. This API looks very useful so thank you for your hard work. I have an old Android device, but sadly following the Android instructions on the official instructions doesn’t work for me (and seems a few users above).

I’m still keen on this so happy to get (my first!) Raspberry pi to get this to work. Anyone able to help with which raspberry pi could use this? For example could a raspberry pi pico run this API? Or a raspberry pi zero? Or what gen raspberry pi should I start with (gen 3, gen 4 etc?) Thanks in advance!

I was using Raspberry Pi 3b+ along with other applications and it worked fine. You can give pi zero W or pi zero and I believe this should work

1 Like