Skip to content

Commit b7bf6b6

Browse files
committed
Canada API fix - complete?
1 parent 8124db9 commit b7bf6b6

File tree

3 files changed

+122
-31
lines changed

3 files changed

+122
-31
lines changed

src/lib/bluelink-regions/base.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import PersistedLog from '../scriptable-utils/io/PersistedLog'
44
const KEYCHAIN_CACHE_KEY = 'egmp-bluelink-cache'
55
export const DEFAULT_STATUS_CHECK_INTERVAL = 3600 * 1000
66
const BLUELINK_LOG_FILE = 'egmp-bluelink-log'
7-
const DEFAULT_API_DOMAIN = 'https://mybluelink.ca/tods/api/'
7+
const DEFAULT_API_HOST = 'mybluelink.ca'
8+
const DEFAULT_API_DOMAIN = `https://${DEFAULT_API_HOST}/tods/api/`
89

910
export interface BluelinkTokens {
1011
accessToken: string
1112
refreshToken?: string
1213
expiry: number
14+
authCookie: string | undefined
1315
}
1416

1517
export interface BluelinkCar {
@@ -54,6 +56,8 @@ export interface RequestProps {
5456
method?: string
5557
noAuth?: boolean
5658
headers?: Record<string, string>
59+
validResponseFunction: (resp: Record<string, any>, data: Record<string, any>) => { valid: boolean; retry: boolean }
60+
noRetry?: boolean
5761
}
5862

5963
export interface DebugLastRequest {
@@ -92,6 +96,7 @@ export class Bluelink {
9296
protected cache: Cache
9397
protected vin: string | undefined
9498
protected statusCheckInterval: number
99+
protected apiHost: string
95100
protected apiDomain: string
96101

97102
protected additionalHeaders: Record<string, string>
@@ -106,6 +111,7 @@ export class Bluelink {
106111
this.config = config
107112
this.vin = vin
108113
this.apiDomain = DEFAULT_API_DOMAIN
114+
this.apiHost = DEFAULT_API_HOST
109115
this.statusCheckInterval = DEFAULT_STATUS_CHECK_INTERVAL
110116
this.additionalHeaders = {}
111117
this.authHeader = 'Authentication'
@@ -127,9 +133,12 @@ export class Bluelink {
127133
return
128134
}
129135
this.cache = cache
136+
await this.refreshLogin()
137+
}
130138

139+
protected async refreshLogin(force?: boolean) {
131140
// if we are here we have logged in successfully at least once and can refresh if supported
132-
if (!this.tokenValid()) {
141+
if (force || !this.tokenValid()) {
133142
let tokens = undefined
134143
if (Object.hasOwn(this, 'refreshTokens')) {
135144
// @ts-ignore - this is why we check the sub-class has this as its not always implemented
@@ -144,6 +153,7 @@ export class Bluelink {
144153
if (!tokens) this.loginFailure = true
145154
else {
146155
this.tokens = tokens as BluelinkTokens
156+
this.cache.token = this.tokens
147157
this.saveCache()
148158
}
149159
}
@@ -288,16 +298,28 @@ export class Bluelink {
288298
}
289299

290300
protected async request(props: RequestProps): Promise<{ resp: { [key: string]: any }; json: any }> {
301+
let requestTokens: BluelinkTokens | undefined = undefined
302+
if (!props.noAuth) {
303+
requestTokens = this.tokens ? this.tokens : this.cache.token
304+
}
305+
291306
const req = new Request(props.url)
292307
req.method = props.method ? props.method : props.data ? 'POST' : 'GET'
293308
req.headers = {
294-
Accept: 'application/json',
309+
Accept: 'application/json, text/plain, */*',
310+
'Accept-Encoding': 'gzip, deflate, br, zstd',
311+
'Accept-Language': 'en-US,en;q=0.9',
295312
...(props.data && {
296313
'Content-Type': 'application/json',
297314
}),
298-
...(!props.noAuth && {
299-
[this.authHeader]: this.tokens ? this.tokens?.accessToken : this.cache.token.accessToken,
300-
}),
315+
...(!props.noAuth &&
316+
requestTokens?.accessToken && {
317+
[this.authHeader]: requestTokens?.accessToken,
318+
}),
319+
...(!props.noAuth &&
320+
requestTokens?.authCookie && {
321+
Cookie: requestTokens.authCookie,
322+
}),
301323
...this.additionalHeaders,
302324
...(props.headers && {
303325
...props.headers,
@@ -319,6 +341,16 @@ export class Bluelink {
319341
if (this.config.debugLogging) await this.logger.log(`Sending request ${JSON.stringify(this.debugLastRequest)}`)
320342
const json = await req.loadJSON()
321343
await this.logger.log(`response ${JSON.stringify(req.response)} data: ${JSON.stringify(json)}`)
344+
345+
const checkResponse = props.validResponseFunction(req.response, json)
346+
if (!props.noRetry && checkResponse.retry) {
347+
// re-auth and call ourselves
348+
await this.refreshLogin(true)
349+
return await this.request({
350+
...props,
351+
noRetry: true,
352+
})
353+
}
322354
return { resp: req.response, json: json }
323355
} catch (error) {
324356
const errorString = `Failed to send request to ${props.url}, request ${JSON.stringify(this.debugLastRequest)} - error ${error}`

src/lib/bluelink-regions/canada.ts

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,30 @@ import {
88
} from './base'
99
import { Config } from '../../config'
1010

11-
const DEFAULT_API_DOMAIN = 'https://mybluelink.ca/tods/api/'
11+
const DEFAULT_API_DOMAIN = 'mybluelink.ca'
1212
const API_DOMAINS: Record<string, string> = {
13-
hyundai: 'https://mybluelink.ca/tods/api/',
14-
kia: 'https://kiaconnect.ca/tods/api/',
13+
hyundai: 'mybluelink.ca',
14+
kia: 'kiaconnect.ca',
1515
}
1616

1717
const MAX_COMPLETION_POLLS = 20
1818

1919
export class BluelinkCanada extends Bluelink {
2020
constructor(config: Config, statusCheckInterval?: number) {
2121
super(config)
22-
this.apiDomain = config.manufacturer
22+
this.apiHost = config.manufacturer
2323
? this.getApiDomain(config.manufacturer, API_DOMAINS, DEFAULT_API_DOMAIN)
2424
: DEFAULT_API_DOMAIN
25+
this.apiDomain = `https://${this.apiHost}/tods/api/`
2526
this.statusCheckInterval = statusCheckInterval || DEFAULT_STATUS_CHECK_INTERVAL
2627
this.additionalHeaders = {
27-
from: 'CWP',
28+
from: 'SPA',
29+
client_id: 'HATAHSPACA0232141ED9722C67715A0B',
30+
client_secret: 'CLISCR01AHSPA',
2831
language: '0',
32+
brand: this.apiHost === 'mybluelink.ca' ? 'H' : 'kia',
2933
offset: `-${new Date().getTimezoneOffset() / 60}`,
34+
'User-Agent': 'MyHyundai/2.0.25 (iPhone; iOS 18.3; Scale/3.00)',
3035
}
3136
this.authHeader = 'Accesstoken'
3237
this.tempLookup = {
@@ -64,26 +69,61 @@ export class BluelinkCanada extends Bluelink {
6469
return obj
6570
}
6671

67-
private requestResponseValid(payload: any): boolean {
72+
private requestResponseValid(
73+
resp: Record<string, any>,
74+
payload: Record<string, any>,
75+
): { valid: boolean; retry: boolean } {
6876
if (Object.hasOwn(payload, 'responseHeader') && payload.responseHeader.responseCode == 0) {
69-
return true
77+
return { valid: true, retry: false }
7078
}
71-
return false
79+
if (Object.hasOwn(payload, 'responseHeader') && payload.responseHeader.responseCode == 1) {
80+
// check failure
81+
if (
82+
Object.hasOwn(payload, 'error') &&
83+
Object.hasOwn(payload.error, 'errorDesc') &&
84+
(payload.error.errorDesc.toLocaleString().includes('expired') ||
85+
payload.error.errorDesc.toLocaleString().includes('deleted') ||
86+
payload.error.errorDesc.toLocaleString().includes('ip validation'))
87+
) {
88+
return { valid: false, retry: true }
89+
}
90+
}
91+
return { valid: false, retry: false }
92+
}
93+
94+
protected async getSessionCookie(): Promise<string> {
95+
const req = new Request(`https://${this.apiHost}/login`)
96+
req.headers = this.additionalHeaders
97+
req.method = 'GET'
98+
await req.load()
99+
for (const cookie of req.response.cookies) {
100+
if (cookie.name === 'dtCookie') {
101+
return `dtCookie=${cookie.value}`
102+
}
103+
}
104+
return ''
72105
}
73106

74107
protected async login(): Promise<BluelinkTokens | undefined> {
108+
// get cookie
109+
const cookieValue = await this.getSessionCookie()
75110
const resp = await this.request({
76111
url: this.apiDomain + 'v2/login',
77112
data: JSON.stringify({
78113
loginId: this.config.auth.username,
79114
password: this.config.auth.password,
80115
}),
116+
headers: {
117+
Cookie: cookieValue,
118+
},
81119
noAuth: true,
120+
validResponseFunction: this.requestResponseValid,
82121
})
83-
if (this.requestResponseValid(resp.json)) {
122+
if (this.requestResponseValid(resp.resp, resp.json).valid) {
84123
return {
85124
accessToken: resp.json.result.token.accessToken,
86125
expiry: Math.floor(Date.now() / 1000) + resp.json.result.token.expireIn, // we only get a expireIn not a actual date
126+
authCookie: cookieValue,
87127
}
88128
}
89129

@@ -98,8 +138,9 @@ export class BluelinkCanada extends Bluelink {
98138
data: JSON.stringify({
99139
vehicleId: id,
100140
}),
141+
validResponseFunction: this.requestResponseValid,
101142
})
102-
if (!this.requestResponseValid(resp.json)) {
143+
if (!this.requestResponseValid(resp.resp, resp.json).valid) {
103144
const error = `Failed to set car ${id}: ${JSON.stringify(resp.json)} request ${JSON.stringify(this.debugLastRequest)}`
104145
if (this.config.debugLogging) await this.logger.log(error)
105146
throw Error(error)
@@ -110,8 +151,9 @@ export class BluelinkCanada extends Bluelink {
110151
const resp = await this.request({
111152
url: this.apiDomain + 'vhcllst',
112153
method: 'POST',
154+
validResponseFunction: this.requestResponseValid,
113155
})
114-
if (this.requestResponseValid(resp.json) && resp.json.result.vehicles.length > 0) {
156+
if (this.requestResponseValid(resp.resp, resp.json).valid && resp.json.result.vehicles.length > 0) {
115157
let vehicle = resp.json.result.vehicles[0]
116158
if (this.vin) {
117159
for (const v of resp.json.result.vehicles) {
@@ -202,9 +244,10 @@ export class BluelinkCanada extends Bluelink {
202244
headers: {
203245
Vehicleid: id,
204246
},
247+
validResponseFunction: this.requestResponseValid,
205248
})
206249

207-
if (this.requestResponseValid(resp.json)) {
250+
if (this.requestResponseValid(resp.resp, resp.json).valid) {
208251
return forceUpdate
209252
? this.returnCarStatus(resp.json.result.status, forceUpdate, resp.json.result.status.odometer)
210253
: this.returnCarStatus(resp.json.result.status, forceUpdate, resp.json.result.vehicle.odometer)
@@ -223,8 +266,10 @@ export class BluelinkCanada extends Bluelink {
223266
data: JSON.stringify({
224267
pin: this.config.auth.pin,
225268
}),
269+
validResponseFunction: this.requestResponseValid,
270+
headers: {},
226271
})
227-
if (this.requestResponseValid(resp.json)) {
272+
if (this.requestResponseValid(resp.resp, resp.json).valid) {
228273
return resp.json.result.pAuth
229274
}
230275
const error = `Failed to get auth code: ${JSON.stringify(resp.json)} request ${JSON.stringify(this.debugLastRequest)}`
@@ -248,9 +293,10 @@ export class BluelinkCanada extends Bluelink {
248293
Pauth: authCode,
249294
TransactionId: transactionId,
250295
},
296+
validResponseFunction: this.requestResponseValid,
251297
})
252298

253-
if (!this.requestResponseValid(resp.json)) {
299+
if (!this.requestResponseValid(resp.resp, resp.json).valid) {
254300
const error = `Failed to poll for command completion: ${JSON.stringify(resp.json)} request ${JSON.stringify(this.debugLastRequest)}`
255301
if (this.config.debugLogging) await this.logger.log(error)
256302
throw Error(error)
@@ -297,8 +343,9 @@ export class BluelinkCanada extends Bluelink {
297343
Vehicleid: id,
298344
Pauth: authCode,
299345
},
346+
validResponseFunction: this.requestResponseValid,
300347
})
301-
if (this.requestResponseValid(resp.json)) {
348+
if (this.requestResponseValid(resp.resp, resp.json).valid) {
302349
const transactionId = resp.resp.headers.transactionId
303350
return await this.pollForCommandCompletion(id, authCode, transactionId)
304351
}
@@ -331,8 +378,9 @@ export class BluelinkCanada extends Bluelink {
331378
Vehicleid: id,
332379
Pauth: authCode,
333380
},
381+
validResponseFunction: this.requestResponseValid,
334382
})
335-
if (this.requestResponseValid(resp.json)) {
383+
if (this.requestResponseValid(resp.resp, resp.json).valid) {
336384
const transactionId = resp.resp.headers.transactionId
337385
return await this.pollForCommandCompletion(id, authCode, transactionId)
338386
}
@@ -374,8 +422,9 @@ export class BluelinkCanada extends Bluelink {
374422
Vehicleid: id,
375423
Pauth: authCode,
376424
},
425+
validResponseFunction: this.requestResponseValid,
377426
})
378-
if (this.requestResponseValid(resp.json)) {
427+
if (this.requestResponseValid(resp.resp, resp.json).valid) {
379428
const transactionId = resp.resp.headers.transactionId
380429
return await this.pollForCommandCompletion(id, authCode, transactionId)
381430
}
@@ -397,8 +446,9 @@ export class BluelinkCanada extends Bluelink {
397446
Vehicleid: id,
398447
Pauth: authCode,
399448
},
449+
validResponseFunction: this.requestResponseValid,
400450
})
401-
if (this.requestResponseValid(resp.json)) {
451+
if (this.requestResponseValid(resp.resp, resp.json).valid) {
402452
const transactionId = resp.resp.headers.transactionId
403453
return await this.pollForCommandCompletion(id, authCode, transactionId)
404454
}

src/lib/bluelink-regions/usa.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,14 @@ export class BluelinkUSA extends Bluelink {
7676
return obj
7777
}
7878

79-
private requestResponseValid(resp: Record<string, any>): boolean {
79+
private requestResponseValid(
80+
resp: Record<string, any>,
81+
_data: Record<string, any>,
82+
): { valid: boolean; retry: boolean } {
8083
if (Object.hasOwn(resp, 'statusCode') && resp.statusCode === 200) {
81-
return true
84+
return { valid: true, retry: false }
8285
}
83-
return false
86+
return { valid: false, retry: false }
8487
}
8588

8689
private carHeaders(): Record<string, string> {
@@ -99,12 +102,14 @@ export class BluelinkUSA extends Bluelink {
99102
password: this.config.auth.password,
100103
}),
101104
noAuth: true,
105+
validResponseFunction: this.requestResponseValid,
102106
})
103-
if (this.requestResponseValid(resp.resp)) {
107+
if (this.requestResponseValid(resp.resp, resp.json).valid) {
104108
return {
105109
accessToken: resp.json.access_token,
106110
refreshToken: resp.json.refresh_token,
107111
expiry: Math.floor(Date.now() / 1000) + resp.json.expires_in, // we only get a expireIn not a actual date
112+
authCookie: undefined,
108113
}
109114
}
110115

@@ -120,12 +125,14 @@ export class BluelinkUSA extends Bluelink {
120125
refresh_token: this.cache.token.refreshToken,
121126
}),
122127
noAuth: true,
128+
validResponseFunction: this.requestResponseValid,
123129
})
124-
if (this.requestResponseValid(resp.resp)) {
130+
if (this.requestResponseValid(resp.resp, resp.json).valid) {
125131
return {
126132
accessToken: resp.json.access_token,
127133
refreshToken: resp.json.refresh_token,
128134
expiry: Math.floor(Date.now() / 1000) + resp.json.expires_in, // we only get a expireIn not a actual date
135+
authCookie: undefined,
129136
}
130137
}
131138

@@ -137,8 +144,9 @@ export class BluelinkUSA extends Bluelink {
137144
protected async getCar(): Promise<BluelinkCar> {
138145
const resp = await this.request({
139146
url: this.apiDomain + `ac/v2/enrollment/details/${this.config.auth.username}`,
147+
validResponseFunction: this.requestResponseValid,
140148
})
141-
if (this.requestResponseValid(resp.resp) && resp.json.enrolledVehicleDetails.length > 0) {
149+
if (this.requestResponseValid(resp.resp, resp.json).valid && resp.json.enrolledVehicleDetails.length > 0) {
142150
let vehicle = resp.json.enrolledVehicleDetails[0].vehicleDetails
143151
if (this.vin) {
144152
for (const v of resp.json.enrolledVehicleDetails) {
@@ -206,9 +214,10 @@ export class BluelinkUSA extends Bluelink {
206214
...this.carHeaders(),
207215
refresh: forceUpdate ? 'true' : 'false',
208216
},
217+
validResponseFunction: this.requestResponseValid,
209218
})
210219

211-
if (this.requestResponseValid(resp.resp)) {
220+
if (this.requestResponseValid(resp.resp, resp.json).valid) {
212221
return this.returnCarStatus(resp.json.vehicleStatus, forceUpdate)
213222
}
214223

0 commit comments

Comments
 (0)