Hue Sync Box Integration

Hey all, big time newb here (first post) – so far I’ve been learning so much about HA and can’t get enough of it. I’m moving over from hubitat. One big thing that I see that’s missing is the Hue Sync Box controls.

Doing a quick search it looks like some discussion was had about the API, but not much came from it.

I noticed that someone wrote a Hubitat driver for it, I’m not at all someone who is capable of understanding what goes into a driver. Does anyone think we can make a HA integration based off some of this code?

From this thread:

The link to the repository seems to be broken, so I’ve pasted the code from my Hubitat instance to here.

/*****************************************************************************************************************
 *  Copyright Daniel Terryn
 *
 *  Name: Driver to communicate with Hue Sync Box....
 *
 *  Date: 2020-02-07
 *
 *  Version: 1.20
 *
 *  Author: Daniel Terryn
 *
 *  Description: A driver to retrieve to control the Hue Sync Box....
 *
 *  License:
 *   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.
 *****************************************************************************************************************
 *  Change History:
 *
 *    Date        Who            What
 *    ----        ---            ----
 *    2020-02-07  Daniel Terryn  Original Creation
 *    2020-02-26  Daniel Terryn  Added reboot command, fixed Input 4
 *
 * 
 */

metadata {
    definition (name: "Hue Sync Box", author: "dan.t", namespace: "dan.t", importUrl: "https://raw.githubusercontent.com/danTapps/Hubitat/master/Drivers/Hue%20Sync%20Box/hue_sync_box.groovy") {
        capability "Refresh"
        capability "Switch"
        capability "Initialize"
        capability "Presence Sensor"

        command "setMode", [[name:"Set Mode*", type: "ENUM", description: "Set Mode", constraints: ["passthrough", "video", "music", "game"] ] ]
        command "setInput", [[name:"Set Input*", type: "ENUM", description: "Set Input", constraints: ["input1", "input2", "input3", "input4"] ] ]
        command "setItensity", [[name:"Mode*", type: "ENUM", description: "Mode", constraints: ["video", "music", "game"] ],[name:"Intensity*", type: "ENUM", description: "Intensity", constraints: ["subtle", "moderate", "high", "intense"] ] ]
        //command "toggleMode"
        command "checkForUpdates"
        command "reboot"
        command "registerHueSyncBox"
        
        attribute "brightness", "number"
        attribute "mode", "string"
        attribute "hdmiSource", "string"
        attribute "lastSyncMode", "string"
        attribute "firmwareVersion", "string"
        attribute "lastCheckedUpdate", "string"
        attribute "updateAvailable", "string"
        attribute "intensity", "string"
    }
    preferences {
        input ( name: "ip", type: "text", title: "HUE Sync Box IP Address", description: "IP Address in form 192.168.1.226", required: true, displayDuringSetup: true )
        input ( name: "api_key", type: "text", title: "API Key", required: false, displayDuringSetup: true )
        input ( name: 'pollInterval', type: 'enum', title: 'Update interval (in seconds)', options: ['10', '30', '60', '300'], required: true, displayDuringSetup: true, defaultValue: "60" )
        input ( name: "configLoggingLevelIDE", title: "IDE Live Logging Level:\nMessages with this level and higher will be logged to the IDE.", type: "enum",
            options: [
                "0" : "None",
                "1" : "Error",
                "2" : "Warning",
                "3" : "Info",
                "4" : "Debug",
                "5" : "Trace"
            ],
            defaultValue: "3", displayDuringSetup: true, required: false )      
    }
}


def installed() {
    logger("Executing 'installed()'", "info")
    initialize()
}

def initialize() {
    logger("Executing 'initialize()'", "debug")
    updated()
}

def updated() {
    logger("Executing 'updated()'", "debug")
    configure()
    state.authTryCount = 0
}

def configure() {
    logger("Executing 'configure()'", "info")
    state.loggingLevelIDE = (settings.configLoggingLevelIDE) ? settings.configLoggingLevelIDE.toInteger() : 3

    unschedule()
    def pollInt = settings["pollInterval"]?.toInteger()
    // If change polling options in UI, may need to modify some of these cases:
    switch (pollInt ?: 0) {
        case 0:
            logger("Polling disabled; not scheduling")
            break
        case 1..59:
            logger("Scheduling polling every ${pollInt} seconds")
            schedule("${Math.round(Math.random() * pollInt)}/${pollInt} * * ? * * *", "refresh")
            break
        case 60..259:
            logger("Scheduling polling every 1 minute")
            runEvery1Minute("refresh")
            break
        case 300..1800:
            logger("Schedulig polling every 5 minutes")
            runEvery5Minutes("refresh")
            break
        default:
            logger("Scheduling polling every hour")
            runEvery1Hour("refresh")                
    }
    refresh()
}

def registerHueSyncBox()
{
    if (settings.api_key != null) {
        log.error "Hue Sync Box is already regsitered, please clear the API key setting to re-register your sync box"
        return
    }
    if (settings.ip == null) {
        log.error "Hue Sync Box IP has to be set"
        return
    }
    if (state.authTryCount > 19) {
        log.debug "I tried 20 times to register, I quit now. Check your settings."
        return
    }
    log.debug "Running registration, press the power button for 2 seconds on the Hue Sync Box and make sure the TV is on, attempt ${state.authTryCount+1}"

    unschedule()
    def body = [:]
    body.appName = "HueSyncBoxDriver"
    body.instanceName = "HueSyncBoxDriver"
    body.appSecret = "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="
    
    def requestParams =
    [
        uri:  "https://${settings["ip"]}/api/v1/registrations",
        contentType: "application/json",
        ignoreSSLIssues:  true,
        body : toJson(body)
    ]
    try{
        httpPost(requestParams)
        {
            response ->
        	    if (response?.status == 200)
	            {
                    if (response?.data?.accessToken) {
                        device.updateSetting("api_key", [value:response?.data?.accessToken, type:"text"])
                        configure()
                    }
                    else {
                        log.error("Unknown error adding Hue Sync Box ${response?.data}")
                        runIn(10, "registerHueSyncBox")
                        state.authTryCount = state.authTryCount + 1                        
                    }
                }
	            else
	            {
                    log.error "Error in registration Response ${response?.status}"
                    runIn(10, "registerHueSyncBox")
                    state.authTryCount = state.authTryCount + 1
                }
        }
    } catch (groovyx.net.http.HttpResponseException hre) {
        log.error "Error:${hre.getResponse()?.getData()}"
        runIn(10, "registerHueSyncBox")
        state.authTryCount = state.authTryCount + 1
    } catch (Exception e) {	
        log.error "Something went wrong when posting: ${e}"
        runIn(10, "registerHueSyncBox")
        state.authTryCount = state.authTryCount + 1
    }      
}

private sendUsernameRequest() {
    def userDesc = "HueSyncBox"
    def body = [:]
    body.appName = userDesc
    body.instanceName = userDesc
    body.appSecret = "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="

    def requestParams =
    [
        uri:  "https://${settings["syncBoxIP"]}/api/v1/registrations",
        contentType: "application/json",
        ignoreSSLIssues:  true,
        body : toJson(body)
    ]
    //asynchttpPost("parseUsernameResponse", requestParams)
    log.debug "${requestParams}"
    try{
        httpPost(requestParams)
        {
            response ->
        	    if (response?.status == 200)
	            {
                    if (response?.data?.accessToken) {
                        state["accessToken"] = response?.data?.accessToken
                        state["syncBoxAuthorized"] = true
                    }
                    else {
                        log.error("Unknown error adding Hue Sync Box ${response?.data}")
                    }
                }
	            else
	            {
                    log.error "Error in registration Response ${response?.status}"
                }
        }
    } catch (groovyx.net.http.HttpResponseException hre) {
          log.error "Error:${hre.getResponse()?.getData()}"
    } catch (Exception e) {	
          log.error "Something went wrong when posting: ${e}"
    }          
}


def refresh() {
    if (settings.api_key == null) {
        log.error "Hue Sync Box is not registered, please register the Hue Sync Box with the registration flow"
        return
    }
    if (settings.ip == null) {
        log.error "Hue Sync Box IP is not set, please updated your setting"
        return
    }
    logger("Executing 'refresh()'", "debug")
    try{
        def data = sendAsyncHttpGet("")
        def switchState = "off"
        if (data?.execution?.syncActive == true) {
            switchState = "on"
            if (data?.execution?.mode)
                if (data?.execution[data?.execution?.mode]?.intensity) 
                    valueChangeEvent("intensity", data?.execution[data?.execution?.mode]?.intensity, "")
        } else {
            valueChangeEvent("intensity", "off", "")
        }
        valueChangeEvent("switch", switchState, "")
        if (data?.execution?.brightness)
            valueChangeEvent("brightness", data?.execution?.brightness, "")
        if (data?.execution?.mode)
            valueChangeEvent("mode", data?.execution?.mode, "")
        if (data?.execution?.hdmiSource)
            valueChangeEvent("hdmiSource", data?.execution?.hdmiSource, "")
        if (data?.execution?.lastSyncMode)
            valueChangeEvent("lastSyncMode", data?.execution?.lastSyncMode, "")
        if (data?.device?.firmwareVersion)
            valueChangeEvent("firmwareVersion", data?.device?.firmwareVersion, "")
        if (data?.device?.lastCheckedUpdate)
            valueChangeEvent("lastCheckedUpdate", data?.device?.lastCheckedUpdate, "")
        if (data?.device?.updatableFirmwareVersion)
            valueChangeEvent("updateAvailable", "true", "")
        else
            valueChangeEvent("updateAvailable", "false", "")
        valueChangeEvent("presence", "present", "")
    } catch (Exception e){
        logger("refresh() exception ${e}", "error")    
        valueChangeEvent("presence", "not present", "")
    }      
}

def toggleMode()
{
    try{
        def currentMode = state?.data_points["mode"]
        setMode(state?.data_points["lastSyncMode"])
        valueChangeEvent("lastSyncMode", currentMode, "")        

    } catch (Exception e){
        logger("toggleMode() exception ${e}", "error")    
    }
}

def checkForUpdates()
{
    try{
        sendAsyncHttpPut("/device", "{\"action\": \"checkForFirmwareUpdates\" }")
    } catch (Exception e){
        logger("checkForUpdates() exception ${e}", "error")    
    }

}

def reboot()
{
    runActionCmd("doSoftwareRestart")
}

def setInput(input)
{
    logger("Receive \"setInput(\"${input}\")\" command", "info")

    try{
        sendAsyncHttpPut("/execution", "{\"hdmiSource\":\"${input}\"}")
        valueChangeEvent("hdmiSource", input, "")        
    } catch (Exception e){
        logger("setInput() exception ${e}", "error")    
    }
}

def setMode(mode)
{
    logger("Receive \"setMode(\"${mode}\")\" command", "info")
    try{
        sendAsyncHttpPut("/execution", "{\"mode\": \"${mode}\"}")
        valueChangeEvent("mode", mode, "")
        if (mode == "passthrough")
            valueChangeEvent("intensity", "", "")

    } catch (Exception e){
        logger("setMode() exception ${e}", "error")    
    }
}

def setItensity(mode, intensity)
{
    logger("Receive \"setItensity(\"${mode}\", \"${intensity}\")\" command", "info")

    try{
        sendAsyncHttpPut("/execution", "{\"${mode}\":{ \"intensity\": \"${intensity}\"}}")
        valueChangeEvent("intensity", intensity, "")
    } catch (Exception e){
        logger("setItensity() exception ${e}", "error")    
    }    
}

def on() {
    logger("Receive \"on()\" command", "info")

    try{
        setMode("video")
        valueChangeEvent("switch", "on", "")
    } catch (Exception e){
        logger("on() exception ${e}", "error")    
    }      
}

def off() {
    logger("Receive \"off()\" command", "info")    
    try{
        setMode("passthrough")
        valueChangeEvent("switch", "off", "")
    } catch (Exception e){
        logger("off() exception ${e}", "error")    
    }      
}

def valueChangeEvent(def deviceAttribute, def newValue, def newUnit = "")
{
    def success = false
    def oldValue = null
    if (newValue == null)
        return false
    if (state?.data_points)
    {
        if (state?.data_points["${deviceAttribute}"])
        {
            oldValue = state?.data_points["${deviceAttribute}"]
            if (state?.data_points["${deviceAttribute}"] == newValue)
                return false
            else if (state?.data_points["${deviceAttribute}"].toString().equals(newValue.toString()))
                return false
        }
    }
    else
        state.data_points = [:]
    
    state.data_points["${deviceAttribute}"] = newValue
    
    logger("Send new ${deviceAttribute} state, old: ${oldValue}, new: ${newValue}", "info")
    def descriptionText = "${device.displayName} ${deviceAttribute} is ${newValue}"
    sendEvent(name: deviceAttribute, value: newValue, unit:  newUnit, descriptionText: descriptionText)
    
    return true;
}

def sendAsyncHttpPut(message, body)
{
    def requestParams =
    [
        uri:  "https://${settings.ip}/api/v1"+message,
        contentType: "application/json",
        headers: ["Authorization": "Bearer ${settings.api_key}"],
        timeout: 200, 
        ignoreSSLIssues:  true,
		body : body
    ]
        
    logger("sendAsyncHttpPut: ${requestParams}", "debug")
    
    try{
        httpPut(requestParams)  //change to httpGet for the get test.
        {
          response ->
	        if (response?.status == 200)
	        {
                logger(response.data, "debug")
		        return response.data
	        }
	        else
	        {
                logger("httpPut ${response?.status}", "warn")
	        }
        }
    } catch (org.apache.http.conn.HttpHostConnectException e) {
        logger("httpPut HttpHostConnectException ${e}", "error")
        valueChangeEvent("presence", "not present", "")
    } catch (org.apache.http.NoHttpResponseException e) {
        logger("httpPut NoHttpResponseException ${e}", "error")
        valueChangeEvent("presence", "not present", "")
    } catch (Exception e){
        logger("httpPut Exception ${e}", "error")
        valueChangeEvent("presence", "not present", "")
    }    
}

def sendAsyncHttpGet(message)
{
    def result = null
    def requestParams =
    [
        uri:  "https://${settings.ip}/api/v1"+message,
        contentType: "application/json",
        headers: ["Authorization": "Bearer ${settings.api_key}"],
        timeout: 200, 
        ignoreSSLIssues:  true
    ]
    logger("sendAsyncHttpGet: ${requestParams}", "debug")
 
    try{
        httpGet(requestParams)  //change to httpGet for the get test.
        {
          response ->
	        if (response?.status == 200)
	        {
                logger(response.data, "debug")
		        result = response.data
	        }
	        else
	        {
		        log.warn "${response?.status}"
	        }
        }
    } catch (org.apache.http.conn.HttpHostConnectException e) {
        logger("httpGet HttpHostConnectException ${e}", "error")
        valueChangeEvent("presence", "not present", "")
    } catch (org.apache.http.NoHttpResponseException e) {
        logger("httpGet NoHttpResponseException ${e}", "error")
        valueChangeEvent("presence", "not present", "")
    } catch (Exception e){
        logger("httpGet Exception ${e}", "error")
        valueChangeEvent("presence", "not present", "")
    }   
    return result
}

/**
 *  logger()
 *
 *  Wrapper function for all logging.
 **/
private logger(msg, level = "debug") {

    switch(level) {
        case "error":
            if (state.loggingLevelIDE >= 1) log.error msg
            break

        case "warn":
            if (state.loggingLevelIDE >= 2) log.warn msg
            break

        case "info":
            if (state.loggingLevelIDE >= 3) log.info msg
            break

        case "debug":
            if (state.loggingLevelIDE >= 4) log.debug msg
            break

        case "trace":
            if (state.loggingLevelIDE >= 5) log.trace msg
            break

        default:
            log.debug msg
            break
    }
}

def toJson(Map m)
{
    return new groovy.json.JsonBuilder(m).toString()
}