)state.mowerData ?: [:]
Map mower= mdata && dni ? mdata[dni] : null
String typ=evt.type
if(mower && typ){
switch(typ){
case 'status-event':
case 'positions-event':
case 'settings-event':
if(evt.attributes){
Map ma= (Map)mdata[dni].attributes
((Map)evt.attributes).each {
LOG("wsEvtHandler - type: ${typ} key: ${it.key} value: ${it.value}", 4, sDEBUG)
if((String)it.key in ['cuttingHeight','headlight']){
Map mas= (Map)ma.settings
mas[it.key]=it.value
didChg=true
}else{
if((String)it.key in ['calendar','positions','battery','mower','metadata','planner','statistics']){
ma[it.key]=it.value
didChg=true
}else{
LOG("wsEvtHandler NOT FOUND - type: ${typ} key: ${it.key} value: ${it.value}", 4, sDEBUG)
}
}
}
//LOG("wsEvtHandler mdata[${dni}]: ${mdata[dni]}", 4, sDEBUG)
//LOG("wsEvtHandler ma: ${ma}", 4, sDEBUG)
mdata[dni].attributes=ma
}
break
default:
LOG("wsEvtHandler NO MATCH - type: ${typ} evt: ${evt}", 4, sDEBUG)
}
if(fndMower && didChg && mowerLoc){
mowerLoc[dni]= getMowerLocation(mower)
state.mowersLocation= mowerLoc
}
}else{
if(dni) LOG("wsEvtHandler NO mower or type - type: ${typ} mower: ${mower}", 4, sDEBUG)
}
state.mowerData=mdata
//log.debug "resp: ${state.mowerData}"
if(fndMower && didChg){
updTsVal("getAutoUpdDt")
updateLastPoll(true)
Boolean a= updateMowerChildren()
}
}
String getCookieS(){
return (String)state.authToken
}
static String getCallbackUrl() { return "https://cloud.hubitat.com/oauth/stateredirect" }
static String getMowerApiEndpoint() { return "https://api.amc.husqvarna.dev/v1" }
static String getApiEndpoint() { return "https://api.authentication.husqvarnagroup.dev/v1/oauth2" }
//static String getWssEndpoint() { return "wss://ws.openapi.husqvarna.dev/v1"}
//String getBuildRedirectUrl() { return "${serverUrl}/oauth/stateredirect?access_token=${state.accessToken}" }
String getStateUrl() { return "${getHubUID()}/apps/${app?.id}/callback?access_token=${state?.accessToken}" }
String getHusqvarnaApiKey(){ return (String)settings.apiKey }
String getHusqvarnaApiSecret(){ return (String)settings.apiSecret }
// OAuth Init URL
String oauthInitUrl(){
LOG("oauthInitUrl", 4)
state.oauthInitState=getStateUrl() // HE does redirect a little differently
//log.debug "oauthInitState: ${state.oauthInitState}"
Map oauthParams=[
response_type: "code",
client_id: getHusqvarnaApiKey(), // actually, the AutoMower Manager app's client ID
scope: "iam:read amc:api", // was app
redirect_uri: getCallbackUrl(),
state: state.oauthInitState
]
String res= getApiEndpoint()+"/authorize?${toQueryString(oauthParams)}"
LOG("oauthInitUrl - location: ${res}", 4, sDEBUG)
return res
}
void parseAuthResponse(resp){
String msgH="Display http response | "
//log.debug "response data: ${myObj(resp.data)} ${resp.data}"
String str
str=sBLANK
resp.data.each{
str += "\n${it.key} --> ${it.value}, "
}
LOG(msgH+"response data: ${str}",4,sDEBUG)
LOG(msgH+"response data object type: ${myObj(resp.data)}",4,sDEBUG)
str=sBLANK
resp.getHeaders().each{
str += "\n${it.name}: ${it.value}, "
}
log.debug msgH+"response headers: ${str}"
log.debug msgH+"isSuccess: ${resp.isSuccess()} | statucode: ${resp.status}"
//str=sBLANK
//log.debug "resp param ${resp.params}"
//resp.params.each{ str += "${it.name}: ${it.value}"}
//log.debug "response params: ${str}"
}
private static String encodeURIComponent(value){
// URLEncoder converts spaces to + which is then indistinguishable from any
// actual + characters in the value. Match encodeURIComponent in ECMAScript
// which encodes "a+b c" as "a+b%20c" rather than URLEncoder's "a+b+c"
return URLEncoder.encode(
"${value}".toString().replaceAll('\\+','__wc_plus__'),
'UTF-8'
).replaceAll('\\+','%20').replaceAll('__wc_plus__','+')
}
def callback(){
LOG("callback()>> params: ${params}" /* params.code ${params.code}, params.state ${params.state}, state.oauthInitState ${state.oauthInitState}"*/, 4, sDEBUG)
def code=params.code
String oauthState=params.state
String eMsg
eMsg= sNULL
if(oauthState == state.oauthInitState){
LOG("callback() --> States matched!", 4)
Map rdata=[
grant_type: "authorization_code",
code : code,
client_id : getHusqvarnaApiKey(),
client_secret : getHusqvarnaApiSecret(),
state : oauthState,
redirect_uri: callbackUrl,
]
//String tokenUrl=getApiEndpoint()+"/token?${toQueryString(tokenParams)}"
String tokenUrl=getApiEndpoint()+"/token"
String data=rdata.collect{ String k,v -> encodeURIComponent(k)+'='+encodeURIComponent(v) }.join('&')
Map reqP=[
uri: tokenUrl,
query: null,
contentType: "application/x-www-form-urlencoded",
// requestContentType: "application/json",
body: data,
timeout: 30
]
LOG("callback()-->reqP ${reqP}", 4)
try{
httpPost(reqP){ resp ->
if(resp && resp.data && resp.isSuccess()){
// parseAuthResponse(resp)
String kk
resp.data.each{ kk=it.key }
Map ndata=(Map)new JsonSlurper().parseText(kk)
log.debug "ndata : ${ndata}"
state.refreshToken=ndata.refresh_token
state.authToken=ndata.access_token
Long tt= wnow() + (ndata.expires_in * 1000)
//log.error "tt is ${tt}"
state.authTokenExpires=tt
atomicState.refreshToken=ndata.refresh_token
atomicState.authToken=ndata.access_token
//atomicState.authTokenExpires=tt
//log.error "state.authTokenExpires is ${state.authTokenExpires}"
LOG("Expires in ${ndata.expires_in} seconds", 3)
LOG("swapped token: $ndata; state.refreshToken: ${state.refreshToken}; state.authToken: ${(String)state.authToken}", 3)
state.remove('oauthInitState')
eMsg= success()
def dev= getSocketDevice()
if(dev){
dev.updateCookies(ndata.access_token)
if(!(Boolean)dev.isSocketActive()){ dev.triggerInitialize() }
}
}else{ eMsg= fail() }
}
} catch(Exception e){
LOG("auth callback()", 1, sERROR, e)
//if(resp) parseAuthResponse(resp)
eMsg= fail()
}
}else{
LOG("callback() failed oauthState != state.oauthInitState", 1, sWARN)
eMsg= fail()
}
render contentType: 'text/html', data: eMsg
}
static String success(){
String message="""
Your AutoConnect Account is now connected!
Close this window and click 'Done' to finish setup.
"""
return connectionStatus(message)
}
static String fail(){
String message="""
The connection could not be established!
Close this window and click 'Done' to return to the menu.
"""
return connectionStatus(message)
}
static String connectionStatus(String message, Boolean close=false){
String redirectHtml= close ? """""" : sBLANK
/*String redirectHtml=sBLANK
if(redirectUrl){
redirectHtml="""
"""
} */
String hubIcon='https://raw.githubusercontent.com/SANdood/Icons/master/Hubitat/HubitatLogo.png'
String html="""
Husqvarna connection
${redirectHtml}
""".toString()
return html
}
static String myObj(obj){
if(obj instanceof String){return 'String'}
else if(obj instanceof Map){return 'Map'}
else if(obj instanceof List){return 'List'}
else if(obj instanceof ArrayList){return 'ArrayList'}
else if(obj instanceof Integer){return 'Int'}
else if(obj instanceof BigInteger){return 'BigInt'}
else if(obj instanceof Long){return 'Long'}
else if(obj instanceof Boolean){return 'Bool'}
else if(obj instanceof BigDecimal){return 'BigDec'}
else if(obj instanceof Float){return 'Float'}
else if(obj instanceof Byte){return 'Byte'}
else if(obj instanceof ByteArrayInputStream){return 'ByteArrayInputStream'}
else{ return 'unknown'}
}
Boolean weAreLost(String msgH, String meth){
String msg
msg= sBLANK
if(!(String)state.authToken){
apiLost(msgH+"weAreLost() found no auth token, called by ${meth}")
}
if(apiConnected() == sLOST){
msg += "found connection lost to husqvarna | "
if( refreshAuthToken(meth) ){
msg += " - Was able to recover the lost connection. Please ignore any notifications received. | "
LOG(msgH+msg, 4, sINFO)
}else{
msg += " - Unable to refresh token and get mowers do to loss of API Connection. Please ensure you are authorized."
LOG(msgH+msg, 1, sERROR)
return true
}
}
return false
}
// Get the list of mowers for use in the settings pages
Map getAutoMowers(Boolean frc=false, String meth="followup", Boolean isRetry=false){
String msgH="getAutoMowers(force: $frc, calledby: $meth, isRetry: $isRetry) | "
if(debugLevel(4)){ LOG(msgH+"====> entered ",4,sTRACE) }
else LOG(msgH, 3,sTRACE)
if(weAreLost(msgH, 'getAutoMowers')){
return null
}
Map res
res=[:]
String cached,msg
cached=sBLANK
Boolean skipIt
skipIt=false
Boolean myfrc=(!state.mowerData || !state.mowersWithNames)
Integer lastU=getLastTsValSecs("getAutoUpdDt") // last attempt
if( (frc && lastU < 60)){ skipIt=true }
if( (!frc && lastU < 150) ){ skiptIt=true } // related to getMinMinsBtwPolls
Map mowers
mowers=[:]
Map mowersLocation
mowersLocation=[:]
msg=sBLANK
if(myfrc || !skipIt){
updTsVal("getAutoUpdDt")
Map deviceListParams=[
uri: getMowerApiEndpoint() +"/mowers",
headers: [
"Content-Type": "application/vnd.api+json",
"Authorization": "Bearer ${(String)state.authToken}",
"Authorization-Provider": "husqvarna",
"X-Api-Key":getHusqvarnaApiKey()
],
query: null,
timeout: 30
]
if(debugLevel(4)){
msg+="http params -- ${deviceListParams} "
}
msg +="HTTPGET "
if(msg){
LOG(msgH + msg, 3, sTRACE)
msg=sBLANK
}
Boolean exitout
exitout=false
try{
httpGet(deviceListParams){ resp ->
LOG(msgH + "httpGet() ${resp.status} Response", 4, sTRACE)
String rdata
Map adata
if(resp){
rdata=resp.data.text // need to save first time since it is a ByteArrayInputStream
if(rdata) adata=(Map)new JsonSlurper().parseText(rdata)
}
if(resp && resp.isSuccess() && resp.status == 200 && adata){
List