homebridge-jlr-smartcar 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 +118 -0
- package/config.schema.json +49 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/platform.d.ts +19 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +145 -0
- package/dist/platform.js.map +1 -0
- package/dist/settings.d.ts +3 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +6 -0
- package/dist/settings.js.map +1 -0
- package/dist/smartcar-client.d.ts +46 -0
- package/dist/smartcar-client.d.ts.map +1 -0
- package/dist/smartcar-client.js +369 -0
- package/dist/smartcar-client.js.map +1 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/vehicle-accessory.d.ts +23 -0
- package/dist/vehicle-accessory.d.ts.map +1 -0
- package/dist/vehicle-accessory.js +120 -0
- package/dist/vehicle-accessory.js.map +1 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# homebridge-jlr-smartcar
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/homebridge-jlr-smartcar)
|
|
4
|
+
[](https://github.com/StefanPeetz/homebridge-jlr-incontrol-v2/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Homebridge plugin for **Jaguar Land Rover InControl** – powered by the [Smartcar API](https://smartcar.com).
|
|
8
|
+
|
|
9
|
+
> **Why Smartcar?**
|
|
10
|
+
> JLR deprecated their unofficial password-based API in 2024 and now requires OTP/Passkey for all logins, making direct automation impossible. Smartcar holds an official JLR partnership and provides a stable OAuth 2.0 API.
|
|
11
|
+
|
|
12
|
+
## Supported features
|
|
13
|
+
|
|
14
|
+
| Feature | Status |
|
|
15
|
+
|---|---|
|
|
16
|
+
| Lock / Unlock | ✅ |
|
|
17
|
+
| Battery level (EV/PHEV) | ✅ |
|
|
18
|
+
| Charging status | ✅ |
|
|
19
|
+
| Low battery alert | ✅ |
|
|
20
|
+
| Fuel level | ✅ |
|
|
21
|
+
| Range (km) | ✅ |
|
|
22
|
+
| Odometer | ✅ |
|
|
23
|
+
| Location | ✅ |
|
|
24
|
+
| Climate / Preconditioning | ❌ Not available via Smartcar |
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g homebridge-jlr-smartcar
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or install via the **Homebridge UI** by searching for `homebridge-jlr-smartcar`.
|
|
33
|
+
|
|
34
|
+
## Setup
|
|
35
|
+
|
|
36
|
+
### 1. Create a free Smartcar app
|
|
37
|
+
|
|
38
|
+
1. Go to [dashboard.smartcar.com](https://dashboard.smartcar.com) and sign up
|
|
39
|
+
2. Create a new application
|
|
40
|
+
3. Add `http://localhost:52625/callback` to **Redirect URIs**
|
|
41
|
+
4. Note your **Client ID** and **Client Secret**
|
|
42
|
+
|
|
43
|
+
### 2. Configure Homebridge
|
|
44
|
+
|
|
45
|
+
Add to `config.json`:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"platforms": [
|
|
50
|
+
{
|
|
51
|
+
"platform": "JlrInControl",
|
|
52
|
+
"name": "JLR InControl",
|
|
53
|
+
"clientId": "YOUR_SMARTCAR_CLIENT_ID",
|
|
54
|
+
"clientSecret": "YOUR_SMARTCAR_CLIENT_SECRET",
|
|
55
|
+
"pollIntervalSeconds": 300
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 3. Authorize your vehicle (one-time)
|
|
62
|
+
|
|
63
|
+
Restart Homebridge. The logs will show:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
[Smartcar] ACTION REQUIRED: Open this URL in your browser:
|
|
67
|
+
[Smartcar] http://localhost:52625/auth
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Open that URL → log in with your JLR account → authorize. Tokens are saved to `~/.homebridge/smartcar-tokens.json` and refresh automatically – no repeat login needed.
|
|
71
|
+
|
|
72
|
+
### Configuration options
|
|
73
|
+
|
|
74
|
+
| Option | Type | Default | Description |
|
|
75
|
+
|---|---|---|---|
|
|
76
|
+
| `clientId` | string | **required** | Smartcar Client ID |
|
|
77
|
+
| `clientSecret` | string | **required** | Smartcar Client Secret |
|
|
78
|
+
| `redirectUri` | string | `http://localhost:52625/callback` | Must match your Smartcar app settings |
|
|
79
|
+
| `pin` | string | | Vehicle PIN (reserved for future use) |
|
|
80
|
+
| `pollIntervalSeconds` | integer | `300` | How often to poll vehicle state (min: 60) |
|
|
81
|
+
|
|
82
|
+
## Smartcar pricing
|
|
83
|
+
|
|
84
|
+
| Tier | Calls/month | Price |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| Free | 500 | $0 |
|
|
87
|
+
| Starter | Unlimited | $2.99 / month |
|
|
88
|
+
|
|
89
|
+
At 300s poll interval: ~8,640 calls/month → Starter plan recommended for daily use.
|
|
90
|
+
At 1800s (30 min): ~1,440 calls/month → fits within free tier.
|
|
91
|
+
|
|
92
|
+
## Building from source
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
git clone https://github.com/StefanPeetz/homebridge-jlr-incontrol-v2.git
|
|
96
|
+
cd homebridge-jlr-incontrol-v2
|
|
97
|
+
npm install
|
|
98
|
+
npm run build
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Releasing a new version
|
|
102
|
+
|
|
103
|
+
1. Bump `version` in `package.json`
|
|
104
|
+
2. Commit and push to `main`
|
|
105
|
+
3. GitHub Actions auto-creates the Git tag
|
|
106
|
+
4. Create a **GitHub Release** from that tag
|
|
107
|
+
5. The `publish.yml` workflow automatically publishes to npm
|
|
108
|
+
|
|
109
|
+
> **Prerequisite:** Add your npm token as a repository secret named `NPM_TOKEN`
|
|
110
|
+
> (GitHub repo → Settings → Secrets → Actions → New secret)
|
|
111
|
+
|
|
112
|
+
## Changelog
|
|
113
|
+
|
|
114
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluginAlias": "JlrInControl",
|
|
3
|
+
"pluginType": "platform",
|
|
4
|
+
"singular": true,
|
|
5
|
+
"headerDisplay": "Jaguar Land Rover InControl via [Smartcar API](https://smartcar.com). Create a free app at [dashboard.smartcar.com](https://dashboard.smartcar.com), add `http://localhost:52625/callback` as redirect URI, then paste your credentials below.",
|
|
6
|
+
"schema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"name": {
|
|
10
|
+
"title": "Platform name",
|
|
11
|
+
"type": "string",
|
|
12
|
+
"default": "JLR InControl"
|
|
13
|
+
},
|
|
14
|
+
"clientId": {
|
|
15
|
+
"title": "Smartcar Client ID",
|
|
16
|
+
"type": "string",
|
|
17
|
+
"required": true
|
|
18
|
+
},
|
|
19
|
+
"clientSecret": {
|
|
20
|
+
"title": "Smartcar Client Secret",
|
|
21
|
+
"type": "string",
|
|
22
|
+
"required": true
|
|
23
|
+
},
|
|
24
|
+
"redirectUri": {
|
|
25
|
+
"title": "OAuth Redirect URI",
|
|
26
|
+
"type": "string",
|
|
27
|
+
"default": "http://localhost:52625/callback",
|
|
28
|
+
"description": "Must exactly match what you entered in the Smartcar dashboard."
|
|
29
|
+
},
|
|
30
|
+
"pollIntervalSeconds": {
|
|
31
|
+
"title": "Poll interval (seconds)",
|
|
32
|
+
"type": "integer",
|
|
33
|
+
"default": 300,
|
|
34
|
+
"minimum": 60,
|
|
35
|
+
"description": "How often to fetch vehicle state. At 300 s ≈ 8,640 calls/month (Starter plan). At 1800 s ≈ 1,440/month (Free plan)."
|
|
36
|
+
},
|
|
37
|
+
"notifyWebhookUrl": {
|
|
38
|
+
"title": "Re-auth notification webhook URL (optional)",
|
|
39
|
+
"type": "string",
|
|
40
|
+
"description": "When the Smartcar token is about to expire (7 days before), a POST request with JSON body is sent here. Works with ntfy.sh, Pushover, Home Assistant webhooks, n8n, etc. Leave empty to disable."
|
|
41
|
+
},
|
|
42
|
+
"pin": {
|
|
43
|
+
"title": "Vehicle PIN (reserved)",
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "Currently unused. Reserved for future JLR security PIN support."
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;yBAIvB,KAAK,GAAG;AAAlB,kBAEE"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const settings_1 = require("./settings");
|
|
3
|
+
const platform_1 = require("./platform");
|
|
4
|
+
module.exports = (api) => {
|
|
5
|
+
api.registerPlatform(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, platform_1.JlrPlatform);
|
|
6
|
+
};
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,yCAAwD;AACxD,yCAAyC;AAEzC,iBAAS,CAAC,GAAQ,EAAE,EAAE;IACpB,GAAG,CAAC,gBAAgB,CAAC,sBAAW,EAAE,wBAAa,EAAE,sBAAW,CAAC,CAAC;AAChE,CAAC,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service, Characteristic } from 'homebridge';
|
|
2
|
+
export declare class JlrPlatform implements DynamicPlatformPlugin {
|
|
3
|
+
readonly log: Logger;
|
|
4
|
+
readonly api: API;
|
|
5
|
+
readonly Service: typeof Service;
|
|
6
|
+
readonly Characteristic: typeof Characteristic;
|
|
7
|
+
private readonly accessories;
|
|
8
|
+
private client;
|
|
9
|
+
private readonly config;
|
|
10
|
+
private reauthSensorAccessory?;
|
|
11
|
+
private reauthSensorService?;
|
|
12
|
+
constructor(log: Logger, config: PlatformConfig, api: API);
|
|
13
|
+
configureAccessory(accessory: PlatformAccessory): void;
|
|
14
|
+
private setupReauthSensor;
|
|
15
|
+
private setReauthSensor;
|
|
16
|
+
private init;
|
|
17
|
+
private registerVehicles;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=platform.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,GAAG,EACH,qBAAqB,EACrB,MAAM,EACN,iBAAiB,EACjB,cAAc,EACd,OAAO,EACP,cAAc,EACf,MAAM,YAAY,CAAC;AAUpB,qBAAa,WAAY,YAAW,qBAAqB;aAarC,GAAG,EAAE,MAAM;aAEX,GAAG,EAAE,GAAG;IAd1B,SAAgB,OAAO,EAAE,OAAO,OAAO,CAAC;IACxC,SAAgB,cAAc,EAAE,OAAO,cAAc,CAAC;IAEtD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA2B;IACvD,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IAGtC,OAAO,CAAC,qBAAqB,CAAC,CAAoB;IAClD,OAAO,CAAC,mBAAmB,CAAC,CAA+C;gBAGzD,GAAG,EAAE,MAAM,EAC3B,MAAM,EAAE,cAAc,EACN,GAAG,EAAE,GAAG;IAS1B,kBAAkB,CAAC,SAAS,EAAE,iBAAiB,GAAG,IAAI;IAMtD,OAAO,CAAC,iBAAiB;IAyBzB,OAAO,CAAC,eAAe;YAmBT,IAAI;IAwClB,OAAO,CAAC,gBAAgB;CAuBzB"}
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.JlrPlatform = void 0;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const settings_1 = require("./settings");
|
|
39
|
+
const smartcar_client_1 = require("./smartcar-client");
|
|
40
|
+
const vehicle_accessory_1 = require("./vehicle-accessory");
|
|
41
|
+
// UUID suffix for the "Re-Auth Required" sensor
|
|
42
|
+
const REAUTH_SENSOR_SUFFIX = '-reauth-sensor';
|
|
43
|
+
class JlrPlatform {
|
|
44
|
+
constructor(log, config, api) {
|
|
45
|
+
this.log = log;
|
|
46
|
+
this.api = api;
|
|
47
|
+
this.accessories = [];
|
|
48
|
+
this.Service = api.hap.Service;
|
|
49
|
+
this.Characteristic = api.hap.Characteristic;
|
|
50
|
+
this.config = config;
|
|
51
|
+
this.api.on('didFinishLaunching', () => this.init());
|
|
52
|
+
}
|
|
53
|
+
configureAccessory(accessory) {
|
|
54
|
+
this.accessories.push(accessory);
|
|
55
|
+
}
|
|
56
|
+
// ─── Re-auth sensor ───────────────────────────────────────────────────────
|
|
57
|
+
setupReauthSensor() {
|
|
58
|
+
const uuid = this.api.hap.uuid.generate('jlr-smartcar' + REAUTH_SENSOR_SUFFIX);
|
|
59
|
+
const existing = this.accessories.find(a => a.UUID === uuid);
|
|
60
|
+
const accessory = existing
|
|
61
|
+
?? new this.api.platformAccessory('JLR Re-Auth Required', uuid);
|
|
62
|
+
this.reauthSensorService =
|
|
63
|
+
accessory.getService(this.Service.OccupancySensor) ??
|
|
64
|
+
accessory.addService(this.Service.OccupancySensor, 'JLR Re-Auth Required');
|
|
65
|
+
// Start clear
|
|
66
|
+
this.reauthSensorService
|
|
67
|
+
.getCharacteristic(this.Characteristic.OccupancyDetected)
|
|
68
|
+
.updateValue(this.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED);
|
|
69
|
+
if (!existing) {
|
|
70
|
+
this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
|
|
71
|
+
this.log.info('[JLR] Registered re-auth sensor accessory');
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
this.api.updatePlatformAccessories([accessory]);
|
|
75
|
+
}
|
|
76
|
+
this.reauthSensorAccessory = accessory;
|
|
77
|
+
}
|
|
78
|
+
setReauthSensor(required) {
|
|
79
|
+
if (!this.reauthSensorService)
|
|
80
|
+
return;
|
|
81
|
+
const Char = this.Characteristic.OccupancyDetected;
|
|
82
|
+
this.reauthSensorService
|
|
83
|
+
.getCharacteristic(this.Characteristic.OccupancyDetected)
|
|
84
|
+
.updateValue(required ? Char.OCCUPANCY_DETECTED : Char.OCCUPANCY_NOT_DETECTED);
|
|
85
|
+
if (required) {
|
|
86
|
+
this.log.warn('[JLR] HomeKit "JLR Re-Auth Required" sensor is now ACTIVE. ' +
|
|
87
|
+
'Open http://localhost:52625/auth to re-authorize.');
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
this.log.info('[JLR] HomeKit "JLR Re-Auth Required" sensor cleared.');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ─── Init ─────────────────────────────────────────────────────────────────
|
|
94
|
+
async init() {
|
|
95
|
+
if (!this.config.clientId || !this.config.clientSecret) {
|
|
96
|
+
this.log.error('[JLR] Missing clientId / clientSecret in config.json. ' +
|
|
97
|
+
'Create a free app at https://dashboard.smartcar.com');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Register the re-auth sensor before doing anything network-related
|
|
101
|
+
this.setupReauthSensor();
|
|
102
|
+
const tokenPath = path.join(this.api.user.storagePath(), 'smartcar-tokens.json');
|
|
103
|
+
this.client = new smartcar_client_1.SmartcarClient({
|
|
104
|
+
clientId: this.config.clientId,
|
|
105
|
+
clientSecret: this.config.clientSecret,
|
|
106
|
+
redirectUri: this.config.redirectUri,
|
|
107
|
+
tokenStorePath: tokenPath,
|
|
108
|
+
notifyWebhookUrl: this.config.notifyWebhookUrl,
|
|
109
|
+
log: this.log,
|
|
110
|
+
});
|
|
111
|
+
// Wire up the callback so the sensor updates immediately
|
|
112
|
+
this.client.onReauthRequired = (required) => this.setReauthSensor(required);
|
|
113
|
+
try {
|
|
114
|
+
await this.client.ensureAuthenticated();
|
|
115
|
+
const vehicles = await this.client.getVehicles();
|
|
116
|
+
this.registerVehicles(vehicles);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
this.log.error('[JLR] Init failed: %s', err.message);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// ─── Vehicle registration ─────────────────────────────────────────────────
|
|
123
|
+
registerVehicles(vehicles) {
|
|
124
|
+
const pollInterval = (this.config.pollIntervalSeconds ?? 300) * 1000;
|
|
125
|
+
for (const vehicle of vehicles) {
|
|
126
|
+
const uuid = this.api.hap.uuid.generate(vehicle.vin);
|
|
127
|
+
const existing = this.accessories.find(a => a.UUID === uuid);
|
|
128
|
+
const accessory = existing ?? new this.api.platformAccessory(vehicle.nickname, uuid);
|
|
129
|
+
accessory.context.vehicle = vehicle;
|
|
130
|
+
accessory.context.pin = this.config.pin ?? '';
|
|
131
|
+
const acc = new vehicle_accessory_1.VehicleAccessory(this, accessory, this.client, this.log);
|
|
132
|
+
acc.startPolling(pollInterval);
|
|
133
|
+
if (!existing) {
|
|
134
|
+
this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
|
|
135
|
+
this.log.info('[JLR] Registered new vehicle: %s (%s)', vehicle.nickname, vehicle.vin);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
this.api.updatePlatformAccessories([accessory]);
|
|
139
|
+
this.log.info('[JLR] Restored vehicle: %s (%s)', vehicle.nickname, vehicle.vin);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
exports.JlrPlatform = JlrPlatform;
|
|
145
|
+
//# sourceMappingURL=platform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,iBAAiB,CAAC;AAC5C,eAAO,MAAM,WAAW,4BAA8B,CAAC"}
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":";;;AAAa,QAAA,aAAa,GAAG,cAAc,CAAC;AAC/B,QAAA,WAAW,GAAK,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Logger } from 'homebridge';
|
|
2
|
+
import { JlrVehicleSummary, JlrVehicleState } from './types';
|
|
3
|
+
export declare class SmartcarClient {
|
|
4
|
+
private http;
|
|
5
|
+
private tokens?;
|
|
6
|
+
private tokenPath;
|
|
7
|
+
private oauthServer?;
|
|
8
|
+
private readonly clientId;
|
|
9
|
+
private readonly clientSecret;
|
|
10
|
+
private readonly redirectUri;
|
|
11
|
+
private readonly notifyWebhookUrl?;
|
|
12
|
+
private readonly log;
|
|
13
|
+
onReauthRequired?: (required: boolean) => void;
|
|
14
|
+
constructor(params: {
|
|
15
|
+
clientId: string;
|
|
16
|
+
clientSecret: string;
|
|
17
|
+
redirectUri?: string;
|
|
18
|
+
tokenStorePath: string;
|
|
19
|
+
notifyWebhookUrl?: string;
|
|
20
|
+
log: Logger;
|
|
21
|
+
});
|
|
22
|
+
private saveTokens;
|
|
23
|
+
private loadTokens;
|
|
24
|
+
/**
|
|
25
|
+
* Returns true if the refresh_token is less than REAUTH_WARNING_THRESHOLD away
|
|
26
|
+
* from expiry (or already expired / missing).
|
|
27
|
+
*/
|
|
28
|
+
needsReauth(): boolean;
|
|
29
|
+
/** Days until re-auth is required (negative = already overdue). */
|
|
30
|
+
daysUntilReauth(): number;
|
|
31
|
+
private triggerReauthNotification;
|
|
32
|
+
private isAccessTokenValid;
|
|
33
|
+
ensureAuthenticated(): Promise<void>;
|
|
34
|
+
private buildAuthUrl;
|
|
35
|
+
private startOAuthFlow;
|
|
36
|
+
private exchangeCode;
|
|
37
|
+
private refreshTokens;
|
|
38
|
+
private authHeaders;
|
|
39
|
+
private get;
|
|
40
|
+
private post;
|
|
41
|
+
getVehicles(): Promise<JlrVehicleSummary[]>;
|
|
42
|
+
getVehicleState(vehicleId: string, vin: string): Promise<JlrVehicleState>;
|
|
43
|
+
lock(vehicleId: string): Promise<void>;
|
|
44
|
+
unlock(vehicleId: string): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=smartcar-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smartcar-client.d.ts","sourceRoot":"","sources":["../src/smartcar-client.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACpC,OAAO,EAAkB,iBAAiB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAY7E,qBAAa,cAAc;IACzB,OAAO,CAAC,IAAI,CAAgB;IAC5B,OAAO,CAAC,MAAM,CAAC,CAAiB;IAChC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,WAAW,CAAC,CAAc;IAElC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAKtB,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;gBAE1C,MAAM,EAAE;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,cAAc,EAAE,MAAM,CAAC;QACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,GAAG,EAAE,MAAM,CAAC;KACb;IAYD,OAAO,CAAC,UAAU;IAMlB,OAAO,CAAC,UAAU;IAWlB;;;OAGG;IACH,WAAW,IAAI,OAAO;IAQtB,mEAAmE;IACnE,eAAe,IAAI,MAAM;YAQX,yBAAyB;IA+BvC,OAAO,CAAC,kBAAkB;IAIpB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IA0B1C,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,cAAc;YA8DR,YAAY;YAuBZ,aAAa;IAsC3B,OAAO,CAAC,WAAW;YAIL,GAAG;YAQH,IAAI;IAUZ,WAAW,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;IA4B3C,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IA+DzE,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAItC,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAG/C"}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
//
|
|
3
|
+
// Smartcar API client for JLR InControl Homebridge plugin
|
|
4
|
+
//
|
|
5
|
+
// OAuth 2.0 flow:
|
|
6
|
+
// 1. User visits http://homebridge-ip:52625/auth (one-time setup)
|
|
7
|
+
// 2. Redirect to Smartcar consent page
|
|
8
|
+
// 3. Smartcar redirects back to /callback with ?code=...
|
|
9
|
+
// 4. We exchange code for tokens and store them in tokenStore
|
|
10
|
+
// 5. Plugin uses refresh_token automatically from then on
|
|
11
|
+
//
|
|
12
|
+
// Re-auth notification:
|
|
13
|
+
// - 7 days before refresh_token expires: HomeKit "Auth Required" sensor fires
|
|
14
|
+
// + optional webhook POST to notifyWebhookUrl
|
|
15
|
+
// - On actual refresh failure: same, plus OAuth server starts automatically
|
|
16
|
+
//
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
51
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
52
|
+
};
|
|
53
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
+
exports.SmartcarClient = void 0;
|
|
55
|
+
const axios_1 = __importDefault(require("axios"));
|
|
56
|
+
const http = __importStar(require("http"));
|
|
57
|
+
const fs = __importStar(require("fs"));
|
|
58
|
+
const SMARTCAR_AUTH_URL = 'https://connect.smartcar.com/oauth/authorize';
|
|
59
|
+
const SMARTCAR_TOKEN_URL = 'https://auth.smartcar.com/oauth/token';
|
|
60
|
+
const SMARTCAR_API_BASE = 'https://api.smartcar.com/v2.0';
|
|
61
|
+
const OAUTH_SERVER_PORT = 52625;
|
|
62
|
+
// Smartcar refresh tokens expire after ~60 days.
|
|
63
|
+
// We warn 7 days before expiry so there is plenty of time to re-auth.
|
|
64
|
+
const REFRESH_TOKEN_TTL_MS = 60 * 24 * 60 * 60 * 1000; // 60 days
|
|
65
|
+
const REAUTH_WARNING_THRESHOLD = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
66
|
+
class SmartcarClient {
|
|
67
|
+
constructor(params) {
|
|
68
|
+
this.clientId = params.clientId;
|
|
69
|
+
this.clientSecret = params.clientSecret;
|
|
70
|
+
this.redirectUri = params.redirectUri ?? `http://localhost:${OAUTH_SERVER_PORT}/callback`;
|
|
71
|
+
this.tokenPath = params.tokenStorePath;
|
|
72
|
+
this.notifyWebhookUrl = params.notifyWebhookUrl;
|
|
73
|
+
this.log = params.log;
|
|
74
|
+
this.http = axios_1.default.create({ timeout: 30000 });
|
|
75
|
+
}
|
|
76
|
+
// ─── Token persistence ────────────────────────────────────────────────────
|
|
77
|
+
saveTokens(tokens) {
|
|
78
|
+
fs.writeFileSync(this.tokenPath, JSON.stringify(tokens, null, 2), 'utf-8');
|
|
79
|
+
this.tokens = tokens;
|
|
80
|
+
this.log.info('[Smartcar] Tokens saved to %s', this.tokenPath);
|
|
81
|
+
}
|
|
82
|
+
loadTokens() {
|
|
83
|
+
try {
|
|
84
|
+
const raw = fs.readFileSync(this.tokenPath, 'utf-8');
|
|
85
|
+
return JSON.parse(raw);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// ─── Re-auth warning ──────────────────────────────────────────────────────
|
|
92
|
+
/**
|
|
93
|
+
* Returns true if the refresh_token is less than REAUTH_WARNING_THRESHOLD away
|
|
94
|
+
* from expiry (or already expired / missing).
|
|
95
|
+
*/
|
|
96
|
+
needsReauth() {
|
|
97
|
+
if (!this.tokens)
|
|
98
|
+
return true;
|
|
99
|
+
const refreshTokenExpiresAt = (this.tokens.refresh_token_obtained_at ?? (this.tokens.expires_at - 7200 * 1000))
|
|
100
|
+
+ REFRESH_TOKEN_TTL_MS;
|
|
101
|
+
return refreshTokenExpiresAt - Date.now() < REAUTH_WARNING_THRESHOLD;
|
|
102
|
+
}
|
|
103
|
+
/** Days until re-auth is required (negative = already overdue). */
|
|
104
|
+
daysUntilReauth() {
|
|
105
|
+
if (!this.tokens)
|
|
106
|
+
return 0;
|
|
107
|
+
const refreshTokenExpiresAt = (this.tokens.refresh_token_obtained_at ?? (this.tokens.expires_at - 7200 * 1000))
|
|
108
|
+
+ REFRESH_TOKEN_TTL_MS;
|
|
109
|
+
return Math.round((refreshTokenExpiresAt - Date.now()) / (24 * 60 * 60 * 1000));
|
|
110
|
+
}
|
|
111
|
+
async triggerReauthNotification() {
|
|
112
|
+
const days = this.daysUntilReauth();
|
|
113
|
+
const authUrl = `http://localhost:${OAUTH_SERVER_PORT}/auth`;
|
|
114
|
+
this.log.warn('[Smartcar] ⚠️ Re-auth required in %d day(s). Open: %s', Math.max(0, days), authUrl);
|
|
115
|
+
// Trip HomeKit sensor
|
|
116
|
+
this.onReauthRequired?.(true);
|
|
117
|
+
// Optional webhook (e.g. ntfy.sh, Pushover, Home Assistant, n8n)
|
|
118
|
+
if (this.notifyWebhookUrl) {
|
|
119
|
+
try {
|
|
120
|
+
await this.http.post(this.notifyWebhookUrl, {
|
|
121
|
+
title: 'JLR InControl: Re-auth required',
|
|
122
|
+
message: `Smartcar token expires in ${Math.max(0, days)} day(s). Open ${authUrl} to re-authorize.`,
|
|
123
|
+
url: authUrl,
|
|
124
|
+
days,
|
|
125
|
+
});
|
|
126
|
+
this.log.info('[Smartcar] Webhook notification sent to %s', this.notifyWebhookUrl);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
this.log.warn('[Smartcar] Webhook notification failed: %s', err.message);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// ─── Session management ───────────────────────────────────────────────────
|
|
134
|
+
isAccessTokenValid() {
|
|
135
|
+
return !!(this.tokens && this.tokens.expires_at > Date.now() + 60000);
|
|
136
|
+
}
|
|
137
|
+
async ensureAuthenticated() {
|
|
138
|
+
if (!this.tokens) {
|
|
139
|
+
this.tokens = this.loadTokens() ?? undefined;
|
|
140
|
+
}
|
|
141
|
+
if (!this.tokens) {
|
|
142
|
+
this.log.warn('[Smartcar] No tokens found – starting OAuth setup server...');
|
|
143
|
+
await this.startOAuthFlow();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// Proactive warning: 7 days before refresh_token dies
|
|
147
|
+
if (this.needsReauth()) {
|
|
148
|
+
await this.triggerReauthNotification();
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// Clear HomeKit sensor if everything is fine
|
|
152
|
+
this.onReauthRequired?.(false);
|
|
153
|
+
}
|
|
154
|
+
if (!this.isAccessTokenValid()) {
|
|
155
|
+
await this.refreshTokens();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// ─── OAuth 2.0 flow ───────────────────────────────────────────────────────
|
|
159
|
+
buildAuthUrl() {
|
|
160
|
+
const params = new URLSearchParams({
|
|
161
|
+
response_type: 'code',
|
|
162
|
+
client_id: this.clientId,
|
|
163
|
+
redirect_uri: this.redirectUri,
|
|
164
|
+
scope: 'required:read_vehicle_info read_vin read_charge read_battery read_fuel read_location read_odometer control_security',
|
|
165
|
+
mode: 'live',
|
|
166
|
+
});
|
|
167
|
+
return `${SMARTCAR_AUTH_URL}?${params.toString()}`;
|
|
168
|
+
}
|
|
169
|
+
startOAuthFlow() {
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
// If server already running (e.g. triggered twice), don't double-bind
|
|
172
|
+
if (this.oauthServer?.listening) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.oauthServer = http.createServer((req, res) => {
|
|
176
|
+
const url = new URL(req.url ?? '/', `http://localhost:${OAUTH_SERVER_PORT}`);
|
|
177
|
+
if (url.pathname === '/auth') {
|
|
178
|
+
const authUrl = this.buildAuthUrl();
|
|
179
|
+
res.writeHead(302, { Location: authUrl });
|
|
180
|
+
res.end();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (url.pathname === '/callback') {
|
|
184
|
+
const code = url.searchParams.get('code');
|
|
185
|
+
const error = url.searchParams.get('error');
|
|
186
|
+
if (error || !code) {
|
|
187
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
188
|
+
res.end('<h2>❌ Auth failed: ' + (error ?? 'no code') + '</h2>');
|
|
189
|
+
reject(new Error('OAuth error: ' + error));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
this.exchangeCode(code)
|
|
193
|
+
.then(() => {
|
|
194
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
195
|
+
res.end('<h2>✅ Smartcar connected!</h2>' +
|
|
196
|
+
'<p>You can close this tab. Homebridge will continue automatically.</p>');
|
|
197
|
+
this.oauthServer?.close();
|
|
198
|
+
// Clear the HomeKit alert
|
|
199
|
+
this.onReauthRequired?.(false);
|
|
200
|
+
resolve();
|
|
201
|
+
})
|
|
202
|
+
.catch(err => {
|
|
203
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
204
|
+
res.end('<h2>❌ Token exchange failed</h2><pre>' + err.message + '</pre>');
|
|
205
|
+
reject(err);
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
res.writeHead(404);
|
|
210
|
+
res.end();
|
|
211
|
+
});
|
|
212
|
+
this.oauthServer.listen(OAUTH_SERVER_PORT, () => {
|
|
213
|
+
const authUrl = `http://localhost:${OAUTH_SERVER_PORT}/auth`;
|
|
214
|
+
this.log.warn('[Smartcar] ════════════════════════════════════════════════════');
|
|
215
|
+
this.log.warn('[Smartcar] ACTION REQUIRED: Open this URL in your browser:');
|
|
216
|
+
this.log.warn('[Smartcar] %s', authUrl);
|
|
217
|
+
this.log.warn('[Smartcar] ════════════════════════════════════════════════════');
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
async exchangeCode(code) {
|
|
222
|
+
this.log.info('[Smartcar] Exchanging auth code for tokens...');
|
|
223
|
+
const resp = await this.http.post(SMARTCAR_TOKEN_URL, new URLSearchParams({
|
|
224
|
+
grant_type: 'authorization_code',
|
|
225
|
+
code,
|
|
226
|
+
redirect_uri: this.redirectUri,
|
|
227
|
+
}), {
|
|
228
|
+
auth: { username: this.clientId, password: this.clientSecret },
|
|
229
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
230
|
+
});
|
|
231
|
+
this.saveTokens({
|
|
232
|
+
access_token: resp.data.access_token,
|
|
233
|
+
refresh_token: resp.data.refresh_token,
|
|
234
|
+
expires_at: Date.now() + (resp.data.expires_in ?? 7200) * 1000,
|
|
235
|
+
refresh_token_obtained_at: Date.now(),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
async refreshTokens() {
|
|
239
|
+
if (!this.tokens?.refresh_token)
|
|
240
|
+
throw new Error('No refresh token available');
|
|
241
|
+
this.log.info('[Smartcar] Refreshing access token...');
|
|
242
|
+
try {
|
|
243
|
+
const resp = await this.http.post(SMARTCAR_TOKEN_URL, new URLSearchParams({
|
|
244
|
+
grant_type: 'refresh_token',
|
|
245
|
+
refresh_token: this.tokens.refresh_token,
|
|
246
|
+
}), {
|
|
247
|
+
auth: { username: this.clientId, password: this.clientSecret },
|
|
248
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
249
|
+
});
|
|
250
|
+
this.saveTokens({
|
|
251
|
+
access_token: resp.data.access_token,
|
|
252
|
+
refresh_token: resp.data.refresh_token ?? this.tokens.refresh_token,
|
|
253
|
+
expires_at: Date.now() + (resp.data.expires_in ?? 7200) * 1000,
|
|
254
|
+
// Only reset the clock if Smartcar issued a new refresh_token
|
|
255
|
+
refresh_token_obtained_at: resp.data.refresh_token
|
|
256
|
+
? Date.now()
|
|
257
|
+
: this.tokens.refresh_token_obtained_at,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
this.log.error('[Smartcar] Token refresh failed – starting re-auth...');
|
|
262
|
+
this.tokens = undefined;
|
|
263
|
+
fs.rmSync(this.tokenPath, { force: true });
|
|
264
|
+
await this.triggerReauthNotification();
|
|
265
|
+
await this.startOAuthFlow();
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// ─── API helpers ──────────────────────────────────────────────────────────
|
|
270
|
+
authHeaders() {
|
|
271
|
+
return { Authorization: `Bearer ${this.tokens.access_token}` };
|
|
272
|
+
}
|
|
273
|
+
async get(path) {
|
|
274
|
+
await this.ensureAuthenticated();
|
|
275
|
+
const resp = await this.http.get(`${SMARTCAR_API_BASE}${path}`, {
|
|
276
|
+
headers: this.authHeaders(),
|
|
277
|
+
});
|
|
278
|
+
return resp.data;
|
|
279
|
+
}
|
|
280
|
+
async post(path, body) {
|
|
281
|
+
await this.ensureAuthenticated();
|
|
282
|
+
const resp = await this.http.post(`${SMARTCAR_API_BASE}${path}`, body, {
|
|
283
|
+
headers: { ...this.authHeaders(), 'Content-Type': 'application/json' },
|
|
284
|
+
});
|
|
285
|
+
return resp.data;
|
|
286
|
+
}
|
|
287
|
+
// ─── Vehicle queries ──────────────────────────────────────────────────────
|
|
288
|
+
async getVehicles() {
|
|
289
|
+
const data = await this.get('/vehicles');
|
|
290
|
+
const ids = data.vehicles ?? [];
|
|
291
|
+
const summaries = await Promise.all(ids.map(async (id) => {
|
|
292
|
+
try {
|
|
293
|
+
const info = await this.get(`/vehicles/${id}`);
|
|
294
|
+
const vinData = await this.get(`/vehicles/${id}/vin`);
|
|
295
|
+
return {
|
|
296
|
+
id,
|
|
297
|
+
vin: vinData.vin,
|
|
298
|
+
nickname: `${info.year} ${info.make} ${info.model}`,
|
|
299
|
+
model: info.model,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
this.log.warn('[Smartcar] Could not load info for vehicle %s', id);
|
|
304
|
+
return { id, vin: id, nickname: id };
|
|
305
|
+
}
|
|
306
|
+
}));
|
|
307
|
+
this.log.info('[Smartcar] Found %d vehicle(s)', summaries.length);
|
|
308
|
+
return summaries;
|
|
309
|
+
}
|
|
310
|
+
async getVehicleState(vehicleId, vin) {
|
|
311
|
+
const [chargeRes, locationRes, odometerRes] = await Promise.allSettled([
|
|
312
|
+
this.get(`/vehicles/${vehicleId}/charge`),
|
|
313
|
+
this.get(`/vehicles/${vehicleId}/location`),
|
|
314
|
+
this.get(`/vehicles/${vehicleId}/odometer`),
|
|
315
|
+
]);
|
|
316
|
+
let batteryLevel;
|
|
317
|
+
let charging;
|
|
318
|
+
let fuelLevelPercent;
|
|
319
|
+
let rangeKm;
|
|
320
|
+
if (chargeRes.status === 'fulfilled') {
|
|
321
|
+
const c = chargeRes.value;
|
|
322
|
+
charging = c.state === 'CHARGING';
|
|
323
|
+
if (c.battery) {
|
|
324
|
+
batteryLevel = Math.round(c.battery.percentRemaining * 100);
|
|
325
|
+
const rv = c.battery.range.value;
|
|
326
|
+
rangeKm = c.battery.range.unit === 'miles' ? Math.round(rv * 1.60934) : Math.round(rv);
|
|
327
|
+
}
|
|
328
|
+
if (c.fuel)
|
|
329
|
+
fuelLevelPercent = Math.round(c.fuel.percentRemaining * 100);
|
|
330
|
+
}
|
|
331
|
+
let latitude;
|
|
332
|
+
let longitude;
|
|
333
|
+
let isMoving;
|
|
334
|
+
if (locationRes.status === 'fulfilled') {
|
|
335
|
+
latitude = locationRes.value.latitude;
|
|
336
|
+
longitude = locationRes.value.longitude;
|
|
337
|
+
isMoving = (locationRes.value.speed?.value ?? 0) > 2;
|
|
338
|
+
}
|
|
339
|
+
let odometerKm;
|
|
340
|
+
if (odometerRes.status === 'fulfilled') {
|
|
341
|
+
const d = odometerRes.value.distance;
|
|
342
|
+
odometerKm = d.unit === 'miles' ? Math.round(d.value * 1.60934) : Math.round(d.value);
|
|
343
|
+
}
|
|
344
|
+
let isLocked = false;
|
|
345
|
+
try {
|
|
346
|
+
const sec = await this.get(`/vehicles/${vehicleId}/security`);
|
|
347
|
+
isLocked = sec.isLocked;
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
this.log.debug('[Smartcar] security endpoint not available for this vehicle');
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
vin, isLocked, batteryLevel, charging,
|
|
354
|
+
lowBattery: batteryLevel !== undefined ? batteryLevel < 20 : undefined,
|
|
355
|
+
fuelLevelPercent, rangeKm, odometerKm,
|
|
356
|
+
latitude, longitude, isMoving,
|
|
357
|
+
lastUpdated: new Date().toISOString(),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
// ─── Commands ─────────────────────────────────────────────────────────────
|
|
361
|
+
async lock(vehicleId) {
|
|
362
|
+
await this.post(`/vehicles/${vehicleId}/security`, { action: 'LOCK' });
|
|
363
|
+
}
|
|
364
|
+
async unlock(vehicleId) {
|
|
365
|
+
await this.post(`/vehicles/${vehicleId}/security`, { action: 'UNLOCK' });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
exports.SmartcarClient = SmartcarClient;
|
|
369
|
+
//# sourceMappingURL=smartcar-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smartcar-client.js","sourceRoot":"","sources":["../src/smartcar-client.ts"],"names":[],"mappings":";AAAA,EAAE;AACF,0DAA0D;AAC1D,EAAE;AACF,kBAAkB;AAClB,oEAAoE;AACpE,wCAAwC;AACxC,0DAA0D;AAC1D,+DAA+D;AAC/D,2DAA2D;AAC3D,EAAE;AACF,wBAAwB;AACxB,+EAA+E;AAC/E,iDAAiD;AACjD,6EAA6E;AAC7E,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEF,kDAA6C;AAC7C,2CAA6B;AAC7B,uCAAyB;AAKzB,MAAM,iBAAiB,GAAK,8CAA8C,CAAC;AAC3E,MAAM,kBAAkB,GAAI,uCAAuC,CAAC;AACpE,MAAM,iBAAiB,GAAK,+BAA+B,CAAC;AAC5D,MAAM,iBAAiB,GAAK,KAAK,CAAC;AAElC,iDAAiD;AACjD,sEAAsE;AACtE,MAAM,oBAAoB,GAAQ,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,UAAU;AACtE,MAAM,wBAAwB,GAAK,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,UAAU;AAEtE,MAAa,cAAc;IAiBzB,YAAY,MAOX;QACC,IAAI,CAAC,QAAQ,GAAY,MAAM,CAAC,QAAQ,CAAC;QACzC,IAAI,CAAC,YAAY,GAAQ,MAAM,CAAC,YAAY,CAAC;QAC7C,IAAI,CAAC,WAAW,GAAS,MAAM,CAAC,WAAW,IAAI,oBAAoB,iBAAiB,WAAW,CAAC;QAChG,IAAI,CAAC,SAAS,GAAW,MAAM,CAAC,cAAc,CAAC;QAC/C,IAAI,CAAC,gBAAgB,GAAI,MAAM,CAAC,gBAAgB,CAAC;QACjD,IAAI,CAAC,GAAG,GAAiB,MAAM,CAAC,GAAG,CAAC;QACpC,IAAI,CAAC,IAAI,GAAgB,eAAK,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED,6EAA6E;IAErE,UAAU,CAAC,MAAsB;QACvC,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;QAC3E,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,+BAA+B,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IACjE,CAAC;IAEO,UAAU;QAChB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YACrD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAmB,CAAC;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,6EAA6E;IAE7E;;;OAGG;IACH,WAAW;QACT,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QAC9B,MAAM,qBAAqB,GACzB,CAAC,IAAI,CAAC,MAAM,CAAC,yBAAyB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC;cAC/E,oBAAoB,CAAC;QACzB,OAAO,qBAAqB,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,wBAAwB,CAAC;IACvE,CAAC;IAED,mEAAmE;IACnE,eAAe;QACb,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO,CAAC,CAAC;QAC3B,MAAM,qBAAqB,GACzB,CAAC,IAAI,CAAC,MAAM,CAAC,yBAAyB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC;cAC/E,oBAAoB,CAAC;QACzB,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,qBAAqB,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC;IAClF,CAAC;IAEO,KAAK,CAAC,yBAAyB;QACrC,MAAM,IAAI,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;QACpC,MAAM,OAAO,GAAG,oBAAoB,iBAAiB,OAAO,CAAC;QAE7D,IAAI,CAAC,GAAG,CAAC,IAAI,CACX,wDAAwD,EACxD,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,EACjB,OAAO,CACR,CAAC;QAEF,sBAAsB;QACtB,IAAI,CAAC,gBAAgB,EAAE,CAAC,IAAI,CAAC,CAAC;QAE9B,iEAAiE;QACjE,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC1B,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE;oBAC1C,KAAK,EAAI,iCAAiC;oBAC1C,OAAO,EAAE,6BAA6B,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,iBAAiB,OAAO,mBAAmB;oBAClG,GAAG,EAAM,OAAO;oBAChB,IAAI;iBACL,CAAC,CAAC;gBACH,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,4CAA4C,EAAE,IAAI,CAAC,gBAAgB,CAAC,CAAC;YACrF,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,4CAA4C,EAAG,GAAa,CAAC,OAAO,CAAC,CAAC;YACtF,CAAC;QACH,CAAC;IACH,CAAC;IAED,6EAA6E;IAErE,kBAAkB;QACxB,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAM,CAAC,CAAC;IACzE,CAAC;IAED,KAAK,CAAC,mBAAmB;QACvB,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE,IAAI,SAAS,CAAC;QAC/C,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,6DAA6D,CAAC,CAAC;YAC7E,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;YAC5B,OAAO;QACT,CAAC;QAED,sDAAsD;QACtD,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YACvB,MAAM,IAAI,CAAC,yBAAyB,EAAE,CAAC;QACzC,CAAC;aAAM,CAAC;YACN,6CAA6C;YAC7C,IAAI,CAAC,gBAAgB,EAAE,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,EAAE,CAAC;YAC/B,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;QAC7B,CAAC;IACH,CAAC;IAED,6EAA6E;IAErE,YAAY;QAClB,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;YACjC,aAAa,EAAE,MAAM;YACrB,SAAS,EAAM,IAAI,CAAC,QAAQ;YAC5B,YAAY,EAAG,IAAI,CAAC,WAAW;YAC/B,KAAK,EAAU,qHAAqH;YACpI,IAAI,EAAW,MAAM;SACtB,CAAC,CAAC;QACH,OAAO,GAAG,iBAAiB,IAAI,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;IACrD,CAAC;IAEO,cAAc;QACpB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,sEAAsE;YACtE,IAAI,IAAI,CAAC,WAAW,EAAE,SAAS,EAAE,CAAC;gBAChC,OAAO;YACT,CAAC;YAED,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;gBAChD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,oBAAoB,iBAAiB,EAAE,CAAC,CAAC;gBAE7E,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;oBAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;oBACpC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;oBAC1C,GAAG,CAAC,GAAG,EAAE,CAAC;oBACV,OAAO;gBACT,CAAC;gBAED,IAAI,GAAG,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;oBACjC,MAAM,IAAI,GAAI,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;oBAC3C,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;oBAE5C,IAAI,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;wBACnB,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;wBACpD,GAAG,CAAC,GAAG,CAAC,qBAAqB,GAAG,CAAC,KAAK,IAAI,SAAS,CAAC,GAAG,OAAO,CAAC,CAAC;wBAChE,MAAM,CAAC,IAAI,KAAK,CAAC,eAAe,GAAG,KAAK,CAAC,CAAC,CAAC;wBAC3C,OAAO;oBACT,CAAC;oBAED,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC;yBACpB,IAAI,CAAC,GAAG,EAAE;wBACT,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;wBACpD,GAAG,CAAC,GAAG,CACL,gCAAgC;4BAChC,wEAAwE,CACzE,CAAC;wBACF,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,CAAC;wBAC1B,0BAA0B;wBAC1B,IAAI,CAAC,gBAAgB,EAAE,CAAC,KAAK,CAAC,CAAC;wBAC/B,OAAO,EAAE,CAAC;oBACZ,CAAC,CAAC;yBACD,KAAK,CAAC,GAAG,CAAC,EAAE;wBACX,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;wBACpD,GAAG,CAAC,GAAG,CAAC,uCAAuC,GAAG,GAAG,CAAC,OAAO,GAAG,QAAQ,CAAC,CAAC;wBAC1E,MAAM,CAAC,GAAG,CAAC,CAAC;oBACd,CAAC,CAAC,CAAC;oBACL,OAAO;gBACT,CAAC;gBAED,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;gBACnB,GAAG,CAAC,GAAG,EAAE,CAAC;YACZ,CAAC,CAAC,CAAC;YAEH,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,iBAAiB,EAAE,GAAG,EAAE;gBAC9C,MAAM,OAAO,GAAG,oBAAoB,iBAAiB,OAAO,CAAC;gBAC7D,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,iEAAiE,CAAC,CAAC;gBACjF,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,4DAA4D,CAAC,CAAC;gBAC5E,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;gBAC1C,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,iEAAiE,CAAC,CAAC;YACnF,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,YAAY,CAAC,IAAY;QACrC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,+CAA+C,CAAC,CAAC;QAC/D,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAC/B,kBAAkB,EAClB,IAAI,eAAe,CAAC;YAClB,UAAU,EAAI,oBAAoB;YAClC,IAAI;YACJ,YAAY,EAAE,IAAI,CAAC,WAAW;SAC/B,CAAC,EACF;YACE,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE;YAC9D,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;SACjE,CACF,CAAC;QAEF,IAAI,CAAC,UAAU,CAAC;YACd,YAAY,EAAiB,IAAI,CAAC,IAAI,CAAC,YAAY;YACnD,aAAa,EAAgB,IAAI,CAAC,IAAI,CAAC,aAAa;YACpD,UAAU,EAAmB,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,GAAG,IAAI;YAC/E,yBAAyB,EAAI,IAAI,CAAC,GAAG,EAAE;SACxC,CAAC,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,aAAa;QACzB,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,aAAa;YAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;QAC/E,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,uCAAuC,CAAC,CAAC;QAEvD,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAC/B,kBAAkB,EAClB,IAAI,eAAe,CAAC;gBAClB,UAAU,EAAK,eAAe;gBAC9B,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,aAAa;aACzC,CAAC,EACF;gBACE,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE;gBAC9D,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;aACjE,CACF,CAAC;YAEF,IAAI,CAAC,UAAU,CAAC;gBACd,YAAY,EAAe,IAAI,CAAC,IAAI,CAAC,YAAY;gBACjD,aAAa,EAAc,IAAI,CAAC,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,MAAM,CAAC,aAAa;gBAC/E,UAAU,EAAiB,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,GAAG,IAAI;gBAC7E,8DAA8D;gBAC9D,yBAAyB,EAAE,IAAI,CAAC,IAAI,CAAC,aAAa;oBAChD,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;oBACZ,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,yBAAyB;aAC1C,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,uDAAuD,CAAC,CAAC;YACxE,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;YACxB,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAC3C,MAAM,IAAI,CAAC,yBAAyB,EAAE,CAAC;YACvC,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;YAC5B,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED,6EAA6E;IAErE,WAAW;QACjB,OAAO,EAAE,aAAa,EAAE,UAAU,IAAI,CAAC,MAAO,CAAC,YAAY,EAAE,EAAE,CAAC;IAClE,CAAC;IAEO,KAAK,CAAC,GAAG,CAAI,IAAY;QAC/B,MAAM,IAAI,CAAC,mBAAmB,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAI,GAAG,iBAAiB,GAAG,IAAI,EAAE,EAAE;YACjE,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE;SAC5B,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAEO,KAAK,CAAC,IAAI,CAAI,IAAY,EAAE,IAAa;QAC/C,MAAM,IAAI,CAAC,mBAAmB,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAI,GAAG,iBAAiB,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE;YACxE,OAAO,EAAE,EAAE,GAAG,IAAI,CAAC,WAAW,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SACvE,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED,6EAA6E;IAE7E,KAAK,CAAC,WAAW;QACf,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAyB,WAAW,CAAC,CAAC;QACjE,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC;QAEhC,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,GAAG,CACjC,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;YACnB,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAExB,aAAa,EAAE,EAAE,CAAC,CAAC;gBACtB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,GAAG,CAAkB,aAAa,EAAE,MAAM,CAAC,CAAC;gBACvE,OAAO;oBACL,EAAE;oBACF,GAAG,EAAO,OAAO,CAAC,GAAG;oBACrB,QAAQ,EAAE,GAAG,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE;oBACnD,KAAK,EAAK,IAAI,CAAC,KAAK;iBACA,CAAC;YACzB,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,+CAA+C,EAAE,EAAE,CAAC,CAAC;gBACnE,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAuB,CAAC;YAC5D,CAAC;QACH,CAAC,CAAC,CACH,CAAC;QAEF,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,gCAAgC,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;QAClE,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,SAAiB,EAAE,GAAW;QAClD,MAAM,CAAC,SAAS,EAAE,WAAW,EAAE,WAAW,CAAC,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC;YACrE,IAAI,CAAC,GAAG,CAIL,aAAa,SAAS,SAAS,CAAC;YACnC,IAAI,CAAC,GAAG,CACN,aAAa,SAAS,WAAW,CAClC;YACD,IAAI,CAAC,GAAG,CAAgD,aAAa,SAAS,WAAW,CAAC;SAC3F,CAAC,CAAC;QAEH,IAAI,YAAgC,CAAC;QACrC,IAAI,QAA6B,CAAC;QAClC,IAAI,gBAAoC,CAAC;QACzC,IAAI,OAA2B,CAAC;QAEhC,IAAI,SAAS,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YACrC,MAAM,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC;YAC1B,QAAQ,GAAG,CAAC,CAAC,KAAK,KAAK,UAAU,CAAC;YAClC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;gBACd,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,gBAAgB,GAAG,GAAG,CAAC,CAAC;gBAC5D,MAAM,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC;gBACjC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACzF,CAAC;YACD,IAAI,CAAC,CAAC,IAAI;gBAAE,gBAAgB,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,gBAAgB,GAAG,GAAG,CAAC,CAAC;QAC3E,CAAC;QAED,IAAI,QAA4B,CAAC;QACjC,IAAI,SAA6B,CAAC;QAClC,IAAI,QAA6B,CAAC;QAClC,IAAI,WAAW,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YACvC,QAAQ,GAAI,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC;YACvC,SAAS,GAAG,WAAW,CAAC,KAAK,CAAC,SAAS,CAAC;YACxC,QAAQ,GAAI,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QACxD,CAAC;QAED,IAAI,UAA8B,CAAC;QACnC,IAAI,WAAW,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YACvC,MAAM,CAAC,GAAG,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC;YACrC,UAAU,GAAG,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACxF,CAAC;QAED,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,GAAG,CAAwB,aAAa,SAAS,WAAW,CAAC,CAAC;YACrF,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;QAC1B,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,6DAA6D,CAAC,CAAC;QAChF,CAAC;QAED,OAAO;YACL,GAAG,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ;YACrC,UAAU,EAAE,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,YAAY,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS;YACtE,gBAAgB,EAAE,OAAO,EAAE,UAAU;YACrC,QAAQ,EAAE,SAAS,EAAE,QAAQ;YAC7B,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACtC,CAAC;IACJ,CAAC;IAED,6EAA6E;IAE7E,KAAK,CAAC,IAAI,CAAC,SAAiB;QAC1B,MAAM,IAAI,CAAC,IAAI,CAAC,aAAa,SAAS,WAAW,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACzE,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,SAAiB;QAC5B,MAAM,IAAI,CAAC,IAAI,CAAC,aAAa,SAAS,WAAW,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC3E,CAAC;CACF;AArYD,wCAqYC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface SmartcarTokens {
|
|
2
|
+
access_token: string;
|
|
3
|
+
refresh_token: string;
|
|
4
|
+
expires_at: number;
|
|
5
|
+
refresh_token_obtained_at?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface PluginConfig {
|
|
8
|
+
platform: string;
|
|
9
|
+
name: string;
|
|
10
|
+
clientId: string;
|
|
11
|
+
clientSecret: string;
|
|
12
|
+
redirectUri?: string;
|
|
13
|
+
pin?: string;
|
|
14
|
+
pollIntervalSeconds?: number;
|
|
15
|
+
notifyWebhookUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface JlrVehicleSummary {
|
|
18
|
+
id: string;
|
|
19
|
+
vin: string;
|
|
20
|
+
nickname: string;
|
|
21
|
+
model?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface JlrVehicleState {
|
|
24
|
+
vin: string;
|
|
25
|
+
isLocked: boolean;
|
|
26
|
+
batteryLevel?: number;
|
|
27
|
+
charging?: boolean;
|
|
28
|
+
lowBattery?: boolean;
|
|
29
|
+
fuelLevelPercent?: number;
|
|
30
|
+
rangeKm?: number;
|
|
31
|
+
odometerKm?: number;
|
|
32
|
+
latitude?: number;
|
|
33
|
+
longitude?: number;
|
|
34
|
+
isMoving?: boolean;
|
|
35
|
+
lastUpdated: string;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,yBAAyB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { PlatformAccessory, Logger } from 'homebridge';
|
|
2
|
+
import { JlrPlatform } from './platform';
|
|
3
|
+
import { SmartcarClient } from './smartcar-client';
|
|
4
|
+
export declare class VehicleAccessory {
|
|
5
|
+
private readonly platform;
|
|
6
|
+
private readonly accessory;
|
|
7
|
+
private readonly client;
|
|
8
|
+
private readonly log;
|
|
9
|
+
private lockService;
|
|
10
|
+
private batteryService;
|
|
11
|
+
private state;
|
|
12
|
+
private pollTimer?;
|
|
13
|
+
private get vehicle();
|
|
14
|
+
constructor(platform: JlrPlatform, accessory: PlatformAccessory, client: SmartcarClient, log: Logger);
|
|
15
|
+
private setupServices;
|
|
16
|
+
private getLockCurrentState;
|
|
17
|
+
private getLockTargetState;
|
|
18
|
+
private setLockTargetState;
|
|
19
|
+
startPolling(intervalMs: number): void;
|
|
20
|
+
private poll;
|
|
21
|
+
private pushStateToHomeKit;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=vehicle-accessory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vehicle-accessory.d.ts","sourceRoot":"","sources":["../src/vehicle-accessory.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EAEjB,MAAM,EACP,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGnD,qBAAa,gBAAgB;IAYzB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAdtB,OAAO,CAAC,WAAW,CAAW;IAC9B,OAAO,CAAC,cAAc,CAAW;IAEjC,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,SAAS,CAAC,CAAiC;IAEnD,OAAO,KAAK,OAAO,GAElB;gBAGkB,QAAQ,EAAE,WAAW,EACrB,SAAS,EAAE,iBAAiB,EAC5B,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,MAAM;IAO9B,OAAO,CAAC,aAAa;IA+CrB,OAAO,CAAC,mBAAmB;IAM3B,OAAO,CAAC,kBAAkB;YAMZ,kBAAkB;IAoBhC,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;YAKxB,IAAI;IAgBlB,OAAO,CAAC,kBAAkB;CA2B3B"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VehicleAccessory = void 0;
|
|
4
|
+
class VehicleAccessory {
|
|
5
|
+
get vehicle() {
|
|
6
|
+
return this.accessory.context.vehicle;
|
|
7
|
+
}
|
|
8
|
+
constructor(platform, accessory, client, log) {
|
|
9
|
+
this.platform = platform;
|
|
10
|
+
this.accessory = accessory;
|
|
11
|
+
this.client = client;
|
|
12
|
+
this.log = log;
|
|
13
|
+
this.state = null;
|
|
14
|
+
this.setupServices();
|
|
15
|
+
}
|
|
16
|
+
// ─── HomeKit services ─────────────────────────────────────────────────────
|
|
17
|
+
setupServices() {
|
|
18
|
+
const { Service, Characteristic } = this.platform;
|
|
19
|
+
// Accessory info
|
|
20
|
+
this.accessory
|
|
21
|
+
.getService(Service.AccessoryInformation)
|
|
22
|
+
.setCharacteristic(Characteristic.Manufacturer, 'Jaguar Land Rover')
|
|
23
|
+
.setCharacteristic(Characteristic.Model, this.vehicle.model ?? 'JLR Vehicle')
|
|
24
|
+
.setCharacteristic(Characteristic.SerialNumber, this.vehicle.vin);
|
|
25
|
+
// Lock mechanism
|
|
26
|
+
this.lockService =
|
|
27
|
+
this.accessory.getService(Service.LockMechanism) ??
|
|
28
|
+
this.accessory.addService(Service.LockMechanism, 'Door Lock');
|
|
29
|
+
this.lockService
|
|
30
|
+
.getCharacteristic(Characteristic.LockCurrentState)
|
|
31
|
+
.onGet(() => this.getLockCurrentState());
|
|
32
|
+
this.lockService
|
|
33
|
+
.getCharacteristic(Characteristic.LockTargetState)
|
|
34
|
+
.onGet(() => this.getLockTargetState())
|
|
35
|
+
.onSet((value) => this.setLockTargetState(value));
|
|
36
|
+
// Battery (EV)
|
|
37
|
+
this.batteryService =
|
|
38
|
+
this.accessory.getService(Service.Battery) ??
|
|
39
|
+
this.accessory.addService(Service.Battery, 'Battery');
|
|
40
|
+
this.batteryService
|
|
41
|
+
.getCharacteristic(Characteristic.BatteryLevel)
|
|
42
|
+
.onGet(() => this.state?.batteryLevel ?? 0);
|
|
43
|
+
this.batteryService
|
|
44
|
+
.getCharacteristic(Characteristic.ChargingState)
|
|
45
|
+
.onGet(() => {
|
|
46
|
+
if (this.state?.charging)
|
|
47
|
+
return 1; // CHARGING
|
|
48
|
+
return 0; // NOT_CHARGING
|
|
49
|
+
});
|
|
50
|
+
this.batteryService
|
|
51
|
+
.getCharacteristic(Characteristic.StatusLowBattery)
|
|
52
|
+
.onGet(() => (this.state?.lowBattery ? 1 : 0));
|
|
53
|
+
}
|
|
54
|
+
// ─── Lock handlers ────────────────────────────────────────────────────────
|
|
55
|
+
getLockCurrentState() {
|
|
56
|
+
const { Characteristic } = this.platform;
|
|
57
|
+
if (this.state?.isLocked)
|
|
58
|
+
return Characteristic.LockCurrentState.SECURED;
|
|
59
|
+
return Characteristic.LockCurrentState.UNSECURED;
|
|
60
|
+
}
|
|
61
|
+
getLockTargetState() {
|
|
62
|
+
const { Characteristic } = this.platform;
|
|
63
|
+
if (this.state?.isLocked)
|
|
64
|
+
return Characteristic.LockTargetState.SECURED;
|
|
65
|
+
return Characteristic.LockTargetState.UNSECURED;
|
|
66
|
+
}
|
|
67
|
+
async setLockTargetState(value) {
|
|
68
|
+
const { Characteristic } = this.platform;
|
|
69
|
+
try {
|
|
70
|
+
if (value === Characteristic.LockTargetState.SECURED) {
|
|
71
|
+
this.log.info('[%s] Locking...', this.vehicle.nickname);
|
|
72
|
+
await this.client.lock(this.vehicle.id);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
this.log.info('[%s] Unlocking...', this.vehicle.nickname);
|
|
76
|
+
await this.client.unlock(this.vehicle.id);
|
|
77
|
+
}
|
|
78
|
+
// optimistic update
|
|
79
|
+
if (this.state)
|
|
80
|
+
this.state.isLocked = value === Characteristic.LockTargetState.SECURED;
|
|
81
|
+
this.pushStateToHomeKit();
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
this.log.error('[%s] Lock command failed: %s', this.vehicle.nickname, err.message);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ─── Polling ──────────────────────────────────────────────────────────────
|
|
88
|
+
startPolling(intervalMs) {
|
|
89
|
+
this.poll(); // immediate first poll
|
|
90
|
+
this.pollTimer = setInterval(() => this.poll(), intervalMs);
|
|
91
|
+
}
|
|
92
|
+
async poll() {
|
|
93
|
+
try {
|
|
94
|
+
this.state = await this.client.getVehicleState(this.vehicle.id, this.vehicle.vin);
|
|
95
|
+
this.log.debug('[%s] State: locked=%s battery=%s%% charging=%s', this.vehicle.nickname, this.state.isLocked, this.state.batteryLevel ?? 'N/A', this.state.charging);
|
|
96
|
+
this.pushStateToHomeKit();
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
this.log.warn('[%s] Poll failed: %s', this.vehicle.nickname, err.message);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
pushStateToHomeKit() {
|
|
103
|
+
if (!this.state)
|
|
104
|
+
return;
|
|
105
|
+
const { Characteristic } = this.platform;
|
|
106
|
+
this.lockService
|
|
107
|
+
.updateCharacteristic(Characteristic.LockCurrentState, this.state.isLocked
|
|
108
|
+
? Characteristic.LockCurrentState.SECURED
|
|
109
|
+
: Characteristic.LockCurrentState.UNSECURED);
|
|
110
|
+
if (this.state.batteryLevel !== undefined) {
|
|
111
|
+
this.batteryService.updateCharacteristic(Characteristic.BatteryLevel, this.state.batteryLevel);
|
|
112
|
+
this.batteryService.updateCharacteristic(Characteristic.StatusLowBattery, this.state.lowBattery ? 1 : 0);
|
|
113
|
+
}
|
|
114
|
+
if (this.state.charging !== undefined) {
|
|
115
|
+
this.batteryService.updateCharacteristic(Characteristic.ChargingState, this.state.charging ? 1 : 0);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exports.VehicleAccessory = VehicleAccessory;
|
|
120
|
+
//# sourceMappingURL=vehicle-accessory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vehicle-accessory.js","sourceRoot":"","sources":["../src/vehicle-accessory.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "homebridge-jlr-smartcar",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Homebridge plugin for Jaguar Land Rover InControl via Smartcar API",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"watch": "tsc --watch",
|
|
10
|
+
"lint": "eslint src --ext .ts",
|
|
11
|
+
"prepublishOnly": "npm run build"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"homebridge-plugin",
|
|
15
|
+
"jaguar",
|
|
16
|
+
"landrover",
|
|
17
|
+
"jlr",
|
|
18
|
+
"incontrol",
|
|
19
|
+
"smartcar"
|
|
20
|
+
],
|
|
21
|
+
"author": "StefanPeetz",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"homepage": "https://github.com/StefanPeetz/homebridge-jlr-smartcar#readme",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/StefanPeetz/homebridge-jlr-smartcar.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/StefanPeetz/homebridge-jlr-smartcar/issues"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.0.0",
|
|
33
|
+
"homebridge": ">=1.6.0"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"axios": "^1.7.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@homebridge/plugin-ui-utils": "^2.0.0",
|
|
40
|
+
"@types/node": "^20.0.0",
|
|
41
|
+
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
42
|
+
"@typescript-eslint/parser": "^7.0.0",
|
|
43
|
+
"eslint": "^8.57.0",
|
|
44
|
+
"homebridge": "^1.8.0",
|
|
45
|
+
"typescript": "^5.4.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"homebridge": "^1.6.0"
|
|
49
|
+
}
|
|
50
|
+
}
|