/* * Air Things Allview Cloud Interface * * Licensed Virtual 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 * ---- --- ---- * 16Oct2022 thebearmay Code cleanup * 16Nov2023 thebearmay Allow removal of a child device outside of app uninstall */ static String version() { return '0.1.5' } import groovy.transform.Field import java.net.URLEncoder import groovy.json.JsonOutput import groovy.json.JsonSlurper import java.text.SimpleDateFormat definition ( name: "Air Things Cloud", namespace: "thebearmay", author: "Jean P. May, Jr.", description: "Air Things Allview Cloud Interface", importUrl: "https://raw.githubusercontent.com/thebearmay/hubitat/main/airthings/airThingsCntrl.groovy", installOnOpen: true, oauth: false, iconUrl: "", iconX2Url: "" ) preferences { page name: "mainPage" } void installed() { if(debugEnabled) log.trace "installed()" state?.isInstalled = true initialize() } void updated(){ if(debugEnabled) log.trace "updated()" if(!state?.isInstalled) { state?.isInstalled = true } if(debugEnabled) runIn(1800,logsOff) } void initialize(){ } void logsOff(){ app.updateSetting("debugEnabled",[value:"false",type:"bool"]) } def mainPage(){ dynamicPage (name: "mainPage", title: "", install: true, uninstall: true) { if (app.getInstallationState() == 'COMPLETE') { section("Main") { input "debugEnabled", "bool", title:"Enable Debug Logging:", submitOnChange:true, required:false, defaultValue:false if(debugEnabled) { unschedule() runIn(1800,logsOff) } } section("Application Credentials", hideable: false, hidden: false){ paragraph "Link to AirThings for ID/Secret" input "userName", "text", title: "Airthings API ID:",width:4 input "pwd", "password", title: "Airthings Secret:", width:4 input "authBtn", "button", title: "Create Initial Authorization" if(state?.authBtnPushed) { state.authBtnPushed = false authToken = getAuth("initialAuth") apiGet("devices") } if(state.temp_token != null) { if(state.temp_token.size() > 50) eos = 50 else eos = state.temp_token.size() paragraph "Token: ${state.temp_token.toString().substring(0,eos)}. . . . ." paragraph "Expires: ${state?.tokenExpiresDisp}" } if(state?.temp_token != null){ input "devBtn", "button", title: "Get Devices" if(state?.devBtnPushed) { state.devBtnPushed = false apiGet("devices") } if (state.numberDevices == null) state.numberDevices = 0 paragraph "Found ${state.numberDevices} devices" } } section("Child Device Maintenance"){ /*input "forceCreate","button", title: "Force Device Creation" if(state?.forceCreatePushed) { state.forceCreatePushed = false createChildDev("Air-FC001", "Forced", "Air-FC001", "Virtual") createChildDev("Air-FC002", "Forced", "Air-FC002", "Virtual") if(!state.numberDevices) state.numberDevices = 2 else state.numberDevices+=2 }*/ if (state?.numberDevices > 0){ chdList = [] getChildDevices().each{ chdList.add("$it") } //log.debug chdList input "removeChild", "enum", title:"Remove child device", defaultValue:"None", submitOnChange:true, options:chdList if(removeChild ){ getChildDevices().each{ if("$it" == "$removeChild") deleteChildDevice(it.getDeviceNetworkId()) } app.updateSetting("removeChild",[value:null,type:"enum"]) } } } section("Reset Application Name", hideable: true, hidden: true){ input "nameOverride", "text", title: "New Name for Application", multiple: false, required: false, submitOnChange: true, defaultValue: app.getLabel() if(nameOverride != app.getLabel()) app.updateLabel(nameOverride) } } else { section("") { paragraph title: "Click Done", "Please click Done to install app before continuing" } } } } //Begin App Authorization void getAuth(command){ bodyMap = [grant_type:"client_credentials",client_id:"$userName", client_secret:"$pwd","scope": ["read:device:current_values"]] def bodyText = JsonOutput.toJson(bodyMap) Map requestParams = [ uri: "https://accounts-api.airthings.com/v1/token", requestContentType: 'application/json', contentType: 'application/json', body: "$bodyText" ] if(debugEnabled) log.debug "$requestParams" httpPost (requestParams) { resp -> if(debugEnabled) log.debug "${resp.properties} - ${command} - ${resp.getStatus()} " if(resp.getStatus() == 200 || resp.getStatus() == 207){ if(resp.data){ Map jsonData = (HashMap) resp.data state.temp_token = jsonData.access_token if(debugEnabled) log.debug "Token: ${jsonData.access_token}" state.tokenExpires = (jsonData.expires_in.toLong()*1000) + new Date().getTime().toLong() SimpleDateFormat sdf= new SimpleDateFormat("HH:mm:ss yyyy-MM-dd") state.tokenExpiresDisp = sdf.format(new Date(state.tokenExpires)) } } } } // End App Authorization // Begin API def apiGet (command){ if(!state.tokenExpires) return if(new Date().getTime().toLong() >= state?.tokenExpires?.toLong() - 3000) //if token has expired or is within 3 seconds of expiring getAuth("reAuth") // commands should take the form "devices/${devId}/optionalParams Map requestParams = [ uri: "https://ext-api.airthings.com/v1/$command", headers: [ Authorization: "Bearer ${state.temp_token}", requestContentType: 'application/json', contentType: 'application/json' ] ] if(debugEnabled) log.debug "$requestParams" asynchttpGet("getApi", requestParams, [cmd:"${command}"]) } def getApi(resp, data){ try { if(debugEnabled) log.debug "$resp.properties - $data.cmd - ${resp.getStatus()}" if(resp.getStatus() == 200 || resp.getStatus() == 207){ if(resp.data){ if(data.cmd == "devices"){ jsonData = (HashMap) resp.json numDev = 0 jsonData.devices.each{ if(debugEnabled) log.debug "${it.id}, ${it.deviceType}, ${it.segment.name}" createChildDev(it.id, it.deviceType, it.segment.name, it.location.name) numDev++ } state.numberDevices = numDev } else if(data.cmd.contains("latest-samples")) { start = data.cmd.indexOf('/')+1 end = data.cmd.indexOf('/',start) devId = data.cmd.substring(start,end) cd = getChildDevice("${app.id}-$devId") jsonData = (HashMap) resp.json cd.dataRefresh(jsonData) } else { log.error "Unhandled Command: '${data.cmd}'" } } } else if(resp.getStatus() == 401) { apiGet("${data.cmd}") } } catch (Exception e) { log.error "getApi - $e.message" } } // End API void createChildDev(devId, devType, devName, devLoc){ if(!this.getChildDevice("${app.id}-$devId")) cd = addChildDevice("thebearmay", "Air Things Device", "${app.id}-$devId", [name: "${devName}", isComponent: true, deviceId:"$devId", label:"$devName"]) else cd = getChildDevice("${app.id}-$devId") cd.updateDataValue("deviceId","$devId") cd.updateDataValue("deviceType","$devType") cd.updateDataValue("location","$devLoc") } void updateChild(devId){ apiGet("devices/${devId}/latest-samples") } void appButtonHandler(btn) { switch(btn) { case ("authBtn"): state.authBtnPushed = true break case ("devBtn"): state.devBtnPushed = true break case ("forceCreate"): state.forceCreatePushed = true break default: if(debugEnabled) log.error "Undefined button $btn pushed" break } } void intialize() { } void uninstalled(){ chdList = getChildDevices() chdList.each{ deleteChildDevice(it.getDeviceNetworkId()) } }