tonesto7 / homebridge-smartthings

SmartThings Homebridge Plugin
383 stars 765 forks source link

Hubitat Hub Support? #239

Closed ogiewon closed 6 years ago

ogiewon commented 6 years ago

Paul,

I would love to see a version of homebridge-smartthings revised to work on the new Hubitat Elevation Hub. This new hub was designed by a team of very capable SmartThings users, so the new system is very backwards compatible with existing SmartThings groovy code. One of the major advantage of this new system is that everything runs locally on the hub, resulting is excellent performance. It exposes both local and cloud OAUTH endpoints, so Homebridge should be able to communicate on the local LAN. This would result in excellent performance.

I have attempted to modify the JSON Complete API SmartApp to run on the Hubitat Hub. It successfully compiles and does run. I have modified a few sections to add the Hubitat endpoints (showing both the cloud and local endpoints). Unfortunately, the hub does not appear to support HTTPS encrypted traffic on the local network (i.e. connection refused error using https). The cloud endpoint does, of course, support https. I am not sure how to modify the homebridge-smartthings plugin to allow for unencrypted local traffic. I was able to get the homebridge-smartthings plugin to work somewhat, using the Hubitat cloud endpoint, just for testing. However, only devices in the "switch" group showed up in Homekit on my iPhone. The other devices all were listed as not belonging to a group. The two "switch" devices did show up in Homekit on my phone, and I could control them!

So, it seems like it is very close to working. Since Hubitat currently does not really have a native user interface (i.e. no smartphone app exists), having Homebridge running with a fast local LAN connection would be a very welcome option to iOS users.

Thoughts? Want to take on a challenge that right up your alley? ;)

Dan

Here is my modified version of your SmartApp that I have been testing with on Hubitat. [Update 2/11/2018: I figured out how to determine the Hubitat hub's local IP address on the fly without user input. Version below has been updated accordingly.

/**
 *  JSON Complete API
 *
 *  Copyright 2017 Paul Lovelace
 *
 *  Modifications for Hubitat (in progress) - Dan Ogorchock 2/11/2018
 */
definition(
    name: "JSON Complete API",
    namespace: "pdlove",
    author: "Paul Lovelace",
    description: "API for JSON with complete set of devices",
    category: "SmartThings Labs",
    iconUrl:   "https://raw.githubusercontent.com/pdlove/homebridge-smartthings/master/smartapps/JSON%401.png",
    iconX2Url: "https://raw.githubusercontent.com/pdlove/homebridge-smartthings/master/smartapps/JSON%402.png",
    iconX3Url: "https://raw.githubusercontent.com/pdlove/homebridge-smartthings/master/smartapps/JSON%403.png",
    oauth: true)

preferences {
    page(name: "copyConfig")
}

//When adding device groups, need to add here
def copyConfig() {
    if (!state.accessToken) {
        createAccessToken()
    }
    dynamicPage(name: "copyConfig", title: "Configure Devices", install:true, uninstall:true) {
        section("Select devices to include in the /devices API call") {
            paragraph "Version 0.5.5"
            input "deviceList", "capability.refresh", title: "Most Devices", multiple: true, required: false
            input "sensorList", "capability.sensor", title: "Sensor Devices", multiple: true, required: false
            input "switchList", "capability.switch", title: "All Switches", multiple: true, required: false
            //paragraph "Devices Selected: ${deviceList ? deviceList?.size() : 0}\nSensors Selected: ${sensorList ? sensorList?.size() : 0}\nSwitches Selected: ${switchList ? switchList?.size() : 0}"
        }
        section("Configure Pubnub") {
            input "pubnubSubscribeKey", "text", title: "PubNub Subscription Key", multiple: false, required: false
            input "pubnubPublishKey", "text", title: "PubNub Publish Key", multiple: false, required: false
            input "subChannel", "text", title: "Channel (Can be anything)", multiple: false, required: false
        }
        section() {
            paragraph "View this SmartApp's configuration to use it in other places."
            //Original SmartThings cloud endpoint
            //href url:"${apiServerUrl("/api/smartapps/installations/${app.id}/config?access_token=${state.accessToken}")}", style:"embedded", required:false, title:"Config", description:"Tap, select, copy, then click \"Done\""
            //Hubitat cloud endpoint
            //href url:"${getApiServerUrl()}/${hubUID}/apps/${app.id}/config?access_token=${state.accessToken}", style:"embedded", required:false, title:"Config", description:"Tap, select, copy, then click \"Done\""
            //Hubitat local endpoint 
            href url:"http://${location.hubs[0].getDataValue("localIP")}/apps/api/${app.id}/config?access_token=${state.accessToken}", style:"embedded", required:false, title:"Config", description:"Tap, select, copy, then click \"Done\""
        }

        section() {
            paragraph "View the JSON generated from the installed devices."
            //Original SmartThings cloud endpoint
            //href url:"${apiServerUrl("/api/smartapps/installations/${app.id}/devices?access_token=${state.accessToken}")}", style:"embedded", required:false, title:"Device Results", description:"View accessories JSON"
            //Hubitat cloud endpoint
            //href url:"${getApiServerUrl()}/${hubUID}/apps/${app.id}/devices?access_token=${state.accessToken}", style:"embedded", required:false, title:"Device Results", description:"View accessories JSON"
            //Hubitat local endpoint 
            href url:"http://${location.hubs[0].getDataValue("localIP")}/apps/api/${app.id}/devices?access_token=${state.accessToken}", style:"embedded", required:false, title:"Device Results", description:"View accessories JSON"
        }
        section() {
            paragraph "Enter the name you would like shown in the smart app list"
            label title:"SmartApp Label (optional)", required: false 
        }
    }
}

def renderDevices() {
    def deviceData = []
        deviceList.each { 
            try {
            deviceData << [name: it.displayName,
                    basename: it.name,
                    deviceid: it.id, 
                    status: it.status,
                    manufacturerName: it.getManufacturerName(),
                    modelName: it.getModelName(),
                    lastTime: it.getLastActivity(),
                    capabilities: deviceCapabilityList(it), 
                    commands: deviceCommandList(it), 
                    attributes: deviceAttributeList(it)
                    ]
            } catch (e) {
                log.error("Error Occurred Parsing Device "+it.displayName+", Error " + e)
            }
        }    
        sensorList.each { 
            try {
            deviceData << [name: it.displayName,
                    basename: it.name,
                    deviceid: it.id, 
                    status: it.status,
                    manufacturerName: it.getManufacturerName(),
                    modelName: it.getModelName(),
                    lastTime: it.getLastActivity(),
                    capabilities: deviceCapabilityList(it), 
                    commands: deviceCommandList(it), 
                    attributes: deviceAttributeList(it)
                    ]
            } catch (e) {
                log.error("Error Occurred Parsing Device "+it.displayName+", Error " + e)
            }
        }    
        switchList.each { 
            try {
            deviceData << [name: it.displayName,
                    basename: it.name,
                    deviceid: it.id, 
                    status: it.status,
                    manufacturerName: it.getManufacturerName(),
                    modelName: it.getModelName(),
                    lastTime: it.getLastActivity(),
                    capabilities: deviceCapabilityList(it), 
                    commands: deviceCommandList(it), 
                    attributes: deviceAttributeList(it)
                    ]
            } catch (e) {
                log.error("Error Occurred Parsing Device "+it.displayName+", Error " + e)
            }
        }    
    return deviceData
}

def findDevice(paramid) {
    def device = deviceList.find { it.id == paramid }
    if (device) return device
    device = sensorList.find { it.id == paramid }
    if (device) return device
    device = switchList.find { it.id == paramid }

    return device
 }
//No more individual device group definitions after here.

def installed() {
    log.debug "Installed with settings: ${settings}"
    initialize()
}

def updated() {
    log.debug "Updated with settings: ${settings}"
    unsubscribe()
    initialize()
}

def initialize() {
    if(!state.accessToken) {
         createAccessToken()
    }
    registerAll()
    state.subscriptionRenewed = 0
    subscribe(location, null, HubResponseEvent, [filterEvents:false])
    log.debug "0.5.5"
}

def authError() {
    [error: "Permission denied"]
}
def renderConfig() {
    def configJson = new groovy.json.JsonOutput().toJson([
        description: "JSON API",
        platforms: [
            [
                platform: "SmartThings",
                name: "SmartThings",
                //
                //Original SmartThings cloud endpoint
                //app_url: apiServerUrl("/api/smartapps/installations/"),
                //
                //Hubitat cloud endpoint
                //app_url: "${getApiServerUrl()}/${hubUID}/apps/",
                //
                //Hubitat local endpoint
                app_url: "http://${location.hubs[0].getDataValue("localIP")}/apps/api/",
                app_id: app.id,
                access_token:  state.accessToken
            ]
        ],
    ])

    def configString = new groovy.json.JsonOutput().prettyPrint(configJson)
    render contentType: "text/plain", data: configString
}
def renderLocation() {
    [
        latitude: location.latitude,
        longitude: location.longitude,
        mode: location.mode,
        name: location.name,
        temperature_scale: location.temperatureScale,
        zip_code: location.zipCode,
        hubIP: hubitatLocalIP,
        //hubIP: location.hubs[0].localIP,
        smartapp_version: '0.5.5'
    ]
}
def CommandReply(statusOut, messageOut) {
    def replyData =
        [
            status: statusOut,
            message: messageOut
        ]

    def replyJson    = new groovy.json.JsonOutput().toJson(replyData)
    render contentType: "application/json", data: replyJson
}
def deviceCommand() {
    log.info("Command Request")
    def device = findDevice(params.id)    
    def command = params.command

    if (!device) {
        log.error("Device Not Found")
        CommandReply("Failure", "Device Not Found")
    } else if (!device.hasCommand(command)) {
        log.error("Device "+device.displayName+" does not have the command "+command)
        CommandReply("Failure", "Device "+device.displayName+" does not have the command "+command)
    } else {
        def value1 = request.JSON?.value1
        def value2 = request.JSON?.value2
        try {
            if (value2) {
                device."$command"(value1,value2)
            } else if (value1) {
                device."$command"(value1)
            } else {
                device."$command"()
            }
            log.info("Command Successful for Device "+device.displayName+", Command "+command)
            CommandReply("Success", "Device "+device.displayName+", Command "+command)
        } catch (e) {
            log.error("Error Occurred For Device "+device.displayName+", Command "+command)
            CommandReply("Failure", "Error Occurred For Device "+device.displayName+", Command "+command)
        }
    }
}
def deviceAttribute() {
    def device = findDevice(params.id)    
    def attribute = params.attribute
    if (!device) {
        httpError(404, "Device not found")
    } else {
        def currentValue = device.currentValue(attribute)
        [currentValue: currentValue]
    }
}
def deviceQuery() {
    def device = findDevice(params.id)    
    if (!device) { 
        device = null
        httpError(404, "Device not found")
    } 

    if (result) {
        def jsonData =
            [
                name: device.displayName,
                deviceid: device.id,
                capabilities: deviceCapabilityList(device),
                commands: deviceCommandList(device),
                attributes: deviceAttributeList(device)
            ]
        def resultJson = new groovy.json.JsonOutput().toJson(jsonData)
        render contentType: "application/json", data: resultJson
    }
}
def deviceCapabilityList(device) {
    def i=0
    device.capabilities.collectEntries { capability->
        [
            (capability.name):1
        ]
    }
}
def deviceCommandList(device) {
    def i=0
    device.supportedCommands.collectEntries { command->
        [
            (command.name): (command.arguments)
        ]
    }
}
def deviceAttributeList(device) {
    device.supportedAttributes.collectEntries { attribute->
        try {
            [
                (attribute.name): device.currentValue(attribute.name)
            ]
        } catch(e) {
            [
                (attribute.name): null
            ]
        }
    }
}
def getAllData() {
    //Since we're about to send all of the data, we'll count this as a subscription renewal and clear out pending changes.
    state.subscriptionRenewed = now()
    state.devchanges = []

    def deviceData =
    [   location: renderLocation(),
        deviceList: renderDevices() ]
    def deviceJson = new groovy.json.JsonOutput().toJson(deviceData)
    render contentType: "application/json", data: deviceJson
}
def startSubscription() {
//This simply registers the subscription.
    state.subscriptionRenewed = now()
    def deviceJson = new groovy.json.JsonOutput().toJson([status: "Success"])
    render contentType: "application/json", data: deviceJson    
}
def endSubscription() {
//Because it takes too long to register for an api command, we don't actually unregister.
//We simply blank the devchanges and change the subscription renewal to two hours ago.
    state.devchanges = []
    state.subscriptionRenewed = 0
    def deviceJson = new groovy.json.JsonOutput().toJson([status: "Success"])
    render contentType: "application/json", data: deviceJson     
}
def registerAll() {
//This has to be done at startup because it takes too long for a normal command.
    log.debug "Registering All Events"
    state.devchanges = []
    registerChangeHandler(deviceList)
    registerChangeHandler(sensorList)
    registerChangeHandler(switchList)
}
def registerChangeHandler(myList) {
    myList.each { myDevice ->
        def theAtts = myDevice.supportedAttributes
        theAtts.each {att ->
            subscribe(myDevice, att.name, changeHandler)
        log.debug "Registering ${myDevice.displayName}.${att.name}"
        }
    }
}
def changeHandler(evt) {
    //Send to Pubnub if we need to.
    if (pubnubPublishKey!=null) {
        def deviceData = [device: evt.deviceId, attribute: evt.name, value: evt.value, date: evt.date]
        def changeJson = new groovy.json.JsonOutput().toJson(deviceData)
        def changeData = URLEncoder.encode(changeJson)
        def uri = "http://pubsub.pubnub.com/publish/${pubnubPublishKey}/${pubnubSubscribeKey}/0/${subChannel}/0/${changeData}"
        log.debug "${uri}"
        httpGet(uri)
    }

    if (state.directIP!="") {
        //Send Using the Direct Mechanism
        def deviceData = [device: evt.deviceId, attribute: evt.name, value: evt.value, date: evt.date]
        //How do I control the port?!?
        log.debug "Sending Update to ${state.directIP}:${state.directPort}"
        def result = new hubitat.device.HubAction(
            method: "GET",
            path: "/update",
            headers: [
                HOST: "${state.directIP}:${state.directPort}",
                change_device: evt.deviceId,
                change_attribute: evt.name,
                change_value: evt.value,
                change_date: evt.date
            ]
        )
        sendHubCommand(result)
    }

    //Only add to the state's devchanges if the endpoint has renewed in the last 10 minutes.
    if (state.subscriptionRenewed>(now()-(1000*60*10))) {
        if (evt.isStateChange()) {
            state.devchanges << [device: evt.deviceId, attribute: evt.name, value: evt.value, date: evt.date]
      }
    } else if (state.subscriptionRenewed>0) { //Otherwise, clear it
        log.debug "Endpoint Subscription Expired. No longer storing changes for devices."
        state.devchanges=[]
        state.subscriptionRenewed=0
    }
}
def getChangeEvents() {
    //Store the changes so we can swap it out very quickly and eliminate the possibility of losing any.
    //This is mainly to make this thread safe because I'm willing to bet that a change event can fire
    //while generating/sending the JSON.
    def oldchanges = state.devchanges
    state.devchanges=[]
    state.subscriptionRenewed = now()
    if (oldchanges.size()==0) {
        def deviceJson = new groovy.json.JsonOutput().toJson([status: "None"])
        render contentType: "application/json", data: deviceJson    
    } else {
        def changeJson = new groovy.json.JsonOutput().toJson([status: "Success", attributes:oldchanges])
        render contentType: "application/json", data: changeJson
    }
}
def enableDirectUpdates() {
    log.debug("Command Request")
    state.directIP = params.ip
    state.directPort = params.port
    log.debug("Trying ${state.directIP}:${state.directPort}")
    def result = new hubitat.device.HubAction(
            method: "GET",
            path: "/initial",
            headers: [
                HOST: "${state.directIP}:${state.directPort}"
            ],
            query: deviceData
        )
     sendHubCommand(result)
}

def HubResponseEvent(evt) {
    log.debug(evt.description)
}

def locationHandler(evt) {
    def description = evt.description
    def hub = evt?.hubId

    log.debug "cp desc: " + description
    if (description.count(",") > 4)
    {
def bodyString = new String(description.split(',')[5].split(":")[1].decodeBase64())
log.debug(bodyString)
}
}

def getSubscriptionService() {
    def replyData =
        [
            pubnub_publishkey: pubnubPublishKey,
            pubnub_subscribekey: pubnubSubscribeKey,
            pubnub_channel: subChannel
        ]

    def replyJson    = new groovy.json.JsonOutput().toJson(replyData)
    render contentType: "application/json", data: replyJson
}

mappings {
/*    if (!params.access_token || (params.access_token && params.access_token != state.accessToken)) {
        path("/devices")                        { action: [GET: "authError"] }
        path("/config")                         { action: [GET: "authError"] }
        path("/location")                       { action: [GET: "authError"] }
        path("/:id/command/:command")           { action: [POST: "authError"] }
        path("/:id/query")                      { action: [GET: "authError"] }
        path("/:id/attribute/:attribute")       { action: [GET: "authError"] }
        path("/subscribe")                      { action: [GET: "authError"] }
        path("/getUpdates")                     { action: [GET: "authError"] }
        path("/unsubscribe")                    { action: [GET: "authError"] }
        path("/startDirect/:ip/:port")          { action: [GET: "authError"] }
        path("/getSubcriptionService")          { action: [GET: "authError"] }

    } else {
*/
        path("/devices")                        { action: [GET: "getAllData"] }
        path("/config")                         { action: [GET: "renderConfig"]  }
        path("/location")                       { action: [GET: "renderLocation"] }
        path("/:id/command/:command")           { action: [POST: "deviceCommand"] }
        path("/:id/query")                      { action: [GET: "deviceQuery"] }
        path("/:id/attribute/:attribute")       { action: [GET: "deviceAttribute"] }
        path("/subscribe")                      { action: [GET: "startSubscription"] }
        path("/getUpdates")                     { action: [GET: "getChangeEvents"] }
        path("/unsubscribe")                    { action: [GET: "endSubscription"] }
        path("/startDirect/:ip/:port")          { action: [GET: "enableDirectUpdates"] }
        path("/getSubcriptionService")          { action: [GET: "getSubscriptionService"] }
//    }
}
Quiqui64 commented 6 years ago

Dan,

You are the man, I have been looking for this type of information.

JDogg016 commented 6 years ago

So if I am not mistaken, one would need to shut down the ST Homekit/Homebridge integration. But what would the steps (ie the config.json file) look like to do an install because I would LOVE to test this!!!

ogiewon commented 6 years ago

I believe you are correct. I originally tried having both “platforms” in the config.json file. To get it to work at all, I had to remove the real SmartThings platform. Afterwards, I had to use the Hubitat cloud endpoint. I was then able to see a few devices in HomeKit on my phone that were in the “switch” group, but nothing from the “sensor” group.

So, it is a promising start.

JDogg016 commented 6 years ago

So to remove the ST platform would be to simply eliminate it from the config.json, but what are you placing into the config.json instead?

Justin Bennett, Esq.

Personal E-Mail Account. Although this was sent from my personal e-mail this communication may contain privileged or other confidential information. If you are not the intended recipient, or believe that you have received this communication in error, please do not print, copy, retransmit, disseminate, or otherwise use the information contained herein. Also, please notify sender that you have received this communication in error, and delete the copy thereof you have received. Thank you.

On Feb 12, 2018, 10:52 AM -0500, ogiewon notifications@github.com, wrote:

I believe you are correct. I originally tried having both “platforms” in the config.json file. To get it to work at all, I had to remove the real SmartThings platform. Afterwards, I had to use the Hubitat cloud endpoint. I was then able to see a few devices in HomeKit on my phone that were in the “switch” group, but nothing from the “sensor” group. So, it is a promising start. — You are receiving this because you commented. Reply to this email directly, view it on GitHub, or mute the thread.

ogiewon commented 6 years ago

In your Hubitat JSON Complete API app, you can select the devices you want exposed (same as ST) and then display the Config data. Basically the exact same data that you’d get from the SmartThings App on your phone. But instead, you get it from the Hubitat hub’s web page instead.

In the code posted above, it will present the LOCAL OAUTH endpoint data needed for config.json. This endpoint is http only, not https. Thus, the reason I posted this issue.

pdlove commented 6 years ago

This looks like exactly the device I wanted ages ago and closely resembles where Home Automation started. I'll need to get one ordered. The current Smartthings connection uses https and it is a simple matter (I think) to swap it out to http if that's the only issue. I'm fairly sure you can just remove the s in the first line of lib/smartthingsapi.js https://github.com/pdlove/homebridge-smartthings/blob/0b20eae7448a928fa97ad5df2f5566994aec600f/lib/smartthingsapi.js#L1 Bt No guarantees that won't break something else, but give it a try. I'll get one ordered and be able to test more when it comes in.

JDogg016 commented 6 years ago

This would be excellent!

ogiewon commented 6 years ago

Here is my most up to date version of the "JASON Complete API" groovy code for Hubitat. This version uses recently added API calls for generating the Endpoint information. This version is configured to generate the LOCAL endpoint when displaying the information needed for config.json.

To use the cloud endpoint, just comment/uncomment the following section appropriately.

def renderConfig() {
    def configJson = new groovy.json.JsonOutput().toJson([
        description: "JSON API",
        platforms: [
            [
                platform: "SmartThings",
                name: "SmartThings",
                //
                //Original SmartThings cloud endpoint
                //app_url: apiServerUrl("/api/smartapps/installations/"),
                //
                //Hubitat cloud endpoint
                //app_url: "${getApiServerUrl()}/${hubUID}/apps/",
                //
                //Hubitat local endpoint
                app_url: getLocalApiServerUrl() + "/",
                app_id: app.id,
                access_token:  state.accessToken
            ]
        ],
    ])

    def configString = new groovy.json.JsonOutput().prettyPrint(configJson)
    render contentType: "text/plain", data: configString
}

Here is the full version of the Hubitat App. I have not tested it yet using @pdlove 's recommended changes to the lib/smartthingsapi.js file.

/**
 *  JSON Complete API
 *
 *  Copyright 2017 Paul Lovelace
 *
 *  Modifications for Hubitat (in progress) - Dan Ogorchock 2/14/2018
 */
definition(
    name: "JSON Complete API",
    namespace: "pdlove",
    author: "Paul Lovelace",
    description: "API for JSON with complete set of devices",
    category: "SmartThings Labs",
    iconUrl:   "https://raw.githubusercontent.com/pdlove/homebridge-smartthings/master/smartapps/JSON%401.png",
    iconX2Url: "https://raw.githubusercontent.com/pdlove/homebridge-smartthings/master/smartapps/JSON%402.png",
    iconX3Url: "https://raw.githubusercontent.com/pdlove/homebridge-smartthings/master/smartapps/JSON%403.png",
    oauth: true)

preferences {
    page(name: "copyConfig")
}

//When adding device groups, need to add here
def copyConfig() {
    if (!state.accessToken) {
        createAccessToken()
    }
    dynamicPage(name: "copyConfig", title: "Configure Devices", install:true, uninstall:true) {
        section("Select devices to include in the /devices API call") {
            paragraph "Version 0.5.5"
            input "deviceList", "capability.refresh", title: "Most Devices", multiple: true, required: false
            input "sensorList", "capability.sensor", title: "Sensor Devices", multiple: true, required: false
            input "switchList", "capability.switch", title: "All Switches", multiple: true, required: false
            //paragraph "Devices Selected: ${deviceList ? deviceList?.size() : 0}\nSensors Selected: ${sensorList ? sensorList?.size() : 0}\nSwitches Selected: ${switchList ? switchList?.size() : 0}"
        }
        section("Configure Pubnub") {
            input "pubnubSubscribeKey", "text", title: "PubNub Subscription Key", multiple: false, required: false
            input "pubnubPublishKey", "text", title: "PubNub Publish Key", multiple: false, required: false
            input "subChannel", "text", title: "Channel (Can be anything)", multiple: false, required: false
        }
        section() {
            paragraph "View this SmartApp's configuration to use it in other places."
            //Original SmartThings cloud endpoint
            //href url:"${apiServerUrl("/api/smartapps/installations/${app.id}/config?access_token=${state.accessToken}")}", style:"embedded", required:false, title:"Config", description:"Tap, select, copy, then click \"Done\""

            //Hubitat cloud endpoint
            //href url:"${getApiServerUrl()}/${hubUID}/apps/${app.id}/config?access_token=${state.accessToken}", style:"embedded", required:false, title:"Config", description:"Tap, select, copy, then click \"Done\""

            //Hubitat local endpoint 
            href url:fullLocalApiServerUrl("config") + "?access_token=${state.accessToken}", style:"embedded", required:false, title:"Config", description:"Tap, select, copy, then click \"Done\""
        }

        section() {
            paragraph "View the JSON generated from the installed devices."
            //Original SmartThings cloud endpoint
            //href url:"${apiServerUrl("/api/smartapps/installations/${app.id}/devices?access_token=${state.accessToken}")}", style:"embedded", required:false, title:"Device Results", description:"View accessories JSON"

            //Hubitat cloud endpoint
            //href url:"${getApiServerUrl()}/${hubUID}/apps/${app.id}/devices?access_token=${state.accessToken}", style:"embedded", required:false, title:"Device Results", description:"View accessories JSON"

            //Hubitat local endpoint 
            href url:fullLocalApiServerUrl("devices") + "?access_token=${state.accessToken}", style:"embedded", required:false, title:"Device Results", description:"View accessories JSON"
        }
        section() {
            paragraph "Enter the name you would like shown in the smart app list"
            label title:"SmartApp Label (optional)", required: false 
        }
    }
}

def renderDevices() {
    def deviceData = []
        deviceList.each { 
            try {
            deviceData << [name: it.displayName,
                    basename: it.name,
                    deviceid: it.id, 
                    status: it.status,
                    manufacturerName: it.getManufacturerName(),
                    modelName: it.getModelName(),
                    lastTime: it.getLastActivity(),
                    capabilities: deviceCapabilityList(it), 
                    commands: deviceCommandList(it), 
                    attributes: deviceAttributeList(it)
                    ]
            } catch (e) {
                log.error("Error Occurred Parsing Device "+it.displayName+", Error " + e)
            }
        }    
        sensorList.each { 
            try {
            deviceData << [name: it.displayName,
                    basename: it.name,
                    deviceid: it.id, 
                    status: it.status,
                    manufacturerName: it.getManufacturerName(),
                    modelName: it.getModelName(),
                    lastTime: it.getLastActivity(),
                    capabilities: deviceCapabilityList(it), 
                    commands: deviceCommandList(it), 
                    attributes: deviceAttributeList(it)
                    ]
            } catch (e) {
                log.error("Error Occurred Parsing Device "+it.displayName+", Error " + e)
            }
        }    
        switchList.each { 
            try {
            deviceData << [name: it.displayName,
                    basename: it.name,
                    deviceid: it.id, 
                    status: it.status,
                    manufacturerName: it.getManufacturerName(),
                    modelName: it.getModelName(),
                    lastTime: it.getLastActivity(),
                    capabilities: deviceCapabilityList(it), 
                    commands: deviceCommandList(it), 
                    attributes: deviceAttributeList(it)
                    ]
            } catch (e) {
                log.error("Error Occurred Parsing Device "+it.displayName+", Error " + e)
            }
        }    
    return deviceData
}

def findDevice(paramid) {
    def device = deviceList.find { it.id == paramid }
    if (device) return device
    device = sensorList.find { it.id == paramid }
    if (device) return device
    device = switchList.find { it.id == paramid }

    return device
 }
//No more individual device group definitions after here.

def installed() {
    log.debug "Installed with settings: ${settings}"
    initialize()
}

def updated() {
    log.debug "Updated with settings: ${settings}"
    unsubscribe()
    initialize()
}

def initialize() {
    if(!state.accessToken) {
         createAccessToken()
    }
    registerAll()
    state.subscriptionRenewed = 0
    subscribe(location, null, HubResponseEvent, [filterEvents:false])
    log.debug "0.5.5"
}

def authError() {
    [error: "Permission denied"]
}
def renderConfig() {
    def configJson = new groovy.json.JsonOutput().toJson([
        description: "JSON API",
        platforms: [
            [
                platform: "SmartThings",
                name: "SmartThings",
                //
                //Original SmartThings cloud endpoint
                //app_url: apiServerUrl("/api/smartapps/installations/"),
                //
                //Hubitat cloud endpoint
                //app_url: "${getApiServerUrl()}/${hubUID}/apps/",
                //
                //Hubitat local endpoint
                app_url: getLocalApiServerUrl() + "/",
                app_id: app.id,
                access_token:  state.accessToken
            ]
        ],
    ])

    def configString = new groovy.json.JsonOutput().prettyPrint(configJson)
    render contentType: "text/plain", data: configString
}
def renderLocation() {
    [
        latitude: location.latitude,
        longitude: location.longitude,
        mode: location.mode,
        name: location.name,
        temperature_scale: location.temperatureScale,
        zip_code: location.zipCode,
        hubIP: location.hubs[0].getDataValue("localIP"),
        //hubIP: location.hubs[0].localIP,
        smartapp_version: '0.5.5'
    ]
}
def CommandReply(statusOut, messageOut) {
    def replyData =
        [
            status: statusOut,
            message: messageOut
        ]

    def replyJson    = new groovy.json.JsonOutput().toJson(replyData)
    render contentType: "application/json", data: replyJson
}
def deviceCommand() {
    log.info("Command Request")
    def device = findDevice(params.id)    
    def command = params.command

    if (!device) {
        log.error("Device Not Found")
        CommandReply("Failure", "Device Not Found")
    } else if (!device.hasCommand(command)) {
        log.error("Device "+device.displayName+" does not have the command "+command)
        CommandReply("Failure", "Device "+device.displayName+" does not have the command "+command)
    } else {
        def value1 = request.JSON?.value1
        def value2 = request.JSON?.value2
        try {
            if (value2) {
                device."$command"(value1,value2)
            } else if (value1) {
                device."$command"(value1)
            } else {
                device."$command"()
            }
            log.info("Command Successful for Device "+device.displayName+", Command "+command)
            CommandReply("Success", "Device "+device.displayName+", Command "+command)
        } catch (e) {
            log.error("Error Occurred For Device "+device.displayName+", Command "+command)
            CommandReply("Failure", "Error Occurred For Device "+device.displayName+", Command "+command)
        }
    }
}
def deviceAttribute() {
    def device = findDevice(params.id)    
    def attribute = params.attribute
    if (!device) {
        httpError(404, "Device not found")
    } else {
        def currentValue = device.currentValue(attribute)
        [currentValue: currentValue]
    }
}
def deviceQuery() {
    def device = findDevice(params.id)    
    if (!device) { 
        device = null
        httpError(404, "Device not found")
    } 

    if (result) {
        def jsonData =
            [
                name: device.displayName,
                deviceid: device.id,
                capabilities: deviceCapabilityList(device),
                commands: deviceCommandList(device),
                attributes: deviceAttributeList(device)
            ]
        def resultJson = new groovy.json.JsonOutput().toJson(jsonData)
        render contentType: "application/json", data: resultJson
    }
}
def deviceCapabilityList(device) {
    def i=0
    device.capabilities.collectEntries { capability->
        [
            (capability.name):1
        ]
    }
}
def deviceCommandList(device) {
    def i=0
    device.supportedCommands.collectEntries { command->
        [
            (command.name): (command.arguments)
        ]
    }
}
def deviceAttributeList(device) {
    device.supportedAttributes.collectEntries { attribute->
        try {
            [
                (attribute.name): device.currentValue(attribute.name)
            ]
        } catch(e) {
            [
                (attribute.name): null
            ]
        }
    }
}
def getAllData() {
    //Since we're about to send all of the data, we'll count this as a subscription renewal and clear out pending changes.
    state.subscriptionRenewed = now()
    state.devchanges = []

    def deviceData =
    [   location: renderLocation(),
        deviceList: renderDevices() ]
    def deviceJson = new groovy.json.JsonOutput().toJson(deviceData)
    render contentType: "application/json", data: deviceJson
}
def startSubscription() {
//This simply registers the subscription.
    state.subscriptionRenewed = now()
    def deviceJson = new groovy.json.JsonOutput().toJson([status: "Success"])
    render contentType: "application/json", data: deviceJson    
}
def endSubscription() {
//Because it takes too long to register for an api command, we don't actually unregister.
//We simply blank the devchanges and change the subscription renewal to two hours ago.
    state.devchanges = []
    state.subscriptionRenewed = 0
    def deviceJson = new groovy.json.JsonOutput().toJson([status: "Success"])
    render contentType: "application/json", data: deviceJson     
}
def registerAll() {
//This has to be done at startup because it takes too long for a normal command.
    log.debug "Registering All Events"
    state.devchanges = []
    registerChangeHandler(deviceList)
    registerChangeHandler(sensorList)
    registerChangeHandler(switchList)
}
def registerChangeHandler(myList) {
    myList.each { myDevice ->
        def theAtts = myDevice.supportedAttributes
        theAtts.each {att ->
            subscribe(myDevice, att.name, changeHandler)
        log.debug "Registering ${myDevice.displayName}.${att.name}"
        }
    }
}
def changeHandler(evt) {
    //Send to Pubnub if we need to.
    if (pubnubPublishKey!=null) {
        def deviceData = [device: evt.deviceId, attribute: evt.name, value: evt.value, date: evt.date]
        def changeJson = new groovy.json.JsonOutput().toJson(deviceData)
        def changeData = URLEncoder.encode(changeJson)
        def uri = "http://pubsub.pubnub.com/publish/${pubnubPublishKey}/${pubnubSubscribeKey}/0/${subChannel}/0/${changeData}"
        log.debug "${uri}"
        httpGet(uri)
    }

    if (state.directIP!="") {
        //Send Using the Direct Mechanism
        def deviceData = [device: evt.deviceId, attribute: evt.name, value: evt.value, date: evt.date]
        //How do I control the port?!?
        log.debug "Sending Update to ${state.directIP}:${state.directPort}"
        def result = new hubitat.device.HubAction(
            method: "GET",
            path: "/update",
            headers: [
                HOST: "${state.directIP}:${state.directPort}",
                change_device: evt.deviceId,
                change_attribute: evt.name,
                change_value: evt.value,
                change_date: evt.date
            ]
        )
        //log.debug "changeHandler: sending result = ${result}"
        sendHubCommand(result)
    }

    //Only add to the state's devchanges if the endpoint has renewed in the last 10 minutes.
    if (state.subscriptionRenewed>(now()-(1000*60*10))) {
//      if (evt.isStateChange()) {
        log.debug "evt.getIsStateChange() = ${evt.getIsStateChange()}"
        if (evt.getIsStateChange()) {
                state.devchanges << [device: evt.deviceId, attribute: evt.name, value: evt.value, date: evt.date]     
        } else if (state.subscriptionRenewed>0) { //Otherwise, clear it
            log.debug "Endpoint Subscription Expired. No longer storing changes for devices."
            state.devchanges=[]
            state.subscriptionRenewed=0
        }
    }
}    
def getChangeEvents() {
    //Store the changes so we can swap it out very quickly and eliminate the possibility of losing any.
    //This is mainly to make this thread safe because I'm willing to bet that a change event can fire
    //while generating/sending the JSON.
    def oldchanges = state.devchanges
    state.devchanges=[]
    state.subscriptionRenewed = now()
    if (oldchanges.size()==0) {
        def deviceJson = new groovy.json.JsonOutput().toJson([status: "None"])
        render contentType: "application/json", data: deviceJson    
    } else {
        def changeJson = new groovy.json.JsonOutput().toJson([status: "Success", attributes:oldchanges])
        render contentType: "application/json", data: changeJson
    }
}
def enableDirectUpdates() {
    log.debug("Command Request")
    state.directIP = params.ip
    state.directPort = params.port
    log.debug("Trying ${state.directIP}:${state.directPort}")
    def result = new hubitat.device.HubAction(
            method: "GET",
            path: "/initial",
            headers: [
                HOST: "${state.directIP}:${state.directPort}"
            ],
            query: deviceData
        )
     sendHubCommand(result)
}

def HubResponseEvent(evt) {
    log.debug(evt.description)
}

def locationHandler(evt) {
    def description = evt.description
    def hub = evt?.hubId

    log.debug "cp desc: " + description
    if (description.count(",") > 4)
    {
def bodyString = new String(description.split(',')[5].split(":")[1].decodeBase64())
log.debug(bodyString)
}
}

def getSubscriptionService() {
    def replyData =
        [
            pubnub_publishkey: pubnubPublishKey,
            pubnub_subscribekey: pubnubSubscribeKey,
            pubnub_channel: subChannel
        ]

    def replyJson    = new groovy.json.JsonOutput().toJson(replyData)
    render contentType: "application/json", data: replyJson
}

mappings {
/*    if (!params.access_token || (params.access_token && params.access_token != state.accessToken)) {
        path("/devices")                        { action: [GET: "authError"] }
        path("/config")                         { action: [GET: "authError"] }
        path("/location")                       { action: [GET: "authError"] }
        path("/:id/command/:command")           { action: [POST: "authError"] }
        path("/:id/query")                      { action: [GET: "authError"] }
        path("/:id/attribute/:attribute")       { action: [GET: "authError"] }
        path("/subscribe")                      { action: [GET: "authError"] }
        path("/getUpdates")                     { action: [GET: "authError"] }
        path("/unsubscribe")                    { action: [GET: "authError"] }
        path("/startDirect/:ip/:port")          { action: [GET: "authError"] }
        path("/getSubcriptionService")          { action: [GET: "authError"] }

    } else {
*/
        path("/devices")                        { action: [GET: "getAllData"] }
        path("/config")                         { action: [GET: "renderConfig"]  }
        path("/location")                       { action: [GET: "renderLocation"] }
        path("/:id/command/:command")           { action: [POST: "deviceCommand"] }
        path("/:id/query")                      { action: [GET: "deviceQuery"] }
        path("/:id/attribute/:attribute")       { action: [GET: "deviceAttribute"] }
        path("/subscribe")                      { action: [GET: "startSubscription"] }
        path("/getUpdates")                     { action: [GET: "getChangeEvents"] }
        path("/unsubscribe")                    { action: [GET: "endSubscription"] }
        path("/startDirect/:ip/:port")          { action: [GET: "enableDirectUpdates"] }
        path("/getSubcriptionService")          { action: [GET: "getSubscriptionService"] }
//    }
}
ogiewon commented 6 years ago

Ok, another quick update. I was able to get the homebridge-smartthings plugin to communicate to Hubitat via the LAN using http (instead of https) by making two changes:

I added the change recommended by @pdlove

homebridge-smartthings/lib/smartthingsapi.js

Line 1 -> var http = require('https');

and had to change 443 to 80 in

Line 88 -> app_port = appURL.port || "80";

Here is the console output on my Raspberry Pi. Note that only devices in the 'all switches' group are properly recognized. Those in the 'sensor devices' or 'most devices' are never properly recognized by the plug-in.

pi@raspberrypi2:~/.homebridge $ homebridge WARNING The program 'node' uses the Apple Bonjour compatibility layer of Avahi. WARNING Please fix your application to use the native API of Avahi! WARNING For more information see http://0pointer.de/avahi-compat?s=libdns_sd&e=node WARNING The program 'node' called 'DNSServiceRegister()' which is not supported (or only supported partially) in the Apple Bonjour compatibility layer of Avahi. WARNING Please fix your application to use the native API of Avahi! WARNING For more information see http://0pointer.de/avahi-compat?s=libdns_sd&e=node&f=DNSServiceRegister [2018-2-14 20:39:51] Loaded plugin: homebridge-smartthings [2018-2-14 20:39:51] Registering platform 'homebridge-smartthings.SmartThings' [2018-2-14 20:39:51] --- [2018-2-14 20:39:51] Loaded config.json with 0 accessories and 1 platforms. [2018-2-14 20:39:51] --- [2018-2-14 20:39:51] Loading 1 platforms... [2018-2-14 20:39:51] [SmartThings] Initializing SmartThings platform... [2018-2-14 20:39:51] [SmartThings] Fetching Smart Things devices. [2018-2-14 20:39:51] [SmartThings] Device Skipped - Group unknown, Name Office Zigbee Contact Sensor, ID 33, JSON: {"name":"Office Zigbee Contact Sensor","basename":"Generic Zigbee Contact Sensor","deviceid":"33","status":"ACTIVE","manufacturerName":null,"modelName":null,"lastTime":null,"capabilities":{"TemperatureMeasurement":1,"Battery":1,"ContactSensor":1,"Configuration":1,"Refresh":1,"Sensor":1},"commands":{"configure":null,"enrollResponse":null,"refresh":null},"attributes":{"battery":88,"contact":"closed","temperature":72.67}} [2018-2-14 20:39:51] [SmartThings] Device Skipped - Group unknown, Name Office Zigbee Motion Sensor, ID 65, JSON: {"name":"Office Zigbee Motion Sensor","basename":"Generic Zigbee Motion Sensor","deviceid":"65","status":"ACTIVE","manufacturerName":null,"modelName":null,"lastTime":null,"capabilities":{"TemperatureMeasurement":1,"Battery":1,"MotionSensor":1,"Configuration":1,"Refresh":1,"Sensor":1},"commands":{"configure":null,"enrollResponse":null,"refresh":null},"attributes":{"battery":77,"motion":"inactive","temperature":72.87}} [2018-2-14 20:39:51] [SmartThings] Device Added - Group switch, Name Garage Overhead, ID 388 [2018-2-14 20:39:51] [SmartThings] Device Added - Group switch, Name Outside Backyard Floodlights, ID 453 [2018-2-14 20:39:51] [SmartThings] Device Added - Group switch, Name Outside Front Door Lights, ID 452 [2018-2-14 20:39:51] [SmartThings] Device Added - Group switch, Name Outside Frontyard Floodlights, ID 454 [2018-2-14 20:39:51] [SmartThings] Device Added - Group switch, Name Outside Garage Door Light, ID 364 [2018-2-14 20:39:51] [SmartThings] Unknown Capabilities: ["TemperatureMeasurement","ContactSensor","MotionSensor"] [2018-2-14 20:39:51] [SmartThings] Initializing platform accessory 'Garage Overhead'... [2018-2-14 20:39:51] [SmartThings] Initializing platform accessory 'Outside Backyard Floodlights'... [2018-2-14 20:39:51] [SmartThings] Initializing platform accessory 'Outside Front Door Lights'... [2018-2-14 20:39:51] [SmartThings] Initializing platform accessory 'Outside Frontyard Floodlights'... [2018-2-14 20:39:51] [SmartThings] Initializing platform accessory 'Outside Garage Door Light'... Setup Payload: X-HM://0023ISYWYCGDJ Scan this code with your HomeKit app on your iOS device to pair with Homebridge: . . . .

Or enter this code with your HomeKit app on your iOS device to pair with Homebridge: . . .

[2018-2-14 20:39:52] Homebridge is running on port 51826. [2018-2-14 20:39:52] [SmartThings] Direct Connect Is Listening On 192.168.1.145:8000 [2018-2-14 20:39:52] [SmartThings] SmartThings Hub Communication Established

ogiewon commented 6 years ago

Looks like Hubitat is stripping out spaces in the JSON devices data, causing any Capability that normally has a space in the name (e.g. "Temperature Measurement") to never match what /accessories/smartthings.js is looking for.

Not sure if it is best to try to fix this on the Hubitat "Complete JSON Api" side of things, or the /accessories/smartthings.js module. Thoughts?

JDogg016 commented 6 years ago

I assume all the "Smartthings" logging on your RPI console is NOT smart things itself but rather hubitat?

ogiewon commented 6 years ago

Correct. I am trying to make as few changes to @pdlove 's homebridge-smartthings plug in as possible. Eventually a fork of this code may need to exist, where all of the hard-coded "SmartThings" references are replaced with "Hubitat". Hopefully, it will even be possible to run both the homebridge-smartthings and homebridge-hubitat plug-ins side by side on the same system.

pdlove commented 6 years ago

The SmartThings plugin will require massive work as they get their own REST API to a point that we can use it. That said, I've also ordered a hubitat so I may have a more efficient integration coming up with that product.

JDogg016 commented 6 years ago

I am all in on this concept. ST has proven too unreliable for me and from the device installs I have done with Hubitat the system works as advertised. Even disconnected my internet connection and still watched it all hum along beautifully.

I hope this can be done... from when I used your ST Homebridge integration I marveled at the speed in which Homekit handled commands through both the home app and Siri (much faster than Alexa/ST) and rightfully or wrongly attributed it to the fact that the apple tv was executing things locally.

If you can help get home kit integration into hubitat. Off the charts insane automation will be forthcoming.

ogiewon commented 6 years ago

I have the homebridge-smartthings plug-in working on Hubitat, EXCEPT for the Direct Update feature. It appears that the routine in index.js is having trouble parsing the “headers” portion of the GET request that is sent. I have asked the Hubitat developers for advice, since the data received from Hubitat must be different than what ST is sending for the exact same command in the JSON Complete API groovy app.

pdlove commented 6 years ago

@JDogg016 the speed was due to having worked with the developers to optimize the speed as best as possible. I have low tolerance for delays between a motion detector picking up motion and an action occurring. With Smartthings, your IOS device communicates with the Homebridge Hub, which communicates with the cloud, which communicates with the Smartthings Hub which sends the update directly to Homebridge and back to IOS. Alexa simply replaces Homebridge with a cloud but there's still considerable delay (in my opinion) when using Alexa locally with the Hue Hub emulator that floating around on the web.

@ogiewon My hub will be in Monday. I'll get some devices on it and take a look. I'm also working on a proper project plan for the work I need to still do one homebridge-smartthings and homebridge in general. It's too big of a project for me to keep track of in my head anymore. I'll post a link when I have it.

JDogg016 commented 6 years ago

I think you will be quite pleased with Hubitat. Also, I thought your ST-Homebridge integration was brilliant and I applaud the speed of that app and can't imagine the responsiveness if ported over to Hubitat. I'm sure if you can help there will be a bunch of us looking to "reimburse" you for your hubitat.

emelbardis commented 6 years ago

Interesting project. Can CoRE/webcore be run on hubitat too? I'd love to have a 100% local solution as ST outages seem to be on the rise.

ogiewon commented 6 years ago

Technically, sure you could get CoRE to run...maybe even webCoRE...but it will require a very good ST/Hubitat groovy programmer...

pdlove commented 6 years ago

I've half-way started a hubitat-dedicated project. I'm having trouble with my ZWave devices on it so I haven't gotten very far. I do have a few Zigbee lights on it and, while it has a long way to go, it's a great concept. I'll reference the other project here when I get it uploaded this weekend. Basically it will, in it's current state, use a modified smartapp to get the device information but updates will be streamed straight from the hub via web sockets. I'm working on extracting the device information straight from the hub and the only issue I'm having is that the capabilities aren't exposed on any of the web pages so I can't just scrape it from the html.

BobFrankston commented 6 years ago

This looks very useful and far more sophisticated than the kludge I wrote for my use on SmartThings. What is the status of this project. (I tried to install it on SmartThings but ran into problems -- will get back to it when I have time to delve more).

tooluser commented 6 years ago

I'm interested as well, and would be game to help.

ogiewon commented 6 years ago

@tooluser - This has already been completed for Hubitat by @tonesto7

Take a look here

https://community.hubitat.com/t/homebridge-plug-in/1155?u=ogiewon

ogiewon commented 6 years ago

Hubitat port of this is available at https://github.com/tonesto7/homebridge-hubitat-tonesto7. Therefore, I am closing this issue for now.

tooluser commented 6 years ago

@ogiewon Thanks for taking the time to reply, it's hard to follow the maze of existing work - that helped immensely!

BobFrankston commented 6 years ago

This appears to be tied to home bridge -- is there documentation on the message format and how to automatically report on any new devices that are added to the system?