/*
* Dexcom Glucose Monitor Master
*
* 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
* ---- --- ----
*
*/
import groovy.transform.Field
import java.net.URLEncoder
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import java.text.SimpleDateFormat
static String version() { return '0.0.5' }
definition (
name: "Dexcom Master",
namespace: "thebearmay",
author: "Jean P. May, Jr.",
description: "Dexcom Glucose Monitor Integration",
category: "Utility",
importUrl: "https://raw.githubusercontent.com/thebearmay/hubitat/main/dexcom/dexMaster.groovy",
oauth: true,
installOnOpen: true,
iconUrl: "",
iconX2Url: ""
)
preferences {
page name: "mainPage"
}
mappings {
path("/a"){
action: [POST: "authReturn",
GET: "authReturn"]
}
}
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 "apiUri", "enum", title: "API URI", options:[["https://api.dexcom.eu":"EU"],["https://api.dexcom.jp":"Japan"],["https://api.dexcom.com":"US"],["https://sandbox-api.dexcom.com":"Sandbox"]], description:"Select API Location", submitOnChange: true, width:4
input "dexClient", "string", title:"App Client Id
Obtain an App Client Id from Dexcom by registering as a developer", submitOnChange: true, width:4
input "dexSecret", "password", title:"App Client Secret", description:"Enter Secret obtained from the Dexcom App registration", submitOnChange:true, width:4
input "debugEnabled", "bool", title:"Enable Debug", submitOnChange:true, width:4
if(debugEnabled) {
unschedule()
runIn(1800,logsOff)
}
}
section("
Dexcom Authorization
", hideable: false, hidden: false){
if(state.accessToken == null) createAccessToken()
paragraph "Hubitat Access Token: ${state.accessToken}"
input "resetToken", "button", title:"Reset Hubitat Token"
paragraph "Redirect URL: ${getFullApiServerUrl()}/a?access_token=${state.accessToken}&"
paragraph "${"${getFullApiServerUrl()}/a?access_token=${state.accessToken}&".size()} characters"
input "tinyUrl", "string", title:"Redirect Override", description:"Use TinyUrl or similar if redirect exceeds 128 characters", submitOnChange: true, width:4
input "initAuth", "button", title: "Get Dexcom Auth Token"
if (state.iAuthReq){
state.devCount = 0
state.iAuthReq = false
redirect = URLEncoder.encode("${getFullApiServerUrl()}/a?access_token=${state.accessToken}&", "UTF-8")
state.redirect = tinyUrl ? URLEncoder.encode("$tinyUrl","UTF-8") : redirect
iaUri = "$apiUri/v2/oauth2/login?client_id=$dexClient&redirect_uri=${tinyUrl ? URLEncoder.encode("$tinyUrl","UTF-8") : redirect }&response_type=code&scope=offline_access"
if(debugEnabled) paragraph iaUri
paragraph ""
}
/*
if(state.dexAuthCode){
input "secondAuth", "button", title: "Get Dexcom Authorization"
if(state.secAuth == true || state.secAuth == 'true'){
state.secAuth = false
getSecondAuth()
}
}
if(state.dexAccessToken){
input "getDevs", "button", title: "Get Devices"
if(state.reqDev == true || state.reqDev == 'true') {
state.reqDev = false
getDevices()
}
if (state.devCount)
paragraph "${state.devCount} Device(s) Created"
}*/
if (state.devCount)
paragraph "Authorization Received - ${state.devCount} Device(s) Created"
}
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"
}
}
}
}
def getSecondAuth(){
bodyText = "client_id=$dexClient&client_secret=$dexSecret&code=${state.dexAuthCode}&grant_type=authorization_code&redirect_uri=${state.redirect}"
Map requestParams =
[
uri: "$apiUri/v2/oauth2/token",
//textParser: true,
headers: [
"Content-Type" : 'application/x-www-form-urlencoded'
//"Accept" : 'application/json'
],
body: "$bodyText"
]
if(debugEnabled)
log.debug "$requestParams"
try {
httpPost(requestParams) { resp ->
if(debugEnabled) log.debug 'secondAuth'
state.dexAccessToken = resp.data.access_token
state.refreshToken = resp.data.refresh_token
lastRefresh = new Date().getTime()
state.lastRefresh = lastRefresh
state.tExpire = lastRefresh + 7190000 // actual refresh interval is 7200 seconds
getDevices()
}
} catch (ex) {
log.error "secondAuth: $ex"
}
}
def getRefresh(){
bodyText = "client_id=$dexClient&client_secret=$dexSecret&refresh_token=${state.refreshToken}&grant_type=refresh_token&redirect_uri=${state.redirect}"
Map requestParams =
[
uri: "$apiUri/v2/oauth2/token",
//textParser: true,
headers: [
"Content-Type" : 'application/x-www-form-urlencoded'
//"Accept" : 'application/json'
],
body: "$bodyText"
]
if(debugEnabled)
log.debug "$requestParams"
try {
httpPost(requestParams) { resp ->
if(debugEnabled) log.debug 'getRefresh'
state.dexAccessToken = resp.data.access_token
state.refreshToken = resp.data.refresh_token
lastRefresh = new Date().getTime()
state.lastRefresh = lastRefresh
state.tExpire = lastRefresh + 7190000 // actual refresh interval is 7200 seconds
}
} catch (ex) {
log.error "getRefresh: $ex"
}
}
def authReturn(){
if(debugEnabled) log.debug params
pMap = (HashMap) params
if (params['?code']) //sandbox
state.dexAuthCode = params['?code']
else if (params['code']) //live
state.dexAuthCode = params['code']
else
log.error "Unknown response with parameters $params"
runIn(5,'getSecondAuth')
render contentType:'application/json', data:'{"status":200}',status:200
}
def getDevices(){
// /v3/users/self/devices
if(new Date().getTime() > (Long)state.tExpire)
getRefresh()
Map requestParams =
[
uri: "$apiUri/v3/users/self/devices",
headers: [
"Authorization" : "Bearer $state.dexAccessToken",
"Accept" : 'application/json'
]
]
asynchttpGet("processDevices",requestParams)
}
def processDevices(resp, data){
if(debugEnabled) log.debug resp.json
rCount = 0
resp.json.records.each {
if(debugEnabled) log.debug "${it.transmitterId}, ${it.transmitterGeneration}"
if(it.transmitterId != null) {
rCount++
chd = addChildDevice("thebearmay","Dexcom Glucose Monitor","DGM${app.id}-$rCount", [name: "Dexcom ${app.id}-$rCount", isComponent: false, label:"Dexcom ${app.id}-$rCount ${it.transmitterGeneration}"])
chd.updateDataValue("dexUserId", "${resp.json.userId}")
chd.updateDataValue("dexTransmitId", "${it.transmitterId}")
chd.updateDataValue("dexGen", "${it.transmitterGeneration}")
if (it.softwareVersion) chd.updateDataValue("dexSoftVersion", "${it.softwareVersion}")
if (it.softwareNumber) chd.updateDataValue("dexSoftNumber", "${it.softwareNumber}")
}
state.devCount = rCount
}
}
void getGlucose(DNI, glucoseRange) {
// /v3/users/self/egvs
if(new Date().getTime() > (Long)state.tExpire)
getRefresh()
if(glucoseRange == null || glucoseRange < 1)
glucoseRange = 600
Long eDate = new Date().getTime()
Long sDate = eDate - ((Long) glucoseRange * 1000)
sdf = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss")
startDate = URLEncoder.encode(sdf.format(new Date(sDate)),"UTF-8")
endDate = URLEncoder.encode(sdf.format(new Date(eDate)), "UTF-8")
if(debugEnabled) log.debug "start: $startDate end: $endDate"
Map requestParams =
[
uri: "$apiUri/v3/users/self/egvs?startDate=$startDate&endDate=$endDate",
headers: [
"Authorization" : "Bearer $state.dexAccessToken",
"Accept" : 'application/json'
]
]
if(debugEnabled) log.debug "$requestParams"
asynchttpGet("processGlucose",requestParams, [dni:DNI])
}
def processGlucose(resp, data){
if(debugEnabled)
log.debug resp.properties
try {
DNI = data['dni']
chd = getChildDevice("$DNI")
if (resp?.json?.records) {
chd.updateAttr("glucose", resp.json.records[0].value, resp.json.records[0].unit)
chd.updateAttr("glucoseStatus", resp.json.records[0].status)
chd.updateAttr("glucoseTrend", resp.json.records[0].trend)
chd.updateAttr("glucoseRate", resp.json.records[0].trendRate, resp.json.records[0].rateUnit)
} else {
chd.updateAttr("glucoseStatus", "unknown")
}
} catch (ex) {
log.error "Get Glucose: $ex
${resp?.properties}"
}
}
void getAlert(DNI, alertRange) {
// /v3/users/self/alerts
if(new Date().getTime() > (Long)state.tExpire)
getRefresh()
if(alertRange == null || alertRange < 1)
alertRange = 600
Long eDate = new Date().getTime()
Long sDate = eDate - ((Long) alertRange * 1000)
sdf = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss")
startDate = URLEncoder.encode(sdf.format(new Date(sDate)),"UTF-8")
endDate = URLEncoder.encode(sdf.format(new Date(eDate)), "UTF-8")
if(debugEnabled) log.debug "start: $startDate end: $endDate"
Map requestParams =
[
uri: "$apiUri/v3/users/self/alerts?startDate=$startDate&endDate=$endDate",
headers: [
"Authorization" : "Bearer $state.dexAccessToken",
"Accept" : 'application/json'
]
]
if(debugEnabled) log.debug "$requestParams"
asynchttpGet("processAlerts",requestParams, [dni:DNI])
}
def processAlerts(resp, data){
if(debugEnabled)
log.debug resp.properties
try {
DNI = data['dni']
chd = getChildDevice("$DNI")
if (resp?.json?.records) {
alertList = []
resp.json.records.each{
alertList.add(["${it.alertName}":"${it.alertStatus}"])
}
chd.updateAttr("alertJson", JsonOutput.toJson(alertList))
} else {
alertJson = JsonOutput.toJson(["noAlerts":"unknown"])
chd.updateAttr("alertJson", alertJson)
}
} catch (ex) {
log.error "Get Alerts: $ex
${resp?.properties}"
}
}
void appButtonHandler(btn) {
switch(btn) {
case "initAuth":
state.iAuthReq = true
break
case "resetToken":
createAccessToken()
break
case "secondAuth":
state.secAuth = true
break
case "getDevs":
state.reqDev = true
break
default:
log.error "Undefined button $btn pushed"
break
}
}