node-chargepoint 0.3.4 → 0.4.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 +3 -157
- package/dist/cli.cjs +96 -9
- package/dist/index.cjs +30 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -5
- package/dist/index.d.ts +18 -5
- package/dist/index.js +30 -10
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -289,172 +289,18 @@ for (const s of filtered) {
|
|
|
289
289
|
|
|
290
290
|
## CLI
|
|
291
291
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
### Authentication
|
|
295
|
-
|
|
296
|
-
Credentials are read from environment variables:
|
|
297
|
-
|
|
298
|
-
```bash
|
|
299
|
-
export CP_USERNAME="user@example.com"
|
|
300
|
-
export CP_TOKEN="<coulomb_sess cookie value>"
|
|
301
|
-
# or use --password / CP_PASSWORD for password-based login
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
### Commands
|
|
305
|
-
|
|
306
|
-
```bash
|
|
307
|
-
# Show account info
|
|
308
|
-
chargepoint -u user@example.com -t $CP_TOKEN account
|
|
309
|
-
|
|
310
|
-
# List registered vehicles
|
|
311
|
-
chargepoint -u user@example.com -t $CP_TOKEN vehicles
|
|
312
|
-
|
|
313
|
-
# Show current charging session status
|
|
314
|
-
chargepoint -u user@example.com -t $CP_TOKEN status
|
|
315
|
-
|
|
316
|
-
# List home charger IDs
|
|
317
|
-
chargepoint -u user@example.com -t $CP_TOKEN chargers
|
|
318
|
-
|
|
319
|
-
# Start a charging session on a device
|
|
320
|
-
chargepoint -u user@example.com -t $CP_TOKEN start <deviceId>
|
|
321
|
-
|
|
322
|
-
# Show details for a charging station
|
|
323
|
-
chargepoint -u user@example.com -t $CP_TOKEN station <deviceId>
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
Global options:
|
|
327
|
-
|
|
328
|
-
```
|
|
329
|
-
-u, --username <username> ChargePoint username (or set CP_USERNAME)
|
|
330
|
-
-t, --token <token> Coulomb session token (or set CP_TOKEN)
|
|
331
|
-
-p, --password <password> Password login fallback (or set CP_PASSWORD)
|
|
332
|
-
-V, --version Print version
|
|
333
|
-
-h, --help Display help
|
|
334
|
-
```
|
|
292
|
+
See [docs/cli.md](docs/cli.md) for usage, commands, and global options.
|
|
335
293
|
|
|
336
294
|
---
|
|
337
295
|
|
|
338
296
|
## Error Handling
|
|
339
297
|
|
|
340
|
-
|
|
341
|
-
import {
|
|
342
|
-
ChargePoint,
|
|
343
|
-
LoginError,
|
|
344
|
-
InvalidSession,
|
|
345
|
-
DatadomeCaptcha,
|
|
346
|
-
CommunicationError,
|
|
347
|
-
APIError,
|
|
348
|
-
} from 'node-chargepoint';
|
|
349
|
-
|
|
350
|
-
try {
|
|
351
|
-
await client.loginWithPassword('bad-password');
|
|
352
|
-
} catch (err) {
|
|
353
|
-
if (err instanceof LoginError) {
|
|
354
|
-
console.error('Wrong credentials');
|
|
355
|
-
} else if (err instanceof InvalidSession) {
|
|
356
|
-
console.error('Session expired — re-authenticate');
|
|
357
|
-
} else if (err instanceof DatadomeCaptcha) {
|
|
358
|
-
console.error('Bot protection triggered:', err.captchaUrl);
|
|
359
|
-
} else if (err instanceof CommunicationError) {
|
|
360
|
-
console.error(`API error ${err.statusCode}:`, err.message);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
**Error hierarchy:**
|
|
366
|
-
|
|
367
|
-
```
|
|
368
|
-
APIError
|
|
369
|
-
├── CommunicationError (non-2xx response)
|
|
370
|
-
│ ├── LoginError (authentication failed)
|
|
371
|
-
│ └── InvalidSession (session expired — HTTP 401)
|
|
372
|
-
└── DatadomeCaptcha (Datadome bot protection — HTTP 403)
|
|
373
|
-
```
|
|
298
|
+
See [docs/error-handling.md](docs/error-handling.md) for the error class hierarchy and handling examples.
|
|
374
299
|
|
|
375
300
|
---
|
|
376
301
|
|
|
377
302
|
## Development
|
|
378
303
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
```bash
|
|
382
|
-
git clone https://github.com/musicbender/node-chargepoint.git
|
|
383
|
-
cd node-chargepoint
|
|
384
|
-
pnpm install
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
### Type checking
|
|
388
|
-
|
|
389
|
-
```bash
|
|
390
|
-
pnpm typecheck
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
### Tests
|
|
394
|
-
|
|
395
|
-
```bash
|
|
396
|
-
pnpm test
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
Tests use [Vitest](https://vitest.dev/) and [MSW](https://mswjs.io/) to intercept `fetch` calls. No real network traffic is made during tests.
|
|
400
|
-
|
|
401
|
-
### E2E Tests
|
|
402
|
-
|
|
403
|
-
An optional E2E suite runs against the live ChargePoint API using a real home charger. It requires valid credentials and is not run as part of `pnpm test`.
|
|
404
|
-
|
|
405
|
-
**Setup:**
|
|
406
|
-
|
|
407
|
-
Create a `.env.e2e` file in the project root (it is gitignored):
|
|
408
|
-
|
|
409
|
-
```
|
|
410
|
-
CP_USERNAME=your-chargepoint-email@example.com
|
|
411
|
-
CP_PASSWORD=your-chargepoint-password
|
|
412
|
-
|
|
413
|
-
# After first run, paste the printed token here to avoid re-logging in:
|
|
414
|
-
# CP_TOKEN=
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
> **Tip:** `coulomb_sess` tokens contain special characters (`#`, `?`). Always wrap the value in
|
|
418
|
-
> double quotes when exporting to your shell:
|
|
419
|
-
> ```bash
|
|
420
|
-
> export CP_TOKEN="abc...#D???"
|
|
421
|
-
> ```
|
|
422
|
-
|
|
423
|
-
**Run (read-only — safe with charger in any state):**
|
|
424
|
-
|
|
425
|
-
```bash
|
|
426
|
-
pnpm test:e2e
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
On first run the session token is printed. Paste it into `CP_TOKEN` in `.env.e2e` to skip
|
|
430
|
-
password re-authentication on subsequent runs.
|
|
431
|
-
|
|
432
|
-
**Run with mutation tests** (schedule, amperage limit, LED brightness — each test restores
|
|
433
|
-
original values):
|
|
434
|
-
|
|
435
|
-
```bash
|
|
436
|
-
E2E_MUTATIONS=true pnpm test:e2e
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
The following operations are intentionally excluded from the automated suite due to their
|
|
440
|
-
disruptive nature: `restartHomeCharger()`, `startChargingSession()`, `stopChargingSession()`.
|
|
441
|
-
Use the CLI to invoke these manually.
|
|
442
|
-
|
|
443
|
-
### Build
|
|
444
|
-
|
|
445
|
-
```bash
|
|
446
|
-
pnpm build
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
Produces `dist/index.js` (ESM), `dist/index.cjs` (CommonJS), and `dist/index.d.ts` (type declarations) via [tsup](https://tsup.egoist.dev/).
|
|
450
|
-
|
|
451
|
-
### Checks
|
|
452
|
-
|
|
453
|
-
| Command | Purpose |
|
|
454
|
-
|---|---|
|
|
455
|
-
| `pnpm typecheck` | TypeScript strict type checking |
|
|
456
|
-
| `pnpm test` | Run all tests |
|
|
457
|
-
| `pnpm build` | Build CJS + ESM + `.d.ts` |
|
|
458
|
-
| `pnpm prepublishOnly` | Build + typecheck (runs automatically before `pnpm publish`) |
|
|
304
|
+
See [docs/development.md](docs/development.md) for setup, tests, E2E tests, and build instructions.
|
|
459
305
|
|
|
460
306
|
---
|
package/dist/cli.cjs
CHANGED
|
@@ -26,9 +26,81 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
26
|
// src/cli.ts
|
|
27
27
|
var import_commander = require("commander");
|
|
28
28
|
|
|
29
|
+
// package.json
|
|
30
|
+
var package_default = {
|
|
31
|
+
name: "node-chargepoint",
|
|
32
|
+
version: "0.4.0",
|
|
33
|
+
description: "A Node.js/TypeScript wrapper for the ChargePoint EV charging network API. Based on python-chargepoint by Marc Billow.",
|
|
34
|
+
keywords: [
|
|
35
|
+
"chargepoint",
|
|
36
|
+
"ev",
|
|
37
|
+
"electric-vehicle",
|
|
38
|
+
"charging",
|
|
39
|
+
"evse"
|
|
40
|
+
],
|
|
41
|
+
homepage: "https://github.com/musicbender/node-chargepoint",
|
|
42
|
+
repository: {
|
|
43
|
+
type: "git",
|
|
44
|
+
url: "git+https://github.com/musicbender/node-chargepoint.git"
|
|
45
|
+
},
|
|
46
|
+
bugs: {
|
|
47
|
+
url: "https://github.com/musicbender/node-chargepoint/issues"
|
|
48
|
+
},
|
|
49
|
+
license: "MIT",
|
|
50
|
+
type: "module",
|
|
51
|
+
main: "./dist/index.cjs",
|
|
52
|
+
module: "./dist/index.js",
|
|
53
|
+
types: "./dist/index.d.ts",
|
|
54
|
+
exports: {
|
|
55
|
+
".": {
|
|
56
|
+
import: {
|
|
57
|
+
types: "./dist/index.d.ts",
|
|
58
|
+
default: "./dist/index.js"
|
|
59
|
+
},
|
|
60
|
+
require: {
|
|
61
|
+
types: "./dist/index.d.cts",
|
|
62
|
+
default: "./dist/index.cjs"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
bin: {
|
|
67
|
+
chargepoint: "./dist/cli.cjs"
|
|
68
|
+
},
|
|
69
|
+
files: [
|
|
70
|
+
"dist",
|
|
71
|
+
"LICENSE"
|
|
72
|
+
],
|
|
73
|
+
scripts: {
|
|
74
|
+
build: "tsup",
|
|
75
|
+
typecheck: "tsc --noEmit",
|
|
76
|
+
lint: "eslint src tests",
|
|
77
|
+
test: "vitest run",
|
|
78
|
+
"test:watch": "vitest",
|
|
79
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
80
|
+
prepublishOnly: "pnpm build && pnpm typecheck"
|
|
81
|
+
},
|
|
82
|
+
engines: {
|
|
83
|
+
node: ">=22"
|
|
84
|
+
},
|
|
85
|
+
packageManager: "pnpm@11.0.9+sha512.34ce82e6780233cf9cad8685029a8f81d2e06196c5a9bad98879f7424940c6817c4e4524fb7d38b8553ceed48b9758b8ebaf1abd3600c232c4c8cf7366086f38",
|
|
86
|
+
dependencies: {
|
|
87
|
+
commander: "^12.1.0"
|
|
88
|
+
},
|
|
89
|
+
devDependencies: {
|
|
90
|
+
"@types/node": "^22.0.0",
|
|
91
|
+
eslint: "^10.3.0",
|
|
92
|
+
msw: "^2.7.0",
|
|
93
|
+
tsup: "^8.3.0",
|
|
94
|
+
typescript: "^5.8.0",
|
|
95
|
+
"typescript-eslint": "^8.59.2",
|
|
96
|
+
vitest: "^2.1.0"
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
29
100
|
// src/constants.ts
|
|
30
101
|
var DISCOVERY_API = "https://discovery.chargepoint.com/discovery/v3/globalconfig";
|
|
31
|
-
var VERSION =
|
|
102
|
+
var VERSION = package_default.version;
|
|
103
|
+
var NODE_CHARGEPOINT_VERSION = package_default.version;
|
|
32
104
|
var USER_AGENT = `node-chargepoint/${VERSION}`;
|
|
33
105
|
|
|
34
106
|
// src/exceptions.ts
|
|
@@ -333,12 +405,12 @@ var ChargingSession = class _ChargingSession {
|
|
|
333
405
|
this.sessionId
|
|
334
406
|
);
|
|
335
407
|
}
|
|
336
|
-
static async start(deviceId, client) {
|
|
408
|
+
static async start(deviceId, client, options) {
|
|
337
409
|
await sendCommand(client, "start", deviceId);
|
|
338
|
-
const deadline = Date.now() + 15e3;
|
|
410
|
+
const deadline = Date.now() + (options?.pollTimeoutMs ?? 15e3);
|
|
339
411
|
let status = await client.getUserChargingStatus();
|
|
340
412
|
while (!status && Date.now() < deadline) {
|
|
341
|
-
await sleep(2e3);
|
|
413
|
+
await sleep(options?.pollIntervalMs ?? 2e3);
|
|
342
414
|
status = await client.getUserChargingStatus();
|
|
343
415
|
}
|
|
344
416
|
if (!status) {
|
|
@@ -363,6 +435,8 @@ var ChargePoint = class _ChargePoint {
|
|
|
363
435
|
_coulombToken = null;
|
|
364
436
|
_region;
|
|
365
437
|
_userId = null;
|
|
438
|
+
_timeout;
|
|
439
|
+
_debug;
|
|
366
440
|
/** The current session token. Save this after login to avoid re-authenticating. */
|
|
367
441
|
get coulombToken() {
|
|
368
442
|
return this._coulombToken;
|
|
@@ -379,10 +453,16 @@ var ChargePoint = class _ChargePoint {
|
|
|
379
453
|
if (options.coulombToken) {
|
|
380
454
|
client._setToken(options.coulombToken, region);
|
|
381
455
|
}
|
|
456
|
+
client._timeout = options.timeout;
|
|
457
|
+
client._debug = options.debug;
|
|
382
458
|
return client;
|
|
383
459
|
}
|
|
384
460
|
_setToken(token, region) {
|
|
385
|
-
|
|
461
|
+
try {
|
|
462
|
+
this._coulombToken = decodeURIComponent(token);
|
|
463
|
+
} catch {
|
|
464
|
+
throw new APIError("Malformed coulomb_sess token: invalid percent-encoding");
|
|
465
|
+
}
|
|
386
466
|
this._region = region;
|
|
387
467
|
}
|
|
388
468
|
/** @internal Used by session.ts and tests. */
|
|
@@ -398,12 +478,19 @@ var ChargePoint = class _ChargePoint {
|
|
|
398
478
|
headers.set("cp-session-token", this._coulombToken);
|
|
399
479
|
headers.set("cp-region", this._region);
|
|
400
480
|
}
|
|
401
|
-
|
|
481
|
+
this._debug?.(`${method} ${url}`);
|
|
482
|
+
const signal = this._timeout !== void 0 ? AbortSignal.timeout(this._timeout) : void 0;
|
|
483
|
+
const response = await fetch(url, { ...init, method, headers, signal });
|
|
484
|
+
this._debug?.(`${response.status} ${url}`);
|
|
402
485
|
const setCookies = typeof response.headers.getSetCookie === "function" ? response.headers.getSetCookie() : [response.headers.get("set-cookie") ?? ""].filter(Boolean);
|
|
403
486
|
for (const cookie of setCookies) {
|
|
404
487
|
const match = /^coulomb_sess=([^;]+)/.exec(cookie);
|
|
405
488
|
if (match) {
|
|
406
|
-
|
|
489
|
+
try {
|
|
490
|
+
this._coulombToken = decodeURIComponent(match[1] ?? "");
|
|
491
|
+
} catch {
|
|
492
|
+
throw new CommunicationError(response.status, "Malformed coulomb_sess cookie: invalid percent-encoding");
|
|
493
|
+
}
|
|
407
494
|
break;
|
|
408
495
|
}
|
|
409
496
|
}
|
|
@@ -702,8 +789,8 @@ var ChargePoint = class _ChargePoint {
|
|
|
702
789
|
await session.refresh();
|
|
703
790
|
return session;
|
|
704
791
|
}
|
|
705
|
-
async startChargingSession(deviceId) {
|
|
706
|
-
return ChargingSession.start(deviceId, this);
|
|
792
|
+
async startChargingSession(deviceId, options) {
|
|
793
|
+
return ChargingSession.start(deviceId, this, options);
|
|
707
794
|
}
|
|
708
795
|
// ---------------------------------------------------------------------------
|
|
709
796
|
// Stations
|
package/dist/index.cjs
CHANGED
|
@@ -6,9 +6,14 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
|
6
6
|
|
|
7
7
|
var https__default = /*#__PURE__*/_interopDefault(https);
|
|
8
8
|
|
|
9
|
+
// package.json
|
|
10
|
+
var package_default = {
|
|
11
|
+
version: "0.4.0"};
|
|
12
|
+
|
|
9
13
|
// src/constants.ts
|
|
10
14
|
var DISCOVERY_API = "https://discovery.chargepoint.com/discovery/v3/globalconfig";
|
|
11
|
-
var VERSION =
|
|
15
|
+
var VERSION = package_default.version;
|
|
16
|
+
var NODE_CHARGEPOINT_VERSION = package_default.version;
|
|
12
17
|
var USER_AGENT = `node-chargepoint/${VERSION}`;
|
|
13
18
|
|
|
14
19
|
// src/exceptions.ts
|
|
@@ -310,12 +315,12 @@ var ChargingSession = class _ChargingSession {
|
|
|
310
315
|
this.sessionId
|
|
311
316
|
);
|
|
312
317
|
}
|
|
313
|
-
static async start(deviceId, client) {
|
|
318
|
+
static async start(deviceId, client, options) {
|
|
314
319
|
await sendCommand(client, "start", deviceId);
|
|
315
|
-
const deadline = Date.now() + 15e3;
|
|
320
|
+
const deadline = Date.now() + (options?.pollTimeoutMs ?? 15e3);
|
|
316
321
|
let status = await client.getUserChargingStatus();
|
|
317
322
|
while (!status && Date.now() < deadline) {
|
|
318
|
-
await sleep(2e3);
|
|
323
|
+
await sleep(options?.pollIntervalMs ?? 2e3);
|
|
319
324
|
status = await client.getUserChargingStatus();
|
|
320
325
|
}
|
|
321
326
|
if (!status) {
|
|
@@ -340,6 +345,8 @@ var ChargePoint = class _ChargePoint {
|
|
|
340
345
|
_coulombToken = null;
|
|
341
346
|
_region;
|
|
342
347
|
_userId = null;
|
|
348
|
+
_timeout;
|
|
349
|
+
_debug;
|
|
343
350
|
/** The current session token. Save this after login to avoid re-authenticating. */
|
|
344
351
|
get coulombToken() {
|
|
345
352
|
return this._coulombToken;
|
|
@@ -356,10 +363,16 @@ var ChargePoint = class _ChargePoint {
|
|
|
356
363
|
if (options.coulombToken) {
|
|
357
364
|
client._setToken(options.coulombToken, region);
|
|
358
365
|
}
|
|
366
|
+
client._timeout = options.timeout;
|
|
367
|
+
client._debug = options.debug;
|
|
359
368
|
return client;
|
|
360
369
|
}
|
|
361
370
|
_setToken(token, region) {
|
|
362
|
-
|
|
371
|
+
try {
|
|
372
|
+
this._coulombToken = decodeURIComponent(token);
|
|
373
|
+
} catch {
|
|
374
|
+
throw new APIError("Malformed coulomb_sess token: invalid percent-encoding");
|
|
375
|
+
}
|
|
363
376
|
this._region = region;
|
|
364
377
|
}
|
|
365
378
|
/** @internal Used by session.ts and tests. */
|
|
@@ -375,12 +388,19 @@ var ChargePoint = class _ChargePoint {
|
|
|
375
388
|
headers.set("cp-session-token", this._coulombToken);
|
|
376
389
|
headers.set("cp-region", this._region);
|
|
377
390
|
}
|
|
378
|
-
|
|
391
|
+
this._debug?.(`${method} ${url}`);
|
|
392
|
+
const signal = this._timeout !== void 0 ? AbortSignal.timeout(this._timeout) : void 0;
|
|
393
|
+
const response = await fetch(url, { ...init, method, headers, signal });
|
|
394
|
+
this._debug?.(`${response.status} ${url}`);
|
|
379
395
|
const setCookies = typeof response.headers.getSetCookie === "function" ? response.headers.getSetCookie() : [response.headers.get("set-cookie") ?? ""].filter(Boolean);
|
|
380
396
|
for (const cookie of setCookies) {
|
|
381
397
|
const match = /^coulomb_sess=([^;]+)/.exec(cookie);
|
|
382
398
|
if (match) {
|
|
383
|
-
|
|
399
|
+
try {
|
|
400
|
+
this._coulombToken = decodeURIComponent(match[1] ?? "");
|
|
401
|
+
} catch {
|
|
402
|
+
throw new CommunicationError(response.status, "Malformed coulomb_sess cookie: invalid percent-encoding");
|
|
403
|
+
}
|
|
384
404
|
break;
|
|
385
405
|
}
|
|
386
406
|
}
|
|
@@ -679,8 +699,8 @@ var ChargePoint = class _ChargePoint {
|
|
|
679
699
|
await session.refresh();
|
|
680
700
|
return session;
|
|
681
701
|
}
|
|
682
|
-
async startChargingSession(deviceId) {
|
|
683
|
-
return ChargingSession.start(deviceId, this);
|
|
702
|
+
async startChargingSession(deviceId, options) {
|
|
703
|
+
return ChargingSession.start(deviceId, this, options);
|
|
684
704
|
}
|
|
685
705
|
// ---------------------------------------------------------------------------
|
|
686
706
|
// Stations
|
|
@@ -722,5 +742,6 @@ exports.CommunicationError = CommunicationError;
|
|
|
722
742
|
exports.DatadomeCaptcha = DatadomeCaptcha;
|
|
723
743
|
exports.InvalidSession = InvalidSession;
|
|
724
744
|
exports.LoginError = LoginError;
|
|
745
|
+
exports.NODE_CHARGEPOINT_VERSION = NODE_CHARGEPOINT_VERSION;
|
|
725
746
|
//# sourceMappingURL=index.cjs.map
|
|
726
747
|
//# sourceMappingURL=index.cjs.map
|