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 CHANGED
@@ -289,172 +289,18 @@ for (const s of filtered) {
289
289
 
290
290
  ## CLI
291
291
 
292
- After installation, a `chargepoint` command is available.
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
- ```typescript
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
- ### Setup
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 = "0.0.1-alpha.0";
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
- this._coulombToken = decodeURIComponent(token);
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
- const response = await fetch(url, { ...init, method, headers });
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
- this._coulombToken = decodeURIComponent(match[1] ?? "");
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 = "0.0.1-alpha.0";
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
- this._coulombToken = decodeURIComponent(token);
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
- const response = await fetch(url, { ...init, method, headers });
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
- this._coulombToken = decodeURIComponent(match[1] ?? "");
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