homebridge-kia-eu 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # homebridge-kia-eu
2
+
3
+ Homebridge plugin for **Kia Connect (UVO) vehicles in Europe**, powered by
4
+ [bluelinky](https://github.com/Hacksore/bluelinky).
5
+
6
+ This is an EU adaptation of [`jfriend615/homebridge-kia`](https://github.com/jfriend615/homebridge-kia)
7
+ (which targets the US Kia Connect API). The HomeKit accessory layer is shared; the
8
+ Kia API client was replaced with `bluelinky` so it speaks to Kia's European servers
9
+ (OAuth2 + control PIN + request stamping), and remote climate uses Celsius.
10
+
11
+ > ⚠️ The EU Kia/Hyundai API is unofficial and occasionally changes. `bluelinky`'s EU
12
+ > support is community-maintained and can break when Kia updates its app. If logins
13
+ > suddenly fail, check for a newer `bluelinky` release.
14
+
15
+ ## Features
16
+
17
+ - Door lock / unlock
18
+ - Remote climate start / stop (Celsius)
19
+ - EV battery charge level and charging state (or 12V battery for combustion cars)
20
+ - Low fuel warning, engine/ignition state, tire pressure warning
21
+ - Door, window, hood, and trunk sensors
22
+
23
+ ## Requirements
24
+
25
+ - Node.js `20.18.0` or newer
26
+ - Homebridge `1.8.0` or newer
27
+ - A **Kia (European) account** with an **active Kia Connect / UVO subscription**
28
+ - Your Kia Connect **PIN** (set in the Kia app) — the EU API requires it for status reads and remote commands
29
+
30
+ > ℹ️ The PIN is validated server-side and has a limited number of attempts before a
31
+ > temporary lockout. Make sure it is correct before starting Homebridge.
32
+
33
+ ## Installation
34
+
35
+ Install through the Homebridge UI (search for `homebridge-kia-eu`), or with npm:
36
+
37
+ ```bash
38
+ npm install -g homebridge-kia-eu
39
+ ```
40
+
41
+ Then restart Homebridge.
42
+
43
+ ## Configuration
44
+
45
+ Configure through the Homebridge UI, or add a platform block to `config.json`:
46
+
47
+ ```json
48
+ {
49
+ "platform": "KiaConnectEU",
50
+ "name": "Kia Connect EU",
51
+ "username": "you@example.com",
52
+ "password": "your-password",
53
+ "pin": "1234",
54
+ "language": "en",
55
+ "vehicleIndex": 0,
56
+ "pollIntervalMinutes": 30,
57
+ "showLock": true,
58
+ "showClimate": true,
59
+ "showStatus": true,
60
+ "showBody": false,
61
+ "showBattery": true,
62
+ "climateTemperature": 21
63
+ }
64
+ ```
65
+
66
+ | Setting | Description |
67
+ | --- | --- |
68
+ | `username` | Kia Connect email |
69
+ | `password` | Kia Connect password |
70
+ | `pin` | Kia Connect PIN (numeric). Required for status reads and commands |
71
+ | `language` | EU UI language (`en`, `de`, `fr`, …). Default `en` |
72
+ | `vehicleIndex` | Which vehicle to use if the account has several (0 = first) |
73
+ | `pollIntervalMinutes` | Status refresh interval, minimum `5` |
74
+ | `showLock` | Show the HomeKit lock service |
75
+ | `showClimate` | Show the HomeKit climate switch |
76
+ | `showStatus` | Show low-fuel, engine, and tire-warning sensors |
77
+ | `showBody` | Show door, window, hood, and trunk sensors |
78
+ | `showBattery` | Show the battery service (EV charge, or 12V) |
79
+ | `climateTemperature` | Remote climate target temperature in °C (14–30) |
80
+
81
+ ## HomeKit services
82
+
83
+ Up to five accessories are created per vehicle:
84
+
85
+ - `${vehicleName} Lock` — `LockMechanism`
86
+ - `${vehicleName} Climate` — `Switch`
87
+ - `${vehicleName} Status` — `LeakSensor` (low fuel), `OccupancySensor` (engine),
88
+ `LeakSensor` (tire pressure), plus `HumiditySensor`/`TemperatureSensor` placeholders
89
+ - `${vehicleName} Body` — `ContactSensor` services for doors, windows, hood, trunk
90
+ - `${vehicleName} Battery` — `Battery` (EV charge + charging state, or 12V battery)
91
+
92
+ ## EU API limitations
93
+
94
+ The European API does **not** expose a few things the US API does:
95
+
96
+ - **Ambient outside temperature** — the temperature sensor will not report a value.
97
+ - **Fuel percentage** — only a low-fuel warning and distance-to-empty are available.
98
+
99
+ For EVs, the battery service reports the high-voltage traction battery's charge and
100
+ whether it is charging.
101
+
102
+ ## Rate limiting & battery care
103
+
104
+ The EU API is rate limited (roughly 200 calls/day) and waking the car to refresh
105
+ status repeatedly can drain its 12V battery. This plugin:
106
+
107
+ - polls **cached** server status (does not wake the car) on the configured interval;
108
+ - only forces a live refresh immediately after you issue a lock/climate command.
109
+
110
+ Keep `pollIntervalMinutes` conservative (the default is 30).
111
+
112
+ ## Authentication notes
113
+
114
+ Unlike the US plugin, there is **no OTP step**. The plugin logs in with your email,
115
+ password, and PIN on Homebridge start. If you change your password in the Kia app,
116
+ update it here and restart Homebridge.
117
+
118
+ ## Development
119
+
120
+ ```bash
121
+ npm install
122
+ npm run build
123
+ npm run lint
124
+ npm test
125
+ ```
126
+
127
+ ## Credits
128
+
129
+ - Original US plugin: [jfriend615/homebridge-kia](https://github.com/jfriend615/homebridge-kia)
130
+ - EU API library: [Hacksore/bluelinky](https://github.com/Hacksore/bluelinky)
131
+
132
+ ## License
133
+
134
+ ISC
@@ -0,0 +1,127 @@
1
+ {
2
+ "pluginAlias": "KiaConnectEU",
3
+ "pluginType": "platform",
4
+ "singular": true,
5
+ "headerDisplay": "Kia Connect (UVO) for Europe, powered by [bluelinky](https://github.com/Hacksore/bluelinky). Requires an active Kia Connect subscription. Your account PIN is needed for remote commands.",
6
+ "schema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "name": {
10
+ "title": "Name",
11
+ "type": "string",
12
+ "default": "Kia Connect EU",
13
+ "required": true
14
+ },
15
+ "username": {
16
+ "title": "Kia Connect Email",
17
+ "type": "string",
18
+ "format": "email",
19
+ "required": true
20
+ },
21
+ "password": {
22
+ "title": "Kia Connect Password",
23
+ "type": "string",
24
+ "required": true
25
+ },
26
+ "pin": {
27
+ "title": "Kia Connect PIN",
28
+ "type": "string",
29
+ "required": true,
30
+ "pattern": "^[0-9]{4,6}$",
31
+ "description": "Numeric PIN from the Kia Connect app. Required to authorise remote commands."
32
+ },
33
+ "language": {
34
+ "title": "Language",
35
+ "type": "string",
36
+ "default": "en",
37
+ "oneOf": [
38
+ { "title": "English", "enum": ["en"] },
39
+ { "title": "Deutsch", "enum": ["de"] },
40
+ { "title": "Français", "enum": ["fr"] },
41
+ { "title": "Italiano", "enum": ["it"] },
42
+ { "title": "Español", "enum": ["es"] },
43
+ { "title": "Nederlands", "enum": ["nl"] },
44
+ { "title": "Polski", "enum": ["pl"] },
45
+ { "title": "Čeština", "enum": ["cs"] },
46
+ { "title": "Dansk", "enum": ["da"] },
47
+ { "title": "Suomi", "enum": ["fi"] },
48
+ { "title": "Magyar", "enum": ["hu"] },
49
+ { "title": "Norsk", "enum": ["no"] },
50
+ { "title": "Slovenčina", "enum": ["sk"] },
51
+ { "title": "Svenska", "enum": ["sv"] }
52
+ ]
53
+ },
54
+ "pollIntervalMinutes": {
55
+ "title": "Poll Interval (minutes)",
56
+ "type": "integer",
57
+ "default": 30,
58
+ "minimum": 5,
59
+ "description": "How often to refresh cached vehicle status. Keep this conservative — the EU API is rate limited (~200 calls/day)."
60
+ },
61
+ "showLock": {
62
+ "title": "Show Door Lock",
63
+ "type": "boolean",
64
+ "default": true
65
+ },
66
+ "showClimate": {
67
+ "title": "Show Remote Climate",
68
+ "type": "boolean",
69
+ "default": true
70
+ },
71
+ "showStatus": {
72
+ "title": "Show Vehicle Sensors",
73
+ "type": "boolean",
74
+ "default": true,
75
+ "description": "Low fuel warning, engine/ignition state, and tire pressure. (Outside temperature and fuel percentage are not provided by the EU API.)"
76
+ },
77
+ "showBody": {
78
+ "title": "Show Body Sensors",
79
+ "type": "boolean",
80
+ "default": false,
81
+ "description": "Expose door, window, hood, and trunk sensors."
82
+ },
83
+ "showBattery": {
84
+ "title": "Show Battery",
85
+ "type": "boolean",
86
+ "default": true,
87
+ "description": "EV high-voltage battery charge and charging state where available, otherwise the 12V battery."
88
+ },
89
+ "climateTemperature": {
90
+ "title": "Remote Climate Temperature (°C)",
91
+ "type": "integer",
92
+ "default": 21,
93
+ "minimum": 14,
94
+ "maximum": 30,
95
+ "description": "Target temperature when starting climate control remotely."
96
+ },
97
+ "vehicleIndex": {
98
+ "title": "Vehicle Index",
99
+ "type": "integer",
100
+ "default": 0,
101
+ "minimum": 0,
102
+ "description": "If you have multiple vehicles, specify the index (0 = first vehicle)"
103
+ }
104
+ }
105
+ },
106
+ "layout": [
107
+ "username",
108
+ "password",
109
+ "pin",
110
+ "language",
111
+ { "key": "pollIntervalMinutes" },
112
+ { "key": "vehicleIndex" },
113
+ {
114
+ "type": "fieldset",
115
+ "title": "Accessory Visibility",
116
+ "expandable": true,
117
+ "items": [
118
+ "showLock",
119
+ "showClimate",
120
+ "showStatus",
121
+ "showBody",
122
+ "showBattery"
123
+ ]
124
+ },
125
+ { "key": "climateTemperature" }
126
+ ]
127
+ }
@@ -0,0 +1,12 @@
1
+ import type { KiaConnectConfig } from './kia/types.js';
2
+ export type AccessoryCategory = 'lock' | 'climate' | 'status' | 'body' | 'battery';
3
+ export declare const ACCESSORY_CATEGORIES: readonly AccessoryCategory[];
4
+ export interface AccessoryPresentation {
5
+ enabledCategories: AccessoryCategory[];
6
+ showLock: boolean;
7
+ showClimate: boolean;
8
+ showStatus: boolean;
9
+ showBody: boolean;
10
+ showBattery: boolean;
11
+ }
12
+ export declare function resolveAccessoryPresentation(config: KiaConnectConfig): AccessoryPresentation;
@@ -0,0 +1,36 @@
1
+ export const ACCESSORY_CATEGORIES = [
2
+ 'lock',
3
+ 'climate',
4
+ 'status',
5
+ 'body',
6
+ 'battery',
7
+ ];
8
+ export function resolveAccessoryPresentation(config) {
9
+ const showLock = config.showLock ?? true;
10
+ const showClimate = config.showClimate ?? true;
11
+ const showStatus = config.showStatus ?? true;
12
+ const showBody = config.showBody ?? false;
13
+ const showBattery = config.showBattery ?? true;
14
+ return {
15
+ enabledCategories: ACCESSORY_CATEGORIES.filter((category) => {
16
+ switch (category) {
17
+ case 'lock':
18
+ return showLock;
19
+ case 'climate':
20
+ return showClimate;
21
+ case 'status':
22
+ return showStatus;
23
+ case 'body':
24
+ return showBody;
25
+ case 'battery':
26
+ return showBattery;
27
+ }
28
+ }),
29
+ showLock,
30
+ showClimate,
31
+ showStatus,
32
+ showBody,
33
+ showBattery,
34
+ };
35
+ }
36
+ //# sourceMappingURL=accessory-layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accessory-layout.js","sourceRoot":"","sources":["../../src/accessory-layout.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,MAAM,oBAAoB,GAAiC;IAChE,MAAM;IACN,SAAS;IACT,QAAQ;IACR,MAAM;IACN,SAAS;CACV,CAAC;AAWF,MAAM,UAAU,4BAA4B,CAAC,MAAwB;IACnE,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC;IACzC,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,IAAI,CAAC;IAC/C,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,IAAI,IAAI,CAAC;IAC7C,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,KAAK,CAAC;IAC1C,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,IAAI,CAAC;IAE/C,OAAO;QACL,iBAAiB,EAAE,oBAAoB,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE;YAC1D,QAAQ,QAAQ,EAAE,CAAC;gBACnB,KAAK,MAAM;oBACT,OAAO,QAAQ,CAAC;gBAClB,KAAK,SAAS;oBACZ,OAAO,WAAW,CAAC;gBACrB,KAAK,QAAQ;oBACX,OAAO,UAAU,CAAC;gBACpB,KAAK,MAAM;oBACT,OAAO,QAAQ,CAAC;gBAClB,KAAK,SAAS;oBACZ,OAAO,WAAW,CAAC;YACrB,CAAC;QACH,CAAC,CAAC;QACF,QAAQ;QACR,WAAW;QACX,UAAU;QACV,QAAQ;QACR,WAAW;KACZ,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { API } from 'homebridge';
2
+ declare const _default: (api: API) => void;
3
+ export default _default;
@@ -0,0 +1,6 @@
1
+ import { KiaConnectPlatform } from './platform.js';
2
+ import { PLATFORM_NAME } from './settings.js';
3
+ export default (api) => {
4
+ api.registerPlatform(PLATFORM_NAME, KiaConnectPlatform);
5
+ };
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,eAAe,CAAC,GAAQ,EAAE,EAAE;IAC1B,GAAG,CAAC,gBAAgB,CAAC,aAAa,EAAE,kBAAkB,CAAC,CAAC;AAC1D,CAAC,CAAC"}
@@ -0,0 +1,87 @@
1
+ import type { Logger } from 'homebridge';
2
+ import { type ClimateOptions, type LoginResult, type VehicleState, type VehicleSummary } from './types.js';
3
+ export interface KiaClientOptions {
4
+ username: string;
5
+ password: string;
6
+ pin: string;
7
+ language?: string;
8
+ }
9
+ /**
10
+ * Subset of bluelinky's RawVehicleStatus (region EU) that we actually read.
11
+ * Declared locally so we don't couple to bluelinky's internal type paths and so
12
+ * the mapper degrades gracefully when fields are missing.
13
+ */
14
+ interface EuRawStatus {
15
+ doorLock?: boolean;
16
+ doorOpen?: Record<string, number | undefined>;
17
+ windowOpen?: Record<string, number | undefined>;
18
+ trunkOpen?: boolean;
19
+ hoodOpen?: boolean;
20
+ engine?: boolean;
21
+ airCtrlOn?: boolean;
22
+ defrost?: boolean;
23
+ lowFuelLight?: boolean;
24
+ battery?: {
25
+ batSoc?: number;
26
+ };
27
+ dte?: {
28
+ value?: number;
29
+ unit?: number;
30
+ };
31
+ tirePressureLamp?: Record<string, number | undefined>;
32
+ evStatus?: {
33
+ batteryCharge?: boolean;
34
+ batteryStatus?: number;
35
+ batteryPlugin?: number;
36
+ drvDistance?: Array<{
37
+ rangeByFuel?: {
38
+ evModeRange?: {
39
+ value?: number;
40
+ };
41
+ totalAvailableRange?: {
42
+ value?: number;
43
+ };
44
+ };
45
+ }>;
46
+ };
47
+ lastStatusDate?: string;
48
+ dateTime?: string;
49
+ }
50
+ /**
51
+ * Maps a bluelinky EU raw status payload into the region-agnostic VehicleState
52
+ * the HomeKit accessory layer consumes.
53
+ *
54
+ * Note: the EU API does not expose ambient outside temperature or a fuel
55
+ * percentage, so those remain null. EV charge, charging state and range are
56
+ * available and are surfaced through the battery accessory.
57
+ */
58
+ export declare function mapRawStatus(raw: EuRawStatus | null | undefined): VehicleState;
59
+ export declare class KiaApiClient {
60
+ private readonly log;
61
+ private readonly options;
62
+ private client?;
63
+ private readonly vehicles;
64
+ private summaries;
65
+ constructor(log: Logger, options: KiaClientOptions);
66
+ /**
67
+ * Logs in via bluelinky. The constructor auto-logs-in and emits 'ready' with
68
+ * the account's vehicles, or 'error' on failure.
69
+ */
70
+ login(): Promise<LoginResult>;
71
+ getVehicles(): Promise<VehicleSummary[]>;
72
+ getVehicleStatus(vehicleKey: string, refresh?: boolean): Promise<VehicleState>;
73
+ lockDoors(vehicleKey: string): Promise<string>;
74
+ unlockDoors(vehicleKey: string): Promise<string>;
75
+ startClimate(vehicleKey: string, options?: ClimateOptions): Promise<string>;
76
+ stopClimate(vehicleKey: string): Promise<string>;
77
+ /**
78
+ * bluelinky resolves command calls once the server accepts them, so there is
79
+ * no separate transaction to poll. Retained for accessory-layer compatibility.
80
+ */
81
+ waitForAction(_vehicleKey: string, _actionId: string): Promise<boolean>;
82
+ private indexVehicles;
83
+ private safeCall;
84
+ private requireVehicle;
85
+ private wrapError;
86
+ }
87
+ export {};
@@ -0,0 +1,264 @@
1
+ import { BlueLinky } from 'bluelinky';
2
+ import { AuthenticationError, KiaApiError, } from './types.js';
3
+ import { CLIMATE_DURATION_MINUTES, DEFAULT_CLIMATE_TEMP_C, DEFAULT_LANGUAGE, LOGIN_TIMEOUT_MS, MAX_CLIMATE_TEMP_C, MIN_CLIMATE_TEMP_C, } from '../settings.js';
4
+ function parseNumber(value) {
5
+ if (value === undefined || value === null || value === '') {
6
+ return null;
7
+ }
8
+ const num = Number(value);
9
+ return Number.isNaN(num) ? null : num;
10
+ }
11
+ function isOpen(value) {
12
+ return (value ?? 0) !== 0;
13
+ }
14
+ function clamp(value, min, max) {
15
+ return Math.min(Math.max(value, min), max);
16
+ }
17
+ function errorMessage(error) {
18
+ if (error instanceof Error) {
19
+ return error.message;
20
+ }
21
+ if (typeof error === 'string') {
22
+ return error;
23
+ }
24
+ try {
25
+ return JSON.stringify(error);
26
+ }
27
+ catch {
28
+ return String(error);
29
+ }
30
+ }
31
+ const AUTH_HINTS = ['token', 'auth', 'login', 'session', 'credential', 'pin', '401', '403'];
32
+ function looksLikeAuthError(message) {
33
+ const lower = message.toLowerCase();
34
+ return AUTH_HINTS.some((hint) => lower.includes(hint));
35
+ }
36
+ /**
37
+ * Maps a bluelinky EU raw status payload into the region-agnostic VehicleState
38
+ * the HomeKit accessory layer consumes.
39
+ *
40
+ * Note: the EU API does not expose ambient outside temperature or a fuel
41
+ * percentage, so those remain null. EV charge, charging state and range are
42
+ * available and are surfaced through the battery accessory.
43
+ */
44
+ export function mapRawStatus(raw) {
45
+ const doors = raw?.doorOpen ?? {};
46
+ const windows = raw?.windowOpen ?? {};
47
+ const tire = raw?.tirePressureLamp ?? {};
48
+ const ev = raw?.evStatus;
49
+ const evRange = ev?.drvDistance?.[0]?.rangeByFuel?.evModeRange?.value;
50
+ return {
51
+ // Doors (1 = open)
52
+ frontLeftDoorOpen: isOpen(doors.frontLeft),
53
+ frontRightDoorOpen: isOpen(doors.frontRight),
54
+ rearLeftDoorOpen: isOpen(doors.backLeft),
55
+ rearRightDoorOpen: isOpen(doors.backRight),
56
+ hoodOpen: raw?.hoodOpen === true,
57
+ trunkOpen: raw?.trunkOpen === true,
58
+ // Lock
59
+ locked: raw?.doorLock === true,
60
+ // Engine / climate
61
+ engineRunning: raw?.engine === true,
62
+ airControlOn: raw?.airCtrlOn === true,
63
+ defrostOn: raw?.defrost === true,
64
+ // Ambient temperature is not provided by the EU API.
65
+ outsideTemperature: null,
66
+ // 12V battery
67
+ batteryPercentage: parseNumber(raw?.battery?.batSoc),
68
+ // EV high-voltage battery
69
+ evBatteryPercentage: parseNumber(ev?.batteryStatus),
70
+ evCharging: ev?.batteryCharge === true,
71
+ evPluggedIn: isOpen(ev?.batteryPlugin),
72
+ evRange: parseNumber(evRange),
73
+ // Fuel — percentage is not exposed; distance-to-empty is.
74
+ fuelLevel: null,
75
+ fuelLevelLow: raw?.lowFuelLight === true,
76
+ fuelDrivingRange: parseNumber(raw?.dte?.value),
77
+ // Windows (1 = open)
78
+ frontLeftWindowOpen: isOpen(windows.frontLeft),
79
+ frontRightWindowOpen: isOpen(windows.frontRight),
80
+ rearLeftWindowOpen: isOpen(windows.backLeft),
81
+ rearRightWindowOpen: isOpen(windows.backRight),
82
+ // Tire pressure warning (any lamp lit)
83
+ tirePressureWarning: Object.values(tire).some((v) => (v ?? 0) !== 0),
84
+ // Odometer / location are fetched on demand elsewhere; not part of status.
85
+ odometer: null,
86
+ latitude: null,
87
+ longitude: null,
88
+ // Meta
89
+ lastUpdated: raw?.lastStatusDate ?? raw?.dateTime ?? null,
90
+ };
91
+ }
92
+ export class KiaApiClient {
93
+ log;
94
+ options;
95
+ client;
96
+ vehicles = new Map();
97
+ summaries = [];
98
+ constructor(log, options) {
99
+ this.log = log;
100
+ this.options = options;
101
+ }
102
+ /**
103
+ * Logs in via bluelinky. The constructor auto-logs-in and emits 'ready' with
104
+ * the account's vehicles, or 'error' on failure.
105
+ */
106
+ login() {
107
+ return new Promise((resolve) => {
108
+ let settled = false;
109
+ const finish = (result) => {
110
+ if (settled) {
111
+ return;
112
+ }
113
+ settled = true;
114
+ clearTimeout(timer);
115
+ resolve(result);
116
+ };
117
+ const timer = setTimeout(() => {
118
+ finish({ success: false, error: 'Timed out waiting for Kia Connect login' });
119
+ }, LOGIN_TIMEOUT_MS);
120
+ try {
121
+ const config = {
122
+ username: this.options.username,
123
+ password: this.options.password,
124
+ pin: this.options.pin,
125
+ brand: 'kia',
126
+ region: 'EU',
127
+ language: this.options.language ?? DEFAULT_LANGUAGE,
128
+ autoLogin: true,
129
+ };
130
+ const client = new BlueLinky(config);
131
+ client.on('ready', (vehicles) => {
132
+ this.client = client;
133
+ this.indexVehicles(vehicles);
134
+ this.log.info(`Logged in to Kia Connect (EU); found ${this.summaries.length} vehicle(s)`);
135
+ finish({ success: true });
136
+ });
137
+ client.on('error', (error) => {
138
+ finish({ success: false, error: errorMessage(error) });
139
+ });
140
+ }
141
+ catch (error) {
142
+ finish({ success: false, error: errorMessage(error) });
143
+ }
144
+ });
145
+ }
146
+ async getVehicles() {
147
+ if (this.summaries.length > 0) {
148
+ return this.summaries;
149
+ }
150
+ if (!this.client) {
151
+ throw new AuthenticationError('Not logged in to Kia Connect');
152
+ }
153
+ const vehicles = await this.client.getVehicles();
154
+ this.indexVehicles(vehicles);
155
+ return this.summaries;
156
+ }
157
+ async getVehicleStatus(vehicleKey, refresh = false) {
158
+ const vehicle = this.requireVehicle(vehicleKey);
159
+ try {
160
+ const raw = await vehicle.status({ refresh, parsed: false });
161
+ return mapRawStatus(raw);
162
+ }
163
+ catch (error) {
164
+ throw this.wrapError(error, 'fetch vehicle status');
165
+ }
166
+ }
167
+ async lockDoors(vehicleKey) {
168
+ const vehicle = this.requireVehicle(vehicleKey);
169
+ try {
170
+ return (await vehicle.lock()) ?? '';
171
+ }
172
+ catch (error) {
173
+ throw this.wrapError(error, 'lock doors');
174
+ }
175
+ }
176
+ async unlockDoors(vehicleKey) {
177
+ const vehicle = this.requireVehicle(vehicleKey);
178
+ try {
179
+ return (await vehicle.unlock()) ?? '';
180
+ }
181
+ catch (error) {
182
+ throw this.wrapError(error, 'unlock doors');
183
+ }
184
+ }
185
+ async startClimate(vehicleKey, options) {
186
+ const vehicle = this.requireVehicle(vehicleKey);
187
+ const temperature = clamp(options?.temperature ?? DEFAULT_CLIMATE_TEMP_C, MIN_CLIMATE_TEMP_C, MAX_CLIMATE_TEMP_C);
188
+ try {
189
+ const result = await vehicle.start({
190
+ hvac: true,
191
+ duration: CLIMATE_DURATION_MINUTES,
192
+ temperature,
193
+ defrost: options?.defrost ?? false,
194
+ heatedFeatures: false,
195
+ unit: 'C',
196
+ });
197
+ return result ?? '';
198
+ }
199
+ catch (error) {
200
+ throw this.wrapError(error, 'start climate');
201
+ }
202
+ }
203
+ async stopClimate(vehicleKey) {
204
+ const vehicle = this.requireVehicle(vehicleKey);
205
+ try {
206
+ return (await vehicle.stop()) ?? '';
207
+ }
208
+ catch (error) {
209
+ throw this.wrapError(error, 'stop climate');
210
+ }
211
+ }
212
+ /**
213
+ * bluelinky resolves command calls once the server accepts them, so there is
214
+ * no separate transaction to poll. Retained for accessory-layer compatibility.
215
+ */
216
+ async waitForAction(_vehicleKey, _actionId) {
217
+ return true;
218
+ }
219
+ indexVehicles(vehicles) {
220
+ this.vehicles.clear();
221
+ this.summaries = vehicles.map((vehicle) => {
222
+ const vin = this.safeCall(() => vehicle.vin());
223
+ const id = this.safeCall(() => vehicle.id());
224
+ const nickname = this.safeCall(() => vehicle.nickname());
225
+ const model = this.safeCall(() => vehicle.name());
226
+ const key = vin || id;
227
+ if (key) {
228
+ this.vehicles.set(key, vehicle);
229
+ }
230
+ return {
231
+ id,
232
+ vin,
233
+ key,
234
+ name: nickname || model || 'Kia Vehicle',
235
+ model: model || 'Kia',
236
+ };
237
+ });
238
+ }
239
+ safeCall(fn) {
240
+ try {
241
+ return fn() ?? '';
242
+ }
243
+ catch {
244
+ return '';
245
+ }
246
+ }
247
+ requireVehicle(vehicleKey) {
248
+ const vehicle = this.vehicles.get(vehicleKey);
249
+ if (!vehicle) {
250
+ throw new KiaApiError(`Vehicle ${vehicleKey} is not available`);
251
+ }
252
+ return vehicle;
253
+ }
254
+ wrapError(error, action) {
255
+ if (error instanceof KiaApiError) {
256
+ return error;
257
+ }
258
+ const message = `Failed to ${action}: ${errorMessage(error)}`;
259
+ return looksLikeAuthError(message)
260
+ ? new AuthenticationError(message)
261
+ : new KiaApiError(message);
262
+ }
263
+ }
264
+ //# sourceMappingURL=client.js.map