From d787c1b41e193fcfd6e9e03ba20b0b42861d658b Mon Sep 17 00:00:00 2001 From: Michael Macaulay Date: Sat, 4 Jun 2016 15:02:55 -0500 Subject: [PATCH] MSA-1338: I believe this should replace the current app on the app store "TCP Bulbs (connect)" as it's a more reliable integration. I did a couple of things to make this more reliable: 1. I re-fetch the authorization token every 5 minutes 2. If a request fails, I again re-try to fetch the token and re-submit the request 3. I try again on 400 errors in addition to 500 and 401 errors. Code is all open source under Apache 2.0 license hosted at github at https://github.com/mmacaula/tcp-bulbs --- .../wackford/tcp-bulb.src/tcp-bulb.groovy | 164 +++-- .../tcp-bulbs-more-reliable.groovy | 679 ++++++++++++++++++ 2 files changed, 760 insertions(+), 83 deletions(-) create mode 100644 smartapps/mmacaula/tcp-bulbs-more-reliable.src/tcp-bulbs-more-reliable.groovy diff --git a/devicetypes/wackford/tcp-bulb.src/tcp-bulb.groovy b/devicetypes/wackford/tcp-bulb.src/tcp-bulb.groovy index 186c8b2..729a1ed 100644 --- a/devicetypes/wackford/tcp-bulb.src/tcp-bulb.groovy +++ b/devicetypes/wackford/tcp-bulb.src/tcp-bulb.groovy @@ -13,14 +13,14 @@ * Documented Header * * Change 2: 2014-03-15 - * Fixed bug where we weren't coming on when changing + * Fixed bug where we weren't coming on when changing * levels down. * - * Change 3: 2014-04-02 (lieberman) + * Change 3: 2014-04-02 (lieberman) * Changed sendEvent() to createEvent() in parse() * * Change 4: 2014-04-12 (wackford) - * Added current power usage tile + * Added current power usage tile * * Change 5: 2014-09-14 (wackford) * a. Changed createEvent() to sendEvent() in parse() to @@ -33,7 +33,7 @@ * b. added refresh on udate * c. added uninstallFromChildDevice to handle removing from settings * d. Changed to allow bulb to 100%, was possible to get past logic at 99 - * + * * Change 7: 2014-11-09 (wackford) * a. Added bulbpower calcs to device. TCP is broken * b. Changed to set dim level first then on. Much easier on the eys coming from bright. @@ -42,7 +42,7 @@ * Code ***************************************************************** */ -// for the UI + // for the UI metadata { definition (name: "TCP Bulb", namespace: "wackford", author: "Todd Wackford") { capability "Switch" @@ -52,28 +52,26 @@ metadata { capability "Switch Level" attribute "stepsize", "string" - + command "levelUp" command "levelDown" - command "on" - command "off" - command "setBulbPower" + command "on" + command "off" + command "setBulbPower" } simulator { // TODO: define status and reply messages here } - - preferences { + + preferences { input "stepsize", "number", title: "Step Size", description: "Dimmer Step Size", defaultValue: 5 } - + tiles { standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" - state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" - state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + state "on", label:'${name}', action:"off", icon:"st.switches.light.on", backgroundColor:"#79b821" + state "off", label:'${name}', action:"on", icon:"st.switches.light.off", backgroundColor:"#ffffff" } controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) { state "level", action:"switch level.setLevel" @@ -84,15 +82,15 @@ metadata { valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { state "level", label: 'Level ${currentValue}%' } - standardTile("lUp", "device.switchLevel", inactiveLabel: false,decoration: "flat", canChangeIcon: false) { - state "default", action:"levelUp", icon:"st.illuminance.illuminance.bright" - } - standardTile("lDown", "device.switchLevel", inactiveLabel: false,decoration: "flat", canChangeIcon: false) { - state "default", action:"levelDown", icon:"st.illuminance.illuminance.light" - } - valueTile( "power", "device.power", inactiveLabel: false, decoration: "flat") { - state "power", label: '${currentValue} Watts' - } + standardTile("lUp", "device.switchLevel", inactiveLabel: false,decoration: "flat", canChangeIcon: false) { + state "default", action:"levelUp", icon:"st.illuminance.illuminance.bright" + } + standardTile("lDown", "device.switchLevel", inactiveLabel: false,decoration: "flat", canChangeIcon: false) { + state "default", action:"levelDown", icon:"st.illuminance.illuminance.light" + } + valueTile( "power", "device.power", inactiveLabel: false, decoration: "flat") { + state "power", label: '${currentValue} Watts' + } main(["switch"]) details(["switch", "lUp", "lDown", "levelSliderControl", "level" , "power", "refresh" ]) @@ -103,10 +101,10 @@ metadata { def parse(description) { //log.debug "parse() - $description" def results = [] - - if ( description == "updated" ) - return - + + if ( description == "updated" ) + return + if (description?.name && description?.value) { results << createEvent(name: "${description?.name}", value: "${description?.value}") @@ -116,73 +114,73 @@ def parse(description) { // handle commands def setBulbPower(value) { state.bulbPower = value - log.debug "In child with bulbPower of ${state.bulbPower}" + log.debug "In child with bulbPower of ${state.bulbPower}" } def on() { log.debug "Executing 'on'" - sendEvent(name:"switch",value:on) + sendEvent(name:"switch",value:on) parent.on(this) def levelSetting = device.latestValue("level") as Float ?: 1.0 - def bulbPowerMax = device.latestValue("setBulbPower") as Float - def calculatedPower = bulbPowerMax * (levelSetting / 100) - sendEvent(name: "power", value: calculatedPower.round(1)) - - if (device.latestValue("level") == null) { + //def bulbPowerMax = device.latestValue("setBulbPower") as Float + //def calculatedPower = bulbPowerMax * (levelSetting / 100) + //sendEvent(name: "power", value: calculatedPower.round(1)) + + if (device.latestValue("level") == null) { sendEvent( name: "level", value: 1.0 ) - } + } } def off() { log.debug "Executing 'off'" - sendEvent(name:"switch",value:off) + sendEvent(name:"switch",value:off) parent.off(this) - sendEvent(name: "power", value: 0.0) + sendEvent(name: "power", value: 0.0) } def levelUp() { def level = device.latestValue("level") as Integer ?: 0 - def step = state.stepsize as float - - level+= step - - if ( level > 100 ) - level = 100 - - setLevel(level) + def step = state.stepsize as float + + level+= step + + if ( level > 100 ) + level = 100 + + setLevel(level) } def levelDown() { def level = device.latestValue("level") as Integer ?: 0 - def step = state.stepsize as float - - level-= step - + def step = state.stepsize as float + + level-= step + if ( level < 1 ) - level = 1 - - setLevel(level) + level = 1 + + setLevel(level) } def setLevel(value) { log.debug "in setLevel with value: ${value}" def level = value as Integer - - sendEvent( name: "level", value: level ) - sendEvent( name: "switch.setLevel", value:level ) + + sendEvent( name: "level", value: level ) + sendEvent( name: "switch.setLevel", value:level ) parent.setLevel( this, level ) + - - if (( level > 0 ) && ( level <= 100 )) - on() - else - off() - - def levelSetting = level as float - def bulbPowerMax = device.latestValue("setBulbPower") as float - def calculatedPower = bulbPowerMax * (levelSetting / 100) - sendEvent(name: "power", value: calculatedPower.round(1)) + if (( level > 0 ) && ( level <= 100 )) + on() + else + off() + + //def levelSetting = level as float + //def bulbPowerMax = device.latestValue("setBulbPower") as float + //def calculatedPower = bulbPowerMax * (levelSetting / 100) + //sendEvent(name: "power", value: calculatedPower.round(1)) } def poll() { @@ -200,29 +198,29 @@ def installed() { } def updated() { - initialize() - refresh() + initialize() + refresh() } def initialize() { if ( !settings.stepsize ) - state.stepsize = 10 //set the default stepsize - else - state.stepsize = settings.stepsize + state.stepsize = 10 //set the default stepsize + else + state.stepsize = settings.stepsize } /******************************************************************************* - Method :uninstalled(args) - (args) :none - returns:Nothing - ERRORS :No error handling is done - - Purpose:This is standard ST method. - Gets called when "remove" is selected in child device "preferences" - tile. It also get's called when "deleteChildDevice(child)" is - called from parent service manager app. - *******************************************************************************/ + Method :uninstalled(args) + (args) :none + returns:Nothing + ERRORS :No error handling is done + + Purpose:This is standard ST method. + Gets called when "remove" is selected in child device "preferences" + tile. It also get's called when "deleteChildDevice(child)" is + called from parent service manager app. +*******************************************************************************/ def uninstalled() { log.debug "Executing 'uninstall' in device type" - parent.uninstallFromChildDevice(this) + parent.uninstallFromChildDevice(this) } diff --git a/smartapps/mmacaula/tcp-bulbs-more-reliable.src/tcp-bulbs-more-reliable.groovy b/smartapps/mmacaula/tcp-bulbs-more-reliable.src/tcp-bulbs-more-reliable.groovy new file mode 100644 index 0000000..6d51d3a --- /dev/null +++ b/smartapps/mmacaula/tcp-bulbs-more-reliable.src/tcp-bulbs-more-reliable.groovy @@ -0,0 +1,679 @@ +/** + * TCP Bulbs (Connect) + * + * Copyright 2014 Todd Wackford + * + * 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 java.security.MessageDigest; + +private apiUrl() { "https://tcp.greenwavereality.com/gwr/gop.php?" } + +definition( + name: "TCP Bulbs - more reliable", + namespace: "mmacaula", + author: "Mike Macaulay", + description: "Connect your TCP bulbs to SmartThings using Cloud to Cloud integration. You must create a remote login acct on TCP Mobile App.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp@2x.png", + singleInstance: true +) + + +preferences { + def msg = """Tap 'Next' after you have entered in your TCP Mobile remote credentials. + +Once your credentials are accepted, SmartThings will scan your TCP installation for Bulbs.""" + + page(name: "selectDevices", title: "Connect Your TCP Lights to SmartThings", install: false, uninstall: true, nextPage: "chooseBulbs") { + section("TCP Connected Remote Credentials") { + input "username", "text", title: "Enter TCP Remote Email/UserName", required: true + input "password", "password", title: "Enter TCP Remote Password", required: true + paragraph msg + } + } + + page(name: "chooseBulbs", title: "Choose Bulbs to Control With SmartThings", content: "initialize") +} + +def installed() { + debugOut "Installed with settings: ${settings}" + + unschedule() + unsubscribe() + + setupBulbs() + + log.debug "schedule every 5 minutes syncronizeDevices)" + runEvery5Minutes(syncronizeDevices) +} + +def updated() { + debugOut "Updated with settings: ${settings}" + + unschedule() + + setupBulbs() + + log.debug "schedule update every 5 minutes syncronizeDevices)" + runEvery5Minutes(syncronizeDevices) +} + +def uninstalled() +{ + unschedule() //in case we have hanging runIn()'s +} + +private removeChildDevices(delete) +{ + debugOut "deleting ${delete.size()} bulbs" + debugOut "deleting ${delete}" + delete.each { + deleteChildDevice(it.device.deviceNetworkId) + } +} + +def uninstallFromChildDevice(childDevice) +{ + def errorMsg = "uninstallFromChildDevice was called and " + if (!settings.selectedBulbs) { + debugOut errorMsg += "had empty list passed in" + return + } + + def dni = childDevice.device.deviceNetworkId + + if ( !dni ) { + debugOut errorMsg += "could not find dni of device" + return + } + + def newDeviceList = settings.selectedBulbs - dni + app.updateSetting("selectedBulbs", newDeviceList) + + debugOut errorMsg += "completed succesfully" +} + + +def setupBulbs() { + debugOut "In setupBulbs" + + def bulbs = state.devices + def deviceFile = "TCP Bulb" + + selectedBulbs.each { did -> + //see if this is a selected bulb and install it if not already + def d = getChildDevice(did) + + if(!d) { + def newBulb = bulbs.find { (it.did) == did } + d = addChildDevice("wackford", deviceFile, did, null, [name: "${newBulb?.name}", label: "${newBulb?.name}", completedSetup: true]) + + /*if ( isRoom(did) ) { //change to the multi light group icon for a room device + d.setIcon("switch", "on", "st.lights.multi-light-bulb-on") + d.setIcon("switch", "off", "st.lights.multi-light-bulb-off") + d.save() + }*/ + + } else { + debugOut "We already added this device" + } + } + + // Delete any that are no longer in settings + def delete = getChildDevices().findAll { !selectedBulbs?.contains(it.deviceNetworkId) } + removeChildDevices(delete) + + //we want to ensure syncronization between rooms and bulbs + //syncronizeDevices() +} + +def initialize() { + + atomicState.token = "" + + getToken() + + if ( atomicState.token == "error" ) { + return dynamicPage(name:"chooseBulbs", title:"TCP Login Failed!\r\nTap 'Done' to try again", nextPage:"", install:false, uninstall: false) { + section("") {} + } + } else { + "we're good to go" + debugOut "We have Token." + } + + //getGatewayData() //we really don't need anything from the gateway + + deviceDiscovery() + + def options = devicesDiscovered() ?: [] + + def msg = """Tap 'Done' after you have selected the desired devices.""" + + return dynamicPage(name:"chooseBulbs", title:"TCP and SmartThings Connected!", nextPage:"", install:true, uninstall: true) { + section("Tap Below to View Device List") { + input "selectedBulbs", "enum", required:false, title:"Select Bulb/Fixture", multiple:true, options:options + paragraph msg + } + } +} + +def deviceDiscovery() { + def data = "1${atomicState.token}" + + def Params = [ + cmd: "RoomGetCarousel", + data: "${data}", + fmt: "json" + ] + + def cmd = toQueryString(Params) + + def rooms = "" + log.debug 'trying to discover devices' + apiPost(cmd) { response -> + rooms = response.data.gip.room + } + + debugOut "rooms data = ${rooms}" + + def devices = [] + def bulbIndex = 1 + def lastRoomName = null + def deviceList = [] + + if ( rooms[1] == null ) { + def roomId = rooms.rid + def roomName = rooms.name + devices = rooms.device + if ( devices[1] != null ) { + debugOut "Room Device Data: did:${roomId} roomName:${roomName}" + //deviceList += ["name" : "${roomName}", "did" : "${roomId}", "type" : "room"] + devices.each({ + debugOut "Bulb Device Data: did:${it?.did} room:${roomName} BulbName:${it?.name}" + deviceList += ["name" : "${roomName} ${it?.name}", "did" : "${it?.did}", "type" : "bulb"] + }) + } else { + debugOut "Bulb Device Data: did:${it?.did} room:${roomName} BulbName:${it?.name}" + deviceList += ["name" : "${roomName} ${it?.name}", "did" : "${it?.did}", "type" : "bulb"] + } + } else { + rooms.each({ + devices = it.device + def roomName = it.name + if ( devices[1] != null ) { + def roomId = it?.rid + debugOut "Room Device Data: did:${roomId} roomName:${roomName}" + //deviceList += ["name" : "${roomName}", "did" : "${roomId}", "type" : "room"] + devices.each({ + debugOut "Bulb Device Data: did:${it?.did} room:${roomName} BulbName:${it?.name}" + deviceList += ["name" : "${roomName} ${it?.name}", "did" : "${it?.did}", "type" : "bulb"] + }) + } else { + debugOut "Bulb Device Data: did:${devices?.did} room:${roomName} BulbName:${devices?.name}" + deviceList += ["name" : "${roomName} ${devices?.name}", "did" : "${devices?.did}", "type" : "bulb"] + } + }) + } + devices = ["devices" : deviceList] + state.devices = devices.devices +} + +Map devicesDiscovered() { + def devices = state.devices + def map = [:] + if (devices instanceof java.util.Map) { + devices.each { + def value = "${it?.name}" + def key = it?.did + map["${key}"] = value + } + } else { //backwards compatable + devices.each { + def value = "${it?.name}" + def key = it?.did + map["${key}"] = value + } + } + map +} + +def getGatewayData() { + debugOut "In getGatewayData" + + def data = "1${atomicState.token}" + + def qParams = [ + cmd: "GatewayGetInfo", + data: "${data}", + fmt: "json" + ] + + def cmd = toQueryString(qParams) + + apiPost(cmd) { response -> + debugOut "the gateway reponse is ${response.data.gip.gateway}" + } + +} + +def getToken(Closure callback) { + + atomicState.token = "" + + if (password) { + def hashedPassword = generateMD5(password) + + def data = "1${username}${hashedPassword}" + + def qParams = [ + cmd : "GWRLogin", + data: "${data}", + fmt : "json" + ] + + def cmd = toQueryString(qParams) + + apiPost(cmd) { response -> + def status = response.data.gip.rc + + //sendNotificationEvent("Get token status ${status}") + + if (status != "200") {//success code = 200 + def errorText = response.data.gip.error + debugOut "Error logging into TCP Gateway. Error = ${errorText}" + atomicState.token = "error" + } else { + atomicState.token = response.data.gip.token + if(callback){ + callback.call() + } + } + } + } else { + log.warn "Unable to log into TCP Gateway. Error = Password is null" + atomicState.token = "error" + } +} + +def apiPost(String data, Integer retryCount = 0, Closure callback) { + debugOut "In apiPost with data: ${data}" + def params = [ + uri: apiUrl(), + body: data + ] + + httpPost(params) { + response -> + def rc = response.data.gip.rc + + if ( rc == "200" ) { + debugOut ("Return Code = ${rc} = Command Succeeded.") + callback.call(response) + + } else if ( rc.startsWith("4") || rc.startsWith("5") ) { + debugOut "Return Code = ${rc} = Error: Something happened!" //Error code from gateway + sendNotificationEvent("Return Code = ${rc} = Error: Something happened! Retry # ${retryCount}" ) + log.debug "Refreshing Token" + if(retryCount > 5){ + // give up, send a notification + sendNotificationEvent("TCP Lighting is having Communication Errors. Error code = ${rc}. Gave up after ${retryCount} tries") + } + getToken({ -> + def updatedTokenData = data.replaceFirst("[^<]*", '${atomicState.token}') + // try again if we got our token + sendNotificationEvent('re-fetched token, trying again') + + apiPost(updatedTokenData, retryCount++, callback) + }) + //callback.call(response) //stubbed out so getToken works (we had race issue) + + } else { + log.error "Return Code = ${rc} = Error!" //Error code from gateway + sendNotificationEvent("TCP Lighting is having Communication Errors. Error code = ${rc}. Check that TCP Gateway is online") + callback.call(response) + } + } +} + + +//this is not working. TCP power reporting is broken. Leave it here for future fix +def calculateCurrentPowerUse(deviceCapability, usePercentage) { + debugOut "In calculateCurrentPowerUse()" + + debugOut "deviceCapability: ${deviceCapability}" + debugOut "usePercentage: ${usePercentage}" + + def calcPower = usePercentage * 1000 + def reportPower = calcPower.round(1) as String + + debugOut "report power = ${reportPower}" + + return reportPower +} + +def generateSha256(String s) { + + MessageDigest digest = MessageDigest.getInstance("SHA-256") + digest.update(s.bytes) + new BigInteger(1, digest.digest()).toString(16).padLeft(40, '0') +} + +def generateMD5(String s) { + MessageDigest digest = MessageDigest.getInstance("MD5") + digest.update(s.bytes); + new BigInteger(1, digest.digest()).toString(16).padLeft(32, '0') +} + +String toQueryString(Map m) { + return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") +} + +def checkDevicesOnline(bulbs) { + debugOut "In checkDevicesOnline()" + + def onlineBulbs = [] + def thisBulb = [] + + bulbs.each { + def dni = it?.did + thisBulb = it + + def data = "1${atomicState.token}${dni}" + + def qParams = [ + cmd: "DeviceGetInfo", + data: "${data}", + fmt: "json" + ] + + def cmd = toQueryString(qParams) + + def bulbData = [] + + apiPost(cmd) { response -> + bulbData = response.data.gip + } + + if ( bulbData?.offline == "1" ) { + debugOut "${it?.name} is offline with offline value of ${bulbData?.offline}" + + } else { + debugOut "${it?.name} is online with offline value of ${bulbData?.offline}" + onlineBulbs += thisBulb + } + } + return onlineBulbs +} + +def syncronizeDevices() { + debugOut "In syncronizeDevices" + + def update = getChildDevices().findAll { selectedBulbs?.contains(it.deviceNetworkId) } + + update.each { + def dni = getChildDevice( it.deviceNetworkId ) + debugOut "dni = ${dni}" + + if (isRoom(dni)) { + pollRoom(dni) + } else { + poll(dni) + } + } + getToken() + +} + +boolean isRoom(dni) { + def device = state.devices.find() {(( it.type == 'room') && (it.did == "${dni}"))} +} + +boolean isBulb(dni) { + def device = state.devices.find() {(( it.type == 'bulb') && (it.did == "${dni}"))} +} + +def debugEvent(message, displayEvent) { + + def results = [ + name: "appdebug", + descriptionText: message, + displayed: displayEvent + ] + log.debug "Generating AppDebug Event: ${results}" + sendEvent (results) + +} + +def debugOut(msg) { + log.debug msg + //sendNotificationEvent(msg) //Uncomment this for troubleshooting only +} + + +/************************************************************************** + Child Device Call In Methods + **************************************************************************/ +def on(childDevice) { + debugOut "On request from child device" + + def dni = childDevice.device.deviceNetworkId + def data = "" + def cmd = "" + + if ( isRoom(dni) ) { // this is a room, not a bulb + data = "1$atomicState.token${dni}power1" + cmd = "RoomSendCommand" + } else { + data = "1$atomicState.token${dni}power1" + cmd = "DeviceSendCommand" + } + + def qParams = [ + cmd: cmd, + data: "${data}", + fmt: "json" + ] + + cmd = toQueryString(qParams) + + apiPost(cmd) { response -> + debugOut "ON result: ${response.data}" + } + + //we want to ensure syncronization between rooms and bulbs + //runIn(2, "syncronizeDevices") +} + +def off(childDevice) { + debugOut "Off request from child device" + + def dni = childDevice.device.deviceNetworkId + def data = "" + def cmd = "" + + if ( isRoom(dni) ) { // this is a room, not a bulb + data = "1$atomicState.token${dni}power0" + cmd = "RoomSendCommand" + } else { + data = "1$atomicState.token${dni}power0" + cmd = "DeviceSendCommand" + } + + def qParams = [ + cmd: cmd, + data: "${data}", + fmt: "json" + ] + + cmd = toQueryString(qParams) + + apiPost(cmd) { response -> + debugOut "${response.data}" + } + + //we want to ensure syncronization between rooms and bulbs + //runIn(2, "syncronizeDevices") +} + +def setLevel(childDevice, value) { + debugOut "setLevel request from child device" + + def dni = childDevice.device.deviceNetworkId + def data = "" + def cmd = "" + + if ( isRoom(dni) ) { // this is a room, not a bulb + data = "1${atomicState.token}${dni}level${value}" + cmd = "RoomSendCommand" + } else { + data = "1${atomicState.token}${dni}level${value}" + cmd = "DeviceSendCommand" + } + + def qParams = [ + cmd: cmd, + data: "${data}", + fmt: "json" + ] + + cmd = toQueryString(qParams) + + apiPost(cmd) { response -> + debugOut "${response.data}" + } + + //we want to ensure syncronization between rooms and bulbs + //runIn(2, "syncronizeDevices") +} + +// Really not called from child, but called from poll() if it is a room +def pollRoom(dni) { + debugOut "In pollRoom" + def data = "" + def cmd = "" + def roomDeviceData = [] + + data = "1${atomicState.token}${dni}name,power,control,status,state" + cmd = "RoomGetDevices" + + def qParams = [ + cmd: cmd, + data: "${data}", + fmt: "json" + ] + + cmd = toQueryString(qParams) + + apiPost(cmd) { response -> + roomDeviceData = response.data.gip + } + + debugOut "Room Data: ${roomDeviceData}" + + def totalPower = 0 + def totalLevel = 0 + def cnt = 0 + def onCnt = 0 //used to tally on/off states + + roomDeviceData.device.each({ + if ( getChildDevice(it.did) ) { + totalPower += it.other.bulbpower.toInteger() + totalLevel += it.level.toInteger() + onCnt += it.state.toInteger() + cnt += 1 + } + }) + + def avgLevel = totalLevel/cnt + def usingPower = totalPower * (avgLevel / 100) as float + def room = getChildDevice( dni ) + + //the device is a room but we use same type file + sendEvent( dni, [name: "setBulbPower",value:"${totalPower}"] ) //used in child device calcs + + //if all devices in room are on, room is on + if ( cnt == onCnt ) { // all devices are on + sendEvent( dni, [name: "switch",value:"on"] ) + sendEvent( dni, [name: "power",value:usingPower.round(1)] ) + + } else { //if any device in room is off, room is off + sendEvent( dni, [name: "switch",value:"off"] ) + sendEvent( dni, [name: "power",value:0.0] ) + } + + debugOut "Room Using Power: ${usingPower.round(1)}" +} + +def poll(childDevice) { + debugOut "In poll() with ${childDevice}" + + + def dni = childDevice.device.deviceNetworkId + + def bulbData = [] + def data = "" + def cmd = "" + + if ( isRoom(dni) ) { // this is a room, not a bulb + pollRoom(dni) + return + } + + data = "1${atomicState.token}${dni}" + cmd = "DeviceGetInfo" + + def qParams = [ + cmd: cmd, + data: "${data}", + fmt: "json" + ] + + cmd = toQueryString(qParams) + + apiPost(cmd) { response -> + bulbData = response.data.gip + debugOut "This Bulbs Data Return = ${bulbData}" + + def bulb = getChildDevice( dni ) + + //set the devices power max setting to do calcs within the device type + if ( bulbData.other.bulbpower ) + sendEvent( dni, [name: "setBulbPower",value:"${bulbData.other.bulbpower}"] ) + + if (( bulbData.state == "1" ) && ( bulb?.currentValue("switch") != "on" )) + sendEvent( dni, [name: "switch",value:"on"] ) + + if (( bulbData.state == "0" ) && ( bulb?.currentValue("switch") != "off" )) + sendEvent( dni, [name: "switch",value:"off"] ) + + //if ( bulbData.level != bulb?.currentValue("level")) { + // sendEvent( dni, [name: "level",value: "${bulbData.level}"] ) + // sendEvent( dni, [name: "setLevel",value: "${bulbData.level}"] ) + //} + + if (( bulbData.state == "1" ) && ( bulbData.other.bulbpower )) { + def levelSetting = bulbData.level as float + def bulbPowerMax = bulbData.other.bulbpower as float + def calculatedPower = bulbPowerMax * (levelSetting / 100) + sendEvent( dni, [name: "power", value: calculatedPower.round(1)] ) + } + + if (( bulbData.state == "0" ) && ( bulbData.other.bulbpower )) + sendEvent( dni, [name: "power", value: 0.0] ) + + } + + +}