tesla-fleet-api 0.3.0 → 0.5.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 +284 -1
- package/dist/_virtual/_rolldown/runtime.cjs +32 -0
- package/dist/_virtual/_rolldown/runtime.mjs +12 -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 +99 -0
- package/dist/bluetooth/constants.d.cts +70 -0
- package/dist/bluetooth/constants.d.mts +70 -0
- package/dist/bluetooth/constants.mjs +72 -0
- package/dist/bluetooth/esphome.cjs +292 -0
- package/dist/bluetooth/esphome.d.cts +83 -0
- package/dist/bluetooth/esphome.d.mts +83 -0
- package/dist/bluetooth/esphome.mjs +291 -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 +155 -0
- package/dist/bluetooth/homey.d.cts +75 -0
- package/dist/bluetooth/homey.d.mts +75 -0
- package/dist/bluetooth/homey.mjs +150 -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 +95 -0
- package/dist/bluetooth/transport.d.mts +95 -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 +53 -0
- package/dist/bluetooth.d.cts +10 -0
- package/dist/bluetooth.d.mts +10 -0
- package/dist/bluetooth.mjs +9 -0
- package/dist/charging.cjs +53 -0
- package/dist/charging.d.cts +31 -0
- package/dist/charging.d.mts +30 -0
- package/dist/charging.mjs +53 -0
- package/dist/commands.cjs +1797 -0
- package/dist/commands.d.cts +881 -0
- package/dist/commands.d.mts +881 -0
- package/dist/commands.mjs +1791 -0
- package/dist/const.cjs +322 -0
- package/dist/const.d.cts +308 -0
- package/dist/const.d.mts +308 -0
- package/dist/const.mjs +282 -0
- package/dist/energy.cjs +142 -0
- package/dist/energy.d.cts +101 -0
- package/dist/energy.d.mts +100 -0
- package/dist/energy.mjs +142 -0
- 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 +457 -0
- package/dist/energyspecific.d.cts +258 -0
- package/dist/energyspecific.d.mts +258 -0
- package/dist/energyspecific.mjs +451 -0
- package/dist/exceptions.cjs +1488 -0
- package/dist/exceptions.d.cts +1143 -0
- package/dist/exceptions.d.mts +1143 -0
- package/dist/exceptions.mjs +1325 -0
- 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 +287 -0
- package/dist/index.d.cts +28 -0
- package/dist/index.d.mts +28 -0
- package/dist/index.mjs +27 -0
- 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/partner.cjs +32 -0
- package/dist/partner.d.cts +24 -0
- package/dist/partner.d.mts +23 -0
- package/dist/partner.mjs +32 -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 -0
- package/dist/router.d.mts +3 -0
- package/dist/router.mjs +3 -0
- package/dist/signing/crypto.cjs +59 -0
- package/dist/signing/crypto.mjs +52 -0
- 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 +186 -0
- package/dist/signing/session.d.cts +106 -0
- package/dist/signing/session.d.mts +106 -0
- package/dist/signing/session.mjs +184 -0
- package/dist/tariff.cjs +319 -0
- package/dist/tariff.d.cts +40 -0
- package/dist/tariff.d.mts +40 -0
- package/dist/tariff.mjs +317 -0
- package/dist/teslafleetapi.cjs +326 -0
- package/dist/teslafleetapi.d.cts +160 -0
- package/dist/teslafleetapi.d.mts +160 -0
- package/dist/teslafleetapi.mjs +326 -0
- package/dist/teslemetry.cjs +183 -0
- package/dist/teslemetry.d.cts +113 -0
- package/dist/teslemetry.d.mts +113 -0
- package/dist/teslemetry.mjs +181 -0
- 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 +71 -0
- package/dist/tessie.d.cts +69 -0
- package/dist/tessie.d.mts +69 -0
- package/dist/tessie.mjs +71 -0
- 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/commands.d.cts +28 -0
- package/dist/types/commands.d.mts +28 -0
- package/dist/types/live_status.d.cts +30 -0
- package/dist/types/live_status.d.mts +30 -0
- package/dist/types/products.d.cts +58 -0
- package/dist/types/products.d.mts +58 -0
- package/dist/types/responses.d.cts +13 -0
- package/dist/types/responses.d.mts +13 -0
- package/dist/types/site_info.d.cts +154 -0
- package/dist/types/site_info.d.mts +154 -0
- package/dist/types/vehicle.d.cts +33 -0
- package/dist/types/vehicle.d.mts +33 -0
- package/dist/types/vehicle_data.d.cts +278 -0
- package/dist/types/vehicle_data.d.mts +278 -0
- package/dist/types.cjs +0 -0
- package/dist/types.d.cts +9 -0
- package/dist/types.d.mts +9 -0
- package/dist/types.mjs +1 -0
- package/dist/user.cjs +28 -0
- package/dist/user.d.cts +17 -0
- package/dist/user.d.mts +16 -0
- package/dist/user.mjs +28 -0
- 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 +847 -0
- package/dist/vehicle.d.cts +566 -0
- package/dist/vehicle.d.mts +565 -0
- package/dist/vehicle.mjs +847 -0
- package/dist/vehiclesigned.cjs +35 -0
- package/dist/vehiclesigned.d.cts +23 -0
- package/dist/vehiclesigned.d.mts +22 -0
- package/dist/vehiclesigned.mjs +35 -0
- package/dist/vehiclespecific.cjs +715 -0
- package/dist/vehiclespecific.d.cts +497 -0
- package/dist/vehiclespecific.d.mts +496 -0
- package/dist/vehiclespecific.mjs +715 -0
- package/package.json +178 -38
- package/dist/charging.d.ts +0 -30
- package/dist/charging.js +0 -51
- package/dist/charging.js.map +0 -1
- package/dist/commands.d.ts +0 -352
- package/dist/commands.js +0 -875
- package/dist/commands.js.map +0 -1
- package/dist/energy.d.ts +0 -94
- package/dist/energy.js +0 -125
- package/dist/energy.js.map +0 -1
- package/dist/energyspecific.d.ts +0 -77
- package/dist/energyspecific.js +0 -97
- package/dist/energyspecific.js.map +0 -1
- package/dist/helpers.d.ts +0 -2
- package/dist/helpers.js +0 -10
- package/dist/helpers.js.map +0 -1
- package/dist/index.d.ts +0 -15
- package/dist/index.js +0 -15
- package/dist/index.js.map +0 -1
- package/dist/partner.d.ts +0 -23
- package/dist/partner.js +0 -31
- package/dist/partner.js.map +0 -1
- package/dist/signing/crypto.d.ts +0 -10
- package/dist/signing/crypto.js +0 -26
- package/dist/signing/crypto.js.map +0 -1
- package/dist/signing/errors.d.ts +0 -29
- package/dist/signing/errors.js +0 -40
- package/dist/signing/errors.js.map +0 -1
- package/dist/signing/session.d.ts +0 -69
- package/dist/signing/session.js +0 -102
- package/dist/signing/session.js.map +0 -1
- package/dist/tariff.d.ts +0 -29
- package/dist/tariff.js +0 -302
- package/dist/tariff.js.map +0 -1
- package/dist/teslafleetapi.d.ts +0 -58
- package/dist/teslafleetapi.js +0 -120
- package/dist/teslafleetapi.js.map +0 -1
- package/dist/teslemetry.d.ts +0 -29
- package/dist/teslemetry.js +0 -39
- package/dist/teslemetry.js.map +0 -1
- package/dist/tessie.d.ts +0 -23
- package/dist/tessie.js +0 -28
- package/dist/tessie.js.map +0 -1
- package/dist/types/commands.d.ts +0 -44
- package/dist/types/commands.js +0 -11
- package/dist/types/commands.js.map +0 -1
- package/dist/types/live_status.d.ts +0 -26
- package/dist/types/live_status.js +0 -2
- package/dist/types/live_status.js.map +0 -1
- package/dist/types/products.d.ts +0 -54
- package/dist/types/products.js +0 -2
- package/dist/types/products.js.map +0 -1
- package/dist/types/responses.d.ts +0 -5
- package/dist/types/responses.js +0 -2
- package/dist/types/responses.js.map +0 -1
- package/dist/types/site_info.d.ts +0 -150
- package/dist/types/site_info.js +0 -2
- package/dist/types/site_info.js.map +0 -1
- package/dist/types/vehicle.d.ts +0 -23
- package/dist/types/vehicle.js +0 -10
- package/dist/types/vehicle.js.map +0 -1
- package/dist/types/vehicle_data.d.ts +0 -274
- package/dist/types/vehicle_data.js +0 -2
- package/dist/types/vehicle_data.js.map +0 -1
- package/dist/user.d.ts +0 -16
- package/dist/user.js +0 -26
- package/dist/user.js.map +0 -1
- package/dist/vehicle.d.ts +0 -548
- package/dist/vehicle.js +0 -774
- package/dist/vehicle.js.map +0 -1
- package/dist/vehiclesigned.d.ts +0 -13
- package/dist/vehiclesigned.js +0 -19
- package/dist/vehiclesigned.js.map +0 -1
- package/dist/vehiclespecific.d.ts +0 -432
- package/dist/vehiclespecific.js +0 -601
- package/dist/vehiclespecific.js.map +0 -1
- package/src/charging.ts +0 -66
- package/src/commands.ts +0 -1134
- package/src/energy.ts +0 -217
- package/src/energyspecific.ts +0 -116
- package/src/helpers.ts +0 -11
- package/src/index.ts +0 -21
- package/src/partner.ts +0 -37
- package/src/signing/crypto.ts +0 -29
- package/src/signing/errors.ts +0 -50
- package/src/signing/session.ts +0 -132
- package/src/tariff.ts +0 -375
- package/src/teslafleetapi.ts +0 -182
- package/src/teslemetry.ts +0 -54
- package/src/tessie.ts +0 -42
- package/src/types/commands.ts +0 -62
- package/src/types/live_status.ts +0 -27
- package/src/types/products.ts +0 -61
- package/src/types/responses.ts +0 -1
- package/src/types/site_info.ts +0 -159
- package/src/types/vehicle.ts +0 -33
- package/src/types/vehicle_data.ts +0 -286
- package/src/user.ts +0 -35
- package/src/vehicle.ts +0 -886
- package/src/vehiclesigned.ts +0 -20
- package/src/vehiclespecific.ts +0 -818
package/dist/tariff.mjs
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { InvalidResponse } from "./exceptions.mjs";
|
|
2
|
+
// src/tariff.ts
|
|
3
|
+
function isObject(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Extract the `tariff_content_v2` object from a raw API response.
|
|
8
|
+
*
|
|
9
|
+
* Accepts the `site_info` read envelope (`{ response: { tariff_content_v2 } }`), the
|
|
10
|
+
* `time_of_use_settings` write envelope (`{ tou_settings: { tariff_content_v2 } }`), or the bare
|
|
11
|
+
* `tariff_content_v2` object itself. Throws {@link InvalidResponse} for a null body, an
|
|
12
|
+
* unrecognized envelope, or an extracted object without the minimal Tariff V2 shape (`seasons`
|
|
13
|
+
* and `energy_charges`) - checked identically on every path, since a malformed body (including an
|
|
14
|
+
* empty `{}` nested under a recognized envelope) is not the same as "no tariff configured".
|
|
15
|
+
*/
|
|
16
|
+
function unwrapTariffV2(response) {
|
|
17
|
+
if (response == null) throw new InvalidResponse("tariff response body was null");
|
|
18
|
+
if (!isObject(response)) throw new InvalidResponse(String(response));
|
|
19
|
+
let candidate;
|
|
20
|
+
if ("tariff_content_v2" in response) candidate = response.tariff_content_v2;
|
|
21
|
+
else if ("response" in response || "tou_settings" in response) {
|
|
22
|
+
const inner = "response" in response ? response.response : response.tou_settings;
|
|
23
|
+
candidate = isObject(inner) ? inner.tariff_content_v2 : void 0;
|
|
24
|
+
} else candidate = response;
|
|
25
|
+
if (isObject(candidate) && "seasons" in candidate && "energy_charges" in candidate) return candidate;
|
|
26
|
+
throw new InvalidResponse(response);
|
|
27
|
+
}
|
|
28
|
+
const MINUTES_PER_DAY = 1440;
|
|
29
|
+
function formatParts(date, timeZone) {
|
|
30
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
31
|
+
timeZone,
|
|
32
|
+
year: "numeric",
|
|
33
|
+
month: "2-digit",
|
|
34
|
+
day: "2-digit",
|
|
35
|
+
hour: "2-digit",
|
|
36
|
+
minute: "2-digit",
|
|
37
|
+
second: "2-digit",
|
|
38
|
+
hourCycle: "h23"
|
|
39
|
+
}).formatToParts(date);
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const p of parts) out[p.type] = Number(p.value);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/** Tesla weekday (Mon=0..Sun=6) for a calendar date, via JS Date.getUTCDay() (Sun=0..Sat=6). */
|
|
45
|
+
function dowFromDate(year, month, day) {
|
|
46
|
+
return (new Date(Date.UTC(year, month - 1, day)).getUTCDay() + 6) % 7;
|
|
47
|
+
}
|
|
48
|
+
function getWallClock(now, timeZone) {
|
|
49
|
+
const { year, month, day, hour, minute } = formatParts(now, timeZone);
|
|
50
|
+
return {
|
|
51
|
+
year,
|
|
52
|
+
month,
|
|
53
|
+
day,
|
|
54
|
+
dow: dowFromDate(year, month, day),
|
|
55
|
+
minuteOfDay: hour * 60 + minute
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Converts a target site-local wall-clock (year/month/day/hour/minute in `timeZone`) to the UTC
|
|
60
|
+
* instant it represents. Iterates because the zone's UTC offset at the target instant is itself
|
|
61
|
+
* what we're solving for (DST) - 3 rounds converges for any real-world offset change.
|
|
62
|
+
*/
|
|
63
|
+
function wallClockToUtcMillis(year, month, day, hour, minute, timeZone) {
|
|
64
|
+
const target = Date.UTC(year, month - 1, day, hour, minute);
|
|
65
|
+
let guess = target;
|
|
66
|
+
for (let i = 0; i < 3; i++) {
|
|
67
|
+
const p = formatParts(new Date(guess), timeZone);
|
|
68
|
+
const next = target - (Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second) - guess);
|
|
69
|
+
if (next === guess) break;
|
|
70
|
+
guess = next;
|
|
71
|
+
}
|
|
72
|
+
return guess;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Adds `deltaMinutes` of wall-clock time (not elapsed real time) to a wall-clock reading, staying
|
|
76
|
+
* in pure calendar arithmetic throughout - no `timeZone`/`Intl` round trip, so this is safe to use
|
|
77
|
+
* for "what day/weekday is N minutes from now" even across a DST transition, where a real-time-ms
|
|
78
|
+
* shift and a wall-clock-minutes shift land on different calendar days.
|
|
79
|
+
*/
|
|
80
|
+
function wallClockAt(wallClock, deltaMinutes) {
|
|
81
|
+
const naive = new Date(Date.UTC(wallClock.year, wallClock.month - 1, wallClock.day, 0, wallClock.minuteOfDay + deltaMinutes));
|
|
82
|
+
const year = naive.getUTCFullYear();
|
|
83
|
+
const month = naive.getUTCMonth() + 1;
|
|
84
|
+
const day = naive.getUTCDate();
|
|
85
|
+
const minuteOfDay = naive.getUTCHours() * 60 + naive.getUTCMinutes();
|
|
86
|
+
return {
|
|
87
|
+
year,
|
|
88
|
+
month,
|
|
89
|
+
day,
|
|
90
|
+
dow: dowFromDate(year, month, day),
|
|
91
|
+
minuteOfDay
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function dowInRange(d, fromDow, toDow) {
|
|
95
|
+
return fromDow <= toDow ? d >= fromDow && d <= toDow : d >= fromDow || d <= toDow;
|
|
96
|
+
}
|
|
97
|
+
function seasonContains(season, month, day) {
|
|
98
|
+
if (!season?.tou_periods) return false;
|
|
99
|
+
const fromMonth = season.fromMonth ?? 1;
|
|
100
|
+
const fromDay = season.fromDay ?? 1;
|
|
101
|
+
const toMonth = season.toMonth ?? 12;
|
|
102
|
+
const toDay = season.toDay ?? 31;
|
|
103
|
+
const from = fromMonth * 100 + fromDay;
|
|
104
|
+
const to = toMonth * 100 + toDay;
|
|
105
|
+
const cur = month * 100 + day;
|
|
106
|
+
return from <= to ? cur >= from && cur <= to : cur >= from || cur <= to;
|
|
107
|
+
}
|
|
108
|
+
function findSeason(seasons, month, day) {
|
|
109
|
+
if (!seasons) return null;
|
|
110
|
+
for (const [name, season] of Object.entries(seasons)) if (seasonContains(season, month, day)) return {
|
|
111
|
+
name,
|
|
112
|
+
season
|
|
113
|
+
};
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Expands a season's `tou_periods` into the windows active on one specific calendar day (given
|
|
118
|
+
* as a Tesla weekday, Mon=0..Sun=6). A period whose `fromDayOfWeek..toDayOfWeek` includes `dow`
|
|
119
|
+
* contributes its own window (clamped to this day, `endMin` capped at 1440 if it crosses
|
|
120
|
+
* midnight); a period that crosses midnight and includes *yesterday* also contributes its
|
|
121
|
+
* early-morning continuation here, `trueStartMin` carrying its real (pre-midnight) start so a
|
|
122
|
+
* currently-active continuation can still report an accurate `currentStart`.
|
|
123
|
+
*/
|
|
124
|
+
function dayPeriods(touPeriods, dow) {
|
|
125
|
+
const segments = [];
|
|
126
|
+
const prevDow = (dow + 6) % 7;
|
|
127
|
+
for (const [periodName, def] of Object.entries(touPeriods)) for (const window of def.periods ?? []) {
|
|
128
|
+
const fromDow = window.fromDayOfWeek ?? 0;
|
|
129
|
+
const toDow = window.toDayOfWeek ?? 6;
|
|
130
|
+
const fromMin = (window.fromHour ?? 0) * 60 + (window.fromMinute ?? 0);
|
|
131
|
+
const toMin = (window.toHour ?? 0) * 60 + (window.toMinute ?? 0);
|
|
132
|
+
const crossesMidnight = toMin <= fromMin;
|
|
133
|
+
if (dowInRange(dow, fromDow, toDow)) {
|
|
134
|
+
const endMin = crossesMidnight ? MINUTES_PER_DAY : toMin;
|
|
135
|
+
if (endMin > fromMin) segments.push({
|
|
136
|
+
periodName,
|
|
137
|
+
startMin: fromMin,
|
|
138
|
+
endMin,
|
|
139
|
+
trueStartMin: fromMin
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (crossesMidnight && toMin > 0 && dowInRange(prevDow, fromDow, toDow)) segments.push({
|
|
143
|
+
periodName,
|
|
144
|
+
startMin: 0,
|
|
145
|
+
endMin: toMin,
|
|
146
|
+
trueStartMin: fromMin - MINUTES_PER_DAY
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return segments;
|
|
150
|
+
}
|
|
151
|
+
function lookupPrice(charges, seasonName, periodName) {
|
|
152
|
+
if (!charges) return null;
|
|
153
|
+
const seasonRates = charges[seasonName]?.rates ?? charges["ALL"]?.rates;
|
|
154
|
+
if (!seasonRates) return null;
|
|
155
|
+
const value = Object.prototype.hasOwnProperty.call(seasonRates, periodName) ? seasonRates[periodName] : seasonRates["ALL"];
|
|
156
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
157
|
+
}
|
|
158
|
+
const EMPTY_RATE = {
|
|
159
|
+
price: null,
|
|
160
|
+
periodName: null,
|
|
161
|
+
seasonName: null
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Resolves one grid's (buy's, or sell's) state at `wallClock`: the active period if any, and the
|
|
165
|
+
* next boundary regardless of whether a period is currently active - a sparse tariff's gap still
|
|
166
|
+
* has a "next change" (the gap's own end) and a "since" (the gap's own start), just like an active
|
|
167
|
+
* period does (its own end and start).
|
|
168
|
+
*/
|
|
169
|
+
function scheduleAt(seasons, wallClock) {
|
|
170
|
+
const matched = findSeason(seasons, wallClock.month, wallClock.day);
|
|
171
|
+
if (!matched) return null;
|
|
172
|
+
const todaySegs = dayPeriods(matched.season.tou_periods, wallClock.dow);
|
|
173
|
+
const current = todaySegs.find((s) => s.startMin <= wallClock.minuteOfDay && wallClock.minuteOfDay < s.endMin) ?? null;
|
|
174
|
+
const laterTodayStarts = todaySegs.filter((s) => s.startMin > wallClock.minuteOfDay).map((s) => s.startMin);
|
|
175
|
+
let nextMin = current ? current.endMin : null;
|
|
176
|
+
if (laterTodayStarts.length > 0) nextMin = nextMin === null ? Math.min(...laterTodayStarts) : Math.min(nextMin, Math.min(...laterTodayStarts));
|
|
177
|
+
else if (nextMin === null || nextMin >= MINUTES_PER_DAY) {
|
|
178
|
+
const tomorrowWallClock = wallClockAt(wallClock, MINUTES_PER_DAY - wallClock.minuteOfDay);
|
|
179
|
+
const tomorrowMatched = findSeason(seasons, tomorrowWallClock.month, tomorrowWallClock.day);
|
|
180
|
+
const tomorrowSegs = tomorrowMatched ? dayPeriods(tomorrowMatched.season.tou_periods, tomorrowWallClock.dow) : [];
|
|
181
|
+
const continuation = current && tomorrowMatched?.name === matched.name ? tomorrowSegs.find((s) => s.startMin === 0 && s.periodName === current.periodName) : void 0;
|
|
182
|
+
if (continuation) nextMin = MINUTES_PER_DAY + continuation.endMin;
|
|
183
|
+
else if (tomorrowSegs.length > 0) {
|
|
184
|
+
const tomorrowFirst = MINUTES_PER_DAY + Math.min(...tomorrowSegs.map((s) => s.startMin));
|
|
185
|
+
nextMin = nextMin === null ? tomorrowFirst : Math.min(nextMin, tomorrowFirst);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
let sinceMin;
|
|
189
|
+
if (current) sinceMin = current.trueStartMin;
|
|
190
|
+
else {
|
|
191
|
+
const earlierTodayEnds = todaySegs.filter((s) => s.endMin <= wallClock.minuteOfDay).map((s) => s.endMin);
|
|
192
|
+
if (earlierTodayEnds.length > 0) sinceMin = Math.max(...earlierTodayEnds);
|
|
193
|
+
else {
|
|
194
|
+
const yesterdayWallClock = wallClockAt(wallClock, -1440);
|
|
195
|
+
const yesterdayMatched = findSeason(seasons, yesterdayWallClock.month, yesterdayWallClock.day);
|
|
196
|
+
const yesterdaySegs = yesterdayMatched ? dayPeriods(yesterdayMatched.season.tou_periods, yesterdayWallClock.dow) : [];
|
|
197
|
+
sinceMin = yesterdaySegs.length > 0 ? Math.max(...yesterdaySegs.map((s) => s.endMin)) - MINUTES_PER_DAY : null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
seasonName: matched.name,
|
|
202
|
+
current,
|
|
203
|
+
nextChangeGM: nextMin === null ? null : nextMin - wallClock.minuteOfDay,
|
|
204
|
+
sinceGM: sinceMin === null ? null : sinceMin - wallClock.minuteOfDay
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/** Walks one calendar day at a time across the horizon, re-resolving the season fresh on each day (via pure wall-clock arithmetic, not elapsed-ms/DST-sensitive arithmetic) so a horizon crossing a season boundary re-prices and re-labels correctly. */
|
|
208
|
+
function collectSegments(seasons, wallClock, horizonMinutes) {
|
|
209
|
+
const segments = [];
|
|
210
|
+
let dayStartGM = -wallClock.minuteOfDay;
|
|
211
|
+
while (dayStartGM < horizonMinutes) {
|
|
212
|
+
const dayWallClock = wallClockAt(wallClock, dayStartGM);
|
|
213
|
+
const matched = findSeason(seasons, dayWallClock.month, dayWallClock.day);
|
|
214
|
+
if (matched) for (const seg of dayPeriods(matched.season.tou_periods, dayWallClock.dow)) {
|
|
215
|
+
const startGM = dayStartGM + seg.startMin;
|
|
216
|
+
const endGM = dayStartGM + seg.endMin;
|
|
217
|
+
if (endGM > 0 && startGM < horizonMinutes) segments.push({
|
|
218
|
+
startGM,
|
|
219
|
+
endGM,
|
|
220
|
+
periodName: seg.periodName,
|
|
221
|
+
seasonName: matched.name
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
dayStartGM += MINUTES_PER_DAY;
|
|
225
|
+
}
|
|
226
|
+
segments.sort((a, b) => a.startGM - b.startGM);
|
|
227
|
+
const merged = [];
|
|
228
|
+
for (const seg of segments) {
|
|
229
|
+
const last = merged[merged.length - 1];
|
|
230
|
+
if (last && last.endGM === seg.startGM && last.periodName === seg.periodName && last.seasonName === seg.seasonName) last.endGM = seg.endGM;
|
|
231
|
+
else merged.push({ ...seg });
|
|
232
|
+
}
|
|
233
|
+
return merged;
|
|
234
|
+
}
|
|
235
|
+
function buildUpcoming(buySeasons, buyCharges, sellSeasons, sellCharges, hasSell, wallClock, horizonMinutes, toDate) {
|
|
236
|
+
const buySegs = collectSegments(buySeasons, wallClock, horizonMinutes);
|
|
237
|
+
const sellSegs = hasSell ? collectSegments(sellSeasons, wallClock, horizonMinutes) : [];
|
|
238
|
+
const boundaries = /* @__PURE__ */ new Set([0, horizonMinutes]);
|
|
239
|
+
const addBoundary = (gm) => {
|
|
240
|
+
if (gm > 0 && gm < horizonMinutes) boundaries.add(gm);
|
|
241
|
+
};
|
|
242
|
+
for (const s of buySegs) {
|
|
243
|
+
addBoundary(s.startGM);
|
|
244
|
+
addBoundary(s.endGM);
|
|
245
|
+
}
|
|
246
|
+
for (const s of sellSegs) {
|
|
247
|
+
addBoundary(s.startGM);
|
|
248
|
+
addBoundary(s.endGM);
|
|
249
|
+
}
|
|
250
|
+
const sorted = [...boundaries].sort((a, b) => a - b);
|
|
251
|
+
const periods = [];
|
|
252
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
253
|
+
const segStart = sorted[i];
|
|
254
|
+
const segEnd = sorted[i + 1];
|
|
255
|
+
if (segStart === segEnd) continue;
|
|
256
|
+
const buyAt = buySegs.find((s) => s.startGM <= segStart && segStart < s.endGM);
|
|
257
|
+
const sellAt = sellSegs.find((s) => s.startGM <= segStart && segStart < s.endGM);
|
|
258
|
+
periods.push({
|
|
259
|
+
start: toDate(segStart),
|
|
260
|
+
end: toDate(segEnd),
|
|
261
|
+
buy: buyAt ? {
|
|
262
|
+
price: lookupPrice(buyCharges, buyAt.seasonName, buyAt.periodName),
|
|
263
|
+
periodName: buyAt.periodName,
|
|
264
|
+
seasonName: buyAt.seasonName
|
|
265
|
+
} : EMPTY_RATE,
|
|
266
|
+
sell: sellAt ? {
|
|
267
|
+
price: lookupPrice(sellCharges, sellAt.seasonName, sellAt.periodName),
|
|
268
|
+
periodName: sellAt.periodName,
|
|
269
|
+
seasonName: sellAt.seasonName
|
|
270
|
+
} : EMPTY_RATE
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return periods;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Resolves the current buy/sell time-of-use rate for a Tariff V2 object at a given instant.
|
|
277
|
+
* `tariff` carries no timezone; the caller must supply the site's IANA zone (e.g. from
|
|
278
|
+
* `site_info.installation_time_zone`) so wall-clock period boundaries match correctly.
|
|
279
|
+
*/
|
|
280
|
+
function getTariffPeriods(tariff, now, opts) {
|
|
281
|
+
if (!tariff?.seasons || !tariff?.energy_charges) return null;
|
|
282
|
+
if (!opts?.timeZone) throw new Error("getTariffPeriods requires opts.timeZone");
|
|
283
|
+
const timeZone = opts.timeZone;
|
|
284
|
+
const wallClock = getWallClock(now, timeZone);
|
|
285
|
+
const buySchedule = scheduleAt(tariff.seasons, wallClock);
|
|
286
|
+
if (!buySchedule?.current) return null;
|
|
287
|
+
const sellSeasons = tariff.sell_tariff?.seasons ?? tariff.seasons;
|
|
288
|
+
const sellSchedule = tariff.sell_tariff ? scheduleAt(sellSeasons, wallClock) : null;
|
|
289
|
+
const buyRate = {
|
|
290
|
+
price: lookupPrice(tariff.energy_charges, buySchedule.seasonName, buySchedule.current.periodName),
|
|
291
|
+
periodName: buySchedule.current.periodName,
|
|
292
|
+
seasonName: buySchedule.seasonName
|
|
293
|
+
};
|
|
294
|
+
const sellRate = sellSchedule?.current ? {
|
|
295
|
+
price: lookupPrice(tariff.sell_tariff.energy_charges, sellSchedule.seasonName, sellSchedule.current.periodName),
|
|
296
|
+
periodName: sellSchedule.current.periodName,
|
|
297
|
+
seasonName: sellSchedule.seasonName
|
|
298
|
+
} : EMPTY_RATE;
|
|
299
|
+
const toDate = (gm) => {
|
|
300
|
+
const target = wallClockAt(wallClock, gm);
|
|
301
|
+
const hour = Math.floor(target.minuteOfDay / 60);
|
|
302
|
+
const minute = target.minuteOfDay % 60;
|
|
303
|
+
return new Date(wallClockToUtcMillis(target.year, target.month, target.day, hour, minute, timeZone));
|
|
304
|
+
};
|
|
305
|
+
const nextChangeGM = sellSchedule?.nextChangeGM != null ? Math.min(buySchedule.nextChangeGM, sellSchedule.nextChangeGM) : buySchedule.nextChangeGM;
|
|
306
|
+
const resolution = {
|
|
307
|
+
buy: buyRate,
|
|
308
|
+
sell: sellRate,
|
|
309
|
+
currentStart: toDate(sellSchedule?.sinceGM != null ? Math.max(buySchedule.sinceGM, sellSchedule.sinceGM) : buySchedule.sinceGM),
|
|
310
|
+
nextChange: toDate(nextChangeGM),
|
|
311
|
+
currency: typeof tariff.currency === "string" ? tariff.currency : null,
|
|
312
|
+
upcoming: null
|
|
313
|
+
};
|
|
314
|
+
if (opts.horizonHours) resolution.upcoming = buildUpcoming(tariff.seasons, tariff.energy_charges, sellSeasons, tariff.sell_tariff?.energy_charges, !!tariff.sell_tariff, wallClock, opts.horizonHours * 60, toDate);
|
|
315
|
+
return resolution;
|
|
316
|
+
}
|
|
317
|
+
export { getTariffPeriods, unwrapTariffV2 };
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
const require_charging = require("./charging.cjs");
|
|
2
|
+
const require_energy = require("./energy.cjs");
|
|
3
|
+
const require_partner = require("./partner.cjs");
|
|
4
|
+
const require_user = require("./user.cjs");
|
|
5
|
+
const require_vehicle = require("./vehicle.cjs");
|
|
6
|
+
const require_const = require("./const.cjs");
|
|
7
|
+
const require_exceptions = require("./exceptions.cjs");
|
|
8
|
+
const require_log = require("./log.cjs");
|
|
9
|
+
const require_crypto = require("./signing/crypto.cjs");
|
|
10
|
+
const require_keys = require("./signing/keys.cjs");
|
|
11
|
+
let node_crypto = require("node:crypto");
|
|
12
|
+
// src/teslafleetapi.ts
|
|
13
|
+
var TeslaFleetApi = class {
|
|
14
|
+
/** Class backing `vehicles`; a subclass (e.g. `Teslemetry`) overrides it instead of its constructor. */
|
|
15
|
+
static Vehicles = require_vehicle.default;
|
|
16
|
+
/** Class backing `energySites`; a subclass overrides it instead of its constructor. */
|
|
17
|
+
static EnergySites = require_energy.default;
|
|
18
|
+
server = null;
|
|
19
|
+
region = null;
|
|
20
|
+
accessToken;
|
|
21
|
+
charging;
|
|
22
|
+
energySites;
|
|
23
|
+
partner;
|
|
24
|
+
user;
|
|
25
|
+
vehicles;
|
|
26
|
+
/** Log to `console.debug` when no `logger` is set. */
|
|
27
|
+
debug = false;
|
|
28
|
+
logger;
|
|
29
|
+
/** Abort a request (including reading its body) after this many milliseconds; `null` waits indefinitely. */
|
|
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;
|
|
36
|
+
constructor(options) {
|
|
37
|
+
this.debug = !!options.debug;
|
|
38
|
+
this.logger = options.logger ?? null;
|
|
39
|
+
this.#fetch = options.fetch ?? null;
|
|
40
|
+
this.timeout = options.timeout ?? null;
|
|
41
|
+
this.accessToken = options.accessToken ?? null;
|
|
42
|
+
if (options.server) this.server = options.server;
|
|
43
|
+
else if (options.region && require_const.is_valid_region(options.region)) {
|
|
44
|
+
this.region = options.region;
|
|
45
|
+
this.server = require_const.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;
|
|
49
|
+
if (options.chargingScope !== false) this.charging = new require_charging.default(this);
|
|
50
|
+
if (options.energyScope !== false) this.energySites = new factories.EnergySites(this);
|
|
51
|
+
if (options.userScope !== false) this.user = new require_user.default(this);
|
|
52
|
+
if (options.partnerScope !== false) this.partner = new require_partner.default(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(require_const.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 (require_const.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 require_exceptions.InvalidRegion) continue;
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
throw new require_exceptions.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 require_exceptions.MissingToken();
|
|
94
|
+
return token;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Turn a response into its decoded JSON body, throwing the matching
|
|
98
|
+
* `TeslaFleetError` for an error status and `ResponseError` for a
|
|
99
|
+
* successful response whose body is not JSON.
|
|
100
|
+
*/
|
|
101
|
+
async _response(resp) {
|
|
102
|
+
await require_exceptions.raise_for_status(resp);
|
|
103
|
+
const text = await resp.text();
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(text);
|
|
106
|
+
} catch {
|
|
107
|
+
this._log(`Response type is: ${resp.headers.get("content-type")}`);
|
|
108
|
+
throw new require_exceptions.ResponseError(text, resp.status);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Make a request to the Tesla Fleet API.
|
|
113
|
+
* @param method
|
|
114
|
+
* @param path
|
|
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.
|
|
117
|
+
* @returns The decoded JSON body.
|
|
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.
|
|
121
|
+
*/
|
|
122
|
+
async _request(method, path, params = null, json = null) {
|
|
123
|
+
const command = path.slice(path.lastIndexOf("/") + 1);
|
|
124
|
+
let data;
|
|
125
|
+
try {
|
|
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.5.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: ${require_log.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: ${require_log.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 require_exceptions.TeslaFleetError) throw error;
|
|
162
|
+
throw new require_exceptions.TeslaFleetConnectionError(null, null, { cause: error });
|
|
163
|
+
}
|
|
164
|
+
this._log(`Response JSON: ${require_log.redactedJson(data)}`);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
require_log.logRequestError((m) => this._log(m), command, this._transport_name, error);
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
require_log.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 = [require_const.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(require_const.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 require_exceptions.raise_for_status(resp);
|
|
201
|
+
const tokenData = await resp.json();
|
|
202
|
+
this.accessToken = tokenData.access_token;
|
|
203
|
+
return tokenData;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Returns products mapped to user.
|
|
207
|
+
* @returns An array of products including both vehicles and energy sites.
|
|
208
|
+
*/
|
|
209
|
+
async products() {
|
|
210
|
+
return this._request("GET", "api/1/products");
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Return Tesla products separated into vehicles and energy_sites
|
|
214
|
+
* @returns A dictionary of vehicles and energy sites, each with an array of those products
|
|
215
|
+
*/
|
|
216
|
+
async products_by_type() {
|
|
217
|
+
return this.products().then(({ response: products }) => ({
|
|
218
|
+
vehicles: products.filter((product) => "vin" in product),
|
|
219
|
+
energy_sites: products.filter((product) => "energy_site_id" in product)
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
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 require_keys.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 require_exceptions.LibraryError("Private key is not set");
|
|
257
|
+
return require_crypto.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"
|
|
268
|
+
});
|
|
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 require_exceptions.LibraryError(unsetMessage);
|
|
276
|
+
return require_keys.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 require_keys.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 require_keys.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 (0, node_crypto.createPublicKey)(this.#rsaKey()).export({
|
|
317
|
+
type: "spki",
|
|
318
|
+
format: "pem"
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
#rsaKey() {
|
|
322
|
+
if (this.#rsaPrivateKey == null) throw new require_exceptions.LibraryError("RSA private key is not set");
|
|
323
|
+
return this.#rsaPrivateKey;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
exports.default = TeslaFleetApi;
|