node-chargepoint 0.3.5 → 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
@@ -29,7 +29,7 @@ var import_commander = require("commander");
29
29
  // package.json
30
30
  var package_default = {
31
31
  name: "node-chargepoint",
32
- version: "0.3.5",
32
+ version: "0.4.0",
33
33
  description: "A Node.js/TypeScript wrapper for the ChargePoint EV charging network API. Based on python-chargepoint by Marc Billow.",
34
34
  keywords: [
35
35
  "chargepoint",
@@ -80,7 +80,7 @@ var package_default = {
80
80
  prepublishOnly: "pnpm build && pnpm typecheck"
81
81
  },
82
82
  engines: {
83
- node: ">=24"
83
+ node: ">=22"
84
84
  },
85
85
  packageManager: "pnpm@11.0.9+sha512.34ce82e6780233cf9cad8685029a8f81d2e06196c5a9bad98879f7424940c6817c4e4524fb7d38b8553ceed48b9758b8ebaf1abd3600c232c4c8cf7366086f38",
86
86
  dependencies: {
@@ -100,6 +100,7 @@ var package_default = {
100
100
  // src/constants.ts
101
101
  var DISCOVERY_API = "https://discovery.chargepoint.com/discovery/v3/globalconfig";
102
102
  var VERSION = package_default.version;
103
+ var NODE_CHARGEPOINT_VERSION = package_default.version;
103
104
  var USER_AGENT = `node-chargepoint/${VERSION}`;
104
105
 
105
106
  // src/exceptions.ts
@@ -404,12 +405,12 @@ var ChargingSession = class _ChargingSession {
404
405
  this.sessionId
405
406
  );
406
407
  }
407
- static async start(deviceId, client) {
408
+ static async start(deviceId, client, options) {
408
409
  await sendCommand(client, "start", deviceId);
409
- const deadline = Date.now() + 15e3;
410
+ const deadline = Date.now() + (options?.pollTimeoutMs ?? 15e3);
410
411
  let status = await client.getUserChargingStatus();
411
412
  while (!status && Date.now() < deadline) {
412
- await sleep(2e3);
413
+ await sleep(options?.pollIntervalMs ?? 2e3);
413
414
  status = await client.getUserChargingStatus();
414
415
  }
415
416
  if (!status) {
@@ -434,6 +435,8 @@ var ChargePoint = class _ChargePoint {
434
435
  _coulombToken = null;
435
436
  _region;
436
437
  _userId = null;
438
+ _timeout;
439
+ _debug;
437
440
  /** The current session token. Save this after login to avoid re-authenticating. */
438
441
  get coulombToken() {
439
442
  return this._coulombToken;
@@ -450,10 +453,16 @@ var ChargePoint = class _ChargePoint {
450
453
  if (options.coulombToken) {
451
454
  client._setToken(options.coulombToken, region);
452
455
  }
456
+ client._timeout = options.timeout;
457
+ client._debug = options.debug;
453
458
  return client;
454
459
  }
455
460
  _setToken(token, region) {
456
- 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
+ }
457
466
  this._region = region;
458
467
  }
459
468
  /** @internal Used by session.ts and tests. */
@@ -469,12 +478,19 @@ var ChargePoint = class _ChargePoint {
469
478
  headers.set("cp-session-token", this._coulombToken);
470
479
  headers.set("cp-region", this._region);
471
480
  }
472
- 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}`);
473
485
  const setCookies = typeof response.headers.getSetCookie === "function" ? response.headers.getSetCookie() : [response.headers.get("set-cookie") ?? ""].filter(Boolean);
474
486
  for (const cookie of setCookies) {
475
487
  const match = /^coulomb_sess=([^;]+)/.exec(cookie);
476
488
  if (match) {
477
- 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
+ }
478
494
  break;
479
495
  }
480
496
  }
@@ -773,8 +789,8 @@ var ChargePoint = class _ChargePoint {
773
789
  await session.refresh();
774
790
  return session;
775
791
  }
776
- async startChargingSession(deviceId) {
777
- return ChargingSession.start(deviceId, this);
792
+ async startChargingSession(deviceId, options) {
793
+ return ChargingSession.start(deviceId, this, options);
778
794
  }
779
795
  // ---------------------------------------------------------------------------
780
796
  // Stations
package/dist/index.cjs CHANGED
@@ -8,11 +8,12 @@ var https__default = /*#__PURE__*/_interopDefault(https);
8
8
 
9
9
  // package.json
10
10
  var package_default = {
11
- version: "0.3.5"};
11
+ version: "0.4.0"};
12
12
 
13
13
  // src/constants.ts
14
14
  var DISCOVERY_API = "https://discovery.chargepoint.com/discovery/v3/globalconfig";
15
15
  var VERSION = package_default.version;
16
+ var NODE_CHARGEPOINT_VERSION = package_default.version;
16
17
  var USER_AGENT = `node-chargepoint/${VERSION}`;
17
18
 
18
19
  // src/exceptions.ts
@@ -314,12 +315,12 @@ var ChargingSession = class _ChargingSession {
314
315
  this.sessionId
315
316
  );
316
317
  }
317
- static async start(deviceId, client) {
318
+ static async start(deviceId, client, options) {
318
319
  await sendCommand(client, "start", deviceId);
319
- const deadline = Date.now() + 15e3;
320
+ const deadline = Date.now() + (options?.pollTimeoutMs ?? 15e3);
320
321
  let status = await client.getUserChargingStatus();
321
322
  while (!status && Date.now() < deadline) {
322
- await sleep(2e3);
323
+ await sleep(options?.pollIntervalMs ?? 2e3);
323
324
  status = await client.getUserChargingStatus();
324
325
  }
325
326
  if (!status) {
@@ -344,6 +345,8 @@ var ChargePoint = class _ChargePoint {
344
345
  _coulombToken = null;
345
346
  _region;
346
347
  _userId = null;
348
+ _timeout;
349
+ _debug;
347
350
  /** The current session token. Save this after login to avoid re-authenticating. */
348
351
  get coulombToken() {
349
352
  return this._coulombToken;
@@ -360,10 +363,16 @@ var ChargePoint = class _ChargePoint {
360
363
  if (options.coulombToken) {
361
364
  client._setToken(options.coulombToken, region);
362
365
  }
366
+ client._timeout = options.timeout;
367
+ client._debug = options.debug;
363
368
  return client;
364
369
  }
365
370
  _setToken(token, region) {
366
- 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
+ }
367
376
  this._region = region;
368
377
  }
369
378
  /** @internal Used by session.ts and tests. */
@@ -379,12 +388,19 @@ var ChargePoint = class _ChargePoint {
379
388
  headers.set("cp-session-token", this._coulombToken);
380
389
  headers.set("cp-region", this._region);
381
390
  }
382
- 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}`);
383
395
  const setCookies = typeof response.headers.getSetCookie === "function" ? response.headers.getSetCookie() : [response.headers.get("set-cookie") ?? ""].filter(Boolean);
384
396
  for (const cookie of setCookies) {
385
397
  const match = /^coulomb_sess=([^;]+)/.exec(cookie);
386
398
  if (match) {
387
- 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
+ }
388
404
  break;
389
405
  }
390
406
  }
@@ -683,8 +699,8 @@ var ChargePoint = class _ChargePoint {
683
699
  await session.refresh();
684
700
  return session;
685
701
  }
686
- async startChargingSession(deviceId) {
687
- return ChargingSession.start(deviceId, this);
702
+ async startChargingSession(deviceId, options) {
703
+ return ChargingSession.start(deviceId, this, options);
688
704
  }
689
705
  // ---------------------------------------------------------------------------
690
706
  // Stations
@@ -726,5 +742,6 @@ exports.CommunicationError = CommunicationError;
726
742
  exports.DatadomeCaptcha = DatadomeCaptcha;
727
743
  exports.InvalidSession = InvalidSession;
728
744
  exports.LoginError = LoginError;
745
+ exports.NODE_CHARGEPOINT_VERSION = NODE_CHARGEPOINT_VERSION;
729
746
  //# sourceMappingURL=index.cjs.map
730
747
  //# sourceMappingURL=index.cjs.map