tesla-fleet-api 0.4.0 → 0.6.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 +269 -1
- package/dist/_virtual/_rolldown/runtime.cjs +10 -0
- package/dist/bluetooth/adapters/shared.cjs +38 -0
- package/dist/bluetooth/adapters/shared.mjs +36 -0
- package/dist/bluetooth/async.cjs +121 -0
- package/dist/bluetooth/async.d.cts +10 -0
- package/dist/bluetooth/async.d.mts +10 -0
- package/dist/bluetooth/async.mjs +116 -0
- package/dist/bluetooth/broadcast.cjs +153 -0
- package/dist/bluetooth/broadcast.d.cts +59 -0
- package/dist/bluetooth/broadcast.d.mts +59 -0
- package/dist/bluetooth/broadcast.mjs +151 -0
- package/dist/bluetooth/constants.cjs +96 -0
- package/dist/bluetooth/constants.d.cts +68 -0
- package/dist/bluetooth/constants.d.mts +68 -0
- package/dist/bluetooth/constants.mjs +70 -0
- package/dist/bluetooth/framing.cjs +125 -0
- package/dist/bluetooth/framing.d.cts +45 -0
- package/dist/bluetooth/framing.d.mts +45 -0
- package/dist/bluetooth/framing.mjs +122 -0
- package/dist/bluetooth/homey.cjs +165 -0
- package/dist/bluetooth/homey.d.cts +84 -0
- package/dist/bluetooth/homey.d.mts +84 -0
- package/dist/bluetooth/homey.mjs +159 -0
- package/dist/bluetooth/names.cjs +24 -0
- package/dist/bluetooth/names.d.cts +10 -0
- package/dist/bluetooth/names.d.mts +10 -0
- package/dist/bluetooth/names.mjs +20 -0
- package/dist/bluetooth/noble.cjs +178 -0
- package/dist/bluetooth/noble.d.cts +58 -0
- package/dist/bluetooth/noble.d.mts +58 -0
- package/dist/bluetooth/noble.mjs +177 -0
- package/dist/bluetooth/streamglue.cjs +157 -0
- package/dist/bluetooth/streamglue.d.cts +37 -0
- package/dist/bluetooth/streamglue.d.mts +37 -0
- package/dist/bluetooth/streamglue.mjs +154 -0
- package/dist/bluetooth/teslabluetooth.cjs +67 -0
- package/dist/bluetooth/teslabluetooth.d.cts +41 -0
- package/dist/bluetooth/teslabluetooth.d.mts +41 -0
- package/dist/bluetooth/teslabluetooth.mjs +66 -0
- package/dist/bluetooth/transport.cjs +10 -0
- package/dist/bluetooth/transport.d.cts +97 -0
- package/dist/bluetooth/transport.d.mts +97 -0
- package/dist/bluetooth/transport.mjs +10 -0
- package/dist/bluetooth/vehiclebluetooth.cjs +1272 -0
- package/dist/bluetooth/vehiclebluetooth.d.cts +495 -0
- package/dist/bluetooth/vehiclebluetooth.d.mts +495 -0
- package/dist/bluetooth/vehiclebluetooth.mjs +1270 -0
- package/dist/bluetooth/verify.cjs +93 -0
- package/dist/bluetooth/verify.d.cts +35 -0
- package/dist/bluetooth/verify.d.mts +35 -0
- package/dist/bluetooth/verify.mjs +91 -0
- package/dist/bluetooth.cjs +52 -0
- package/dist/bluetooth.d.cts +10 -0
- package/dist/bluetooth.d.mts +10 -0
- package/dist/bluetooth.mjs +9 -0
- package/dist/commands.cjs +1080 -85
- package/dist/commands.d.cts +585 -54
- package/dist/commands.d.mts +583 -51
- package/dist/commands.mjs +1084 -93
- package/dist/const.cjs +22 -0
- package/dist/const.d.cts +21 -0
- package/dist/const.d.mts +21 -0
- package/dist/const.mjs +20 -1
- package/dist/energy.cjs +24 -19
- package/dist/energy.d.cts +21 -15
- package/dist/energy.d.mts +21 -15
- package/dist/energy.mjs +24 -19
- package/dist/energyisland.cjs +38 -0
- package/dist/energyisland.mjs +37 -0
- package/dist/energyparsers.cjs +118 -0
- package/dist/energyparsers.d.cts +39 -0
- package/dist/energyparsers.d.mts +39 -0
- package/dist/energyparsers.mjs +116 -0
- package/dist/energyspecific.cjs +375 -17
- package/dist/energyspecific.d.cts +199 -19
- package/dist/energyspecific.d.mts +197 -16
- package/dist/energyspecific.mjs +370 -18
- package/dist/exceptions.cjs +3 -2
- package/dist/exceptions.d.cts +3 -2
- package/dist/exceptions.d.mts +3 -2
- package/dist/exceptions.mjs +3 -2
- package/dist/funnel.cjs +297 -0
- package/dist/funnel.d.cts +118 -0
- package/dist/funnel.d.mts +118 -0
- package/dist/funnel.mjs +292 -0
- package/dist/index.cjs +61 -0
- package/dist/index.d.cts +22 -8
- package/dist/index.d.mts +22 -8
- package/dist/index.mjs +19 -6
- package/dist/log.cjs +75 -0
- package/dist/log.d.cts +4 -0
- package/dist/log.d.mts +4 -0
- package/dist/log.mjs +71 -0
- package/dist/oauth.cjs +143 -0
- package/dist/oauth.d.cts +69 -0
- package/dist/oauth.d.mts +69 -0
- package/dist/oauth.mjs +143 -0
- package/dist/powerwall/client.cjs +818 -0
- package/dist/powerwall/client.d.cts +297 -0
- package/dist/powerwall/client.d.mts +297 -0
- package/dist/powerwall/client.mjs +807 -0
- package/dist/powerwall/components.cjs +188 -0
- package/dist/powerwall/components.d.cts +45 -0
- package/dist/powerwall/components.d.mts +45 -0
- package/dist/powerwall/components.mjs +182 -0
- package/dist/powerwall/energysite.cjs +216 -0
- package/dist/powerwall/energysite.d.cts +104 -0
- package/dist/powerwall/energysite.d.mts +104 -0
- package/dist/powerwall/energysite.mjs +214 -0
- package/dist/powerwall/exceptions.cjs +59 -0
- package/dist/powerwall/exceptions.d.cts +38 -0
- package/dist/powerwall/exceptions.d.mts +38 -0
- package/dist/powerwall/exceptions.mjs +51 -0
- package/dist/powerwall/gateway.cjs +102 -0
- package/dist/powerwall/gateway.d.cts +57 -0
- package/dist/powerwall/gateway.d.mts +57 -0
- package/dist/powerwall/gateway.mjs +101 -0
- package/dist/powerwall/merge.cjs +163 -0
- package/dist/powerwall/merge.d.cts +63 -0
- package/dist/powerwall/merge.d.mts +63 -0
- package/dist/powerwall/merge.mjs +159 -0
- package/dist/powerwall/models.d.cts +95 -0
- package/dist/powerwall/models.d.mts +95 -0
- package/dist/powerwall/queries.cjs +17 -0
- package/dist/powerwall/queries.mjs +10 -0
- package/dist/powerwall/transport.cjs +232 -0
- package/dist/powerwall/transport.mjs +227 -0
- package/dist/powerwall.cjs +47 -0
- package/dist/powerwall.d.cts +9 -0
- package/dist/powerwall.d.mts +9 -0
- package/dist/powerwall.mjs +8 -0
- package/dist/router/base.cjs +203 -0
- package/dist/router/base.d.cts +112 -0
- package/dist/router/base.d.mts +112 -0
- package/dist/router/base.mjs +200 -0
- package/dist/router/energysite.cjs +65 -0
- package/dist/router/energysite.d.cts +47 -0
- package/dist/router/energysite.d.mts +47 -0
- package/dist/router/energysite.mjs +60 -0
- package/dist/router.cjs +13 -0
- package/dist/router.d.cts +3 -1
- package/dist/router.d.mts +3 -1
- package/dist/router.mjs +3 -1
- package/dist/signing/crypto.cjs +28 -0
- package/dist/signing/crypto.mjs +26 -1
- package/dist/signing/energy.cjs +123 -0
- package/dist/signing/energy.d.cts +45 -0
- package/dist/signing/energy.d.mts +45 -0
- package/dist/signing/energy.mjs +120 -0
- package/dist/signing/jws.cjs +229 -0
- package/dist/signing/jws.d.cts +45 -0
- package/dist/signing/jws.d.mts +45 -0
- package/dist/signing/jws.mjs +211 -0
- package/dist/signing/keys.cjs +191 -0
- package/dist/signing/keys.d.cts +21 -0
- package/dist/signing/keys.d.mts +21 -0
- package/dist/signing/keys.mjs +182 -0
- package/dist/signing/session.cjs +104 -30
- package/dist/signing/session.d.cts +60 -23
- package/dist/signing/session.d.mts +60 -23
- package/dist/signing/session.mjs +105 -31
- package/dist/tariff.cjs +27 -0
- package/dist/tariff.d.cts +11 -0
- package/dist/tariff.d.mts +11 -0
- package/dist/tariff.mjs +27 -1
- package/dist/teslafleetapi.cjs +256 -55
- package/dist/teslafleetapi.d.cts +110 -14
- package/dist/teslafleetapi.d.mts +110 -14
- package/dist/teslafleetapi.mjs +258 -57
- package/dist/teslemetry.cjs +152 -5
- package/dist/teslemetry.d.cts +95 -5
- package/dist/teslemetry.d.mts +93 -3
- package/dist/teslemetry.mjs +151 -6
- package/dist/teslemetryenergy.cjs +287 -0
- package/dist/teslemetryenergy.d.cts +154 -0
- package/dist/teslemetryenergy.d.mts +154 -0
- package/dist/teslemetryenergy.mjs +282 -0
- package/dist/teslemetryvehicle.cjs +407 -0
- package/dist/teslemetryvehicle.d.cts +238 -0
- package/dist/teslemetryvehicle.d.mts +238 -0
- package/dist/teslemetryvehicle.mjs +405 -0
- package/dist/tessie.cjs +42 -5
- package/dist/tessie.d.cts +51 -5
- package/dist/tessie.d.mts +49 -3
- package/dist/tessie.mjs +42 -5
- package/dist/tessievehicle.cjs +514 -0
- package/dist/tessievehicle.d.cts +333 -0
- package/dist/tessievehicle.d.mts +333 -0
- package/dist/tessievehicle.mjs +513 -0
- package/dist/types/live_status.d.cts +16 -16
- package/dist/types/live_status.d.mts +16 -16
- package/dist/types/responses.d.cts +5 -1
- package/dist/types/responses.d.mts +5 -1
- package/dist/types/vehicle.d.cts +7 -1
- package/dist/types/vehicle.d.mts +7 -1
- package/dist/types.d.cts +3 -3
- package/dist/types.d.mts +3 -3
- package/dist/util.cjs +41 -0
- package/dist/util.d.cts +18 -0
- package/dist/util.d.mts +18 -0
- package/dist/util.mjs +40 -0
- package/dist/vehicle.cjs +79 -36
- package/dist/vehicle.d.cts +42 -26
- package/dist/vehicle.d.mts +42 -26
- package/dist/vehicle.mjs +80 -37
- package/dist/vehiclesigned.cjs +20 -5
- package/dist/vehiclesigned.d.cts +14 -5
- package/dist/vehiclesigned.d.mts +14 -5
- package/dist/vehiclesigned.mjs +20 -5
- package/dist/vehiclespecific.cjs +126 -15
- package/dist/vehiclespecific.d.cts +82 -18
- package/dist/vehiclespecific.d.mts +82 -18
- package/dist/vehiclespecific.mjs +126 -15
- package/package.json +56 -3
package/dist/teslafleetapi.d.mts
CHANGED
|
@@ -3,25 +3,39 @@ import Energy from "./energy.mjs";
|
|
|
3
3
|
import Partner from "./partner.mjs";
|
|
4
4
|
import User from "./user.mjs";
|
|
5
5
|
import Vehicle from "./vehicle.mjs";
|
|
6
|
-
import { Method, Region } from "./const.mjs";
|
|
7
|
-
import { EnergyProduct,
|
|
6
|
+
import { Method, Region, Scope } from "./const.mjs";
|
|
7
|
+
import { EnergyProduct, ProductsData, VehicleProduct } from "./types/products.mjs";
|
|
8
|
+
import { Logger } from "./log.mjs";
|
|
9
|
+
import { KeyObject } from "node:crypto";
|
|
8
10
|
export type ProductsByType = {
|
|
9
11
|
vehicles: VehicleProduct[];
|
|
10
12
|
energy_sites: EnergyProduct[];
|
|
11
13
|
};
|
|
14
|
+
/** An access token, or a function returning the current one (e.g. after an OAuth refresh). */
|
|
15
|
+
export type AccessToken = string | (() => Promise<string | null>);
|
|
12
16
|
export default class TeslaFleetApi {
|
|
17
|
+
#private;
|
|
18
|
+
/** Class backing `vehicles`; a subclass (e.g. `Teslemetry`) overrides it instead of its constructor. */
|
|
19
|
+
static Vehicles: new (parent: TeslaFleetApi) => Vehicle;
|
|
20
|
+
/** Class backing `energySites`; a subclass overrides it instead of its constructor. */
|
|
21
|
+
static EnergySites: new (parent: TeslaFleetApi) => Energy;
|
|
13
22
|
server: string | null;
|
|
14
|
-
|
|
23
|
+
region: Region | null;
|
|
24
|
+
accessToken: AccessToken | null;
|
|
15
25
|
charging?: Charging;
|
|
16
|
-
|
|
26
|
+
energySites?: Energy;
|
|
17
27
|
partner?: Partner;
|
|
18
28
|
user?: User;
|
|
19
|
-
|
|
29
|
+
vehicles?: Vehicle;
|
|
30
|
+
/** Log to `console.debug` when no `logger` is set. */
|
|
20
31
|
debug: boolean;
|
|
32
|
+
logger: Logger | null;
|
|
21
33
|
/** Abort a request (including reading its body) after this many milliseconds; `null` waits indefinitely. */
|
|
22
34
|
timeout: number | null;
|
|
35
|
+
/** Transport identity for debug logging; Teslemetry and Tessie override this. */
|
|
36
|
+
readonly _transport_name: string;
|
|
23
37
|
constructor(options: {
|
|
24
|
-
accessToken?:
|
|
38
|
+
accessToken?: AccessToken;
|
|
25
39
|
region?: Region;
|
|
26
40
|
server?: string;
|
|
27
41
|
chargingScope?: boolean;
|
|
@@ -30,8 +44,26 @@ export default class TeslaFleetApi {
|
|
|
30
44
|
userScope?: boolean;
|
|
31
45
|
vehicleScope?: boolean;
|
|
32
46
|
debug?: boolean;
|
|
47
|
+
logger?: Logger;
|
|
48
|
+
/** Used for every request in place of the global `fetch`. */
|
|
49
|
+
fetch?: typeof fetch;
|
|
33
50
|
timeout?: number;
|
|
34
51
|
});
|
|
52
|
+
/** Write a debug line to `logger`, or to `console.debug` when `debug` is set. */
|
|
53
|
+
_log(message: string): void;
|
|
54
|
+
/** The injected `fetch`, resolved per call so the global is read late. */
|
|
55
|
+
protected _fetch(input: string, init?: RequestInit): Promise<Response>;
|
|
56
|
+
/**
|
|
57
|
+
* Find the server for this user's region by asking each Tesla server in
|
|
58
|
+
* turn, skipping any that answers `InvalidRegion`.
|
|
59
|
+
* @returns The region, e.g. `"na"`.
|
|
60
|
+
*/
|
|
61
|
+
find_server(): Promise<string>;
|
|
62
|
+
/**
|
|
63
|
+
* The current access token, calling it first if it is a function.
|
|
64
|
+
* @throws MissingToken when there is no token.
|
|
65
|
+
*/
|
|
66
|
+
access_token(): Promise<string>;
|
|
35
67
|
/**
|
|
36
68
|
* Turn a response into its decoded JSON body, throwing the matching
|
|
37
69
|
* `TeslaFleetError` for an error status and `ResponseError` for a
|
|
@@ -42,23 +74,87 @@ export default class TeslaFleetApi {
|
|
|
42
74
|
* Make a request to the Tesla Fleet API.
|
|
43
75
|
* @param method
|
|
44
76
|
* @param path
|
|
45
|
-
* @param params
|
|
46
|
-
* @param json
|
|
77
|
+
* @param params Query parameters; null and undefined values are dropped.
|
|
78
|
+
* @param json Body; null and undefined values are dropped, and a GET sends none.
|
|
47
79
|
* @returns The decoded JSON body.
|
|
48
|
-
* @throws
|
|
49
|
-
*
|
|
50
|
-
* request could not complete or timed out.
|
|
80
|
+
* @throws `MissingToken` when there is no access token, a `TeslaFleetError`
|
|
81
|
+
* subclass for an error status, or `TeslaFleetConnectionError` (with the
|
|
82
|
+
* original error as `cause`) when the request could not complete or timed out.
|
|
51
83
|
*/
|
|
52
84
|
_request(method: Method, path: string, params?: Record<string, any> | null, json?: Record<string, any> | null): Promise<any>;
|
|
85
|
+
/**
|
|
86
|
+
* Returns `"ok"` if the API is operating normally. Sends no headers.
|
|
87
|
+
*/
|
|
88
|
+
status(): Promise<string>;
|
|
89
|
+
/**
|
|
90
|
+
* Generate a partner token using client credentials authentication, and use
|
|
91
|
+
* it as the access token for later requests.
|
|
92
|
+
* @param client_id Partner application client ID.
|
|
93
|
+
* @param client_secret Partner application client secret.
|
|
94
|
+
* @param scopes Scopes to request.
|
|
95
|
+
* @returns The token response, including `access_token`.
|
|
96
|
+
*/
|
|
97
|
+
partner_login(client_id: string, client_secret: string, scopes?: Scope[]): Promise<Record<string, any>>;
|
|
53
98
|
/**
|
|
54
99
|
* Returns products mapped to user.
|
|
55
100
|
* @returns An array of products including both vehicles and energy sites.
|
|
56
101
|
*/
|
|
57
|
-
products(): Promise<
|
|
102
|
+
products(): Promise<ProductsData>;
|
|
58
103
|
/**
|
|
59
104
|
* Return Tesla products separated into vehicles and energy_sites
|
|
60
105
|
* @returns A dictionary of vehicles and energy sites, each with an array of those products
|
|
61
106
|
*/
|
|
62
107
|
products_by_type(): Promise<ProductsByType>;
|
|
63
|
-
|
|
64
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Get or create the P-256 key that signs vehicle commands, and keep it as
|
|
110
|
+
* `private_key`. The file is an unencrypted PEM that Python's
|
|
111
|
+
* `get_private_key` shares, created at mode 0600. If another process wins the
|
|
112
|
+
* create race, its file is read instead.
|
|
113
|
+
* @param path Key file.
|
|
114
|
+
* @returns The raw 32-byte key `Commands` takes.
|
|
115
|
+
* @throws PrivateKeyError for an existing file that cannot be used.
|
|
116
|
+
*/
|
|
117
|
+
get_private_key(path?: string): Promise<Buffer>;
|
|
118
|
+
/** The raw 32-byte P-256 key, which `createSigned` falls back to. */
|
|
119
|
+
get private_key(): Buffer | null;
|
|
120
|
+
set private_key(key: Buffer | null);
|
|
121
|
+
/** Whether the private key has been set. */
|
|
122
|
+
get has_private_key(): boolean;
|
|
123
|
+
/** The public key in PEM format. */
|
|
124
|
+
get public_pem(): string;
|
|
125
|
+
/** The public key's uncompressed point, in hex. */
|
|
126
|
+
get public_uncompressed_point(): string;
|
|
127
|
+
/**
|
|
128
|
+
* The EC public key in DER SubjectPublicKeyInfo form, as an energy gateway
|
|
129
|
+
* expects when registering an ECC authorized client. It rejects the raw point
|
|
130
|
+
* that vehicles use.
|
|
131
|
+
*/
|
|
132
|
+
get ec_public_der_spki(): Buffer;
|
|
133
|
+
/** `ec_public_der_spki`, base64-encoded. */
|
|
134
|
+
get ec_public_der_spki_b64(): string;
|
|
135
|
+
/**
|
|
136
|
+
* Get or create the RSA key for energy gateway client registration, and keep
|
|
137
|
+
* it as `rsa_private_key`. The file is an unencrypted PEM that Python's
|
|
138
|
+
* `get_rsa_private_key` shares, created at mode 0600. If another process wins
|
|
139
|
+
* the create race, its file is read instead. Generation runs off the event loop.
|
|
140
|
+
* @param path Key file.
|
|
141
|
+
* @param key_size Modulus length in bits for a new key; 4096 is what Powerwall v1r expects.
|
|
142
|
+
* @throws PrivateKeyError for an existing file that cannot be used.
|
|
143
|
+
*/
|
|
144
|
+
get_rsa_private_key(path?: string, key_size?: number): Promise<KeyObject>;
|
|
145
|
+
/** The RSA key for energy gateway client registration. */
|
|
146
|
+
get rsa_private_key(): KeyObject | null;
|
|
147
|
+
set rsa_private_key(key: KeyObject | null);
|
|
148
|
+
/** Whether the RSA private key has been set. */
|
|
149
|
+
get has_rsa_private_key(): boolean;
|
|
150
|
+
/**
|
|
151
|
+
* The RSA public key in DER PKCS#1 form, as an energy gateway expects when
|
|
152
|
+
* registering an authorized client.
|
|
153
|
+
*/
|
|
154
|
+
get rsa_public_der_pkcs1(): Buffer;
|
|
155
|
+
/** `rsa_public_der_pkcs1`, base64-encoded. */
|
|
156
|
+
get rsa_public_der_pkcs1_b64(): string;
|
|
157
|
+
/** The RSA public key in PEM (SubjectPublicKeyInfo) format. */
|
|
158
|
+
get rsa_public_pem(): string;
|
|
159
|
+
}
|
|
160
|
+
export type { Logger };
|
package/dist/teslafleetapi.mjs
CHANGED
|
@@ -3,33 +3,95 @@ import Energy from "./energy.mjs";
|
|
|
3
3
|
import Partner from "./partner.mjs";
|
|
4
4
|
import User from "./user.mjs";
|
|
5
5
|
import Vehicle from "./vehicle.mjs";
|
|
6
|
-
import { SERVERS, is_valid_region } from "./const.mjs";
|
|
7
|
-
import { ResponseError, TeslaFleetConnectionError, TeslaFleetError, raise_for_status } from "./exceptions.mjs";
|
|
6
|
+
import { SERVERS, Scope, TOKEN_URLS, is_valid_region } from "./const.mjs";
|
|
7
|
+
import { InvalidRegion, LibraryError, MissingToken, ResponseError, TeslaFleetConnectionError, TeslaFleetError, raise_for_status } from "./exceptions.mjs";
|
|
8
|
+
import { logRequestError, logRequestResult, redactedJson, redactedQuery } from "./log.mjs";
|
|
9
|
+
import { publicKeyFor } from "./signing/crypto.mjs";
|
|
10
|
+
import { ecPrivateKeyFile, ecPublicKey, rsaPrivateKeyFile, rsaPublicDerPkcs1 } from "./signing/keys.mjs";
|
|
11
|
+
import { createPublicKey } from "node:crypto";
|
|
8
12
|
// src/teslafleetapi.ts
|
|
9
13
|
var TeslaFleetApi = class {
|
|
14
|
+
/** Class backing `vehicles`; a subclass (e.g. `Teslemetry`) overrides it instead of its constructor. */
|
|
15
|
+
static Vehicles = Vehicle;
|
|
16
|
+
/** Class backing `energySites`; a subclass overrides it instead of its constructor. */
|
|
17
|
+
static EnergySites = Energy;
|
|
10
18
|
server = null;
|
|
19
|
+
region = null;
|
|
11
20
|
accessToken;
|
|
12
21
|
charging;
|
|
13
|
-
|
|
22
|
+
energySites;
|
|
14
23
|
partner;
|
|
15
24
|
user;
|
|
16
|
-
|
|
25
|
+
vehicles;
|
|
26
|
+
/** Log to `console.debug` when no `logger` is set. */
|
|
17
27
|
debug = false;
|
|
28
|
+
logger;
|
|
18
29
|
/** Abort a request (including reading its body) after this many milliseconds; `null` waits indefinitely. */
|
|
19
30
|
timeout;
|
|
31
|
+
/** Transport identity for debug logging; Teslemetry and Tessie override this. */
|
|
32
|
+
_transport_name = "fleet";
|
|
33
|
+
#fetch;
|
|
34
|
+
#privateKey = null;
|
|
35
|
+
#rsaPrivateKey = null;
|
|
20
36
|
constructor(options) {
|
|
21
37
|
this.debug = !!options.debug;
|
|
38
|
+
this.logger = options.logger ?? null;
|
|
39
|
+
this.#fetch = options.fetch ?? null;
|
|
22
40
|
this.timeout = options.timeout ?? null;
|
|
23
41
|
this.accessToken = options.accessToken ?? null;
|
|
24
42
|
if (options.server) this.server = options.server;
|
|
25
|
-
else if (options.region && is_valid_region(options.region))
|
|
26
|
-
|
|
27
|
-
|
|
43
|
+
else if (options.region && is_valid_region(options.region)) {
|
|
44
|
+
this.region = options.region;
|
|
45
|
+
this.server = SERVERS[options.region];
|
|
46
|
+
} else throw new Error("Either server or region must be provided.");
|
|
47
|
+
this._log(`Using server ${this.server}`);
|
|
48
|
+
const factories = this.constructor;
|
|
28
49
|
if (options.chargingScope !== false) this.charging = new Charging(this);
|
|
29
|
-
if (options.energyScope !== false) this.
|
|
50
|
+
if (options.energyScope !== false) this.energySites = new factories.EnergySites(this);
|
|
30
51
|
if (options.userScope !== false) this.user = new User(this);
|
|
31
52
|
if (options.partnerScope !== false) this.partner = new Partner(this);
|
|
32
|
-
if (options.vehicleScope !== false) this.
|
|
53
|
+
if (options.vehicleScope !== false) this.vehicles = new factories.Vehicles(this);
|
|
54
|
+
}
|
|
55
|
+
/** Write a debug line to `logger`, or to `console.debug` when `debug` is set. */
|
|
56
|
+
_log(message) {
|
|
57
|
+
if (this.logger) this.logger.debug(message);
|
|
58
|
+
else if (this.debug) console.debug(message);
|
|
59
|
+
}
|
|
60
|
+
/** The injected `fetch`, resolved per call so the global is read late. */
|
|
61
|
+
_fetch(input, init) {
|
|
62
|
+
return (this.#fetch ?? fetch)(input, init);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Find the server for this user's region by asking each Tesla server in
|
|
66
|
+
* turn, skipping any that answers `InvalidRegion`.
|
|
67
|
+
* @returns The region, e.g. `"na"`.
|
|
68
|
+
*/
|
|
69
|
+
async find_server() {
|
|
70
|
+
for (const server of Object.values(SERVERS)) {
|
|
71
|
+
this.server = server;
|
|
72
|
+
try {
|
|
73
|
+
const { response } = await this._request("GET", "api/1/users/region");
|
|
74
|
+
if (response) {
|
|
75
|
+
this.server = response.fleet_api_base_url;
|
|
76
|
+
if (is_valid_region(response.region)) this.region = response.region;
|
|
77
|
+
this._log(`Using server ${this.server}`);
|
|
78
|
+
return response.region;
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error instanceof InvalidRegion) continue;
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
throw new LibraryError("Could not find a valid Tesla API server.");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The current access token, calling it first if it is a function.
|
|
89
|
+
* @throws MissingToken when there is no token.
|
|
90
|
+
*/
|
|
91
|
+
async access_token() {
|
|
92
|
+
const token = typeof this.accessToken === "function" ? await this.accessToken() : this.accessToken;
|
|
93
|
+
if (token == null) throw new MissingToken();
|
|
94
|
+
return token;
|
|
33
95
|
}
|
|
34
96
|
/**
|
|
35
97
|
* Turn a response into its decoded JSON body, throwing the matching
|
|
@@ -42,6 +104,7 @@ var TeslaFleetApi = class {
|
|
|
42
104
|
try {
|
|
43
105
|
return JSON.parse(text);
|
|
44
106
|
} catch {
|
|
107
|
+
this._log(`Response type is: ${resp.headers.get("content-type")}`);
|
|
45
108
|
throw new ResponseError(text, resp.status);
|
|
46
109
|
}
|
|
47
110
|
}
|
|
@@ -49,77 +112,215 @@ var TeslaFleetApi = class {
|
|
|
49
112
|
* Make a request to the Tesla Fleet API.
|
|
50
113
|
* @param method
|
|
51
114
|
* @param path
|
|
52
|
-
* @param params
|
|
53
|
-
* @param json
|
|
115
|
+
* @param params Query parameters; null and undefined values are dropped.
|
|
116
|
+
* @param json Body; null and undefined values are dropped, and a GET sends none.
|
|
54
117
|
* @returns The decoded JSON body.
|
|
55
|
-
* @throws
|
|
56
|
-
*
|
|
57
|
-
* request could not complete or timed out.
|
|
118
|
+
* @throws `MissingToken` when there is no access token, a `TeslaFleetError`
|
|
119
|
+
* subclass for an error status, or `TeslaFleetConnectionError` (with the
|
|
120
|
+
* original error as `cause`) when the request could not complete or timed out.
|
|
58
121
|
*/
|
|
59
122
|
async _request(method, path, params = null, json = null) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
this.debug && console.debug(`Sending request to ${path}`);
|
|
63
|
-
if (params) {
|
|
64
|
-
params = Object.fromEntries(Object.entries(params).filter(([_, v]) => v != null));
|
|
65
|
-
this.debug && console.debug(`Parameters: ${JSON.stringify(params)}`);
|
|
66
|
-
}
|
|
67
|
-
if (json) {
|
|
68
|
-
json = Object.fromEntries(Object.entries(json).filter(([_, v]) => v != null));
|
|
69
|
-
this.debug && console.debug(`Body: ${JSON.stringify(json)}`);
|
|
70
|
-
}
|
|
71
|
-
const headers = {
|
|
72
|
-
Authorization: `Bearer ${this.accessToken}`,
|
|
73
|
-
"Content-Type": "application/json",
|
|
74
|
-
"X-Library": `node tesla_fleet_api`
|
|
75
|
-
};
|
|
76
|
-
const query = params ? "?" + new URLSearchParams(params).toString() : "";
|
|
123
|
+
const command = path.slice(path.lastIndexOf("/") + 1);
|
|
124
|
+
let data;
|
|
77
125
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
126
|
+
if (!this.server) throw new Error("Server was not set at init. Call find_server() first.");
|
|
127
|
+
if (method === "GET") json = null;
|
|
128
|
+
const headers = {
|
|
129
|
+
Authorization: `Bearer ${await this.access_token()}`,
|
|
130
|
+
"X-Library": `node tesla_fleet_api 0.6.0`
|
|
131
|
+
};
|
|
132
|
+
const query = new URLSearchParams();
|
|
133
|
+
if (params) {
|
|
134
|
+
for (const [key, value] of Object.entries(params)) if (value != null) query.append(key, typeof value === "boolean" ? value ? "true" : "false" : String(value));
|
|
135
|
+
this._log(`Parameters: ${redactedQuery(query)}`);
|
|
136
|
+
}
|
|
137
|
+
let body = null;
|
|
138
|
+
if (json) {
|
|
139
|
+
json = Object.fromEntries(Object.entries(json).filter(([_, v]) => v != null));
|
|
140
|
+
body = JSON.stringify(json);
|
|
141
|
+
this._log(`Body: ${redactedJson(json)}`);
|
|
142
|
+
headers["Content-Type"] = "application/json";
|
|
143
|
+
}
|
|
144
|
+
const search = query.size ? `?${query.toString()}` : "";
|
|
145
|
+
const url = `${this.server}/${path}${search}`;
|
|
146
|
+
try {
|
|
147
|
+
const resp = await this._fetch(url, {
|
|
148
|
+
method,
|
|
149
|
+
headers,
|
|
150
|
+
body,
|
|
151
|
+
signal: this.timeout != null ? AbortSignal.timeout(this.timeout) : null
|
|
152
|
+
});
|
|
153
|
+
this._log(`Status ${resp.status} from ${this.server}/${path}`);
|
|
154
|
+
for (const [header, label] of [
|
|
155
|
+
["x-txid", "Response TXID"],
|
|
156
|
+
["RateLimit-Reset", "Rate limit reset"],
|
|
157
|
+
["Retry-After", "Retry after"]
|
|
158
|
+
]) if (resp.headers.has(header)) this._log(`${label}: ${resp.headers.get(header)}`);
|
|
159
|
+
data = await this._response(resp);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error instanceof TeslaFleetError) throw error;
|
|
162
|
+
throw new TeslaFleetConnectionError(null, null, { cause: error });
|
|
163
|
+
}
|
|
164
|
+
this._log(`Response JSON: ${redactedJson(data)}`);
|
|
85
165
|
} catch (error) {
|
|
86
|
-
|
|
87
|
-
throw
|
|
166
|
+
logRequestError((m) => this._log(m), command, this._transport_name, error);
|
|
167
|
+
throw error;
|
|
88
168
|
}
|
|
169
|
+
logRequestResult((m) => this._log(m), command, this._transport_name, data);
|
|
170
|
+
return data;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Returns `"ok"` if the API is operating normally. Sends no headers.
|
|
174
|
+
*/
|
|
175
|
+
async status() {
|
|
176
|
+
if (!this.server) throw new Error("Server was not set at init. Call find_server() first.");
|
|
177
|
+
return (await this._fetch(`${this.server}/status`)).text();
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Generate a partner token using client credentials authentication, and use
|
|
181
|
+
* it as the access token for later requests.
|
|
182
|
+
* @param client_id Partner application client ID.
|
|
183
|
+
* @param client_secret Partner application client secret.
|
|
184
|
+
* @param scopes Scopes to request.
|
|
185
|
+
* @returns The token response, including `access_token`.
|
|
186
|
+
*/
|
|
187
|
+
async partner_login(client_id, client_secret, scopes = [Scope.OPENID]) {
|
|
188
|
+
if (!this.server) throw new Error("Server was not set at init. Call find_server() first.");
|
|
189
|
+
const resp = await this._fetch(TOKEN_URLS[this.region ?? "na"], {
|
|
190
|
+
method: "POST",
|
|
191
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
192
|
+
body: new URLSearchParams({
|
|
193
|
+
grant_type: "client_credentials",
|
|
194
|
+
client_id,
|
|
195
|
+
client_secret,
|
|
196
|
+
audience: this.server,
|
|
197
|
+
scope: scopes.join(" ")
|
|
198
|
+
})
|
|
199
|
+
});
|
|
200
|
+
await raise_for_status(resp);
|
|
201
|
+
const tokenData = await resp.json();
|
|
202
|
+
this.accessToken = tokenData.access_token;
|
|
203
|
+
return tokenData;
|
|
89
204
|
}
|
|
90
205
|
/**
|
|
91
206
|
* Returns products mapped to user.
|
|
92
207
|
* @returns An array of products including both vehicles and energy sites.
|
|
93
208
|
*/
|
|
94
209
|
async products() {
|
|
95
|
-
return this._request("GET", "api/1/products")
|
|
210
|
+
return this._request("GET", "api/1/products");
|
|
96
211
|
}
|
|
97
212
|
/**
|
|
98
213
|
* Return Tesla products separated into vehicles and energy_sites
|
|
99
214
|
* @returns A dictionary of vehicles and energy sites, each with an array of those products
|
|
100
215
|
*/
|
|
101
216
|
async products_by_type() {
|
|
102
|
-
return this.products().then((products) => ({
|
|
217
|
+
return this.products().then(({ response: products }) => ({
|
|
103
218
|
vehicles: products.filter((product) => "vin" in product),
|
|
104
219
|
energy_sites: products.filter((product) => "energy_site_id" in product)
|
|
105
220
|
}));
|
|
106
221
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
222
|
+
/**
|
|
223
|
+
* Get or create the P-256 key that signs vehicle commands, and keep it as
|
|
224
|
+
* `private_key`. The file is an unencrypted PEM that Python's
|
|
225
|
+
* `get_private_key` shares, created at mode 0600. If another process wins the
|
|
226
|
+
* create race, its file is read instead.
|
|
227
|
+
* @param path Key file.
|
|
228
|
+
* @returns The raw 32-byte key `Commands` takes.
|
|
229
|
+
* @throws PrivateKeyError for an existing file that cannot be used.
|
|
230
|
+
*/
|
|
231
|
+
async get_private_key(path = "private_key.pem") {
|
|
232
|
+
const key = await ecPrivateKeyFile(path);
|
|
233
|
+
this.private_key = key;
|
|
234
|
+
return key;
|
|
235
|
+
}
|
|
236
|
+
/** The raw 32-byte P-256 key, which `createSigned` falls back to. */
|
|
237
|
+
get private_key() {
|
|
238
|
+
return this.#privateKey;
|
|
239
|
+
}
|
|
240
|
+
set private_key(key) {
|
|
241
|
+
this.#privateKey = key;
|
|
242
|
+
}
|
|
243
|
+
/** Whether the private key has been set. */
|
|
244
|
+
get has_private_key() {
|
|
245
|
+
return this.#privateKey != null;
|
|
246
|
+
}
|
|
247
|
+
/** The public key in PEM format. */
|
|
248
|
+
get public_pem() {
|
|
249
|
+
return this.#ecPublicKey("Private key is not set").export({
|
|
250
|
+
type: "spki",
|
|
251
|
+
format: "pem"
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
/** The public key's uncompressed point, in hex. */
|
|
255
|
+
get public_uncompressed_point() {
|
|
256
|
+
if (this.#privateKey == null) throw new LibraryError("Private key is not set");
|
|
257
|
+
return publicKeyFor(this.#privateKey).toString("hex");
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The EC public key in DER SubjectPublicKeyInfo form, as an energy gateway
|
|
261
|
+
* expects when registering an ECC authorized client. It rejects the raw point
|
|
262
|
+
* that vehicles use.
|
|
263
|
+
*/
|
|
264
|
+
get ec_public_der_spki() {
|
|
265
|
+
return this.#ecPublicKey("EC private key is not set").export({
|
|
266
|
+
type: "spki",
|
|
267
|
+
format: "der"
|
|
122
268
|
});
|
|
123
269
|
}
|
|
270
|
+
/** `ec_public_der_spki`, base64-encoded. */
|
|
271
|
+
get ec_public_der_spki_b64() {
|
|
272
|
+
return this.ec_public_der_spki.toString("base64");
|
|
273
|
+
}
|
|
274
|
+
#ecPublicKey(unsetMessage) {
|
|
275
|
+
if (this.#privateKey == null) throw new LibraryError(unsetMessage);
|
|
276
|
+
return ecPublicKey(this.#privateKey);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Get or create the RSA key for energy gateway client registration, and keep
|
|
280
|
+
* it as `rsa_private_key`. The file is an unencrypted PEM that Python's
|
|
281
|
+
* `get_rsa_private_key` shares, created at mode 0600. If another process wins
|
|
282
|
+
* the create race, its file is read instead. Generation runs off the event loop.
|
|
283
|
+
* @param path Key file.
|
|
284
|
+
* @param key_size Modulus length in bits for a new key; 4096 is what Powerwall v1r expects.
|
|
285
|
+
* @throws PrivateKeyError for an existing file that cannot be used.
|
|
286
|
+
*/
|
|
287
|
+
async get_rsa_private_key(path = "tedapi_rsa_private.pem", key_size = 4096) {
|
|
288
|
+
const key = await rsaPrivateKeyFile(path, key_size);
|
|
289
|
+
this.rsa_private_key = key;
|
|
290
|
+
return key;
|
|
291
|
+
}
|
|
292
|
+
/** The RSA key for energy gateway client registration. */
|
|
293
|
+
get rsa_private_key() {
|
|
294
|
+
return this.#rsaPrivateKey;
|
|
295
|
+
}
|
|
296
|
+
set rsa_private_key(key) {
|
|
297
|
+
this.#rsaPrivateKey = key;
|
|
298
|
+
}
|
|
299
|
+
/** Whether the RSA private key has been set. */
|
|
300
|
+
get has_rsa_private_key() {
|
|
301
|
+
return this.#rsaPrivateKey != null;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The RSA public key in DER PKCS#1 form, as an energy gateway expects when
|
|
305
|
+
* registering an authorized client.
|
|
306
|
+
*/
|
|
307
|
+
get rsa_public_der_pkcs1() {
|
|
308
|
+
return rsaPublicDerPkcs1(this.#rsaKey());
|
|
309
|
+
}
|
|
310
|
+
/** `rsa_public_der_pkcs1`, base64-encoded. */
|
|
311
|
+
get rsa_public_der_pkcs1_b64() {
|
|
312
|
+
return this.rsa_public_der_pkcs1.toString("base64");
|
|
313
|
+
}
|
|
314
|
+
/** The RSA public key in PEM (SubjectPublicKeyInfo) format. */
|
|
315
|
+
get rsa_public_pem() {
|
|
316
|
+
return createPublicKey(this.#rsaKey()).export({
|
|
317
|
+
type: "spki",
|
|
318
|
+
format: "pem"
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
#rsaKey() {
|
|
322
|
+
if (this.#rsaPrivateKey == null) throw new LibraryError("RSA private key is not set");
|
|
323
|
+
return this.#rsaPrivateKey;
|
|
324
|
+
}
|
|
124
325
|
};
|
|
125
326
|
export { TeslaFleetApi as default };
|