Skip to content

Commit e6793de

Browse files
committed
- Check for login failure and handle via presenting setting screen
- Handle Kia API endpoint and config entry for manufacturer - Add VIN to configuration screen / config to handle multiple car situation
1 parent e0cf5e3 commit e6793de

File tree

9 files changed

+130
-53
lines changed

9 files changed

+130
-53
lines changed

docs/app-assets/car-images/ev6.png

66.9 KB
Loading

src/app.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
dateStringOptions,
1414
getChargeCompletionString,
1515
} from 'lib/util'
16-
import { initRegionalBluelink } from './lib/bluelink'
1716

1817
interface updatingActions {
1918
status?: {
@@ -58,8 +57,7 @@ const { present, connect, setState } = getTable<{
5857

5958
const MIN_API_REFRESH_TIME = 900000 // 15 minutes
6059

61-
export async function createApp(config: Config) {
62-
const bl = await initRegionalBluelink(config)
60+
export async function createApp(config: Config, bl: Bluelink) {
6361
await loadTintedIcons()
6462

6563
// not blocking call - render UI with last cache and then update from a non forced remote call (i.e. to server but not to car)

src/config.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@ export interface Auth {
1010
}
1111

1212
export interface Config {
13+
manufacturer: string | undefined
1314
auth: Auth
1415
tempType: 'C' | 'F'
1516
climateTempWarm: number
1617
climateTempCold: number
1718
allowWidgetRemoteRefresh: boolean
1819
debugLogging: boolean
20+
vin: string | undefined
1921
}
2022

2123
export interface FlattenedConfig {
24+
manufacturer: string | undefined
2225
username: string
2326
password: string
2427
pin: string
@@ -28,9 +31,11 @@ export interface FlattenedConfig {
2831
climateTempCold: number
2932
allowWidgetRemoteRefresh: boolean
3033
debugLogging: boolean
34+
vin: string | undefined
3135
}
3236

3337
const SUPPORTED_REGIONS = ['canada']
38+
const SUPPORTED_MANUFACTURERS = ['Hyundai', 'Kia']
3439

3540
const DEFAULT_TEMPS = {
3641
C: {
@@ -55,6 +60,7 @@ const DEFAULT_CONFIG = {
5560
climateTempWarm: DEFAULT_TEMPS.C.warm,
5661
debugLogging: false,
5762
allowWidgetRemoteRefresh: false,
63+
manufacturer: undefined,
5864
} as Config
5965

6066
export function configExists(): boolean {
@@ -83,12 +89,16 @@ export function getConfig(): Config {
8389
if (Keychain.contains(KEYCHAIN_BLUELINK_CONFIG_KEY)) {
8490
config = JSON.parse(Keychain.get(KEYCHAIN_BLUELINK_CONFIG_KEY))
8591
}
86-
if (!config) {
92+
if (!config || !configValid) {
8793
config = DEFAULT_CONFIG
8894
}
8995
return config
9096
}
9197

98+
function configValid(config: Config): boolean {
99+
return config && Object.hasOwn(config, 'auth')
100+
}
101+
92102
export async function loadConfigScreen() {
93103
return await form<FlattenedConfig>({
94104
title: 'Bluelink Configuration settings',
@@ -103,6 +113,8 @@ export async function loadConfigScreen() {
103113
climateTempCold,
104114
debugLogging,
105115
allowWidgetRemoteRefresh,
116+
manufacturer: manufacturer,
117+
vin: vin,
106118
}) => {
107119
setConfig({
108120
auth: {
@@ -116,6 +128,8 @@ export async function loadConfigScreen() {
116128
climateTempWarm: climateTempWarm,
117129
allowWidgetRemoteRefresh: allowWidgetRemoteRefresh,
118130
debugLogging: debugLogging,
131+
manufacturer: manufacturer?.toLowerCase(),
132+
vin: vin ? vin.toUpperCase() : undefined,
119133
} as Config)
120134
},
121135
onStateChange: (state, previousState): Partial<FlattenedConfig> => {
@@ -172,6 +186,18 @@ export async function loadConfigScreen() {
172186
allowCustom: false,
173187
isRequired: true,
174188
},
189+
manufacturer: {
190+
type: 'dropdown',
191+
label: 'Choose your Car Manufacturer',
192+
options: SUPPORTED_MANUFACTURERS,
193+
allowCustom: false,
194+
isRequired: false,
195+
},
196+
vin: {
197+
type: 'textInput',
198+
label: 'Optional VIN of car',
199+
isRequired: false,
200+
},
175201
tempType: {
176202
type: 'dropdown',
177203
label: 'Choose your preferred temperature scale',

src/index.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,36 @@
1+
import { initRegionalBluelink } from 'lib/bluelink'
12
import { createWidget } from 'widget'
23
import { createApp } from 'app'
34
import { processSiriRequest } from 'siri'
45
import { getConfig, loadConfigScreen, configExists } from 'config'
6+
import { confirm } from './lib/scriptable-utils'
57
;(async () => {
6-
if (config.runsInWidget && configExists()) {
7-
const widget = await createWidget(getConfig())
8+
if (!configExists() && (config.runsWithSiri || config.runsInWidget)) return
9+
const blConfig = getConfig()
10+
const bl = await initRegionalBluelink(blConfig)
11+
12+
if (!bl || bl.loginFailed()) {
13+
if (config.runsWithSiri || config.runsInWidget) {
14+
return
15+
}
16+
await confirm('Login Failed - please re-check your credentials', {
17+
confirmButtonTitle: 'Ok',
18+
includeCancel: false,
19+
})
20+
await loadConfigScreen()
21+
return
22+
}
23+
24+
if (config.runsInWidget) {
25+
const widget = await createWidget(blConfig, bl)
826
Script.setWidget(widget)
927
Script.complete()
10-
} else if (config.runsWithSiri && configExists()) {
11-
Script.setShortcutOutput(await processSiriRequest(getConfig(), args.shortcutParameter))
28+
} else if (config.runsWithSiri) {
29+
Script.setShortcutOutput(await processSiriRequest(blConfig, bl, args.shortcutParameter))
1230
Script.complete()
1331
} else {
1432
try {
15-
const resp = configExists() ? await createApp(getConfig()) : await loadConfigScreen()
33+
const resp = await createApp(blConfig, bl)
1634
// @ts-ignore - undocumented api
1735
App.close() // add this back after dev
1836
return resp

src/lib/bluelink-regions/base.ts

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ 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/'
78

89
export interface BluelinkTokens {
910
accessToken: string
@@ -77,7 +78,8 @@ export interface ClimateRequest {
7778

7879
const carImageHttpURL = 'https://bluelink.andyfase.com/app-assets/car-images/'
7980
const carImageMap: Record<string, string> = {
80-
ioniq5: 'ioniq5.png',
81+
'ioniq 5': 'ioniq5.png',
82+
'ev 6': 'ev6.png',
8183
default: 'ioniq5.png',
8284
}
8385

@@ -88,37 +90,61 @@ export class Bluelink {
8890
protected cache: Cache
8991
protected vin: string | undefined
9092
protected statusCheckInterval: number
93+
protected apiDomain: string
9194

9295
protected additionalHeaders: Record<string, string>
9396
protected authHeader: string
9497
protected tempLookup: TempConversion | undefined
9598
protected tokens: BluelinkTokens | undefined
9699
protected debugLastRequest: DebugLastRequest | undefined
97100
protected logger: any
101+
protected loginFailure: boolean
98102

99103
constructor(config: Config, vin?: string) {
100104
this.vin = vin
105+
this.apiDomain = DEFAULT_API_DOMAIN
101106
this.statusCheckInterval = DEFAULT_STATUS_CHECK_INTERVAL
102107
this.additionalHeaders = {}
103108
this.authHeader = 'Authentication'
104109
this.tokens = undefined
110+
this.loginFailure = false
105111
this.debugLastRequest = undefined
106112
this.tempLookup = undefined
107113
this.logger = PersistedLog(BLUELINK_LOG_FILE)
108114
}
109115

110-
protected async superInit(config: Config, vin?: string, statusCheckInterval?: number) {
116+
protected async superInit(config: Config, statusCheckInterval?: number) {
111117
this.config = config
112-
this.vin = vin
118+
this.vin = this.config.vin
113119
this.statusCheckInterval = statusCheckInterval || DEFAULT_STATUS_CHECK_INTERVAL
114120

115-
this.cache = await this.loadCache()
121+
const cache = await this.loadCache()
122+
if (!cache) {
123+
this.loginFailure = true
124+
return
125+
}
126+
this.cache = cache
116127
if (!this.tokenValid()) {
117-
this.cache.token = await this.login()
118-
this.saveCache()
128+
const tokens = await this.login()
129+
if (!tokens) this.loginFailure = true
130+
else {
131+
this.tokens = tokens
132+
this.saveCache()
133+
}
119134
}
120135
}
121136

137+
protected getApiDomain(lookup: string, domains: Record<string, string>, _default: string): string {
138+
for (const [key, domain] of Object.entries(domains)) {
139+
if (key === lookup) return domain
140+
}
141+
return _default
142+
}
143+
144+
public loginFailed(): boolean {
145+
return this.loginFailure
146+
}
147+
122148
public getCachedStatus(): Status {
123149
return {
124150
car: this.cache.car,
@@ -216,14 +242,19 @@ export class Bluelink {
216242
Keychain.set(KEYCHAIN_CACHE_KEY, JSON.stringify(this.cache))
217243
}
218244

219-
protected async loadCache(): Promise<Cache> {
245+
protected async loadCache(): Promise<Cache | undefined> {
220246
let cache: Cache | undefined = undefined
221247
if (Keychain.contains(KEYCHAIN_CACHE_KEY)) {
222248
cache = JSON.parse(Keychain.get(KEYCHAIN_CACHE_KEY))
223249
}
224250
if (!cache) {
225251
// initial use - load car and status
226-
this.tokens = await this.login()
252+
const tokens = await this.login()
253+
if (!tokens) {
254+
this.loginFailure = true
255+
return
256+
}
257+
this.tokens = tokens
227258
const car = await this.getCar()
228259
cache = {
229260
token: this.tokens,
@@ -314,7 +345,7 @@ export class Bluelink {
314345
})
315346
}
316347

317-
protected async login(): Promise<BluelinkTokens> {
348+
protected async login(): Promise<BluelinkTokens | undefined> {
318349
// implemented in country specific sub-class
319350
throw Error('Not Implemented')
320351
}

0 commit comments

Comments
 (0)